Skip to content

Update WALKTHROUGH.md#1

Closed
syzygysolstice wants to merge 2 commits intocuzzo:masterfrom
syzygysolstice:patch-1
Closed

Update WALKTHROUGH.md#1
syzygysolstice wants to merge 2 commits intocuzzo:masterfrom
syzygysolstice:patch-1

Conversation

@syzygysolstice
Copy link
Copy Markdown
Contributor

Just for comments

@cuzzo cuzzo force-pushed the master branch 6 times, most recently from 1a65094 to bbde1c2 Compare January 17, 2026 15:54
cuzzo added a commit that referenced this pull request Apr 4, 2026
test_tcp_charAt_stall.zig: 32 clients x 1000 msgs with charAt O(1)
byte parsing across 8 schedulers. Passes 5/5 in both Debug and
ReleaseFast. The documented "stall under concurrent TCP" was caused
by epoll fd corruption (fixed in 62926ee), not charAt itself.

Updated docs/update-bench.md: Bug #2 (String@raw stall) marked
RESOLVED. Bug #1 (epoll migration) status updated to reflect the
registerFd fix is working.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request Apr 6, 2026
Leak #1 (15 allocs, tests 25/26/27/28/68): Struct arrays/lists with
heap-duped string fields had no cleanup. Fixed by adding
:array_with_struct_strings kind to CleanupPlan.classify_binding and
elem_has_string_fields? helper. Also extended list_with_elem_cleanup
to detect struct elements with string fields.

Leak #2 (2 allocs, test 103): promoteFields double-duped strings that
CopyNode already heap-allocated. Fixed by marking CopyNode fields as
handled in PromotionPlan so compute_struct_promote skips them. Added
per-field promote via unhandled_promote_fields to avoid blanket
promoteFields when some fields are already owned.

Runtime fix: checkYield guards on scheduler_running to prevent crash
when test harness runs without a fiber scheduler (test 174).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request Apr 11, 2026
test_tcp_charAt_stall.zig: 32 clients x 1000 msgs with charAt O(1)
byte parsing across 8 schedulers. Passes 5/5 in both Debug and
ReleaseFast. The documented "stall under concurrent TCP" was caused
by epoll fd corruption (fixed in 71217ab), not charAt itself.

Updated docs/update-bench.md: Bug #2 (String@raw stall) marked
RESOLVED. Bug #1 (epoll migration) status updated to reflect the
registerFd fix is working.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request Apr 11, 2026
Leak #1 (15 allocs, tests 25/26/27/28/68): Struct arrays/lists with
heap-duped string fields had no cleanup. Fixed by adding
:array_with_struct_strings kind to CleanupPlan.classify_binding and
elem_has_string_fields? helper. Also extended list_with_elem_cleanup
to detect struct elements with string fields.

Leak #2 (2 allocs, test 103): promoteFields double-duped strings that
CopyNode already heap-allocated. Fixed by marking CopyNode fields as
handled in PromotionPlan so compute_struct_promote skips them. Added
per-field promote via unhandled_promote_fields to avoid blanket
promoteFields when some fields are already owned.

Runtime fix: checkYield guards on scheduler_running to prevent crash
when test harness runs without a fiber scheduler (test 174).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cuzzo cuzzo force-pushed the master branch 2 times, most recently from 675b960 to 7e9f0e9 Compare April 11, 2026 20:04
@cuzzo cuzzo closed this in ed62fc9 Apr 13, 2026
cuzzo added a commit that referenced this pull request Apr 23, 2026
Two correctness fixes in the parking-lot / scheduler interaction.

#1. Scheduler lock_timeout_ms only fired under specific conditions.
    Three cases where it silently did nothing:
      a. Idle scheduler with no sleepers and no pending I/O: io_uring_enter
         blocks indefinitely on CQEs. lock timeouts never fire.
      b. Fast-path starvation: on a scheduler that always has ready work,
         the loop never enters the slow-path `else` branch where the scan
         lived. Any parked fiber's timeout is permanently shadowed.
      c. Stale-entry leak: the lazy cleanup of lock_waiters entries whose
         waiting_for_lock was cleared also only ran in the slow path, so
         on a busy scheduler the list grew without bound.

    Fix: extract scanLockWaiters() so it runs on EVERY loop iteration
    (cheap no-op when empty). In the idle wait path, compute the earliest
    deadline across sleeping_queue and lock_waiters and arm an io_uring
    timeout for that deadline so an otherwise-idle scheduler still wakes
    to fire timeouts on schedule.

