Acid, le lisp-like de la communauté !

Créons notre langage de programmation ! Pour le fun !

a marqué ce sujet comme résolu.

C'est vrai, ça doit être super intéressant. Si tu veux t'y attaquer sur l'autre projet (impératif), je suis en train d'écrire le front-end, il me manque le back-end. ^^

mehdidou99

Je le vois plus comme un projet independant vers lequel vous pourriez compiler n'importe quel petit langage en fait. En réalité c'est facile à faire, en une soirée tu peux avoir un truc qui marche. Mais ça m'intéresse de toucher a des features rigolotes comme la possibilité de faire nativement des appels système, celle de threader vs. Un GIL, pourquoi pas un JIT…

+2 -0

Du coup Eusèbe, tu critique la spec en disant, entre autre, que le système de typage est bancale. Pourrais tu pointer ce qui est bancale ? (Je t'ai pas vue le justifier et ça m'intéresse car à la lire j'ai pas repéré, ton avis m'intéresse). De plus, du coup, tu partirai sur quoi comme "spec" pour ce type de langage ?

Kje

Principalement, un décalage entre les objectifs et les propriétés concrètes du système, en plus d'une formalisation hâtive de parties importantes qui laisse des questions en suspens. Par exemple, pour éviter d'avoir à écrire une inférence de types, on décide de typer explicitement toutes les fonctions du toplevel. Mais la sémantique de ces contraintes n'est pas claire : si j'écris (hastype (lambda (List a) (List a)) ma_fonction), quelle est la signification de la variable de type a ? La sémantique habituelle de ML, qui est la source d'inspiration directe, est de lui donner un sens existentiel : pour simplifier un peu, « il existe un type a tel que ma_fonction est de type list a -> list a ». Mais on peut aussi décider que c'est une quantification universelle : « pour tout type a, ma fonction est de type list a -> list a ».

Maintenant, imaginons que je donne cette contrainte de type à une fonction de type list int -> list int. Selon le sens de la variable a, le comportement attendu diffère radicalement : dans le premier cas, le typeur accepte, mais doit quand même retenir que le type de ma fonction est list int -> list int pour les appels suivants. Pour cela, il a besoin de calculer le type de la fonction puis de vérifier que la contrainte est effectivement plus générale (puis il oublie la contrainte dans la suite).

Dans le deuxième cas, le typeur doit refuser la contrainte : elle n'est pas une généralisation d'un type de la fonction. Ça ne demande pas forcément d'écrire un calcul complet du type le plus général de la fonction, mais il faut quand même quelque chose de plus compliqué que juste vérifier que les types sont corrects à chaque opération du programme. De toute façon, pour des raisons un peu compliquées, ce n'est pas le bon choix : on a besoin des variables existentielles si on veut faire du ML (par exemple, let f : 'a. 'a list -> 'a list = List.map (fun x -> x) ne type pas dans OCaml).

Dans tous les cas, s'aventurer dans ce genre de systèmes de types en faisant de l'à-peu-près est une garantie à peu près certaine de se casser la figure. Si vous voulez des types (je pense vraiment qu'il vaut mieux commencer par s'en passer, surtout si vous n'avez pas l'habitude de manipuler ce genre de choses dans vos propres programmes), prenez un système déjà spécifié, sans essayer de faire des modifications au petit bonheur la chance pour le simplifier.

Qu'on ne me fasse pas dire ce que je n'ai pas dit : c'est un domaine passionnant, et je serais ravi qu'on parle un peu plus de typage et un peu moins de C++/Python/Javascript sur ZdS :-° Pour autant, il ne faut pas mettre la charrue avant les boeufs : avant d'implémenter un typeur, il faut déjà avoir une idée de comment implémenter un langage non typé, et il faut avoir une idée de comment le système de types fonctionne (ce qui, en l'occurrence, se fait très bien en pratiquant un peu avec OCaml ou Haskell). Et avant de formaliser son propre système, il faut avoir les idées bien claires sur la théorie sous-jacente.

Il y a aussi quelques défauts de moindre importance, ou qui dans l'absolu n'en sont pas vraiment mais se trouvent être des mauvaises idées dans le cadre actuel : les trouzmille types d'entier, l'ajout inutile des flottants et des caractères, les tuples qui n'apportent rien… en fait, des points qui vont dans une tendance à laquelle il faut faire attention qui consiste à insister beaucoup sur une surspécification des détails (vous vous rappelez de « est-ce qu'on écrit -> ou lambda ? » ?) en négligeant les fondations (du bikeshedding, en somme.)

