ArkScript, un langage de scripting fonctionnel!

a marqué ce sujet comme résolu.

Salut Zeste de Savoir ! Je viens vous partager un projet sur lequel je travaille depuis avril 2019, ArkScript.

Pour la petite histoire : après les échecs de Kafe, j’ai voulu me remettre le pied à l’étrier en me lançant sur un petit langage lisp like, interprété. De là est né Ark (renommé plus tard en ArkScript, cf la discussion sur reddit). Puis je me suis dit "et si cette fois-ci, je le compilais vraiment ce langage?" et au lieu de me lancer dans la VM avant de faire le compilo, j’ai fait les choses proprement, et voilà où nous en sommes.

ArkScript reprend la philosophie originelle de Kafe "scripting de jeux vidéos", mais c’est tout, les autres points sont bien différents. Petit à petit cette philosophie a évolué en "un langage facile à utiliser pour scripter n’importe quel projet en C++".

Concepts et objectifs

En bref :

  • un langage immuable par défaut (passage par valeur),
  • un langage fortement et dynamiquement typé,
  • pas de références cachées à la Python avec les listes,
  • très peu de mots clés (9),
  • des closures à capture explicite,
  • privilégier les features avant les performances,
    • élimination de dépendance cycliques,
    • processeur de macros Turing Complete,
    • élimination de dead code,
  • de l’asynchrone avec n’importe quelle fonction,

ArkScript est inspiré de Lisp (entre autre pour la facilité à le parser), tout est immuable par défaut, et tout est passé par valeur. Cela représente un coût à l’exécution, coût que je suis prêt à payer (cependant toutes les fonctions travaillant sur des listes évitent le plus possible les copies, sans pour autant impliquer des références cachées comme le ferait Python). L’objectif du projet n’est pas d’être plus performant que du C bien fait, mais de privilégier les features (même si j’ai travaillé un peu sur les performances pour ne pas rester en rade), de manière à être plus productif quand on code avec ce langage.

Je voulais que le langage tourne sur VM pour le gain de performance comparé à une interprétation d’un arbre, et pour la portabilité: on compile une fois un code Ark et il marche partout tant que la VM est installée.

Le parsing se veut très simple, étant un lisp like il n’y a pas grand chose à faire. La partie la plus complexe, c’est le couple compilateur/VM, car il faut faire pas mal de choses pour fournir un langage haut niveau. On a donc de l’élimination de dépendances cycliques au parsing, de la réduction d’expressions constantes par le compilateur ainsi que de l’élimination de dead code (au scope global uniquement) et des expression statements (ce qui permet de ne pas polluer la VM avec des valeurs inutilisées). Le tout est saupoudré d’un processeur de macros Turing Complete (on peut faire des macros récursives, co-récursives, etc).

Le langage est dynamiquement et fortement typé, et autorise les listes hétérogènes. De plus, il n’y a aucune référence cachée que l’utilisateur peut manipuler, ce que je trouve très important pour conserver cet aspect d’immuabilité quand on déclare une variable avec let. En interne, côté VM, des références sont utilisées mais n’atteindront jamais l’utilisateur telle quelle et verront leur valeur copiée au moment voulu (copy on write) uniquement.

Enfin, ArkScript se veut concis et n’a que 9 mots clés à offrir: let, mut et set pour les constantes/variables, begin pour les blocks, if et while pour le contrôle de flux, del pour vider la mémoire manuellement (ArkScript profite également du RRID du langage d’implémentation, le C++), import qui permet d’importer du code Ark et des binaires qui utilisent une API spéciale (par exemple pour charger la SFML, manipuler la console…), et fun pour déclarer une fonction ou closure (à capture explicite).

Le langage se veut petit pour être utilisable dans un projet sans plomber la taille de celui-ci, la machine virtuelle tient sur moins de 3000 lignes (en comptant tous les fichiers des dossiers VM/). Voir ce benchmark d’octobre 2020 qui compare ArkScript 3.0.13 à d’autres langages dont ChaiScript, Lua et Python.

Un écosystème de plugins

Le langage se veut extensible, et pour ça il expose une API pour créer facilement un plugin C++, qui peut être chargé par la machine virtuelle, par exemple pour :

  • faire des appels HTTP
  • jouer avec les couleurs du terminal
  • parser du JSON
  • communiquer avec une base SQLite3
  • et bien plus encore

Des exemples

Un petit exemple qui utilises les closures avec la capture explicite (via la notation &capture), le closure field reading (dot notation) pour simuler de l’orienté objet (sans possibilité de modifier l’objet de l’extérieur, tout est en lecture seule quand on n’est pas dans la closure):

(let create-human (fun (name age weight) {
    # functions can be invoked in the closure scope
    (let set-age (fun (new-age) (set age new-age)))

    # this will be our "constructor"
    (fun (&set-age &name &age &weight) ())}))

(let bob (create-human "Bob" 0 144))
(let john (create-human "John" 12 15))

(print bob.age)
(bob.set-age 10)
(print bob.age)

(print john.age)

La fonction d’Ackermann Péter qui me sert pour mes benchmarks comme elle est récursive non primitive, donc pas optimisable par un compilateur, très pratique pour tester l’implémentation d’un langage:

(let ackermann (fun (m n) {
    (if (> m 0)
        (if (= 0 n)
            (ackermann (- m 1) 1)
            (ackermann (- m 1) (ackermann m (- n 1))))
        (+ 1 n) )}))

(print (ackermann 3 6))

Les releases sont disponibles ici (la lib standard est fournie avec chaque release, ainsi que les modules en .arkm).

Voici le repo GitHub du projet https://github.com/ArkScript-lang/Ark. N’hésitez pas à passer faire un tour sur le site du projet et sa documentation ! Enfin si vous avez besoin d’aide, passez faire un tour sur le discord communautaire, nous nous ferons un plaisir de vous aider.

Dernière version Code size

+3 -0

(06/07/2024) Ce post inclut un élément de syntaxe disparu dans la v4 (quoting)

Bonsoir !

Depuis le dernier post, beaucoup de changements ont eu lieu, que vous pouvez tous retrouver ici: https://github.com/ArkScript-lang/Ark/blob/dev/CHANGELOG.md

