1 Punkte von GN⁺ 2024-09-27 | 1 Kommentare | Auf WhatsApp teilen
  • Der High C Compiler für FM TOWNS von 1989 bot über die Unterstützung der DOS-Umgebung hinaus mehrere benutzerorientierte Sprachfunktionen, die für C-Compiler jener Zeit ungewöhnlich waren.
  • In Kombination mit Phar Laps DOS Extender wurde er im Entwicklungsfluss genutzt, der in einer 16-Bit-MS-DOS-Umgebung den 32-Bit-80386 ausnutzte, und wurde zum 1st-Party-C-Compiler von FM TOWNS.
  • Unterstriche in Zahlenliteralen, Label-Argumente, case-Bereiche, verschachtelte Funktionen und Generatoren sind Funktionen, die erst viel später in C/C++-Standards aufgenommen wurden oder bis heute nicht standardisiert sind.
  • Verschachtelte Funktionen boten in Form eines nicht-entkommenden Closures mit gemeinsam übergebenem Funktionszeiger und Kontextzeiger einen „full function value“ und waren damit ausdrucksstärker als gewöhnliche C-Funktionszeiger.
  • Generatoren wurden als syntaktischer Zucker über verschachtelten Funktionen implementiert und funktionierten als einfache Struktur, bei der der Rumpf der for-Schleife des Aufrufers in eine verschachtelte Funktion umgewandelt und als yield-Argument übergeben wurde.

Die Rolle von FM TOWNS und High C

  • Ein Handbuch für einen C-Compiler aus den 1980er-Jahren, das aus einem Stapel von FM-TOWNS-Büchern auftauchte, enthielt überraschend viele Spracherweiterungen.
  • Um C und verwandte Sprachen in realen Umgebungen einzusetzen, waren lange Zeit Vendor-Erweiterungen nötig.
    • In heutigen, von GCC, Clang und MSVC geprägten Umgebungen konzentrieren sich Erweiterungen meist auf plattformspezifische Verarbeitung oder Low-Level-Detailkontrolle.
    • In den 1980er-Jahren konkurrierten mehr und kleinere Firmen um Verbreitung, daher waren auch die Erweiterungen vielfältiger.
  • Phar Lap entwickelte einen der frühen DOS Extender, die in einer 16-Bit-MS-DOS-Umgebung die Nutzung des 32-Bit-80386-Prozessors ermöglichten.
  • MetaWare portierte den High C Compiler im Auftrag von Phar Lap auf das DOS-Extender-SDK von Phar Lap.
  • Fujitsu integrierte Phar Laps DOS Extender in das OS der auf dem 803386 basierenden FM TOWNS-Plattform, und High C wurde zum 1st-Party-C-Compiler dieser Plattform.
  • FM TOWNS erschien 1989, unmittelbar vor der Ratifizierung von C89, dem ersten ANSI-C-Standard.

Kleine Komfortfunktionen, die ihrer Zeit voraus waren

  • Unterstrich-Trennzeichen in Zahlenliteralen

    • Um lange Zahlenliterale besser lesbar zu machen, konnten innerhalb von Zahlen Unterstrich-Trennzeichen verwendet werden.
    • C++ führte in C++14 Apostroph-Trennzeichen wie 1'000'000 ein.
    • C führte eine ähnliche Funktion erst in C23 ein.
  • Label-Argumente

    • Bei Funktionen mit vielen Parametern oder mit Typen wie bool, deren Bedeutung an der Aufrufstelle schwer zu erkennen ist, konnten Argumentnamen angegeben werden.
    • Die Label-Argumente von High C funktionieren ähnlich wie ein populäres Feature von Python.
      • Argument-Labels sind optional.
      • Wenn Labels verwendet werden, können Argumente mit der Syntax argumentName => value in beliebiger Reihenfolge angegeben werden.
      • Argumente mit und ohne Label können gemischt werden, aber für alle Parameter der Funktion muss ein entsprechendes Argument vorhanden sein.
    • In Standard-C und C++ gibt es diese Funktion bis heute nicht.
  • case-Bereiche

    • Wie Pascals case low..high konnte ein Wertebereich auf einmal abgeglichen werden.
    • Standard-C und C++ haben diese Funktion nicht übernommen.