Nohar : go for it !

+4 -0

La sémantique habituelle de ML, qui est la source d'inspiration directe, est de lui donner un sens existentiel

Alors c’est justement là que tu te trompes : la source d’inspiration directe, c’est Haskell. Plus précisément, Haskell sans les classes de types, que je réserve pour plus tard, si besoin. Donc les contraintes sont de type forall : une fonction sur des listes qui utiliserait dans son traitement une fonction qui ne porte que sur les entiers ne pourrait avoir le type (lambda (List a) (List a)). :)

Si vous voulez des types […] prenez un système déjà spécifié, sans essayer de faire des modifications au petit bonheur la chance pour le simplifier.

C’est largement le cas. Comme dit plus haut, j’ai repris le système de types de Haskell, en remplaçant une manière de surcharger des fonctions (les classes de types) par une autre (les modules homonymes des types), a priori plus aisée à mettre en place, car plus explicite. ;-)

Il y a aussi quelques défauts de moindre importance, ou qui dans l'absolu n'en sont pas vraiment mais se trouvent être des mauvaises idées dans le cadre actuel : les trouzmille types d'entier, l'ajout inutile des flottants et des caractères, les tuples qui n'apportent rien… en fait, des points qui vont dans une tendance à laquelle il faut faire attention qui consiste à insister beaucoup sur une surspécification des détails (vous vous rappelez de « est-ce qu'on écrit -> ou lambda ? » ?) en négligeant les fondations (du bikeshedding, en somme.)

Bon, c’est dommage, tu retombes dans le travers de « laissez tomber, c’est trop compliqué pour vous »… En toute sincérité, qu’est-ce que cela change ? Y a-t-il une différence fondamentale de fonctionnement entre l’utilisation d’entiers signés sur 16 bits et d’entiers non signés sur 32 bits, sachant que toutes les machines modernes gèrent les uns et les autres nativement, et que a fortiori, la plupart des langages d’implémentation sauront les gérer aussi sans qu’on ait rien à faire ?

Je répète, y a-t-il quoi que ce soit de réellement plus difficile à gérer 8 types d’entiers bornés plutôt qu’un seul ? Ou est-ce que ce sera juste un chouïa plus long à taper le code ? Idem pour les tuples, ce ne sont jamais que des plages de mémoire adjacentes / tuples / structures de langage impératif : où se trouve la difficulté supplémentaire ?

Il y a effectivement deux difficultés dans ces types natifs. Les entiers non bornés, tout d’abord. Ceux-là ont été explicitement demandés un peu plus haut dans la discussion. Et la solution sera très certainement de faire appel à une bibliothèque tierce, comme la plupart des langages qui en ont. Les caractères Unicode, ensuite. Là, de fait, rien à dire, ça risque d’être un peu plus compliqué qu’un simple char de C. Encore une fois, la solution viendra certainement d’une bibliothèque tierce ou d’un langage d’implémentation qui gère nativement ce genre de caractères (comme Rust ou Haskell, par exemple).

+1 -0

Je le vois plus comme un projet independant vers lequel vous pourriez compiler n'importe quel petit langage en fait. En réalité c'est facile à faire, en une soirée tu peux avoir un truc qui marche. Mais ça m'intéresse de toucher a des features rigolotes comme la possibilité de faire nativement des appels système, celle de threader vs. Un GIL, pourquoi pas un JIT…

nohar

Ça me plairait de te donner un coup de main, ça me ferait la main avec Python. :)

+1 -0

Je le vois plus comme un projet independant vers lequel vous pourriez compiler n'importe quel petit langage en fait. En réalité c'est facile à faire, en une soirée tu peux avoir un truc qui marche.

nohar

Peut-être un point de convergence avec Seventh ?

Si on peut avoir rapidement un truc fonctionnel je vais essayer de dev quelques chose ce soir si possible.

+1 -0

Je le vois plus comme un projet independant vers lequel vous pourriez compiler n'importe quel petit langage en fait. En réalité c'est facile à faire, en une soirée tu peux avoir un truc qui marche. Mais ça m'intéresse de toucher a des features rigolotes comme la possibilité de faire nativement des appels système, celle de threader vs. Un GIL, pourquoi pas un JIT…

nohar

Ça me plairait de te donner un coup de main, ça me ferait la main avec Python. :)

mehdidou99

OK, je comptais partir dessus en Rust à la base, mais va pour ça, lançons un sujet pour écrire une VM avec Python. :)

+1 -0

