Commit Graph

74 Commits

Author SHA1 Message Date
Brummel 098fa7e9be iter rt.1: roundtrip-invariant audit tests — 3 new tests, all PASS first-shot
Three new reader-only tests pin the .ail.json / .ailx bijection
plus AST-variant coverage. All three passed first observation
across the current corpus — no roundtrip, schema-coverage, or
CLI-drift gaps surfaced.

- every_ailx_fixture_matches_its_json_counterpart (replaces the
  3-pair handwritten exhibits): 57 paired .ailx fixtures pass.
- every_ast_variant_is_observed_in_the_fixture_corpus (new): 34/34
  AST variants exercised across 136 fixtures; exhaustive matches
  without _ wildcard so AST drift fails the build.
- cli_render_then_parse_preserves_canonical_bytes_on_every_fixture
  (new): 136 fixtures round-trip through ail render → tempfile →
  ail parse with BLAKE3 identity on canonical bytes.

No production code, no DESIGN.md changes (those follow in later
iterations). Tests are pure readers of the repo per spec
acceptance #7 — tempfile crate added as workspace dep for the CLI
roundtrip's intermediate file outside the repo.
2026-05-12 09:31:38 +02:00
Brummel 92e830e6c2 iter 23.5: prelude free fns + E2E — Eq/Ord milestone close
Five polymorphic prelude free fns (ne/lt/le/gt/ge) plus three E2E
fixtures: positive composition over Int/Bool/Str, user-ADT with
user-written Eq/Ord on IntBox, and Float-NoInstance with a
Float-aware diagnostic addendum cross-referencing DESIGN.md §"Float
semantics".

Two checker-side fixes were needed alongside the prelude additions,
both symmetric to the iter 23.4-prep visibility fix:
- linearity::check_module_with_visible — workspace-aware callee-mode
  resolution so a Borrow-mode user fn forwarding into a prelude poly
  fn (e.g. `at_most x y = not (gt x y)`) doesn't false-fire
  consume-while-borrowed.
- mono::collect_mono_targets — distinguish rigid forall vars from
  unbound metavars; rigid-bearing free-fn targets skip (they get
  re-collected with concrete subst when the enclosing fn itself
  monomorphises). Without this, a polymorphic body calling another
  polymorphic free fn produced a spurious __Unit specialisation
  whose body referenced compare at Unit.

Roadmap split: shipped Eq/Ord half checked, Show + print-rewire half
remains pending behind the heap-Str ABI prerequisite. DESIGN.md
§"Prelude classes" amended.

One pre-existing fixture (test_22b1_missing_method) renamed its
class method ne → tne to avoid collision with the new prelude `ne`.

Bench: check.py + cross_lang.py exit 0; compile_check.py exits 1
with one sub-ms check_ms.local_rec_capture at +26% (tolerance 25%) —
prelude-growth-related noise-band fluctuation, ratifiable per spec
§248-249 and journal Concerns.
2026-05-12 00:38:52 +02:00
Brummel 1d039b782e iter ct.4.2: pin canonical-form hashes for migrated fixtures + unmigrated sanity 2026-05-11 09:51:19 +02:00
Brummel f22f88b547 iter ct.1.7: on-disk fixture tests for canonical-form diagnostics 2026-05-11 01:46:58 +02:00
Brummel 727d2c13ec iter ct.1.6: wire validator into load_workspace; refresh kind-mismatch test 2026-05-11 01:42:45 +02:00
Brummel 5e653ff536 iter ct.1.5a-followup: forall-constraints recursion + docstring + drop iteration-tag comments
Three Minor items from the ct.1.5a quality review:

1. normalize_type_for_registry now recurses into
   Type::Forall.constraints[].type_, symmetric to walk_type. Closes a
   latent gap: a Forall with bare cross-module constraint types reaching
   the registry would have produced a key disagreeing with the qualified
   form. RED test: ct1_5a_normalize_recurses_into_forall_constraints.

2. Documented the bare-name limitation on Registry::type_def_module:
   two modules each defining 'type Foo' collide at insert; the proper
   fix (re-key as (owning_module, bare_name)) is out of ct.1's scope.

3. Removed the four iter-tag explanatory paragraphs at the
   normalize_type_for_lookup call sites (check/lib.rs check_fn,
   mono.rs three sites). Replaced each with a one-line pointer to the
   helper's docstring per project comment policy.
