Acid, le lisp-like de la communauté !

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

a marqué ce sujet comme résolu.

Oui. Mais le truc, c'est que l'AST Python, tu ne peux le générer qu'en python. Du coup, je vais créer un script Python qui convertit une représentation (en json) en AST. Comme ça, les parsers dans les différents langages génèrent cette représentation, et tout est interprété au même endroit.

+2 -0

Personnellement pour le JSON j'opterai pour un truc plus verbeux mais assez simple finalement à comprendre. Par exemple, pour le code suivant:

1
2
3
4
5
(def add (lambda (x y)
    (+ x y)
))

(print (add 1 3))

On obtiendrait quelque chose comme ça:

 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
{
    "path": "/home/****/Desktop/code/python/PyAcid/examples/test.acid",
    "instructions": [
        {
            "type": "declaration",
            "name": "add",
            "arguments": [
                {
                    "type": "lambda",
                    "parameters": ["x", "y"],
                    "body": {
                        "type": "call",
                        "name": "+",
                        "arguments": [
                            { "type": "variable",
                              "name": "x" },
                            { "type": "variable",
                              "name": "y" }
                        ]
                    }
                }
            ]   
        },
        {
            "type": "call",
            "name": "print",
            "arguments": [
                {
                    "type": "call",
                    "name": "add",
                    "arguments": [
                        { "type": "int_literal",
                          "value": 1 },
                        { "type": "int_literal",
                          "value": 3 }
                    ]
                }
            ]
        }
    ]
}

C'est vrai qu'on peut déduire toutes ces informations depuis la représentation de Folaefolc mais l'avantage ici c'est que je peux répartir l'interprétation de l'objet JSON grâce à l'attribut type. C'est clairement plus lourd dans le fichier, mais ça sera moins lourd dans le code.

@mehdidou99: Je vote pour les crochets, c'est moins LISP mais ça permet de s'entrainer sur le parsing.

PS: Suite à une demande de the_new_sky sur GitHub, je me demande si je vais pas mettre des annotations sur mes fonctions. Devrais-je utilier le module typing ? Ça impliquerait un passage à Python 3.5 mais du coup ça me soule un peu. A vous de voir :)

+0 -0

J'ai fait un petit exemple de déclaration de fonction :

1
2
3
4
5
6
7
8
(hastype (List a -> a) somme)

(define somme (lambda liste (
    match liste (
        (Nil 0)
        ((Cons val suite) (+ val (somme suite)))
    )
)))

Le problème, c'est que s'il y a un vrai type qui s'appelle a, il peut y avoir ambiguïté. Comment spécifier que a est un type générique ?

+0 -0

@the_new_sky:

J'ai codé un truc vite fait, mais il me semble que c'est en accord avec les specs données par Dominus Carnufex:

1
2
3
(define carrédelhypothénuse (lambda (x y) (
    (+ (* x x) (* y y))
)))

@mehdidou99:

Le problème, c'est que s'il y a un vrai type qui s'appelle a, il peut y avoir ambiguïté. Comment spécifier que a est un type générique ?

En Haskell, les types doivent avoir un nom qui commencent par une lettre majuscule, comme ça il n'y a pas d’ambiguïté.

Tu as mis quel langage pour avoir de la coloration syntaxique ?

Ouais y'a pas JSON, du coup j'ai mis JavaScript (js)

Il y a juste le define qui est devenu def mais de toute façon c'est actuellement parsé comme un simple appel et non comme un mot clé spécial.

Bon, d'abord il faut savoir que les AST, se situent dans le module standard ast

1
>>> import ast

Commençons par isoler tous les noeuds possibles des AST. C'est-à-dire toutes les classes du module ast qui héritent de ast.AST :

1
2
3
4
>>> AST_CLASSES = {
...    name: cls for name, cls in ast.__dict__.items()
...    if isinstance(cls, type) and issubclass(cls, ast.AST)
... }

Si vous voulez une rapide aide sur ces classes : for key, cls in AST_CLASSES: print(key, ":\n", cls.__doc__, "\n").

Maintenant comment pouvez-vous faire pour savoir quelle tête ils doivent avoir ?

Sachez que :

  • ast.parse(CODE) produit un ast à partir d'un code Python contenu dans une chaîne de caractères,
  • ast.dump(AST) "dumpe" la représentation d'un ast,
1
2
>>> ast.dump(ast.parse("print('Hello, world!')"))
"Module(body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Str(s='Hello, world!')], keywords=[]))])"

C'est assez touffu pour un hello world, je suis d'accord, mais ceci est l'AST d'un programme complet. Représentons-le sous la forme de dictionnaires et de listes (un json, quoi)…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> my_ast = {
...     "type": "Module",
...     "body": [
...         {
...             "type": "Expr",
...             "value": {
...                 "type": "Call",
...                 "func": {
...                     "type": "Name",
...                     "id": "print",
...                     "ctx": {"type": "Load"}
...                 },
...                 "args": [
...                     {
...                         "type": "Str",
...                         "s": "Hello, World!"
...                     },
...                 ],
...                 "keywords": []
...             },
...         }
...     ]
... }

Il ne nous reste plus qu'à interpréter ce dictionnaire pour reconstituer l'AST Python. Ça peut se faire avec un peu de magie noire. ;)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> from functools import singledispatch
>>> @singledispatch
... def deserialize(struct):
...     return struct
... 
>>> @deserialize.register(dict)
... def _(struct):
...     clsname = struct.pop("type", None)
...     if not clsname:   # Ceci n'est pas un noeud AST
...         return struct
...     kwargs = {key: deserialize(val) for key, val in struct.items()}
...     cls = AST_CLASSES[clsname]
...     if issubclass(cls, ast.stmt) or issubclass(cls, ast.expr):
...         kwargs.setdefault("lineno", 1)
...         kwargs.setdefault("col_offset", 0)
...     return cls(**kwargs)
... 
>>> @deserialize.register(list)
... def _(struct):
...     return [deserialize(elt) for elt in struct]
... 
>>> tmp = deserialize(my_ast)
>>> ast.dump(tmp)
"Module(body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Str(s='Hello, World!')], keywords=[]))])"

Le hack à propos de "lineno" et "col_offset" est là parce qu'en principe, toutes les expressions et les instructions (expr, stmt) en Python doivent être associé(e)s à un numéro de ligne et une position dans la ligne. Ici, il n'y en a pas puisqu'on produit un AST "à poil", donc je mets les valeurs par défaut : ligne 1, colonne 0.

Maintenant, il vous suffit de charger ce dictionnaire à partir d'un fichier json (avec le module standard json) ou d'un yaml (avec le package PyYaml) pour charger un AST depuis n'importe quel fichier.

Mais comment l'exécuter ?

La builtin compile produit un code object à partir d'un AST :

1
2
3
>>> code = compile(tmp, 'hello world', 'exec')
>>> code
<code object <module> at 0x7fda655659c0, file "hello world", line 1>

Ce code dispose de quelques attributs. Vous pouvez par exemple en extraire le bytecode, ou bien l'exécuter directement :

1
2
>>> exec(code)
Hello, World!

Voilà. Ça devrait faire un bon début. :)

+7 -0