J'ai essayé d'imaginer un ensemble d'instruction. En gros on aurai ça:

  • PUSH_INT <value>: Ajoute un entier au haut de la pile
  • LOAD_GLOBAL <id>: Ajoute la valeur de id en haut de la pile
  • LOAD_LOCAL <n>: Ajoute le n-ième argument en haut de la pile
  • POP: Enlève l'élément en haut de la pile
  • APPLY <n>: Applique à l'élément en haut de pile les n éléments précédents
  • PROC [<instructions> ...]: Ajoute en haut de la pile une procédure avec les instructions données
  • STORE <id>: Assigne à id l'élément en haut de pile

Avec ça on peut traduire du Acid facilement. Si on prend l'exemple suivant:

1
2
3
4
5
6
7
(define foo (lambda (x y)
  (* x (+ y 2))
))

(define main (lambda ()
  (print ((foo 1 2)))
))

… on obtiendrait le bytecode suivant:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
INSTRUCTION                     STACK                  LOCAL               ENVIRONMENT

                                []                     []                  {}
PROC [
  LOAD_LOCAL 1                    [x]                  [x, y]                {}
  LOAD_LOCAL 2                    [x, y]               [x, y]                {}
  PUSH_INT 2                      [x, y, 2]            [x, y]                {}
  LOAD_GLOBAL +                   [x, y, 2, +]         [x, y]                {}
  APPLY 2                         [x, (+ y 2)]         [x, y]                {}
  LOAD_GLOBAL *                   [x, (+ y 2), *]      [x, y]                {}
  APPLY 2                         [(* x (+ y 2))]      [x, y]                {}
]                               [<proc>]                                   {}
STORE foo                       [<proc>]                                   {foo: <proc>}
POP                             []                                         {foo: <proc>}
PROC [
  PUSH_INT 1                      [1]                  []                    {foo: <proc>}
  PUSH_INT 2                      [1, 2]               []                    {foo: <proc>}
  LOAD_GLOBAL foo                 [1, 2, foo]          []                    {foo: <proc>}
  APPLY 2                         [(foo 1 2)]          []                    {foo: <proc>}
  LOAD_GLOBAL print               [(foo 1 2), print]   []                    {foo: <proc>}
  APPLY 1                         [(print (foo 1 2))]  []                    {foo: <proc>}
]                               [<proc>]                                   {foo: <proc>}
STORE main                      [<proc>]                                   {foo: <proc>, main: <proc>}
APPLY 0                         [None]                                     {foo: <proc>, main: <proc>}
POP                             []                                         {foo: <proc>, main: <proc>}

L'instruction PROC ressemble à la définition d'une fonction mais en fait c'est juste un PUSH de bytecode ordinaire (donc le code reste "linéaire").

La sémantique habituelle de ML, qui est la source d'inspiration directe, est de lui donner un sens existentiel

Alors c’est justement là que tu te trompes : la source d’inspiration directe, c’est Haskell. Plus précisément, Haskell sans les classes de types, que je réserve pour plus tard, si besoin.

Et sans un tas d'autres choses, mais Haskell est un ML (qui est plus une famille de langages et de systèmes de types qu'un dialecte précis - d'ailleurs, même dans les langages qui s'appellent « ML », il y a plusieurs implémentations différentes). Désolé si ce n'était pas clair.

Donc les contraintes sont de type forall : une fonction sur des listes qui utiliserait dans son traitement une fonction qui ne porte que sur les entiers ne pourrait avoir le type (lambda (List a) (List a)). :)

Ce n'est écrit nulle part dans ta spécification, et c'est très important. En fait, ta spécification ne parle même nulle part des variables de types, il y a seulement une vague ligne « les types peuvent prendre des paramètres » : sans connaître à l'avance un langage fonctionnel typé (ce qui n'est le cas d'à peu près personne dans cet atelier), aucune chance de comprendre comment ça marche (et même en ayant l'habitude, comme tu peux le constater, ce n'est pas suffisamment bien défini).

Ceci étant dit, forcer un typage explicite des fonctions du top-level ne te permettra pas de faire l'économie d'un typeur qui fasse un minimum d'inférence (en fait, ça ne changera même pas grand chose). Tu n'y arriveras pas avec une vérification « à la C » qui se contente de s'assurer que « les types sont compatibles » là où c'est pertinent de se poser la question.

Si vous voulez des types […] prenez un système déjà spécifié, sans essayer de faire des modifications au petit bonheur la chance pour le simplifier.

C’est largement le cas. Comme dit plus haut, j’ai repris le système de types de Haskell, en remplaçant une manière de surcharger des fonctions (les classes de types) par une autre (les modules homonymes des types), a priori plus aisée à mettre en place, car plus explicite. ;-)