2026-05-11 01:39:26 +02:00
Brummel 7872ccfc23 iter ct.1.5a: registry-side normalize-before-hash 2026-05-11 01:32:21 +02:00
Brummel 219908ed54 iter ct.1.3-followup: test for Constraint.class in ClassDef-method Forall 2026-05-11 01:11:47 +02:00
Brummel 9e2e84332f iter ct.1.3: validator rejects qualified class-name fields 2026-05-11 01:08:50 +02:00
Brummel 3723495d8a iter ct.1.2: validator extends to Term::Ctor.type_name 2026-05-11 01:04:39 +02:00
Brummel 000b84dda7 iter ct.1.1: validator scaffolding + Type::Con canonical-form rules
Adds BareCrossModuleTypeRef / BadCrossModuleTypeRef / QualifiedClassName
to WorkspaceLoadError plus the validate_canonical_type_names function
that enforces the canonical-form rule on every Type::Con name reachable
from any def (including Term::Lam.param_tys / .ret_ty and Term::LetRec.ty).
Term::Ctor.type_name and class-name fields land in Tasks 2 + 3; the
validator is not yet wired into load_workspace (Task 6). Seven RED-first
unit tests cover the cases enumerated in spec section Testing strategy
plus a Lam-embedded-type smuggle-prevention test.

main.rs gains diagnostic arms for the three new variants to keep the
exhaustive match compiling.
2026-05-11 00:58:37 +02:00
Brummel 12d9130567 iter 23.3.3: prelude — class Ord a extends Eq + Ord Int/Bool/Str instances 2026-05-10 23:13:02 +02:00
Brummel 7172e7e8c9 iter 23.2.3: prelude — class Eq a + Eq Int/Bool/Str instances 2026-05-10 22:35:51 +02:00
Brummel 7289bbc9d4 iter 23.2.3-prep: reconcile workspace tests with auto-loaded prelude class Eq
The auto-loaded prelude (iter 23.2 work-in-progress) injects
`class Eq` plus three primitive instances (Eq Int, Eq Bool, Eq Str)
into every workspace. Five workspace::tests assertions broke against
this new baseline:

- iter22b1_missing_method_fires_diagnostic
- instance_without_superclass_instance_fires
- instance_overriding_nonexistent_method_fires
  Their fixtures redeclare `class Eq` (and `class Ord` in one case),
  which now collides with the prelude. Renamed each fixture's
  `Eq`/`eq` -> `TEq`/`teq` and `Ord`/`lt` -> `TOrd`/`tlt`, matching
  the scheme already used in the two earlier-renamed test_22b2
  fixtures. Method `ne` stays (does not collide). Assertion strings
  in workspace.rs updated to match.

- iter22b1_workspace_with_no_classes_has_empty_registry
- iter22b1_instance_in_class_module_loads_clean
  Their asserted registry counts assumed an otherwise-empty registry.
  Rewritten to filter by `defining_module != "prelude"`: the property
  is "no NON-prelude entries" / "exactly one fixture-owned entry",
  not raw count.

Doc comments updated alongside each assertion change so the rationale
for TEq/TOrd (collision avoidance) is local. No production code
touched.
2026-05-10 22:34:30 +02:00
Brummel 927f7ea38f iter 23.1.2 fixup: align workspace-root idiom + add collision test for ReservedModuleName 2026-05-10 21:16:56 +02:00
Brummel 3742583924 iter 23.1.2: load_workspace auto-injects prelude module 2026-05-10 21:13:40 +02:00
Brummel b2d31824a1 floats iter 1.4 fixup: drop intra-doc-links to private hex_u64 (rustdoc baseline preservation) 2026-05-10 14:49:21 +02:00
Brummel 1a4e2f04b7 floats iter 1.4: refresh canonical.rs 'no floats' doc comment 2026-05-10 14:47:43 +02:00
Brummel 7c95a69780 floats iter 1.3: bit-stability tests for Literal::Float 2026-05-10 14:45:34 +02:00
Brummel 93bae2d02a floats iter 1.2 fixup: replace iter-N comment with durable rationale for explicit assertions 2026-05-10 14:44:14 +02:00
Brummel aa5b88e8d4 floats iter 1.2: register Float as a primitive type name 2026-05-10 14:42:23 +02:00
Brummel 93fe2da33c floats iter 1.1 fixup: trim form_a.md FLOAT bullet to match neighbour style 2026-05-10 14:40:16 +02:00
Brummel ec2811194b floats iter 1.1: Literal::Float variant + canonical hex serde + drift-test anchors 2026-05-10 14:36:08 +02:00
Brummel e21f264b93 design-md-consolidation 3.4 nit: drop task-ref doc-comment prefix + align def-kind list with lowercase tags 2026-05-10 12:57:11 +02:00
Brummel 934a6e18e6 design-md-consolidation 3.4 fixup: align DESIGN.md ClassDef/InstanceDef JSON tags with ast.rs lowercase serde rename 2026-05-10 12:53:36 +02:00
Brummel 4651fed5b7 design-md-consolidation 3.4: add design_schema_drift.rs — exhaustive-match drift test for ast.rs vs DESIGN.md §Data-model 2026-05-10 12:51:39 +02:00
Brummel e5d1c1e33b design-md-consolidation 3.3: ast.rs doc-comment names DESIGN.md §Data-model as canonical schema, drift test as enforcement 2026-05-10 12:48:21 +02:00
Brummel 9523a1cd63 iter 22-tidy.4: pin lockstep invariant + tighten module doc 2026-05-10 04:13:41 +02:00
Brummel bbfadb2d72 iter 22-tidy.4: ailang-core::primitives — single home for the primitive-name set 2026-05-10 04:11:29 +02:00
Brummel 6869bd5d0c iter 22b.2.7: fix — cycle termination guard, hoist type_repr 2026-05-09 18:23:52 +02:00
Brummel ae7281ac41 iter 22b.2.7: fix — use examples_dir() helper in superclass-instance test 2026-05-09 18:20:18 +02:00
Brummel a546c5851c iter 22b.2.7: missing-superclass-instance diagnostic 2026-05-09 18:18:12 +02:00
Brummel b252f837af iter 22b.2.6: fix — structural Origin enum, fn-fn collision delegation, test path helper
Address quality review of a9aa248:

