Acid, le lisp-like de la communauté !

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

a marqué ce sujet comme résolu.

Salut,

Maintenant que j'ai écrit le parser en Haskell, il faut que je le transforme en AST Python en JSON depuis Haskell ? Ou bien finalement on part sur une traduction AST Acid ?

Il faudrait définir un schéma du compilateur pour y voir plus clair — en tout cas moi ça m'aidrait parce que je suis un peu perdu. De ce que j'ai compris, notre algorithme donnerait quelque chose comme ça:

code – parser -> AST Acid – serializer -> AST Acid en JSON – interpréter/compilateur python -> AST Python – exécution

PS: @mehdidou99: c'est quoi une entité ? (je suis super nul en C++ et j'ai pas trouvé sur le net :( )

PS 2: Je dois commenter mon code Haskell, je sais. Et aussi rédiger des tests (jamais fait ça en Haskell, c'est l'occasion d'essayer)

+4 -0

Je pense que ton plan me convient, faut juste définir l'AST Acid.

Puis on pourra ouvrir un repo séparé : Acid JIT Compiler (il compile en Bytecode qui doit être interprété… c'est pas courant comme plan mais le plus simple pour nous.)

+1 -2

Je sais pas trop…

Peut-être que c'est plus facile pour nous, mais on est ici pour apprendre comment écrire un compilateur, pas pour apprendre à écrire ce compilateur. Dans ma vision des choses, on devrait coder le projet avec des techniques largement employées. Encore mieux: on devrait coder plusieurs compilateurs avec différents combinaisons de techniques (une technique par étape). Comme à la fin on aura abordé les différentes manières d'écrire un compilateur, et on pourra choisir les techniques qui conviendront le mieux à notre projet.

Par technique j'entends machine virtuelle à piles, à graphes, compilateur JIT, optimisation, static checker, parsers combinatoires, etc…

En fait, il est complètement pourri, mon lexer générique… Bon, on reprend et on réfléchit avant de vomir du code, maintenant ! :)

@mehdidou99: c'est quoi une entité ? (je suis super nul en C++ et j'ai pas trouvé sur le net :( )

AlphaZeta

En fait, ce n'est pas un concept propre au C++ mais un concept d'OO. Recherche "classe à sémantique d'entité" si tu veux te renseigner.

+0 -0

Vu que faire des tests et documenter son projet c'est moins intéressant que de coder, j'ai commencé la rédaction d'un interpréteur minimal en Haskell. Il peut exécuter le code d'exemple:

1
2
3
4
5
6
7
8
9
(define add (lambda (x y)
    (+ x y)
))

// ligne de commentaire

/* bloc de commentaire */

(print (add 1 3))

Et il m'affiche ça:

1
4

Après y'a pas encore de types, donc c'est facile, mais je doute qu'on puisse étendre facilement mon code actuel…

Edit: Mon implémentation gère naturellement l'application partielle parce que c'était plus facile que de restreindre à une application complète. Du coup je peux interpréter le programme Acid suivant:

1
2
3
(define increment (+ 1))

(print (increment 16))

… et ça me donne bien 17 :)

Par contre je sais pas comment ça va se passer avec les fonctions variadiques (genre qui acceptent un nombre indéterminé d'arguments) :°

+7 -0

Comme promis, et bien qu’avec un peu de retard, une nouvelle spécification d’Acid, complète, cette fois. Avec un exemple de programme complet en bas, qui doit afficher la factorielle de 42.

Je suis fatigué, donc je n’ai pas rédigé les explications allant avec la spec, ni commenté le code source. Ça viendra plus tard. Malgré tout, n’hésitez pas à lire le tout et à me faire des remarques : il est toujours possible de la retravailler / corriger.

Quand elle aura été suffisamment approuvée, je ferai une PR sur le repo principal. :)

Généralités

(1) La spécification d’Acid comporte deux types de code : le code basique est indispensable au fonctionnement de tout programme Acid, et doit impérativement être compris par toute implémentation d’un compilateur ou d’un interpréteur Acid ; le sucre syntaxique, au contraire, offre des facilités d’écriture que les implémentations ne sont pas tenues d’offrir, même si cela est fortement recommandé.

(2) Un code source Acid est composé d’une succession d’expressions, dont l’ordre n’est pas significatif. Une expression est entourée de parenthèses, et peut être constituée de :

  • au plus un mot-clé, nécessairement en première position de l’expression, par exemple lambda ;
  • des identifiants, par exemple acid ;
  • des valeurs littérales, par exemple 42 ;
  • des expressions.