Le gros des changements, c’est:

  • une équipe qui grossi ! J’étais seul quand j’ai fait le premier post, et maintenant nous sommes 6
  • un code plus clair, qui sépare exécution (VM) et données (State)
  • un REPL (la sauvegarde de scope est en train d’être faite par un de nos contributeurs)
  • une bibliothèque standard qui grossi de jour en jour
  • des améliorations notables en terme de performance
  • toujours plus de détections d’erreurs, essentiellement sur les importations cycliques, mais aussi des erreurs plus claires au runtime

Le langage est désormais suffisamment mature pour que l’on puisse coder un forEach directement en ArkScript, sans toucher au code C++, et ça c’est beau quand même

(let forEach (fun (L code) {
    (mut i 0)
    (while (< i (len L)) {
        (mut element (@ L i))
        (code)
        (set i (+ 1 i))})}))

(forEach [1 2 3 4] '{
    (print element)})
+0 -0

(06/07/2024) Ce post inclut un élément de syntaxe modifié dans la v4 ((import "http.arkm") -> (import http))

Salut tout le monde!

Quoi de neuf depuis l’an dernier?

En bref:

En détails:

Déjà, une migration sur https://github.com/ArkScript-lang/ ! Dans le futur plus de dépôts vont s’y retrouver dont un gestionnaire de modules pour ArkScript qui est envisagé depuis un moment maintenant. Les modules ont été migrés sur un dépôt séparé pour plus de visibilité.

L’API a suffisamment évolué pour permettre l’appels de fonctions Ark depuis un module (mis en œuvre dans le module HTTP qui permet de faire des requêtes get/put/delete et bien d’autres, mais aussi de mettre en œuvre des serveurs web (toujours en développement)).

Petit exemple d’usage du module http :

(import "http.arkm")

(let srv (httpCreateServer))
(let f (fun (str) {
    (print "hello world")
    (+ "we got: '" str "', it worked!")}))
(httpServerGet srv "/hi" f)
(httpServerListen srv "localhost" 80)

Qui lance un serveur qui affichera we got: 'test', it worked! (pour le moment le seul argument passé est un "test", dans le futur on peut imaginer envoyer les valeurs d’une requête GET par exemple.

En espérant bientôt vous revoir, et pourquoi vous reparler sur le serveur discord du projet si vous souhaitez contribuer !

+1 -0

Long time no see!

Mais je suis porteur de beaucoup de bonnes nouvelles :)

Tout d’abord, le projet est maintenant dans une organisation dédiée: https://github.com/ArkScript-lang, sur laquelle on a donc le langage en lui même, les modules de bases (en C++), ArkDoc (un doxygen maison pour documenter du code ArkScript), le code source du site de présentation et de la documentation.

Ensuite, nous sommes maintenant 10 à travailler sur ArkScript, dont 3 contributeurs très actifs.

Les avancées sur le projet concernent essentiellement des mises à jour interne à la machine virtuelle et au compilateur, pour la rendre plus rapide (d’où la prolifération de branches orientées optimisations). Une bonne nouvelle est qu’une allocation de liste de 1000 éléments (125 essais) prenait 75µs, seulement 15µs maintenant, et l’usage mémoire global a été diminué. Le compilateur est bien plus rapide également, compiler 10k lignes de code ArkScript prend 14 secondes, contre plusieurs minutes auparavant (le couple lexer/parser va être amélioré pour faire descendre ce nombre encore plus bas).

La lib standard a légèrement été modifiée pour des conventions de nommage et de code mais rien de nouveau de ce côté, justement on attend vos retours, vos cas d’usages, vos questions sur le langage pour l’améliorer! Alors n’hésitez pas à essayer le langage, il est suffisamment stable pour être utilisé en lieu et place de Lua ou Python par exemple! :)

La liste complète de ce qui a été changé est ici: https://github.com/ArkScript-lang/Ark/blob/dev/CHANGELOG.md

Bonne journée à tous, en espérant vous revoir utiliser ArkScript !

+1 -0

Coucou tout le monde!

Actuellement on bosse sur une amélioration du REPL (outil en ligne de commande pour tester ArkScript vite fait, comme l’interpréteur Python) avec coloration et complétion, et on nettoie les projets pour éviter qu’un git clone ne traine 6000 dépendances inutiles avec lui.

Jusqu’ici on a pu améliorer les performances de la VM de façon significative en diminuant la taille de Ark::Value, le type de base traité par la VM, et on retravaille le site web et la documentation (le wiki étant voué à disparaitre). On a également (enfin) un dépot séparé pour les benchmarks: https://github.com/ArkScript-lang/benchmarks, n’hésitez pas à y contribuer !

+2 -0

(06/07/2024) Ce post inclut un élément de syntaxe disparu dans la v4 (str:format %% est devenu {}, en suivant la syntaxe de fmtlib.dev)

Salut !

On a donc un nouvel interpréteur (REPL) qui a de la persistance entre chaque ligne tapée (avant chaque morceau de code exécuté était oublié dès qu’on tapait un nouveau code), de la coloration et de l’auto complétion, le tout en utilisant la lib replxx (en C).

Ensuite, j’ai récemment ajouté l’élimination de dead code (au scope global uniquement) pendant la compilation, on a un lexer fait maison sans regex et bien plus rapide (et qui gère enfin les séquences d’échappement, bien que je doive trouver un moyen de faire fonctionner les séquences \u \U et \x), un meilleur chargement des plugins (dynamique et non statique désormais, donc on peut faire de l’importation conditionnelle en fonction de l’OS par exemple).

Aussi, niveau usage mémoire, les plugins ajoutant des objets à ArkScript (eg une fenêtre SFML) peuvent désormais voir ces objets détruits en bonne et due forme quand le scope dans lequel est l’objet est détruit.

Niveau fonctionnement interne, la nouvelle string s’est dotée d’une fonction de formatage template, donc str:format a également été impacté. Eg: (str:format "test % something %% %% 0x%x the end %% ok" 256 "hello" -12345 3735928559) donnera test something hello -12345 0xdeadbeef the end %% ok. Si on donne trop d’arguments et pas assez de séquences %%, ils ne seront pas utilisés, si on en donne pas assez, les %% resteront tels quels, permettant de faire un formatage en plusieurs étapes. Ainsi exit les segfault/undefined behavior sur un printf("%s hello %i", "world").