- Replace string-prefix discriminator (`prior.starts_with("class ")`)
  with a structural `Origin { Class | Fn }` enum; `kind` is now a
  `match` on variants, formatting is reserved for error construction.
- Fix fn-fn collision misreport: in the `Def::Fn` arm, only fire
  `MethodNameCollision` when the prior origin is a class. Two
  `Def::Fn` with the same name are `CheckError::DuplicateDef`'s job
  in `ailang-check`, not this pre-pass.
- Tests use `examples_dir()` like the rest of the module; path
  construction is no longer ad-hoc.
- Tests assert `first_origin` / `second_origin` so an origin-format
  regression cannot silently break the `kind` discriminator.
- Drop forward-looking 22b.4 prelude reservation note from the
  variant doc — keep only what's load-bearing for 22b.2.
2026-05-09 18:13:49 +02:00
Brummel a9aa248910 iter 22b.2.6: method-name-collision diagnostic 2026-05-09 18:07:52 +02:00
Brummel 1f6d2322c3 iter 22b.2.5: fix — move OverridingNonExistentMethod after UnboundConstraintTypeVar 2026-05-09 18:01:51 +02:00
Brummel 634bd5eca3 iter 22b.2.5: overriding-non-existent-method diagnostic 2026-05-09 17:59:10 +02:00
Brummel 257b47625d iter 22b.2.4: constraint-references-unbound-type-var diagnostic 2026-05-09 17:54:20 +02:00
Brummel 4b84e781d4 iter 22b.2.3: invalid-superclass-param class-schema diagnostic 2026-05-09 17:50:29 +02:00
Brummel 928deb9d74 iter 22b.2.2: kind-mismatch class-schema diagnostic 2026-05-09 17:45:27 +02:00
Brummel 04bde14a9e iter 22b.2.1: fix — revert PartialEq semantic change, drop unused derives 2026-05-09 17:38:20 +02:00
Brummel b636b883a3 iter 22b.2.1: add Constraint struct, Type::Forall.constraints 2026-05-09 17:34:39 +02:00
Brummel 057784934a iter 22b.1.3: registry coherence-check fixtures + tests
Adds seven fixtures under examples/test_22b1_* exercising the
workspace-load coherence checks plus four positive/negative tests in
ailang-core/src/workspace.rs:

