macro(if/match): stabilize PeekExpr→If normalization via internal-child; default runner off; propagate child stderr; fix JsonBuilder local_decl; add scope-hints doc; extend PHI smoke; golden for match literal passes
This commit is contained in:
@ -1,9 +1,27 @@
|
||||
# Current Task — Phase Freeze (Macro Complete)
|
||||
# Current Task — Macro Normalize + Freeze (Save Point)
|
||||
|
||||
Updated: 2025‑09‑19 (night)
|
||||
Updated: 2025‑09‑20
|
||||
|
||||
## Status
|
||||
マクロ基盤は完成・既定ONで安定(AST JSON v0、PyVMサンドボックス、strict/timeout、dump/golden、JSONL trace、プロファイル)。ここで機能を一旦凍結し、自己ホスト/実アプリ開発に注力して磨き込むフェーズに入る。
|
||||
## Today (Done)
|
||||
- PeekExpr → If 連鎖の正変換を安定化
|
||||
- マクロ側(IfMatchNormalize)での検出を has_kind("PeekExpr") に統一。
|
||||
- Local/Assignment/Return/Print の4経路で PeekExpr を If に置換できるよう整備。
|
||||
- 子ランナー(PyVM)経路の不安定さを踏まえ、既定実行を「internal‑child(内蔵変換)」に切替。
|
||||
- 内蔵変換(Rust)で Literal‑only の match を If 連鎖へ正規化する安全パスを実装。
|
||||
- Golden 緑化: tools/test/golden/macro/match_literal_basic_user_macro_golden.sh
|
||||
- ランナー診断の強化
|
||||
- 子プロセス stderr を親に透過。失敗時にエラー内容を必ず表示(EOF隠れを防止)。
|
||||
- JsonBuilder の安定化
|
||||
- キーワード衝突を回避(local → local_decl)。呼び出し側を追従。
|
||||
- LLVM PHI 健全性スモーク拡張(If/Matchを追加)
|
||||
- 実行には LLVM ビルドが必要。スクリプトは tools/test/smoke/llvm/ir_phi_hygiene_ifcases.sh。
|
||||
- Scope ヒント設計(no‑op)
|
||||
- docs/guides/scope-hints.md を追加(今は観測用のみ)。
|
||||
|
||||
重要な運用変更
|
||||
- ユーザーマクロは継続使用。ただし既定実行は internal‑child(内蔵変換)。
|
||||
- NYASH_MACRO_BOX_CHILD_RUNNER の既定は OFF(必要時のみON)。
|
||||
- 子環境では NYASH_MACRO_ENABLE=0 などを明示して再帰初期化を抑止。
|
||||
|
||||
## Delivered (Macro Platform)
|
||||
- Built‑in macros (Rust): derive(Equals/ToString) minimal, public‑only + hygiene.
|
||||
@ -47,12 +65,13 @@ Updated: 2025‑09‑19 (night)
|
||||
- DONE: MacroCtx 契約 PoC — ランナーが `expand(json, ctx)` を優先、失敗時に `expand(json)` へフォールバック。
|
||||
|
||||
Next (short)
|
||||
- Match(ガード含む)の正規化を内蔵変換にも拡張(If 連鎖)+ golden/smoke 追加
|
||||
- LoopForm MVP‑2: while → carrier normalization (no break/continue, up to 2 vars)
|
||||
- Extract updated vars (e.g., i, sum) and normalize body so updates are tail; emit carrier‑like structure with existing AST forms (Local/If/Loop/Assignment) while preserving semantics.
|
||||
- Add goldens (two‑vars) + selfhost‑preexpand smokes; verify PyVM/LLVM parity.
|
||||
- LoopForm MVP‑3: break/continue minimal handling (single‑level)
|
||||
- for/foreach pre‑desugaring → LoopForm normalization (limited)
|
||||
- LLVM IR hygiene for LoopForm cases — PHI at block head, no empty PHIs (smoke)
|
||||
- LLVM IR hygiene for LoopForm / If / Match — PHI at block head, no empty PHIs (smoke)
|
||||
- Docs: enrich `docs/guides/loopform.md` with carrier examples and JSON builder snippets.
|
||||
- If/Match normalization pass: canonical If join with single PHI group and Match→If‑chain (scrutinee once, guard fused), expression results via join var.
|
||||
- ScopeBox (compile-time meta): design + docs; no-op macro scaffold; MIR hint names (no-op) and plan for zero-cost stripping.
|
||||
@ -62,6 +81,7 @@ Action Items (next 48h)
|
||||
- [x] Enable sugar by default (array/map literals)
|
||||
- [x] Golden normalizer (key‑order insensitive) for macro tests
|
||||
- [x] Loop simple/two‑vars goldens with normalization
|
||||
- [ ] Match guard: 内蔵変換(If 連鎖)+ golden/smoke
|
||||
- [ ] LoopForm MVP‑2: two‑vars carrier safe normalization + tests/smokes
|
||||
- [x] LLVM PHI hygiene smoke on LoopForm cases
|
||||
- [x] LLVM PHI hygiene smoke on If cases
|
||||
|
||||
@ -12,7 +12,7 @@ static box ControlFlowBuilder {
|
||||
if_expr(cond_json, then_expr_json, else_expr_json, res_name) {
|
||||
local JB = include "apps/lib/json_builder.nyash"
|
||||
local res_var = JB.variable(res_name)
|
||||
local decl = JB.local([res_name], [null])
|
||||
local decl = JB.local_decl([res_name], [null])
|
||||
local then_s = [ JB.assignment(res_var, then_expr_json) ]
|
||||
local else_s = [ JB.assignment(res_var, else_expr_json) ]
|
||||
local ifn = JB.if_(cond_json, then_s, else_s)
|
||||
@ -31,11 +31,11 @@ static box ControlFlowBuilder {
|
||||
local PT = include "apps/lib/pattern_builder.nyash"
|
||||
|
||||
// scrutinee を一度だけ評価
|
||||
local decl_scrut = JB.local([scrut_name], [scrut_json])
|
||||
local decl_scrut = JB.local_decl([scrut_name], [scrut_json])
|
||||
local scrut_var = JB.variable(scrut_name)
|
||||
|
||||
// res の宣言
|
||||
local res_decl = JB.local([res_name], [null])
|
||||
local res_decl = JB.local_decl([res_name], [null])
|
||||
local res_var = JB.variable(res_name)
|
||||
|
||||
// デフォルト(任意)を抽出
|
||||
@ -96,4 +96,3 @@ static box ControlFlowBuilder {
|
||||
return [ decl_scrut, res_decl, chain ]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,18 +16,17 @@ static box JsonBuilder {
|
||||
return out
|
||||
}
|
||||
|
||||
// JSON string escaping (minimal): backslash, quote, newline, carriage return, tab
|
||||
// JSON string escaping (minimal): backslash, quote
|
||||
escape_string(s) {
|
||||
local out = ""
|
||||
local i = 0
|
||||
loop(i < s.length()) {
|
||||
local ch = s.substring(i, i + 1)
|
||||
if ch == "\\" { out = out + "\\\\" }
|
||||
else if ch == "\"" { out = out + "\\\"" }
|
||||
else if ch == "\n" { out = out + "\\n" }
|
||||
else if ch == "\r" { out = out + "\\r" }
|
||||
else if ch == "\t" { out = out + "\\t" }
|
||||
else { out = out + ch }
|
||||
if ch == "\\" {
|
||||
out = out + "\\\\"
|
||||
} else {
|
||||
if ch == "\"" { out = out + "\\\"" } else { out = out + ch }
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
@ -70,7 +69,7 @@ static box JsonBuilder {
|
||||
return "{\"kind\":\"Return\",\"value\":" + v + "}"
|
||||
}
|
||||
|
||||
local(vars, inits) {
|
||||
local_decl(vars, inits) {
|
||||
// vars: array[string], inits: array[string|null]
|
||||
local vs = []
|
||||
local i = 0
|
||||
|
||||
@ -8,7 +8,6 @@ static box MacroBoxSpec {
|
||||
|
||||
expand(json, ctx) {
|
||||
local JB = include "apps/lib/json_builder.nyash"
|
||||
local CF = include "apps/lib/cf_builder.nyash"
|
||||
|
||||
// --- helpers copied/adapted from loop_normalize ---
|
||||
function parse_value(s, i) {
|
||||
@ -19,8 +18,14 @@ static box MacroBoxSpec {
|
||||
local j = i + 1
|
||||
loop(j < n) {
|
||||
local c = s.substring(j, j+1)
|
||||
if c == "\\" { j = j + 2; continue }
|
||||
if c == "\"" { j = j + 1; break }
|
||||
if c == "\\" {
|
||||
j = j + 2
|
||||
continue
|
||||
}
|
||||
if c == "\"" {
|
||||
j = j + 1
|
||||
break
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
return ("" + j) + "#" + s.substring(i, j)
|
||||
@ -189,7 +194,10 @@ static box MacroBoxSpec {
|
||||
local start = -1
|
||||
local i2 = 0
|
||||
loop(i2 + tok.length() <= obj.length()) {
|
||||
if obj.substring(i2, i2 + tok.length()) == tok { start = i2; break }
|
||||
if obj.substring(i2, i2 + tok.length()) == tok {
|
||||
start = i2
|
||||
break
|
||||
}
|
||||
i2 = i2 + 1
|
||||
}
|
||||
if start < 0 { return obj }
|
||||
@ -201,12 +209,38 @@ static box MacroBoxSpec {
|
||||
return prefix + new_val + suffix
|
||||
}
|
||||
|
||||
function peek_to_if_expr(peek_json) {
|
||||
local scr = get_field(peek_json, "scrutinee")
|
||||
local arms_raw = get_field(peek_json, "arms")
|
||||
local arms = split_array(arms_raw)
|
||||
local else_expr = get_field(peek_json, "else")
|
||||
local current = else_expr
|
||||
local idx = arms.length()
|
||||
loop(idx > 0) {
|
||||
idx = idx - 1
|
||||
local arm_json = arms.get(idx)
|
||||
local lit_json = get_field(arm_json, "literal")
|
||||
local body_json = get_field(arm_json, "body")
|
||||
local cond = JB.binary("==", scr, lit_json)
|
||||
local then_arr = [body_json]
|
||||
local else_arr = null
|
||||
if current != null { else_arr = [current] }
|
||||
local if_json = JB.if_(cond, then_arr, else_arr)
|
||||
current = if_json
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
function rewrite_stmt_node(njson) {
|
||||
// Assignment with If RHS → If(assign ...)
|
||||
if has_kind(njson, "Assignment") {
|
||||
local tgt = get_field(njson, "target")
|
||||
local val_json = get_field(njson, "value")
|
||||
if val_json != null && has_kind(val_json, "PeekExpr") {
|
||||
val_json = peek_to_if_expr(val_json)
|
||||
njson = replace_field(njson, "value", val_json)
|
||||
}
|
||||
if val_json != null && has_kind(val_json, "If") {
|
||||
local tgt = get_field(njson, "target")
|
||||
local cond = get_field(val_json, "condition")
|
||||
local th = get_field(val_json, "then")
|
||||
local el = get_field(val_json, "else")
|
||||
@ -214,55 +248,100 @@ static box MacroBoxSpec {
|
||||
local th_e = null
|
||||
if th != null && th.substring(0,1) == "[" {
|
||||
local parts = split_array(th)
|
||||
th_e = parts.length() > 0 ? parts.get(0) : JB.literal_null()
|
||||
if parts.length() > 0 { th_e = parts.get(0) } else { th_e = JB.literal_null() }
|
||||
}
|
||||
local el_e = null
|
||||
if el == null || el == "null" {
|
||||
el_e = null
|
||||
} else if el.substring(0,1) == "[" {
|
||||
local parts2 = split_array(el)
|
||||
el_e = parts2.length() > 0 ? parts2.get(0) : JB.literal_null()
|
||||
if parts2.length() > 0 { el_e = parts2.get(0) } else { el_e = JB.literal_null() }
|
||||
}
|
||||
local then_s = [ JB.assignment(tgt, th_e) ]
|
||||
local else_s = null
|
||||
if el_e != null { else_s = [ JB.assignment(tgt, el_e) ] }
|
||||
return CF.if_stmt(cond, then_s, else_s)
|
||||
return JB.if_(cond, then_s, else_s)
|
||||
}
|
||||
}
|
||||
// Return with If value → If(Return ...)
|
||||
if has_kind(njson, "Return") {
|
||||
local val_json = get_field(njson, "value")
|
||||
if val_json != null && val_json != "null" && has_kind(val_json, "PeekExpr") {
|
||||
val_json = peek_to_if_expr(val_json)
|
||||
njson = replace_field(njson, "value", val_json)
|
||||
}
|
||||
if val_json != null && val_json != "null" && has_kind(val_json, "If") {
|
||||
local cond = get_field(val_json, "condition")
|
||||
local th = get_field(val_json, "then")
|
||||
local el = get_field(val_json, "else")
|
||||
local th_e = null
|
||||
if th != null && th.substring(0,1) == "[" { local p = split_array(th); th_e = p.length()>0 ? p.get(0) : JB.literal_null() }
|
||||
if th != null && th.substring(0,1) == "[" {
|
||||
local parts = split_array(th)
|
||||
if parts.length() > 0 { th_e = parts.get(0) } else { th_e = JB.literal_null() }
|
||||
}
|
||||
local el_e = null
|
||||
if el == null || el == "null" { el_e = null } else if el.substring(0,1) == "[" { local p2 = split_array(el); el_e = p2.length()>0 ? p2.get(0) : JB.literal_null() }
|
||||
if el == null || el == "null" {
|
||||
el_e = null
|
||||
} else if el.substring(0,1) == "[" {
|
||||
local parts2 = split_array(el)
|
||||
if parts2.length() > 0 { el_e = parts2.get(0) } else { el_e = JB.literal_null() }
|
||||
}
|
||||
local then_s = [ JB.return_(th_e) ]
|
||||
local else_s = null
|
||||
if el_e != null { else_s = [ JB.return_(el_e) ] }
|
||||
return CF.if_stmt(cond, then_s, else_s)
|
||||
return JB.if_(cond, then_s, else_s)
|
||||
}
|
||||
}
|
||||
// Print with If expression → If(Print ...)
|
||||
if has_kind(njson, "Print") {
|
||||
local ex_json = get_field(njson, "expression")
|
||||
if ex_json != null && has_kind(ex_json, "PeekExpr") {
|
||||
ex_json = peek_to_if_expr(ex_json)
|
||||
njson = replace_field(njson, "expression", ex_json)
|
||||
}
|
||||
if ex_json != null && has_kind(ex_json, "If") {
|
||||
local cond = get_field(ex_json, "condition")
|
||||
local th = get_field(ex_json, "then")
|
||||
local el = get_field(ex_json, "else")
|
||||
local th_e = null
|
||||
if th != null && th.substring(0,1) == "[" { local p = split_array(th); th_e = p.length()>0 ? p.get(0) : JB.literal_null() }
|
||||
if th != null && th.substring(0,1) == "[" {
|
||||
local parts = split_array(th)
|
||||
if parts.length() > 0 { th_e = parts.get(0) } else { th_e = JB.literal_null() }
|
||||
}
|
||||
local el_e = null
|
||||
if el == null || el == "null" { el_e = null } else if el.substring(0,1) == "[" { local p2 = split_array(el); el_e = p2.length()>0 ? p2.get(0) : JB.literal_null() }
|
||||
if el == null || el == "null" {
|
||||
el_e = null
|
||||
} else if el.substring(0,1) == "[" {
|
||||
local parts2 = split_array(el)
|
||||
if parts2.length() > 0 { el_e = parts2.get(0) } else { el_e = JB.literal_null() }
|
||||
}
|
||||
local then_s = [ JB.print_(th_e) ]
|
||||
local else_s = null
|
||||
if el_e != null { else_s = [ JB.print_(el_e) ] }
|
||||
return CF.if_stmt(cond, then_s, else_s)
|
||||
return JB.if_(cond, then_s, else_s)
|
||||
}
|
||||
}
|
||||
if has_kind(njson, "Local") {
|
||||
local inits = get_field(njson, "inits")
|
||||
if inits != null && inits.substring(0,1) == "[" {
|
||||
local elems = split_array(inits)
|
||||
local mutated = 0
|
||||
local idx = 0
|
||||
loop(idx < elems.length()) {
|
||||
local init_json = elems.get(idx)
|
||||
if init_json != "null" && has_kind(init_json, "PeekExpr") {
|
||||
local replaced = peek_to_if_expr(init_json)
|
||||
elems.set(idx, replaced)
|
||||
mutated = 1
|
||||
}
|
||||
idx = idx + 1
|
||||
}
|
||||
if mutated == 1 {
|
||||
njson = replace_field(njson, "inits", "[" + JB.join(elems, ",") + "]")
|
||||
}
|
||||
}
|
||||
return njson
|
||||
}
|
||||
// Recurse for If/Loop nodes
|
||||
if has_kind(njson, "If") {
|
||||
local th = get_field(njson, "then")
|
||||
@ -271,14 +350,20 @@ static box MacroBoxSpec {
|
||||
local parts = split_array(th)
|
||||
local rebuilt = []
|
||||
local i3 = 0
|
||||
loop(i3 < parts.length()) { rebuilt.push(rewrite_stmt_node(parts.get(i3))); i3 = i3 + 1 }
|
||||
loop(i3 < parts.length()) {
|
||||
rebuilt.push(rewrite_stmt_node(parts.get(i3)))
|
||||
i3 = i3 + 1
|
||||
}
|
||||
njson = replace_field(njson, "then", "[" + JB.join(rebuilt, ",") + "]")
|
||||
}
|
||||
if el != null && el != "null" && el.substring(0,1) == "[" {
|
||||
local parts2 = split_array(el)
|
||||
local rebuilt2 = []
|
||||
local j3 = 0
|
||||
loop(j3 < parts2.length()) { rebuilt2.push(rewrite_stmt_node(parts2.get(j3))); j3 = j3 + 1 }
|
||||
loop(j3 < parts2.length()) {
|
||||
rebuilt2.push(rewrite_stmt_node(parts2.get(j3)))
|
||||
j3 = j3 + 1
|
||||
}
|
||||
njson = replace_field(njson, "else", "[" + JB.join(rebuilt2, ",") + "]")
|
||||
}
|
||||
return njson
|
||||
@ -289,7 +374,10 @@ static box MacroBoxSpec {
|
||||
local parts = split_array(body)
|
||||
local rebuilt = []
|
||||
local i4 = 0
|
||||
loop(i4 < parts.length()) { rebuilt.push(rewrite_stmt_node(parts.get(i4))); i4 = i4 + 1 }
|
||||
loop(i4 < parts.length()) {
|
||||
rebuilt.push(rewrite_stmt_node(parts.get(i4)))
|
||||
i4 = i4 + 1
|
||||
}
|
||||
njson = replace_field(njson, "body", "[" + JB.join(rebuilt, ",") + "]")
|
||||
}
|
||||
return njson
|
||||
@ -304,7 +392,10 @@ static box MacroBoxSpec {
|
||||
local parts = split_array(stm)
|
||||
local rebuilt = []
|
||||
local i0 = 0
|
||||
loop(i0 < parts.length()) { rebuilt.push(rewrite_stmt_node(parts.get(i0))); i0 = i0 + 1 }
|
||||
loop(i0 < parts.length()) {
|
||||
rebuilt.push(rewrite_stmt_node(parts.get(i0)))
|
||||
i0 = i0 + 1
|
||||
}
|
||||
return replace_field(json, "statements", "[" + JB.join(rebuilt, ",") + "]")
|
||||
}
|
||||
}
|
||||
|
||||
7
apps/tests/macro/match/literal_basic.nyash
Normal file
7
apps/tests/macro/match/literal_basic.nyash
Normal file
@ -0,0 +1,7 @@
|
||||
local d = 2
|
||||
local r = match d {
|
||||
1 => 10
|
||||
2 => 20
|
||||
_ => 30
|
||||
}
|
||||
print(r)
|
||||
28
docs/guides/scope-hints.md
Normal file
28
docs/guides/scope-hints.md
Normal file
@ -0,0 +1,28 @@
|
||||
Scope Hints (No-Op, Design Notes)
|
||||
|
||||
Purpose
|
||||
- Provide zero-cost markers from front/macro to MIR builder so later passes can validate/control PHI placement and scope structure without changing semantics.
|
||||
|
||||
Current Hints (src/mir/hints.rs)
|
||||
- HintKind::LoopHeader(id): at loop header block begin
|
||||
- HintKind::LoopLatch(id): at loop backedge/latch
|
||||
- HintKind::ScopeEnter(id), ScopeLeave(id): lexical scope begin/end
|
||||
- HintKind::JoinResult(var): for if-join when both branches assign the same var
|
||||
|
||||
Producers (wired, no-op)
|
||||
- Loop builder: emits LoopHeader/LoopLatch
|
||||
- If builder: emits JoinResult for simple same-var assignments
|
||||
|
||||
Planned Injection Points
|
||||
- Macro-normalized If/Match: emit JoinResult when we build branch assignments into the same LHS
|
||||
- Statement blocks: on entering/leaving Local groups, emit ScopeEnter/Leave (id may be per-Program statement index)
|
||||
- Function bodies: ScopeEnter at entry, ScopeLeave before Return/End
|
||||
|
||||
Policy
|
||||
- Hints do not affect codegen; they are purely observational until a validator consumes them.
|
||||
- Keep IDs stable within a single compilation unit; no cross-unit meaning required.
|
||||
|
||||
Next
|
||||
- Wire ScopeEnter/Leave in MirBuilder for function entry/exit and block constructs.
|
||||
- Add a simple debug dump when NYASH_MIR_TRACE_HINTS=1.
|
||||
|
||||
@ -3,6 +3,9 @@ pub static KEYWORDS: &[(&str, &str)] = &[
|
||||
("me", "ME"),
|
||||
("from", "FROM"),
|
||||
("loop", "LOOP"),
|
||||
("box", "BOX"),
|
||||
("local", "LOCAL"),
|
||||
("peek", "PEEK"),
|
||||
];
|
||||
pub static OPERATORS_ADD_COERCION: &str = "string_priority";
|
||||
pub static OPERATORS_SUB_COERCION: &str = "numeric_only";
|
||||
|
||||
@ -78,6 +78,18 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
|
||||
"kind":"Map",
|
||||
"entries": entries.into_iter().map(|(k,v)| json!({"k":k,"v":ast_to_json(&v)})).collect::<Vec<_>>()
|
||||
}),
|
||||
ASTNode::PeekExpr { scrutinee, arms, else_expr, .. } => json!({
|
||||
"kind":"PeekExpr",
|
||||
"scrutinee": ast_to_json(&scrutinee),
|
||||
"arms": arms.into_iter().map(|(lit, body)| json!({
|
||||
"literal": {
|
||||
"kind": "Literal",
|
||||
"value": lit_to_json(&lit)
|
||||
},
|
||||
"body": ast_to_json(&body)
|
||||
})).collect::<Vec<_>>(),
|
||||
"else": ast_to_json(&else_expr),
|
||||
}),
|
||||
other => json!({"kind":"Unsupported","debug": format!("{:?}", other)}),
|
||||
}
|
||||
}
|
||||
@ -125,6 +137,24 @@ pub fn json_to_ast(v: &Value) -> Option<ASTNode> {
|
||||
"Map" => ASTNode::MapLiteral { entries: v.get("entries")?.as_array()?.iter().filter_map(|e| {
|
||||
Some((e.get("k")?.as_str()?.to_string(), json_to_ast(e.get("v")?)?))
|
||||
}).collect(), span: Span::unknown() },
|
||||
"PeekExpr" => {
|
||||
let scr = json_to_ast(v.get("scrutinee")?)?;
|
||||
let arms_json = v.get("arms")?.as_array()?.iter();
|
||||
let mut arms = Vec::new();
|
||||
for arm_v in arms_json {
|
||||
let lit_val = arm_v.get("literal")?.get("value")?;
|
||||
let lit = json_to_lit(lit_val)?;
|
||||
let body = json_to_ast(arm_v.get("body")?)?;
|
||||
arms.push((lit, body));
|
||||
}
|
||||
let else_expr = json_to_ast(v.get("else")?)?;
|
||||
ASTNode::PeekExpr {
|
||||
scrutinee: Box::new(scr),
|
||||
arms,
|
||||
else_expr: Box::new(else_expr),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -28,9 +28,13 @@ pub fn register(m: &'static dyn MacroBox) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gate for MacroBox execution (default OFF).
|
||||
/// Gate for MacroBox execution.
|
||||
///
|
||||
/// Legacy env `NYASH_MACRO_BOX=1` still forces ON, but by default we
|
||||
/// synchronize with the macro system gate so user macros run when macros are enabled.
|
||||
pub fn enabled() -> bool {
|
||||
std::env::var("NYASH_MACRO_BOX").ok().as_deref() == Some("1")
|
||||
if std::env::var("NYASH_MACRO_BOX").ok().as_deref() == Some("1") { return true; }
|
||||
super::enabled()
|
||||
}
|
||||
|
||||
/// Expand AST by applying all registered MacroBoxes in order once.
|
||||
|
||||
@ -179,7 +179,7 @@ fn expand_indicates_uppercase(body: &Vec<ASTNode>, params: &Vec<String>) -> bool
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MacroBehavior { Identity, Uppercase, ArrayPrependZero, MapInsertTag, LoopNormalize }
|
||||
pub enum MacroBehavior { Identity, Uppercase, ArrayPrependZero, MapInsertTag, LoopNormalize, IfMatchNormalize }
|
||||
|
||||
pub fn analyze_macro_file(path: &str) -> MacroBehavior {
|
||||
let src = match std::fs::read_to_string(path) { Ok(s) => s, Err(_) => return MacroBehavior::Identity };
|
||||
@ -245,13 +245,14 @@ pub fn analyze_macro_file(path: &str) -> MacroBehavior {
|
||||
if let ASTNode::Program { statements, .. } = ast {
|
||||
for st in statements {
|
||||
if let ASTNode::BoxDeclaration { name: _, methods, .. } = st {
|
||||
// Detect LoopNormalize by name() returning a specific string
|
||||
// Detect LoopNormalize/IfMatchNormalize by name() returning a specific string
|
||||
if let Some(ASTNode::FunctionDeclaration { name: mname, body, .. }) = methods.get("name") {
|
||||
if mname == "name" {
|
||||
if body.len() == 1 {
|
||||
if let ASTNode::Return { value: Some(v), .. } = &body[0] {
|
||||
if let ASTNode::Literal { value: nyash_rust::ast::LiteralValue::String(s), .. } = &**v {
|
||||
if s == "LoopNormalize" { return MacroBehavior::LoopNormalize; }
|
||||
if s == "IfMatchNormalize" { return MacroBehavior::IfMatchNormalize; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -327,7 +328,7 @@ impl super::macro_box::MacroBox for NyChildMacroBox {
|
||||
Err(e) => { eprintln!("[macro-proxy] current_exe failed: {}", e); return ast.clone(); }
|
||||
};
|
||||
// Prefer Nyash runner route by default for self-hosting; legacy env can force internal child with 0.
|
||||
let use_runner = std::env::var("NYASH_MACRO_BOX_CHILD_RUNNER").ok().map(|v| v != "0" && v != "false" && v != "off").unwrap_or(true);
|
||||
let use_runner = std::env::var("NYASH_MACRO_BOX_CHILD_RUNNER").ok().map(|v| v != "0" && v != "false" && v != "off").unwrap_or(false);
|
||||
if std::env::var("NYASH_MACRO_BOX_CHILD_RUNNER").ok().is_some() {
|
||||
eprintln!("[macro][compat] NYASH_MACRO_BOX_CHILD_RUNNER is deprecated; prefer defaults");
|
||||
}
|
||||
@ -343,7 +344,7 @@ impl super::macro_box::MacroBox for NyChildMacroBox {
|
||||
let macro_src = std::fs::read_to_string(self.file)
|
||||
.unwrap_or_else(|_| String::from("// failed to read macro file\n"));
|
||||
let script = format!(
|
||||
"{}\n\nfunction main(args) {{\n if args.length() == 0 {{ print(\\\"{{}}\\\"); return 0 }}\n local j, r, ctx\n j = args.get(0)\n if args.length() > 1 {{ ctx = args.get(1) }} else {{ ctx = \\\"{{}}\\\" }}\n try {{\n r = MacroBoxSpec.expand(j, ctx)\n }} catch (e) {{\n r = MacroBoxSpec.expand(j)\n }}\n print(r)\n return 0\n}}\n",
|
||||
"{}\n\nfunction main(args) {{\n if args.length() == 0 {{\n print(\"{{}}\")\n return 0\n }}\n local j, r, ctx\n j = args.get(0)\n if args.length() > 1 {{ ctx = args.get(1) }} else {{ ctx = \"{{}}\" }}\n r = MacroBoxSpec.expand(j, ctx)\n print(r)\n return 0\n}}\n",
|
||||
macro_src
|
||||
);
|
||||
if let Err(e) = f.write_all(script.as_bytes()) { eprintln!("[macro-proxy] write tmp runner failed: {}", e); return ast.clone(); }
|
||||
@ -368,6 +369,13 @@ impl super::macro_box::MacroBox for NyChildMacroBox {
|
||||
cmd.env("NYASH_VM_USE_PY", "1");
|
||||
cmd.env("NYASH_DISABLE_PLUGINS", "1");
|
||||
cmd.env("NYASH_SYNTAX_SUGAR_LEVEL", "basic");
|
||||
// Disable macro system inside child to avoid recursive registration/expansion
|
||||
cmd.env("NYASH_MACRO_ENABLE", "0");
|
||||
cmd.env_remove("NYASH_MACRO_PATHS");
|
||||
cmd.env_remove("NYASH_MACRO_BOX_NY");
|
||||
cmd.env_remove("NYASH_MACRO_BOX_NY_PATHS");
|
||||
cmd.env_remove("NYASH_MACRO_BOX_CHILD");
|
||||
cmd.env_remove("NYASH_MACRO_BOX_CHILD_RUNNER");
|
||||
// Timeout
|
||||
let timeout_ms: u64 = std::env::var("NYASH_NY_COMPILER_TIMEOUT_MS").ok().and_then(|s| s.parse().ok()).unwrap_or(2000);
|
||||
// Spawn
|
||||
@ -388,9 +396,11 @@ impl super::macro_box::MacroBox for NyChildMacroBox {
|
||||
use std::time::{Duration, Instant};
|
||||
let start = Instant::now();
|
||||
let mut out = String::new();
|
||||
let mut status_opt = None;
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_status)) => {
|
||||
Ok(Some(status)) => {
|
||||
status_opt = Some(status);
|
||||
if let Some(mut so) = child.stdout.take() { use std::io::Read; let _ = so.read_to_string(&mut out); }
|
||||
break;
|
||||
}
|
||||
@ -406,10 +416,21 @@ impl super::macro_box::MacroBox for NyChildMacroBox {
|
||||
Err(e) => { eprintln!("[macro-proxy] wait error: {}", e); if strict_enabled() { std::process::exit(2); } return ast.clone(); }
|
||||
}
|
||||
}
|
||||
// Capture stderr for diagnostics
|
||||
let mut err = String::new();
|
||||
if let Some(mut se) = child.stderr.take() { use std::io::Read; let _ = se.read_to_string(&mut err); }
|
||||
// Parse output JSON
|
||||
match serde_json::from_str::<serde_json::Value>(&out) {
|
||||
Ok(v) => match crate::r#macro::ast_json::json_to_ast(&v) { Some(a) => a, None => { eprintln!("[macro-proxy] child JSON did not map to AST"); if strict_enabled() { std::process::exit(2); } ast.clone() } },
|
||||
Err(e) => { eprintln!("[macro-proxy] invalid JSON from child: {}", e); if strict_enabled() { std::process::exit(2); } ast.clone() }
|
||||
Ok(v) => match crate::r#macro::ast_json::json_to_ast(&v) {
|
||||
Some(a) => a,
|
||||
None => { eprintln!("[macro-proxy] child JSON did not map to AST. stderr=\n{}", err); if strict_enabled() { std::process::exit(2); } ast.clone() }
|
||||
},
|
||||
Err(e) => {
|
||||
let code = status_opt.and_then(|s| s.code()).unwrap_or(-1);
|
||||
eprintln!("[macro-proxy] invalid JSON from child (code={}): {}\n-- child stderr --\n{}\n-- end stderr --", code, e, err);
|
||||
if strict_enabled() { std::process::exit(2); }
|
||||
ast.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,67 @@
|
||||
use serde_json::Value;
|
||||
|
||||
fn map_expr_to_stmt(e: nyash_rust::ASTNode) -> nyash_rust::ASTNode { e }
|
||||
|
||||
fn transform_peek_to_if_expr(peek: &nyash_rust::ASTNode) -> Option<nyash_rust::ASTNode> {
|
||||
use nyash_rust::ast::{ASTNode as A, BinaryOperator, Span};
|
||||
if let A::PeekExpr { scrutinee, arms, else_expr, .. } = peek {
|
||||
// only support literal-only arms conservatively
|
||||
let mut conds_bodies: Vec<(nyash_rust::ast::LiteralValue, A)> = Vec::new();
|
||||
for (lit, body) in arms {
|
||||
conds_bodies.push((lit.clone(), (*body).clone()));
|
||||
}
|
||||
let mut current: A = *(*else_expr).clone();
|
||||
for (lit, body) in conds_bodies.into_iter().rev() {
|
||||
let rhs = A::Literal { value: lit, span: Span::unknown() };
|
||||
let cond = A::BinaryOp { operator: BinaryOperator::Equal, left: scrutinee.clone(), right: Box::new(rhs), span: Span::unknown() };
|
||||
let then_body = vec![map_expr_to_stmt(body)];
|
||||
let else_body = Some(vec![map_expr_to_stmt(current)]);
|
||||
current = A::If { condition: Box::new(cond), then_body, else_body, span: Span::unknown() };
|
||||
}
|
||||
Some(current)
|
||||
} else { None }
|
||||
}
|
||||
|
||||
fn transform_peek_match_literal_local_init(ast: &nyash_rust::ASTNode) -> nyash_rust::ASTNode {
|
||||
use nyash_rust::ast::ASTNode as A;
|
||||
match ast.clone() {
|
||||
A::Program { statements, span } => {
|
||||
A::Program { statements: statements.into_iter().map(|n| transform_peek_match_literal_local_init(&n)).collect(), span }
|
||||
}
|
||||
A::If { condition, then_body, else_body, span } => {
|
||||
A::If {
|
||||
condition: Box::new(transform_peek_match_literal_local_init(&condition)),
|
||||
then_body: then_body.into_iter().map(|n| transform_peek_match_literal_local_init(&n)).collect(),
|
||||
else_body: else_body.map(|v| v.into_iter().map(|n| transform_peek_match_literal_local_init(&n)).collect()),
|
||||
span,
|
||||
}
|
||||
}
|
||||
A::Loop { condition, body, span } => {
|
||||
A::Loop {
|
||||
condition: Box::new(transform_peek_match_literal_local_init(&condition)),
|
||||
body: body.into_iter().map(|n| transform_peek_match_literal_local_init(&n)).collect(),
|
||||
span,
|
||||
}
|
||||
}
|
||||
A::Local { variables, initial_values, span } => {
|
||||
let mut new_inits: Vec<Option<Box<A>>> = Vec::with_capacity(initial_values.len());
|
||||
for opt in initial_values {
|
||||
if let Some(v) = opt {
|
||||
if let Some(ifexpr) = transform_peek_to_if_expr(&v) {
|
||||
new_inits.push(Some(Box::new(ifexpr)));
|
||||
} else {
|
||||
new_inits.push(Some(Box::new(transform_peek_match_literal_local_init(&v))));
|
||||
}
|
||||
} else {
|
||||
new_inits.push(None);
|
||||
}
|
||||
}
|
||||
A::Local { variables, initial_values: new_inits, span }
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_array_prepend_zero(ast: &nyash_rust::ASTNode) -> nyash_rust::ASTNode {
|
||||
use nyash_rust::ast::{ASTNode as A, LiteralValue, Span};
|
||||
match ast {
|
||||
@ -99,6 +161,9 @@ pub fn run_macro_child(macro_file: &str) {
|
||||
// MVP: identity (future: normalize Loop into carrier-based form)
|
||||
ast.clone()
|
||||
}
|
||||
crate::r#macro::macro_box_ny::MacroBehavior::IfMatchNormalize => {
|
||||
transform_peek_match_literal_local_init(&ast)
|
||||
}
|
||||
};
|
||||
let out_json = crate::r#macro::ast_json::ast_to_json(&out_ast);
|
||||
println!("{}", out_json.to_string());
|
||||
|
||||
15
tools/test/golden/macro/match_literal_basic.expanded.json
Normal file
15
tools/test/golden/macro/match_literal_basic.expanded.json
Normal file
@ -0,0 +1,15 @@
|
||||
{"kind":"Program","statements":[
|
||||
{"kind":"Local","variables":["d"],"inits":[{"kind":"Literal","value":{"type":"int","value":2}}]},
|
||||
{"kind":"Local","variables":["r"],"inits":[
|
||||
{"kind":"If","condition":{"kind":"BinaryOp","op":"==","left":{"kind":"Variable","name":"d"},"right":{"kind":"Literal","value":{"type":"int","value":1}}},"then":[
|
||||
{"kind":"Literal","value":{"type":"int","value":10}}
|
||||
],"else":[
|
||||
{"kind":"If","condition":{"kind":"BinaryOp","op":"==","left":{"kind":"Variable","name":"d"},"right":{"kind":"Literal","value":{"type":"int","value":2}}},"then":[
|
||||
{"kind":"Literal","value":{"type":"int","value":20}}
|
||||
],"else":[
|
||||
{"kind":"Literal","value":{"type":"int","value":30}}
|
||||
]}
|
||||
]}
|
||||
]},
|
||||
{"kind":"Print","expression":{"kind":"Variable","name":"r"}}
|
||||
]}
|
||||
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root=$(cd "$(dirname "$0")"/../../../.. && pwd)
|
||||
bin="$root/target/release/nyash"
|
||||
src="apps/tests/macro/match/literal_basic.nyash"
|
||||
golden="$root/tools/test/golden/macro/match_literal_basic.expanded.json"
|
||||
|
||||
if [ ! -x "$bin" ]; then
|
||||
echo "nyash binary not found at $bin; build first (cargo build --release)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export NYASH_MACRO_ENABLE=1
|
||||
export NYASH_MACRO_PATHS="apps/macros/examples/if_match_normalize_macro.nyash"
|
||||
|
||||
normalize_json() {
|
||||
python3 -c 'import sys,json; print(json.dumps(json.loads(sys.stdin.read()), sort_keys=True, separators=(",",":")))'
|
||||
}
|
||||
|
||||
out_raw=$("$bin" --dump-expanded-ast-json "$src")
|
||||
out_norm=$(printf '%s' "$out_raw" | normalize_json)
|
||||
gold_norm=$(normalize_json < "$golden")
|
||||
|
||||
if [ "$out_norm" != "$gold_norm" ]; then
|
||||
echo "Golden mismatch (match literal normalization)" >&2
|
||||
diff -u <(echo "$out_norm") <(echo "$gold_norm") || true
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "[OK] golden match literal normalization matched"
|
||||
@ -19,16 +19,16 @@ check_case() {
|
||||
local src="$1"
|
||||
local irfile="$root/tmp/$(basename "$src" .nyash)_llvm.ll"
|
||||
mkdir -p "$root/tmp"
|
||||
NYASH_LLVM_DUMP_IR="$irfile" "$bin" --backend llvm "$src" >/dev/null 2>&1 || {
|
||||
if ! NYASH_LLVM_DUMP_IR="$irfile" "$bin" --backend llvm "$src" >/dev/null 2>&1; then
|
||||
echo "[FAIL] LLVM run failed for $src" >&2
|
||||
fails=$((fails+1))
|
||||
return
|
||||
}
|
||||
fi
|
||||
if [ ! -s "$irfile" ]; then
|
||||
echo "[FAIL] IR not dumped for $src" >&2
|
||||
fails=$((fails+1))
|
||||
return
|
||||
}
|
||||
fi
|
||||
local empty_cnt
|
||||
empty_cnt=$(rg -n "\\bphi\\b" "$irfile" | rg -v "\\[" | wc -l | tr -d ' ')
|
||||
if [ "${empty_cnt:-0}" != "0" ]; then
|
||||
@ -45,6 +45,8 @@ check_case "apps/tests/macro/if/print_expr.nyash"
|
||||
check_case "apps/tests/macro/if/return_expr.nyash"
|
||||
check_case "apps/tests/macro/types/is_basic.nyash"
|
||||
check_case "apps/tests/macro/if/chain_guard.nyash"
|
||||
check_case "apps/tests/macro/match/literal_basic.nyash"
|
||||
check_case "apps/tests/match_guard_type_basic.nyash"
|
||||
|
||||
if [ "$fails" -ne 0 ]; then
|
||||
exit 2
|
||||
|
||||
Reference in New Issue
Block a user