Niveau lib std, elle a bougé sur un nouveau dépôt pour qu’on puisse l’update sans toucher au dépôt principal, on est en train de faire une grosse passe de documentation et renommage pour clarifier pas mal de choses, avec des noms de variables de type namespace:fonction, eg: list:reverse. Tant qu’on n’a pas de macro, le namespace sera obligatoire et non modifiable, car il fait parti à 100% du nom de la variable/constante.

Niveau erreurs, on a de meilleurs messages d’erreurs pour indiquer où on s’est trompé (au lexing et parsing, compilation et VM également).

Au lexing/parsing on aura quelque chose comme:

main:007> (if)
class Ark::ParseError: ParseError: no more token to consume
On line 1:59, got TokenType::Grouping
    1 | (if)
      |    ^
    2 | no more token to consume
Ark::State::doString failed

Dans la VM, on a de meilleurs messages quand une variable est inconnue / inutilisable ou quand on a des soucis d’importation:

main:001> (print a)
unbound variable: a
At IP: 2, PP: 0

main:001> (let a 12) (print (a))
Can't call 'a': it isn't a Function but a Number
At IP: 11, PP: 0

main:012> (import "blabla")
could not load plugin: blabla
At IP: 44, PP: 0

main:013> (import "blabla.ark")
class std::runtime_error: While processing file FILE, couldn't import blabla.ark: file not found
Ark::State::doString failed
+2 -0

Hey!

Après quelques mois sans nouvelles, je reviens pour la hacktoberfest.

Jusqu’ici le bilan est plutôt positif, on a des contributeurs qui travaillent bien sur le package manager, nuclear (en Python), et on a reçu quelques PR sur le langage lui même (correctifs de bugs surtout).

Niveau versions, on a sorti la 3.0.12 récemment, qui amène son plus gros lot de nouveautés.

Changelog:

Added

  • using a macro to define the default filename (when none is given, eg when loading bytecode files or from the REPL)
  • PLUGIN <const id> instruction to load plugin dynamically and not when the VM boots up
  • updated search paths for (import "lib.ark"), looking in ./, lib/std/ and lib/
  • added a case to display NOT instructions in the bytecode reader
  • T& as<T>() in usertype
  • enhanced error message when calling a non-function object
  • eliminating unused global scope variables in the compiler
  • adding a new feature enabled by default: FeatureRemoveUnusedVars (not enabled for the REPL | for obvious reasons)
  • added replxx as a submodule
  • added custom destructor to the user type, called when a scope is destroyed and when we use (del obj)
  • added a GVL (global virtual machine lock) to be able to use the VM in a multithreaded context
  • dockerfile + specific github action to build and push stable and nightly docker images, thanks to @yardenshoham
  • added guards to the bytecode reader to stop reading if we’re missing an entry point; now telling the user about it

Changed

  • updated the string module to benefit from the new format member function
  • updated the logger to remove fmt/format
  • changed the argument order for Ark::State
  • renamed the cache directory __arkscript__
  • operator @ can now handle negative indexes to get elements from the end of the given container
  • the standard library is now in another repository
  • moved the modules to lib/ext
  • the value of CODE_SEGMENT_START is again 0x03 (because we removed the plugin table)
  • renamed isDir? to dir? for consistency
  • the lexer is no longer using regexes but a char after char method
  • an ArkScript program is no longer a single bloc, but can be composed of multiple bloc, thus we don’t need to use a single big {} or (begin) bloc for all the program
  • enhancing lexer and parser error messages
  • else clause in if constructions is now optional
  • updating error messages in the VM
  • updated the repl to add auto completion, coloration and persistance by @PierrePharel
  • moving the parser, lexer and AST node to Compiler/ from Parser/
  • better import error messages at parsing
  • format can now handle any value type
  • updated the tests to use the new standard library, and testing every VM instruction and builtins (we didn’t test everything before, this way we can be sure we don’t break anything in the VM after each update)
  • renaming builtins to add a namespace to them (math:, sys:, str:, list: and such)
  • firstOf, tailOf and headOf now returns [] or "" instead of nil when they have nothing to do
  • adding a brand new scoping system, lighter, more powerful
  • str:find now returns the index where the substring was found
  • str:removeAt was fixed to throw an error when the index is strictly equal to the length of the string (can not work since accessing elements in string is 0-index based)

Removed

  • removed fmt/format from our dependencies
  • PLUGIN_TABLE was removed to use the PLUGIN instruction
  • not_() from usertype
  • removed Parser/Utf8Converter

A par ça, on est en train de stabiliser quelques modules, qui seront installables via le package manager, on rajoute des tests (toujours plus car trop c’est jamais assez), et on table sur un rewrite complet de la VM pour aller vers de nouvelles pistes d’optimisations.

En gros tout va pour le mieux, on attire des contributeurs et des utilisateurs, et ça c’est génial :)

+0 -0

(06/07/2024) Ce post inclut un élément de syntaxe disparu dans la v4 (macro: !{name val} -> ($ name val))

Hello, ça faisait longtemps, me revoilà avec de bonnes nouvelles!

On bosse toujours beaucoup sur le langage avec au programme: de l’UTF8 (pour les strings et peut être les identifiers), très bientôt des macros avec gestion des arguments variadiques, diverses optimisations, et on est train d’agrandir la lib standard et les modules.

Pas mal de correctifs ont pu être fait sur les modules et pour tester tout cela, j’ai fait une mini blockchain dans le langage: https://github.com/ArkScript-lang/Ark/blob/dev/examples/blockchain.ark.

Les deux ans du projet c’est fin avril, d’ici là j’espère avoir fini le support des macros qui devront permettre de faire autant voir plus que le préprocesseur C:

!{name 12}
!{if (= name 12)
  !{a (...args) (print args)}
  !{a (...args) (print "no")}}
!{foo (a b c) {(let a 12) (let b 12) (let c 12)}}

Avec possibilité d’appeler une macro depuis une autre pour un peu plus de piment.

