Les extensions de langage disparues du compilateur MetaWare High C (2023)
(duriansoftware.com)- 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
forde 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é
- 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
-
Plages de
case- Le langage permettait de faire correspondre en une fois toute une plage de valeurs, comme avec
case low..highen Pascal - Le C et le C++ standard n’ont pas adopté cette fonctionnalité
- Le langage permettait de faire correspondre en une fois toute une plage de valeurs, comme avec
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
gotovers 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 formefor variable... <- foo(arguments...) do { ... }, permettait d’itérer successivement sur les valeurs générées
- À l’intérieur, on pouvait appeler plusieurs fois la fonction magique
- 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
yielddu 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
- Une fonction imbriquée à l’intérieur d’un générateur pouvait capturer le comportement de
- 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 commevoid 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
forcôté appelant était transformé en fonction imbriquée- Cette fonction imbriquée était ensuite transmise comme argument
yieldau générateur - La structure était simple, mais efficace
- Cette fonction imbriquée était ensuite transmise comme argument
- Comme les fonctions imbriquées prenaient en charge la sortie non locale, les
break,continueetgotoqui sortaient du corps de la boucleforfonctionnaient eux aussi en effectuant ungotovers 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
Avis de Hacker News
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
breakoureturnétaient compilés. Est-ce que la fonctionyielddevait être transformée pour renvoyer un code d’état, ensuite vérifié au point d’appel ?case, arguments nommés, fonctions imbriquées, fonctions imbriquées statiques, et des mécanismes proches des générateursPar exemple, des formes comme
int a = 1_234_567;,case 5 .. case 6:outest(b:3, a:4);sont possiblesLes 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.fooLes mécanismes proches des générateurs sont décrits ici : https://dlang.org/spec/statement.html#foreach_over_struct_an...
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
Pourquoi les arguments nommés prennent-ils la forme
test(a:4, b:3)et nontest(.a=4, b.=3);?Je me demande aussi comment on pourrait gérer des types de première classe en C
lcc-wina 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
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 ?
yield[0]. Le langage Icon, à peu près à la même époque, avait aussi un mécanisme de générateurs similaire [1], avecsuspendà la place deyield. 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
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 fonctionhttps://bitsavers.org/pdf/metaware/…
Voir l’annexe A, environ 50 pages avant la fin du fichier
À l’époque où j’apprenais et écrivais du code, je les avais découverts via des sites un peu douteux
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
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
https://f.duriansoftware.com/@joe/113195961485703110
¥net 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[0] https://en.wikipedia.org/wiki/JIS_X_0201
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èmeIl 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
C:¥et nonC:\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 intervallesOn oublie souvent à quel point C était incroyablement primitif par rapport à de nombreux autres langages de l’époque
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
try_fold()(https://scribe.rip/@veedrac/rust-is-slow-and-i-am-the-cure-3...)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