1 points par GN⁺ 2024-09-27 | 1 commentaires | Partager sur WhatsApp
  • En 1989, le High C Compiler pour FM TOWNS ne se contentait pas d’être compatible avec l’environnement DOS : il intégrait aussi plusieurs fonctionnalités de langage orientées utilisateur, rares pour un compilateur C de l’époque
  • Associé au DOS extender de Phar Lap, il est devenu le compilateur C officiel de la plateforme FM TOWNS dans un contexte de développement exploitant le 80386 32 bits sous MS-DOS 16 bits
  • Les séparateurs par soulignement dans les littéraux numériques, les arguments étiquetés, les plages de case, les fonctions imbriquées et les générateurs sont des fonctionnalités qui ne sont arrivées que bien plus tard dans les standards C/C++, ou n’y figurent toujours pas
  • Les fonctions imbriquées fournissaient une forme de « full function value » correspondant à une closure non échappante, en transmettant à la fois un pointeur de fonction et un pointeur de contexte, ce qui les rendait plus expressives qu’un simple pointeur de fonction C
  • Les générateurs étaient implémentés comme du sucre syntaxique au-dessus des fonctions imbriquées, en transformant le corps de la boucle for de l’appelant en fonction imbriquée passée comme argument à yield, selon une structure simple

La place de FM TOWNS et de High C

  • Un manuel de compilateur C des années 1980, retrouvé dans un lot de livres sur FM TOWNS, s’est révélé contenir bien plus d’extensions de langage que prévu
  • Pendant longtemps, utiliser C et les langages de sa famille en conditions réelles nécessitait des extensions de fournisseur
    • Aujourd’hui, dans un écosystème dominé par GCC, Clang et MSVC, les extensions tendent à se concentrer sur le traitement spécifique aux plateformes et le contrôle fin des détails bas niveau
    • Dans les années 1980, davantage d’entreprises, plus petites, se disputaient l’adoption, et les extensions étaient donc bien plus variées
  • Phar Lap a créé l’un des premiers DOS extender, permettant d’exploiter le processeur 80386 32 bits dans un environnement MS-DOS 16 bits
  • À la demande de Phar Lap, MetaWare a porté High C Compiler vers le SDK DOS extender de Phar Lap
  • Fujitsu a intégré le DOS extender de Phar Lap dans l’OS de la plateforme FM TOWNS basée sur le 80386, et High C est devenu le compilateur C officiel de cette plateforme
  • FM TOWNS est sorti en 1989, juste avant la ratification de C89, premier standard ANSI C

De petites fonctionnalités pratiques en avance sur leur temps

  • Séparateurs par soulignement dans les littéraux numériques

    • Il était possible d’insérer des soulignements dans les nombres pour rendre les longs littéraux numériques plus lisibles
    • C++ a introduit un séparateur similaire avec l’apostrophe, comme dans 1'000'000, dans C++14
    • C n’a adopté une fonctionnalité comparable qu’avec C23
  • Arguments étiquetés

    • Il était possible d’ajouter un nom aux arguments lors de l’appel de fonctions ayant beaucoup de paramètres, ou utilisant souvent des types comme bool, dont le sens n’est pas évident au point d’appel
    • Les arguments étiquetés de High C fonctionnaient d’une manière proche d’une fonctionnalité populaire de Python
      • Les étiquettes d’argument étaient facultatives
      • Lorsqu’elles étaient utilisées, les arguments pouvaient être fournis dans n’importe quel ordre avec la syntaxe argumentName => value
      • Il était possible de mélanger arguments étiquetés et non étiquetés, mais chaque paramètre de la fonction devait avoir un argument correspondant
    • Ni le C ni le C++ standard ne proposent encore cette fonctionnalité
  • Plages de case

    • Le langage permettait de faire correspondre en une fois toute une plage de valeurs, comme avec case low..high en Pascal
    • Le C et le C++ standard n’ont pas adopté cette fonctionnalité