Une des plus belles optimisation est un passage (en interne) sur une stack linéaire unique plutôt qu’une stack de stack, ce qui nous a fait passé de 90ms à 55ms sur Ackermann Péter (3, 6), un test de récursion extrêmement gourmand. On reste loin de Python mais le but n’est pas de le rattraper/dépasser, simplement d’être suffisant rapide pour être utilisable convenablement.

Le changelog complet est ici pour les fanatiques d’updates: https://github.com/ArkScript-lang/Ark/blob/add3881d200b78275d02b492626aa140124e15aa/CHANGELOG.md

+2 -0

Bonjour, cela faisait longtemps que je n’avais pas posté ici, j’ai trop négligé la communication pour me concentrer sur le code et les cours.

Les avancées

Depuis le dernier message, le projet a subit moulte refacto:

  • 210 fichiers modifiés,
  • 11’273 lignes ajoutées,
  • 8’065 lignes supprimées (pas assez à mon goût)

Dans les grandes lignes, le projet a été remanié pour faciliter son intégration dans d’autres projets, et également envisager un publication sur les dépôts d’Arch Linux. Nous avons rajouté de nombreux tests, que ce soit dans le langage ou en C++, pour pouvoir continuer à garantir le fonctionnement du langage et de son intégration dans des projets en C++ (une interface C est envisagée).

Je me dois également de dire que le macro processor fonctionne depuis que j’avais annoncé son existence. Beaucoup de bugs y ont été trouvé, mais un nouveau développeur nous a rejoint et nous a grandement aidé pour les résoudre, en améliorant la structure du dit macro processor.

Grande nouveauté aussi, l’intégration de fonctions qui travaillent explicitement par référence: append!, concat! et pop! (et leurs équivalents sans !). Ces fonctions sont utilisées pour travailler sur des listes, qui sont autrement copiées autant de fois que nécessaire quand on utilise les versions safe (sans !), qui retournent toutes une nouvelle liste créée à partir de leurs arguments. Les fonctions unsafe (dans le sens "casse l’immutabilité du langage") prennent en argument des références sur objets mutables (donc (append! [1 2] 3) est invalide), modifie leur argument (le premier, les autres peuvent être des copies/références (non) mutables) in place et retourne toujours nil.

Aussi, nous avons fait le grand saut et défini un clang-format pour avoir un code formatté de manière unique, et facilement lisible par toute l’équipe (qui varie entre 4 et 6 personnes qui sont là de manière permanente). La CI a été revue et nous permet maintenant de générer des binaires pour Windows, Linux, et MacOS, sachant qu’un InnoSetup est créé pour les versions Windows désormais.

Notre génération de documentation, ArkDoc a été entièrement recodé en Python et nous permet de sortir plusieurs formats (HTML, Markdown (encore en cours, mais en soit ce n’est pas trop compliqué, si un jour je m’y mets)) : https://arkscript-lang.dev/std/ (qui redirige en théorie vers la dernière version de la documentation).

Ce qui est prévu pour le futur

  • un refactor de la machine virtuelle, pour permettre un ajout plus simple de parallélisme
  • l’élimination d’un side-effect d’une VM à pile : le trashing: (while true 1) va pousser la valeur 1 sur la pile à l’infini et on peut l’empêcher en modifiant notre compilateur mais ça n’est pas trivial
  • améliorer notre REPL (plus de contrôles, accès à la documentation directement)
  • amélioration des erreurs de type pour aider l’utilisateur
  • réécriture du package manager, qui n’a jamais été testé ni utilisé bien qu’il soit considéré comme "fini"

On a encore (et toujours) beaucoup de travail, mais on approche de quelque chose qu’on pourrait considérer comme "fini" (plus dans le sens "complet, qui marche bien" que "terminé" cependant).

+2 -0

Hello Zeste de Savoir !

Le projet est toujours vivant, je manquais simplement de contenu pour faire un nouveau post ici.

Entre aujourd’hui et le dernier post, en prenant compte uniquement de la branche de dev, un petit refacto du projet est passé :

  • 88 fichiers modifiés,
  • 3093 lignes ajoutées,
  • 1991 supprimées

J’ai bien précisé la branche de dev, parce qu’une petite nouvelle est apparue, qui vise spécifiquement la prochaine grosse release, ArkScript 4.0 ! Au programme on a :

  • de meilleures erreurs de types (déjà disponibles en 3.1.3),
  • la suggestion de symboles proches quand on tombe sur un unbound symbol pendant la compilation (3.2.0),
  • la conversion de closures en chaine de caractères avec leurs champs,
  • l’ajout d’async/await en tant que builtins,
  • la gestion des unused expressions qui polluaient la stack (dont je parlais dans le précédent post, disponible en 3.2.0),
  • l’optimisation de tail call (3.2.0) quand une fonction s’appelle elle-même (sera généralisé en 4.0),
  • plein de correctifs de petits bugs ennuyeux

On peut voir le travail effectué depuis l’an passé comme du nettoyage et un peu d’house keeping, les vacances étant arrivées pour une partie de l’équipe, le développement ralenti un peu. Cependant on prévoit des améliorations géniales pour le reste de l’année, entre la génération d’un AST sous forme de JSON (utile pour faire un LSP, écrire des tests pour le lexer/parser/optimiseur/macro processeur, pour avoir un nouveau backend qui transpile en WASM ou autre…), du fuzzing pour détecter le plus d’erreurs possibles dans l’implémentation du langage.

La documentation du langage continue d’évoluer et de s’améliorer, si vous avez des remarques, des questions ou mêmes des suggestions n’hésitez pas !

(let size 1000)
(let data (list:fill size 1))  # une liste de remplie de mille 1
(let sum (fun (a b src) {
    (mut acc 0)
    (while (< a b) {
        (set acc (+ acc (@ src a)))
        (set a (+ 1 a))})
    acc
}))


(let workers [
    (async sum 0 (/ size 4) data)
    (async sum (/ size 4) (/ size 2) data)
    (async sum (/ size 2) (- size (/ size 4)) data)
    (async sum (- size (/ size 4)) size data)])

(let res-async
    (list:reduce
        (list:map
            workers
            (fun (w) (await w)))
        (fun (a b) (+ a b))))
proposition de variable et coloration des erreurs
proposition de variable et coloration des erreurs
(let make (fun (a b c)
    (fun (&a &b &c) ())))