Le système de namespaces n'a rien à voir avec les typeclasses (parce que, précisément, il n'y a aucune surcharge). Mais indépendamment de ça, ce n'est pas à moi qu'il faut le dire ici, c'est dans tes spécifications qu'il faut l'écrire (et l'écrire explicitement : tu ne peux pas compter sur le lecteur pour deviner où tu penses que les quantifications se font, et pour le coup ce n'est pas quelque chose que tu peux laisser au choix de l'implémentation).

Il y a aussi quelques défauts de moindre importance, ou qui dans l'absolu n'en sont pas vraiment mais se trouvent être des mauvaises idées dans le cadre actuel : les trouzmille types d'entier, l'ajout inutile des flottants et des caractères, les tuples qui n'apportent rien… en fait, des points qui vont dans une tendance à laquelle il faut faire attention qui consiste à insister beaucoup sur une surspécification des détails (vous vous rappelez de « est-ce qu'on écrit -> ou lambda ? » ?) en négligeant les fondations (du bikeshedding, en somme.)

Bon, c’est dommage, tu retombes dans le travers de « laissez tomber, c’est trop compliqué pour vous »…

Où est-ce que j'ai écrit dans le message que tu cites la moindre phrase qui te fasse penser ça ?

En toute sincérité, qu’est-ce que cela change ? Y a-t-il une différence fondamentale de fonctionnement entre l’utilisation d’entiers signés sur 16 bits et d’entiers non signés sur 32 bits, sachant que toutes les machines modernes gèrent les uns et les autres nativement, et que a fortiori, la plupart des langages d’implémentation sauront les gérer aussi sans qu’on ait rien à faire ?

Ce que ça change, c'est des complications complètement inutiles à développer et à maintenir alors qu'il y a déjà largement assez à côté. Honnêtement, qu'est-ce que ça change que le langage soit capable de faire la différence entre des entiers signés ou non-signés sur 32 ou 64 bits ?

Je répète, y a-t-il quoi que ce soit de réellement plus difficile à gérer 8 types d’entiers bornés plutôt qu’un seul ? Ou est-ce que ce sera juste un chouïa plus long à taper le code ? Idem pour les tuples, ce ne sont jamais que des plages de mémoire adjacentes / tuples / structures de langage impératif : où se trouve la difficulté supplémentaire ?

Il y a quoi de réellement plus limitant à se restreindre à un seul type d'entiers ? Encore une fois, vous ne codez pas un langage pour qu'il soit utilisable, mais pour apprendre. Dans ce cadre, une fonctionnalité qui complique votre tâche sans apport pédagogique concret est à jeter à la poubelle. C'est le cas ici.

Il y a effectivement deux difficultés dans ces types natifs. Les entiers non bornés, tout d’abord. Ceux-là ont été explicitement demandés un peu plus haut dans la discussion. Et la solution sera très certainement de faire appel à une bibliothèque tierce, comme la plupart des langages qui en ont.

Mais alors ça n'a aucun intérêt : ça devient une fonctionnalité « pour l'utilisation » au lieu d'être une fonctionnalité « pour l'apprentissage ». Soit vous voulez comprendre comment on implémente des entiers à taille variable, et il faut le faire vous-mêmes, soit vous considérez (mon avis personnel est que c'est le bon choix) que c'est quelque chose d'indépendant à l'implémentation d'un langage et qu'il vaudrait mieux faire ça sous forme de lib dans un autre projet, et vous pouvez oublier.

Les caractères Unicode, ensuite. Là, de fait, rien à dire, ça risque d’être un peu plus compliqué qu’un simple char de C. Encore une fois, la solution viendra certainement d’une bibliothèque tierce ou d’un langage d’implémentation qui gère nativement ce genre de caractères (comme Rust ou Haskell, par exemple).

Pareil que pour les entiers non bornés, sauf que pour le coup, vous n'avez vraiment pas envie d'implémenter ça vous-mêmes. Soit votre langage le gère nativement (coup de bol, Python le fait) et vous n'avez pas besoin de vous en préoccuper, soit vous ne devriez pas vous en préoccuper parce que c'est totalement orthogonal aux problématiques d'interprétation et de compilation.

Maintenant, si vraiment vous y tenez, je le redis : je n'empêche personne de le faire. Vous avez un avis et des conseils qui cherchent à vous faire avancer le plus possible dans la direction « apprendre à implémenter un langage », libre à vous de les suivre ou pas.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte