Sommaire
Bonjour Nal,
Il y a quelques temps, je t'ai présenté Letlang, un projet de langage de programmation fonctionnelle.
Il a pour but d'être compilé vers du code natif, avec un système de type strict et expressif basé sur une logique d'ordre supérieur.
Malheureusement, avec le boulot (mission freelance + Kubirds), j'ai pas beaucoup de temps à y consacrer, à part quelques heures par-ci par-là.
L'un des objectifs initiaux de ce langage était d'en faire un langage compilé. Comment ? En produisant du LLVM IR. Je me suis gratté la tête pas mal de temps sur comment représenter les structures de haut niveau que je veux introduit dans un langage bas niveau comme LLVM IR.
Et puis… un déclic. Vous connaissez un langage un peu plus haut niveau qui compile via LLVM ? Oui, c'est ça, le Rust.
Le compilateur Letlang est écrit en Rust, alors pourquoi pas simplement produire du code Rust et ensuite appeler rustc
?
Voici un exemple :
use std::{
process::{Command, Stdio},
io::Writer,
error::Error,
};
fn main() -> Result<(), Box<dyn Error>> {
let source_code = b"fn main() { println!(\"hello world\"); }";
let cmd = Commamd::new("rustc").args(["-o", "test", "-"]);
let mut proc = cmd.stdin(Stdio::piped()).spawn()?;
if let Some(mut stream) = proc.stdin.as_mut() {
if let Ok(_size) = stream.write(source_code) {
proc.wait()?;
}
else {
proc.kill()?;
return Err(/* some error */);
}
}
else {
proc.kill()?;
return Err(/* some error */)
}
Ok(())
}
Un petit cargo run
et hop, on a un binaire test
qui affiche hello world
.
À partir de là, le compilateur devient assez simple :
- on a l'AST décrit avec le système de type de Rust (sous forme d'enum et de struct)
- on utilise le design pattern visitor pour parcourir cet AST et produire le code source Rust
- enfin, on envoi le code source produit à
rustc
DISCLAIMER: Je fais du Rust seulement depuis quelques mois. Je ne prétend pas être un expert dans le langage, et il est tout à fait possible que le code que je produis ne soit pas le plus efficace / optimisé en terme d'usage de la mémoire / … Je suis bien sûr ouvert à vos critiques les plus viscérales pour me permettre de progresser :)
Créer l'AST (Abstract Syntax Tree)
C'est la partie la plus simple, on définit grâce au système de type Rust la structure du langage. Lorsque l'on implémentera le parseur, on va extraire les informations dans cet arbre, que l'on passera tel quel au compilateur.
On peut donc déjà commencer à travailler sur le compilateur avant même d'avoir le parseur.
Petit exemple :
#[derive(Clone, Debug, PartialEq)]
enum Expression {
Number(f64),
BinaryOperation { lhs: Box<Expression>, op: String, rhs: Box<Expression> },
UnaryOperation { op: String, expr: Box<Expression> },
}
impl Enumeration {
pub fn number(val: 64) -> Box<Self> {
Box::new(Self::Number(val))
}
pub fn add(a: Box<Expression>, b: Box<Expression>) -> Box<Self> {
Box::new(Self::BinaryOperation {
lhs: a,
op: "+".to_string(),
rhs: b,
})
}
pub fn factorial(n: Box<Expression>) -> Box<Self> {
Box::new(Self::UnaryOperation {
op: "!".to_string(),
expr: n,
})
}
}
L'usage de Box<T>
est requis car il s'agit d'un type récursif. L'implémentation qui suit fournit juste quelques helpers pour créer l'AST :
let ast = Expression::factorial(
Expression::add(
Expression::number(2),
Expression::number(3),
)
);
Produire le code Rust
Afin de me simplifier la vie lors de la production du code Rust, j'ai décidé d'utiliser la biliothèque tinytemplate.
Le context de compilation
La première étape est de créer une structure Context
que l'on va utiliser lors du parcours de l'AST :
use tinytemplate::TinyTemplate;
struct Context<'a> {
pub tt: TinyTemplate<'a>,
// other contextual data, like symbol lookup tables etc...
}
Et l'implémentation qui va bien :
use tinytemplate::format_unescaped;
static SOME_NODE_CODE: &'static str = include_str!("some_node_code.rs.tt");
impl Context<'_> {
pub fn new() -> Self {
let mut tt = TinyTemplate::new();
tt.set_default_formatter(&format_unescaped); // on veut pas de " etc...
tt.add_template("some_node", SOME_NODE_CODE).expect("should be a valid template");
Self { tt, /* ... */ }
}
}
Préparer la gestion d'erreur
Lors du parcours de l'AST, il est possible de rencontrer des erreurs. On va se créer un type spécial pour pouvoir les retourner :
use std::fmt;
type Result<T> = std::result::Result<T, CompilationError>;
#[derive(Debug)]
struct CompilationError {
message: String,
}
impl CompilationError {
pub fn new(message: String) -> Self {
Self { message }
}
}
impl fmt::Display for CompilationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "CompilationError: {}", self.message)
}
}
impl std::error::Error for CompilationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
Le design pattern Visitor
Un dernier prérequis est le trait qu'il va falloir implémenter pour chaque partie de l'AST :
trait Visitor {
type Node;
fn visit(node: Box, context: &mut context) -> Result;
}
```
Prendre des p'tits bouts d'trucs, et les assembler ensemble
Enfin! On peut commencer à travailler :
use serde_json::json;
impl Visitor for Expression {
type Node = Expression;
fn visit(node: Box<Expression>, context: &mut context) -> Result<String> {
match *node {
Expression::Number(val) => Ok(format!("{}", val),
Expression::UnaryOperation { op, expr } => {
let inner_code = Expression::visit(expr, context)?;
let data = json!({
"operator": op,
"inner_code": inner_code,
})
let res = context.tt.render("some_node", &data);
if let Ok(code) = res {
Ok(code)
}
else {
Err(CompilationError::new("oops".to_string()))
}
},
Expression::BinaryOperation { lhs, op, rhs } => {
let lhs_code = Expression::visit(lhs, context)?;
let rhs_code = Expression::visit(rhs, context)?;
// [...] similaire a UnaryOperation
}
}
}
}
Si je reprend mon AST initial, je peux maintenant le traduire en Rust :
let ast = Expression::factorial(
Expression::add(
Expression::number(2),
Expression::number(3),
)
);
let mut context = Context::new();
let source_code = Expression::visit(ast, &context)?;
// call rustc with `source_code.as_bytes()`
Conclusion
Cette solution facilite grandement l'implémentation du compilateur. En effet, l'implémentation du runtime sera faite dans un langage qui me donne certaine garantie concernant la sécurité de la mémoire. Chose que je n'aurais certainement pas pu fournir avec LLVM IR directement.
De même, si je veux ajouter des dépendances au runtime (comme par exemple GMP pour les nombres à précision arbitraire), ou permettre d'inline du Rust en Letlang, ou d'utiliser l'ecosystème Rust en Letlang, tout de suite, cela rend le langage un peu plus utile :)
Cela veut également dire que pour compiler du code Letlang, on aura besoin de la toolchain Rust. C'est parfaitement acceptable je trouve.
Et toi Nal, qu'en penses tu ?
# j'en dis que
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 4.
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
[^] # Re: j'en dis que
Posté par David Delassus (site web personnel) . Évalué à 2. Dernière modification le 08 mars 2022 à 03:01.
Ah! Si un modo passe par la:
Au final, tout les langages ne sont ils pas un rhabillage du langage cible (javascript pour typescript, erlang pour elixir, …) ? (:
J'ai plus confiance dans ma capacité à implémenter ce que je souhaite en Rust qu'en LLVM IR ou Assembleur.
Et j'ai plus confiance dans le résultat que cela va produire. Il me semble que débugger du Rust sera plus simple que débugger du LLVM IR.
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: j'en dis que
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 5.
Pas tous les langages (je ne crois pas) …mais dans l'ensemble t'as pas tort ;p Si tu te retrouves à « débugger du Rust » ne crains-tu pas qu'au niveau des messages on se retrouve un peu loin des paradigmes du langage initial ? J'en sais trop rien. En tout cas chouette passe-temps. :)
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
[^] # Re: j'en dis que
Posté par David Delassus (site web personnel) . Évalué à 2.
Je prévois une compilation en plusieurs phases :
Mon hypothèse, c'est qu'une fois arrivée à l'etape 4, on a la garantie que le code Rust produit ne générera pas d'erreur de compilation.
Les éléments qui me permettent de soutenir cette hypothèse sont :
En fait, c'est comme se demander : que se passe-t-il si après avoir compilé le C en ASM, l'assembleur renvoi une erreur ? Le soucis est au niveau de GCC ici, pas au niveau du code que je lui fournit.
Maintenant, que mon hypothèse est posée, l'implémentation suivi par les tests la confirmera ou l'infirmera.
A ce moment là, je peux toujours identifier grâce au message d'erreur de
rustc
quel est le bout de code qui produit l'erreur, et grâce aux annotations de l'AST produites en phase 0 à 3, retrouver le code Letlang concerné.NB: Dans ce journal, j'explique comment j'ai commencé l'implémentation de la phase 4 et 5, en construisant à la main l'AST "valide".
J'ai fait il y a quelques temps une grammaire pest pour la phase 0 et 1. Mais comme je change d'avis sur la syntaxe comme je change de chemise, c'était contre productif.
Par contre, l'AST lui risque pas de bouger beaucoup, après tout, les concepts sont la, et les informations dont j'ai besoin pour les représenter aussi. Au final, commencer par la phase 4 et 5 c'est plus simple, et ça me fournit une suite de test pour implémenter les phases précédentes.
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
# lisp ?
Posté par Papey . Évalué à 4.
Je ne suis pas expert du sujet mais j'avais cru comprendre que les lisp permettaient de faire évoluer facilement le langage via les macros simples et via les readers macros pour les cas plus complexes. Est-ce une option que tu as considérée et si oui, pourquoi ne pas l'avoir choisie ?
[^] # Re: lisp ?
Posté par David Delassus (site web personnel) . Évalué à 2.
Le lisp c'est très bien. Paraîtrait même que c'est le papa de nombreuses fonctionnalités que tu retrouves dans quasiment tout les langages. Rust inclus.
Seulement voilà, je n'ai jamais écrit une ligne de Lisp. Donc l'idée ne m'a pas traversé l'esprit. Et maintenant que tu en parles, non je ne pense pas que cela serait mon choix. Rust apporte des garanties de sécurité de la mémoire qui sont trop intéressante.
Et puis le plus important je pense : je veux progresser en Rust.
Accessoirement:
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: lisp ?
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 6.
En fait les non-Lisper ont une coloration syntaxique pourrave et c'est ça le problème
What fan-Ruster sees:
What non-Ruster sees:
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
[^] # Re: lisp ?
Posté par reno . Évalué à 2.
Personellement j'ai toujours trouvé dommage que le lisp soit '(fonction arg1 arg2 …)' un simple changement 'fonction(arg1 arg2 …)' aurait été un bon compromis.
[^] # Re: lisp ?
Posté par reno . Évalué à 2.
Sinon avec son GC Lisp est memory-safe donc je ne vois pas trop où est l'avantage de Rust ici..
[^] # Re: lisp ?
Posté par David Delassus (site web personnel) . Évalué à 4.
Rust est memory safe sans GC.
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: lisp ?
Posté par chimrod (site web personnel) . Évalué à 6.
Au contraire, ça casse la cohérence du langage. Le bloc de parenthèses représente une unité d’exécution du langage.
(fonction arg1 arg2 …)
peut se substituer à(EXPRESSION)
. Il faut plus les lire comme les{…}
du C ou Java que les parenthèses qui listent les arguments d’une fonction.[^] # Re: lisp ?
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 4. Dernière modification le 08 mars 2022 à 21:51.
pour ta notation va ajouter de la confusion (pour moi) alors que (je trouve que) l'on délimite mieux les portées des uns et des autres avec
(f (g ) )
…surtout quand chacun peut avoir plusieurs arguments. Accessoirement, tu peux facilement convertir la notation préfixée en notation sufixée (c'est le même AST) là où ce que tu proposes ajoute un surcoût de parsing…“It is seldom that liberty of any kind is lost all at once.” ― David Hume
[^] # Re: lisp ?
Posté par David Delassus (site web personnel) . Évalué à 3.
Le meilleur exemple "non-lisp" (entre grosses guillemets) c'est les templates Go :
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: lisp ?
Posté par reno . Évalué à 3.
Pas sur de comprendre ou est la confusion dans f(x g(y z) w)?
y et z sont les arguments de g
x, le resultat de g(y z) et w sont les arguments de f.
Dites les critiques, vous êtes sûrs que vous ne réagissez pas de manière un peu 'instinctive'?
Si le Lisp fait comme ça c'est 'forcément' bien?
f(x y …) est + proche de la notation mathématique classique que (f x y …),
mais pour moi l'intérêt principal est que ça montre bien la différence entre le premier argument et les autres:
dans (+ 1 2) on note bien que + est traité différemment de 1 et 2..
Le Lisp classique a d'ailleurs '(x y) pour l'abréviation de (quote x y)..
Avec ma(*) notation 'préfix' pas besoin d'abréviation c'est quote(x y) ou '(x y) normalement, comme quoi c'est une notation encore plus homoiconique que le Lisp!
*: je ne suis pas le seul a avoir eu l'idée, il y en a au moins un autre mais je n'ai plus le lien..
[^] # Re: lisp ?
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 4. Dernière modification le 09 mars 2022 à 01:08.
D'une part ce n'était pas une critique, d'autre part j'ai bien précisé que c'était selon moi qui n'ai pas les habitudes « 'instinctive' »s que toi. Donc ce n'est pas que « Si le Lisp fait comme ça c'est 'forcément' bien » ni que d'autres n'aient pas eu ton idée. Attention cependant à ne pas parler de « notation mathématique classique » ; comme l'a mentionné chimrod dans un autre commentaire, il s'agit de S-expressions (où il n'y a pas d'obligation que le + soit traité différemment…)
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
[^] # Re: lisp ?
Posté par David Delassus (site web personnel) . Évalué à 3.
Lorsque l'on propose un changement de syntaxe à un langage, il faut regarder comment ça s'intègre avec le reste du langage.
Par exemple, l'opérateur de pipeline en Elixir:
Si je voudrais l'ajouter tel quel en C, ça donnerait :
Mais est-ce que ça a du sens en C ? En C un opérateur s'applique sur des valeurs, or
b()
n'est pas une valeur.Du coup, est-ce que
a |> b
est un pointeur de fonction ? Si oui, cela pointe vers quoi ? Est-ce que cela produit un nouveau symbole dans le.o
?Toutes ces questions me font répondre que : non, l'opérateur de pipeline n'a pas sa place dans le langage C.
La totalité du langage Lisp repose sur le principe suivant :
( expression )
, etf a b c
est une expression ouf
est une fonction. Ce qui veut dire qu'en Lisp, il n'y a pas d'opérateurs,+
est aussi une fonction.Dans mon exemple,
f
prend 3 arguments, maintenant pour le second exemple :(g (cons a b c))
. Icig
prend un seul argument, une liste, construite aveccons
.On a
(defvar a 42)
,(defun ...)
,(lambda ...)
et(defmacro ...)
. Tout se ressemble. C'est cohérent. Du coup introduiref(a b c)
, ça casse la cohérence.Après m'être documenté,
'x
est l'abbréviation de(quote x)
. Et donc'(x y)
devrait être(quote (x y))
. Il semble que les différents Lisp (Scheme et compagnie) gèrent ça différemment.cf: https://stackoverflow.com/a/20643658/1020897
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: lisp ?
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 3.
Comme pour l'exemple du pipe pour C, il y a eu des tentatives d'utilisation de M-expression (dont justement MLisp et CGOL au moins) mais ça n'a pas vraiment pris… (pourtant il y avait bien des langages comme ForTran et ALGOL qui étaient bien connus et utilisés dans la période.) Comme quoi, la cohérence d'ensemble compte aussi.
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
[^] # Re: lisp ?
Posté par Thomas Douillard . Évalué à 1.
On peut cependant constater que quasi aucun langage moderne ne reprend cette syntaxe, fusse-t-elle cohérente. Et ce bien que Lisp fasse partie de la culture générale de beaucoup d’informaticiens.
[^] # Re: lisp ?
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 5.
tout dépend…
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
[^] # Re: lisp ?
Posté par Thomas Douillard . Évalué à 2.
Et un des arguments de vente de Racket c’est qu’on peut changer la syntaxe : https://eighty-twenty.org/2019/07/21/indentation-syntax-for-racket
[^] # Re: lisp ?
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 4.
Chose qu'on fait aussi avec les autres Lisp ;-) (d'où la suggestion initiale du fil de discussion de parser el angage en Lisp : ça sait interpréter facilement n'importe quel autre langage sans trop d'effort quand on a l'habitude du langage)
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
[^] # Re: lisp ?
Posté par jmiven . Évalué à 3.
Ce n'est comme tu le relèves pas très important, mais le projet Racket date de 1995. Certes ça s'appelait initialement "PLT Scheme", mais le renommage ne s'est pas accompagné de changements majeurs, c'était toujours le même projet et le même langage.
[^] # Re: lisp ?
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 3.
Pas faux :-D
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
[^] # Re: lisp ?
Posté par reno . Évalué à 3.
Pour ton exemple sur |> je suis d'accord que ça n'a pas d'intérêt dans un langage ou les opérateurs s'applique sur des valeurs, mais pas pour la même raison que toi:
x |> f serait une façon tout à fait acceptable d'écrire f(x), mais ça ne marche plus si on veut faire x |> f(y) a la place de f(x,y)..
Et je ne parle pas d'ajouter f(x y) au Lisp classique mais de remplacer (a b) par a(b) partout.
Donc defvar(a 42) défun(…) +(1 2)..
On peut très bien traduire mécaniquement l'un en l'autre, sauf '(x y) bien sûr qui n'est pas une S-expression.. ;-)
# Générer du Rust
Posté par Gof (site web personnel) . Évalué à 4.
Tant qu'a générer du Rust, tu peux aussi utiliser des macro procédurale.
Donc l'utilisateur devra juste avoir un Cargo.toml et un main.rs avec
Et la macro se charge de produire le code en rust.
Pour les erreurs, je suggère codemap-diagnostic
Et compiler le résultat tranquilles dans ta chambre ?
Ceci est juste une idée à la con, mais si tu pourrait avoir un self-contained Cargo.toml (qui est à la fois valid toml et rust) comme fichier pour le build système:
[^] # Re: Générer du Rust
Posté par David Delassus (site web personnel) . Évalué à 4.
Idée intéressante, mais je trouve qu'utiliser des templates est moins capilo-tracté que manipuler le TokenStream de Rust. J'ai jamais réussi à faire ce que je veux avec les macros de Rust, je les trouve atroces.
Mais bon, ça c'est parce que mon langage préféré, Elixir, a juste la meilleure syntaxe du monde (totalement subjectif, totalement assumé) pour les macros :)
Excellent ! Je garde de côté, merci :)
J'ai pas compris l'exemple, ce code irait dans le main.rs du projet ? Et il est censé faire quoi ?
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: Générer du Rust
Posté par Gof (site web personnel) . Évalué à 5.
Tu peux utiliser des templates dans une macro, et simplement convertir la le résultat avec TokenStream::parse a la fin
L'aventage de la macro comparé a apeller rust toi même c'est que ça s'intègre avec cargo qui gère les flag compliqué pour toi comme la cross compilation, le compilation incrementale, et que sais-je.
L'idée est qu'il n'y a pas de main.rs. Uniquement un Cargo.toml qui dit a cargo d'ouvrir lui même (a cause du
path = "Cargo.toml"
) et de simplement apeller ta macro qui fait le reste.Mais pas sur que ce soit idéal en pratique.
[^] # Re: Générer du Rust
Posté par David Delassus (site web personnel) . Évalué à 3.
Ah effectivement, ça peut être intéressant.
A terme, j'aimerais bien que mon compilo soit self-hosted (écrire letlang en letlang). Du coup je vais finir par devoir les gérer moi même ces flags, non ?
Fun, je vais regarder ça de plus près.
Merci pour tes retours.
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
# Commentaire supprimé
Posté par Anonyme . Évalué à 3.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Re: Deuxième partie
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 3.
La totalité est/sera là.
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.