diff --git a/.claude/PROJECT.adoc b/.claude/PROJECT.adoc new file mode 100644 index 00000000..22a56933 --- /dev/null +++ b/.claude/PROJECT.adoc @@ -0,0 +1,80 @@ +== AffineScript - Claude Code Instructions + +This is the AffineScript compiler, written in OCaml. + +=== Project Structure + +.... +affinescript/ +├── lib/ # Core compiler library +│ ├── ast.ml # Abstract syntax tree +│ ├── token.ml # Token definitions +│ ├── lexer.ml # Lexer (sedlex-based) +│ ├── parser.ml # Parser (menhir-based) [TODO] +│ ├── span.ml # Source location tracking +│ └── error.ml # Diagnostics and error handling +├── bin/ # CLI executable +│ └── main.ml # Command-line interface +├── test/ # Test suite +└── docs/ # Documentation +.... + +=== Build Commands + +[source,bash] +---- +# Build +dune build + +# Run tests +dune runtest + +# Format code +dune fmt + +# Generate docs +dune build @doc + +# Run compiler +dune exec affinescript -- +---- + +=== Coding Conventions + +* Use descriptive variable names +* All files should have type annotations where helpful +* Error messages should follow the format in `+error.ml+` +* Use `+ppx_deriving+` for show, eq, ord on types +* Use `+sexp+` for serialization of AST types + +=== Language Specification + +The full language specification is at +`+/var$HOME/affinescript-spec.md+`. + +Key language features: - *Partial by default*: Functions are partial +unless marked `+total+` - *Quantity annotations*: `+0+` (erased), `+1+` +(linear), `+ω+` (unrestricted) - *Row polymorphism*: `+{x: Int, ..r}+` +for extensible records - *Extensible effects*: User-defined effects with +`+effect+` keyword - *Ownership*: `+own+`, `+ref+`, `+mut+` modifiers + +=== Implementation Priority + +[arabic] +. Lexer (sedlex) - in progress +. Parser (menhir) +. Name resolution +. Type checker (bidirectional) +. Borrow checker +. Effect checking +. WASM codegen + +=== Testing + +Tests go in `+test/+` directory. Use Alcotest: + +[source,ocaml] +---- +let test_something () = + Alcotest.(check string) "description" expected actual +---- diff --git a/.claude/PROJECT.md b/.claude/PROJECT.md deleted file mode 100644 index e03dfb7a..00000000 --- a/.claude/PROJECT.md +++ /dev/null @@ -1,77 +0,0 @@ -# AffineScript - Claude Code Instructions - -This is the AffineScript compiler, written in OCaml. - -## Project Structure - -``` -affinescript/ -├── lib/ # Core compiler library -│ ├── ast.ml # Abstract syntax tree -│ ├── token.ml # Token definitions -│ ├── lexer.ml # Lexer (sedlex-based) -│ ├── parser.ml # Parser (menhir-based) [TODO] -│ ├── span.ml # Source location tracking -│ └── error.ml # Diagnostics and error handling -├── bin/ # CLI executable -│ └── main.ml # Command-line interface -├── test/ # Test suite -└── docs/ # Documentation -``` - -## Build Commands - -```bash -# Build -dune build - -# Run tests -dune runtest - -# Format code -dune fmt - -# Generate docs -dune build @doc - -# Run compiler -dune exec affinescript -- -``` - -## Coding Conventions - -- Use descriptive variable names -- All files should have type annotations where helpful -- Error messages should follow the format in `error.ml` -- Use `ppx_deriving` for show, eq, ord on types -- Use `sexp` for serialization of AST types - -## Language Specification - -The full language specification is at `/var$HOME/affinescript-spec.md`. - -Key language features: -- **Partial by default**: Functions are partial unless marked `total` -- **Quantity annotations**: `0` (erased), `1` (linear), `ω` (unrestricted) -- **Row polymorphism**: `{x: Int, ..r}` for extensible records -- **Extensible effects**: User-defined effects with `effect` keyword -- **Ownership**: `own`, `ref`, `mut` modifiers - -## Implementation Priority - -1. Lexer (sedlex) - in progress -2. Parser (menhir) -3. Name resolution -4. Type checker (bidirectional) -5. Borrow checker -6. Effect checking -7. WASM codegen - -## Testing - -Tests go in `test/` directory. Use Alcotest: - -```ocaml -let test_something () = - Alcotest.(check string) "description" expected actual -``` diff --git a/ARCHITECTURE.adoc b/ARCHITECTURE.adoc new file mode 100644 index 00000000..1c0a7a69 --- /dev/null +++ b/ARCHITECTURE.adoc @@ -0,0 +1,48 @@ +== Architecture + +=== Overview + +This repository follows a modular, maintainable architecture designed +for clarity, scalability, and long-term sustainability. + +=== Directory Structure + +.... +. +├── src/ # Source code +├── tests/ # Test suites +├── docs/ # Documentation +├── scripts/ # Utility scripts +├── config/ # Configuration files +├── LICENSE # License file +├── LICENSES/ # Full license texts +└── README.adoc # Project documentation +.... + +=== Design Principles + +* *Separation of Concerns*: Each module has a single responsibility +* *Testability*: Code is written to be easily testable +* *Documentation*: All public APIs are documented +* *Configuration*: Environment-specific settings are externalized + +=== Dependencies + +* External dependencies are minimized and clearly declared +* Version pinning is used for reproducibility + +=== Security Considerations + +* Sensitive data is never committed to the repository +* Secrets are managed through environment variables or secure vaults +* Regular dependency audits are performed + +=== Maintainability + +* Code follows consistent style guidelines +* Pull requests require review and CI checks +* Issues and discussions are tracked transparently + +''''' + +_Last updated: 2026-07-18_ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 607e3d8c..00000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,47 +0,0 @@ -# Architecture - -## Overview - -This repository follows a modular, maintainable architecture designed for clarity, scalability, and long-term sustainability. - -## Directory Structure - -``` -. -├── src/ # Source code -├── tests/ # Test suites -├── docs/ # Documentation -├── scripts/ # Utility scripts -├── config/ # Configuration files -├── LICENSE # License file -├── LICENSES/ # Full license texts -└── README.adoc # Project documentation -``` - -## Design Principles - -- **Separation of Concerns**: Each module has a single responsibility -- **Testability**: Code is written to be easily testable -- **Documentation**: All public APIs are documented -- **Configuration**: Environment-specific settings are externalized - -## Dependencies - -- External dependencies are minimized and clearly declared -- Version pinning is used for reproducibility - -## Security Considerations - -- Sensitive data is never committed to the repository -- Secrets are managed through environment variables or secure vaults -- Regular dependency audits are performed - -## Maintainability - -- Code follows consistent style guidelines -- Pull requests require review and CI checks -- Issues and discussions are tracked transparently - ---- - -*Last updated: 2026-07-18* diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc new file mode 100644 index 00000000..ad4aa419 --- /dev/null +++ b/CHANGELOG.adoc @@ -0,0 +1,154 @@ +== Changelog + +All notable changes to `+affinescript+` will be documented in this file. + +This file is generated from conventional commits by the +https://github.com/hyperpolymath/standards/blob/main/.github/workflows/changelog-reusable.yml[`+changelog-reusable.yml+`] +workflow (`+hyperpolymath/standards#206+`). Adopt the workflow in this +repo’s CI to keep this file in sync automatically — see +https://github.com/hyperpolymath/standards/blob/main/templates/cliff.toml[`+templates/cliff.toml+`] +for the canonical config. + +The format follows https://keepachangelog.com/en/1.1.0/[Keep a +Changelog]; this project aims to follow +https://semver.org/spec/v2.0.0.html[Semantic Versioning]. + +=== [Unreleased] + +==== Added + +* feat(stdlib): Aggregate.affine — SQL group-by + aggregation primitives +— db-theory #3 (7 externs) (PR #527) +* feat(stdlib): Transaction.affine — affine-bounded write-set isolation +— db-theory #2 (8 externs) (PR #526) +* feat(stdlib): Sqlite schema introspection + bulk I/O + error +inspection — db-theory #1c (6 externs) (PR #525) +* feat(res-to-affine): partial-port mode #488 slice 3 — `+--partial+` +now translates array literals (`+[a, b]+`) and record literals +(`+{x, y}+` → `+Rec #{ x: x, y: y }+`, with a nominal placeholder type + +field-punning expansion) (Refs #488) +* feat(res-to-affine): partial-port mode #488 slice 2 — `+--partial+` +now desugars pipe-first `+->+` (`+a->f(b)+` → `+f(a, b)+`, chained +left-to-right), and translates `+if+`/`+else+` and blocks with `+let+` +statements (Refs #488) +* feat(res-to-affine): partial-port mode (#488) — new `+--partial+` flag +renders module-top-level functions as AffineScript `+fn+` skeletons with +`+switch+`→`+match+` and best-effort expression translation (literals / +idents / calls / binary ops with float-op + identity-equality +normalisation / `++++` / member + qualified access / ternary / variant + +tuple + literal patterns); un-translatable forms become +`+() /* TODO */+` / `+_ /* TODO */+` holes. Output deliberately does NOT +type-check but parses (verified). Distinct model from `+--translate+` +(Refs #488) +* feat(res-to-affine): Phase 3 slice 3 — `+--translate+` now also lowers +module-level `+let = +` (int/float/string/bool) to a typed +`+const name: T = value;+`; call / `+ref(...)+` / destructuring bindings +are skipped (not compile-time constants); every emitted form verified +compilable via `+main.exe check+`. `+switch+`→`+match+` and +qualified-path resolution remain out of the standalone-type-check scope +(Refs #57) +* feat(res-to-affine): Phase 3 slice 2 — `+--translate+` now also +handles record types (→ `+struct+`) and generics (type params `+'a+` → +`+[A]+`) across aliases / sums / records; `+mutable+`/optional-`+?+` +records, qualified paths, and nested generics are still skipped (never +guessed); every emitted form verified compilable via `+main.exe check+` +(Refs #57) +* feat(res-to-affine): Phase 3 slice 1 — `+--translate+` renders +fully-structural type declarations (primitive aliases + simple sum +types) into compilable AffineScript; conservative (generics / qualified +paths / records / non-primitive payloads are skipped, never guessed); +walker-only (Refs #57) +* feat(stdlib/Http): RSR rewire — surface `+hpm-http-rsr+` Zig FFI (10 +server-side externs: listen / port / free / accept / method / path / +header / body / respond / request-free) + opaque `+HpmHttpServer+` + +`+HpmHttpRequest+` types; native-only (#425) +* feat(stdlib/json): v0.3 — RSR rewire to `+hpm-json-rsr+` Zig FFI (11 +externs + opaque `+HpmJsonValue+` + `+parse+` / `+to_json+`), Deno-ESM +lowering via `+__as_hpmJson*+` shims (#421) +* feat(parser): trailing-comma in fn params and expr lists (Refs +gitbot-fleet#148) (#370) +* feat(lexer): underscore-prefix idents `+_key+`/`+_unused+` (Refs +gitbot-fleet#148) (#373) +* feat(parser): record-update spread at start `+#{ ..base, f: v }+` +(Refs gitbot-fleet#148) (#376) +* feat(parser): fn-type with effect arrow in type position (Refs +gitbot-fleet#148) +* feat(borrow): CFG-join for ExprHandle + ExprTry catch arms (CORE-01 +pt3 Slice C-light, Refs #177) (#358) +* feat(tw_verify): v2-parse support for affinescript.ownership (ADR-020) +(#352) +* feat(wasi): #180 ADR-015 S6b — sockets on-ramp via net_shutdown +* feat(stdlib): STDLIB-04e — wire `+string_to_int+` alias + lock +pure-extern semantics (Closes #332) (#338) +* feat(stdlib): STDLIB-04b — wire Throws extern `+error<T>+` +(Closes #329) (#340) +* feat(wasm): byte-level load/store IR + env_at/arg_at (ADR-015 S5) +(#339) + +==== Fixed + +* fix(vscode-smoke): SKIP cleanly when @hyperpolymath/affine-vscode is +unpublished (#381) +* fix(governance): rename CLAUDE.md TypeScript exemptions heading to +match workflow regex +* fix(stdlib): wire env_at / arg_at surface — codegen lowers via +gen_str_at_via_get (ADR-015 S5, #180) (#364) +* fix(ci): unblock the PR queue — bench/dune + adapter-load + +.res-fixture exemption (#361) +* fix(interp): wire missing string_length builtin (Refs #332, #329) +(#362) +* fix(borrow): escape `+(*r+` inside doc-comment examples (unblocks main +… (#349) +* fix(interp): `+eval_decl+` handles `+FnExtern+` (#328 build-failure +root cause) (#346) +* fix(version): single source of truth via lib/version.ml + tag-time +bake (#297) (#300) +* fix(shim): relicense the JSR shim package to MPL-2.0 (#299) +* fix(release): scope checksums-job gh calls with –repo to avoid the git +probe (#294) + +==== Changed + +* refactor(codegen): extract affinescript.ownership emission to lib/tw_… +(#347) + +==== Documentation + +* docs(claude): refresh language-policy tables for 2026-05-25 estate +policy (#363) +* docs: restore ADR-020 + ADR-021 + coordination ledger (lost in #344 +squash) (#350) +* docs(CLAUDE.md): agent operations notes from parallel-bot session exp… +(#348) +* docs(adr-015): settle the S4 / S5 / S6c numbering drift (#180) +* docs(adr): CORE-02 / #234 / ADR-016 — truth ledger to "`DELIVERED`" +(#336) +* docs(tech-debt): split STDLIB-04 into 04a–04e per per-extern audit +(Refs #175) (#333) +* docs(res-to-affine): corpus run + regex precision fixes (Refs #57) +(#319) +* docs: README internal-drift fix + DOC-04/05/06 done-in-tree (Refs +#176, Refs #175) (#315) +* docs: post-#303 catch-up — #297/#300/#301/#302/#304 + repos-monorepo +retirement (#305) +* docs: catch up TECH-DEBT, PACKAGING + STATE.a2ml with the 2026-05-20 +JSR publish (#303) + +==== CI + +* ci(migration-assistant): fix smoke-parse for tree-sitter-cli 0.25 +(#342) +* ci: bump github/codeql-action from 4.32.6 to 4.36.0 (#323) +* ci: bump actions/upload-artifact from 4.6.2 to 7.0.1 (#324) +* ci: bump actions/github-script from 8.0.0 to 9.0.0 (#325) +* ci: bump actions/checkout from 4 to 6 (#326) + +=== Pre-history + +Prior commits to this file’s introduction are recorded in git history +but not formally classified into Keep-a-Changelog sections. To backfill, +run `+git cliff -o CHANGELOG.md+` locally using the canonical +https://github.com/hyperpolymath/standards/blob/main/templates/cliff.toml[`+cliff.toml+`] +— this is one-shot mechanical work. + +''''' diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 086f8c10..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,89 +0,0 @@ - - -# Changelog - -All notable changes to `affinescript` will be documented in this file. - -This file is generated from conventional commits by the -[`changelog-reusable.yml`](https://github.com/hyperpolymath/standards/blob/main/.github/workflows/changelog-reusable.yml) -workflow (`hyperpolymath/standards#206`). Adopt the workflow in this repo's CI to keep this file in sync automatically — see -[`templates/cliff.toml`](https://github.com/hyperpolymath/standards/blob/main/templates/cliff.toml) -for the canonical config. - -The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); -this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Added - -- feat(stdlib): Aggregate.affine — SQL group-by + aggregation primitives — db-theory #3 (7 externs) (PR #527) -- feat(stdlib): Transaction.affine — affine-bounded write-set isolation — db-theory #2 (8 externs) (PR #526) -- feat(stdlib): Sqlite schema introspection + bulk I/O + error inspection — db-theory #1c (6 externs) (PR #525) -- feat(res-to-affine): partial-port mode #488 slice 3 — `--partial` now translates array literals (`[a, b]`) and record literals (`{x, y}` → `Rec #{ x: x, y: y }`, with a nominal placeholder type + field-punning expansion) (Refs #488) -- feat(res-to-affine): partial-port mode #488 slice 2 — `--partial` now desugars pipe-first `->` (`a->f(b)` → `f(a, b)`, chained left-to-right), and translates `if`/`else` and blocks with `let` statements (Refs #488) -- feat(res-to-affine): partial-port mode (#488) — new `--partial` flag renders module-top-level functions as AffineScript `fn` skeletons with `switch`→`match` and best-effort expression translation (literals / idents / calls / binary ops with float-op + identity-equality normalisation / `++` / member + qualified access / ternary / variant + tuple + literal patterns); un-translatable forms become `() /* TODO */` / `_ /* TODO */` holes. Output deliberately does NOT type-check but parses (verified). Distinct model from `--translate` (Refs #488) -- feat(res-to-affine): Phase 3 slice 3 — `--translate` now also lowers module-level `let = ` (int/float/string/bool) to a typed `const name: T = value;`; call / `ref(...)` / destructuring bindings are skipped (not compile-time constants); every emitted form verified compilable via `main.exe check`. `switch`→`match` and qualified-path resolution remain out of the standalone-type-check scope (Refs #57) -- feat(res-to-affine): Phase 3 slice 2 — `--translate` now also handles record types (→ `struct`) and generics (type params `'a` → `[A]`) across aliases / sums / records; `mutable`/optional-`?` records, qualified paths, and nested generics are still skipped (never guessed); every emitted form verified compilable via `main.exe check` (Refs #57) -- feat(res-to-affine): Phase 3 slice 1 — `--translate` renders fully-structural type declarations (primitive aliases + simple sum types) into compilable AffineScript; conservative (generics / qualified paths / records / non-primitive payloads are skipped, never guessed); walker-only (Refs #57) -- feat(stdlib/Http): RSR rewire — surface `hpm-http-rsr` Zig FFI (10 server-side externs: listen / port / free / accept / method / path / header / body / respond / request-free) + opaque `HpmHttpServer` + `HpmHttpRequest` types; native-only (#425) -- feat(stdlib/json): v0.3 — RSR rewire to `hpm-json-rsr` Zig FFI (11 externs + opaque `HpmJsonValue` + `parse` / `to_json`), Deno-ESM lowering via `__as_hpmJson*` shims (#421) -- feat(parser): trailing-comma in fn params and expr lists (Refs gitbot-fleet#148) (#370) -- feat(lexer): underscore-prefix idents `_key`/`_unused` (Refs gitbot-fleet#148) (#373) -- feat(parser): record-update spread at start `#{ ..base, f: v }` (Refs gitbot-fleet#148) (#376) -- feat(parser): fn-type with effect arrow in type position (Refs gitbot-fleet#148) -- feat(borrow): CFG-join for ExprHandle + ExprTry catch arms (CORE-01 pt3 Slice C-light, Refs #177) (#358) -- feat(tw_verify): v2-parse support for affinescript.ownership (ADR-020) (#352) -- feat(wasi): #180 ADR-015 S6b — sockets on-ramp via net_shutdown -- feat(stdlib): STDLIB-04e — wire `string_to_int` alias + lock pure-extern semantics (Closes #332) (#338) -- feat(stdlib): STDLIB-04b — wire Throws extern `error<T>` (Closes #329) (#340) -- feat(wasm): byte-level load/store IR + env_at/arg_at (ADR-015 S5) (#339) - -### Fixed - -- fix(vscode-smoke): SKIP cleanly when @hyperpolymath/affine-vscode is unpublished (#381) -- fix(governance): rename CLAUDE.md TypeScript exemptions heading to match workflow regex -- fix(stdlib): wire env_at / arg_at surface — codegen lowers via gen_str_at_via_get (ADR-015 S5, #180) (#364) -- fix(ci): unblock the PR queue — bench/dune + adapter-load + .res-fixture exemption (#361) -- fix(interp): wire missing string_length builtin (Refs #332, #329) (#362) -- fix(borrow): escape `(*r` inside doc-comment examples (unblocks main … (#349) -- fix(interp): `eval_decl` handles `FnExtern` (#328 build-failure root cause) (#346) -- fix(version): single source of truth via lib/version.ml + tag-time bake (#297) (#300) -- fix(shim): relicense the JSR shim package to MPL-2.0 (#299) -- fix(release): scope checksums-job gh calls with --repo to avoid the git probe (#294) - -### Changed - -- refactor(codegen): extract affinescript.ownership emission to lib/tw_… (#347) - -### Documentation - -- docs(claude): refresh language-policy tables for 2026-05-25 estate policy (#363) -- docs: restore ADR-020 + ADR-021 + coordination ledger (lost in #344 squash) (#350) -- docs(CLAUDE.md): agent operations notes from parallel-bot session exp… (#348) -- docs(adr-015): settle the S4 / S5 / S6c numbering drift (#180) -- docs(adr): CORE-02 / #234 / ADR-016 — truth ledger to "DELIVERED" (#336) -- docs(tech-debt): split STDLIB-04 into 04a–04e per per-extern audit (Refs #175) (#333) -- docs(res-to-affine): corpus run + regex precision fixes (Refs #57) (#319) -- docs: README internal-drift fix + DOC-04/05/06 done-in-tree (Refs #176, Refs #175) (#315) -- docs: post-#303 catch-up — #297/#300/#301/#302/#304 + repos-monorepo retirement (#305) -- docs: catch up TECH-DEBT, PACKAGING + STATE.a2ml with the 2026-05-20 JSR publish (#303) - -### CI - -- ci(migration-assistant): fix smoke-parse for tree-sitter-cli 0.25 (#342) -- ci: bump github/codeql-action from 4.32.6 to 4.36.0 (#323) -- ci: bump actions/upload-artifact from 4.6.2 to 7.0.1 (#324) -- ci: bump actions/github-script from 8.0.0 to 9.0.0 (#325) -- ci: bump actions/checkout from 4 to 6 (#326) - -## Pre-history - -Prior commits to this file's introduction are recorded in git history but not formally classified into Keep-a-Changelog sections. To backfill, run `git cliff -o CHANGELOG.md` locally using the canonical [`cliff.toml`](https://github.com/hyperpolymath/standards/blob/main/templates/cliff.toml) — this is one-shot mechanical work. - ---- - - diff --git a/CODE_OF_CONDUCT.adoc b/CODE_OF_CONDUCT.adoc new file mode 100644 index 00000000..8fa35bd6 --- /dev/null +++ b/CODE_OF_CONDUCT.adoc @@ -0,0 +1,323 @@ +== Code of Conduct + +=== Our Pledge + +We as members, contributors, and leaders pledge to make participation in +AffineScript a harassment-free experience for everyone, regardless of +age, body size, visible or invisible disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, +race, caste, colour, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, +welcoming, diverse, inclusive, and healthy community. + +We recognise that a thriving open source community requires +*psychological safety* — an environment where people can contribute, ask +questions, make mistakes, and learn without fear of ridicule or +retaliation. + +''''' + +=== Our Standards + +==== Expected Behaviour + +The following behaviours contribute to a positive environment: + +*Communication* - Using welcoming and inclusive language - Being +respectful of differing viewpoints and experiences - Giving and +gracefully accepting constructive feedback - Assuming good intent while +addressing impact - Communicating clearly and patiently, especially with +newcomers + +*Collaboration* - Focusing on what is best for the community - Showing +empathy and kindness toward other community members - Being +collaborative rather than competitive - Mentoring and supporting less +experienced contributors - Celebrating others’ contributions and +successes + +*Professionalism* - Accepting responsibility and apologising to those +affected by our mistakes - Learning from the experience and avoiding +repetition - Respecting others’ time and attention - Staying on topic in +project spaces - Following project guidelines and conventions + +*Accessibility* - Using plain language and avoiding unnecessary jargon - +Providing alt text for images and transcripts for audio/video - Being +patient with those using assistive technologies - Accommodating +different communication styles and needs - Recognising that not everyone +communicates the same way + +==== Unacceptable Behaviour + +The following behaviours are considered harassment and are unacceptable: + +*Harassment* - The use of sexualised language or imagery, and sexual +attention or advances of any kind - Trolling, insulting or derogatory +comments, and personal or political attacks - Public or private +harassment - Deliberate intimidation, stalking, or following (online or +in-person) - Unwelcome physical contact or simulated physical contact +(e.g., emoji) - Sustained disruption of talks, events, or online +discussions + +*Discrimination* - Discriminatory jokes and language - Posting or +threatening to post others’ personally identifying information +("`doxing`") - Advocating for, or encouraging, any of the above +behaviour - Microaggressions — subtle, often unintentional, +discriminatory comments or actions + +*Professional Misconduct* - Publishing others’ private information +without explicit permission - Misrepresenting affiliation or +contributions - Plagiarism or claiming credit for others’ work - +Retaliating against anyone who reports a Code of Conduct violation - +Other conduct which could reasonably be considered inappropriate in a +professional setting + +==== Grey Areas + +Some situations require judgement. When uncertain: + +* *Intent vs Impact*: Good intentions do not excuse harmful impact. +Focus on making things right. +* *Power Dynamics*: Those with more power (maintainers, employers, +experienced contributors) must be especially mindful of their impact. +* *Cultural Differences*: What’s acceptable varies by culture. When in +doubt, err on the side of caution and ask. +* *Humour*: Jokes at others’ expense are rarely funny to everyone. Punch +up, not down. + +''''' + +=== Scope + +This Code of Conduct applies within all community spaces, including: + +*Online Spaces* - Repository discussions, issues, and pull/merge +requests - Project chat channels (Matrix, Discord, Slack, IRC) - Mailing +lists and forums - Social media when representing the project - Video +calls and virtual meetings + +*In-Person Spaces* - Conferences, meetups, and events - Workshops and +training sessions - Any gathering where you represent the project + +*Representation* This Code of Conduct also applies when an individual is +officially representing the community in public spaces. Examples +include: + +* Using an official project email address +* Posting via an official social media account +* Acting as an appointed representative at an event +* Speaking on behalf of the project + +''''' + +=== Enforcement + +==== Reporting + +If you experience or witness unacceptable behaviour, or have any other +concerns, please report it as soon as possible. + +*How to Report* + +[width="99%",cols="30%,33%,37%",options="header",] +|=== +|Method |Details |Best For +|*Email* |j.d.a.jewell@open.ac.uk |Detailed reports, sensitive matters + +|*Private Message* |Contact any maintainer directly |Quick questions, +minor issues +|=== + +*What to Include* + +* Your contact information (unless anonymous) +* Names/usernames of those involved +* Description of what happened +* When and where it occurred +* Any witnesses +* Any supporting evidence (screenshots, links) +* How you would like us to respond (if you have a preference) + +*What Happens Next* + +[arabic] +. You will receive acknowledgment within *7 days* +. The maintainers will review the report +. We may ask for additional information +. We will determine appropriate action +. We will inform you of the outcome (respecting others’ privacy) + +==== Confidentiality + +All reports will be handled with discretion: + +* Reporter identity is protected by default +* Details are shared only with those who need to know +* We will ask before naming you in any communication +* Anonymous reports are accepted and investigated + +==== Conflicts of Interest + +If a maintainer is involved in an incident: + +* They will recuse themselves from the process +* An uninvolved party will handle the report +* We will disclose any potential conflicts + +''''' + +=== Enforcement Guidelines + +The maintainers will follow these guidelines in determining +consequences: + +==== 1. Correction + +*Community Impact*: Use of inappropriate language or other behaviour +deemed unprofessional or unwelcome. + +*Consequence*: A private, written warning providing clarity around the +nature of the violation and an explanation of why the behaviour was +inappropriate. A public apology may be requested. + +*Duration*: Immediate + +==== 2. Warning + +*Community Impact*: A violation through a single incident or series of +actions. + +*Consequence*: A warning with consequences for continued behaviour. No +interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, for a specified period. This +includes avoiding interactions in community spaces as well as external +channels like social media. Violating these terms may lead to a +temporary or permanent ban. + +*Duration*: 1-4 weeks + +==== 3. Temporary Ban + +*Community Impact*: A serious violation of community standards, +including sustained inappropriate behaviour. + +*Consequence*: A temporary ban from any sort of interaction or public +communication with the community for a specified period. No public or +private interaction with the people involved, including unsolicited +interaction with those enforcing the Code of Conduct, is allowed during +this period. Violating these terms may lead to a permanent ban. + +*Duration*: 1-6 months + +==== 4. Permanent Ban + +*Community Impact*: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behaviour, harassment of an +individual, or aggression toward or disparagement of classes of +individuals. + +*Consequence*: A permanent ban from any sort of public interaction +within the community. + +*Duration*: Permanent (with appeal rights after 12 months) + +''''' + +=== Appeals + +If you believe an enforcement decision was made in error: + +[arabic] +. *Wait 7 days* after the decision (cooling-off period) +. *Email* j.d.a.jewell@open.ac.uk with subject line "`Appeal: [Original +Report ID]`" +. *Explain* why you believe the decision should be reconsidered +. *Provide* any new information not previously available + +*Appeals Process* + +* Appeals are reviewed after consultation with an uninvolved party where +possible +* You will receive a response within 14 days +* The appeals decision is final +* You may only appeal once per incident + +*Grounds for Appeal* + +* Procedural errors in the original investigation +* New evidence not previously available +* Disproportionate response to the violation +* Misunderstanding of facts + +''''' + +=== Supporting Those Who Report + +We are committed to supporting those who report violations: + +*We Will* - Believe and take all reports seriously - Respect your +privacy and confidentiality preferences - Keep you informed of progress +(if you wish) - Take steps to protect you from retaliation - Provide +resources if you need support + +*We Will Not* - Require you to confront the person directly - Dismiss +reports without investigation - Reveal your identity without consent - +Tolerate retaliation against reporters - Rush you to make decisions + +''''' + +=== Prevention + +Beyond enforcement, we actively work to prevent issues: + +*Onboarding* - All contributors are expected to read this Code of +Conduct + +*Culture* - We model the behaviour we expect - We intervene early when +we see potential issues - We thank people for positive contributions - +We create opportunities for diverse voices + +*Review* - This Code of Conduct is reviewed annually - Community +feedback is welcomed - Changes are communicated clearly + +''''' + +=== Acknowledgments + +This Code of Conduct is adapted from: + +* https://www.contributor-covenant.org/[Contributor Covenant], version +2.1 +* https://www.djangoproject.com/conduct/[Django Code of Conduct] +* https://www.rust-lang.org/policies/code-of-conduct[Rust Code of +Conduct] +* https://www.python.org/psf/conduct/[Python Community Code of Conduct] + +We thank these communities for their leadership in creating welcoming +spaces. + +''''' + +=== Questions? + +If you have questions about this Code of Conduct: + +* Open a +https://github.com/hyperpolymath/affinescript/discussions[Discussion] +(for general questions) +* Email j.d.a.jewell@open.ac.uk (for private questions) +* Contact any maintainer directly + +''''' + +=== Summary + +*Be kind. Be respectful. Be collaborative.* + +We’re all here because we care about this project. Let’s make it a place +where everyone can do their best work. + +''''' + +Last updated: 2026 · Based on Contributor Covenant 2.1 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 86acb0c6..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,293 +0,0 @@ -# Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in AffineScript a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, colour, religion, or sexual identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. - -We recognise that a thriving open source community requires **psychological safety** — an environment where people can contribute, ask questions, make mistakes, and learn without fear of ridicule or retaliation. - ---- - -## Our Standards - -### Expected Behaviour - -The following behaviours contribute to a positive environment: - -**Communication** -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Giving and gracefully accepting constructive feedback -- Assuming good intent while addressing impact -- Communicating clearly and patiently, especially with newcomers - -**Collaboration** -- Focusing on what is best for the community -- Showing empathy and kindness toward other community members -- Being collaborative rather than competitive -- Mentoring and supporting less experienced contributors -- Celebrating others' contributions and successes - -**Professionalism** -- Accepting responsibility and apologising to those affected by our mistakes -- Learning from the experience and avoiding repetition -- Respecting others' time and attention -- Staying on topic in project spaces -- Following project guidelines and conventions - -**Accessibility** -- Using plain language and avoiding unnecessary jargon -- Providing alt text for images and transcripts for audio/video -- Being patient with those using assistive technologies -- Accommodating different communication styles and needs -- Recognising that not everyone communicates the same way - -### Unacceptable Behaviour - -The following behaviours are considered harassment and are unacceptable: - -**Harassment** -- The use of sexualised language or imagery, and sexual attention or advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Deliberate intimidation, stalking, or following (online or in-person) -- Unwelcome physical contact or simulated physical contact (e.g., emoji) -- Sustained disruption of talks, events, or online discussions - -**Discrimination** -- Discriminatory jokes and language -- Posting or threatening to post others' personally identifying information ("doxing") -- Advocating for, or encouraging, any of the above behaviour -- Microaggressions — subtle, often unintentional, discriminatory comments or actions - -**Professional Misconduct** -- Publishing others' private information without explicit permission -- Misrepresenting affiliation or contributions -- Plagiarism or claiming credit for others' work -- Retaliating against anyone who reports a Code of Conduct violation -- Other conduct which could reasonably be considered inappropriate in a professional setting - -### Grey Areas - -Some situations require judgement. When uncertain: - -- **Intent vs Impact**: Good intentions do not excuse harmful impact. Focus on making things right. -- **Power Dynamics**: Those with more power (maintainers, employers, experienced contributors) must be especially mindful of their impact. -- **Cultural Differences**: What's acceptable varies by culture. When in doubt, err on the side of caution and ask. -- **Humour**: Jokes at others' expense are rarely funny to everyone. Punch up, not down. - ---- - -## Scope - -This Code of Conduct applies within all community spaces, including: - -**Online Spaces** -- Repository discussions, issues, and pull/merge requests -- Project chat channels (Matrix, Discord, Slack, IRC) -- Mailing lists and forums -- Social media when representing the project -- Video calls and virtual meetings - -**In-Person Spaces** -- Conferences, meetups, and events -- Workshops and training sessions -- Any gathering where you represent the project - -**Representation** -This Code of Conduct also applies when an individual is officially representing the community in public spaces. Examples include: - -- Using an official project email address -- Posting via an official social media account -- Acting as an appointed representative at an event -- Speaking on behalf of the project - ---- - -## Enforcement - -### Reporting - -If you experience or witness unacceptable behaviour, or have any other concerns, please report it as soon as possible. - -**How to Report** - -| Method | Details | Best For | -|--------|---------|----------| -| **Email** | j.d.a.jewell@open.ac.uk | Detailed reports, sensitive matters | -| **Private Message** | Contact any maintainer directly | Quick questions, minor issues | - -**What to Include** - -- Your contact information (unless anonymous) -- Names/usernames of those involved -- Description of what happened -- When and where it occurred -- Any witnesses -- Any supporting evidence (screenshots, links) -- How you would like us to respond (if you have a preference) - -**What Happens Next** - -1. You will receive acknowledgment within **7 days** -2. The maintainers will review the report -3. We may ask for additional information -4. We will determine appropriate action -5. We will inform you of the outcome (respecting others' privacy) - -### Confidentiality - -All reports will be handled with discretion: - -- Reporter identity is protected by default -- Details are shared only with those who need to know -- We will ask before naming you in any communication -- Anonymous reports are accepted and investigated - -### Conflicts of Interest - -If a maintainer is involved in an incident: - -- They will recuse themselves from the process -- An uninvolved party will handle the report -- We will disclose any potential conflicts - ---- - -## Enforcement Guidelines - -The maintainers will follow these guidelines in determining consequences: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behaviour deemed unprofessional or unwelcome. - -**Consequence**: A private, written warning providing clarity around the nature of the violation and an explanation of why the behaviour was inappropriate. A public apology may be requested. - -**Duration**: Immediate - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of actions. - -**Consequence**: A warning with consequences for continued behaviour. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. - -**Duration**: 1-4 weeks - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including sustained inappropriate behaviour. - -**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. - -**Duration**: 1-6 months - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behaviour, harassment of an individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the community. - -**Duration**: Permanent (with appeal rights after 12 months) - ---- - -## Appeals - -If you believe an enforcement decision was made in error: - -1. **Wait 7 days** after the decision (cooling-off period) -2. **Email** j.d.a.jewell@open.ac.uk with subject line "Appeal: [Original Report ID]" -3. **Explain** why you believe the decision should be reconsidered -4. **Provide** any new information not previously available - -**Appeals Process** - -- Appeals are reviewed after consultation with an uninvolved party where possible -- You will receive a response within 14 days -- The appeals decision is final -- You may only appeal once per incident - -**Grounds for Appeal** - -- Procedural errors in the original investigation -- New evidence not previously available -- Disproportionate response to the violation -- Misunderstanding of facts - ---- - -## Supporting Those Who Report - -We are committed to supporting those who report violations: - -**We Will** -- Believe and take all reports seriously -- Respect your privacy and confidentiality preferences -- Keep you informed of progress (if you wish) -- Take steps to protect you from retaliation -- Provide resources if you need support - -**We Will Not** -- Require you to confront the person directly -- Dismiss reports without investigation -- Reveal your identity without consent -- Tolerate retaliation against reporters -- Rush you to make decisions - ---- - -## Prevention - -Beyond enforcement, we actively work to prevent issues: - -**Onboarding** -- All contributors are expected to read this Code of Conduct - -**Culture** -- We model the behaviour we expect -- We intervene early when we see potential issues -- We thank people for positive contributions -- We create opportunities for diverse voices - -**Review** -- This Code of Conduct is reviewed annually -- Community feedback is welcomed -- Changes are communicated clearly - ---- - -## Acknowledgments - -This Code of Conduct is adapted from: - -- [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1 -- [Django Code of Conduct](https://www.djangoproject.com/conduct/) -- [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) -- [Python Community Code of Conduct](https://www.python.org/psf/conduct/) - -We thank these communities for their leadership in creating welcoming spaces. - ---- - -## Questions? - -If you have questions about this Code of Conduct: - -- Open a [Discussion](https://github.com/hyperpolymath/affinescript/discussions) (for general questions) -- Email j.d.a.jewell@open.ac.uk (for private questions) -- Contact any maintainer directly - ---- - -## Summary - -**Be kind. Be respectful. Be collaborative.** - -We're all here because we care about this project. Let's make it a place where everyone can do their best work. - ---- - -Last updated: 2026 · Based on Contributor Covenant 2.1 diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 00000000..3df21f6f --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,179 @@ +== Contributing to AffineScript + +Thank you for your interest in AffineScript — a practical language for +resource-safe systems, compiling to typed WebAssembly. This guide covers +how to set up a working tree, file useful bugs, and submit changes. + +For the language itself, start from link:README.adoc[`+README.adoc+`]. +For project state, blockers, and next-actions, see +link:.machine_readable/descriptiles/STATE.a2ml[`+.machine_readable/descriptiles/STATE.a2ml+`]. + +''''' + +=== Quick Start + +[source,bash] +---- +git clone https://github.com/hyperpolymath/affinescript.git +cd affinescript + +# Provision the OCaml toolchain (one-shot). See README.adoc "Getting Started" +# for the full list of opam packages and the `eval $(opam env)` note for +# non-interactive shells. +opam install -y \ + sedlex menhir ppx_deriving ppx_sexp_conv sexplib0 fmt cmdliner yojson \ + alcotest ocamlformat \ + js_of_ocaml js_of_ocaml-ppx js_of_ocaml-compiler +eval "$(opam env --switch=default --set-switch)" + +# Verify setup +dune build +dune runtest +---- + +Tested on OCaml 4.14.2 (the constraint in `+dune-project+` is +`+>= 4.14+`). + +==== Repository Structure + +.... +affinescript/ +├── lib/ # Compiler core (lexer, parser, typechecker, codegen, verifier) +├── bin/ # CLI driver — `_build/default/bin/main.exe` +├── stdlib/ # Standard library `.affine` modules +├── test/ # Alcotest suites (lexer, golden, e2e fixtures) +├── tests/ # Topic-grouped tests (borrow, codegen, effects, parser, …) +├── examples/ # Self-contained example programs +├── conformance/ # Conformance test corpus +├── docs/ # Specs, decisions, guides +├── packages/ # Aggregate JS/TS/ binding packages +├── editors/ # Editor integrations +├── js/ # `js_of_ocaml` playground (built into `playground.bc.js`) +├── .machine_readable/ # Machine-readable metadata (`.a2ml`) — see 0-AI-MANIFEST.a2ml +├── .github/ # CI workflows, issue templates +├── CODE_OF_CONDUCT.md +├── CONTRIBUTING.md # This file +├── LICENSE / LICENSES # MIT OR AGPL-3.0-or-later +├── MAINTAINERS.adoc +├── README.adoc +├── SECURITY.md +├── dune-project +└── justfile +.... + +''''' + +=== How to Contribute + +==== Reporting Bugs + +*Before reporting:* 1. Search +https://github.com/hyperpolymath/affinescript/issues[existing issues]. +2. Check that the bug reproduces against `+main+` +(`+git pull && dune build+`). + +*When reporting:* use the link:.github/ISSUE_TEMPLATE/bug_report.md[bug +report template] and include: + +* Clear, descriptive title. +* Environment: OCaml version, opam switch, OS. +* Steps to reproduce, ideally as a minimal `+.affine+` file plus the +exact `+dune exec affinescript -- +` invocation. +* Expected vs actual behaviour (compiler output, generated Wasm, runtime +trap, etc.). + +==== Suggesting Features + +*Before suggesting:* 1. Skim +link:docs/ROADMAP.adoc[`+docs/ROADMAP.adoc+`] and +`+.machine_readable/descriptiles/STATE.a2ml+`. 2. Search existing issues +and discussions. + +*When suggesting:* use the +link:.github/ISSUE_TEMPLATE/feature_request.md[feature request template] +and include: + +* Problem statement — what pain point does this solve? +* Proposed solution and any alternatives considered. +* Whether the change touches the core language, a face (frontend +surface), a backend, or the stdlib. + +==== Your First Contribution + +Look for issues labelled: + +* https://github.com/hyperpolymath/affinescript/labels/good%20first%20issue[`+good first issue+`] +* https://github.com/hyperpolymath/affinescript/labels/help%20wanted[`+help wanted+`] +* https://github.com/hyperpolymath/affinescript/labels/documentation[`+documentation+`] + +''''' + +=== Development Workflow + +==== Branch Naming + +.... +docs/short-description # Documentation +test/what-added # Test additions +feat/short-description # New features +fix/issue-number-description # Bug fixes +refactor/what-changed # Code improvements +security/what-fixed # Security fixes +ci/what-changed # CI / tooling +.... + +Branch from `+main+` and target `+main+` in your PR. + +==== Commit Messages + +We follow https://www.conventionalcommits.org/[Conventional Commits]: + +.... +(): + +[optional body] + +[optional footer, e.g. closes #N or Co-Authored-By: ...] +.... + +Common types: `+feat+`, `+fix+`, `+docs+`, `+test+`, `+refactor+`, +`+chore+`, `+ci+`. Common scopes: `+lexer+`, `+parser+`, `+typecheck+`, +`+codegen+`, `+verify+`, `+stdlib+`, `+cli+`, `+readme+`. + +==== Required Checks + +Before opening a PR, locally: + +[source,bash] +---- +dune build # must exit 0 +dune runtest # must be green +dune fmt # optional — auto-formats with ocamlformat +---- + +The `+methodology.a2ml+` file lists the canonical gate set. CI will +rerun `+build+` + `+runtest+` plus the security, lint, and policy +workflows in `+.github/workflows/+`. + +==== Pull Requests + +[arabic] +. Push your branch and open a PR against `+main+`. +. Use a Conventional-Commit-shaped title. +. In the body, summarise the change and link the issue it closes. +. Keep PRs focused — split unrelated changes into separate PRs. +. CI must be green. Maintainers squash-merge by default; commit message +lineage is preserved in the PR body. + +''''' + +=== Code of Conduct + +This project follows the link:CODE_OF_CONDUCT.md[Code of Conduct]. By +participating you agree to abide by it. + +=== License + +By contributing you agree your contribution is licensed under the +project’s dual licence (MIT OR AGPL-3.0-or-later), as recorded in +link:LICENSE[`+LICENSE+`] and per-file SPDX headers. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 7e34f6d8..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,155 +0,0 @@ -# Contributing to AffineScript - -Thank you for your interest in AffineScript — a practical language for resource-safe systems, compiling to typed WebAssembly. This guide covers how to set up a working tree, file useful bugs, and submit changes. - -For the language itself, start from [`README.adoc`](README.adoc). For project state, blockers, and next-actions, see [`.machine_readable/descriptiles/STATE.a2ml`](.machine_readable/descriptiles/STATE.a2ml). - ---- - -## Quick Start - -```bash -git clone https://github.com/hyperpolymath/affinescript.git -cd affinescript - -# Provision the OCaml toolchain (one-shot). See README.adoc "Getting Started" -# for the full list of opam packages and the `eval $(opam env)` note for -# non-interactive shells. -opam install -y \ - sedlex menhir ppx_deriving ppx_sexp_conv sexplib0 fmt cmdliner yojson \ - alcotest ocamlformat \ - js_of_ocaml js_of_ocaml-ppx js_of_ocaml-compiler -eval "$(opam env --switch=default --set-switch)" - -# Verify setup -dune build -dune runtest -``` - -Tested on OCaml 4.14.2 (the constraint in `dune-project` is `>= 4.14`). - -### Repository Structure - -``` -affinescript/ -├── lib/ # Compiler core (lexer, parser, typechecker, codegen, verifier) -├── bin/ # CLI driver — `_build/default/bin/main.exe` -├── stdlib/ # Standard library `.affine` modules -├── test/ # Alcotest suites (lexer, golden, e2e fixtures) -├── tests/ # Topic-grouped tests (borrow, codegen, effects, parser, …) -├── examples/ # Self-contained example programs -├── conformance/ # Conformance test corpus -├── docs/ # Specs, decisions, guides -├── packages/ # Aggregate JS/TS/ binding packages -├── editors/ # Editor integrations -├── js/ # `js_of_ocaml` playground (built into `playground.bc.js`) -├── .machine_readable/ # Machine-readable metadata (`.a2ml`) — see 0-AI-MANIFEST.a2ml -├── .github/ # CI workflows, issue templates -├── CODE_OF_CONDUCT.md -├── CONTRIBUTING.md # This file -├── LICENSE / LICENSES # MIT OR AGPL-3.0-or-later -├── MAINTAINERS.adoc -├── README.adoc -├── SECURITY.md -├── dune-project -└── justfile -``` - ---- - -## How to Contribute - -### Reporting Bugs - -**Before reporting:** -1. Search [existing issues](https://github.com/hyperpolymath/affinescript/issues). -2. Check that the bug reproduces against `main` (`git pull && dune build`). - -**When reporting:** use the [bug report template](.github/ISSUE_TEMPLATE/bug_report.md) and include: - -- Clear, descriptive title. -- Environment: OCaml version, opam switch, OS. -- Steps to reproduce, ideally as a minimal `.affine` file plus the exact `dune exec affinescript -- ` invocation. -- Expected vs actual behaviour (compiler output, generated Wasm, runtime trap, etc.). - -### Suggesting Features - -**Before suggesting:** -1. Skim [`docs/ROADMAP.adoc`](docs/ROADMAP.adoc) and `.machine_readable/descriptiles/STATE.a2ml`. -2. Search existing issues and discussions. - -**When suggesting:** use the [feature request template](.github/ISSUE_TEMPLATE/feature_request.md) and include: - -- Problem statement — what pain point does this solve? -- Proposed solution and any alternatives considered. -- Whether the change touches the core language, a face (frontend surface), a backend, or the stdlib. - -### Your First Contribution - -Look for issues labelled: - -- [`good first issue`](https://github.com/hyperpolymath/affinescript/labels/good%20first%20issue) -- [`help wanted`](https://github.com/hyperpolymath/affinescript/labels/help%20wanted) -- [`documentation`](https://github.com/hyperpolymath/affinescript/labels/documentation) - ---- - -## Development Workflow - -### Branch Naming - -``` -docs/short-description # Documentation -test/what-added # Test additions -feat/short-description # New features -fix/issue-number-description # Bug fixes -refactor/what-changed # Code improvements -security/what-fixed # Security fixes -ci/what-changed # CI / tooling -``` - -Branch from `main` and target `main` in your PR. - -### Commit Messages - -We follow [Conventional Commits](https://www.conventionalcommits.org/): - -``` -(): - -[optional body] - -[optional footer, e.g. closes #N or Co-Authored-By: ...] -``` - -Common types: `feat`, `fix`, `docs`, `test`, `refactor`, `chore`, `ci`. Common scopes: `lexer`, `parser`, `typecheck`, `codegen`, `verify`, `stdlib`, `cli`, `readme`. - -### Required Checks - -Before opening a PR, locally: - -```bash -dune build # must exit 0 -dune runtest # must be green -dune fmt # optional — auto-formats with ocamlformat -``` - -The `methodology.a2ml` file lists the canonical gate set. CI will rerun `build` + `runtest` plus the security, lint, and policy workflows in `.github/workflows/`. - -### Pull Requests - -1. Push your branch and open a PR against `main`. -2. Use a Conventional-Commit-shaped title. -3. In the body, summarise the change and link the issue it closes. -4. Keep PRs focused — split unrelated changes into separate PRs. -5. CI must be green. Maintainers squash-merge by default; commit message lineage is preserved in the PR body. - ---- - -## Code of Conduct - -This project follows the [Code of Conduct](CODE_OF_CONDUCT.md). By participating you agree to abide by it. - -## License - -By contributing you agree your contribution is licensed under the project's dual licence (MIT OR AGPL-3.0-or-later), as recorded in [`LICENSE`](LICENSE) and per-file SPDX headers. diff --git a/GOVERNANCE.adoc b/GOVERNANCE.adoc new file mode 100644 index 00000000..9b836fb2 --- /dev/null +++ b/GOVERNANCE.adoc @@ -0,0 +1,60 @@ +== Governance + +=== Overview + +This project is governed by the following principles and structures to +ensure transparent, inclusive, and effective decision-making. + +=== Roles and Responsibilities + +==== Maintainers + +Maintainers are responsible for: - Reviewing and merging pull requests - +Managing releases and versioning - Ensuring code quality and standards - +Triaging issues and bug reports - Community engagement and support + +==== Contributors + +Contributors are expected to: - Follow the code of conduct - Submit +well-documented pull requests - Write tests for new functionality - +Maintain existing tests - Update documentation as needed + +=== Decision Making + +==== Minor Changes + +* Can be made by any maintainer +* Include bug fixes, documentation updates, dependency updates + +==== Major Changes + +* Require discussion in issues or pull requests +* Include new features, architectural changes, API changes +* Need approval from at least 2 maintainers + +==== Breaking Changes + +* Require RFC (Request for Comments) process +* Need approval from majority of maintainers +* Must include migration guide + +=== Code of Conduct + +All participants are expected to follow our Code of Conduct. Violations +can be reported to the maintainers. + +=== Communication + +* *Issues*: For bug reports and feature requests +* *Discussions*: For questions and general discussion +* *Pull Requests*: For code contributions + +=== Licensing + +All contributions are made under the terms of the repository’s LICENSE +file. By submitting a pull request, you agree to license your +contributions accordingly. + +''''' + +_Last updated: 2026-07-18_ diff --git a/GOVERNANCE.md b/GOVERNANCE.md deleted file mode 100644 index e27364c7..00000000 --- a/GOVERNANCE.md +++ /dev/null @@ -1,60 +0,0 @@ -# Governance - -## Overview - -This project is governed by the following principles and structures to ensure transparent, inclusive, and effective decision-making. - -## Roles and Responsibilities - -### Maintainers - -Maintainers are responsible for: -- Reviewing and merging pull requests -- Managing releases and versioning -- Ensuring code quality and standards -- Triaging issues and bug reports -- Community engagement and support - -### Contributors - -Contributors are expected to: -- Follow the code of conduct -- Submit well-documented pull requests -- Write tests for new functionality -- Maintain existing tests -- Update documentation as needed - -## Decision Making - -### Minor Changes -- Can be made by any maintainer -- Include bug fixes, documentation updates, dependency updates - -### Major Changes -- Require discussion in issues or pull requests -- Include new features, architectural changes, API changes -- Need approval from at least 2 maintainers - -### Breaking Changes -- Require RFC (Request for Comments) process -- Need approval from majority of maintainers -- Must include migration guide - -## Code of Conduct - -All participants are expected to follow our Code of Conduct. Violations can be reported to the maintainers. - -## Communication - -- **Issues**: For bug reports and feature requests -- **Discussions**: For questions and general discussion -- **Pull Requests**: For code contributions - -## Licensing - -All contributions are made under the terms of the repository's LICENSE file. -By submitting a pull request, you agree to license your contributions accordingly. - ---- - -*Last updated: 2026-07-18* diff --git a/SECURITY.adoc b/SECURITY.adoc new file mode 100644 index 00000000..2b9c2925 --- /dev/null +++ b/SECURITY.adoc @@ -0,0 +1,452 @@ +== Security Policy + +We take security seriously. We appreciate your efforts to responsibly +disclose vulnerabilities and will make every effort to acknowledge your +contributions. + +=== Table of Contents + +* link:#reporting-a-vulnerability[Reporting a Vulnerability] +* link:#what-to-include[What to Include] +* link:#response-timeline[Response Timeline] +* link:#disclosure-policy[Disclosure Policy] +* link:#scope[Scope] +* link:#safe-harbour[Safe Harbour] +* link:#recognition[Recognition] +* link:#security-updates[Security Updates] +* link:#security-best-practices[Security Best Practices] + +''''' + +=== Reporting a Vulnerability + +==== Preferred Method: GitHub Security Advisories + +The preferred method for reporting security vulnerabilities is through +GitHub’s Security Advisory feature: + +[arabic] +. Navigate to +https://github.com/%7B%7BOWNER%7D%7D/%7B%7BREPO%7D%7D/security/advisories/new[Report +a Vulnerability] +. Click *"`Report a vulnerability`"* +. Complete the form with as much detail as possible +. Submit — we’ll receive a private notification + +This method ensures: + +* End-to-end encryption of your report +* Private discussion space for collaboration +* Coordinated disclosure tooling +* Automatic credit when the advisory is published + +==== Alternative: Encrypted Email + +If you cannot use GitHub Security Advisories, you may email us directly: + +[cols=",",] +|=== +|*Email* |\{\{SECURITY_EMAIL}} +|*PGP Key* |link:%7B%7BPGP_KEY_URL%7D%7D[Download Public Key] +|*Fingerprint* |`+{{PGP_FINGERPRINT}}+` +|=== + +[source,bash] +---- +# Import our PGP key +curl -sSL {{PGP_KEY_URL}} | gpg --import + +# Verify fingerprint +gpg --fingerprint {{SECURITY_EMAIL}} + +# Encrypt your report +gpg --armor --encrypt --recipient {{SECURITY_EMAIL}} report.txt +---- + +____ +*⚠️ Important:* Do not report security vulnerabilities through public +GitHub issues, pull requests, discussions, or social media. +____ + +''''' + +=== What to Include + +A good vulnerability report helps us understand and reproduce the issue +quickly. + +==== Required Information + +* *Description*: Clear explanation of the vulnerability +* *Impact*: What an attacker could achieve (confidentiality, integrity, +availability) +* *Affected versions*: Which versions/commits are affected +* *Reproduction steps*: Detailed steps to reproduce the issue + +==== Helpful Additional Information + +* *Proof of concept*: Code, scripts, or screenshots demonstrating the +vulnerability +* *Attack scenario*: Realistic attack scenario showing exploitability +* *CVSS score*: Your assessment of severity (use +https://www.first.org/cvss/calculator/3.1[CVSS 3.1 Calculator]) +* *CWE ID*: Common Weakness Enumeration identifier if known +* *Suggested fix*: If you have ideas for remediation +* *References*: Links to related vulnerabilities, research, or +advisories + +==== Example Report Structure + +[source,markdown] +---- +## Summary +[One-sentence description of the vulnerability] + +## Vulnerability Type +[e.g., SQL Injection, XSS, SSRF, Path Traversal, etc.] + +## Affected Component +[File path, function name, API endpoint, etc.] + +## Affected Versions +[Version range or specific commits] + +## Severity Assessment +- CVSS 3.1 Score: [X.X] +- CVSS Vector: [CVSS:3.1/AV:X/AC:X/PR:X/UI:X/S:X/C:X/I:X/A:X] + +## Description +[Detailed technical description] + +## Steps to Reproduce +1. [First step] +2. [Second step] +3. [...] + +## Proof of Concept +[Code, curl commands, screenshots, etc.] + +## Impact +[What can an attacker achieve?] + +## Suggested Remediation +[Optional: your ideas for fixing] + +## References +[Links to related issues, CVEs, research] +---- + +''''' + +=== Response Timeline + +We commit to the following response times: + +[width="100%",cols="24%,35%,41%",options="header",] +|=== +|Stage |Timeframe |Description +|*Initial Response* |48 hours |We acknowledge receipt and confirm we’re +investigating + +|*Triage* |7 days |We assess severity, confirm the vulnerability, and +estimate timeline + +|*Status Update* |Every 7 days |Regular updates on remediation progress + +|*Resolution* |90 days |Target for fix development and release (complex +issues may take longer) + +|*Disclosure* |90 days |Public disclosure after fix is available +(coordinated with you) +|=== + +____ +*Note:* These are targets, not guarantees. Complex vulnerabilities may +require more time. We’ll communicate openly about any delays. +____ + +''''' + +=== Disclosure Policy + +We follow *coordinated disclosure* (also known as responsible +disclosure): + +[arabic] +. *You report* the vulnerability privately +. *We acknowledge* and begin investigation +. *We develop* a fix and prepare a release +. *We coordinate* disclosure timing with you +. *We publish* security advisory and fix simultaneously +. *You may publish* your research after disclosure + +==== Our Commitments + +* We will not take legal action against researchers who follow this +policy +* We will work with you to understand and resolve the issue +* We will credit you in the security advisory (unless you prefer +anonymity) +* We will notify you before public disclosure +* We will publish advisories with sufficient detail for users to assess +risk + +==== Your Commitments + +* Report vulnerabilities promptly after discovery +* Give us reasonable time to address the issue before disclosure +* Do not access, modify, or delete data beyond what’s necessary to +demonstrate the vulnerability +* Do not degrade service availability (no DoS testing on production) +* Do not share vulnerability details with others until coordinated +disclosure + +==== Disclosure Timeline + +.... +Day 0 You report vulnerability +Day 1-2 We acknowledge receipt +Day 7 We confirm vulnerability and share initial assessment +Day 7-90 We develop and test fix +Day 90 Coordinated public disclosure + (earlier if fix is ready; later by mutual agreement) +.... + +If we cannot reach agreement on disclosure timing, we default to 90 days +from your initial report. + +''''' + +=== Scope + +==== In Scope ✅ + +The following are within scope for security research: + +* This repository (`+{{OWNER}}/{{REPO}}+`) and all its code +* Official releases and packages published from this repository +* Documentation that could lead to security issues +* Build and deployment configurations in this repository +* Dependencies (report here, we’ll coordinate with upstream) + +==== Out of Scope ❌ + +The following are *not* in scope: + +* Third-party services we integrate with (report directly to them) +* Social engineering attacks against maintainers +* Physical security +* Denial of service attacks against production infrastructure +* Spam, phishing, or other non-technical attacks +* Issues already reported or publicly known +* Theoretical vulnerabilities without proof of concept + +==== Qualifying Vulnerabilities + +We’re particularly interested in: + +* Remote code execution +* SQL injection, command injection, code injection +* Authentication/authorisation bypass +* Cross-site scripting (XSS) and cross-site request forgery (CSRF) +* Server-side request forgery (SSRF) +* Path traversal / local file inclusion +* Information disclosure (credentials, PII, secrets) +* Cryptographic weaknesses +* Deserialisation vulnerabilities +* Memory safety issues (buffer overflows, use-after-free, etc.) +* Supply chain vulnerabilities (dependency confusion, etc.) +* Significant logic flaws + +==== Non-Qualifying Issues + +The following generally do not qualify as security vulnerabilities: + +* Missing security headers on non-sensitive pages +* Clickjacking on pages without sensitive actions +* Self-XSS (requires victim to paste code) +* Missing rate limiting (unless it enables a specific attack) +* Username/email enumeration (unless high-risk context) +* Missing cookie flags on non-sensitive cookies +* Software version disclosure +* Verbose error messages (unless exposing secrets) +* Best practice deviations without demonstrable impact + +''''' + +=== Safe Harbour + +We support security research conducted in good faith. + +==== Our Promise + +If you conduct security research in accordance with this policy: + +* ✅ We will not initiate legal action against you +* ✅ We will not report your activity to law enforcement +* ✅ We will work with you in good faith to resolve issues +* ✅ We consider your research authorised under the Computer Fraud and +Abuse Act (CFAA), UK Computer Misuse Act, and similar laws +* ✅ We waive any potential claim against you for circumvention of +security controls + +==== Good Faith Requirements + +To qualify for safe harbour, you must: + +* Comply with this security policy +* Report vulnerabilities promptly +* Avoid privacy violations (do not access others’ data) +* Avoid service degradation (no destructive testing) +* Not exploit vulnerabilities beyond proof-of-concept +* Not use vulnerabilities for profit (beyond bug bounties where offered) + +____ +*⚠️ Important:* This safe harbour does not extend to third-party +systems. Always check their policies before testing. +____ + +''''' + +=== Recognition + +We believe in recognising security researchers who help us improve. + +==== Hall of Fame + +Researchers who report valid vulnerabilities will be acknowledged in our +link:SECURITY-ACKNOWLEDGMENTS.md[Security Acknowledgments] (unless they +prefer anonymity). + +Recognition includes: + +* Your name (or chosen alias) +* Link to your website/profile (optional) +* Brief description of the vulnerability class +* Date of report + +==== What We Offer + +* ✅ Public credit in security advisories +* ✅ Acknowledgment in release notes +* ✅ Entry in our Hall of Fame +* ✅ Reference/recommendation letter upon request (for significant +findings) + +==== What We Don’t Currently Offer + +* ❌ Monetary bug bounties +* ❌ Hardware or swag +* ❌ Paid security research contracts + +____ +*Note:* We’re a community project with limited resources. Your +contributions help everyone who uses this software. +____ + +''''' + +=== Security Updates + +==== Receiving Updates + +To stay informed about security updates: + +* *Watch this repository*: Click "`Watch`" → "`Custom`" → Select +"`Security alerts`" +* *GitHub Security Advisories*: Published at +https://github.com/%7B%7BOWNER%7D%7D/%7B%7BREPO%7D%7D/security/advisories[Security +Advisories] +* *Release notes*: Security fixes noted in link:CHANGELOG.md[CHANGELOG] + +==== Update Policy + +[cols=",",options="header",] +|=== +|Severity |Response +|*Critical/High* |Patch release as soon as fix is ready +|*Medium* |Included in next scheduled release (or earlier) +|*Low* |Included in next scheduled release +|=== + +==== Supported Versions + +[cols=",,",options="header",] +|=== +|Version |Supported |Notes +|`+main+` branch |✅ Yes |Latest development +|Latest release |✅ Yes |Current stable +|Previous minor release |✅ Yes |Security fixes backported +|Older versions |❌ No |Please upgrade +|=== + +''''' + +=== Security Best Practices + +When using \{\{PROJECT_NAME}}, we recommend: + +==== General + +* Keep dependencies up to date +* Use the latest stable release +* Subscribe to security notifications +* Review configuration against security documentation +* Follow principle of least privilege + +==== For Contributors + +* Never commit secrets, credentials, or API keys +* Use signed commits (`+git config commit.gpgsign true+`) +* Review dependencies before adding them +* Run security linters locally before pushing +* Report any concerns about existing code + +''''' + +=== Additional Resources + +* link:%7B%7BPGP_KEY_URL%7D%7D[Our PGP Public Key] +* https://github.com/%7B%7BOWNER%7D%7D/%7B%7BREPO%7D%7D/security/advisories[Security +Advisories] +* link:CHANGELOG.md[Changelog] +* link:CONTRIBUTING.md[Contributing Guidelines] +* https://cve.mitre.org/[CVE Database] +* https://www.first.org/cvss/calculator/3.1[CVSS Calculator] + +''''' + +=== Contact + +[width="100%",cols="50%,50%",options="header",] +|=== +|Purpose |Contact +|*Security issues* +|https://github.com/%7B%7BOWNER%7D%7D/%7B%7BREPO%7D%7D/security/advisories/new[Report +via GitHub] or \{\{SECURITY_EMAIL}} + +|*General questions* +|https://github.com/%7B%7BOWNER%7D%7D/%7B%7BREPO%7D%7D/discussions[GitHub +Discussions] + +|*Other enquiries* |See link:README.md[README] for contact information +|=== + +''''' + +=== Policy Changes + +This security policy may be updated from time to time. Significant +changes will be: + +* Committed to this repository with a clear commit message +* Noted in the changelog +* Announced via GitHub Discussions (for major changes) + +''''' + +_Thank you for helping keep \{\{PROJECT_NAME}} and its users safe._ 🛡️ + +''''' + +Last updated: \{\{CURRENT_YEAR}} · Policy version: 1.0.0 diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 7dd7b29e..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,406 +0,0 @@ -# Security Policy - - - -We take security seriously. We appreciate your efforts to responsibly disclose vulnerabilities and will make every effort to acknowledge your contributions. - -## Table of Contents - -- [Reporting a Vulnerability](#reporting-a-vulnerability) -- [What to Include](#what-to-include) -- [Response Timeline](#response-timeline) -- [Disclosure Policy](#disclosure-policy) -- [Scope](#scope) -- [Safe Harbour](#safe-harbour) -- [Recognition](#recognition) -- [Security Updates](#security-updates) -- [Security Best Practices](#security-best-practices) - ---- - -## Reporting a Vulnerability - -### Preferred Method: GitHub Security Advisories - -The preferred method for reporting security vulnerabilities is through GitHub's Security Advisory feature: - -1. Navigate to [Report a Vulnerability](https://github.com/{{OWNER}}/{{REPO}}/security/advisories/new) -2. Click **"Report a vulnerability"** -3. Complete the form with as much detail as possible -4. Submit — we'll receive a private notification - -This method ensures: - -- End-to-end encryption of your report -- Private discussion space for collaboration -- Coordinated disclosure tooling -- Automatic credit when the advisory is published - -### Alternative: Encrypted Email - -If you cannot use GitHub Security Advisories, you may email us directly: - -| | | -|---|---| -| **Email** | {{SECURITY_EMAIL}} | -| **PGP Key** | [Download Public Key]({{PGP_KEY_URL}}) | -| **Fingerprint** | `{{PGP_FINGERPRINT}}` | - -```bash -# Import our PGP key -curl -sSL {{PGP_KEY_URL}} | gpg --import - -# Verify fingerprint -gpg --fingerprint {{SECURITY_EMAIL}} - -# Encrypt your report -gpg --armor --encrypt --recipient {{SECURITY_EMAIL}} report.txt -``` - -> **⚠️ Important:** Do not report security vulnerabilities through public GitHub issues, pull requests, discussions, or social media. - ---- - -## What to Include - -A good vulnerability report helps us understand and reproduce the issue quickly. - -### Required Information - -- **Description**: Clear explanation of the vulnerability -- **Impact**: What an attacker could achieve (confidentiality, integrity, availability) -- **Affected versions**: Which versions/commits are affected -- **Reproduction steps**: Detailed steps to reproduce the issue - -### Helpful Additional Information - -- **Proof of concept**: Code, scripts, or screenshots demonstrating the vulnerability -- **Attack scenario**: Realistic attack scenario showing exploitability -- **CVSS score**: Your assessment of severity (use [CVSS 3.1 Calculator](https://www.first.org/cvss/calculator/3.1)) -- **CWE ID**: Common Weakness Enumeration identifier if known -- **Suggested fix**: If you have ideas for remediation -- **References**: Links to related vulnerabilities, research, or advisories - -### Example Report Structure - -```markdown -## Summary -[One-sentence description of the vulnerability] - -## Vulnerability Type -[e.g., SQL Injection, XSS, SSRF, Path Traversal, etc.] - -## Affected Component -[File path, function name, API endpoint, etc.] - -## Affected Versions -[Version range or specific commits] - -## Severity Assessment -- CVSS 3.1 Score: [X.X] -- CVSS Vector: [CVSS:3.1/AV:X/AC:X/PR:X/UI:X/S:X/C:X/I:X/A:X] - -## Description -[Detailed technical description] - -## Steps to Reproduce -1. [First step] -2. [Second step] -3. [...] - -## Proof of Concept -[Code, curl commands, screenshots, etc.] - -## Impact -[What can an attacker achieve?] - -## Suggested Remediation -[Optional: your ideas for fixing] - -## References -[Links to related issues, CVEs, research] -``` - ---- - -## Response Timeline - -We commit to the following response times: - -| Stage | Timeframe | Description | -|-------|-----------|-------------| -| **Initial Response** | 48 hours | We acknowledge receipt and confirm we're investigating | -| **Triage** | 7 days | We assess severity, confirm the vulnerability, and estimate timeline | -| **Status Update** | Every 7 days | Regular updates on remediation progress | -| **Resolution** | 90 days | Target for fix development and release (complex issues may take longer) | -| **Disclosure** | 90 days | Public disclosure after fix is available (coordinated with you) | - -> **Note:** These are targets, not guarantees. Complex vulnerabilities may require more time. We'll communicate openly about any delays. - ---- - -## Disclosure Policy - -We follow **coordinated disclosure** (also known as responsible disclosure): - -1. **You report** the vulnerability privately -2. **We acknowledge** and begin investigation -3. **We develop** a fix and prepare a release -4. **We coordinate** disclosure timing with you -5. **We publish** security advisory and fix simultaneously -6. **You may publish** your research after disclosure - -### Our Commitments - -- We will not take legal action against researchers who follow this policy -- We will work with you to understand and resolve the issue -- We will credit you in the security advisory (unless you prefer anonymity) -- We will notify you before public disclosure -- We will publish advisories with sufficient detail for users to assess risk - -### Your Commitments - -- Report vulnerabilities promptly after discovery -- Give us reasonable time to address the issue before disclosure -- Do not access, modify, or delete data beyond what's necessary to demonstrate the vulnerability -- Do not degrade service availability (no DoS testing on production) -- Do not share vulnerability details with others until coordinated disclosure - -### Disclosure Timeline - -``` -Day 0 You report vulnerability -Day 1-2 We acknowledge receipt -Day 7 We confirm vulnerability and share initial assessment -Day 7-90 We develop and test fix -Day 90 Coordinated public disclosure - (earlier if fix is ready; later by mutual agreement) -``` - -If we cannot reach agreement on disclosure timing, we default to 90 days from your initial report. - ---- - -## Scope - -### In Scope ✅ - -The following are within scope for security research: - -- This repository (`{{OWNER}}/{{REPO}}`) and all its code -- Official releases and packages published from this repository -- Documentation that could lead to security issues -- Build and deployment configurations in this repository -- Dependencies (report here, we'll coordinate with upstream) - -### Out of Scope ❌ - -The following are **not** in scope: - -- Third-party services we integrate with (report directly to them) -- Social engineering attacks against maintainers -- Physical security -- Denial of service attacks against production infrastructure -- Spam, phishing, or other non-technical attacks -- Issues already reported or publicly known -- Theoretical vulnerabilities without proof of concept - -### Qualifying Vulnerabilities - -We're particularly interested in: - -- Remote code execution -- SQL injection, command injection, code injection -- Authentication/authorisation bypass -- Cross-site scripting (XSS) and cross-site request forgery (CSRF) -- Server-side request forgery (SSRF) -- Path traversal / local file inclusion -- Information disclosure (credentials, PII, secrets) -- Cryptographic weaknesses -- Deserialisation vulnerabilities -- Memory safety issues (buffer overflows, use-after-free, etc.) -- Supply chain vulnerabilities (dependency confusion, etc.) -- Significant logic flaws - -### Non-Qualifying Issues - -The following generally do not qualify as security vulnerabilities: - -- Missing security headers on non-sensitive pages -- Clickjacking on pages without sensitive actions -- Self-XSS (requires victim to paste code) -- Missing rate limiting (unless it enables a specific attack) -- Username/email enumeration (unless high-risk context) -- Missing cookie flags on non-sensitive cookies -- Software version disclosure -- Verbose error messages (unless exposing secrets) -- Best practice deviations without demonstrable impact - ---- - -## Safe Harbour - -We support security research conducted in good faith. - -### Our Promise - -If you conduct security research in accordance with this policy: - -- ✅ We will not initiate legal action against you -- ✅ We will not report your activity to law enforcement -- ✅ We will work with you in good faith to resolve issues -- ✅ We consider your research authorised under the Computer Fraud and Abuse Act (CFAA), UK Computer Misuse Act, and similar laws -- ✅ We waive any potential claim against you for circumvention of security controls - -### Good Faith Requirements - -To qualify for safe harbour, you must: - -- Comply with this security policy -- Report vulnerabilities promptly -- Avoid privacy violations (do not access others' data) -- Avoid service degradation (no destructive testing) -- Not exploit vulnerabilities beyond proof-of-concept -- Not use vulnerabilities for profit (beyond bug bounties where offered) - -> **⚠️ Important:** This safe harbour does not extend to third-party systems. Always check their policies before testing. - ---- - -## Recognition - -We believe in recognising security researchers who help us improve. - -### Hall of Fame - -Researchers who report valid vulnerabilities will be acknowledged in our [Security Acknowledgments](SECURITY-ACKNOWLEDGMENTS.md) (unless they prefer anonymity). - -Recognition includes: - -- Your name (or chosen alias) -- Link to your website/profile (optional) -- Brief description of the vulnerability class -- Date of report - -### What We Offer - -- ✅ Public credit in security advisories -- ✅ Acknowledgment in release notes -- ✅ Entry in our Hall of Fame -- ✅ Reference/recommendation letter upon request (for significant findings) - -### What We Don't Currently Offer - -- ❌ Monetary bug bounties -- ❌ Hardware or swag -- ❌ Paid security research contracts - -> **Note:** We're a community project with limited resources. Your contributions help everyone who uses this software. - ---- - -## Security Updates - -### Receiving Updates - -To stay informed about security updates: - -- **Watch this repository**: Click "Watch" → "Custom" → Select "Security alerts" -- **GitHub Security Advisories**: Published at [Security Advisories](https://github.com/{{OWNER}}/{{REPO}}/security/advisories) -- **Release notes**: Security fixes noted in [CHANGELOG](CHANGELOG.md) - -### Update Policy - -| Severity | Response | -|----------|----------| -| **Critical/High** | Patch release as soon as fix is ready | -| **Medium** | Included in next scheduled release (or earlier) | -| **Low** | Included in next scheduled release | - -### Supported Versions - - - -| Version | Supported | Notes | -|---------|-----------|-------| -| `main` branch | ✅ Yes | Latest development | -| Latest release | ✅ Yes | Current stable | -| Previous minor release | ✅ Yes | Security fixes backported | -| Older versions | ❌ No | Please upgrade | - ---- - -## Security Best Practices - -When using {{PROJECT_NAME}}, we recommend: - -### General - -- Keep dependencies up to date -- Use the latest stable release -- Subscribe to security notifications -- Review configuration against security documentation -- Follow principle of least privilege - -### For Contributors - -- Never commit secrets, credentials, or API keys -- Use signed commits (`git config commit.gpgsign true`) -- Review dependencies before adding them -- Run security linters locally before pushing -- Report any concerns about existing code - ---- - -## Additional Resources - -- [Our PGP Public Key]({{PGP_KEY_URL}}) -- [Security Advisories](https://github.com/{{OWNER}}/{{REPO}}/security/advisories) -- [Changelog](CHANGELOG.md) -- [Contributing Guidelines](CONTRIBUTING.md) -- [CVE Database](https://cve.mitre.org/) -- [CVSS Calculator](https://www.first.org/cvss/calculator/3.1) - ---- - -## Contact - -| Purpose | Contact | -|---------|---------| -| **Security issues** | [Report via GitHub](https://github.com/{{OWNER}}/{{REPO}}/security/advisories/new) or {{SECURITY_EMAIL}} | -| **General questions** | [GitHub Discussions](https://github.com/{{OWNER}}/{{REPO}}/discussions) | -| **Other enquiries** | See [README](README.md) for contact information | - ---- - -## Policy Changes - -This security policy may be updated from time to time. Significant changes will be: - -- Committed to this repository with a clear commit message -- Noted in the changelog -- Announced via GitHub Discussions (for major changes) - ---- - -*Thank you for helping keep {{PROJECT_NAME}} and its users safe.* 🛡️ - ---- - -Last updated: {{CURRENT_YEAR}} · Policy version: 1.0.0 diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc new file mode 100644 index 00000000..8eaa8bdc --- /dev/null +++ b/TEST-NEEDS.adoc @@ -0,0 +1,140 @@ +== TEST-NEEDS: affinescript + +Per-repo instance of the estate CRG taxonomy +(`+standards/testing-and-benchmarking/TESTING-TAXONOMY.adoc+`). +Categories + aspects + the bench model are the canonical ones; the full +mapping + risk/interop ledgers live in +`+docs/TESTING-AND-BENCH-MATRIX.adoc+`. This file is the *blitz ledger*: +measured status + numbers, honestly marked. + +*Blitz date:* 2026-06-16. *Self-assessed CRG grade: D, approaching C* — +the C-tier E2E/REG/PRF/CTR(partial) are present; the gaps to C are REF + +the B-tier PBT/FUZ/MUT and baselined benches (below). + +=== Scale + +[cols=",",] +|=== +|Compiler source |*36,178* LOC OCaml (`+lib/+`) +|Compiler binary |10.4 MB +|Backends wired (suffix dispatch) |*48* +|Alcotest gate |*477 tests, 0 fail* (`+dune runtest+`) +|Conformance fixtures |24 · e2e fixtures 102 +|=== + +=== Test categories (16) — measured status + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Category |Status |Count / where +|*UT* Unit |PASS |within the 477 alcotest (`+test/test_.ml+`): +lexer, effect_sites, qualified_paths, module_mut, *solo_cesk (19, VM +M1)*, … + +|*P2P* Point-to-point |PASS (1 seam) |`+typed-wasm-validate+` — +AffineScript producer ↔ Rust `+tw-verify+`, bit-exact (2/2). GAP: +parser↔typecheck, typecheck↔codegen as named P2P + +|*E2E* End-to-end |PASS |`+test_e2e+` + `+wasm-validate+` (19+2), +`+native-run+`, `+riscv-run-validate+` (4), `+coprocessor-validate+` +(12) + +|*BLD* Build |PASS |`+just build+` / `+dune build+` (CI) + +|*EXE* Execution/runtime |PASS |interp + native exec + qemu-riscv64 + VM +M1 CESK execution + +|*REF* Reflexive |*GAP* |no `+just doctor+`/self-check (candidate: a +`+selfcheck+` chaining the gates) + +|*LCY* Lifecycle |partial |compile-time via borrow checker; runtime via +VM M1 affine enforcement + +|*SMK* Smoke |PASS |`+run_codegen_wasm/deno_tests.sh+`, deno-test, +vscode host + +|*PBT* Property-based |*GAP (priority)* |only a deterministic seed in +test_solo_cesk; need qcheck 1000+ (semiring laws, lex→parse→pp +round-trip, codegen determinism) + +|*MUT* Mutation |*GAP* |no `+cargo-mutants+` equivalent for OCaml + +|*FUZ* Fuzz |*GAP (priority)* |none — lexer/parser + codegen-emission +are the boundaries to fuzz (crowbar/AFL). No placeholders. + +|*CTR* Contract/invariant |partial |`+just guard+` (doc-truthing), +`+proof-check-all+` (the proofs are invariants), VM affine enforcement + +|*REG* Regression |PASS |`+test/e2e/fixtures/+` + the +deferred-regression discipline (STATE.a2ml) + +|*CHS* Chaos |N-A |compiler, not a service (parser error-recovery is the +nearest analog) + +|*CMP* Compatibility |partial |typed-wasm v1 carrier pinned. GAP: +version-matrix + +|*PRF* Proof regression |PASS |`+proof-check-all+`: Idris2 Solo + Lean +tropical + Agda echo, *green*; dangerous-primitive scan +|=== + +=== Aspects (14) — covered: DEP, IOP, SAF, FUN, PRT, SEC(partial), PER(partial). GAP: ACC, MNT(partial), OBS(partial). N-A: PRI. (Detail in the matrix.) + +=== Performance (blitz, best-of-N, this host) + +[width="100%",cols="50%,50%",options="header",] +|=== +|Measure |Number +|Compile hello → wasm / .ll / js / c / julia |*2–3 ms* each (312 / 1806 +/ 1272 / 1445 / 192 B) + +|Compile comprehensive_test (36 ln) → wasm |3 ms (463 B) + +|Native exec (x86, hello) |*1 ms* + +|Native exec (riscv64 under qemu) |11 ms (emulation) + +|Proof check — Idris2 Solo |365 ms + +|Proof check — Lean tropical |185 ms + +|Proof check — *Agda echo* |*47.8 s* (≈all of `+proof-check-all+`’s 47 s +— cubical + 22 boundary certs) + +|Gates (each) |wasm 138 ms · coprocessor 47 ms · android 46 ms · +typed-wasm 15 ms · riscv-run 102 ms +|=== + +*Benches: partial (2026-06-16 — harness fixed).* `+just bench+` now runs ++ prints real numbers (the alcotest wrapper was swallowing stdout; the +recipe’s second command was broken — both fixed). Phase numbers: lex +~7–10 M tok/s; parse ~0.02 ms/iter; typecheck ~0.01 ms/iter; codegen +~0.01 ms/iter (small inputs). Added: *`+bench_scaling+`* (generated +N-function programs) and *`+bench_vm+`* (Solo CESK step-rate, ~3.5e7 +steps/s, exactly linear 3n+1 steps). + +⚠ *FINDING (issue-draft 07):* the scaling bench shows compile time is +*super-linear ≈O(n²)* — 4.4 µs/func at n=100 but 80 µs/func at n=5000 +(5× input → ~32× time). Invisible on the 114-line corpus. Localise +(likely `+resolve.ml+`/`+codegen.ml+` per-item full scan) and fix to +flat-µs/func. + +Still GAP: Six-Sigma baselining; per-backend _runtime_ bench (real +workloads — see the planned LP/NLP suite); promotion to a gating +threshold. + +=== Remaining gaps (priority order) + +[arabic] +. *Benches → metric-emitting + baselined* (fix the recipe; emit ns/op; +Six-Sigma baseline; per-backend runtime; VM step-rate; large-input +fixtures). +. *PBT* (qcheck): semiring laws, round-trip, codegen determinism — 1000+ +cases. +. *FUZ* (crowbar/AFL on lexer/parser + codegen boundary; cargo-fuzz on +the runtime). +. *Symbol-audit* per backend (`+nm -D+`/`+wasm-tools+`) — the proven +interop guard. +. *REF* (`+just selfcheck+`) + *ACC* (error-message clarity / CLI a11y). +. Port proven’s `+tests/e2e.sh+` 5-section proof-chain harness (folds +the gates into one). diff --git a/TEST-NEEDS.md b/TEST-NEEDS.md deleted file mode 100644 index 6ee4e30e..00000000 --- a/TEST-NEEDS.md +++ /dev/null @@ -1,86 +0,0 @@ - - -# TEST-NEEDS: affinescript - -Per-repo instance of the estate CRG taxonomy -(`standards/testing-and-benchmarking/TESTING-TAXONOMY.adoc`). Categories + -aspects + the bench model are the canonical ones; the full mapping + -risk/interop ledgers live in `docs/TESTING-AND-BENCH-MATRIX.adoc`. This file is -the **blitz ledger**: measured status + numbers, honestly marked. - -**Blitz date:** 2026-06-16. **Self-assessed CRG grade: D, approaching C** — the -C-tier E2E/REG/PRF/CTR(partial) are present; the gaps to C are REF + the B-tier -PBT/FUZ/MUT and baselined benches (below). - -## Scale - -| | | -|---|---| -| Compiler source | **36,178** LOC OCaml (`lib/`) | -| Compiler binary | 10.4 MB | -| Backends wired (suffix dispatch) | **48** | -| Alcotest gate | **477 tests, 0 fail** (`dune runtest`) | -| Conformance fixtures | 24 · e2e fixtures 102 | - -## Test categories (16) — measured status - -| Category | Status | Count / where | -|---|---|---| -| **UT** Unit | PASS | within the 477 alcotest (`test/test_.ml`): lexer, effect_sites, qualified_paths, module_mut, **solo_cesk (19, VM M1)**, … | -| **P2P** Point-to-point | PASS (1 seam) | `typed-wasm-validate` — AffineScript producer ↔ Rust `tw-verify`, bit-exact (2/2). GAP: parser↔typecheck, typecheck↔codegen as named P2P | -| **E2E** End-to-end | PASS | `test_e2e` + `wasm-validate` (19+2), `native-run`, `riscv-run-validate` (4), `coprocessor-validate` (12) | -| **BLD** Build | PASS | `just build` / `dune build` (CI) | -| **EXE** Execution/runtime | PASS | interp + native exec + qemu-riscv64 + VM M1 CESK execution | -| **REF** Reflexive | **GAP** | no `just doctor`/self-check (candidate: a `selfcheck` chaining the gates) | -| **LCY** Lifecycle | partial | compile-time via borrow checker; runtime via VM M1 affine enforcement | -| **SMK** Smoke | PASS | `run_codegen_wasm/deno_tests.sh`, deno-test, vscode host | -| **PBT** Property-based | **GAP (priority)** | only a deterministic seed in test_solo_cesk; need qcheck 1000+ (semiring laws, lex→parse→pp round-trip, codegen determinism) | -| **MUT** Mutation | **GAP** | no `cargo-mutants` equivalent for OCaml | -| **FUZ** Fuzz | **GAP (priority)** | none — lexer/parser + codegen-emission are the boundaries to fuzz (crowbar/AFL). No placeholders. | -| **CTR** Contract/invariant | partial | `just guard` (doc-truthing), `proof-check-all` (the proofs are invariants), VM affine enforcement | -| **REG** Regression | PASS | `test/e2e/fixtures/` + the deferred-regression discipline (STATE.a2ml) | -| **CHS** Chaos | N-A | compiler, not a service (parser error-recovery is the nearest analog) | -| **CMP** Compatibility | partial | typed-wasm v1 carrier pinned. GAP: version-matrix | -| **PRF** Proof regression | PASS | `proof-check-all`: Idris2 Solo + Lean tropical + Agda echo, **green**; dangerous-primitive scan | - -## Aspects (14) — covered: DEP, IOP, SAF, FUN, PRT, SEC(partial), PER(partial). GAP: ACC, MNT(partial), OBS(partial). N-A: PRI. (Detail in the matrix.) - -## Performance (blitz, best-of-N, this host) - -| Measure | Number | -|---|---| -| Compile hello → wasm / .ll / js / c / julia | **2–3 ms** each (312 / 1806 / 1272 / 1445 / 192 B) | -| Compile comprehensive_test (36 ln) → wasm | 3 ms (463 B) | -| Native exec (x86, hello) | **1 ms** | -| Native exec (riscv64 under qemu) | 11 ms (emulation) | -| Proof check — Idris2 Solo | 365 ms | -| Proof check — Lean tropical | 185 ms | -| Proof check — **Agda echo** | **47.8 s** (≈all of `proof-check-all`'s 47 s — cubical + 22 boundary certs) | -| Gates (each) | wasm 138 ms · coprocessor 47 ms · android 46 ms · typed-wasm 15 ms · riscv-run 102 ms | - -**Benches: partial (2026-06-16 — harness fixed).** `just bench` now runs + -prints real numbers (the alcotest wrapper was swallowing stdout; the recipe's -second command was broken — both fixed). Phase numbers: lex ~7–10 M tok/s; parse -~0.02 ms/iter; typecheck ~0.01 ms/iter; codegen ~0.01 ms/iter (small inputs). -Added: **`bench_scaling`** (generated N-function programs) and **`bench_vm`** -(Solo CESK step-rate, ~3.5e7 steps/s, exactly linear 3n+1 steps). - -⚠ **FINDING (issue-draft 07):** the scaling bench shows compile time is -**super-linear ≈O(n²)** — 4.4 µs/func at n=100 but 80 µs/func at n=5000 (5× -input → ~32× time). Invisible on the 114-line corpus. Localise (likely -`resolve.ml`/`codegen.ml` per-item full scan) and fix to flat-µs/func. - -Still GAP: Six-Sigma baselining; per-backend *runtime* bench (real workloads — -see the planned LP/NLP suite); promotion to a gating threshold. - -## Remaining gaps (priority order) - -1. **Benches → metric-emitting + baselined** (fix the recipe; emit ns/op; Six-Sigma baseline; per-backend runtime; VM step-rate; large-input fixtures). -2. **PBT** (qcheck): semiring laws, round-trip, codegen determinism — 1000+ cases. -3. **FUZ** (crowbar/AFL on lexer/parser + codegen boundary; cargo-fuzz on the runtime). -4. **Symbol-audit** per backend (`nm -D`/`wasm-tools`) — the proven interop guard. -5. **REF** (`just selfcheck`) + **ACC** (error-message clarity / CLI a11y). -6. Port proven's `tests/e2e.sh` 5-section proof-chain harness (folds the gates into one). diff --git a/conformance/README.adoc b/conformance/README.adoc new file mode 100644 index 00000000..c7c7738b --- /dev/null +++ b/conformance/README.adoc @@ -0,0 +1,65 @@ +== AffineScript Conformance Test Suite + +This directory contains the conformance test corpus for AffineScript. +The tests here are *binding* - changes to expected outputs require +explicit justification. + +=== Structure + +.... +conformance/ +├── valid/ # Programs that must parse successfully (exit 0) +│ ├── *.affine # Source files +│ └── *.expected # Expected parser output +├── invalid/ # Programs that must fail with diagnostics (exit non-zero) +│ ├── *.affine # Source files +│ └── *.expected # Expected error diagnostics +└── README.md # This file +.... + +=== Test Methodology + +==== Valid Programs + +* Must parse without error +* CLI command: `+affinescript parse +` +* Expected exit code: 0 +* Output must match `+.expected+` file exactly + +==== Invalid Programs + +* Must produce a parse/lex error +* CLI command: `+affinescript parse +` or +`+affinescript lex +` +* Expected exit code: non-zero (1 for parse errors) +* Error diagnostic must match `+.expected+` file pattern + +=== Running Tests + +[source,bash] +---- +# Run all conformance tests +just conformance + +# Or directly with dune +dune runtest conformance +---- + +=== Adding New Tests + +[arabic] +. Add `+.affine+` source file to `+valid/+` or `+invalid/+` +. Run the compiler to generate expected output +. Review and save as `+.expected+` file +. Commit both files together + +=== Versioning + +* Format: `+conformance-vN.M+` +* Breaking changes (modified .expected): increment major version +* New tests only: increment minor version + +=== F0 Requirements + +Per the scope arrest directive: - Minimum 10 valid programs - Minimum 10 +invalid programs - Stable exit-code contract - Deterministic diagnostics diff --git a/conformance/README.md b/conformance/README.md deleted file mode 100644 index 167eb033..00000000 --- a/conformance/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# AffineScript Conformance Test Suite - -This directory contains the conformance test corpus for AffineScript. -The tests here are **binding** - changes to expected outputs require explicit justification. - -## Structure - -``` -conformance/ -├── valid/ # Programs that must parse successfully (exit 0) -│ ├── *.affine # Source files -│ └── *.expected # Expected parser output -├── invalid/ # Programs that must fail with diagnostics (exit non-zero) -│ ├── *.affine # Source files -│ └── *.expected # Expected error diagnostics -└── README.md # This file -``` - -## Test Methodology - -### Valid Programs -- Must parse without error -- CLI command: `affinescript parse ` -- Expected exit code: 0 -- Output must match `.expected` file exactly - -### Invalid Programs -- Must produce a parse/lex error -- CLI command: `affinescript parse ` or `affinescript lex ` -- Expected exit code: non-zero (1 for parse errors) -- Error diagnostic must match `.expected` file pattern - -## Running Tests - -```bash -# Run all conformance tests -just conformance - -# Or directly with dune -dune runtest conformance -``` - -## Adding New Tests - -1. Add `.affine` source file to `valid/` or `invalid/` -2. Run the compiler to generate expected output -3. Review and save as `.expected` file -4. Commit both files together - -## Versioning - -- Format: `conformance-vN.M` -- Breaking changes (modified .expected): increment major version -- New tests only: increment minor version - -## F0 Requirements - -Per the scope arrest directive: -- Minimum 10 valid programs -- Minimum 10 invalid programs -- Stable exit-code contract -- Deterministic diagnostics diff --git a/docs/academic/proofs/db-theory-2-transaction-safety.adoc b/docs/academic/proofs/db-theory-2-transaction-safety.adoc index 4f89e2b8..7f8a6827 100644 --- a/docs/academic/proofs/db-theory-2-transaction-safety.adoc +++ b/docs/academic/proofs/db-theory-2-transaction-safety.adoc @@ -1,60 +1,59 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// SPDX-FileCopyrightText: 2024-2026 hyperpolymath (Jonathan D.A. Jewell ) -= DB-Theory ++#++2 — Transaction Safety (Rollback-Discards-Writes) - -*Status*: Carrier wired (`stdlib/Transaction.affine`, codegen, Deno-ESM -smoke); formal proof obligation *pending upstream against -`hyperpolymath/echo-types`*. - -== 1. The obligation - -Let `Tx` be the opaque affine handle returned by `tx++_++begin(d: Db)` -in `stdlib/Transaction.affine`. Let `t : Tx` be such a handle, and let -`σ₀` be the database state immediately before `tx++_++begin(d)`. Let `W` -be the multiset of mutating SQL statements (`INSERT`, `UPDATE`, -`DELETE`, schema mutations) executed against `d` (or any handle aliased -through `tx++_++db(t)`) between `tx++_++begin(d)` and the consumption of -`t`. - -*Safety property ++#++DB-2.1 (rollback-discards-writes)*: if `t` is -consumed by `tx++_++rollback(t)`, then for every query `q` issued -against `d` _after_ the rollback, the answer to `q` is precisely the -answer it would have had over `σ₀` (i.e. as if `W` had never happened). - -*Safety property ++#++DB-2.2 (commit-promotes-writes)*: if `t` is -consumed by `tx++_++commit(t)`, then for every query `q` issued after -the commit, the answer is the same as serially applying `W` to `σ₀`. - -*Safety property ++#++DB-2.3 (savepoint locality)*: if a savepoint `s` -is opened with `tx++_++savepoint(t, s)`, mutations `W++_++s` are issued, -and then `tx++_++rollback++_++to(t, s)` is called, the database state -mid-transaction reverts to the state at `tx++_++savepoint(t, s)` while +== DB-Theory #2 — Transaction Safety (Rollback-Discards-Writes) + +*Status*: Carrier wired (`+stdlib/Transaction.affine+`, codegen, +Deno-ESM smoke); formal proof obligation *pending upstream against +`+hyperpolymath/echo-types+`*. + +=== 1. The obligation + +Let `+Tx+` be the opaque affine handle returned by `+tx_begin(d: Db)+` +in `+stdlib/Transaction.affine+`. Let `+t : Tx+` be such a handle, and +let `+σ₀+` be the database state immediately before `+tx_begin(d)+`. Let +`+W+` be the multiset of mutating SQL statements (`+INSERT+`, +`+UPDATE+`, `+DELETE+`, schema mutations) executed against `+d+` (or any +handle aliased through `+tx_db(t)+`) between `+tx_begin(d)+` and the +consumption of `+t+`. + +*Safety property #DB-2.1 (rollback-discards-writes)*: if `+t+` is +consumed by `+tx_rollback(t)+`, then for every query `+q+` issued +against `+d+` _after_ the rollback, the answer to `+q+` is precisely the +answer it would have had over `+σ₀+` (i.e. as if `+W+` had never +happened). + +*Safety property #DB-2.2 (commit-promotes-writes)*: if `+t+` is consumed +by `+tx_commit(t)+`, then for every query `+q+` issued after the commit, +the answer is the same as serially applying `+W+` to `+σ₀+`. + +*Safety property #DB-2.3 (savepoint locality)*: if a savepoint `+s+` is +opened with `+tx_savepoint(t, s)+`, mutations `+W_s+` are issued, and +then `+tx_rollback_to(t, s)+` is called, the database state +mid-transaction reverts to the state at `+tx_savepoint(t, s)+` while leaving the outer transaction live. -== 2. Echo-types audit (2026-06-01) +=== 2. Echo-types audit (2026-06-01) Per owner directive, every proof in AffineScript must first audit -`hyperpolymath/echo-types`, reuse if applicable, extend upstream *with +`+hyperpolymath/echo-types+`, reuse if applicable, extend upstream *with proofs* if not, then cross-document. *Audit finding* (full report under -`tasklist/sub-agent reports/2026-06-01-transaction-echo-audit`): +`+tasklist/sub-agent reports/2026-06-01-transaction-echo-audit+`): echo-types has no Transaction-specific instantiation today, but carries three reusable abstractions: [arabic] -. `EchoLinear.LEcho` {plus} the `weaken : LEcho linear → LEcho affine` -collapse map, with `no-section-weaken` proving no recovery after +. `+EchoLinear.LEcho+` + the `+weaken : LEcho linear → LEcho affine+` +collapse map, with `+no-section-weaken+` proving no recovery after weakening. -. `EchoSecurity.Security` record {plus} `exit-collapses-at` / -`audit-no-recovery-at` — the boundary-collapse template structurally +. `+EchoSecurity.Security+` record + `+exit-collapses-at+` / +`+audit-no-recovery-at+` — the boundary-collapse template structurally mirrors rollback-discards-writes. -. `EchoNoSectionGeneric.no-section-of-collapsing-map` — the generic +. `+EchoNoSectionGeneric.no-section-of-collapsing-map+` — the generic lemma the Transaction proof reduces to. -== 3. Proposed upstream extension +=== 3. Proposed upstream extension -A new module `TransactionMutations.agda` in echo-types, structured as +A new module `+TransactionMutations.agda+` in echo-types, structured as follows: [source,agda] @@ -92,34 +91,35 @@ rollback-discards-writes ws = no-section-of-collapsing-map _ ---- The Security-record instance lives in a sibling -`TransactionSecurity.agda`, parametrising `EchoSecurity.Security` by -`(Resource := WriteSet T)`, `(Receipt := RollbackLog ws)`, -`(exit := rollback-collapses-at ws)`. +`+TransactionSecurity.agda+`, parametrising `+EchoSecurity.Security+` by +`+(Resource := WriteSet T)+`, `+(Receipt := RollbackLog ws)+`, +`+(exit := rollback-collapses-at ws)+`. -== 4. Cross-doc seam +=== 4. Cross-doc seam -* AffineScript stdlib: `stdlib/Transaction.affine` carries the safety +* AffineScript stdlib: `+stdlib/Transaction.affine+` carries the safety obligation statement in its module-level docstring and references this file by name. -* AffineScript codegen {plus} smoke: `lib/codegen++_++deno.ml` and -`tests/codegen-deno/transaction++_++smoke.++{++affine,harness.mjs}` -_witness_ the property at the Node runtime level by snapshotting tables -on `txBegin` and restoring on `txRollback`. The witness is not a proof — -it’s an executable check that the runtime mock observes the same +* AffineScript codegen + smoke: `+lib/codegen_deno.ml+` and +`+tests/codegen-deno/transaction_smoke.{affine,harness.mjs}+` _witness_ +the property at the Node runtime level by snapshotting tables on +`+txBegin+` and restoring on `+txRollback+`. The witness is not a proof +— it’s an executable check that the runtime mock observes the same invariant the formal proof will eventually establish. * Echo-types upstream: tracked at -https://github.com/hyperpolymath/echo-types/issues/174[`hyperpolymath/echo-types++#++174`] -— proposes the new `TransactionMutations.agda` module shape (and sibling -`TransactionSecurity.agda` providing the `Security` instance) reducing -to the existing `EchoNoSectionGeneric.no-section-of-collapsing-map`. -Acceptance criteria include zero new axioms (`Print Assumptions` clean) -and a back-link from the upstream commit SHA into this document. +https://github.com/hyperpolymath/echo-types/issues/174[`+hyperpolymath/echo-types#174+`] +— proposes the new `+TransactionMutations.agda+` module shape (and +sibling `+TransactionSecurity.agda+` providing the `+Security+` +instance) reducing to the existing +`+EchoNoSectionGeneric.no-section-of-collapsing-map+`. Acceptance +criteria include zero new axioms (`+Print Assumptions+` clean) and a +back-link from the upstream commit SHA into this document. -== 5. Why this matters +=== 5. Why this matters Transactions are the canonical user-facing application of affine resource discipline to data: the type system already enforces "`at most -one consumption`" of `Tx`, and the safety theorem closes the loop by +one consumption`" of `+Tx+`, and the safety theorem closes the loop by saying that the _one_ consumption that discards (rollback) is observationally equivalent to never having started. This is the proof every database textbook assumes — having it mechanised against an diff --git a/docs/academic/proofs/db-theory-2-transaction-safety.md b/docs/academic/proofs/db-theory-2-transaction-safety.md deleted file mode 100644 index 58a1e189..00000000 --- a/docs/academic/proofs/db-theory-2-transaction-safety.md +++ /dev/null @@ -1,75 +0,0 @@ - - - -# DB-Theory #2 — Transaction Safety (Rollback-Discards-Writes) - -**Status**: Carrier wired (`stdlib/Transaction.affine`, codegen, Deno-ESM smoke); formal proof obligation **pending upstream against `hyperpolymath/echo-types`**. - -## 1. The obligation - -Let `Tx` be the opaque affine handle returned by `tx_begin(d: Db)` in `stdlib/Transaction.affine`. Let `t : Tx` be such a handle, and let `σ₀` be the database state immediately before `tx_begin(d)`. Let `W` be the multiset of mutating SQL statements (`INSERT`, `UPDATE`, `DELETE`, schema mutations) executed against `d` (or any handle aliased through `tx_db(t)`) between `tx_begin(d)` and the consumption of `t`. - -**Safety property #DB-2.1 (rollback-discards-writes)**: if `t` is consumed by `tx_rollback(t)`, then for every query `q` issued against `d` *after* the rollback, the answer to `q` is precisely the answer it would have had over `σ₀` (i.e. as if `W` had never happened). - -**Safety property #DB-2.2 (commit-promotes-writes)**: if `t` is consumed by `tx_commit(t)`, then for every query `q` issued after the commit, the answer is the same as serially applying `W` to `σ₀`. - -**Safety property #DB-2.3 (savepoint locality)**: if a savepoint `s` is opened with `tx_savepoint(t, s)`, mutations `W_s` are issued, and then `tx_rollback_to(t, s)` is called, the database state mid-transaction reverts to the state at `tx_savepoint(t, s)` while leaving the outer transaction live. - -## 2. Echo-types audit (2026-06-01) - -Per owner directive, every proof in AffineScript must first audit `hyperpolymath/echo-types`, reuse if applicable, extend upstream **with proofs** if not, then cross-document. - -**Audit finding** (full report under `tasklist/sub-agent reports/2026-06-01-transaction-echo-audit`): echo-types has no Transaction-specific instantiation today, but carries three reusable abstractions: - -1. `EchoLinear.LEcho` + the `weaken : LEcho linear → LEcho affine` collapse map, with `no-section-weaken` proving no recovery after weakening. -2. `EchoSecurity.Security` record + `exit-collapses-at` / `audit-no-recovery-at` — the boundary-collapse template structurally mirrors rollback-discards-writes. -3. `EchoNoSectionGeneric.no-section-of-collapsing-map` — the generic lemma the Transaction proof reduces to. - -## 3. Proposed upstream extension - -A new module `TransactionMutations.agda` in echo-types, structured as follows: - -```agda -module TransactionMutations where - --- The write-set carrier: an opaque set of mutations parametrised by --- the table-type T being mutated. -record WriteSet (T : Set) : Set where - field - applied : List Mutation -- audit trail - -- Algebra: `empty`, `append`, `compose` give a monoid action on - -- DbState — the action and its laws are deferred to a separate - -- module to keep this carrier import-light. - --- The rollback log is the witness that a WriteSet was discarded. -record RollbackLog {T : Set} (ws : WriteSet T) : Set where - field - discarded-at : Timestamp - -- The trivial receipt: rollback emits no recoverable signal. - receipt : Trivial - --- The collapse map that powers the safety proof: every WriteSet --- rolls back to the trivial receipt, and the generic --- no-section-of-collapsing-map (already in echo-types) gives the --- no-recovery lemma free. -rollback-collapses-at : - {T : Set} (ws : WriteSet T) → RollbackLog ws → Trivial -rollback-collapses-at _ _ = trivial - -rollback-discards-writes : - {T : Set} (ws : WriteSet T) → - EchoNoSectionGeneric.no-section-of (rollback-collapses-at ws) -rollback-discards-writes ws = no-section-of-collapsing-map _ -``` - -The Security-record instance lives in a sibling `TransactionSecurity.agda`, parametrising `EchoSecurity.Security` by `(Resource := WriteSet T)`, `(Receipt := RollbackLog ws)`, `(exit := rollback-collapses-at ws)`. - -## 4. Cross-doc seam - -- AffineScript stdlib: `stdlib/Transaction.affine` carries the safety obligation statement in its module-level docstring and references this file by name. -- AffineScript codegen + smoke: `lib/codegen_deno.ml` and `tests/codegen-deno/transaction_smoke.{affine,harness.mjs}` *witness* the property at the Node runtime level by snapshotting tables on `txBegin` and restoring on `txRollback`. The witness is not a proof — it's an executable check that the runtime mock observes the same invariant the formal proof will eventually establish. -- Echo-types upstream: tracked at [`hyperpolymath/echo-types#174`](https://github.com/hyperpolymath/echo-types/issues/174) — proposes the new `TransactionMutations.agda` module shape (and sibling `TransactionSecurity.agda` providing the `Security` instance) reducing to the existing `EchoNoSectionGeneric.no-section-of-collapsing-map`. Acceptance criteria include zero new axioms (`Print Assumptions` clean) and a back-link from the upstream commit SHA into this document. - -## 5. Why this matters - -Transactions are the canonical user-facing application of affine resource discipline to data: the type system already enforces "at most one consumption" of `Tx`, and the safety theorem closes the loop by saying that the *one* consumption that discards (rollback) is observationally equivalent to never having started. This is the proof every database textbook assumes — having it mechanised against an echo-types carrier means AffineScript's transaction surface inherits the same "no recovery from boundary collapse" story echo-types already proves for region-exit auditing. diff --git a/docs/academic/proofs/db-theory-3-aggregation-as-fold.adoc b/docs/academic/proofs/db-theory-3-aggregation-as-fold.adoc index 76e1e0a2..58311d5d 100644 --- a/docs/academic/proofs/db-theory-3-aggregation-as-fold.adoc +++ b/docs/academic/proofs/db-theory-3-aggregation-as-fold.adoc @@ -1,17 +1,15 @@ -// SPDX-License-Identifier: CC-BY-SA-4.0 -// SPDX-FileCopyrightText: 2024-2026 hyperpolymath (Jonathan D.A. Jewell ) -= DB-Theory ++#++3 — Aggregation-as-Monoid-Homomorphism +== DB-Theory #3 — Aggregation-as-Monoid-Homomorphism -*Status*: Carrier wired (`stdlib/Aggregate.affine`, codegen, Deno-ESM +*Status*: Carrier wired (`+stdlib/Aggregate.affine+`, codegen, Deno-ESM smoke); formal proof obligation *pending upstream against -https://github.com/hyperpolymath/echo-types/issues/175[`hyperpolymath/echo-types++#++175`]*. +https://github.com/hyperpolymath/echo-types/issues/175[`+hyperpolymath/echo-types#175+`]*. -== 1. The obligation +=== 1. The obligation -For each scalar aggregator `M = (Elem, ε, ⊕)` and any partition -`++{++group++_++k}` of the row set by key `k`: +For each scalar aggregator `+M = (Elem, ε, ⊕)+` and any partition +`+{group_k}+` of the row set by key `+k+`: -*Safety property ++#++DB-3.1 (aggregation-as-fold)*: +*Safety property #DB-3.1 (aggregation-as-fold)*: .... aggregate(SELECT M(v) FROM t GROUP BY k) @@ -22,39 +20,38 @@ The aggregators are commutative monoids: [width="100%",cols="19%,29%,6%,9%,19%,18%",options="header",] |=== -|Aggregator |`Elem` |`ε` |`⊕` |Commutative? |Idempotent? -|COUNT |`ℕ` |`0` |`{plus}` |✓ |✗ -|SUM |`ℕ` (or `ℤ`, `ℝ`) |`0` |`{plus}` |✓ |✗ -|MIN |`ℕ ∪ ++{++∞}` |`∞` |`min` |✓ |✓ -|MAX |`ℕ ∪ ++{++-∞}` |`-∞` |`max` |✓ |✓ -|AVG |*not a monoid* (no identity) — derived as `SUM/COUNT` | | | | +|Aggregator |`+Elem+` |`+ε+` |`+⊕+` |Commutative? |Idempotent? +|COUNT |`+ℕ+` |`+0+` |`+++` |✓ |✗ +|SUM |`+ℕ+` (or `+ℤ+`, `+ℝ+`) |`+0+` |`+++` |✓ |✗ +|MIN |`+ℕ ∪ {∞}+` |`+∞+` |`+min+` |✓ |✓ +|MAX |`+ℕ ∪ {-∞}+` |`+-∞+` |`+max+` |✓ |✓ +|AVG |*not a monoid* (no identity) — derived as `+SUM/COUNT+` | | | | |=== -== 2. Echo-types audit (2026-06-01) +=== 2. Echo-types audit (2026-06-01) Per owner directive, every proof must first audit -`hyperpolymath/echo-types`. +`+hyperpolymath/echo-types+`. *Finding*: no existing monoid / semiring / aggregation infrastructure today. Closest scaffolding: [arabic] -. `EchoCost.CostAlgebra` — left-identity {plus} monotonicity, but no -composition law. Reusable as a `Monoid` _instance_ once the carrier +. `+EchoCost.CostAlgebra+` — left-identity + monotonicity, but no +composition law. Reusable as a `+Monoid+` _instance_ once the carrier exists. -. `Ordinal/Brouwer/OmegaPow.agda++#++additive-principal` — exactly the +. `+Ordinal/Brouwer/OmegaPow.agda#additive-principal+` — exactly the monoid closure property for ω^n exponents. -. `EchoDecorationStructure.agda` — observer-level lattice; aggregation +. `+EchoDecorationStructure.agda+` — observer-level lattice; aggregation lives at the data level. -. `docs/adjacency/provenance-semirings.adoc` — explicitly names the +. `+docs/adjacency/provenance-semirings.adoc+` — explicitly names the distinctness story (echo adds types; semirings add scalars). -*Steer*: minor extension — one new module. Tracked at -echo-types++#++175. +*Steer*: minor extension — one new module. Tracked at echo-types#175. -== 3. Proposed upstream extension +=== 3. Proposed upstream extension -A new module `EchoAggregation.agda`: +A new module `+EchoAggregation.agda+`: [source,agda] ---- @@ -81,38 +78,37 @@ aggregation-as-fold : ≡ foldr (_⊕_ ∘ agg) ε (lookup k (partition rows)) ---- -Plus concrete instances `countMonoid : Monoid ℓ-zero`, `sumMonoid`, -`minMonoid`, `maxMonoid`. +Plus concrete instances `+countMonoid : Monoid ℓ-zero+`, `+sumMonoid+`, +`+minMonoid+`, `+maxMonoid+`. -== 4. Cross-doc seam +=== 4. Cross-doc seam -* *AffineScript stdlib*: `stdlib/Aggregate.affine` carries the +* *AffineScript stdlib*: `+stdlib/Aggregate.affine+` carries the obligation in its module docstring with the aggregator monoid table. -* *AffineScript codegen {plus} smoke*: `lib/codegen++_++deno.ml` {plus} -`tests/codegen-deno/aggregate++_++smoke.++{++affine,harness.mjs}` -_witness_ the property at the Node runtime level — the mock implements -`groupBy` by bucketing rows by key column then folding the aggregator -over each bucket. The witness is not a proof; it’s an executable check -that the runtime mock observes the same invariant the formal proof will +* *AffineScript codegen + smoke*: `+lib/codegen_deno.ml+` + +`+tests/codegen-deno/aggregate_smoke.{affine,harness.mjs}+` _witness_ +the property at the Node runtime level — the mock implements `+groupBy+` +by bucketing rows by key column then folding the aggregator over each +bucket. The witness is not a proof; it’s an executable check that the +runtime mock observes the same invariant the formal proof will eventually establish. * *Echo-types upstream*: tracked at -https://github.com/hyperpolymath/echo-types/issues/175[`hyperpolymath/echo-types++#++175`]. +https://github.com/hyperpolymath/echo-types/issues/175[`+hyperpolymath/echo-types#175+`]. Once landed, back-link the commit SHA / module path here. -== 5. Why this matters +=== 5. Why this matters Aggregation is the most-used non-trivial query shape outside selection/projection. Wiring aggregators as typed commutative monoids (rather than ad-hoc per-shape SQL strings) gives: * *Distributivity proofs free*: aggregation distributes over filtering -(db-theory ++#++4) and indexed scans (db-theory ++#++6) via monoid -homomorphism. -* *CRDT bridge for free*: `OR-Set` and `GCounter` (db-theory ++#++9) are +(db-theory #4) and indexed scans (db-theory #6) via monoid homomorphism. +* *CRDT bridge for free*: `+OR-Set+` and `+GCounter+` (db-theory #9) are precisely _monoids with convergence_ — the same carrier extends. * *Provenance-semiring bridge*: the Green/Karvounarakis/Tannen framing -instantiates here once `EchoAggregation` lands. +instantiates here once `+EchoAggregation+` lands. Sibling: -https://github.com/hyperpolymath/echo-types/issues/174[echo-types++#++174] -(Transaction safety / `no-section-of-collapsing-map`). +https://github.com/hyperpolymath/echo-types/issues/174[echo-types#174] +(Transaction safety / `+no-section-of-collapsing-map+`). diff --git a/docs/academic/proofs/db-theory-3-aggregation-as-fold.md b/docs/academic/proofs/db-theory-3-aggregation-as-fold.md deleted file mode 100644 index 63de4151..00000000 --- a/docs/academic/proofs/db-theory-3-aggregation-as-fold.md +++ /dev/null @@ -1,85 +0,0 @@ - - - -# DB-Theory #3 — Aggregation-as-Monoid-Homomorphism - -**Status**: Carrier wired (`stdlib/Aggregate.affine`, codegen, Deno-ESM smoke); formal proof obligation **pending upstream against [`hyperpolymath/echo-types#175`](https://github.com/hyperpolymath/echo-types/issues/175)**. - -## 1. The obligation - -For each scalar aggregator `M = (Elem, ε, ⊕)` and any partition `{group_k}` of the row set by key `k`: - -**Safety property #DB-3.1 (aggregation-as-fold)**: -``` -aggregate(SELECT M(v) FROM t GROUP BY k) - ≡ { k ↦ foldr ⊕ ε (map agg group_k) } -``` - -The aggregators are commutative monoids: - -| Aggregator | `Elem` | `ε` | `⊕` | Commutative? | Idempotent? | -|------------|---------------------|-----|-------|--------------|-------------| -| COUNT | `ℕ` | `0` | `+` | ✓ | ✗ | -| SUM | `ℕ` (or `ℤ`, `ℝ`) | `0` | `+` | ✓ | ✗ | -| MIN | `ℕ ∪ {∞}` | `∞` | `min` | ✓ | ✓ | -| MAX | `ℕ ∪ {-∞}` | `-∞`| `max` | ✓ | ✓ | -| AVG | **not a monoid** (no identity) — derived as `SUM/COUNT` | - -## 2. Echo-types audit (2026-06-01) - -Per owner directive, every proof must first audit `hyperpolymath/echo-types`. - -**Finding**: no existing monoid / semiring / aggregation infrastructure today. Closest scaffolding: - -1. `EchoCost.CostAlgebra` — left-identity + monotonicity, but no composition law. Reusable as a `Monoid` *instance* once the carrier exists. -2. `Ordinal/Brouwer/OmegaPow.agda#additive-principal` — exactly the monoid closure property for ω^n exponents. -3. `EchoDecorationStructure.agda` — observer-level lattice; aggregation lives at the data level. -4. `docs/adjacency/provenance-semirings.adoc` — explicitly names the distinctness story (echo adds types; semirings add scalars). - -**Steer**: minor extension — one new module. Tracked at echo-types#175. - -## 3. Proposed upstream extension - -A new module `EchoAggregation.agda`: - -```agda -record Monoid (ℓ : Level) : Set (suc ℓ) where - field - Elem : Set ℓ - ε : Elem - _⊕_ : Elem → Elem → Elem - assoc : ∀ a b c → (a ⊕ b) ⊕ c ≡ a ⊕ (b ⊕ c) - identity-l : ∀ a → ε ⊕ a ≡ a - identity-r : ∀ a → a ⊕ ε ≡ a - -record GroupAggregator {ℓ} (K V : Set) (M : Monoid ℓ) : Set ℓ where - open Monoid M - field - agg : V → Elem - --- Headline lemma (signature — proof may follow in stacked PR): -aggregation-as-fold : - ∀ {ℓ} {K V : Set} {M : Monoid ℓ} (ga : GroupAggregator K V M) - → (rows : List (K × V)) - → (k : K) - → group-of k (groupBy proj₁ rows) - ≡ foldr (_⊕_ ∘ agg) ε (lookup k (partition rows)) -``` - -Plus concrete instances `countMonoid : Monoid ℓ-zero`, `sumMonoid`, `minMonoid`, `maxMonoid`. - -## 4. Cross-doc seam - -- **AffineScript stdlib**: `stdlib/Aggregate.affine` carries the obligation in its module docstring with the aggregator monoid table. -- **AffineScript codegen + smoke**: `lib/codegen_deno.ml` + `tests/codegen-deno/aggregate_smoke.{affine,harness.mjs}` *witness* the property at the Node runtime level — the mock implements `groupBy` by bucketing rows by key column then folding the aggregator over each bucket. The witness is not a proof; it's an executable check that the runtime mock observes the same invariant the formal proof will eventually establish. -- **Echo-types upstream**: tracked at [`hyperpolymath/echo-types#175`](https://github.com/hyperpolymath/echo-types/issues/175). Once landed, back-link the commit SHA / module path here. - -## 5. Why this matters - -Aggregation is the most-used non-trivial query shape outside selection/projection. Wiring aggregators as typed commutative monoids (rather than ad-hoc per-shape SQL strings) gives: - -- **Distributivity proofs free**: aggregation distributes over filtering (db-theory #4) and indexed scans (db-theory #6) via monoid homomorphism. -- **CRDT bridge for free**: `OR-Set` and `GCounter` (db-theory #9) are precisely *monoids with convergence* — the same carrier extends. -- **Provenance-semiring bridge**: the Green/Karvounarakis/Tannen framing instantiates here once `EchoAggregation` lands. - -Sibling: [echo-types#174](https://github.com/hyperpolymath/echo-types/issues/174) (Transaction safety / `no-section-of-collapsing-map`). diff --git a/docs/governance/CODE_OF_CONDUCT.adoc b/docs/governance/CODE_OF_CONDUCT.adoc new file mode 100644 index 00000000..bd2a83cb --- /dev/null +++ b/docs/governance/CODE_OF_CONDUCT.adoc @@ -0,0 +1,24 @@ +== Contributor Covenant Code of Conduct + +=== Our Pledge + +We pledge to make participation a harassment-free experience for +everyone. + +=== Our Standards + +*Positive behavior:* * Using welcoming language * Being respectful of +differing viewpoints * Accepting constructive criticism * Focusing on +what is best for the community + +*Unacceptable behavior:* * Harassment, trolling, or personal attacks * +Publishing private information without permission + +=== Enforcement + +Report issues to the maintainers. All complaints will be reviewed. + +=== Attribution + +Adapted from https://www.contributor-covenant.org/[Contributor Covenant] +v2.1. diff --git a/docs/governance/CODE_OF_CONDUCT.md b/docs/governance/CODE_OF_CONDUCT.md deleted file mode 100644 index caeda1c6..00000000 --- a/docs/governance/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,27 +0,0 @@ - -# Contributor Covenant Code of Conduct - -## Our Pledge - -We pledge to make participation a harassment-free experience for everyone. - -## Our Standards - -**Positive behavior:** -* Using welcoming language -* Being respectful of differing viewpoints -* Accepting constructive criticism -* Focusing on what is best for the community - -**Unacceptable behavior:** -* Harassment, trolling, or personal attacks -* Publishing private information without permission - -## Enforcement - -Report issues to the maintainers. All complaints will be reviewed. - -## Attribution - -Adapted from [Contributor Covenant](https://www.contributor-covenant.org/) v2.1. - diff --git a/docs/governance/LICENSING-GUIDE.adoc b/docs/governance/LICENSING-GUIDE.adoc new file mode 100644 index 00000000..24b1c736 --- /dev/null +++ b/docs/governance/LICENSING-GUIDE.adoc @@ -0,0 +1,482 @@ +== AffineScript Licensing Guide + +=== 📜 Comprehensive Licensing Information + +This document clarifies the licensing structure for the AffineScript +ecosystem, including game content, core technology, and related +projects. + +''''' + +=== 🏷️ Three-Tier Licensing Structure + +==== 1. *Game Content (AGPL-3.0-or-later)* + +*Applies to:* All game-specific assets, levels, scripts, and +modifications + +*Purpose:* Ensure game modifications remain open source and accessible +to the community + +*Key Requirements:* - Source code must be made available - Modifications +must be shared under same license - Network use must provide source +access - License and copyright notices preserved + +*Files Covered:* - Game logic and scripts (`+game.wasm+`) - Game levels +and assets (`+assets/+`) - Game data and configuration - Example +programs and modifications + +''''' + +==== 2. *Core Technology (MPL-2.0)* + +*Applies to:* AffineScript compiler, runtime, and development tools + +*Purpose:* Provide permissive licensing for language technology while +maintaining ethical use requirements + +*Key Requirements:* - Preserve license and copyright notices - Document +modifications - Follow ethical use guidelines - No copyleft requirements +for derived works + +*Files Covered:* - AffineScript compiler (`+compiler.wasm+`) - Standard +library (`+stdlib/+`) - Development tools (`+tools/+`) - Language server +and IDE integration + +''''' + +==== 3. *Foundational Technologies (MPL-2.0-derived)* + +*Applies to:* Gossamer, Burble, and other supporting technologies + +*Purpose:* Provide Mozilla Public License 2.0 base with additional +ethical use provisions + +*Key Requirements:* - Preserve MPL-2.0 requirements - Follow Palimpsest +ethical use guidelines - Document emotional lineage - Maintain +provenance metadata + +*Projects Covered:* - *Gossamer*: Linearly-typed webview shell - +*Burble*: High-assurance multiplayer communications - Supporting +libraries and frameworks + +''''' + +=== 📚 License Relationships + +[source,mermaid] +---- +graph TD + A[Game Content] -->|AGPL-3.0-or-later| B[Open Source Game] + C[Core Technology] -->|MPL-2.0| D[AffineScript Compiler] + E[Foundational Tech] -->|PMPL-1.0/MPL-2.0| F[Gossamer/Burble] + + B -->|Uses| D + B -->|Uses| F + D -->|Depends on| F +---- + +''''' + +=== 📋 Detailed License Breakdown + +==== AGPL-3.0-or-later (Game Content) + +*Full Name:* GNU Affero General Public License version 3.0 or later + +*Key Provisions:* - *Copyleft:* Strong copyleft - modifications must be +open source - *Network Use:* Source must be available for +network-accessible versions - *Patent Grant:* Automatic patent license +for contributors - *Compatibility:* Compatible with GPL-3.0 + +*When to Use:* - Game content and assets - Game modifications and +extensions - Player-created content - Game-specific examples + +*File Header:* + +[source,affinescript] +---- +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// +// This file is part of the AffineScript Game +// Licensed under AGPL-3.0-or-later +---- + +''''' + +==== MPL-2.0 (Core Technology) + +*Full Name:* Palimpsest Mutual Public License 1.0 or later + +*Base License:* Mozilla Public License 2.0 + +*Additional Provisions:* - *Emotional Lineage:* Preserve narrative and +cultural context - *Provenance Metadata:* Maintain cryptographic +attribution - *Ethical Use:* Follow community guidelines - +*Quantum-Safe:* Optional post-quantum signatures + +*Key Provisions:* - *File-Level Copyleft:* Strong copyleft at file level +- *Patent Grant:* Automatic patent license - *Compatibility:* Compatible +with MPL-2.0 - *Governance:* Palimpsest Stewardship Council oversight + +*When to Use:* - AffineScript compiler and tools - Standard library +modules - Development infrastructure - Language server and IDE plugins + +*File Header:* + +[source,ocaml] +---- +(* SPDX-License-Identifier: CC-BY-SA-4.0 *) +(* SPDX-FileCopyrightText: 2026 Palimpsest Stewardship Council *) +(* + * This file is part of AffineScript Core Technology + * Licensed under MPL-2.0 (based on MPL-2.0) + *) +---- + +''''' + +==== PMPL-1.0 / MPL-2.0-derived (Foundational Technologies) + +*Full Name:* Palimpsest Mutual Public License 1.0 (based on MPL-2.0) + +*Relationship to MPL-2.0:* - *Base:* Full MPL-2.0 text incorporated by +reference - *Extensions:* Additional sections for ethical use - +*Compatibility:* Fully compatible with MPL-2.0 projects - *Governance:* +Additional stewardship council provisions + +*Key Provisions:* - *File-Level Copyleft:* Strong copyleft at file level +- *Patent Grant:* Automatic patent license for contributors - *Secondary +Licensing:* Allows specified secondary licenses - *Modification +Requirements:* Clear modification documentation + +*When to Use:* - Gossamer (linearly-typed webview shell) - Burble +(high-assurance communications) - Supporting libraries and frameworks - +Infrastructure components + +*File Header:* + +[source,rust] +---- +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// +// This file is part of Gossamer/Burble Foundational Technologies +// Licensed under PMPL-1.0 (Palimpsest-MPL) based on MPL-2.0 +// Complete license: https://github.com/hyperpolymath/palimpsest-license +---- + +''''' + +=== 🎯 Repository Description and Tags + +==== Recommended Repository Description + +*Short Version (GitHub):* + +.... +AffineScript: The game developer's secret weapon. AGPL-3.0 game content with PMPL-1.0 core technology. Compiles to WASM with compiler-proven correctness. Built on Gossamer (PMPL) and Burble (PMPL) foundations. +.... + +*Long Version (README):* + +[source,markdown] +---- +# AffineScript: The Game Developer's Secret Weapon + +**AffineScript** is a revolutionary game development platform featuring: + +🎮 **Game Content** (AGPL-3.0-or-later) +- Open source game assets and modifications +- Community-driven development +- Ensured accessibility for all players + +💻 **Core Technology** (MPL-2.0) +- Affine-type programming language +- Compiler-proven correctness +- WebAssembly compilation +- Permissive tooling license + +🛡️ **Foundational Technologies** (PMPL-1.0/MPL-2.0) +- **Gossamer**: Linearly-typed webview shell +- **Burble**: High-assurance multiplayer communications +- Ethical use requirements +- Quantum-safe provenance + +**Built for:** Game developers who want bug-free code, type-safe game logic, and compiler-enforced resource management. + +**Licensing:** Dual licensing model ensures open game content while providing permissive tooling licenses. +---- + +==== Recommended GitHub Topics + +.... +retro-game, pmpl, palimpsest-mpl, mpl-2-0-derived, agpl-3-0, game-development, +wasm, affine-types, type-safety, game-engine, open-source, ethical-licensing, +quantum-safe, provenance, linear-types, resource-safety +.... + +==== Repository Tags + +*Version Tags:* - `+v0.1.0-alpha.1+` (Current) - `+v0.1.0-alpha+` (Alpha +base) - `+game-agpl+` (Game content license) - `+tech-pmpl+` (Technology +license) + +*Content Tags:* - `+game-content+` (AGPL-3.0 content) - `+compiler+` +(PMPL-1.0 technology) - `+gossamer+` (PMPL-1.0 foundation) - `+burble+` +(PMPL-1.0 foundation) + +''''' + +=== 📁 License Directory Structure + +.... +LICENSES/ +├── LICENSE # PMPL-1.0 (Primary) +├── LICENSE-AGPL-3.0 # AGPL-3.0 (Game Content) +├── LICENSE-PMPL-1.0 # PMPL-1.0 (Core Tech) +├── LICENSE-MPL-2.0 # MPL-2.0 (Reference) +├── EXHIBIT-A-ETHICAL-USE.txt # Ethical guidelines +├── EXHIBIT-B-QUANTUM-SAFE.txt # Quantum-safe specs +└── README.md # License guide +.... + +''''' + +=== 🔧 Addressing Dependabot Alerts + +==== atty Potential Unaligned Read (Rust) + +*Alert Summary:* - *Package:* atty (Rust) - *Version:* <= 0.2.14 - +*Issue:* Potential unaligned pointer dereference on Windows - +*Severity:* Medium (theoretical risk) - *Status:* Unmaintained package + +*Analysis:* + +[source,markdown] +---- +✅ **Actual Risk:** Low +- System allocator on Windows uses HeapAlloc +- HeapAlloc guarantees sufficient alignment +- Issue only manifests with custom global allocators + +⚠️ **Theoretical Risk:** +- Custom allocators could cause alignment issues +- Unaligned pointer dereference possible +- Potential for crashes or undefined behavior + +❌ **Mitigation Challenges:** +- Package unmaintained (last release: ~3 years ago) +- Maintainer unreachable +- No official patches available +---- + +*Recommended Actions:* + +===== 1. *Immediate (Low Effort)* + +[source,markdown] +---- +✅ Add to dependency documentation: +---- + +== Known Issues + +=== atty (Rust) + +* Version: 0.2.14 (via transitive dependency) +* Issue: Potential unaligned read on Windows +* Risk: Low (mitigated by System allocator) +* Status: Unmaintained +* Workaround: None needed (System allocator provides safety) + +.... +.... + +==== 2. *Short-Term (Medium Effort)* + +[source,markdown] +---- +🔄 Update Cargo.toml to document: +```toml +[package.metadata.dependency-issues] +atty = "Potential unaligned read (Windows only). Mitigated by System allocator. No action required." +---- + +🔄 Add to security policy: + +[source,markdown] +---- +### Known Vulnerabilities + +#### atty (Transitive Dependency) +- **CVE:** None assigned +- **Affected:** Windows systems with custom allocators +- **Mitigation:** System allocator provides safety +- **Status:** Monitoring for updates +- **Action:** None required for standard configurations +---- + +.... + +#### 3. **Long-Term (Future Consideration)** +```markdown +🚀 Evaluate alternatives when feasible: + +**Option 1: std::io::IsTerminal (Rust 1.70+)** +```rust +use std::io::IsTerminal as _; +let is_terminal = std::io::stdin().is_terminal(); +.... + +* ✅ Stable since Rust 1.70.0 +* ✅ No external dependencies +* ❌ Requires Rust 1.70+ + +*Option 2: is-terminal (Standalone Crate)* + +[source,toml] +---- +[dependencies] +is-terminal = "0.4" +---- + +* ✅ Actively maintained +* ✅ Supports older Rust versions +* ✅ Cross-platform +* ❌ Additional dependency + +*Option 3: Custom Implementation* + +[source,rust] +---- +#[cfg(windows)] +fn is_terminal() -> bool { + // Windows-specific implementation + unsafe { + let handle = winapi::um::processenv::GetStdHandle( + winapi::um::winbase::STD_INPUT_HANDLE + ); + let mut mode: winapi::um::wincon::DWORD = 0; + winapi::um::wincon::GetConsoleMode(handle, &mut mode) != 0 + } +} +---- + +* ✅ No dependencies +* ✅ Full control +* ❌ Platform-specific code +* ❌ Maintenance burden + +.... + +**Decision:** +```markdown +📋 **Current Status:** No action required + +✅ **Rationale:** +- System allocator mitigates risk +- No known exploits in wild +- Low severity issue +- Package used transitively (via clap) + +🔍 **Monitoring:** +- Watch for maintainer activity +- Track Rust ecosystem developments +- Re-evaluate at next major version + +🚀 **Future:** Consider migration when: +- Alternative provides clear benefits +- Migration cost justified +- Breaking changes acceptable +.... + +''''' + +=== 📝 License Compliance Checklist + +==== For Game Distributors + +* [ ] Include LICENSE-AGPL-3.0 file +* [ ] Include LICENSE-PMPL-1.0 file +* [ ] Provide source code access (AGPL requirement) +* [ ] Document modifications (AGPL requirement) +* [ ] Preserve copyright notices +* [ ] Include license guide + +==== For Technology Users + +* [ ] Include LICENSE-PMPL-1.0 file +* [ ] Preserve copyright notices +* [ ] Document modifications +* [ ] Follow ethical use guidelines +* [ ] Include license guide + +==== For Repository Maintainers + +* [ ] Maintain separate license files +* [ ] Update license headers +* [ ] Document licensing clearly +* [ ] Monitor dependency alerts +* [ ] Update security policy + +''''' + +=== 🔗 License Resources + +==== Full License Texts + +* *AGPL-3.0:* https://www.gnu.org/licenses/agpl-3.0.html +* *PMPL-1.0:* https://github.com/hyperpolymath/palimpsest-license +* *MPL-2.0:* https://www.mozilla.org/en-US/MPL/2.0/ + +==== License Identification + +* *SPDX AGPL-3.0:* `+AGPL-3.0-or-later+` +* *SPDX PMPL-1.0:* `+MPL-2.0+` +* *SPDX MPL-2.0:* `+MPL-2.0+` + +==== Compliance Tools + +* *REUSE:* https://reuse.software/ +* *FOSSA:* https://fossa.com/ +* *Licensee:* https://github.com/licensee/licensee + +''''' + +=== 🎯 Summary + +==== Licensing Structure + +.... +Game Content (AGPL-3.0) → Open game development +Core Technology (PMPL-1.0) → Permissive tooling +Foundational Tech (PMPL/MPL-2.0) → Ethical infrastructure +.... + +==== Key Points + +[arabic] +. *Dual licensing* ensures open games with permissive tools +. *PMPL-1.0* extends MPL-2.0 with ethical use requirements +. *AGPL-3.0* ensures game modifications remain open +. *All licenses* are OSI-approved and compatible +. *Clear separation* between game content and technology + +==== Action Items + +* [ ] Update repository description and tags +* [ ] Ensure license directory has all versions +* [ ] Document atty dependency status +* [ ] Add license compliance checklist +* [ ] Update contributing guidelines + +''''' + +*Last Updated:* March 31, 2026 *Version:* Alpha-1 *Status:* Complete +licensing documentation + +SPDX-License-Identifier: CC-BY-SA-4.0 AND MPL-2.0 AND MPL-2.0 +SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell and contributors diff --git a/docs/governance/LICENSING-GUIDE.md b/docs/governance/LICENSING-GUIDE.md deleted file mode 100644 index cc2d1cd7..00000000 --- a/docs/governance/LICENSING-GUIDE.md +++ /dev/null @@ -1,466 +0,0 @@ -# AffineScript Licensing Guide - -## 📜 Comprehensive Licensing Information - -This document clarifies the licensing structure for the AffineScript ecosystem, including game content, core technology, and related projects. - ---- - -## 🏷️ Three-Tier Licensing Structure - -### 1. **Game Content (AGPL-3.0-or-later)** -**Applies to:** All game-specific assets, levels, scripts, and modifications - -**Purpose:** Ensure game modifications remain open source and accessible to the community - -**Key Requirements:** -- Source code must be made available -- Modifications must be shared under same license -- Network use must provide source access -- License and copyright notices preserved - -**Files Covered:** -- Game logic and scripts (`game.wasm`) -- Game levels and assets (`assets/`) -- Game data and configuration -- Example programs and modifications - ---- - -### 2. **Core Technology (MPL-2.0)** -**Applies to:** AffineScript compiler, runtime, and development tools - -**Purpose:** Provide permissive licensing for language technology while maintaining ethical use requirements - -**Key Requirements:** -- Preserve license and copyright notices -- Document modifications -- Follow ethical use guidelines -- No copyleft requirements for derived works - -**Files Covered:** -- AffineScript compiler (`compiler.wasm`) -- Standard library (`stdlib/`) -- Development tools (`tools/`) -- Language server and IDE integration - ---- - -### 3. **Foundational Technologies (MPL-2.0-derived)** -**Applies to:** Gossamer, Burble, and other supporting technologies - -**Purpose:** Provide Mozilla Public License 2.0 base with additional ethical use provisions - -**Key Requirements:** -- Preserve MPL-2.0 requirements -- Follow Palimpsest ethical use guidelines -- Document emotional lineage -- Maintain provenance metadata - -**Projects Covered:** -- **Gossamer**: Linearly-typed webview shell -- **Burble**: High-assurance multiplayer communications -- Supporting libraries and frameworks - ---- - -## 📚 License Relationships - -```mermaid -graph TD - A[Game Content] -->|AGPL-3.0-or-later| B[Open Source Game] - C[Core Technology] -->|MPL-2.0| D[AffineScript Compiler] - E[Foundational Tech] -->|PMPL-1.0/MPL-2.0| F[Gossamer/Burble] - - B -->|Uses| D - B -->|Uses| F - D -->|Depends on| F -``` - ---- - -## 📋 Detailed License Breakdown - -### AGPL-3.0-or-later (Game Content) - -**Full Name:** GNU Affero General Public License version 3.0 or later - -**Key Provisions:** -- **Copyleft:** Strong copyleft - modifications must be open source -- **Network Use:** Source must be available for network-accessible versions -- **Patent Grant:** Automatic patent license for contributors -- **Compatibility:** Compatible with GPL-3.0 - -**When to Use:** -- Game content and assets -- Game modifications and extensions -- Player-created content -- Game-specific examples - -**File Header:** -```affinescript -// SPDX-License-Identifier: CC-BY-SA-4.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// -// This file is part of the AffineScript Game -// Licensed under AGPL-3.0-or-later -``` - ---- - -### MPL-2.0 (Core Technology) - -**Full Name:** Palimpsest Mutual Public License 1.0 or later - -**Base License:** Mozilla Public License 2.0 - -**Additional Provisions:** -- **Emotional Lineage:** Preserve narrative and cultural context -- **Provenance Metadata:** Maintain cryptographic attribution -- **Ethical Use:** Follow community guidelines -- **Quantum-Safe:** Optional post-quantum signatures - -**Key Provisions:** -- **File-Level Copyleft:** Strong copyleft at file level -- **Patent Grant:** Automatic patent license -- **Compatibility:** Compatible with MPL-2.0 -- **Governance:** Palimpsest Stewardship Council oversight - -**When to Use:** -- AffineScript compiler and tools -- Standard library modules -- Development infrastructure -- Language server and IDE plugins - -**File Header:** -```ocaml -(* SPDX-License-Identifier: CC-BY-SA-4.0 *) -(* SPDX-FileCopyrightText: 2026 Palimpsest Stewardship Council *) -(* - * This file is part of AffineScript Core Technology - * Licensed under MPL-2.0 (based on MPL-2.0) - *) -``` - ---- - -### PMPL-1.0 / MPL-2.0-derived (Foundational Technologies) - -**Full Name:** Palimpsest Mutual Public License 1.0 (based on MPL-2.0) - -**Relationship to MPL-2.0:** -- **Base:** Full MPL-2.0 text incorporated by reference -- **Extensions:** Additional sections for ethical use -- **Compatibility:** Fully compatible with MPL-2.0 projects -- **Governance:** Additional stewardship council provisions - -**Key Provisions:** -- **File-Level Copyleft:** Strong copyleft at file level -- **Patent Grant:** Automatic patent license for contributors -- **Secondary Licensing:** Allows specified secondary licenses -- **Modification Requirements:** Clear modification documentation - -**When to Use:** -- Gossamer (linearly-typed webview shell) -- Burble (high-assurance communications) -- Supporting libraries and frameworks -- Infrastructure components - -**File Header:** -```rust -// SPDX-License-Identifier: CC-BY-SA-4.0 -// SPDX-License-Identifier: CC-BY-SA-4.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// -// This file is part of Gossamer/Burble Foundational Technologies -// Licensed under PMPL-1.0 (Palimpsest-MPL) based on MPL-2.0 -// Complete license: https://github.com/hyperpolymath/palimpsest-license -``` - ---- - -## 🎯 Repository Description and Tags - -### Recommended Repository Description - -**Short Version (GitHub):** -``` -AffineScript: The game developer's secret weapon. AGPL-3.0 game content with PMPL-1.0 core technology. Compiles to WASM with compiler-proven correctness. Built on Gossamer (PMPL) and Burble (PMPL) foundations. -``` - -**Long Version (README):** -```markdown -# AffineScript: The Game Developer's Secret Weapon - -**AffineScript** is a revolutionary game development platform featuring: - -🎮 **Game Content** (AGPL-3.0-or-later) -- Open source game assets and modifications -- Community-driven development -- Ensured accessibility for all players - -💻 **Core Technology** (MPL-2.0) -- Affine-type programming language -- Compiler-proven correctness -- WebAssembly compilation -- Permissive tooling license - -🛡️ **Foundational Technologies** (PMPL-1.0/MPL-2.0) -- **Gossamer**: Linearly-typed webview shell -- **Burble**: High-assurance multiplayer communications -- Ethical use requirements -- Quantum-safe provenance - -**Built for:** Game developers who want bug-free code, type-safe game logic, and compiler-enforced resource management. - -**Licensing:** Dual licensing model ensures open game content while providing permissive tooling licenses. -``` - -### Recommended GitHub Topics - -``` -retro-game, pmpl, palimpsest-mpl, mpl-2-0-derived, agpl-3-0, game-development, -wasm, affine-types, type-safety, game-engine, open-source, ethical-licensing, -quantum-safe, provenance, linear-types, resource-safety -``` - -### Repository Tags - -**Version Tags:** -- `v0.1.0-alpha.1` (Current) -- `v0.1.0-alpha` (Alpha base) -- `game-agpl` (Game content license) -- `tech-pmpl` (Technology license) - -**Content Tags:** -- `game-content` (AGPL-3.0 content) -- `compiler` (PMPL-1.0 technology) -- `gossamer` (PMPL-1.0 foundation) -- `burble` (PMPL-1.0 foundation) - ---- - -## 📁 License Directory Structure - -``` -LICENSES/ -├── LICENSE # PMPL-1.0 (Primary) -├── LICENSE-AGPL-3.0 # AGPL-3.0 (Game Content) -├── LICENSE-PMPL-1.0 # PMPL-1.0 (Core Tech) -├── LICENSE-MPL-2.0 # MPL-2.0 (Reference) -├── EXHIBIT-A-ETHICAL-USE.txt # Ethical guidelines -├── EXHIBIT-B-QUANTUM-SAFE.txt # Quantum-safe specs -└── README.md # License guide -``` - ---- - -## 🔧 Addressing Dependabot Alerts - -### atty Potential Unaligned Read (Rust) - -**Alert Summary:** -- **Package:** atty (Rust) -- **Version:** <= 0.2.14 -- **Issue:** Potential unaligned pointer dereference on Windows -- **Severity:** Medium (theoretical risk) -- **Status:** Unmaintained package - -**Analysis:** -```markdown -✅ **Actual Risk:** Low -- System allocator on Windows uses HeapAlloc -- HeapAlloc guarantees sufficient alignment -- Issue only manifests with custom global allocators - -⚠️ **Theoretical Risk:** -- Custom allocators could cause alignment issues -- Unaligned pointer dereference possible -- Potential for crashes or undefined behavior - -❌ **Mitigation Challenges:** -- Package unmaintained (last release: ~3 years ago) -- Maintainer unreachable -- No official patches available -``` - -**Recommended Actions:** - -#### 1. **Immediate (Low Effort)** -```markdown -✅ Add to dependency documentation: -``` -# Known Issues - -## atty (Rust) -- Version: 0.2.14 (via transitive dependency) -- Issue: Potential unaligned read on Windows -- Risk: Low (mitigated by System allocator) -- Status: Unmaintained -- Workaround: None needed (System allocator provides safety) -``` -``` - -#### 2. **Short-Term (Medium Effort)** -```markdown -🔄 Update Cargo.toml to document: -```toml -[package.metadata.dependency-issues] -atty = "Potential unaligned read (Windows only). Mitigated by System allocator. No action required." -``` - -🔄 Add to security policy: -```markdown -### Known Vulnerabilities - -#### atty (Transitive Dependency) -- **CVE:** None assigned -- **Affected:** Windows systems with custom allocators -- **Mitigation:** System allocator provides safety -- **Status:** Monitoring for updates -- **Action:** None required for standard configurations -``` -``` - -#### 3. **Long-Term (Future Consideration)** -```markdown -🚀 Evaluate alternatives when feasible: - -**Option 1: std::io::IsTerminal (Rust 1.70+)** -```rust -use std::io::IsTerminal as _; -let is_terminal = std::io::stdin().is_terminal(); -``` -- ✅ Stable since Rust 1.70.0 -- ✅ No external dependencies -- ❌ Requires Rust 1.70+ - -**Option 2: is-terminal (Standalone Crate)** -```toml -[dependencies] -is-terminal = "0.4" -``` -- ✅ Actively maintained -- ✅ Supports older Rust versions -- ✅ Cross-platform -- ❌ Additional dependency - -**Option 3: Custom Implementation** -```rust -#[cfg(windows)] -fn is_terminal() -> bool { - // Windows-specific implementation - unsafe { - let handle = winapi::um::processenv::GetStdHandle( - winapi::um::winbase::STD_INPUT_HANDLE - ); - let mut mode: winapi::um::wincon::DWORD = 0; - winapi::um::wincon::GetConsoleMode(handle, &mut mode) != 0 - } -} -``` -- ✅ No dependencies -- ✅ Full control -- ❌ Platform-specific code -- ❌ Maintenance burden -``` - -**Decision:** -```markdown -📋 **Current Status:** No action required - -✅ **Rationale:** -- System allocator mitigates risk -- No known exploits in wild -- Low severity issue -- Package used transitively (via clap) - -🔍 **Monitoring:** -- Watch for maintainer activity -- Track Rust ecosystem developments -- Re-evaluate at next major version - -🚀 **Future:** Consider migration when: -- Alternative provides clear benefits -- Migration cost justified -- Breaking changes acceptable -``` - ---- - -## 📝 License Compliance Checklist - -### For Game Distributors -- [ ] Include LICENSE-AGPL-3.0 file -- [ ] Include LICENSE-PMPL-1.0 file -- [ ] Provide source code access (AGPL requirement) -- [ ] Document modifications (AGPL requirement) -- [ ] Preserve copyright notices -- [ ] Include license guide - -### For Technology Users -- [ ] Include LICENSE-PMPL-1.0 file -- [ ] Preserve copyright notices -- [ ] Document modifications -- [ ] Follow ethical use guidelines -- [ ] Include license guide - -### For Repository Maintainers -- [ ] Maintain separate license files -- [ ] Update license headers -- [ ] Document licensing clearly -- [ ] Monitor dependency alerts -- [ ] Update security policy - ---- - -## 🔗 License Resources - -### Full License Texts -- **AGPL-3.0:** https://www.gnu.org/licenses/agpl-3.0.html -- **PMPL-1.0:** https://github.com/hyperpolymath/palimpsest-license -- **MPL-2.0:** https://www.mozilla.org/en-US/MPL/2.0/ - -### License Identification -- **SPDX AGPL-3.0:** `AGPL-3.0-or-later` -- **SPDX PMPL-1.0:** `MPL-2.0` -- **SPDX MPL-2.0:** `MPL-2.0` - -### Compliance Tools -- **REUSE:** https://reuse.software/ -- **FOSSA:** https://fossa.com/ -- **Licensee:** https://github.com/licensee/licensee - ---- - -## 🎯 Summary - -### Licensing Structure -``` -Game Content (AGPL-3.0) → Open game development -Core Technology (PMPL-1.0) → Permissive tooling -Foundational Tech (PMPL/MPL-2.0) → Ethical infrastructure -``` - -### Key Points -1. **Dual licensing** ensures open games with permissive tools -2. **PMPL-1.0** extends MPL-2.0 with ethical use requirements -3. **AGPL-3.0** ensures game modifications remain open -4. **All licenses** are OSI-approved and compatible -5. **Clear separation** between game content and technology - -### Action Items -- [ ] Update repository description and tags -- [ ] Ensure license directory has all versions -- [ ] Document atty dependency status -- [ ] Add license compliance checklist -- [ ] Update contributing guidelines - ---- - -**Last Updated:** March 31, 2026 -**Version:** Alpha-1 -**Status:** Complete licensing documentation - -SPDX-License-Identifier: CC-BY-SA-4.0 AND MPL-2.0 AND MPL-2.0 -SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell and contributors \ No newline at end of file diff --git a/docs/governance/SECURITY.adoc b/docs/governance/SECURITY.adoc new file mode 100644 index 00000000..8ce97f61 --- /dev/null +++ b/docs/governance/SECURITY.adoc @@ -0,0 +1,37 @@ +== Security Policy + +=== Supported Versions + +[cols=",",options="header",] +|=== +|Version |Supported +|0.1.x |:white_check_mark: +|=== + +=== Reporting a Vulnerability + +If you discover a security vulnerability in AffineScript, please report +it by: + +[arabic] +. *Do NOT* open a public GitHub issue +. Email the maintainers directly (see GitHub profile) +. Include: +* Description of the vulnerability +* Steps to reproduce +* Potential impact +* Suggested fix (if any) + +We will respond within 48 hours and work with you to understand and +address the issue. + +=== Security Considerations + +AffineScript is a compiler that: - Reads source files from disk - +Produces WebAssembly output - Does not execute network operations - Does +not execute arbitrary code during compilation + +The primary security concerns are: - Malicious input causing compiler +crashes (DoS) - Generated WASM with unintended behavior + +We take these seriously and appreciate responsible disclosure. diff --git a/docs/governance/SECURITY.md b/docs/governance/SECURITY.md deleted file mode 100644 index d1cdd6c3..00000000 --- a/docs/governance/SECURITY.md +++ /dev/null @@ -1,35 +0,0 @@ -# Security Policy - -## Supported Versions - -| Version | Supported | -| ------- | ------------------ | -| 0.1.x | :white_check_mark: | - -## Reporting a Vulnerability - -If you discover a security vulnerability in AffineScript, please report it by: - -1. **Do NOT** open a public GitHub issue -2. Email the maintainers directly (see GitHub profile) -3. Include: - - Description of the vulnerability - - Steps to reproduce - - Potential impact - - Suggested fix (if any) - -We will respond within 48 hours and work with you to understand and address the issue. - -## Security Considerations - -AffineScript is a compiler that: -- Reads source files from disk -- Produces WebAssembly output -- Does not execute network operations -- Does not execute arbitrary code during compilation - -The primary security concerns are: -- Malicious input causing compiler crashes (DoS) -- Generated WASM with unintended behavior - -We take these seriously and appreciate responsible disclosure. diff --git a/docs/history/MODULE-SYSTEM-PROGRESS.adoc b/docs/history/MODULE-SYSTEM-PROGRESS.adoc new file mode 100644 index 00000000..b3a768d9 --- /dev/null +++ b/docs/history/MODULE-SYSTEM-PROGRESS.adoc @@ -0,0 +1,475 @@ +== AffineScript Compiler Implementation Progress + +*Session Date:* 2026-01-23 *Status:* Phases 1 & 2 Complete - Moving to +Phase 3 + +=== 3-Phase Implementation Plan + +==== Phase 1: Module System ✅ COMPLETE + +*Goal:* Finish the remaining 10% of module system implementation + +*Blocking Issue:* Type information wasn’t being transferred during +imports - Symbols were registered but `+var_types+` hashtable entries +weren’t copied - Result: `+CannotInfer+` errors when using imported +functions + +*Solution Implemented:* - Created `+resolve_and_typecheck_module+` to +type-check modules before importing - Modified +`+import_resolved_symbols+` to copy type information: +`+ocaml match Hashtbl.find_opt source_types sym.Symbol.sym_id with | Some scheme -> Hashtbl.replace dest_types sym.Symbol.sym_id scheme | None -> ()+` +- Updated `+resolve_program_with_loader+` to return both resolution and +type contexts - Fixed signature issues (result type arity, Types.scheme +vs Typecheck.scheme) + +*Files Modified:* - lib/resolve.ml (+150 lines) - bin/main.ml +(integrated module loader) + +*Tests Passing:* - ✅ test_simple_import.affine - Single function import +- ✅ test_import.affine - Multiple imports (Core + Math) - ✅ +test_math_functions.affine - Complex math operations + +*Commit:* `+a1b2c3d+` "`fix: Transfer type information during module +imports`" + +==== Phase 2: Function Calls in WASM ✅ COMPLETE + +*Goal:* Implement function call compilation to WebAssembly + +*Problem:* WASM codegen couldn’t compile function calls - ExprApp +returned "`not yet supported`" + +*Solution Implemented:* - Added `+func_indices: (string * int) list+` to +codegen context - Implemented ExprApp case to: * Evaluate arguments +left-to-right * Look up function index from func_indices map * Generate +Call instruction with correct index - Modified TopFn to register +function name-to-index mappings before generation + +*Files Modified:* - lib/codegen.ml (~30 lines added) + +*Tests Passing:* - ✅ test_function_call.affine - Simple helper function +(returns 42) - ✅ test_recursive_call.affine - Factorial recursion +(returns 120) - ✅ test_multiple_calls.affine - Multiple functions + +composition (returns 135) - All tests verified with Node.js WASM +execution + +*Commit:* `+31a60c5+` "`feat: Implement function calls in WASM codegen +(Phase 2 complete)`" + +==== Phase 3: Advanced Type System Features 🔨 IN PROGRESS + +*Goal:* Expand type system capabilities for AffineScript’s unique +features + +*Remaining Type System Features:* + +===== 3.1 Dependent Types + +* Type-level computation +* Refined types (e.g., `+Vec n Int+` where n is a value) +* Proof-carrying code support + +===== 3.2 Row Polymorphism + +* Extensible records: `+{ x: Int | r }+` +* Polymorphic variants with row types +* Effect row types for effect system + +===== 3.3 Effect System Inference + +* Effect annotations: `+fn foo() -> Int [IO, State]+` +* Effect polymorphism +* Effect handler type checking +* Integration with borrow checker + +===== 3.4 Linear Types Refinement + +* Full affine type tracking +* Uniqueness types +* Integration with existing borrow checker + +===== 3.5 Higher-Kinded Types + +* Type constructors as parameters +* Functor, Applicative, Monad instances +* Generic programming abstractions + +*Implementation Strategy:* 1. Start with row polymorphism (foundation +for effects) 2. Add effect inference (builds on rows) 3. Implement +dependent types (most complex) 4. Refine linear types (integrate with +borrow checker) 5. Add higher-kinded types (advanced generics) + +*Current Status:* Planning phase + +''''' + +=== Module System Infrastructure (Phase 1 - Complete) + +==== 1. Module Loader Created ✅ + +*File:* `+lib/module_loader.ml+` (272 lines) + +*Features:* - Module path to file path resolution (`+Math.Geometry+` → +`+stdlib/Math/Geometry.affine+`) - Configurable search paths (stdlib, +current dir, additional paths) - Module file parsing and caching - +Circular dependency detection - Dependency loading (imports within +modules) + +*Configuration:* - `+AFFINESCRIPT_STDLIB+` environment variable support +- Default stdlib path: `+./stdlib+` - Search order: current dir → stdlib +→ additional paths + +==== 2. Resolution System Enhanced ✅ + +*File:* `+lib/resolve.ml+` (+150 lines) + +*Key Functions:* - `+resolve_and_typecheck_module+`: Resolve AND +type-check before importing - `+import_resolved_symbols+`: Import public +symbols with type info - `+import_specific_items+`: Import selected +symbols with type info - `+resolve_program_with_loader+`: Full program +resolution with modules + +*Features:* - Selective imports: `+use Core::{min, max}+` ✅ - Glob +imports: `+use Core::*+` ✅ - Visibility checking (Public, PubCrate) ✅ +- Type information transfer ✅ + +==== 3. Standard Library Fixed ✅ + +*Core.affine:* - Removed underscore-prefixed parameters - Removed +lambdas (parser limitation) - Added explicit `+return+` statements - +Status: ✅ Working + +*Math.affine:* - Converted `+const+` to functions - Removed float +operations (type checker limitation) - Added explicit `+return+` +statements - Status: ✅ Working + +=== Function Call Implementation (Phase 2 - Complete) + +==== Code Generation Enhancement + +*Context Enhancement:* + +[source,ocaml] +---- +type context = { + (* ... existing fields ... *) + func_indices : (string * int) list; (* name -> index map *) +} +---- + +*ExprApp Implementation:* + +[source,ocaml] +---- +| ExprApp (func_expr, args) -> + (* 1. Evaluate arguments left-to-right *) + let* (ctx_final, all_arg_code) = + List.fold_left (fun acc arg -> ...) (Ok (ctx, [])) args in + + (* 2. Look up function index *) + match func_expr with + | ExprVar id -> + match List.assoc_opt id.name ctx_final.func_indices with + | Some func_idx -> Ok (ctx_final, all_arg_code @ [Call func_idx]) + | None -> Error (UnboundVariable ...) + | _ -> Error (UnsupportedFeature "Indirect calls") +---- + +*Function Registration:* + +[source,ocaml] +---- +| TopFn fd -> + (* Register function name before generation *) + let func_idx = List.length ctx.funcs in + let ctx' = { ctx with + func_indices = ctx.func_indices @ [(fd.fd_name.name, func_idx)] + } in + (* Now gen_function can look up other functions *) +---- + +==== Test Coverage + +[cols=",,,",options="header",] +|=== +|Test |Feature |Expected |Result +|test_function_call.affine |Simple call |42 |✅ PASS +|test_recursive_call.affine |Recursion |120 |✅ PASS +|test_multiple_calls.affine |Composition |135 |✅ PASS +|=== + +=== Module System Features Status + +[width="100%",cols="38%,33%,29%",options="header",] +|=== +|Feature |Status |Notes +|Module loading |✅ |File system search, parsing + +|Dependency resolution |✅ |Recursive loading + +|Circular dep detection |✅ |Prevents infinite loops + +|Selective imports |✅ |`+use A::{x, y}+` + +|Glob imports |✅ |`+use A::*+` + +|Visibility checking |✅ |Public/PubCrate filtering + +|Symbol registration |✅ |Symbols added to table + +|Type information transfer |✅ |*FIXED* + +|Cross-module constructor codegen |✅ |Directly-imported enum +constructors (`+use prelude::{Option, Some, None}+`) lower on every +backend (#138) + +|Re-exports (transitive) |❌ |A module surfacing names it itself +imported (`+use option+` → prelude’s `+Option+`) — not implemented + +|Nested modules |❌ |Not implemented +|=== + +=== Known Limitations + +==== Parser Limitations + +[arabic] +. *No const declarations* - Had to convert to functions +. *No lambda expressions* - Removed from stdlib +. *No implicit returns* - Must use `+return+` everywhere +. *No underscore parameters* - `+_x+` not allowed + +==== Type Checker Limitations + +[arabic] +. *No Float comparisons* - Float operations removed +. *No function types as parameters* - Higher-order functions don’t work +yet +. *Limited polymorphism* - Working on row polymorphism +. *No dependent types* - Phase 3 feature +. *No effect inference* - Phase 3 feature + +==== WASM Codegen Limitations + +[arabic] +. *No indirect calls* - Function pointers not supported yet +. *No closures* - Would require heap allocation +. *No exceptions* - Effect system will handle this +. *Limited types* - Only I32/F64, no structs yet + +==== Module System Limitations + +[arabic] +. *No re-exports* - Can’t `+pub use+` to re-export +. *No nested modules* - Only flat hierarchy +. *No module-qualified calls* - Can’t call `+Math.pow()+` after +`+use Math+` + +=== Architecture Decisions + +==== 1. Module Loader is Parse-Only + +*Decision:* Module_loader only handles file loading and parsing + +*Rationale:* Avoids circular dependency between Module_loader and +Resolve modules + +*Benefits:* - Clean separation of concerns - No circular dependencies - +Resolve module controls all symbol resolution logic + +==== 2. Per-Module Symbol Tables + +*Decision:* Each loaded module gets its own symbol table during +resolution + +*Rationale:* Modules should have isolated namespaces + +*Benefits:* - Clean module boundaries - No symbol pollution between +modules - Easy to track what’s public vs private + +==== 3. Type-Check Before Import + +*Decision:* Modules are fully type-checked before their symbols are +imported + +*Rationale:* Ensures imported functions have valid types + +*Benefits:* - Type errors caught at module boundary - Type schemes +available for import - Cleaner error messages + +=== What We Have Now (After Phases 1 & 2) + +==== ✅ Complete + +* Lexer (tokens, spans, error reporting) +* Parser (full syntax, imports, patterns, effects) +* AST (comprehensive node types) +* Symbol resolution (scoping, modules, imports) +* Type checking (basic inference, annotations) +* Borrow checker (affine types, use-after-move) +* Interpreter (evaluation, standard library) +* REPL (interactive development) +* Module system (loading, importing, type transfer) +* WASM codegen (expressions, function calls) + +==== 🔨 Partial + +* Type system (basic inference works, advanced features pending) +* WASM codegen (basic features work, missing closures/structs) +* Standard library (Core + Math work, Option/Result need fixes) + +==== ❌ Not Started + +* Dependent types +* Row polymorphism +* Effect inference +* Higher-kinded types +* Advanced WASM features (closures, exceptions, structs) + +=== Next Steps (Phase 3) + +==== Immediate + +[arabic] +. Implement row polymorphism for records +. Add effect system type checking +. Integrate effects with borrow checker + +==== Short-term + +[arabic] +. Fix Option.affine and Result.affine (explicit returns) +. Add more stdlib modules +. Improve error messages + +==== Medium-term + +[arabic] +. Implement dependent types +. Add higher-kinded types +. Complete WASM features (closures, structs) + +==== Long-term + +[arabic] +. Self-hosting (compiler written in AffineScript) +. Proof-carrying code +. Formal verification integration + +=== Session Summary + +*Date:* 2026-01-23 *Tasks Completed:* Priority #1 and #2 from "`1 2 3`" +directive + +==== Phase 1: Module System (✅ Complete) + +* Fixed type information transfer during imports +* All module import tests passing +* Standard library usable + +==== Phase 2: Function Calls (✅ Complete) + +* Implemented WASM function call codegen +* All call tests passing (simple, recursive, composition) +* WASM output verified with Node.js + +*Total Changes:* - ~650 lines added (Phase 1) - ~30 lines added (Phase +2) - 12 files modified - 8 test files created - 2 major features +completed + +*Commits:* 1. Phase 1: Type information transfer fix 2. Phase 2: +Function call implementation + +*Current State:* Ready to begin Phase 3 (Advanced Type System) + +''''' + +=== Decision: stdlib namespace model (2026-05-17, issue #132 / ADR-011) + +Settles the open question gating the #128 stdlib-AOT epic: does the +stdlib have real modules, or stay flat-and-deduplicated? + +*Decision: real modules with qualified paths.* Not a flat de-duplicated +prelude. Status: *accepted, settled* (ADR-011 in +`+.machine_readable/descriptiles/META.a2ml+`; ledger entry in +`+docs/specs/SETTLED-DECISIONS.adoc+`). + +==== Rationale + +The compiler already has the machinery — the grammar accepts +`+module X;+`, `+use path;+` and `+::+`-qualified paths; +`+module_loader.ml+` resolves module paths with search paths, nested +modules and caching; and the _newer_ stdlib files (Core, Crypto, Ajv, +Sqlite, Grammy, Deno, Network, Vscode, VscodeLanguageClient) already +declare `+module X;+` and use qualified constructors +(`+Ordering::Less+`). Only the legacy core files (prelude, option, +result, collections, string, io, testing, effects, math, traits) are +flat interpreter-era code with conflicting duplicate definitions +(`+prelude.map(arr, f)+` vs `+option.map(f, opt)+`). Choosing real +modules aligns the legacy files with the model the language already +commits to, and makes the AOT pipeline exercise real cross-module +resolution — the actual objective of #128. + +==== Model + +* Every `+stdlib/*.affine+` declares `+module ;+`. +* Cross-file use is explicit: `+use option::{Option, Some, None};+` / +qualified `+Result::unwrap+`. +* Exactly one canonical definition per name, owned by its module. The +prelude/option/result overlaps are resolved by single ownership; the +others `+use+` the owner (no signature-divergent copies). +* A minimal prelude module may _re-export_ the universally needed names +(`+Option+`, `+Result+`, `+Some+`/`+None+`/`+Ok+`/`+Err+`) — re-exports +only. +* The b895374 seeded `+Some/None/Ok/Err+` builtins are removed once +resolution flows through the module path (#138); not load-bearing. + +==== Downstream sequencing (this epic) + +[width="100%",cols="50%,50%",options="header",] +|=== +|Issue |Work unlocked by this decision +|#133 |Remove prelude/option/result conflicting dups via single +ownership; non-owners `+use+` the owner. + +|#135 |Bring legacy core files under `+module+`/`+use+` as each is made +to compile resolve→typecheck→codegen. + +|#137 |Multi-module integration test (`+use+`s several stdlib modules +together) — now a meaningful test. + +|#138 |Delete the b895374 seeded-builtins band-aid once the prelude +re-export module exists. +|=== + +No code change in #132 (decision + documentation only). + +==== #138 codegen follow-up (2026-06-20) + +Removing the `+b895374+` seeded `+Some/None/Ok/Err+` builtins (front-end +half of #138) correctly routed those constructors through the module +path, so `+check+` passes — but it surfaced a codegen gap: a consumer +that imports prelude’s `+Option+`/`+Result+` and applies their +constructors type-checked yet failed to compile, because the backends +learn variant tags only from `+TopType+` decls and imported types never +reached them. + +* *Core-Wasm backend* (`+Codegen.gen_imports+`): wired up only `+TopFn+` +(→ wasm import) and `+TopConst+` (→ global); imported types were +dropped. It now also registers the constructor tags / struct layouts of +imported public types, reusing the local-type registration in +`+gen_decl+`. +* *Other backends* (Deno / JS / Julia / C / Rust / …): +`+Module_loader.flatten_imports+` now inlines imported public +`+TopType+` decls (a separate namespace from fn/const, local-wins, +deduped) so the `+prog_decls+`-iterating codegens see them. + +Scope: *directly-imported* constructors lower on every backend. +*Transitive re-export* (a module re-exposing constructors it itself +imported) remains unimplemented — see the status table above. Unrelated +and still open: the core-Wasm pattern-codegen gap for tuple patterns +(`+UnsupportedFeature "Only variable and wildcard patterns supported in tuple patterns"+`, +which `+stdlib/option.affine+` / `+result.affine+` hit) and the +mixed-representation match of a zero-arg variant against a +constructor-with-args arm — both reproduce with purely local enums and +are independent of cross-module linking. diff --git a/docs/history/MODULE-SYSTEM-PROGRESS.md b/docs/history/MODULE-SYSTEM-PROGRESS.md deleted file mode 100644 index 425907af..00000000 --- a/docs/history/MODULE-SYSTEM-PROGRESS.md +++ /dev/null @@ -1,429 +0,0 @@ -# AffineScript Compiler Implementation Progress - -**Session Date:** 2026-01-23 -**Status:** Phases 1 & 2 Complete - Moving to Phase 3 - -## 3-Phase Implementation Plan - -### Phase 1: Module System ✅ COMPLETE - -**Goal:** Finish the remaining 10% of module system implementation - -**Blocking Issue:** Type information wasn't being transferred during imports -- Symbols were registered but `var_types` hashtable entries weren't copied -- Result: `CannotInfer` errors when using imported functions - -**Solution Implemented:** -- Created `resolve_and_typecheck_module` to type-check modules before importing -- Modified `import_resolved_symbols` to copy type information: - ```ocaml - match Hashtbl.find_opt source_types sym.Symbol.sym_id with - | Some scheme -> Hashtbl.replace dest_types sym.Symbol.sym_id scheme - | None -> () - ``` -- Updated `resolve_program_with_loader` to return both resolution and type contexts -- Fixed signature issues (result type arity, Types.scheme vs Typecheck.scheme) - -**Files Modified:** -- lib/resolve.ml (+150 lines) -- bin/main.ml (integrated module loader) - -**Tests Passing:** -- ✅ test_simple_import.affine - Single function import -- ✅ test_import.affine - Multiple imports (Core + Math) -- ✅ test_math_functions.affine - Complex math operations - -**Commit:** `a1b2c3d` "fix: Transfer type information during module imports" - -### Phase 2: Function Calls in WASM ✅ COMPLETE - -**Goal:** Implement function call compilation to WebAssembly - -**Problem:** WASM codegen couldn't compile function calls - ExprApp returned "not yet supported" - -**Solution Implemented:** -- Added `func_indices: (string * int) list` to codegen context -- Implemented ExprApp case to: - * Evaluate arguments left-to-right - * Look up function index from func_indices map - * Generate Call instruction with correct index -- Modified TopFn to register function name-to-index mappings before generation - -**Files Modified:** -- lib/codegen.ml (~30 lines added) - -**Tests Passing:** -- ✅ test_function_call.affine - Simple helper function (returns 42) -- ✅ test_recursive_call.affine - Factorial recursion (returns 120) -- ✅ test_multiple_calls.affine - Multiple functions + composition (returns 135) -- All tests verified with Node.js WASM execution - -**Commit:** `31a60c5` "feat: Implement function calls in WASM codegen (Phase 2 complete)" - -### Phase 3: Advanced Type System Features 🔨 IN PROGRESS - -**Goal:** Expand type system capabilities for AffineScript's unique features - -**Remaining Type System Features:** - -#### 3.1 Dependent Types -- Type-level computation -- Refined types (e.g., `Vec n Int` where n is a value) -- Proof-carrying code support - -#### 3.2 Row Polymorphism -- Extensible records: `{ x: Int | r }` -- Polymorphic variants with row types -- Effect row types for effect system - -#### 3.3 Effect System Inference -- Effect annotations: `fn foo() -> Int [IO, State]` -- Effect polymorphism -- Effect handler type checking -- Integration with borrow checker - -#### 3.4 Linear Types Refinement -- Full affine type tracking -- Uniqueness types -- Integration with existing borrow checker - -#### 3.5 Higher-Kinded Types -- Type constructors as parameters -- Functor, Applicative, Monad instances -- Generic programming abstractions - -**Implementation Strategy:** -1. Start with row polymorphism (foundation for effects) -2. Add effect inference (builds on rows) -3. Implement dependent types (most complex) -4. Refine linear types (integrate with borrow checker) -5. Add higher-kinded types (advanced generics) - -**Current Status:** Planning phase - ---- - -## Module System Infrastructure (Phase 1 - Complete) - -### 1. Module Loader Created ✅ - -**File:** `lib/module_loader.ml` (272 lines) - -**Features:** -- Module path to file path resolution (`Math.Geometry` → `stdlib/Math/Geometry.affine`) -- Configurable search paths (stdlib, current dir, additional paths) -- Module file parsing and caching -- Circular dependency detection -- Dependency loading (imports within modules) - -**Configuration:** -- `AFFINESCRIPT_STDLIB` environment variable support -- Default stdlib path: `./stdlib` -- Search order: current dir → stdlib → additional paths - -### 2. Resolution System Enhanced ✅ - -**File:** `lib/resolve.ml` (+150 lines) - -**Key Functions:** -- `resolve_and_typecheck_module`: Resolve AND type-check before importing -- `import_resolved_symbols`: Import public symbols with type info -- `import_specific_items`: Import selected symbols with type info -- `resolve_program_with_loader`: Full program resolution with modules - -**Features:** -- Selective imports: `use Core::{min, max}` ✅ -- Glob imports: `use Core::*` ✅ -- Visibility checking (Public, PubCrate) ✅ -- Type information transfer ✅ - -### 3. Standard Library Fixed ✅ - -**Core.affine:** -- Removed underscore-prefixed parameters -- Removed lambdas (parser limitation) -- Added explicit `return` statements -- Status: ✅ Working - -**Math.affine:** -- Converted `const` to functions -- Removed float operations (type checker limitation) -- Added explicit `return` statements -- Status: ✅ Working - -## Function Call Implementation (Phase 2 - Complete) - -### Code Generation Enhancement - -**Context Enhancement:** -```ocaml -type context = { - (* ... existing fields ... *) - func_indices : (string * int) list; (* name -> index map *) -} -``` - -**ExprApp Implementation:** -```ocaml -| ExprApp (func_expr, args) -> - (* 1. Evaluate arguments left-to-right *) - let* (ctx_final, all_arg_code) = - List.fold_left (fun acc arg -> ...) (Ok (ctx, [])) args in - - (* 2. Look up function index *) - match func_expr with - | ExprVar id -> - match List.assoc_opt id.name ctx_final.func_indices with - | Some func_idx -> Ok (ctx_final, all_arg_code @ [Call func_idx]) - | None -> Error (UnboundVariable ...) - | _ -> Error (UnsupportedFeature "Indirect calls") -``` - -**Function Registration:** -```ocaml -| TopFn fd -> - (* Register function name before generation *) - let func_idx = List.length ctx.funcs in - let ctx' = { ctx with - func_indices = ctx.func_indices @ [(fd.fd_name.name, func_idx)] - } in - (* Now gen_function can look up other functions *) -``` - -### Test Coverage - -| Test | Feature | Expected | Result | -|------|---------|----------|--------| -| test_function_call.affine | Simple call | 42 | ✅ PASS | -| test_recursive_call.affine | Recursion | 120 | ✅ PASS | -| test_multiple_calls.affine | Composition | 135 | ✅ PASS | - -## Module System Features Status - -| Feature | Status | Notes | -|---------|--------|-------| -| Module loading | ✅ | File system search, parsing | -| Dependency resolution | ✅ | Recursive loading | -| Circular dep detection | ✅ | Prevents infinite loops | -| Selective imports | ✅ | `use A::{x, y}` | -| Glob imports | ✅ | `use A::*` | -| Visibility checking | ✅ | Public/PubCrate filtering | -| Symbol registration | ✅ | Symbols added to table | -| Type information transfer | ✅ | **FIXED** | -| Cross-module constructor codegen | ✅ | Directly-imported enum constructors (`use prelude::{Option, Some, None}`) lower on every backend (#138) | -| Re-exports (transitive) | ❌ | A module surfacing names it itself imported (`use option` → prelude's `Option`) — not implemented | -| Nested modules | ❌ | Not implemented | - -## Known Limitations - -### Parser Limitations -1. **No const declarations** - Had to convert to functions -2. **No lambda expressions** - Removed from stdlib -3. **No implicit returns** - Must use `return` everywhere -4. **No underscore parameters** - `_x` not allowed - -### Type Checker Limitations -1. **No Float comparisons** - Float operations removed -2. **No function types as parameters** - Higher-order functions don't work yet -3. **Limited polymorphism** - Working on row polymorphism -4. **No dependent types** - Phase 3 feature -5. **No effect inference** - Phase 3 feature - -### WASM Codegen Limitations -1. **No indirect calls** - Function pointers not supported yet -2. **No closures** - Would require heap allocation -3. **No exceptions** - Effect system will handle this -4. **Limited types** - Only I32/F64, no structs yet - -### Module System Limitations -1. **No re-exports** - Can't `pub use` to re-export -2. **No nested modules** - Only flat hierarchy -3. **No module-qualified calls** - Can't call `Math.pow()` after `use Math` - -## Architecture Decisions - -### 1. Module Loader is Parse-Only - -**Decision:** Module_loader only handles file loading and parsing - -**Rationale:** Avoids circular dependency between Module_loader and Resolve modules - -**Benefits:** -- Clean separation of concerns -- No circular dependencies -- Resolve module controls all symbol resolution logic - -### 2. Per-Module Symbol Tables - -**Decision:** Each loaded module gets its own symbol table during resolution - -**Rationale:** Modules should have isolated namespaces - -**Benefits:** -- Clean module boundaries -- No symbol pollution between modules -- Easy to track what's public vs private - -### 3. Type-Check Before Import - -**Decision:** Modules are fully type-checked before their symbols are imported - -**Rationale:** Ensures imported functions have valid types - -**Benefits:** -- Type errors caught at module boundary -- Type schemes available for import -- Cleaner error messages - -## What We Have Now (After Phases 1 & 2) - -### ✅ Complete -- Lexer (tokens, spans, error reporting) -- Parser (full syntax, imports, patterns, effects) -- AST (comprehensive node types) -- Symbol resolution (scoping, modules, imports) -- Type checking (basic inference, annotations) -- Borrow checker (affine types, use-after-move) -- Interpreter (evaluation, standard library) -- REPL (interactive development) -- Module system (loading, importing, type transfer) -- WASM codegen (expressions, function calls) - -### 🔨 Partial -- Type system (basic inference works, advanced features pending) -- WASM codegen (basic features work, missing closures/structs) -- Standard library (Core + Math work, Option/Result need fixes) - -### ❌ Not Started -- Dependent types -- Row polymorphism -- Effect inference -- Higher-kinded types -- Advanced WASM features (closures, exceptions, structs) - -## Next Steps (Phase 3) - -### Immediate -1. Implement row polymorphism for records -2. Add effect system type checking -3. Integrate effects with borrow checker - -### Short-term -1. Fix Option.affine and Result.affine (explicit returns) -2. Add more stdlib modules -3. Improve error messages - -### Medium-term -1. Implement dependent types -2. Add higher-kinded types -3. Complete WASM features (closures, structs) - -### Long-term -1. Self-hosting (compiler written in AffineScript) -2. Proof-carrying code -3. Formal verification integration - -## Session Summary - -**Date:** 2026-01-23 -**Tasks Completed:** Priority #1 and #2 from "1 2 3" directive - -### Phase 1: Module System (✅ Complete) -- Fixed type information transfer during imports -- All module import tests passing -- Standard library usable - -### Phase 2: Function Calls (✅ Complete) -- Implemented WASM function call codegen -- All call tests passing (simple, recursive, composition) -- WASM output verified with Node.js - -**Total Changes:** -- ~650 lines added (Phase 1) -- ~30 lines added (Phase 2) -- 12 files modified -- 8 test files created -- 2 major features completed - -**Commits:** -1. Phase 1: Type information transfer fix -2. Phase 2: Function call implementation - -**Current State:** Ready to begin Phase 3 (Advanced Type System) - ---- - -## Decision: stdlib namespace model (2026-05-17, issue #132 / ADR-011) - -Settles the open question gating the #128 stdlib-AOT epic: does the stdlib -have real modules, or stay flat-and-deduplicated? - -**Decision: real modules with qualified paths.** Not a flat de-duplicated -prelude. Status: **accepted, settled** (ADR-011 in -`.machine_readable/descriptiles/META.a2ml`; ledger entry in -`docs/specs/SETTLED-DECISIONS.adoc`). - -### Rationale - -The compiler already has the machinery — the grammar accepts `module X;`, -`use path;` and `::`-qualified paths; `module_loader.ml` resolves module -paths with search paths, nested modules and caching; and the *newer* -stdlib files (Core, Crypto, Ajv, Sqlite, Grammy, Deno, Network, Vscode, -VscodeLanguageClient) already declare `module X;` and use qualified -constructors (`Ordering::Less`). Only the legacy core files (prelude, -option, result, collections, string, io, testing, effects, math, traits) -are flat interpreter-era code with conflicting duplicate definitions -(`prelude.map(arr, f)` vs `option.map(f, opt)`). Choosing real modules -aligns the legacy files with the model the language already commits to, -and makes the AOT pipeline exercise real cross-module resolution — the -actual objective of #128. - -### Model - -- Every `stdlib/*.affine` declares `module ;`. -- Cross-file use is explicit: `use option::{Option, Some, None};` / - qualified `Result::unwrap`. -- Exactly one canonical definition per name, owned by its module. The - prelude/option/result overlaps are resolved by single ownership; the - others `use` the owner (no signature-divergent copies). -- A minimal prelude module may *re-export* the universally needed names - (`Option`, `Result`, `Some`/`None`/`Ok`/`Err`) — re-exports only. -- The b895374 seeded `Some/None/Ok/Err` builtins are removed once - resolution flows through the module path (#138); not load-bearing. - -### Downstream sequencing (this epic) - -| Issue | Work unlocked by this decision | -|---|---| -| #133 | Remove prelude/option/result conflicting dups via single ownership; non-owners `use` the owner. | -| #135 | Bring legacy core files under `module`/`use` as each is made to compile resolve→typecheck→codegen. | -| #137 | Multi-module integration test (`use`s several stdlib modules together) — now a meaningful test. | -| #138 | Delete the b895374 seeded-builtins band-aid once the prelude re-export module exists. | - -No code change in #132 (decision + documentation only). - -### #138 codegen follow-up (2026-06-20) - -Removing the `b895374` seeded `Some/None/Ok/Err` builtins (front-end half of -#138) correctly routed those constructors through the module path, so `check` -passes — but it surfaced a codegen gap: a consumer that imports prelude's -`Option`/`Result` and applies their constructors type-checked yet failed to -compile, because the backends learn variant tags only from `TopType` decls and -imported types never reached them. - -- **Core-Wasm backend** (`Codegen.gen_imports`): wired up only `TopFn` - (→ wasm import) and `TopConst` (→ global); imported types were dropped. It now - also registers the constructor tags / struct layouts of imported public types, - reusing the local-type registration in `gen_decl`. -- **Other backends** (Deno / JS / Julia / C / Rust / …): `Module_loader.flatten_imports` - now inlines imported public `TopType` decls (a separate namespace from - fn/const, local-wins, deduped) so the `prog_decls`-iterating codegens see them. - -Scope: **directly-imported** constructors lower on every backend. **Transitive -re-export** (a module re-exposing constructors it itself imported) remains -unimplemented — see the status table above. Unrelated and still open: the -core-Wasm pattern-codegen gap for tuple patterns (`UnsupportedFeature "Only -variable and wildcard patterns supported in tuple patterns"`, which -`stdlib/option.affine` / `result.affine` hit) and the mixed-representation match -of a zero-arg variant against a constructor-with-args arm — both reproduce with -purely local enums and are independent of cross-module linking. diff --git a/docs/reference/ABI-FFI.md b/docs/reference/ABI-FFI.adoc similarity index 74% rename from docs/reference/ABI-FFI.md rename to docs/reference/ABI-FFI.adoc index ecc8ac99..56210c05 100644 --- a/docs/reference/ABI-FFI.md +++ b/docs/reference/ABI-FFI.adoc @@ -1,19 +1,22 @@ -{{~ Aditionally delete this line and fill out the template below ~}} +\{\{~ Aditionally delete this line and fill out the template below ~}} -# {{PROJECT}} ABI/FFI Documentation +== \{\{PROJECT}} ABI/FFI Documentation -## Overview +=== Overview -This library follows the **Hyperpolymath RSR Standard** for ABI and FFI design: +This library follows the *Hyperpolymath RSR Standard* for ABI and FFI +design: -- **ABI (Application Binary Interface)** defined in **Idris2** with formal proofs -- **FFI (Foreign Function Interface)** implemented in **Zig** for C compatibility -- **Generated C headers** bridge Idris2 ABI to Zig FFI -- **Any language** can call through standard C ABI +* *ABI (Application Binary Interface)* defined in *Idris2* with formal +proofs +* *FFI (Foreign Function Interface)* implemented in *Zig* for C +compatibility +* *Generated C headers* bridge Idris2 ABI to Zig FFI +* *Any language* can call through standard C ABI -## Architecture +=== Architecture -``` +.... ┌─────────────────────────────────────────────┐ │ ABI Definitions (Idris2) │ │ src/abi/ │ @@ -45,11 +48,11 @@ This library follows the **Hyperpolymath RSR Standard** for ABI and FFI design: │ Any Language via C ABI │ │ - Rust, , Julia, Python, etc. │ └─────────────────────────────────────────────┘ -``` +.... -## Directory Structure +=== Directory Structure -``` +.... {{project}}/ ├── src/ │ ├── abi/ # ABI definitions (Idris2) @@ -77,15 +80,17 @@ This library follows the **Hyperpolymath RSR Standard** for ABI and FFI design: ├── rust/ ├── / └── julia/ -``` +.... -## Why Idris2 for ABI? +=== Why Idris2 for ABI? -### 1. **Formal Verification** +==== 1. *Formal Verification* -Idris2's dependent types allow proving properties about the ABI at compile-time: +Idris2’s dependent types allow proving properties about the ABI at +compile-time: -```idris +[source,idris] +---- -- Prove struct size is correct public export exampleStructSize : HasSize ExampleStruct 16 @@ -97,13 +102,14 @@ fieldAligned : Divides 8 (offsetOf ExampleStruct.field) -- Prove ABI is platform-compatible public export abiCompatible : Compatible (ABI 1) (ABI 2) -``` +---- -### 2. **Type Safety** +==== 2. *Type Safety* Encode invariants that C/Zig cannot express: -```idris +[source,idris] +---- -- Non-null pointer guaranteed at type level data Handle : Type where MkHandle : (ptr : Bits64) -> {auto 0 nonNull : So (ptr /= 0)} -> Handle @@ -111,13 +117,14 @@ data Handle : Type where -- Array with length proof data Buffer : (n : Nat) -> Type where MkBuffer : Vect n Byte -> Buffer n -``` +---- -### 3. **Platform Abstraction** +==== 3. *Platform Abstraction* Platform-specific types with compile-time selection: -```idris +[source,idris] +---- CInt : Platform -> Type CInt Linux = Bits32 CInt Windows = Bits32 @@ -125,13 +132,14 @@ CInt Windows = Bits32 CSize : Platform -> Type CSize Linux = Bits64 CSize Windows = Bits64 -``` +---- -### 4. **Safe Evolution** +==== 4. *Safe Evolution* Prove that new ABI versions are backward-compatible: -```idris +[source,idris] +---- -- Compiler enforces compatibility abiUpgrade : ABI 1 -> ABI 2 abiUpgrade old = MkABI2 { @@ -140,71 +148,78 @@ abiUpgrade old = MkABI2 { -- Can add new fields new_features = defaults } -``` +---- -## Why Zig for FFI? +=== Why Zig for FFI? -### 1. **C ABI Compatibility** +==== 1. *C ABI Compatibility* Zig exports C-compatible functions naturally: -```zig +[source,zig] +---- export fn library_function(param: i32) i32 { return param * 2; } -``` +---- -### 2. **Memory Safety** +==== 2. *Memory Safety* Compile-time safety without runtime overhead: -```zig +[source,zig] +---- // Null check enforced at compile time const handle = init() orelse return error.InitFailed; defer free(handle); -``` +---- -### 3. **Cross-Compilation** +==== 3. *Cross-Compilation* Built-in cross-compilation to any platform: -```bash +[source,bash] +---- zig build -Dtarget=x86_64-linux zig build -Dtarget=aarch64-macos zig build -Dtarget=x86_64-windows -``` +---- -### 4. **Zero Dependencies** +==== 4. *Zero Dependencies* No runtime, no libc required (unless explicitly needed): -```zig +[source,zig] +---- // Minimal binary size pub const lib = @import("std"); // Only includes what you use -``` +---- -## Building +=== Building -### Build FFI Library +==== Build FFI Library -```bash +[source,bash] +---- cd ffi/zig zig build # Build debug zig build -Doptimize=ReleaseFast # Build optimized zig build test # Run tests -``` +---- -### Generate C Header from Idris2 ABI +==== Generate C Header from Idris2 ABI -```bash +[source,bash] +---- cd src/abi idris2 --cg c-header Types.idr -o ../../generated/abi/{{project}}.h -``` +---- -### Cross-Compile +==== Cross-Compile -```bash +[source,bash] +---- cd ffi/zig # Linux x86_64 @@ -215,13 +230,14 @@ zig build -Dtarget=aarch64-macos # Windows x86_64 zig build -Dtarget=x86_64-windows -``` +---- -## Usage +=== Usage -### From C +==== From C -```c +[source,c] +---- #include "{{project}}.h" int main() { @@ -237,16 +253,19 @@ int main() { {{project}}_free(handle); return 0; } -``` +---- Compile with: -```bash + +[source,bash] +---- gcc -o example example.c -l{{project}} -L./zig-out/lib -``` +---- -### From Idris2 +==== From Idris2 -```idris +[source,idris] +---- import {{PROJECT}}.ABI.Foreign main : IO () @@ -259,11 +278,12 @@ main = do free handle putStrLn "Success" -``` +---- -### From Rust +==== From Rust -```rust +[source,rust] +---- #[link(name = "{{project}}")] extern "C" { fn {{project}}_init() -> *mut std::ffi::c_void; @@ -282,11 +302,12 @@ fn main() { {{project}}_free(handle); } } -``` +---- -### From Julia +==== From Julia -```julia +[source,julia] +---- const lib{{project}} = "lib{{project}}" function init() @@ -312,27 +333,30 @@ try finally cleanup(handle) end -``` +---- -## Testing +=== Testing -### Unit Tests (Zig) +==== Unit Tests (Zig) -```bash +[source,bash] +---- cd ffi/zig zig build test -``` +---- -### Integration Tests +==== Integration Tests -```bash +[source,bash] +---- cd ffi/zig zig build test-integration -``` +---- -### ABI Verification (Idris2) +==== ABI Verification (Idris2) -```idris +[source,idris] +---- -- Compile-time verification %runElab verifyABI @@ -342,44 +366,44 @@ main = do verifyLayoutsCorrect verifyAlignmentsCorrect putStrLn "ABI verification passed" -``` +---- -## Contributing +=== Contributing When modifying the ABI/FFI: -1. **Update ABI first** (`src/abi/*.idr`) - - Modify type definitions - - Update proofs - - Ensure backward compatibility - -2. **Generate C header** - ```bash - idris2 --cg c-header src/abi/Types.idr -o generated/abi/{{project}}.h - ``` - -3. **Update FFI implementation** (`ffi/zig/src/main.zig`) - - Implement new functions - - Match ABI types exactly - -4. **Add tests** - - Unit tests in Zig - - Integration tests - - ABI verification tests - -5. **Update documentation** - - Function signatures - - Usage examples - - Migration guide (if breaking changes) - -## License - -{{LICENSE}} - -## See Also - -- [Idris2 Documentation](https://idris2.readthedocs.io) -- [Zig Documentation](https://ziglang.org/documentation/master/) -- [Rhodium Standard Repositories](https://github.com/hyperpolymath/rhodium-standard-repositories) -- [FFI Migration Guide](../ffi-migration-guide.md) -- [ABI Migration Guide](../abi-migration-guide.md) +[arabic] +. *Update ABI first* (`+src/abi/*.idr+`) +* Modify type definitions +* Update proofs +* Ensure backward compatibility +. *Generate C header* ++ +[source,bash] +---- +idris2 --cg c-header src/abi/Types.idr -o generated/abi/{{project}}.h +---- +. *Update FFI implementation* (`+ffi/zig/src/main.zig+`) +* Implement new functions +* Match ABI types exactly +. *Add tests* +* Unit tests in Zig +* Integration tests +* ABI verification tests +. *Update documentation* +* Function signatures +* Usage examples +* Migration guide (if breaking changes) + +=== License + +\{\{LICENSE}} + +=== See Also + +* https://idris2.readthedocs.io[Idris2 Documentation] +* https://ziglang.org/documentation/master/[Zig Documentation] +* https://github.com/hyperpolymath/rhodium-standard-repositories[Rhodium +Standard Repositories] +* link:../ffi-migration-guide.md[FFI Migration Guide] +* link:../abi-migration-guide.md[ABI Migration Guide] diff --git a/editors/tree-sitter-affinescript/README.adoc b/editors/tree-sitter-affinescript/README.adoc new file mode 100644 index 00000000..99b70cb3 --- /dev/null +++ b/editors/tree-sitter-affinescript/README.adoc @@ -0,0 +1,87 @@ +== tree-sitter-affinescript + +Tree-sitter grammar for AffineScript - affine types, effects, and +dependent types. + +=== Features + +* *Incremental parsing* - Fast, efficient parsing for large files +* *Syntax highlighting* - Semantic token-based highlighting +* *Code navigation* - Structural queries for IDE features +* *Error recovery* - Continues parsing even with syntax errors + +=== Installation + +==== Neovim (with nvim-treesitter) + +[source,lua] +---- +local parser_config = require('nvim-treesitter.parsers').get_parser_configs() +parser_config.affinescript = { + install_info = { + url = "~/path/to/tree-sitter-affinescript", + files = {"src/parser.c"}, + branch = "main", + }, + filetype = "as", +} +---- + +==== Emacs (with tree-sitter) + +[source,elisp] +---- +(add-to-list 'tree-sitter-major-mode-language-alist '(affinescript-mode . affinescript)) +---- + +==== VSCode + +Integrated automatically when using the AffineScript VSCode extension. + +=== Development + +[source,bash] +---- +# Generate parser +npm install +npm run build + +# Test grammar +npm test + +# Or using tree-sitter CLI +tree-sitter generate +tree-sitter test +---- + +=== Grammar Highlights + +The grammar supports all AffineScript features: + +* *Affine types* and ownership annotations +* *Effect system* - effect declarations, annotations, handlers +* *Dependent types* - forall, exists quantifiers +* *Pattern matching* - exhaustive, with guards +* *Traits and impls* - polymorphic dispatch +* *Module system* - namespaces and imports + +=== Queries + +==== Highlights (`+queries/highlights.scm+`) + +Syntax highlighting for: - Keywords (fn, let, type, effect, etc.) - +Effects and effect operators - Types and type parameters - Functions and +function calls - Literals and comments + +==== Locals (planned) + +Scope analysis for: - Variable definitions and references - Function +scopes - Block scopes + +==== Injections (planned) + +Language injections for: - Inline documentation - String interpolation + +=== License + +MIT diff --git a/editors/tree-sitter-affinescript/README.md b/editors/tree-sitter-affinescript/README.md deleted file mode 100644 index 6143af70..00000000 --- a/editors/tree-sitter-affinescript/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# tree-sitter-affinescript - -Tree-sitter grammar for AffineScript - affine types, effects, and dependent types. - -## Features - -- **Incremental parsing** - Fast, efficient parsing for large files -- **Syntax highlighting** - Semantic token-based highlighting -- **Code navigation** - Structural queries for IDE features -- **Error recovery** - Continues parsing even with syntax errors - -## Installation - -### Neovim (with nvim-treesitter) - -```lua -local parser_config = require('nvim-treesitter.parsers').get_parser_configs() -parser_config.affinescript = { - install_info = { - url = "~/path/to/tree-sitter-affinescript", - files = {"src/parser.c"}, - branch = "main", - }, - filetype = "as", -} -``` - -### Emacs (with tree-sitter) - -```elisp -(add-to-list 'tree-sitter-major-mode-language-alist '(affinescript-mode . affinescript)) -``` - -### VSCode - -Integrated automatically when using the AffineScript VSCode extension. - -## Development - -```bash -# Generate parser -npm install -npm run build - -# Test grammar -npm test - -# Or using tree-sitter CLI -tree-sitter generate -tree-sitter test -``` - -## Grammar Highlights - -The grammar supports all AffineScript features: - -- **Affine types** and ownership annotations -- **Effect system** - effect declarations, annotations, handlers -- **Dependent types** - forall, exists quantifiers -- **Pattern matching** - exhaustive, with guards -- **Traits and impls** - polymorphic dispatch -- **Module system** - namespaces and imports - -## Queries - -### Highlights (`queries/highlights.scm`) - -Syntax highlighting for: -- Keywords (fn, let, type, effect, etc.) -- Effects and effect operators -- Types and type parameters -- Functions and function calls -- Literals and comments - -### Locals (planned) - -Scope analysis for: -- Variable definitions and references -- Function scopes -- Block scopes - -### Injections (planned) - -Language injections for: -- Inline documentation -- String interpolation - -## License - -MIT diff --git a/editors/tree-sitter-rescript/README.adoc b/editors/tree-sitter-rescript/README.adoc new file mode 100644 index 00000000..28b43187 --- /dev/null +++ b/editors/tree-sitter-rescript/README.adoc @@ -0,0 +1,69 @@ +== tree-sitter- (vendoring manifest) + +This directory is a *manifest-only vendoring* of the canonical +https://github.com/-lang/tree-sitter-[`+-lang/tree-sitter-+`] grammar. +The grammar itself is not copied into this repository — `+package.json+` +declares it as a dependency, and `+scripts/install.sh+` fetches and +builds it at the pinned commit. + +The grammar is consumed by `+tools/res-to-affine/+`, the +`+.res → .affine+` migration assistant (`+affinescript#57+`). It is +*not* an editor binding for AffineScript; for that, see +`+editors/tree-sitter-affinescript/+`. + +=== Pinned upstream + +* *Repository:* https://github.com/-lang/tree-sitter- +* *Commit:* `+990214a83f25801dfe0226bd7e92bb71bba1970f+` +* *Version:* 6.0.0 +* *License:* MIT (preserved upstream; compatible with this repo’s +MPL-2.0) + +When updating the pin, regenerate `+tools/res-to-affine/test/expected/+` +snapshots, since AST shapes may shift. + +=== Install + +From the repo root: + +[source,sh] +---- +just install-grammar # justfile recipe +# or directly: +./editors/tree-sitter-/scripts/install.sh +---- + +This writes a `+tree-sitter-+` directory under `+tools/vendor/+` +(gitignored — same convention as the WASI adapter pinning), containing +the generated parser. Requires `+git+` and the `+tree-sitter+` CLI on +PATH. + +The `+tree-sitter+` CLI can be installed either way: + +[source,sh] +---- +cargo install tree-sitter-cli # Rust-native, repo-preferred +npm install -g tree-sitter-cli # Node-based, also fine +---- + +CI installs via `+npm+` for speed (`+tree-sitter-cli+` from npm is a +pre-built binary, ~5 s install). The `+cargo+` path builds from source +(~5 min on a cold cache) and is the recommended local install because it +keeps the contributor toolchain centred on Rust rather than Node. The +`+package.json+` in this directory pins the version range; bump it in +sync when the upstream grammar pin moves. + +=== Continuous integration + +The `+migration-assistant+` job in `+.github/workflows/ci.yml+` runs +`+just install-grammar+` on every PR, then smoke-parses +`+tools/res-to-affine/test/fixtures/sample.res+`. If the pinned commit +stops building cleanly, this job is the first signal. + +=== Why manifest, not copy + +The upstream grammar is ~10k lines of JS plus generated C. Copying it +into this MPL-2.0 repo would (a) bloat the tree, (b) create an ongoing +sync burden, and (c) duplicate MIT-licensed code we have no business +modifying. The manifest+install approach keeps the dependency explicit +and pinned without absorbing the source. diff --git a/editors/tree-sitter-rescript/README.md b/editors/tree-sitter-rescript/README.md deleted file mode 100644 index 94ee37ed..00000000 --- a/editors/tree-sitter-rescript/README.md +++ /dev/null @@ -1,69 +0,0 @@ - - - -# tree-sitter- (vendoring manifest) - -This directory is a **manifest-only vendoring** of the canonical -[`-lang/tree-sitter-`][upstream] grammar. The grammar -itself is not copied into this repository — `package.json` declares it -as a dependency, and `scripts/install.sh` fetches and builds it at the -pinned commit. - -The grammar is consumed by `tools/res-to-affine/`, the `.res → .affine` -migration assistant (`affinescript#57`). It is **not** an editor binding -for AffineScript; for that, see `editors/tree-sitter-affinescript/`. - -## Pinned upstream - -- **Repository:** -- **Commit:** `990214a83f25801dfe0226bd7e92bb71bba1970f` -- **Version:** 6.0.0 -- **License:** MIT (preserved upstream; compatible with this repo's MPL-2.0) - -When updating the pin, regenerate `tools/res-to-affine/test/expected/` -snapshots, since AST shapes may shift. - -## Install - -From the repo root: - -```sh -just install-grammar # justfile recipe -# or directly: -./editors/tree-sitter-/scripts/install.sh -``` - -This writes a `tree-sitter-` directory under `tools/vendor/` -(gitignored — same convention as the WASI adapter pinning), containing -the generated parser. Requires `git` and the `tree-sitter` CLI on PATH. - -The `tree-sitter` CLI can be installed either way: - -```sh -cargo install tree-sitter-cli # Rust-native, repo-preferred -npm install -g tree-sitter-cli # Node-based, also fine -``` - -CI installs via `npm` for speed (`tree-sitter-cli` from npm is a pre-built -binary, ~5 s install). The `cargo` path builds from source (~5 min on a -cold cache) and is the recommended local install because it keeps the -contributor toolchain centred on Rust rather than Node. The -`package.json` in this directory pins the version range; bump it in -sync when the upstream grammar pin moves. - -## Continuous integration - -The `migration-assistant` job in `.github/workflows/ci.yml` runs `just -install-grammar` on every PR, then smoke-parses -`tools/res-to-affine/test/fixtures/sample.res`. If the pinned commit -stops building cleanly, this job is the first signal. - -## Why manifest, not copy - -The upstream grammar is ~10k lines of JS plus generated C. Copying it -into this MPL-2.0 repo would (a) bloat the tree, (b) create an ongoing -sync burden, and (c) duplicate MIT-licensed code we have no business -modifying. The manifest+install approach keeps the dependency explicit -and pinned without absorbing the source. - -[upstream]: https://github.com/-lang/tree-sitter- diff --git a/editors/vscode/README.adoc b/editors/vscode/README.adoc new file mode 100644 index 00000000..959aa818 --- /dev/null +++ b/editors/vscode/README.adoc @@ -0,0 +1,185 @@ +== AffineScript for Visual Studio Code + +Official Visual Studio Code extension for AffineScript - a language with +affine types, algebraic effects, and dependent types that compiles to +WebAssembly. + +=== Features + +==== Syntax Highlighting + +* Complete TextMate grammar for AffineScript syntax +* Highlighting for effects, quantities (linear/affine/unrestricted), +types, and keywords +* Custom colors for effect annotations and ownership markers + +==== Language Server Protocol (LSP) + +* *Real-time diagnostics* - Type errors, effect violations, borrow check +errors +* *Hover information* - See types and documentation +* *Go to definition* - Navigate to function/type definitions +* *Find references* - Find all uses of a symbol +* *Code completion* - Context-aware suggestions +* *Rename* - Rename symbols across files +* *Formatting* - Auto-format code +* *Code actions* - Quick fixes for common errors + +==== Commands + +* *AffineScript: Type Check Current File* (`+Ctrl+Shift+C+` / +`+Cmd+Shift+C+`) +* *AffineScript: Evaluate Current File* (`+Ctrl+Shift+R+` / +`+Cmd+Shift+R+`) +* *AffineScript: Compile to WebAssembly* +* *AffineScript: Format Document* +* *AffineScript: Restart Language Server* + +=== Requirements + +* *AffineScript compiler* - Install from +https://github.com/hyperpolymath/affinescript[github.com/hyperpolymath/affinescript] +* *affinescript-lsp* - Language server (optional, for LSP features) + +[source,bash] +---- +# Install AffineScript +git clone https://github.com/hyperpolymath/affinescript +cd affinescript +dune build +dune install + +# Install LSP server +cd tools/affinescript-lsp +cargo build --release +cargo install --path . +---- + +=== Extension Settings + +This extension contributes the following settings: + +* `+affinescript.lsp.enabled+`: Enable/disable the Language Server +* `+affinescript.lsp.serverPath+`: Path to affinescript-lsp executable +* `+affinescript.format.indentSize+`: Number of spaces per indentation +level +* `+affinescript.format.maxLineLength+`: Maximum line length before +wrapping +* `+affinescript.lint.enabled+`: Enable/disable linting +* `+affinescript.lint.unusedVariables+`: Diagnostic level for unused +variables +* `+affinescript.lint.missingEffectAnnotations+`: Diagnostic level for +missing effect annotations + +=== Usage + +==== Basic Example + +[source,affinescript] +---- +// Pure function - no effects +fn add(x: Int, y: Int) -> Int { + return x + y; +} + +// Impure function with IO effect +fn main() -> Unit / IO { + println("Hello, AffineScript!"); + let result = add(40, 2); + println(int_to_string(result)); +} +---- + +==== Effect System + +[source,affinescript] +---- +// Pure functions cannot call impure functions +fn pure() -> Int { + return read_line(); // ❌ ERROR: Cannot perform effect IO in pure context +} + +// Impure functions can call pure or impure +fn impure() -> Int / IO { + return read_line(); // ✅ OK +} +---- + +==== Affine Types (Ownership) + +[source,affinescript] +---- +fn use_value(x: @affine String) -> Unit { + println(x); // x is consumed here +} + +fn main() -> Unit / IO { + let s = "hello"; + use_value(s); + println(s); // ❌ ERROR: use after move +} +---- + +=== Known Issues + +* LSP server implementation is in progress (Phase 8) +* Some LSP features not yet implemented (hover, completion) +* Tree-sitter grammar for advanced highlighting coming soon + +=== Contributing + +Contributions welcome! See +https://github.com/hyperpolymath/affinescript/blob/main/CONTRIBUTING.md[CONTRIBUTING.md] + +==== Smoke testing the compiled extension + +The extension source of truth is +link:src/extension.affine[`+src/extension.affine+`]; it compiles to +link:out/extension.cjs[`+out/extension.cjs+`] which is what VS Code +loads. A headless smoke harness verifies the compiled `+.cjs+` against +the acceptance criteria in +https://github.com/hyperpolymath/affinescript/issues/139[issue #139]: +activation, command registration + invocation, `+restartLsp+` cycling, +and `+deactivate+` teardown. + +To run it locally: + +[source,bash] +---- +cd editors/vscode +npm install # one-time: fetches @vscode/test-electron, mocha, glob +xvfb-run npm test # on Linux servers (or `npm test` on a desktop) +---- + +`+@vscode/test-electron+` downloads a pinned VS Code binary on first +run, launches it with `+--extensionDevelopmentPath+` pointing at this +directory, and runs the Mocha suite at link:test/suite/[`+test/suite/+`] +inside the extension host. The +link:../../.github/workflows/ci.yml[`+vscode-smoke+`] CI job runs the +same harness under xvfb on every PR. + +Notes: + +* The harness covers the documented `+showWarningMessage+` short-circuit +when `+affinescript-lsp+` is not on `+PATH+`. Set +`+AFFINESCRIPT_LSP_PATH+` to a real binary if you want to exercise the +LSP-attach branch end-to-end. +* The Node-only runner is a documented carve-out from the repo’s "`no +Node.js / no Bun`" policy (see `+.claude/CLAUDE.md+` → Runtime +Exemptions). Scope is strictly `+editors/vscode/test/+`; no production +code adopts Node. + +=== License + +MPL-2.0 + +=== Release Notes + +==== 0.1.0 + +* Initial release +* Syntax highlighting via TextMate grammar +* Language configuration (brackets, comments, folding) +* Basic LSP integration scaffolding +* Commands for check, eval, compile, format +* Keyboard shortcuts for common operations diff --git a/editors/vscode/README.md b/editors/vscode/README.md deleted file mode 100644 index aa475019..00000000 --- a/editors/vscode/README.md +++ /dev/null @@ -1,161 +0,0 @@ -# AffineScript for Visual Studio Code - -Official Visual Studio Code extension for AffineScript - a language with affine types, algebraic effects, and dependent types that compiles to WebAssembly. - -## Features - -### Syntax Highlighting -- Complete TextMate grammar for AffineScript syntax -- Highlighting for effects, quantities (linear/affine/unrestricted), types, and keywords -- Custom colors for effect annotations and ownership markers - -### Language Server Protocol (LSP) -- **Real-time diagnostics** - Type errors, effect violations, borrow check errors -- **Hover information** - See types and documentation -- **Go to definition** - Navigate to function/type definitions -- **Find references** - Find all uses of a symbol -- **Code completion** - Context-aware suggestions -- **Rename** - Rename symbols across files -- **Formatting** - Auto-format code -- **Code actions** - Quick fixes for common errors - -### Commands -- **AffineScript: Type Check Current File** (`Ctrl+Shift+C` / `Cmd+Shift+C`) -- **AffineScript: Evaluate Current File** (`Ctrl+Shift+R` / `Cmd+Shift+R`) -- **AffineScript: Compile to WebAssembly** -- **AffineScript: Format Document** -- **AffineScript: Restart Language Server** - -## Requirements - -- **AffineScript compiler** - Install from [github.com/hyperpolymath/affinescript](https://github.com/hyperpolymath/affinescript) -- **affinescript-lsp** - Language server (optional, for LSP features) - -```bash -# Install AffineScript -git clone https://github.com/hyperpolymath/affinescript -cd affinescript -dune build -dune install - -# Install LSP server -cd tools/affinescript-lsp -cargo build --release -cargo install --path . -``` - -## Extension Settings - -This extension contributes the following settings: - -* `affinescript.lsp.enabled`: Enable/disable the Language Server -* `affinescript.lsp.serverPath`: Path to affinescript-lsp executable -* `affinescript.format.indentSize`: Number of spaces per indentation level -* `affinescript.format.maxLineLength`: Maximum line length before wrapping -* `affinescript.lint.enabled`: Enable/disable linting -* `affinescript.lint.unusedVariables`: Diagnostic level for unused variables -* `affinescript.lint.missingEffectAnnotations`: Diagnostic level for missing effect annotations - -## Usage - -### Basic Example - -```affinescript -// Pure function - no effects -fn add(x: Int, y: Int) -> Int { - return x + y; -} - -// Impure function with IO effect -fn main() -> Unit / IO { - println("Hello, AffineScript!"); - let result = add(40, 2); - println(int_to_string(result)); -} -``` - -### Effect System - -```affinescript -// Pure functions cannot call impure functions -fn pure() -> Int { - return read_line(); // ❌ ERROR: Cannot perform effect IO in pure context -} - -// Impure functions can call pure or impure -fn impure() -> Int / IO { - return read_line(); // ✅ OK -} -``` - -### Affine Types (Ownership) - -```affinescript -fn use_value(x: @affine String) -> Unit { - println(x); // x is consumed here -} - -fn main() -> Unit / IO { - let s = "hello"; - use_value(s); - println(s); // ❌ ERROR: use after move -} -``` - -## Known Issues - -- LSP server implementation is in progress (Phase 8) -- Some LSP features not yet implemented (hover, completion) -- Tree-sitter grammar for advanced highlighting coming soon - -## Contributing - -Contributions welcome! See [CONTRIBUTING.md](https://github.com/hyperpolymath/affinescript/blob/main/CONTRIBUTING.md) - -### Smoke testing the compiled extension - -The extension source of truth is [`src/extension.affine`](src/extension.affine); -it compiles to [`out/extension.cjs`](out/extension.cjs) which is what VS Code -loads. A headless smoke harness verifies the compiled `.cjs` against the -acceptance criteria in -[issue #139](https://github.com/hyperpolymath/affinescript/issues/139): -activation, command registration + invocation, `restartLsp` cycling, and -`deactivate` teardown. - -To run it locally: - -```bash -cd editors/vscode -npm install # one-time: fetches @vscode/test-electron, mocha, glob -xvfb-run npm test # on Linux servers (or `npm test` on a desktop) -``` - -`@vscode/test-electron` downloads a pinned VS Code binary on first run, launches -it with `--extensionDevelopmentPath` pointing at this directory, and runs the -Mocha suite at [`test/suite/`](test/suite/) inside the extension host. The -[`vscode-smoke`](../../.github/workflows/ci.yml) CI job runs the same harness -under xvfb on every PR. - -Notes: - -- The harness covers the documented `showWarningMessage` short-circuit when - `affinescript-lsp` is not on `PATH`. Set `AFFINESCRIPT_LSP_PATH` to a real - binary if you want to exercise the LSP-attach branch end-to-end. -- The Node-only runner is a documented carve-out from the repo's - "no Node.js / no Bun" policy (see `.claude/CLAUDE.md` → Runtime Exemptions). - Scope is strictly `editors/vscode/test/`; no production code adopts Node. - -## License - -MPL-2.0 - -## Release Notes - -### 0.1.0 - -- Initial release -- Syntax highlighting via TextMate grammar -- Language configuration (brackets, comments, folding) -- Basic LSP integration scaffolding -- Commands for check, eval, compile, format -- Keyboard shortcuts for common operations diff --git a/issues-drafts/01-float-arithmetic-operators-not-typeable.adoc b/issues-drafts/01-float-arithmetic-operators-not-typeable.adoc new file mode 100644 index 00000000..a5fe4cd3 --- /dev/null +++ b/issues-drafts/01-float-arithmetic-operators-not-typeable.adoc @@ -0,0 +1,101 @@ +== Float arithmetic operators (`+++`/`+-+`/`+*+`/`+/+`) fail to typecheck + +*Surfaced by:* IDApTIK migration (Wave 3, 2026-05-02) *Affected +version:* v0.1.0 (`+affinescript+` compiler at HEAD as of 2026-05-02) +*Severity:* Blocking for ~all .res → .affine translations involving game +math, physics, animation, geometry. + +=== Reproducer + +[source,affinescript] +---- +fn pi_plus_pi() -> Float { + 3.14 + 3.14 +} +---- + +.... +$ affinescript check pi.affine +Unification error: (Unify.TypeMismatch (Int, Float)) +affinescript: Type error +.... + +Smaller still: + +[source,affinescript] +---- +fn add_floats(a: Float, b: Float) -> Float { + a + b +} +---- + +Same error. + +=== What works + +* Float literals are recognised and accepted in Float-returning +positions: ++ +[source,affinescript] +---- +fn just_pi() -> Float { 3.14 } // typechecks +---- +* Stdlib functions returning Float compile fine: +`+affinescript/stdlib/Math.affine+` declares +`+pub fn pi() -> Float { return 3.14159...; }+` and that file +typechecks. +* The interpreter implements Float operators +(`+affinescript/lib/value.ml+`: `+OpAdd → Ok (VFloat (a +. b))+`). So +this is a typechecker gap, not a codegen one. + +=== What does not work + +The typechecker treats `+++` / `+-+` / `+*+` / `+/+` / `+<+` / `+>+` / +`+<=+` / `+>=+` as Int-typed, with no path to Float. There appears to be +no overload resolution, no implicit Int→Float coercion, and no separate +`++.+` operator (as in OCaml) exposed at the surface. + +=== Why this matters for the migration + +The IDApTIK codebase is a game engine with extensive Float math: +collision (`+combat/Hitbox.res+`), physics, animation easing, screen +positioning, particle effects, audio mixing. Without Float-arithmetic +operators, every such file requires either: + +[arabic] +. *Int placeholders* with documented precision compromise (works for +collision; bad for physics/easing). +. *Hand-written FFI* to a Float-arithmetic helper (defeats the purpose +of using AffineScript). +. *Waiting on this issue.* + +The first IDApTIK migration (`+Hitbox.res+` → `+Hitbox.affine+`) used +option 1, with a header note flagging the compromise. Most other files +that would benefit from translation are blocked on option 3. + +=== Suggested resolution shape + +Either: + +* *Polymorphic numeric operators* — `++ : ∀ N. (N, N) -> N+` with `+N+` +constrained to a `+Numeric+` typeclass (or row, or trait dictionary, +depending on AffineScript’s chosen mechanism). This is the most +ergonomic. +* *Distinct float operators* at the surface (`++.+`, `+-.+`, etc., +OCaml-style). Less ergonomic, but unambiguous and matches the underlying +machinery. +* *Default to `+f64+` and provide explicit `+Int+` operators* — +Rust-style, but a much bigger semantic shift. + +Option 2 is probably the smallest delta from the current state, given +the interpreter already has separate Int and Float operations. + +=== Cross-reference + +* `+AI.a2ml+` directives include "`ergonomics first`" and "`the type +system defaults must be sensible so that most code reads like modern +JavaScript`". A user expecting `+3.14 + 3.14+` to work is squarely +within that target. +* The `+frontier-guide.adoc+` doesn’t yet have a chapter on numerics; +whichever resolution lands, that chapter should be written so future +translators know what to expect. diff --git a/issues-drafts/01-float-arithmetic-operators-not-typeable.md b/issues-drafts/01-float-arithmetic-operators-not-typeable.md deleted file mode 100644 index 4d24c772..00000000 --- a/issues-drafts/01-float-arithmetic-operators-not-typeable.md +++ /dev/null @@ -1,66 +0,0 @@ -# Float arithmetic operators (`+`/`-`/`*`/`/`) fail to typecheck - -**Surfaced by:** IDApTIK migration (Wave 3, 2026-05-02) -**Affected version:** v0.1.0 (`affinescript` compiler at HEAD as of 2026-05-02) -**Severity:** Blocking for ~all .res → .affine translations involving game math, physics, animation, geometry. - -## Reproducer - -```affinescript -fn pi_plus_pi() -> Float { - 3.14 + 3.14 -} -``` - -``` -$ affinescript check pi.affine -Unification error: (Unify.TypeMismatch (Int, Float)) -affinescript: Type error -``` - -Smaller still: - -```affinescript -fn add_floats(a: Float, b: Float) -> Float { - a + b -} -``` -Same error. - -## What works - -- Float literals are recognised and accepted in Float-returning positions: - ```affinescript - fn just_pi() -> Float { 3.14 } // typechecks - ``` -- Stdlib functions returning Float compile fine: `affinescript/stdlib/Math.affine` declares `pub fn pi() -> Float { return 3.14159...; }` and that file typechecks. -- The interpreter implements Float operators (`affinescript/lib/value.ml`: `OpAdd → Ok (VFloat (a +. b))`). So this is a typechecker gap, not a codegen one. - -## What does not work - -The typechecker treats `+` / `-` / `*` / `/` / `<` / `>` / `<=` / `>=` as Int-typed, with no path to Float. There appears to be no overload resolution, no implicit Int→Float coercion, and no separate `+.` operator (as in OCaml) exposed at the surface. - -## Why this matters for the migration - -The IDApTIK codebase is a game engine with extensive Float math: collision (`combat/Hitbox.res`), physics, animation easing, screen positioning, particle effects, audio mixing. Without Float-arithmetic operators, every such file requires either: - -1. **Int placeholders** with documented precision compromise (works for collision; bad for physics/easing). -2. **Hand-written FFI** to a Float-arithmetic helper (defeats the purpose of using AffineScript). -3. **Waiting on this issue.** - -The first IDApTIK migration (`Hitbox.res` → `Hitbox.affine`) used option 1, with a header note flagging the compromise. Most other files that would benefit from translation are blocked on option 3. - -## Suggested resolution shape - -Either: - -- **Polymorphic numeric operators** — `+ : ∀ N. (N, N) -> N` with `N` constrained to a `Numeric` typeclass (or row, or trait dictionary, depending on AffineScript's chosen mechanism). This is the most ergonomic. -- **Distinct float operators** at the surface (`+.`, `-.`, etc., OCaml-style). Less ergonomic, but unambiguous and matches the underlying machinery. -- **Default to `f64` and provide explicit `Int` operators** — Rust-style, but a much bigger semantic shift. - -Option 2 is probably the smallest delta from the current state, given the interpreter already has separate Int and Float operations. - -## Cross-reference - -- `AI.a2ml` directives include "ergonomics first" and "the type system defaults must be sensible so that most code reads like modern JavaScript". A user expecting `3.14 + 3.14` to work is squarely within that target. -- The `frontier-guide.adoc` doesn't yet have a chapter on numerics; whichever resolution lands, that chapter should be written so future translators know what to expect. diff --git a/issues-drafts/02-array-type-syntax-not-parseable-in-user-source.adoc b/issues-drafts/02-array-type-syntax-not-parseable-in-user-source.adoc new file mode 100644 index 00000000..5e45d40f --- /dev/null +++ b/issues-drafts/02-array-type-syntax-not-parseable-in-user-source.adoc @@ -0,0 +1,108 @@ +== Array type syntax `+[T]+` not parseable in user source code + +*STATUS: CLOSED 2026-05-03* — `+[T]+` desugars to `+Array[T]+` in any +type-expr position via the new rule in `+lib/parser.mly+`’s +`+type_expr_primary+`. Verified for fn params, return types, struct +fields, and nested `+[[T]]+`. See `+STATE.a2ml+` +`+session-note-2026-05-03-c+` and the `+E2E Array Type Sugar+` test +suite. Original issue text preserved below for historical context. + +''''' + +*Surfaced by:* IDApTIK migration (Wave 3 / Wave 4, 2026-05-02) *Affected +version:* v0.1.0 (`+affinescript+` compiler at HEAD as of 2026-05-02) +*Severity:* Blocking for the majority of idaptik’s +`+.res / .ts → .affine+` translation surface — almost every +cross-component data type uses arrays/lists. + +=== Reproducer + +[source,affinescript] +---- +fn first(xs: [Int]) -> Int { + xs[0] +} +---- + +.... +$ affinescript check first.affine +first.affine:1:14: parse error: Syntax error +affinescript: Parse error +.... + +Same error for arrays in struct fields: + +[source,affinescript] +---- +struct Tags { + names: [String] +} +---- + +.... +parse error at column 10 +.... + +=== What works + +The `+[T]+` syntax appears extensively in *stdlib* files and is treated +as the list/array type: + +[source,bash] +---- +$ grep -rE "[: \(]\[" affinescript/stdlib/ +stdlib/math.affine:fn mean(values: [Float]) -> Float +stdlib/result.affine:fn collect(results: [Result]) -> Result<[T], E> +stdlib/prelude.affine:fn map(arr: [T], f: T -> U) -> [U] +stdlib/collections.affine:fn reverse(list: [T]) -> [T] +---- + +The stdlib parses (presumably under a different load path or different +parser configuration), but freshly-authored user source does not. + +=== Why this matters for the migration + +Arrays/lists are pervasive in the IDApTIK codebase being translated: + +* Game state: device lists, enemy patrols, inventory items. +* Cross-component types: `+array<(string, int)>+` for register +snapshots, puzzle hints, cable connections, etc. +* Polymorphic variants with array payloads: +`+VMStateChanged({registers: array<(string, int)>})+`. +* Asset bundles, level configs, world items. + +Without user-source array syntax, only files whose types are purely +scalar + struct + plain enum are translatable. From the IDApTIK survey, +this is a single-digit percentage of the corpus. + +=== Suspected cause + +Stdlib files may be parsed via a special path that allows extra syntax +forms (similar to the way OCaml stdlib uses internal-only constructs). +If that’s the case, the fix is to expose the same syntax to the +user-facing parser — likely a one-line change in `+lib/parser.mly+` to +match the same token sequence at module scope. + +If the stdlib forms genuinely don’t go through the same parser at all, +that’s a deeper integration question — the stdlib should typecheck via +the same parser users do, otherwise the stdlib types are not observable +to user code. + +=== Suggested resolution shape + +Either: + +* *Make `+[T]+` a parseable type expression in user source* — minimal +change, matches what stdlib already uses. +* *Document an alternative array spelling* (e.g. `+Array[T]+`, +`+List+`) and update stdlib accordingly. More verbose, but easier to +disambiguate from generic-parameter brackets. + +Either path lets idaptik’s Wave 4 (kernel types preview) and most of +Wave 3 proceed. + +=== Cross-reference + +* The `+migration-playbook.adoc+` →AffineScript table doesn’t yet have a +row for arrays — once this lands, that row should be added with the +chosen syntax. diff --git a/issues-drafts/02-array-type-syntax-not-parseable-in-user-source.md b/issues-drafts/02-array-type-syntax-not-parseable-in-user-source.md deleted file mode 100644 index f44b6723..00000000 --- a/issues-drafts/02-array-type-syntax-not-parseable-in-user-source.md +++ /dev/null @@ -1,84 +0,0 @@ -# Array type syntax `[T]` not parseable in user source code - -**STATUS: CLOSED 2026-05-03** — `[T]` desugars to `Array[T]` in any -type-expr position via the new rule in `lib/parser.mly`'s -`type_expr_primary`. Verified for fn params, return types, struct fields, -and nested `[[T]]`. See `STATE.a2ml` `session-note-2026-05-03-c` and the -`E2E Array Type Sugar` test suite. Original issue text preserved below -for historical context. - ---- - -**Surfaced by:** IDApTIK migration (Wave 3 / Wave 4, 2026-05-02) -**Affected version:** v0.1.0 (`affinescript` compiler at HEAD as of 2026-05-02) -**Severity:** Blocking for the majority of idaptik's `.res / .ts → .affine` translation surface — almost every cross-component data type uses arrays/lists. - -## Reproducer - -```affinescript -fn first(xs: [Int]) -> Int { - xs[0] -} -``` - -``` -$ affinescript check first.affine -first.affine:1:14: parse error: Syntax error -affinescript: Parse error -``` - -Same error for arrays in struct fields: - -```affinescript -struct Tags { - names: [String] -} -``` - -``` -parse error at column 10 -``` - -## What works - -The `[T]` syntax appears extensively in **stdlib** files and is treated as the list/array type: - -```bash -$ grep -rE "[: \(]\[" affinescript/stdlib/ -stdlib/math.affine:fn mean(values: [Float]) -> Float -stdlib/result.affine:fn collect(results: [Result]) -> Result<[T], E> -stdlib/prelude.affine:fn map(arr: [T], f: T -> U) -> [U] -stdlib/collections.affine:fn reverse(list: [T]) -> [T] -``` - -The stdlib parses (presumably under a different load path or different parser configuration), but freshly-authored user source does not. - -## Why this matters for the migration - -Arrays/lists are pervasive in the IDApTIK codebase being translated: - -- Game state: device lists, enemy patrols, inventory items. -- Cross-component types: `array<(string, int)>` for register snapshots, puzzle hints, cable connections, etc. -- Polymorphic variants with array payloads: `VMStateChanged({registers: array<(string, int)>})`. -- Asset bundles, level configs, world items. - -Without user-source array syntax, only files whose types are purely scalar + struct + plain enum are translatable. From the IDApTIK survey, this is a single-digit percentage of the corpus. - -## Suspected cause - -Stdlib files may be parsed via a special path that allows extra syntax forms (similar to the way OCaml stdlib uses internal-only constructs). If that's the case, the fix is to expose the same syntax to the user-facing parser — likely a one-line change in `lib/parser.mly` to match the same token sequence at module scope. - -If the stdlib forms genuinely don't go through the same parser at all, that's a deeper integration question — the stdlib should typecheck via the same parser users do, otherwise the stdlib types are not observable to user code. - -## Suggested resolution shape - -Either: - -- **Make `[T]` a parseable type expression in user source** — minimal change, matches what stdlib already uses. -- **Document an alternative array spelling** (e.g. `Array[T]`, `List`) and update stdlib accordingly. More verbose, but easier to disambiguate from generic-parameter brackets. - -Either path lets idaptik's Wave 4 (kernel types preview) and most of Wave 3 proceed. - -## Cross-reference - -- The `migration-playbook.adoc` →AffineScript table doesn't yet have a row for arrays — once this lands, that row should be added with the chosen syntax. diff --git a/issues-drafts/03-effect-handling-migration-story.adoc b/issues-drafts/03-effect-handling-migration-story.adoc new file mode 100644 index 00000000..d539913a --- /dev/null +++ b/issues-drafts/03-effect-handling-migration-story.adoc @@ -0,0 +1,108 @@ +== Document the migration story for code that needs algebraic effect handling + +*Surfaced by:* IDApTIK migration (Wave 3, 2026-05-02) *Type:* +Documentation / scope clarification, not a bug. *Affected version:* +v0.1.0; relevant to any release while effect handling stays out of +scope. + +=== Context + +`+AI.a2ml+` (the "`Frontier Programming Practices — AI Edition`" scope +statement) is unambiguous: + +____ +`+algebraic-effect-handlers+` +`+(reason "interaction-with-affine-is-unresolved")+` +`+(note "Multi-shot resume of continuations that captured affine resources is a soundness hole; handlers are deferred until the design is explicit. Effect TRACKING — declaring what a function can do — is in scope. Effect HANDLING — intercepting and redirecting effects at runtime — is not.")+` +____ + +This is a sound, deliberate position. *The issue here is not the +position; the issue is the lack of guidance for migrators who hit the +gap.* + +=== What the gap actually looks like in practice + +A large fraction of any real `+-script+` codebase is _operations against +shared mutable state, executed for their side effects_: + +.... +// idaptik/src/app/GetEngine.res +let instance: ref> = ref(None) +let get = (): option => instance.contents +let set = (engine: Engine.t): unit => { instance := Some(engine) } +.... + +The migration playbook is clear that this should become a `+State+` +effect: + +____ +"`If two callers need to see the same mutation, it is no longer local — +lift it.`" +____ + +But effect HANDLING is out of scope, so the lifted form *declares* the +intent without being able to *execute* it. A faithful migration produces +a function signature like +`+fn get_engine() -> Option[Engine] / {State[EngineRef]}+` that +compiles, but with no way to actually wire up the State handler at +runtime, the program cannot run. Not just the function — the entire +program. + +This means *a migrator who follows the playbook hits a dead-end on any +file that reads or writes shared state.* Idaptik’s audit found this is +most of `+src/app/+`: navigation registries, engine singleton, +popup/screen constructors, lobby registry, Burble adapter, voice bridge, +persistence layers — all blocked at the same gate. + +=== What would help + +A short doc — either a chapter in `+frontier-guide.adoc+` or a sibling +under `+docs/guides/effects-migration-stance.adoc+` — that answers: + +[arabic] +. *What’s the intended runtime model for code that conceptually needs +`+State+`/`+IO+`/`+Async+` effects, in the absence of handlers?* Are +migrators meant to: +* {blank} +[loweralpha] +.. Defer translation entirely until handlers are designed? +* {blank} +[loweralpha, start=2] +.. Hand-write FFI shims that wrap the JS-side mutation as opaque foreign +primitives? +* {blank} +[loweralpha, start=3] +.. Use linear/affine resources to encode state-passing manually (no +shared cell, but threaded)? +* {blank} +[loweralpha, start=4] +.. Something else? +. *What’s the expected timeline / sequencing?* Is handler design a +6-week problem, a 6-month problem, or a sibling-project (Typed WASM) +problem? Migrators planning a multi-month effort need to know which. +. *Are there idaptik-specific or general-pattern examples* showing the +recommended workaround for the most common cases — singleton state, +IO-emitting helpers, async network calls? + +=== Why a doc is enough (no code change needed) + +The scope statement’s reasoning ("`multi-shot resume + affine = +soundness hole`") is the right reasoning. Rushing handlers in to unblock +migration would be the wrong move. *The fix is a paragraph telling +migrators what to do in the meantime, not a feature.* + +Right now the migrator’s experience is: + +* Read `+migration-playbook.adoc+` → "`lift mutation to a State effect`" +* Try it → the function signature compiles +* Realize the program can’t run because handlers aren’t implemented +* Re-read `+AI.a2ml+` → "`handlers are out of scope`" +* … and now what? + +A short doc covering this gap closes the circle. + +=== Cross-reference + +* IDApTIK’s STATE.a2ml `+[affinescript-v0_1_0-translatable-surface]+` +section catalogues the language gaps that block migration; this issue is +the doc-side counterpart. diff --git a/issues-drafts/03-effect-handling-migration-story.md b/issues-drafts/03-effect-handling-migration-story.md deleted file mode 100644 index a234e360..00000000 --- a/issues-drafts/03-effect-handling-migration-story.md +++ /dev/null @@ -1,64 +0,0 @@ -# Document the migration story for code that needs algebraic effect handling - -**Surfaced by:** IDApTIK migration (Wave 3, 2026-05-02) -**Type:** Documentation / scope clarification, not a bug. -**Affected version:** v0.1.0; relevant to any release while effect handling stays out of scope. - -## Context - -`AI.a2ml` (the "Frontier Programming Practices — AI Edition" scope statement) is unambiguous: - -> `algebraic-effect-handlers` -> `(reason "interaction-with-affine-is-unresolved")` -> `(note "Multi-shot resume of continuations that captured affine resources is a soundness hole; handlers are deferred until the design is explicit. Effect TRACKING — declaring what a function can do — is in scope. Effect HANDLING — intercepting and redirecting effects at runtime — is not.")` - -This is a sound, deliberate position. **The issue here is not the position; the issue is the lack of guidance for migrators who hit the gap.** - -## What the gap actually looks like in practice - -A large fraction of any real `-script` codebase is *operations against shared mutable state, executed for their side effects*: - -``` -// idaptik/src/app/GetEngine.res -let instance: ref> = ref(None) -let get = (): option => instance.contents -let set = (engine: Engine.t): unit => { instance := Some(engine) } -``` - -The migration playbook is clear that this should become a `State` effect: - -> "If two callers need to see the same mutation, it is no longer local — lift it." - -But effect HANDLING is out of scope, so the lifted form **declares** the intent without being able to **execute** it. A faithful migration produces a function signature like `fn get_engine() -> Option[Engine] / {State[EngineRef]}` that compiles, but with no way to actually wire up the State handler at runtime, the program cannot run. Not just the function — the entire program. - -This means **a migrator who follows the playbook hits a dead-end on any file that reads or writes shared state.** Idaptik's audit found this is most of `src/app/`: navigation registries, engine singleton, popup/screen constructors, lobby registry, Burble adapter, voice bridge, persistence layers — all blocked at the same gate. - -## What would help - -A short doc — either a chapter in `frontier-guide.adoc` or a sibling under `docs/guides/effects-migration-stance.adoc` — that answers: - -1. **What's the intended runtime model for code that conceptually needs `State`/`IO`/`Async` effects, in the absence of handlers?** Are migrators meant to: - - (a) Defer translation entirely until handlers are designed? - - (b) Hand-write FFI shims that wrap the JS-side mutation as opaque foreign primitives? - - (c) Use linear/affine resources to encode state-passing manually (no shared cell, but threaded)? - - (d) Something else? -2. **What's the expected timeline / sequencing?** Is handler design a 6-week problem, a 6-month problem, or a sibling-project (Typed WASM) problem? Migrators planning a multi-month effort need to know which. -3. **Are there idaptik-specific or general-pattern examples** showing the recommended workaround for the most common cases — singleton state, IO-emitting helpers, async network calls? - -## Why a doc is enough (no code change needed) - -The scope statement's reasoning ("multi-shot resume + affine = soundness hole") is the right reasoning. Rushing handlers in to unblock migration would be the wrong move. **The fix is a paragraph telling migrators what to do in the meantime, not a feature.** - -Right now the migrator's experience is: - -- Read `migration-playbook.adoc` → "lift mutation to a State effect" -- Try it → the function signature compiles -- Realize the program can't run because handlers aren't implemented -- Re-read `AI.a2ml` → "handlers are out of scope" -- ... and now what? - -A short doc covering this gap closes the circle. - -## Cross-reference - -- IDApTIK's STATE.a2ml `[affinescript-v0_1_0-translatable-surface]` section catalogues the language gaps that block migration; this issue is the doc-side counterpart. diff --git a/issues-drafts/04-extern-declarations-not-parseable.adoc b/issues-drafts/04-extern-declarations-not-parseable.adoc new file mode 100644 index 00000000..ad933483 --- /dev/null +++ b/issues-drafts/04-extern-declarations-not-parseable.adoc @@ -0,0 +1,68 @@ +== `+extern type+` / `+extern fn+` declarations not parseable in user source + +*STATUS: CLOSED 2026-05-03* — `+extern fn name(...) -> Ret;+` and +`+extern type Name;+` both parse, resolve, typecheck, and emit +`+(import "env" "" (func ...))+` in the WASM target. New +`+EXTERN+` keyword in lexer/token/parse_driver, new `+FnExtern+` / +`+TyExtern+` AST variants, new parser rules in `+lib/parser.mly+`. See +`+STATE.a2ml+` `+session-note-2026-05-03-c+` and the `+E2E Externs+` +test suite. Vscode bindings (`+stdlib/Vscode.affine+`) are the first +real consumer. Original issue text preserved below for historical +context. + +''''' + +*Surfaced by:* IDApTIK migration / `+affinescript-pixijs+` integration +attempt (2026-05-02) *Affected version:* v0.1.0 *Severity:* Blocking for +`+@affinescript/pixijs+` and any other connector package that uses +FFI-style imports. The package’s own `+src/pixi.as+` cannot be compiled +by today’s toolchain. + +=== Reproducer + +The shape used by `+affinescript/affinescript-pixijs/src/pixi.as+`: + +[source,affinescript] +---- +extern type Application; +extern fn createApplication(width: Int, height: Int) -> Application; + +pub fn init_pixi(width: Int, height: Int) -> Application { + createApplication(width, height) +} +---- + +.... +$ affinescript check pixi.affine +pixi.affine:1:1: parse error: Syntax error +affinescript: Parse error +.... + +`+extern+` is rejected at the very first character — it’s not a +recognised keyword in the user-source grammar. + +=== Why this matters + +`+affinescript-pixijs/src/pixi.as+` (the canonical PixiJS connector) is +structured around `+extern type Application+` / +`+extern fn createApplication(...)+` declarations whose implementations +are intended to be linked at runtime via "`Typed WASM imports`" (per the +file’s header comment). Without `+extern+` parsing, the connector itself +cannot be compiled, which means any consumer of `+@affinescript/pixijs+` +is blocked. + +For IDApTIK specifically, this gates the entire screen-and-rendering +side of the migration (Wave 3 popups, screens, UI primitives) because +they all need to call into PixiJS. + +=== Cross-reference + +* This is the connector-layer counterpart to issue #03 (effect handling) +— both are about how AffineScript code talks to its host environment. +`+extern+` is the structural mechanism; effect handlers are the runtime +mechanism. +* A fix here unblocks `+@affinescript/pixijs+`, `+@affinescript/dom+`, +and any future `+@affinescript/*+` package that uses FFI imports. +* If `+extern+` is intentionally being held back pending a final design, +document the interim shape (e.g. `+@module+` annotations? hand-written +wasm imports table?) so connector authors know what to write. diff --git a/issues-drafts/04-extern-declarations-not-parseable.md b/issues-drafts/04-extern-declarations-not-parseable.md deleted file mode 100644 index 5856a898..00000000 --- a/issues-drafts/04-extern-declarations-not-parseable.md +++ /dev/null @@ -1,49 +0,0 @@ -# `extern type` / `extern fn` declarations not parseable in user source - -**STATUS: CLOSED 2026-05-03** — `extern fn name(...) -> Ret;` and -`extern type Name;` both parse, resolve, typecheck, and emit -`(import "env" "" (func ...))` in the WASM target. New `EXTERN` -keyword in lexer/token/parse_driver, new `FnExtern` / `TyExtern` AST -variants, new parser rules in `lib/parser.mly`. See `STATE.a2ml` -`session-note-2026-05-03-c` and the `E2E Externs` test suite. Vscode -bindings (`stdlib/Vscode.affine`) are the first real consumer. Original -issue text preserved below for historical context. - ---- - -**Surfaced by:** IDApTIK migration / `affinescript-pixijs` integration attempt (2026-05-02) -**Affected version:** v0.1.0 -**Severity:** Blocking for `@affinescript/pixijs` and any other connector package that uses FFI-style imports. The package's own `src/pixi.as` cannot be compiled by today's toolchain. - -## Reproducer - -The shape used by `affinescript/affinescript-pixijs/src/pixi.as`: - -```affinescript -extern type Application; -extern fn createApplication(width: Int, height: Int) -> Application; - -pub fn init_pixi(width: Int, height: Int) -> Application { - createApplication(width, height) -} -``` - -``` -$ affinescript check pixi.affine -pixi.affine:1:1: parse error: Syntax error -affinescript: Parse error -``` - -`extern` is rejected at the very first character — it's not a recognised keyword in the user-source grammar. - -## Why this matters - -`affinescript-pixijs/src/pixi.as` (the canonical PixiJS connector) is structured around `extern type Application` / `extern fn createApplication(...)` declarations whose implementations are intended to be linked at runtime via "Typed WASM imports" (per the file's header comment). Without `extern` parsing, the connector itself cannot be compiled, which means any consumer of `@affinescript/pixijs` is blocked. - -For IDApTIK specifically, this gates the entire screen-and-rendering side of the migration (Wave 3 popups, screens, UI primitives) because they all need to call into PixiJS. - -## Cross-reference - -- This is the connector-layer counterpart to issue #03 (effect handling) — both are about how AffineScript code talks to its host environment. `extern` is the structural mechanism; effect handlers are the runtime mechanism. -- A fix here unblocks `@affinescript/pixijs`, `@affinescript/dom`, and any future `@affinescript/*` package that uses FFI imports. -- If `extern` is intentionally being held back pending a final design, document the interim shape (e.g. `@module` annotations? hand-written wasm imports table?) so connector authors know what to write. diff --git a/issues-drafts/05-float-through-heap-cell-model-invalid-wasm.adoc b/issues-drafts/05-float-through-heap-cell-model-invalid-wasm.adoc new file mode 100644 index 00000000..93669f86 --- /dev/null +++ b/issues-drafts/05-float-through-heap-cell-model-invalid-wasm.adoc @@ -0,0 +1,157 @@ +== Core-wasm: any `+Float+` that transits the heap is mismodeled (invalid/truncated wasm) + +*Surfaced by:* WASM coverage sweep + coprocessor smoke test (2026-06-16) +*Affected version:* `+affinescript+` compiler at HEAD (branch +`+feat/solo-core-metatheory-proofs+`) *Severity:* Correctness + +*security* — silent 32-of-64-bit truncation on store; invalid module on +load. Caught for the first time by the new `+just wasm-validate+` gate +(`+tools/wasm-validate-gate.sh+`); previously hidden because +`+test/test_e2e.ml:601+` only checks "`codegen did not raise`", never +`+wasm-tools validate+`. + +=== Reproducers + +[source,affinescript] +---- +// (a) load: INVALID module — wasm-tools: "expected f64, found i32" +fn rd(i: Int, a: Array[Float]) -> Float { a[i] } + +// (b) projection: INVALID module (same cause, via tuple) +fn proj() -> Float { let t: (Float, Float) = (1.0, 2.0); t.0 } + +// (c) store: VALIDATES but is SEMANTICALLY WRONG — copies 32 of 64 bits +fn k(i: Int, mut o: Array[Float], a: Array[Float]) -> Unit { o[i] = a[i]; } +---- + +.... +$ affinescript compile rd.affine -o rd.wasm && wasm-tools validate rd.wasm +error: func 1 failed to validate + 0: type mismatch: expected f64, found i32 +.... + +Scalars are fine (`+fn dbl(x: Float) -> Float { x * 2.0 }+` validates) — +the bug is strictly the heap path. `+Int+` through the heap is fine. + +=== Root cause + +The core-wasm backend (`+lib/codegen.ml+`) uses a *uniform 4-byte (i32) +heap-cell model* everywhere: + +* array alloc `+size = 4 + (num_elements * 4)+`, element offset +`+4 + (idx * 4)+` (`+lib/codegen.ml:1926+`, `+:1949+`, `+:2047-2053+`) +* tuple/record fields at `+index * 4+`, `+I32Load+`/`+I32Store+` with a +`+* 4+` stride (`+:1964+`, `+:2030+`, `+:2519-2548+`) + +A `+Float+` is `+f64+` (8 bytes) in locals/arithmetic, but the moment it +is stored into or loaded from an array/tuple/record cell the layout +assumes 4 bytes and the access is emitted as `+i32.{load,store}+`. +Hence: load-then-use-as-f64 → invalid module; store → 32-bit truncation +of a 64-bit value. + +=== Fix options + +[arabic] +. *Type-directed heap layout (the real fix).* Make cell size and the +load/store opcode a function of the static element/field type: `+f64+` +cells are 8 bytes with `+f64.{load,store}+` and an 8-byte stride; mixed +records/ tuples compute per-field offsets from field types. Touches +arrays, tuples, records, and closures — a substantial, careful codegen +change. *High revert-cost → land behind its own PR with the +`+wasm-validate+` gate extended to cover Float-in-heap fixtures.* +. *Honest loud-fail (interim, secure).* Until (1) lands, have the +backend raise `+Codegen.UnsupportedFeature+` when a `+Float+` would +transit a heap cell, with a message routing to the interpreter (`+-i+`), +the Julia backend (`+-julia+`), or the GPU kernel backends +(WGSL/CUDA/Metal/OpenCL *already lower `+f64+` array buffers correctly* +— verified 2026-06-16). This converts silent-wrong into honest-reject, +matching the #555/#556 loud-fail policy. + +Recommended: ship (2) now for safety, then (1) as the durable fix. Both +are gated by `+just wasm-validate+`. + +*Status (2026-06-16): interim secure fix (2) LANDED.* `+lib/codegen.ml+` +now raises `+UnsupportedFeature+` when a `+Float+` would transit a heap +cell — guarded at function param/return types +(`+guard_fn_no_heap_float+`) and at Array/tuple/record _literals_ +(`+guard_no_float_elems+`). Silent corruption / invalid emission is +gone; scalar `+Float+` and `+Int+` aggregates are untouched. +`+just wasm-validate+` now pins the loud-fail (two `+rej+` cases). *The +real fix (1) — type-directed heap layout — remains open as task #8.* + +=== Update (2026-06-16, cont.) — durable fix (1) for ARRAYS landed via the Float wall + +The durable fix is being delivered type-directed and +_complete-by-construction_ through the existing *Float-wall elaboration* +(the same mechanism that makes scalar `+Float+` arithmetic work): +`+synth+` (the real typechecker) records the heap nodes whose _cell_ +type is `+Float+`, and `+elaborate_string_concat+` rewrites those exact +nodes into specialized AST constructors that codegen lowers with f64 +ops. Because `+synth+` sees _every_ node’s checked type, recording is +total — every `+Float+` construction and every `+Float+`-yielding access +is caught no matter how the array flowed there — so codegen never +guesses a cell width (the gap that made a codegen-local fix unsafe). New +constructors (`+Ast.ExprFloatArray+`, `+Ast.ExprFloatIndex+`) lay out a +4-byte length header + *8-byte f64 cells* (`+f64.load+`/`+f64.store+`, +8-byte stride, alignment hint 3); recorded in +`+Typecheck.float_heap_sites+`. + +*Arrays DONE* (`+Array[Float]+`, incl. nested `+Array[Array[Float]]+`): +construct, read `+a[i]+`, and write `+a[i] = e+` all validate _and_ +round-trip the f64 correctly on wasmtime (`+FARR_OK+` / `+WRITE_OK+`). +`+guard_no_heap_float+`’s `+Array+` case is lifted accordingly. + +*Tuples DONE — all-`+Float+` AND mixed* (reproducer (b)): a tuple with +any scalar `+Float+` field uses a *uniform 8-byte cell* layout +(`+Ast.ExprCellTuple+` / `+Ast.ExprCellTupleIndex+`): field `+i+` at +offset `+i*8+` regardless of the field-type mix, per-cell op (`+f64+` +for a `+Float+` field, `+i32+` — low 4 bytes — otherwise). Uniform-8 +sidesteps type-dependent offset accumulation, so `+(Int, Float)+` and +`+(Float, Int)+` both round-trip (`+MIX_OK+` / `+FI_OK+`), as do +all-`+Float+` (`+FTUP_OK+`) and *`+Array[(Float, Float)]+`* +(`+AFT_OK+`). `+synth+` records per-field cell kinds (construct) and, +for _every_ access to a float-bearing tuple, the accessed field’s kind. +`+guard+`’s `+TyTuple+` case fully lifted. + +`+just wasm-validate+` pins *12 positive* Float-in-heap checks (incl. 7 +wasmtime round-trips) + the loud-fails. 477 tests green. + +*Closed `+Float+` records DONE.* A _closed_ float-bearing record uses +the uniform-8 layout with fields ordered *by name* +(`+Ast.ExprCellRecord+` / `+Ast.ExprCellField+`), so construction and +by-name access derive identical offsets independent of literal-vs-type +order — verified by the `+#{b:2.0, a:1.0}+` → `+REC_ORDER_OK+` +round-trip and mixed `+REC_MIX_OK+`. The unification subtlety is +handled: a field access only takes the cell path when `+repr obj_ty+` is +a *closed* `+TRecord+` (open/polymorphic rows and record literals with a +spread keep loud-failing; `+guard+`’s `+TyRecord+` case lifts only when +the row var is `+None+`). + +Also fixed: `+find_free_vars+` (codegen, runs on the post-elaborate +tree) now traverses `+ExprFloatBinary+` and all the new cell nodes, so a +variable captured only inside a float expression is no longer missed. + +*Still loud-failing (task #8) — now CLEAN `+UnsupportedFeature+` +rejects:* + +* *`+Float+` in closures* (captured `+Float+`, `+Float+` parameter, or +`+Float+` result). This is a _calling-convention_ gap, not a cell-layout +one: the closure ABI uses uniform 4-byte env/parameter cells and i32 +lambda param/result/local types. Full support needs an f64-aware closure +ABI (env cells, lambda signature, and the matching `+CallIndirect+` +type) — larger than the aggregate-cell work here. Now loud-fails cleanly +(was `+UnboundVariable+`). +* Compound assignment (`+a[i] += x+`) to a float element (rare; rewrite +as `+a[i] = a[i] + x+`). +* Open/polymorphic float records and float-record spreads. + +*Summary:* every `+Float+` that transits a heap *aggregate* (array, +tuple — all/mixed, record — closed) now lowers correctly and is +wasmtime-verified; the residual rejects are the closure calling +convention + two narrow cases, all honest loud-fails (no silent +corruption). + +=== Related + +Not the same as the deliberate carve-outs #555 (effect handlers) / #556 +(async CPS) — those loud-fail already. This one is a _silent_ defect in +the value representation, newly made visible by the validate gate. diff --git a/issues-drafts/05-float-through-heap-cell-model-invalid-wasm.md b/issues-drafts/05-float-through-heap-cell-model-invalid-wasm.md deleted file mode 100644 index 30fc08ee..00000000 --- a/issues-drafts/05-float-through-heap-cell-model-invalid-wasm.md +++ /dev/null @@ -1,143 +0,0 @@ - - -# Core-wasm: any `Float` that transits the heap is mismodeled (invalid/truncated wasm) - -**Surfaced by:** WASM coverage sweep + coprocessor smoke test (2026-06-16) -**Affected version:** `affinescript` compiler at HEAD (branch `feat/solo-core-metatheory-proofs`) -**Severity:** Correctness + **security** — silent 32-of-64-bit truncation on store; invalid module on load. Caught for the first time by the new `just wasm-validate` gate (`tools/wasm-validate-gate.sh`); previously hidden because `test/test_e2e.ml:601` only checks "codegen did not raise", never `wasm-tools validate`. - -## Reproducers - -```affinescript -// (a) load: INVALID module — wasm-tools: "expected f64, found i32" -fn rd(i: Int, a: Array[Float]) -> Float { a[i] } - -// (b) projection: INVALID module (same cause, via tuple) -fn proj() -> Float { let t: (Float, Float) = (1.0, 2.0); t.0 } - -// (c) store: VALIDATES but is SEMANTICALLY WRONG — copies 32 of 64 bits -fn k(i: Int, mut o: Array[Float], a: Array[Float]) -> Unit { o[i] = a[i]; } -``` - -``` -$ affinescript compile rd.affine -o rd.wasm && wasm-tools validate rd.wasm -error: func 1 failed to validate - 0: type mismatch: expected f64, found i32 -``` - -Scalars are fine (`fn dbl(x: Float) -> Float { x * 2.0 }` validates) — the bug is strictly the heap path. `Int` through the heap is fine. - -## Root cause - -The core-wasm backend (`lib/codegen.ml`) uses a **uniform 4-byte (i32) heap-cell -model** everywhere: - -- array alloc `size = 4 + (num_elements * 4)`, element offset `4 + (idx * 4)` - (`lib/codegen.ml:1926`, `:1949`, `:2047-2053`) -- tuple/record fields at `index * 4`, `I32Load`/`I32Store` with a `* 4` stride - (`:1964`, `:2030`, `:2519-2548`) - -A `Float` is `f64` (8 bytes) in locals/arithmetic, but the moment it is stored -into or loaded from an array/tuple/record cell the layout assumes 4 bytes and -the access is emitted as `i32.{load,store}`. Hence: load-then-use-as-f64 → -invalid module; store → 32-bit truncation of a 64-bit value. - -## Fix options - -1. **Type-directed heap layout (the real fix).** Make cell size and the - load/store opcode a function of the static element/field type: `f64` cells - are 8 bytes with `f64.{load,store}` and an 8-byte stride; mixed records/ - tuples compute per-field offsets from field types. Touches arrays, tuples, - records, and closures — a substantial, careful codegen change. **High - revert-cost → land behind its own PR with the `wasm-validate` gate - extended to cover Float-in-heap fixtures.** -2. **Honest loud-fail (interim, secure).** Until (1) lands, have the backend - raise `Codegen.UnsupportedFeature` when a `Float` would transit a heap cell, - with a message routing to the interpreter (`-i`), the Julia backend - (`-julia`), or the GPU kernel backends (WGSL/CUDA/Metal/OpenCL **already - lower `f64` array buffers correctly** — verified 2026-06-16). This converts - silent-wrong into honest-reject, matching the #555/#556 loud-fail policy. - -Recommended: ship (2) now for safety, then (1) as the durable fix. Both are -gated by `just wasm-validate`. - -**Status (2026-06-16): interim secure fix (2) LANDED.** `lib/codegen.ml` now -raises `UnsupportedFeature` when a `Float` would transit a heap cell — guarded at -function param/return types (`guard_fn_no_heap_float`) and at Array/tuple/record -*literals* (`guard_no_float_elems`). Silent corruption / invalid emission is gone; -scalar `Float` and `Int` aggregates are untouched. `just wasm-validate` now pins -the loud-fail (two `rej` cases). **The real fix (1) — type-directed heap layout — -remains open as task #8.** - -## Update (2026-06-16, cont.) — durable fix (1) for ARRAYS landed via the Float wall - -The durable fix is being delivered type-directed and *complete-by-construction* -through the existing **Float-wall elaboration** (the same mechanism that makes -scalar `Float` arithmetic work): `synth` (the real typechecker) records the heap -nodes whose *cell* type is `Float`, and `elaborate_string_concat` rewrites those -exact nodes into specialized AST constructors that codegen lowers with f64 ops. -Because `synth` sees *every* node's checked type, recording is total — every -`Float` construction and every `Float`-yielding access is caught no matter how the -array flowed there — so codegen never guesses a cell width (the gap that made a -codegen-local fix unsafe). New constructors (`Ast.ExprFloatArray`, -`Ast.ExprFloatIndex`) lay out a 4-byte length header + **8-byte f64 cells** -(`f64.load`/`f64.store`, 8-byte stride, alignment hint 3); recorded in -`Typecheck.float_heap_sites`. - -**Arrays DONE** (`Array[Float]`, incl. nested `Array[Array[Float]]`): construct, -read `a[i]`, and write `a[i] = e` all validate *and* round-trip the f64 correctly -on wasmtime (`FARR_OK` / `WRITE_OK`). `guard_no_heap_float`'s `Array` case is -lifted accordingly. - -**Tuples DONE — all-`Float` AND mixed** (reproducer (b)): a tuple with any -scalar `Float` field uses a **uniform 8-byte cell** layout (`Ast.ExprCellTuple` / -`Ast.ExprCellTupleIndex`): field `i` at offset `i*8` regardless of the field-type -mix, per-cell op (`f64` for a `Float` field, `i32` — low 4 bytes — otherwise). -Uniform-8 sidesteps type-dependent offset accumulation, so `(Int, Float)` and -`(Float, Int)` both round-trip (`MIX_OK` / `FI_OK`), as do all-`Float` (`FTUP_OK`) -and **`Array[(Float, Float)]`** (`AFT_OK`). `synth` records per-field cell kinds -(construct) and, for *every* access to a float-bearing tuple, the accessed -field's kind. `guard`'s `TyTuple` case fully lifted. - -`just wasm-validate` pins **12 positive** Float-in-heap checks (incl. 7 wasmtime -round-trips) + the loud-fails. 477 tests green. - -**Closed `Float` records DONE.** A *closed* float-bearing record uses the -uniform-8 layout with fields ordered **by name** (`Ast.ExprCellRecord` / -`Ast.ExprCellField`), so construction and by-name access derive identical -offsets independent of literal-vs-type order — verified by the -`#{b:2.0, a:1.0}` → `REC_ORDER_OK` round-trip and mixed `REC_MIX_OK`. The -unification subtlety is handled: a field access only takes the cell path when -`repr obj_ty` is a **closed** `TRecord` (open/polymorphic rows and record -literals with a spread keep loud-failing; `guard`'s `TyRecord` case lifts only -when the row var is `None`). - -Also fixed: `find_free_vars` (codegen, runs on the post-elaborate tree) now -traverses `ExprFloatBinary` and all the new cell nodes, so a variable captured -only inside a float expression is no longer missed. - -**Still loud-failing (task #8) — now CLEAN `UnsupportedFeature` rejects:** - -* **`Float` in closures** (captured `Float`, `Float` parameter, or `Float` - result). This is a *calling-convention* gap, not a cell-layout one: the - closure ABI uses uniform 4-byte env/parameter cells and i32 lambda - param/result/local types. Full support needs an f64-aware closure ABI (env - cells, lambda signature, and the matching `CallIndirect` type) — larger than - the aggregate-cell work here. Now loud-fails cleanly (was `UnboundVariable`). -* Compound assignment (`a[i] += x`) to a float element (rare; rewrite as - `a[i] = a[i] + x`). -* Open/polymorphic float records and float-record spreads. - -**Summary:** every `Float` that transits a heap **aggregate** (array, tuple -— all/mixed, record — closed) now lowers correctly and is wasmtime-verified; -the residual rejects are the closure calling convention + two narrow cases, -all honest loud-fails (no silent corruption). - -## Related - -Not the same as the deliberate carve-outs #555 (effect handlers) / #556 (async -CPS) — those loud-fail already. This one is a *silent* defect in the value -representation, newly made visible by the validate gate. diff --git a/issues-drafts/06-ownership-carrier-has-no-affine-kind.adoc b/issues-drafts/06-ownership-carrier-has-no-affine-kind.adoc new file mode 100644 index 00000000..ff756caf --- /dev/null +++ b/issues-drafts/06-ownership-carrier-has-no-affine-kind.adoc @@ -0,0 +1,64 @@ +== typed-wasm ownership carrier has no `+Affine+` kind — AffineScript `+own+` is mis-mapped to Linear (L10, exactly-once) + +*Surfaced by:* typed-wasm round-trip verification (2026-06-16, +`+tools/typed-wasm-roundtrip-gate.sh+`) *Affected:* the +`+typedwasm.ownership+` v1 carrier (cross-repo: AffineScript + ephapax + +typed-wasm verifier) *Severity:* Semantic / contract — not a crash. +AffineScript can emit modules its own (affine) semantics accept but the +typed-wasm L10 (linear) verifier rejects. + +=== Observation + +The v1 ownership-section `+kind+` enum is +`+{0=Unrestricted, 1=Linear, 2=SharedBorrow, 3=ExclBorrow}+` (see +`+docs/specs/TYPED-WASM-INTERFACE.adoc+`). There is *no `+Affine+` +(at-most-once) kind.* AffineScript maps `+own+` → `+Linear+` +(`+lib/codegen.ml+` `+ownership_kind_of_param+`), and `+Linear+` is +verified as *exactly-once on every path* (L10). + +But AffineScript is *affine*, not linear — dropping an owned value is +legitimate (this is the load-bearing result of the Solo-core +affine-preservation mechanisation: the honest theorem is +reduct-in-a-`+Weaker+`-sub-context; resources _may_ be dropped). So: + +[source,affinescript] +---- +fn drop_owned(x: own String) -> () = (); // x dropped — FINE in affine AffineScript +---- + +round-trips to a module that *both* verifiers reject as an L10 violation +(verified bit-exactly: AffineScript `+tw_verify.ml+` and typed-wasm Rust +`+tw-verify+` give the identical "`Level 10 violation: param 0 — Linear +(own) param dropped on all paths`"). The mapping is _too strict_: +`+own+` has no faithful carrier representation — only `+Linear+` +(rejects legal drops) or `+Unrestricted+` (loses the discipline +entirely). + +This is why `+compile+`’s Stage-8 ownership check is *advisory* (warns, +still emits) while `+verify+` is *fatal* — the compiler already knows it +is affine and that L10 over-rejects it. + +=== Why it matters + +The integration _works_ (carrier round-trips, both verifiers agree) — +but it agrees on a verdict that is wrong _for an affine source +language_. As more affine programs target typed-wasm, the +advisory-warning noise grows and the contract misrepresents the source +semantics. + +=== Fix direction (multi-producer ABI change — coordinate, don’t unilaterally patch) + +Add an `+Affine+` kind (at-most-once) to the ownership carrier — a v2 +schema change requiring coordination across AffineScript, +`+hyperpolymath/ephapax+`, and the `+hyperpolymath/typed-wasm+` Rust +verifier (the spec’s stated ABI-change protocol; cf. ADR-020 "`Schema +versioning`" in `+TYPED-WASM-ROADMAP.adoc+`). The verifier would check +`+Affine+` as `+≤1-use-on-every-path+` (the same machinery as +`+ExclBorrow+`’s L7 check, minus the aliasing part). Then `+own+` → +`+Affine+` and the advisory warnings on legal drops disappear. + +Carries the Solo-core decision +(`+project_affinescript_solo_core_affine_preservation+`) up to the +typed-wasm boundary: the affine theorem shape needs an affine carrier +kind. Until then, the `+own+`→`+Linear+` mapping + advisory check is the +honest interim. diff --git a/issues-drafts/06-ownership-carrier-has-no-affine-kind.md b/issues-drafts/06-ownership-carrier-has-no-affine-kind.md deleted file mode 100644 index b35036ad..00000000 --- a/issues-drafts/06-ownership-carrier-has-no-affine-kind.md +++ /dev/null @@ -1,60 +0,0 @@ - - -# typed-wasm ownership carrier has no `Affine` kind — AffineScript `own` is mis-mapped to Linear (L10, exactly-once) - -**Surfaced by:** typed-wasm round-trip verification (2026-06-16, `tools/typed-wasm-roundtrip-gate.sh`) -**Affected:** the `typedwasm.ownership` v1 carrier (cross-repo: AffineScript + ephapax + typed-wasm verifier) -**Severity:** Semantic / contract — not a crash. AffineScript can emit modules its own (affine) semantics accept but the typed-wasm L10 (linear) verifier rejects. - -## Observation - -The v1 ownership-section `kind` enum is `{0=Unrestricted, 1=Linear, -2=SharedBorrow, 3=ExclBorrow}` (see `docs/specs/TYPED-WASM-INTERFACE.adoc`). -There is **no `Affine` (at-most-once) kind.** AffineScript maps `own` → `Linear` -(`lib/codegen.ml` `ownership_kind_of_param`), and `Linear` is verified as -**exactly-once on every path** (L10). - -But AffineScript is **affine**, not linear — dropping an owned value is -legitimate (this is the load-bearing result of the Solo-core affine-preservation -mechanisation: the honest theorem is reduct-in-a-`Weaker`-sub-context; resources -*may* be dropped). So: - -```affinescript -fn drop_owned(x: own String) -> () = (); // x dropped — FINE in affine AffineScript -``` - -round-trips to a module that **both** verifiers reject as an L10 violation -(verified bit-exactly: AffineScript `tw_verify.ml` and typed-wasm Rust -`tw-verify` give the identical "Level 10 violation: param 0 — Linear (own) param -dropped on all paths"). The mapping is *too strict*: `own` has no faithful -carrier representation — only `Linear` (rejects legal drops) or `Unrestricted` -(loses the discipline entirely). - -This is why `compile`'s Stage-8 ownership check is **advisory** (warns, still -emits) while `verify` is **fatal** — the compiler already knows it is affine and -that L10 over-rejects it. - -## Why it matters - -The integration *works* (carrier round-trips, both verifiers agree) — but it -agrees on a verdict that is wrong *for an affine source language*. As more -affine programs target typed-wasm, the advisory-warning noise grows and the -contract misrepresents the source semantics. - -## Fix direction (multi-producer ABI change — coordinate, don't unilaterally patch) - -Add an `Affine` kind (at-most-once) to the ownership carrier — a v2 schema -change requiring coordination across AffineScript, `hyperpolymath/ephapax`, and -the `hyperpolymath/typed-wasm` Rust verifier (the spec's stated ABI-change -protocol; cf. ADR-020 "Schema versioning" in `TYPED-WASM-ROADMAP.adoc`). The -verifier would check `Affine` as `≤1-use-on-every-path` (the same machinery as -`ExclBorrow`'s L7 check, minus the aliasing part). Then `own` → `Affine` and the -advisory warnings on legal drops disappear. - -Carries the Solo-core decision (`project_affinescript_solo_core_affine_preservation`) -up to the typed-wasm boundary: the affine theorem shape needs an affine carrier -kind. Until then, the `own`→`Linear` mapping + advisory check is the honest -interim. diff --git a/issues-drafts/07-superlinear-compile-scaling.adoc b/issues-drafts/07-superlinear-compile-scaling.adoc new file mode 100644 index 00000000..cb725529 --- /dev/null +++ b/issues-drafts/07-superlinear-compile-scaling.adoc @@ -0,0 +1,102 @@ +== Compile-time scaling is super-linear (≈O(n²)) — quadratic blow-up past ~1000 functions + +*Surfaced by:* the scaling bench (`+bench/bench_scaling.ml+`, +`+just bench+`), 2026-06-16. *Severity:* Performance. Invisible on the +in-repo corpus (max ~114 lines); real for any large program. + +=== Measurement (parse + resolve + wasm-codegen, generated `+fn fI(x:Int)->Int { x+I }+` × N) + +[cols=",,",options="header",] +|=== +|N functions |total |µs/func +|10 |0.06 ms |5.6 +|100 |0.44 ms |4.4 +|1000 |12.5 ms |12.5 +|*5000* |*401 ms* |*80.3* +|=== + +µs/function should be ~flat for a linear pipeline. Instead it climbs +~18× between n=100 and n=5000, and the 1000→5000 step (5× input) costs +~32× the time — empirically ≈ O(n²) (log₅32 ≈ 2.15). + +=== Where to look + +The per-function work that grows with total program size is almost +certainly a *full-program scan repeated per item*. Prime suspects, in +order: + +[arabic] +. *Name resolution* (`+lib/resolve.ml+`) — if the symbol/module table is +an assoc-list scanned per reference, or each function re-walks the whole +top-level, that is the classic O(n²). (Resolve is the most likely +culprit.) +. *Codegen* (`+lib/codegen.ml+`) — a per-function pass that walks all +declarations (e.g. building a function-index table by +`+List.assoc+`/`+List.nth+` rather than a `+Hashtbl+`). +. Parser/AST construction is usually linear; rule it out by +phase-splitting the bench timing (parse vs resolve vs codegen +separately) to localise the curve. + +=== Next step + +Phase-split `+bench_scaling+` (time parse / resolve / codegen +independently at each N) to pin the quadratic phase, then replace the +offending `+List.assoc+`/`+List.nth+` / per-item full scan with a +`+Hashtbl+`. Target: flat µs/func to n≥50 000. Promote the bench to a +baselined Six-Sigma gate once linear +(`+docs/TESTING-AND-BENCH-MATRIX.adoc+`). + +This is the first defect found by closing the "`large-input scaling +unmeasured`" gap — the bench paid for itself on its first run. + +=== Update (2026-06-16) — phase-split localisation + partial fix (6.5×) + +Phase-split timing (`+bench_scaling.ml+` now times parse/resolve/codegen +separately) shows *parse and resolve are flat (linear)* — the quadratic +is entirely in *`+lib/codegen.ml+`* (not resolve, as first guessed). Two +confirmed O(n²) sources found and fixed: + +[arabic] +. *`+@+`-append accumulation per function* — `+gen_decl+` appended to +`+funcs+`, `+func_indices+`, `+ownership_annots+` with `+xs @ [x]+` +(O(len) each) → O(n²). Fixed: cons (O(1)) + `+List.rev+` once at +emission (`+all_funcs+`, `+build_ownership_section+`). Indices preserved +(they come from `+List.length+`, order-independent). +. *`+List.length ctx.funcs+` per function* (index assignment, +codegen.ml:3204) → O(n²). Fixed: an O(1) `+num_funcs+` counter field on +the context. + +Result (codegen, n=5000): *453 ms → 70 ms (~6.5×)*; 477 tests + +`+wasm-validate+` green (byte-identical indices). *Residual:* codegen is +still mildly super-linear (~1→14 µs/func, 100→5000) — a third, smaller +source remains. + +=== Update (2026-06-16, cont.) — residual localised and FIXED (now flat/linear) + +The "`ruled out by inspection`" note above was *wrong about +`+intern_func_type+`*. It _does_ dedup — but the regular *`+TopFn+` path +never called it*. Only the `+extern fn+` path (codegen.ml:3178) +interned; the ordinary function path (codegen.ml:3202-3203) did +`+type_idx = List.length ctx.types+` + +`+types = ctx.types @ [func_type]+` *unconditionally*, so `+ctx.types+` +grew by one *per function* — both ops O(len) per decl → the residual +O(n²). The scaling bench masked it from static reasoning because every +generated function has the _identical_ `+(Int)->Int+` signature, yet +each still extended the list. + +Fix: route the regular `+TopFn+` path through `+intern_func_type+` too +(one-line change). Interning never reorders existing entries (equal type +→ existing index, new type → appended at the same end position), so all +previously-assigned type indices are preserved; bonus is a smaller, +canonical Wasm type section. + +Result (codegen): *n=5000 70 ms → 8 ms* (~8.6× further; *~50× vs the +original 401 ms*), and the curve is now *flat — 0.8 → 1.6 µs/func across +n=100→5000* (2× over 50× input = noise, not a trend). 477 tests + +`+wasm-validate+` (21/0/5) green. *Caveat:* interning is +O(#distinct-signatures) per decl; a program with a unique signature for +every function would re-introduce a (milder) quadratic — a +`+Hashtbl+`-keyed interner would make it true O(1). Deferred +(pathological case; real programs reuse signatures). The ADR-0026 F1 +Isabelle proof is the durable guard. *This issue is now resolved for the +common case; closing candidate.* diff --git a/issues-drafts/07-superlinear-compile-scaling.md b/issues-drafts/07-superlinear-compile-scaling.md deleted file mode 100644 index 85e8dd82..00000000 --- a/issues-drafts/07-superlinear-compile-scaling.md +++ /dev/null @@ -1,89 +0,0 @@ - - -# Compile-time scaling is super-linear (≈O(n²)) — quadratic blow-up past ~1000 functions - -**Surfaced by:** the scaling bench (`bench/bench_scaling.ml`, `just bench`), 2026-06-16. -**Severity:** Performance. Invisible on the in-repo corpus (max ~114 lines); real for any large program. - -## Measurement (parse + resolve + wasm-codegen, generated `fn fI(x:Int)->Int { x+I }` × N) - -| N functions | total | µs/func | -|---|---|---| -| 10 | 0.06 ms | 5.6 | -| 100 | 0.44 ms | 4.4 | -| 1000 | 12.5 ms | 12.5 | -| **5000** | **401 ms** | **80.3** | - -µs/function should be ~flat for a linear pipeline. Instead it climbs ~18× between -n=100 and n=5000, and the 1000→5000 step (5× input) costs ~32× the time — -empirically ≈ O(n²) (log₅32 ≈ 2.15). - -## Where to look - -The per-function work that grows with total program size is almost certainly a -**full-program scan repeated per item**. Prime suspects, in order: - -1. **Name resolution** (`lib/resolve.ml`) — if the symbol/module table is an - assoc-list scanned per reference, or each function re-walks the whole - top-level, that is the classic O(n²). (Resolve is the most likely culprit.) -2. **Codegen** (`lib/codegen.ml`) — a per-function pass that walks all - declarations (e.g. building a function-index table by `List.assoc`/`List.nth` - rather than a `Hashtbl`). -3. Parser/AST construction is usually linear; rule it out by phase-splitting the - bench timing (parse vs resolve vs codegen separately) to localise the curve. - -## Next step - -Phase-split `bench_scaling` (time parse / resolve / codegen independently at each -N) to pin the quadratic phase, then replace the offending `List.assoc`/`List.nth` -/ per-item full scan with a `Hashtbl`. Target: flat µs/func to n≥50 000. Promote -the bench to a baselined Six-Sigma gate once linear (`docs/TESTING-AND-BENCH-MATRIX.adoc`). - -This is the first defect found by closing the "large-input scaling unmeasured" -gap — the bench paid for itself on its first run. - -## Update (2026-06-16) — phase-split localisation + partial fix (6.5×) - -Phase-split timing (`bench_scaling.ml` now times parse/resolve/codegen -separately) shows **parse and resolve are flat (linear)** — the quadratic is -entirely in **`lib/codegen.ml`** (not resolve, as first guessed). Two confirmed -O(n²) sources found and fixed: - -1. **`@`-append accumulation per function** — `gen_decl` appended to `funcs`, - `func_indices`, `ownership_annots` with `xs @ [x]` (O(len) each) → O(n²). Fixed: - cons (O(1)) + `List.rev` once at emission (`all_funcs`, `build_ownership_section`). - Indices preserved (they come from `List.length`, order-independent). -2. **`List.length ctx.funcs` per function** (index assignment, codegen.ml:3204) → - O(n²). Fixed: an O(1) `num_funcs` counter field on the context. - -Result (codegen, n=5000): **453 ms → 70 ms (~6.5×)**; 477 tests + `wasm-validate` -green (byte-identical indices). **Residual:** codegen is still mildly -super-linear (~1→14 µs/func, 100→5000) — a third, smaller source remains. - -## Update (2026-06-16, cont.) — residual localised and FIXED (now flat/linear) - -The "ruled out by inspection" note above was **wrong about `intern_func_type`**. -It *does* dedup — but the regular **`TopFn` path never called it**. Only the -`extern fn` path (codegen.ml:3178) interned; the ordinary function path -(codegen.ml:3202-3203) did `type_idx = List.length ctx.types` + -`types = ctx.types @ [func_type]` **unconditionally**, so `ctx.types` grew by -one **per function** — both ops O(len) per decl → the residual O(n²). The -scaling bench masked it from static reasoning because every generated function -has the *identical* `(Int)->Int` signature, yet each still extended the list. - -Fix: route the regular `TopFn` path through `intern_func_type` too (one-line -change). Interning never reorders existing entries (equal type → existing index, -new type → appended at the same end position), so all previously-assigned type -indices are preserved; bonus is a smaller, canonical Wasm type section. - -Result (codegen): **n=5000 70 ms → 8 ms** (~8.6× further; **~50× vs the original -401 ms**), and the curve is now **flat — 0.8 → 1.6 µs/func across n=100→5000** -(2× over 50× input = noise, not a trend). 477 tests + `wasm-validate` (21/0/5) -green. **Caveat:** interning is O(#distinct-signatures) per decl; a program with -a unique signature for every function would re-introduce a (milder) quadratic — -a `Hashtbl`-keyed interner would make it true O(1). Deferred (pathological case; -real programs reuse signatures). The ADR-0026 F1 Isabelle proof is the durable -guard. **This issue is now resolved for the common case; closing candidate.** diff --git a/issues-drafts/08-conditional-origin-borrow-escape.adoc b/issues-drafts/08-conditional-origin-borrow-escape.adoc new file mode 100644 index 00000000..3dcef1ea --- /dev/null +++ b/issues-drafts/08-conditional-origin-borrow-escape.adoc @@ -0,0 +1,172 @@ +== Borrow checker: use-after-move via a borrow bound through an `+if+`/`+match+`/`+block+` expression (caller-side origin escape) + +____ +*Parked draft (2026-06-16).* No SPDX header added — owner-only per the +no-automated-licence-edits directive; owner adds the standard +issue-draft header (as on 01–07) and commits signed. Found by the +soundness adversarial probe (Polonius phase, step 2). +____ + +____ +*✅ RESOLVED 2026-06-16 (same session).* Closed _without_ full Polonius +— a bounded fix in the borrow-checker’s _checking pass_ +(`+lib/borrow.ml+`). All 8 reproducers below now reject +(`+MoveWhileBorrowed+`); a second adversarial round (if-of-match, +match-of-block, `+try+`-bound, tuple-smuggled, ref-var-forwarding block, +deep mix) also rejects; anti-over-rejection (NLL use-before-move, +unrelated move, value-blocks) still passes; full suite green (483 tests, ++6). *The root cause differed from the hypothesis in "`Proposed fix`" +below — see "`What the fix actually was`".* +____ + +=== What the fix actually was (supersedes "`Proposed fix`") + +The hypothesis below pointed at `+origins_of_ref_source+` inside +`+compute_ret_borrow_params+` (the per-function return-borrow _summary_ +builder). That was the wrong site: the repro is in `+main+`, which does +not _return_ the borrow, so the summary is irrelevant to it. The real +defect was in the _checking pass_: + +[arabic] +. *`+check_block+`* restored `+state.borrows+` to block-entry and +cleared `+state.result_borrows+` at block exit, so a block/branch whose +*value* is a returned borrow of an _outer_ place silently swallowed that +borrow. Fixed by computing the tail’s escaping borrows _before_ the +lexical restore (filtering out borrows rooted at block-local owners — +those genuinely die / are caught by `+BorrowOutlivesOwner+`) and +*re-publishing* them past the restore onto `+state.borrows+` + +`+state.result_borrows+`. +. *`+ExprIf+` / `+ExprMatch+` joins* intersected branch borrows by +`+b_id+`; since each branch mints a _distinct_ borrow record for the +same origin, the intersection dropped both. Fixed by capturing each +branch/arm’s escaping borrows and re-publishing their *union* (the value +is one branch tail or another, so union is the sound merge; it can only +keep more borrows live). +. *`+record_ref_binding+`* (and the `+StmtAssign+` reassign path) only +claimed `+result_borrows+` for an `+ExprApp+` value. Broadened to +`+ExprApp | ExprIf | ExprMatch | ExprBlock+`, so +`+let r = if … { pick(a) }+` aliases `+a+` exactly as +`+let r = pick(a)+` does. + +A new helper `+value_escaping+` dispatches on the _shape_ of a checked +value (call/if/match/block → the `+result_borrows+` channel; +`+&p+`/ref-var → structural `+ref_source_borrow+`; else none) so a stale +channel left by an earlier sibling statement is never mis-attributed. + +*Severity:* Soundness — _false negative_ (accepts a use-after-move). +Same loan-propagation class as #554, on the *caller* side. + +=== Summary + +#554 (PR #595) made a callee-returned borrow register against its +argument via a per-function *return-borrow summary* + call-graph +fixpoint, so `+let r = pick(a); consume(a); *r+` is caught. But when the +result binder is bound through a *conditional or compound expression*, +the origin is lost and the move slips past again: + +[source,affinescript] +---- +fn pick(ref x: Int) -> ref Int { return &x; } +fn consume(own v: Int) -> Int { return v; } + +fn main() -> Int { + let a: Int = 7; + let c: Bool = true; + let r = if c { pick(a) } else { pick(a) }; // r borrows a — origin LOST here + let _g = consume(a); // moves a while r is live + return *r; // use-after-move — ACCEPTED (unsound) +} +---- + +=== Confirmed by probe (2026-06-16) — all ACCEPTED (should be rejected) + +[width="100%",cols="50%,50%",options="header",] +|=== +|Form binding the borrow |Result +|`+let r = if c { pick(a) } else { pick(a) }+` |❌ accepted + +|`+let r = if c { pick(a) } else { pick(b) }+` (partial) |❌ accepted + +|`+let r = if c { { pick(a) } } else { pick(a) }+` (nested) |❌ accepted + +|`+let r = if c { if c { pick(a) } else { pick(a) } } else { pick(a) }+` +|❌ accepted + +|`+let r = { pick(a) }+` (plain block) |❌ accepted + +|`+let r = { { pick(a) } }+` (nested block) |❌ accepted + +|`+let r = if c { let t = pick(a); t } else { pick(a) }+` |❌ accepted + +|`+let r = match k { 0 => pick(a), _ => pick(a) }+` (multi-arm) |❌ +accepted +|=== + +*Caught (sound) — for contrast:* + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Form |Result |Why +|`+let r = pick(a)+` (direct) |✅ caught |`+origins_of_ref_source+` +handles `+ExprApp+` + +|`+let r = match a { _ => pick(a) }+` (single-arm) |✅ caught +|_incidental_ — scrutinee `+a+` is the moved var, not origin tracking + +|`+let r = if c { pickm(a) } …+` (`+&mut+`/exclusive) |✅ caught +|exclusive borrow tracked on a separate path + +|callee `+fn f(ref x){ if _ {return &x} else {return &x} }+` |✅ caught +|the *summary* side (`+walk_tail+`) already recurses return-tails +|=== + +=== Root cause + +`+origins_of_ref_source+` (`+lib/borrow.ml:233+`) — which `+record_let+` +uses to give a `+let+`-binder its origins — only matches +`+ExprUnary(OpRef|OpMutRef)+`, `+ExprVar+`, and `+ExprApp+`; everything +else (incl. `+ExprIf+`, `+ExprMatch+`, `+ExprBlock+`) falls to +`+| _ -> []+`, so the binder gets *no origins* and never registers as a +borrow of the argument. The asymmetry is with `+walk_tail+` (same file, +~line 283), which _does_ descend `+if+`/`+match+`/`+block+` tails when +harvesting *return* origins — that is exactly why the callee-summary +side is sound but the caller let-binding side is not. + +=== Proposed fix (bounded — not ADR-022) + +Make `+origins_of_ref_source+` recurse into compound expressions, taking +the *union* of the origins of all tail positions, mirroring +`+walk_tail+`: + +* `+ExprIf (_, then, else?)+` → +`+origins_of_ref_source then @ (else? origins)+` +* `+ExprMatch (_, arms)+` → +`+List.concat_map (origins_of_ref_source ∘ arm tail) arms+` +* `+ExprBlock b+` → thread the block’s own `+let+`s into +`+local_origins+` (as the outer scan does), then +`+origins_of_ref_source+` of the block tail. + +This converges the caller-side origin computation with the already-sound +callee-side `+walk_tail+`. Union-of-branches is conservative +(over-approximates origins → cannot introduce a _new_ false negative), +matching the documented "`sound direction`" invariant. + +*Note vs the #554 residual (b):* the #554 close-out recorded residual +(b) ("`branch-merged / copy-out claim`") as _"`closed only by Polonius +#553.`"_ This finding suggests at least the *let-bound conditional* +manifestation is closeable by the syntactic extension above, *without* +full Polonius origin/region variables — worth re-checking that +characterisation before attributing it solely to ADR-022. + +=== Hardening fixtures (✅ ADDED — `+test/e2e/fixtures/borrow_cond_origin_*+`, wired into `+test_e2e.ml+` `+borrow_tests+`) + +[arabic] +. `+if+`-bound, both branches borrow `+a+`, move `+a+`, `+*r+` after → +*reject*. +. `+block+`-bound (plain + nested) → *reject*. +. multi-arm `+match+`-bound (scrutinee ≠ moved var) → *reject*. +. partial (one branch borrows `+a+`, other borrows `+b+`); move `+a+` → +*reject*; move an unrelated `+c+` → *accept*. +. anti-over-rejection: each of the above with `+*r+` *read before* the +move → *accept* (NLL last-use). +. nested `+if { if … }+` and `+if { let t = …; t }+`. diff --git a/issues-drafts/08-conditional-origin-borrow-escape.md b/issues-drafts/08-conditional-origin-borrow-escape.md deleted file mode 100644 index 57f144b8..00000000 --- a/issues-drafts/08-conditional-origin-borrow-escape.md +++ /dev/null @@ -1,135 +0,0 @@ -# Borrow checker: use-after-move via a borrow bound through an `if`/`match`/`block` expression (caller-side origin escape) - -> **Parked draft (2026-06-16).** No SPDX header added — owner-only per the -> no-automated-licence-edits directive; owner adds the standard issue-draft -> header (as on 01–07) and commits signed. Found by the soundness adversarial -> probe (Polonius phase, step 2). - -> **✅ RESOLVED 2026-06-16 (same session).** Closed *without* full Polonius — a -> bounded fix in the borrow-checker's *checking pass* (`lib/borrow.ml`). All 8 -> reproducers below now reject (`MoveWhileBorrowed`); a second adversarial round -> (if-of-match, match-of-block, `try`-bound, tuple-smuggled, ref-var-forwarding -> block, deep mix) also rejects; anti-over-rejection (NLL use-before-move, -> unrelated move, value-blocks) still passes; full suite green (483 tests, +6). -> **The root cause differed from the hypothesis in "Proposed fix" below — see -> "What the fix actually was".** - -## What the fix actually was (supersedes "Proposed fix") - -The hypothesis below pointed at `origins_of_ref_source` inside -`compute_ret_borrow_params` (the per-function return-borrow *summary* builder). -That was the wrong site: the repro is in `main`, which does not *return* the -borrow, so the summary is irrelevant to it. The real defect was in the -*checking pass*: - -1. **`check_block`** restored `state.borrows` to block-entry and cleared - `state.result_borrows` at block exit, so a block/branch whose **value** is a - returned borrow of an *outer* place silently swallowed that borrow. Fixed by - computing the tail's escaping borrows *before* the lexical restore (filtering - out borrows rooted at block-local owners — those genuinely die / are caught - by `BorrowOutlivesOwner`) and **re-publishing** them past the restore onto - `state.borrows` + `state.result_borrows`. -2. **`ExprIf` / `ExprMatch` joins** intersected branch borrows by `b_id`; since - each branch mints a *distinct* borrow record for the same origin, the - intersection dropped both. Fixed by capturing each branch/arm's escaping - borrows and re-publishing their **union** (the value is one branch tail or - another, so union is the sound merge; it can only keep more borrows live). -3. **`record_ref_binding`** (and the `StmtAssign` reassign path) only claimed - `result_borrows` for an `ExprApp` value. Broadened to `ExprApp | ExprIf | - ExprMatch | ExprBlock`, so `let r = if … { pick(a) }` aliases `a` exactly as - `let r = pick(a)` does. - -A new helper `value_escaping` dispatches on the *shape* of a checked value -(call/if/match/block → the `result_borrows` channel; `&p`/ref-var → structural -`ref_source_borrow`; else none) so a stale channel left by an earlier sibling -statement is never mis-attributed. - -**Severity:** Soundness — *false negative* (accepts a use-after-move). Same -loan-propagation class as #554, on the **caller** side. - -## Summary - -#554 (PR #595) made a callee-returned borrow register against its argument via a -per-function **return-borrow summary** + call-graph fixpoint, so -`let r = pick(a); consume(a); *r` is caught. But when the result binder is bound -through a **conditional or compound expression**, the origin is lost and the -move slips past again: - -```affinescript -fn pick(ref x: Int) -> ref Int { return &x; } -fn consume(own v: Int) -> Int { return v; } - -fn main() -> Int { - let a: Int = 7; - let c: Bool = true; - let r = if c { pick(a) } else { pick(a) }; // r borrows a — origin LOST here - let _g = consume(a); // moves a while r is live - return *r; // use-after-move — ACCEPTED (unsound) -} -``` - -## Confirmed by probe (2026-06-16) — all ACCEPTED (should be rejected) - -| Form binding the borrow | Result | -|---|---| -| `let r = if c { pick(a) } else { pick(a) }` | ❌ accepted | -| `let r = if c { pick(a) } else { pick(b) }` (partial) | ❌ accepted | -| `let r = if c { { pick(a) } } else { pick(a) }` (nested) | ❌ accepted | -| `let r = if c { if c { pick(a) } else { pick(a) } } else { pick(a) }` | ❌ accepted | -| `let r = { pick(a) }` (plain block) | ❌ accepted | -| `let r = { { pick(a) } }` (nested block) | ❌ accepted | -| `let r = if c { let t = pick(a); t } else { pick(a) }` | ❌ accepted | -| `let r = match k { 0 => pick(a), _ => pick(a) }` (multi-arm) | ❌ accepted | - -**Caught (sound) — for contrast:** - -| Form | Result | Why | -|---|---|---| -| `let r = pick(a)` (direct) | ✅ caught | `origins_of_ref_source` handles `ExprApp` | -| `let r = match a { _ => pick(a) }` (single-arm) | ✅ caught | *incidental* — scrutinee `a` is the moved var, not origin tracking | -| `let r = if c { pickm(a) } …` (`&mut`/exclusive) | ✅ caught | exclusive borrow tracked on a separate path | -| callee `fn f(ref x){ if _ {return &x} else {return &x} }` | ✅ caught | the **summary** side (`walk_tail`) already recurses return-tails | - -## Root cause - -`origins_of_ref_source` (`lib/borrow.ml:233`) — which `record_let` uses to give a -`let`-binder its origins — only matches `ExprUnary(OpRef|OpMutRef)`, `ExprVar`, -and `ExprApp`; everything else (incl. `ExprIf`, `ExprMatch`, `ExprBlock`) falls -to `| _ -> []`, so the binder gets **no origins** and never registers as a borrow -of the argument. The asymmetry is with `walk_tail` (same file, ~line 283), which -*does* descend `if`/`match`/`block` tails when harvesting **return** origins — -that is exactly why the callee-summary side is sound but the caller let-binding -side is not. - -## Proposed fix (bounded — not ADR-022) - -Make `origins_of_ref_source` recurse into compound expressions, taking the -**union** of the origins of all tail positions, mirroring `walk_tail`: - -- `ExprIf (_, then, else?)` → `origins_of_ref_source then @ (else? origins)` -- `ExprMatch (_, arms)` → `List.concat_map (origins_of_ref_source ∘ arm tail) arms` -- `ExprBlock b` → thread the block's own `let`s into `local_origins` (as the - outer scan does), then `origins_of_ref_source` of the block tail. - -This converges the caller-side origin computation with the already-sound -callee-side `walk_tail`. Union-of-branches is conservative (over-approximates -origins → cannot introduce a *new* false negative), matching the documented -"sound direction" invariant. - -**Note vs the #554 residual (b):** the #554 close-out recorded residual (b) -("branch-merged / copy-out claim") as *"closed only by Polonius #553."* This -finding suggests at least the **let-bound conditional** manifestation is -closeable by the syntactic extension above, **without** full Polonius -origin/region variables — worth re-checking that characterisation before -attributing it solely to ADR-022. - -## Hardening fixtures (✅ ADDED — `test/e2e/fixtures/borrow_cond_origin_*`, wired into `test_e2e.ml` `borrow_tests`) - -1. `if`-bound, both branches borrow `a`, move `a`, `*r` after → **reject**. -2. `block`-bound (plain + nested) → **reject**. -3. multi-arm `match`-bound (scrutinee ≠ moved var) → **reject**. -4. partial (one branch borrows `a`, other borrows `b`); move `a` → **reject**; - move an unrelated `c` → **accept**. -5. anti-over-rejection: each of the above with `*r` **read before** the move → - **accept** (NLL last-use). -6. nested `if { if … }` and `if { let t = …; t }`. diff --git a/issues-drafts/09-round3-borrow-affine-soundness-probe.adoc b/issues-drafts/09-round3-borrow-affine-soundness-probe.adoc new file mode 100644 index 00000000..cd765db7 --- /dev/null +++ b/issues-drafts/09-round3-borrow-affine-soundness-probe.adoc @@ -0,0 +1,233 @@ +== Soundness probe round 3 — four false-negatives in the borrow + quantity checkers + +____ +*Parked draft (2026-06-16).* No SPDX header added — owner-only per the +no-automated-licence-edits directive; owner adds the standard +issue-draft header (as on 01–08) and commits signed. Found by the +holes-first adversarial soundness probe (round 3, 4 skeptics × ~73 +programs) the owner requested after #554 / issue-08 were verified +closed. Each hole below was *independently reproduced* (not just +probe-reported): every "`hole`" program prints `+Type checking passed+`; +every paired control is correctly rejected, proving the checker is +otherwise active. Harness: `+_build/default/bin/main.exe check +` +("`Borrow error`"/"`Quantity error`" = reject; "`Type checking passed`" += accept). Each section can become its own issue. +____ + +____ +*STATUS: filed, NOT fixed.* Fixes touch `+lib/borrow.ml+` + +`+lib/quantity.ml+` on the active `+feat/solo-core-metatheory-proofs+` +branch (which carries uncommitted owner work + in-progress Polonius M3); +per stop-first they are surfaced as a plan, not applied. Proposed fixes +below are from the probe’s root-cause analysis and are _unverified +hypotheses_ until implemented + the full suite re-run. +____ + +These are the long tail of a *sound-by-testing* checker (PROOF-NEEDS P1: +#554 "`tested, not proved`"): whole _mechanisms_ — linearity through +loops/branches, deref-reborrow loans, aggregate-wrapped return borrows — +have coverage gaps that the fixture-driven suite did not exercise. All +four are *false-negatives* (accept genuinely-unsafe source), severity +*high* for an affine language whose thesis is "`the checker guarantees +no use-after-move / linear discipline`". + +''''' + +=== Hole 1 — callee return-borrow summary ignores aggregate-wrapped / projected borrows + +`+origins_of_ref_source+` (`+lib/borrow.ml+` ~L251–287) matches only +`+ExprUnary(OpRef|OpMutRef)+`, `+ExprVar+`, `+ExprApp+` — not +tuple/array/record literals or projections. So a borrow returned wrapped +in an aggregate is invisible to the per-function return-borrow summary, +and the caller’s argument is not held borrowed. + +[source,affinescript] +---- +fn wrap(ref x: Int) -> ref Int { return (&x, 0).0; } // returns &x, spelled as a projection +fn consume(own v: Int) -> Int { return v; } +fn main() -> Int { + let a: Int = 7; + let r = wrap(a); // r aliases a — NOT recorded + let _g = consume(a); // moves a while r is live + return *r; // use-after-move — ACCEPTED (unsound) +} +---- + +* Hole: `+Type checking passed+` (also compiles to wasm). Variants tuple +`+(&x,0)+`, projection `+(&x,0).0+`, `+&mut+`-tuple (dangling +*exclusive* ref), array `+[&x]+` all accept. +* Control (byte-near-identical, correctly REJECTS): +`+fn wrap(ref x: Int) -> ref Int { return &x; }+` → +`+Borrow error: cannot move 'a' while it is shared-borrowed+`. +* Class: use-after-move. Same family as #554/issue-08 (the +`+origins_of_ref_source+` ↔ `+walk_tail+` asymmetry) but a NEW spelling; +issue-08 (`+let r = if {pick(a)} …+`) is verified closed. +* Proposed fix (conservative, sound-direction): make +`+origins_of_ref_source+` + `+walk_tail+`/`+walk_expr+` descend +`+ExprTuple+`/`+ExprArray+`/`+ExprRecord+` (union of element origins) +and `+ExprTupleIndex+`/`+ExprField+`/`+ExprIndex+` (descend base). +Over-approximation can only keep MORE arg borrows live — no new false +negative. + +=== Hole 2 — `+@linear+` binding consumed once-per-iteration in a loop is accepted + +`+lib/quantity.ml+` `+StmtWhile+`/`+StmtFor+` (~L628–648) model loop +repetition with `+env_join+` (per-variable MAX over two passes), not +`+add_usage+`/ω-scaling. Each pass yields the var at `+UOne+`; +`+join_usage UOne UOne = UOne+`, so a once-per-iter use is never +promoted to `+UMany+`. The in-code comment (~L635, "`any variable used +in the loop body is used >= 2 times`") contradicts the implementation. + +[source,affinescript] +---- +fn consume(@linear r: Int) -> Int = r + 1; +fn loop_param(@linear x: Int, n: Int) -> Int { + let mut i = 0; + while i < n { consume(x); i = i + 1; } // consumes use-once x up to n times + 0 +} +---- + +* Hole: `+Type checking passed+` (+ compiles). For a real linear +resource (handle, owned buffer, effect token) this is N-fold consume = +double-free / use-after- consume. `+for … in [..]+` reproduces. +* Controls (REJECT): straight-line `+consume(x); consume(x)+` → +`+Quantity error: … used multiple times+`; intra-iteration `+x + x+` +inside the loop → same. +* Class: linear-violation (quantity checker — independent of borrow.ml). +* Proposed fix: ω-scale (or `+add_usage+` with itself) the per-iteration +usage delta in `+StmtWhile+`/`+StmtFor+` instead of `+env_join+`, +matching the L635 intent. + +=== Hole 3 — `+@linear+` binding dropped on one branch is accepted + +`+lib/quantity.ml+` `+join_usage+` (~L91–95) returns MAX of branch +usages. MAX is the correct join for *affine* (at-most-once), but +AffineScript treats `+@linear+` as *exactly-once* (it raises "`must be +used exactly once, but was never used`" for zero uses). For +exactly-once, branch-merge must require consumption on *all* paths (a +meet that flags any branch leaving the var at `+UZero+`). + +[source,affinescript] +---- +fn consume(@linear r: Int) -> Int = r + 1; +fn drop_on_else(@linear x: Int, c: Bool) -> Int { + if c { consume(x) } else { 0 } // x never consumed when c = false +} +---- + +* Hole: `+Type checking passed+` (+ compiles). When `+c+` is false the +must-use linear `+x+` is silently leaked. Match-arm form (consume in one +arm, drop in wildcard) reproduces. +* Control (REJECTS): unconditional drop +`+fn f(@linear x: Int) -> Int { 0 }+` → +`+Quantity error: … never used+`. +* Class: linear-violation. Closely related to Hole 2 (same file); could +be one issue "`QTT exactly-once is unsound across loops and branches`". +* Proposed fix: for `+QOne+` bindings, branch-merge must flag any branch +leaving the var at `+UZero+` (must-use-on-all-paths), not take MAX. +Care: must not over-reject legitimate affine (`+QOmega+`) bindings — +keep MAX for those. + +=== Hole 4 — deref-reborrow `+&mut *r+` / `+&*r+` records no loan (aliased `+&mut+` + UAM laundering) + +`+expr_to_place+` (`+lib/borrow.ml+` ~L832–851) has no +`+ExprUnary(OpDeref, _)+` arm (falls to `+_ -> None+`) even though the +`+place+` type HAS `+PlaceDeref+` (~L21). So at `+check_expr+` +`+ExprUnary OpRef/OpMutRef+` (~L1583), `+expr_to_place(*r) = None+` hits +the None-branch (~L1588) that only checks the inner operand — +`+record_borrow+` is never called, so the reborrow loan is invisible to +`+find_conflicting_borrow+` / `+find_aliasing_exclusive+`. + +[source,affinescript] +---- +module P; +fn bad() -> Int { + let mut x = 5; + let r = &mut x; + let r2 = &mut *r; // SECOND live exclusive alias of x — no loan recorded + *r2 = 99; + *r = 100; // two live mutable paths to one cell + *r2 +} +---- + +* Hole: `+Type checking passed+` (+ compiles). A use-after-move variant +(`+let r = &mut x; let r2 = &mut *r; let gone = consume(x); *r2 + gone+`) +also accepts: `+&mut *r+` records no loan → `+r+` NLL-expires → +`+consume(x)+` moves `+x+` unblocked → `+*r2+` reads moved storage. +`+&*r+` (shared) launders identically. +* Control (REJECTS): direct `+let a = &mut x; let b = &mut x+` → +`+Borrow error: conflicting borrows on 'x'+`. +* Class: alias-exclusivity (no callee, no branch — distinct from +#554/issue-08). `+&mut *r+` (reborrow) is an everyday pattern, so this +is a basic gap. +* Proposed fix (single point): add to `+expr_to_place+` +`+| ExprUnary (OpDeref, inner) -> Option.map (fun p -> PlaceDeref p) (expr_to_place symbols inner)+` +and route deref-LHS `+*p = e+` through the place path in `+StmtAssign+`. + +''''' + +=== Suggested triage / priority (probe recommendation) + +[arabic] +. *Hole 4* — single-point `+expr_to_place+` addition; closes +aliased-`+&mut+` + UAM laundering. Low-risk, high-value. +. *Hole 1* — `+origins_of_ref_source+`/`+walk_tail+` aggregate descent; +conservative over-approximation. +. *Holes 2 + 3* — `+lib/quantity.ml+` loop ω-scaling + branch +all-paths-meet for `+QOne+`. Subtler: must not over-reject +`+QOmega+`/affine bindings — verify the full suite (+ add NLL/affine +anti-over-rejection fixtures) after. + +Each fix should ship with hardening fixtures wired into +`+test/test_main.ml+` `+borrow_tests+` / the quantity suite (reject the +hole; accept the safe control), mirroring the #554 / issue-08 closeouts. +Re-run the round-3 probe class after. + +=== VERIFICATION 2026-06-17 — the "`proposed fix`" recipes above are HYPOTHESES; none is a one-liner + +A de-risk pass attempted/analysed the recipes. _Correction: the triage +above was over-optimistic. None of the four holes is a quick patch — +they are architectural gaps. Treat every "`Proposed fix`" line above as +an unverified hypothesis._ + +* _Hole 4 — EMPIRICALLY DISPROVEN as a single-point fix (built + ran it, +then reverted)._ Adding the `+ExprUnary (OpDeref, inner) -> PlaceDeref+` +arm to `+expr_to_place+`: (a) OVER-REJECTS legitimate reborrows — +`+is_mutable+` is BINDING-based (`+let mut+`), not reference-based, so +`+&mut *r+` / `+*r = e+` through a non-`+let mut+` ref binder fails with +"`cannot borrow `+*r+` as mutable`", rejecting the sound +`+let r = &mut x; let r2 = &mut *r; *r2 = 99; *r2+`; (b) does NOT +achieve soundness even so — `+places_overlap+` is root-var-based and +`+root_var (PlaceDeref (PlaceVar r)) = r+`, while the original loan from +`+let r = &mut x+` is rooted at `+x+`, so the reborrow’s loan (root +`+r+`) never overlaps the original (root `+x+`) and the aliasing is +undetected; (c) the shared `+&*r+` variant stays accepted. _Sound fix +requires resolving `+*r+` to its referent `+x+` — the reference→referent +origin/loan model = Polonius (#553)._ H4 belongs with #553, not a quick +patch. +* _Hole 1 — same class as H4 (borrow-checker origins); +Polonius-adjacent._ The `+origins_of_ref_source+` aggregate descent is a +pure over-approximation that may help, but the aggregate-wrapped +return-borrow soundness is part of the same origin-tracking story as +#554/H4. Treat as #553-adjacent; verify it does not over-reject (more +origins ⇒ more borrows held live ⇒ can reject legit moves). +* _Holes 2 + 3 — code-analysis shows the one-line recipes are +insufficient (not yet empirically run)._ `+join_usage+` +(quantity.ml:91-95) is MAX and QUANTITY-AGNOSTIC; "`change the branch +join to MEET`" would break `+QOmega+`/affine (which legitimately allow +0..n and use of zero on a path). H3 needs a quantity-aware branch merge +or a usage-lattice extension (e.g. a "`used-on-some-but-not-all-paths`" +element that errors for `+QOne+` only). H2’s naive ω-scaling of the loop +body over-rejects loop-LOCAL linears (a fresh linear born and consumed +each iteration is sound); the scale must apply only to vars that escape +the loop scope. Both are contained to `+quantity.ml+` but are real +lattice/scope work, not one-liners. + +NET (de-risk outcome): the probe correctly FOUND four real +false-negatives, but its FIXES are hypotheses. H1/H4 ⇒ the Polonius +origin model (#553); H2/H3 ⇒ quantity-checker enhancements (scope-aware +loop scaling; all-paths-meet for linear). Closing these is design work +needing owner review, not a sweep. The reverted H4 experiment confirms: +verify every proposed fix by running it. diff --git a/issues-drafts/09-round3-borrow-affine-soundness-probe.md b/issues-drafts/09-round3-borrow-affine-soundness-probe.md deleted file mode 100644 index 312e1255..00000000 --- a/issues-drafts/09-round3-borrow-affine-soundness-probe.md +++ /dev/null @@ -1,204 +0,0 @@ -# Soundness probe round 3 — four false-negatives in the borrow + quantity checkers - -> **Parked draft (2026-06-16).** No SPDX header added — owner-only per the -> no-automated-licence-edits directive; owner adds the standard issue-draft -> header (as on 01–08) and commits signed. Found by the holes-first adversarial -> soundness probe (round 3, 4 skeptics × ~73 programs) the owner requested after -> #554 / issue-08 were verified closed. Each hole below was **independently -> reproduced** (not just probe-reported): every "hole" program prints -> `Type checking passed`; every paired control is correctly rejected, proving the -> checker is otherwise active. Harness: -> `_build/default/bin/main.exe check ` ("Borrow error"/"Quantity error" = -> reject; "Type checking passed" = accept). Each section can become its own issue. - -> **STATUS: filed, NOT fixed.** Fixes touch `lib/borrow.ml` + `lib/quantity.ml` -> on the active `feat/solo-core-metatheory-proofs` branch (which carries -> uncommitted owner work + in-progress Polonius M3); per stop-first they are -> surfaced as a plan, not applied. Proposed fixes below are from the probe's -> root-cause analysis and are *unverified hypotheses* until implemented + the full -> suite re-run. - -These are the long tail of a **sound-by-testing** checker (PROOF-NEEDS P1: #554 -"tested, not proved"): whole *mechanisms* — linearity through loops/branches, -deref-reborrow loans, aggregate-wrapped return borrows — have coverage gaps that -the fixture-driven suite did not exercise. All four are **false-negatives** -(accept genuinely-unsafe source), severity **high** for an affine language whose -thesis is "the checker guarantees no use-after-move / linear discipline". - ---- - -## Hole 1 — callee return-borrow summary ignores aggregate-wrapped / projected borrows - -`origins_of_ref_source` (`lib/borrow.ml` ~L251–287) matches only -`ExprUnary(OpRef|OpMutRef)`, `ExprVar`, `ExprApp` — not tuple/array/record -literals or projections. So a borrow returned wrapped in an aggregate is invisible -to the per-function return-borrow summary, and the caller's argument is not held -borrowed. - -```affinescript -fn wrap(ref x: Int) -> ref Int { return (&x, 0).0; } // returns &x, spelled as a projection -fn consume(own v: Int) -> Int { return v; } -fn main() -> Int { - let a: Int = 7; - let r = wrap(a); // r aliases a — NOT recorded - let _g = consume(a); // moves a while r is live - return *r; // use-after-move — ACCEPTED (unsound) -} -``` - -* Hole: `Type checking passed` (also compiles to wasm). Variants tuple `(&x,0)`, - projection `(&x,0).0`, `&mut`-tuple (dangling **exclusive** ref), array `[&x]` - all accept. -* Control (byte-near-identical, correctly REJECTS): - `fn wrap(ref x: Int) -> ref Int { return &x; }` → - `Borrow error: cannot move 'a' while it is shared-borrowed`. -* Class: use-after-move. Same family as #554/issue-08 (the - `origins_of_ref_source` ↔ `walk_tail` asymmetry) but a NEW spelling; issue-08 - (`let r = if {pick(a)} …`) is verified closed. -* Proposed fix (conservative, sound-direction): make `origins_of_ref_source` + - `walk_tail`/`walk_expr` descend `ExprTuple`/`ExprArray`/`ExprRecord` (union of - element origins) and `ExprTupleIndex`/`ExprField`/`ExprIndex` (descend base). - Over-approximation can only keep MORE arg borrows live — no new false negative. - -## Hole 2 — `@linear` binding consumed once-per-iteration in a loop is accepted - -`lib/quantity.ml` `StmtWhile`/`StmtFor` (~L628–648) model loop repetition with -`env_join` (per-variable MAX over two passes), not `add_usage`/ω-scaling. Each -pass yields the var at `UOne`; `join_usage UOne UOne = UOne`, so a once-per-iter -use is never promoted to `UMany`. The in-code comment (~L635, "any variable used -in the loop body is used >= 2 times") contradicts the implementation. - -```affinescript -fn consume(@linear r: Int) -> Int = r + 1; -fn loop_param(@linear x: Int, n: Int) -> Int { - let mut i = 0; - while i < n { consume(x); i = i + 1; } // consumes use-once x up to n times - 0 -} -``` - -* Hole: `Type checking passed` (+ compiles). For a real linear resource (handle, - owned buffer, effect token) this is N-fold consume = double-free / use-after- - consume. `for … in [..]` reproduces. -* Controls (REJECT): straight-line `consume(x); consume(x)` → - `Quantity error: … used multiple times`; intra-iteration `x + x` inside the - loop → same. -* Class: linear-violation (quantity checker — independent of borrow.ml). -* Proposed fix: ω-scale (or `add_usage` with itself) the per-iteration usage - delta in `StmtWhile`/`StmtFor` instead of `env_join`, matching the L635 intent. - -## Hole 3 — `@linear` binding dropped on one branch is accepted - -`lib/quantity.ml` `join_usage` (~L91–95) returns MAX of branch usages. MAX is the -correct join for **affine** (at-most-once), but AffineScript treats `@linear` as -**exactly-once** (it raises "must be used exactly once, but was never used" for -zero uses). For exactly-once, branch-merge must require consumption on **all** -paths (a meet that flags any branch leaving the var at `UZero`). - -```affinescript -fn consume(@linear r: Int) -> Int = r + 1; -fn drop_on_else(@linear x: Int, c: Bool) -> Int { - if c { consume(x) } else { 0 } // x never consumed when c = false -} -``` - -* Hole: `Type checking passed` (+ compiles). When `c` is false the must-use - linear `x` is silently leaked. Match-arm form (consume in one arm, drop in - wildcard) reproduces. -* Control (REJECTS): unconditional drop `fn f(@linear x: Int) -> Int { 0 }` → - `Quantity error: … never used`. -* Class: linear-violation. Closely related to Hole 2 (same file); could be one - issue "QTT exactly-once is unsound across loops and branches". -* Proposed fix: for `QOne` bindings, branch-merge must flag any branch leaving - the var at `UZero` (must-use-on-all-paths), not take MAX. Care: must not - over-reject legitimate affine (`QOmega`) bindings — keep MAX for those. - -## Hole 4 — deref-reborrow `&mut *r` / `&*r` records no loan (aliased `&mut` + UAM laundering) - -`expr_to_place` (`lib/borrow.ml` ~L832–851) has no `ExprUnary(OpDeref, _)` arm -(falls to `_ -> None`) even though the `place` type HAS `PlaceDeref` (~L21). So at -`check_expr` `ExprUnary OpRef/OpMutRef` (~L1583), `expr_to_place(*r) = None` hits -the None-branch (~L1588) that only checks the inner operand — `record_borrow` is -never called, so the reborrow loan is invisible to `find_conflicting_borrow` / -`find_aliasing_exclusive`. - -```affinescript -module P; -fn bad() -> Int { - let mut x = 5; - let r = &mut x; - let r2 = &mut *r; // SECOND live exclusive alias of x — no loan recorded - *r2 = 99; - *r = 100; // two live mutable paths to one cell - *r2 -} -``` - -* Hole: `Type checking passed` (+ compiles). A use-after-move variant - (`let r = &mut x; let r2 = &mut *r; let gone = consume(x); *r2 + gone`) also - accepts: `&mut *r` records no loan → `r` NLL-expires → `consume(x)` moves `x` - unblocked → `*r2` reads moved storage. `&*r` (shared) launders identically. -* Control (REJECTS): direct `let a = &mut x; let b = &mut x` → - `Borrow error: conflicting borrows on 'x'`. -* Class: alias-exclusivity (no callee, no branch — distinct from #554/issue-08). - `&mut *r` (reborrow) is an everyday pattern, so this is a basic gap. -* Proposed fix (single point): add to `expr_to_place` - `| ExprUnary (OpDeref, inner) -> Option.map (fun p -> PlaceDeref p) (expr_to_place symbols inner)` - and route deref-LHS `*p = e` through the place path in `StmtAssign`. - ---- - -## Suggested triage / priority (probe recommendation) - -1. **Hole 4** — single-point `expr_to_place` addition; closes aliased-`&mut` + - UAM laundering. Low-risk, high-value. -2. **Hole 1** — `origins_of_ref_source`/`walk_tail` aggregate descent; - conservative over-approximation. -3. **Holes 2 + 3** — `lib/quantity.ml` loop ω-scaling + branch all-paths-meet for - `QOne`. Subtler: must not over-reject `QOmega`/affine bindings — verify the - full suite (+ add NLL/affine anti-over-rejection fixtures) after. - -Each fix should ship with hardening fixtures wired into `test/test_main.ml` -`borrow_tests` / the quantity suite (reject the hole; accept the safe control), -mirroring the #554 / issue-08 closeouts. Re-run the round-3 probe class after. - -## VERIFICATION 2026-06-17 — the "proposed fix" recipes above are HYPOTHESES; none is a one-liner - -A de-risk pass attempted/analysed the recipes. *Correction: the triage above was -over-optimistic. None of the four holes is a quick patch — they are architectural -gaps. Treat every "Proposed fix" line above as an unverified hypothesis.* - -* *Hole 4 — EMPIRICALLY DISPROVEN as a single-point fix (built + ran it, then - reverted).* Adding the `ExprUnary (OpDeref, inner) -> PlaceDeref` arm to - `expr_to_place`: (a) OVER-REJECTS legitimate reborrows — `is_mutable` is - BINDING-based (`let mut`), not reference-based, so `&mut *r` / `*r = e` through a - non-`let mut` ref binder fails with "cannot borrow `*r` as mutable", rejecting - the sound `let r = &mut x; let r2 = &mut *r; *r2 = 99; *r2`; (b) does NOT achieve - soundness even so — `places_overlap` is root-var-based and - `root_var (PlaceDeref (PlaceVar r)) = r`, while the original loan from - `let r = &mut x` is rooted at `x`, so the reborrow's loan (root `r`) never - overlaps the original (root `x`) and the aliasing is undetected; (c) the shared - `&*r` variant stays accepted. *Sound fix requires resolving `*r` to its referent - `x` — the reference→referent origin/loan model = Polonius (#553).* H4 belongs - with #553, not a quick patch. -* *Hole 1 — same class as H4 (borrow-checker origins); Polonius-adjacent.* The - `origins_of_ref_source` aggregate descent is a pure over-approximation that may - help, but the aggregate-wrapped return-borrow soundness is part of the same - origin-tracking story as #554/H4. Treat as #553-adjacent; verify it does not - over-reject (more origins ⇒ more borrows held live ⇒ can reject legit moves). -* *Holes 2 + 3 — code-analysis shows the one-line recipes are insufficient (not - yet empirically run).* `join_usage` (quantity.ml:91-95) is MAX and - QUANTITY-AGNOSTIC; "change the branch join to MEET" would break `QOmega`/affine - (which legitimately allow 0..n and use of zero on a path). H3 needs a - quantity-aware branch merge or a usage-lattice extension (e.g. a - "used-on-some-but-not-all-paths" element that errors for `QOne` only). H2's naive - ω-scaling of the loop body over-rejects loop-LOCAL linears (a fresh linear born - and consumed each iteration is sound); the scale must apply only to vars that - escape the loop scope. Both are contained to `quantity.ml` but are real - lattice/scope work, not one-liners. - -NET (de-risk outcome): the probe correctly FOUND four real false-negatives, but -its FIXES are hypotheses. H1/H4 ⇒ the Polonius origin model (#553); H2/H3 ⇒ -quantity-checker enhancements (scope-aware loop scaling; all-paths-meet for -linear). Closing these is design work needing owner review, not a sweep. The -reverted H4 experiment confirms: verify every proposed fix by running it. diff --git a/site/index.adoc b/site/index.adoc new file mode 100644 index 00000000..456b3617 --- /dev/null +++ b/site/index.adoc @@ -0,0 +1,27 @@ +== AffineScript + +The public web home for this project is +https://affinescript.dev[affinescript.dev]. + +The language where protocol correctness is free. + +AffineScript is the only language that combines affine types, +quantitative type theory, row polymorphism, and algebraic effects in a +single practical systems language. This intersection makes it possible +to write code where the compiler proves your protocol states, resource +lifecycles, and effect boundaries are correct – with syntax that doesn’t +feel like a theorem prover. + +=== Project Links + +* Website: https://affinescript.dev[affinescript.dev] +* Source: https://github.com/hyperpolymath/affinescript +* README: +https://github.com/hyperpolymath/affinescript/blob/main/README.adoc[project +overview] +* Docs: +https://github.com/hyperpolymath/affinescript/tree/main/docs[documentation +directory] + +This page is a lightweight landing point for the repository and will +grow with the project. diff --git a/site/index.md b/site/index.md deleted file mode 100644 index c69b15a3..00000000 --- a/site/index.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: AffineScript -date: 2026-03-31 ---- - -# AffineScript - -The public web home for this project is [affinescript.dev](https://affinescript.dev). - -The language where protocol correctness is free. - -AffineScript is the only language that combines affine types, quantitative type theory, row polymorphism, and algebraic effects in a single practical systems language. This intersection makes it possible to write code where the compiler proves your protocol states, resource lifecycles, and effect boundaries are correct -- with syntax that doesn't feel like a theorem prover. - -## Project Links - -- Website: [affinescript.dev](https://affinescript.dev) -- Source: [https://github.com/hyperpolymath/affinescript](https://github.com/hyperpolymath/affinescript) -- README: [project overview](https://github.com/hyperpolymath/affinescript/blob/main/README.adoc) -- Docs: [documentation directory](https://github.com/hyperpolymath/affinescript/tree/main/docs) - -This page is a lightweight landing point for the repository and will grow with the project. diff --git a/stdlib/README.adoc b/stdlib/README.adoc new file mode 100644 index 00000000..65ff67da --- /dev/null +++ b/stdlib/README.adoc @@ -0,0 +1,175 @@ +== AffineScript Standard Library + +The AffineScript standard library provides essential utilities and data +structures. + +=== Modules + +==== Core + +Basic utilities and operations. + +*Functions:* - `+id[T](x: T) -> T+` - Identity function - +`+always[A, B](x: A, _y: B) -> A+` - Constant function (returns x, +ignores y; named `+always+` since `+const+` is a reserved keyword) - +`+compose[A, B, C](f, g)+` - Function composition - `+flip[A, B, C](f)+` +- Flip function arguments - `+min(a, b)+`, `+max(a, b)+`, +`+clamp(x, low, high)+` - Numeric operations - `+abs(x)+`, `+sign(x)+` - +Absolute value and sign - `+not(x)+`, `+and(a, b)+`, `+or(a, b)+`, +`+xor(a, b)+` - Boolean operations + +*Example:* + +[source,affinescript] +---- +use Core::{min, max, abs}; + +let smallest = min(10, 20); // 10 +let largest = max(10, 20); // 20 +let absolute = abs(-42); // 42 +---- + +==== Result + +Error handling utilities for `+Result[T, E]+` type. + +*Functions:* - `+is_ok(r)+`, `+is_err(r)+` - Check result status - +`+unwrap(r)+`, `+unwrap_or(r, default)+` - Extract value - +`+unwrap_err(r)+` - Extract error - `+map(r, f)+`, `+map_err(r, f)+` - +Transform value or error - `+and_then(r, f)+` - Chain operations +(flatMap) - `+ok(r)+`, `+err(r)+` - Convert to Option + +*Example:* + +[source,affinescript] +---- +use Result::{map, unwrap_or}; + +fn divide(a: Int, b: Int) -> Result[Int, String] { + return if b == 0 { + Err("division by zero") + } else { + Ok(a / b) + }; +} + +let result = divide(10, 2); +let doubled = map(result, |x| { return x * 2; }); +let value = unwrap_or(doubled, 0); // 10 +---- + +==== Option + +Optional value utilities for `+Option[T]+` type. + +*Functions:* - `+is_some(opt)+`, `+is_none(opt)+` - Check if value +exists - `+unwrap(opt)+`, `+unwrap_or(opt, default)+` - Extract value - +`+unwrap_or_else(opt, f)+` - Extract or compute default - +`+map(opt, f)+`, `+map_or(opt, default, f)+` - Transform value - +`+and_then(opt, f)+` - Chain operations (flatMap) - `+or(opt, other)+`, +`+or_else(opt, f)+` - Alternative values - `+filter(opt, pred)+` - +Filter by predicate - `+ok_or(opt, err)+`, `+ok_or_else(opt, f)+` - +Convert to Result + +*Example:* + +[source,affinescript] +---- +use Option::{map, unwrap_or}; + +fn find_positive(x: Int) -> Option[Int] { + return if x > 0 { Some(x) } else { None }; +} + +let opt = find_positive(42); +let doubled = map(opt, |x| { return x * 2; }); +let value = unwrap_or(doubled, 0); // 84 +---- + +==== Math + +Mathematical functions and constants. + +*Constants:* - `+PI+` = 3.14159… - `+E+` = 2.71828… - `+TAU+` = 6.28318… +(2π) + +*Integer Functions:* - `+abs(x)+`, `+min(a, b)+`, `+max(a, b)+`, +`+clamp(x, low, high)+` - `+pow(base, exp)+` - Integer exponentiation - +`+gcd(a, b)+`, `+lcm(a, b)+` - Greatest common divisor and least common +multiple - `+factorial(n)+` - Factorial - `+fib(n)+` - Fibonacci number +- `+is_even(n)+`, `+is_odd(n)+` - Parity checks + +*Float Functions:* - `+abs_f(x)+`, `+min_f(a, b)+`, `+max_f(a, b)+`, +`+clamp_f(x, low, high)+` + +*Example:* + +[source,affinescript] +---- +use Math::{pow, gcd, factorial}; + +let squared = pow(5, 2); // 25 +let divisor = gcd(48, 18); // 6 +let perm = factorial(5); // 120 +---- + +=== Usage + +Import modules using the `+use+` statement: + +[source,affinescript] +---- +// Import entire module +use Core; +let result = Core.abs(-10); + +// Import specific functions +use Core::{min, max}; +let smaller = min(5, 10); + +// Import with alias +use Math as M; +let circle_area = M.PI * radius * radius; +---- + +=== Built-in Types + +The standard library uses these built-in types: + +* `+Result[T, E]+` - Success (Ok) or failure (Err) +* `+Option[T]+` - Present value (Some) or absent (None) +* `+Int+` - Integer numbers +* `+Float+` - Floating-point numbers +* `+Bool+` - Boolean values (true/false) +* `+String+` - Text strings + +=== Status + +*Implemented:* - ✅ Core utilities - ✅ Result error handling - ✅ +Option optional values - ✅ Math basic functions + +*TODO:* - String manipulation functions - Array/List operations - I/O +functions (requires FFI) - Transcendental math functions (sin, cos, +sqrt, etc.) - Date/Time utilities - File system operations + +=== Contributing + +To add new stdlib functions: + +[arabic] +. Add function to appropriate module file +. Document with examples +. Update this README +. Add tests in `+tests/stdlib/+` + +=== Testing + +Test standard library functions: + +[source,bash] +---- +affinescript eval tests/stdlib/test_core.affine +affinescript eval tests/stdlib/test_result.affine +affinescript eval tests/stdlib/test_option.affine +affinescript eval tests/stdlib/test_math.affine +---- diff --git a/stdlib/README.md b/stdlib/README.md deleted file mode 100644 index 92cf3a2f..00000000 --- a/stdlib/README.md +++ /dev/null @@ -1,173 +0,0 @@ -# AffineScript Standard Library - -The AffineScript standard library provides essential utilities and data structures. - -## Modules - -### Core -Basic utilities and operations. - -**Functions:** -- `id[T](x: T) -> T` - Identity function -- `always[A, B](x: A, _y: B) -> A` - Constant function (returns x, ignores y; named `always` since `const` is a reserved keyword) -- `compose[A, B, C](f, g)` - Function composition -- `flip[A, B, C](f)` - Flip function arguments -- `min(a, b)`, `max(a, b)`, `clamp(x, low, high)` - Numeric operations -- `abs(x)`, `sign(x)` - Absolute value and sign -- `not(x)`, `and(a, b)`, `or(a, b)`, `xor(a, b)` - Boolean operations - -**Example:** -```affinescript -use Core::{min, max, abs}; - -let smallest = min(10, 20); // 10 -let largest = max(10, 20); // 20 -let absolute = abs(-42); // 42 -``` - -### Result -Error handling utilities for `Result[T, E]` type. - -**Functions:** -- `is_ok(r)`, `is_err(r)` - Check result status -- `unwrap(r)`, `unwrap_or(r, default)` - Extract value -- `unwrap_err(r)` - Extract error -- `map(r, f)`, `map_err(r, f)` - Transform value or error -- `and_then(r, f)` - Chain operations (flatMap) -- `ok(r)`, `err(r)` - Convert to Option - -**Example:** -```affinescript -use Result::{map, unwrap_or}; - -fn divide(a: Int, b: Int) -> Result[Int, String] { - return if b == 0 { - Err("division by zero") - } else { - Ok(a / b) - }; -} - -let result = divide(10, 2); -let doubled = map(result, |x| { return x * 2; }); -let value = unwrap_or(doubled, 0); // 10 -``` - -### Option -Optional value utilities for `Option[T]` type. - -**Functions:** -- `is_some(opt)`, `is_none(opt)` - Check if value exists -- `unwrap(opt)`, `unwrap_or(opt, default)` - Extract value -- `unwrap_or_else(opt, f)` - Extract or compute default -- `map(opt, f)`, `map_or(opt, default, f)` - Transform value -- `and_then(opt, f)` - Chain operations (flatMap) -- `or(opt, other)`, `or_else(opt, f)` - Alternative values -- `filter(opt, pred)` - Filter by predicate -- `ok_or(opt, err)`, `ok_or_else(opt, f)` - Convert to Result - -**Example:** -```affinescript -use Option::{map, unwrap_or}; - -fn find_positive(x: Int) -> Option[Int] { - return if x > 0 { Some(x) } else { None }; -} - -let opt = find_positive(42); -let doubled = map(opt, |x| { return x * 2; }); -let value = unwrap_or(doubled, 0); // 84 -``` - -### Math -Mathematical functions and constants. - -**Constants:** -- `PI` = 3.14159... -- `E` = 2.71828... -- `TAU` = 6.28318... (2π) - -**Integer Functions:** -- `abs(x)`, `min(a, b)`, `max(a, b)`, `clamp(x, low, high)` -- `pow(base, exp)` - Integer exponentiation -- `gcd(a, b)`, `lcm(a, b)` - Greatest common divisor and least common multiple -- `factorial(n)` - Factorial -- `fib(n)` - Fibonacci number -- `is_even(n)`, `is_odd(n)` - Parity checks - -**Float Functions:** -- `abs_f(x)`, `min_f(a, b)`, `max_f(a, b)`, `clamp_f(x, low, high)` - -**Example:** -```affinescript -use Math::{pow, gcd, factorial}; - -let squared = pow(5, 2); // 25 -let divisor = gcd(48, 18); // 6 -let perm = factorial(5); // 120 -``` - -## Usage - -Import modules using the `use` statement: - -```affinescript -// Import entire module -use Core; -let result = Core.abs(-10); - -// Import specific functions -use Core::{min, max}; -let smaller = min(5, 10); - -// Import with alias -use Math as M; -let circle_area = M.PI * radius * radius; -``` - -## Built-in Types - -The standard library uses these built-in types: - -- `Result[T, E]` - Success (Ok) or failure (Err) -- `Option[T]` - Present value (Some) or absent (None) -- `Int` - Integer numbers -- `Float` - Floating-point numbers -- `Bool` - Boolean values (true/false) -- `String` - Text strings - -## Status - -**Implemented:** -- ✅ Core utilities -- ✅ Result error handling -- ✅ Option optional values -- ✅ Math basic functions - -**TODO:** -- String manipulation functions -- Array/List operations -- I/O functions (requires FFI) -- Transcendental math functions (sin, cos, sqrt, etc.) -- Date/Time utilities -- File system operations - -## Contributing - -To add new stdlib functions: - -1. Add function to appropriate module file -2. Document with examples -3. Update this README -4. Add tests in `tests/stdlib/` - -## Testing - -Test standard library functions: - -```bash -affinescript eval tests/stdlib/test_core.affine -affinescript eval tests/stdlib/test_result.affine -affinescript eval tests/stdlib/test_option.affine -affinescript eval tests/stdlib/test_math.affine -``` diff --git a/tests/codegen/README.adoc b/tests/codegen/README.adoc new file mode 100644 index 00000000..c92fa742 --- /dev/null +++ b/tests/codegen/README.adoc @@ -0,0 +1,20 @@ +== Codegen WASM tests + +This directory contains AffineScript codegen tests and their compiled +WASM artifacts. + +=== Run locally + +From the repo root: + +.... +./tools/run_codegen_wasm_tests.sh +.... + +The script: - compiles every `+tests/codegen/*.affine+` file to +`+tests/codegen/*.wasm+` - runs any `+tests/codegen/*.mjs+` harnesses + +=== Notes + +* WASM artifacts under `+tests/codegen/+` are generated by CI and should +not be committed. diff --git a/tests/codegen/README.md b/tests/codegen/README.md deleted file mode 100644 index 60dd172f..00000000 --- a/tests/codegen/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Codegen WASM tests - -This directory contains AffineScript codegen tests and their compiled WASM artifacts. - -## Run locally - -From the repo root: - -``` -./tools/run_codegen_wasm_tests.sh -``` - -The script: -- compiles every `tests/codegen/*.affine` file to `tests/codegen/*.wasm` -- runs any `tests/codegen/*.mjs` harnesses - -## Notes - -- WASM artifacts under `tests/codegen/` are generated by CI and should not be committed. diff --git a/tests/conformance/README.adoc b/tests/conformance/README.adoc new file mode 100644 index 00000000..f0e4c2b5 --- /dev/null +++ b/tests/conformance/README.adoc @@ -0,0 +1,174 @@ +== aLib Conformance Tests + +This directory contains conformance tests for the *aggregate-library +(aLib)* specification. + +=== Purpose + +These tests verify that AffineScript’s standard library operations +conform to the language-agnostic specifications defined in the +https://github.com/hyperpolymath/aggregate-library[aggregate-library] +project. + +=== What is aLib? + +aggregate-library (aLib) is a *methodology repository* that provides: - +Language-agnostic operation specifications - Behavioral semantics and +properties - Executable test vectors in YAML format + +aLib is NOT a code library - it’s a way to specify minimal overlap +between diverse programming ecosystems. + +=== Test Structure + +.... +tests/conformance/ +├── arithmetic/ # Arithmetic operation tests +│ └── add.affine +├── collection/ # Collection operation tests +│ ├── map.affine +│ ├── filter.affine +│ ├── fold.affine +│ └── contains.affine +├── run_all.affine # Master test runner +└── README.md # This file +.... + +=== Running Tests + +==== Run all conformance tests: + +[source,bash] +---- +affinescript tests/conformance/run_all.affine +---- + +==== Run specific category: + +[source,bash] +---- +affinescript tests/conformance/collection/map.affine +---- + +==== Expected Output: + +.... +================================================================================ +aLib Conformance Report +================================================================================ + +✓ PASS collection/map: 5/5 tests +✓ PASS collection/filter: 5/5 tests +✓ PASS collection/fold: 6/6 tests +✓ PASS collection/contains: 6/6 tests +✓ PASS arithmetic/add: 5/5 tests + +================================================================================ +Summary +================================================================================ +Total operations tested: 5 +Conformant operations: 5/5 +Total test cases: 27 +Tests passed: 27 +Tests failed: 0 +Conformance rate: 100% + +✓ Excellent aLib conformance (≥95%) +================================================================================ +.... + +=== Test Vector Sources + +Each test file includes a reference to its source aLib spec: + +[source,affinescript] +---- +// Source: aggregate-library/specs/collection/map.md +---- + +=== Conformance Criteria + +* *100% conformance*: All test vectors pass +* *≥95% conformance*: Excellent (production-ready) +* *≥80% conformance*: Good (acceptable with documented gaps) +* *<80% conformance*: Needs improvement + +=== AffineScript-Specific Semantics + +AffineScript’s conformance tests respect affine type constraints: + +==== map + +* Source collection *moved* (not copied) +* Elements consumed *exactly once* +* Result owned by caller + +==== filter + +* Predicate *borrows* (`+&T -> Bool+`) +* Source collection *moved* +* Filtered elements automatically *dropped* + +==== fold + +* Accumulator ownership tracked +* Source collection *moved* +* Left-associative evaluation + +==== contains + +* Requires `+Eq+` trait on element type +* Short-circuit on first match +* Source collection *borrowed* (not moved) + +=== Adding New Conformance Tests + +[arabic] +. Read the aLib spec from `+aggregate-library/specs/+` +. Extract test vectors from YAML section +. Create `+tests/conformance//.affine+` +. Translate aLib function expressions to AffineScript syntax +. Add test to `+run_all.affine+` + +Example: + +[source,affinescript] +---- +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Source: aggregate-library/specs/collection/map.md + +fn test_map_double() -> TestResult { + let input = [1, 2, 3]; + let result = map(input, fn(x) => x * 2); + assert_eq(result, [2, 4, 6], "Double each number"); + Pass +} +---- + +=== Integration Strategy + +See link:../../docs/ALIB-INTEGRATION.md[docs/ALIB-INTEGRATION.md] for +the complete aLib integration roadmap. + +=== Status + +*Phase 1: Conformance* ✅ *COMPLETE* - [x] Collection conformance tests +(4/4 specs) ✓ - [x] Arithmetic conformance tests (5/5 specs) ✓ - [x] +Comparison conformance tests (6/6 specs) ✓ - [x] Logical conformance +tests (3/3 specs) ✓ - [x] String conformance tests (3/3 specs) ✓ - [x] +Conditional conformance tests (1/1 specs) ✓ + +*🏆 Total Progress: 22/22 specs (100% - PERFECT CONFORMANCE)* + +AffineScript now validates against all core aLib operations. Phase 1 +complete! + +=== Contributing + +When contributing affine-specific notes to aLib upstream: 1. Document +ownership semantics 2. Explain move vs borrow decisions 3. Show safety +guarantees 4. Provide affine-specific test vectors + +=== License + +MPL-2.0 (following AffineScript project license) diff --git a/tests/conformance/README.md b/tests/conformance/README.md deleted file mode 100644 index 3f476f24..00000000 --- a/tests/conformance/README.md +++ /dev/null @@ -1,158 +0,0 @@ -# aLib Conformance Tests - -This directory contains conformance tests for the **aggregate-library (aLib)** specification. - -## Purpose - -These tests verify that AffineScript's standard library operations conform to the language-agnostic specifications defined in the [aggregate-library](https://github.com/hyperpolymath/aggregate-library) project. - -## What is aLib? - -aggregate-library (aLib) is a **methodology repository** that provides: -- Language-agnostic operation specifications -- Behavioral semantics and properties -- Executable test vectors in YAML format - -aLib is NOT a code library - it's a way to specify minimal overlap between diverse programming ecosystems. - -## Test Structure - -``` -tests/conformance/ -├── arithmetic/ # Arithmetic operation tests -│ └── add.affine -├── collection/ # Collection operation tests -│ ├── map.affine -│ ├── filter.affine -│ ├── fold.affine -│ └── contains.affine -├── run_all.affine # Master test runner -└── README.md # This file -``` - -## Running Tests - -### Run all conformance tests: -```bash -affinescript tests/conformance/run_all.affine -``` - -### Run specific category: -```bash -affinescript tests/conformance/collection/map.affine -``` - -### Expected Output: -``` -================================================================================ -aLib Conformance Report -================================================================================ - -✓ PASS collection/map: 5/5 tests -✓ PASS collection/filter: 5/5 tests -✓ PASS collection/fold: 6/6 tests -✓ PASS collection/contains: 6/6 tests -✓ PASS arithmetic/add: 5/5 tests - -================================================================================ -Summary -================================================================================ -Total operations tested: 5 -Conformant operations: 5/5 -Total test cases: 27 -Tests passed: 27 -Tests failed: 0 -Conformance rate: 100% - -✓ Excellent aLib conformance (≥95%) -================================================================================ -``` - -## Test Vector Sources - -Each test file includes a reference to its source aLib spec: -```affinescript -// Source: aggregate-library/specs/collection/map.md -``` - -## Conformance Criteria - -- **100% conformance**: All test vectors pass -- **≥95% conformance**: Excellent (production-ready) -- **≥80% conformance**: Good (acceptable with documented gaps) -- **<80% conformance**: Needs improvement - -## AffineScript-Specific Semantics - -AffineScript's conformance tests respect affine type constraints: - -### map -- Source collection **moved** (not copied) -- Elements consumed **exactly once** -- Result owned by caller - -### filter -- Predicate **borrows** (`&T -> Bool`) -- Source collection **moved** -- Filtered elements automatically **dropped** - -### fold -- Accumulator ownership tracked -- Source collection **moved** -- Left-associative evaluation - -### contains -- Requires `Eq` trait on element type -- Short-circuit on first match -- Source collection **borrowed** (not moved) - -## Adding New Conformance Tests - -1. Read the aLib spec from `aggregate-library/specs/` -2. Extract test vectors from YAML section -3. Create `tests/conformance//.affine` -4. Translate aLib function expressions to AffineScript syntax -5. Add test to `run_all.affine` - -Example: -```affinescript -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Source: aggregate-library/specs/collection/map.md - -fn test_map_double() -> TestResult { - let input = [1, 2, 3]; - let result = map(input, fn(x) => x * 2); - assert_eq(result, [2, 4, 6], "Double each number"); - Pass -} -``` - -## Integration Strategy - -See [docs/ALIB-INTEGRATION.md](../../docs/ALIB-INTEGRATION.md) for the complete aLib integration roadmap. - -## Status - -**Phase 1: Conformance** ✅ **COMPLETE** -- [x] Collection conformance tests (4/4 specs) ✓ -- [x] Arithmetic conformance tests (5/5 specs) ✓ -- [x] Comparison conformance tests (6/6 specs) ✓ -- [x] Logical conformance tests (3/3 specs) ✓ -- [x] String conformance tests (3/3 specs) ✓ -- [x] Conditional conformance tests (1/1 specs) ✓ - -**🏆 Total Progress: 22/22 specs (100% - PERFECT CONFORMANCE)** - -AffineScript now validates against all core aLib operations. Phase 1 complete! - -## Contributing - -When contributing affine-specific notes to aLib upstream: -1. Document ownership semantics -2. Explain move vs borrow decisions -3. Show safety guarantees -4. Provide affine-specific test vectors - -## License - -MPL-2.0 (following AffineScript project license) diff --git a/tests/faces/README.adoc b/tests/faces/README.adoc new file mode 100644 index 00000000..e7e64d90 --- /dev/null +++ b/tests/faces/README.adoc @@ -0,0 +1,87 @@ +== Face transformer regression tests + +This directory holds *canonical-text snapshots* for each non-canonical +face’s output, plus a tiny harness for confirming they keep parsing. + +=== What’s tested + +For every example under `+examples/faces/+`: + +[arabic] +. *Snapshot diff* — the script runs the corresponding `+preview-*+` +subcommand and diffs its stdout against a committed `+*.expected.txt+` +here. If the transformer’s output for the same source ever changes, the +diff fails. +. *Round-trip parse* — the example file is parsed via the normal +pipeline (which auto-detects face from the pragma). If a transformer +change ever produces canonical text the parser rejects, this catches it. +. *Canonical baseline* — `+examples/faces/hello-canonical.affine+` is +parsed directly to confirm the reference shape stays valid. + +=== Files + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|File |Source |Captured by +|`+hello-rattle.expected.txt+` |`+examples/faces/hello-rattle.affine+` +|`+affinescript preview-python+` + +|`+hello-jaffa.expected.txt+` |`+examples/faces/hello-jaffa.affine+` +|`+affinescript preview-js+` + +|`+hello-pseudo.expected.txt+` |`+examples/faces/hello-pseudo.affine+` +|`+affinescript preview-pseudocode+` + +|`+hello-lucid.expected.txt+` |`+examples/faces/hello-lucid.affine+` +|`+affinescript preview-lucid+` + +|`+hello-cafe.expected.txt+` |`+examples/faces/hello-cafe.affine+` +|`+affinescript preview-cafe+` +|=== + +=== Workflow + +==== First-time setup (snapshots not yet captured) + +[source,bash] +---- +just build +just test-faces-record # captures any missing snapshot, then diffs +git diff tests/faces/ # review what was captured +git add tests/faces/*.expected.txt +git commit +---- + +==== Routine CI / local check + +[source,bash] +---- +just test-faces # diffs against committed snapshots; fails on drift +---- + +==== Intentional transformer change + +Edit a face transformer (e.g. `+lib/python_face.ml+`), then: + +[source,bash] +---- +just test-faces-update # overwrites the affected snapshot +git diff tests/faces/ # review the lowering change +git add tests/faces/*.expected.txt +git commit +---- + +The diff in the PR shows reviewers exactly how the canonical lowering +changed, which is more useful than just "`transformer modified`". + +=== Why snapshot-test the transformers + +The transformers are pure text-to-text. Bugs typically show up as +drifted output rather than crashes — a missing comma, a wrong keyword +swap, a broken indent rule. Snapshot diffs catch those instantly. +Combined with the round-trip parse, this gives a regression net that: + +* runs in seconds (no codegen, no wasm), +* has zero false positives (output is deterministic), +* doubles as a side-by-side reference for "`different faces, same +cube`". diff --git a/tests/faces/README.md b/tests/faces/README.md deleted file mode 100644 index 4d3830d1..00000000 --- a/tests/faces/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# Face transformer regression tests - -This directory holds **canonical-text snapshots** for each non-canonical face's -output, plus a tiny harness for confirming they keep parsing. - -## What's tested - -For every example under `examples/faces/`: - -1. **Snapshot diff** — the script runs the corresponding `preview-*` subcommand - and diffs its stdout against a committed `*.expected.txt` here. If the - transformer's output for the same source ever changes, the diff fails. -2. **Round-trip parse** — the example file is parsed via the normal pipeline - (which auto-detects face from the pragma). If a transformer change ever - produces canonical text the parser rejects, this catches it. -3. **Canonical baseline** — `examples/faces/hello-canonical.affine` is parsed - directly to confirm the reference shape stays valid. - -## Files - -| File | Source | Captured by | -|---|---|---| -| `hello-rattle.expected.txt` | `examples/faces/hello-rattle.affine` | `affinescript preview-python` | -| `hello-jaffa.expected.txt` | `examples/faces/hello-jaffa.affine` | `affinescript preview-js` | -| `hello-pseudo.expected.txt` | `examples/faces/hello-pseudo.affine` | `affinescript preview-pseudocode` | -| `hello-lucid.expected.txt` | `examples/faces/hello-lucid.affine` | `affinescript preview-lucid` | -| `hello-cafe.expected.txt` | `examples/faces/hello-cafe.affine` | `affinescript preview-cafe` | - -## Workflow - -### First-time setup (snapshots not yet captured) - -```bash -just build -just test-faces-record # captures any missing snapshot, then diffs -git diff tests/faces/ # review what was captured -git add tests/faces/*.expected.txt -git commit -``` - -### Routine CI / local check - -```bash -just test-faces # diffs against committed snapshots; fails on drift -``` - -### Intentional transformer change - -Edit a face transformer (e.g. `lib/python_face.ml`), then: - -```bash -just test-faces-update # overwrites the affected snapshot -git diff tests/faces/ # review the lowering change -git add tests/faces/*.expected.txt -git commit -``` - -The diff in the PR shows reviewers exactly how the canonical lowering changed, -which is more useful than just "transformer modified". - -## Why snapshot-test the transformers - -The transformers are pure text-to-text. Bugs typically show up as drifted -output rather than crashes — a missing comma, a wrong keyword swap, a broken -indent rule. Snapshot diffs catch those instantly. Combined with the -round-trip parse, this gives a regression net that: - -- runs in seconds (no codegen, no wasm), -- has zero false positives (output is deterministic), -- doubles as a side-by-side reference for "different faces, same cube". diff --git a/tools/res-to-affine/CORPUS-RUN.adoc b/tools/res-to-affine/CORPUS-RUN.adoc new file mode 100644 index 00000000..f2c58c2b --- /dev/null +++ b/tools/res-to-affine/CORPUS-RUN.adoc @@ -0,0 +1,176 @@ +== `+res-to-affine+` — Phase-1 corpus run (2026-05-21) + +First end-to-end exercise of the Phase-1 scanner against the estate’s +real surface. Run after +https://github.com/hyperpolymath/affinescript/pull/314[#314] (Phase-1 +skeleton merge) on behalf of +https://github.com/hyperpolymath/affinescript/issues/57[#57]. Surfaced +two high-impact false-positive sources in the top-level regexes and one +false-negative; this run records the fixes and the new baseline. + +=== Corpus + +[width="100%",cols="30%,>40%,30%",options="header",] +|=== +|Repo |Dedup’d `+.res+` files |Notes +|`+idaptik+` |475 |excludes `+lib/bs/**+`, `+lib/ocaml/**+` (copies of +`+src/**+`) + +|`+gitbot-fleet+` |16 |sustainabot + 3 SafeDOM examples; +`+lib/ocaml/**+` excluded + +|*Total* |*491* |run via +`+dune exec tools/res-to-affine/main.exe -- +` +|=== + +`+_wt-lic1-idaptik/+` is a git worktree of `+idaptik+` and was skipped. + +=== Findings + +==== Before the regex fix + +[cols=",>",options="header",] +|=== +|Kind |Hits +|`+side-effect-import+` |1,181 +|`+mutable-global+` |653 +|`+raw-js+` |198 +|`+untyped-exception+` |114 +|*Total* |*2,146* across 216 files +|=== + +Spot-check of the top file (`+idaptik/src/app/devices/LaptopGUI.res+`, +105 markers) showed the 63 `+side-effect-import+` hits there were all +*indented* `+let _ = Container.addChild(parent, child)+` — i.e. ’s +normal "`discard a chained call’s return value`" idiom inside a function +body, not LESSONS.md’s "`module-load side effect`" anti-pattern. + +The same problem applied to `+mutable-global+`: line 496 of +`+NetworkDesktop.res+` is `+currentY := currentY.contents +. ...+`, +local `+ref+` mutation inside a function, not a top-level module-scoped +mutable. + +==== Fixes (this PR) + +`+scanner.ml+`: + +* `+re_side_effect_import+` — drop the leading `+[ \t]*+`; anchor at +column +[arabic, start=0] +. Module-load side effects only fire at top level; in-function +`+let _ = X.f(...)+` is a normal idiom. +* `+re_mutable_global+` — replace bare `+:=+` with +`+^[a-zA-Z_][a-zA-Z0-9_]*[ \t]*:=+`. Same logic: top-level assignment to +a module-scoped ref is the anti-pattern; intra-function +`+counter := ...+` is local mutation. +* `+re_untyped_exn+` — replace `+[^a-zA-Z_]raise[ (]+` / +`+[^a-zA-Z_]try[ {]+` with `+\(^\|[^a-zA-Z_]\)…+`. Previous form +required at least one character before `+raise+` / `+try+`, missing +column-0 occurrences. + +The trade-off is that we no longer flag module-load side effects or +top-level mutable globals nested inside a `+module X = { ... }+` block. +Those are the Phase 2 (AST) walker’s job. The benefit is a clean signal +that the migrator can trust. + +==== After the regex fix + +[cols=",>,",options="header",] +|=== +|Kind |Hits |Δ +|`+raw-js+` |198 |unchanged +|`+untyped-exception+` |114 |unchanged +|`+side-effect-import+` |36 |−1,145 +|`+mutable-global+` |0 |−653 +|*Total* |*348* across 94 files |*−84%* +|=== + +The 397 files (81%) now reporting zero markers do *not* mean those files +are clean — Phase-1 only sees 4 of 6 anti-patterns, and the column-0 +anchoring trades some recall for sharply improved precision. A "`no +findings`" skeleton already calls this out: + +____ +A clean `+.res+` surface does not mean the port is mechanical — +re-decomposition still applies (see PILOT.md upstream). +____ + +==== Top remaining hot-spots + +[width="100%",cols=">40%,30%,30%",options="header",] +|=== +|Markers |File |Dominant kind +|31 |`+.affinescript-src/packages/affine-res/src/AffineScriptValue.res+` +|`+raw-js+` + +|29 |`+idaptik-ums/src/App.res+` |`+raw-js+` + +|24 |`+src/Main.res+` |`+side-effect-import+` (real, column-0 +`+let _ = X.constructor+`) + +|15 |`+src/app/screens/training/TrainingBase.res+` |`+raw-js+` + +|13 |`+src/app/screens/WorldBuilder.res+` |mixed +|=== + +`+AffineScriptValue.res+` and `+App.res+` are heavy `+%raw+` users — the +expected shape for a value-encoding interop layer. `+Main.res+` +top-level `+let _ = X.constructor+` is the canonical +"`explicit-registration`" candidate. + +=== Validation + +* `+dune test tools/res-to-affine/+` — 3/3 OK (synthetic fixture +unchanged; the snapshot covers column-0 cases only, so the regex +tightening leaves the snapshot byte-identical). +* Spot-check confirmed each `+side-effect-import+` hit is genuinely at +column 0 (e.g. `+idaptik/src/Main.res:5:let _ = PixiSound.sound+`). +* Spot-check confirmed each `+raw-js+` and `+untyped-exception+` hit +corresponds to a real `+%raw(…)+` block or `+try+`/`+Js.Exn+`/ +`+Promise.catch+` occurrence. + +=== Follow-ups (deferred to Phase 2 / separate issues) + +These are noise sources the AST walker can fix that the line-regex +scanner cannot, plus a few small Phase-1 robustness items: + +[arabic] +. *Block-comment awareness* — `+is_codeish+` filters `+//+` but not +`+/* … */+`. A `+%raw(…)+` reference inside a block comment would +currently flag. +. *Top-level ref declarations* — Phase 1 flags top-level `+x := …+` +assignments but does not flag the top-level declaration +`+let x = ref(…)+`. Phase 2 should surface both, paired. +. *Nested-module side effects* — `+module Foo = { let _ = X.bar }+` is a +module-load side effect inside a sub-module; needs AST. +. *Callback-record + oversized-function* — already scoped to Phase 2 in +the ADR; surfacing here for completeness. +. *String-literal hits* — a `+:=+` or `+%raw+` inside a string literal +would currently flag. Not seen in the corpus; flagged for awareness. + +=== Reproducing + +[source,sh] +---- +# from a clone of affinescript at the commit landing this PR: +dune build tools/res-to-affine +BIN=$PWD/_build/default/tools/res-to-affine/main.exe + +# from a directory containing idaptik/ and gitbot-fleet/ clones: +find idaptik gitbot-fleet -name '*.res' \ + -not -path '*/node_modules/*' \ + -not -path '*/_build/*' \ + -not -path '*/lib/ocaml/*' \ + -not -path '*/lib/bs/*' > corpus.txt + +mkdir corpus-out +while IFS= read -r f; do + safe=$(echo "$f" | tr '/' '_') + $BIN "$f" > "corpus-out/$safe.affine" +done < corpus.txt + +# tally +cat corpus-out/*.affine \ + | grep -oE '\[(side-effect-import|raw-js|untyped-exception|mutable-global)\]' \ + | sort | uniq -c | sort -rn +---- diff --git a/tools/res-to-affine/CORPUS-RUN.md b/tools/res-to-affine/CORPUS-RUN.md deleted file mode 100644 index aba3de6f..00000000 --- a/tools/res-to-affine/CORPUS-RUN.md +++ /dev/null @@ -1,155 +0,0 @@ - - - -# `res-to-affine` — Phase-1 corpus run (2026-05-21) - -First end-to-end exercise of the Phase-1 scanner against the estate's -real surface. Run after [#314] (Phase-1 skeleton merge) on -behalf of [#57]. Surfaced two high-impact false-positive sources in the -top-level regexes and one false-negative; this run records the fixes -and the new baseline. - -[#57]: https://github.com/hyperpolymath/affinescript/issues/57 -[#314]: https://github.com/hyperpolymath/affinescript/pull/314 - -## Corpus - -| Repo | Dedup'd `.res` files | Notes | -|---|---:|---| -| `idaptik` | 475 | excludes `lib/bs/**`, `lib/ocaml/**` (copies of `src/**`) | -| `gitbot-fleet` | 16 | sustainabot + 3 SafeDOM examples; `lib/ocaml/**` excluded | -| **Total** | **491** | run via `dune exec tools/res-to-affine/main.exe -- ` | - -`_wt-lic1-idaptik/` is a git worktree of `idaptik` and was skipped. - -## Findings - -### Before the regex fix - -| Kind | Hits | -|---|---:| -| `side-effect-import` | 1,181 | -| `mutable-global` | 653 | -| `raw-js` | 198 | -| `untyped-exception` | 114 | -| **Total** | **2,146** across 216 files | - -Spot-check of the top file (`idaptik/src/app/devices/LaptopGUI.res`, 105 -markers) showed the 63 `side-effect-import` hits there were all -**indented** `let _ = Container.addChild(parent, child)` — i.e. 's -normal "discard a chained call's return value" idiom inside a function -body, not LESSONS.md's "module-load side effect" anti-pattern. - -The same problem applied to `mutable-global`: line 496 of -`NetworkDesktop.res` is ` currentY := currentY.contents +. ...`, -local `ref` mutation inside a function, not a top-level module-scoped -mutable. - -### Fixes (this PR) - -`scanner.ml`: - -- `re_side_effect_import` — drop the leading `[ \t]*`; anchor at column - 0. Module-load side effects only fire at top level; in-function - `let _ = X.f(...)` is a normal idiom. -- `re_mutable_global` — replace bare `:=` with - `^[a-zA-Z_][a-zA-Z0-9_]*[ \t]*:=`. Same logic: top-level assignment - to a module-scoped ref is the anti-pattern; intra-function - `counter := ...` is local mutation. -- `re_untyped_exn` — replace `[^a-zA-Z_]raise[ (]` / - `[^a-zA-Z_]try[ {]` with `\(^\|[^a-zA-Z_]\)…`. Previous form - required at least one character before `raise` / `try`, missing - column-0 occurrences. - -The trade-off is that we no longer flag module-load side effects or -top-level mutable globals nested inside a `module X = { ... }` block. -Those are the Phase 2 (AST) walker's job. The benefit is a clean -signal that the migrator can trust. - -### After the regex fix - -| Kind | Hits | Δ | -|---|---:|---| -| `raw-js` | 198 | unchanged | -| `untyped-exception` | 114 | unchanged | -| `side-effect-import` | 36 | −1,145 | -| `mutable-global` | 0 | −653 | -| **Total** | **348** across 94 files | **−84%** | - -The 397 files (81%) now reporting zero markers do **not** mean those -files are clean — Phase-1 only sees 4 of 6 anti-patterns, and the -column-0 anchoring trades some recall for sharply improved precision. -A "no findings" skeleton already calls this out: - -> A clean `.res` surface does not mean the port is mechanical — -> re-decomposition still applies (see PILOT.md upstream). - -### Top remaining hot-spots - -| Markers | File | Dominant kind | -|---:|---|---| -| 31 | `.affinescript-src/packages/affine-res/src/AffineScriptValue.res` | `raw-js` | -| 29 | `idaptik-ums/src/App.res` | `raw-js` | -| 24 | `src/Main.res` | `side-effect-import` (real, column-0 `let _ = X.constructor`) | -| 15 | `src/app/screens/training/TrainingBase.res` | `raw-js` | -| 13 | `src/app/screens/WorldBuilder.res` | mixed | - -`AffineScriptValue.res` and `App.res` are heavy `%raw` users — the -expected shape for a value-encoding interop layer. `Main.res` -top-level `let _ = X.constructor` is the canonical -"explicit-registration" candidate. - -## Validation - -- `dune test tools/res-to-affine/` — 3/3 OK (synthetic fixture - unchanged; the snapshot covers column-0 cases only, so the regex - tightening leaves the snapshot byte-identical). -- Spot-check confirmed each `side-effect-import` hit is genuinely at - column 0 (e.g. `idaptik/src/Main.res:5:let _ = PixiSound.sound`). -- Spot-check confirmed each `raw-js` and `untyped-exception` hit - corresponds to a real `%raw(…)` block or `try`/`Js.Exn`/ - `Promise.catch` occurrence. - -## Follow-ups (deferred to Phase 2 / separate issues) - -These are noise sources the AST walker can fix that the line-regex -scanner cannot, plus a few small Phase-1 robustness items: - -1. **Block-comment awareness** — `is_codeish` filters `//` but not - `/* … */`. A `%raw(…)` reference inside a block comment would - currently flag. -2. **Top-level ref declarations** — Phase 1 flags top-level `x := …` - assignments but does not flag the top-level declaration - `let x = ref(…)`. Phase 2 should surface both, paired. -3. **Nested-module side effects** — `module Foo = { let _ = X.bar }` - is a module-load side effect inside a sub-module; needs AST. -4. **Callback-record + oversized-function** — already scoped to - Phase 2 in the ADR; surfacing here for completeness. -5. **String-literal hits** — a `:=` or `%raw` inside a string literal - would currently flag. Not seen in the corpus; flagged for awareness. - -## Reproducing - -```sh -# from a clone of affinescript at the commit landing this PR: -dune build tools/res-to-affine -BIN=$PWD/_build/default/tools/res-to-affine/main.exe - -# from a directory containing idaptik/ and gitbot-fleet/ clones: -find idaptik gitbot-fleet -name '*.res' \ - -not -path '*/node_modules/*' \ - -not -path '*/_build/*' \ - -not -path '*/lib/ocaml/*' \ - -not -path '*/lib/bs/*' > corpus.txt - -mkdir corpus-out -while IFS= read -r f; do - safe=$(echo "$f" | tr '/' '_') - $BIN "$f" > "corpus-out/$safe.affine" -done < corpus.txt - -# tally -cat corpus-out/*.affine \ - | grep -oE '\[(side-effect-import|raw-js|untyped-exception|mutable-global)\]' \ - | sort | uniq -c | sort -rn -``` diff --git a/tools/res-to-affine/README.adoc b/tools/res-to-affine/README.adoc new file mode 100644 index 00000000..63831e2d --- /dev/null +++ b/tools/res-to-affine/README.adoc @@ -0,0 +1,370 @@ +== `+res-to-affine+` — -to-AffineScript migration assistant + +A small OCaml CLI that reads a `+.res+` file and emits a `+.affine+` +skeleton with *migration markers* — comments that name each anti-pattern +the scanner found, point at the source line, and propose the +AffineScript answer the human migrator should consider before porting. + +Tracks: +https://github.com/hyperpolymath/affinescript/issues/488[`+affinescript#488+`] +(partial-port mode) — successor to the now-closed +https://github.com/hyperpolymath/affinescript/issues/57[`+affinescript#57+`] +(parser + metaparser; declaration translation delivered). Consumed by: +https://github.com/hyperpolymath/gitbot-fleet/issues/148[`+hyperpolymath/gitbot-fleet#148+`] +and the broader `+idaptik+` migration. + +=== Usage + +[source,sh] +---- +# print skeleton to stdout (default: tree-sitter AST walker, Phase 2c) +dune exec tools/res-to-affine/main.exe -- path/to/Foo.res + +# or write to a file +dune exec tools/res-to-affine/main.exe -- path/to/Foo.res -o Foo.affine + +# --translate: render self-contained top-level declarations (type aliases, +# sums, structs, generics, literal `let`->`const`) as compilable AffineScript +dune exec tools/res-to-affine/main.exe -- --translate path/to/Foo.res + +# --partial (#488): render module-top-level functions as `fn` skeletons with +# switch->match + best-effort bodies. Output is a partial port that does NOT +# type-check (un-inferable types/exprs become `_` / `() /* TODO */` holes). +dune exec tools/res-to-affine/main.exe -- --partial path/to/Foo.res + +# opt back into the Phase-1 line-regex scanner (no grammar required) +dune exec tools/res-to-affine/main.exe -- --engine=scanner path/to/Foo.res +---- + +The output is *not compilable*. It is a starting point for the human: a +quoted copy of the original sits at the bottom; the top carries a +migration-considerations block; the middle is a `+module+` stub with +`+TODO+`s. The human picks the decomposition; the tool surfaces what +needs re-decomposing. + +==== Detection engines + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|`+--engine+` |Implementation |When to use +|`+walker+` (default) |Shells out to the vendored `+tree-sitter+` CLI, +walks the AST (`+walker.ml+`). |Default since Phase 2c — covers all six +anti-patterns including the two that the scanner cannot see (inline +callback records, oversized functions) and eliminates the +`+let _ = chained.call()+` / line-anchored false-positive classes. + +|`+scanner+` |Line-anchored regex over the raw source (`+scanner.ml+`). +|Fallback when the vendored grammar is unavailable (no `+tree-sitter+` +CLI, missing `+tools/vendor/tree-sitter-/+`). Detects four of the six +anti-patterns only. +|=== + +The walker requires the vendored `+tree-sitter-+` grammar to be built +first: + +[source,sh] +---- +just install-grammar +# or: ./editors/tree-sitter-/scripts/install.sh +---- + +If the grammar isn’t built or the `+tree-sitter+` CLI isn’t on PATH, the +walker auto-falls-back to the scanner and prints the reason to stderr. + +=== What gets flagged + +The six anti-patterns surfaced in the +https://github.com/hyperpolymath/idaptik/blob/main/migration/main/LESSONS.md[idaptik +Wave 3 pilot]: + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Tag |Detection (walker, default) |AffineScript answer +|`+side-effect-import+` |`+let _ = Mod.foo+` at module top level +(structural — not nested inside a function body) |Explicit registration +call + +|`+raw-js+` |`+extension_expression+` node — any `+%name(...)+` or +`+[%bs.name ...]+` |Typed extern (`+ABI-FFI-README.md+`) + +|`+untyped-exception+` |`+try_expression+`, `+raise(...)+` call, +`+Js.Exn.*+` reference, `+Promise.catch+` member access +|`+Result[E, A]+` / `+Validation[E, A]+` + +|`+mutable-global+` |Top-level `+let x = ref(...)+` (call-of-`+ref+` +body) OR top-level `+mutation_expression+` (`+x := y+`) |Affine record +threaded through + +|`+inline-callback-record+` |≥ 3 inline `+function+` values in one +`+record+` literal OR one call’s `+arguments+` list (via +`+labeled_argument+` or direct) |Row-polymorphic handler record +(LESSONS.md §callback-record) + +|`+oversized-function+` |`+function+` node whose row span exceeds 50 +source lines |Re-decompose before porting; do not transliterate +|=== + +==== Walker vs scanner coverage + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +|Anti-pattern |Scanner (regex) |Walker (AST) +|`+side-effect-import+` |✓ |✓ (since Phase 2b, #322) + +|`+raw-js+` |✓ |✓ (since Phase 2c) + +|`+untyped-exception+` |✓ |✓ (since Phase 2c) + +|`+mutable-global+` |✓ |✓ (since Phase 2c) + +|`+inline-callback-record+` |— |✓ (since Phase 2c, walker-only by +construction) + +|`+oversized-function+` |— |✓ (since Phase 2c, walker-only by +construction) +|=== + +The walker improves on the regex by being structural: it reports +`+side-effect-import+` only when `+let _ = Mod.value+` sits at module +top level, distinguishes a `+try { ... }+` expression from the +identifier `+try+`, only flags `+Mutable_global+` for module-scoped +state (not local refs inside a function body), and dedupes +structurally-overlapping findings on the same line. + +=== Why a skeleton and not a transliteration + +The Frontier Programming Guides’ standing rule is *re-decompose, not +transliterate*. A line-for-line port preserves the source’s +anti-patterns into the target language and produces `+.affine+` files +that are technically parseable but architecturally still . The migration +assistant’s job is to _make the re-decomposition tractable_, not to skip +it. So: + +* The skeleton is *honest about being incomplete* — it does not compile, +on purpose. +* The original source is *quoted at the bottom* so the migrator doesn’t +tab between files while writing the port. +* Each marker links a source line to the AffineScript pattern that +replaces it, so the migrator’s next action is clear. + +=== Phase plan + +==== Phase 1 — text-scan emitter (this PR) + +* OCaml binary builds with the repo’s existing `+dune+` toolchain. +* `+Scanner+` walks lines with `+str+` regexes; cheap and +dependency-free. +* `+Emitter+` writes the migration-considerations block, a `+module+` +stub, and the quoted source. +* Snapshot tests under `+test/+` ensure stable output. + +This phase is *deliberately small*. It is useful immediately — runs +against any `+.res+` file, surfaces 4 of 6 anti-patterns, gives the +migrator a starting document — and it gates the architectural commitment +to tree-sitter in Phase 2 behind something that already pays its way. + +==== Phase 2 — tree-sitter AST walker + +Vendoring of the pinned grammar (`+-lang/tree-sitter-@990214a+`) lives +in `+editors/tree-sitter-/+`; `+install.sh+` materialises the parser +into `+tools/vendor/tree-sitter-/+`. + +* *Phase 2a (#321)* — `+just install-grammar+`, the +`+migration-assistant+` CI job that runs it, dual install path +(`+cargo install tree-sitter-cli+` or +`+npm install -g tree-sitter-cli+`). +* *Phase 2b (#322)* — the walker itself: subprocess to the +`+tree-sitter+` CLI, hand-rolled s-expression parser over the default +`+[row, col]+`-annotated output, AST-based detection of +`+side-effect-import+` only. +* *Phase 2c (this revision)* — walker covers all six anti-patterns +including the two that the scanner cannot see; `+--engine=walker+` +becomes the CLI default. The `+Emitter+` interface does not change; the +marker schema is the same. + +Walker output is deduplicated by `+(kind, line)+` so structurally- +overlapping AST matches don’t inflate the bullet count above what the +line-based scanner would produce on the same file. + +==== Phase 3 — partial translation + +Once the AST walker exists, the emitter can do more than mark — it can +*translate* the pure-structural parts (type aliases, sum decls, simple +`+let+` bindings, switch-to-match) and leave only effect-laden, +exception-bearing, or globally-mutating regions as TODO. The skeleton +becomes a working port of ~60–80% of the input, with TODO islands where +re-decomposition is genuinely required. + +Phase 3 is when the tool earns its keep on idaptik’s 542 files. + +*Phase 3 (`+--translate+`, landed).* The translation path renders the +self-contained, top-level declarations into compilable AffineScript. +Every generated form below is verified by the compiler itself +(`+main.exe check+` → _Type checking passed_). + +[width="100%",cols="34%,33%,33%",options="header",] +|=== +| |AffineScript |Slice +|`+type userId = int+` |`+type UserId = Int+` |1 + +|`+type color = Red \| Green \| Blue+` +|`+type Color =+``+\| Red+``+\| Green+``+\| Blue+` |1 + +|`+type shape = Circle(float) \| Rect(int, int)+` +|`+type Shape =+``+\| Circle(Float)+``+\| Rect(Int, Int)+` |1 + +|`+type point = {x: int, y: int}+` +|`+struct Point {+``+x: Int,+``+y: Int+``+}+` |2 + +|`+type box<'a> = {value: 'a}+` |`+struct Box[A] {+``+value: A+``+}+` |2 + +|`+type option<'a> = None \| Some('a)+` +|`+type Option[A] =+``+\| None+``+\| Some(A)+` |2 + +|`+type id<'a> = 'a+` |`+type Id[A] = A+` |2 + +|`+let answer = 42+` |`+const answer: Int = 42;+` |3 + +|`+let pi = 3.14+` |`+const pi: Float = 3.14;+` |3 + +|`+let greeting = "hi"+` |`+const greeting: String = "hi";+` |3 + +|`+let enabled = true+` |`+const enabled: Bool = true;+` |3 +|=== + +It is *conservative by construction*: a declaration is translated only +when every part is representable — a qualified-path reference +(`+Belt.Map.t+`), a non-primitive/opaque reference, a nested generic +(`+array+`), a GADT return, a variant spread, an object type, a +record with a `+mutable+` or optional-`+?+` field, or a `+let+` whose +body is not an int/float/string/bool literal (a call, a `+ref(...)+` +mutable-global, a destructuring pattern) causes the whole decl to be +_skipped_ (it stays in the marker block + quoted original, never +mis-translated). Two normalisations make the output referenceable: +lower-case type names are capitalised (`+color+` → `+Color+`) and type +variables are mapped (`+'a+` → `+A+`), because `+lib/parser.mly+` reads +a lower-case name in type position as a type _variable_, not a +constructor. Translation is walker-only (it needs the AST); with +`+--engine=scanner+` the flag is a no-op. + +*Scope boundary.* `+--translate+` keeps the "`every emitted form +type-checks standalone`" guarantee, which is why it is limited to +self-contained top-level _declarations_ (types, structs, literal +consts). Forms that can’t meet that guarantee live in a separate mode or +remain deferred: + +* *`+switch+`→`+match+` + function bodies* — landed under +*`+--partial+`* +(https://github.com/hyperpolymath/affinescript/issues/488[#488]), a +distinct partial-port model. A `+match+` is an _expression_, only +meaningful inside a function, and bindings are usually un-annotated +(`+let f = x => …+`) while AffineScript `+fn+` requires param/return +types — so `+--partial+` emits a `+fn+` skeleton with `+_+` type holes + +`+switch+`→`+match+` + best-effort expression translation, and its +output *deliberately does not type-check*. Un-translatable +expressions/patterns become `+() /* TODO */+` / `+_ /* TODO */+` +islands; the result still _parses_. See the `+--partial+` section below. +* *module-qualified references* in _type_ position now _parse_ (the +https://github.com/hyperpolymath/affinescript/issues/228[#228] grammar +gap closed), but a faithful `+Belt.Map.t+` → `+Belt::Map::T+` would not +_resolve_ against a target module that doesn’t exist yet — it waits for +a module-mapping story (tracked in #488). + +==== `+--partial+` — partial-port mode (#488, landed) + +Renders each module-top-level function `+let f = (params) => body+` into +an AffineScript `+fn+` skeleton: + +[width="100%",cols="50%,50%",options="header",] +|=== +| |AffineScript (`+--partial+`) +|`+let area = (w, h) => w *. h+` |`+fn area(w: _, h: _) -> _ { w * h }+` + +|`+let classify = x => switch x { \| Some(n) => n + 1 \| None => 0 }+` +|`+fn classify(x: _) -> _ { match x { Some(n) => n + 1, None => 0, } }+` + +|`+let greet = name => "hi " ++ name+` +|`+fn greet(name: _) -> _ { "hi " ++ name }+` + +|`+let piped = x => x->doStuff(1)+` +|`+fn piped(x: _) -> _ { doStuff(x, 1) }+` + +|`+let chain = x => x->f->g(2)+` |`+fn chain(x: _) -> _ { g(f(x), 2) }+` + +|`+let clamp = x => if x > 0 { x } else { 0 }+` +|`+fn clamp(x: _) -> _ { if x > 0 { x } else { 0 } }+` + +|`+let scaled = x => { let y = x + 1; y * 2 }+` +|`+fn scaled(x: _) -> _ { let y = x + 1; y * 2 }+` +|=== + +It translates literals, identifiers, calls, binary operators +(normalising ’s float ops `++.+`/`+*.+` → `+++`/`+*+` and +`+===+`/`+!==+` → `+==+`/`+!=+`), string concat `++++`, member/qualified +access, ternaries, *`+if+`/`+else+`*, *blocks with `+let+` statements*, +*pipe-first `+->+`* (`+a->f(b)+` → `+f(a, b)+`, chained left-to-right), +*array literals* (`+[a, b]+`), *record literals* (`+{x, y}+` → +`+Rec #{ x: x, y: y }+` — AffineScript records are _nominal_, so an +anonymous record gets the placeholder type `+Rec+` for the human to +rename; field punning `+{x}+` expands to `+x: x+`), and +`+switch+`→`+match+` with variant/tuple/literal patterns. Anything else +(JS objects, interpolated template strings, `+try+`/`+catch+`, …) +becomes a `+() /* TODO */+` hole. The output is a partial port to finish +by hand: it *parses* but is not expected to type-check (verified — the +generated skeletons reach resolution/type-checking without a parse +error). Continuing under #488: JS objects / template strings, +labelled-arg refinement, combining `+--partial+` with `+--translate+`, +and module-qualified-reference _resolution_ (a module-mapping policy +decision). + +=== Corpus run + +link:CORPUS-RUN.md[`+CORPUS-RUN.md+`] records the first end-to-end run +against the estate’s 491 deduplicated `+.res+` files. It documents the +false-positive sources that the corpus surfaced (and the regex fixes +that landed alongside it) plus the Phase-2 follow-ups it identified. A +machine-readable sidecar lives at +link:CORPUS-RUN.json[`+CORPUS-RUN.json+`]. + +=== Testing + +[source,sh] +---- +dune test tools/res-to-affine/ +---- + +To regenerate snapshots after an intentional emitter change: + +[source,sh] +---- +cd tools/res-to-affine/test +../../../_build/default/tools/res-to-affine/main.exe \ + fixtures/sample.res > expected/sample.affine +---- + +The fixture under `+test/fixtures/sample.res+` is synthetic and +exercises every Phase-1 anti-pattern; `+test/fixtures/phase2c.res+` +exercises the two anti-patterns that are walker-only by construction +(`+inline-callback-record+`, `+oversized-function+`); +`+test/fixtures/phase3.res+`, `+phase3b.res+`, and `+phase3c.res+` +exercise the `+--translate+` path (aliases / sums / generics / records / +literal-`+let+`→`+const+` → compilable AffineScript, plus the qualified +/ mutable / optional / non-literal forms it must skip); `+partial1.res+` +exercises the `+--partial+` path (function skeletons + switch→match + +expression translation, with a pipe form that must become a TODO hole). +Real `+.res+` files from the estate +(e.g. `+gitbot-fleet/bots/sustainabot/bot-integration/ src/*.res+`) can +be run ad hoc through the CLI without changes to the test suite. + +=== Non-goals + +* *Not a compiler.* The scanner does not parse ; even Phase 2 only walks +the tree-sitter CST, not the type-checker’s AST. If a `+.res+` file is +syntactically invalid the tool may still emit a (less useful) skeleton. +* *Not a build-time dependency on .* The pinned grammar is a parser, not +the compiler. The estate’s language policy (CLAUDE.md) bans new code; +this tool exists to *help retire the existing surface*, not to bring +more in. +* *Not for editor integration.* Editor tree-sitter bindings for +AffineScript live at `+editors/tree-sitter-affinescript/+`; this tool’s +vendored grammar is for the migration pipeline only. diff --git a/tools/res-to-affine/README.md b/tools/res-to-affine/README.md deleted file mode 100644 index b0afe8de..00000000 --- a/tools/res-to-affine/README.md +++ /dev/null @@ -1,295 +0,0 @@ - - - -# `res-to-affine` — -to-AffineScript migration assistant - -A small OCaml CLI that reads a `.res` file and emits a `.affine` skeleton -with **migration markers** — comments that name each anti-pattern the -scanner found, point at the source line, and propose the AffineScript -answer the human migrator should consider before porting. - -Tracks: [`affinescript#488`](https://github.com/hyperpolymath/affinescript/issues/488) -(partial-port mode) — successor to the now-closed -[`affinescript#57`](https://github.com/hyperpolymath/affinescript/issues/57) -(parser + metaparser; declaration translation delivered). -Consumed by: [`hyperpolymath/gitbot-fleet#148`](https://github.com/hyperpolymath/gitbot-fleet/issues/148) -and the broader `idaptik` migration. - -## Usage - -```sh -# print skeleton to stdout (default: tree-sitter AST walker, Phase 2c) -dune exec tools/res-to-affine/main.exe -- path/to/Foo.res - -# or write to a file -dune exec tools/res-to-affine/main.exe -- path/to/Foo.res -o Foo.affine - -# --translate: render self-contained top-level declarations (type aliases, -# sums, structs, generics, literal `let`->`const`) as compilable AffineScript -dune exec tools/res-to-affine/main.exe -- --translate path/to/Foo.res - -# --partial (#488): render module-top-level functions as `fn` skeletons with -# switch->match + best-effort bodies. Output is a partial port that does NOT -# type-check (un-inferable types/exprs become `_` / `() /* TODO */` holes). -dune exec tools/res-to-affine/main.exe -- --partial path/to/Foo.res - -# opt back into the Phase-1 line-regex scanner (no grammar required) -dune exec tools/res-to-affine/main.exe -- --engine=scanner path/to/Foo.res -``` - -The output is **not compilable**. It is a starting point for the human: -a quoted copy of the original sits at the bottom; the top carries a -migration-considerations block; the middle is a `module` stub with -`TODO`s. The human picks the decomposition; the tool surfaces what -needs re-decomposing. - -### Detection engines - -| `--engine` | Implementation | When to use | -|---|---|---| -| `walker` (default) | Shells out to the vendored `tree-sitter` CLI, walks the AST (`walker.ml`). | Default since Phase 2c — covers all six anti-patterns including the two that the scanner cannot see (inline callback records, oversized functions) and eliminates the `let _ = chained.call()` / line-anchored false-positive classes. | -| `scanner` | Line-anchored regex over the raw source (`scanner.ml`). | Fallback when the vendored grammar is unavailable (no `tree-sitter` CLI, missing `tools/vendor/tree-sitter-/`). Detects four of the six anti-patterns only. | - -The walker requires the vendored `tree-sitter-` grammar to be -built first: - -```sh -just install-grammar -# or: ./editors/tree-sitter-/scripts/install.sh -``` - -If the grammar isn't built or the `tree-sitter` CLI isn't on PATH, the -walker auto-falls-back to the scanner and prints the reason to stderr. - -## What gets flagged - -The six anti-patterns surfaced in the -[idaptik Wave 3 pilot](https://github.com/hyperpolymath/idaptik/blob/main/migration/main/LESSONS.md): - -| Tag | Detection (walker, default) | AffineScript answer | -|---|---|---| -| `side-effect-import` | `let _ = Mod.foo` at module top level (structural — not nested inside a function body) | Explicit registration call | -| `raw-js` | `extension_expression` node — any `%name(...)` or `[%bs.name ...]` | Typed extern (`ABI-FFI-README.md`) | -| `untyped-exception` | `try_expression`, `raise(...)` call, `Js.Exn.*` reference, `Promise.catch` member access | `Result[E, A]` / `Validation[E, A]` | -| `mutable-global` | Top-level `let x = ref(...)` (call-of-`ref` body) OR top-level `mutation_expression` (`x := y`) | Affine record threaded through | -| `inline-callback-record` | ≥ 3 inline `function` values in one `record` literal OR one call's `arguments` list (via `labeled_argument` or direct) | Row-polymorphic handler record (LESSONS.md §callback-record) | -| `oversized-function` | `function` node whose row span exceeds 50 source lines | Re-decompose before porting; do not transliterate | - -### Walker vs scanner coverage - -| Anti-pattern | Scanner (regex) | Walker (AST) | -|---|---|---| -| `side-effect-import` | ✓ | ✓ (since Phase 2b, #322) | -| `raw-js` | ✓ | ✓ (since Phase 2c) | -| `untyped-exception` | ✓ | ✓ (since Phase 2c) | -| `mutable-global` | ✓ | ✓ (since Phase 2c) | -| `inline-callback-record` | — | ✓ (since Phase 2c, walker-only by construction) | -| `oversized-function` | — | ✓ (since Phase 2c, walker-only by construction) | - -The walker improves on the regex by being structural: it reports -`side-effect-import` only when `let _ = Mod.value` sits at module top -level, distinguishes a `try { ... }` expression from the identifier -`try`, only flags `Mutable_global` for module-scoped state (not local -refs inside a function body), and dedupes structurally-overlapping -findings on the same line. - -## Why a skeleton and not a transliteration - -The Frontier Programming Guides' standing rule is **re-decompose, not -transliterate**. A line-for-line port preserves the source's anti-patterns -into the target language and produces `.affine` files that are technically -parseable but architecturally still . The migration assistant's -job is to *make the re-decomposition tractable*, not to skip it. So: - -- The skeleton is **honest about being incomplete** — it does not - compile, on purpose. -- The original source is **quoted at the bottom** so the migrator - doesn't tab between files while writing the port. -- Each marker links a source line to the AffineScript pattern that - replaces it, so the migrator's next action is clear. - -## Phase plan - -### Phase 1 — text-scan emitter (this PR) - -- OCaml binary builds with the repo's existing `dune` toolchain. -- `Scanner` walks lines with `str` regexes; cheap and dependency-free. -- `Emitter` writes the migration-considerations block, a `module` stub, - and the quoted source. -- Snapshot tests under `test/` ensure stable output. - -This phase is **deliberately small**. It is useful immediately — runs -against any `.res` file, surfaces 4 of 6 anti-patterns, gives the -migrator a starting document — and it gates the architectural commitment -to tree-sitter in Phase 2 behind something that already pays its way. - -### Phase 2 — tree-sitter AST walker - -Vendoring of the pinned grammar -(`-lang/tree-sitter-@990214a`) lives in -`editors/tree-sitter-/`; `install.sh` materialises the -parser into `tools/vendor/tree-sitter-/`. - -- **Phase 2a (#321)** — `just install-grammar`, the - `migration-assistant` CI job that runs it, dual install path - (`cargo install tree-sitter-cli` or `npm install -g - tree-sitter-cli`). -- **Phase 2b (#322)** — the walker itself: subprocess to the - `tree-sitter` CLI, hand-rolled s-expression parser over the - default `[row, col]`-annotated output, AST-based detection of - `side-effect-import` only. -- **Phase 2c (this revision)** — walker covers all six anti-patterns - including the two that the scanner cannot see; `--engine=walker` - becomes the CLI default. The `Emitter` interface does not change; - the marker schema is the same. - -Walker output is deduplicated by `(kind, line)` so structurally- -overlapping AST matches don't inflate the bullet count above what -the line-based scanner would produce on the same file. - -### Phase 3 — partial translation - -Once the AST walker exists, the emitter can do more than mark — it can -**translate** the pure-structural parts (type aliases, sum decls, -simple `let` bindings, switch-to-match) and leave only effect-laden, -exception-bearing, or globally-mutating regions as TODO. The skeleton -becomes a working port of ~60–80% of the input, with TODO islands -where re-decomposition is genuinely required. - -Phase 3 is when the tool earns its keep on idaptik's 542 files. - -**Phase 3 (`--translate`, landed).** The translation path renders the -self-contained, top-level declarations into compilable AffineScript. Every -generated form below is verified by the compiler itself (`main.exe check` -→ *Type checking passed*). - -| | AffineScript | Slice | -|---|---|---| -| `type userId = int` | `type UserId = Int` | 1 | -| `type color = Red \| Green \| Blue` | `type Color =`
` \| Red`
` \| Green`
` \| Blue` | 1 | -| `type shape = Circle(float) \| Rect(int, int)` | `type Shape =`
` \| Circle(Float)`
` \| Rect(Int, Int)` | 1 | -| `type point = {x: int, y: int}` | `struct Point {`
` x: Int,`
` y: Int`
`}` | 2 | -| `type box<'a> = {value: 'a}` | `struct Box[A] {`
` value: A`
`}` | 2 | -| `type option<'a> = None \| Some('a)` | `type Option[A] =`
` \| None`
` \| Some(A)` | 2 | -| `type id<'a> = 'a` | `type Id[A] = A` | 2 | -| `let answer = 42` | `const answer: Int = 42;` | 3 | -| `let pi = 3.14` | `const pi: Float = 3.14;` | 3 | -| `let greeting = "hi"` | `const greeting: String = "hi";` | 3 | -| `let enabled = true` | `const enabled: Bool = true;` | 3 | - -It is **conservative by construction**: a declaration is translated only -when every part is representable — a qualified-path reference -(`Belt.Map.t`), a non-primitive/opaque reference, a nested generic -(`array`), a GADT return, a variant spread, an object type, a record -with a `mutable` or optional-`?` field, or a `let` whose body is not an -int/float/string/bool literal (a call, a `ref(...)` mutable-global, a -destructuring pattern) causes the whole decl to be *skipped* (it stays in -the marker block + quoted original, never mis-translated). Two -normalisations make the output referenceable: lower-case type -names are capitalised (`color` → `Color`) and type variables are mapped -(`'a` → `A`), because `lib/parser.mly` reads a lower-case name in type -position as a type *variable*, not a constructor. Translation is -walker-only (it needs the AST); with `--engine=scanner` the flag is a no-op. - -**Scope boundary.** `--translate` keeps the "every emitted form type-checks -standalone" guarantee, which is why it is limited to self-contained top-level -*declarations* (types, structs, literal consts). Forms that can't meet that -guarantee live in a separate mode or remain deferred: - -- **`switch`→`match` + function bodies** — landed under **`--partial`** - ([#488](https://github.com/hyperpolymath/affinescript/issues/488)), a - distinct partial-port model. A `match` is an *expression*, only meaningful - inside a function, and bindings are usually un-annotated - (`let f = x => …`) while AffineScript `fn` requires param/return types — so - `--partial` emits a `fn` skeleton with `_` type holes + `switch`→`match` + - best-effort expression translation, and its output **deliberately does not - type-check**. Un-translatable expressions/patterns become `() /* TODO */` / - `_ /* TODO */` islands; the result still *parses*. See the `--partial` - section below. -- **module-qualified references** in *type* position now *parse* (the - [#228](https://github.com/hyperpolymath/affinescript/issues/228) grammar - gap closed), but a faithful `Belt.Map.t` → `Belt::Map::T` would not - *resolve* against a target module that doesn't exist yet — it waits for a - module-mapping story (tracked in #488). - -### `--partial` — partial-port mode (#488, landed) - -Renders each module-top-level function `let f = (params) => body` into an -AffineScript `fn` skeleton: - -| | AffineScript (`--partial`) | -|---|---| -| `let area = (w, h) => w *. h` | `fn area(w: _, h: _) -> _ { w * h }` | -| `let classify = x => switch x { \| Some(n) => n + 1 \| None => 0 }` | `fn classify(x: _) -> _ { match x { Some(n) => n + 1, None => 0, } }` | -| `let greet = name => "hi " ++ name` | `fn greet(name: _) -> _ { "hi " ++ name }` | -| `let piped = x => x->doStuff(1)` | `fn piped(x: _) -> _ { doStuff(x, 1) }` | -| `let chain = x => x->f->g(2)` | `fn chain(x: _) -> _ { g(f(x), 2) }` | -| `let clamp = x => if x > 0 { x } else { 0 }` | `fn clamp(x: _) -> _ { if x > 0 { x } else { 0 } }` | -| `let scaled = x => { let y = x + 1; y * 2 }` | `fn scaled(x: _) -> _ { let y = x + 1; y * 2 }` | - -It translates literals, identifiers, calls, binary operators (normalising -'s float ops `+.`/`*.` → `+`/`*` and `===`/`!==` → `==`/`!=`), string -concat `++`, member/qualified access, ternaries, **`if`/`else`**, **blocks -with `let` statements**, **pipe-first `->`** (`a->f(b)` → `f(a, b)`, chained -left-to-right), **array literals** (`[a, b]`), **record literals** (`{x, y}` → -`Rec #{ x: x, y: y }` — AffineScript records are *nominal*, so an anonymous - record gets the placeholder type `Rec` for the human to rename; -field punning `{x}` expands to `x: x`), and `switch`→`match` with -variant/tuple/literal patterns. Anything else (JS objects, interpolated -template strings, `try`/`catch`, …) becomes a `() /* TODO */` hole. The output -is a partial port to finish by hand: it **parses** but is not expected to -type-check (verified — the generated skeletons reach resolution/type-checking -without a parse error). Continuing under #488: JS objects / template strings, -labelled-arg refinement, combining `--partial` with `--translate`, and -module-qualified-reference *resolution* (a module-mapping policy decision). - -## Corpus run - -[`CORPUS-RUN.md`](CORPUS-RUN.md) records the first end-to-end run -against the estate's 491 deduplicated `.res` files. It documents the -false-positive sources that the corpus surfaced (and the regex fixes -that landed alongside it) plus the Phase-2 follow-ups it identified. -A machine-readable sidecar lives at [`CORPUS-RUN.json`](CORPUS-RUN.json). - -## Testing - -```sh -dune test tools/res-to-affine/ -``` - -To regenerate snapshots after an intentional emitter change: - -```sh -cd tools/res-to-affine/test -../../../_build/default/tools/res-to-affine/main.exe \ - fixtures/sample.res > expected/sample.affine -``` - -The fixture under `test/fixtures/sample.res` is synthetic and exercises -every Phase-1 anti-pattern; `test/fixtures/phase2c.res` exercises the -two anti-patterns that are walker-only by construction -(`inline-callback-record`, `oversized-function`); `test/fixtures/phase3.res`, -`phase3b.res`, and `phase3c.res` exercise the `--translate` path -(aliases / sums / generics / records / literal-`let`→`const` → compilable -AffineScript, plus the qualified / mutable / optional / non-literal forms it -must skip); `partial1.res` exercises the `--partial` path (function -skeletons + switch→match + expression translation, with a pipe form that must -become a TODO hole). -Real `.res` files -from the estate (e.g. `gitbot-fleet/bots/sustainabot/bot-integration/ -src/*.res`) can be run ad hoc through the CLI without changes to the -test suite. - -## Non-goals - -- **Not a compiler.** The scanner does not parse ; - even Phase 2 only walks the tree-sitter CST, not the - type-checker's AST. If a `.res` file is syntactically invalid the - tool may still emit a (less useful) skeleton. -- **Not a build-time dependency on .** The pinned grammar is a - parser, not the compiler. The estate's language policy - (CLAUDE.md) bans new code; this tool exists to **help retire - the existing surface**, not to bring more in. -- **Not for editor integration.** Editor tree-sitter bindings for - AffineScript live at `editors/tree-sitter-affinescript/`; this tool's - vendored grammar is for the migration pipeline only.