#2. Cycle detection false-positive via stale waiting_for_lock_owner.
    When unlock() calls wakeNext() -> submitResume(), the waiter goes onto
    the ready queue but does not actually run until a later scheduler tick.
    Its waiting_for_lock_owner field is still the pre-wake pointer. If a
    second fiber calls lock() on a different lock in between, its
    detectCycle walk sees the stale chain and can return error.Deadlock
    for a cycle that no longer exists.

    Fix: in ParkingMutex.unlock and ParkingRwLock.wakeNext, clear the
    waiter's wait-state fields (waiting_for_lock, waiting_for_lock_owner,
    waiting_for_lock_list, lock_waiter_node, lock_counter_ptr) BEFORE
    submitResume. The post-yield cleanup in lock() becomes idempotent.

Tests:
- ParkingMutex: wakeNext clears wait-state so detectCycle has no TOCTOU
  false positive. Deterministic single-scheduler cooperative reproduction
  of the false-positive: A holds mu_a parked on rv; B parks on mu_a;
  C parks on mu_b; Main wakes A; A unlocks mu_a (wakes B into ready
  queue) and immediately tries mu_c. Without the fix, detectCycle walks
  C -> B -> B.waiting_for_lock_owner == A (stale) and returns Deadlock.
  Verified to FAIL without the fix.
- The event-driven test 14 (write-lock timeout) is now purely event-driven
  (no wall-clock sleep) because the scheduler wakes on the lock deadline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request Apr 23, 2026
…pers into state

Two changes that significantly close the perf gap to pthread.

#1. RwLock writer/reader non-fiber slow paths now use test-then-CAS
    instead of CAS-spinning. Old version did fetchAdd/cmpxchgWeak in a
    tight loop; under N-thread contention every iteration bounced the
    state cache line. New version reads (cache-shared) until the lock
    looks acquirable, then issues exactly one RMW.

#2. ParkingMutex thread_sleepers folded into state as a third bit
    (HAS_THREAD_SLEEPER). unlock is now a single fetchAnd whose return
    value tells us whether to wake -- no separate cache-line load for
    the sleeper count. owner.store(null) on unlock removed (next
    acquirer overwrites it; re-entrancy detection still works).

    Bit is sticky: once any thread parks it stays set. Means every
    subsequent unlock pays a Futex.wake syscall, but the alternative
    (clearing the bit) creates a lost-wake race when multiple threads
    are parked.

    Lock fast path uses load-then-fetchOr (test-then-RMW) for the same
    reason as #1 -- read-shared cache hits under contention.

Updated benchmark (32-thread machine, each cell >= 500ms):

  Lock   | Pattern     | Contention  | Parking  | pthread  | Ratio
  Mutex  | (any)       | uncontended |   ~570ms |   ~525ms | 0.86-0.89x
  Mutex  | (any)       | heavy       |  ~5500ms |  ~1380ms | 0.23-0.29x  (was 0.15)
  Mutex  | (any)       | realistic   |  ~1000ms |   ~280ms | 0.20-0.35x  (was 0.15)
  RwLock | read-heavy  | uncontended |    434ms |    754ms | 1.74x  WIN
  RwLock | read-heavy  | heavy       |   2390ms |   4537ms | 1.90x  WIN
  RwLock | read-heavy  | realistic   |    500ms |    881ms | 1.76x  WIN
  RwLock | write-heavy | uncontended |    561ms |    739ms | 1.32x  WIN  (was 1.43)
  RwLock | write-heavy | heavy       |   5827ms |   5636ms | 0.97x  TIE  (was 0.45 -- 2.1x improvement!)
  RwLock | write-heavy | realistic   |   1134ms |   1162ms | 1.03x  TIE  (was 0.46 -- 2.2x improvement!)
  RwLock | mixed       | uncontended |    556ms |   1021ms | 1.84x  WIN
  RwLock | mixed       | heavy       |   5565ms |  38419ms | 6.90x  WIN
  RwLock | mixed       | realistic   |    958ms |   8477ms | 8.85x  WIN