Fonctions imbriquées et full function value

  • High C permettait de déclarer des fonctions imbriquées à l’intérieur d’autres fonctions, comme en Pascal
  • Son mode d’implémentation se rapprochait d’une forme plus complète que celle du Pascal standard ou de l’extension de fonctions imbriquées de GCC
  • High C permettait non seulement les déclarations de fonctions imbriquées, mais aussi la déclaration du type full function value
    • Contrairement à un pointeur de fonction C traditionnel, il contenait à la fois un pointeur de fonction et un pointeur de contexte
    • Cela permettait de retrouver le contexte capturé par une fonction imbriquée
    • Il s’agissait d’une closure non échappante, dont la durée de vie ne se prolongeait pas au-delà du retour de la fonction englobante
  • L’extension de fonctions imbriquées de GCC essayait de référencer une fonction imbriquée comme un pointeur de fonction ordinaire en écrivant du code exécutable sur la pile d’appel pour faire office de thunk du pointeur de contexte
    • Cette approche a entraîné d’importants risques de sécurité, au point que de nombreuses plateformes ont fini par la désactiver complètement
  • Les références à des fonctions locales dans High C pouvaient être utilisées comme des valeurs de première classe, sans toutefois voir leur durée de vie prolongée au-delà du retour de la fonction externe
  • Les fonctions imbriquées pouvaient aussi effectuer un goto vers leur fonction parente
    • Cela permettait une sortie non locale hors d’une fonction imbriquée, à la manière des blocs Smalltalk
    • On pouvait ainsi construire des fonctions se comportant comme des structures de contrôle
  • Objective-C a obtenu en 2009 les blocks, utilisables comme closures échappantes, et C++ a introduit les lambdas en 2011
  • Aucune de ces deux fonctionnalités n’offrait de capacité de sortie non locale
  • Le C standard ne dispose toujours pas de fonctionnalité officielle de fonctions imbriquées

Coroutines génératrices

  • MetaWare mettait particulièrement en avant sa fonctionnalité de générateurs, au point d’y consacrer tout un chapitre
  • En 1989, High C prenait déjà en charge des coroutines génératrices de style Python en C simple
  • Une fonction génératrice se déclarait avec la syntaxe void foo(Arg arguments) -> (Yield yields)
    • À l’intérieur, on pouvait appeler plusieurs fois la fonction magique yield(values...) pour produire une séquence de valeurs
    • Côté appelant, une nouvelle syntaxe de boucle for, de la forme for variable... <- foo(arguments...) do { ... }, permettait d’itérer successivement sur les valeurs générées
  • Cette implémentation pouvait se combiner de manière sophistiquée avec les fonctions imbriquées
    • Une fonction imbriquée à l’intérieur d’un générateur pouvait capturer le comportement de yield du générateur externe
    • Une fonction imbriquée pouvait s’appeler récursivement elle-même pour parcourir un arbre ou une structure de données récursive, tout en faisant un yield à chaque étape
  • Ce modèle semble difficile à reproduire dans Python ou dans beaucoup de langages grand public à générateurs/coroutines

Mode d’implémentation des générateurs et différence avec les langages standard

  • Les générateurs de High C fonctionnaient comme du sucre syntaxique au-dessus des fonctions imbriquées, sans runtime évolué
  • Une déclaration de générateur de la forme void foo(Arg arguments) -> (Yield yields) était équivalente à une fonction ordinaire déclarée comme void foo(void yield(Yield yields)!, Arg arguments)
    • yield était un paramètre implicite de type « full function value »
    • Les appels à yield(values) dans le corps du générateur n’étaient que des appels de fonction ordinaires vers ce paramètre implicite
  • Le corps de la boucle for côté appelant était transformé en fonction imbriquée
    • Cette fonction imbriquée était ensuite transmise comme argument yield au générateur
    • La structure était simple, mais efficace
  • Comme les fonctions imbriquées prenaient en charge la sortie non locale, les break, continue et goto qui sortaient du corps de la boucle for fonctionnaient eux aussi en effectuant un goto vers l’emplacement approprié à l’extérieur de la boucle
  • Il paraît peu probable que le C standard cherche à intégrer ce type de fonctionnalité
  • C++20 propose une fonctionnalité de coroutines très souple et complexe, fondée sur une transformation de coroutines à la compilation
    • Il semble possible de l’utiliser pour implémenter des générateurs
    • En revanche, le résultat ne se combinera probablement pas avec des fonctions locales d’une manière aussi intuitive

