From ae24c2c0df0a93038c414a0a121d06424ba07a00 Mon Sep 17 00:00:00 2001 From: Vestin Hategekimana <80328639+CyuHat@users.noreply.github.com> Date: Fri, 23 Oct 2026 23:44:17 +0200 Subject: [PATCH 1/2] Multiple translation and improvement in PR - pull_request_template.md translated and improved - CONTRIBUTION.md translated - RELEASING.md translated --- .github/pull_request_template.md | 121 ++++++-- CONTRIBUTING.md | 196 +++++-------- RELEASING.md | 480 +++++++++++++++---------------- 3 files changed, 408 insertions(+), 389 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index cb3932e8..ebb749ee 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,28 +1,93 @@ -## Ce que fait cette PR - - - -## Vérifications - -- [ ] `cargo test --workspace` passe en local -- [ ] `cargo fmt --all` appliqué -- [ ] `typr case run` ne montre aucun `REGRESS` - -## Si cette PR touche le parseur ou le typage - -- [ ] Un cas a été ajouté dans `cases/` (`typr case add`) pour le comportement - corrigé, ou une raison de ne pas en ajouter est donnée ci-dessous - -## Si cette PR est une RFC - -- [ ] Le fichier est `rfcs/0000-.md` (le numéro est attribué à la fusion, - c'est celui de cette PR) et suit `rfcs/0000-template.md` - -Une RFC ne se relit pas comme du code : les fils de commentaires se posent sur -les phrases. Évite de force-pusher par-dessus une relecture en cours. Voir -[rfcs/README.md](../rfcs/README.md). - -## Si cette PR change une version - -Les versions ne se modifient pas à la main : `nu publish.nu bump ` puis -`nu publish.nu sync`. Le job `coherence` échoue sinon. Voir `RELEASING.md`. + + +Fixes #NNNN + +## Summary + + + +## RFC boundary + + + +- [ ] This PR does **not** change what TypR does. It implements/fixes already-decided behavior (link the issue). +- [ ] This PR **does** change language behavior (new syntax, typing rule, or generated R form). Linked RFC exists in `rfcs/`: # + +*(If neither box can honestly be checked, the idea probably belongs in Discussions → Ideas first.)* + +## Type of change + + + +- [ ] Compiler core (`crates/typr-core`): parsing, type checking, transpilation +- [ ] CLI (`crates/typr-cli`) +- [ ] LSP (`crates/typr-lsp`) +- [ ] MCP server (`crates/typr-mcp`), including `tutor` lesson data +- [ ] Editor integrations (`editors/` VS Code/Positron, RStudio addins, Vim/Neovim) +- [ ] Cases catalog (`cases/`) +- [ ] Packaging / docker / install / release tooling + +## Verification checklist (all PRs) + +These mirror the CI jobs and they block merge anyway. Check them before so you don't wait on CI: + +- [ ] `cargo test --workspace` passes locally. + *(Note: `export RUST_MIN_STACK=8388608` first, type checking recursion overflows the default 2 MB stack.)* +- [ ] `cargo fmt --all` applied. +- [ ] `typr case run` shows no 'REGRESS'. +- [ ] No binaries committed; versions stay aligned (CI `coherence`). +- [ ] Release build compiles (`cargo build --release`, CI `build-check`). +- [ ] If touching `editors/`: VS Code extension compiles and the R package is analyzable (CI `editors`). + +## If this PR touches the parser or type system + +Per CONTRIBUTING.md: a new case should be added to `cases/` (`typr case add`) for the corrected behavior, **or** a reason for not adding one is given below. + + + +**Reason for not adding a case:** + + + +## If this PR is an RFC + +The file is `rfcs/0000-.md` (the number is assigned at merge time, which is this PR's number) and follows `rfcs/0000-template.md`. + +> An RFC discussion does not work like code: comment threads should be posed on specific sentences. Avoid talking-over-an-existing-review in progress. See [`rfcs/README.md`](./rfcs/README.md). + +## Compiler core details + +- [ ] Existing R code still behaves identically (TypR is a complement of R, not a fork of its semantics). +- [ ] Generated R stays idiomatic, readers should not see artifacts of this change. +- [ ] Behavior justified: either an issue (already-decided behavior) or an accepted RFC is linked above. + +## Editor grammars + +Hand-written grammars have historically drifted: highlighting Rust keywords (`impl`, `trait`, `mut`, …) that don't exist in TypR while missing real TypR keywords (`opaque`, `module`, `record`, `object`, `interface`, `typeconstructor`, `recursive`, `embed`, `@export`, kind sigils). + +- [ ] If this PR adds/changes TypR keywords or tokens: every editor grammar in `editors/` was updated to match. + +## MCP server + +The MCP server exposes the compiler to AI agents (Claude Code, Claude Desktop, …), tools run in-process, no filesystem, no `.typr_cache`. + +- [ ] Tool behavior and output shape are unchanged, or any change is intentional and documented (agents depend on this contract). +- [ ] If `tutor` lessons were touched: `typr tutor check` still passes for affected lessons. + +## Documentation + +User-facing documentation lives in the separate `we-data-ch/typr.github.io` repo. + +- [ ] User-facing changes have a corresponding PR in `we-data-ch/typr.github.io`. + *(Warning: `onBrokenLinks` is set to `throw` there, a dead link fails that repo's CI before merge, not after.)* +- [ ] Link to the docs PR: # + +## Test plan + + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e7bc061..cfdffd74 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,120 +1,76 @@ -# Contribuer à TypR - -## Modèle de branches - -- `main` — ce qui est publiable. Les tags de release en partent, et - `nu publish.nu release` refuse de publier depuis autre chose. -- `develop` — intégration. -- Les contributions passent par une PR vers `develop`. - -`main` devrait être protégée sur GitHub : PR obligatoire, CI verte requise. -Sans ça, la garde de `publish.nu` ne protège que celui qui l'utilise. - -## Ce que la CI vérifie - -| Job | Vérifie | -|---|---| -| `test` | `cargo test --workspace` — 868 tests, bloquant | -| `build-check` | compilation en `--release` | -| `cases` | rejeu du catalogue `cases/` — échoue sur toute `REGRESS` | -| `coherence` | versions alignées, aucun binaire commité | -| `editors` | l'extension VS Code compile, le package R est analysable | - -`RUST_MIN_STACK=8388608` est indispensable : le typage est récursif et déborde -la pile par défaut de 2 Mo sur au moins un test. La CI le définit ; en local, -exporte-le si `cargo test` s'interrompt sur un `stack overflow`. - -## Proposer un changement de langage (RFC) - -Une correction de bug se discute dans une issue. Un **changement de langage** — -une syntaxe nouvelle, une règle de typage, une autre forme de R engendré — passe -d'abord par une RFC : une proposition écrite, relue en PR, fusionnée dans -`rfcs/` si elle est acceptée. Le but n'est pas la cérémonie, c'est que le -*raisonnement* survive : six mois plus tard, « pourquoi TypR fait comme ça ? » a -une réponse écrite. - -La frontière : **si la réponse à « que fait TypR ici ? » change, c'est une -RFC ; si le compilateur ne fait que rattraper une réponse déjà donnée, c'est une -issue.** Une idée encore floue va dans la catégorie -[Ideas](https://github.com/we-data-ch/typr/discussions/categories/ideas) des -Discussions — c'est souvent là qu'elle se règle, et sinon elle en sort meilleure. - -Le processus complet, les étiquettes (`rfc-draft` / `rfc-accepted` / -`rfc-rejected`) et le gabarit sont dans [rfcs/README.md](rfcs/README.md). Les -notes de conception du workspace (`spécifications/`, `ai_context/*.md`) en sont -la matière première : une RFC, c'est leur face publique. - -## Travailler sur le parseur ou le typage - -Le catalogue `cases/` est un ensemble de reproductions curées, chacune avec sa -sortie R attendue. C'est à la fois la documentation d'un comportement et son -test de non-régression. - -```bash -typr case list # état du catalogue -typr case run # rejoue tout ; sort en 1 sur REGRESS -typr case add --from # crée un cas depuis un vrai projet -typr case freeze # fige la sortie actuelle comme golden -``` - -Une correction de bug sans cas associé n'a rien qui l'empêche de revenir. - -## Travailler sur la coloration syntaxique - -Les grammaires d'éditeur sont **générées**. La source de vérité unique est le -manifeste de syntaxe, `crates/typr-core/src/components/syntax/mod.rs` : une -liste ordonnée de règles, une entrée par lexème que le parseur reconnaît -réellement. - -```bash -typr syntax --json # le manifeste -typr syntax --target tmlanguage -typr syntax --write # régénère les grammaires sous editors/ -typr syntax --check # échoue si une grammaire commitée a dérivé -``` - -N'édite jamais `editors/vscode/syntaxes/typr.tmLanguage.json` à la main : le -fichier porte un en-tête qui le dit, et `cargo test -p typr-cli` le compare à -ce que le manifeste produit. - -Deux tests gardent l'invariant dans les deux sens -(`components::syntax::tests`) : - -- ajouter un `tag("…")` au parseur sans l'ajouter au manifeste fait échouer - `every_parser_tag_is_in_the_manifest` ; -- mettre dans le manifeste un mot que le parseur ne connaît pas fait échouer - `manifest_claims_no_word_the_parser_does_not_know`. - -C'est le second qui manquait : les grammaires écrites à la main coloraient -`impl`, `trait`, `struct`, `enum`, `where`, `mut`, `Option`, `Result` — -copiés d'une grammaire Rust, inexistants en TypR — pendant que `opaque`, -`module`, `record`, `object`, `interface`, `typeconstructor`, `recursive`, -`embed`, `@export` et les sigils de kind n'étaient colorés nulle part. - -## Travailler sur la documentation - -La documentation vit dans un dépôt séparé : `we-data-ch/typr.github.io`. -Chaque PR y déclenche une construction complète du site — `onBrokenLinks` est -réglé sur `throw`, donc un lien mort fait échouer la CI avant la fusion, pas -après. - -Chaque bloc ` ```typr ` du site est passé au vrai compilateur en CI -(`npm run check:examples`) : un exemple qui cesse de compiler bloque la -construction. L'oracle est la **dernière release** — la version que le lecteur a -réellement installée — et un second workflow rejoue la même chose chaque nuit -contre `develop`, en avertissement seulement. C'est le préavis « la prochaine -release va casser tel exemple » : quand il se déclenche, la PR du compilateur et -celle de la doc doivent partir ensemble. - -## Versions - -Personne ne modifie une version à la main. `Cargo.toml [workspace.package] -version` est la seule source de vérité ; `nu publish.nu sync` la propage à -l'extension VS Code et au runner RStudio, et le job `coherence` échoue en cas -d'écart. Voir [RELEASING.md](RELEASING.md). - -## Binaires - -Aucun binaire ne doit être commité. Ceux du runner RStudio sont assemblés par la -CI au moment de construire le tarball de release. Le job `coherence` échoue si -un binaire réapparaît dans l'index. +# Contributing to TypR + +## Branch model + +- `main` holds what is publishable. Release tags are cut from it, and `nu publish.nu release` refuses to publish from anything else. +- `develop` is where integration happens. +- All contributions land through a PR targeting `develop`. + +`main` should be protected on GitHub, with mandatory PRs and green CI. Without that, the guard in `publish.nu` only protects whoever happens to use it. + +## What the CI checks + +| Job | Checks | +|---|---| +| `test` | `cargo test --workspace` (868 tests, blocking) | +| `build-check` | `--release` compilation | +| `cases` | replays the `cases/` catalog and fails on any `REGRESS` | +| `coherence` | aligned versions, no committed binaries | +| `editors` | the VS Code extension compiles, the R package is analyzable | + +`RUST_MIN_STACK=8388608` matters more than it looks. Type checking is recursive, and it overflows the default 2 MB stack on at least one test. CI sets it for you. Locally, export it yourself if `cargo test` ever dies with a `stack overflow`. + +## Proposing a language change (RFC) + +A bug fix is discussed in an issue. A **language change**, meaning new syntax, a typing rule, or a different form of generated R, goes through an RFC first. That means a written proposal, reviewed as a PR, merged into `rfcs/` if accepted. The point is not ceremony. The point is making sure the reasoning survives: six months later, when someone asks why TypR behaves this way, there is a written answer. + +Here is the dividing line. If the answer to "what does TypR do here?" changes, it is an RFC. If the compiler is just catching up to an answer that was already given, it is an issue. An idea that is still fuzzy belongs in the [Ideas](https://github.com/we-data-ch/typr/discussions/categories/ideas) category of Discussions. It often gets resolved right there, and when it doesn't, it comes out sharper anyway. + +The full process, the labels (`rfc-draft`, `rfc-accepted`, `rfc-rejected`), and the template live in [rfcs/README.md](rfcs/README.md). The workspace design notes (`spécifications/`, `ai_context/*.md`) are its raw material. An RFC is their public face. + +## Working on the parser or the type system + +The `cases/` catalog is a set of curated reproductions, each with its expected R output. It serves two purposes at once: documenting a behavior and testing that it does not regress. + +```bash +typr case list # catalog status +typr case run # replays everything, exits 1 on REGRESS +typr case add --from # creates a case from a real project +typr case freeze # freezes the current output as golden +``` + +A bug fix without an associated case has nothing stopping it from coming back. + +## Working on syntax highlighting + +Editor grammars are **generated**. The single source of truth is the syntax manifest at `crates/typr-core/src/components/syntax/mod.rs`: an ordered list of rules, one entry per token the parser actually recognizes. + +```bash +typr syntax --json # the manifest +typr syntax --target tmlanguage +typr syntax --write # regenerates the grammars under editors/ +typr syntax --check # fails if a committed grammar has drifted +``` + +Never edit `editors/vscode/syntaxes/typr.tmLanguage.json` by hand. The file carries a header saying exactly that, and `cargo test -p typr-cli` compares it against what the manifest produces. + +Two tests guard the invariant in both directions (`components::syntax::tests`): + +- add a `tag("…")` to the parser without adding it to the manifest, and `every_parser_tag_is_in_the_manifest` fails; +- put a word in the manifest the parser does not know, and `manifest_claims_no_word_the_parser_does_not_know` fails. + +The second one was missing, and it showed. The hand-written grammars happily highlighted `impl`, `trait`, `struct`, `enum`, `where`, `mut`, `Option`, and `Result`, all copied from a Rust grammar and none of them existing in TypR. Meanwhile `opaque`, `module`, `record`, `object`, `interface`, `typeconstructor`, `recursive`, `embed`, `@export`, and the kind sigils got no highlighting at all. + +## Working on the documentation + +The documentation lives in a separate repository: `we-data-ch/typr.github.io`. Every PR there triggers a full build of the site. `onBrokenLinks` is set to `throw`, so a dead link breaks the CI before merging rather than after. + +Every ` ```typr ` block on the site is run through the real compiler in CI (`npm run check:examples`). If an example stops compiling, the build stops too. The oracle is the **latest release**, meaning the version readers actually have installed. A second workflow replays the same checks every night against `develop`, as warnings only. Think of it as the heads-up that the next release will break a given example. When it fires, the compiler PR and the doc PR should ship together. + +## Versions + +Nobody bumps a version by hand. `Cargo.toml [workspace.package] version` is the single source of truth. `nu publish.nu sync` propagates it to the VS Code extension and the RStudio runner, and the `coherence` job fails on any mismatch. See [RELEASING.md](RELEASING.md). + +## Binaries + +Never commit a binary. The RStudio runner's binaries are assembled by CI when building the release tarball, and the `coherence` job fails if one sneaks back into the index. diff --git a/RELEASING.md b/RELEASING.md index b0cdd135..d9486407 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,241 +1,239 @@ -# Publier une version de TypR - -Un seul geste déclenche toute la distribution : **pousser un tag `vX.Y.Z`**. -Tout le reste est fait par `.github/workflows/release.yml`. - -## Source de vérité - -`[workspace.package] version` dans `Cargo.toml`. Rien d'autre ne porte de version -de référence — l'extension VS Code et le runner RStudio en sont dérivés par -`nu publish.nu sync`, et le playground lit celle que la CI écrit à côté du WASM. - -La CI **refuse** de publier si le tag, `Cargo.toml`, `editors/vscode/package.json` -et `editors/rstudio/DESCRIPTION` ne disent pas tous la même chose (job `verify`), -et le même contrôle tourne sur chaque PR (job `coherence` de `ci.yml`). - -## Procédure - -```bash -git switch develop -nu publish.nu ship patch # ou minor / major -``` - -C'est tout. `ship` enchaîne la montée de version, la pull request, l'attente de -la CI, la fusion, le tag et la resynchronisation de `develop`. - -Il s'interrompt **une seule fois**, quand les checks sont verts et juste avant -de fusionner : - -``` -Prêt à fusionner la PR #22 et à poser v0.5.10. -Le tag déclenche la publication sur crates.io, où un numéro ne peut plus être repris. -Continuer ? [o/N] -``` - -C'est le dernier moment réversible. Répondre autre chose que `o` laisse la PR -ouverte et n'a rien publié ; `nu publish.nu ship --resume` reprend plus tard. - -Même chose si la CI échoue : rien n'est fusionné ni tagué, la PR reste ouverte, -on corrige sur `develop` et on relance avec `--resume`. - -Ajouter `--yes` supprime la confirmation, pour un usage non interactif. À éviter -autrement : c'est la seule barrière avant l'irréversible. - -### Ce que ship fait, dans l'ordre - -1. vérifie que `gh` est authentifié, qu'on est sur `develop`, que l'arbre est - propre et que `develop` est à jour ; -2. fusionne `main` dans `develop` — sans ça la PR embarquerait une régression ; -3. `bump`, qui propage la version à l'extension VS Code et au runner RStudio ; -4. `cargo check --workspace`, qui met `Cargo.lock` à jour. **En cas d'échec la - version est restaurée** : un numéro qui ne compile pas ne reste pas dans - l'arbre de travail ; -5. commit, push, ouverture de la PR ; -6. attente des checks obligatoires ; -7. *confirmation* ; -8. fusion, puis `release` — qui refait ses quatre gardes, volontairement en - double : c'est la dernière barrière ; -9. `develop` remis au niveau de `main`. - -### À la main - -L'enchaînement reste décomposable, ce qui est utile pour reprendre une release -partiellement passée : - -```bash -git switch develop && git pull -nu publish.nu bump patch -cargo check --workspace -git commit -am "release v0.5.10" && git push origin develop -gh pr create --base main --head develop --fill -gh pr merge --merge --delete-branch=false - -git switch main && git pull -nu publish.nu release --dry-run -nu publish.nu release - -git switch develop && git merge main && git push origin develop -``` - -`release` ne pousse **que le tag** : `main` est protégée, son contenu n'y arrive -que par pull request. S'il détecte que `main` locale diverge de `origin/main`, -il s'arrête plutôt que de taguer dans le vide. - -Un numéro de version déjà publié ne se réutilise pas. `release` refuse un tag -existant en local comme sur le distant, et `cargo publish` refuserait de toute -façon d'écraser une version présente sur crates.io. - -`nu publish.nu check` compare ensuite ce qui est réellement publié sur chaque canal. - -## Ce que la CI fait à partir du tag - -``` -tag vX.Y.Z - │ - ├─ verify ......... refuse si tag ≠ Cargo.toml ≠ éditeurs - │ - ├─ build .......... 8 cibles (Linux musl + Linux GNU + Windows + macOS, - │ × x86_64/aarch64) - │ └─ release .... GitHub Release + checksums SHA-256 - │ ├─ docker ..... image depuis le binaire Linux, tags X.Y.Z + latest - │ ├─ rstudio .... tarball R avec les binaires frais embarqués - │ └─ docs ....... repository_dispatch → reconstruction du site - │ - ├─ crates-io ...... typr-core → typr-lsp → typr-cli → typr - ├─ wasm ........... WASM + version.json → dépôt du playground - ├─ vscode ......... .vsix → Marketplace + Open VSX + attaché à la release - ├─ vim ............ tarball du plugin, attaché à la release - ├─ brew ........... formule générée → tap `we-data-ch/homebrew-typr` - ├─ winget ......... manifestes générés → PR `microsoft/winget-pkgs` - └─ scoop .......... manifeste généré → bucket `we-data-ch/scoop-bucket` -``` - -Les branches sont indépendantes : si le Marketplace échoue, les binaires et -crates.io sont quand même publiés. - -### Rejouer une branche qui a échoué - -```bash -gh workflow run release.yml -f tag=v0.5.9 -``` - -Le workflow accepte un tag en entrée, ce qui évite d'avoir à re-taguer. - -**Avec une limite qu'il faut connaître : il recharge le dépôt *au tag*.** Un -correctif poussé après coup n'est donc pas pris en compte — le rejeu recompile -exactement le même arbre. - -Le rejeu sert aux échecs d'**environnement** : jeton expiré, registre -indisponible, réseau. Pour un échec de **code**, il faut un tag neuf, donc une -nouvelle version. - -C'est ce qui s'est produit à la v0.5.8 : le job Docker échouait sur le -`Dockerfile` lui-même, et il a fallu sortir une v0.5.9 pour livrer le correctif. -Rejouer la v0.5.8 aurait rebuté sur le même fichier. - -## L'ordre de crates.io n'est pas négociable - -`typr-cli` déclare `typr-lsp = { path = ..., version = "0.5" }`. Une dépendance -`path` **portant un numéro de version** exige que le crate existe sur le registre -au moment du `cargo publish`. `typr-lsp` doit donc être publié avant `typr-cli`, -lui-même avant `typr`. - -C'est précisément ce qui avait bloqué la chaîne : `typr-lsp` n'ayant jamais été -publié, `typr-cli` et `typr` sont restés en 0.5.5 pendant que `typr-core` passait -en 0.5.7 et que les releases GitHub annonçaient 0.5.7. - -## Secrets requis - -| Secret | Utilisé par | Où l'obtenir | -|---|---|---| -| `CARGO_REGISTRY_TOKEN` | `crates-io` | crates.io → Account Settings → API Tokens | -| `DOCKERHUB_USERNAME` | `docker` | nom de compte Docker Hub | -| `DOCKERHUB_TOKEN` | `docker` | Docker Hub → Account Settings → Personal access tokens | -| `VSCE_PAT` | `vscode` | Azure DevOps → PAT, scope *Marketplace: Manage* | -| `PLAYGROUND_DEPLOY_TOKEN` | `wasm` | PAT GitHub, scope `repo` sur le dépôt du playground | -| `DOCS_DISPATCH_TOKEN` | `docs` | PAT GitHub, scope `repo` sur `typr.github.io` | -| `HOMEBREW_TAP_TOKEN` | `brew` | PAT GitHub, scope `repo` sur `homebrew-typr` | -| `WINGET_CREATE_GITHUB_TOKEN` | `winget` | PAT GitHub, scope `public_repo` (lu par `wingetcreate` dans l'environnement) | -| `SCOOP_BUCKET_TOKEN` | `scoop` | PAT GitHub, écriture sur `scoop-bucket` | - -## Le tap Homebrew - -`we-data-ch/homebrew-typr` ne contient que `Formula/typr.rb`, `README.md` et -`LICENSE` — aucun binaire. Le job `brew` génère la formule à partir du -`checksums.txt` de la release, la fait valider par `brew style` et -`brew audit --strict`, puis la commite. La formule n'est donc jamais écrite à la -main : voir `packaging/homebrew/README.md`. - -Le dépôt a été créé le 30 septembre 2026 et contient déjà la formule de -v0.5.12 ; la CI le tient ensuite à chaque tag. S'il fallait le refaire, le geste -est : - -```bash -gh repo create we-data-ch/homebrew-typr --public --description "Tap Homebrew pour TypR" -``` - -Deux façons de le tenir, à trancher une fois pour toutes : - -- **`main` libre** — la CI pousse directement, comme `vscode` et `wasm` le font - pour leurs dépôts. Le tag fait foi, et le tap ne peut jamais être en retard. -- **`main` protégé, admission via le dépôt `we-data-ch/homebrew-typr-autoupdate`** - — la CI n'écrit plus dans le tap mais dans le dépôt d'admission, qui ouvre une - PR que l'on relit. `homebrew-typr/typr` reste installable entre-temps, par ce que - `brew install` lit est la branche par défaut. Le tap devient alors impossible à - modifier sans relecture humaine, au prix d'un `brew update-autoupdate` à - installer sur les postes des mainteneurs. - -Le choix du lot 3 est le premier : il est cohérent avec le reste de la chaîne, et -une formule générée par la CI depuis le `checksums.txt` n'a pas à être relue — ce -qui la relèverait, c'est son unicité, pas sa rédaction. - -## Les canaux WinGet et Scoop - -Aucun binaire non plus : `winget-pkgs` ne publie que trois fichiers YAML, et le -bucket Scoop que des URL et leurs SHA-256. Les deux canaux lisent le **même** -`checksums.txt` que la formule Homebrew, et sont rendus par le même script, -`packaging/render.sh` — voir `packaging/README.md`. - -Ce que les deux jobs font de plus que le tap, c'est **installer pour de vrai** -avant de publier : - -- `winget` télécharge les deux archives Windows et compare leurs empreintes à - celles des manifestes rendus, puis passe le dossier à `wingetcreate submit`, - qui le valide avec le validateur officiel avant d'ouvrir la PR ; -- `scoop` lance `scoop install` sur le manifeste local, attend le shim `typr`, et - exige que `typr --version` annonce la version de la release. - -Un canal qui refuse d'installer ne publie donc rien — mieux vaut un job rouge -qu'un manifeste que personne ne peut installer. - -**Le dépôt `we-data-ch/scoop-bucket` doit exister avant le premier tag.** Le job -`clone` échoue bruyamment s'il est absent, parce qu'un `git push` vers une URL -inexistante échouerait lui aussi, mais trois minutes plus tard : - -```bash -gh repo create we-data-ch/scoop-bucket --public --description "Bucket Scoop pour TypR" -``` - -Les secrets manquants ne bloquent pas la release : le rendu et les installations -locales ont lieu, seule la publication est sautée avec un avertissement. C'est -voulu — pour qu'un tag serve à autre chose qu'à produire un job rouge. - -Le bucket garde dix versions, purgées par ordre d'ajout dans l'historique et non -par ordre alphabétique. `typr update` sert à cela : sans purge, un utilisateur qui -n'aurait pas fait `scoop update` pendant un an verrait une liste de manifests -toujours plus longue. - -## Aucun binaire dans le dépôt - -Les binaires du runner RStudio sont assemblés par la CI au moment de construire -le tarball, jamais commités. L'ancien dépôt `typr_runner` pesait 140 Mo de `.git` -pour 100 Ko de source, à cause de 33 commits « [auto] updated binaries ». -Le job `coherence` de `ci.yml` échoue si un binaire réapparaît dans l'index. - -## Développement local - -```bash -nu deploy.nu # binaire de debug → ~/sh/typr -./build-wasm.sh # WASM → playground voisin, avec version.json -nu docker/deploy.nu # image locale depuis le binaire release -``` +# Publishing a TypR Release + +A single gesture triggers the whole distribution: **push a `vX.Y.Z` tag**. +Everything else is done by `.github/workflows/release.yml`. + +## Source of truth + +`[workspace.package] version` in `Cargo.toml`. Nothing else carries a reference +version: the VS Code extension and the RStudio runner are derived from it by +`nu publish.nu sync`, and the playground reads the one the CI writes next to the WASM. + +The CI **refuses** to publish if the tag, `Cargo.toml`, `editors/vscode/package.json` +and `editors/rstudio/DESCRIPTION` do not all say the same thing (job `verify`), +and the same check runs on every PR (job `coherence` in `ci.yml`). + +## Procedure + +```bash +git switch develop +nu publish.nu ship patch # or minor / major +``` + +That is all. `ship` chains the version bump, the pull request, waiting for +the CI, the merge, the tag, and resyncing `develop`. + +It interrupts **only once**, when the checks are green and just before +merging: + +``` +Ready to merge PR #22 and set v0.5.10. +The tag triggers publication on crates.io, where a number can no longer be reused. +Continue? [o/N] +``` + +This is the last reversible moment. Answering anything other than `o` leaves the PR +open and publishes nothing; `nu publish.nu ship --resume` picks it up later. + +Same thing if the CI fails: nothing is merged or tagged, the PR stays open, +you fix it on `develop` and rerun with `--resume`. + +Adding `--yes` removes the confirmation, for non-interactive use. Otherwise avoid +it: it is the only barrier before the irreversible. + +### What ship does, in order + +1. checks that `gh` is authenticated, that we are on `develop`, that the tree is + clean, and that `develop` is up to date; +2. merges `main` into `develop` (without this the PR would carry a regression); +3. `bump`, which propagates the version to the VS Code extension and the RStudio runner; +4. `cargo check --workspace`, which updates `Cargo.lock`. **On failure the + version is restored**: a number that does not compile does not stay in the + working tree; +5. commit, push, open the PR; +6. wait for the required checks; +7. *confirmation*; +8. merge, then `release`, which repeats its four guards, deliberately + twice: it is the last barrier; +9. `develop` brought back level with `main`. + +### By hand + +The sequence remains decomposable, which is useful for resuming a partially +completed release: + +```bash +git switch develop && git pull +nu publish.nu bump patch +cargo check --workspace +git commit -am "release v0.5.10" && git push origin develop +gh pr create --base main --head develop --fill +gh pr merge --merge --delete-branch=false + +git switch main && git pull +nu publish.nu release --dry-run +nu publish.nu release + +git switch develop && git merge main && git push origin develop +``` + +`release` pushes **only the tag**: `main` is protected, its content only arrives there +through a pull request. If it detects that the local `main` diverges from `origin/main`, +it stops rather than tagging into the void. + +An already published version number is not reused. `release` refuses an existing tag +both locally and remotely, and `cargo publish` would refuse anyway +to overwrite a version present on crates.io. + +`nu publish.nu check` then compares what is actually published on each channel. + +## What the CI does from the tag + +``` +tag vX.Y.Z + │ + ├─ verify ......... refuses if tag ≠ Cargo.toml ≠ editors + │ + ├─ build .......... 8 targets (Linux musl + Linux GNU + Windows + macOS, + │ × x86_64/aarch64) + │ └─ release .... GitHub Release + SHA-256 checksums + │ ├─ docker ..... image from the Linux binary, tags X.Y.Z + latest + │ ├─ rstudio .... R tarball with the fresh binaries embedded + │ └─ docs ....... repository_dispatch → site rebuild + │ + ├─ crates-io ...... typr-core → typr-lsp → typr-cli → typr + ├─ wasm ........... WASM + version.json → playground repo + ├─ vscode ......... .vsix → Marketplace + Open VSX + attached to the release + ├─ vim ............ plugin tarball, attached to the release + ├─ brew ........... generated formula → tap `we-data-ch/homebrew-typr` + ├─ winget ......... generated manifests → PR `microsoft/winget-pkgs` + └─ scoop .......... generated manifest → bucket `we-data-ch/scoop-bucket` +``` + +The branches are independent: if the Marketplace fails, the binaries and +crates.io are still published. + +### Replaying a branch that failed + +```bash +gh workflow run release.yml -f tag=v0.5.9 +``` + +The workflow accepts a tag as input, which avoids having to re-tag. + +**With a limitation you need to know: it reloads the repository *at the tag*.** A +fix pushed afterwards is therefore not taken into account: the replay recompiles +exactly the same tree. + +The replay is for **environment** failures: expired token, registry +unavailable, network. For a **code** failure, you need a fresh tag, hence a +new version. + +That is what happened at v0.5.8: the Docker job failed on the +`Dockerfile` itself, and a v0.5.9 had to be shipped to deliver the fix. +Replaying v0.5.8 would have hit the same file again. + +## The crates.io order is not negotiable + +`typr-cli` declares `typr-lsp = { path = ..., version = "0.5" }`. A `path` +dependency **carrying a version number** requires that the crate exist on the registry +at the time of `cargo publish`. `typr-lsp` must therefore be published before `typr-cli`, +itself before `typr`. + +This is precisely what had blocked the chain: `typr-lsp` never having been +published, `typr-cli` and `typr` stayed at 0.5.5 while `typr-core` moved +to 0.5.7 and the GitHub releases announced 0.5.7. + +## Required secrets + +| Secret | Used by | Where to get it | +|---|---|---| +| `CARGO_REGISTRY_TOKEN` | `crates-io` | crates.io → Account Settings → API Tokens | +| `DOCKERHUB_USERNAME` | `docker` | Docker Hub account name | +| `DOCKERHUB_TOKEN` | `docker` | Docker Hub → Account Settings → Personal access tokens | +| `VSCE_PAT` | `vscode` | Azure DevOps → PAT, scope *Marketplace: Manage* | +| `PLAYGROUND_DEPLOY_TOKEN` | `wasm` | GitHub PAT, scope `repo` on the playground repo | +| `DOCS_DISPATCH_TOKEN` | `docs` | GitHub PAT, scope `repo` on `typr.github.io` | +| `HOMEBREW_TAP_TOKEN` | `brew` | GitHub PAT, scope `repo` on `homebrew-typr` | +| `WINGET_CREATE_GITHUB_TOKEN` | `winget` | GitHub PAT, scope `public_repo` (read by `wingetcreate` in the environment) | +| `SCOOP_BUCKET_TOKEN` | `scoop` | GitHub PAT, write on `scoop-bucket` | + +## The Homebrew tap + +`we-data-ch/homebrew-typr` contains only `Formula/typr.rb`, `README.md` and +`LICENSE`: no binaries. The `brew` job generates the formula from the +`checksums.txt` of the release, has it validated by `brew style` and +`brew audit --strict`, then commits it. The formula is therefore never written by +hand: see `packaging/homebrew/README.md`. + +The repository was created on 30 September 2026 and already contains the formula for +v0.5.12; the CI then keeps it up to date at each tag. If it had to be redone, the gesture +is: + +```bash +gh repo create we-data-ch/homebrew-typr --public --description "Homebrew tap for TypR" +``` + +Two ways to maintain it, to be decided once and for all: + +- **free `main`**: the CI pushes directly, as `vscode` and `wasm` do + for their repositories. The tag is authoritative, and the tap can never be behind. +- **protected `main`, admission via the `we-data-ch/homebrew-typr-autoupdate` repository**: + the CI no longer writes to the tap but to the admission repository, which opens a + PR that we review. `homebrew-typr/typr` remains installable in the meantime, since what + `brew install` reads is the default branch. The tap then becomes impossible to + modify without human review, at the cost of a `brew update-autoupdate` to + install on the maintainers' machines. + +The choice for batch 3 is the first one: it is consistent with the rest of the chain, and +a formula generated by the CI from `checksums.txt` does not need to be reviewed: what +would hold it back is its uniqueness, not its wording. + +## The WinGet and Scoop channels + +No binaries here either: `winget-pkgs` publishes only three YAML files, and the +Scoop bucket only URLs and their SHA-256. Both channels read the **same** +`checksums.txt` as the Homebrew formula, and are rendered by the same script, +`packaging/render.sh`: see `packaging/README.md`. + +What the two jobs do beyond the tap is **actually install** before publishing: + +- `winget` downloads the two Windows archives and compares their fingerprints to + those of the rendered manifests, then passes the folder to `wingetcreate submit`, + which validates it with the official validator before opening the PR; +- `scoop` runs `scoop install` on the local manifest, waits for the `typr` shim, and + requires that `typr --version` reports the version of the release. + +A channel that refuses to install therefore publishes nothing: better a red job +than a manifest nobody can install. + +**The `we-data-ch/scoop-bucket` repository must exist before the first tag.** The +`clone` job fails loudly if it is absent, because a `git push` to a nonexistent URL +would also fail, but three minutes later: + +```bash +gh repo create we-data-ch/scoop-bucket --public --description "Scoop bucket for TypR" +``` + +Missing secrets do not block the release: rendering and local installations +take place, only the publication is skipped with a warning. This is +intentional, so that a tag serves something other than producing a red job. + +The bucket keeps ten versions, purged in order of addition in the history and not +in alphabetical order. `typr update` is for this: without purging, a user who +had not run `scoop update` for a year would see an ever longer list of manifests. + +## No binaries in the repository + +The RStudio runner binaries are assembled by the CI at the time of building +the tarball, never committed. The former `typr_runner` repository weighed 140 MB of `.git` +for 100 KB of source, because of 33 "[auto] updated binaries" commits. +The `coherence` job in `ci.yml` fails if a binary reappears in the index. + +## Local development + +```bash +nu deploy.nu # debug binary → ~/sh/typr +./build-wasm.sh # WASM → neighboring playground, with version.json +nu docker/deploy.nu # local image from the release binary +``` From 809945917eb98681fd450466678f4f5f8162095b Mon Sep 17 00:00:00 2001 From: Vestin Hategekimana <80328639+CyuHat@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:28:47 +0200 Subject: [PATCH 2/2] Clean-up Removing useless files --- STORAGE.db | 0 comp_correction.md | 159 ---- exemples.md | 547 ------------ install_plan.md | 915 -------------------- interface_generic_unification2.md | 337 -------- interface_generic_unification_plan.md | 370 -------- refined_types.md | 1144 ------------------------- refined_types_plan.md | 251 ------ stdlib.md | 426 --------- syntaxe.md | 738 ---------------- 10 files changed, 4887 deletions(-) delete mode 100644 STORAGE.db delete mode 100644 comp_correction.md delete mode 100644 exemples.md delete mode 100644 install_plan.md delete mode 100644 interface_generic_unification2.md delete mode 100644 interface_generic_unification_plan.md delete mode 100644 refined_types.md delete mode 100644 refined_types_plan.md delete mode 100644 stdlib.md delete mode 100644 syntaxe.md diff --git a/STORAGE.db b/STORAGE.db deleted file mode 100644 index e69de29b..00000000 diff --git a/comp_correction.md b/comp_correction.md deleted file mode 100644 index 3c8ccaa9..00000000 --- a/comp_correction.md +++ /dev/null @@ -1,159 +0,0 @@ -# Spécifications de correction compilateur - -> Deux trous du compilateur `typr`, identifiés le **2026-09-09** en passant les 45 blocs -> ` ```typr noplayground ` de la documentation au compilateur (0.5.10). La doc a raison, le -> compilateur a tort : chaque défaut est documenté plus bas avec un repro minimal exécutable et -> des critères d'acceptation. -> -> Implémentation de référence : **`we-data-ch/typr`**, porte d'entrée `typr case add` — un -> correctif sans cas derrière lui n'a rien qui l'empêche de régresser (voir `cases/README.md`). -> Le suivi côté doc de ces deux points : A et B dans `doc_correction.md`. - ---- - -## A · Types singleton pour `bool` et `num` - -**Source.** `docs/reference/types.md:27` (section « Literal types »). Le bloc, qui liste les -types singleton, est présentement `noplayground` parce que la moitié de ses lignes ne compilent -pas. La carte de référence `syntaxe.md:100` liste explicitement les quatre — `3`, `3.14`, `true`, -`"chat"` sont des types valides, plus précis que `int`/`num`/`bool`/`char`. - -### Comportement actuel - -Les littéraux entiers et chaînes reçoivent bien leur type singleton ; les booléens et les -flottants non — ils sont re-typés `bool` / `num`, d'où une *type error* sur une forme que le -parser accepte : - -| Ligne | Résultat | -|---|---| -| `let x: 3 = 3;` | ✅ | -| `let name: "hello" = "hello";` | ✅ | -| `let flag: true = true;` | ❌ `Type error: type true doesn't match type bool` | -| `let x: 3.14 = 3.14;` | ❌ `Type error: type 3.14 doesn't match type num` | - -### Comportement attendu - -Tout littéral utilisé comme type (type singleton) doit être attribué à la valeur littérale -correspondante : - -- `let flag: true = true;` doit compiler sans erreur, avec `flag : true` (singleton) ; -- `let flag: true = false;` doit être rejeté ; -- `let x: 3.14 = 3.14;` doit compiler sans erreur, avec `x : 3.14` (singleton) ; -- `let x: 3.14 = 3.0;` / `let x: 3.14 = 3.141;` doit être rejeté. - -### Repro (à poser sous forme de cas `typr case add`, séparément ou réunis) - -Cas positifs : - -``` -let flag: true = true; -let x: 3.14 = 3.14; -``` - -Cas négatifs (attendus en *type error*) : - -``` -let flag: true = false; -let x: 3.14 = 3.0; -``` - -### Critères d'acceptation - -1. Les quatre types singleton (`3`, `3.14`, `true`, `"chat"`) se lient à leur littéral. -2. L'inférence de type après liaison rapporte le singleton, pas la base (`true` et non `bool`, - `3.14` et non `num`). -3. Les cas négatifs échouent au *type checking* (pas à la syntaxe). -4. `npm run check:examples` passe ; le bloc de `docs/reference/types.md:27` redevient - vérifiable — `noplayground` à retirer. - ---- - -## B · Styles structurels : nom de paramètre de fonction ignoré dans la comparaison de types - -**Source.** `docs/reference/functions.md:131` (section « Closures »). Le bloc est présentement -`noplayground` bien que le code qu'il montre soit du TypR valide. C'est le plus net des deux : -une ligne le reproduit, et il mord dès qu'on annote un retour de fonction. - -### Comportement actuel - -`(int) -> int` se désucre en `fn(a: int) -> int` — paramètre nommé `a` par défaut — et les types -de fonction sont comparés **nom de paramètre compris** : - -``` -let f: (int) -> int <- fn(a: int): int { a }; # ✅ (le nom devine le désucrage) -let f: (int) -> int <- fn(z: int): int { z }; # ❌ -``` - -> ``` -> Type error: The output type of the function don't match it's type annotation -> Expected: fn(a: int) -> int -> Found: fn(z: int) -> int -> ``` - -### Comportement attendu - -Un type de fonction est **structurel** : sa valeur ne dépend pas du nom des paramètres. -`fn(z: int) -> int` doit être accepté partout où `fn(a: int) -> int` l'est, et -`let f: (int) -> int <- fn(z: int): int { z };` doit compiler sans erreur. - -Le nom du paramètre est une commodité d'écriture côté implémentation, pas une information de -type. En l'état, toute annotation de retour de fonction — closure, callback — n'est acceptée que -si l'implémenteur devine le nom choisi par le désucrage. - -### Repro - -Cas positif : - -``` -let f: (int) -> int <- fn(z: int): int { z }; -``` - -Également concerné (le même bug en plus long), le bloc complet de la doc : - -``` -let make_adder <- fn(n: int): (int) -> int { - fn(x: int): int { x + n } -}; - -let add5 <- make_adder(5); -add5(3); # 8 -``` - -Cas négatifs (à conserver : le typage structurel doit rester strict sur les **types**, pas sur -les noms) : - -``` -let f: (int) -> int <- fn(a: char): int { 1 }; # ❌ type de paramètre différent -let f: (int) -> int <- fn(a: int): char { "x" }; # ❌ type de retour différent -let f: (int, bool) -> int <- fn(a: int): int { a }; # ❌ arité différente -``` - -### Critères d'acceptation - -1. La comparaison de deux types de fonction ignore le nom des paramètres. -2. La comparaison reste stricte sur le type et l'arité des paramètres et sur le type de retour. -3. `npm run check:examples` passe ; le bloc de `docs/reference/functions.md:131` redevient - vérifiable **sans le toucher** — `noplayground` à retirer. - ---- - -## Rappel de méthode - -Pour rejouer un repro localement sans dépôt du compilateur : - -```bash -cd "$(mktemp -d)" -printf 'let flag: true = true;\n' > t.ty -typr check t.ty -``` - -Le premier `typr check` d'un répertoire y écrit `context.json` et `std.ty` (préchargement de la -bibliothèque standard) — c'est normal, et c'est pour ça qu'on travaille dans un répertoire -jetable. - -Après correction du compilateur, côté doc : - -```bash -npm run check:examples # les 180 blocs -npm run check:examples -- --noplayground # ce qui reste exclu -``` \ No newline at end of file diff --git a/exemples.md b/exemples.md deleted file mode 100644 index e29687d0..00000000 --- a/exemples.md +++ /dev/null @@ -1,547 +0,0 @@ -## 1. Les différents moments où ce besoin apparaît - -| Période | Source | Besoin exprimé / observé | Exemple | -| --------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | -| 2013 | R-devel | Déclarer les types des paramètres et retours pour permettre notamment la composition automatique de fonctions dans des IDE/workflows | `slice(x, pivot, inclusive) : a × numeric × logical → list` ([Seminar for Statistics][1]) | -| 2015–2017 | Checkmate / assertions | Les développeurs écrivent manuellement des contrats de type, taille, NA, plage | chaîne scalaire, entier positif de longueur 3, proportion dans `[0,1]` ([The R Journal][2]) | -| 2018 | vctrs | Besoin de rendre les fonctions **type-stable** et **size-stable**, afin qu’on puisse prédire le résultat sans exécuter le code | prédire le type et la taille de sortie depuis ceux des entrées ([vctrs.r-lib.org][3]) | -| 2019 | Stack Overflow | Demande explicite de types pour paramètres et retours | fonction de probabilité prenant un entier/numérique et retournant un numérique ([Stack Overflow][4]) | -| 2019 | Turcotte & Vitek | Déterminer quels types seraient réellement utiles dans R | distinction scalaire/vecteur, longueur des vecteurs, structure des data frames, coercition/recyclage | -| 2020 | étude empirique sur 412 packages | Mesurer ce que les programmes R utilisent réellement | beaucoup de scalaires, classes, vecteurs, matrices, data frames ; polymorphisme important ([alexi turcotte][5]) | -| 2020 | même étude | Le typage pourrait remplacer les contrôles de paramètres écrits à la main | `is.character(x)`, `length(x)==1`, `!is.na(x)` ([alexi turcotte][5]) | -| 2025 | R-devel | Demande à nouveau très simple : ajouter une syntaxe pour documenter les types des arguments | `f <- function(a:chr, b:data.frame, c:logi) ...` ([Seminar for Statistics][6]) | -| 2025 | R-devel / Simon Urbanek | Confirmation qu’un mécanisme expérimental existait déjà dans R5 | `function(a, character x, integer y=1:10)` ([Seminar for Statistics][7]) | -| 2026 | useR! | Nouveau système de typage statique avec inférence, classes, data frames, polymorphisme | types ensemblistes + inférence ; exemple de fonction surchargée ([DRA event platform (Indico) (Indico)][8]) | -| 2026 | D3S Prague | Besoin de typer les transformations **de données**, pas seulement les variables | `group_by → summarize → ggplot`, avec schéma de data frame et contraintes de `aes()` ([d3s.mff.cuni.cz][9]) | - -Il y a donc une continuité étonnamment forte sur plus de dix ans. - ---- - -# 2. Le premier besoin : « qu'est-ce que cette fonction accepte et retourne ? » - -C'est probablement le besoin le plus universel. - -En 2019, quelqu'un demande très explicitement comment déclarer les types des paramètres et du retour d'une fonction R, avec l'idée suivante : « `k` doit être un integer/numeric/... et la fonction retourne un numeric ». ([Stack Overflow][4]) - -Et dès 2013, sur R-devel, le besoin était déjà formulé de manière plus ambitieuse : connaître les types des arguments et des retours pour pouvoir composer automatiquement les fonctions dans une sorte de workflow graphique. ([Seminar for Statistics][1]) - -C'est important pour TypR : **la signature de fonction est donc une motivation historique très robuste**. - -Le problème réel n'est pas nécessairement : - -```r -add <- function(x: double, y: double): double -``` - -mais plutôt quelque chose comme : - -```text -read_data : String → DataFrame -clean_data : DataFrame → DataFrame -fit_model : DataFrame → Model -predict : Model × DataFrame → Vector -``` - -Autrement dit : **rendre visible le contrat entre les blocs du programme**. - ---- - -# 3. Mais le vrai problème R commence avec scalaire ≠ vecteur - -C'est l'un des résultats les plus intéressants de la recherche empirique. - -Dans R, `5` et `c(5, 6, 7)` ont le même `typeof`: `double`. Pourtant, certaines fonctions veulent véritablement une valeur unique. - -L'étude de Turcotte et al. trouve que **33,33 % des paramètres observés sont des scalaires** dans leur corpus. ([alexi turcotte][5]) - -Ils donnent un exemple très concret : - -```r -hankel.matrix <- function(n, x) { - if (n != trunc(n)) - stop("n is not an integer") - - if (!is.vector(x)) - stop("x is not a vector") - - if (length(x) < n) - stop("x is too short") - - ... -} -``` - -Ici, le développeur sait déjà quelque chose de beaucoup plus précis que : - -```text -n : numeric -x : vector -``` - -Il sait : - -```text -n : integer scalar -x : numeric vector -length(x) >= n -``` - -C'est exactement le genre d'information qu'un système de types R intéressant doit être capable d'exprimer. ([alexi turcotte][5]) - ---- - -# 4. Le cas encore plus R : les longueurs de vecteurs - -Le papier de 2019 prend un exemple particulièrement révélateur : - -```r -norm <- function(data) { - data / c(1, 2, 4) -} -``` - -Puis : - -```r -norm(c(5, 4, 8)) -``` - -fonctionne comme attendu, mais : - -```r -norm(c(5, 2)) -``` - -déclenche le **recycling** de R et produit implicitement une valeur supplémentaire. - -Les auteurs proposent alors plusieurs niveaux de typage possibles : - -```text -(double, double) -> double -(integer, integer) -> integer -``` - -ou quelque chose de plus précis : - -```text -double[2] × double[2] -> double[2] -double[n] × double[n] -> double[n] -``` - - - -C'est un point extrêmement important : **la taille n'est pas seulement une propriété de données ; dans R, elle peut changer la sémantique d'une opération.** - ---- - -# 5. Les data frames : probablement le besoin le plus spécifiquement “R” - -C'est là que la recherche devient particulièrement intéressante pour ton projet. - -Le papier de 2019 dit explicitement que les data frames sont au cœur des analyses R et qu'un type de data frame suffisamment riche pourrait vérifier : - -* les noms des colonnes ; -* le type de chaque colonne ; -* le nombre de lignes ; -* la compatibilité entre plusieurs data frames. - -L'étude empirique de 2020 confirme leur importance : parmi les classes les plus fréquentes dans les appels observés, les matrices représentent environ 12 %, les `data.frame` 7,5 %, les formules 2 %, les facteurs 2 % et les tibbles 2 %. Les classes représentent environ 31 % des types d'arguments observés. ([alexi turcotte][5]) - -Et l'étude montre un exemple beaucoup plus proche des vrais bugs R : - -```r -df <- data.frame(revenue = c(100, 200, 300)) - -vat <- df$reveneu * 0.21 -``` - -La faute de frappe `reveneu` produit `NULL`, puis le problème peut réapparaître beaucoup plus loin sous une forme comme un résultat numérique inattendu. Le système présenté en 2026 propose justement qu'un type de data frame connaissant la colonne `revenue` puisse **refuser `df$reveneu` immédiatement**. ([DRA event platform (Indico) (Indico)][10]) - -À mon avis, **c'est beaucoup plus représentatif du besoin R qu'un simple `integer` contre `character`.** - ---- - -# 6. Le cas `factor` montre que “type” signifie aussi sémantique - -Le poster 2026 donne cet exemple : - -```r -scores <- factor(c("1", "2", "3")) -wt <- c(0.2, 0.3, 0.5) - -weighted.mean(scores, wt) -``` - -R accepte l'appel jusqu'à produire : - -```text -Warning: '*' not meaningful for factors -NA -``` - -Le problème est que `scores` est stocké comme des codes entiers, mais **sémantiquement ce n'est pas un vecteur numérique ordinaire**. Le système proposé peut donc distinguer un `int` d'un entier classique et refuser l'appel à `weighted.mean`. ([DRA event platform (Indico) (Indico)][10]) - -C'est une autre leçon essentielle : - -> Les utilisateurs de R ne veulent pas seulement typer la représentation mémoire ; ils veulent typer **ce que les données signifient**. - -C'est exactement la raison pour laquelle S3, `Date`, `POSIXct`, `factor`, matrices, tibbles, etc. sont si importants. - ---- - -# 7. Les assertions montrent ce que les développeurs veulent vraiment exprimer - -C'est probablement la donnée la plus utile pour ta question. - -L'étude de 2020 regarde les `stopifnot()` et assertions réellement écrits dans les packages. - -Exemple : - -```r -stopifnot( - is.character(x), - length(x) == 1L, - !is.na(x) -) -``` - -Ce n'est donc pas simplement : - -```text -x : character -``` - -mais : - -```text -x : character - ∩ length(1) - ∩ non-NA -``` - -L'étude trouve 1 995 assertions dans 153 des 412 packages analysés. Leur système pouvait **complètement remplacer 50,4 %** des assertions et **simplifier 61,3 %** d'entre elles. À l'échelle de tout CRAN, ils comptent 32,3 milliers d'assertions dans 15,9 milliers de fonctions. ([alexi turcotte][5]) - -Checkmate arrive exactement au même endroit, mais avec une DSL d'assertions. Par exemple : - -```text -character scalar -factor de longueur ≥ 1 sans NA -integer de longueur 3 avec valeurs positives -numeric scalar dans [0,1] -numeric vector positif, fini et sans NA -``` - -([The R Journal][2]) - -Donc le besoin pratique peut être formulé ainsi : - -> **« Je veux pouvoir exprimer en une seule spécification ce que je suis actuellement obligé d'écrire avec `is.*`, `length()`, `is.na()`, des comparaisons et des validations personnalisées. »** - -C'est une conclusion très forte. - ---- - -# 8. Ce que l'étude de 2020 dit sur les types réellement utilisés - -Le corpus est particulièrement intéressant parce qu'il ne repose pas sur des opinions : les auteurs ont observé **25 215 fonctions dans 412 packages**. ([alexi turcotte][5]) - -Les catégories les plus fréquentes parmi les paramètres sont notamment : - -| Catégorie | Part approximative | -| ---------------------------- | -----------------: | -| scalaires | 33,3 % | -| classes | 23,1 % | -| vecteurs | 12,4 % | -| `...` | 8,7 % | -| `NULL` | 7,3 % | -| `any` | 7,2 % | -| listes | 3,4 % | -| vecteurs pouvant contenir NA | 2,8 % | - -([alexi turcotte][5]) - -Et **58 % des fonctions** étaient monomorphes selon leur modèle, tandis que 42 % avaient au moins un paramètre ou retour polymorphe. ([alexi turcotte][5]) - -Cela donne une image assez différente de « R est tellement dynamique qu'on ne peut pas le typer ». - -Une très grande quantité du code possède en réalité des **contrats suffisamment réguliers pour être décrits**. - ---- - -# 9. Le point où les travaux récents vont encore plus loin : typer les pipelines de données - -Le projet de D3S est particulièrement révélateur parce qu'il ne part plus du problème abstrait « ajouter des types à R ». - -Il part d'un vrai bout de code R : - -```r -tdec <- summarize( - group_by(titanic, decade = round(age / 10)), - count = sum(survived) -) - -ggplot(tdec, aes(x = decade, y = count)) + - geom_point() -``` - -Et pose exactement les questions qu'un développeur humain se pose : - -```text -titanic.age : numeric ? -titanic.survived : numeric/logical ? - -group_by(...) : quelles colonnes restent ? - -summarize(...) : quelles nouvelles colonnes sont produites ? - -tdec : possède-t-il decade et count ? - -aes(...) : x et y existent-ils ? - -geom_point() : quels champs exige-t-il ? -``` - -Le projet cherche précisément à typer ces transformations de **forme de data frame** et la grammaire de `ggplot2`. ([d3s.mff.cuni.cz][9]) - -À mon sens, c'est aujourd'hui **l'exemple le plus représentatif du “type system for R” spécifique au langage**, parce qu'il combine : - -```text -types des valeurs -+ -longueurs -+ -classes -+ -schéma de data frame -+ -transformation du schéma -+ -polymorphisme -+ -contexte d'évaluation -``` - ---- - -# 10. Et le travail 2026 confirme cette direction - -Le système présenté à useR! 2026 est maintenant fondé sur des **set-theoretic types** et supporte explicitement : - -```text -vecteurs -scalaires -listes -records -classes S3 -data frames -polymorphisme -fonctions génériques -``` - -Le poster donne par exemple : - -```text -df : { id : dbl, ... } -``` - -et peut accepter : - -```r -data.frame(id = 1L:3L) -``` - -mais refuser : - -```r -data.frame(id = c("a", "b", "c")) -``` - -parce que `id` devait être numérique. ([DRA event platform (Indico) (Indico)][10]) - -Il introduit aussi des fonctions comme : - -```text -nrow : vector | array | data.frame → integer -``` - -et : - -```text -df$col : data.frame containing col:'a → 'a -``` - -C'est une manière très intéressante de formaliser une chose que les développeurs R font déjà mentalement. ([DRA event platform (Indico) (Indico)][10]) - ---- - -# 11. Le problème du “type” est donc en réalité constitué de 5 couches - -Après avoir regroupé tout ça, je réduirais ce que les gens cherchent à : - -```text -1. Valeur - double, integer, character, logical... - -2. Cardinalité / forme - scalar, vector, matrix, length(n)... - -3. Valeur manquante - T vs T pouvant contenir NA - T vs NULL - -4. Structure / sémantique - factor, Date, POSIXct, data.frame, Model... - -5. Contrat relationnel - x et y ont le même type - output dépend du type de x - dataframe après transformation possède telles colonnes -``` - -C'est remarquable parce que cela converge presque exactement avec le poster 2026 : il présente explicitement le **storage type**, la **shape** et les **S3 classes** comme des informations que R cache actuellement au point d'utilisation. ([DRA event platform (Indico) (Indico)][10]) - ---- - -# 12. Quel est alors l'exemple le plus représentatif ? - -Je distinguerais deux réponses. - -### Le meilleur exemple de typage “général” - -Une fonction R avec un vrai contrat : - -```text -function( - x : numeric vector, - n : positive integer scalar -) -> numeric vector -``` - -avec éventuellement : - -```text -length(x) >= n -``` - -C'est représentatif parce qu'il correspond directement aux assertions que les développeurs écrivent déjà dans leurs fonctions. ([alexi turcotte][5]) - -### Le meilleur exemple de typage “R” - -Je choisirais plutôt **un pipeline de data frame** : - -```r -penguins - |> filter(...) - |> mutate(...) - |> summarise(...) - |> ggplot(aes(...)) -``` - -avec un système capable de savoir : - -```text -penguins - : { - species : factor, - body_mass_g : double, - ... - } - -filter(...) - : conserve le schéma - -mutate(...) - : ajoute/modifie des colonnes - -summarise(...) - : change le nombre de lignes - + produit un nouveau schéma - -ggplot(...) - : exige des colonnes existantes -``` - -C'est celui qui capture le mieux ce qui est **particulier à R**, plutôt que de simplement reproduire TypeScript/Rust/Haskell. - ---- - -# 13. Et pour TypR, je pense que l'exemple encore plus fort serait celui-ci - -En combinant les résultats de toutes ces sources : - -```text -type Customer = { - id: int, - name: chr, - revenue: dbl -} - -load_customers : path → DataFrame - -normalize : - DataFrame<{ revenue: dbl, ... }> - → DataFrame<{ revenue: dbl, ... }> - -average_revenue : - DataFrame<{ revenue: dbl, ... }> - → dbl1 -``` - -Puis : - -```text -customers - |> load_customers - |> normalize - |> average_revenue -``` - -Et le système pourrait détecter : - -```text -df$reveneu -``` - -ou : - -```text -average_revenue(df_with_revenue = character) -``` - -ou encore : - -```text -mean(revenue) -``` - -lorsque `revenue` n'existe pas dans le schéma. - -Cela combine **les trois motivations qui reviennent le plus souvent** : - -> **contrats de fonctions + structure des données + prévention des erreurs dans les pipelines.** - -Et c'est beaucoup plus proche de ce que les travaux R récents essaient effectivement de résoudre que l'image classique « R mais avec `int`, `bool`, `string` ». - -Un dernier élément renforce cette interprétation : vctrs affirme explicitement que lorsqu'un développeur ne peut pas prédire le **type et la taille** des variables simplement en lisant le code, cela rend le code difficile à raisonner sur papier. Leur notion de type/size stability est justement née de ce problème de lecture du code. ([vctrs.r-lib.org][3]) - -### Ma synthèse - -Je formulerais donc le besoin utilisateur comme : - -> **Les développeurs R ne demandent pas principalement de pouvoir déclarer des types. Ils demandent de pouvoir déclarer et vérifier les invariants que R laisse actuellement implicites : le type et la cardinalité des valeurs, leur structure sémantique, le schéma des data frames et les relations entre les entrées et sorties des fonctions.** - -C'est, à mon avis, une formulation beaucoup plus solide pour positionner TypR. - -Les sources primaires les plus importantes que j'ai trouvées sont l'étude empirique OOPSLA 2020, le travail fondateur de 2019, le projet D3S actuel et le poster useR! 2026. ([alexi turcotte][5]) - -[1]: https://stat.ethz.ch/pipermail/r-devel/2013-August/067331.html?utm_source=chatgpt.com "[Rd] Type annotations for R function parameters." -[2]: https://journal.r-project.org/articles/RJ-2017-028/?utm_source=chatgpt.com "The R Journal: checkmate: Fast Argument Checks for Defensive R Programming" -[3]: https://vctrs.r-lib.org/articles/stability.html?utm_source=chatgpt.com "Type and size stability • vctrs" -[4]: https://stackoverflow.com/questions/58781871/r-programming-is-it-possible-to-declare-return-or-parameter-types-of-functions?utm_source=chatgpt.com "R programming: Is it possible to declare return or parameter types of functions? - Stack Overflow" -[5]: https://reallytg.github.io/files/papers/types-for-r-oopsla20-final.pdf "Designing Types for R, Empirically" -[6]: https://stat.ethz.ch/pipermail/r-devel/2025-September/084164.html?utm_source=chatgpt.com "[Rd] Declaring Types at Function Declaration" -[7]: https://stat.ethz.ch/pipermail/r-devel/2025-November/084223.html?utm_source=chatgpt.com "[Rd] Declaring Types at Function Declaration" -[8]: https://events.digital-research.academy/event/109/contributions/482/ "useR! 2026 (6-July 9, 2026): A Type System for the R Language · DRA event platform (Indico)" -[9]: https://d3s.mff.cuni.cz/projects/primus24/?utm_source=chatgpt.com "Type systems for data-centric programming | D3S" -[10]: https://events.digital-research.academy/event/109/contributions/482/attachments/103/321/A%20Type%20System%20for%20the%20R%20Language.pdf "A Type System for the R Language" diff --git a/install_plan.md b/install_plan.md deleted file mode 100644 index b24dcdd4..00000000 --- a/install_plan.md +++ /dev/null @@ -1,915 +0,0 @@ -# Plan — installation de TypR en une commande - -Objectif : qu'un utilisateur choisisse **un seul canal** parmi quatre, sans -compter sur Cargo, Docker ou un téléchargement manuel depuis la page des -releases. - -Ce plan part de l'existant : les 6 binaires, `checksums.txt` et la CI de release -sont déjà en place. Il ne s'agit donc que d'ajouter la couche de distribution -par-dessus, pas de refaire ce qui marche. - ---- - -## 0. Avancement - -| Lot | État | -|---|---| -| 0a — cibles musl dans la matrice | **fait** | -| 0b — `+crt-static` sur `*-msvc` | **fait** (liaison vérifiée en CI, pas en local) | -| 0c — binaire musl dans Rocky 9 / Debian 12 / Alpine | **fait** | -| 1 — scripts d'installation + hébergement + CI | **fait** (voir ci-dessous) | -| 2 — tests CI sur ces scripts | **couvert par le lot 1** — les jobs `install`, `install-windows` et `install-hosting` sont ceux-là | -| 3 — Homebrew | **fait** (voir ci-dessous) | -| 4 — WinGet et Scoop | **fait** (voir ci-dessous) | -| 5 → 7 | à faire | - -### Ce que le lot 0 a changé - -`.github/workflows/release.yml` est passé de 6 à 8 cibles. - -- `x86_64-unknown-linux-musl` et `aarch64-unknown-linux-musl` sont entrés dans - la matrice, construits par `cross` comme `aarch64-unknown-linux-gnu` l'était - déjà. Plus de cible `*-sys` : le binaire est `static-pie linked`, sans - `PT_INTERP`, et `objdump -T | grep -c GLIBC_` renvoie **0**. -- Les deux cibles GNU sont sorties de `ubuntu-latest` pour `ubuntu-22.04`. Le - plancher de glibc de la voie de repli est ainsi figé à 2.35 au lieu de monter - tout seul à ~2.42 en novembre 2026 avec le runner. -- Les deux cibles `*-msvc` reçoivent `-C target-feature=+crt-static` par - `RUSTFLAGS`. Comme `--target` est toujours passé, cargo n'applique pas la - variable aux build scripts ; le crate C embarqué (oniguruma, via `syntect`) - est donc compilé en `/MT` et non en `/MD` — `cc` lit `crt-static` dans - `CARGO_CFG_TARGET_FEATURE` pour choisir. Sans cela, le runtime du - VC++ Redistributable revenait par la porte du code C. - -### Mesures (30 septembre 2026) - -Binaire `x86_64-unknown-linux-musl` construit par `cross`, profil `release` : - -| Système | Avant (GNU) | Après (musl) | -|---|---|---| -| Rocky Linux 9 (glibc 2.34) | `GLIBC_2.39 not found` | `typr-cli 0.5.12` | -| Debian 12 (glibc 2.36) | `GLIBC_2.39 not found` | `typr-cli 0.5.12` | -| Ubuntu 22.04 (glibc 2.35) | `GLIBC_2.39 not found` | `typr-cli 0.5.12` | -| Alpine 3.20 (musl) | non exécutable | `typr-cli 0.5.12` | -| Debian 13, Ubuntu 24.04 | `typr-cli 0.5.12` | `typr-cli 0.5.12` | - -`typr check` produit des diagnostics identiques à ceux du binaire glibc sur le -même fichier : le comportement n'a pas bougé, seul le lien a changé. - -### Deux garde-fous dans la CI - -La propriété qui fait marcher ces binaires — la liaison statique — est -invisible dans le code : rien ne la casse quand elle disparaît, et c'est -précisément le mode de défaillance qu'on cherche à supprimer. Deux étapes -l'interdisent désormais. - -- `Runtime C statique (Windows)` lit la table d'imports PE avec `dumpbin` et - échoue si `VCRUNTIME140`, `MSVCP140` ou un `api-ms-win-crt-*` réapparaît. -- `Binaire musl statique et exécutable` échoue si le binaire musl déclare un - `PT_INTERP` — un binaire musl qui trahirait son interpréteur ne tournerait - que sur Alpine — puis lance `typr --version` dans `rockylinux:9`, - `debian:12` et `alpine:3.20`. - -La seconde remplace la checklist de la section 4 : le test qui compte n'a pas -lieu d'être fait à la main une fois, mais à chaque release. - -### Reste à confirmer en CI - -La liaison `+crt-static` sur `x86_64-pc-windows-msvc` reste **non vérifiée -localement** : ni le linker MSVC ni le SDK ne sont disponibles ici. `rustc` -accepte le flag pour les deux cibles `*-msvc` (vérifié jusqu'à l'émission des -métadonnées), et le garde-fou `dumpbin` tranchera à la prochaine release. - -### Ce que le lot 1 a livré - -Sept fichiers sous `install/`, tous du texte, aucun binaire. - -| Fichier | Contenu | -|---|---| -| `install/install.sh` | `sh` POSIX : musl par défaut, GNU sur `--gnu`, SHA-256, stable/beta | -| `install/install.ps1` | PowerShell 5.1 et 7 : `.zip`, SHA-256, PATH par registre | -| `install/README.md` | usage, options, codes de sortie, dépannage | -| `install/tests/run-tests.sh` | 59 assertions contre un serveur de fixtures | -| `install/tests/run-tests.ps1` | 49 assertions, plus PSScriptAnalyzer | -| `install/tests/fixture-server.py` | `/releases/latest`, `releases.atom`, archives, `checksums.txt` | -| `install/tests/Dockerfile` | image PowerShell 7.4 + PSScriptAnalyzer + shellcheck | - -Trois jobs de CI : `install` (shellcheck, `dash -n`, les deux suites), -`install-windows` (le script sous Windows PowerShell 5.1 réel), et -`install-hosting` (copie vers `typr.github.io/static/install/`). - -**59 + 49 assertions passent** ; shellcheck est propre sur les deux scripts -shell. Le vrai test d'installation a réussi en local : -`TYPR_INSTALL_DIR=$(mktemp -d) ./install/install.sh --gnu --version v0.5.12` -puis `typr-cli 0.5.12`. - -Cinq défauts réels sont sortis des tests, tous invisibles à la lecture : - -- `Try-Save-File` rendait `$null` — donc falsy — sur un téléchargement réussi, - parce qu'une fonction PowerShell renvoie ce que son `try` a émis, et un - succès n'émet rien. Chaque installation Windows échouait. -- `$input` est une variable automatique PowerShell. Y écrire pouvait faire lire - un `$null` à `CopyTo` et rendre le téléchargement silencieusement infructueux. -- Le fichier n'avait pas de BOM UTF-8. PowerShell 5.1 lit un `.ps1` sans BOM - en ANSI avec la page de code de la machine : sur un Windows français ou - allemand, chaque accent de ce fichier devenait illisible. Le BOM est - maintenant testé explicitement, sinon un reformatage l'effacerait en silence. -- Une ligne `checksums.txt` au SHA malformé était signalée « aucune ligne pour - cette archive » au lieu de « ligne malformée ». Deux diagnostics très - différents — release mal publiée contre fichier corrompu — se confondaient. - `install.sh` et `install.ps1` nomment désormais la même cause. -- Le harnais de test perdait les codes de retour : `& script.ps1` ne propage - pas le `exit` du script appelé au processus. Le refus d'un tag illisible (2) - était indiscernable d'un refus d'environnement (1). - -Deux points restent **non vérifiés localement**, et la CI les couvre sans -pretendre le contraire : la liaison `+crt-static` (ci-dessus) et le démarrage -d'un binaire Windows réel — le runner n'a pas de VC++ Redistributable, et la -fixture n'est de toute façon pas un exécutable Windows. - -### Ce que le lot 3 a livré - -La formule, ses deux variantes et son générateur — sous `packaging/homebrew/`, -toujours aucun binaire dans le dépôt. - -| Fichier | Contenu | -|---|---| -| `Formula/typr.rb.in` | `desc`, `homepage`, `license`, `livecheck`, `on_macos` | -| `parts/on_linux.rb.in` | le bloc `on_linux` musl, inséré seulement s'il existe | -| `parts/on_linux_absent.rb.in` | le commentaire qui le remplace sinon | -| `README.md.in` | le README du tap ; sa description et son URL sont **relues dans la formule rendue**, pas recopiées | -| `render.sh` | `sh` POSIX : tag + `checksums.txt` → formule + README | -| `tests/run-tests.sh` | 81 assertions, sans réseau ni brew | -| `README.md` | le mode d'emploi du canal | - -`render.sh` ne fait qu'une chose, et c'est le cœur du lot : **lire les -empreintes dans le `checksums.txt` de la release**. La formule est donc -impossible à publier avec une empreinte recopiée — la seule façon de l'obtenir -est de modifier le générateur, qui est relu et testé à chaque PR. - -Le tag est pris avec ou sans `v` initial ; `--checksums` accepte une URL ou un -fichier local ; **rien n'est écrit** dans `--out` si le rendu échoue en cours de -route, pour qu'une formule tronquée ne puisse pas être commitée par erreur. - -Deux jobs de CI. `packaging` tourne sous Linux : shellcheck, `dash -n`, -`bash -n`, la suite. `packaging-audit` tourne sous macOS et fait passer la -formule rendue à `brew style` et `brew audit --strict` — sur une PR, donc, et -non à la release. C'est le seul endroit où le DSL Homebrew de la formule peut -être jugé, aucun runner Linux n'ayant Homebrew. - -Le job `brew` de `release.yml` reprend le même chemin sur le `checksums.txt` -réellement publié, **télécharge une archive et compare son empreinte à celle -inscrite dans la formule** avant de commiter vers `we-data-ch/homebrew-typr` — -là où une erreur de sélection se verrait enfin. Sans `HOMEBREW_TAP_TOKEN`, la -validation a lieu quand même et seule la publication est sautée, avec un -avertissement ; un refus de Homebrew arrête le job. - -**81 assertions passent.** Cinq mutations du générateur ont été essayées pour -vérifier que la suite a vraiment du mordant — c'est elle qui l'affirme, pas la -relecture du gabarit : - -| Mutation | Détectée par | -|---|---| -| les deux `sha256` macOS intervertis | les deux blocs `url`+`sha256`, vérifiés côte à côte | -| garde-fou des marqueurs `@@` supprimé | 18 assertions | -| cible GNU choisie pour Linux | 12 assertions | -| bloc `on_linux` désindentation | l'imbrication `on_linux` / `on_intel` | -| garde-fou du retour à la ligne final supprimé | 2 assertions | - -La troisième a été trouvée en écrivant la suite : la version précédente -vérifiait que les quatre empreintes *apparaissaient* dans le fichier, ce que -satisfait aussi deux cibles interverties. La désindentation, elle, venait de la -normalisation du gabarit — invisible pour tout test qui ne compte pas les -espaces, et rattrapée par `brew style` dix minutes plus tard si l'on n'y prenait -garde. - -**Ce que le lot n'a pas pu faire, et le dit.** Aucune version publiée ne contient -de binaire musl : `v0.5.12`, la dernière, n'a que six artefacts, tous GNU ou -Apple. Le générateur produit donc, pour elle, une formule **macOS seule**, et le -README du tap l'annonce au lieu de promettre Linux. Aucun repli GNU n'a été -ajouté : ce serait réintroduire, par la porte de Homebrew, le défaut -`GLIBC_2.39 not found` que le lot 0 vient de corriger partout ailleurs. Le bloc -`on_linux` réapparaîtra seul, à la première release qui publiera du musl. - -Reste une seule chose à faire, et elle n'est pas dans ce dépôt : -`HOMEBREW_TAP_TOKEN` (PAT GitHub, scope `repo` sur `we-data-ch/homebrew-typr`) -dans les secrets de la CI. Tant qu'il manque, le job `brew` valide la formule et -saute la publication, avec un avertissement. - -Le dépôt `we-data-ch/homebrew-typr` a été créé le 30 septembre 2026, vide puis -amorcé avec la formule v0.5.12 rendue depuis le `checksums.txt` de cette release -— `brew install we-data-ch/typr/typr` fonctionne donc dès aujourd'hui sur macOS, -et la prochaine release remplacera ce fichier par le sien, ce qui exercera le -chemin complet `release.yml` → tap. - -### Ce que le lot 4 a livré - -Les deux canaux sont rendus par **un seul script**, `packaging/render.sh`, au -niveau `packaging/` — et non deux sous `packaging/winget/` et `packaging/scoop/` -comme le plan les posait. La raison est celle qui vaut partout ailleurs dans ce -dépôt : les deux canaux lisent le **même** `checksums.txt` et doivent donc -pouvoir être cassés ensemble. Un générateur par canal laisserait passer le jour -où l'un des deux est régénéré et pas l'autre — et les deux canaux -désinstalleront la même version, avec des empreintes différentes. - -```sh -packaging/render.sh --tag v0.5.12 --out /tmp/manifests # 6 fichiers -``` - -| Fichier | Contenu | -|---|---| -| `packaging/winget/*.yaml.in` | version + installateurs (`x64`, `arm64`) + locale `en-US` | -| `packaging/scoop/typr.json.in` | manifeste Scoop | -| `packaging/scoop/README.md.in` | README du bucket, relu dans le manifeste | -| `packaging/tests/run-tests.sh` | 122 assertions, dont la validation par les schémas officiels | -| `packaging/tests/validate-schemas.py` | schémas WinGet et Scoop, en cache, `--offline` disponible | - -Le job `packaging-win` de `ci.yml` rend depuis une fixture et fait valider par les -schémas officiels. Les jobs `winget` et `scoop` de `release.yml` rendent depuis le -`checksums.txt` de la release, **vérifient les deux archives téléchargées**, puis -publient : PR `winget-pkgs` via `wingetcreate submit` d'un côté, commit dans le -bucket de l'autre. Le job `scoop` va plus loin et **installe** : `scoop install` -sur le manifeste local, puis `typr --version` doit annoncer la version de la -release. - -Trois défauts ont été trouvés en faisant, que rien ne montrait à la relecture. -Les trois sont couverts par la suite, et détaillés dans `packaging/README.md` : - -- **`ReleaseDate` sans guillemets** — lu par un analyseur YAML 1.1, `2026-01-02` - est une *date* ; le schéma WinGet exige une *chaîne*. 23 tests cassent si le - guillemet disparaît. -- **Une clé `_comment_urls` dans le manifeste Scoop** — le schéma de Scoop refuse - toute clé inconnue et n'accorde que `_comment` ; le bucket n'aurait pas - installé. -- **`url` et `hash` réordonnés** — les deux tableaux sont alignés par index chez - Scoop ; c'est un défaut qui ne se voit qu'à l'installation, sur une - architecture ou pas l'autre. - -**Deux affirmations de ce plan étaient fausses.** Elles sont corrigées en §6 : - -- `InstallLocation` n'est pas un dossier de destination, c'est un *argument* - passé à l'installeur — et un zip n'a pas d'installeur. WinGet installe sous - `%LOCALAPPDATA%\Microsoft\WinGet\Packages\…`. La case « même dossier que - `install.ps1` » de la vérification est donc **abandonnée**, pas ratée : - `irm | iex` reste le canal qui promet `%LOCALAPPDATA%`. -- « `.installer.yaml` **si signature** » : ce fichier est obligatoire, signé ou - non — c'est lui qui décrit les installateurs. `SignatureSha256` n'a pas sa - place ici : les archives ne sont pas signées, et WinGet n'en exige pas pour - publier. - -Sur ARM64, WinGet associe `arm64` au binaire natif. Scoop, lui, cherche la -chaîne littérale `arm64` dans le manifeste et ne trouve pas `aarch64` : le -binaire x86_64 y tourne par émulation, ce que Windows 11 sait faire. Le plan -annonçait une sélection native par architecture ; le bucket fournit **les deux** -URL et laisse Scoop choisir son miroir, ce qui est déjà le natif quand il le prend. -Ajouter un bloc `architecture.arm64` décoratif aurait coûté une illusion — et un -risque de régression sur les vraies installations ARM64, ce qui vaut mieux éviter. - -Reste à faire, et ce n'est pas dans ce dépôt : créer -[`we-data-ch/scoop-bucket`](https://github.com/we-data-ch/scoop-bucket) (le job -échoue bruyamment s'il n'existe pas) et définir les secrets -`WINGET_CREATE_GITHUB_TOKEN` (scope `public_repo`) et `SCOOP_BUCKET_TOKEN`. Tant -qu'ils manquent, les deux jobs rendent, valident, téléchargent et vérifient les -archives, puis sautent la publication avec un avertissement. - ---- - -## 1. État actuel - -`.github/workflows/release.yml` produit, à partir d'un unique tag `vX.Y.Z` : - -| Cible | Artefact | -|---|---| -| `x86_64-unknown-linux-gnu` | `typr-vX.Y.Z-x86_64-unknown-linux-gnu.tar.gz` | -| `aarch64-unknown-linux-gnu` | `typr-vX.Y.Z-aarch64-unknown-linux-gnu.tar.gz` | -| `x86_64-pc-windows-msvc` | `typr-vX.Y.Z-x86_64-pc-windows-msvc.zip` | -| `aarch64-pc-windows-msvc` | `typr-vX.Y.Z-aarch64-pc-windows-msvc.zip` | -| `x86_64-apple-darwin` | `typr-vX.Y.Z-x86_64-apple-darwin.tar.gz` | -| `aarch64-apple-darwin` | `typr-vX.Y.Z-aarch64-apple-darwin.tar.gz` | - -Plus `checksums.txt` au format `sha256sum` (`release.yml:157`), et une version -stable (`X.Y.Z`) et une version instable (`X.Y.Z-alpha.N`) qui est marquée -`prerelease` (`release.yml:169`). - -Ce qui manque est uniquement l'agent qui transforme ces fichiers en une -commande. Le reste du plan est de la distribution, pas de la compilation. - ---- - -## 2. Décisions - -### Ce qu'on fait - -| Canal | Commande | OS | -|---|---|---| -| Script Unix | `curl -fsSL …/install.sh \| sh` | macOS, Linux | -| Script PowerShell | `irm …/install.ps1 \| iex` | Windows | -| Homebrew | `brew install we-data-ch/typr/typr` | macOS, Linux | -| WinGet | `winget install we-data-ch.TypR` | Windows | -| Scoop | `scoop install typr` | Windows | - -### Ce qu'on ne fait pas, et pourquoi - -- **`.msi` / `.pkg` (installeur graphique)** — `.pkg` macOS non notarisé produit - « TypR est endommagé » pour tout le monde ; la notarisation exige un compte - Apple Developer payant. Un binaire posé par `curl | sh` **n'a pas** l'attribut - `com.apple.quarantine` (seuls les navigateurs le posent) et passe sans - friction. Un `.msi` non signé se heurte au même mur avec SmartScreen. Le seul - cas qui le justifierait est un déploiement d'entreprise par GPO — à rouvrir - si ce cas se présente. -- **Code signing / notarization des binaires** — même raison, hors de portée - d'un projet open source sans certificat payant. -- **Wrapper npm** (`npm i -g typr`) — la technique esbuild/biome fonctionne - partout, mais impose Node à l'utilisateur. L'audience TypR (R, data) n'a pas - Node acquis. À réexaminer plus tard, pas comme canal principal. - -### Deux décisions à confirmer - -Les questions n'ont pas encore reçu de réponse ; le plan part sur les valeurs -par défaut ci-dessous. - -1. **Emplacement** — `~/.local/bin` (défaut retenu). Zéro `sudo`, aucun - privilège. L'installateur affiche un avertissement si le dossier n'est pas - dans le `PATH` au lieu d'échouer. Homebrew reste le canal « propre » pour qui - veut une installation système. - *Alternative :* `/usr/local/bin` avec `sudo`, plus proche des conventions - mais qui casse les installations en CI et dans les conteneurs. - -2. **Hébergement des scripts** — GitHub Pages sur le dépôt de documentation - existant (`we-data-ch.github.io/typr.github.io/install/install.sh`), ce qui ne - coûte aucune infrastructure. Un vrai domaine (`typr.sh`) est plus joli mais - demande DNS et redirection ; à faire plus tard si le besoin se fait sentir. - ---- - -## 3. Points de vigilance avant de commencer - -### 3.1 Le glibc des binaires Linux — défaut confirmé, à corriger avant tout - -Le diagnostic a été exécuté sur le binaire **publié** en `v0.5.12`. Le défaut -est réel, et il bloque l'installation sur une large part des machines. - -`objdump -T` sur `typr-v0.5.12-x86_64-unknown-linux-gnu` : - -``` -GLIBC_2.39 pidfd_spawnp (faible) -GLIBC_2.39 pidfd_getpid (faible) -``` - -Exécution réelle dans des conteneurs qui n'ont rien de TypR : - -| Système | glibc / libc | Résultat | -|---|---|---| -| Ubuntu 24.04 (le runner) | 2.39 | fonctionne | -| Debian 13 (trixie) | 2.41 | fonctionne — `typr-cli 0.5.12` | -| Ubuntu 22.04 | 2.35 | **`GLIBC_2.39 not found`** | -| Debian 12 (bookworm) | 2.36 | **`GLIBC_2.39 not found`** | -| Rocky Linux 9 | 2.34 | **`GLIBC_2.39 not found`** | -| Alpine 3.20 | musl | non exécutable — aucun build musl | - -Deux enseignements. - -**Les symboles faibles ne sauvent rien.** `pidfd_spawnp` et `pidfd_getpid` -(venus de `std::process::Command`) sont marqués faibles : on pourrait croire que -le chargeur les résoudra à `NULL` et que le repli prendra le relais. Testé : le -chargeur refuse quand même, parce que c'est la **référence versionnée** qui est -absente, pas seulement le symbole. Il n'y a donc aucun contournement à tenter — -seule une chaîne de compilation avec un plancher plus bas règle le problème. - -**Rocky 9 échoue.** C'est le point le plus douloureux : RHEL / Rocky / AlmaLinux -9 est une base très répandue chez les utilisateurs R professionnels, en -particulier en entreprise. Debian 12 et Ubuntu 22.04 ne sont pas rares non plus. - -**Et le plancher va monter tout seul.** `ubuntu-latest` passe à **Ubuntu 26.04 -en novembre 2026** (annonce de `actions/runner-images#14748`). Sans action, la -glibc requise passera de 2.39 à ~2.42 et le terrain cassera davantage, à chaque -release, sans qu'une ligne de code soit modifiée. C'est un défaut qui s'aggrave -silencieusement — la raison de plus pour traiter le fond, pas seulement le -symptôme. - -**Décision recommandée : ajouter les deux cibles musl.** Elles sont liées -stiquement, donc sans contrainte de glibc du tout — elles fonctionnent sur -Rocky 9, Debian 12, Ubuntu 22.04 **et** Alpine. Cela règle le problème de glibc -et le cas Alpine d'un seul geste, pour deux cibles de plus dans la matrice. - -`cross` est déjà utilisé par la matrice pour `aarch64-unknown-linux-gnu` -(`release.yml:110`), et son image musl est disponible : rien de nouveau à -installer, seulement deux entrées à ajouter. - -| Cible | Rôle | Priorité | -|---|---|---| -| `x86_64-unknown-linux-musl` | remplaçante de la version GNU sur x86_64 | 1 | -| `aarch64-unknown-linux-musl` | remplaçante sur ARM | 1 | - -Deux variantes possibles si l'on veut rester sur des binaires glibc dynamiques, -par ordre de préférence : - -- `cargo-zigbuild` en visant `glibc = "2.28"` — plancher très bas, couvre - RHEL 8. Mais ne couvre pas Alpine, et ajoute une dépendance d'outillage. -- compiler la cible GNU sur `ubuntu-22.04` au lieu de `ubuntu-latest` — plancher - 2.35, couvre Debian 12 et Ubuntu 22.04 mais **pas** Rocky 9 (2.34). Un - correctif partiel. - -Dans tous les cas, sortir la cible GNU de `latest` une fois musl disponible : -le script doit viser musl par défaut et ne retomber sur GNU que si l'utilisateur -le demande explicitement. - -### 3.2 macOS — aucun problème - -`LC_VERSION_MIN_MACOSX` du binaire `v0.5.12-x86_64-apple-darwin` annonce un -minimum de **10.12.0** (Sierra), compilé avec un SDK 26.5. Toutes les versions -de macOS réellement en usage sont couvertes. Rien à faire. - -### 3.3 Windows — le runtime MSVC manque sur les machines minimalistes - -Les DLL importées par `typr.exe` : - -``` -kernel32.dll, USER32.dll, advapi32.dll, ntdll.dll, bcryptprimitives.dll -VCRUNTIME140.dll -api-ms-win-crt-runtime / -string / -math / -stdio / -locale / -heap -``` - -`VCRUNTIME140.dll` et les `api-ms-win-crt-*` proviennent du **VC++ Redistributable -de Microsoft**. Ils sont présents sur un poste de développement ou une machine -qui a déjà installé beaucoup de choses, mais **absents d'un Windows 10/11 -frais**. Le scénario est exactement celui qu'on veut éviter : l'utilisateur copie -la commande, l'installation se termine sans erreur, puis `typr --version` échoue -avec une `DLL introuvable` — une faute d'orthographe dans un message de -chargeur. - -**Correction : `-C target-feature=+crt-static`** sur les cibles `*-msvc`, ce qui -lie statiquement le runtime C et supprime la dépendance. rustc accepte le flag -pour `x86_64-pc-windows-msvc` ; **la liaison n'a pas pu être vérifiée localement** -(pas de linker MSVC dans cet environnement). À valider en CI en comparant la -liste des DLL importées du binaire produit. - -Ce correctif est peu coûteux et supprime une classe entière de tickets, donc il -fait partie du lot 0. - -### 3.4 `aarch64-pc-windows-msvc` — à réévaluer - -Cette cible ne fonctionne que sur Windows sur ARM (Snapdragon X). Elle occupe un -créneau de la matrice pour une audience quasi inexistante. Ce n'est pas un -problème de fonctionnement, seulement de temps de CI. À garder pour la -complétude, à retirer si la matrice devient coûteuse. - -### 3.5 Résoudre « latest » sans dépendance - -Les scripts ne doivent pas supposer `jq` ni `python` sur la machine. - -L'astuce retenue : l'API GitHub demande `jq`, mais l'URL -`https://github.com//releases/latest` **redirige** vers `/tag/vX.Y.Z`. -Il suffit de lire l'en-tête `Location` : - -```bash -curl -fsSI https://github.com/we-data-ch/typr/releases/latest \ - | grep -i '^location:' | sed 's|.*/tag/||' | tr -d '\r\n' -``` - -Deux conséquences utiles : - -- `/releases/latest` **exclut** les prereleases — exactement la sémantique de la - branche stable, alignée sur `prerelease` dans `release.yml:169`. Le canal - beta devra, lui, lister `/releases` et filtrer. -- Aucune dépendance à un outil de parsing. - -### 3.6 Vérifier le SHA-256 — ne pas utiliser `sha256sum -c` tel quel - -Testé sur `v0.5.12` : `checksums.txt` contient bien les 6 lignes attendues, au -format `sha256sum`. Mais `sha256sum -c checksums.txt` **échoue**, parce qu'il -tente de vérifier les 5 archives qui n'ont pas été téléchargées : - -``` -typr-v0.5.12-x86_64-unknown-linux-gnu.tar.gz: OK -typr-v0.5.12-aarch64-apple-darwin.tar.gz: FAILED open or read -… (4 autres) -``` - -Le code de sortie est non nul même quand le fichier nous intéresse est valide. -Le script doit donc **extraire la seule ligne qui correspond**, puis la -vérifier : - -```bash -grep " $ARTIFACT\$" checksums.txt | sha256sum -c - -``` - -Testé : cette variante sort `OK` avec un code de retour nul. Elle reste le seul -endroit où une erreur de formatage passerait inaperçue, donc à couvrir par un -test qui fournit volontairement un SHA falsifié. - -`checksums.txt` ne contient que les 6 archives de binaires — ni le `.vsix`, ni -le tarball RStudio, ni le plugin Vim, ni le WASM, qui sont joints plus tard par -d'autres jobs. C'est cohérent avec l'usage du script d'installation, et le plan -n'a pas à en tenir compte autrement. - -L'option `TYPR_INSTALL_VERIFY=0` permet de sauter la vérification pour un -dépannage réseau, sans jamais être le mode par défaut. - -### 3.7 Récupérer le binaire après un échec - -`install.sh` télécharge dans un fichier temporaire, vérifie le SHA, puis déplace. -En cas d'échec le fichier temporaire est nettoyé : on n'installe jamais un binaire -partiellement téléchargé. - -### 3.8 Le job `coherence` interdit les binaires commités - -Le job de CI échoue si un binaire réapparaît dans l'index (`RELEASING.md:157`). -Les scripts ne doivent installer que dans `$HOME`, jamais dans le dépôt, et le -nouveau dossier `install/` ne doit contenir aucun artefact — uniquement du texte. - ---- - -## 4. Phase 1 — Scripts d'installation - -Socle : les trois canaux suivants ne font que résoudre une URL et lire un SHA. -Tout ce qui est commun à Unix et Windows (résolution de version, choix du -canal, message de fin) vit dans les scripts, pas dans la CI. - -### Livrables - -``` -install/install.sh # POSIX sh, Linux + macOS -install/install.ps1 # PowerShell, Windows -install/README.md # usage et dépannage, destiné aux contributeurs -``` - -### `install/install.sh` - -- `#!/bin/sh`, `set -eu`, aucune dépendance hors `curl`/`tar`/`sha256sum` - (macOS fournit `shasum -a 256`, pas `sha256sum` : les deux doivent être - Tentés). -- Résolution de la cible par `uname -s` et `uname -m`, avec correspondance - explicite `uname -m` → triple Rust. Sous Rosetta, `uname -m` renvoie `x86_64` - sur une machine Apple Silicon, et le binaire Intel y fonctionne — aucun cas - particulier n'est nécessaire. -- Support d'arguments : `--version ` pour épingler, `--channel `, - `--dry-run`, `--help`. -- Détection du dossier cible : `$TYPR_INSTALL_DIR`, sinon `$HOME/.local/bin`. -- Si le dossier cible n'est pas dans le `PATH`, afficher la ligne à ajouter et le - `~/.profile`, sans échouer. -- Message final indiquant `typr --version` et le lien vers la documentation. - -### `install/install.ps1` - -- Cible `%LOCALAPPDATA%\Programs\typr` — convention utilisateur moderne, aucun - droit administrateur. Surchargeable par `$env:TYPR_INSTALL_DIR`. -- `irm … | iex` :PowerShell impose de rester sur une seule ligne dans le - message du README, et d'afficher l'URL avant exécution. -- Extraction via `Expand-Archive`, donc format `.zip` et non `.tar.gz`. -- Ajout au `PATH` utilisateur par registre - (`[Environment]::SetEnvironmentVariable('Path', …, 'User')`) — qui ne prend - effet qu'au prochain démarrage du terminal. Le script doit le dire - explicitement, **et** mettre à jour `$env:Path` de la session courante pour - que `typr --version` fonctionne tout de suite dans le même terminal. - -### Vérification - -Tester au minimum, pour chaque OS et chaque architecture disponible : - -```bash -# Unix -TYPR_INSTALL_DIR=$(mktemp -d) ./install/install.sh --dry-run -TYPR_INSTALL_DIR=$(mktemp -d) ./install/install.sh -$TYPR_INSTALL_DIR/typr --version -``` - -Le `typr --version` de la dernière ligne est le test qui compte : c'est -précisément l'étape qui échoue aujourd'hui sur d'anciennes distributions. -Ajouter un contrôle explicite dans la CI, sur des images Docker sans rien -d'autre : - -```bash -# ce test a échoué sur les 3 premières distributions en septembre 2026 -docker run --rm -v "$BIN:/w/typr" rockylinux:9 sh -c '/w/typr --version' -docker run --rm -v "$BIN:/w/typr" debian:12 sh -c '/w/typr --version' -docker run --rm -v "$BIN:/w/typr" ubuntu:22.04 sh -c '/w/typr --version' -docker run --rm -v "$BIN:/w/typr" alpine:3.20 sh -c '/w/typr --version' -``` - -- [ ] Les 4 conteneurs ci-dessus répondent une version -- [ ] Linux x86_64 et aarch64, macOS x86_64 et aarch64 -- [ ] Windows x86_64 via `irm | iex`, sur une image sans VC++ Redistributable -- [ ] `--version ` installe bien l'ancienne version -- [ ] SHA volontairement falsifié → le script refuse d'installer et le dit -- [ ] `checksums.txt` absent ou malformé → erreur explicite, pas un `FAILED open or read` -- [ ] `PATH` non modifiable → message clair, pas d'erreur opaque -- [ ] Idempotence : deux exécutions successives ne cassent rien - ---- - -## 5. Phase 2 — Homebrew - -Le canal le plus attendu sur macOS, et il couvre Linux au passage. - -### Livrables - -Dépôt `we-data-ch/homebrew-typr`, créé à blanc puis tenu par la CI : - -``` -README.md # généré à partir de la formule -Formula/typr.rb # généré depuis checksums.txt -LICENSE # copié depuis ce dépôt -``` - -Le générateur et ses tests restent ici, pas dans le tap : le tap ne contient que -ce qu'il faut pour installer, et sa seule source est `release.yml`. - -### Contenu de la formule - -Le plan prévoyait un `url` unique plus un `if Hardware::CPU.arm?`. C'est -insuffisant : avec Linux dans le canal, **les deux** systèmes ont deux -architectures, et un seul `url` au niveau racine ne peut pas désigner le bon -binaire. Le gabarit construit utilise donc les deux variantes du DSL moderne : - -```ruby -class Typr < Formula - desc "A typed superset of R — transpiler and type checker" - homepage "https://we-data-ch.github.io/typr.github.io/" - license "Apache-2.0" - version "X.Y.Z" - - livecheck do - url :stable - strategy :github_latest - end - - on_macos do - on_intel do - url "…/typr-vX.Y.Z-x86_64-apple-darwin.tar.gz" - sha256 "" - end - - on_arm do - url "…/typr-vX.Y.Z-aarch64-apple-darwin.tar.gz" - sha256 "" - end - end - - # on_linux : idem, en musl. Ce bloc n'existe que si la release en publie. - - def install - bin.install "typr" - end - - test do - assert_match version.to_s, shell_output("#{bin}/typr --version") - end - - def caveats - "typR is pre-1.0: version 0.x releases can change CLI surface." - end -end -``` - -- Homebrew gère le `PATH`, les mises à jour (`brew upgrade`) et le - désinstallateur : le script Unix n'a plus à s'en préoccuper pour ce canal. -- **Le `sha256` doit être rempli automatiquement**, jamais écrit à la main. - Extrapoler `release.yml` avec un job `brew` qui lit `checksums.txt` — le - fichier contient déjà la ligne correspondant à l'artefact macOS, il n'y a rien - à recalculer — et commite la formule mise à jour. -- Avec `livecheck`, Homebrew suit le tag et propose la mise à jour tout seul. - -### Vérification - -- [ ] `brew install --formula we-data-ch/typr/typr` depuis un Mac clean — humain -- [ ] Fonctionne sur Apple Silicon **et** Intel — humain -- [ ] `brew upgrade typr` passe à la bonne version — humain -- [x] `brew audit --strict` ne signale rien — `packaging-audit` et `brew` -- [x] Le SHA publié correspond à `checksums.txt` de la release — job `brew`, qui - télécharge l'archive et compare -- [ ] `brew install we-data-ch/typr/typr` résout le tap — le dépôt existe - (30 septembre 2026) ; la ligne n'est cochée qu'après un essai sur un Mac - -Ce que la CI ne peut pas faire, et qui reste à faire à la main une fois le tap -créé : ouvrir un Mac vierge et taper `brew install`. `brew audit --strict` juge la -formule, pas l'installation. - ---- - -## 6. Phase 3 — WinGet et Scoop - -Deux manifests, aucun binaire. Ils puisent dans les `.zip` déjà publiés. - -> **Ce qui suit a été corrigé après implémentation.** Les deux affirmations -> fausses du plan initial sont signalées en place ; voir « Ce que le lot 4 a -> livré » pour le reste. - -### Livrables - -``` -packaging/render.sh le générateur des deux canaux -packaging/winget/we-data-ch.TypR.yaml.in version -packaging/winget/we-data-ch.TypR.installer.yaml.in installateurs x64 + arm64 -packaging/winget/we-data-ch.TypR.locale.en-US.yaml.in locale -packaging/scoop/typr.json.in manifeste Scoop -packaging/scoop/README.md.in README du bucket -packaging/tests/ suite + validation des schémas -``` - -Le `.installer.yaml` n'est pas conditionné à une signature : WinGet l'exige pour -décrire les installateurs, signés ou non. - -### WinGet - -- `PackageIdentifier: we-data-ch.TypR` -- `PackageVersion` sans le `v` initial — WinGet l'exige. -- `InstallerType: zip`, `InstallerSwitches: Silent` et `SilentWithProgress` avec - `/S` pour `7z`… **à ne pas inventer** : le type `zip` de WinGet extrait sans - copie dans un dossier. -- ~~Le chemin `InstallLocation` doit être `%LOCALAPPDATA%\Programs\typr`, pour - coïncider avec ce que fait `install.ps1`.~~ **Faux.** `InstallLocation` est un - *argument passé à l'installeur*, pas un dossier de destination ; un zip n'a pas - d'installeur, et le champ n'aurait rien fait. WinGet extrait sous - `%LOCALAPPDATA%\Microsoft\WinGet\Packages\…` et y met l'alias `typr` sur le - PATH — ce qui garde l'installation sans droits administrateur, le seul point - qui comptait dans cette case. -- Le fichier est publié dans `microsoft/winget-pkgs` par une pull request - ouverte automatiquement depuis la CI. `wingetcreate` n'a pas de commande - `validate` autonome : c'est `wingetcreate submit` qui valide avec l'outil - officiel, puis calcule lui-même le chemin de destination et ouvre la PR. - -### Scoop - -Le manifest est plus court que celui de WinGet et ne demande pas de PR -maintenue manuellement. `scoop install typr` après -`scoop bucket add typr https://github.com/we-data-ch/scoop-bucket`. - -`url` et `hash` sont des **tableaux alignés par index** : les deux URL sont -fournies, Scoop les propose en miroirs et vérifie l'empreinte du fichier -réellement téléchargé. Voir la note ARM64 dans « Ce que le lot 4 a livré ». - -Dans les deux cas, la version publiée doit venir de la même source que le reste — -`needs.verify.outputs.version` — et non d'un fichier écrit à la main. C'est -fait, et par construction : `render.sh` refuse de produire quoi que ce soit -depuis un `checksums.txt` qui ne contiendrait pas les deux empreintes Windows. - -### Vérification - -- [x] Les manifestes rendus passent les **schémas officiels** WinGet et Scoop — - 122 assertions, dont six mutations vérifiées détectées à la main. -- [ ] `winget install we-data-ch.TypR` sur Windows 10 et 11 — **impossible en - local** : c'est fait par le job `winget` sur `windows-latest`, avec - `wingetcreate submit`. -- [x] L'installation **sans droits administrateur** est acquise : WinGet comme - Scoop écrivent dans le profil utilisateur. C'est vérifié par construction - (aucun `InstallerSwitches`, aucun `RequireExplicitUpgrade`, aucun - installeur MSI), pas par une exécution. -- [ ] `scoop install typr` puis `scoop update typr` — **impossible en local** : - c'est fait par le job `scoop`, qui installe le manifeste rendu avant de - commiter. -- [x] Les deux canaux installent **le même binaire** : la même archive - `typr-vX.Y.Z-x86_64-pc-windows-msvc.zip`, lue dans le même - `checksums.txt`, et le même `typr.exe` à sa racine. La suite le vérifie. -- ~~Les deux installers aboutissent au même dossier et au même binaire que - `install.ps1`~~ **Abandonnée**, voir §6 WinGet : le dossier ne peut pas être - choisi pour un zip portable. Ce qui est vérifié, c'est le binaire — pas le - chemin. - ---- - -## 7. Phase 4 — Documentation - -### README - -Remplacer le tableau `Install` (README.md:62-69) : - -```markdown -## Install - -| Plateforme | Commande | -|---|---| -| **Toutes** | `curl -fsSL https://we-data-ch.github.io/typr.github.io/install/install.sh \| sh` | -| **Windows** | `irm https://we-data-ch.github.io/typr.github.io/install/install.ps1 \| iex` | -| macOS / Linux (Homebrew) | `brew install we-data-ch/typr/typr` | -| Windows (WinGet) | `winget install we-data-ch.TypR` | -| Windows (Scoop) | `scoop install typr` | -| Cargo | `cargo install typr` | -| Docker | `docker run --rm -it fabricehategekimana/typr:latest` | -| RStudio / Positron | `typr.runner_*.tar.gz` — release | -| VS Code / Positron | extension **TypR** dans le Marketplace | -| Vim / Neovim | `typr-vim-*.tar.gz` — release, ou un plugin manager | -``` - -Faire figurer en tête les deux lignes universelles : ce sont elles que l'on veut -que la personne copie. - -### Page d'installation du site - -Créer `docs/install.md` (ou l'équivalent dans le dépôt `typr.github.io`) avec, -dans cet ordre : la commande unique, le choix du canal, la vérification du -SHA-256, la mise à jour, le dépannage (PATH non modifié, Gatekeeper, -SmartScreen, architecture non supportée). - -Préciser explicitement que l'installation **n'exige pas `sudo`** et -**n'écrit rien hors de `$HOME`** — c'est ce qui lève la principale réticence. - -### Vérification - -- [ ] Les commandes du README sont copiées et exécutées telles quelles -- [ ] `RELEASING.md` mentionne les nouveaux canaux dans le diagramme -- [ ] `publish.nu check` compare bien les quatre canaux (voir ci-dessous) - ---- - -## 8. Phase 5 — `typr upgrade` (optionnelle) - -À ne faire que si les releases sont fréquentes ; sinon le script et Homebrew -suffisent. - -Le binaire serait `typr upgrade` : même logique de résolution de version et de -vérification de SHA que les scripts, mais à l'intérieur du programme — donc testée -par `cargo test` plutôt que par un script shell. - -Le risque est la duplication entre le code Rust et les scripts. Le traiter en -conséquence : soit le script devient le client d'un sous-commande `typr install`, -soit la logique est factorisée dans une petite bibliothèque partagée. - ---- - -## 9. CI - -### Étapes dans `release.yml` - -``` -tag vX.Y.Z - │ - ├─ verify … verify … release … - │ ├─ brew ……… commit de la formule mise à jour - │ ├─ winget … PR vers microsoft/winget-pkgs - │ └─ scoop … commit du manifest - └─ … -``` - -Ces jobs restent **indépendants** des autres canaux, comme le sont déjà -`vscode` et `vim` : un échec de publication Homebrew ne doit pas empêcher les -binaires d'arriver. C'est la règle que `RELEASING.md:108` énonce déjà. - -### Étapes dans `ci.yml` - -- Job `install`, sur une PR : `bash -n install/install.sh`, `sh -n`, et - exécution réelle avec `--dry-run`. -- Analyse avec `shellcheck` pour `install.sh`, `PSScriptAnalyzer` pour - `install.ps1`. -- Pour WinGet et Scoop, valider le YAML/JSON contre leur schéma. -- Vérifier que `install/` ne contient aucun binaire — le job `coherence` - l'interdit déjà pour l'ensemble du dépôt. - -### `publish.nu` - -Étendre `check` pour interroger l'API Homebrew et WinGet et confirmer que la -version publiée est bien celle du tag. Sans cela, un canal en retard devient -invisible : les autres canaux sont mis à jour au tag, lui seul dériverait en -silence. - ---- - -## 10. Ordre d'exécution - -| # | Lot | Dépend de | Effort | -|---|---|---|---| -| 0a | **Cibles musl** dans la matrice de `release.yml` | — | 2 h | -| 0b | **`+crt-static`** sur les cibles `*-msvc` | — | 30 min | -| 0c | Vérifier que le binaire musl démarre dans Rocky 9, Debian 12, Alpine | 0a | 1 h | -| 1 | `install.sh` + `install.ps1` + hébergement | 0 | 1 j | -| 2 | Tests CI sur les scripts | 1 | 3 h | -| 3 | Homebrew tap + job CI | 1 | 1 j | -| 4 | WinGet + Scoop + PR automatique | 1 | 1 j | -| 5 | README, page d'installation, `RELEASING.md` | 1-4 | 3 h | -| 6 | `publish.nu check` étendu | 1-4 | 2 h | -| 7 | `typr upgrade` (optionnel) | — | plus tard | - -Le lot 0 est **précisément-identified et non négociable** : le diagnostic a -montré que le binaire actuel ne démarre ni sur Rocky 9, ni sur Debian 12, ni sur -Ubuntu 22.04. Publier un script d'installation qui lance `typr --version` et -échoue dessus convertirait le canal principal en source de tickets — c'est-à-dire -exactement l'expérience que ce plan cherche à supprimer. - -Les lots 3 et 4 sont indépendants de 2 et parallélisables. - ---- - -## 11. Risques - -| Risque | Impact | Réponse | -|---|---|---| -| glibc des binaires Linux trop récent | **Confirmé** — l'installation échoue sur Rocky 9, Debian 12, Ubuntu 22.04, Alpine | Lot 0a : cibles musl, liées statiquement | -| Le plancher glibc remontera seul en novembre 2026 | Nouvelle rupture, sans changement de code | Lot 0a : musl supprime la dépendance à la glibc du runner | -| `VCRUNTIME140.dll` absent sur Windows minimal | Installation réussie puis `typr --version` échoue | Lot 0b : `+crt-static` | -| SmartScreen bloque `typr.exe` | Avertissement, pas un blocage | Un binaire posé par `install.ps1` ne porte pas de MOTW | -| Gatekeeper sur macOS | Rare — plancher à macOS 10.12, et `curl \| sh` ne pose pas la quarantaine | Documenter `xattr -d com.apple.quarantine` pour qui télécharge d'abord par navigateur | -| Un canal dérive en silence | Le README promet une version absente d'un canal | `publish.nu check` étendu (lot 6) | -| Le script d'installation casse les gens | Mauvaise expérience sur le canal principal | `--dry-run`, `--version` épinglé, idempotence, tests sur 6 cibles en CI | - ---- - -## 12. hors périmètre - -- Signature et notarification (coût, hors budget d'un projet open source). -- Canal `apt` / `dnf` : la distribution Linux est trop fragmentée pour que le - gain justifie l'effort. Le script Unix — et musl — couvrent ces systèmes sans - coût supplémentaire. -- Mise à jour automatique silencieuse, qui crée plus de problèmes qu'elle n'en - résout (mise à jour pendant une session LSP en cours, binaire remplacé sous - les pieds du processus). `typr upgrade` explicite suffit. -- Paquet `apt` / `dnf` : sans objet, la ligne ci-dessus couvre le sujet. \ No newline at end of file diff --git a/interface_generic_unification2.md b/interface_generic_unification2.md deleted file mode 100644 index 1e034804..00000000 --- a/interface_generic_unification2.md +++ /dev/null @@ -1,337 +0,0 @@ -# RFC TypR – Génériques implicites basés sur les interfaces - -> **Objet :** permettre plusieurs paramètres génériques distincts partageant la même interface, -> sans introduire de `T`, `U` explicites, ni casser le modèle “interface = type”. - ---- - -## 1. Contexte et objectifs - -### 1.1. Contexte - -Dans TypR : - -- **Les interfaces sont des types à part entière**, définis via des alias : - ```typr - type Printable <- interface { - print: (Self) -> char - }; - ``` -- Une interface sert **à la fois** : - - de **type structurel** (annotation classique), - - de **contrainte générique** (on peut écrire `fn(xs: Iterable): Iterable`). - -Ce modèle est volontairement simple pour des développeurs venant de R : -pas de `T`, `U`, pas de syntaxe de type paramétrique explicite. - -### 1.2. Problème - -Avec ce modèle, une fonction : - -```typr -let compare <- fn(a: Lovable, b: Lovable): bool { - ... -}; -``` - -ne peut pas distinguer deux **types concrets différents** pour `a` et `b` : - -- `Lovable` est un **type structurel unique**, -- donc `a` et `b` sont du **même type structurel**. - -On ne peut pas exprimer : -“`a` est un type qui satisfait `Lovable`, `b` est un autre type (potentiellement différent) qui satisfait aussi `Lovable`”. - -### 1.3. Objectif - -Introduire un mécanisme de **génériques implicites** qui : - -- **sépare les paramètres** (`a` et `b` peuvent être de types différents), -- **reste structurel** (pas de nominalisme), -- **reste simple** pour des devs R (pas de `T`, `U`), -- **respecte** le modèle mental : *interface = type = contrainte*. - ---- - -## 2. Design proposé – paramètres implicites `@Id` - -### 2.1. Idée centrale - -On ajoute une **annotation légère** sur les paramètres : - -```typr -let compare <- fn(a: Lovable@A, b: Lovable@B): bool { - ... -}; -``` - -- `Lovable` : interface/type structurel. -- `@A`, `@B` : **identifiants de paramètre générique**, *non typés*. -- Chaque identifiant (`A`, `B`) représente un **paramètre générique distinct**. - -### 2.2. Règle intuitive - -- Deux annotations `Lovable@A` et `Lovable@B` : - - partagent la **même interface** (`Lovable`), - - mais sont **deux paramètres génériques différents**. -- L’inférence peut donc déduire : - - `A = Cat`, - - `B = Dog`, - - tant que `Cat` et `Dog` satisfont `Lovable`. - ---- - -## 3. Syntaxe formelle - -### 3.1. Définition d’interface - -Syntaxe inchangée : - -```typr -type Lovable <- interface { - love: (Self) -> char -}; -``` - -### 3.2. Annotation de paramètre - -#### 3.2.1. Forme générale - -Pour un paramètre : - -```typr -name: Interface@Id -``` - -- **`name`** : identifiant du paramètre (variable). -- **`Interface`** : alias de type pointant vers une interface. -- **`Id`** : identifiant de paramètre générique (nom court, style `A`, `B`, `Item`, etc.). - -#### 3.2.2. Forme minimale - -Si aucun `@Id` n’est fourni : - -```typr -name: Interface -``` - -- Comportement **actuel** : - tous les paramètres annotés avec `Interface` **partagent le même type structurel**. -- Ce mode reste valide et est le **mode par défaut**. - -#### 3.2.3. Exemple - -```typr -let pair <- fn(a: Lovable@Left, b: Lovable@Right): (Lovable@Left, Lovable@Right) { - (a, b) -}; -``` - ---- - -## 4. Sémantique – modèle de types - -### 4.1. Interfaces comme types structurels - -On conserve : - -- Une interface `I` est un **type structurel**. -- Un alias : - ```typr - type I <- interface { ... }; - ``` - introduit un **type nommé** `I` avec une **structure requise**. - -### 4.2. Paramètres génériques implicites - -Pour chaque identifiant `Id` utilisé dans `Interface@Id` : - -- Le compilateur introduit un **paramètre de type implicite** \(\alpha_{Id}\). -- La contrainte associée est : - \[ - \alpha_{Id} : Interface - \] - c’est‑à‑dire : - “\(\alpha_{Id}\) doit satisfaire la structure de `Interface`”. - -### 4.3. Distinction des paramètres - -Deux annotations : - -```typr -a: Lovable@A -b: Lovable@B -``` - -induisent : - -- deux paramètres de type distincts \(\alpha_A\) et \(\alpha_B\), -- chacun contraint par `Lovable`. - -Il n’y a **aucune relation** imposée entre \(\alpha_A\) et \(\alpha_B\) -(sauf si le corps de la fonction impose des contraintes supplémentaires). - ---- - -## 5. Règles d’inférence - -### 5.1. Collecte des paramètres - -Pour une fonction : - -```typr -let f <- fn(a: Lovable@A, b: Lovable@B): bool { - ... -}; -``` - -Le compilateur : - -1. **Collecte** les identifiants `A`, `B`. -2. Crée un ensemble de paramètres de type : - \[ - \{\alpha_A, \alpha_B\} - \] -3. Associe les contraintes : - \[ - \alpha_A : Lovable,\quad \alpha_B : Lovable - \] - -### 5.2. Résolution à l’appel - -À l’appel : - -```typr -f(cat, dog); -``` - -Le compilateur : - -- infère le type de `cat` : `Cat`, -- infère le type de `dog` : `Dog`, -- vérifie : - - `Cat` satisfait `Lovable`, - - `Dog` satisfait `Lovable`, -- unifie : - - \(\alpha_A = Cat\), - - \(\alpha_B = Dog\). - -### 5.3. Cas sans `@Id` - -Pour : - -```typr -let f <- fn(a: Lovable, b: Lovable): bool { - ... -}; -``` - -Le compilateur : - -- introduit **un seul** paramètre \(\alpha\), -- avec contrainte : - \[ - \alpha : Lovable - \] -- à l’appel, `a` et `b` doivent être du **même type** \(\alpha\). - ---- - -## 6. Exemples d’usage - -### 6.1. Comparaison de deux Lovable distincts - -```typr -type Lovable <- interface { - love: (Self) -> char -}; - -let compare <- fn(a: Lovable@A, b: Lovable@B): bool { - a.love() == b.love() -}; -``` - -- `A` et `B` peuvent être deux types différents. -- On compare leur “score d’amour”. - -### 6.2. Fonction symétrique - -```typr -let swap <- fn(a: Lovable@A, b: Lovable@B): (Lovable@B, Lovable@A) { - (b, a) -}; -``` - -- Le type de retour encode la permutation des paramètres génériques. - -### 6.3. Interface générique + paramètres implicites - -```typr -type Box <- interface { - value: T -}; - -let merge <- fn(a: Box@A, b: Box@B): (Box@A, Box@B) { - (a, b) -}; -``` - -- `Box@A` et `Box@B` peuvent encapsuler des types différents. -- L’inférence doit gérer : - - le paramètre implicite `A`, - - le paramètre explicite `T` de `Box`. - ---- - -## 7. Points ouverts et variantes - -### 7.1. Identifiants optionnels - -On peut autoriser : - -- `Lovable@A` (nom explicite), -- `Lovable@_` (paramètre anonyme), -- `Lovable#` (syntaxe alternative plus légère). - -À discuter selon la **lisibilité** et la **culture R**. - -### 7.2. Réutilisation d’un identifiant - -Si on écrit : - -```typr -let pair <- fn(a: Lovable@X, b: Lovable@X): (Lovable@X, Lovable@X) { - (a, b) -}; -``` - -- `a` et `b` partagent le **même paramètre générique** \(\alpha_X\), -- donc doivent être du **même type**. - -C’est l’équivalent de `T` réutilisé dans les langages classiques, -mais sans introduire `T` comme type nominal. - -### 7.3. Erreurs de typage - -Cas à définir clairement : - -- utilisation de `Lovable@A` dans le retour sans paramètre correspondant, -- identifiant `@A` utilisé mais jamais contraint, -- conflit entre deux interfaces incompatibles sur le même `@A`. - ---- - -## 8. Résumé conceptuel - -- **Interfaces** restent : - - des **types structurels**, - - des **contrats**, - - des **espaces de fonctions**. -- Les **génériques implicites** sont : - - des **identifiants de contrainte** (`@A`, `@B`), - - non typés, - - résolus par **inférence**. - -> On ne demande jamais au développeur R de “penser en `T`, `U`”. -> On lui dit juste : -> “si tu veux que deux paramètres soient indépendants, donne‑leur chacun un petit tag `@A`, `@B`”. diff --git a/interface_generic_unification_plan.md b/interface_generic_unification_plan.md deleted file mode 100644 index bf35bd14..00000000 --- a/interface_generic_unification_plan.md +++ /dev/null @@ -1,370 +0,0 @@ -# Génériques implicites `Interface@Id` — plan d'implémentation - -Plan pour `interface_generic_unification2.md` (RFC), écrit après lecture de `typr-core` et sondes -contre le binaire (`target/debug/typr`, branche `develop`, état au 2026-09-30). - ---- - -## 0. Ce qui existe déjà (et ce que les sondes révèlent) - -| Élément du RFC | État actuel dans le code | -|---|---| -| Interface en paramètre → variable rigide contrainte | **Déjà en place dans le corps** : `type_checking/function.rs:326-340` crée un `Type::Generic("__rigid_N")` + `add_interface_constraint` **par paramètre**. Même mécanique dans `specialize_lambda` (`function_application.rs:1061`). | -| Élimination `e.m() : T[Self ↦ A]` | En place : `try_constrained_variable_match` (`function_application.rs:29`, FILTERING 0). | -| Instanciation au site d'appel | `try_interface_subtype_match` (`function_application.rs:624`) : table `interface_to_concrete` **indexée par l'interface réduite**, et c'est le premier argument qui gagne. | -| Retour interface sans paramètre (§7.3a) | Déjà une erreur : `TypeError::InterfaceReturnOnly` (`function.rs`, `is_interface_return_only`). | -| Sigil `@` en position de type | **Déjà pris** en préfixe : `@A` → `Type::KindedGen(Kind::Interface, "A")` (`parsing/types.rs:917`, RFC `sigils.md`). | -| Génériques explicites `T`, `U` | Fonctionnent correctement : `fn(a: T, b: U): U` appelé avec `(cat, dog)` rend `Dog`. | -| Interfaces paramétrées `interface` (RFC §6.3) | **N'existent pas** (aucune occurrence dans le parseur). | - -Les sondes (fichiers `*.ty` avec `Lovable`, `Cat`, `Dog`) montrent que **le corps et le site d'appel -appliquent deux sémantiques différentes** pour `fn(a: Lovable, b: Lovable)` : - -- **Dans le corps, les variables sont distinctes** : `a : __rigid_0`, `b : __rigid_1`. -- **Au site d'appel, une seule variable est partagée**, et c'est le premier argument qui la fixe. - -Défauts constatés, à capturer en `cases/` avant toute modification : - -1. **Défaut de soundness** : `let second <- fn(a: Lovable, b: Lovable): Lovable { b }` puis - `let r: Cat <- second(cat, dog)` **passe**, et `let r: Dog <- second(cat, dog)` est **rejeté** - (« Received Cat »). Le retour est lié au premier argument, et `b: Dog` n'est jamais confronté - à `a: Cat`. -2. **`Lovable@A` est silencieusement avalé** : `fn(a: Lovable@A, b: Lovable@B)` passe `typr check`, - la signature s'affiche `fn(a: Lovable, b: Lovable) -> bool` et le R généré ne garde aucune trace - de `@A`. Il faudra trouver quel parseur consomme `@A` sans le reporter. -3. **Méthode `(Self, Self)` sur deux rigides distincts acceptée** : - `type Same <- interface { same: (Self, Self) -> bool }; fn(a: Same, b: Same): bool { a.same(b) }` - passe, alors qu'avec des rigides distincts, `b : B` ne devrait pas satisfaire le paramètre `A`. - À expliquer en phase 0 : soit `is_subtype_raw` entre deux rigides est trop permissif, soit un - filtre de repli accepte l'appel. -4. `is_rigid_compatible` (`function.rs:244`) accepte **n'importe quel** rigide dont la borne est - `Lovable` comme retour `Lovable`. C'est pour cela que `{ b }` et `{ a }` passent tous les deux. - ---- - -## 1. Décisions de conception - -### D1. Syntaxe : `I@Id` en suffixe, cohérent avec le sigil préfixe existant - -- `Lovable@A` : variable générique `A`, bornée par `Lovable`. Le `@` suffixe ne se lit qu'**immédiatement - après un nom d'alias**, sans espace. Il ne peut donc pas être confondu avec `@export`, `@pub` ou - `@name: …;`, qui sont au niveau des déclarations. -- `@A` reste ce qu'il est (générique de *kind* interface, sans borne précise). Lecture unifiée : - `@A` ≈ `interface{}@A`. Un même `A` dans `@A` et `Lovable@A` désigne **la même variable**, dont la - borne est `Lovable`. -- `Lovable@_` (RFC §7.1) : identifiant anonyme, donc variable fraîche à chaque occurrence. C'est peu coûteux - à ajouter, à inclure. -- `Lovable#` (variante §7.1) : **rejeté**, parce que `#` est déjà le sigil d'`IndexGen`. -- Les identifiants partagent **l'espace de noms des génériques de la signature**. `fn(a: T, b: Lovable@T)` est - une erreur (`T` est à la fois libre et borné). - -### D2. Représentation : un nouveau variant `Bounded` - -```rust -// components/type/mod.rs — À LA FIN de l'enum (sérialisation .bin, cf. Risques) -Bounded(String, Box, HelpData), // Lovable@A → Bounded("A", Alias("Lovable"), h) -``` - -On le préfère à une extension de `KindedGen`, dont la clé est un `Kind` (4 valeurs) et pas un type. -On le préfère aussi à une table de bornes à côté de la signature : la borne doit voyager avec le type -à travers `Function`, `Vec`, `Tuple` et la substitution. - -`Bounded` n'existe **que dans les signatures**. Dans le corps, il devient un rigide (D4). Au site -d'appel, il est lié comme un générique (D5). - -### D3. Sémantique d'un `I` nu : **option A retenue (2026-09-30) — changement de comportement assumé** - -> **Décision :** un `I` nu ≡ `I@I` (une variable partagée par interface, RFC §5.3). L'option B ci-dessous est écartée ; elle est conservée pour mémoire. - -| Option | `fn(a: Lovable, b: Lovable)` | `cmp(cat, dog)` | Retour `Lovable` | -|---|---|---|---| -| **A (RFC §5.3)** : `I` nu ≡ `I@I`, une variable partagée par interface | `a` et `b` du **même** type | **rejeté** (aujourd'hui accepté) | non ambigu | -| **B** : `I` nu ≡ `I@_`, variable fraîche par occurrence | types indépendants | accepté | **ambigu** s'il y a ≥ 2 paramètres `I` : erreur « ajoutez `@Id` » | - -**Choix : A**, pour trois raisons : - -- c'est le RFC ; -- c'est la seule option sûre pour les interfaces `(Self, Self)` (`Eq`, `Ord`, `Same`), où `a.eq(b)` - n'a de sens que si `a` et `b` ont le même type ; -- elle aligne le corps sur ce que le site d'appel fait déjà à moitié. - -Coût de A : du code qui compile aujourd'hui (`cmp(cat, dog)` avec des `Lovable` nus) sera rejeté, avec -un message qui propose `Lovable@A, Lovable@B`. Le stdlib n'est pas touché (`unique` et `sort` -n'ont qu'un paramètre interface). Il reste à mesurer l'impact sur les tests et la doc en phase 0. - -### D4. Corps : un rigide **par identifiant**, pas par paramètre - -La boucle de `function.rs:326` passe d'un rigide par paramètre à une table `id → rigide`. Deux -paramètres `Lovable@X` partagent `__rigid_k`. Avec D3-A, deux `Lovable` nus partagent aussi le leur. -`is_rigid_compatible` compare le rigide du corps **au rigide de l'identifiant déclaré en retour**, -et non plus seulement à la borne. - -### D5. Site d'appel : `Bounded` passe par l'unification générale - -On ajoute un bras à `unification_helper` (`unification.rs:239`), sur le modèle du bras `KindedGen` -déjà présent : -`(concrete, Bounded(id, bound))` lie `Generic(id) ↦ concrete` **si** `concrete ⊨ bound` -(`interface_satisfaction::check_interface_satisfaction`), et échoue sinon. Deux occurrences du -même `id` passent alors par `UnificationMap::try_new`, qui rejette déjà les liaisons contradictoires. -C'est exactement la règle « même identifiant ⇒ même type » (RFC §7.2). - -`try_interface_subtype_match` ne sert plus qu'aux interfaces nues **après désucrage**. Avec D3-A, -elles deviennent des `Bounded` et la fonction peut disparaître. Égalité exigée pour un même `id` : -on reprend le comportement actuel d'un `T` répété (`fn(a: T, b: T)`), pour ne pas créer deux règles. - -### D6. Hors périmètre : interfaces paramétrées `interface` (RFC §6.3) - -Elles ne sont pas parsées aujourd'hui. `Box@A` avec `Box <- interface` relève d'un RFC distinct. -`Bounded` est conçu pour les accueillir (borne = `Alias("Box", [T])`) sans changement de représentation. - ---- - -## 2. Phases - -### Phase 0 — Filet et mesure (aucun changement de sémantique) - -- `typr case add` pour les défauts 1, 2 et 3 du §0 (`expect.toml` : substrings d'erreur attendues). -- Expliquer le défaut 3 (`a.same(b)` accepté) : tracer `is_subtype_raw(Generic(r1), Generic(r0))`. -- Mesurer D3-A : forcer temporairement un rigide partagé par interface dans `function.rs`, puis lancer - `cargo test --workspace`, `typr case run` et, côté doc, `npm run check:examples`. Lister les échecs. - C'est cette liste qui chiffre le changement de comportement. - -**Statut : FAITE (2026-09-30).** - -- Cas ouverts (échouent tant que le plan n'est pas implémenté ; oracle : `Type errors found`) : - `0086-interface-return-bound-to-first-arg` (défaut 1), `0087-interface-shared-id-different-types` - (défaut 2 / D5), `0088-interface-self-self-distinct-ids` (défaut 3). `typr case run` : ils sont `OPEN`. - Les 4 `REGRESS` restants (0004, 0005, 0006, 0008 : diff golden de `R/main.R`) préexistent et n'ont - aucun rapport. -- **Cause du défaut 3** : `impl PartialEq for Type` (`components/type/mod.rs:1334`) déclare - `(Generic(_), Generic(_)) => true` — deux génériques quelconques sont « égaux ». Or `is_subtype_raw` - commence par `(typ1, typ2) if typ1 == typ2 => true` (`mod.rs:226`) : deux rigides distincts - `__rigid_0` / `__rigid_1` passent donc pour sous-types l'un de l'autre. Ce n'est pas un filtre de - repli. **Conséquence pour la phase 3** : ne pas toucher au `PartialEq` global (il sert à l'unification - et à `UnificationMap`) ; ajouter dans `is_subtype_raw`, avant la garde d'égalité, un bras - `(Generic(a), Generic(b))` qui compare les **noms** quand les deux sont des rigides (`__rigid_`). - Même cause probable pour le défaut 4 (`{ a }` et `{ b }` passent tous deux). -- **Mesure D3-A** (rigide partagé par interface dans le corps, `function.rs`, expérience temporaire - revertie) : `cargo test --workspace` **entièrement vert** (711 + 161 + … tests), `typr case run` - inchangé (mêmes 4 REGRESS préexistants). Un scan de `typr.github.io/docs`, `blog`, `lab/`, - `cases/*/repro` et du stdlib ne trouve **aucune** fonction à deux paramètres de même alias - d'interface. Limite : l'expérience ne touche que le corps ; le rejet de `cmp(cat, dog)` au site - d'appel (D5) n'est pas mesuré, mais le scan statique suggère un impact nul sur le corpus existant. - `npm run check:examples` non lancé (aucun exemple concerné d'après le scan). - -### Phase 1 — Parsing et affichage - -- `parsing/types.rs` : suffixe `opt(preceded(tag("@"), alt((generic_name, tag("_")))))` après un alias - (y compris imbriqué : `[#N, Eq@A]`, `tuple{Lovable@A}`, `(Lovable@A) -> …`). Produit `Type::Bounded`. -- Trouver et supprimer le chemin qui avale `@A` aujourd'hui (défaut 2). Un `@` suffixe mal formé doit - donner une erreur de syntaxe, jamais être ignoré. -- `Type::Bounded` : `Hash`/`Eq`, `type_printer` (`Lovable@A`), `type_category`, `fingerprint`, et tous les - `match` exhaustifs (le compilateur les listera). -- Manifeste de syntaxe `components/syntax/mod.rs` : déclarer le `@` suffixe de type. Puis - `typr syntax --write` et `--check`, et les deux tests `components::syntax::tests`. - -**Statut : FAITE (2026-09-30, non commitée).** - -- `Type::Bounded(id, bound, h)` ajouté en **fin** d'enum ; `Hash` (tag 45), `to_category` (celle de la - borne), `get_help_data`/`set_help_data`, `type_printer` (`format` et `verbose` : `Lovable@A`). - Les autres `match` ont des jokers : le variant ne traverse pas encore `reduce_type`, l'unification - ni `checked_assertions` (phases 2-5). -- Parseur (`type_alias`, `bound_suffix`) : `@` collé à l'alias, suivi de **une seule majuscule** ou de `_`. - `Self` n'est pas un id valable (un générique est une lettre ; `Self` est réservé aux interfaces). -- **Défaut 2 (`@A` avalé)** : la cause est le résolveur de priorité des types, qui écarte un type - résiduel. Un `@` suffixe mal formé (`Lovable @A`, `Lovable@Self`, `Lovable@Abc`) pousse - maintenant `SyntaxError::DetachedBoundSuffix` (**S018**, + entrée `typr-mcp/src/explain.rs`). - `Lovable@` et `Lovable@a` échouaient déjà (point-virgule manquant). -- Manifeste de syntaxe : **rien à changer**, `@` y est déjà (`live("@", "Interface", …)`) ; - `typr syntax --check` passe. -- Tests : 5 nouveaux dans `parsing/types.rs`. `cargo test --workspace` vert ; `typr case run` : mêmes 4 - REGRESS préexistants. -- Limite : `Bounded` est parsé mais **pas encore interprété**. `fn(a: Lovable@A, b: Lovable@B)` passe - `typr check` avec la sémantique d'avant. Le défaut 2 n'est donc corrigé qu'au niveau syntaxe ; le cas - 0087 reste OPEN jusqu'à la phase 4. - -### Phase 2 — Normalisation de la signature - -Un seul passage, appelé depuis `function()` et pour les signatures `@name: …;` : - -- collecter `id → borne` sur les paramètres et le retour ; -- appliquer le désucrage D3 aux `I` nus : `I` devient `Bounded(I, I)` (option A) ; - `@_` donne un id frais ; -- erreurs (RFC §7.3), une variante `TypeError` chacune, codes d'erreur stables : - - un `id` avec deux bornes différentes (`Lovable@A`, `Printable@A`), sauf si l'une est `@A` sans borne ; - - un `id` présent seulement dans le retour : on généralise `InterfaceReturnOnly` ; - - un `id` qui entre en collision avec un générique libre du même nom ; - -**Statut : FAITE (2026-09-30, non commitée).** - -- Nouveau module `type_checking/signature_normalization.rs` : `normalize_signature(context, params, ret)` - renvoie `{ params, ret, errors }`. Il (1) renomme chaque `I@_` en `_0`, `_1`…, (2) désucre un alias - d'interface nu `I` en `Bounded("I", I)` (D3-A ; les `interface { … }` en ligne sont laissés tels quels), - (3) produit les erreurs du §7.3. -- Erreurs : `ConflictingBound` **T047** (deux bornes pour un id ; comparées après `reduce_type`), - `BoundCollidesWithGeneric` **T048** (id égal à un générique libre `T`/`#N` ; `@A` n'est pas compté, D1), - et `InterfaceReturnOnly` **T017** réutilisée pour un id présent seulement au retour. Entrées `explain` - T047/T048 dans `typr-mcp`. -- **Seules les erreurs sont branchées** dans `function()`. La signature désucrée est calculée mais pas - consommée : corps et site d'appel lisent encore la signature brute, donc le comportement des programmes - valides est inchangé (les I nus ne deviennent pas `Bounded` avant les phases 3-4). -- Limite connue : `fn(a: Lovable@A): Lovable@A { a }` est encore rejeté (`Bounded` n'est pas interprété - dans le corps) — c'est l'objet de la phase 3. -- Tests : 6 nouveaux. `RUST_MIN_STACK=8388608 cargo test --workspace` vert, sauf - `typr-graph --test walkthrough` (ordre `Point/x`/`Point/y` d'un `HashSet`, flaky, sans lien). - -### Phase 3 — Typage du corps - -- `function.rs:326` : table `id → rigide` (D4). `add_interface_constraint(rigide, borne)` une seule fois - par id. -- `specialize_lambda` (`function_application.rs:1061`) : même traitement, et `rigid_to_concrete` indexé - par id. -- `is_rigid_compatible` : le retour `Lovable@B` n'accepte que le rigide de `B`. -- Défaut 3 : entre deux rigides distincts, `is_subtype_raw` doit valoir `false`. Donc `a.same(b)` sera - rejeté avec `Same@A, Same@B`, et accepté avec `Same@A, Same@A` ou avec `Same` nu (D3). - -**Statut : FAITE pour `function()` (2026-09-30, non commitée).** - -- `function.rs` : table `id → rigide` (un rigide par id, D3-A pour les `I` nus via la signature - normalisée). Un paramètre `Lovable@A` est typé par sa borne ; la table sert aussi au retour : un retour - `Lovable@B` est vérifié contre le **rigide de `B`** (`checked_ret`, aussi passé à - `set_expected_return_type` pour les `return` anticipés). `is_rigid_compatible` : si le retour déclaré est - un rigide, seul ce rigide passe (le défaut 4 venait de `interface <: Generic(_)`). -- Défaut 3 : bras `(Generic, Generic)` rigides dans `is_subtype_raw` (comparaison par nom). Piège : - `Hash` ignorait le nom d'un `Generic`, donc le cache de sous-typage confondait `(r0, r1)` et `(r0, r0)` ; - les noms de rigides entrent maintenant dans le hash (`is_rigid_name`, `type/mod.rs`). -- Tests : 5 dans `function.rs` (`test_bound_*`, `test_self_self_*`, `test_bare_interface_*`). Piège de test : - `parse2` ne se comporte pas comme le CLI (les rejets passaient) ; le helper utilise `parse_from_string`. -- `typr case run` : 0088 passe à READY (rejeté dans le corps). 0086/0087 restent en attente de la phase 4 - (READY pour de mauvaises raisons : l'appel d'une fonction `Lovable@A` échoue tant que `Bounded` n'est pas - unifié). `cargo test --workspace` vert ; mêmes 4 REGRESS préexistants. -- **Non fait** : `specialize_lambda` (les paramètres de lambda n'ont pas de `Bounded` déclaré ; à traiter en - phase 4 avec `rigid_to_concrete` par id). Rigides imbriqués (`[#N, Lovable@A]`) : toujours sans rigide. -- À corriger : le message de retour affiche `Found: __RIGID_1` ; il faudrait nommer l'id (`B`). - -### Phase 4 — Site d'appel - -- `unification.rs` : bras `Bounded` (D5), avec un occurs check comme pour `Generic`. -- `apply_unification_to_return_type` : la substitution de `Generic(id)` couvre `Bounded(id, _)` dans le - retour, y compris sous `Tuple`, `Vec` et `Function` (le `swap` du RFC §6.2 rend `tuple{Dog, Cat}`). -- `try_named_generic_match` / `collect_interface_bindings` : les signatures mixtes - (`fn(f: (T) -> U, x: Lovable@A)`) et les éléments de tableau (`[#N, Eq@A]`). -- Supprimer `try_interface_subtype_match`, ou le réduire au strict nécessaire (D5). -- Messages d'erreur au site d'appel : pour « `A` lié à `Cat` puis à `Dog` », nommer l'identifiant et les - deux arguments. - -**Statut : FAITE, avec écarts au plan (2026-09-30, non commitée).** - -- **Écart D5** : pas de bras `Bounded` dans `unification_helper`. `signature_normalization::instantiate_at_call` - normalise la signature, lie chaque id à l'argument (`bind_ids` : la borne doit être satisfaite, un même id - exige le même type par sous-typage mutuel, car le `PartialEq` de `Type` est trop lâche pour les génériques), - puis **substitue** `Bounded(id)` par le type lié dans les paramètres et le retour. Les filtres existants - voient ensuite une signature concrète (les `#N`, `T` restants sont à eux). Plus simple qu'un bras - d'unification, et le retour est substitué sous `Vec`/`Tuple`/`Function` par le même `rewrite`. -- Branché au début de `apply_from_variable_inner` : signature rejetée → retirée des candidats ; une signature - est concernée seulement si elle a un `Bounded` déclaré ou un même id ≥ 2 fois après désucrage (un `I` nu - unique garde le chemin historique). Le message d'erreur liste les signatures *déclarées*. -- **`try_interface_subtype_match` conservé** (il sert encore au `I` nu unique, `unique`/`sort`). À réduire plus tard. -- Cas de test : `cmp(cat, dog)` avec `Lovable@A, Lovable@B` OK ; `Lovable@X` répété avec `(cat, dog)` rejeté ; - `Lovable` nu répété avec `(cat, dog)` rejeté (D3-A) ; `id(id(cat))` OK ; retour `Lovable@B` = `Dog`, pas `Cat`. - 4 tests dans `signature_normalization.rs`. Cas 0086/0087/0088 : READY. `cargo test --workspace` vert - (hors `typr-graph walkthrough`, flaky connu) ; mêmes 4 REGRESS préexistants. -- Piège : le cas 0086 utilisait `Dog` ⊃ `Cat` (largeur de record), donc `Dog <: Cat` et le retour `Dog` passait - pour `Cat`. `Dog` n'a plus que `age`. -- **Non fait** : `swap` avec `tuple{…}` (syntaxe littérale non vérifiée), éléments `[#N, Eq@A]` (code prévu via - `Vec` mais sans test), `specialize_lambda` par id, message dédié « `A` lié à `Cat` puis `Dog` » (aujourd'hui : - `NoMatchingSignature`). - -### Phase 5 — Transpilation, `--checked`, LSP - -- **Le R généré ne doit pas changer** (propriété à vérifier, pas une option) : la dispatch S3 - `cmp.Lovable` / `cmp.default` utilise le nom de la borne, pas l'id. -- `transpiling/checked_assertions.rs` : un `Bounded` s'asserte comme sa borne. -- LSP : le survol affiche `Lovable@A` dans la signature et `Cat` au site d'appel. - -**Statut : FAITE (2026-09-30, non commitée). LSP vérifié par lecture seulement.** - -- **La propriété « R inchangé » était fausse** : `fn(a: Lovable@A): Lovable@A` produisait `idf.default` avec - un cast `as.Generic()`, au lieu de `idf.Lovable` / `as.Lovable()` + alias `.default`. Cause : `get_class`, - `get_class_unquoted`, `get_type_anotation` et `get_type_anotation_no_parentheses` (`context/vartype.rs`, - `context/mod.rs`) ne connaissaient pas `Bounded`, et la branche `Lang::Let` de `transpiling/mod.rs` lisait - le type du paramètre de dispatch brut (facettes interface/record). Correctif : `Bounded` est déballé en sa - borne à ces cinq endroits. -- `checked_assertions.rs` : `Bounded` s'asserte comme sa borne (`checked_descriptor`). -- Vérifié : R généré **identique** (diff vide) entre `Lovable` et `Lovable@A`, avec et sans `--checked`. - Test : `test_bounded_id_does_not_change_generated_r` (`transpiling/mod.rs`). -- LSP : aucun changement de code. Le survol passe par `type_printer`, qui affiche déjà `Lovable@A` (phase 1). - Non testé dans le crate `typr-lsp`. -- `cargo test -p typr-core` vert ; `typr case run` : mêmes 4 REGRESS préexistants, 0086-0088 READY. Sous - `cargo test --workspace`, `registry_validate::formals_diff_flags_…` a échoué une fois et passe seul (instable - en parallèle, sans lien). - -### Phase 6 — Documentation et RFC - -- Déplacer le RFC dans `rfcs/` (numéro suivant, format `0000-template.md`), avec les décisions D1-D6. -- `typr.github.io` : page de référence sur les interfaces, `syntaxe.md` (**les deux copies**), glose - du `@` suffixe dans `scripts/syntax-glossary.mjs` (sinon `check:syntax` casse), et exemples vérifiés - par `typr check` (un `compile_fail` pour `cmp(cat, dog)` avec des `Lovable` nus ). -- `ai_context/interface_as_constrained_generic.md` §4.2 et §8.4 : à mettre à jour. Cette spec disait - « paramètres distincts ⇒ variables distinctes » et proposait `where A: I`, que `I@A` remplace. - - -**Statut : FAITE (2026-09-30, non commitée).** - -- RFC : `rfcs/0000-interface-bound-generics.md` (numéro à attribuer au merge de la PR). -- `typr.github.io` : section « `I@Id` » dans `docs/reference/interfaces.md` (4 blocs : 1 `compile_fail` - pour `cmp(cat, dog)` avec `Lovable` nus, 3 vérifiés), et `syntaxe.md` §11. Il n'existe **qu'une** copie de - `syntaxe.md` (`typr.github.io`) ; `typR/typr/syntaxe.md` n'existe pas, contrairement à ce que dit le CLAUDE.md. -- Glossaire `syntax-glossary.mjs` : **inchangé**, car le manifeste n'a pas bougé (`@` y est déjà) ; - `npm run check:syntax` passe. -- `ai_context/interface_as_constrained_generic.md` : §4.2, §8.4 et la note §10 réécrits. -- Piège : `check:examples` prend le `typr` du PATH (installé, ancien). Il faut - `TYPR_BIN=…/target/debug/typr`, sinon les blocs `@Id` échouent à tort. Avec lui : 199 blocs + 7 `compile_fail` OK. -### Suivi — points « non fait » des phases 3-4 (2026-09-30, non commité) - -- **Message de retour** : `name_rigids` (`function.rs`) réécrit les rigides en `Lovable@B` (ou `Lovable` nu) : plus de `__RIGID_1`. -- **Site d'appel** : `CallInstance::Clash(IdClash)` + `TypeError::IdBoundToTwoTypes` **T049** (deux spans : « `X` est `Cat` ici / mais `Dog` ici »). - Émis quand toutes les signatures déclarées sont rejetées par un clash d'id ; sinon `NoMatchingSignature`. Entrée `explain` T049. -- **Rigides imbriqués** : `[#N, Lovable@A]`, `tuple{Lovable@B, Lovable@A}` en paramètre *et* en retour passent par `replace_bounded_with_rigids` - (`function.rs`). `swap` (tuple) et `first` (tableau) sont vérifiés, positif et négatif. -- **Bug trouvé** : `PartialEq for Type` n'avait pas de bras `Bounded` (`_ => false`) : un `Bounded` n'était jamais égal à lui-même, le graphe de types - de `hoist_aliases` se dupliquait à chaque appel, et le vérificateur bouclait (deux fonctions `Lovable@A` appelées avec des lambdas sans type). - Corrigé. Effet de bord : `get_type_anotation` trouvait un alias `FunctionN` au lieu de `as.Generic()` ; `has_bounded` compte désormais comme générique - (R généré à nouveau identique à la forme nue). -- **Hors périmètre, préexistant** : une lambda sans annotation passée en argument (`apply(fn(c) { … }, cat)`) échoue déjà avec `T` (« fn not defined »). - Donc `specialize_lambda` par id n'est pas atteignable tant que ce n'est pas résolu ; lambdas *typées* avec `@A`/`@B` : OK. -- **Reste** : `try_interface_subtype_match` (I nu unique), `[#N, Eq@A]` avec une interface `(Self, Self)` non sondé. -- Tests : 4 nouveaux dans `signature_normalization.rs`. `cargo test --workspace` vert ; mêmes 4 REGRESS. - ---- - -## 3. Tests à écrire (au fil des phases) - -| Programme | Attendu | -|---|---| -| `fn(a: Lovable@A, b: Lovable@B): bool { a.love() == b.love() }`, `cmp(cat, dog)` | OK | -| `fn(a: Lovable@A, b: Lovable@B): Lovable@B { b }`, `let r: Dog <- f(cat, dog)` | OK (défaut 1 corrigé) | -| idem, `let r: Cat <- f(cat, dog)` | erreur | -| `fn(a: Lovable@A, b: Lovable@B): Lovable@A { b }` | erreur dans le corps | -| `fn(a: Lovable@X, b: Lovable@X)` appelé avec `(cat, dog)` | erreur « X lié à Cat puis Dog » | -| `swap` : `tuple{Lovable@B, Lovable@A}` | `tuple{Dog, Cat}` | -| `fn(a: Same@A, b: Same@B): bool { a.same(b) }` | erreur (défaut 3) | -| `fn(a: Same, b: Same): bool { a.same(b) }`, appel `(1, 2)` | OK (D3) | -| `fn(a: Lovable, b: Lovable)` appelé avec `(cat, dog)` | erreur + suggestion `@A/@B` (D3) | -| `Lovable@A` et `Printable@A` dans la même signature | erreur de borne conflictuelle | -| `fn(): Lovable@A` | erreur (retour seul) | -| `double(3)`, `double(double(3))`, `unique`/`sort` sur `[#N, Point]` | inchangés (régression) | -| `Lovable @A` (espace), `Lovable@` | erreur de syntaxe, pas de perte silencieuse | -| R généré de `cmp` | identique à avant | - ---- - -## 4. Risques - -- **Changement de comportement (D3)** : c'est le principal. La phase 0 le chiffre avant qu'on - s'engage. -- **Sérialisation `.bin`** : `Bounded` va à la fin de l'enum `Type` (même contrainte que `Refined`). -- **Deux chemins d'appel** (unification générale et `try_interface_subtype_match`) : risque d'incohérence - s'ils coexistent. D5 les fusionne. -- **Couplage inter-dépôts** : le manifeste de syntaxe et le glossaire de `typr.github.io` doivent - changer ensemble, sinon le build de la doc casse. diff --git a/refined_types.md b/refined_types.md deleted file mode 100644 index 6c19c048..00000000 --- a/refined_types.md +++ /dev/null @@ -1,1144 +0,0 @@ -# Types raffinés — Spécification d’implémentation - -## 1. Statut - -Cette spécification définit l'implémentation des **types raffinés** dans TypR. Elle a pour but de remplacer les types dépendants dans le future. - -L'objectif est de permettre l'expression de contraintes supplémentaires sur des types existants sans créer un nouveau type structurel pour chaque combinaison de contraintes. - -Exemples : - -```typr -int -int & (> 0) - -[int] -[int] & length(5) -``` - -Le système doit également permettre, lorsque cela est possible, de transformer automatiquement les raffinements en **validations runtime dans le code R généré**. - ---- - -# 2. Modèle conceptuel - -Un type raffiné est une **intersection de type** entre un type de base et une ou plusieurs propriétés. - -```text -RefinedType(A, P₁, ..., Pₙ) -``` - -est représenté conceptuellement par : - -```text -A & P₁ & ... & Pₙ -``` - -où : - -* `A` est un type TypR ; -* `Pᵢ` est une propriété vérifiable ; -* `&` est l'opérateur général d'intersection de types. - -Exemple : - -```typr -[int] & length(5) -``` - -signifie : - -> une valeur de type `[int]` qui satisfait également la propriété `length(5)`. - -Le mécanisme de raffinement est donc **générique** et ne doit pas être implémenté comme une famille de types spéciaux (`VectorOfLength`, `PositiveInt`, etc.). - ---- - -# 3. Syntaxe - -## 3.1 Intersection - -La syntaxe générale est : - -```text -TypeExpr & TypeExpr -``` - -`&` est associatif et commutatif au niveau sémantique. - -Ainsi : - -```typr -A & B -B & A -``` - -désignent le même ensemble de valeurs. - -Une expression peut contenir plusieurs intersections : - -```typr -A & B & C -``` - ---- - -## 3.2 Raffinements - -Un raffinement est une expression reconnue comme une propriété applicable à un type. - -Exemples initiaux : - -```typr -int & (> 0) -int & (< 10) - -[int] & length(5) -``` - -La grammaire exacte des propriétés n'est pas imposée par cette spécification ; celles-ci sont définies par le registre interne des raffinements du compilateur. - ---- - -# 4. Sucre syntaxique des vecteurs - -La notation : - -```typr -[5, int] -``` - -doit être acceptée comme sucre syntaxique pour : - -```typr -[int] & length(5) -``` - -Le parser ou une phase de désucrage doit normaliser les deux formes vers la même représentation interne. - -Le type : - -```typr -[5, int] -``` - -ne doit donc pas créer un nouveau constructeur de type distinct. - -La forme canonique interne est : - -```text -Intersection( - Vector(Int), - Length(5) -) -``` - ---- - -# 5. Représentation de type - -L'AST/type representation doit représenter explicitement les intersections. - -Une représentation minimale est : - -```rust -enum Type { - ... - Intersection(Vec), - ... -} -``` - -Cependant, une représentation normalisée séparant type de base et propriétés est recommandée : - -```rust -struct RefinedType { - base: Type, - refinements: Vec, -} -``` - -avec : - -```rust -enum Refinement { - ... -} -``` - -Cette représentation est une optimisation sémantique et ne doit pas changer le comportement du langage. - -Le compilateur doit être capable de retrouver l'équivalent général : - -```text -A & P & Q -``` - -à partir de cette structure. - ---- - -# 6. Normalisation - -Toute intersection doit être normalisée avant les principales opérations du type checker. - -La normalisation doit : - -1. aplatir les intersections imbriquées ; -2. supprimer les doublons ; -3. rendre l'ordre des composants insignifiant ; -4. séparer les contraintes du type de base lorsque cela est possible ; -5. détecter les contradictions évidentes. - -Exemple : - -```typr -int & (> 0) & int -``` - -devient : - -```typr -int & (> 0) -``` - -Et : - -```typr -A & (B & C) -``` - -devient : - -```typr -A & B & C -``` - ---- - -# 7. Types et propriétés - -Une propriété ne constitue pas nécessairement un type indépendant utilisable partout. - -Elle représente une contrainte applicable à un type compatible. - -Exemple : - -```typr -[int] & length(5) -``` - -est valide car `[int]` possède la capacité nécessaire à l'évaluation de `length`. - -En revanche, une expression telle que : - -```typr -int & length(5) -``` - -doit être rejetée si `int` ne possède pas cette capacité. - -Le type checker doit donc vérifier la compatibilité : - -```text -Base type - ↓ -supports refinement? - ↓ -yes / no -``` - ---- - -# 8. Capacités internes - -Les propriétés doivent être associées à des **capacités internes** du compilateur. - -Exemples : - -```text -Lengthable -Comparable -Ordered -``` - -Ces capacités sont des concepts d'implémentation et ne constituent pas, dans cette première version, un mécanisme extensible par les utilisateurs. - -Exemples : - -```text -length(5) → nécessite Lengthable -(> 0) → nécessite Comparable / Ordered -(< 10) → nécessite Comparable / Ordered -``` - -Le système doit permettre au type checker de déterminer qu'un type possède la capacité requise. - ---- - -# 9. Sous-typage - -Un type raffiné est un sous-type du type qu'il raffine. - -```text -A & P <: A -``` - -Exemples : - -```text -int & (> 0) <: int - -[int] & length(5) <: [int] -``` - -De même : - -```text -A & P & Q <: A & P -``` - -Cette règle est fondamentale. - -Une fonction demandant : - -```typr -let f(x: [int]) = ... -``` - -doit accepter une valeur de type : - -```typr -[int] & length(5) -``` - -sans conversion. - ---- - -# 10. Relations entre raffinements - -Le type checker doit exposer au minimum trois opérations conceptuelles sur les raffinements : - -```text -compatible(P, Q) -implies(P, Q) -contradicts(P, Q) -``` - -Exemples : - -```text -length(5) compatible length(5) → true -length(5) contradicts length(10) → true -(> 10) contradicts (< 5) → true -(> 5) implies (> 0) → true -``` - -L'analyse exacte peut être limitée dans une première version. - -Lorsqu'une relation ne peut pas être déterminée statiquement, le compilateur doit conserver la contrainte plutôt que supposer qu'elle est vraie ou fausse. - ---- - -# 11. Contradictions - -Le type checker doit détecter les contradictions manifestes. - -Exemples : - -```typr -int & (> 10) & (< 5) -``` - -```typr -[int] & length(5) & length(10) -``` - -Ces types doivent être considérés comme inhabités. - -Le compilateur peut produire une erreur au moment de leur construction ou de leur utilisation. - -Une analyse complète des domaines numériques n'est pas requise dans la première implémentation. - ---- - -# 12. Raffinements sur les paramètres - -Les raffinements sont autorisés dans les types de paramètres. - -```typr -let f(x: int & (> 0)) = ... -``` - -Le corps de `f` peut considérer `x` comme satisfaisant le raffinement. - -Le compilateur ne doit pas générer une vérification supplémentaire à chaque utilisation de `x`. - -La vérification éventuelle est associée à l'entrée dans le contexte où le contrat est requis. - ---- - -# 13. Raffinements sur les valeurs de retour - -Les raffinements sont également autorisés sur les types de retour. - -```typr -let f(): [int] & length(5) = ... -``` - -Le compilateur doit vérifier que la valeur produite par le corps satisfait le type de retour. - -Lorsque cette propriété ne peut pas être démontrée statiquement, une validation runtime doit être générée. - ---- - -# 14. Inférence - -Le type checker doit conserver les raffinements connus statiquement. - -Exemple : - -```typr -let x: [int] & length(5) = ... -``` - -`x` possède ce type dans l'environnement de typage. - -La version initiale doit également permettre l'inférence de raffinements structurels lorsqu'ils sont déterminables sans analyse complexe. - -Exemple : - -```typr -let x = [1, 2, 3, 4, 5] -``` - -peut être typé : - -```text -[int] & length(5) -``` - -Cette capacité doit être conçue comme une optimisation de l'information disponible et ne doit pas être nécessaire à la correction du système. - ---- - -# 15. Préservation des raffinements - -Les opérations doivent déclarer leur effet sur les raffinements. - -Une opération peut : - -```text -preserve -produce -weaken -invalidate -``` - -une propriété. - -Exemple : - -```typr -let x: [int] & length(5) -let y = x -``` - -Le type de `y` conserve : - -```text -[int] & length(5) -``` - -À l'inverse : - -```typr -let z = x[1] -``` - -produit : - -```text -int -``` - -et ne conserve pas `length(5)`. - ---- - -# 16. Opérations produisant de nouveaux raffinements - -Lorsqu'une opération permet de déduire statiquement une nouvelle propriété, le type checker doit pouvoir l'ajouter au type résultat. - -Exemple : - -```typr -let x: [int] & length(5) -let y = x[1:3] -``` - -peut être typé : - -```text -[int] & length(3) -``` - -si la sémantique de l'opération garantit cette longueur. - -Cette fonctionnalité doit être implémentée de façon déclarative autant que possible. - ---- - -# 17. Invalidations - -Une opération dont le résultat ne permet plus de garantir un raffinement doit supprimer celui-ci du type statique. - -Exemple conceptuel : - -```text -x : [int] & length(5) - -operation(x) - -result : [int] -``` - -si `operation` ne garantit aucune longueur. - -Il est interdit au type checker de conserver automatiquement un raffinement qu'une opération peut rendre faux. - ---- - -# 18. Vérification statique - -Lorsqu'un raffinement peut être prouvé au compile-time, aucune validation runtime correspondante ne doit être générée. - -Exemple : - -```typr -let x: [int] & length(5) = [1,2,3,4,5] -``` - -Le compilateur peut déduire : - -```text -length(x) = 5 -``` - -Aucun test supplémentaire n'est requis dans le code R généré. - ---- - -# 19. Vérification runtime - -Lorsqu'un raffinement est requis mais ne peut pas être prouvé statiquement, le compilateur doit générer une validation runtime. - -Exemple : - -```typr -let f(x: [int] & length(5)) = ... -``` - -Si `x` provient d'une source dynamique, le code généré doit effectuer une vérification équivalente à : - -```r -if (length(x) != 5) { - stop(...) -} -``` - -La forme exacte du code R généré reste une décision de codegen. - ---- - -# 20. Principe "check at boundary" - -Une propriété doit être vérifiée au point où elle devient nécessaire, plutôt qu'à chaque utilisation. - -Exemple : - -```text -source dynamique - ↓ -validation length(5) - ↓ -x : [int] & length(5) - ↓ -plusieurs utilisations sans nouvelle validation -``` - -Une fois la vérification effectuée dans un contexte donné, le type checker peut considérer la propriété comme établie dans ce contexte. - -Le compilateur doit éviter les validations redondantes lorsqu'il dispose déjà d'une preuve ou d'une vérification antérieure encore valide. - ---- - -# 21. Génération des validations - -Chaque `Refinement` doit fournir au codegen les informations nécessaires pour générer sa validation. - -Conceptuellement : - -```rust -trait RefinementCodegen { - fn check( - &self, - value: ValueRef, - context: &CodegenContext, - ) -> Expr; -} -``` - -Exemple : - -```text -Length(5) - ↓ -length(value) == 5 -``` - -et : - -```text -GreaterThan(0) - ↓ -value > 0 -``` - -Le code généré doit être idiomatique R autant que possible. - ---- - -# 22. Origine des validations - -Une validation runtime ne doit être générée que pour une contrainte effectivement imposée par le programme. - -Le compilateur ne doit pas instrumenter arbitrairement toutes les valeurs avec toutes les propriétés connues. - -Exemple : - -```typr -let x: [int] = ... -``` - -ne doit pas déclencher de vérification de longueur. - -En revanche : - -```typr -let y: [int] & length(5) = x -``` - -doit déclencher une validation si aucune preuve statique ne permet de garantir `length(x) == 5`. - ---- - -# 23. Raffinements et coercions - -Une valeur ne doit pas automatiquement être considérée comme raffinée uniquement parce qu'elle possède le type de base. - -Ainsi : - -```text -[int] -``` - -n'implique pas : - -```text -[int] & length(5) -``` - -Une conversion vers un type raffiné doit donc constituer un **site de preuve ou de validation**. - -Conceptuellement : - -```text -[int] - ↓ -check - ↓ -[int] & length(5) -``` - ---- - -# 24. Narrowing / raffinement par condition - -La conception doit permettre ultérieurement le raffinement d'un type à la suite d'une condition. - -Exemple : - -```typr -if length(x) == 5 { - ... -} -``` - -À l'intérieur du bloc, le compilateur pourrait établir : - -```text -x : [int] & length(5) -``` - -Cette fonctionnalité n'est pas obligatoire pour la première version, mais la représentation des raffinements doit être conçue pour la supporter. - ---- - -# 25. Génériques - -Les raffinements doivent être compatibles avec les types génériques. - -Exemple cible : - -```typr -let first(x: [T] & length(> 0)): T = ... -``` - -La validation de ce type nécessite que le système puisse déterminer les capacités dont dépend le raffinement. - -La version initiale peut limiter les raffinements génériques à ceux dont les capacités sont connues statiquement. - ---- - -# 26. Types composites - -Les raffinements ne doivent pas être limités aux vecteurs. - -Le mécanisme doit être applicable à tout type pour lequel la propriété possède une sémantique. - -Exemples futurs : - -```typr -string & length(10) - -int & (> 0) - -Record & some_property(...) -``` - -Les raffinements spécifiques à un domaine ne doivent donc pas être codés directement dans le système général des vecteurs. - ---- - -# 27. Interaction avec les fonctions R - -Pour chaque fonction ou opérateur connu du compilateur, les informations de typage doivent pouvoir préciser : - -```text -arguments consommés -type résultat -raffinements produits -raffinements préservés -raffinements invalidés -``` - -Cela est particulièrement important pour les opérations vectorielles et le mécanisme de lifting de TypR. - -Exemple conceptuel : - -```text -length - [T] → int - -slice - [T] & length(N), range(a,b) - → [T] & length(...) -``` - -Les fonctions externes dont les effets ne sont pas connus doivent être traitées de manière conservatrice. - ---- - -# 28. Interaction avec `c()` - -`c()` et les autres opérations de combinaison de vecteurs doivent être considérées comme potentiellement génératrices de nouveaux raffinements. - -Par exemple : - -```typr -let x = c([1,2], [3,4,5]) -``` - -peut éventuellement être inféré comme : - -```text -[int] & length(5) -``` - -si les longueurs sont connues. - -La conservation des raffinements doit cependant être dépendante de la sémantique réelle de l'opération et non d'une règle générale supposée. - ---- - -# 29. Diagnostic statique - -Les erreurs de typage doivent afficher les raffinements sous une forme lisible. - -Exemple : - -```text -Type mismatch - -Expected: - [int] & length(5) - -Found: - [int] & length(3) -``` - -Pour une contradiction : - -```text -Unsatisfiable type refinement: - -[int] & length(5) & length(10) -``` - -Pour une propriété incompatible : - -```text -Invalid refinement: - -length(5) cannot be applied to int -``` - ---- - -# 30. Diagnostic runtime - -Les validations générées doivent produire des messages permettant d'identifier : - -1. la propriété attendue ; -2. la valeur observée lorsque cela est pertinent ; -3. le point du programme où le contrat a été violé. - -Exemple : - -```text -Type refinement violation: -expected vector length 5, got 3 -``` - -Le format exact du message peut être défini indépendamment du type checker. - ---- - -# 31. Erreurs de vérification - -Lorsqu'une propriété est impossible à établir statiquement, le compilateur ne doit jamais transformer silencieusement l'incertitude en vérité. - -Il existe trois états conceptuels : - -```text -Proven true -Proven false -Unknown -``` - -`Unknown` déclenche une validation runtime lorsqu'une garantie est nécessaire. - -`False` produit une erreur statique lorsque la contradiction est certaine. - ---- - -# 32. Phases d'implémentation - -L'implémentation dans `typr-core` doit suivre au minimum les étapes suivantes : - -```text -Parser - ↓ -AST - ↓ -Desugaring - ↓ -Type normalization - ↓ -Refinement validation - ↓ -Subtype checking - ↓ -Refinement propagation - ↓ -Static proof / constraint checking - ↓ -Runtime-check insertion - ↓ -R code generation -``` - -Le codegen ne doit pas avoir à comprendre la logique complète des raffinements : les décisions de typage et les points de validation doivent être déterminés auparavant. - ---- - -# 33. Invariants du type checker - -Le type checker doit garantir les invariants suivants. - -### Invariant 1 - -Une intersection est indépendante de l'ordre de ses composants. - -```text -A & B = B & A -``` - -### Invariant 2 - -Une intersection redondante est normalisée. - -```text -A & A = A -``` - -### Invariant 3 - -Un raffinement est toujours associé à un type compatible. - -### Invariant 4 - -Un type raffiné est toujours un sous-type de son type de base. - -### Invariant 5 - -Une propriété statiquement prouvée ne nécessite pas de validation runtime. - -### Invariant 6 - -Une propriété inconnue requise au runtime doit provoquer une validation. - -### Invariant 7 - -Une propriété invalidée par une opération ne peut pas être conservée dans le type résultant. - ---- - -# 34. Tests obligatoires - -L'implémentation doit inclure des tests pour au minimum : - -```text -int & (> 0) - -[int] & length(5) - -[5, int] - -A & B == B & A - -A & A == A - -(A & B) & C == A & B & C - -[int] & length(5) <: [int] - -[int] & length(5) & length(10) → contradiction - -[int] → [int] & length(5) → runtime check - -statically provable refinement → no runtime check - -refinement invalidation - -refinement propagation - -parameter refinement - -return refinement -``` - -Des tests de codegen doivent vérifier à la fois : - -```text -correctness of generated R -absence of unnecessary checks -presence of required checks -``` - ---- - -# 35. Première implémentation recommandée - -La première version du système doit rester volontairement limitée. - -Elle doit fournir : - -```text -& -intersection de types - -length(N) -longueur exacte - -> -< -comparaisons numériques simples - -normalisation -déduplication -contradictions évidentes - -subtyping -A & P <: A - -runtime validation -génération automatique des checks R -``` - -La première version ne doit pas chercher à construire un solveur SMT ou un système de preuve général. - ---- - -# 36. Extensions futures - -Le système doit être conçu pour pouvoir accueillir ultérieurement : - -```text -length(> 0) -length(< 100) - ->= -<= - -membership -x ∈ Set - -regex / string constraints - -numeric ranges - -nullability constraints - -data-frame shape constraints - -column constraints - -refinements dépendant de champs - -flow-sensitive refinement - -generic dependent refinements -``` - -Ces extensions ne doivent pas nécessiter de modifier le principe fondamental : - -```text -base type & property -``` - ---- - -# 37. Exemple complet - -Code TypR : - -```typr -type Coordinates = [float] & length(2) - -let distance( - a: Coordinates, - b: Coordinates -): float = ... -``` - -Une valeur déjà connue : - -```typr -let a: Coordinates = [1.0, 2.0] -``` - -peut être prouvée statiquement. - -Une valeur dynamique : - -```typr -let a: [float] = read_coordinates() -let d = distance(a, b) -``` - -nécessite une validation avant l'appel : - -```text -a : [float] - -distance expects: - [float] & length(2) - -therefore: - insert runtime check -``` - -Le R généré pourra conceptuellement contenir : - -```r -if (length(a) != 2) { - stop("Type refinement violation: expected length 2") -} - -distance(a, b) -``` - -Le corps de `distance` peut ensuite manipuler `a` en supposant : - -```text -a : [float] & length(2) -``` - -sans réintroduire la vérification à chaque accès. - ---- - -# 38. Principe architectural final - -Les types raffinés de TypR doivent être implémentés comme une **extension du système d'intersection existant**, et non comme un système de types parallèle. - -Le cœur du modèle est : - -```text -Type - = -Base Type - & -Properties -``` - -Par conséquent : - -```typr -[int] -``` - -reste un type ordinaire, - -```typr -[int] & length(5) -``` - -est une intersection, - -et : - -```typr -[5, int] -``` - -est uniquement une notation abrégée. - -Le compilateur a ensuite la responsabilité de déterminer, pour chaque propriété : - -```text -Est-elle compatible avec le type ? - ↓ -Est-elle déjà prouvée ? - ↓ -Peut-elle être déduite ? - ↓ -A-t-elle été invalidée ? - ↓ -Faut-il générer une validation R ? -``` - -Cette architecture permet à TypR d'utiliser progressivement les raffinements comme **contrats statiques et runtime**, tout en conservant un système de types simple, composable et compatible avec la nature incrémentale de TypR. diff --git a/refined_types_plan.md b/refined_types_plan.md deleted file mode 100644 index 4f976ae0..00000000 --- a/refined_types_plan.md +++ /dev/null @@ -1,251 +0,0 @@ -# Types raffinés — plan d'implémentation - -Plan pour `refined_types.md`, écrit après lecture de `typr-core` (état au 2026-09-29, branche `develop`). - ---- - -## 0. Ce qui existe déjà (et ce qui change la donne) - -L'analyse du code montre que plusieurs pièces de la spec existent déjà, sous une autre forme. - -| Élément de la spec | État actuel dans le code | -|---|---| -| `&` dans les types | Parsé (`parsing/types.rs:1062`) en `Type::Operator(TypeOperator::Intersection, ..)`, mais **limité aux Records/Interfaces** (`type_arithmetic.rs::accepts_record_kind`, `norm_intersection`). `int & int` échoue. | -| Aplatissement, commutativité, déduplication | `IntersectionType` (`components/type/intersection_type.rs`) aplatit l'arbre binaire dans un `HashSet`. L'égalité `A & B == B & A` est déjà en place (`type/mod.rs:1229`). | -| `length(N)` sur les vecteurs | **La longueur est déjà dans le type** : `Type::Vec(VecType, index, elem, h)`. `[5, int]` donne `Vec(S3, Integer(5), int)` et `[int]` donne `Vec(S3, Any, int)`. | -| `[5,int] <: [int]` | Fonctionne déjà (`Integer(5) <: Any`, `type/mod.rs:221`). | -| Contradiction `length(5)` / `length(3)` | Déjà une erreur statique : `let x: [5, int] <- [1,2,3]` est rejeté. | -| Inférence `[1,2,3,4,5] : [5, int]` (§14) | Déjà en place. | -| `c()` produit une longueur (§28) | Déjà en place : `c([1,2],[3,4,5])` a le type `Vec[5, int]`, `c(x, x)` a le type `Vec[10, int]`. | -| Préservation par opération vectorisée | `x + 1` garde `[5, int]` (lifting, `Type::lift`). | -| Validations runtime | Seulement en mode test `--checked` (`transpiling/checked_assertions.rs`, `typr_assert_type`). Elles contrôlent la classe, pas la longueur. | - -Les sondes faites contre le binaire actuel ont aussi révélé des défauts à corriger en chemin : - -- **`int & (> 0)` fait paniquer le parseur** (`parsing/types.rs:974`, `compute_operators` → `_ => panic!()`), parce que `>` est lu comme un `Op` dans `index_algebra`. -- `[int] & length(5)` est parsé en `[any, int] & length` : `length` devient un `Type::Variable` et le `(5)` est perdu. -- `x[1:3]` et `x[x > 2]` sont typés **`int`** alors que ce sont des vecteurs. C'est un bug existant, et c'est précisément la propagation dont parlent les §15-16. -- `[int]` passé là où `[3, int]` est attendu est **une erreur statique** aujourd'hui. La spec (§22-23) en fait un site de validation runtime : **c'est un changement de comportement**, voir D3. - ---- - -## 1. Décisions de conception - -### D1. Représentation : un variant `Refined` plus un variant transitoire `Property` - -```rust -// components/type/mod.rs — à ajouter À LA FIN de l'enum (voir §Risques : sérialisation .bin) -Refined(Box, RefinementSet, HelpData), // base & propriétés, forme normalisée -Property(Refinement, HelpData), // sortie du parseur uniquement ; ne doit jamais survivre à reduce_type -``` - -C'est la forme « `RefinedType { base, refinements }` » que recommande la spec au §5. On n'étend pas `Operator(Intersection)` à la place, pour deux raisons : - -- dans le code actuel, `&` sur des Records veut dire *fusion structurelle*, pas prédicat ; -- un arbre binaire ne donne ni l'ordre canonique ni la détection des contradictions sans renormaliser à chaque fois. - -Le parseur continue de produire `Operator(Intersection, base, Property(p))`. C'est `norm_intersection` qui les replie en `Refined`. - -### D2. `RefinementSet` : raisonner par *mesures* et *intervalles* - -Plutôt qu'une liste de propriétés à comparer deux à deux, on normalise chaque propriété sur une **mesure** munie d'un **intervalle** : - -```rust -// components/type/refinement.rs (nouveau) -pub enum Measure { Value, Length } // extensible : Nchar, Nrow, Ncol... -pub struct Interval { lo: Bound, hi: Bound } // Bound = Open(c) | Closed(c) | Unbounded -pub struct RefinementSet(BTreeMap); // ordonné → Hash/Eq déterministes - -pub enum Refinement { // forme de surface (parse / affichage) - Length(i32), // length(5) → Length ∈ [5,5] - Gt(Lit), Lt(Lit), // (> 0) → Value ∈ (0, +∞) -} -``` - -Avec cette forme, les exigences de la spec tombent d'elles-mêmes : - -| Exigence | Traduction | -|---|---| -| dédup, commutativité, associativité (§6, invariants 1-2) | intersection d'intervalles par mesure | -| `contradicts(P,Q)` (§10-11) | intervalle vide (`(>10)&(<5)`, `length(5)&length(10)`) | -| `implies(P,Q)` | inclusion d'intervalles (`(>5)` ⊂ `(>0)`) | -| `compatible(P,Q)` | intersection non vide | -| extensions §36 (`length(> 0)`, `>=`, plages) | autres bornes sur le même modèle, sans changer le principe | -| narrowing §24 | on intersecte l'intervalle de la condition | - -Bonus : sur `int`, on peut repérer `int & (> 0) & (< 1)` comme vide, en arrondissant les bornes aux entiers. - -Il faut une représentation hachable des constantes numériques (`Type: Hash + Eq`). On réutilise celle de `Tnum` plutôt qu'un `f64` brut. - -### D3. Trois états, et `is_subtype` reste strict - -`is_subtype` est un booléen mis en cache (`SUBTYPE_CACHE`) et utilisé partout : dispatch, unification, sélection de surcharge. **Il ne doit répondre `true` que pour `Proven`.** L'état `Unknown` passe par une fonction séparée, appelée uniquement aux frontières : - -```rust -// processes/type_checking/refinement_check.rs (nouveau) -pub enum Proof { Proven, Refuted, Unknown } -pub enum Coercion { Static, Runtime(RefinementSet /* résidu non prouvé */), Reject } -pub fn coerce_to(found: &Type, expected: &Type, ctx: &Context) -> Coercion -``` - -La règle : `coerce_to` renvoie `Runtime(résidu)` si `base(found) <: base(expected)` et que le résidu est `Unknown`. Il renvoie `Reject` si le résidu est `Refuted`. - -Si on laissait `is_subtype` devenir tolérant, un `[int]` sélectionnerait une surcharge `[5, int]`, et le code deviendrait insoundable sans que rien ne le signale. - -### D4. La longueur des vecteurs reste dans l'index de `Type::Vec`, pour l'instant - -La spec (§4) veut la forme canonique `Intersection(Vector(Int), Length(5))`. On fait **l'inverse en v1** : `[int] & length(5)` se normalise en `Vec(S3, Integer(5), int)`. Une vue `refinements_of(&Type) -> RefinementSet` relit la longueur depuis l'index. - -Pourquoi : -- `Type::Vec(` apparaît dans **135 motifs** répartis sur 20 fichiers ; -- le lifting vectoriel (`Type::lift`) et les génériques d'index `#N` (`[#N, T]`, arithmétique `#N+1`) dépendent tous de ce slot ; -- une migration en une fois casserait les 868 tests pour un gain nul en v1. - -Le §5 l'autorise explicitement (« optimisation sémantique, ne doit pas changer le comportement »), à condition de retrouver `A & P` à partir de la structure : c'est le rôle de `refinements_of`. - -Les intervalles de longueur non ponctuels (`length(> 0)`, futur) iraient dans le `RefinementSet` d'un `Refined(Vec(S3, Any, T), …)`. La migration complète vers `Length` est la phase 9, optionnelle. - -### D5. Les obligations runtime sont décidées au typage, pas au codegen (§32) - -Quand `coerce_to` renvoie `Runtime(résidu)`, le type checker enregistre une **obligation** dans le `Context` : une table latérale indexée par la position (`HelpData`) de l'expression, sur le modèle de `type_recorder.rs`. Le transpileur se contente de la consommer et ne raisonne jamais sur les raffinements. - -Ensuite, la variable entre dans l'environnement avec le type raffiné. C'est le « check at boundary » du §20 : aucune nouvelle vérification aux usages suivants. - -### D6. Forme du R généré - -Le public cible est constitué de développeurs R qui lisent le code généré. Il faut donc que ce soit **court, lisible et inconditionnel** (hors du mode `--checked`). Je recommande de petits helpers du prélude qui renvoient la valeur, pour qu'ils fonctionnent aussi en position d'expression (arguments, `return`) : - -```r -a <- typr_refine_length(read_coordinates(), 2L, "main.ty:12") -distance(typr_refine_length(a, 2L, "main.ty:14"), b) -``` - -Le message produit est `Type refinement violation at main.ty:14: expected length 2, got 3` (§30). - -Pour les comparaisons, le test est `isTRUE(all(x > 0))`, qui est sûr face aux `NA` et aux vecteurs. Une variante « `if (...) stop(...)` inline » reste possible pour les `let`. Le choix est à trancher, voir Q4. - ---- - -## 2. Phases - -Chaque phase laisse `cargo test --workspace` et `typr case run` au vert. - -### Phase 0 — Préparation -- Commiter d'abord le travail en cours sur les angles morts (cas 0068-0072). Il touche `type/mod.rs`, `parsing/types.rs` et `type_checking/mod.rs`, exactement les fichiers de la phase 1, et mélanger les deux diffs serait pénible. -- Trancher les questions ouvertes (§3 ci-dessous), surtout Q1 à Q3. - -### Phase 1 — Modèle (`typr-core/components/type`) -- Créer `refinement.rs` avec `Measure`, `Interval`, `RefinementSet`, `Refinement`, et les opérations `meet`, `is_empty`, `implies`, `compatible`, `contradicts`. -- Ajouter `Type::Refined` et `Type::Property` à la fin de l'enum. Compléter les `match` exhaustifs dans `typr-core`, `typr-graph`, `typr-lsp` et `typr-wasm` : le compilateur les listera. -- Ajouter `refinements_of(&Type)`. Elle lit l'index `Vec` → `Length`, les littéraux `Integer(Val(3))`/`Number` → `Value ∈ [3,3]`, et `Refined` → son ensemble. -- Mettre à jour `type_category.rs` (catégorie `Refined`) et `type_printer.rs` : affichage `[int] & length(5)` et `int & (> 0)`. -- **Tests unitaires** : l'algèbre des intervalles, plus les lignes « A&B==B&A », « A&A==A », « (A&B)&C » et « contradiction » du §34. - -### Phase 2 — Parser et syntaxe -- Dans `parsing/types.rs`, ajouter un parseur `refinement_property`, essayé **avant** `parenthese_value` et `type_variable` dans `single_type` : - - `(` `>`|`<` littéral `)` → `Type::Property(Gt/Lt)` - - `length(` entier `)` → `Type::Property(Length)` -- Corriger la panique de `compute_operators` (erreur de parse au lieu de `panic!`) et ajouter un cas `cases/` pour `int & (> 0)`. -- `[5, int]` ne change pas : c'est déjà la forme de stockage d'après D4. -- **Manifeste de syntaxe** (`components/syntax/mod.rs`) : déclarer `length` comme mot de type et `>`/`<` en position de type. Les deux tests d'invariant l'exigent. Ensuite, lancer `typr syntax --write` pour régénérer la grammaire tmLanguage. -- ⚠️ Côté `typr.github.io`, `scripts/syntax-glossary.mjs` doit recevoir la glose des nouveaux lexèmes, sinon le build de la doc casse. Le changement doit être fait **en même temps** dans les deux dépôts. - -### Phase 3 — Normalisation et validation (`type_arithmetic.rs`) -Étendre `norm_intersection` : -- `(X, Property p)` → `Refined(X, {p})` après le contrôle de capacité. Même chose en symétrique. -- `(Refined(A,ps), Property q)` → `Refined(A, ps ⊓ q)`. -- `(Refined(A,ps), Refined(B,qs))` → `Refined(norm(A&B), ps ⊓ qs)`. -- `(Vec(_, Any, T), Length n)` → `Vec(_, n, T)` (D4). Si `(Vec(_, m, T), Length n)` avec `m ≠ n`, c'est une contradiction. -- Littéral + propriété : `3 & (> 0)` → `3` si prouvé, contradiction si réfuté. -- Un ensemble vide donne l'erreur **`UnsatisfiableRefinement`** (§11, §29). -- Un `Property` seul qui survit à la réduction donne une erreur « une propriété n'est pas un type ». - -Les **capacités** (§8) passent par une simple fonction `capabilities(base) -> {Lengthable, Ordered}` : -- `Vec` → Lengthable ; -- `Integer`/`Number` → Ordered ; -- générique non contraint → rien en v1. - -Si la capacité manque, on lève **`InvalidRefinement("length(5)", int)`**. - -Le cas `&` sur des Records reste inchangé. - -Ajouter les nouveaux variants dans `error_message/type_error.rs` avec leurs codes `T0xx` (utilisés par le MCP) et leurs messages au format du §29. - -`type Coordinates = [num] & length(2)` fonctionne sans travail spécifique, par la réduction des alias. - -### Phase 4 — Sous-typage (`type/mod.rs::is_subtype_raw`) -- `Refined(A, ps) <: B` si `A <: B` (invariant 4, §9). -- `A <: Refined(B, qs)` si `A <: B` et `decide(refinements_of(A), qs) == Proven`. -- `Refined(A, ps) <: Refined(B, qs)` si `A <: B` et `ps implies qs`. -- Placer ces règles **avant** les règles génériques `Operator(Intersection, ..)` existantes, qui feraient sinon `t1 <: typ || t2 <: typ` sur un `Property`. -- Vérifier l'unification (`unification.rs`) : un générique `T` lié à un argument raffiné prend le type raffiné complet (paramétricité, donc sûr). En revanche, les opérateurs arithmétiques et les fonctions non annotées renvoient le **type de base**. L'invariant 7 est ainsi garanti par construction. - -### Phase 5 — Frontières et obligations (§12, 13, 19-23) -- Créer `refinement_check.rs` avec `coerce_to` (D3) et la table d'obligations dans le `Context` (D5). -- Brancher `coerce_to` là où un type attendu rencontre un type trouvé : - - `let_expression.rs` : annotation de `let` ; - - `function_application.rs` : arguments, côté appelant (§37) ; - - `function.rs` : type de retour ; - - les constructeurs de record, dans un second temps. -- Paramètres : à l'intérieur du corps, `x` a le type raffiné sans aucune vérification (§12). -- **Changement de comportement** : `[int]` → `[3, int]` passe d'une erreur statique (T001/T002) à une vérification runtime. Il faut repérer les tests et `cases/` qui attendaient l'erreur (`typr case run` les signalera en REGRESS) et les réécrire comme témoins de l'absence ou de la présence de vérification. - -### Phase 6 — Codegen (`transpiling/`) -- Écrire le module `transpiling/refinement_checks.rs`. Il consomme les obligations et génère les appels du prélude (D6). Chaque `Measure` sait produire son test (§21) : `Length` → `length(x) == n`, `Value` → `x > c` / `x < c`. -- Ajouter les helpers `typr_refine_*` au prélude R, là où vit `typr_assert_type`. -- Aucune vérification n'est émise quand l'obligation est absente (invariants 5 et 6, §18, §22). -- Plus tard, en v1.1 (Q3) : un prologue de vérification pour les fonctions **exportées** d'un package, puisque l'appelant y est du R non typé. - -### Phase 7 — Propagation et invalidation (§15-17, §27-28) -- Corriger au passage `x[1:3]` et `x[x > 2]`, aujourd'hui typés `int`. Ils doivent donner respectivement `[3, int]` (si les bornes sont littérales, sinon `[int]`) et `[int]`. -- `length(x)` avec `x : [5, int]` doit donner le type littéral `5`, ce qui prépare le narrowing. -- Règle par défaut, conservatrice : une fonction dont l'effet n'est pas déclaré renvoie son type de retour déclaré, **sans** raffinement. -- Les effets déclaratifs (preserve/produce/invalidate) sont d'abord codés en dur pour une poignée de primitives (`[`, `:`, `c`, `length`, `rev`, `head`). Une syntaxe d'annotation dans `configs/std/*.ty` pourra venir plus tard. - -### Phase 8 — Tests, cas, documentation -- Les **tests unitaires** couvrent les 14 lignes du §34. -- Côté **`cases/`**, prévoir un cas par règle de codegen. Le format `expect.toml` (des règles sur des sous-chaînes) convient bien à « présence de check » et à « absence de check inutile » : lire `cases/README.md` pour la règle d'absence. Au minimum : - - un littéral prouvé ne produit pas de check ; - - `[int]` → `[5,int]` produit un check ; - - raffinement de paramètre ; - - raffinement de retour ; - - contradiction ; - - capacité invalide ; - - `int & (> 0)` (la panique actuelle). -- Mettre à jour `syntaxe.md`, dans ses **deux** copies, et la page de référence des types dans `typr.github.io` (Diátaxis : reference, plus une explication dans `concepts/`). Les exemples doivent passer `typr check`, car la CI `check:examples` les vérifie. -- Lancer `typr std` si les signatures du std changent (effets de la phase 7). - -### Phase 9 — Plus tard (hors v1) -- Narrowing par condition (§24) : `if (length(x) == 5)` intersecte l'intervalle dans la branche. Le modèle par intervalles le permet directement. -- Génériques `[T] & length(> 0)` (§25) et bornes d'intervalle sur `Length`. -- Migration de l'index de `Type::Vec` vers `Measure::Length`, et remplacement des types dépendants `#N` : c'est l'objectif final annoncé par la spec au §1. - ---- - -## 3. Questions ouvertes sur la spec - -1. **`string & length(10)`** (§26) : en R, `length("abc")` vaut `1`. Parle-t-on de `nchar` ? Si oui, il faut une mesure distincte (`Nchar`), sans quoi le raffinement serait trompeur pour un dev R. -2. **`(> 0)` sur un vecteur** : `[int] & (> 0)` veut-il dire « tous les éléments » ? D'ailleurs un `int` TypR est un vecteur R de longueur 1. Je propose le sens « tous », vérifié par `isTRUE(all(x > 0))`, ce qui règle aussi le cas `NA`. -3. **Où placer les vérifications de paramètres** : côté appelant (§37) ou dans le prologue de l'appelé ? Je recommande l'appelant pour le code TypR (on évite le check quand c'est prouvé, invariant 5), plus un prologue pour les fonctions exportées (appels depuis du R non typé). -4. **Le narrowing implicite avec vérification runtime** (`[int]` → `[5,int]` accepté puis vérifié) remplace une erreur statique actuelle. Veut-on un avertissement, ou une option pour exiger une conversion explicite ? -5. **Forme canonique du §4** : la v1 stocke la longueur dans `Type::Vec` (D4). Il faut confirmer que c'est acceptable tant que `refinements_of` redonne `A & length(N)`. -6. **Bornes `num` vs `int`** : `int & (> 0.5)` doit-il être accepté (équivalent à `>= 1`) ou rejeté ? - ---- - -## 4. Risques - -- **Les `.bin` du std sont sérialisés** (`serde`) : ajouter les variants **à la fin** de `enum Type` pour garder les indices, puis lancer `typr std`. -- **Le cache de sous-typage** : `Refined` doit avoir un `Hash`/`Eq` canonique (d'où le `BTreeMap`), sinon `A&B` et `B&A` occupent deux entrées différentes. -- **Récursion** : la normalisation passe par `reduce_type`. Garder `RUST_MIN_STACK=8388608` en tête si un test déborde. -- **Soundness** : ne jamais laisser `is_subtype` répondre `true` sur `Unknown` (D3). C'est l'invariant qui empêche de « transformer silencieusement l'incertitude en vérité » (§31). - -## 5. Ordre de livraison - -| PR | Contenu | Impact utilisateur | -|---|---|---| -| 1 | Phase 0 + Phase 1 + correction de la panique `int & (> 0)` | aucun (modèle interne) | -| 2 | Phases 2-4 : syntaxe, normalisation, sous-typage, erreurs statiques | `int & (> 0)` et `[int] & length(5)` acceptés ; contradictions et capacités invalides détectées | -| 3 | Phases 5-6 : obligations et codegen | vérifications runtime (changement de comportement Q4) | -| 4 | Phase 7 : propagation, avec la correction de `x[1:3]` | types plus précis | -| 5 | Phase 8, docs (dans les deux dépôts) | — | diff --git a/stdlib.md b/stdlib.md deleted file mode 100644 index aa6bfc06..00000000 --- a/stdlib.md +++ /dev/null @@ -1,426 +0,0 @@ -# 📘 RFC-STDLIB-0001 — Bibliothèque standard R typée : plan d'application - -> **Résumé.** Construire un catalogue unique et curaté des fonctions de la -> standard library R (`base`, puis `stats`, `utils`, …), typées pour TypR, -> qui alimente **deux artefacts distincts** : -> 1. la doc technique consommée par le **MCP** (riche, tous les degrés de -> confiance, jamais chargée par le compilateur) ; -> 2. la surcouche typée embarquée dans le compilateur (sous-ensemble -> « haute confiance », seul à finir dans les `.bin`). -> -> La dépendance est inversée : **la stdlib compilée devient un sous-produit du -> catalogue de doc**, pas l'inverse. - ---- - -## 1. 🎯 Objectif - -Offrir aux utilisateurs (et au MCP) des signatures typées pour les fonctions -R classiques, pour ne pas avoir à écrire de signature ni mémoriser les types. -Deux bénéficiaires, deux tolérances différentes : - -| Bénéficiaire | A besoin de | Tolérance à l'erreur | -|---|---|---| -| **MCP / doc** | signatures + sémantique (params, exemples, règles de coercion, pièges) | faible — un mauvais exemple induit en erreur | -| **Compilateur** | signatures *vérifiables* uniquement | très faible — une signature fausse produit des erreurs de type déroutantes ou des casts R erronés | - -**Le risque principal n'est jamais la mémoire — c'est la soundness.** -Une signature fausse est pire que pas de signature. - ---- - -## 2. État des lieux (ce qui existe déjà) - -Le mécanisme est en place et fonctionnel. Voici l'état après Phase 0 + 1 + 2 + 3 : - -### 2.1. Inventaire - -- **Noms non typés** : `crates/typr-cli/configs/src/functions_R.txt` (761 noms) - → `.std_r.bin` (`build_function_list_vartype`, chaque nom = `UnknownFunction`). -- **Introspection R** : `configs/src/r_name_db.json` (1283 noms, seed base-R, - pkg, s3_generic, s4_generic). - -### 2.2. Catalogue de signatures (fichiers `.ty`) - -Tous les fichiers `.ty` portent désormais des annotations `#!` (pkg, tier, -param, ret, coercion, example, seealso) conformes au format RFC. - -**Sink B — compilateur** (`R_T1_SOURCES`, → `.std_r_typed.bin`) : - -| Fichier | Signatures `@` | Let bindings | Description | -|---|---|---|---| -| `std_R.ty` | 29 | 0 | Opérateurs (+,-,*,/,%%,&&,\|\|), sum, print, reduce/fold/extend, testing, interop | -| `default.ty` | 20 | 0 | TypR-specific : as_vec, map, filter, set_at, add/minus/mul/div, get, seq, join, startsWith, endsWith, contains | -| `file.ty` | 11 | 0 | Système de fichiers (getwd, setwd, dir, file__exists, ...) | -| `option.ty` | 3 | 5 | Option type + unwrap/expect/is_some/is_none | -| `factor.ty` | 6 | 0 | Factor opaque + factor/levels/nlevels | -| `state.ty` | 7 | 0 | State mutable cell + get/set/update/map/derive | -| `ord.ty` | 2 | 0 | Eq/Ord interfaces + unique/sort | -| `lin_alg.ty` | 2 | 2 | dot, t, lvec, cvec | -| `plot.ty` | 1 | 2 | plot signature + Plot record (champ `kind`), bplot, show | -| `system.ty` | 3 | 0 | system2, bsystem2, exec | -| `foreign.ty` | 0 | 0 | Foreign opaque type | - -**Sink A — MCP/doc uniquement** (`R_DOC_ONLY_SOURCES`) : - -| Fichier | Signatures `@` | Description | -|---|---|---| -| `base.ty` | 128 | Fonctions R base (math, stats, comparaison, string, set, matrix, vector) — **doc SPG seulement** | -| `stats.ty` | 103 | Fonctions du package stats (dnorm…rhyper, t.test, cor.test, lm, …) — **doc SPG seulement** | -| `utils.ty` | 55 | Fonctions du package utils (read.csv, write.csv, head, …) — **doc SPG seulement** | - -> ⚠️ **Contrainte architecturale** : `base.ty` est dans `R_DOC_ONLY_SOURCES` -> (pas `R_T1_SOURCES`) car `stdlib_declared_names()` ne doit contenir que les -> fonctions **TypR-propriété** (celles pour lesquelles TypR fournit sa propre -> implémentation dans `std.R` ou la codegen du transpileur). Les fonctions R -> base (`abs`, `sqrt`, `mean`, etc.) ne sont pas TypR-propriété — TypR ne -> fait que les annoter en types pour la doc MCP. Les inclure dans -> `stdlib_declared_names()` casserait le test -> `stdlib_declared_names_covers_the_bundled_ty_signatures`. - -### 2.3. Infrastructure SPG (Phase 1 ✅) - -- **`StdlibMeta`** (`model.rs`) : champs optionnels `tier`, `param_docs`, - `ret_doc`, `coercion_notes`, `examples`, `seealso`, `pkg` — tous `Option<>`, - rétro-compatibles. -- **`stdlib_meta.rs`** : parser `parse_meta_from_source()` — accumulate les - blocs `#!` jusqu'à la ligne `@name:`, puis attache au nom. 8 tests unitaires. -- **`doc_attach.rs`** : lit les annotations `#!` des `.ty` (attach par nom). -- **`build_stdlib_docs()`** : chaîne `R_T1_SOURCES` + `R_DOC_ONLY_SOURCES` - → SPG enrichi avec méta. -- **Tests** : 7 tests `standard_library` + 8 tests `stdlib_meta` = 15/15 ✅. - -### 2.4. Commandes - -- `typr std` : régénère `.std_r.bin` + `.std_r_typed.bin` + `.std_js.bin` + - `.std_js_typed.bin`. -- `typr std doc` : émet le SPG JSON-LD enrichi (T1+T2, avec méta). -- `typr std doc --format md` : émet un digest markdown compact (groupé par - package, badge T1/T2, signature, 1 doc, 1 exemple, seealso). Sortie : - stdout ou `--output `. - -### 2.5. Ressource MCP `typr://stdlib` - -- Digest markdown généré par `typr std doc --format md`, embarqué via - `include_str!` dans `crates/typr-mcp/data/stdlib.md` (~60 Ko, 386 entités, - 14 packages). -- Exposé comme ressource **lecture seule** (`typr://stdlib`) — le MCP la - récupère à la demande, jamais chargée d'office → zéro surcharge de contexte. -- Régénérer après tout changement dans `configs/std/*.ty` : - `typr std doc --format md --output crates/typr-mcp/data/stdlib.md` - -### 2.6. Chargement compilateur - -- `load_r()` / `load_typed_r()` dans `crates/typr-core/src/components/context/vartype.rs`, - blobs bincode embarqués par `include_bytes!` dans `configs/bin/`. - -### 2.7. Blacklist - -- 60 noms refusés (`c`, `lapply`, `sapply`, `rep`, `str`, `cat`, `length`, …) - dans `crates/typr-core/src/utils/standard_library.rs` - (+ `not_in_blacklist` / `validate_vectorization`). - -### 2.7. Records dans le catalogue - -- `plot.ty` et `system.ty` déclarent des records (`type Plot = record{ … }`, - `type System2 = record{ … }`). Le parser accepte la forme explicite - `record{ … }` (spec §2.2) ; la forme nue `{ … }` au niveau ltype top n'est - pas supportée (c'était la cause réelle du SKIPPED historique des deux - fichiers, pas le type-cast ni le champ). Phase 2 les a adaptés en - conséquence (`record{…}`, renommage du champ `type` → `kind` pour éviter - une collision sémantique avec le mot-clé de déclaration) : les deux fichiers - compilent désormais, zéro SKIPPED. - -> ⚠️ **Constat clé** : la blacklist n'existe pas pour la mémoire mais pour la -> soundness. `c()`, `lapply()`… sont intrinsèquement non typables fidèlement -> en base R (coercion, recycling, `...`, dispatch S3). Tout le plan doit -> préserver ce principe. - ---- - -## 3. Principes directeurs - -1. **Une seule source de vérité** : le catalogue (fichiers `.ty` enrichis, - un par package). Rien n'est obtenu ailleurs — ni les `.bin`, ni le SPG doc. -2. **Deux sinks, deux exigences** : - - Sink A — **MCP/doc** : consomme *tout* (T1 + T2), enrichi (exemples, - notes de coercion, relations). Jamais chargé par le compilateur. - - Sink B — **compilateur** : ne charge que le sous-ensemble **T1** - (haute confiance) dans `.std_*_typed.bin`. -3. **Dépendance inversée** : on ne dérive pas la doc de la stdlib compilée ; - on dérive la stdlib compilée du catalogue de doc. -4. **Soundness over coverage** : en cas de doute → `UnknownFunction` + pas - d'erreur (l'utilisateur n'est jamais forcé d'écrire une signature, mais on - ne ment jamais sur un type). -5. **Le compilateur est l'oracle du MCP** : la doc réduit l'espace de recherche - du LLM, `typr check` reste l'arbitre final (philosophie déjà appliquée en CI - par `npm run check:examples`). -6. **TypR-propriété vs R base** : seules les fonctions pour lesquelles TypR - fournit sa propre implémentation (std.R, codegen transpileur) entrent dans - `stdlib_declared_names()`. Les fonctions R base annotées en types vont - dans `R_DOC_ONLY_SOURCES` (doc SPG uniquement). - ---- - -## 4. Architecture cible - -``` - CATALOGUE (source de vérité) - configs/std/.ty — signatures @ + métadonnées - (base.ty, stats.ty, utils.ty, …) TIERs T1/T2/T3 - │ - ┌───────────────┴────────────────┐ - ▼ ▼ - Sink A — MCP/doc Sink B — compilateur - typr std doc typr std - SPG JSON-LD enrichi .std_r_typed.bin (T1 seul) - (T1+T2, exemples, pièges) .std_r.bin (UnknownFunction, T3) - │ │ - ▼ ▼ - typr std doc --format md vartype.rs load_typed_r() - → crates/typr-mcp/data/stdlib.md │ - │ ▼ - ▼ Context typechecking - ressource MCP typr://stdlib - (lecture seule, lookup à la demande) - │ - └──► MCP propose avec la doc, VÉRIFIE avec `typr check` -``` - -Règles de flux : - -| Symbole | Dans `functions_*.txt` | Dans `.ty` T1 | Dans `.ty` T2 | Doc MCP | -|---|---|---|---|---| -| pur, typable (ex. `abs`, `sqrt`, `nrow`) | oui | oui | — | oui | -| typable mais souple (ex. `paste` T2, unions/`Any`) | oui | non | oui | oui | -| non typable (ex. `c`, `lapply`, `with`) | oui | non | non | oui, signale « à typer soi-même » | - -- Le **compilateur** ne voit jamais T2 ni T3 : on garde zéro coût mémoire pour - les tiers 2–3 (un symbole T2/T3 = `UnknownFunction`, pas de blob en plus). -- Le **MCP** voit tout, y compris T3, avec l'avertissement approprié. - -### Sources en pratique - -| Constante | Fichiers | Consommé par | -|---|---|---| -| `R_T1_SOURCES` | std_R.ty, default.ty, file.ty, option.ty, lin_alg.ty, factor.ty, state.ty, ord.ty, plot.ty, system.ty | `typr std` (compilateur) + `typr std doc` (SPG) | -| `R_DOC_ONLY_SOURCES` | base.ty, stats.ty, utils.ty | `typr std doc` (SPG uniquement) | - ---- - -## 5. Le catalogue — format - -### 5.1. Organisation - -Un fichier `.ty` par package R : `base.ty`, `stats.ty`, `utils.ty`, puis -`graphics.ty`, `grDevices.ty`, `methods.ty` (S4, faible priorité), enfin -`datasets.ty`. On s'inspire de la structure existante (`configs/std/*.ty`). - -### 5.2. Signature - -Réutiliser la syntaxe existante `@name: (T) -> U;` — les pipeline et tests -(`preprocess_ty_source`, `build_typed_vartype`) fonctionnent déjà avec. - -### 5.3. Métadonnées par entrée - -Format `#!` implémenté et testé. Chaque bloc `#!` précède la ligne `@name:` : - -``` -#! pkg: base -#! tier: T1 -#! param x: Valeurs à sommer, NA exclues si na.rm = true -#! ret: NA si x vide et na.rm = false -#! coercion: logical/int -> num par R (TypR: exiger num explicitement ?) -#! example: sum(c(1, 2, 3)) # -> 6 -#! example: sum(c(1, NA), na.rm = true) # -> 1 (décisif : comportement NA) -#! seealso: cumsum, prod, rowSums -@sum: (vec[N, num]) -> num; -``` - -Champs disponibles : `pkg`, `tier`, `param` × n, `ret`, `coercion`/`note`, -`example` × n, `seealso`. Le parser (`stdlib_meta.rs`) gère les formes -compactes `#! key:value` et `#! key value`. - ---- - -## 6. Étapes d'implémentation - -### Phase 0 — Inventaire et estimation ✅ - -1. **Boucler la première liste de candidats** depuis `functions_R.txt` (761 noms) - + `r_name_db.json` (pkg, s3_generic). 1283 noms base-R identifiés. -2. **Classer T1/T2/T3** — classification heuristique disponible via - `r_name_db.json` (s3_generic, s4_generic) + liste noire 60 noms. -3. **Créer `configs/std/base.ty`** — squelette vide avec conventions `#!` ✅. - -### Phase 1 — Étendre le format SPG ✅ - -1. `model.rs` : `StdlibMeta` avec tous les champs `Option<>` ✅. -2. `stdlib_meta.rs` : parser `parse_meta_from_source()` avec 8 tests ✅. -3. `doc_attach.rs` : lit les annotations `#!` ✅. -4. `build_stdlib_docs()` : chaîne T1+T2, SPG enrichi ✅. -5. Tests : `spg_nodes_carry_stdlib_meta_end_to_end`, - `doc_only_sources_are_in_spg_but_not_in_typed_bin` ✅. - -### Phase 2 — Rédiger le catalogue (base → stats → utils) 🔄 - -**Migration des signatures existantes** ✅ : -- Tous les fichiers `.ty` (std_R.ty, default.ty, option.ty, factor.ty, - state.ty, file.ty, system.ty, plot.ty, lin_alg.ty, ord.ty, foreign.ty) - portent désormais des annotations `#!` complètes. -- 10 doublons inter-fichiers identifiés et résolus (substr, sub, gsub, - strsplit, tolower, toupper, grepl → base.ty ; seq → default.ty ; - unique, sort → ord.ty). - -**Catalogue R base** ✅ : -- `base.ty` : 128 signatures R base (math, stats, comparaison, string, set, - matrix, vector, type-checking, type-coercion) avec annotations `#!` - complètes (pkg, tier, param, ret, coercion, example, seealso). -- `stats.ty` : 103 signatures du package stats (dnorm…rhyper, pwilcox, - t.test, cor.test, IQR, …), annotations `#!` complètes. -- `utils.ty` : 55 signatures du package utils (read.csv, write.csv, head, - adist, …), annotations `#!` complètes. -- Positionnement `R_DOC_ONLY_SOURCES` (doc SPG uniquement, pas compilateur). - -> **Noms pointés / variadiques** : les signatures dont le nom de fonction -> contient un `.` (`read.csv`, `cor.test`, …) sont quantifiées par des -> backticks (``@`read.csv` : …``) et les paramètres variadiques `...name:` -> sont conservés tels quels — le préprocesseur ne strip que les vrais noms de -> paramètres (`na.rm`, `row.names`), jamais le marqueur variadique. Les types -> 3D (`[#N, #M, int]`) et les listes `[#, T]` sont ré-écrits sous forme -> imbriquée `[#N, [#M, int]]` (le parseur les rejette sinon, et une ligne -> rejetée empoisonne silencieusement tout ce qui suit dans le fichier). - -**Reste à faire** : -- [x] `stats.ty` : fonctions du package stats (rnorm, dnorm, t.test, lm, …) -- [x] `utils.ty` : fonctions du package utils (read.csv, write.csv, …) -- [x] `plot.ty` / `system.ty` : records déclarés via `record{ … }` (forme - nue `{ … }` rejetée), champ `type` renommé `kind` → zéro SKIPPED - (`typr std` compilent tous les fichiers, exit 0, 761 entrées). -- [x] Déterminisme du cache incrémental : clés de cache désormais stables - entre processus (les `HashSet` des variants `Record`/`Interface`/ - `RClass` étaient Debug-sérialisés en ordre aléatoire ; `Context::fingerprint` - les rend dans un ordre canonique trié). -- [ ] Validation `typr case run` (aucun REGRESS) -- [x] Circuit de validation : `cargo test --workspace` vert -- [x] `typr std doc` régénéré et référencé (pack MCP, Phase 3) - -> Garde-fou : `build_typed_vartype` **SKIP** silencieusement tout fichier qui -> panique (standard_library.rs). Un `.ty` trop ambitieux qui fait paniquer -> le parseur ⇒ entrées absentes du binaire ⇒ régression silencieuse. Toujours -> traiter un SKIPPED comme un bug critique dans ce circuit. - -### Phase 3 — Pack MCP ✅ - -1. **Digest compact** ✅ : `typr std doc --format md` — renderer SPG→markdown - dans `md_renderer.rs` (suivant le pattern `rd_renderer.rs`). Sortie : - digest groupé par package (`## \`pkg\` package`), chaque entrée = - `- \`T1\` **\`name\`** \`(params) -> ret\` — 1 ligne doc` + 1 bloc example - + seealso. 7 tests unitaires. -2. **Fichiers de référence** ✅ : `typr std doc --format md --output - crates/typr-mcp/data/stdlib.md` — digest vendored (~60 Ko, 386 entités, - 14 packages, 1723 lignes). Régénérable à volonté. -3. **Ressource MCP** ✅ : `typr://stdlib` exposée dans `typr-mcp/src/lib.rs` - (même pattern que `typr://lexicon` / `typr://operators` : `include_str!`, - `syntax_resources()`, `read_syntax_resource()`). 2 tests MCP ajoutés. -4. **Oracle** ✅ (pré-existant) : `typr check` / `typr build` sont déjà des - outils du MCP (tools `check` et `build`) — pas de travail supplémentaire - nécessaire. -5. **Vérification des exemples** : les `#! example:` du catalogue passent dans - un circuit de validation (cf. Phase 4) — jamais de faux exemple dans la doc. - -### Phase 4 — Circuit de maintenance (anti-régression) ✅ - -1. **Tests Rust** dans `crates/typr-cli` (suite `standard_library.rs`) ✅ : - - toute signature `@…` du catalogue **parse et type-check** (zéro SKIPPED) (`all_catalog_signatures_parse_and_typecheck_without_skipped`) ; - - `stdlib_declared_names()` ⊇ noms `.ty` TypR-propriété (pas R base) (`stdlib_declared_names_covers_bundled_typr_owned_signatures`) ; - - cohérence des tiers : un nom T3 dans `functions_R.txt` n'est dans aucun `.ty` T1 (`tier_consistency_blacklisted_or_t3_names_not_in_t1_sources`) ; - - tous les `#! example:` sont compilables par `typr check` ou annotés par `# noplayground:` (`all_hash_bang_examples_are_typecheckable`). -2. **Consigne blacklist ↔ catalogue** ✅ : un nom blacklisté qui « se typifie un jour » doit passer par un audit explicite (`tier_consistency_blacklisted_or_t3_names_not_in_t1_sources`). -3. **Nettoyage Markdown** ✅ : `md_renderer.rs` filtre `# noplayground:` pour conserver une doc propre sur la ressource MCP `typr://stdlib`. - -### Phase 5 — (Optionnel, plus tard) Packages externes - -Même mécanisme pour `dplyr`, `ggplot2`, `stringr`, … via -`configs/std/ext/dplyr.ty` et la résolution `@extern pkg::name:` déjà supportée -(`stdlib_declared_names`). La doc MCP s'étend, le compilateur reste à T1 -seulement, et pour les packages externes on exige une **preuve** (test case -`cases/`) par entrée T1 promue. - ---- - -## 7. Porte de sortie T2 → T1 (checklist de promotion) - -Une fonction **T2 ne monte en T1** que si toutes les cases sont cochées : - -- [ ] comportement pur et stable (pas de dispatch S3 variable sur args) ; -- [ ] pas de `...`, ou variadique strictement contraint et vérifiable ; -- [ ] pas de coercion silencieuse entre types TypR distincts ; -- [ ] comportement `NA`/`NULL`/vide documenté (`na.rm`, empreinte `vec[0]`) ; -- [ ] au moins un `#! example:` qui verrouille un piège (NA, recycling…) ; -- [ ] le compilateur, avec la signature, ne crée aucune erreur nouvelle sur - `cases/` + suite complète `cargo test --workspace`. - -Promouvoir sans passer la porte = régression soundness. - ---- - -## 8. Réponse au « risque mémoire » - -Quantifié : une entrée typée ≈ 100–300 octets bincode. Même 800 fonctions T1 ≈ -**quelques centaines de Ko au pire**, chargées une fois via `Arc` et partagées — -négligeable en natif comme en WASM (le blob `.std_r_typed.bin` fait déjà ça). - -Pour minimiser malgré tout : -- T2/T3 ne pèsent **rien** dans le compilateur (jamais embarqués) ; -- la surcouche T1 reste un blob séparé → on peut la passer en chargement - **paresseux** plus tard (désérialisation à la demande par symbole) sans - changer le format ; -- les `.ty` ne sont jamais embarqués dans le binaire : ils sont - compilés en `.bin` une seule fois (`typr std`), comme aujourd'hui. - -Donc : **le poids n'est pas un argument pour restreindre la couverture — -la soundness l'est.** - ---- - -## 9. Risques & mitigations - -| Risque | Mitigation | -|---|---| -| Signature fausse → erreurs de type / casts erronés | Porte de promotion T1 (section 7), blacklist maintenue | -| `.ty` qui panique → entrées absentes du binaire | SKIPPED = erreur critique ; test « zéro SKIPPED » en CI | -| Exemple faux dans la doc MCP | Circuit `#! example:` → `typr check` en CI (Phase 4) | -| Régression suite `cases/` | `typr case run` bloque ; toute promotion vérifie zéro REGRESS | -| Dérive catalogue ↔ code compilé | `.bin` régénérés et committés à chaque changement ; tests de cohérence | -| Clés de cache incrémental instables entre processus | `Context::fingerprint()` rend les `Type` via un renderer canonique (variants `Record`/`Interface`/`RClass` triés à chaque niveau), testé par `module_cache_output_is_byte_identical_to_clean_build` | -| Surcharge de contexte MCP | Pack lookup-à-la-demande (ressource `typr://stdlib`, read-only), jamais inondé d'office | -| Fonctions R base dans `stdlib_declared_names()` | `base.ty` dans `R_DOC_ONLY_SOURCES`, exclu de `stdlib_declared_names()` | -| `stdlib.md` déconnecté du catalogue | Fichier vendored régénérable par `typr std doc --format md` ; même pattern que `lexicon.md` / `operators.md` (pas de CI de resync automatique, refresh à la main) | - ---- - -## 10. Critères de succès - -1. `typr std` + `typr std doc` régénèrent, sans SKIPPED non voulus, des - artefacts qui passent `cargo test --workspace` et `typr case run`. -2. Un utilisateur peut écrire `sum(x)` / `mean(x, na.rm = true)` **sans - signature** et le compilateur infère `num` (par exemple), avec une erreur - claire si l'argument est manifestement mal typé. -3. Le MCP, face à « comment fait-on X en TypR ? », répond correctement en - s'appuyant sur le digest et **vérifie** son code par `typr check` (oracle). -4. Zéro régression de soundness : la suite `cases/` ne comporte aucun REGRESS - à la fin de chaque phase. -5. Le catalogue couvre base (T1+T2) puis s'étend à stats/utils sans nouveau - coût mémoire compilateur. - ---- - -## 11. Hors scope (V1) - -- Typage « parfait » de `c()` et des foncteurs dynamiques (`lapply`, `do.call`) — - restent en `UnknownFunction` + note doc. -- S4 `methods` complet. -- Génération de la doc **site** (`typr.github.io`) depuis le catalogue — c'est - un renderer de plus sur le SPG, à faire après le pack MCP. -- Chargement paresseux du blob T1 (optimisation, future). diff --git a/syntaxe.md b/syntaxe.md deleted file mode 100644 index f4f6a973..00000000 --- a/syntaxe.md +++ /dev/null @@ -1,738 +0,0 @@ -# TypR — carte de la syntaxe - -> Référence non officielle, établie par lecture directe du parseur (`crates/typr-core/src/processes/parsing/`) -> — reflète l'état du code à la date de génération, pas une spécification figée. -> -> Sources principales : `parsing/mod.rs`, `parsing/elements.rs`, `parsing/types.rs`, -> `components/language/operators.rs`, `parsing/indexation.rs`, `components/language/mod.rs`. - -## Sommaire - -1. [Lexique & littéraux](#1--lexique--littéraux) -2. [Liaisons & mutation](#2--liaisons--mutation) -3. [Système de types](#3--système-de-types) -4. [Alias, opaque, typeconstructor, signatures](#4--alias-opaque-typeconstructor-signatures) -5. [Fonctions](#5--fonctions) -6. [Records & constructeurs](#6--records--constructeurs) -7. [Unions, tags & `match`](#7--unions-tags--match) -8. [Opérateurs & précédence](#8--opérateurs--précédence) -9. [Contrôle de flux](#9--contrôle-de-flux) -10. [Modules & imports](#10--modules--imports) -11. [Interfaces & validation structurelle](#11--interfaces--validation-structurelle) -12. [Échappatoires vers du code brut](#12--échappatoires-vers-du-code-brut) -13. [TypR vs R — ce qui change vraiment](#13--typr-vs-r--ce-qui-change-vraiment) -14. [Ambiguïtés & pièges connus du parseur](#14--ambiguïtés--pièges-connus-du-parseur) - ---- - -## 1 · Lexique & littéraux - -Chaque instruction de haut niveau se termine par `;`. L'oubli est toléré par le parseur mais -signalé (`SyntaxError::ForgottenSemicolon`) — sauf pour la toute dernière expression d'un bloc, -qui joue le rôle de valeur de retour implicite (comme en R). - -| Littéral | Syntaxe | Remarque | -|---|---|---| -| Entier | `42`, `-7` | `Lang::Integer` | -| Nombre | `3.14`, `-0.5` | un point décimal est obligatoire pour `Number`, sinon c'est un `Integer` | -| Chaîne | `"texte"` ou `'texte'` | guillemets simples et doubles interchangeables ; échappe `\" \' \\ \n \t` | -| Booléen | `true` / `TRUE`, `false` / `FALSE` | forme minuscule *et* majuscule acceptées | -| Nul | `null` / `NULL` | `Lang::Null` — distinct de `NA` | -| Manquant | `na` / `NA` | `Lang::NA` — distinct de `null` | -| Identifiant variable | `mon_nom` | doit commencer par `a-z_` ; `snake_case` attendu ; `__` → `.` en R (`as__x` → `as.x`), pour les variables, fonctions, membres de module et champs de record | -| Identifiant caché | `.cache`, `__cache` | `.` puis `a-z_` ; privé par le nom (jamais `@pub`/`@export`, S020) — `__` en tête ≡ `.` (même liaison) | -| Identifiant type | `MonType` | `PascalCase` obligatoire pour `type`/`opaque`/alias | -| Identifiant cité | `` `+` ``, `` `nom bizarre` `` | backticks — utile pour nommer un opérateur custom | -| Commentaire | `# commentaire` | `//` n'existe pas — un fichier `.ty` avec `//` échoue silencieusement (piège connu, voir §14) | - -Le parseur distingue explicitement `let NomPascal <- ...` (erreur `LetInsteadOfType`) de -`type nom_snake <- ...` (erreur `TypeInsteadOfLet`) — la casse du premier identifiant n'est donc -pas un simple style, elle est vérifiée et corrigée avec un message dédié. - ---- - -## 2 · Liaisons & mutation - -### Déclaration `let` - -```typr -let x <- 42; -let y: int <- 5; -@pub let z <- "hello"; # exportée : publique + testable -@testable let cache <- state(0); # privée, visible en build --test via M$.test_cache -@export let api <- fn(x: int): int { x }; # @pub + #' @export roxygen2 -``` - -`<-` et `=` sont interchangeables comme opérateur d'affectation dans `let`. Un seul `=` n'est en -revanche **jamais** un opérateur infixe binaire dans une expression — il est réservé aux champs -nommés (`x = 1`), à l'affectation top-level et aux valeurs par défaut de paramètres. - -### Déstructuration de tuple - -```typr -let :{a, b, c} <- :{1, 2, 3}; -let :{a, _, c} <- :{1, 2, 3}; # wildcard : ignore l'élément -``` - -Désucré en une variable temporaire + accès positionnel par point (`__tuple_tmp__.1`, `.2`, ...). - -### Affectation & mutation - -```typr -x <- 10; # Assign sur une variable déjà liée -x <- x + 1; - -# sucre "mutation implicite" : x!; réaffecte x au résultat de l'expression -x |> f() |> g()!; # ≡ x <- x |> f() |> g(); -obj.method()!; # ≡ obj <- obj.method(); -``` - -`expr!;` exige que la tête de la chaîne `.`/`|>` soit une variable assignable ; `3!;` par exemple -est rejeté au parsing. - ---- - -## 3 · Système de types - -### Primitifs - -`int` · `num` · `bool` · `char` · `null` · `na` · `Any` · `Empty` · `Self` - -Les littéraux peuvent aussi apparaître *comme type* (types singleton) : `3`, `3.14`, `true`, -`"chat"` sont des types valides, plus précis que `int`/`num`/`bool`/`char`. - -### Records (`list{...}`) - -```typr -type Point <- list { x: int, y: int }; -type Config <- record { name: char, timeout: int }; # synonyme explicite -``` - -### Tuples - -```typr -tuple{int, char} # explicite -Tuple[int, char] # notation crochets -Tuple[T..., U] # variadique : T... capture une séquence de types -``` - -### Tableaux, vecteurs, dataframes - -| Forme | Exemple | Sens | -|---|---|---| -| Array (S3, court) | `[int]` | tableau d'entiers, taille libre (`Any`) | -| Array (S3, complet) | `[#N, int]` | taille indexée par le générique `#N` | -| `Array[...]` | `Array[3, int]` | variante nommée, taille fixe = 3 | -| `Vec[...]` | `Vec[num]`, `Vec[#N, num]` | vecteur R natif | -| `dataframe[...]{...}` | `df[#N]{ name: char, age: int }` | `df` = alias court de `dataframe` ; colonne : `t.age` ≡ `t$age` → `[#N, int]` | -| Record générique nommé | `Tibble[3]{ id: int, active: bool }` | nécessite une déclaration `typeconstructor` préalable | - -Règle d'or du parseur : un bloc `{ ... }` qui suit fait toujours de la construction un *record* — -`Tibble[3]` seul (sans accolades) est un simple alias paramétré, `Tibble[3]{ ... }` est un record -constructeur. - -### Génériques & sigils de « kind » - -```typr -let id <- fn(x: T): T { x }; # T majuscule = générique libre -#N # générique "indice" (dimension de tableau) -$T # générique "label" (nom de champ) -%R # générique contraint : doit être un Record -@I # générique contraint : doit être une Interface -^S # générique contraint : doit être un char -?B # générique contraint : doit être un bool -``` - -### Unions & intersections - -```typr -type Shape <- .Circle(num) | .Square(num); # union par tags -type Combined <- Movable & Drawable; # intersection d'interfaces -``` - -### Fonctions & interfaces (en position de type) - -```typr -(int, char) -> bool # type fonction anonyme -(a: int, b: int) -> int # noms de paramètres optionnels, ignorés au typage -interface { view: (Self) -> char } # capacité structurelle -``` - -Écrire `fn(a: int) -> int` en *position de type* (au lieu de `(int) -> int`) est capté -explicitement et renvoie `SyntaxError::FunctionTypeSyntax` — `fn(...)` n'existe qu'au niveau des -expressions, jamais des types. - -### Arithmétique de types & conditions - -```typr -type Combined <- A + B; # Type::Operator sur des index/dimensions -T if T1 in T2 # type conditionnel (raffinement expérimental) -``` - ---- - -## 4 · Alias, opaque, typeconstructor, signatures - -```typr -type Meters <- int; # alias transparent -opaque Meters <- int; # alias opaque : le sous-jacent est caché du typage externe -type Option = .Some(T) | .None; # alias générique — , pas [T] -opaque Factor <- int; # paramètre "fantôme" : L n'apparaît que dans de futures @signature - -typeconstructor Tibble[N] record; # enregistre un constructeur générique de record -typeconstructor Matrix[N, M, T] recursive; -``` - -#### Signatures (déclaration de type sans corps) - -```typr -@map: (a: [#N, T], f: (T) -> U) -> [#N, U]; # générique -@add: (a: int, b: int) -> int; # surcharge : répéter @add avec d'autres types -@add: (a: num, b: num) -> num; -@as__character: (Self) -> char; # "__" → "." en sortie R (as.character) - -@extern stats::sd: (x: [Any, num]) -> num; # fonction R externe, appel réel package::fn -@extern base::readRDS: (path: char) -> Foreign; # nom nu = fonction déjà visible sans préfixe pkg:: -@importFrom dplyr filter select mutate; # hisse un @importFrom roxygen2 -``` - -Une signature `.ty` peut contenir un vrai corps `let nom <- fn(...){...}` et être type-checkée -avec succès — mais ce corps est **silencieusement jeté** : seule la paire `(nom, type)` survit. -Le seul patron qui marche pour la stdlib est signature seule + implémentation R écrite à la main -dans `std.R`. - -#### `Foreign` — valeurs R externes opaques - -`opaque Foreign <- Any;` (`configs/std/foreign.ty`) est le type compagnon idiomatique de -`@extern` pour *nommer* une valeur R qui existe déjà à l'extérieur (objet S3/S4/RC/R6, package -tiers) sans jamais la construire depuis TypR : - -```typr -type LmModel <- Foreign; -@extern base::readRDS: (path: char) -> LmModel; -@extern stats::coef: (m: LmModel) -> Foreign; # accesseur dédié pour toucher au contenu - -let m: LmModel <- readRDS("modele.rds"); -``` - -La valeur traverse `let` annoté, arguments et retours de fonction **sans jamais être touchée** -(pas de `as.X()`/`struct()` appliqué). Deux limites volontaires à connaître : `m.champ`/`m$champ` -ne type-check **jamais** (`Any` n'a aucun champ structurel connu — il faut un accesseur `@extern` -dédié pour chaque champ/slot/méthode) ; et un appel `@extern` est **positionnel uniquement**, donc -impossible d'appeler directement une fonction R qui exige des arguments nommés (`new("X", x=1)`). -Détail complet — mécanisme, quatre bugs trouvés et corrigés, grille de conformité — dans -`CLAUDE.md` § "Foreign R Values" et `interop_matrix.md` (racine du repo). - ---- - -## 5 · Fonctions - -```typr -let add <- fn(a: int, b: int): int { a + b }; -let greet <- fn(name: char, greeting: char = "Hello"): char { greeting }; # défaut, param final seulement -greet("World"); # "Hello" -greet("World", "Hi"); # "Hi" - -let sum_all <- fn(...xs: int): int { /* ... */ }; # variadique - -let sq <- \(x) x * x; # lambda non typée -let add5 <- \add(a = 5); # application partielle d'une fonction -let origin <- \Point:{ x = 0, y = 0 }; # application partielle d'un constructeur de record -``` - -`fn(...)` exige toujours `: Type` de retour — l'omettre panique explicitement ("You forgot to -specify the function return type"). `\(...)` (lambda) n'a en revanche ni type de paramètres ni -type de retour déclarés. - -**Application partielle vs lambda** : les deux utilisent `\`, désambiguïsés par ce qui suit — -`\(` → lambda, `\identifiant(` → application partielle. `PartialApp` se désucre en -`Lang::Function` pendant le typage — il n'atteint jamais la transpilation. - ---- - -## 6 · Records & constructeurs - -```typr -let p <- :{ x = 1, y = 2 }; # record anonyme (nécessite un contexte de type) -let p <- Point:{ x = 1, y = 2 }; # constructeur explicite → transpile en Point(x=1, y=2) -let q <- Point:{ ...p, y = 9 }; # spread "runtime" : fusion structurelle, override après -let r <- mod$Point:{ x = 1, y = 2 }; # constructeur qualifié par chemin de module - -let arr <- IntBox:[1, 2, 3]; # ArrayConstructorCall -``` - -Autres écritures équivalentes du littéral record : `record{...}`, `object{...}`, `list{...}`, -`:{...}` — la forme est choisie par la *forme* des champs, pas par le mot-clé : des champs -`nom = valeur` donnent un record (`Lang::List`), des valeurs positionnelles donnent un tuple -(`Lang::Tuple`) même avec `list{1, 2, 3}`. - -#### Spread — deux mécanismes distincts - -```typr -Point:{ ..source } # spread "statique" nominal — un seul par appel -Point:{ ...source } # spread "runtime" structurel — un seul dans un constructeur, plusieurs dans un record littéral -:{ ...a, ...b, z = 1 } # record littéral : plusieurs spreads runtime autorisés, fusionnés dans l'ordre puis overridés -``` - -#### Named type embedding - -```typr -type Widget <- list { embed coords: Position, label: char }; -``` - -`embed` est un mot-clé « doux » — reconnu seulement devant `nom: Type` avec espace derrière, donc -un champ réellement nommé `embed` reste parsable. - ---- - -## 7 · Unions, tags & `match` - -```typr -type Shape <- .Circle(num) | .Square(num); -let s <- .Circle(3.14); -let n <- .None; # tag sans valeur - -match s { - .Circle(r) => r * 2.0, - .Square(side) => side, -} -``` - -#### Motifs de `match` disponibles - -| Motif | Exemple | -|---|---| -| Tag avec binding | `.Some(a) => a` | -| Tag sans binding | `.None => 0` | -| Motif de type | `x as int => x + 1` | -| Motif record | `:{nom: n, age: a} => a` ou `list(nom = n, age = a) => a` | -| Motif tuple | `:{a, b} => a` | -| Joker | `_ => défaut` | -| Variable simple | `v => v` | - -#### Constructeur d'union qualifié - -```typr -type Color <- .Red | .Blue; -Color.Red # référence qualifiée à un tag sans charge utile (bare, sans `:{...}`) - -type Rgb <- list { r: int, g: int, b: int }; -type Palette <- .Red | .Blue | Rgb; -Palette.Rgb:{ r = 10, g = 20, b = 30 } # Rgb est un alias record utilisé tel quel comme membre - # de l'union (pas un tag `.Rgb(...)`) — son constructeur - # généré accepte bien des champs nommés. -``` - -**Piège** (audit_type_checking.md U1, corrigé) : la syntaxe `Union.Variant:{ field = val }` / -`TagName:{ field = val }` ne fonctionne **que** quand `Variant` est un alias record utilisé -directement comme membre de l'union (cas `Rgb` ci-dessus) — jamais pour un vrai tag `.Variant(...)` -(scalaire ou à corps record), qu'on l'écrive avec ou sans `:{...}`. Le constructeur généré pour un -tag prend toujours **une seule charge utile positionnelle** (`Variant <- function(x) {...}`), jamais -des champs nommés séparés. Pour construire un tag, utiliser `.Variant(valeur)` — et -`.Variant(:{ ... })` quand la charge utile est elle-même un record. Rejeté à la vérification de -type (`TagFieldConstructorNotSupported`) plutôt que de compiler vers du R qui plante. - ---- - -## 8 · Opérateurs & précédence - -Précédence réelle (la plus élevée se regroupe en premier) — notez que l'accès membre / pipe se -lie *plus fort* que l'arithmétique, contrairement à la plupart des langages. - -| Rang | Opérateurs | Rôle | -|---|---|---| -| 4 (fort) | `.` `\|>` `$` `::` `as!` `in` | accès membre / UFCS, pipe, cast validant | -| 3 | `*` `/` `%` `@` | multiplicatif, produit matriciel | -| 2 | `+` `-` | additif | -| 1 (faible) | `== != < > <= >=`, `and/&&/&`, `or/\|\|/\|`, `%op%` | comparaison, logique, opérateurs custom | - -### UFCS — `.` et `|>` - -```typr -x.f(y) # ≡ f(x, y) — appel de méthode uniforme -x |> f() |> g() # pipe — même désucrage -t.1 # accès positionnel d'un tuple (index 1-based) -mod$member # accès champ de record / module — "::" est un alias historique de "$" -``` - -```typr -# TypR -data |> filter(x > 0) |> mean() -``` -```r -# R -data |> filter(x > 0) |> mean() -# ou via magrittr : data %>% filter(...) %>% mean() -``` - -### Cast validant - -```typr -x as! Point # appelle validate_Point(x) à l'exécution -xs as! [Any, int] # cast vers un type structurel inline (pas un alias) -``` - -### Intervalles - -```typr -1:10 # ≡ seq(1, 10, 1) -1:2:10 # ≡ seq(1, 10, 2) — pas explicite au milieu -``` - -Les variantes doublées (`++ -- ** // %% @@ .. $$ |>>`) et `@`/`@@`/`=` en infixe ont été retirées -du tokenizer (`components/language/operators.rs::op()`/`pipe_op()`) — aucune n'avait de bras de -typage/transpilation ni de signature stdlib `` `op` `` pour les porter, contrairement à -`+ - * / % && || ...` qui retombent réellement sur un appel de fonction stdlib. Les laisser -tokeniser silencieusement un typo (ex. un commentaire `//` de style C, TypR n'utilisant que `#`) -en un opérateur "valide" ne faisait que déplacer l'erreur vers un message confus, loin du vrai -problème ; voir §14. `@` en tête de ligne appartient de toute façon aux annotations (`@pub`, -`@extern`...), pas aux opérateurs. - ---- - -## 9 · Contrôle de flux - -```typr -if (x > 0) { "positif" } else if (x < 0) { "négatif" } else { "zéro" } - -for (item in items) { print(item); }; -while (n > 0) { n <- n - 1; }; -loop { /* ... */ break; }; - -next; # continue de boucle -break; -return valeur; -``` - -Toute forme itérable dotée d'une fonction `as_vec: (Self) -> [T]` peut être utilisée dans un -`for (... in ...)`, pas seulement les tableaux. - ---- - -## 10 · Modules & imports - -```typr -module Math { - let pi <- 3.14159; - @pub let pi_approx <- 3.14; - @pub opaque Radians <- num; -}; - -use Math::pi_approx; -use Math::{pi_approx, sin as s}; -use Math::*; - -import Math; -import Math as M; - -mod Utils; # forme d'import historique équivalente -library(dplyr); # dépendance R classique -use("dplyr", c("filter", "select")); # adaptateur legacy -``` - -Un module compile vers un environnement R ; les membres non `@pub` restent invisibles depuis -l'extérieur (sauf en build `--test` via `@testable`, exposés en `M$.test_nom`). - -Noms cachés : un `.` en tête (`.cache`, `.helper`, champ `.secret`) rend un nom privé *par sa -syntaxe*, comme en R — émis tel quel (`ls()` et l'`exportPattern("^[^\\.]")` par défaut d'un -package l'ignorent), son générique S3 ne reçoit pas de `#' @export`. `@pub`/`@export` sur un nom -caché est une erreur (S020) ; `@testable` reste permis. `__nom` est l'orthographe ASCII de `.nom` -(même liaison), obligatoire juste après l'opérateur `.` : `x..helper()` se lirait comme le -spread `..` et est refusé (S019) — écrire `x.__helper()` ou `x |> .helper()`. - ---- - -## 11 · Interfaces & validation structurelle - -```typr -type Movable <- interface { mv: (Self, int, int) -> Self }; - -let p <- Point:{ x = 1, y = 2 }; -let q <- Movable(p); # validateur compile-time — transpile en `q <- p`, jamais un appel réel -``` - -Une interface décrit une **capacité structurelle** : n'importe quel type dont les fonctions -libres ont ce type comme premier paramètre « l'implémente », sans mot-clé `impl`. Appeler `I(x)` -où `I` est un alias d'interface n'est donc jamais un vrai appel de fonction (les alias vivent dans -un espace de noms séparé des variables) — c'est un contrôle de compatibilité à la compilation qui -échoue avec `InterfaceNotSatisfied` / `IncompatibleInterfaceMethod`. - ---- - -## 12 · Échappatoires vers du code brut - -```typr -extern (x: int, y: char) -> char r#"paste0(x, y)"#; # corps R brut, typé en entrée/sortie - -function(x, y) { x + y } # fonction R brute non typée (RFunction), corps capturé tel quel - -R { # bloc R brut non typé (RBlock) : valeur immédiate, pas un appel - df %>% - dplyr::filter(x > 1) %>% - dplyr::mutate(z = y + 1) -} - -JS { /* ... */ } # bloc JavaScript brut (cible JS) - -Class("data.frame", "tbl") # type dénotant une classe R existante (RClass) - -@{ 1 + x * 2 }@ # bloc vectoriel : re-parsé comme une séquence d'éléments TypR - # (littéraux/appels/variables) — pas du R arbitraire, contrairement à `R { ... }` -``` - -Ce sont les points de sortie délibérés du système de types : `extern` garde une signature -vérifiée par TypR autour d'un corps R opaque ; `function(...)` / `R { ... }` / `JS { ... }` n'ont -aucune vérification du tout. `R { ... }` capture son corps tel quel, accolades équilibrées (comme -`function(...)`), et transpile en un simple bloc R `{ ... }` — qui s'évalue déjà à la valeur de sa -dernière instruction, donc aucun wrapper ni appel n'est émis. C'est la forme à privilégier pour du -R idiomatique difficilement typable (pipes `%>%`/`|>`, NSE dplyr, formules `~`, ...) qu'on veut -juste utiliser pour produire une valeur, sans se soucier du système de types — contrairement à -`@{ ... }@`, dont le contenu doit rester une séquence d'éléments TypR valides. - ---- - -## 13 · TypR vs R — ce qui change vraiment - -| Aspect | R classique | TypR | -|---|---|---| -| Fin d'instruction | saut de ligne suffit | `;` attendu (sinon avertissement de parsing) | -| Typage | dynamique, aucune annotation | statique ; `fn(...)` exige toujours un type de retour | -| Casse des noms | convention libre | vérifiée par le parseur : `snake_case` pour `let`, `PascalCase` pour `type`/alias | -| Listes | `list(a = 1, b = 2)`, sans validation | `list{a=1,b=2}` génère un vrai constructeur + validateur (`as.T`, `validate_T`) | -| Sommes de types | pas de type natif — conventions `class` ad hoc | tags `.A(T) \| .B` + `match` exhaustif | -| Appel de méthode | `UseMethod`/S3, ou `$` sur les objets R6/environnement | UFCS générale : `x.f(y)` ≡ `f(x, y)` pour *tout* type | -| Interfaces | aucune notion formelle | `interface { ... }` + validateur structurel `I(x)` à la compilation | -| Pattern matching | `switch()`, peu typé | `match` avec motifs tag/type/record/tuple/joker | -| Modules | packages / `local()` / environnements ad hoc | `module M { ... }` → environnement R, visibilité `@pub` explicite | -| Génériques | inexistants (dispatch S3 dynamique) | paramètres de type `T`, sigils de kind (`%R @I ^S ?B #N`) | -| Mutation | `x <- f(x)` explicite | sucre `expr!;` équivalent, plus `State` pour la mutation partagée réelle | -| Sortie | du R directement exécuté | compile vers du R idiomatique (`R/*.R`) — 100% du R généré reste lisible et exécutable tel quel | - -> **À retenir** : TypR n'est pas un nouveau runtime — c'est une couche de vérification statique et -> de sucre syntaxique (UFCS, tags, match, interfaces, modules, générique) qui se désucre -> entièrement en R conventionnel avant exécution. Rien de tout ceci n'existe à l'exécution R ; -> tout est résolu par le compilateur TypR. - ---- - -## 14 · Ambiguïtés & pièges connus du parseur - -Le parseur est fait de dizaines de petits combinators `nom` assemblés via `alt()`, sans grammaire -déclarative unique — la plupart des pièges viennent d'un ordre d'essai particulier, ou d'un -tokenizer générique réutilisé dans deux contextes différents. Tout ce qui suit est vérifié -directement dans le code source, pas déduit. - -### Instruction sans `;` avalant la suivante — *corrigé* - -`elements()` assemblait autrefois ses tokens via -`many1(alt((as_excl_operator_token, single_element_token, element_operator_token)))`, sans -imposer l'alternance `Expression (Opérateur Expression)*`. Deux `single_element_token` -consécutifs — donc deux instructions collées faute de `;` — étaient acceptés dans le même vecteur -de tokens sans erreur. `variable_exp` n'excluant aucun mot-clé, la fonction/l'identifiant de -l'instruction suivante finissait comme jeton « expression » surnuméraire, silencieusement absorbé -dans le corps de la première instruction. La seconde instruction disparaissait purement et -simplement de l'AST, sans le moindre message d'erreur. - -```typr -let add_object <- fn(self: Scene, object: Object): Scene { - Scene:{ objects: self.objects.extend(object), ...self } -} // <- ';' manquant ici - -@pub -let print <- fn(self: Scene): Empty { /* ... */ }; // avalé, invisible dans l'AST -``` - -Fixé (`cases/0012`) en remplaçant le tokenizer par -`single_element_token, many0(pair(operator_like_token, single_element_token))` : un jeton -non-opérateur après une expression complète arrête désormais `many0` *sans consommer d'entrée*, -laissant l'instruction suivante au parseur appelant plutôt que de l'avaler. L'oubli d'un `;` -redevient une simple `ForgottenSemicolon` récupérable, pas une perte silencieuse de code. - -### `@` en tête de ligne juste après une instruction non terminée — *corrigé* - -Conséquence directe du piège précédent : le tokenizer générique d'opérateurs reconnaissait aussi -`@`/`@@` comme `Op::At`/`Op::At2` — un opérateur binaire qui n'a jamais eu le moindre bras de -typage ou de transpilation (syntaxe totalement morte à l'exécution). Une instruction sans `;` -suivie d'une déclaration `@pub`/`@export`/`@testable` se retrouvait donc avec son `@` avalé comme -« continuation d'opérateur » du corps précédent, corrompant la première instruction *et* -dépouillant la seconde de son annotation de visibilité. - -Fixé en retirant `@`/`@@` de la liste d'opérateurs infixes génériques -(`components/language/operators.rs::op()`). `@` reste pleinement actif, mais uniquement comme -*préfixe* d'annotation (`@pub`, `@export`, `@testable`, `@extern`, `@nom: Type;`), reconnu par des -parseurs dédiés essayés avant la grammaire d'expression générale. - -### Opérateurs morts du tokenizer — *purgés* - -Suite directe du point précédent : `@`/`@@` n'étaient pas les seuls jetons reconnus par `op()`/ -`pipe_op()` sans avoir le moindre bras de typage/transpilation ni de signature stdlib `` `op` `` -pour les porter. `++ -- ** // %% .. $$ |>>` ainsi qu'un `=` infixe (`Op::Eq2`, déjà exclu de -`bool_op()`) étaient dans le même cas — tombant sur le bras générique de `Lang::Operator` qui -désucre tout opérateur non géré explicitement en appel de `` `op`(e1, e2) ``, sans jamais trouver -de définition correspondante. Le risque concret : un typo courant (`//` pour un commentaire -C-style, alors que TypR n'utilise que `#`) parsait *avec succès* comme une division, et n'échouait -qu'au typage, avec un message éloigné du vrai problème (« Function `` `//` `` not defined »). - -Purgé en retirant ces jetons de `op()`/`pipe_op()` et les variantes `Op` correspondantes -(`Add2/Minus2/Mul2/Div2/Modulo2/Dot2/Dollar2/Pipe2/At/At2/Eq2`) entièrement de l'énum — elles -n'étaient produites nulle part ailleurs dans la base de code une fois le tokenizer purgé. - -Un `//` égaré échoue désormais dès le parsing au lieu de produire un opérateur fantôme — mais pas -comme une erreur brute : un parseur dédié `wrong_comment` (`parsing/mod.rs`, à côté de `comment()`) -le reconnaît explicitement, le traite exactement comme un vrai commentaire `#` (consommé jusqu'à -fin de ligne, ignoré, non fatal) et pousse un `SyntaxError::WrongCommentSyntax` récupérable. Sans -ce traitement dédié, `//` serait retombé sur le piège générique décrit juste en dessous (texte de -fin d'instruction imparsable = **silencieusement tronqué de l'AST**, pas juste mal signalé) — un -typo `//` est un cas assez courant (habitude venue d'autres langages) pour mériter mieux qu'un -`SyntaxError::UnknownElement` générique. - -> ⚠️ **Tentative avortée pour `=` en position de comparaison** (`if (a = b)`) : un plan initial -> prévoyait la même récupération dédiée qu'au-dessus pour `//`, en repêchant un `=` isolé dans -> `op()` lui-même (retomber sur `Op::Eq`, avec un `SyntaxError::SingleEqualsComparison` récupérable). -> Abandonné : `op()` est aussi appelé par la grammaire de *type* (`types.rs::index_operator`/ -> `compute_operators`, pour l'arithmétique de types `type Combined <- A + B;`), qui se trouve juste -> avant le séparateur `= valeur` d'un paramètre par défaut (`greeting: char = "Hello"`). Là, `=` -> n'a aucun sens d'opérateur — le repêcher en `Op::Eq` faisait paniquer `compute_operators` sur une -> combinaison non gérée (`compute_operators`'s `_ => panic!()`, testé via -> `test_non_trailing_default_param_is_an_error` et 3 autres tests `function::tests` cassés). -> `=` reste donc simplement absent du tokenizer, sans erreur dédiée — un futur correctif devrait -> cibler spécifiquement le tokenizer d'expressions (`elements.rs`), pas la primitive `op()` partagée. - -### Découverte connexe — le CLI avalait toutes les erreurs de syntaxe (`typr check`/`build`) — *corrigé* - -En vérifiant le point ci-dessus, découvert que `typr check`/`typr build` (contrairement à -`typr debug --ast`) n'affichaient **aucune** erreur de syntaxe collectée au parsing — -`engine.rs::parse_code`/`parse_code_with_info`/`parse_code_from_str` appelaient -`parse_with_errors()` puis jetaient silencieusement `.errors`, ne gardant que `.ast`. Même chose -une deuxième fois, indépendamment, dans `metaprogramming.rs::ModuleExpander::expand_one` pour -*chaque fichier de module importé* (`mod x;`) : son propre `parse(...)` local jetait aussi ses -erreurs. Concrètement, tout contenu de fin de fichier illisible par la grammaire -(`SyntaxError::UnknownElement`, ou tout autre `push_parse_error` récupérable comme -`WrongCommentSyntax` ci-dessus) était **invisible** via `check_file`/`check_project` dans -`typr-cli/src/project.rs`, alors que `typr debug --ast` les affichait très bien. Reproductible avec -n'importe quel charabia de fin d'instruction, dans le fichier d'entrée ou dans n'importe quel module -importé. - -Corrigé en changeant les signatures de `parse_code`/`parse_code_with_info`/`parse_code_from_str` -(`typr-cli/src/engine.rs`) pour renvoyer aussi `Vec` (entry file + tous les modules -importés, via un nouveau champ `ExpansionInfo::syntax_errors` alimenté par `expand_one`), et en -câblant les 7 points d'appel dans `project.rs` (`check_project`, `check_file`, `build_project_impl`, -`build_file`, `run_file_impl`, `generate_spg`, `document_impl`) sur un nouveau helper -`report_syntax_errors`. Politique de sévérité (décidée explicitement, pas par défaut) : seul -`SyntaxError::UnknownElement` (code réellement perdu de l'AST) fait échouer le pipeline -(`step.fail()` + `exit(1)`) — tout le reste (`ForgottenSemicolon`, `WrongCommentSyntax`, -`LetInsteadOfType`, ...) est toujours entièrement récupéré par le parseur et n'est affiché que -comme avertissement, pour ne pas casser tout projet existant dont le dernier `let` n'a pas de `;` -final (un style aujourd'hui parfaitement toléré). - -En chemin, corrigé un bug d'étiquetage préexistant révélé par ce câblage : le template partagé -`MsgTemplate::Single`/`Double` (`message_template.rs`) avait `"Type error: {text}"` codé en dur — -`SyntaxError::display()` n'avait simplement jamais été appelé en dehors des tests avant cette -correction, donc personne n'avait remarqué qu'une vraie erreur de syntaxe s'affichait comme -« Type error: TypR comments use `#`, not `//` ». `SingleBuilder`/`DoubleBuilder` ont gagné un champ -`kind` (défaut `"Type error"`, préservant les 24 sites d'appel de `type_error.rs` sans changement) ; -les 13 sites de `syntax_error.rs` appellent désormais `.kind("Syntax error")`. - -### Un seul `=` n'est jamais un opérateur binaire - -`bool_op()` exclut délibérément `tag("=")` de ses alternatives, et `op()` n'a plus aucun repli sur -un `=` isolé (voir la tentative avortée documentée juste au-dessus — `compute_operators` paniquerait -dessus, ce qui est exactement ce qui a été observé en essayant). `=` n'existe que dans des positions -de grammaire dédiées : champs nommés (`x = 1`), affectation top-level (`assign()`), valeurs par -défaut de paramètre. Un test d'égalité s'écrit toujours `==`. - -```typr -if (a = b) { /* ... */ } // ne teste PAS l'égalité — utiliser a == b -``` - -### `record` vs `tuple` : la forme des champs décide, pas le mot-clé - -`list{...}`/`:{...}` peuvent produire un `Lang::List` (record) *ou* un `Lang::Tuple` selon la -forme des éléments — `record` est toujours essayé en premier dans `single_element`, mais son -parseur de champ (`argument_val`) exige la forme `nom = valeur` ; s'il échoue sur un premier -élément positionnel, tout le bloc retombe sur `tuple_exp`. - -```typr -list{ a = 1, b = 2 } // → Lang::List (record, champs nommés) -list{ 1, 2, 3 } // → Lang::Tuple (même mot-clé "list", positionnel !) -:{ x = 1, y = 2 } // → Lang::List -:{ 1, 2, 3 } // → Lang::Tuple -``` - -### La « règle d'or » des blocs `{...}` génériques - -Un bloc `{ ... }` qui suit un nom de type paramétré en fait toujours un *record constructeur* -(`Tibble[3]{ id: int }`), jamais un simple usage d'alias. Deux violations de cette règle sont -détectées explicitement plutôt que de mal parser silencieusement : - -```typr -Df[8, int]{ name: char } // SyntaxError::RecordConstructorIndex — un seul index autorisé -Array[5, { a: int }] // SyntaxError::RecordInRecursiveParams — bloc record interdit ici -``` - -Un cas plus subtil couvert par un test de régression dédié : `ltype` ne doit pas confondre le -type de retour d'une fonction avec le corps qui suit — `fn(i: int): Feux { .Rouge }` doit -s'arrêter à `Feux` et laisser `{ .Rouge }` au corps de la fonction, pas l'avaler comme si -`Feux{...}` était un record constructeur. - -### Alias de type PascalCase à une seule lettre : imparsable — *ouvert* - -`pascal_case_no_space` (utilisé par `type_alias`, donc par `type`/`opaque`/`typeconstructor`) est -défini comme `(one_of("A".."Z"), alphanumeric1)` — `alphanumeric1` exige **au moins un** caractère -alphanumérique après la majuscule initiale. Un nom de type d'une seule lettre échoue donc -totalement à ce niveau et retombe ailleurs dans la grammaire (souvent une variable bidon). - -```typr -type A <- int; // n'est PAS un Alias — utiliser au moins 2 caractères -type Ab <- int; // OK -``` - -Asymétrie notable : les noms de *tag*/variante d'union utilisent `pascal_case_helper` -(`opt(alpha1)` — le suffixe est optionnel), donc `.X` (tag à une lettre) parse très bien. Seuls -les noms d'alias de type exigent 2 caractères ou plus. - -### `fn(...)` interdit en position de type - -`fn(args) -> Type` ressemble à un type fonction mais n'est légal qu'au niveau expression ; en -position de type, seule la forme `(Type, Type) -> Type` (sans `fn`) est valide. Le parseur capture -spécifiquement le cas d'erreur (`fn_function_type_error`) pour donner un message ciblé plutôt -qu'un échec générique. - -```typr -let f: fn(a: int) -> int <- ...; // SyntaxError::FunctionTypeSyntax -let f: (int) -> int <- ...; // OK -``` - -### Commentaires `.ty` : uniquement `#` - -`comment()` ne reconnaît que `tag("#")` — il n'existe aucune alternative `//` nulle part dans -`base_parse`. Un fichier de signatures stdlib écrit avec des commentaires `//` n'est pas juste mal -commenté : il échoue à parser et `build_typed_vartype` l'ignore silencieusement en entier -(« Skipped … (syntax not supported) ») — surveiller cette ligne dans la sortie de `typr std`. - -### `use` : un seul mot-clé, quatre grammaires distinctes - -`use` introduit quatre constructions entièrement différentes, désambiguïsées seulement par ce qui -suit immédiatement, dans cet ordre d'essai : - -| Forme | Reconnue si | Produit | -|---|---|---| -| `use("dplyr", c(...));` | `use(` littéral, sans espace | adaptateur R legacy (`Lang::Use`) | -| `use Math::pi;` | un `::` suit le premier identifiant | `Lang::UseModule` | -| `use MonAlias;` | identifiant `PascalCase`, pas de `::` | `Lang::Import` (type) | -| `use ma_variable;` | identifiant `snake_case`, pas de `::` | `Let`/`Alias` (variable) | - -### Détails plus fins - -- `in` comme opérateur de type-condition est tokenisé `tag("in ")` — l'espace final fait partie - du motif, donc `in` immédiatement suivi d'une parenthèse sans espace ne matchera pas. -- `embed` (embedding de type nommé sur un champ de record) est un mot-clé « doux » reconnu - seulement via `tag("embed") + multispace1` — un champ réellement nommé `embed` (suivi - directement de `:`, sans espace) reste donc parsable comme nom de champ ordinaire. -- Le spread statique `..source` (nominal) exige explicitement `not(char('.'))` après les deux - points pour ne jamais être confondu avec `...source` (spread runtime) ou `...` (paramètre - variadique) — les trois partagent un préfixe visuel. -- `nom::alt()` plafonne à 21 alternatives par tuple : `single_element` et `single_type` sont - chacun scindés en deux `alt()` imbriqués pour rester sous la limite. Ajouter un nouveau - combinator à l'un de ces points d'entrée demande de respecter ce découpage, pas d'ajouter une - 22ᵉ branche à plat.