Lorsqu’une expression ne contient qu’un seul élément, ne contenant lui-même pas d’espace, les parenthèses qui l’entourent peuvent être omises.

Voici un exemple de code source Acid, contenant deux expressions.

1
2
3
4
5
6
7
8
(hastype (Bool -> a -> a -> a) if)

(define if (lambda (cond if_true if_false) (
    match cond (
        (True if_true)
        (False if_false)
    )
)))

(3) Les implémentations Acid sont tenues d’accepter les codes sources encodés en UTF-8.

(4) Tout code contenu entre les caractères /* et */ ou entre les caractères // et la fin de la ligne est ignoré.

Lambdas

(5) Une lambda est un objet prenant aucun, un ou plusieurs arguments et renvoyant une valeur de retour, selon un traitement toujours identique, et défini dans le corps de la lambda.

(6) Une lambda est définie au moyen d’une expression comprenant trois éléments : le mot-clé lambda, une expression contenant la liste des identifiants des arguments de la lambda, et une expression décrivant le corps de la lambda.

(7) Si la lambda ne prend aucun argument, le mot-clé et la liste d’arguments peuvent être omis. S’ils sont conservés, la liste d’arguments est représentée comme une simple paire de parenthèses.

(8) Une lambda est appelée au moyen d’une expression contenant la définition de la lambda, ou son identifiant lorsqu’il existe, suivie par les différents arguments que l’on veut lui passer, chacun sous la forme d’une expression.

Voici un exemple de définition d’une lambda, qui appelle une lambda nommée, et un exemple de lambda directement appelée.

1
2
3
4
5
// Lambda non nommée.
(lambda x (lambdanommée x 42))

/* Appel direct de lambda non nommée. */
((lambda (x y) (+ x y)) 34 57)

Types

(9) Acid possède une liste limitée de types natifs.
(9.1) Les types entiers se répartissent en trois catégories :

  • le type Int, qui correspond à un entier signé de la taille d’un registre généraliste de la machine utilisée ;
  • les types Int8, Int16, Int32 et Int64, qui correspondent à des entiers signés tenant respectivement dans 8, 16, 32 et 64 bits ;
  • les types Word8, Word16, Word32 et Word64, qui correspondent aux équivalents non signés des précédents.

(9.2) Les types flottants sont Float pour les flottants simple précision, et Double pour les entiers double précision.
(9.3) Les caractères, de type Char, peuvent couvrir l’ensemble des caractères Unicode.
(9.4) Les tuples sont la réunion ordonnée d’un nombre fixe de valeurs de types potentiellement différents. L’identifiant de leur type est composé du mot-clé tuple suivi de chacun des identifiants des types inclus, le tout entre parenthèses, par exemple (tuple Int Char (tuple Char Char)). Un tuple vide a par conséquent le type (tuple).
(9.5) L’identifiant du type des lambdas est la succession des identifiants du type de chaque argument, puis de la valeur de retour, chacun séparé du suivant par les caractères -> ou , le tout entouré de parenthèses. Par exemple, une lambda qui prend deux caractères et renvoie un entier aura pour type (Char -> Char -> Int).

(10) Dans les valeurs littérales de type flottant, la séparation entre partie entière et partie décimale est marquée par un point.

(11) Les valeurs littérales de type Char sont entourées de guillemets droits simples, par exemple 'a'.

(12) Les tuples sont des expressions, définies par le mot-clé tuple, suivi d’expressions ou de valeurs littérales, par exemple (tuple 'a' 'b' (+ 1 41)).

(13) Un type algébrique est défini par une expression, contenant :

  • le mot-clé type ;
  • une liste de paramètres, qui peut être omise si elle est vide ;
  • une liste de constructeurs, chacun constitué d’un identifiant et, si nécessaire, d’une liste d’identifiants de types que le constructeur prend en argument.

Par exemple, le code suivant définit une liste chaînée simple, avec un paramètre de type.

1
2
3
4
(type a (
    Nil
    (Cons a (List a))
))

(14) En dehors de sa définition, un constructeur doit être précédé de l’identifiant de son type, et du caractère :: ou . Par exemple, si le type précédent s’appelle List, on n’écrira pas Nil, mais List::Nil.