Verschachtelte Funktionen und full function value

  • High C konnte wie Pascal verschachtelte Funktionen innerhalb von Funktionen deklarieren.
  • Die Implementierung kam einer vollständigeren Form näher als Standard-Pascal oder die GCC-Erweiterung für verschachtelte Funktionen.
  • High C konnte nicht nur verschachtelte Funktionsdeklarationen, sondern auch den Typ full function value deklarieren.
    • Anders als ein traditioneller C-Funktionszeiger enthält er zusammen mit dem Funktionszeiger auch einen Kontextzeiger.
    • Dadurch kann der von der verschachtelten Funktion eingefangene Kontext wiedergefunden werden.
    • Es handelt sich um ein nicht-entkommendes Closure, dessen Lebensdauer nicht über die Rückkehr der äußeren Funktion hinausreicht.
  • Die GCC-Erweiterung für verschachtelte Funktionen schrieb ausführbaren Code auf den Aufruf-Stack, um verschachtelte Funktionen als normale Funktionszeiger referenzieren zu können, und thunkte so den Kontextzeiger.
    • Dieses Verfahren führte zu erheblichen Sicherheitsrisiken, weshalb viele Plattformen die Funktion vollständig deaktivierten.
  • Lokale Funktionsreferenzen in High C lassen sich wie Werte erster Klasse verwenden, ihre Lebensdauer wird aber nicht über die Rückkehr der äußeren Funktion hinaus verlängert.
  • Verschachtelte Funktionen konnten auch per goto zur Elternfunktion springen.
    • Wie bei Smalltalk-Blöcken war ein nichtlokaler Ausstieg aus der verschachtelten Funktion möglich.
    • Damit ließen sich Funktionen bauen, die sich wie Kontrollfluss verhalten.
  • Objective-C erhielt 2009 mit Blocks Closures, die auch als escaping closures verwendet werden können, und C++ führte 2011 Lambdas ein.
  • Beide Funktionen boten jedoch keinen nichtlokalen Ausstieg.
  • Im Standard-C gibt es bis heute keine offizielle Funktion für verschachtelte Funktionen.

Generator-Koroutinen

  • MetaWare betonte die Funktion Generatoren so stark, dass ihr ein ganzes Kapitel gewidmet wurde.
  • High C unterstützte bereits 1989 Python-artige Generator-Koroutinen in plain C.
  • Generatorfunktionen werden mit der Syntax void foo(Arg arguments) -> (Yield yields) deklariert.
    • Innerhalb der Funktion kann die magische Funktion yield(values...) mehrfach aufgerufen werden, um eine Sequenz von Werten zu erzeugen.
    • Der Aufrufer durchläuft die erzeugten Werte nacheinander mit einer neuen for-Schleifensyntax der Form for variable... <- foo(arguments...) do { ... }.
  • Diese Implementierung konnte komplex mit verschachtelten Funktionen kombiniert werden.
    • Verschachtelte Funktionen innerhalb eines Generators konnten das yield-Verhalten des äußeren Generators capturen.
    • Eine verschachtelte Funktion konnte sich selbst rekursiv aufrufen, um Bäume oder rekursive Datenstrukturen zu traversieren und bei jedem Schritt yield auszuführen.
  • Diese Form scheint sich in Python oder vielen Mainstream-Sprachen mit Generator-Koroutinen nur schwer umsetzen zu lassen.

