7ae92d3e6099187703f6a22a5e8c72e251facbd2
25 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7ae92d3e60 |
refactor(test): hoist duplicated test helpers into ailang-test-support
closes #60 The fixture-corpus filter (NON_PARSEABLE_FIXTURES + list_ail_fixtures) was copy-pasted across three test crates and drifted out of sync as new reject fixtures landed; the examples_dir / workspace_root path walk was reimplemented ~30 more times across the suite, under two spellings (workspace_root / ws_root). Introduce crates/ailang-test-support — a zero-dependency, dev-only leaf crate — as the single home for these helpers: - NON_PARSEABLE_FIXTURES, list_ail_fixtures - workspace_root, examples_dir - canonical_workspace_root (symlink-resolved variant, for the tsan/ race tests that feed the root into native build steps) All integration-test copies across ailang-core, ailang-surface, ailang-prose, ailang-check, and ail now import from the shared crate; the local definitions and their now-orphaned path imports are removed. Path helpers resolve via the support crate's own CARGO_MANIFEST_DIR, so they return the correct absolute paths from any caller. ail_bin helpers are intentionally left in place: they depend on env!("CARGO_BIN_EXE_ail"), which Cargo only defines when compiling the ail crate's own integration tests, so they cannot move to a shared crate. Behaviour-identical: same paths, same fixture lists; full workspace test suite green. Net -177 LOC. |
||
|
|
76b21c00eb |
feat(lang): eliminate the Implicit ownership default — totality + the drop-soundness it demasks (#55)
Deletes `ParamMode::Implicit`. `ParamMode` is now `{Own, Borrow}`:
every fn-type slot on every signature carries an explicit `own` or
`borrow`, no defaulted position survives anywhere (model 0008 §2,
spec 0062). The parser rejects a bare fn-type slot; `borrow-return`
and `borrow-over-value` reject at the signature; the corpus is
migrated to minimal-ownership modes (consumed ⇒ own, read-only-heap
⇒ borrow, value ⇒ trivial-own). The documented `Implicit`-ret-mode
leak is fixed: an owned heap return now drops exactly once (live=0,
acceptance criterion 5).
This was the easy half. Removing the default ACTIVATED a family of
drop paths that `Implicit` had silently skipped — the pre-cutover
language was leaking (and in places mis-dropping) here rather than
crashing, because an Implicit scrutinee turned the drop off. Making
the modes explicit (Own) turned those paths on and exposed two
latent-bug clusters, all fixed RED-first as part of this cutover:
Drop-soundness family (four legs):
A. lit-sub-pattern double-free — the desugar re-matched the same
owned scrutinee in the lit fall-through; fixed by grouping
consecutive same-ctor arms into one match (bind fields once),
in ailang-core desugar.
B. Cons-husk leak on non-tail arm bodies — the lit-sub-pattern
desugar rebound the owned scrutinee via `Let $mp = xs`, which
bumped consume_count and suppressed the existing fn-return
partial_drop. Fixed by not rebinding a bare-Var scrutinee
(one husk-freeing mechanism, not two).
C. polymorphic `drop_<T>` rc_dec'd monomorphised value fields —
the per-ADT drop fn was emitted once from the polymorphic
TypeDef, defaulting type-var fields to ptr and rc_dec'ing
inline Ints (segfault). Fixed with per-monomorph drop
functions (new ailang-codegen::dropmono): the drop set is
collected from the lowered MIR, value-type fields are skipped,
heap fields still freed once; monomorphic-concrete ADTs keep
their byte-identical un-suffixed drop symbol.
D. static Str literal passed to an `(own Str)` param — the
literal lowers to a header-less rodata constant; the callee's
now-active rc_dec read its length field as a refcount and
freed a static address (segfault). Fixed with the missing
fourth StrRep::Static→Heap promotion in lower_to_mir's App arm,
gated on Own mode (borrow args stay static, no regression).
over-strict-mode lint over-fired: it suggested `(borrow V)` for
value-typed params (which `borrow-over-value` rejects — own is the
only legal mode there) and fired on `(intrinsic)` bodies (whose
consumption the linearity walk cannot observe). Tightened to skip
both; contract 0008 updated to the narrowed firing scope.
Irreversible step — canonical-form hash reset (model 0008 §6,
acceptance criterion 6). Every signature now carries explicit modes,
so the hashable canonical JSON changed for every module. RATIFY:
the corpus-wide hash-pin reset (hash_pin, prelude_module_hash_pin,
mono_hash_stability, eq_ord_e2e, embed_export_hash_stable, the
ct4/iter*/loop_recur schema-extension pins) and the list ir_snapshot
golden were regenerated once, deliberately, as the intended one-time
consequence of removing the mode elision from the canonical form —
not a regression. Each regenerated hash verified deterministic across
two runs.
Also fixes a pre-existing latent failure surfaced by the verification
gate, unrelated to this cutover: the `every_contract_names_a_resolvable_
ratifying_test` resolver (design_index_pin) could not resolve the
" + " dual-link ratifying-test form (`uniqueness.rs + linearity.rs`)
that the #57 audit-close (
|
||
|
|
2769e50a35 |
fix(examples): destructure partition_eithers result once (#57, 0064 iter 4)
Final iteration of spec 0064 (the #55-cutover hardening, #57). Closes class 4, the one genuine corpus bug among the four (not a false positive): std_either_list.partition_eithers projected both (app Pair.fst rest) and (app Pair.snd rest) from one owned `rest`. Pair.fst/snd are own-param projections that move a field out, so each consumes `rest` -> a genuine double-consume that universal linearity activation (#55) would correctly reject. The fix is a body rewrite (destructure `rest` once via (match rest (case (pat-ctor MkPair ls rs) …)) then dispatch on the head), NOT a check change -- the check is right. Latent until activation: partition_eithers' param is a bare (Implicit) slot today, so the check is skipped on it -- no RED->GREEN flip on the real fixture. The rewrite is a pre-emptive corpus fix. Its correctness rests on the unchanged 2 3 2 3 E2E output (std_either_list_demo, a semantically-identical partition) and on the new c4_double_consume must-stay-RED fixture, which -- with explicit modes, so the check is active today -- demonstrates the double-projection shape IS rejected. - examples/std_either_list.ail: partition_eithers Cons arm rewritten; doc updated (destructure-once, no longer "projections"). - examples/c4_double_consume.ail: must-stay-RED pin (the rejected shape), folded into a generalised harden_ownership_heap_double_consume_still_errors loop over {real_consume, c4_double_consume}. - examples/c4_rewrite.ail: stays-clean pin (the accepted destructure-once shape), added to harden_ownership_false_positives_are_clean. No hash-pin on std_either_list (verified); the e2e 2 3 2 3 output pin is the content pin and is preserved. No check/schema/codegen change. Verified: std_either_list checks clean; demo 2 3 2 3; both harden assertions green; cargo test --workspace green; bench/check.py 34/34, compile_check.py 24/24 stable. This is the last of the four #57 hardening classes. Spec 0064 is now code-complete: classes 1 (local fn-param modes), 2 (let-alias redirect), 3 (value-typed let-binder), and 4 (this) all shipped. The cycle is ready for audit; the #55 cutover precondition is met. refs #57 |
||
|
|
be259f9d46 |
feat(check): let-alias borrow propagation via alias-redirect (#57, 0064 iter 3)
Third iteration of spec 0064 (the #55-cutover hardening, #57). Closes false-positive class 2: Term::Let walked its value in Position::Consume, so (let a t (match a …)) over a borrow-mode t consumed t at the binding and tripped consume-while-borrowed -- a let-alias of a borrowed value read in a borrow position is not a consume. Fix: the Checker gains a scoped aliases: HashMap<String,String>. A (let a t body) whose value is a bare Var resolving to a tracked binder records a -> root(t) for the body scope and skips the consume walk of the value. A new resolve_alias helper maps a name to its root, preferring a real binder at each chain step -- so an inner binder named `a` (nested let, pattern binder, lam param) automatically shadows the alias with NO change to with_binder/walk_arm/Lam (the shadowing is resolved centrally in the resolver, not at three scattered introduction sites). Every binder-state lookup keyed by name resolves through the map first: use_var, the App borrow bump + decrement (pushing the resolved root to `lent` so the decrement matches), callee_arg_modes, and the reuse-as source. Design calls (orchestrator): - Alias REDIRECT, not state clone (spec scope decision 3): consuming the alias marks the single root consumed, so a real double-consume through an alias is still caught. A clone would track two independent binders and miss it (unsound). Pinned by the new let_alias_of_owned_double_consume_still_errors unit test: (let a p0 (seq a p0)) over an own heap p0 still fires use-after-consume. - resolve_alias prefers binders -> shadowing needs no edits to the binder-introduction sites. Lower-risk than clearing aliases at each. - reuse-as source (linearity.rs:663) is resolved through aliases too: it is the 4th binder-state-reading site; the spec's Fix 3 named three illustratively. Without it, (reuse-as <alias> …) would mis-fire reuse-as-source-not-bare-var on what IS a Var. Faithful completion of Fix 3. The diagnostic now reports the resolved root name (the actual consumed resource), not the surface alias. Closes the design/contracts/0008-memory-model.md let-alias carve-out (was "has not shipped yet") and extends its Ratified-by trailer to name linearity.rs, where the propagation now lives -- a contract change riding with the feature that forces it. RED-first: examples/c2_let_alias.ail added to harden_ownership_false_positives_are_clean (RED: consume-while-borrowed on count's t); GREEN after. Verified: c2 RED->GREEN; 120 linearity unit tests green; the soundness test fires; harden false-positive + heap double-consume guards green; cargo test --workspace green; bench/check.py 34/34, compile_check.py 24/24 stable. Diagnostic-only; no schema/type/codegen change. Class 4 (partition_eithers body rewrite) remains for the final iteration of spec 0064. refs #57 |
||
|
|
cc5991eeb3 |
feat(check): resolve local function-typed binder modes (#57, 0064 iter 2)
Second iteration of spec 0064 (the #55-cutover hardening, #57). Closes false-positive class 1: applying a local function-typed binder -- a HOF predicate param like std_list.filter's `p` -- treated its arguments as Consume because callee_arg_modes resolved only the global symbol table (locals returned an empty mode vec). So `(app p h)` with `p` a local (borrow ...) predicate consumed the heap element `h`, and reusing `h` in the kept Cons false-fired use-after-consume. Fix: BinderState gains fn_param_modes: Option<Vec<ParamMode>>, seeded at all three function-typed binder introduction sites via a new fn_modes_of helper (strip_forall + Type::Fn { param_modes }, None for non-fn / empty-modes so the caller falls through to globals): params and lam-params from their signature type (param_tys), let-binders from the LetBinderTypes table built in iter 1. callee_arg_modes reads a local Var callee's fn_param_modes before the global lookup (a local binder shadows a global -- correct lexical scope); the existing App arg-walk maps a Borrow mode to a borrow walk unchanged, so the fix is confined to the seeding + the one local-precedence read. Scope call (orchestrator): all three seeding sites, not just the param path that unblocks filter -- the point of this hardening cycle is to leave no latent class (the #57-from-0063-deferral lesson), and the extraction is uniform with the table already built. RED-first: examples/c1_local_hof.ail added to harden_ownership_false_positives_are_clean (RED: use-after-consume on filter_box's h) before the fix; GREEN after. Two in-source unit tests pin both seeding paths (param via signature, let via a hand-built table). Verified: c1 RED->GREEN; 119 linearity unit tests green; harden_ownership_false_positives_are_clean green (now c1 + c3); the heap-double-consume must-stay-RED guard still fires; cargo test --workspace green; bench/check.py 34/34, compile_check.py 24/24 stable. Diagnostic-only; no schema/type/codegen change. The stale `// Locals never carry fn-types in 18c.2` comment in callee_arg_modes (and the fn's doc header) is rewritten to the accurate local-precedence behaviour -- it sat in the replaced lines and directly contradicted the new path. Classes 2 (let-alias redirect) and 4 (partition_eithers rewrite) remain for later iterations of spec 0064. refs #57 |
||
|
|
47964abf7c |
feat(check): tee let-binder types, exempt value-typed let-binders (#57, 0064 iter 1)
First iteration of spec 0064 (the #55-cutover hardening, #57). Closes
false-positive class 3: a value-typed `let`-binder (e.g. a `Float`
result bound and read more than once) tripped `use-after-consume`
because the linearity walk installed every `let`-binder as a default
heap-tracked BinderState. Spec 0063 deferred exactly this class
("value-typed let-binders ... deferred until a corpus shape demands
it"); eqord_3_newton_sqrt.iterate's `(let xnew (app / …) …)` is that
shape.
Mechanism (the design call, user-delegated): the let-binder's type is
ALREADY computed at synth's Let arm and discarded. Stop discarding it.
synth gains a (def,binder)->Type out-param (LetBinderTypes, defined in
linearity.rs); the Let arm records `subst.apply(&v)`. The table is
allocated per-module in check_workspace, threaded through
check_in_workspace -> check_def -> check_fn/check_const/check_instance
-> synth, and passed (immutable) into check_module_with_visible ->
Checker. linearity's Term::Let seeds BinderState.is_value from the
table via the existing type_is_value predicate -- the same per-binder
flag #56 seeds at param/pattern/lam sites, now extended to the let
site. No schema change, no hash shift, no codegen change; the check
stays diagnostic-only.
Not a re-run of inference in the walk (ruled out by
|
||
|
|
aaa70d4c35 |
feat(check): harden the ownership analysis for universal activation (0063)
The strict linearity check (use-after-consume / consume-while-borrowed, linearity.rs) runs today only on the ~45 of 258 all-explicit-mode fns; its activation gate skips any fn with a bare/Implicit param. Deleting ParamMode::Implicit (#55) would turn it on universally and surface ~21% false positives in two classes. This closes both, making the core ownership analysis sharp across the whole codebase for the first time — the precondition for #55. Diagnostic-only: no schema, no hash, no codegen change; the two existing diagnostic codes simply fire on a smaller, more correct set. Fix 1 — value-type exemption. A new is_value_type predicate (ailang-core::primitives) names the unboxed set {Int,Bool,Float,Unit}; BinderState gains an is_value flag seeded from the binder's locally available type (param signatures, ctor field types for pattern binders, lam typed-params), and use_var short-circuits the Consume arm for it. A value type has no refcount and is never consumed, so multi-read is always legal. Str is deliberately NOT in the value set, though is_primitive_name includes it: drop.rs:490-492 lowers only Int/Bool/Float/Unit to non-ptr; Str is a ptr, RC-dec'd, so a multi-consume of Str without clone is a real use-after-free. is_value_type is the Str-excluding subset of is_primitive_name, pinned by a unit test. is_heap_type and the over-strict-mode lint are left untouched (separate concern). Fix 2 — application is a borrow. Term::App walks its callee in Position::Borrow (was Consume). Applying a function value reads it; it stays live for further applications. Global fn-refs are untracked (no-op); a tracked function-typed binder (a HOF param) is no longer consumed by application. This fixes both the own-f-param use-after-consume and the borrow-f-param consume-while-borrowed in the recursion-passing HOF shape (map_int f t). Passing a function value as an arg is unchanged — it follows the callee param_modes. Scope decision: value-typed let-binders stay conservatively tracked as heap (their type is inferred, not annotated, and the linearity walk does not re-run inference). This is soundness-safe — over-strict, never unsound — and not a measured corpus shape; lifting it would need a full binder->type table out of the type-checker. Verification: all three false-positive classes reproduced today against explicit-mode fns and shipped as RED->GREEN fixtures (examples/fp_{value,hof,map}.ail); examples/real_consume.ail stays RED (heap double-consume still fires) to prove the exemption is type-gated. 715 workspace tests green; bench/check.py 0/34 and bench/compile_check.py 0/24 regressed. merge_states carries is_value so the flag survives branch merges. RED-first verified per task. closes #56 |
||
|
|
a92bcaf969 |
fix(check): qualify cross-module kernel type-cons in user ADT fields
A consumer module's ADT field could reference a kernel-tier auto-imported type by its bare name in op/value positions (`(new RawBuf ...)`, `RawBuf.get`) but NOT in the type-constructor position of the field declaration: `(con RawBuf (con Int))` stayed the unqualified `RawBuf<Int>` and failed to unify with the constructor argument's `raw_buf.RawBuf<Int>`, so a RawBuf could not be stored in a user ADT field without spelling the qualified `raw_buf.RawBuf`. `qualify_workspace_module` skipped `Def::Type` entirely, and the `Term::Ctor` arm only qualified field types of cross-module *owning* types — a *local* type carrying a cross-module field was the uncovered case. The fix qualifies each ctor field type in the `Def::Type` arm via the existing `qualify_workspace_types`, which upgrades only genuinely cross-module type-cons (skips `own_local_types` and primitives), so a purely-local field is never spuriously rewritten. This is a check-side workspace transform, not the on-disk canonical form, so no module hash drifts (both hash-pin tests stay green). RED test `rawbuf_in_user_adt_field_resolves_bare_name` in crates/ailang-check/tests/workspace.rs, with the bare-name fixture (now checks/builds/runs -> 42, live=0, the ADT drop cascade frees the buffer slab) and the qualified control twin. This is the Series-substrate shape (a RawBuf wrapped in a user ADT), so the inconsistency would have bitten the pending series milestone. closes #50 |
||
|
|
744ad41e47 |
fix(check): resolve type-scoped borrow-receiver ops in linearity (closes #46)
A function whose parameter is `borrow (RawBuf a)` and which calls RawBuf.get or RawBuf.size on that parameter even once was rejected at `ail check` with [consume-while-borrowed] — the exact borrow-receiver use the ledger advertises for get/size. The diagnostic also misnamed the cause: the receiver is read, not consumed. Root cause: the linearity walk's callee_arg_modes looked up the callee under its spelled name. A type-scoped polymorphic op is spelled `RawBuf.get` at the call site, but check_module_with_visible registers the kernel raw_buf ops under their bare def names (get/size). The lookup missed, the receiver arg defaulted to Consume, and consuming a binder whose borrow_count==1 (the Borrow param) fired the false positive. The kernel signatures themselves are correct (receiver slot is `borrow`); the only defect was the name-resolution gap in the linearity globals lookup. Fix: on a direct-lookup miss, fall back to the bare method name via the existing parse_method_qualifier + qualifier_is_class_shape resolvers, gated on a class/type-shaped qualifier so lowercase cross-module-fn qualifiers (std_list.length) are excluded. This is how the rest of the checker already treats type-scoped polymorphic ops; the fix is general, not RawBuf-specific. This unblocks the natural borrow-helper decomposition (a shared read-only buffer behind a helper fn) — the shape the downstream series milestone's Series.at / Series.total_count need. Surfaced by the raw-buf fieldtest (finding B1, docs/specs/0058). RED-first: rawbuf_borrow_receiver_read_is_linearity_clean. |
||
|
|
9339279181 |
iter prep.3-kernel-tier-modules (DONE 9/9): kernel-tier modules + param-in + stub crate — closes #33
Terminal iteration of the kernel-extension-mechanics milestone. Ships
the four language-level mechanisms named in the spec's § Goal:
Module.kernel + TypeDef.param-in schema, their Form-A surface,
flag-driven kernel-tier auto-injection, and generic param-in checker
enforcement with a new diagnostic.
Schema (Tasks 1+2). Module gains a `kernel: bool` field
(skip_serializing_if = is_false), TypeDef gains a
`param_in: BTreeMap<String, BTreeSet<String>>` field
(skip-if-empty, kebab-renamed to "param-in"). Both fields are
strictly additive — every pre-existing fixture's canonical-JSON hash
is bit-stable except `prelude.ail`, which intentionally gains
`(kernel)`. The struct-literal sweep covered ~104 Module sites and
~35 TypeDef sites across the workspace; the additive serde-default
covers JSON deserialise paths, only Rust struct literals broke.
Form-A surface (Tasks 3+4). `(kernel)` is a bare module-header
attribute; `(param-in (a Int Float) (b Str))` is one outer
TypeDef-body clause carrying one or more inner var-lists (OQ1
decision — mirrors `(ctors …)`, one parser arm, deterministic
BTreeMap iteration). Both round-trip Form-A → JSON → Form-A
bit-identical.
Workspace-load migration (Task 5). The hardcoded `&["prelude"]`
literal at loader.rs:108 became a `modules.values().filter(|m|
m.kernel)` derivation; `parse_prelude()` injection stays because
the prelude has no on-disk manifest in user workspaces. Prelude
now carries `(kernel)` in its source, so the new filter picks it
up automatically. Code-path migration only — observable behaviour
is identical (prelude_free_fns.rs stays green). prelude hash
re-pinned (af372f28c726f29f) with Honesty-Rule provenance comment.
WorkspaceLoadError::ReservedModuleName diagnostic prose
repurposed: any built-in kernel module name is reserved
(currently prelude + kernel_stub), not specifically prelude. CLI
mapping at main.rs updated in lockstep.
Stub crate (Task 6). New `crates/ailang-kernel-stub/` is a
zero-dependency leaf crate carrying only `pub const STUB_AIL:
&str` with the Form-A source of the kernel_stub module (one
parametric TypeDef with param-in, one ctor). The parse hop —
`parse_kernel_stub()` — lives in ailang-surface next to
parse_prelude, keeping the crate-dependency graph acyclic
(`ailang-surface → ailang-kernel-stub → ailang-core`, no
back-edge). The stub is injected unconditionally in all builds as
the ratifying fixture for the kernel-extension mechanism; future
base extensions may add more or retire the stub. Drift-pinned by
`kernel_stub_module_round_trips`.
Checker (Task 7). New `CheckError::ParamNotInRestrictedSet`
variant + code() + ctx() arms + enforcement in
`check_type_well_formed`'s Type::Con arm — generic, data-driven
from the TypeDef, mentions no specific extension type. Two
in-source tests pin both the rejection (`Str` outside `{Int,
Float}`) and the acceptance (`Int` inside) paths.
Workspace-load integration tests (Task 8). New
`workspace_kernel.rs` integration-test crate with three tests:
auto-import without explicit `(import …)` declaration, two
kernel-tier modules co-load, explicit-import-overrides-auto-
import precedence preserved. Loader is import-tree-only so the
auto-import tests use a bridge module that brings the kernel
module into the workspace via the import graph — docstring
captures the reachability nuance for future readers.
Doc-state transitions (Task 9). INDEX.md kernel-extensions row
annotation transitions from "design accepted 2026-05-28; impl in
progress" to "mechanisms milestone closed 2026-05-28; raw-buf and
series milestones pending". Whitepaper STATUS + auto-import +
param-in sections transitioned forward→present for shipped
mechanisms; forward-tense survives only in sections describing
the still-pending raw-buf/series milestones (per Honesty-Rule).
data-model contract gains anchor blocks for both new schema
fields.
Side-effect: every binary's IR snapshot now contains ~52 lines
for `drop_kernel_stub_StubT` because the stub is auto-injected
into every workspace load. Snapshots refreshed; e2e expects 4
modules per workspace (prelude + kernel_stub + entry + zero or
more user modules) instead of the previous 3.
Plan defects scrubbed in the implementation (folded back into
the planner template via the planner's self-review checklist
next time): Task 4 sample test src used fictional
`(ctors (MkT a))` list form (project grammar is per-`(ctor MkT
a)`); Task 6 original wiring would have created a cycle
ailang-surface → ailang-kernel-stub → ailang-surface (inverted —
stub crate is zero-dep, parse hop lives in surface); Task 7 in-
source tests referenced a fictional `check_type_in_module`
helper (used the existing Workspace + check_workspace
convention); Task 8 first integration test expected loader to
auto-load kernel modules from disk (loader is import-tree-only;
tests use a bridge module).
Concern-5 fix folded in pre-commit: workspace.rs ReservedModuleName
doc-prose initially said "in test/dev builds" for kernel_stub —
but stub is unconditionally injected in all builds. Doc copy
tightened to present-state per Honesty-Rule.
Stats: 0 spec-review-loops, 0 quality-review-loops, 2 sweep-script
retries on Task 2 (brace-depth bug on nested vec![Ctor{…}],
recovered via per-file checkout + rewritten anchor-on-existing-
field sweep), 1 e2e-snapshot refresh on Task 6.
|
||
|
|
b586999e81 |
iter prep.1-type-scoped-namespacing (DONE 5/5): TypeDef-first resolution + workspace pre-pass — closes #31
First iteration of the kernel-extension-mechanics milestone. Ships
the type-scoped `<TypeName>.<member>` resolution path as the
canonical form for type-associated operations, narrows the
`BareCrossModuleTypeRef` / `BadCrossModuleTypeRef` diagnostics from
"bare = strictly local" to "bare = in-scope by any path", migrates
12 std-library example fixtures, and introduces a workspace-wide
normalisation pre-pass `prepare_workspace_for_check` shared between
`check_workspace` and `monomorphise_workspace`.
Architectural discovery during implementation: the plan covered the
`Term::Var` dot-qualified resolver layer plus the workspace
validator's bare-name acceptance, but the migration of bare-form
fixtures exposed five sites where bare vs. qualified type-names
needed symmetric treatment — `Term::Ctor` resolution, `Type::Con`
well-formedness, mono's poly-free-fn name/constraint-count
enumeration, codegen's `lookup_ctor_by_type` bare-name path, and
the upstream desugar-then-qualify composition. Rather than
scattering TypeDef-first ladders across each site, the implementer
centralised the work into one pre-pass that walks every consumer
module's `Type::Con.name` and `Term::Ctor.type_name`, rewriting
bare cross-module references to their qualified `<home>.<Type>`
form. This is symmetric to the pre-existing `qualify_local_types`
(owner-side); the new pre-pass is the consumer-side mirror.
Downstream passes see qualified Types regardless of authoring form.
The TypeDef-first ladder still lives in `synth`'s `Term::Var` arm
because `<TypeName>.<member>` is term-position-only — `Maybe.from_maybe`
is a Var, not a Type expression, and the pre-pass does not rewrite
Var names.
Alternatives considered:
(a) Add TypeDef-first ladder at every resolution site separately
(the plan's implicit assumption). Rejected: O(N) extension
sites, each carrying the same workspace-walking logic; the
pre-pass version is O(1) — one pass, every downstream consumer
benefits.
(b) BLOCKED + spec re-brainstorm. Rejected: the architecture
extension is consistent with prep.1's thesis (bare type-name
resolves to the workspace-wide TypeDef) and forward-compatible
with prep.2 (Term::New.type_name falls under the same rewrite)
and prep.3 (kernel-tier TypeDefs enter the workspace map
automatically). No design regression to bounce back over.
Spec updated to document the realisation mechanism honestly: the
"Realisation mechanism — workspace pre-pass" subsection clarifies
that the resolver-level semantics described in "Implementation
shape" are the user-facing contract, and the actual code path is
the pre-pass.
Verification:
- `cargo test --workspace`: ALL GREEN. 87 e2e + every crate's unit
+ integration tests pass with no regressions.
- Three NEW in-source tests pin Task 1's resolver paths:
`type_scoped_member_resolves`, `type_scoped_member_not_found`,
`type_scoped_receiver_not_a_type`.
- One NEW workspace test pins the narrowed validator:
`ct1_validator_accepts_bare_with_explicit_import`.
- One renamed-and-flipped existing test:
`ct1_validator_rejects_bare_xmod_with_import_candidate` →
`ct1_validator_accepts_bare_xmod_with_import_candidate` (the
bare-with-import path is now ACCEPTED).
- One NEW companion test for the workspace-wide ctor lookup:
`ct2_term_ctor_bare_cross_module_via_workspace_resolves`.
- Two pre-existing tests' assertions updated for the new error
wording: `ct1_check_cli::check_human_mode_emits_actionable_message_to_stderr`
and `crates/ailang-check/tests/workspace.rs::unknown_module_prefix_is_reported`.
- 12 migrated `.ail` fixtures verified via the existing e2e
suite (each fixture is the test runner's target for an existing
`build_and_run` assertion).
- Negative fixture `ct_2_bare_cross_module.ail` semantically
preserved: dropped its `(import std_maybe)` so bare `Maybe` is
out-of-scope under the narrowed rule and still fires
`BareCrossModuleTypeRef`.
Concerns:
- The pre-pass introduces a new architectural layer (consumer-side
qualification) that the spec did not originally anticipate. Spec
amendment in this commit documents the layer. Future iterations
reference `prepare_workspace_for_check` as established
infrastructure.
- `examples/test_ct1_bare_xmod_rejected.ail.json` switched its
offending name from bare `Ordering` (which under the prep.1
semantics may now resolve via implicit prelude) to a still-
unresolvable `Mystery_Type`. The CLI test's intent (assert that
a human-mode `ail check` exits non-zero on a still-RED case) is
preserved.
Milestone status: kernel-extension-mechanics (Gitea #6) advances
1/3 iters. Next: prep.2 (`Term::New` construct) issue #32.
|
||
|
|
ac4d545570 |
source: scrub iter-code / Decision-N residue from inline comments
Follow-up to
|
||
|
|
818177d835 |
iter embedding-abi-m1.1 (PARTIAL 3/7): schema + surface + baseline prerequisites; plan Repair
Tasks 1-3 of docs/plans/embedding-abi-m1.1.md, a verified known-good subset (cargo build --workspace exit 0; full workspace green; the roundtrip_cli all-examples gate green): - Task 1: CLI build-path MissingEntryMain baseline pin (examples/embed_noentry_baseline.ail + embed_missing_main_baseline.rs; triple-assertion, layered on the green codegen-unit pin lib.rs:3314). - Task 2: additive FnDef.export: Option<String> (serde default + skip_serializing_if, modelled on FnDef.doc); compile-driven thread of export: None across ~85 FnDef/AstFnDef literals / 18 files incl. all in-source #[cfg(test)] mod tests + drift/hash/spec-drift literals + the codegen lib.rs:3320 in-source pin; hash-stability golden pin; DESIGN.md fn-JSON "export" line. DONE_WITH_CONCERNS (plan symbol content_hash_fn -> def_hash per hash_pin.rs, plan- pseudo-vs-reality class, plan corrected). - Task 3: Form-A (export "<sym>") modifier — parse_export + parse_fn thread + write_fn_def emission + form_a.md grammar; byte-identical round-trip. Task 4 BLOCKED (Boss plan-defect: fixtures declared module != file stem; AILang's loader hard-enforces module==stem at workspace.rs:438, so ail check panicked on load before the gate). Boss resolution (Option 2, overriding the orchestrator's Option 1, rationale in the journal + plan Design-decision 6): keep descriptive embed_* filenames, rename fixture MODULES to their stems — the C symbol backtest_step stays via (export ...), demonstrating spec Decision 2's mangling- decoupling rather than violating it; archive becomes libembed_backtest_step.a, internal symbol @ail_embed_backtest_step_step, host.c unchanged. Plan fixed (Design-decision 6 + Task-4 module==stem + Task-5/6/7 dependent strings + the no-*. Float-head + content_hash_fn concerns folded). Headline fixture corrected; blocked-Task-4 WIP discarded (recreated by the [4,7] re-dispatch from the fixed plan). PARTIAL commit (not the iter's final commit): the implement Repair path mandates committing the known-good subset so the [4,7] re-dispatch starts from a clean tree that includes Tasks 1-3 (Tasks 4-7 structurally depend on the schema field + surface). INDEX line added when the full iter lands post-re-dispatch. |
||
|
|
77b28ad64d |
iter form-a.1 (Tasks 1-5): additive phase + relocation
First half of the form-a-default-authoring milestone-close iter (Boss-decided strategy C, big-bang). All five tasks DONE; cargo test --workspace green at every per-task boundary. T1 — Add three new tests: - parse_is_deterministic_over_every_ail_fixture (round_trip.rs) - cli_parse_then_render_then_parse_is_idempotent (roundtrip_cli.rs) - carve_out_inventory.rs (new file; #[ignore]'d until T8 deletion) T2 — Bulk-render the 99 missing examples/<stem>.ail files via `ail render`. Corpus 58 .ail (pre-iter) -> 157 .ail. Eight .ail.json carve-outs (7 §C4(a) subject-matter + 1 §C4(b) prelude) preserved. One re-loop triggered: load_workspace prefers .ail siblings since ext-cli.1, so the newly-rendered imports broke seven Group-A entries whose JSON entry-paths now resolved imports to fresh .ail. Repair: pre-emptive forward-pull of five T3 migrations + 4 transient #[ignore]s on workspace.rs mod tests (cleanly relocated in T5). T3 — 14 Group-A test files migrated from ailang_core::load_workspace to ailang_surface::load_workspace + .ail paths. Carve-out lines preserved verbatim (7 sites in typeclass_22b2.rs / typeclass_22b3.rs). T4 — 12 Group-B test files: bulk regex flip on build_and_run / build_and_run_with_alloc / build_and_run_with_rc_stats call sites (~70 e2e.rs invocations + 11 subprocess sites). Four files mis-classified Group-A as Group-B in plan recon (mono_hash_stability, prelude_free_fns, print_mono_body_shape, show_mono_synthesis); two files mis-classified Group-B as Group-A (mono_recursive_fn, mono_xmod_qualified_ref). Migrated per actual shape, not plan label. T5 — Relocated #[cfg(test)] mod tests from production source to integration test crates with ailang-surface dev-dependency: - crates/ailang-core/tests/hash_pin.rs (10 tests from hash.rs) - crates/ailang-core/tests/workspace_pin.rs (10 non-carve-out tests from workspace.rs) - crates/ailang-codegen/tests/eq_primitives_pin.rs (3 tests from codegen/src/lib.rs:3717-3799) - ailang-prose/tests/snapshot.rs migrated (helper + 8 fixtures) to .ail + ailang_surface::load_module Carve-out tests in workspace.rs mod tests preserved in-place (3× 22b2 + 3× ct1 = 6 tests). Tempdir-based loader-mechanism tests (3 sites) also preserved — they don't consume examples/. Tests: 560 passed, 0 failed, 4 ignored (was 558 + 3 T1 new - 1 transit carve_out_inventory #[ignore] = 560 active). Tasks 6-12 (bench-driver suffix flips, e2e diff-test rewrite + 4 additional raw-JSON-inspect handlers, .ail.json deletion, retiring obsolete roundtrip tests + schema_coverage corpus flip, §C3 DESIGN.md restatement, §A4 doctrine edits, WhatsNew + roadmap strike) ship in the next dispatch on this iter ID. Known debt inherited to T6-12: 4 raw-JSON-inspect tests in e2e.rs (borrow_own_demo / reuse_as_demo / render_parse_round_trip_canonical / ail_run_accepts_ail_source_with_same_stdout_as_ail_json dual-form smoke) need rewrite or #[ignore] before T8 deletion; recorded in journal Concerns + Known debt sections. |
||
|
|
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).
|
||
|
|
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. |
||
|
|
73545ab086 |
Iter 18d.2: codegen reuse-as in-place rewrite under --alloc=rc
Lowers Term::ReuseAs { source, body=Term::Ctor } under
--alloc=rc to a runtime refcount-1 dispatch. At the reuse-as
site:
%hdr_ptr = getelementptr i8, ptr %src, i64 -8
%refcnt = load i64, ptr %hdr_ptr
%is_one = icmp eq i64 %refcnt, 1
br i1 %is_one, label %reuse, label %fresh
The reuse arm overwrites the source's box in place: stores the
new tag at offset 0 and the new field values at offsets 8, 16,
... — no malloc, no outer drop. The fresh arm allocates a brand-
new box via ailang_rc_alloc, stores tag and fields, then shallow-
dec's the source. Both arms phi-join to the same `ptr` result.
Other allocators (gc, bump) keep 18d.1's identity behaviour
unchanged.
Static shape compatibility is enforced by a new pre-codegen
pass crates/ailang-check/src/reuse_shape.rs. It tracks the
path-resolved ctor of every let-bound and pattern-matched
binder, then checks each Term::ReuseAs site against the body's
ctor. Mismatches emit reuse-as-shape-mismatch with stable
ctx.reason sub-codes (field-count-mismatch, field-type-mismatch,
indeterminate-source-ctor, cross-module-body-ctor, ctor-not-in-
module) and a drop-the-wrapper SuggestedRewrite. Build fails on
mismatch — the structured diagnostic tells the author whether
to fix the shapes or remove the hint.
Field-cleanup design — deliberate scope cut:
The reuse arm does NOT dec the old pointer-typed field values
before overwriting them. Doing so on the canonical fixture
`(reuse-as xs (Cons (+h 1) (map_inc t)))` causes use-after-free:
the recursive map_inc(t) returns t's in-place-rewritten box, so
the "new tail" being stored is the same pointer as the "old
tail". A dec-old-then-store-new schedule would dec the box (to
zero, free), then store the freed pointer.
The root cause is upstream: 18c.4 doesn't yet null-out pattern-
moved field slots, so codegen can't statically distinguish
"old field still owned" from "already moved out". The chosen
trade-off: skip the field dec entirely; pattern-wildcarded
fields (rare in shipping fixtures) leak; pattern-moved fields
are the body's responsibility (consistent with the 18c.4 leak
baseline — Own params still leak today). 18d.2 ships zero
regression vs 18c.4, plus the perf win on the alloc + outer
cascade. Closing the leak is the move-aware-pattern story
queued for 18d.3 / 18e.
Tests:
- e2e reuse_as_demo_under_rc_uses_inplace_rewrite — runs
examples/reuse_as_demo.ail.json under --alloc=rc, asserts
stdout 9 and locks in the IR shape (icmp eq i64 ..., 1; the
reuse arm has no @ailang_rc_alloc call).
- workspace reuse_as_shape_mismatch_is_reported_on_cons_to_nil
— happy negative for the new diagnostic.
- 3 unit tests in reuse_shape::tests covering same-ctor-clean,
cons-to-nil-reject, and indeterminate-source-ctor-reject.
Test deltas: e2e 56, ailang-check unit 46 -> 49 (+3), workspace
9 -> 10 (+1). cargo test --workspace green. Hand-verified
--alloc=rc binary on reuse_as_demo: stdout 9, exit 0.
|
||
|
|
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. |
||
|
|
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"]
|
||
|
|
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> |
||
|
|
1a448309fa |
Iter 6: deps hardening, multi-diagnose, DESIGN audit
- ail deps now filters builtins, fn params, and let/match-pattern bindings. New value_names() in ailang-check::builtins is the single source of truth shared with the typechecker install path. walk_term threads a scope set; qualified `prefix.def` refs pass through. - check_in_workspace returns Vec<CheckError>; check_workspace accumulates body diagnostics across defs and modules. Pass-1 (top-level symbol table) and per-module type-def setup stay fail-fast — corrupt env would taint later diagnostics. - DESIGN.md "What the MVP is NOT" was lying (ADTs, strings landed in Iter 2/3). Renamed to "What is not (yet) supported" and split into "not yet" + supported/smoke-tested. JOURNAL Iter 6 entry records the architecture self-check. Tests: 47 green (was 44). +2 deps filter tests in e2e, +1 multi-diagnose test in ailang-check workspace integration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7577ab8a90 |
Translate project content to English
Make English the project-wide language: all comments, string literals, CLI help text, design docs, journal, agent prompts, and README. Only CLAUDE.md (the user's own instruction file) stays German, and the live conversation between user and Claude continues in German. Adds a new "Project language: English" section to docs/DESIGN.md as the durable convention. No logic changes — translation only. Four internal error strings in the typechecker were retranslated; no test asserts on their wording. Verified: cargo test --workspace passes (44/44). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b2878fe655 |
Iter 5b: Cross-Module-Typcheck mit qualifizierten Verweisen
Neue API check_workspace(&Workspace) -> Vec<Diagnostic>; check_module hebt das Modul intern in einen Trivial-Workspace. Term::Var mit genau einem Punkt im Namen ist ein qualifizierter Verweis <prefix>.<def>, aufgelöst über Import-Map (alias-oder-modulname → echter Modulname). Drei neue Diagnostic-Codes: unknown-module, unknown-import, invalid-def-name. ail check lädt jetzt immer via load_workspace; Loader-Fehler werden im JSON-Modus zu strukturierten Diagnostics (module-not-found, module-cycle, …). DESIGN.md und JOURNAL.md dokumentieren die Konvention. Hash-Stabilität: alle ir_snapshot_*- Tests bitidentisch grün, kein neuer AST-Knoten. |