(15) <sucre syntaxique> Si la bibliothèque standard définit un type de liste chaînée, celui-ci peut-être représenté par des crochets entourant le type, ou la liste des valeurs séparées par des virgules. Par exemple, List Char peut devenir [Char], et Cons 12 (Cons 13 (Cons 14 Nil)) peut devenir [12, 13, 14].

(16) <sucre syntaxique> Si la bibliothèque standard définit un type de liste chaînée, une valeur littérale de type chaîne de caractères peut se représenter entre guillemets droits doubles, par exemple "Acid".

(17) Le type paramétré IO a est un type fictif, servant à représenter les actions d’entrée-sortie. Le paramètre a représente le type de la valeur renvoyée par l’action, soit (tuple) dans le cas d’une action qui ne renvoie rien.

(18) Il est possible de signaler explicitement le type d’un objet au moyen d’une contrainte de type. Celle-ci est une expression composée du mot-clé hastype, d’un identifiant de type, et de l’objet ou de son identifiant, par exemple, (hastype Word64 42).

(19) La présence d’une contrainte de type est obligatoire pour les lambdas nommées.

Filtrage par motifs

(20) Un filtrage par motifs est une expression, composée de :

  • le mot-clé match ;
  • une expression dont la valeur sera comparée aux motifs ;
  • une liste de motifs, composés eux-mêmes d’un constructeur ou d’une valeur, suivi d’une expression à évaluer si la valeur correspond au motif.

Voici un exemple de filtrage par motif sur le type de liste donné en exemple au point 13.

1
2
3
4
match xs (
    (Nil 42)
    ((Cons x next) (+ x 79))
)

(21) Si l’on ne veut pas lier un élément d’un motif à un identifiant, on remplace ce dernier par le masque _.

Par exemple, dans le code précédent, next n’est pas utilisé dans l’expression à évaluer. Il serait donc plus judicieux d’écrire ce qui suit.

1
2
3
4
match xs (
    (Nil 42)
    ((Cons x _) (+ x 79))
)

(22) Tous les motifs d’un filtrage doivent avoir le même type en entrée et en sortie. En outre, les motifs fournis doivent couvrir l’ensemble des valeurs possibles pour le type d’entrée. Si besoin, le motif générique _ peut être utilisé pour obtenir cette complétude.

(23) Les motifs sont comparés à la valeur de référence successivement, dans l’ordre où ils apparaissent dans le code.

Identifiants

(24) Associer un identifiant à un objet se fait au moyen d’un expression, composée du mot-clé define, de l’identifiant à associer, et de l’objet.

Voici un exemple de lambda nommée.

1
2
3
(define neq (lambda (xs ys) (
    not (eq xs ys)
)))

Il est ainsi possible de créer des synonymes de types, comme suit.

1
(define String (List Char))