1 commentaires

 
GN⁺ 2024-09-27
Avis de Hacker News
  • En 2011, j’avais fait le point sur le for piloté par itérateur. C’était déjà l’une de ces fonctionnalités oubliées depuis longtemps, et j’avais aussi abordé la forme qu’elle pourrait prendre si elle entrait dans le standard C++
    Par chance, je possède un exemplaire de l’édition anglaise du High C/C++ Language Reference
    http://jdebp.uk./FGA/metaware-iterator-driven-for.html
    http://jdebp.uk./Proposals/metaware-iterator-driven-for.html
    • Je me demande comment break ou return étaient compilés. Est-ce que la fonction yield devait être transformée pour renvoyer un code d’état, ensuite vérifié au point d’appel ?
    • C’était intentionnel, le smiley à l’envers ?
  • En D, y compris dans Das BetterC, on trouve ce genre de fonctionnalités : underscores dans les littéraux numériques, plages dans les case, arguments nommés, fonctions imbriquées, fonctions imbriquées statiques, et des mécanismes proches des générateurs
    Par exemple, des formes comme int a = 1_234_567;, case 5 .. case 6: ou test(b:3, a:4); sont possibles
    Les fonctions imbriquées statiques ne peuvent pas accéder aux variables de frame de la fonction englobante, ce qui produit des erreurs du type Error: static function test.foo.plus cannot access variable i in frame of function test.foo
    Les mécanismes proches des générateurs sont décrits ici : https://dlang.org/spec/statement.html#foreach_over_struct_an...
    • Pendant toute la lecture de cet article, j’ai pensé à D. Je m’attendais presque à voir Walter Bright apparaître dans les commentaires
    • Je considère aussi que le garbage collector de D est une très bonne fonctionnalité. Dans le code bas niveau, il faut parfois gérer la mémoire manuellement, mais en pratique cela n’a souvent pas beaucoup d’importance, et le garbage collector simplifie énormément de choses
      Par exemple, si l’on crée un service de cache en mémoire, il vaut mieux que les entrées du cache elles-mêmes ne soient pas suivies par le garbage collector. Celui-ci ne connaît généralement pas les vrais schémas d’accès et peut donc gêner. Mais pour la plupart des autres composants du service, la présence d’un garbage collector convient beaucoup mieux
    • J’ai une question. Est-ce que quelqu’un sait pourquoi les gens n’aiment pas l’idée de fonctions imbriquées en C ?
      Pourquoi les arguments nommés prennent-ils la forme test(a:4, b:3) et non test(.a=4, b.=3); ?
      Je me demande aussi comment on pourrait gérer des types de première classe en C
  • Dans le même registre, le compilateur C lcc-win a ajouté la surcharge d’opérateurs, des arguments de fonction par défaut et la surcharge de fonctions. Dans la documentation, voir “generic functions” [1]
    Le compilateur C de Plan 9 a lui aussi introduit plusieurs extensions de langage, dont certaines, comme les structures/unions anonymes, ont ensuite été intégrées au standard C. Aujourd’hui, GCC accepte le flag -fplan9-extensions [2], qui active des fonctionnalités assez utiles, comme la conversion automatique de pointeurs de structures vers des champs anonymes lors des appels de fonction et des affectations
    [1] https://lcc-win32.services.net/C-Tutorial.pdf
    [2] https://gcc.gnu.org/onlinedocs/gcc/Unnamed-Fields.html
  • Qui était le génie qui a créé ces fonctionnalités ? Il devait y avoir quelqu’un de vraiment visionnaire dans cette entreprise
    C’est dommage qu’elles ne se soient pas largement diffusées et qu’elles n’aient pas influencé les standards des langages. C’est étonnant que de telles fonctionnalités aient existé il y a si longtemps
    Le sujet avait déjà été abordé sur Hacker News : https://news.ycombinator.com/item?id=38938402
    Existe-t-il quelque part une copie PDF ?
    • CLU disposait déjà, au milieu ou à la fin des années 1970, d’itérateurs, c’est-à-dire de générateurs et de boucles for avec yield [0]. Le langage Icon, à peu près à la même époque, avait aussi un mécanisme de générateurs similaire [1], avec suspend à la place de yield. Il me semble qu’Ada (1983) avait également ce type de fonctionnalité
      Ces fonctionnalités de langage n’étaient pas totalement inconnues
      [0] https://publications.csail.mit.edu/lcs/pubs/pdf/MIT-LCS-TR-2...
      [1] https://dl.acm.org/doi/pdf/10.1145/800055.802034
    • Bitsavers possède une copie du manuel de référence HC 1.2 (1985)
      Il décrit les underscores dans les nombres, les plages de case, les paramètres nommés, les fonctions imbriquées et même de véritables variables de fonction
      https://bitsavers.org/pdf/metaware/…
      Voir l’annexe A, environ 50 pages avant la fin du fichier
    • MetaWare était une société prolifique de compilateurs basée à Santa Cruz dans les années 80 et 90. J’aimais ce qu’ils faisaient, et leur culture était assez intéressante
      À l’époque où j’apprenais et écrivais du code, je les avais découverts via des sites un peu douteux
    • Ce n’est pas si surprenant. En fouillant dans les archives des langages de programmation de haut niveau postérieurs à FORTRAN, Lisp, ALGOL et COBOL, on trouve beaucoup d’idées de ce genre
      On découvre aussi la riche histoire des langages de programmation système. On voit également à quel point la conception de C et de Go se ressemble dans leur manière d’ignorer ce qui se faisait dans d’autres écosystèmes et les expériences passées
    • C’est dommage que ces fonctionnalités ressemblent à des nouveautés au lieu de faire partie de la liste standard des fonctionnalités fournies par la plupart des langages de programmation

