Skip to content

Commit a7281f5

Browse files
author
nyash-codex
committed
cli: global --emit-mir-json early gate; selfhost: FlowEntryBox (emit-only) + FlowRunner (Mini‑VM); LocalSSA: after‑PHI materialize (ensure_calls/ensure_cond); harness: v1→v0 downgrade extern fallback; tools: build_and_run_wasm.sh; smokes: flow_runner + harness; docs: harness-first + CURRENT_TASK updates (drop tmp/*)
1 parent b099e74 commit a7281f5

46 files changed

Lines changed: 1299 additions & 88 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CURRENT_TASK_SELFHOST.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ Next actions
3636
- [x] Enable commit/push for selfhost branches (local hooks updated)
3737
- [x] PipelineV2: Apply LocalSSA.ensure_cond as final pass (fail‑safe)
3838
- [x] Add quick smokes for If(Compare) CFG and loop counter
39-
- [x] Verify Jump lowering and add docs pointers (quick/selfhost jump smokes; LLVM PHI harness smokes)
39+
- [x] Verify Jump lowering and add docs pointers (quick/selfhost jump smokes; LLVM PHI harness smokes)
40+
- [x] v1→v0 downgrade extern fallback (harness‑only): unresolved Global → externcall when NYASH_LLVM_DOWNGRADE_V1=1
41+
- Runner emit updated: src/runner/mir_json_emit.rs
42+
- New smoke: tools/smokes/v2/profiles/quick/llvm/harness_v1_downgrade_global_extern_compile_ok.sh
43+
- Fail‑Fast maintained for VM/AOT (unresolved Global → error)
4044

4145
Phase 15.7 — NyKernel (Option B) minimal AOT step
4246
- [x] Introduce `crates/hako_kernel` minimal static shim (C‑ABI stubs)
@@ -63,3 +67,14 @@ Addendum — 2025‑10‑01 (late)
6367
- tools/smokes/v2/profiles/quick/selfhost/selfhost_pipeline_v2_method_exec_vm.sh
6468
- tools/smokes/v2/profiles/quick/selfhost/selfhost_pipeline_v2_newbox_exec_vm.sh
6569
- Stage‑1 extractors: hardened to accept negatives/whitespace; emitters now accept string‑form args (e.g., "[5, 7]") and materialize ints. Follow‑up: verify PipelineV2 call path end‑to‑end; boundary smoke will be enabled after confirming args materialization.
70+
71+
Addendum — 2025‑10‑02
72+
- Runner/Flow minimal box化(emit-only入口&VM実行ヘルパ)
73+
- Added: `apps/selfhost-compiler/pipeline_v2/flow_entry.hako` (FlowEntryBox) — emit‑only entry, v0 / v1→v0 互換
74+
- Added: `apps/selfhost/vm/flow_runner.hako` (FlowRunner) — FlowEntry→Mini‑VM 実行の薄い箱
75+
- Mapped modules: `selfhost.compiler.pipeline_v2.flow_entry`, `selfhost.vm.flow_runner`(nyash.toml/hako.toml)
76+
- New smoke: `tools/smokes/v2/profiles/quick/selfhost/selfhost_flow_runner_return_int_vm.sh`(Return(Int 42)→exec=42)
77+
- LocalSSA 材化ポリシー整理(PHI直後に統一)
78+
- ensure_calls: 実装済(v0/v1のrecv/argsに対応)
79+
- ensure_cond: ブロック先頭→PHI直後に変更(copy位置を統一)
80+
- 既存 selfhost PipelineV2 smokesは NYASH_PIPELINE_V2=1 で回して緑を確認

apps/selfhost-compiler/builder/ssa/local.nyash

Lines changed: 258 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ static box LocalSSA {
176176
cur = obj_end
177177
continue
178178
}
179-
// Insert copy at the start of the block
179+
// Insert copy after PHI cluster at block head
180180
// Trace: capture block id of this branch's block
181181
local bid = 0
182182
// Find nearest '"id":<n>' before this block's instructions
@@ -187,9 +187,22 @@ static box LocalSSA {
187187
if bid_csv == "" { bid_csv = "" + bid } else { bid_csv = bid_csv + "," + bid }
188188
}
189189
}
190+
// Compute insertion point just after contiguous PHI nodes
191+
local insert_at = insts_start
192+
{
193+
local scan = insts_start
194+
loop(true) {
195+
local pphi = me._index_of_from(out, "\"op\":\"phi\"", scan)
196+
if pphi < 0 || pphi >= obj_start { break }
197+
local sphi = me._seek_obj_start(out, pphi)
198+
local ephi = me._seek_obj_end(out, sphi)
199+
insert_at = ephi
200+
scan = ephi
201+
}
202+
}
190203
local new_id = me._max_dst_id(out) + 1
191204
local copy_inst = "{\\\"op\\\":\\\"copy\\\",\\\"src\\\":" + digits + ",\\\"dst\\\":" + new_id + "},"
192-
out = out.substring(0, insts_start) + copy_inst + out.substring(insts_start, out.length())
205+
out = out.substring(0, insert_at) + copy_inst + out.substring(insert_at, out.length())
193206
t_copies = t_copies + 1
194207
// Rewrite branch cond to new_id within obj segment (adjust obj positions by insertion)
195208
local delta = copy_inst.length()
@@ -258,6 +271,249 @@ static box LocalSSA {
258271
return json_text
259272
}
260273

274+
// Experimental: call-site operand materialization (minimal)
275+
// Insert copy instructions after the PHI cluster of a block when call/method/new operands
276+
// (recv/args) are not defined earlier in the same block. This keeps operands locally materialized
277+
// without changing semantics. JSON is manipulated textually to avoid adding heavy dependencies.
278+
ensure_calls(json_text) {
279+
if json_text == null { return json_text }
280+
// Only operate on MIR(JSON) text; Stage‑1 remains unchanged
281+
if me._is_mir_json(json_text) == 0 { return json_text }
282+
local out = json_text
283+
local cur = 0
284+
loop(true) {
285+
// Find next call-like instruction (v0 and v1 tolerant)
286+
local p1 = me._index_of_from(out, "\"op\":\"call\"", cur)
287+
local p2 = me._index_of_from(out, "\"op\":\"boxcall\"", cur)
288+
local p3 = me._index_of_from(out, "\"op\":\"newbox\"", cur)
289+
local p4 = me._index_of_from(out, "\"op\":\"mir_call\"", cur)
290+
// choose earliest >=0
291+
local op_pos = -1
292+
local kind = 0 // 1=call,2=boxcall,3=newbox,4=mir_call
293+
if p1 >= 0 { op_pos = p1 kind = 1 }
294+
if p2 >= 0 && (op_pos < 0 || p2 < op_pos) { op_pos = p2 kind = 2 }
295+
if p3 >= 0 && (op_pos < 0 || p3 < op_pos) { op_pos = p3 kind = 3 }
296+
if p4 >= 0 && (op_pos < 0 || p4 < op_pos) { op_pos = p4 kind = 4 }
297+
if op_pos < 0 { break }
298+
299+
// Determine instruction object and enclosing block bounds
300+
local obj_start = me._seek_obj_start(out, op_pos)
301+
local obj_end = me._seek_obj_end(out, obj_start)
302+
local insts_start = me._block_insts_start(out, obj_start)
303+
if insts_start < 0 { cur = obj_end continue }
304+
305+
// Compute insertion point: after the contiguous PHI cluster at the block head
306+
local insert_at = insts_start
307+
local scan = insts_start
308+
loop(true) {
309+
local pphi = me._index_of_from(out, "\"op\":\"phi\"", scan)
310+
if pphi < 0 || pphi >= obj_start { break }
311+
local sphi = me._seek_obj_start(out, pphi)
312+
local ephi = me._seek_obj_end(out, sphi)
313+
insert_at = ephi
314+
scan = ephi
315+
}
316+
317+
// Inline helpers are expanded where needed(Nyash: avoid nested func capture for portability)
318+
319+
// V0: args array inside the instruction object
320+
if kind == 1 || kind == 2 || kind == 3 {
321+
// Receiver for boxcall
322+
if kind == 2 {
323+
local bk = "\"box\":"
324+
local bp = me._index_of_from(out, bk, obj_start)
325+
if bp >= 0 && bp < obj_end {
326+
local digits = me._read_digits(out, bp + bk.length())
327+
// Check if dst:<digits> exists before this instruction in the block
328+
local has_def = 0
329+
if digits != "" {
330+
local def_pat = "\"dst\":" + digits
331+
local s = insts_start
332+
loop(true) {
333+
local p = me._index_of_from(out, def_pat, s)
334+
if p < 0 || p >= obj_start { break }
335+
has_def = 1
336+
break
337+
}
338+
}
339+
if digits != "" && has_def == 0 {
340+
// Insert copy and rewrite box id
341+
local new_id = me._max_dst_id(out) + 1
342+
local copy_inst = "{\\\"op\\\":\\\"copy\\\",\\\"src\\\":" + digits + ",\\\"dst\\\":" + new_id + "},"
343+
out = out.substring(0, insert_at) + copy_inst + out.substring(insert_at, out.length())
344+
local delta = copy_inst.length()
345+
if insert_at <= obj_start { obj_start = obj_start + delta obj_end = obj_end + delta }
346+
// rewrite digits at bp
347+
local head = out.substring(0, bp + bk.length())
348+
local di = bp + bk.length()
349+
loop(true) {
350+
local ch = out.substring(di, di+1)
351+
if ch >= "0" && ch <= "9" { di = di + 1 } else { break }
352+
}
353+
local tail = out.substring(di, out.length())
354+
out = head + new_id + tail
355+
}
356+
}
357+
}
358+
// Args list
359+
local ak = me._index_of_from(out, "\"args\":[", obj_start)
360+
if ak >= 0 && ak < obj_end {
361+
// find end of args list (first closing ']' after ak)
362+
local i = ak + 8
363+
local rb = i
364+
loop(true) {
365+
local ch = out.substring(i, i+1)
366+
if ch == "]" || ch == "" { rb = i break }
367+
i = i + 1
368+
}
369+
// iterate over digits inside args
370+
local pos = ak + 8
371+
loop(pos < rb) {
372+
local ch = out.substring(pos, pos+1)
373+
if ch >= "0" && ch <= "9" {
374+
local digits = me._read_digits(out, pos)
375+
// materialize this operand if needed
376+
if digits != "" {
377+
// Check local def
378+
local has_def = 0
379+
local def_pat = "\"dst\":" + digits
380+
local s = insts_start
381+
loop(true) {
382+
local p = me._index_of_from(out, def_pat, s)
383+
if p < 0 || p >= obj_start { break }
384+
has_def = 1
385+
break
386+
}
387+
if has_def == 0 {
388+
local new_id = me._max_dst_id(out) + 1
389+
local copy_inst = "{\\\"op\\\":\\\"copy\\\",\\\"src\\\":" + digits + ",\\\"dst\\\":" + new_id + "},"
390+
out = out.substring(0, insert_at) + copy_inst + out.substring(insert_at, out.length())
391+
local delta = copy_inst.length()
392+
if insert_at <= obj_start { obj_start = obj_start + delta obj_end = obj_end + delta }
393+
// rewrite occurrence inside [ak..rb)
394+
local head = out.substring(0, ak)
395+
local mid = out.substring(ak, rb)
396+
local q = mid.indexOf(digits)
397+
if q >= 0 {
398+
mid = mid.substring(0, q) + new_id + mid.substring(q + digits.length(), mid.length())
399+
out = head + mid + out.substring(rb, out.length())
400+
// recompute bounds
401+
insts_start = me._block_insts_start(out, obj_start)
402+
insert_at = insts_start
403+
scan = insts_start
404+
loop(true) {
405+
local pphi2 = me._index_of_from(out, "\"op\":\"phi\"", scan)
406+
if pphi2 < 0 || pphi2 >= obj_start { break }
407+
local sphi2 = me._seek_obj_start(out, pphi2)
408+
local ephi2 = me._seek_obj_end(out, sphi2)
409+
insert_at = ephi2
410+
scan = ephi2
411+
}
412+
// refresh rb & pos relative to new string
413+
rb = me._index_of_from(out, "]", ak + 8)
414+
if rb < 0 { rb = obj_end }
415+
pos = ak + 8
416+
continue
417+
}
418+
}
419+
}
420+
// Move after this number (recompute rb due to potential rewrite)
421+
rb = me._index_of_from(out, "]", ak + 8)
422+
if rb < 0 { rb = obj_end }
423+
pos = ak + 8
424+
// advance to next non-digit to avoid infinite loop
425+
loop(true) {
426+
local c2 = out.substring(pos, pos+1)
427+
if !(c2 >= "0" && c2 <= "9") { break }
428+
pos = pos + 1
429+
}
430+
} else {
431+
pos = pos + 1
432+
}
433+
}
434+
}
435+
} else {
436+
// V1: mir_call has args in nested object
437+
local mk = "\"mir_call\":{"
438+
local mp = me._index_of_from(out, mk, obj_start)
439+
if mp >= 0 && mp < obj_end {
440+
local ak = me._index_of_from(out, "\"args\":[", mp)
441+
if ak >= 0 && ak < obj_end {
442+
local i = ak + 8
443+
local rb = i
444+
loop(true) {
445+
local ch = out.substring(i, i+1)
446+
if ch == "]" || ch == "" { rb = i break }
447+
i = i + 1
448+
}
449+
local pos = ak + 8
450+
loop(pos < rb) {
451+
local ch = out.substring(pos, pos+1)
452+
if ch >= "0" && ch <= "9" {
453+
local digits = me._read_digits(out, pos)
454+
// materialize this operand if needed
455+
if digits != "" {
456+
local has_def = 0
457+
local def_pat = "\"dst\":" + digits
458+
local s = insts_start
459+
loop(true) {
460+
local p = me._index_of_from(out, def_pat, s)
461+
if p < 0 || p >= obj_start { break }
462+
has_def = 1
463+
break
464+
}
465+
if has_def == 0 {
466+
local new_id = me._max_dst_id(out) + 1
467+
local copy_inst = "{\\\"op\\\":\\\"copy\\\",\\\"src\\\":" + digits + ",\\\"dst\\\":" + new_id + "},"
468+
out = out.substring(0, insert_at) + copy_inst + out.substring(insert_at, out.length())
469+
local delta = copy_inst.length()
470+
if insert_at <= obj_start { obj_start = obj_start + delta obj_end = obj_end + delta }
471+
local head = out.substring(0, ak)
472+
local mid = out.substring(ak, rb)
473+
local q = mid.indexOf(digits)
474+
if q >= 0 {
475+
mid = mid.substring(0, q) + new_id + mid.substring(q + digits.length(), mid.length())
476+
out = head + mid + out.substring(rb, out.length())
477+
insts_start = me._block_insts_start(out, obj_start)
478+
insert_at = insts_start
479+
scan = insts_start
480+
loop(true) {
481+
local pphi2 = me._index_of_from(out, "\"op\":\"phi\"", scan)
482+
if pphi2 < 0 || pphi2 >= obj_start { break }
483+
local sphi2 = me._seek_obj_start(out, pphi2)
484+
local ephi2 = me._seek_obj_end(out, sphi2)
485+
insert_at = ephi2
486+
scan = ephi2
487+
}
488+
rb = me._index_of_from(out, "]", ak + 8)
489+
if rb < 0 { rb = obj_end }
490+
pos = ak + 8
491+
continue
492+
}
493+
}
494+
}
495+
rb = me._index_of_from(out, "]", ak + 8)
496+
if rb < 0 { rb = obj_end }
497+
pos = ak + 8
498+
loop(true) {
499+
local c2 = out.substring(pos, pos+1)
500+
if !(c2 >= "0" && c2 <= "9") { break }
501+
pos = pos + 1
502+
}
503+
} else {
504+
pos = pos + 1
505+
}
506+
}
507+
}
508+
}
509+
}
510+
511+
// advance cursor beyond this instruction object
512+
cur = obj_end
513+
}
514+
return out
515+
}
516+
261517
// --- Trace API (既定OFF) ---
262518
trace_enable(on) {
263519
if on { me._trace_on = 1 } else { me._trace_on = 0 }

apps/selfhost-compiler/pipeline_v2/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Scope
66
- No new capabilities or extern calls are introduced in Phase 15.7 (spec invariant).
77

88
Boxes
9+
- FlowEntryBox (emit-only): entry to PipelineV2. Accepts Stage‑1 JSON and returns MIR(JSON).
910
- ExecutionPipelineBox: orchestrates ParserBox → EmitterBox; optional BackendBox tag only.
1011
- BackendBox (stub): records backend name ("vm"|"llvm"|"pyvm"); no execution.
1112
- MirBuilderBox (stub/IF): future lowering and optimization entry (not wired in Phase 15.7).
@@ -20,6 +21,7 @@ Responsibilities
2021
- 既存の emit_mir_flow.hako は段階的に委譲→削減(互換維持)
2122
- Dev観測: `NYASH_EMIT_TRACE=1` を想定した最小トレース(現状は無条件1行出力。最終JSON行は変わらず)
2223
- Execute: delegated to Rust Runner (parent→child). This directory must NOT perform execution.
24+
- Note: actual execution helper is provided under `apps/selfhost/vm/flow_runner.hako`.
2325

2426
Non-goals (Phase 15.7)
2527
- No extern/FFI invocations to call backends from Ny code.
@@ -36,6 +38,14 @@ Tracing (dev)
3638
- `PipelineV2.lower_stage1_to_mir_trace(ast_json, prefer_cfg, trace)` — when `trace==1`, emit boxes print a single-line `[emit] ...` before JSON.
3739
- ExecutionPipelineBox は emit-only 経路であり、trace の布告は Runner 引数透過で後段導入予定(既定OFF)。
3840

41+
JSON v1 (MirCall) — experimental
42+
- PipelineV2 provides an opt-in path to emit unified call form:
43+
- `PipelineV2.lower_stage1_to_mir_v1(ast_json, prefer_cfg)` — emits op:`mir_call` with `callee` payload (Global/Method/Constructor) and `args` array.
44+
- `PipelineV2.lower_stage1_to_mir_v1_compat(ast_json, prefer_cfg)` — emits v1 then adapts to legacy v0 (call/boxcall/newbox) via `selfhost.common.json.mir_v1_adapter`.
45+
- Notes
46+
- Mini‑VM (MirVmMin) tolerates `op:"mir_call"` by treating its result as 0 (shape-only). Use the compat path to execute on MirVmMin.
47+
- Default smokes remain on v0. Additional quick shape smokes exist under `tools/smokes/v2/profiles/quick/selfhost/*_v1_shape_vm.sh`.
48+
3949
Fail-Fast
4050
- If parsing returns null/empty, print an error to stderr and return non-zero.
4151
- Do not print non-JSON lines when `NYASH_JSON_ONLY=1`.

0 commit comments

Comments
 (0)