(25) Un identifiant peut être composé de n’importe quelle suite de caractères Unicode, aux exceptions suivantes près.

  • L’identifiant ne doit pas contenir de caractère d’espacement (sauf dans le cas des identifiants de types de tuples ou de lambdas, qui sont entourés de parenthèses).
  • L’identifiant ne doit pas contenir les caractères (, ), [, ], {, ::, , ->, .
  • L’identifiant ne doit pas commencer et finir à la fois par les caractères ', ".
  • L’identifiant ne doit pas être composé exclusivement de chiffres, ou de chiffres et d’un unique point.
  • L’identifiant ne doit pas être un des mots-clés du langage, à savoir abort, define, getchar hastype, lambda, match, putchar, req, seq, tuple, type, _, ni un des types prédéfinis.
  • Les implémentations peuvent définir des mots-clés ou symboles de syntaxe supplémentaires, pour les besoins de sucre syntaxique, qui ne devront alors pas être utilisés comme identifiants.

(26) Un identifiant de lambda ou de type est visible dans l’intégralité du code source. Un identifiant de variable est visible dans l’expression où la variable a été définie et ses sous-expressions. Si un identifiant de variable existe déjà comme identifiant de lambda, il remplace celui-ci dans tout son champ de visibilité.

(27) Un module est constitué de l’ensemble des expressions contenues dans un fichier source donné. Il porte pour identifiant le nom du fichier, ou le nom du dossier qui le contient si le fichier s’appelle mod.acid.

(28) Un type ou une lambda nommée définis dans un module peuvent être utilisés dans un autre module, à condition de faire précéder son identifiant des identifiants de toute la hiérarchie de modules permettant d’y accéder, chacun séparé par le caractère :: ou .

Par exemple, si on a un fichier Data/Bool.acid contenant le code suivant…

1
2
3
4
5
6
7
8
(hastype (Bool -> Bool) not)

(define not (lambda bool (
    match bool (
        (True False)
        (False True)
    )
)))

… dans un autre code source, on peut appeler cette lambda sous la forme Data::Bool::not. Il est ainsi possible de définir des lambdas et types ayant le même identifiant, mais dans des modules séparés, car chacun aura un identifiant qualifié différent.

(29) Si un type est défini dans un module portant le même identifiant, l’identifiant du module est omis dans l’identifiant qualifié du type et de ses constructeurs. Par exemple, si le type de liste précédemment défini se trouve dans un fichier List.acid, l’identifiant qualifié de son premier constructeur est List::Nil et non List::List::Nil. De même, dans ce module et dans lui seul, les constructeurs peuvent être utilisés seuls.

(30) <sucre syntaxique> Il est possible d’intégrer des identifiants d’autres modules à l’espace de nom d’un module donné, au moyen d’une expression composée du mot-clé use, et de l’identifiant qualifié du type, de la lambda ou du constructeur. Par exemple, (use Data::Bool::True) dans un fichier Main.acid permet d’utiliser directement True dans le code source. En revanche, cela ne permet pas d’utiliser Main::True dans un troisième fichier source.

(31) <sucre syntaxique> Il est en outre possible de faire ceci :

  • intégrer d’un coup une liste d’identifiants d’un même module, en les entourant d’accolades et en les séparant par des virgules, par exemple (use Data::List::{Nil, Cons}) ;
  • intégrer tous les identifiants d’un module en n’écrivant rien après ::, par exemple, (use Data::List::).

(32) Les identifiants du module Prelude de la bibliothèque standard et de tous ses sous-modules sont intégrés par défaut.

(33) Un module peut être lié statiquement ou dynamiquement au code source principal. Dans ce dernier cas, le code source du module peut ne contenir que les définitions de type, et les contraintes de types des lambdas nommées. On parle alors de fichier d’en-tête.

(34) Les identifiants de module, de type et de constructeur doivent commencer par une majuscule. Les autres identifiants doivent commencer par une minuscule.

Entrées-sorties

(35) La lambda putchar prend en entrée un caractère, affiche ce caractère, et renvoie un IO (tuple). La lambda getchar lit un caractère fourni par l’utilisateur, et le renvoie sous la forme d’un IO Char.

(36) La lambda seq prend en entrée deux actions d’entrée-sortie, exécute la première, puis la seconde, et renvoie le résultat de la seconde.

(37) La lambda req prend en entrée une valeur de type IO a, et renvoie son contenu.

(38) La lambda abort est particulière : aux yeux du vérificateur de types, elle renvoie n’importe quel type en sortie, en fonction de l’endroit où elle est appelée. Elle prend une chaîne de caractères en entrée, et met fin au programme en affichant cette liste de caractères.

(39) Tout programme destiné à être exécuté (donc pas une bibliothèque), contient une lambda nommée de type IO (tuple), par laquelle l’exécution du programme commencera. Sauf indication contraire fournie au compilateur ou à l’interpréteur, cette lambda est appelée main.

Évolutions futures

Le système d’entrées-sorties est assez mauvais, et pourra être radicalement transformé dans une prochaine version du langage. D’autres éléments pourront être ajoutés, comme un système de FFI, un système de macros, ou d’autres éléments de sucre syntaxique.

Exemple de programme complet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Prelude.acid

(hastype (Bool -> a -> a -> a) if)

(define if (lambda (cond if_true if_false) (
    match cond (
        (True if_true)
        (False if_false)
    )
)))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Bool.acid

(define Bool (type (False True)))

(hastype (Bool -> Bool -> Bool) and)

(define and (lambda (b1 b2) (
    match b1 (
        (True b2)
        (False False)
    )
)))

(hastype (Bool -> Bool) not)

(define not (lambda bool (
    match bool (
        (True False)
        (False True)
    )
)))
 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// List.acid

(use Bool::)

(define List (type a (
    Nil
    (Cons a (List a))
)))

(hastype (List a -> List a -> Bool) eq)

(define eq (lambda (xs ys) (
    match xs (
        (Nil (match ys ((Nil True) ((Cons _ _) False)))
        ((Cons x xnext) (
            match ys (
                (Nil False)
                ((Cons y ynext) (and (Type::a::eq x y) (eq xnext ynext)))
            )
        ))
    )
)))

(hastype (List a -> List a -> Bool) neq)

(define neq (lambda (xs ys) (
    not (eq xs ys)
)))

(hastype (List a -> Word64 -> a) !!)

(define !! (lambda (xs n) (
    match xs (
        (Nil (abort "Out of bounds."))
        ((Cons x next) (if (Word64::eq 0 n)
            (x)
            (!! next (- n 1))
        ))
    )
)))
 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// main.acid

(use Bool::and)
(use List::{Nil, Cons, !!})

(hastype (List Char -> IO (tuple)) print)

(define print (lambda str (
    match str (
        (Nil (putchar '\n'))
        ((Cons char next) (seq (putchar char) (print next)))
    )
)))

(hastype (List Char) numbers)

(define numbers "0123456789")

(hastype (Word64 -> List Char) show)

(define show (lambda n str (
    if (and (Word64::eq 0 n) (List::neq Nil str))
        str
        if (and (Word64::eq 0 n) (List::eq Nil str)
            (Cons '0' Nil)
            (show (div n 10) (Cons (!! numbers (mod n 10)) str))
)))

(hastype (Word64 -> Word64) factorial)

(define factorial (lambda n (
    if (<= n 1)
        1
        (* n (factorial (- n 1)))
)))

(hastype (IO (tuple)) main)

(define main (lambda () (
    print (show (factorial (hastype Word64 42)))
)))
+11 -1

Je vois pas l'intérêt du symbole -> dans le typage des fonctions.

Tel quel ça sent le rajout provenant des langages ML… pourquoi pas, mais à ce moment là autant adopter une forme qui soit pure au sens Lisp : (Bool Bool) au lieu de (Bool -> Bool). La flèche n'apporte aucune indication si elle doit systématiquement séparer deux éléments.

De même, pourquoi se prendre la tête avec la taille de la représentation des entiers en mémoire et les types Int* au lieu d'avoir simplement un type Int qui représente un entier ? Vu le niveau d'abstraction du langage je ne trouve pas ça logique.

Enfin, pourquoi transposer dans un nouveau langage la casserole la plus inélégante du Haskell qu'est la pseudo-monade IO ?

Pour moi cette spec est beaucoup trop proche de Haskell pour être un langage amusant à implémenter.

+2 -1

@nohar :

Les types peuvent prendre des paramètres. Par exemple, tu peux avoir un type List Char. Du coup, avec ta syntaxe, une fonction qui prend disons un entier et une liste de caractères et renvoie un caractère aurait pour type (Int List Char Char). Évidemment, pour le compilo, ce n’est pas gênant, parce que List est forcément suivi d’un autre type qui lui appartient, mais pour un humain, ce n’est pas très lisible, surtout si on commence à utiliser des types moins familiers.

Bon, on pourrait utiliser des parenthèses pour isoler les types paramétrés, comme on le fait pour les fonctions passées en argument / retournées (ici, on aurait (Int (List Char) Char)). Mais je persiste à penser que ce serait lourd à lire. Éventuellement, on pourrait passer l’écriture avec des flèches (ou tout autre symbole clair, si tu as peur que ça fasse trop Haskell) comme sucre syntaxique, je pense que c’est le mieux.

Les types entiers de taille connue (surtout les non signés) sont utiles dans un certain nombre d’applications, notamment en cryptographie.

On est très, très loin d’une monade, même si j’ai repris le nom IO par commodité. Après, si tu as un meilleur système qui s’intègre bien dans la logique du langage pour gérer la succession d’actions IO, je suis preneur. :-)

+0 -0

Le sujet est vachement cool ! :)

Les spécifications sont bien expliqués, merci Dominus Carnufex. Par contre faudra penser à écrire une BNF pour que tout le monde puisse programmer l'interpréteur et étendre le langage (d'un point de vue pratique ^^).