Edit: Par contre je m'interroge toujours sur la pertinence du fait d'écrire l'AST en JSON dans un fichier. Certes ça permet d'écrire le parser en d'autres langages, mais je dirai pas que le parsing soit la partie la plus intéressante de l'écriture d'un compilateur. De plus, si on traduit notre AST Acid en AST Python, on a pas vraiment écrit un interpréteur, ni un compilateur1. On a juste donné le travail à faire au module ast. C'est efficace, mais finalement on aura pas vraiment fait grand chose :(


  1. en fait si, mais si le but est d'apprendre, autant favoriser l'écriture maison pluôt que l'efficacité. 

+2 -0

Bonus track

La transformation inverse peut aussi être utile aux gars qui vont coder leur parseur en Python : il est possible de créer une classe qui "mock" les classes du module ast, de telle façon que lorsque vous en appelez les "constructeurs", ça vous produise un dictionnaire avec une entrée "type": "nom_de_la_classe" et toutes les autres entrées égales aux kwargs passés au constructeur.

De cette façon, votre parseur appellera toujours les mêmes fonctions, mais en remplaçant import ast par votre mock, vous pouvez dumper un json au lieu de produire directement un AST Python.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> class ASTMock:
...     def __getattr__(self, funcname):
...         def func(**kwargs):
...             kwargs["type"] = funcname
...             return kwargs
...         return func
...     
... 
>>> ast = ASTMock()  # au lieu de "import ast", ou après
>>> ast.Module(body=[ast.Expr(value=ast.Call(func=ast.Name(id='print', ctx=ast.Load())))])
{'body': [{'type': 'Expr', 'value': {'func': {'ctx': {'type': 'Load'}, 'id': 'print', 'type': 'Name'}, 'type': 'Call'}}], 'type': 'Module'}

PS: @AlphaZeta : le but est de vous concentrer au départ sur le parseur. Donc sur la définition du langage. Dans un second temps vous pouvez tout à fait changer pour utiliser autre chose que les AST Python, et écrire votre propre interpréteur/compilateur ou que sais-je. Juste, démarrer comme ça vous permet de jouer avec votre langage en même temps que vous en définissez la syntaxe et la sémantique sans tout changer à chaque fois.

+3 -0

Au passage, si vous ne voulez pas vous dépendre trop du module ast de Python, vous pouvez aussi définir dès à présent votre propre AST puis faire un visitor qui le transforme en un AST Python.

Le truc, c'est que votre AST (et donc ce visitor) risquent de beaucoup bouger au départ, et ça peut rendre le travail vraiment très fastidieux pour la moindre modif, donc je vous recommande plutôt de passer à cette étape (créer votre propre structure d'AST) en phase 2, quand le langage sera à peu près stable.

… À moins que vous ne galériez vraiment trop à utiliser les AST de Python (mais ça serait vraiment étonnant vu la nature du langage).

+0 -0

Pour une syntaxe LISP-like je doute que l'AST puisse devenir très compliqué de toute façon. Pour l'instant j'ai que Call (([name] [args...])), Lambda ((lambda ([args...]) [body])), Define ((define [name] [value])), Variable ([name]) et des literals d'entier et de nombres à virgule.

Tout est défini ici.

Ouais t'as raison, il reste pas mal de trucs à faire en fait :euh:

Le truc, c'est que votre AST (et donc ce visitor) risquent de beaucoup bouger au départ, et ça peut rendre le travail vraiment très fastidieux pour la moindre modif

C'est ça que j'ai pas compris en fait, pourquoi ça peut rendre le travail fastidieux pour la moindre modif ?

Honnêtement, utiliser l'AST de Python me paraît vraiment plus simple.

Je ne sais pas trop si je dois merger la Pull Request d'Alpha Zeta, elle me paraît incomplète.

Nous avançons bien mais je pense que nous ne devons pas nous précipiter…

Voici ce qu'est Acid aujourd'hui :

  • Un langage dont la spécification est en V1 et repose sur la syntaxe de Dominus et l'idée d'AST Python de Nohar. Cette spécification est indépendante du langage d'implémentation. La spécification me parait encore bencale, toutes propositions est accueilli ici.

  • À ne pas lier à l'AST Python, un projet d'implémentation en Python.

PS : Sa casse peut-être le défi mais utiliser ply nous ferait gagner du temps.

+0 -0

Pour l'instant, comme l'a dit nohar, il vaut mieux utiliser l'AST. Et une fois qu'on aura une base stable, et qu'on voudra pousser un peu plus loin, il sera peut-être intéressant de créer un visitor qui crée l'AST.

Ainsi, on a déjà une vue sur une évolution possible ; anticipons-là :

  • au niveau du convertisseur json–>AST, il n'y a rien à faire pour l'instant, puisqu'il s'agira bonnemenet et simplement de refaire cette partie si on a besoin du visitor.
  • au niveau des différentes implémentations, il faut veiller à bien séparer parsing et génération de la représentation, pour pouvoir changer facilement de représentation en cas d'adoption du visitor. mais bon, je ne sais pas si c'est vraiment nécessaire de dire ça, étant donné que le SRP, principe fondamental de l'OO facilement généralisable à la programmation dans son ensemble, nous dit de toute façon de le faire.
+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