Révision courante
Rc
/Weak
).Vitte Light (abrégé VITL) est une déclinaison minimaliste du langage Vitte. L’objectif : offrir une syntaxe cohérente, des outils intégrés (formatage, analyse statique, tests, génération de doc) et un modèle mémoire prévisible, afin que débutants, développeurs intermédiaires et professionnels soient vite productifs — que ce soit pour un script d’atelier, un petit binaire livré à un client, ou un module embarqué via FFI dans une base C existante.
Result
, match
exhaustif).
unsafe
hors FFI, contrôle des bornes,
messages d’erreurs guidants.La majorité des tâches quotidiennes (scripts, outils CLI, glue entre systèmes) ne nécessitent ni threads lourds ni runtime sophistiqué. VITL couvre les “80 %” de besoins fréquents, sans la complexité d’un écosystème massif.
Quand il faut “sortir du cadre” (concurrence native, libs spécialisées, frameworks complets), on isole la partie critique en externe (C, etc.) et on relie via FFI.
Débutant
Result
, match
, types/conversions,
lecture/écriture
de fichiers.Développeur intermédiaire
fmt
), de l’analyse (check
) et des tests
(test
).Professionnel
-O3
) et symboles de debug
(-g
).unsafe
proprement.///
.src/main.vitl
avec un “Hello” minimal.vitl run
pour tester en VM.vitl build
et exécuter le binaire généré.Pas de projet à “initialiser”, pas de configuration cachée : un fichier, une commande, c’est tout.
println
, lecture d’un fichier, codes de
sortie
(0/1/2/3).Result
+ ?
, match
exhaustif,
conversions
numériques et chaînes.struct
), méthodes (impl
), petits tests
avec
test "nom"
.
.vitbc
ou FFI direct.
Si votre cœur de besoin est un framework web complet, une UI riche multi-plateforme, ou une concurrence massive multi-threads, vous serez plus efficace en déléguant ces aspects à un composant externe (C/C++/Rust/Go) — VITL reste un excellent ciment autour.
Rc
/Weak
pour la majorité des
cas, sans
GC complexe.Result
+ ?
pour propager simplement,
match
exhaustif.
fmt
, check
, test
,
doc
, IR/bytecode (--emit-*
).
-O3
.
Créez src/main.vitl
et collez :
module app.main
import std.io
fn main() -> i32 {
std.io::println("Bonjour Vitte Light")
return 0
}
vitl run src/main.vitl
vitl build -O2 -o build/app src/main.vitl
./build/app
(Linux/macOS) ou build\\app.exe
(Windows)module app.main
import std.{io, cli, str}
fn main() -> i32 {
let argv = cli::args()
if str::len(argv) < 2 { io::eprintln("usage: app <nom>"); return 2 }
io::println("Bonjour " + argv[1])
return 0
}
# Formater
vitl fmt src/
# Analyser
vitl check src/
# Lancer les tests (dans les fichiers)
vitl test
# Générer doc depuis ///-commentaires
vitl doc src/ -o build/docs.txt
Téléchargez l’exécutable adapté à votre OS, placez-le dans votre PATH
et vérifiez :
vitl --version
. Pour les environnements verrouillés, vous pouvez l’installer localement
dans le
projet
(scripts CI/CD) et l’appeler via un chemin relatif.
chmod +x
), copiez-le dans
/usr/local/bin
ou ajoutez un dossier bin/
à votre PATH
.
/usr/local/bin
ou
~/.local/bin
et ajoutez-le à PATH
dans votre shell.
vitl.exe
dans un dossier référencé par Variables
d’environnement → PATH.Astuce : en CI, mettez en cache le binaire VITL pour accélérer vos pipelines.
mon_projet/
src/
main.vitl # module app.main
build/ # artefacts (binaire, docs, bytecode)
Un fichier = un module. Le nom du module correspond à son chemin disque (ex.
src/util/str.vitl
→
module util.str
).
Importez explicitement ce que vous utilisez (import std.io
, import util.str
).
VITL a-t-il un ramasse-miettes ? Non, la mémoire est gérée par
Rc
/Weak
: quand plus rien ne référence un objet, il est libéré.
Et les threads ? Pas de threads natifs dans la version Light ; utilisez des processus externes ou une lib C via FFI si c’est crucial.
Puis-je faire du réseau / des sockets ? Oui via la stdlib et/ou via FFI pour des stacks spécialisées.
Comment gérer les erreurs proprement ? Utilisez Result<T,E>
et
propagez
avec ?
; couvrez les cas avec match
.
.vitbc
produit à la
volée.unsafe
dans
des
zones confinées).Result
: type somme Ok(T)
/Err(E)
pour
modéliser
succès/échec.match
: sélection de motifs obligeant à couvrir tous les cas (donc
moins
d’implicite).
Explorez 1) Structure de projet pour organiser vos fichiers, puis
3) Syntaxe pour ancrer les bases (variables, match
,
Result
).
Besoin de livrer ? Lisez 2) Outils en ligne de commande.
Intégration C ? Voir 7) FFI. Enfin, gardez un œil sur 11)
Limites
pour comprendre
les compromis qui gardent VITL rapide et prévisible.
Trois dossiers suffisent : /src
(code VITL), /libs
(C/FFI),
/build
(artefacts). Cette séparation garde les sources propres, facilite les revues et les
pipelines CI.
Une structure claire évite des heures perdues : on sait où créer un fichier, où trouver une
fonctionnalité,
où tombent les binaires.
Cette section propose une arborescence de base puis des variantes (simple → complexe), des
conventions
de nommage,
des exemples copier/coller (.gitignore
, tâches build, CI), et des intégrations C
(CMake/Meson).
/src → code source principal (.vitl)
/libs → bibliothèques natives C/FFI (.c, .so, .dll, .a)
/build → binaires, artefacts intermédiaires, bytecode (.vitbc)
mon_projet/
src/
main.vitl
util/str.vitl
libs/
mylib.c
mylib.a
build/
app (binaire natif)
app.vitbc (bytecode)
snake.case
pour modules/fichiers, CamelCase
pour types.Fichier : src/std/io.vitl
Contenu : module std.io
Fichier : src/app/http/client.vitl
Contenu : module app.http.client
Imports clairs : import std.io
, import util.str
. Évite les conflits, favorise
la
réutilisation.
Quand le chemin et le module correspondent, on sait toujours où vit une fonction. Plus simple pour les revues, la navigation IDE et la CI.
mon_projet/
src/main.vitl
src/util/print.vitl
build/
Objectif : aller vite. Tests intégrés au bas des fichiers.
mon_projet/
src/app/main.vitl
src/app/cli/args.vitl
src/std/extra/time.vitl
src/util/fs.vitl
libs/fast.c
build/{debug,release,docs}
On sépare app/
, util/
, std/
local. Dossiers
debug/
et
release/
dans build/
.
mon_projet/
src/
app/main.vitl
app/sub/feature.vitl
core/error.vitl
core/config.vitl
net/http.vitl
net/tcp.vitl
std/extra/...
tests/
test_core.vitl
test_net.vitl
libs/
sqlite/sqlite3.c
wrapper/foo.c
build/
debug/
release/
docs/
scripts/
build.sh
release.sh
docs/
README.md
CHANGELOG.md
Tests isolés dans /tests
, scripts utilitaires dans /scripts
, documentation
dédiée
dans /docs
.
/src
: code VITL uniquement (modules, types, fonctions, tests intégrés)./libs
: code C/C++ enveloppé côté C (shim) pour exposer une API C stable.
/build
: non versionné ; binaires, .vitbc
, docs générées, artefacts
temporaires./tests
(optionnel) : scénarios et jeux de données./scripts
(optionnel) : tâches build/release reproductibles./docs
(optionnel) : guides, captures, changelog, spécifications.snake.case
(net/http.vitl
→
module net.http
).
CamelCase
(struct HttpClient
).snake_case
(fn read_config()
).test "nom explicite du comportement"
.\\
ou /
: centralisez la logique de chemins si
nécessaire.tests/data/
et adressez-les via std.fs
depuis la
racine./tests
.
src/core/math.vitl
... code ...
test "somme de base" { assert(add(1,2) == 3) }
tests/test_math.vitl
... scénarios plus riches, I/O, golden files ...
Documentez vos APIs avec ///
en tête d’éléments publics, puis générez avec
vitl doc src/ -o build/docs.txt
. Conservez les docs “métier” plus longues dans
/docs
.
/build/
debug/ → binaires -O0 -g (débogage)
release/ → binaires -O3 (livraison)
docs/ → doc générée (ex. docs.txt)
*.vitbc → bytecode (exécution VM)
# VITL
/build/
*.vitbc
# OS
.DS_Store
Thumbs.db
# Éditeurs/IDE
.vscode/
.idea/
*.iml
# Logs/temp
*.log
.tmp/
.cache/
# scripts/build.sh
set -euo pipefail
mkdir -p build/release
vitl fmt src/
vitl check src/
vitl build -O3 -o build/release/app src/app/main.vitl
echo "OK → build/release/app"
# scripts/build.ps1
$ErrorActionPreference = "Stop"
New-Item -ItemType Directory -Force -Path build/release | Out-Null
vitl fmt src/
vitl check src/
vitl build -O3 -o build/release/app.exe src/app/main.vitl
Write-Host "OK → build/release/app.exe"
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install VITL
run: |
mkdir -p ~/bin
curl -L <URL_VITL_LINUX> -o ~/bin/vitl
chmod +x ~/bin/vitl
echo "$HOME/bin" >> $GITHUB_PATH
- name: Format & Check
run: |
vitl fmt src/
vitl check src/
- name: Tests
run: vitl test
- name: Build (release)
run: vitl build -O3 -o build/release/app src/app/main.vitl
- name: Upload artefacts
uses: actions/upload-artifact@v4
with:
name: app-linux
path: build/release/app
Adaptez l’étape “Install VITL” à votre source de binaire (URL/paquet interne).
# libs/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(vitl_ffi C)
add_library(foo STATIC foo.c)
target_include_directories(foo PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
# Construire la lib puis lier côté VITL
mkdir -p libs/build && cd libs/build
cmake ..
cmake --build . --config Release
# Ensuite, côté VITL :
vitl build -O2 -L libs/build -lfoo -o build/app src/main.vitl
# libs/meson.build
project('vitl_ffi', 'c')
libfoo = static_library('foo', 'foo.c', include_directories: include_directories('.'))
# Build Meson/Ninja
meson setup libs/build libs
meson compile -C libs/build
# Puis lier lors du build VITL :
vitl build -O2 -L libs/build -lfoo -o build/app src/main.vitl
release/
(-O3
) pour chaque OS visé.LICENSE
, CHANGELOG.md
et un hash (SHA256) des binaires.app
+ fichiers d’exemple (examples/
).CHANGELOG.md
(Keep a Changelog : Added/Changed/Fixed/Removed).README.md
: but, installation, quick start, table des commandes.# Mon Projet VITL
But : une ligne claire ici.
## Installation
- Télécharger le binaire.
- Ajouter au PATH.
## Utilisation
\`\`\`sh
vitl --help
\`\`\`
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
vitl run <fichier.vitl> [-- args...]
— compile en bytecode puis exécute dans la
VM
(idéal pour apprendre, prototyper, CI rapide).vitl build [options] -o <sortie> <fichier.vitl>
— produit un exécutable
natif
(livraison, perf).vitl fmt <chemin>
— formateur de code (style standard).vitl check <chemin>
— analyse statique (imports, types, diagnostics rapides).
vitl test
— exécute tous les blocs test "…"{…}
rencontrés.vitl doc <chemin> -o <fichier>
— génère la doc à partir des commentaires
///
.
# Lancer un fichier
vitl run src/main.vitl
# Passer des arguments utilisateur au programme (utilisez -- pour séparer)
vitl run src/cat.vitl -- README.md
# Exécuter plusieurs fichiers (point d’entrée = premier fichier avec fn main)
vitl run src/app/main.vitl src/util/str.vitl
# Build simple (optimisation par défaut -O2)
vitl build -o build/app src/main.vitl
# Profils typiques
vitl build -O0 -g -o build/debug/app src/main.vitl # Débogage (rapide à compiler)
vitl build -O3 -o build/release/app src/main.vitl # Livraison (perf max)
# Émettre des artefacts intermédiaires
vitl build --emit-ir -o build/app src/main.vitl # IR lisible
vitl build --emit-bytecode -o build/app src/main.vitl # Bytecode VM (.vitbc)
-O0 -g
en dev pour des builds rapides + backtraces lisibles.-O3
, mesurez, puis ajustez (I/O, allocations, FFI).# Formater tout le code (à intégrer en pre-commit)
vitl fmt src/
# Analyse statique (imports manquants, types, règles de base)
vitl check src/
# Tests intégrés (cherche et exécute tous les blocs `test "..." {}`)
vitl test
# Documentation à partir des commentaires /// en tête d’API publiques
vitl doc src/ -o build/docs.txt
# Fichiers de shim C compilés séparément dans libs/
# Lier une lib partagée ou statique pendant le build
vitl build -O2 -L libs -lfoo -o build/app src/main.vitl
# Définir un rpath pour retrouver les .so/.dylib à l’exécution (Linux/macOS)
vitl build -O2 -L libs -lfoo -Wl,-rpath,./libs -o build/app src/main.vitl
libfoo.{so,dylib,a}
.# Hello World (VM puis binaire)
vitl run src/main.vitl
vitl build -O2 -o build/hello src/main.vitl
./build/hello
# Programme CLI avec arguments et codes d’erreur
vitl run src/cat.vitl -- data.txt # 0 si succès, 2 usage, 3 I/O
vitl build -O2 -o build/cat src/cat.vitl
./build/cat data.txt
echo $? # vérifie le code de sortie
# Pipeline CI local minimal (bash)
set -euo pipefail
vitl fmt src/
vitl check src/
vitl test
vitl build -O3 -o build/release/app src/app/main.vitl
-g
pour obtenir des backtraces plus utiles.vitl run
), les messages d’erreur incluent le module et la ligne fautive.--emit-ir
/--emit-bytecode
pour comprendre ce que “voit” le
compilateur.mkdir -p build/{debug,release,docs}
vitl build -O0 -g -o build/debug/app src/app/main.vitl
vitl build -O3 -o build/release/app src/app/main.vitl
vitl doc src/ -o build/docs/api.txt
vitl run
+ vitl fmt
. Gardez un seul fichier
main.vitl
au début.
vitl check
, factorisez en modules, commencez à
tester (vitl test
).chmod +x build/app
, exécuter ./build/app
.build\\app.exe
directement.-o
après build
:
vitl build -O2 -o build/app src/main.vitl
.
--
:
vitl run file.vitl -- arg1
.
run
: exécuter via VM — rapide à itérer.build
: binaire natif — déploiement/perf.fmt
: style cohérent — revues plus simples.check
: diagnostics — trouve tôt les erreurs.test
: qualité — régression évitée.doc
: API — autodoc depuis ///
.-O0|-O1|-O2|-O3
, -g
, -o
,
--emit-ir
,
--emit-bytecode
, -L
, -lX
, -Wl,opts
.
Résumé — Utilisez run
pour explorer, build
pour
livrer. Automatisez fmt
/check
/test
en CI. Inspectez avec
--emit-ir
/--emit-bytecode
. Pour le FFI, liez proprement avec
-L
/-l
et un shim C clair.
Un programme VITL est une suite d’instructions lues de haut en bas. Vous donnez des
noms à des valeurs (variables), vous définissez des fonctions (recettes
réutilisables),
vous contrôlez l’ordre d’exécution (si/alors, boucles), et vous gérez les erreurs
proprement (Result
et ?
).
mut
si la valeur doit
changer.Result
(succès/échec) et se propagent avec
?
.
match
oblige à traiter tous les cas possibles (moins de surprises).src/util/str.vitl
→ module util.str
.str
(vue immuable, légère) vs String
(possédée,
mutable).src/play.vitl
avec le contenu ci-dessous.vitl run src/play.vitl
et observez la sortie.module app.play
import std.io
fn main() -> i32 {
let msg = "Bonjour" // immuable
let mut n = 1 // mutable
n = n + 1
std.io::println(msg + " n=" + n.to_string())
return 0
}
// Sortie attendue : Bonjour n=2
Les commentaires expliquent pourquoi (pas juste “ce que ça fait”). Les commentaires de doc
(///
) apparaissent dans vitl doc
.
// Ligne unique
/* Bloc multi-lignes */
/// Additionne deux entiers (sans débordement contrôlé)
fn add(a:i32, b:i32) -> i32 { return a + b }
snake_case
, types en CamelCase
.max_retries
à mr
.let user_name = "Alice"
struct Point2D { x:f64, y:f64 }
Pas besoin de tout retenir : l’éditeur et les erreurs vous guident.
i32
, i64
(ex. 42
, 0xFF
,
0b1010
)
f64
(ex. 3.14
, 2.0e-3
)true
, false
'a'
, '\n'
"..."
ou brutes r"..."
const PI:f64 = 3.14159
let ok:bool = true
let hex:i32 = 0xCAFE
Immuable par défaut = code plus sûr. Ajoutez mut
uniquement où nécessaire.
const MAX_RETRY:i32 = 3 // constante
let count:i32 = 0 // immuable
let mut total:f64 = 0.0 // mutable
total = total + 1.5
// Inférence de type quand c'est clair
let message = "Salut" // :str
VITL évite les conversions implicites “magiques”. Convertissez avec as
(numérique) ou
to_string()
(texte).
let n:i32 = 3
let x:f64 = 0.5
let y = (n as f64) + x // cast requis
std.io::println("n=" + n.to_string()) // convertir avant concaténation
str
vs String
(images mentales)str
: “post-it” vers un texte (on ne le modifie pas).String
: “carnet” qui vous appartient (on peut écrire dedans).let s:str = "abc"
let mut buf = String::from("abc")
buf.push("d") // "abcd"
std.io::println(buf)
Les indices sont vérifiés : une erreur claire vaut mieux qu’un crash silencieux.
let mut v:[i32] = [1,2,3]
v.push(4)
for x in &v { std.io::println(x.to_string()) }
let third = v[2] // panique si hors bornes → message explicite
// if / else
if y > 0.0 { std.io::println("positif") } else { std.io::println("non positif") }
// while
let mut i = 0
while i < 3 { std.io::println(i.to_string()); i = i + 1 }
// for + intervalles
for k in 0..3 { std.io::println(k.to_string()) } // 0,1,2
for k in 0..=3 { std.io::println(k.to_string()) } // 0..3 inclus
// break / continue
for n in 0..10 {
if n == 5 { break }
if n % 2 == 0 { continue }
std.io::println(n.to_string())
}
Une fonction prend des entrées, produit des sorties, et peut signaler un échec avec
Result
.
VITL n’infère pas le type de retour : écrivez-le.
// Paramètres nommés et type de retour explicite
fn square(x:i32) -> i32 { return x * x }
// Valeur ou erreur (Result)
fn div(a:i32, b:i32) -> Result<i32, str> {
if b == 0 { return Result::Err("division par zéro") }
return Result::Ok(a / b)
}
// Passer par référence pour éviter une copie lourde
fn print_len(s:&String) -> () {
std.io::println(s.len().to_string())
}
Gardez des fonctions courtes qui font une seule chose ; préférez retourner Result
plutôt que d’appeler panic()
.
Result
, Option
et opérateur ?
Result<T,E>
représente un succès (Ok(T)
) ou un échec (Err(E)
).
L’opérateur ?
“bulle”
l’erreur au dessus : s’il y a Err
, la fonction courante s’arrête et renvoie cette erreur.
// I/O : propage l’erreur si le fichier est introuvable/illisible
fn load(path:str) -> Result<String, str> {
let txt = std.fs::read_to_string(path)? // <— propage
return Result::Ok(txt)
}
// Valeur optionnelle (présence/absence)
match std.cli::env("HOME") {
Some(h) => std.io::println(h),
None => std.io::eprintln("HOME non défini"),
}
match
exhaustif & enumsmatch
impose de couvrir tous les cas (soit explicitement, soit via _
pour “autre”).
enum Status { Ok, Err(str) }
fn report(s:Status) -> () {
match s {
Status::Ok => std.io::println("ok"),
Status::Err(e) => std.io::eprintln("erreur:" + e),
}
}
impl
) et récepteurs &self
Les méthodes se définissent dans un bloc impl
. &self
signifie “emprunter
l’objet
sans le copier”.
struct Vec2 { x:f64, y:f64 }
impl Vec2 {
fn norm(&self) -> f64 { return (self.x*self.x + self.y*self.y).sqrt() }
fn dot(&self, o:Vec2) -> f64 { return self.x*o.x + self.y*o.y }
}
fn main() -> i32 {
let v = Vec2 { x:3.0, y:4.0 }
std.io::println(v.norm().to_string()) // 5
return 0
}
Le chemin disque détermine le nom de module. Gardez cette règle et vous saurez toujours “où vit quoi”.
// Fichier : src/util/str.vitl
module util.str
// ... API ...
// Fichier : src/app/main.vitl
module app.main
import std.{io, fs}
import util.str
fn main()->i32 {
std.io::println("Ready")
return 0
}
La version Light supporte des génériques “légers” pour des cas typiques (fonctions d’ordre supérieur, conteneurs simples).
// Exécuter une fonction et mesurer son temps
fn timed<F>(label:str, f:F) -> ()
where F: fn()->() {
let t0 = std.time::now()
f()
std.io::println(label + ":" + (std.time::now()-t0).to_string() + " ms")
}
// Console
std.io::print("sans retour")
std.io::println("avec retour")
std.io::eprintln("erreur: usage invalide")
// Fichiers
_ = std.fs::write_string("out.txt", "ok\n")?
let txt = std.fs::read_to_string("out.txt")?
Result
+ ?
à des codes sentinelles.// Mauvais
module app.main
fn main()->i32 { std.io::pritnln("hi"); return 0 }
// Correct
module app.main
import std.io
fn main()->i32 { std.io::println("hi"); return 0 }
// Mauvais
let n:i32 = 3
std.io::println("n=" + n)
// Correct
std.io::println("n=" + n.to_string())
String
, ajoutez-y trois morceaux (push
),
affichez le résultat.Vec2
, ajoutez une méthode
scale(&mut self, k:f64)
, testez-la.
let
/let mut
· const
· fn
· struct
·
enum
· impl
if
/ while
/ for
avec 0..N
et 0..=N
Result
+ ?
· Absence : Option
std.io
· Fichiers : std.fs
· Chaînes : std.str
Écrire, exécuter, compiler, tester et diagnostiquer des programmes VITL pas à pas. Chaque exemple inclut : le code, la commande pour l’exécuter, la sortie attendue, des variantes, des tests et les erreurs fréquentes.
vitte-light-demo/
).src/
et build/
.src/
.Commandes clés : vitl run <fichier.vitl>
(exécution rapide) ·
vitl build -O2 -o build/app <fichier.vitl>
(binaire natif) ·
vitl fmt src/
(formateur) · vitl test
(tests intégrés).
// src/main.vitl
module app.main
import std.io
fn main() -> i32 {
let msg = "Bonjour Vitte Light"
let mut n = 1
n = n + 1
std.io::println(msg + " · n=" + n.to_string())
return 0
}
Exécution : vitl run src/main.vitl
Sortie attendue : Bonjour Vitte Light · n=2
module app.main
).mut
pour modifier..to_string()
.import std.io
ou faute de
frappe
(pritnln
)..to_string()
.
// A) Bonjour personnalisé via variable
module app.hello
import std.io
fn main()->i32 {
let name = "Alice"
std.io::println("Bonjour, " + name)
return 0
}
// B) Codes de sortie (1 = erreur générique)
module app.exit
import std.io
fn main()->i32 {
if 2 + 2 != 4 {
std.io::eprintln("arithmétique cassée")
return 1
}
return 0
}
// Ajoutez à la fin de src/main.vitl :
test "arithmétique de base" { assert(2 + 2 == 4) }
Lancez : vitl test
→ affichera les tests réussis.
// src/cat.vitl
module app.cat
import std.{fs, io, cli, str}
fn main() -> i32 {
let argv = cli::args()
if str::len(argv) < 2 {
io::eprintln("usage: cat <fichier>")
return 2 // erreur d'usage CLI
}
let path = argv[1]
if !fs::exists(path) {
io::eprintln("introuvable: " + path)
return 3 // erreur I/O
}
let contenu = fs::read_to_string(path)? // propage l'erreur I/O si échec
io::print(contenu)
return 0
}
Exécution : vitl run src/cat.vitl -- data.txt
// A) Taille en octets
module app.size
import std.{fs, io}
fn main()->i32 {
let bytes = fs::read("data.bin")?
io::println("taille=" + bytes.len().to_string() + " octets")
return 0
}
// B) Multi-fichiers (ignore les manquants)
module app.cat_multi
import std.{fs, io, cli, str}
fn main()->i32 {
let a = cli::args()
if str::len(a) < 2 { io::eprintln("usage: cat_multi <f1> [<f2>...]"); return 2 }
for i in 1..str::len(a) {
let p = a[i]
if !fs::exists(p) { io::eprintln("skip: " + p); continue }
io::print(fs::read_to_string(p)?)
}
return 0
}
// C) Chronométrage simple
module app.timed_cat
import std.{fs, io, cli, str, time}
fn main()->i32 {
let a = cli::args()
if str::len(a) != 2 { io::eprintln("usage: timed_cat <f>"); return 2 }
let t0 = time::now()
_ = fs::read_to_string(a[1])?
io::println("ms=" + (time::now()-t0).to_string())
return 0
}
let path = argv[1]
if !fs::exists(path) {
io::eprintln("E2001: fichier introuvable → " + path)
io::eprintln("Astuce: lancez depuis la racine du projet ou passez un chemin absolu.")
return 3
}
// Exemple de test "attendu en erreur"
test "cat sans argument renvoie 2" {
// pseudo-test illustratif : dans une vraie CI, on lance le binaire et on vérifie le code
assert(2 == 2)
}
// src/geom.vitl
module app.geom
import std.{io, math}
struct Vec2 { x: f64, y: f64 }
impl Vec2 {
fn norm(&self) -> f64 { return (self.x*self.x + self.y*self.y).sqrt() }
fn dot(&self, o: Vec2) -> f64 { return self.x*o.x + self.y*o.y }
}
fn main() -> i32 {
let v = Vec2 { x: 3.0, y: 4.0 }
io::println(v.norm().to_string()) // 5
return 0
}
// Tests intégrés
test "norme 3-4-5" {
let v = Vec2 { x:3.0, y:4.0 }
assert(v.norm() == 5.0)
}
test "produit scalaire orthogonal" {
let a = Vec2 { x:1.0, y:0.0 }
let b = Vec2 { x:0.0, y:1.0 }
assert(a.dot(b) == 0.0)
}
Exécution : vitl run src/geom.vitl
→ affiche 5
Tests : vitl test
// src/geom_cli.vitl
module app.geom_cli
import std.{io, cli, str}
struct Vec2 { x:f64, y:f64 }
impl Vec2 { fn norm(&self)->f64 { return (self.x*self.x + self.y*self.y).sqrt() } }
fn parse_f64(s:str) -> Result<f64,str> { return str::to_float(s) }
fn main()->i32 {
let a = cli::args()
if str::len(a) != 3 { io::eprintln("usage: geom_cli <x> <y>"); return 2 }
let x = parse_f64(a[1])?
let y = parse_f64(a[2])?
let v = Vec2 { x:x, y:y }
io::println(v.norm().to_string())
return 0
}
// src/greeter.vitl
module app.greeter
import std.{io, cli, str}
fn main()->i32 {
let a = cli::args()
if str::len(a) != 2 { io::eprintln("usage: greeter <nom>"); return 2 }
let name = a[1]
if str::len(name) == 0 { io::eprintln("nom vide"); return 2 }
io::println("Bonjour, " + name)
return 0
}
Test manuel :
vitl run src/greeter.vitl -- Alice
→ Bonjour, Alice
// src/wc_lines.vitl
module app.wc_lines
import std.{io, fs, cli, str}
fn main()->i32 {
let a = cli::args()
if str::len(a) != 2 { io::eprintln("usage: wc_lines <fichier>"); return 2 }
let p = a[1]
if !fs::exists(p) { io::eprintln("introuvable: " + p); return 3 }
let txt = fs::read_to_string(p)?
// Compte naïf des lignes : nombre de '\n' + 1 si non vide
let mut n:i64 = 0
for ch in &txt {
if ch == '\n' { n = n + 1 }
}
if str::len(txt) > 0 && txt[str::len(txt)-1] != '\n' { n = n + 1 }
io::println(n.to_string())
return 0
}
// src/bench.vitl
module app.bench
import std.{io, time}
fn heavy()->() {
let mut s:i64 = 0
for i in 0..100000 { s = s + i }
io::println("acc=" + s.to_string())
}
fn main()->i32 {
let t0 = time::now()
heavy()
io::println("ms=" + (time::now() - t0).to_string())
return 0
}
// src/config_load.vitl
module app.config_load
import std.{io, fs, str}
fn load_threshold(path:str) -> Result<i64, str> {
if !fs::exists(path) { return Result::Err("absent: " + path) }
let s = fs::read_to_string(path)?
// Nettoie un éventuel retour chariot / fin de ligne
let cleaned = str::replace(str::replace(s, "\r", ""), "\n", "")
return str::to_int(cleaned)
}
fn main()->i32 {
match load_threshold("threshold.txt") {
Result::Ok(v) => { io::println("seuil=" + v.to_string()); return 0 }
Result::Err(e) => { io::eprintln("E: " + e); return 3 }
}
}
mon_projet/
src/
main.vitl // point d'entrée
greeter.vitl // sous-outil
wc_lines.vitl // sous-outil
build/ // binaires
// src/main.vitl
module app.main
import std.io
fn main()->i32 {
std.io::println("Projet prêt. Voir src/greeter.vitl et src/wc_lines.vitl")
return 0
}
Formatez : vitl fmt src/
· Testez : vitl test
·
Compilez : vitl build -O2 -o build/app src/main.vitl
main.vitl
exécutable en une commande.vitl fmt
) exécuté.vitl run
, livrez rapide avec vitl build
.?
, imprimez des messages utiles.références comptées
: Rc<T>
.
Weak<T>
pour le lien de retour.
unsafe
et
emballé
derrière une API sûre.Un programme manipule des valeurs (nombres, textes, tableaux…). Ces valeurs occupent de la mémoire. Deux grands risques : utiliser de la mémoire libérée (crashes, comportements bizarres) et oublier de libérer (fuites). VITL vous aide : il libère automatiquement quand plus personne n’utilise la donnée (comptage de références), et il encadre les appels « bas niveau » via FFI C.
i32
ou une
String
locale).
Rc<T>
.
Weak<T>
.
CString
et on
respecte
qui alloue/libère.Rc<T>
import std.io
fn main() -> i32 {
let a = Rc<i32>::new(42) // 1 détenteur fort
let b = a.clone() // 2 détenteurs forts
std.io::println("ok: " + (*b).to_string())
a.drop() // 1 détenteur fort
b.drop() // 0 → libération automatique
return 0
}
Quand utiliser : configuration partagée, cache simple, nœuds d’arbre, tables consultées par plusieurs modules.
Rc
« par réflexe ». Une valeur locale simple suffit la plupart du temps.
Rc
tous les deux).
Weak<T>
pour briser les cyclesExemple classique : un parent garde ses enfants (fort), chaque enfant garde un lien vers son parent (faible).
// src/tree.vitl
module app.tree
import std.{io, str}
struct Node {
name: String,
parent: Weak<Node>, // lien FAIBLE
children: [Rc<Node>], // liens FORTS
}
fn Node::new(name:str) -> Rc<Node> {
return Rc<Node>::new(Node { name: String::from(name), parent: Weak<Node>::new(), children: [] })
}
fn Node::add_child(this:&Rc<Node>, child:Rc<Node>) -> () {
// le parent garde fort
this.children.push(child.clone())
// l'enfant garde FAIBLE vers le parent
let w = Rc::downgrade(this) // Rc -> Weak
child.parent.set(w) // (API imagée : associer le Weak)
}
fn main()->i32 {
let root = Node::new("root")
let a = Node::new("A")
Node::add_child(&root, a)
io::println("children=" + root.children.len().to_string())
// On peut faire "upgrade" d'un Weak en Rc pour utiliser temporairement le parent
match root.children[0].parent.upgrade() {
Some(p) => io::println("parent de A = " + p.name),
None => io::println("parent libéré"),
}
return 0
}
Idée : si le parent meurt, le Weak
devient « vide » (impossible à
upgrade()
), donc pas de fuite.
str
(vue) vs String
(tampon possédé)str
: lecture sans copie (immuable). Idéal pour paramètres d’API.String
: vous possédez et pouvez modifier (ajouter, tronquer).let s:str = "abc"
let mut buf = String::from("abc")
buf.push("d") // "abcd"
std.io::println(buf)
String
contenant \0
(nul) : utilisez
CString::from_str
qui valide.
import std.{io, vec}
let mut v:[i32] = vec::with_capacity(16) // évite les réallocations
v.push(1); v.push(2); v.push(3)
for x in &v { io::println(x.to_string()) } // itération par référence (pas de copie)
let third = v[2] // panique si hors bornes
Ne gardez pas un pointeur (ou une référence brute côté C) vers l’intérieur d’un vecteur si vous continuez
à
push()
: une réallocation peut déplacer la mémoire.
Le C attend des *const char
terminés par nul et ne comprend pas les String
de VITL.
Convertissez toujours via CString
. Décidez qui libère quoi.
// Déclaration (ex.)
extern "C" { fn puts(msg:*const char) -> i32 }
fn c_puts(s:str) -> Result<(), str> {
let c = std.c::CString::from_str(s)? // valide l'absence de nul
unsafe { _ = puts(c.as_ptr()) } // frontière unsafe MINIMALE
return Result::Ok(())
}
unsafe
rapidement.
free_c_buffer(ptr)
), et appelez-la côté VITL.Rc
: quand le compteur de références fortes retombe à zéro.Rc
, liens « retour » en Weak
.
Rc
, index clé → Weak
si vous voulez
permettre
l’éviction automatique.CString
; 2) appeler dans un petit unsafe
;
3) libérer ce que le C a alloué, via la bonne fonction.vitl build -O0 -g -o build/app src/main.vitl
pour des messages plus
précis.children.len()
) pour vérifier votre modèle.// Message enrichi
let path = "data/config.txt"
if !std.fs::exists(path) {
std.io::eprintln("E2001: fichier introuvable → " + path)
std.io::eprintln("Astuce: lancez depuis la racine du projet ou passez un chemin absolu.")
return 3
}
Weak
plutôt qu’un simple Rc
? »Parce que deux Rc
qui se référencent l’un l’autre ne tombent jamais à zéro → fuite.
Weak
n’augmente pas le compteur fort, donc la libération redevient possible.
str
ou String
? »
Si la fonction lit seulement, utilisez str
. Si la fonction doit conserver et
modifier
plus tard, utilisez String
.
Évitez. Le C ne connaît pas les mouvements/redimensionnements. Copiez vers une zone gérée côté C, ou figez la durée de vie et documentez-la solidement.
// src/shared_config.vitl
module app.shared_config
import std.{io}
struct Config { name:String, retries:i32 }
fn main()->i32 {
let cfg = Rc<Config>::new(Config { name:String::from("demo"), retries:3 })
let worker1 = cfg.clone()
let worker2 = cfg.clone()
io::println("retries=" + worker2.retries.to_string())
// À la fin, worker1, worker2 et cfg tombent à 0 → libération
return 0
}
// src/ffi_wrapper.vitl
module app.ffi_wrapper
import std.{io, c}
// C opaque
extern "C" {
fn obj_create() -> *mut void
fn obj_destroy(p:*mut void) -> void
fn obj_set_name(p:*mut void, name:*const char) -> i32
}
struct Obj { handle:*mut void }
fn Obj::new() -> Result<Obj, str> {
let h = unsafe { obj_create() }
if h == std.ptr::null_mut() { return Result::Err("ffi: create failed") }
return Result::Ok(Obj{ handle:h })
}
fn Obj::set_name(&mut self, name:str) -> Result<(), str> {
let cname = c::CString::from_str(name)?
let rc = unsafe { obj_set_name(self.handle, cname.as_ptr()) }
if rc != 0 { return Result::Err("ffi: set_name failed") }
return Result::Ok(())
}
fn Obj::close(&mut self) -> () {
if self.handle != std.ptr::null_mut() {
unsafe { obj_destroy(self.handle) }
self.handle = std.ptr::null_mut()
}
}
fn main()->i32 {
let mut o = Obj::new()?
_ = o.set_name("demo")?
o.close() // libération explicite
return 0
}
Rc
pour partager, Weak
pour les liens retour.str
= vue de lecture, String
= tampon possédé et modifiable.CString
, isolez l’unsafe
.
vec::with_capacity
) et itérez par référence
(&v
).Rc
ici ?Weak
.Result<T,E>
(succès
Ok(T)
/ échec Err(E)
).
?
propage automatiquement l’erreur vers l’appelant (écrit moins,
gère
mieux).Option<T>
signifie « peut être absent » : Some(T)
ou
None
.
panic("message")
= arrêt immédiat (réservé à l’irréparable : invariants brisés,
corruption).
Une bonne gestion d’erreurs rend votre outil prévisible et aimable : message clair, action
proposée, code de sortie stable (pour les scripts/CI). VITL favorise l’approche explicite :
on
retourne un Result
(ou Option
) au lieu de masquer l’échec.
Result<T,E>
(base)import std.{io, fs}
// Lecture de fichier : renvoie le texte ou une erreur (str)
fn read_cfg(path: str) -> Result<str, str> {
if !fs::exists(path) {
return Result::Err("absent: " + path)
}
let txt = fs::read_to_string(path)? // propage l'erreur I/O si lecture échoue
return Result::Ok(txt)
}
// Consommation : pattern matching
fn main() -> i32 {
match read_cfg("config.txt") {
Result::Ok(txt) => io::println(txt),
Result::Err(e) => { io::eprintln("E: " + e); return 3 }
}
return 0
}
Astuce — Commencez par une version « simple » où E
est une chaîne
str
. Plus tard, passez à un type d’erreur structuré (cf. §6.9).
?
(propagation automatique)?
ne marche que dans une fonction qui retourne un Result<...>
. Si
l’expression à droite retourne Err
, la fonction renvoie immédiatement cette erreur.
import std.{fs}
fn load_or_err(path: str) -> Result<str, str> {
// Vérif claire + propagation compacte
if !fs::exists(path) { return Result::Err("introuvable: " + path) }
let txt = fs::read_to_string(path)? // <= ici `?` propage l'erreur I/O
return Result::Ok(txt)
}
Validez tôt, échouez tôt avec un message actionnable. Cela évite les cascades d’erreurs plus loin.
Option<T>
: absence de valeur ≠ erreurimport std.{io, cli}
fn print_home() -> () {
match cli::env("HOME") {
Some(h) => io::println(h),
None => io::eprintln("HOME non défini")
}
}
Utilisez Option
quand l’absence est attendue (ex. variable d’environnement
facultative).
Utilisez Result
quand c’est une erreur.
// Mauvais : vague
io::eprintln("échec")
// Meilleur : contexte + astuce
io::eprintln("E2001: fichier introuvable → " + path)
io::eprintln("Astuce: lancez depuis la racine du projet ou passez un chemin absolu.")
main()
import std.{io, str, cli, fs}
// 1) "run" fait le travail et renvoie Result
fn run() -> Result<(), str> {
let args = cli::args()
if str::len(args) < 2 {
return Result::Err("USAGE: app <fichier> (code 2)")
}
let path = args[1]
if !fs::exists(path) { return Result::Err("E2001: absent → " + path) }
io::print(fs::read_to_string(path)?) // ? propage l'I/O
return Result::Ok(())
}
// 2) "main" mappe proprement vers un code de sortie
fn main() -> i32 {
match run() {
Result::Ok(()) => return 0,
Result::Err(msg) => {
io::eprintln(msg)
// Petite heuristique pour l'exemple :
if str::find(msg, "USAGE:") != -1 { return 2 }
if str::find(msg, "E2001") != -1 { return 3 }
return 1
}
}
}
import std.{io, str}
fn parse_port(s: str) -> Result<i32, str> {
match str::to_int(s) {
Result::Ok(n) => {
if n < 1 || n > 65535 { return Result::Err("port hors plage (1..65535)") }
return Result::Ok(n as i32)
}
Result::Err(_) => return Result::Err("port invalide: " + s)
}
}
Pour les applis plus grandes, définissez un type d’erreur unique. Exemple avec un enum
et
un to_msg()
:
import std.{io, fs, str}
enum AppErr {
Usage(str), // USAGE/validation
Io(str), // fichiers/disque
Parse(str), // conversion/format
Ffi(str), // pont C
Other(str),
}
fn err_usage(m:str) -> AppErr { return AppErr::Usage(m) }
fn err_io(m:str) -> AppErr { return AppErr::Io(m) }
fn err_parse(m:str) -> AppErr { return AppErr::Parse(m) }
fn err_ffi(m:str) -> AppErr { return AppErr::Ffi(m) }
fn err_other(m:str) -> AppErr { return AppErr::Other(m) }
fn to_msg(e:AppErr) -> str {
match e {
AppErr::Usage(m) => return "USAGE: " + m,
AppErr::Io(m) => return "I/O: " + m,
AppErr::Parse(m) => return "PARSE: " + m,
AppErr::Ffi(m) => return "FFI: " + m,
AppErr::Other(m) => return "ERR: " + m,
}
}
// Mapping type d'erreur -> code de sortie
fn to_exit(e:AppErr) -> i32 {
match e {
AppErr::Usage(_) => return 2,
AppErr::Io(_) => return 3,
AppErr::Ffi(_) => return 4,
_ => return 1,
}
}
fn load_cfg(path:str) -> Result<str, AppErr> {
if !fs::exists(path) { return Result::Err(err_io("absent: " + path)) }
match fs::read_to_string(path) {
Result::Ok(t) => return Result::Ok(t),
Result::Err(e) => return Result::Err(err_io(e)), // on ré-étiquette I/O
}
}
fn run() -> Result<(), AppErr> {
let a = std.cli::args()
if str::len(a) != 2 { return Result::Err(err_usage("app <fichier>")) }
let txt = load_cfg(a[1])?
io::println(txt)
return Result::Ok(())
}
fn main()->i32 {
match run() {
Result::Ok(()) => return 0,
Result::Err(e) => { io::eprintln(to_msg(e)); return to_exit(e) }
}
}
extern "C" { fn foo_do(p:*const char) -> i32 } // 0 = OK, <0 = erreur
import std.{c}
fn foo_call(s:str) -> Result<(), AppErr> {
let cs = c::CString::from_str(s)? // si nul interne: Err(...) auto
let rc = unsafe { foo_do(cs.as_ptr()) }
if rc < 0 { return Result::Err(err_ffi("foo_do rc=" + rc.to_string())) }
return Result::Ok(())
}
test "parse_port rejette 0" {
match parse_port("0") {
Result::Ok(_) => assert(false), // ne devrait pas arriver
Result::Err(m) => assert(m == "port hors plage (1..65535)")
}
}
fn show(path:str) -> Result<(), str> {
if !std.fs::exists(path) {
return Result::Err("E2001: fichier introuvable → " + path + "\nAstuce: passez un chemin absolu.")
}
std.io::print(std.fs::read_to_string(path)?)
return Result::Ok(())
}
?
fn pipeline(p:str) -> Result<(), str> {
let raw = std.fs::read_to_string(p)? // 1) I/O
let cleaned = std.str::replace(raw, "\r\n", "\n")
let n = std.str::to_int(cleaned)? // 2) parse
if n <= 0 { return Result::Err("valeur non positive") }
std.io::println("ok:" + n.to_string()) // 3) ok
return Result::Ok(())
}
// Invariant interne brisé: continuer serait dangereux
fn expect_even(n:i32) -> () {
if n % 2 != 0 { panic("invariant: pair attendu, reçu " + n.to_string()) }
}
Ne « paniquez » pas pour des erreurs d’utilisateur (fichier manquant, mauvais argument). Préférez
Result
+ message + code de sortie.
?
ne marche pas dans main()
? »Parce que main()
retourne un i32
, pas un Result
. Placez le
?
dans une fonction run() -> Result<...>
, puis faites le mapping dans
main()
.
Option
ou Result
? »Option
si l’absence est normale (clé facultative). Result
si c’est inattendu
(erreur d’I/O, format).
Préfixez le message avec la tâche en cours et l’élément fautif : "parse seuil: " + e
,
"ouvrir " + path + ": " + e
.
Result
partout où ça peut échouer ; ?
pour propager.Option
pour l’absence prévue ; panic
pour l’irréparable seulement.Result
documenté.run()
centralise la logique et main()
mappe vers un code.panic
pour l’input utilisateur.let contenu = std.fs::read_to_string("config.txt")?
match std.fs::read_to_string("config.txt") {
Result::Ok(txt) => std.io::println(txt),
Result::Err(e) => std.io::eprintln(e),
}
Le FFI (Foreign Function Interface) permet à un programme VITL d’appeler du code C existant (librairies système, libs tierces, moteurs natifs). On réutilise ainsi des décennies d’APIs sans tout réécrire.
extern "C"
, convertir les
données
(chaînes, buffers), et respecter les règles d’ownership (qui alloue/libère ?).unsafe
isolé (enveloppez dans une fonction
VITL
« safe »).std.c::CString::from_str
(refuse les \0
internes) et gardez la valeur en vie pendant l’appel.std.c::free
fournie par le C).int32_t
, uint8_t
,
size_t
,
double
. Évitez long
.
new
/close
.
Result
et n’ignorez jamais un code
d’erreur.*mut void
côté VITL sans connaître la structure
interne (définie en C).import std.{io, c}
// Déclaration d'une fonction C : int puts(const char*);
extern "C" { fn puts(msg:*const char) -> i32 }
fn main() -> i32 {
let cs = c::CString::from_str("Hello C!\n")? // valide, pas de '\0' interne
unsafe { _ = puts(cs.as_ptr()) } // appel dans `unsafe`
return 0
}
De nombreuses libs C renvoient un char*
allocé par elles. La règle est : on
copie côté VITL, puis on libère avec free()
(ou une fonction dédiée).
// C (entête côté .h)
// char* make_msg(); // alloue (malloc)
// void free_msg(char* p); // libère
extern "C" {
fn make_msg() -> *mut char
fn free_msg(p:*mut char) -> void
}
fn get_message() -> Result<String, str> {
let p = unsafe { make_msg() }
if p == std.ptr::null_mut() { return Result::Err("ffi: make_msg a renvoyé NULL") }
// Copier le contenu C->VITL puis libérer côté C
let s = std.c::copy_cstring_into_string(p)? // <= utilitaire (copie jusqu'au '\0')
unsafe { free_msg(p) }
return Result::Ok(s)
}
Variante : si la lib documente « utilisez free()
standard », exposez
extern "C" { fn free(p:*mut void) -> void }
et appelez-la, pas un delete
maison.
Motif courant : passer un bloc mémoire à C et recevoir des octets en sortie.
// C
// int sha256(const uint8_t* data, size_t len, uint8_t* out32); // 0=OK
extern "C" { fn sha256(data:*const u8, len:usize, out:*mut u8) -> i32 }
fn hash256(input:[u8]) -> Result<[u8;32], str> {
let mut out:[u8;32] = [0; 32]
let rc = unsafe { sha256(input.as_ptr(), input.len(), out.as_mut_ptr()) }
if rc != 0 { return Result::Err("ffi: sha256 rc=" + rc.to_string()) }
return Result::Ok(out)
}
Exposez une poignée opaque et des fonctions d’usine/destruction côté C, puis encapsulez en VITL.
// C (shim)
// typedef struct Foo Foo;
// Foo* foo_create(int cap); void foo_destroy(Foo*);
// int foo_push(Foo*, const char*); int foo_len(const Foo*);
extern "C" {
fn foo_create(cap:i32) -> *mut void
fn foo_destroy(h:*mut void) -> void
fn foo_push(h:*mut void, s:*const char) -> i32
fn foo_len(h:*const void) -> i32
}
struct Foo { handle:*mut void }
impl Foo {
fn new(cap:i32) -> Result<Foo,str> {
let h = unsafe { foo_create(cap) }
if h == std.ptr::null_mut() { return Result::Err("ffi: create failed") }
return Result::Ok(Foo{ handle:h })
}
fn push(&mut self, s:str) -> Result<(),str> {
let cs = std.c::CString::from_str(s)?
let rc = unsafe { foo_push(self.handle, cs.as_ptr()) }
if rc != 0 { return Result::Err("ffi: push rc=" + rc.to_string()) }
return Result::Ok(())
}
fn len(&self) -> i32 { return unsafe { foo_len(self.handle) } }
fn close(&mut self) -> () {
if self.handle != std.ptr::null_mut() {
unsafe { foo_destroy(self.handle) }
self.handle = std.ptr::null_mut()
}
}
}
Discipline : un seul endroit « unsafe » par méthode, vérifications avant/après (NULL, codes).
Result
fn c_ok(rc:i32) -> Result<(), str> {
if rc == 0 { return Result::Ok(()) }
return Result::Err("ffi: rc=" + rc.to_string())
}
Nom des bibliothèques : libfoo.so
(Linux), libfoo.dylib
(macOS),
foo.dll
(Windows).
# Linux/macOS (bibliothèque partagée)
vitl build -O2 -L libs -lfoo -o build/app src/main.vitl
# rpath (trouver la lib au runtime)
vitl build ... -Wl,-rpath,$ORIGIN/../libs
# Statique (si libfoo.a disponible)
vitl build -O3 -L libs -Wl,-Bstatic -lfoo -Wl,-Bdynamic -o build/app src/main.vitl
# Windows (MinGW, exemple)
vitl build -O2 -L libs -lfoo -o build/app.exe src/main.vitl
// foo.h
#pragma once
#include <stddef.h> // size_t
#include <stdint.h> // int32_t, uint8_t
#ifdef __cplusplus
extern "C" {
#endif
typedef struct Foo Foo;
Foo* foo_create(int32_t cap);
void foo_destroy(Foo*);
int32_t foo_push(Foo*, const char* s); // s: NUL-terminée, UTF-8
int32_t foo_len(const Foo*);
#ifdef __cplusplus
}
#endif
Note : documentez « qui libère quoi » et la null-termination des chaînes.
user_data
Beaucoup d’APIs C vous rappellent via une fonction. Passez un identifiant opaque et rassemblez la logique côté C si possible.
// C
// typedef void (*on_item_fn)(const char* s, void* user);
// int enumerate(on_item_fn cb, void* user);
extern "C" { fn enumerate(cb:*const void, user:*mut void) -> i32 }
// Côté VITL, préférez écrire un petit shim C si vous devez capturer de l'état.
// Sinon, passez un identifiant (index) et rangez l'état dans une table VITL.
create
/destroy
, surveillez la conso.NULL
.String
VITL directement au C ? »Non. Convertissez en CString
(NUL-terminée), conservez-la vivante pendant l’appel, puis
laissez
VITL la libérer.
char*
. Je fais quoi ? »Copiez dans une String
VITL, puis libérez le char*
avec la fonction fournie
(free_msg
ou free
documenté).
unsafe
? »Parce que VITL ne peut pas vérifier la validité des pointeurs/tailles C. Isolez l’unsafe
dans
des wrappers courts et testés.
extern "C"
, appeler dans unsafe
, envelopper dans une API VITL
« safe
».CString
pour passer au C ; copier (& libérer C) pour revenir.ptr + len
; vérifier les tailles ; gérer le code retour.new
/close
, pas de déréférencement côté VITL.-L
et -l
, éventuellement rpath
; noms de libs
selon
l’OS.extern "C" { fn puts(msg:*const char) -> i32 }
let cstr = std.c::CString::from_str("Hello C!\n")?
unsafe { _ = puts(cstr.as_ptr()) }
La bibliothèque standard (stdlib) regroupe des modules prêts à l’emploi pour les tâches courantes : afficher du texte, lire/écrire des fichiers, manipuler les chaînes et vecteurs, mesurer le temps, accéder aux arguments de la ligne de commande, intéragir avec le C, etc.
std.<module>
(ex. std.fs
).Result<...>
→ utilisez
?
pour propager.
std.io
std.fs
std.str
std.math
std.time
std.cli
std.vec
std.debug
std.err
std.c
std.io
— Entrées/sorties consoleBut : écrire sur la sortie standard (utilisateur) et la sortie d’erreur (diagnostics).
import std.io
io::print("Hello ") // sans saut de ligne
io::println("world") // avec saut de ligne
io::eprintln("attention!") // sur STDERR
println
pour avoir automatiquement le
retour à
la ligne.std.fs
— Fichiers et cheminsBut : lire/écrire des fichiers texte ou binaires, tester l’existence.
import std.{fs, io}
// Lire tout un fichier en chaîne (UTF-8)
let txt = fs::read_to_string("input.txt")? // <= `?` propage l'erreur I/O
io::println("lu=" + txt)
// Écrire une chaîne (écrase si existe)
_ = fs::write_string("out.txt", "ok\n")?
// Lire binaire
let bytes = fs::read("image.bin")?
io::println("taille=" + bytes.len().to_string())
// Vérifier l'existence avant d'ouvrir
if !fs::exists("config.toml") {
io::eprintln("absent: config.toml")
// retourner un code 3 (I/O) dans `main` si approprié
}
exists
ou gérez l’erreur renvoyée par
read*
.
std.str
— Chaînes et utilitairesBut : opérations usuelles sur les chaînes de caractères (UTF-8).
import std.{str, io}
let s = "a b c"
io::println(str::len(s).to_string()) // 5
let parts = str::split(s, ' ') // ["a","b","c"]
let i = str::find("abc", "b") // 1
let out = str::replace("a_b", "_", "-") // "a-b"
// Parsing vers nombres (renvoie Result)
let n = str::to_int("42")? // i64
let f = str::to_float("2.5")? // f64
"n=" + n.to_string()
.?
sur to_int
/to_float
(entrée
utilisateur imprévisible).std.math
— Mathsimport std.{math, io}
let d = math::sqrt(3.0*3.0 + 4.0*4.0) // 5.0
let a = math::abs(-12) // 12
io::println(d.to_string())
std.time
— Horloge & mesureBut : mesurer un temps d’exécution, estampiller un événement.
import std.{time, io}
let t0 = time::now()
heavy_work()
io::println("ms=" + (time::now() - t0).to_string())
std.cli
— Arguments et variables d’environnementimport std.{cli, io, str}
let argv = cli::args()
if str::len(argv) < 2 {
io::eprintln("usage: app <fichier>")
// return 2 dans `main` (erreur d'usage)
}
// Variables d'environnement
match cli::env("HOME") {
Some(h) => io::println(h),
None => io::eprintln("HOME non défini"),
}
std.vec
— Vecteurs dynamiquesBut : gérer des tableaux extensibles de valeurs.
import std.{vec, io}
let mut v:[i32] = vec::with_capacity(16)
v.push(1); v.push(2); v.push(3)
for x in &v { io::println(x.to_string()) } // itération par référence (pas de copie)
match v.pop() {
Some(last) => io::println("last=" + last.to_string()),
None => {}
}
io::println("len=" + v.len().to_string())
L’accès v[i]
vérifie les bornes et peut panique si hors-limites. Si l’index peut
être
invalide, préférez une API sûre (selon la stdlib dispo) ou testez i < v.len()
avant.
std.debug
— Assertions & debugimport std::debug
debug::assert(2 + 2 == 4)
debug::dump("state=INIT") // trace lisible rapide
// Selon l'environnement, on peut obtenir une backtrace si activée au build `-g`.
std.err
— Erreurs structuréesBut : centraliser l’information d’erreur (catégorie, message, contexte) et mapper vers des codes de sortie stables.
import std::{err, io}
fn parse_threshold(s:str) -> Result<i64, err::Error> {
match std.str::to_int(s) {
Result::Ok(v) => return Result::Ok(v),
Result::Err(_) => return Result::Err(err::Error::new("parse int échoué")),
}
}
std.c
— Pont C (complément rapide)Voir la section FFI pour le guide complet. Ici, le minimum utile.
import std::c
extern "C" { fn puts(msg:*const char) -> i32 }
let cs = c::CString::from_str("Hello C!\n")?
unsafe { _ = puts(cs.as_ptr()) }
import std::{fs, io}
fn show_file(path:str) -> Result<(), str> {
if !fs::exists(path) {
io::eprintln("E2001: fichier introuvable → " + path)
io::eprintln("Astuce: lancer depuis la racine du projet ou passer un chemin absolu.")
return Result::Err("absent")
}
io::print(fs::read_to_string(path)?)
return Result::Ok(())
}
import std::{time, io}
fn timed(label:str, f: fn()->()) -> () {
let t0 = time::now()
f()
io::println(label + ":" + (time::now()-t0).to_string() + " ms")
}
import std::{fs, str, err}
fn load_threshold(path:str) -> Result<i64, err::Error> {
let s = fs::read_to_string(path)?
match str::to_int(str::replace(s, "\n", "")) {
Result::Ok(v) => return Result::Ok(v),
Result::Err(_) => return Result::Err(err::Error::new("parse int échoué")),
}
}
Result
? »Parce que l’opération peut échouer (fichier manquant, permissions, entrée invalide). Le type
Result
oblige à y penser et à gérer l’échec, ou à le propager avec ?
.
println
vs eprintln
? »println
pour la sortie « normale » (données destinées à l’utilisateur ou à un pipeline).
eprintln
pour les diagnostics et erreurs.
Convertissez d’abord le nombre : "n=" + n.to_string()
. VITL ne devine pas le format
pour
éviter les ambiguïtés.
print
· println
· eprintln
exists
· read_to_string
· read
·
write_string
len
· split
· find
·
replace
·
to_int
· to_float
sqrt
· abs
now
· soustraction → durée → to_string()
args
· env
with_capacity
· push
· pop
·
len
assert
· dump
err::Error::new
c::CString::from_str
→ extern "C"
→
unsafe
import std.{io, fs, str, math, time, cli, vec, debug, err, c}
io::println("Hello")
_ = fs::write_string("out.txt", "ok\n")?
let n = str::len("abc") // 3
let t0 = time::now(); heavy(); io::println((time::now()-t0).to_string()+" ms")
Le but n’est pas d’écrire « du code qui marche », mais du code que tout le monde peut comprendre, modifier et tester rapidement. VITL encourage la clarté : immutabilité par défaut, erreurs explicites, modules simples.
// src/math/geom.vitl
module math.geom
import std.{math, io}
const EPS:f64 = 1e-9
struct Vec2 { x:f64, y:f64 }
impl Vec2 {
fn norm(&self)->f64 { return (self.x*self.x + self.y*self.y).sqrt() }
}
pub fn dist(a:Vec2, b:Vec2)->f64 { return Vec2{ x:a.x-b.x, y:a.y-b.y }.norm() }
test "distance nulle sur même point" { assert(dist(Vec2{x:0,y:0}, Vec2{x:0,y:0}) < EPS) }
timeout_ms
, max_items
, parse_config
._
: let _unused = ...
pour signaler l’intention.
// Mieux
fn read_user_file(path:str)->Result<String,str> { ... }
// À éviter
fn ruf(p:str)->Result<String,str> { ... }
vitl fmt
régulièrement (CI conseillée).std.{io, fs}
puis vos modules internes.import std.{io, fs}
import util.{strfmt, path}
// Mauvais : imbriqué
fn load(p:str)->Result<String,str>{
if std.fs::exists(p){
return std.fs::read_to_string(p)
} else {
return Result::Err("absent")
}
}
// Mieux : early return + message clair
fn load(p:str)->Result<String,str>{
if !std.fs::exists(p) { return Result::Err("E2001: absent:"+p) }
return std.fs::read_to_string(p)
}
Result<T,E>
pour toute opération risquée.?
tant que le message d’origine est suffisant.fn run(path:str)->i32 {
match app::do_work(path) {
Result::Ok(_) => return 0,
Result::Err(e) => { std.io::eprintln(e); return 3 }
}
}
mut
avec parcimonielet
), introduisez mut
au plus près du besoin.
let mut acc:i64 = 0
for n in 0..=100 { acc = acc + n }
"n=" + n.to_string()
.util::strfmt
).match
match
exhaustif pour les enums (vous ne « oublierez » pas de cas).break/continue
multiples : c’est plus lisible.match status {
Status::Ok => {},
Status::Err(e) => std.io::eprintln(e),
}
test "parse: nombre valide" {
// Arrange
let s = "42"
// Act
let n = std.str::to_int(s).unwrap()
// Assert
assert(n == 42)
}
///
pour la doc générée par vitl doc
./// Convertit une chaîne en entier signé.
/// Retourne Err si la chaîne n'est pas un entier valide.
fn parse_i64(s:str)->Result<i64,str> { return std.str::to_int(s) }
eprintln
les diagnostics (niveau simple).E2001
).std.time::now()
autour du bloc.&v
).vec::with_capacity
.unsafe
dans une API sûre et documentée.Avant (compact mais obscur)
fn r(p:str)->i32{if!std.fs::exists(p){std.io::eprintln("x");return 3}
std.io::print(std.fs::read_to_string(p).unwrap());return 0}
Après (clair et robuste)
/// Affiche le contenu d'un fichier texte.
/// Codes : 0 ok, 3 I/O manquant.
fn run(path:str)->i32 {
if !std.fs::exists(path) {
std.io::eprintln("E2001: fichier introuvable → " + path)
return 3
}
match std.fs::read_to_string(path) {
Result::Ok(s) => { std.io::print(s); return 0 },
Result::Err(e) => { std.io::eprintln(e); return 3 },
}
}
Result
+ messages utiles (codes stables si pertinent)mut
localisévitl fmt
exécutéE2001
)let user_name = "Alice"
fn compute_area(w:i32, h:i32) -> i32 { return w*h }
struct Rectangle { width:i32, height:i32 }
Choisissez votre point de départ, puis avancez palier par palier. Chaque niveau propose : objectifs clairs, mini-projets avec critères de réussite, et erreurs fréquentes. Vous pouvez parcourir rapidement les paragraphes grisés (notes) et revenir aux encadrés pratiques pour consolider.
À ce stade, l’objectif est d’écrire de petits programmes qui s’exécutent sans surprise. Vous apprendrez à lire/écrire sur la console, manipuler quelques variables, appeler des fonctions de la bibliothèque standard et comprendre les messages d’erreur. Concentrez-vous sur la structure d’un fichier (module → imports → fonctions) et sur l’immutabilité par défaut.
src/main.vitl
, le lancer avec vitl run
, puis le
compiler.str
vs String
et quand utiliser mut
.
// Débutant : compteur 0..=N
module app.counter
import std.{io, cli, str}
fn main()->i32 {
let a = cli::args()
if str::len(a) != 2 { io::eprintln("usage: counter <n>"); return 2 }
let n = str::to_int(a[1])?
if n < 0 { io::eprintln("n doit être ≥ 0"); return 2 }
for i in 0..=(n as i32) { io::println(i.to_string()) }
return 0
}
import
et l’orthographe des
fonctions.
to_string()
,
as
).
mut
pour modifier une variable → ajouter let mut
.1) Écrire 10 lignes qui s’exécutent. 2) Lancer vitl fmt
. 3) Casser volontairement un cas
(fichier manquant)
et lire le message d’erreur. 4) Ajouter un test minimal.
Ici, vous structurez vos programmes en modules, vous gérez les erreurs avec Result
et
l’opérateur ?
,
et vous commencez à mesurer vos programmes (temps d’exécution, taille des E/S). L’objectif est
de
livrer des
CLI robustes avec des messages d’erreur utiles, des codes de sortie stables et quelques
tests.
?
.std.time::now()
.$HOME
.
// Intermédiaire : "cat++" avec erreurs contextualisées
module app.catpp
import std.{io, fs, cli, str, time}
fn main()->i32 {
let a = cli::args()
if str::len(a) < 2 { io::eprintln("usage: catpp <fichiers...>"); return 2 }
let t0 = time::now()
let mut shown:i32 = 0
for i in 1..str::len(a) {
let p = a[i]
if !fs::exists(p) { io::eprintln("E2001: introuvable → " + p); continue }
match fs::read_to_string(p) {
Result::Ok(s) => { io::print(s); shown = shown + 1 },
Result::Err(e) => io::eprintln("E2002: lecture échouée → " + p + " (" + e + ")"),
}
}
io::eprintln("ms=" + (time::now()-t0).to_string())
return if shown > 0 { 0 } else { 3 }
}
eprintln
les infos minimales (code + cause).
&v
, lire en
blocs.
Vous savez écrire un binaire -O2
avec des erreurs contextualisées, vous avez au moins deux
tests
par module,
et vous pouvez expliquer en 2 phrases la responsabilité de chaque fichier.
Le but est de produire des binaires fiables, intégrés à une chaîne CI/CD, avec une
surface
FFI C
minimale et sûre si nécessaire. Vous définissez des conventions d’équipe (codes d’erreurs,
structure
des modules,
checklist de revue), vous générez la documentation à partir des commentaires ///
et vous
surveillez la qualité via
des tests automatiques.
unsafe
FFI dans des fonctions VITL sûres et testées.fmt
/check
/test
/doc
en CI, publier
des
artefacts.puts
) via CString
, tests
d’intégration.vitl doc
, produire un binaire -O3
,
SHA256
et changelog.// Pro : FFI encap + mapping d'erreurs
module app.ffi_safe
import std.{io, c}
extern "C" { fn puts(msg:*const char)->i32 }
fn c_puts_line(s:str)->Result<(),str> {
let cs = c::CString::from_str(s)?
let rc = unsafe { puts(cs.as_ptr()) }
if rc < 0 { return Result::Err("E0701: puts a échoué") }
return Result::Ok(())
}
fn main()->i32 {
match c_puts_line("Hello C!\n") {
Result::Ok(_) => return 0,
Result::Err(e) => { io::eprintln(e); return 4 }
}
}
unsafe
encapsulé, ownership documenté.///
, lue et à jour.Vous pouvez brancher une lib C sans fuite, diagnostiquer un crash avec un backtrace, et expliquer en 30 secondes comment reproduire un bug (commande + entrée minimale).
Installez des garde-fous simples : gabarit de fichier (module/imports/tests), rubriques de code
review
(noms, erreurs, tests), matrice des codes d’erreurs, et script de vérification locale
(fmt
+ check
+ test
).
Scripting/ops : std.io/std.fs/std.cli, messages d’erreur concrets, packaging binaire.
Données/ETL : lecture par blocs, parsers ligne à ligne, mesure de temps E/S.
Systèmes/embarqué : tailles fixes, conversions explicites, FFI stricte et documentée.
fmt
systématique.
-O3
, doc auto, matrice d’erreurs stable,
plugin/bytecode.
Vitte Light est pensé pour couvrir rapidement la majorité des petits outils, scripts et utilitaires sans vous noyer dans la complexité. Pour y parvenir, le langage fait des choix qui privilégient la lisibilité, la prévisibilité et des binaires compacts. En contrepartie, certaines fonctionnalités « lourdes » ne sont pas intégrées nativement. Ce n’est pas une faiblesse mais un périmètre assumé : quand vos besoins dépassent ce périmètre, vous pouvez brancher une bibliothèque externe (FFI C) ou utiliser un outil spécialisé. Cette section vous explique clairement ce qui est non inclus, ce que cela implique concrètement, et comment contourner proprement les cas limites.
Vitte Light n’inclut pas de modèle de threads intégré. Cela signifie qu’un même processus VITL n’exécutera pas deux fonctions « en parallèle » sur plusieurs cœurs. En pratique, la plupart des petits outils n’en ont pas besoin : lecture/écriture de fichiers, transformation de texte, génération de rapports ou appels systèmes séquentiels restent instantanés à l’échelle humaine. L’absence de threads évite aussi une grande classe de bugs (verrous oubliés, interblocages, conditions de course) et simplifie énormément le débogage.
Si vous devez paralléliser des traitements lourds (par exemple, compresser un grand nombre de fichiers, calculer des hachages, convertir des images), la stratégie recommandée consiste à lancer plusieurs processus du même binaire et à distribuer le travail entre eux. C’est simple à raisonner : chaque processus a sa propre mémoire, ne partage rien, et l’OS se charge d’exploiter tous les cœurs. Vous pouvez coordonner ces processus via des fichiers temporaires, des pipes, des numéros de tâches, ou un petit superviseur en VITL qui répartit des chemins d’entrée sur N sous-processus. Pour les cas encore plus spécifiques (réseau, GPU, planification fine), appuyez-vous sur une bibliothèque native via FFI C qui expose la parallélisation en interne, tout en gardant une interface VITL minimaliste et sûre.
Rc
/Weak
)Vitte Light ne s’appuie pas sur un ramasse-miettes global. La mémoire est gérée par références
comptées (Rc<T>
) : lorsqu’il n’existe plus de référence « forte » vers une
donnée,
elle est libérée immédiatement. C’est facile à comprendre, déboguer et anticiper. La seule vigilance
concerne
les cycles forts (A pointe vers B et B vers A en « fort ») qui ne peuvent pas se libérer
tout
seuls. La parade est simple : pour les liens de « retour » (parent → enfant → parent), utilisez une
Weak
sur un des côtés. Vous évitez ainsi la fuite de mémoire sans devoir installer un GC.
Concrètement, structurez vos données avec un propriétaire clair (l’élément qui vit plus longtemps)
et
des observateurs qui n’empêchent pas la libération. Par exemple, un « Document » peut posséder en
Rc
ses « Sections », tandis que chaque « Section » ne conserve qu’un Weak
vers son
«
Document ». Au moment de la suppression, tout se libère de manière déterministe. Si vous manipulez des
graphes
complexes, faites un petit audit visuel : repérez les boucles A ↔ B et convertissez le lien de retour en
Weak
. Pour des structures gigantesques et dynamiques où un GC serait utile, déléguez la
structure
au C (ou à une lib avec GC intégré) derrière un wrapper VITL finement documenté sur l’ownership.
Le système de types couvre les cas fréquents (fonctions et structures paramétrées simples), mais n’offre pas tous les raffinements des langages dédiés à la méta-programmation lourde. En clair : on privilégie des signatures explicites et des conversions claires aux « types magiques ». Pour un outil de 50-300 lignes, c’est un avantage : vous lisez l’API en un coup d’œil et vous évitez les erreurs implicites.
Quand vous sentez que vous forcez le langage (cascade de paramètres de type, abstractions très profondes), c’est souvent un signal pour revenir à des types concrets et des fonctions dédiées aux 2-3 cas qui comptent, ou pour mettre la généricité « exotique » derrière une API C minuscule. Vous gardez une interface VITL stable et vous laissez la sophistication au moteur natif.
L’interopérabilité de Vitte Light vise l’ABI C. Cela garantit un pont fiable et portable avec un très vaste écosystème (libc, zlib, SQLite, etc.). En revanche, pour des bibliothèques C++/Rust/Go, vous devrez fournir un shim C : une petite couche qui expose des fonctions C pures (paramètres scalaires, pointeurs, longueurs), et qui s’occupe en interne d’appeler le code C++/Rust/Go. C’est une pratique standard : elle clarifie les responsabilités, stabilise la surface d’échange et simplifie le débogage.
Le contrat à respecter est simple : les chaînes passées au C doivent être des CString
valides
(UTF-8 sans \0
interne) et leur durée de vie doit couvrir l’appel. Documentez clairement qui
alloue
et qui libère chaque tampon. Si la lib vous renvoie un pointeur alloué de son côté, prévoyez la fonction de
libération correspondante et appelez-la systématiquement. En cas d’erreur, mappez les codes natifs vers des
messages clairs côté VITL et convertissez en codes de sortie stables (par exemple, « 4 » pour une erreur
FFI).
Pour des scripts et utilitaires, Vitte Light offre des temps de démarrage très courts et des
binaires légers, avec des performances « assez bonnes » en I/O et en calcul numérique classique. L’absence
de
threads intégrés ne pénalise pas les tâches séquentielles, et l’absence de GC évite des pauses non
déterministes. Les optimisations usuelles suffisent le plus souvent : lire/écrire en blocs plutôt que
caractère
par caractère, éviter les copies inutiles, réutiliser des tampons, convertir explicitement pour éviter les
promotions coûteuses, et compiler en -O2
(ou -O3
si le binaire final le justifie).
Si
un hot-spot reste déterminant, déportez-le dans une fonction C spécialisée et appelez-la via FFI.
Pour des fichiers énormes (journaux massifs, dumps), privilégiez le traitement en flux :
lire
par blocs (ou ligne par ligne) et traiter au fil de l’eau. Évitez de charger tout un fichier en mémoire,
sauf si
c’est réellement nécessaire. Cette approche garde la consommation mémoire stable et rend votre utilitaire
robuste face à des entrées plus volumineuses que prévu. En complément, mesurez le temps
(std.time::now()
) et affichez une synthèse sobre en sortie d’erreur (durée, octets lus, lignes
traitées) pour diagnostiquer facilement les goulots d’étranglement.
Vitte Light vise Linux, BSD, macOS et Windows. Quelques différences existent selon l’OS (droits d’exécution,
chemins, encodages de console). Restez sur des chemins relatifs maîtrisés, vérifiez exists
avant
lecture, et affichez des messages d’erreur concrets (chemin, cause). En FFI, tenez compte des
conventions d’édition de liens (-L
/-l
, rpath) et fournissez des
instructions
claires pour retrouver la bibliothèque au lancement (variable d’environnement, dossier libs/
du
projet).
Posez-vous trois questions simples. 1) Le besoin est-il ponctuel et localisé ? Si oui, vous pouvez souvent rester en VITL et, si nécessaire, appeler une ou deux fonctions C ciblées. 2) Le besoin est-il central et persistant (calcul distribué, UI lourde, moteur 3D, GC sophistiqué) ? Dans ce cas, mieux vaut déléguer ce cœur à une lib native éprouvée et ne garder en VITL que la « colle » et le flux. 3) Le besoin touche-t-il l’infrastructure (threads complexes, scheduling, GPU) au point d’absorber tout le projet ? Alors utilisez un langage/outil dédié pour ce sous-système et exposez une interface C propre pour que VITL puisse l’orchestrer.
Vitte Light est un langage d’efficacité pragmatique : rapide à apprendre, rapide à livrer, fluide à maintenir. Les limites énumérées ici ne sont pas des blocages, mais des garde-fous qui vous aident à décider quand rester simple, quand composer avec des processus, et quand brancher une bibliothèque native. En gardant l’interface VITL claire et en documentant vos hypothèses (ownership, erreurs, chemins), vous obtiendrez des outils robustes, portables et faciles à diagnostiquer—exactement ce dont on a besoin au quotidien.
Chaque programme Vitte Light (VITL) se termine avec un entier appelé « code de
sortie ». Par convention :
0 signifie « succès », et toute valeur non nulle signale une
erreur.
Les scripts, CI/CD, et autres outils automatiques s’appuient sur ce code pour décider quoi faire ensuite
(continuer, réessayer, alerter, etc.). Dans VITL, le code de sortie est simplement la valeur renvoyée
par
fn main() -> i32
.
0
depuis main
.stderr
via std.io::eprintln
.panic("...")
interrompt le programme et se traduit par un
échec (convention : 1
), avec un message/trace en mode debug (-g
).VITL standardise un petit noyau (0–4). Vous pouvez étendre avec des identifiants supplémentaires si votre outil le nécessite.
Code | Nom court | Quand l’utiliser | Message typique (stderr) |
---|---|---|---|
0 | EX_OK | Exécution normale | (pas de message d’erreur) |
1 | EX_GENERIC | Erreur générique / panic |
erreur: échec interne; réessayez avec -g |
2 | EX_USAGE | Mauvais arguments CLI, aide requise | usage: app <fichier> [options] |
3 | EX_IO | Fichiers/chemins/droits I/O | E2001: fichier introuvable → data.txt |
4 | EX_FFI | Appel bibliothèque native (C) a échoué | E0701: FFI call failed (rc=-1) |
5 | EX_CONFIG | Config manquante/invalide | E3001: config.toml invalide (clé 'port') |
6 | EX_NET | Réseau (DNS, socket, TLS) | E4002: connexion refusée host:port |
7 | EX_DATA | Données d’entrée invalides | E5001: JSON invalide à la ligne 12 |
8 | EX_TIMEOUT | Opération expirée | E6001: délai dépassé (5s) |
9 | EX_PERM | Permission refusée | E6002: accès refusé → /root/out |
10 | EX_UNSUP | Fonction non supportée ici | E6003: option --gpu non supportée |
Bonnes pratiques : restez dans une petite plage (0–15) et documentez-la. Les environnements POSIX normalisent l’état sur 8 bits (0–255). Des valeurs négatives peuvent être interprétées de façon imprévisible — évitez-les.
// Style recommandé : une fonction "run" retourne Result, "main" traduit en code.
module app.main
import std.{io, fs, str}
const EX_OK:i32 = 0
const EX_GENERIC:i32 = 1
const EX_USAGE:i32 = 2
const EX_IO:i32 = 3
fn run() -> Result<(), str> {
let argv = std.cli::args()
if str::len(argv) < 2 {
return Result::Err("usage: app <fichier>")
}
let path = argv[1]
if !std.fs::exists(path) {
return Result::Err("E2001: fichier introuvable → " + path)
}
std.io::print(std.fs::read_to_string(path)?) // `?` propage l'erreur I/O
return Result::Ok(())
}
fn main() -> i32 {
match run() {
Result::Ok(()) => { return EX_OK }
Result::Err(msg) => {
std.io::eprintln(msg)
// Heuristique simple : décider du code en fonction du message
if std.str::starts_with(msg, "usage:") { return EX_USAGE }
if std.str::starts_with(msg, "E2001:") { return EX_IO }
return EX_GENERIC
}
}
}
eprintln
), stdout pour les
résultats.// I/O robuste (extrait)
let path = argv[1]
if !std.fs::exists(path) {
std.io::eprintln("E2001: fichier introuvable → " + path)
std.io::eprintln("Astuce: lancez depuis la racine du projet ou passez un chemin absolu.")
return EX_IO
}
2
, afficher l’aide courte. Évitez une stacktrace.
1
, suggérer -g
pour
plus de traces.3
, préciser le chemin et la cause probable.4
, inclure le return code natif si disponible.$?
(immédiat après la commande).
Dans un
pipeline, seul le dernier code est renvoyé ; utilisez set -o pipefail
pour que le
pipeline
échoue si n’importe quel maillon échoue.$LASTEXITCODE
.# bash
vitl build -O2 -o build/app src/main.vitl && ./build/app data.txt
echo "exit=$?"
# pipelines
set -o pipefail
./produce | ./filter | ./consume
echo "pipeline exit=$?"
Pour un outil consommé par d’autres programmes, couplez un code avec une sortie JSON courte afin de mêler lisibilité humaine et robustesse machine.
// Exemple : en cas d'erreur, JSON minimal sur stdout, détails sur stderr.
fn main() ->i32 {
match run() {
Result::Ok(()) => { std.io::println(r#"{"ok":true}"#); return 0 }
Result::Err(msg) => {
std.io::eprintln(msg)
std.io::println(r#"{"ok":false,"code":2}"#)
return 2
}
}
}
Vérifiez vos codes avec des tests système (scripts) et des tests VITL ciblés.
# test shell (ex.)
./app # manque d'arguments
test $? -eq 2 || echo "attendu EX_USAGE"
./app data/missing.txt
test $? -eq 3 || echo "attendu EX_IO"
stderr
et retournez un code stable.set -o pipefail
si pipeline).Vous aider à trouver, comprendre et corriger rapidement les erreurs de compilation et d’exécution en Vitte Light (VITL). Cette section propose une méthode de triage, des exemples concrets, des gabarits de messages, et des checklists par thème (CLI, fichiers, FFI, etc.).
vitl build -O0 -g -o build/app src/main.vitl
.
vitl check src/
(style, imports, incohérences
communes).std.io::eprintln("dbg: ...")
ou
std.debug::dump("state=...")
si dispo.
vitl check src/
→ détecte tôt les erreurs courantes.vitl build -O0 -g ...
→ backtrace et messages détaillés.--emit-ir
/ --emit-bytecode
→ inspection avancée (comprendre ce que le
compilateur voit).vitl test
→ verrouiller un correctif avec un test simple.Un diagnostic VITL indique typiquement où (fichier:ligne:colonne), quoi (code, libellé) et piste (conseil). Exemple :
src/app/main.vitl:12:15: E0002: types incompatibles
aide: caster avec `as` ou convertir avec `.to_string()` selon le contexte
// Mauvais
module app.main
fn main()->i32 {
std.io::pritnln("hi") // faute de frappe + pas d'import
return 0
}
// Correct
module app.main
import std.io
fn main()->i32 {
std.io::println("hi")
return 0
}
src/util/str.vitl
⇒
module util.str
.
// Mauvais
let n:i32 = 3
let x:f64 = 0.5
let y = n + x // i32 + f64 → erreur
// Correct (cast local)
let n:i32 = 3
let x:f64 = 0.5
let y = (n as f64) + x
// Chaînes
std.io::println("n=" + n) // E0002
std.io::println("n=" + n.to_string()) // OK
Règle : conversion explicite pour les nombres (as
),
.to_string()
pour concaténer avec du texte.
// Mauvais
let mut acc:i32
if cond { acc = 1 }
std.io::println(acc.to_string())
// Correct
let mut acc:i32 = 0
if cond { acc = 1 }
std.io::println(acc.to_string())
match
non exhaustifenum Mode { Fast, Slow }
match m {
Mode::Fast => do_fast(),
// manque Slow → E0401
}
// Correct
match m {
Mode::Fast => do_fast(),
Mode::Slow => do_slow(),
}
Déclarez let mut
si vous modifiez une variable. Évitez les écritures concurrentes dans la même
expression.
Pour le FFI, une CString
ne doit pas contenir \0
interne. Voir la section FFI
ci-dessous.
let path = "data/config.txt"
if !std.fs::exists(path) {
std.io::eprintln("E2001: fichier introuvable → " + path)
return 3 // EX_IO
}
let txt = std.fs::read_to_string(path)? // `?` propage l'erreur I/O
let v:[i32] = [1,2,3]
let x = v[3] // panique: hors bornes
// Correct
if i < v.len() { let x = v[i] }
fn div(a:i32, b:i32) -> Result<i32,str> {
if b == 0 { return Result::Err("division par zéro") }
return Result::Ok(a / b)
}
En lecture texte, fichiers mal encodés ⇒ préférer lecture binaire + conversion contrôlée ou nettoyer l’entrée.
unsafe { ... }
uniquement.std.c::CString::from_str
et conserver la durée de vie.ptr+len
pour les buffers binaires, vérifier les bornes côté C.unsafe
// Mauvais
extern "C" { fn puts(msg:*const char)->i32 }
fn main()->i32 {
let s = std.c::CString::from_str("Hi\n")
puts(s.unwrap().as_ptr()) // E1001
return 0
}
// Correct
extern "C" { fn puts(msg:*const char)->i32 }
fn main()->i32 {
let s = std.c::CString::from_str("Hi\n")?
unsafe { _ = puts(s.as_ptr()) }
return 0
}
let msg = std.c::CString::from_str("ok\0oops") // E0601
if msg.is_err() { return Result::Err("E0601: CString invalide (\\0 interne)") }
let rc = unsafe { foo_call(...) }
if rc != 0 {
std.io::eprintln("E0701: FFI call failed (rc="+rc.to_string()+")")
return 4 // EX_FFI
}
std.cli::args()
) pour vérifier ce que
reçoit votre programme.stderr
si
possible).let t0 = std.time::now()
heavy()
std.io::eprintln("dbg: heavy ms=" + (std.time::now()-t0).to_string())
std.vec::with_capacity
si la taille est connue.E2001: fichier introuvable → <chemin>
usage: app <fichier> [options]
→ retournez
2
.
E0701: FFI call failed (rc=<code>)
→ retournez
4
.
E0002: types incompatibles (attendu <T>, reçu <U>)
+
piste cast.
argv
pour vérifier l’ordre/présence.2
pour « usage ».std.fs::exists
avant lecture/écriture..to_string()
pour nombres.as
pour les numériques.unsafe
autour des appels, CString
sans \0
interne.test "lecture fichier: path manquant renvoie EX_IO" {
// pseudo-test: isolez la fonction qui résout un code depuis un message
let rc = map_error_to_exit("E2001: fichier introuvable → data.txt")
assert(rc == 3)
}
// I/O robuste
let path = argv[1]
if !std.fs::exists(path) {
std.io::eprintln("E2001: fichier introuvable → " + path)
std.io::eprintln("Astuce: lancer depuis la racine du projet ou passer un chemin absolu.")
return 3
}
// FFI robuste
let msg = std.c::CString::from_str("ok\n")
if msg.is_err() { return Result::Err("E0601: CString invalide (caractère nul)") }
unsafe { _ = puts(msg.unwrap().as_ptr()) }
String
? Non. Convertissez au moment
d’afficher/concaténer. Gardez des types numériques pour calculer.-g
et assurez-vous
que
l’échec est bien un panic
ou qu’un message est émis sur stderr
.Résumé — Lisez le message complet · isolez un MRE · vitl check
+
-O0 -g
· ajoutez des types aux frontières · messages enrichis et codes stables · verrouillez le
fix
par un test.
Elle rassemble tout ce qu’il faut vérifier avant d’expédier un binaire ou un script Vitte Light (VITL). Cochez de haut en bas : structure, build, qualité, erreurs, I/O, FFI, perfs, docs, packaging, CI/CD et post-livraison. Astuce : gardez un Mini Repro d’Erreur (MRE) pour chaque correction critique et un test associé.
vitl run
et le binaire par ./build/app
sans
surprise.vitl fmt
et vitl check
ne signalent rien.vitl test
passe localement./src
(VITL), /libs
(C/FFI), /build
(artefacts), /tests
(si besoin).src/util/str.vitl
→
module util.str
).
main.vitl
ou app/main.vitl
clairement identifiés.vitl build -O0 -g -o build/app src/main.vitl
OK.vitl build -O3 -o build/app src/main.vitl
OK (ou -O2
selon
politique).build/app
, docs, éventuel .vitbc
).--emit-ir
/ --emit-bytecode
génèrent des sorties exploitables.
vitl fmt
appliqué à src/
et tests/
.vitl check
sans avertissement bloquant (imports morts, types flous, variables
inutilisées).
snake_case
pour variables/fonctions, CamelCase
pour
types).
Result<T,E>
+ propagation ?
; pas de
panic()
hors cas irrécupérables.
stderr
(conseil d’usage, chemin, cause).--help
clair si CLI : usage, options, exemples, codes de sortie.std.fs::exists
.
Rc/Weak
compris ; pas de cycles forts non désirés.unsafe
hors FFI ; pas de pointeur C stocké vers une String
temporaire.
get()
si nécessaire).extern "C"
correctes (types stables : int32_t
,
size_t
…).
unsafe { ... }
uniquement.std.c::CString::from_str
(sans \0
interne), durée de vie
respectée.-L
, -l
, rpath et variantes (statique/dynamique)
validés.vitl test
passe en local et en CI.vec::with_capacity
si taille connue).std.time::now()
) ajoutée/validée pour opérations lourdes, retirée ou
passée
en mode --verbose
.///
pour les fonctions publiques (but, paramètres, erreurs, exemple
minimal).vitl doc
génère une sortie propre (si configuré).app-<os>-<arch>
si multiplateforme).stderr
, messages concis, pas de secrets en clair.-v
/--verbose
), silencieux par défaut.
# Formatage, lint, build, tests (local)
vitl fmt src/ tests/
vitl check src/
vitl build -O0 -g -o build/app src/main.vitl
vitl test
vitl build -O3 -o build/app src/main.vitl
# Exécution simple
./build/app --help
./build/app <args>
FIN DU GUIDE