+1 -0

En fait ce qui me dérange n'est pas tellement la proximité avec Haskell (ou OCaml) mais plutôt que les ajouts syntaxiques cassent complètement l'intérêt d'une syntaxe Lisp.

Typiquement les listes de Lisp n'ont pas besoin de séparateurs mais de délimiteurs, et un parseur Lisp n'a pas besoin de prendre en compte la succession de deux formes pour les associer : il suffit de mettre des parenthèses pour ça. C'est ce qui le rend pur syntaxiquement parlant, et qui fait qu'on peut parser le langage sans passer par une grammaire.

+3 -0

@AlphaZeta : C'est en effet encore une autre qualité du Lisp perdue au profit d'une sémantique plus fonctionnelle (la composition).

Il ne suffit plus que de virer les parenthèses pour retomber sur un banal langage ML.

+1 -0

Un avis purement personnel sur le langage :

  • J'ai l'impression que le langage ce cherche dans sa définition, il est entre le lisp et le haskell, entre du haut niveau et du plus bas niveau.
  • Pour les entiers, autant ça peut être pratique d'avoir des Int*, autant le type Int lui pourrait être un entier arbitrairement grand. Avec quelques fonctions de cast on aurait le meilleur des deux mondes : Un type entier aussi grand que necessaire pour la majorité des dev qui n'ont pas envie de ce soucier de la taille et des entiers de tailles fixes lorsque nécessaire.
  • On est manifestement pas dans un langage fonctionnel pur, pourquoi créer une pseudo nomade pour les IO ? Je ne vois pas l'intérêt de faire renvoyer un IO Char à getchar, elle pourrait très bien renvoyer directement un char.
  • Pourquoi mettre des virgules pour séparer les littéral de list chaîné ? [1 2 3] pourrait très bien fonctionner, non ?
  • Pour définir la valeur d'un tuple, pourquoi imposer le mot tuple ? Est ce qu'une suite d'expression ne peut pas représenter un tuple directement ? En gros pourquoi devoir écrire (tuple 1 2 3) plutôt que (1 2 3) ?
  • Enfin, as ton besoin d'un typage fort et obligatoire ? Le lisp se prête bien au typage dynamique, quitte a faire un typage optionnel.