Le lien vers le manuel du compilateur se trouve ici : https://winworldpc.com/product/metaware-high-c-cpp/33x
Le PDF du manuel C porte une mention de copyright de 2007

  • La soumission précédente et les commentaires sont ici : https://news.ycombinator.com/item?id=38938402
    • Aujourd’hui, Joe Groff l’a remis en avant sur le FediVerse, donc il semble que ce soit pour cela que ça réapparaisse ici aussi
      https://f.duriansoftware.com/@joe/113195961485703110
    • L’auteur semble avoir republié hier le même contenu à une autre URL. Étrange
  • Si vous vous demandez pourquoi, dans l’exemple de l’image, le littéral de chaîne se termine par ¥n et non par \n, c’est probablement parce que ces exemples de code ont été écrits en Shift-JIS. En Shift-JIS, ¥ occupe la position du \ ASCII
    • À l’origine, il s’agissait de JIS Roman [0], une variante japonaise de l’ASCII datant de 1969. Shift-JIS a ajouté bien plus tard la prise en charge d’un jeu de caractères à deux octets
      [0] https://en.wikipedia.org/wiki/JIS_X_0201
    • Le problème est qu’en Shift-JIS, le code ASCII de l’antislash est aussi utilisé comme second octet de caractères sur 2 octets. Du coup, en C, les littéraux de chaînes japonais ne fonctionnent pas toujours correctement
      Pour cet usage, EUC-JP convient mieux, car il n’a pas ce problème. En Pascal, si l’on utilise les commentaires (* *) et non les commentaires { }, Shift-JIS n’a pas ce problème
    • L’auteur ne donne pas d’information sur la date de parution de ce livre, et je n’en trouve pas. Mais au moment de sa sortie, la norme Shift-JIS n’existait probablement pas encore
      Il est beaucoup plus probable qu’on ait utilisé à la place JIS X 0201 (https://en.m.wikipedia.org.org/wiki/JIS_X_0201), qui a servi de base à Shift-JIS
    • De même, l’invite DOS japonaise affichait C:¥ et non C:\
  • Ces extensions sont des fonctionnalités d’Ada. Ada a des libellés de la forme Call (Param_A => 1, Param_B => "Foo");, des underscores dans les nombres en base arbitraire (X : Integer := 1_000;), des sous-programmes imbriqués et des vérifications fondées sur des intervalles
    • Comme l’article le dit aussi, Pascal possédait ces fonctionnalités avant Ada, et les types de tâches avec point d’entrée peuvent en fait être vus comme des générateurs
      On oublie souvent à quel point C était incroyablement primitif par rapport à de nombreux autres langages de l’époque
  • Indépendamment du contenu, la typographie de ce livre est intéressante. Elle donne une impression à la fois magnifique et atroce
    Je ne connais pas assez les conventions typographiques japonaises ni les règles de crénage, mais on dirait qu’ils ont pris une police à chasse variable contenant à la fois des kanjis et des caractères latins, puis l’ont forcée à entrer dans des cases à chasse fixe
    En tout cas, c’est appréciable que les exemples de code ne soient pas en 8 pt comme dans beaucoup de livres que je possède
  • Les générateurs me font penser au problème de l’itération interne/externe en Rust et à try_fold() (https://scribe.rip/@veedrac/rust-is-slow-and-i-am-the-cure-3...)
  • Les générateurs, en particulier, semblent très en avance sur leur temps. Peut-être que Fujitsu a simplement pu les implémenter parce qu’ils n’avaient pas à se soucier d’un long processus de normalisation
    Mais c’est précisément pour cette raison que ces extensions sont restées relativement peu connues, et qu’elles ont apparemment dû être redécouvertes et réinventées des décennies plus tard dans le C/C++ moderne
    • Ce n’était pas Fujitsu, mais MetaWare. MetaWare avait une solide expérience des compilateurs et, à la même époque, disposait aussi d’un compilateur Pascal assez connu. Pascal avait déjà des fonctions imbriquées
    • C aurait pu devenir un bien meilleur langage s’il n’avait pas été accaparé par des gens qui insistaient pour que même le complément à deux ne soit pas inscrit dans la norme
    • Les coroutines et les générateurs étaient déjà des concepts bien compris à l’époque. Il suffit de regarder Icon. Donc la raison principale semble vraiment plutôt être qu’ils n’avaient pas à se soucier du poids de la normalisation