Commit Graph

5 Commits

Author SHA1 Message Date
Brummel 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 b49f57d) — the flat drop FUNCTION ships here,
its call-insertion needs a codegen resolution mechanism.

What ships (all green):
- raw_buf kernel-tier submodule (crates/ailang-kernel/src/raw_buf/):
  RawBuf TypeDef (param-in {Int,Float,Bool}, ctor B a) + four
  (intrinsic) ops new/get/set/size. parse_raw_buf + workspace injection
  (kernel-tier auto-imported); workspace count 4 -> 5.
- 12 scope-qualified INTERCEPTS entries RawBuf_{new,get,set,size}__{Int,
  Float,Bool} + emit fns: new allocs an @ailang_rc_alloc slab
  (8-byte i64 size header + n*width element bytes), get/set
  getelementptr+load/store at offset 8 + i*width, size loads the header.
  Mechanical on the raw-buf.3 naming + bijection machinery; bijection
  green (4 markers -> 12 entries).
- Term::New desugar (crates/ailang-core/src/desugar.rs): (new T <types>
  <values>) -> (app T.new <values>), runs before check so the
  type-scoped callee flows through the raw-buf.3 scope threading to
  RawBuf_new__T; drops the NewArg::Type (element type inferred from
  use). Both codegen Term::New deferral arms removed (replaced with
  unreachable!). Ratified by new_stubt_builds_and_runs.
- Flat intrinsic-storage drop FUNCTION @drop_raw_buf_RawBuf (single
  @ailang_rc_dec on the slab), emitted for any TypeDef whose new op is
  (intrinsic)-bodied — distinguishes RawBuf (intrinsic new) from StubT
  (real-body new -> generic ADT drop).
- E2E: raw_buf_int (-> 60), raw_buf_float (-> 4.0), raw_buf_bool
  (-> 42), raw_buf_param_in_reject (param-not-in-restricted-set).

In-scope additions beyond the literal plan (both sound, ratified):
- qualify_workspace_term now normalises a monomorphic cross-module
  type-scoped callee (StubT.new) to <home>.f; without it
  new_stubt_builds_and_runs cannot build (StubT.new is monomorphic, so
  the raw-buf.3 poly-mono path mints no symbol). Same-module + polymorphic
  callees carved out.
- diagnostic-behaviour: (new T ..) missing-new-op now surfaces
  type-scoped-member-not-found (desugar runs before check, bypassing
  synth's Term::New arm); new-arg-kind-mismatch obsoleted. Two unit
  tests updated to the new behaviour. param-in reject unaffected.

The 5 .ll snapshots gained @drop_raw_buf_RawBuf (+ partial) — injecting
raw_buf emits its drop fn into every program; benign, regenerated to the
final IR. (The plan's "snapshots stay green" assumption was wrong.)

Verification (orchestrator, this session): cargo test --workspace 676
passed / 0 failed / 2 ignored. raw_buf_int_e2e prints 60; bijection,
round-trip, new_stubt, float/bool/reject all green. The raw_buf_no_leak
test is NOT in this commit — it moves to raw-buf.5 with the drop-call
resolution that makes it pass (the slab currently leaks at scope close;
tracked, fixed next iter; milestone not released until close).
2026-05-30 00:49:51 +02:00
Brummel 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).
2026-05-29 19:39:35 +02:00
Brummel 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.
2026-05-29 17:46:50 +02:00
Brummel 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.
2026-05-29 17:21:32 +02:00
Brummel 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.
2026-05-29 11:06:55 +02:00