Ce qui me gène c'est qu'on a l'impression d'être dans un pseudo-haskell avec des parenthèse. Le gros intérêt d'un lisp normalement est que fondamentalement on écrit normalement directement des tuples qui représentent l'ast du langage : code et données se mélangent. Les expressions du langages sont manipulable directement par le langage. C'est ce qui fait la force du lisp : un système de macro ultra développé. Là j'ai plus l'impression qu'on tend vers un haskell-like? Alors certes la notation prefixé facile le parsing mais on perd tous les avantages du lisp ce qui est vraiment dommage.


edit: Pour résumer je pense qu'il pourrait être bien de clarifier la situation :

  • Soit vous voulez faire un lisp-like car facile a parser et implémenter et donc viser à garder ces caractéristiques : typage dynamique, notation préfixé, traitement des listes dynamique
  • Soit vous voulez faire un haskell-like car plus interessant (?) et donc autant utiliser une notation plus classique.
+0 -0

Par contre faudra penser à écrire une BNF pour que tout le monde puisse programmer l'interpréteur et étendre le langage (d'un point de vue pratique ^^).

Yarflam

Alors pour moi, BNF, ça veut dire Bibliothèque Nationale de France, donc je veux bien une traduction. :-P

  • Pour les entiers, autant ça peut être pratique d'avoir des Int*, autant le type Int lui pourrait être un entier arbitrairement grand. Avec quelques fonctions de cast on aurait le meilleur des deux mondes : Un type entier aussi grand que necessaire pour la majorité des dev qui n'ont pas envie de ce soucier de la taille et des entiers de tailles fixes lorsque nécessaire.

Cette question aurait dû être présente dans les explications données à côté de la spec. Je me suis demandé en écrivant s’il serait possible de définir le type entier (ou flottant, d’ailleurs) arbitrairement grand dans la bibliothèque standard, à partir des types plus basiques, ou si ce serait vraiment trop inefficace.

Du coup, je voulais attendre de voir à l’usage avant de l’intégrer dans les types natifs. Mais on peut le mettre, ça ne me dérange pas.

  • On est manifestement pas dans un langage fonctionnel pur, pourquoi créer une pseudo nomade pour les IO ? Je ne vois pas l'intérêt de faire renvoyer un IO Char à getchar, elle pourrait très bien renvoyer directement un char.

Comme j’ai dit : proposez-moi un système qui marche mieux, et je l’adopte. ;)

  • Pourquoi mettre des virgules pour séparer les littéral de list chaîné ? [1 2 3] pourrait très bien fonctionner, non ?

