6869bd5d0c7beb112e193371616eb1500d2d6c13
12 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b636b883a3 | iter 22b.2.1: add Constraint struct, Type::Forall.constraints | ||
|
|
50b68267fe |
check: mode-strict-because suppression (iter 19b)
Closes the 19a/19a.1/19b arc. Corpus signal from 19a.1 (5/65
fixtures fire over-strict-mode, all deliberate RC codegen-test
fixtures) justified shipping the suppress mechanism end-to-end.
Schema: Suppress { code, because } on FnDef. Pre-19b hashes
bit-identical via skip_serializing_if. Typechecker drops matching
diagnostics; empty 'because' is Error severity; wrong codes are
silent no-ops (open-set registry).
Form-A: (suppress (code "...") (because "...")) clause, parser
+ printer round-trip clean. Form-B: '// @suppress <code>: <reason>'
above the doc string, lossless contract metadata.
5 RC fixtures migrated (.ail.json + .ailx + .prose.txt). Corpus
signal: 5/65 -> 0/65. Lint still fires on any future fn that's
accidentally over-strict without an authored reason.
Test counts: ailang-check 55->61, ailang-core 26->28, ailang-surface
21->26, ailang-prose 49->52, e2e 70 unchanged.
Known debt: .ailx comment headers lost on regen (ail render's
contract excludes comments); parse_suppress_attr accepts
duplicate code/because attrs without diagnose (bounded by canonical
print order).
|
||
|
|
ce6ab8ee44 |
Iter 18e: (drop-iterative) annotation + worklist allocator
Closes the 18-arc's stack-recursion limit. Recursive drop cascades from 18c.4 overflow on long ADT chains (Linux's 8 MB default stack maxes out around 1M cells of List). The new opt-in (drop-iterative) annotation on a Def::Type switches the synthesised drop_<m>_<T> body from recursive to iterative-with- explicit-worklist for that type. Schema: - TypeDef.drop_iterative: bool. Default false; serde-skip when false so existing fixtures' canonical JSON hashes stay stable. - Form-A: (drop-iterative) clause inside (data T ...). Worklist runtime (4 new ABI symbols in runtime/rc.c): - ailang_drop_worklist_new(initial_capacity) - ailang_drop_worklist_push(wl, ptr) - ailang_drop_worklist_pop(wl) -> ptr - ailang_drop_worklist_free(wl) Heap stretchy buffer, doubling on overflow, null-filtering on push. Lean 4 / Roc precedent documented in the runtime; the slot-repurposing strategy was considered and rejected because not every box has a free pointer-typed slot to thread the worklist through (Cons head is i64, slot 1 is ptr but it's the field we're following — no free slot). Codegen (emit_iterative_drop_fn_for_type): for a drop_iterative type, drop_<m>_<T>(ptr %p) emits a worklist loop. Fields of the SAME annotated type push onto the worklist (mono-typed); fields of DIFFERENT types call their own drop fn directly (recursive on those, but only if THEY are themselves recursive — i.e. one level of cascade jump maximum). Mono-typed-worklist is sound for the deep-self- recursion case the iter targets (List of List of T just needs the spine flattened). Tests: - examples/rc_drop_iterative_long_list — 1M-cell List of Int with (drop-iterative) annotation. - alloc_rc_drop_iterative_handles_million_cell_list E2E — builds + runs under --alloc=rc, asserts clean exit. With annotation: exits 0. Without annotation (control): SIGSEGV at exit code 139 (verified by hand). Worklist is load- bearing. - iter18e_drop_iterative_emits_worklist_body_no_self_recursion IR-shape: worklist body has br to loop_head AND no direct recursive call into drop_<m>_<T>. - iter18e_no_annotation_keeps_recursive_drop_body — control: unannotated variant still emits the 18c.4 recursive shape. - 3 surface parse-tests for the annotation round-trip. Test deltas: e2e 58 -> 61 (+3), surface 18 -> 21 (+3). All other buckets unchanged. cargo test --workspace green. Known debt (deliberate): - Mono-typed worklist: cross-type drop-iterative fields call the other type's drop fn directly. A heterogeneous worklist would be more general but adds tag tracking complexity for a case (drop-iterative T containing drop-iterative T') that's narrower than the deep-self- recursion target. Documented in emit_iterative_drop_fn_for_type's doc. - Closure / Type::Var / Type::Forall fields fall back to shallow ailang_rc_dec via field_drop_call — same as the recursive variant. - Dynamic-tag partial-drop fallback (head_or_zero epilogue shallow dec when moved_slots non-empty) — out of scope per brief. |
||
|
|
74379b29e5 |
Iter 18d.1: Term::ReuseAs schema + linearity check
Schema floor for explicit reuse hints. Adds Term::ReuseAs
{ source: Box<Term>, body: Box<Term> } with serde tag "reuse-as",
form-A (reuse-as <source> <body>). Schema choice (wrapper, not
modifier-on-Ctor) and rationale recorded in DESIGN.md.
Codegen is identity under all --alloc strategies — lower body,
drop source on the floor. Iter 18d.2 will lower this as the
in-place rewrite under --alloc=rc.
User-facing diagnostics:
- typecheck reuse-as-non-allocating-body: body must be a
Term::Ctor or Term::Lam, the two AST shapes that allocate.
Other shapes (literal, var, app, ...) emit this with a
suggested_rewrite that drops the wrapper.
- linearity reuse-as-source-not-bare-var: source must be a
Term::Var referring to an in-scope binder. Anything else
(literal, nested expression) is rejected here. Suggested
rewrite drops the wrapper.
- linearity use-after-consume at a reuse-as site: source must
not have been consumed earlier in the body. Suggested rewrite
drops the wrapper.
- Visiting reuse-as marks source consumed for the rest of the
body, so a subsequent use of the same binder flags
use-after-consume against it.
Uniqueness inference treats source as Consume — the consume
shows up in the side table for the codegen consumer.
Tests:
- 4 surface parse-tests (round-trip, no-args, one-arg, full
parse_term/term_to_form_a round-trip).
- 1 typecheck unit test (non-allocating body).
- 2 linearity unit tests (non-var source, after-consume).
- 1 integration test (happy-path map_inc in all-explicit mode
is linearity-clean).
- 1 E2E test running the canonical map_inc-via-reuse-as fixture
under --alloc=gc, asserting stdout 9.
- New fixture examples/reuse_as_demo.{ailx,ail.json}.
Test deltas: E2E 55 -> 56, ailang-check unit 43 -> 46,
ailang-check workspace 8 -> 9, surface 14 -> 18. cargo test
--workspace green. No existing fixture's canonical JSON changed.
|
||
|
|
09fb5bb113 |
Iter 18c.2: linearity check + suggested_rewrites
Adds a per-fn linearity check that runs on every fn whose param_modes are all explicit (Borrow or Own, no Implicit), tracks each binder's consume/borrow state through the AST, and emits two diagnostics with form-A `suggested_rewrites`: - `use-after-consume` — a binder is referenced after it has already been consumed. - `consume-while-borrowed` — a binder is consumed while a borrow of it is still live (sibling arg slot, or an enclosing-fn Borrow param). Fns with any Implicit param skip the check entirely; that's the back-compat lane that keeps every existing fixture green. Pure diagnostic addition: no IR change, no codegen change, no runtime change. Each diagnostic carries a non-empty `suggested_rewrites` whose `replacement` is form-A AILang text (typically `(clone <name>)`). Adds two new public entrypoints to ailang-surface — `parse_term` and `term_to_form_a` — so the round-trip `parse_term(term_to_form_a(t)) ≡ t` is the contract every emitted replacement must satisfy. Tests assert the contract. Linearity pass runs only on modules that typechecked clean (any upstream typecheck error suppresses it for that module — running on partly-defined IR would produce noise). Tests: 3 unit tests in `linearity::tests`, 3 integration tests in `ailang-check/tests/workspace.rs`, 1 serialisation test in `diagnostic.rs`. `cargo test --workspace` green. |
||
|
|
7dfc88a4f3 |
Iter 18c.1: Term::Clone schema (no inc emission yet)
Adds (clone X) as a new Term variant. Form-A:
(let p (expensive_fn x)
(do consume_a (clone p)) ; explicit RC inc — needs 18c.3
(do consume_b p))
Schema floor for the LLM-aware RC design (Decision 10): explicit
clone is the author-visible alternative to implicit sharing. In
18c.1 the variant is purely additive — typechecker treats it as
identity (same type as inner), codegen lowers it to the inner
term's IR with no extra calls. Iter 18c.3 will replace the
codegen identity-arm with a single call:
Term::Clone { value } =>
let v = self.lower_term(value, ...)?;
self.emit("call void @ailang_rc_inc(ptr {v})");
Ok(v)
Match-arm count: ~10 sites across 6 files, all mechanical
structural recursion through `value`. The codegen `lower_term`
arm carries an explicit comment marking the 18c.3 emission seam.
Hash invariant: `(clone X)` uses a new serde tag "clone" that no
pre-18c.1 fixture contains. Every existing fixture's canonical
JSON is bit-identical (git diff examples/ empty).
New fixture examples/clone_demo.{ailx,ail.json}:
(let x 42 (do io/print_int (clone x)))
Stdout 42 under both --alloc=gc and --alloc=rc.
Tail-position propagates through clone (verify_tail_positions
treats `(clone tail-call)` as itself a tail call), per the
identity-of-clone semantics in 18c.1.
Verified:
- cargo build --workspace clean
- cargo test --workspace green: 52 e2e (+1), 14 surface parse
(+2 for clone tests), all other buckets unchanged
- ail render round-trip exact
- clone_demo --alloc=gc → 42; --alloc=rc → 42
|
||
|
|
b9ca8894a5 |
Iter 18a: (borrow T) / (own T) mode annotations on fn signatures
Adds form-A surface (borrow T) / (own T) wrappers in fn-type
params and ret slots. Internally these are not new Type variants
but per-position metadata on Type::Fn:
Type::Fn { params, param_modes, ret, ret_mode, effects }
enum ParamMode { Implicit, Own, Borrow }
This keeps Type itself unchanged, so unification, occurs, apply,
and ~190 other Type match-arms in the typechecker need no new
branch. Fields use serde skip-if-default predicates so canonical
JSON hashes for every pre-18a fixture stay bit-identical (sum,
list, hof, closure, list_map, etc.: zero diff under git).
Iter 18a is purely additive: typechecker treats modes as
transparent (mode-compat check deferred to 18c), no codegen
change (--alloc=gc / Boehm path unchanged), no linearity
enforcement. The annotation info flows into the JSON side-table
that 18c will consume.
Equality of mode slices is length-tolerant: a vec![] (the form
written by mechanically-updated construction sites that elide
modes) compares equal to vec![Implicit; n]. This kept the patch
from being a 100-site mechanical migration through the
typechecker. 18c will choose: keep the slack, or normalise to
full-length on construction.
New fixture examples/borrow_own_demo.{ailx,ail.json} exercises
both modes. list_length declares (borrow (con List)); sum_list
declares (own (con List)). Stdout: 3 then 6.
Documentation: DESIGN.md schema-additions block clarified that
modes are Type::Fn metadata (not Type variants); migration plan
flag rename --memory=rc → --alloc=rc to match the existing CLI
flag.
Verification:
- cargo build --workspace green
- cargo test --workspace green (154 tests, +1 vs baseline 153)
- git diff examples/{sum,list,hof,closure,list_map,...}.ail.json: empty
- borrow_own_demo runtime stdout: "3\n6\n"
- canonical JSON contains "param_modes":["borrow"] and ["own"]
|
||
|
|
8860600e37 |
Iter 16b.1 — local recursive let (no-capture)
Add `(let-rec NAME (params ...) (type ...) (body ...) (in ...))` as a form-A surface and a `Term::LetRec` AST variant. The 16a desugar pass lifts each LetRec whose body has no captures from the enclosing scope to a synthetic top-level fn `<hint>$lr_N` and substitutes the original name; typecheck and codegen never see LetRec. Capture detection panics at desugar time, queued for 16b.2. Tests: 95 → 99 (+1 e2e local_rec_factorial_demo, +2 desugar unit, +1 parse unit). The new fixture `examples/local_rec_demo` runs `fact` at n=1, 3, 5 → prints 1, 6, 120. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
41d406bcbb |
Iter 14g: Term::If restored (revert of 14d)
Reconsidered 14d's removal of Term::If. The decision was wrong.
"No redundancies" requires judgment; reducibility (if -> match)
is not redundancy in the strong sense. Term::If is a primitive
control-flow shape; bool branching is the second most common
shape after sequencing, and removing it cost 3x tokens on every
branch site (`(if c a b)` 4 tokens vs the match-on-Bool form
12 tokens).
Meta-pattern fixed: I had been treating user observations as
directives. User said "if is a subset of match"; I jumped to
remove it citing CLAUDE.md, with no independent conviction.
The leak appeared in 14f's JOURNAL prose ("three lines for what
if used to do in one"), which read as regret. Two feedback
memories saved (memory/feedback_user_suggestions_not_directives,
memory/feedback_no_nostalgia_for_removed_features) to head this
off.
Implementation: mechanical reverse-application of 14d's diff at
every site (AST, check including the 14e tail-position arm,
codegen 4 sites, surface parser/printer, pretty, CLI walker,
e2e test mutation). Removed lower_bool_match helper — it existed
only because 14d's migration shape needed codegen for non-ptr
match scrutinees; with Term::If back, match-on-Bool returns to
its pre-14d unsupported state. Three fixtures (sum, sort, max3)
restored to pre-14d shape. gc_stress (added in 14f) also
migrated back to (if ...) since it was authored under the wrong
constraint.
14e (musttail) and 14f (GC_malloc) verified intact in IR.
Hashes restored to pre-14d values:
- sum.sum: db33f57cb329935e
- sort.insert: 697fcb9f30f8633a
- max3.max: 65c45d6a45dd0a72
- max3.max3: 624b14429bf302f5
All other defs across all 18 fixtures keep their post-14f
hashes. Tests 80/80 green; cargo doc 0 warnings. LOC delta
+265/-295 net -30.
DESIGN.md Decision 7 preserved with a "Status: REVERTED" header
for audit trail. Form-(A) `if-term` production restored.
Plan: back to 15a (std_maybe stdlib module).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d64031c234 |
Iter 14e: explicit, verified tail calls
Decision 8 ships. Term::App and Term::Do gain tail: bool with serde-default false and skip-when-false serialisation. New typecheck pass verify_tail_positions enforces tail-position rules (Scheme-style propagation through match arms, seq.rhs, let body, lam body). Codegen emits musttail call for marked App calls. Hash invariance verified: only the two migrated print_list defs (list_map_poly.print_list, sort.print_list) changed hashes; all other defs across all 18 fixtures kept bit-identical hashes — confirms the skip-when-false serialisation rule works. Tests 76 -> 79: tail_call_in_non_tail_position_is_rejected, tail_call_in_tail_position_is_accepted, plus an IR-grep e2e test asserting that print_list's recursive call site emits musttail in the lowered IR. Existing 25 e2e tests unchanged in behaviour (map -> [2,3,4], sort -> sorted list). IR evidence at the recursive site: %v7 = musttail call i8 @ail_list_map_poly_print_list(ptr %v6) ret i8 %v7 Two deviations called out in the implementer report and JOURNAL: 1. tail-do uses tail call, not musttail. Cross-type return (runtime helpers return i32, AILang Unit is i8) would have LLVM reject musttail. Path is implemented but not exercised by any current fixture; proper fix is runtime-helper signature change, punted. 2. block_terminated flag in codegen so tail-call emit (musttail call + ret) doesn't get a duplicate trailing ret from surrounding code (match-arm phi, fn-body, lambda thunk). Internal plumbing; required for IR well-formedness. Form (A) productions now at ~30, exactly the constraint-1 budget. Future surface additions need to retire something or explicit-budget-rebalance in DESIGN.md. GC notes from implementer survey land in JOURNAL: - Allocations cluster in lower_ctor; every term-ctor does malloc(8+8n). - Tail recursion does not reduce alloc pressure, only stack. For map-style ctor-blocked recursions, allocation IS the bottleneck. - Per-fn arena is sound only when fn return type contains no boxed ADT. Most current fixtures violate this. Plan 14f: Boehm conservative GC (GC_malloc, -lgc) as a first cut. Single-iter integration, no AST/schema change. Stress test: build a 100k Cons list, observe RSS doesn't blow up. After 14f the language is feature-complete enough for stdlib work (15a). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8d97a924de |
Iter 14d: remove Term::If as a redundancy
Term::If was semantically a subset of Term::Match on Bool. Per CLAUDE.md the language must contain no redundancies; two AST nodes for the same operation produces an authoring decision with no semantic content and a duplicate codegen path. Removed. Migration shape (applied to sum, sort, max3 fixtures): (if c a b) -> (match c (case (lit-bool true) a) (case _ b)) No schema version bump (per user direction): no third-party consumes ailang/v0, so version ceremony is pure overhead. Edited AST and fixtures in place; pinned hashes in hash.rs updated. Implementer deviation, called out and justified: a tightly-scoped lower_bool_match helper (~95 LOC) was needed in codegen because the existing match path rejects i1 scrutinees and Pattern::Lit. Helper accepts only the canonical two-arm migration shape, errors on anything else, emits the same br/phi IR Term::If used to. No generalisation of the ADT-match codegen. Diff: 13 files, +286/-221 (net +65 LOC). AST got smaller (one variant gone), form-(A) got smaller (one production gone), typecheck got smaller (one branch gone). Codegen got slightly larger by the bool-match helper. Hash deltas: sum.sum, sort.insert, max3.max, max3.max3 changed. All other defs (e.g. sum.main, sort.IntList, sort.sort, sort.print_list, max3.main) kept bit-identical hashes — confirms canonical-JSON byte format intact. Verification: 76/76 tests green; sum->55, max3->17, sort->[1,1,2, 3,3,4,5,5,5,6,9] (identical to pre-migration). cargo doc 0 warnings. Tail-call survey by implementer (informs 14e): print_list recursions are already in tail position (rhs of seq inside match arm); map/sort/insert recursions are NOT (constructor-blocked inside Cons applications). 14e annotation will benefit terminal recursions; ctor-blocked ones need accumulator-form rewrites in source, not a compiler-side transform. Decision 7 added to DESIGN.md. JOURNAL entry has the language- completion sequence (14d done, 14e tail-calls, 14f GC, 15a stdlib). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
706f90bacd |
Iter 14c: ailang-surface ships — form (A) parser + pretty-printer
Strictly additive new crate per Decision 6 architectural pin. JSON-AST stays the source of truth; ailang-surface is one producer/consumer of ailang_core::ast::Module values. ailang-check and ailang-codegen unchanged. Crate contents: - src/lex.rs (~264 LOC): 3-rule lexical core. Whitespace and parens delimit tokens; semicolon to EOL is comment; first-character classifier (digit -> int, " -> string, else -> ident). - src/parse.rs (~1041 LOC): hand-written recursive descent. One Rust fn per EBNF production. No parser-combinator dep. - src/print.rs (~371 LOC): deterministic pretty-printer. Round-trip contract with parse() is the surface's correctness gate. - tests/round_trip.rs (~128 LOC): integration test runs every examples/*.ail.json fixture through print -> parse -> canonical JSON, asserts canonical-byte equality with the original. Two AST-driven form widenings beyond the 14b sketch (both folded into DESIGN.md Decision 6): - lam-term carries (typed name type) params, ret type, and optional effects (Term::Lam has parallel param_tys/ret_ty/ effects fields). - import-clause admits (import name (as alias)?) (Import.alias is Option<String>). Production count ~28, under 30-rule budget. Constraint 1 (formalisable for foreign LLM) intact. Verification: - cargo build --workspace green. - cargo test --workspace: 76 tests green (was 64; +9 surface unit, +2 round-trip integration). All 17 fixtures round-trip byte-identical at canonical level. 3 hand-written .ailx exhibits parse to canonical JSON identical to their .ail.json siblings. - cargo doc --no-deps zero warnings (DESIGN.md item 6 invariant). Manual smoke test (ail parse → ail run): hello, box, list_map_poly all produce expected output through the form-(A) authoring lane end-to-end. CLI: ail parse <file.ailx> [-o <file.ail.json>]. .ail.json remains a first-class input to every existing subcommand. Plan 14d: stdlib (std_list.ailx with length/filter/fold/concat/ reverse/head/tail), authored in form (A) from day one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |