72503c40fe14bcc2703f2e7fbe22fb69087089d7
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7d9ab4d29d |
refactor(codegen): parameterise the copy-pasted IR-emission families
Emitted IR is byte-identical for every case — proven by the IR-pin
suites (eq_primitives_pin, ord_int_intercept_ir_pin, the raw_buf_*
and drop/leak pins) and the full e2e suite, all green.
intercepts.rs:
- The twelve emit_rawbuf_{new,get,set,size}_{int,float,bool} functions
were copy-paste across three element types, differing only in the
element width (8/8/1) and the LLVM load/store type (i64/double/i1).
Collapsed to four parameterised cores; the element variants are thin
call sites passing the (width, type) pair.
- Replaced the repeated, off-by-one-prone parameter-SSA extraction
idiom (`locals[n-2]`, `locals[n-1]`) with an Emitter::last_param_ssas
helper.
- Factored the shared IR-Str bytes GEP out of emit_eq_str /
emit_compare_str into emit_str_bytes_gep.
drop.rs:
- The three emit_*_drop_fn_for_type methods shared one wrapping shape
(monomorphic short-circuit, else loop over instantiations dispatching
to a *_for_instantiation helper); hoisted into emit_drop_fn_wrapper
taking the per-instantiation emitter as a fn pointer.
- emit_flat_intrinsic_drop_fn / _partial_drop_fn differed only in the
symbol prefix and an ignored mask param; merged into one inner
emitter.
No INTERCEPTS-registry or (intrinsic)-marker changes.
|
||
|
|
13e590cb46 |
iter raw-buf.6 kernel-stub-retirement (DONE 5/5): retire the stub, raw_buf is the sole base extension (refs #7)
Closes the raw-buf milestone. raw_buf (shipped raw-buf.1-.5; the
owned-drop resolution landed under bug #42/#43) has subsumed every
ratification role kernel_stub held -- Term::New end-to-end, the
param-in reject, kernel-tier auto-import, the monomorphic intrinsic
path, and the type-scoped polymorphic intrinsic mechanism (RawBuf's
four ops). The stub is now redundant; this iteration removes it.
Pure removal (-636/+49 across 22 files, 7 deleted):
- Rust surface: drop `mod kernel_stub` + `STUB_AIL` re-export
(ailang-kernel), `parse_kernel_stub` + its workspace injection
(ailang-surface), the `answer` + two `StubT_peek__{Int,Float}`
INTERCEPTS entries + their three emit fns (ailang-codegen). The
(intrinsic) markers leave with the deleted source, so the
marker<->entry bijection holds in lockstep.
- Source + tests: delete crates/ailang-kernel/src/kernel_stub/,
kernel_stub_module_round_trips, the two stub-consumer e2e tests,
and mono_scoped_symbol.rs (its scope-qualified-mono mechanism is
now pinned by RawBuf's ops + the intercepts bijection).
- Fixtures: delete kernel_answer.ail, new_stubt_smoke.ail,
peek_mono_pin_smoke.ail, and fieldtest kem_3_stub_consumer.ail
(referenced the deleted StubT; documented a since-fixed bug).
- Pins: re-baseline both workspace-count content-pins 5 -> 4
(workspace_pin.rs + the e2e.rs twin) and regenerate all five IR
snapshots (400 stub-IR lines removed; no user IR changed -- the
stub auto-injected into every build).
- Ledger: design/INDEX.md + design/models/0007 to present-state
(raw_buf is the live ratifier; raw-buf milestone shipped, series
pending), plus architecture-comment sweep across workspace.rs,
mono.rs, check/codegen lib.rs, workspace_kernel.rs.
Scope decisions (orchestrator, pre-plan): the self-contained inline
serde_json check-layer fixtures in ailang-check/src/lib.rs keep their
incidental StubT/kernel_stub sample names (they construct what they
reference; not stub-ratification) -- the one permitted residual.
kem_4 fieldtest kept (uses a user NumBox ADT, comment-only edit).
hash.rs `name: "answer"` left (generic serde doc example).
Plan-gap caught at implement: the planner's verbatim edit list
under-enumerated four honesty sites; one (mono.rs:562) carried a
literal `StubT_peek__Int` -- a hard-gate symbol the verbatim list
missed, which would have failed Task 4's removal gate. Filled
(comment-only, no behaviour). Confirms the planner self-review
filter-completeness risk; the implement gate caught it.
Verification: full workspace suite green (0 failed); hard symbol gate
(STUB_AIL|parse_kernel_stub|emit_answer|emit_stubt_peek|StubT_peek__)
zero matches across crates/ examples/ design/; soft gate residual only
the kept inline fixtures; kernel crate is lib.rs + raw_buf/ only.
Regression: compile_check 24/24 stable (exit 0; uniform downward
check_ms within tol -- stub no longer parsed per compile), cross_lang
25/25, check 34/34 stable (exit 0; an earlier exit-1 on
rc_over_bump was machine-load ratio noise -- denominator bump_s got
faster, rc_s also improved -- confirmed green on re-run, not
baselined).
|
||
|
|
8ac8756682 |
iter raw-buf.4 rawbuf-payload-termnew-desugar (DONE, drop-call deferred to .5): RawBuf works, prints 60 (refs #7)
Ships RawBuf end-to-end as a consumer of the raw-buf.3 scope-qualified
intrinsic mechanism, plus the general Term::New construction sugar.
Working subset committed; the drop-CALL ratification (no-leak) is
re-carved to raw-buf.5 (see
|
||
|
|
fd37fa6489 |
iter raw-buf.3 typescoped-intrinsic-mechanism (DONE 2/2): scope-qualified mono symbols + bijection expansion (refs #7)
New language mechanism: a type-scoped polymorphic top-level (intrinsic) op now mints a scope-qualified mono symbol (StubT_peek__Int, not the colliding bare peek__Int), and the intrinsic<->intercept bijection expresses one such polymorphic marker as its N per-param-in-element entries. Ratified standalone on the stub via a new StubT.peek op; no RawBuf, no Term::New, no codegen E2E this iteration (RawBuf consumes the mechanism in raw-buf.4). Task 1 — scope threading. Added scope: Option<String> to FreeFnCall + MonoTarget::FreeFn + the synth free_fn_owner tuple. Populated Some(prefix) ONLY on the type-scoped T.f resolution branch (lib.rs:3538, gated on type_home.is_some()); None on every local / same-module / implicit-import branch, so existing bare-resolved symbols stay byte-identical. Consumed via a scoped_base helper at both mono-symbol mint sites (synthesise + rewrite, which read the same MonoTarget → automatically in lockstep) and folded into mono_target_key so a scoped op and a bare op of the same name never dedup-collide. Task 2 — ratifier + bijection. Added StubT.peek to the stub source; extended workspace_intrinsic_markers with a type-scoped-poly arm that finds the unique same-module TypeDef referenced in the fn signature and expands over its param-in set, building the T_f__<elem> strings via the SAME mono_symbol_n on Type::Con elems so collector and mint agree byte-for-byte; registered StubT_peek__Int/Float entries + emit fns; extended kernel_stub_module_round_trips to cover peek; added mono_scoped_symbol integration test (own-param calls, no Term::New, no codegen — asserts both scoped symbols minted and bare peek__Int absent). Latent bug fixed inline (regression caught at the full-suite gate, RED via the existing escape_local_demo E2E). kernel_stub is implicitly imported, so poly_free_fn_names_for_module unconditionally added peek's bare name to every consumer; a consumer with its OWN monomorphic peek then had its local call mistreated as a poly-free-fn site, over-advancing the mono slot cursor and misaligning a print target. Fix: suppress the bare implicit-import name when the current module declares a same-name global, mirroring synth's resolution ladder (same-module global beats implicit-import). Applied in lockstep to both poly_free_fn name + and constraint-count maps; dot-qualified / type-scoped spellings stay unconditional. This was a pre-existing inconsistency between mono's name-set and synth's resolution that peek (first implicitly-imported poly free fn colliding with a fixture-local name) exposed. Plan deviations (all sound): collect_con_heads omits Borrow/Own recursion (no such Type variant — borrow/own are ParamMode on Type::Fn, verified ast.rs:752); scope bound via the existing synthesise_mono_fn_for_free_fn MonoTarget destructure rather than a new param; the mono-pin fixture uses own params (borrow + peek-returns-a trips consume-while-borrowed; peek is never instantiated so the mode is immaterial to the scope-symbol ratification). Verification (orchestrator, this session): cargo build --workspace clean; cargo test --workspace 670 passed / 0 failed / 2 ignored (669 baseline + the one new mono test). Bijection green (1 marker -> 2 entries), round-trip green, escape_local_demo green. .ll snapshots unchanged (peek polymorphic -> never instantiated without a call site). Existing eq__*/ compare__*/float_* symbols byte-identical (mono_hash_stability green). |
||
|
|
caa3618c3e |
iter intrinsic-bodies.2-migration-lock (DONE 4/4): prelude → (intrinsic) + bijection pin (refs #9)
Second and final iteration of the intrinsic-bodies milestone. Migrates
the 13 authored prelude dummy bodies to (intrinsic), rebaselines the two
hash pins that move, and locks the registry to the source with a
bijection. Behaviour-preserving: the codegen intercepts emit identical
IR; the prelude placeholder bodies were already dead (intercepted by
name before lower_term, since raw-buf.1).
What landed (4 tasks):
Task 1 — examples/prelude.ail: 13 authored dummy bodies → (intrinsic).
7 Eq/Ord instance methods (eq Int/Bool/Str/Unit, compare
Int/Bool/Str) keep their lambda shell (params/ret = the method's
local signature, Design X); the inner (body false)/(body true)/
(body (term-ctor Ordering EQ)) becomes (intrinsic). 6 float_* fns
((body false) → (intrinsic) in the fn body slot). The honesty-rule
infraction the milestone exists to fix is now closed: no prelude
body lies about what runs.
Task 2 — hash rebaselines (the prelude bytes changed):
prelude module hash af372f28c726f29f → 2ea61ef21ebc1913
eq__Int 86ed4988438924d3 → cc7b99b63d1e44ae
eq__Bool d62d4d8c51f433f8 → fd0412f127986512
eq__Str 3d32f377c66b03e0 → fa269285754a52da
compare__Int 6d6c20520766368b → 0a02bd9effc9746c
compare__Bool 02b64e8fadc913eb → d0dc108dacf4e543
compare__Str 9645929d53cd3cc9 → d1419595dc52a456
The 4 show__* mono pins did NOT move (Show bodies are real).
Task 3 — intercepts.rs: registry_contains_all_legacy_arms (one-
directional, hardcoded 18-name list) replaced by
intercepts_bijection_with_intrinsic_markers. The in-source test
walks prelude + kernel_stub pre-mono for Term::Intrinsic markers,
recovers each marker's codegen symbol (mono_symbol(method, type) for
instance methods; the fn name for top-level float_*/answer), and
asserts a bijection over the intrinsic-backed class: (A) every
marker resolves to an INTERCEPTS entry, (B) every INTERCEPTS entry
not on the optimisation-only allowlist has a marker, plus a guard
that the allowlist names are themselves registered. The allowlist is
the 5 *__Int icmp-family entries, which intercept the monomorphised
specialisation of the real-bodied polymorphic lt/le/gt/ge/ne — an
optimisation, not a compiler-supplied body, so no marker. The pin
passing independently confirms Task 1 missed no prelude site.
Task 4 — confirmed no codegen path lowers an intercepted body (already
true since raw-buf.1: try_emit_primitive_instance_body matches by
name before f.body is inspected). Refreshed the now-accurate comment
at lib.rs:1310-1319. design/contracts/0007-honesty-rule.md needed NO
edit — it is a general present-tense rule and does not name the
prelude dummies (confirmed by grep), so editing it would have been
invented work.
Verification:
cargo test --workspace → 669 passed, 0 failed (post-.1 baseline held;
one test replaced net-0).
Behaviour-preservation ratifiers GREEN (the safety net):
eq_primitives_smoke_compiles_and_runs,
compare_primitives_smoke_prints_1_2_3_thrice (e2e.rs);
float_compare_smoke_prints_true_true_false (float_compare_smoke_e2e.rs);
eq_ord_polymorphic_runs_end_to_end (eq_ord_e2e.rs).
Bijection pin GREEN. Both hash pins GREEN at the new baselines.
bench/check.py + compile_check.py → 0 regressed.
Plan-text imprecision (no outcome impact, noted for the record): Task 1
Step 5 wrote `--test e2e` for all four ratifiers, but
float_compare_smoke_prints_true_true_false and
eq_ord_polymorphic_runs_end_to_end live in their own test targets
(float_compare_smoke_e2e.rs, eq_ord_e2e.rs). The verification-filter
self-review checked that the fn names exist, not that they sit in the
named target — a sharper form of the filter-zero discipline. The
implementer ran each in its real target; all four green.
Milestone intrinsic-bodies is now feature-complete (.1 mechanism +
.2 migration+lock). Audit + close follow.
|
||
|
|
52ff8738b8 |
iter intrinsic-bodies.1-mechanism (DONE 8/8): Term::Intrinsic leaf + cross-crate wiring (refs #9)
First iteration of the intrinsic-bodies milestone. Introduces the
Form-A `(intrinsic)` body marker as a new leaf AST term and wires it
through surface, checker, codegen, and a kernel_stub ratifier. The
prelude migration + hard-lockstep pin + dead-path removal are .2.
What landed (8 tasks):
Task 1 — Term::Intrinsic unit variant (ast.rs), tag "t":"intrinsic"
via the enum's rename_all=lowercase. Additive: no existing fixture
carries it, hashes bit-identical. In-core exhaustive-match arms
(canonical/hash/visit/pretty/desugar/workspace) added as leaves.
Task 2 — surface parse + print: (intrinsic) as a fn body-slot clause
and a lambda positional body, mapped to/from Term::Intrinsic.
Task 3 — cross-crate walker sweep (check/codegen/prose/ail-main):
leaf no-op/identity arms at every no-wildcard Term match the
compiler flagged.
Task 4 — checker: new Env.current_module_kernel_tier flag (m.kernel
|| m.name=="prelude"), set alongside current_module. A def whose
body is intrinsic (top-level fn OR instance-method lambda, via the
shared is_intrinsic_body helper) is checked signature-only; an
intrinsic body outside kernel-tier/prelude is rejected with
intrinsic-outside-kernel-tier.
Task 5 — codegen: an intrinsic-bodied fn routes through the existing
try_emit_primitive_instance_body / intercepts::lookup path; if no
intercept fired it is an internal error, never a lower_term
fallthrough. lower_term and the synth walker get Term::Intrinsic
internal-error arms (an intrinsic body reaching either is an
escape bug).
Task 6 — answer intercept (ret i64 42) registered in INTERCEPTS;
the `answer : () -> Int` intrinsic added to STUB_AIL;
examples/kernel_intrinsic_smoke.ail added so schema_coverage
observes Term::Intrinsic in the examples/ corpus.
Task 7 — E2E ratifier: examples/kernel_answer.ail calls
kernel_stub.answer and prints 42; answer_intrinsic_builds_and_runs_printing_42
asserts it end-to-end (source → native).
Task 8 — design/contracts/0002-data-model.md gains the
{ "t": "intrinsic" } Term entry + fn/lam prose; form_a.md grammar
note updated.
Verification:
cargo test --workspace → 669 passed, 0 failed (baseline 667 +2:
intrinsic_in_user_module_is_rejected, answer_intrinsic_builds_and_runs_printing_42).
bench/check.py + bench/compile_check.py → 0 regressed.
Reject E2E (subprocess ail check --json, exit 1, code
intrinsic-outside-kernel-tier) GREEN.
Round-trip + hash pins GREEN — Term::Intrinsic is additive, no
existing fixture carries it, no hash moved.
Three implementation completions beyond the plan (all behaviour-
preserving, surfaced during execution):
1. The signature-only skip had to apply at the mono pass's two
synth-on-body re-entry sites (collect_mono_targets,
collect_residuals_ordered), not only check_fn — else an intrinsic
body hits synth's Term::Intrinsic internal-error guard. Repaired by
extracting the shared crate::is_intrinsic_body helper and applying
it at all three synth-on-body paths. Not a representation surprise:
the same signature-only treatment, more call sites.
2. The compiler-enumerated exhaustive-match set was broader than the
plan's named grep set (the plan anticipated this and made the sweep
compile-driven). Extra leaf arms in core desugar/workspace, check
reuse-as + qualify_workspace_term, codegen synth_with_extras,
ail/src/main.rs, and four test targets.
3. Fixture corrections: emit_answer needed the body-close
(block terminator) the plan snippet omitted; kernel_answer.ail's
main is (ret Unit)(effects IO) using (app print ...) since
io/print_int does not exist (the plan flagged this for the
implementer to resolve against real effect-op names).
IR snapshots (hello/sum/list/max3/ws_main.ll) refreshed: purely
additive @ail_kernel_stub_answer fn+adapter+closure, emitted into
every workspace exactly as the pre-existing @ail_kernel_stub_new
already was (kernel_stub is auto-injected; confirmed new was present
in the pre-iter hello.ll baseline). No user-fn IR changed.
The .2 iteration migrates the 18 prelude dummy bodies to (intrinsic),
upgrades registry_contains_all_legacy_arms to a source<->registry
bijection pin, and removes the dead body-lowering path.
|
||
|
|
140a0c0ef7 |
iter raw-buf.1-intercept-registry (DONE 5/5): try_emit_primitive_instance_body → registry table (refs #7)
Pure refactor — first iter of the raw-buf milestone. The
hardcoded 18-arm match in try_emit_primitive_instance_body
(crates/ailang-codegen/src/lib.rs L2841-3147 pre-iter, ~305
lines) becomes a 6-line shim that consults a single registry
table in the new sibling module crates/ailang-codegen/src/intercepts.rs.
Zero behavioural change. The wrapper name survives, all 14
in-source comment-refs to try_emit_primitive_instance_body stay
valid pointers to the canonical dispatch entry.
What landed (5 tasks):
Task 1 — module skeleton: Intercept struct, INTERCEPTS static
table (empty), lookup(name), check_sig(intercept, params, ret).
mod intercepts; wired into lib.rs at L43.
Task 2 — populate: 18 free emit fns lifted from the legacy
match arms; INTERCEPTS populated with one entry per name. Three
Emitter fields (body, locals, block_terminated) and three helper
methods (emit_compare_ladder, emit_ordering_arm,
emit_direct_int_icmp_intercept) bumped to pub(crate) so the
sibling module can reach them. lower_ctor stayed at its existing
visibility — not consumed by sibling-module fns.
Task 3 — rewire dispatch: try_emit_primitive_instance_body body
becomes `match intercepts::lookup(fn_name) { Some(i) => {
check_sig(i, params, ret)?; (i.emit)(self)?; Ok(true) } None =>
Ok(false) }`. The 305-line match collapses to 9 lines.
Task 4 — fold wants_alwaysinline into the registry: every entry
in INTERCEPTS carries wants_alwaysinline: bool. The predicate
intercept_emit_wants_alwaysinline now consults the registry for
branch 1 (the literal 13-name list the predicate hardcoded). One
source of truth instead of two — closes the silent-drift class
recon flagged.
Task 5 — registry_contains_all_legacy_arms pin test: enumerates
all 18 legacy names + asserts each resolves to a registered
entry. A half-finished migration that drops any name becomes a
hard test fail.
Two judgement calls the implementer made (both behaviour-
preserving, both worth knowing):
(1) eq__Unit param shape: the plan named expected_params: &[]
per the recon's "params ignored" reading. Reality: the call
site delivers ["i8","i8"] (Unit lowers to i8 per codegen //!
header). The registry entry uses ["i8","i8"]; the emit fn
discards the operand SSAs and returns `ret i1 1` — semantically
equivalent to the legacy arm that checked only ret_ty and
ignored params. An inline rustdoc on the entry names the
rationale.
(2) intercept_emit_wants_alwaysinline has TWO branches, not one
as the plan inferred. Branch 1 = the literal 13-name list (which
the iter's drift-closure intent covers). Branch 2 = a stem-prefix
carve-out matching {ne|lt|le|gt|ge}__* that covers polymorphic-
body monomorphizations (lt__Bool, ne__Str, ne__MyADT, …) which
are NOT registry entries and never were. A literal "replace with
one-line registry lookup" would silently drop alwaysinline on
those names, breaking the bench-perf parity established in iter
operator-routing-eq-ord.1. Branch 2 stays as it was, with a
fresh comment naming what the registry covers vs. what branch 2
covers.
Follow-up consideration (not in this iter): extend the Intercept
variant to a predicate-only entry shape so branch 2 can fold
into the registry as well, OR accept the carve-out as the final
shape. Either is reasonable. Filed as a backlog idea (see Gitea
issue created post-commit if labelled).
Verification:
cargo test --workspace → 667 passed, 0 failed, 2 ignored
(baseline was 666 + 2 ignored;
+1 from registry_contains_all_legacy_arms)
Four E2E ratifiers GREEN (named in spec § Testing strategy
raw-buf.1):
- eq_primitives_smoke_compiles_and_runs (crates/ail/tests/e2e.rs)
- compare_primitives_smoke_prints_1_2_3_thrice (crates/ail/tests/e2e.rs)
- float_compare_smoke_prints_true_true_false
- eq_ord_polymorphic_runs_end_to_end
Alwaysinline pins GREEN (regression net for branch-2 carve-out):
- crates/ailang-codegen/tests/eq_primitives_pin.rs
- crates/ail/tests/prelude_eq_alwaysinline_ir_pin.rs
New pin GREEN:
- intercepts::tests::registry_contains_all_legacy_arms
Diff size: lib.rs −318 / +26 (net −292); intercepts.rs +488 (new
file). Net repository LOC delta: +196. The size reduction in
lib.rs is the wrong proxy — the value is the consolidation: one
table, one drift-closed predicate, one pin test enumerating the
migration surface.
The raw-buf.2 iter will rename crates/ailang-kernel-stub/ to
crates/ailang-kernel/ and reshape it as the family-crate that
hosts raw_buf alongside kernel_stub. The registry shipped here
is the substrate raw-buf.3 will plug RawBuf's 12 codegen
intercepts into.
|