Oui. Faisons ça, du coup.

  • Pour définir la valeur d'un tuple, pourquoi imposer le mot tuple ? Est ce qu'une suite d'expression ne peut pas représenter un tuple directement ? En gros pourquoi devoir écrire (tuple 1 2 3) plutôt que (1 2 3) ?

Dans ton système, comment distinguerais-tu un appel de fonction d’un tuple contenant la valeur renvoyée par ta fonction ? Pire encore, si on adopte la notation des types de lambdas sans flèche suggérée par nohar, comment distingues-tu (Char Char Char) qui est le type d’une lambda de (Char Char Char) qui est le type d’un tuple ?

Accessoirement, je préférais que les parenthèses soient strictement cantonnées à leur rôle de délimiteur de nœuds de l’AST, plutôt qu’elles aient aussi un rôle significatif.

  • Enfin, as ton besoin d'un typage fort et obligatoire ? Le lisp se prête bien au typage dynamique, quitte a faire un typage optionnel.

Oui, c’est la principale caractéristique de Acid par rapport à un Lisp. Sinon, on ne fait rien de plus qu’un buildmyownlisp.

Ce qui me gène c'est qu'on a l'impression d'être dans un pseudo-haskell avec des parenthèse. Le gros intérêt d'un lisp normalement est que fondamentalement on écrit normalement directement des tuples qui représentent l'ast du langage : code et données se mélangent. Les expressions du langages sont manipulable directement par le langage. C'est ce qui fait la force du lisp : un système de macro ultra développé. Là j'ai plus l'impression qu'on tend vers un haskell-like? Alors certes la notation prefixé facile le parsing mais on perd tous les avantages du lisp ce qui est vraiment dommage.

Visiblement, la confusion vient du fait que the_new_sky parle de Lisp-like dans son titre. Alors je le rappelle, la présente spec est l’extension de ma proposition de départ ici.

La base de réflexion est le Core, le sous-langage du Haskell (d’où l’influence évidente de celui-ci), qui une fois nettoyé de quelques aspects non essentiels représente ce qu’on peut faire de plus minimaliste comme langage fonctionnel pur fortement typé. À partir de cette base, j’y ai ajouté des éléments venus de Rust, lorsque cela me paraissait plus agréable / pratique que l’équivalent haskellien, et un système d’IO moche en attendant d’avoir mieux, puisqu’on a décidé de ne pas avoir de FFI dans un premier temps.

Et j’ai orienté la syntaxe vers celle de Lisp, parce qu’elle est facile à parser, en m’en éloignant sans doute un peu trop (cf. les types de lambdas). Il n’a jamais été question, pour moi en tout cas, de suivre fidèlement la philosophie de Lisp, en particulier le typage dynamique, et les fonctions qui s’appliquent magiquement sur des listes alors qu’elles sont prévues pour bosser sur des valeurs.

Maintenant, si ce n’est pas ce que vous voulez, libre à vous de rédiger une autre spec qui répond mieux à vos attentes. La mienne est et reste une simple proposition. :)

+0 -0

Pour définir la valeur d'un tuple, pourquoi imposer le mot tuple ? Est ce qu'une suite d'expression ne peut pas représenter un tuple directement ? En gros pourquoi devoir écrire (tuple 1 2 3) plutôt que (1 2 3) ?

Kje

Prenons un exemple: (a b c). Là par contre c'est pas vraiment possible. Au parsing, on peut pas directement vérifier si la variable désigne une fonction ou une valeur "normale". Et quand bien même on écrirait une seconde étape après le parsing pour voir s'il s'agit d'une valeur ou d'une fonction, ça impliquerait qu'on ne pourrais pas faire de tuple de fonctions. Du coup je propose quelque chose comme (1, 2, 3) comme ça il n'y a aucune ambiguïté.

Par souci de cohérence, si on choisit la syntaxe (1, 2, 3), j'aurai tendance à préférer la syntaxe [1, 2, 3] pour les listes, mais avec les listes on a le choix donc faut voir.

Par contre je suis d'accord avec nohar, la notation avec des flèches est pas super cohérent avec la grammaire de LISP. On pourrait imaginer quelque chose comme (-> A B C T) mais encore une fois ça demande des fonctions variadiques. Je propose d'implémenter les fonctions variadiques dans mon interpréteur Haskell pour voir si ça va bien avec le reste.

+0 -0
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