Implementierung der Generatoren und Unterschiede zu Standardsprachen

  • Die Generatoren von High C funktionieren ohne High-Level-Runtime als syntaktischer Zucker über verschachtelten Funktionen.
  • Eine Generator-Deklaration der Form void foo(Arg arguments) -> (Yield yields) ist äquivalent zu einer normalen Funktionsdeklaration void foo(void yield(Yield yields)!, Arg arguments).
    • yield ist ein impliziter Parameter vom Typ „full function value“.
    • Ein Aufruf von yield(values) im Generatorrumpf ist ein gewöhnlicher Funktionsaufruf dieses impliziten Funktionsparameters.
  • Der Rumpf der for-Schleife auf Seiten des Aufrufers wird in eine verschachtelte Funktion umgewandelt.
    • Diese verschachtelte Funktion wird als yield-Argument an den Generator übergeben.
    • Die Struktur ist einfach, aber effektiv.
  • Da verschachtelte Funktionen nichtlokale Ausstiege unterstützen, funktionieren auch break, continue und goto, die aus dem Rumpf der for-Schleife herausführen, als goto an die passende Position außerhalb der Schleife.
  • Es ist unwahrscheinlich, dass Standard-C solche Funktionen integrieren wird.
  • C++20 bietet eine sehr flexible und komplexe Koroutinen-Funktion auf Basis von Coroutine-Transformationen zur Compile-Zeit.
    • Damit lassen sich vermutlich Generatoren implementieren.
    • Das Ergebnis dürfte sich jedoch nicht so intuitiv mit lokalen Funktionen kombinieren lassen.

