diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index fc4f85c3..ad942978 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -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 diff --git a/apps/lib/cf_builder.nyash b/apps/lib/cf_builder.nyash index ec220a91..dfe71ef8 100644 --- a/apps/lib/cf_builder.nyash +++ b/apps/lib/cf_builder.nyash @@ -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 ] } } - diff --git a/apps/lib/json_builder.nyash b/apps/lib/json_builder.nyash index 5c144e08..3523b595 100644 --- a/apps/lib/json_builder.nyash +++ b/apps/lib/json_builder.nyash @@ -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 diff --git a/apps/macros/examples/if_match_normalize_macro.nyash b/apps/macros/examples/if_match_normalize_macro.nyash index 666814dd..c0df57d9 100644 --- a/apps/macros/examples/if_match_normalize_macro.nyash +++ b/apps/macros/examples/if_match_normalize_macro.nyash @@ -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, ",") + "]") } } diff --git a/apps/tests/macro/match/literal_basic.nyash b/apps/tests/macro/match/literal_basic.nyash new file mode 100644 index 00000000..cbeb310a --- /dev/null +++ b/apps/tests/macro/match/literal_basic.nyash @@ -0,0 +1,7 @@ +local d = 2 +local r = match d { + 1 => 10 + 2 => 20 + _ => 30 +} +print(r) diff --git a/docs/guides/scope-hints.md b/docs/guides/scope-hints.md new file mode 100644 index 00000000..d151dcf4 --- /dev/null +++ b/docs/guides/scope-hints.md @@ -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. + diff --git a/src/grammar/generated.rs b/src/grammar/generated.rs index 0f04d259..d94cb9c1 100644 --- a/src/grammar/generated.rs +++ b/src/grammar/generated.rs @@ -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"; diff --git a/src/macro/ast_json.rs b/src/macro/ast_json.rs index d5070b39..956612d5 100644 --- a/src/macro/ast_json.rs +++ b/src/macro/ast_json.rs @@ -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::>() }), + 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::>(), + "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 { "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, }) } diff --git a/src/macro/macro_box.rs b/src/macro/macro_box.rs index b97d9415..c831e7c3 100644 --- a/src/macro/macro_box.rs +++ b/src/macro/macro_box.rs @@ -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. diff --git a/src/macro/macro_box_ny.rs b/src/macro/macro_box_ny.rs index 8445183f..b2a0f94c 100644 --- a/src/macro/macro_box_ny.rs +++ b/src/macro/macro_box_ny.rs @@ -179,7 +179,7 @@ fn expand_indicates_uppercase(body: &Vec, params: &Vec) -> 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::(&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() + } } } } diff --git a/src/runner/modes/macro_child.rs b/src/runner/modes/macro_child.rs index 0c14ff92..ca4a5b3a 100644 --- a/src/runner/modes/macro_child.rs +++ b/src/runner/modes/macro_child.rs @@ -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 { + 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>> = 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()); diff --git a/tools/test/golden/macro/match_literal_basic.expanded.json b/tools/test/golden/macro/match_literal_basic.expanded.json new file mode 100644 index 00000000..99eacf93 --- /dev/null +++ b/tools/test/golden/macro/match_literal_basic.expanded.json @@ -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"}} +]} diff --git a/tools/test/golden/macro/match_literal_basic_user_macro_golden.sh b/tools/test/golden/macro/match_literal_basic_user_macro_golden.sh new file mode 100644 index 00000000..ac755d48 --- /dev/null +++ b/tools/test/golden/macro/match_literal_basic_user_macro_golden.sh @@ -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" diff --git a/tools/test/smoke/llvm/ir_phi_hygiene_ifcases.sh b/tools/test/smoke/llvm/ir_phi_hygiene_ifcases.sh index b65c6485..083ffb38 100644 --- a/tools/test/smoke/llvm/ir_phi_hygiene_ifcases.sh +++ b/tools/test/smoke/llvm/ir_phi_hygiene_ifcases.sh @@ -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