(let foo (make 1 2 3))
(print foo)  # output: (.a=1 .b=2 .c=3), avant: Closure<4>
functionName: needs argumentCount argument(s), got actualArgumentCount.
  -> arg1Name (wanted type) was arg1Value (of type arg1Type)
  -> arg2Name (wanted type) was arg2Value (of type arg2Type)
+3 -0

Bonsoir la compagnie !

Depuis le dernier message, j’ai pris du temps pour travailler sur les messages d’erreurs du langage, pour mieux aider les utilisateurs quand une erreur survient, ainsi que plus de warnings à la compilation:


J’ai commencé la chasse aux bugs en intégrant AFL++, un fuzzer très pratique qui m’a remonté pas mal de bouts de code capables de faire planter le lexer, parser, macro processor, rarement le compilateur, et de temps en temps la machine virtuelle. Bref, que de la joie, mon appart est bien chauffé au moins, et j’ai du travail jusqu’à la fin de l’année.

une instance du fuzzer en action
une instance du fuzzer en action

Pour donner quelques chiffres:

  • 6 instances du fuzzer ont tourné, chacune pendant 1 heure
  • le load average est monté à 12 (sur un Xeon à 8 coeurs/16 threads c’est pas mal)
  • l’usage de RAM était capé à 64MB par instance
  • une moyenne de 500 crash remontés par instance
  • une moyenne de 1000 hangs remontés par instance (il est fort pour générer des (while true ()) et similaire)
  • la température CPU qui a grimpé de 45° à 80°, et celle de mon appart de +2° (qui a besoin de chauffage ?)

J’ai dû fixer une petite dizaine de bugs grâce à ça, c’est fastidieux mais très intéressant. En partant des crash remontés, j’ai donc commencé une collection de "code qui plante mais ça doit être détecté".

quelques tests d'échecs
quelques tests d’échecs

Et un test est un couple de deux fichiers:

# input.ark
(())

# input.expected
TypeError: A Nil isn't a callable

Côté avancement sur la v4, j’ai réécrit des bouts du compilateur et nettoyé le code pour le rendre plus simple à lire, et terminé l’intégration du nouveau format de bytecode, où chaque instruction est sur 4 bytes ce qui rend le peeking bien plus facile.

Une nouvelle syntaxe pour les imports est également prévue: nouvelle syntaxe des imports

Les plus attentifs auront remarqué que cette syntaxe va empêcher l’inclusion de scripts de dossiers parents, ce qui était actuellement possible avec un (import "../file.ark"). Je pense que c’est un moindre mal qui poussera à réfléchir un peu plus à la structure d’un projet, mais pour autant ça ne devrait pas être catastrophique.

Cependant, cette nouvelle syntaxe n’est pas figée dans le marbre. Bien que le sens actuel de (import folder.foo.bar) soit "importe folder/foo/bar.ark depuis le dossier du script actuel", il est possible que cela soit réévalué pour suivre les packages à la Scala.

Ainsi, si on exécute main.ark, qui import foo/bar.ark, et que ce dernier veut faire un (import bacon.yellow.color), on chercherait bacon/yellow/color.ark à partir du dossier d’exécution de main.ark et non du dossier où est présent bar.ark.


En dehors de ces améliorations de la QoL, cela fait bientôt deux semaines que je travaille à l’élaboration d’un nouveau parser pour le langage, dont vous pouvez trouver la source sur GitHub: https://github.com/SuperFola/parser-combinators. En effet le lexer/parser actuel est difficilement maintenable, presque impossible à faire évoluer sans casser quelque chose, et sûrement bourré de bugs que je n’ai pas encore trouvé.

C’est pour ça que j’ai tenté une approche parser combinators, et le résultat est là, 2 fois moins de code, 2 fois plus simple à lire, des performances que je considère comme correcte (je dois mesurer le parser actuel pour pouvoir comparer cependant). Pour couronner le tout, 0 warning (Wall, Wextra, Wconversion, Wshadow et pedantic, je suis fou), 0 fuite de mémoire, des tests dans tous les sens et bientôt du fuzzing.

Running ./build/bench
Run on (8 X 24.121 MHz CPU s)
CPU Caches:
  L1 Data 64 KiB
  L1 Instruction 128 KiB
  L2 Unified 4096 KiB (x8)
Load Average: 1.29, 1.69, 1.91
------------------------------------------------------------
Benchmark                  Time             CPU   Iterations
------------------------------------------------------------
Simple - 39 nodes      0.067 ms        0.067 ms       103688
Medium - 83 nodes      0.395 ms        0.395 ms        17430
Big - 665 nodes         31.9 ms         31.8 ms          224

A la prochaine !

+3 -0

Hello Zeste de savoir!

Cela fait maintenant 581 jours que je n’ai pas donné de nouvelles ici. Entre temps, c’est environ 21880 lignes ajoutées et 18238 supprimées dans divers refacto, réparties en 202 commits (l’année 2023 a été très vide, le travail a repris en début d’année 2024)!

Un parser? Encore?

Pour reprendre la fin du dernier message, le nouveau parser a été intégré à ArkScript de façon définitive, et l’ancien décommissionné. L’intégration a été plus facile que prévue si mes souvenirs sont bons, beaucoup de code laid et dur à maintenir ayant disparu.

Et par dessus le marché, ces derniers mois j’ai pu optimiser un peu plus ce parser, qui prenait un temps fou à calculer la position du curseur dans le fichier (pas si facile que garder un compteur comme on revient souvent en arrière, cf le monstre qui aide à positionner les sauts de ligne). Ce qui nous donne les perf suivantes:

---------------------------------------------------------------------------
Benchmark                                 Time             CPU   Iterations
---------------------------------------------------------------------------
New parser - Simple - 39 nodes/0      0.045 ms        0.045 ms        15421 nodesAvg=92.526k nodesRate=132.633k/s uselessLines/sec=265.266k/s
New parser - Medium - 83 nodes/1      0.126 ms        0.126 ms         5514 nodesAvg=55.14k nodesRate=79.4941k/s uselessLines/sec=198.735k/s
New parser - Big - 665 nodes/2         1.66 ms         1.66 ms          422 nodesAvg=8.018k nodesRate=11.4657k/s uselessLines/sec=191.9k/s