1 Kommentare

 
GN⁺ 2024-09-27
Hacker-News-Kommentare
  • 2011 hatte ich einmal Iterator-basiertes for zusammengefasst. Es war schon damals eine der längst vergessenen Funktionen, und ich behandelte auch, wie es aussehen könnte, wenn es in den damaligen C++-Standard aufgenommen würde.
    Mit etwas Glück besitze ich ein Exemplar der englischen Ausgabe der High C/C++ Language Reference.
    http://jdebp.uk./FGA/metaware-iterator-driven-for.html
    http://jdebp.uk./Proposals/metaware-iterator-driven-for.html
    • Ich frage mich, wie break oder return kompiliert wurden. Musste die yield-Funktion in eine Form umgewandelt werden, die einen Statuscode zurückgibt, der dann an der Aufrufstelle geprüft wird?
    • War dieses umgedrehte Smiley-Gesicht beabsichtigt?
  • D hat, auch in Das BetterC, solche Funktionen: Unterstriche in numerischen Literalen, case-Bereiche, benannte Argumente, verschachtelte Funktionen, statische verschachtelte Funktionen und etwas Generator-Ähnliches.
    Möglich sind zum Beispiel Formen wie int a = 1_234_567;, case 5 .. case 6: und test(b:3, a:4);.
    Statische verschachtelte Funktionen können nicht auf Frame-Variablen der äußeren Funktion zugreifen, sodass Fehler wie Error: static function test.foo.plus cannot access variable i in frame of function test.foo entstehen.
    Die generatorähnliche Funktion findet sich unter https://dlang.org/spec/statement.html#foreach_over_struct_an....
    • Beim Lesen dieses Artikels musste ich die ganze Zeit an D denken. Ich hatte das Gefühl, Walter Bright würde in den Kommentaren auftauchen.
    • Auch der Garbage Collector von D ist meiner Ansicht nach eine wirklich gute Funktion. In Low-Level-Code braucht man manchmal manuelle Speicherverwaltung, aber in der Praxis spielt das oft keine große Rolle, und es gibt viele Bereiche, in denen ein Garbage Collector die Arbeit deutlich erleichtert.
      Wenn man zum Beispiel einen In-Memory-Cache-Dienst baut, ist es besser, wenn die Cache-Einträge selbst nicht vom Garbage Collector verfolgt werden. Der Garbage Collector kennt die tatsächlichen Zugriffsmuster oft nicht und stört deshalb eher. Für die meisten anderen Komponenten dieses Dienstes passt ein Garbage Collector aber besser.
    • Ich habe eine Frage: Weiß jemand, warum Leute das Konzept verschachtelter Funktionen in C nicht mögen?
      Warum haben benannte Argumente die Form test(a:4, b:3) und nicht test(.a=4, b.=3);?
      Außerdem frage ich mich, wie man in C First-Class-Typen handhaben könnte.
  • In einem verwandten Zusammenhang hat der C-Compiler lcc-win Operator-Overloading, Standardargumente für Funktionen und Function Overloading hinzugefügt. In der Dokumentation siehe „generic functions“ [1].
    Auch der Plan-9-C-Compiler führte mehrere Spracherweiterungen ein; einige davon, etwa anonyme Structs/Unions, gingen später in den C-Standard ein. Heute akzeptiert GCC das Flag -fplan9-extensions [2] und kann damit ziemlich nützliche Funktionen aktivieren, etwa die automatische Umwandlung von Struct-Pointern in anonyme Felder bei Funktionsaufrufen und Zuweisungen.
    [1] https://lcc-win32.services.net/C-Tutorial.pdf
    [2] https://gcc.gnu.org/onlinedocs/gcc/Unnamed-Fields.html
  • Wer war wohl das Genie, das diese Funktionen geschaffen hat? Offenbar gab es in diesem Unternehmen jemanden mit großem Weitblick.
    Schade, dass sie sich nicht weltweit verbreitet und die Sprachstandards beeinflusst haben. Es ist erstaunlich, dass es solche Funktionen schon so früh gab.
    Auf Hacker News wurde das früher schon einmal behandelt: https://news.ycombinator.com/item?id=38938402
    Gibt es irgendwo eine PDF-Kopie?
    • CLU hatte bereits Mitte bis Ende der 1970er Iteratoren, also for-Schleifen mit Generatoren und yield [0]. Die Icon-Sprache aus ähnlicher Zeit hatte ebenfalls ähnliche Generator-Funktionen [1] und verwendete suspend für yield. Soweit ich weiß, hatte auch Ada (1983) so etwas.
      Diese Sprachfunktionen waren also nicht völlig unbekannt.
      [0] https://publications.csail.mit.edu/lcs/pubs/pdf/MIT-LCS-TR-2...
      [1] https://dl.acm.org/doi/pdf/10.1145/800055.802034
    • Bei Bitsavers gibt es eine Kopie des HC-1.2-Referenzhandbuchs (1985).
      Es beschreibt Unterstriche in Zahlen, case-Bereiche, benannte Parameter, verschachtelte Funktionen und sogar vollwertige Funktionsvariablen.
      https://bitsavers.org/pdf/metaware/…
      Siehe Appendix A, ungefähr 50 Seiten vor dem Dateiende.
    • MetaWare war in den 80er- und 90er-Jahren eine produktive Compiler-Firma in Santa Cruz. Ich mochte, was sie gebaut haben, und auch ihre Kultur war ziemlich interessant.
      Damals, als ich Code lernte und schrieb, bin ich über leicht dubiose Seiten auf sie gestoßen.
    • So überraschend ist das nicht. Wenn man in Archiven zu höheren Programmiersprachen nach FORTRAN, Lisp, ALGOL und COBOL gräbt, findet man viele solcher Sprachideen.
      Man entdeckt dabei auch die reiche Geschichte der Systemprogrammiersprachen. Außerdem sieht man, wie ähnlich sich das Design von C und Go darin ist, dass es ignoriert hat, was in anderen Ökosystemen geschah und welche Erfahrungen es bereits gab.
    • Schade, dass solche Funktionen wie neue Features wirken und nicht als Teil der Standardausstattung, die die meisten Programmiersprachen bieten.

