Journal [Letlang] Écrire un compilateur en Rust

15
7
mar.
2022

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 &quot; 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  (site web personnel, Mastodon) . Évalué à 4.

    • Ton compilo a mangé des lignes dans la partie « Le design pattern Visitor » ;)
    • Finalement, Letlang est devenu un (r)habillage de Rust avec ton déclic (:

    “It is seldom that liberty of any kind is lost all at once.” ― David Hume

    • [^] # Re: j'en dis que

      Posté par  (site web personnel) . Évalué à 2. Dernière modification le 08 mars 2022 à 03:01.

      Ton compilo a mangé des lignes dans la partie « Le design pattern Visitor » ;)

      Ah! Si un modo passe par la:

      trait Visitor {
        type Node;
      
        fn visit(node: Box<Self::Node>, context: &mut Context) -> Result<String>;
      }

      Finalement, Letlang est devenu un (r)habillage de Rust avec ton déclic (:

      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  (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  (site web personnel) . Évalué à 2.

          ne crains-tu pas qu'au niveau des messages on se retrouve un peu loin des paradigmes du langage initial ?

          Je prévois une compilation en plusieurs phases :

          • Phase 0: l'analyse lexicale transforme le texte en "Token Stream"
          • Phase 1: l'analyse syntaxique transforme le "Token Stream" en un AST annoté de metadata (nom du fichier, ligne, colonne, token, …)
          • Phase 2: l'analyse sémantique parcours ce premier AST pour vérifier le respect des règles du langage
          • Phase 3: l'analyse statique parcours à nouveau l'AST pour vérifier la cohérence des types du mieux qu'il peut, ainsi que l'inférence de type quand c'est possible
          • Phase 4: le compilateur traduit l'AST annoté par les 2 phases précédentes en langage Rust
          • Phase 5: appel du compilateur Rust

          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 :

          • l'AST consommé par l'étape 4 est censé avoir toutes les informations nécessaire pour produire un code complet
          • le code produit par les différents noeuds de l'AST a été testé en amont (un Literal::Number va toujours produire le même code)
          • l'AST garanti qu'on ne trouvera pas un "bloc de fonction" utilisé comme string literal pour un import, donc le code Rust produit reflète cet aspect aussi, on ne peut trouver que du code Rust valide dans le contexte/scope ou il est produit car c'est comme ça que l'implémentation aura été faite

          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  . É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  (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: les parenthèses de lisp...

      https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg

      • [^] # Re: lisp ?

        Posté par  (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:

        fn main    
          let s = "Hey, is this a complex-program, or a complex_program" 
          let chunks:Vec<_> = s split(                    ) collect() 
        
          println!("Split on multiple separators =      ", chunks) 
        }

        What non-Ruster sees:

               () {
                = "                      -       ,             _       ";
                    :   <_> =  .     (&[',', '-', '_'][..]).       ();
        
                 !("                             = {:?} ",      );
        }

        “It is seldom that liberty of any kind is lost all at once.” ― David Hume

      • [^] # Re: lisp ?

        Posté par  . É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  . É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  (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  (site web personnel, Mastodon) . Évalué à 4. Dernière modification le 08 mars 2022 à 21:51.

          pour f\circ g 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  (site web personnel) . Évalué à 3.

            (je trouve que) l'on délimite mieux les portées des uns et des autres avec (f (g ) )

            Le meilleur exemple "non-lisp" (entre grosses guillemets) c'est les templates Go :

            {{ $d := dict "foo" (list 1 2 3) "bar" (list 1 2 3) }}
            {{ $test := eq (index (index $d "foo") 0) (index (index $d "bar") 0) }}
            

            https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg

          • [^] # Re: lisp ?

            Posté par  . É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  (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  (site web personnel) . Évalué à 3.

              Dites les critiques, vous êtes sûrs que vous ne réagissez pas de manière un peu 'instinctive'?

              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:

              a |> b() |> c()
              # vs
              c(b(a))

              Si je voudrais l'ajouter tel quel en C, ça donnerait :

              a |> b() |> c()

              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 ), et f a b c est une expression ou f 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)). Ici g prend un seul argument, une liste, construite avec cons.

              On a (defvar a 42), (defun ...), (lambda ...) et (defmacro ...). Tout se ressemble. C'est cohérent. Du coup introduire f(a b c), ça casse la cohérence.

              Le Lisp classique a d'ailleurs '(x y) pour l'abbréviation de (quote x y)..

              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  (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  . É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  (site web personnel, Mastodon) . Évalué à 5.

                    tout dépend…

                    • Perso, dans mon entourage, je ne vois pas beaucoup d'informaticien-ne-s pouvant écrire un hello world en Lisp ; sauf si t'entends par culture générale d'en avoir lu le mot une fois quelque part (et encore sur un sondage avec les collègue présent-e-s aujourd'hui je n'en même pas 15% qui connaissent de nom…)
                    • Si la définition de langage moderne est d'avoir une autre syntaxe alors la lapalissade est un peu tautologique et autoréalisatrice. Si c'est par rapport à l'âge, il y a bien des trucs assez récents comme Racket et Hy sans oublier R7RS small, et un peu avant LFE et Clojure sans oublier R7RS, et tout juste avant Visual LISP en 2000
                    • LISP est né vers 1955, et les expériences que j'ai cité ont eu lieu à partir de 1968 (MLisp) avec un regain d'intérêt vers 1977 (CGOL) alors que CL naitra en 1984 seulement…
                    • Quand on élabore d'autres langages on explore d'autres voix/voies en n'étant influencé que par ce qu'on connait bien (comprenne qui peut), on ne veut pas juste reprendre des choses existantes (cf. justement journal précédent sur Leaflet)

                    “It is seldom that liberty of any kind is lost all at once.” ― David Hume

                    • [^] # Re: lisp ?

                      Posté par  . É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  (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  . É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  . É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  (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

    letlang::compile_file!("main.letlang");

    Et la macro se charge de produire le code en rust.

    Pour les erreurs, je suggère codemap-diagnostic

    Prendre des p'tits bouts d'trucs, et les assembler ensemble

    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:

    #[letlang::letlang] load_file!("main.letlang"); /*
    
    [package]
    name = "my_letlang_app"
    version = "0.1.0"
    edition = "2021"
    
    [[bin]]
    name = "my_letlang_app"
    path = "Cargo.toml"
    
    [dependencies]
    letlang = "*"
    
    # */
    • [^] # Re: Générer du Rust

      Posté par  (site web personnel) . Évalué à 4.

      Tant qu'a générer du Rust, tu peux aussi utiliser des macro procédurale.

      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 :)

      Pour les erreurs, je suggère codemap-diagnostic

      Excellent ! Je garde de côté, merci :)

      tu pourrait avoir un self-contained Cargo.toml (qui est à la fois valid toml et rust) comme fichier pour le build système:

      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  (site web personnel) . Évalué à 5.

        utiliser des templates est moins capilo-tracté que manipuler le TokenStream

        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.

        J'ai pas compris l'exemple, ce code irait dans le main.rs du projet ?

        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  (site web personnel) . Évalué à 3.

          Tu peux utiliser des templates dans une macro, et simplement convertir la le résultat avec TokenStream::parse a la fin

          Ah effectivement, ça peut être intéressant.

          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.

          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 ?

          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.

          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  . Évalué à 3.

    Ce commentaire a été supprimé par l’équipe de modération.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.