(sur exactement le même matérial, un Mac M1 Pro à 8 coeurs et 32Go de RAM)

La syntaxe des macros évolue

Auparavant on avait une syntaxe pas très belle et pas logique:

!{name 12}
!{if cond then else}
!{func (arg arg2) body}

Maintenant on utilise la même syntaxe que le reste du langage avec un token magique, $:

($ name 12)
($if cond then else)
($ func (arg arg2) body)

Et on a de nouvelles primitives comme $repr pour obtenir une string d’un noeud au compile time, symcat pour concaténer des symboles (utile dans des macros récursives définissant des variables temporaires), argcount pour avoir le nombre d’arguments d’une fonction. (il va falloir uniformiser les noms pour qu’ils commencent tous par $, c’est dans mon backlog).

Note

On pourrait utiliser un pattern matching pour extraire le nombre d’arguments d’un noeud, mais ça reste quelque chose de complexe à implémenter, et pour le moment le besoin ne s’en fait pas assez sentir. Ce qui est sûr c’est que ça serait vachement cool à implémenter, ne serait-ce que pour l’expérience, mais vu l’état du macro processor, ça va attendre encore un peu.

Toujours plus de benchmarks

Comme j’aime les nombres et stats en tout genre, j’ai décidé d’ajouter plus de benchmarks l’an passé, avec des bench pour la VM que je peux lancer sur ma machine, et sortir des CSV. Ajouter une pincée de Python et je peux suivre les évolutions de perf à chaque commit si je le souhaite, pour traquer de potentielles régressions. Plutôt cool non?

On peut voir l’évolution entre plusieurs commits ici, avec les pertes en ms et % entre chaque commit et la base (le 0–684ea758), successivement:

                          |           | 0-684ea758   | 1-d45d7ea1         | 2-abb043b4          | 3-75161de7          | 4-ad889963
--------------------------+-----------+--------------+--------------------+---------------------+---------------------+---------------------
 quicksort                | real_time | 0.152787ms   | -0.008 (-5.3205%)  | -0.009 (-5.8277%)   | -0.009 (-6.2021%)   | -0.011 (-7.4961%)
                          | cpu_time  | 0.152334ms   | -0.008 (-5.3383%)  | -0.009 (-6.0518%)   | -0.010 (-6.2993%)   | -0.011 (-7.4757%)
 ackermann/iterations:50  | real_time | 81.2917ms    | -7.924 (-9.7471%)  | -12.798 (-15.7432%) | -12.939 (-15.9169%) | -14.163 (-17.4218%)
                          | cpu_time  | 80.9612ms    | -7.867 (-9.7172%)  | -12.764 (-15.7655%) | -12.980 (-16.0327%) | -14.116 (-17.4358%)
 fibonacci/iterations:100 | real_time | 7.51618ms    | -1.321 (-17.5797%) | -1.417 (-18.8548%)  | -1.392 (-18.5215%)  | -1.483 (-19.7270%)
                          | cpu_time  | 7.4984ms     | -1.324 (-17.6618%) | -1.421 (-18.9530%)  | -1.407 (-18.7577%)  | -1.484 (-19.7880%)
 man_or_boy               | real_time | 0.015211ms   | -0.000 (-1.2682%)  | -0.000 (-1.2839%)   | -0.000 (-2.1590%)   | -0.000 (-1.4010%)
                          | cpu_time  | 0.0151691ms  | -0.000 (-1.2855%)  | -0.000 (-1.4859%)   | -0.000 (-2.2711%)   | -0.000 (-1.7134%)

Pas trop mal, mais j’aimerai automatiser tout ça… C’est pourquoi maintenant des benchmarks sont lancés par la CI à chaque commit sur dev. Rassurez vous, je ne regarde pas le temps, les environnements de CI étant non reproductibles, mais les ratios entre ArkScript et d’autres langages, et on peut visualiser le tout sur le site : https://arkscript-lang.dev/benchmarks.html.

Le passage en C++20

Par la suite, je me suis dit qu’il était grand temps de passer en C++20 (aussi parce que je voulais intégrer boost-ext/ut qui est en C++20), et j’ai pris sur moi de moderniser le code (ranges & views, ça rend le code bien plus clair).

En même temps, je suis finalement passé sur CLion, et je crois bien que ce fut la meilleure décision de ma vie. L’intégration avec clangd est bien meilleure que VSCode et IntelliSense, même si CLion mange la RAM comme je mange des donuts… (promis, ce post n’est pas sponso, simplement avoir un debugger qui ne me montre pas juste l’asm, ça change la vie ; et pouvoir placer des breakpoints aussi).

Le plus dur dans cette migration aura été d’adapter la CI pour avoir des compilateurs supportant le C++20 (le support de gcc < 13 et clang < 15 a donc été droppé).

Des tests, en veux tu en voilà

Ayant la tête dans la CI, je me suis aussi mis à écrire beaucoup de tests, en C++ cette fois, pour tester le parser, le bytecode reader, la VM, l’embedding VM+State, … et honnêtement c’est tout con et facile à lire, regardez moi cette simplicité :

ut::suite<"AST"> ast_suite = [] {
    using namespace ut;

    "[generate valid ast]"_test = [] {
        iter_test_files(
            "ASTSuite",
            [](TestData&& data) {
                JsonCompiler compiler(false, { ARK_TESTS_ROOT "lib/" });

                std::string json;
                should("parse " + data.stem) = [&] {
                    expect(nothrow([&] {
                        mut(compiler).feed(data.path);
                        json = mut(compiler).compile();
                    }));
                };

                should("output the expected AST for " + data.stem) = [&] {
                    expect(that % json == data.expected);
                };
            },
            /* expected_ext= */ "json");
    };
};

Pour un output très clair et un error reporting (quand ça plante, ce qui maintenant arrive rarement) sympa:

Suite 'global': all tests passed (0 asserts in 0 tests)
Suite 'BytecodeReader': all tests passed (15 asserts in 7 tests)
Suite 'AST': all tests passed (14 asserts in 15 tests)
Suite 'VM': all tests passed (18 asserts in 15 tests)
Suite 'Parser': all tests passed (50 asserts in 52 tests)
Suite '*hidden*': all tests passed (64 asserts in 32 tests)
Fuzzing

J’ai aussi continué le fuzzing, et je suis content de vous dire que je fuzz approximativement 3 fois plus vite qu’avant grâce à un meilleur parser et des optimisations bien ciblées (et toujours alimentées avant par des benchmarks et du profiling) !

Pour m’aider dans cette tâche de trier 300+ output marqués KO par les fuzzers, j’ai fait quelques scripts (triage en python, à la git add -p, lancement des fuzzers avec différents paramètres pour une meilleure couverture).

A l’heure où j’écris, il me reste ~64 erreurs (dont certaines se recoupent, d’où l’utilité du script python pour trier rapidement les fausses erreurs (aka ArkScript qui remonte une erreur) et les vraies erreurs (un sanitizer qui remonte un problème), en étant parti de 400+ erreurs en 4 heures de fuzz testing!

Grâce aux fuzzers, la base de tests s’enrichit également, car on teste aussi les erreurs et leur reporting maintenant, pour s’assurer qu’on ne fait pas réapparaitre une erreur et que tout est toujours géré correctement : https://github.com/ArkScript-lang/Ark/tree/79bfe1f596b5a17ed4a684191adf0addd3507d25/tests/errors (organisés par catégorie d’erreur, avec un fichier .ark et un .expected pour le bout de message d’erreur que l’on attend, j’en suis plutôt fier).

De meilleurs rapports de tests pour ArkScript

Je n’aimais pas avoir des assert en ArkScript, car les tests plantaient immédiatement. Il fallait les corriger un à un et le reporting était laid, puis tout relancer. Inspiré par mockito en Scala, j’ai donc conçu une lib de tests en ArkScript, pour ArkScript. Voyez donc:

(import std.Testing)

(test:suite aaa {
    (mut foobar 2)
    (test:eq 1 2 "no test:case")

    (test:case "given an explicit expected value" {
        (test:eq 1 foobar "foobar is 1")
        (test:eq 1 foobar)})

    (test:case "given a computed expected value" {
        (let c 1)
        (test:eq c foobar "foobar is equal to c")
        (test:case "nested case"
            (test:eq c foobar))})})

Ce qui reporte ceci : image.png

Exécuter du code ArkScript… en ligne?

Quoi de mieux qu’un playground pour tester le langage, au lieu de cloner le repo et compiler le projet sur sa machine?

https://playground.arkscript-lang.dev/

Il a même été intégré sur la page tutoriel du langage pour essayer immédiatement les exemples, tout en apprenant : https://arkscript-lang.dev/tutorials/language.html.

Plus d’uniformité sur le site web

Actuellement on se balade avec trois sites différents (quatre avec le playground), et ça ne me plait pas :

  • le site vitrine, /, avec des tutoriels, des guidelines, des exemples
  • la documentation C++ du projet, /impl, pour les mainteneurs et contributeurs/contributrices externes (et… la documentation des modules codés en C++?? hélas oui)
  • la documentation de la lib standard, /std
  • et le playground, évidemment

J’ai donc commencé il y a quelques mois un travail de report de toute la doc sur le site "principal" dans /, ainsi dans /impl seulement la doc de l’implémentation restera. Les tutoriels, guides d’intégration ou encore de création de modules sont tous sur le site principal ! Une seule interface, plus simple à gérer et maintenir pour moi (la page des liens : https://arkscript-lang.dev/documentation.html).

A terme, la documentation des modules sera elle absorbée via ArkDoc, notre outil de génération de documentation à partir de commentaires dans le code ArkScript (et C++, les builtins sont documentées avec le même outil ; en le modifiant un peu on devrait pouvoir documenter les modules avec aussi).

Un formateur de code

Les lecteurs et lectrices avisé.es auront peut être remarqué qu’un test était marqué *hidden* un peu plus haut… il s’agit de la suite de tests du formateur de code d’ArkScript !

C’est donc officiel, la CLI s’est doté d’un outil pouvant reformaté un fichier en place ou en dry-run (auquel cas le formatage qui se serait fait est simplement écrit dans la console). Il est très opinionated et pas du tout configurable, à la gofmt : comme ça pas de chichi, tout le monde devra fonctionner de la même manière.

Dès que je me serais assuré que celui-ci est stable et n’avale pas les commentaires par erreur, je formaterai l’ensemble du code ArkScript du projet avec (exemples, lib standard, tests…)! N’hésitez pas à le tester et à faire des retours, c’est de loin le bout de code le plus complexe du projet, malgré sa petite taille (environ 500 lignes à tout casser).

J’espère pouvoir écrire un article à ce sujet, le sujet est très intéressant et plus compliqué qu’il n’y parait (shameless plug, j’ai maintenant un blog en dehors de toute plateforme comme dev.to ou substack).

Le mot de la fin?

Petit sneak peak de tout ce que je veux encore faire sur ce projet (une tâche abattue en fait en général pousser deux, ou plus) :

le Notion d'ArkScript
le Notion d’ArkScript

Je me rends aussi compte que j’ai tendance à faire des posts très longs, j’espère qu’ils vous plaisent/plairont autant que ça me plait de les écrire ; il est vrai que poster une fois tous les 4 mois (ou plutôt 18 mois dans le cas présent) aide à la rédaction de posts aussi longs, il y a matière à discuter !

J’essayerai de revenir plus tôt la prochaine fois ;)

+7 -0

Hello Zeste de Savoir !

Pendant ces 90 derniers jours, c’est 795 fichiers qui ont été modifiés, pour 3105 insertions et 9135 suppressions (nettoyage de jeu de données pour les fuzzers), en seulement 97 commits (je suis en train de retravailler le tout dernier, on en reparle bientôt).

Quelqu’un a dit… macros?

Les macros sont bien pratiques, pour améliorer la syntaxe, créer des DSL comme celui utilisé pour les tests:

(test:suite list {
    (test:case "append and return a new list" {
        (test:eq (append a 4) [1 2 3 4])
        (test:eq a [1 2 3])
        (test:eq (append a a) [1 2 3 [1 2 3]])
        (test:eq a [1 2 3]) })})

Sauf qu’un besoin est vite apparu, étant donné que test:suite, test:case et test:eq (et dérivés) sont des macros, tous leurs arguments se retrouvent évalués (si possible) par le macro processor. Y compris un (@ (list 1 2 3) 0) qui retournerait… le builtin list et non 1.

On manque d’un système de quoting / unquoting / quasiquoting ! Et c’est un assez gros impact sur la syntaxe, que je ne suis pas encore prêt à faire, souhaitant garder le langage le plus simple possible. Une solution intermédiaire fut d’introduire une nouvelle macro qui stoppe l’évaluation dans une macro, en prenant un bloc de code et en le collant tel quel dans le code: ($paste node).

Tous les arguments de test:eq sont donc passés dans des $paste pour éviter une évaluation non souhaitée, et tout va pour le mieux !

Le nom $paste est provisoire, je n’ai pas trouvé mieux pour le moment. N’hésitez pas si vous avez des suggestions.

Tooling, en veux tu en voilà

Développer un langage, c’est bien. S’assurer qu’il fonctionne comme convenu à tout instant, c’est mieux. Pour ça, on a des tests, mais on peut faire bien plus que ça ! Comme des benchmarks pour traquer les régressions de performances, et du fuzzing pour de la détection automatique de bugs. On peut même guider l’écriture de tests avec des analyses du code, comme du coverage. Vous vous en doutez peut être, j’ai fait les 3 (4 si on compte les tests).

Je vous avais déjà parlé des benchmarks et de mon script pour calculer les différences en perf entre différent commit, maintenant on mesure aussi les performances du parser et du compilateur.

New parser - Simple - 39 nodes/0      0.045 ms        0.045 ms        15467 nodesAvg=92.802k nodesRate=132.546k/s uselessLines/sec=287.183k/s
New parser - Medium - 83 nodes/1      0.130 ms        0.129 ms         5405 nodesAvg=54.05k nodesRate=77.3058k/s uselessLines/sec=193.265k/s
New parser - Big - 665 nodes/2         1.74 ms         1.73 ms          405 nodesAvg=7.695k nodesRate=10.958k/s uselessLines/sec=183.402k/s
Welder - Simple - 39 nodes/0          0.164 ms        0.164 ms         4207
Welder - Medium - 83 nodes/1          0.309 ms        0.308 ms         2249
Welder - Big - 665 nodes/2             5.33 ms         5.32 ms          130

Si on compare avec les performances du parser dans le post précédent, on reste dans l’attendu, quelques variations légères mais rien de catastrophique. Les nouvelles statistiques sont celles du welder, qui chaine le parser, import solver, macro processor, ast optimizer, name and scope resolver et compilateur. Six différentes passes une à une, pas trop mauvais en soit de pouvoir compiler un fichier de 319 lignes et 665 noeuds en 5ms!

Petit apparté sur le compilateur

Les plus attentifs auront remarqué deux choses : "name and scope resolver" et "six différentes passes".

Il y a peu j’ai découpé le compilateur en une suite de passes, qui prennent un AST et sortent un AST (hormis pour le parser qui prend un fichier et sort un AST, et le compilateur qui prend un AST et sort du bytecode), et une nouvelle est apparue, sortie du compilateur : la résolution de nom et de scopes.

Elle s’occupe de vérifier que l’on utilise que des variables déclarées et valides (on ne peut pas nommer une variable "print" ou tout autre builtin), et propose des alternatives dans les messages d’erreur si besoin (eg fog -> foo?) en se basant sur les variables existantes et une distance de Leveinshtein. La résolution des scopes, peut détecter des variables dites unbound, qui existent plus tard et sont référencées avant leur utilisation.

Encore du tooling

ArkScript a enfin du coverage ! Ça semble tout simple, et ça l’est quand on comprend comment le mettre en place. Le plus dur du travail a été de consolider les tests en un seul exécutable pour pouvoir faire le coverage en coup. Sans ça il faut s’amuser à joindre des fichiers .gcda et/ou .gcno de ce que j’ai compris, et je ne voulais pas m’ennuyer avec ça (en plus ça simplifie énormément le lancement des tests, plus besoin de script bash compliqué, un seul target unittests qui lance toutes les suites).

./unittests
./unittests

Deux nouvelles suites pour ce faire : Lang (les unittests en ArkScript), et Diagnostics (la validation des messages d’erreur, découpés en deux catégories : runtime et compile time).

Pour un coverage actuel de 73%. Not bad, mais peut mieux faire. Dans les jours et semaines à venir je vais ajouter des tests pour augmenter la couverture de tests, bien que 100% de coverage ne veuille pas dire qu’on a pas de bug, juste que le code est exécuté (les 100% de coverage ne sont pas un but, ni atteignable ni souhaitable).

Formatons du code

Le formatteur de code ArkScript étant devenu plus stable, on a maintenant une action github pour vérifier que du code est formatté correctement dans la CI.

Toujours moins de.. bugs?

Une bonne nouvelle, c’est que grace au fuzzing j’ai pu corriger moulte bugs :

  • des crash dans le macro processor dû à une évaluation récursive sans borne,
  • des caractères utf8 invalides lu par le parser qui généraient des crash,
  • des futures qu’on ne pouvait pas await plusieurs fois,
  • et pas mal d’autres que vous pouvez retrouver ici : Changelog.md, pour un total de 25 bugs déjà corrigés, qui ont maintenant leur test dédié pour éviter les régressions

Le mot de la fin?

Je suis en train de travailler sur une petite optimisation (guidé par de nombreux benchmarks et beaucoup de scepticisme), du computed goto dans la machine virtuelle. Pour le moment, j’observe un gain de 10% de performance sur les tests les plus gourmands, toujours sur la même machine (MacBook Pro M1, 8 coeurs).

Tâches enregistrées
Tâches enregistrées

A l’heure actuelle, 75 tâches sont réalisées, 3 en cours, 19 ont besoin de plus de détails (toujours à l’état d’idée) et 33 sont à faire (et 5 ont été abandonnées). Une tâche de faite en fait naitre environ 1 ou 2 autres, mais on y arrive petit à petit.

Si tout continu tel quel, on se revoit le 24 décembre pour la prochaine update. Sinon, bonne fin d’année et à 2025 !

PS: j’ai traduit les messages que j’ai posté dans ce post sur mon blog: https://lexp.lt/categories/arkscript/

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