- iter22b1_instance_in_class_module_loads_clean: positive case;
  test_22b1_orphan_class.ail.json declares Show + instance Show Int
  in the same module (the class's). Registry has one entry.
- iter22b1_orphan_instance_fires_diagnostic: declares Show in
  test_22b1_orphan_third_classmod, declares instance Show Int in
  test_22b1_orphan_third (a third module). Fires OrphanInstance.
- iter22b1_duplicate_instance_fires_diagnostic: per the JOURNAL hint,
  module A defines class Show + instance Show MyInt (legal, A is
  class's module), module B defines type MyInt + instance Show MyInt
  (legal, B is type's module). Entry imports both. Fires
  DuplicateInstance with first/second_module distinct.
- iter22b1_missing_method_fires_diagnostic: class Eq with two
  non-default methods (eq, ne), instance Eq Int specifies only ne.
  Fires MissingMethod { method: "eq" }.

The surface round-trip gate (ailang-surface/tests/round_trip.rs)
filters out test_22b1_* fixtures because the Form-B parser arms for
ClassDef/InstanceDef are deferred to 22b.4. Without the filter, every
fixture's printed text would re-parse-fail on the unknown `class` /
`instance` head.

Test count: 291 → 295 (4 new workspace tests, all green).
2026-05-09 12:40:06 +02:00
Brummel eb4db9dafc iter 22b.1.2: workspace registry skeleton + coherence checks
Adds the workspace-global typeclass instance registry per Decision 11
"Resolution and monomorphisation". Built at the end of load_workspace
after the DFS over imports completes; keyed by (class-name, type-hash)
where type-hash uses the new canonical::type_hash (16-hex prefix,
parallel to def_hash and module_hash).

Three coherence checks fire from build_registry, each surfacing as a
distinct WorkspaceLoadError variant:

- OrphanInstance: `instance C T` not in C's or T's defining module.
- DuplicateInstance: two entries share the same (class, type-hash) key.
- MissingMethod: instance omits a required (non-default) method.

The CLI's workspace_error_to_diagnostic is extended with the three new
codes (orphan-instance / duplicate-instance / missing-method).

All existing Workspace { ... } construction sites get the new
`registry` field, defaulting to Registry::default() at synthetic /
test-only paths and threading through ws.registry.clone() at codepath
sites that already hold a real workspace.

Includes hash-stability regression tests (iter22b1_schema_extension_*
and iter22b1_classdef_empty_optionals_hash_stable) and the empty-
registry positive test against examples/sum.ail.json. Test count:
288 → 291 (3 new tests, all pass).
2026-05-09 12:37:00 +02:00
Brummel f25d7b6cd6 iter 22b.1.1: ClassDef/InstanceDef AST + downstream match arms
Adds Def::Class(ClassDef) and Def::Instance(InstanceDef) variants per
Decision 11 §"Form-A schema". All optional fields (superclass, doc,
default body) carry skip_serializing_if so canonical-JSON bytes of
pre-22b fixtures stay bit-identical.

Downstream match-on-Def sites are extended with explicit Class/Instance
arms. Behaviour for 22b.1 is placeholder (skip in typecheck/codegen,
one-line summary in pretty/manifest, placeholder marker in prose/print)
with TODO comments naming the deferred sub-iters (22b.2 typecheck,
22b.3 codegen, 22b.4 prose-projection arms).
2026-05-09 12:31:23 +02:00
Brummel 72626fa94f ail: embed Form-A spec in merge-prose prompt (iter 20f)
Closes a design hole shipped in 20d: the merge-prose prompt instructed
the LLM to emit JSON-AST and gave a 12-line schema-essentials reminder.
JSON-AST is the canonical hashable artefact, not a writing surface; the
reminder was not a language spec. Foreign LLMs had no realistic shot.

20f makes two coupled changes:

1) The LLM now emits Form-A (the canonical authoring surface fixed by
   Decision 6). merge-prose loads the original via ailang_core::load_module
   and re-renders via ailang_surface::print before embedding (round-trip
   is a gating contract on the surface crate, so this is lossless). The
   user runs `ail parse foo.new.ailx` to recover JSON, then `ail check`.

2) crates/ailang-core/specs/form_a.md is the complete LLM-targeted
   Form-A specification — grammar, every term/pattern/type/def keyword,
   schema invariants, pitfall catalogue, four few-shot modules from
   examples/*.ailx. Exported as ailang_core::FORM_A_SPEC via include_str!
   and embedded verbatim in every merge-prose prompt.

Drift detection in crates/ailang-core/tests/spec_drift.rs: every variant
of Term, Pattern, Type, Def, Literal is reached via exhaustive `match`.
The arms are not the assertion — adding a new variant without updating
the match is a compile error before the test runs. Once matched, an
anchor string is asserted to appear in FORM_A_SPEC. 8 tests, all green.

The hand-written + mechanical-drift-test combo addresses the user's
"distance to code is too big" concern about a docs-only spec. Generator
overkill rejected: structural drift is mechanically caught, but prose
explanation, schema-invariant catalogue, pitfall list, and few-shot
corpus cannot be emitted from AST shape alone.

Tests:
  - ailang-core: +8 spec_drift tests; existing 12 unchanged
  - ail unit (3): rewritten in lockstep — assert (own T)/(effects IO)
    landmarks, FORM-A SPECIFICATION header, FORM_A_SPEC body verbatim
  - ail e2e merge_prose_prints_framed_prompt: rewritten — assert
    `(module foo` + `FORM-A SPECIFICATION` instead of `ailang/v0`
  - Workspace: all green

Richer integration paths (LLM tool-use, MCP server, LSP) were named
in the design discussion and deferred per "kiss". All three layer
additively on the static-prompt path; static prompt remains the
lowest-common-denominator fallback.
2026-05-08 23:31:27 +02:00
Brummel 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).
2026-05-08 19:36:04 +02:00
Brummel 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.
2026-05-08 12:53:09 +02:00
Brummel 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.
2026-05-08 10:55:31 +02:00
Brummel 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
2026-05-08 02:26:12 +02:00
Brummel 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"]
2026-05-08 02:07:07 +02:00