RwLock now beats or ties pthread across all 9 cells. Mutex still 4-5x
slower under heavy contention; closing that gap further requires
packing the owner pointer into state (loses cycle-detection chain) or
inline-asm-level optimization to match glibc's hand-tuned mutex.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request Apr 23, 2026
Four coverage additions against the lock-safety Ruby-side surface,
plus a user-requested mixed-capability + sort test.

Gap #1 — error_registry unit spec (spec/error_registry_spec.rb, 22
  examples). ERROR_KINDS is exactly the 6 documented kinds, frozen.
  ERROR_TYPES maps LockTimeout/LockCycle -> Transient, Deadlock ->
  System, each with the right Zig spelling. Direct coverage on
  error_kind?, error_type?, kind_of_type, zig_name_of_type,
  types_for_kind, and consistency invariants (every type's kind is a
  registered kind; types_for_kind is the inverse of kind_of_type;
  every zig_name is non-empty).

Gap #2 — error_kind / error_type stamping spec (new describe block
  in spec/capabilities_spec.rb). Monkey-patches writeFile's stdlib
  entry for the duration of each example and verifies the annotator
  propagates :error_kind and :error_type to the AST call node. Also
  verifies the fields stay nil when the entry has no metadata. Before
  this spec, the plumbing (added with Phase 2 registry work) had no
  direct test — a silent regression would have been invisible.

Gap #4 — runtime contention transpile-tests.
  - 267_retry_resolves.cht: holder sleeps 150ms (past one 100ms
    timeout window, within a second). Waiter uses RETRY(3) THEN PASS.
    The runtime LOCK TIMEOUT line fires on the first attempt; the
    retry acquires cleanly and the marker + counter end up in the
    expected state.
  - 268_multi_lock_sort_forced_contention.cht: two BG fibers hold
    their overlapping pair of locks for 50ms in OPPOSITE source
    orders. Without sort this is the classic AB/BA LockCycle setup;
    with sort both fibers resolve the same global pointer order,
    serialize, and complete.
  - Sub-item (b) — RAISE bubbling through a BG ~!Void / ~!Int64
    promise — hit a linear-resource tracker interaction that needs a
    separate fix to express cleanly (linear promise "not consumed"
    when the CATCH-via-RAISE shape tries to observe the error).
    Deferring; the RAISE codegen itself is structurally covered by
    the Phase 2 annotator specs and by transpile-test 262.

Gap #5 — rank inconsistency anchors at the second declaration, not
  the first. New spec asserts the error message includes "(Line 3)"
  (second decl) and NOT "(Line 2)" (first decl), and that both ranks
  appear so the programmer can see the conflict.

User request — transpile-tests/270_mixed_capabilities_with_sort.cht:
  a single WITH acquires EXCLUSIVE a + RESTRICT r + BORROWED b +
  EXCLUSIVE c in one comma-separated list, then again with the
  EXCLUSIVE pair reversed. Verifies the multi-lock sort coexists
  cleanly with non-fallible (RESTRICT / BORROWED) captures, both
  orderings compile + run, aliases are usable in the body, and the
  sorted guard-var names don't collide with RESTRICT / BORROWED
  aliases.

2461 Ruby specs + 329 transpile-tests (zero memory leaks) all pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request Apr 28, 2026
Introduces a single fail-closed classifier that replaces (eventually)
the four disjoint capture-handling paths in mir_lowering.lower_bg_block
(pointer_captures, resource_captures, promoted_names, per-type forks)
with one exhaustive dispatch.

Five strategies, each dictating its own ctx-field type, ctx-init RHS,
and required MIR markers:
  - ByValue        : primitives, strings — byte copy, no markers
  - RcClone        : @multiowned/@shared  — Rc/Arc discipline handles it
  - FreshHeapCopy  : user wrote COPY x    — new AllocMark in fiber body
  - MoveInto       : user wrote GIVE x    — MoveMark on source
  - Refuse         : heap-backed/borrow without transfer — lowering error