Der Link zum Compiler-Handbuch ist unter https://winworldpc.com/product/metaware-high-c-cpp/33x zu finden
Das PDF des C-Handbuchs trägt einen Copyright-Hinweis von 2007

  • Die frühere Einreichung und die Kommentare dazu sind hier: https://news.ycombinator.com/item?id=38938402
    • Heute hat Joe Groff das im FediVerse wieder hervorgeholt, daher ist es wohl auch hier erneut aufgetaucht
      https://f.duriansoftware.com/@joe/113195961485703110
    • Der Autor scheint denselben Inhalt gestern unter einer anderen URL erneut veröffentlicht zu haben. Merkwürdig
  • Falls ihr euch fragt, warum die String-Literale in den Beispielen in der Abbildung mit ¥n statt mit \n enden: Diese Codebeispiele scheinen in Shift-JIS geschrieben zu sein. In Shift-JIS steht an der Stelle des ASCII-\ ein ¥
    • Ursprünglich war das JIS Roman [0], eine japanische ASCII-Variante von 1969. Shift-JIS fügte erst viel später Unterstützung für einen Double-Byte-Zeichensatz hinzu
      [0] https://en.wikipedia.org/wiki/JIS_X_0201
    • Das Problem ist, dass der ASCII-Code des Backslash in Shift-JIS auch als zweites Byte eines 2-Byte-Zeichens verwendet wird. Daher funktionieren japanische String-Literale in C manchmal nicht richtig
      Für diesen Zweck ist EUC-JP besser, weil es dieses Problem nicht hat. In Pascal tritt dieses Problem mit Shift-JIS nicht auf, wenn man (* *)-Kommentare verwendet und keine { }-Kommentare
    • Der Autor hat nicht angegeben, wann dieses Buch erschienen ist, und auch durch Suchen findet man keine Information dazu. Aber als das Buch erschien, gab es den Shift-JIS-Standard vermutlich noch nicht
      Wahrscheinlicher ist stattdessen, dass JIS X 0201 (https://en.m.wikipedia.org.org/wiki/JIS_X_0201), die Grundlage von Shift-JIS, verwendet wurde
    • Ähnlich war der japanische DOS-Prompt C:¥ statt C:\
  • Diese Erweiterungen sind Ada-Features. Ada hat Labels in der Form Call (Param_A => 1, Param_B => "Foo");, Unterstriche in Zahlen beliebiger Basis (X : Integer := 1_000;), verschachtelte Subprogramme und bereichsbasierte Prüfungen
    • Wie der Artikel sagt, hatte Pascal solche Features schon vor Ada, und Task-Typen mit Einstiegspunkt kann man im Grunde als Generatoren betrachten
      Man vergisst offenbar oft, wie unglaublich primitiv C damals im Vergleich zu vielen anderen Sprachen war
  • Unabhängig vom Inhalt ist die Typografie dieses Buchs interessant. Sie wirkt gleichzeitig schön und schrecklich
    Ich kenne mich mit japanischer Schreibweise oder Kerning-Regeln nicht gut genug aus, aber es sieht so aus, als hätte man eine Proportionalschrift mit sowohl Kanji als auch lateinischen Buchstaben genommen und sie gewaltsam in ein monospaced Raster gepresst
    Immerhin ist es schön, dass die Codebeispiele nicht wie in vielen meiner Bücher in 8-pt-Schrift gesetzt sind
  • Bei den Generatoren musste ich an Rusts Problem der internen/externen Iteration und an try_fold() denken (https://scribe.rip/@veedrac/rust-is-slow-and-i-am-the-cure-3...)
  • Besonders die Generatoren wirken ihrer Zeit weit voraus. Vielleicht konnte Fujitsu sie einfach implementieren, weil man sich nicht um ein langes Standardisierungsverfahren kümmern musste
    Aber genau deshalb scheinen solche Erweiterungen relativ unbekannt geblieben zu sein und mussten Jahrzehnte später in modernem C/C++ wiederentdeckt und neu erfunden werden
    • Es war nicht Fujitsu, sondern MetaWare. MetaWare war ein Unternehmen mit ziemlich viel Compiler-Erfahrung und hatte zur selben Zeit auch einen recht bekannten Pascal-Compiler. Pascal hatte bereits verschachtelte Funktionen
    • C hätte eine deutlich bessere Sprache werden können, wenn es nicht von Leuten vereinnahmt worden wäre, die darauf bestanden, dass nicht einmal Zweierkomplement in den Standard aufgenommen werden dürfe
    • Koroutinen und Generatoren waren damals bereits gut verstandene Konzepte. Man sehe sich nur Icon an. Daher scheint der wichtigste Grund wirklich eher gewesen zu sein, dass man sich keine Sorgen um den Standardisierungsaufwand machen musste