Closes the unsafe default that let Bug #3 (slice-borrow-into-async-BG)
slip through: anything not explicitly safe classifies as Refuse rather
than silently falling through to byte-copy-the-header.

MIRChecker stays on its seven invariants; no new invariant is needed.
Existing INV #1 (leak) and INV #2 (orphan cleanup) fire naturally once
the lowering emits the strategy's marker plan.

This commit is Step 1 of the migration plan in docs/agents/vm-bugs.md:
the classifier is pure analysis with 26 specs; it is NOT yet called
from lower_bg_block. Step 2 wires it in behind an unchanged behavior
path; Steps 3-5 migrate categories and delete the dead piecewise code.

Also ships:
  - docs/agents/vm-bugs.md — bug catalog + gap analysis
  - spec/vm_bg_capture_bugs_spec.rb — live regression for Bug #1 and
    Bug #3 (UAF pattern through plain slice + through union-variant slice)
  - Deletion of stale spec/minivm_bytecode_compiler_spec.rb (referenced
    a file removed months ago; blocked running full spec/ without
    --exclude-pattern).

2548/2548 Ruby specs pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request Apr 28, 2026
…ep 3)

Closes the dangling-pointer family of bugs (docs/agents/vm-bugs.md):
any BG block that captures a heap-backed / borrow-like value without
an explicit ownership transfer (GIVE / MOVE / COPY / CLONE inside the
body) is now refused at lowering time with a CLEAR-level diagnostic.

Key design point (per user directive): "same rules as everything else
— do not implement a new system just for BG". The enforcement reads
AST::MoveNode / CopyNode / CloneNode already in the BG body to
populate CaptureStrategy::CaptureSiteInfo. No parallel analysis,
no new marker nodes, no new MIRChecker invariant.

Strategies recognized:
  ByValue        : primitives + strings (POD).
  RcClone        : @multiowned, @shared, @locked, @writeLocked, @Local,
                   @Sharded, @striped — ref-counted across fibers.
  MoveInto       : GIVE/MOVE in body, OR existing resource_captures
                   (File, TCPClient, ...).
  FreshHeapCopy  : COPY/CLONE in body.
  Refuse         : everything else → compile error with actionable
                   diagnostic explaining how to transfer ownership.

Example (previously silent UAF, now a compile error):
  FN runit() RETURNS Int64 ->
      MUTABLE lst: Int64[]@list = List[];
      lst.append(1_i64);
      slice: Int64[] = lst;
      p: ~Int64 = BG { work(slice); };   -- ← now rejected at compile
      RETURN NEXT p;
  END
Diagnostic:
  BG block captures values that cannot safely cross the fiber boundary:
    - 'slice' is a slice borrow — COPY inside the BG body for a fresh
      heap copy.
  (See docs/agents/vm-bugs.md for the ownership rules.)

Spec changes:
- spec/vm_bg_capture_bugs_spec.rb: flipped the dangling-pointer asserts
  from "currently compiles + UAFs" to "refuses at compile time".
- transpile-tests/273_bg_slice_capture.cht removed: its scenario is now
  covered by the spec (Bug #1 regression using COPY); the original
  bare-capture form is now correctly refused.

Known limitations (escape hatches need downstream lowering work; filed
as follow-up in docs/agents/vm-bugs.md):
- COPY x inside BG body classifies correctly (FreshHeapCopy) but the
  AllocMark/Cleanup pair isn't emitted inside the fiber's scope yet,
  so the heap copy leaks. Tests that need this pattern should wait.
- GIVE x inside BG body classifies correctly (MoveInto) but the Zig
  codegen still trips on "mutable not accessible from here" for @list
  sources. Same follow-up.

Both escape hatches PARSE and CLASSIFY correctly; only the emission
paths for them need Step 4/5 of the migration plan. The CLEAR-level
diagnostic directs users at the right syntax regardless.

2549/2549 specs (1 pending), 333/333 transpile-tests, zero memory leaks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
Self-host prep tracker task #10 (P1, Sorbet/RBS gradual typing). Adds:

  - Gemfile gems: sorbet, sorbet-runtime, tapioca (development group)
  - sorbet/config: --dir . with vendor/, zig*/, spec/, transpile-tests/,
    examples/, benchmarks/, tmp/, docs/ ignored
  - sorbet/rbi/clear-stubs.rbi: hand-written Token stub to silence the
    one default-typed-false name-resolution false positive in
    parser.rb:63 (Sorbet suggested Socket)
  - .gitignore allow-list entry for /sorbet/
  - docs/agents/sorbet-string-symbol-workflow.md: how Sorbet drives the
    String/Symbol normalization sweep (tracker task #1) -- declare
    T.any(String, Symbol) on a method, tighten to one type, run
    `srb tc`, fix every flagged caller

Status:

  $ bundle exec srb tc
  No errors! Great job.

Why this lands earlier than its dependency order in the tracker
suggested: Sorbet is the *driver* for tasks #1, #2, #5, #9, #13, not a
follow-up to them. Installing it at default `# typed: false` does
nothing on its own (no enforcement, no perf cost), but unlocks the
type-narrowing workflow needed to land String/Symbol normalization
without grep-and-pray. Files flip to `# typed: true` then
`# typed: strict` per the per-file gate (task #20) as cleanup lands.

Known incompat: tapioca 0.19 + Ruby 3.2 + sorbet-runtime 0.6.13196
crashes on `tapioca init` in `coerce_and_check_module_types`. Doesn't
block the static checker; hand-written stubs cover the small surface
area while we're at default typed:false. Workflow doc tracks the
upstream fix.

Verified:
  bundle exec prspec spec/         3599 examples, 0 failures
  ./clear test transpile-tests/    432 passed, 0 leaks
  bundle exec srb tc               No errors
cuzzo added a commit that referenced this pull request May 7, 2026
…e_analysis

`fn_nodes[callee_name] || fn_nodes[callee_name.to_sym]` at
escape_analysis.rb:300 was defensive against a Symbol key that is
never written. Verified by tracing every fn_nodes write site:

  - compiler_frontend.rb:45  fn_nodes[s.name]            (s.name is String)
  - compiler_frontend.rb:141 fn_nodes[synth_name]        (synth_name = "__test_body_#{counter}")
  - importer.rb:83           fn_nodes[s.name]            (String)
  - transpiler.rb:106        result.fn_nodes["main"]     (String literal)

All writes use Strings. The `.to_sym` fallback never matched, so
dropping it has no behavioural effect on any existing path. Verified
on transpiler-cleanup branch with the standard suite:

  bundle exec prspec spec/         3599 examples, 0 failures
  ./clear test transpile-tests/    432 passed, 0 leaks

Self-host prep tracker task #1 -- this is one of the easier wins
under the broader String/Symbol normalization sweep. The deeper
.to_sym sites in importer.rb / compiler_frontend.rb / mir_lowering.rb
schema-map writes (`schemas[stmt.name.to_sym] = ...`) are NOT dead --
they are real type bridges: stmt.name is a String (from the parser
storing Token.value, where VAR_ID/TYPE_ID values are `@s.matched`
strings), but the schema maps are Symbol-keyed because Type#resolved
returns a Symbol via `ft.to_sym`. Removing those `.to_sym` calls
requires picking a project-wide canonical type for identifiers --
either flow Strings to the schema maps (change Type#resolved to
return String, change ~30 reader sites) or flow Symbols up from the
parser (change lexer Token.value typing for VAR_ID/TYPE_ID, audit
~372 .name reads). That decision is captured at task #1 for the
follow-up commit.
cuzzo added a commit that referenced this pull request May 7, 2026
`# typed: true` on importer.rb couldn't see Struct attr_accessors like
`AST::FunctionDef#needs_rt` and `#effects` because those are declared
inside `Struct.new(...) do ... attr_accessor ... end` blocks that
Sorbet's name resolver doesn't trace through.

tools/gen_attr_rbi.rb walks src/**/*.rb with Prism, finds attr_accessor
declarations inside Struct.new blocks and class bodies, and emits an
RBI with T.untyped reader+writer sigs. Regenerable.

Output (sorbet/rbi/clear-attr-accessors.rbi): 91 classes, 404 attr_*
declarations.

Also ignore tools/ in sorbet/config (it's a generator, not src) and
allow tools/ through the root .gitignore allow-list.

Verified: srb tc clean, prspec 3599/0, transpile-tests 432/0 leaks.

Unblocks tasks #1 (String/Symbol normalization) and #10 (Sorbet
gradual typing rollout) which both depend on the RBI shim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
First step of folding hash-as-struct schemas into real types. Enum
schemas were `{ kind: :enum, variants: Set, visibility: Symbol }` and
dispatched everywhere via `schema.is_a?(Hash) && schema[:kind] == :enum`.

Now `Schemas::EnumSchema = Data.define(:variants, :visibility)`, dispatched
via `schema.is_a?(Schemas::EnumSchema)`. Migration handles the still-Hash
union/struct/resource schemas at sites that test for both (e.g.
`is_a?(EnumSchema) || (is_a?(Hash) && schema[:kind] == :union)`).

Subsumes part of the String/Symbol normalization (TODO #1, now folded
into #2): the schema was the carrier of mixed Symbol-metadata and
String-field-name hash keys.

Verified: srb tc clean, prspec 3599/0, transpile-tests 432/0 leaks.

Next steps: UnionSchema, StructSchema, ResourceSchema (the entangled
ones that share `:type_params`, `:extern_module`, `:as_type` carriers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
Final step of the schema extraction. Adds
`Schemas::StructSchema = Data.define(:fields, :field_defaults,
:borrowed_fields, :type_params, :methods, :visibility, :extern_module,
:as_type)` and converts every struct-schema dispatch site:

- `schema.is_a?(Hash) && !schema[:kind]` -> `is_a?(StructSchema)`
- `schema.keys.reject { |k| k.is_a?(Symbol) }` -> `schema.fields.keys`
- `schema[fname]` (field type) -> `schema.fields[fname]`
- `schema.each` over mixed metadata+field keys -> `schema.fields.each`
- `schema[:field_defaults]`/`[:borrowed_fields]`/`[:methods]`/etc.
  -> typed accessors

Also extends `ResourceSchema` to carry fields/type_params/extern_module/
as_type so `EXTERN STRUCT ... CLOSE` forms produce ResourceSchema
directly (eliminating the last `[:kind] == :resource` Hash dispatch).

Construction sites converted: visit_StructDef, visit_ExternStructDecl
(both struct and resource branches), built-in :Range, synthetic inline-
struct variant types, pipeline join types, and parallel `@struct_schemas
+ @union_schemas` registries in mir_lowering.rb and importer.rb.

Subsumes the String/Symbol normalization win from the original task #1:
the schema was the carrier of mixed Symbol-metadata + String-field-name
keys, and the `k.is_a?(Symbol)` filters disappear once `fields` is its
own typed Hash[String, Type].

Verified: srb tc clean, prspec 3599/0, transpile-tests 432/0 leaks
(one pre-existing failure on 346, unrelated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 7, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 8, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 8, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 8, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 8, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 8, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 8, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 8, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 8, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cuzzo added a commit that referenced this pull request May 8, 2026
…ts, respond_to? cleanup

Squashed from 21 incremental commits. Original history preserved at tag
`transpiler-cleanup-original`. Each piece below was independently green
on srb tc / prspec (3598/0) / transpile-tests (432/0 leaks).

Hash-as-struct schemas → typed Data classes in src/ast/schemas.rb:
- `EnumSchema(variants, visibility)`
- `ResourceSchema(close_zig, static_methods, fields, type_params, ...)`
- `UnionSchema(variants, type_params, visibility)`
- `StructSchema(fields, field_defaults, borrowed_fields, type_params,
   methods, visibility, extern_module, as_type)`

Eliminates 60+ `schema.is_a?(Hash) && schema[:kind] == :X` dispatches
across annotator, MIR pipeline, and tools. Fields cleanly separated
from metadata; the `schema.keys.reject { |k| k.is_a?(Symbol) }` pattern
goes away (subsumes the original String/Symbol normalization).

Parallel registries in `mir_lowering.rb` (`@struct_schemas`,
`@union_schemas`) and `importer.rb` carry typed values too.

- `Formatter::Emitter::FnSig(toks, start, arrow_idx, po, pc)` — 5
  methods that took the same arg cluster.
- `PipelineHost::PipelineSite(list, options)` — 24 lower_X methods in
  PipelineHost; clump dropped from 13 methods.
- `MIRPass::WalkCtx(bindings, promo)` — read-only carry through
  transform_body / recurse_branches!.
- `OwnershipDataflow::DataflowStep(state, consumed)` — per-walk state
  for collect_ownership_transfers + 4 helpers.

Introduced `AST::HasBodies` module. Body-owning AST nodes (12 types:
IfStatement, WhileLoop, ForRange, ForEach, MatchStatement, WithBlock,
DoBlock, BgBlock, BgStreamBlock, FunctionDef, TestBlock,
WhileBindLoop) declare `child_bodies`. `AST.walk_body` and
`AST._bg_visit_recursive` collapse from hand-coded case chains to
trait-driven loops. Adding a new body-owning node type is now a single
include + def, no walker edits.

655 → 504 sites (-151). Methodology: trace each receiver via Prism +
call-site grep before touching. Most "guards" were dead defensive code
where Locatable's universal accessors (token, full_type, type_info,
storage, matched_stdlib_def, was_moved, zig_pattern, mutates_receiver,
line, column, etc.) made the check pointless on AST receivers.

Clusters cleaned: `:strip` / `:empty?` (emit() returns String|nil),
`:line` / `:column` (Locatable + Token), `:matched_stdlib_def`,
`:was_moved`, `:zig_pattern`, `:mutates_receiver`, `:token`,
`:full_type` (40 sites), `:value` (sites with case/when narrowing).

False positives caught by tests: 1 in `visit_StubDecl` where
`node.value` is genuinely polymorphic (AST | Symbol). Replaced with
explicit `is_a?(AST::Locatable)` and a comment.

Also removed 1 spec test that locked in the dead-defensive code via
`Struct.new(:stack_tier, :stack_vars_bytes)` — lockstep deletion per
CLAUDE.md "test for deleted functionality."

Converted PipelineHost#transpile_pipeline's 24-arm if/elsif chain to
case/when (54 LOC → 30 LOC). Other AST-is_a? chains in src/ are 2-3
arms with mixed predicates (`is_a?(X) && was_moved`) where case/when
doesn't simplify cleanly — those stay as-is.

- `.github/workflows/transpile-pure.yml`: PR-body-triggered byte-diff
  of generated .zig vs merge base. Gated on `#TRANSPILE_PURE` marker.
- `clear emit-zig <path> -o <outdir>` CLI subcommand: walks .cht files
  and writes generated .zig to a mirrored tree without compiling.
- Used by the workflow to capture deterministic transpiler output.

- Gemfile: sorbet, sorbet-runtime, tapioca added to dev group.
- `sorbet/config` + `sorbet/rbi/clear-stubs.rbi` (minimal stubs to
  unblock `srb tc`).
- `tools/gen_attr_rbi.rb` (Prism-based RBI generator for AST attr_*
  declarations) → `sorbet/rbi/clear-attr-accessors.rbi` (91 classes,
  404 attr_* shims).
- Test pilot: importer.rb at `# typed: true` clean.

- `tools/respond_to_inventory.rb` — Prism walks src/ast/ast.rb to map
  AST classes → attrs (Struct members + include Locatable +
  attr_accessor + custom getters), then walks src/**/*.rb for every
  respond_to?(:X) site. Outputs CSVs and a summary md.
- `tools/respond_to_narrowing.rb` — per-site receiver-type classifier.
  For each respond_to? site, finds prior is_a? guards, case/when
  arms, .X assignments from Locatable attrs, and walker-block params.
  Classifies as ast_locatable / typed_specific /
  from_locatable_attr / walker_yielded / unknown. Drove Phase 1f's
  full_type sweep (40 sites, 1 false positive).
- `docs/agents/respond_to_inventory.md` — methodology + phased plan.

- TODO.md updated: P0 self-host prep #1 String/Symbol normalization
  folded into #2 (the schema work subsumes it).
- Two pure-deletion commits removed dead code unrelated to the main
  themes (4 dead methods debride flagged, dead defensive guards in
  escape_analysis).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants