chore: Phase 25.1 完了 - LoopForm v2/Stage1 CLI/環境変数削減 + Phase 26-D からの変更

Phase 25.1 完了成果:
-  LoopForm v2 テスト・ドキュメント・コメント完備
  - 4ケース(A/B/C/D)完全テストカバレッジ
  - 最小再現ケース作成(SSAバグ調査用)
  - SSOT文書作成(loopform_ssot.md)
  - 全ソースに [LoopForm] コメントタグ追加

-  Stage-1 CLI デバッグ環境構築
  - stage1_cli.hako 実装
  - stage1_bridge.rs ブリッジ実装
  - デバッグツール作成(stage1_debug.sh/stage1_minimal.sh)
  - アーキテクチャ改善提案文書

-  環境変数削減計画策定
  - 25変数の完全調査・分類
  - 6段階削減ロードマップ(25→5、80%削減)
  - 即時削除可能変数特定(NYASH_CONFIG/NYASH_DEBUG)

Phase 26-D からの累積変更:
- PHI実装改善(ExitPhiBuilder/HeaderPhiBuilder等)
- MIRビルダーリファクタリング
- 型伝播・最適化パス改善
- その他約300ファイルの累積変更

🎯 技術的成果:
- SSAバグ根本原因特定(条件分岐内loop変数変更)
- Region+next_iパターン適用完了(UsingCollectorBox等)
- LoopFormパターン文書化・テスト化完了
- セルフホスティング基盤強化

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: ChatGPT <noreply@openai.com>
Co-Authored-By: Task Assistant <task@anthropic.com>
This commit is contained in:
nyash-codex
2025-11-21 06:25:17 +09:00
parent baf028a94f
commit f9d100ce01
366 changed files with 14322 additions and 5236 deletions

View File

@ -1,5 +1,5 @@
use nyash_rust::ast::{ASTNode, BinaryOperator, LiteralValue, Span, UnaryOperator};
use serde_json::{json, Value};
use nyash_rust::ast::{ASTNode, LiteralValue, BinaryOperator, UnaryOperator, Span};
pub fn ast_to_json(ast: &ASTNode) -> Value {
match ast.clone() {
@ -7,7 +7,9 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
"kind": "Program",
"statements": statements.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>()
}),
ASTNode::Loop { condition, body, .. } => json!({
ASTNode::Loop {
condition, body, ..
} => json!({
"kind": "Loop",
"condition": ast_to_json(&condition),
"body": body.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>()
@ -27,18 +29,32 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
"target": ast_to_json(&target),
"value": ast_to_json(&value),
}),
ASTNode::Local { variables, initial_values, .. } => json!({
ASTNode::Local {
variables,
initial_values,
..
} => json!({
"kind": "Local",
"variables": variables,
"inits": initial_values.into_iter().map(|opt| opt.map(|v| ast_to_json(&v))).collect::<Vec<_>>()
}),
ASTNode::If { condition, then_body, else_body, .. } => json!({
ASTNode::If {
condition,
then_body,
else_body,
..
} => json!({
"kind": "If",
"condition": ast_to_json(&condition),
"then": then_body.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>(),
"else": else_body.map(|v| v.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>()),
}),
ASTNode::TryCatch { try_body, catch_clauses, finally_body, .. } => json!({
ASTNode::TryCatch {
try_body,
catch_clauses,
finally_body,
..
} => json!({
"kind": "TryCatch",
"try": try_body.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>(),
"catch": catch_clauses.into_iter().map(|cc| json!({
@ -48,7 +64,14 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
})).collect::<Vec<_>>(),
"cleanup": finally_body.map(|v| v.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>())
}),
ASTNode::FunctionDeclaration { name, params, body, is_static, is_override, .. } => json!({
ASTNode::FunctionDeclaration {
name,
params,
body,
is_static,
is_override,
..
} => json!({
"kind": "FunctionDeclaration",
"name": name,
"params": params,
@ -58,24 +81,38 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
}),
ASTNode::Variable { name, .. } => json!({"kind":"Variable","name":name}),
ASTNode::Literal { value, .. } => json!({"kind":"Literal","value": lit_to_json(&value)}),
ASTNode::BinaryOp { operator, left, right, .. } => json!({
ASTNode::BinaryOp {
operator,
left,
right,
..
} => json!({
"kind":"BinaryOp",
"op": bin_to_str(&operator),
"left": ast_to_json(&left),
"right": ast_to_json(&right),
}),
ASTNode::UnaryOp { operator, operand, .. } => json!({
ASTNode::UnaryOp {
operator, operand, ..
} => json!({
"kind":"UnaryOp",
"op": un_to_str(&operator),
"operand": ast_to_json(&operand),
}),
ASTNode::MethodCall { object, method, arguments, .. } => json!({
ASTNode::MethodCall {
object,
method,
arguments,
..
} => json!({
"kind":"MethodCall",
"object": ast_to_json(&object),
"method": method,
"arguments": arguments.into_iter().map(|a| ast_to_json(&a)).collect::<Vec<_>>()
}),
ASTNode::FunctionCall { name, arguments, .. } => json!({
ASTNode::FunctionCall {
name, arguments, ..
} => json!({
"kind":"FunctionCall",
"name": name,
"arguments": arguments.into_iter().map(|a| ast_to_json(&a)).collect::<Vec<_>>()
@ -88,7 +125,12 @@ 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::MatchExpr { scrutinee, arms, else_expr, .. } => json!({
ASTNode::MatchExpr {
scrutinee,
arms,
else_expr,
..
} => json!({
"kind":"MatchExpr",
"scrutinee": ast_to_json(&scrutinee),
"arms": arms.into_iter().map(|(lit, body)| json!({
@ -108,45 +150,163 @@ pub fn json_to_ast(v: &Value) -> Option<ASTNode> {
let k = v.get("kind")?.as_str()?;
Some(match k {
"Program" => {
let stmts = v.get("statements")?.as_array()?.iter().filter_map(json_to_ast).collect::<Vec<_>>();
ASTNode::Program { statements: stmts, span: Span::unknown() }
let stmts = v
.get("statements")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect::<Vec<_>>();
ASTNode::Program {
statements: stmts,
span: Span::unknown(),
}
}
"Loop" => ASTNode::Loop {
condition: Box::new(json_to_ast(v.get("condition")?)?),
body: v.get("body")?.as_array()?.iter().filter_map(json_to_ast).collect::<Vec<_>>(),
body: v
.get("body")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect::<Vec<_>>(),
span: Span::unknown(),
},
"Print" => ASTNode::Print { expression: Box::new(json_to_ast(v.get("expression")?)?), span: Span::unknown() },
"Return" => ASTNode::Return { value: v.get("value").and_then(json_to_ast).map(Box::new), span: Span::unknown() },
"Break" => ASTNode::Break { span: Span::unknown() },
"Continue" => ASTNode::Continue { span: Span::unknown() },
"Assignment" => ASTNode::Assignment { target: Box::new(json_to_ast(v.get("target")?)?), value: Box::new(json_to_ast(v.get("value")?)?), span: Span::unknown() },
"Local" => {
let vars = v.get("variables")?.as_array()?.iter().filter_map(|s| s.as_str().map(|x| x.to_string())).collect();
let inits = v.get("inits")?.as_array()?.iter().map(|initv| {
if initv.is_null() { None } else { json_to_ast(initv).map(Box::new) }
}).collect();
ASTNode::Local { variables: vars, initial_values: inits, span: Span::unknown() }
"Print" => ASTNode::Print {
expression: Box::new(json_to_ast(v.get("expression")?)?),
span: Span::unknown(),
},
"Return" => ASTNode::Return {
value: v.get("value").and_then(json_to_ast).map(Box::new),
span: Span::unknown(),
},
"Break" => ASTNode::Break {
span: Span::unknown(),
},
"Continue" => ASTNode::Continue {
span: Span::unknown(),
},
"Assignment" => ASTNode::Assignment {
target: Box::new(json_to_ast(v.get("target")?)?),
value: Box::new(json_to_ast(v.get("value")?)?),
span: Span::unknown(),
},
"Local" => {
let vars = v
.get("variables")?
.as_array()?
.iter()
.filter_map(|s| s.as_str().map(|x| x.to_string()))
.collect();
let inits = v
.get("inits")?
.as_array()?
.iter()
.map(|initv| {
if initv.is_null() {
None
} else {
json_to_ast(initv).map(Box::new)
}
})
.collect();
ASTNode::Local {
variables: vars,
initial_values: inits,
span: Span::unknown(),
}
}
"If" => ASTNode::If {
condition: Box::new(json_to_ast(v.get("condition")?)?),
then_body: v
.get("then")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect::<Vec<_>>(),
else_body: v.get("else").and_then(|a| {
a.as_array()
.map(|arr| arr.iter().filter_map(json_to_ast).collect::<Vec<_>>())
}),
span: Span::unknown(),
},
"If" => ASTNode::If { condition: Box::new(json_to_ast(v.get("condition")?)?), then_body: v.get("then")?.as_array()?.iter().filter_map(json_to_ast).collect::<Vec<_>>(), else_body: v.get("else").and_then(|a| a.as_array().map(|arr| arr.iter().filter_map(json_to_ast).collect::<Vec<_>>())), span: Span::unknown() },
"FunctionDeclaration" => ASTNode::FunctionDeclaration {
name: v.get("name")?.as_str()?.to_string(),
params: v.get("params")?.as_array()?.iter().filter_map(|s| s.as_str().map(|x| x.to_string())).collect(),
body: v.get("body")?.as_array()?.iter().filter_map(json_to_ast).collect(),
params: v
.get("params")?
.as_array()?
.iter()
.filter_map(|s| s.as_str().map(|x| x.to_string()))
.collect(),
body: v
.get("body")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect(),
is_static: v.get("static").and_then(|b| b.as_bool()).unwrap_or(false),
is_override: v.get("override").and_then(|b| b.as_bool()).unwrap_or(false),
span: Span::unknown(),
},
"Variable" => ASTNode::Variable { name: v.get("name")?.as_str()?.to_string(), span: Span::unknown() },
"Literal" => ASTNode::Literal { value: json_to_lit(v.get("value")?)?, span: Span::unknown() },
"BinaryOp" => ASTNode::BinaryOp { operator: str_to_bin(v.get("op")?.as_str()?)?, left: Box::new(json_to_ast(v.get("left")?)?), right: Box::new(json_to_ast(v.get("right")?)?), span: Span::unknown() },
"UnaryOp" => ASTNode::UnaryOp { operator: str_to_un(v.get("op")?.as_str()?)?, operand: Box::new(json_to_ast(v.get("operand")?)?), span: Span::unknown() },
"MethodCall" => ASTNode::MethodCall { object: Box::new(json_to_ast(v.get("object")?)?), method: v.get("method")?.as_str()?.to_string(), arguments: v.get("arguments")?.as_array()?.iter().filter_map(json_to_ast).collect(), span: Span::unknown() },
"FunctionCall" => ASTNode::FunctionCall { name: v.get("name")?.as_str()?.to_string(), arguments: v.get("arguments")?.as_array()?.iter().filter_map(json_to_ast).collect(), span: Span::unknown() },
"Array" => ASTNode::ArrayLiteral { elements: v.get("elements")?.as_array()?.iter().filter_map(json_to_ast).collect(), span: Span::unknown() },
"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() },
"Variable" => ASTNode::Variable {
name: v.get("name")?.as_str()?.to_string(),
span: Span::unknown(),
},
"Literal" => ASTNode::Literal {
value: json_to_lit(v.get("value")?)?,
span: Span::unknown(),
},
"BinaryOp" => ASTNode::BinaryOp {
operator: str_to_bin(v.get("op")?.as_str()?)?,
left: Box::new(json_to_ast(v.get("left")?)?),
right: Box::new(json_to_ast(v.get("right")?)?),
span: Span::unknown(),
},
"UnaryOp" => ASTNode::UnaryOp {
operator: str_to_un(v.get("op")?.as_str()?)?,
operand: Box::new(json_to_ast(v.get("operand")?)?),
span: Span::unknown(),
},
"MethodCall" => ASTNode::MethodCall {
object: Box::new(json_to_ast(v.get("object")?)?),
method: v.get("method")?.as_str()?.to_string(),
arguments: v
.get("arguments")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect(),
span: Span::unknown(),
},
"FunctionCall" => ASTNode::FunctionCall {
name: v.get("name")?.as_str()?.to_string(),
arguments: v
.get("arguments")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect(),
span: Span::unknown(),
},
"Array" => ASTNode::ArrayLiteral {
elements: v
.get("elements")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect(),
span: Span::unknown(),
},
"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(),
},
"MatchExpr" => {
let scr = json_to_ast(v.get("scrutinee")?)?;
let arms_json = v.get("arms")?.as_array()?.iter();
@ -166,18 +326,47 @@ pub fn json_to_ast(v: &Value) -> Option<ASTNode> {
}
}
"TryCatch" => {
let try_b = v.get("try")?.as_array()?.iter().filter_map(json_to_ast).collect::<Vec<_>>();
let try_b = v
.get("try")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect::<Vec<_>>();
let mut catches = Vec::new();
if let Some(arr) = v.get("catch").and_then(|x| x.as_array()) {
for c in arr.iter() {
let exc_t = match c.get("type") { Some(t) if !t.is_null() => t.as_str().map(|s| s.to_string()), _ => None };
let var = match c.get("var") { Some(vv) if !vv.is_null() => vv.as_str().map(|s| s.to_string()), _ => None };
let body = c.get("body")?.as_array()?.iter().filter_map(json_to_ast).collect::<Vec<_>>();
catches.push(nyash_rust::ast::CatchClause { exception_type: exc_t, variable_name: var, body, span: Span::unknown() });
let exc_t = match c.get("type") {
Some(t) if !t.is_null() => t.as_str().map(|s| s.to_string()),
_ => None,
};
let var = match c.get("var") {
Some(vv) if !vv.is_null() => vv.as_str().map(|s| s.to_string()),
_ => None,
};
let body = c
.get("body")?
.as_array()?
.iter()
.filter_map(json_to_ast)
.collect::<Vec<_>>();
catches.push(nyash_rust::ast::CatchClause {
exception_type: exc_t,
variable_name: var,
body,
span: Span::unknown(),
});
}
}
let cleanup = v.get("cleanup").and_then(|cl| cl.as_array().map(|arr| arr.iter().filter_map(json_to_ast).collect::<Vec<_>>()));
ASTNode::TryCatch { try_body: try_b, catch_clauses: catches, finally_body: cleanup, span: Span::unknown() }
let cleanup = v.get("cleanup").and_then(|cl| {
cl.as_array()
.map(|arr| arr.iter().filter_map(json_to_ast).collect::<Vec<_>>())
});
ASTNode::TryCatch {
try_body: try_b,
catch_clauses: catches,
finally_body: cleanup,
span: Span::unknown(),
}
}
_ => return None,
})

View File

@ -32,14 +32,18 @@ impl MacroCtx {
}
}
pub fn gensym(&self, prefix: &str) -> String { gensym(prefix) }
pub fn gensym(&self, prefix: &str) -> String {
gensym(prefix)
}
pub fn report(&self, level: &str, message: &str) {
eprintln!("[macro][{}] {}", level, message);
}
pub fn get_env(&self, key: &str) -> Option<String> {
if !self.caps.env { return None; }
if !self.caps.env {
return None;
}
std::env::var(key).ok()
}
}

View File

@ -1,5 +1,5 @@
use nyash_rust::ast::Span;
use nyash_rust::{ASTNode, ast::LiteralValue, ast::BinaryOperator};
use nyash_rust::{ast::BinaryOperator, ast::LiteralValue, ASTNode};
use std::time::Instant;
/// HIR Patch description (MVP placeholder)
@ -16,10 +16,20 @@ pub struct MacroEngine {
impl MacroEngine {
pub fn new() -> Self {
let max_passes = std::env::var("NYASH_MACRO_MAX_PASSES").ok().and_then(|v| v.parse().ok()).unwrap_or(32);
let cycle_window = std::env::var("NYASH_MACRO_CYCLE_WINDOW").ok().and_then(|v| v.parse().ok()).unwrap_or(8);
let max_passes = std::env::var("NYASH_MACRO_MAX_PASSES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(32);
let cycle_window = std::env::var("NYASH_MACRO_CYCLE_WINDOW")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(8);
let trace = std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1");
Self { max_passes, cycle_window, trace }
Self {
max_passes,
cycle_window,
trace,
}
}
/// Expand all macros with depth/cycle guards and return patched AST.
@ -29,25 +39,48 @@ impl MacroEngine {
let mut history: std::collections::VecDeque<ASTNode> = std::collections::VecDeque::new();
for pass in 0..self.max_passes {
let t0 = Instant::now();
let before_len = crate::r#macro::ast_json::ast_to_json(&cur).to_string().len();
let before_len = crate::r#macro::ast_json::ast_to_json(&cur)
.to_string()
.len();
let next0 = self.expand_node(&cur);
// Apply user MacroBoxes once per pass (if enabled)
let next = crate::r#macro::macro_box::expand_all_once(&next0);
let after_len = crate::r#macro::ast_json::ast_to_json(&next).to_string().len();
let after_len = crate::r#macro::ast_json::ast_to_json(&next)
.to_string()
.len();
let dt = t0.elapsed();
if self.trace { eprintln!("[macro][engine] pass={} changed={} bytes:{}=>{} dt={:?}", pass, (next != cur), before_len, after_len, dt); }
if self.trace {
eprintln!(
"[macro][engine] pass={} changed={} bytes:{}=>{} dt={:?}",
pass,
(next != cur),
before_len,
after_len,
dt
);
}
jsonl_trace(pass, before_len, after_len, next != cur, dt);
if next == cur { return (cur, patches); }
if next == cur {
return (cur, patches);
}
// cycle detection in small window
if history.iter().any(|h| *h == next) {
eprintln!("[macro][engine] cycle detected at pass {} — stopping expansion", pass);
eprintln!(
"[macro][engine] cycle detected at pass {} — stopping expansion",
pass
);
return (cur, patches);
}
history.push_back(cur);
if history.len() > self.cycle_window { let _ = history.pop_front(); }
if history.len() > self.cycle_window {
let _ = history.pop_front();
}
cur = next;
}
eprintln!("[macro][engine] max passes ({}) exceeded — stopping expansion", self.max_passes);
eprintln!(
"[macro][engine] max passes ({}) exceeded — stopping expansion",
self.max_passes
);
(cur, patches)
}
@ -57,23 +90,55 @@ impl MacroEngine {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][visit] Program: statements={}", statements.len());
}
let new_stmts = statements.into_iter().map(|n| {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][visit] child kind...",);
}
self.expand_node(&n)
}).collect();
ASTNode::Program { statements: new_stmts, span }
let new_stmts = statements
.into_iter()
.map(|n| {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][visit] child kind...",);
}
self.expand_node(&n)
})
.collect();
ASTNode::Program {
statements: new_stmts,
span,
}
}
ASTNode::BoxDeclaration { name, fields, public_fields, private_fields, mut methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, is_static, static_init, span } => {
ASTNode::BoxDeclaration {
name,
fields,
public_fields,
private_fields,
mut methods,
constructors,
init_fields,
weak_fields,
is_interface,
extends,
implements,
type_parameters,
is_static,
static_init,
span,
} => {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][visit] BoxDeclaration: {} (fields={})", name, fields.len());
eprintln!(
"[macro][visit] BoxDeclaration: {} (fields={})",
name,
fields.len()
);
}
// Derive set: default Equals+ToString when macro is enabled
let derive_all = std::env::var("NYASH_MACRO_DERIVE_ALL").ok().as_deref() == Some("1");
let derive_set = std::env::var("NYASH_MACRO_DERIVE").ok().unwrap_or_else(|| "Equals,ToString".to_string());
let derive_all =
std::env::var("NYASH_MACRO_DERIVE_ALL").ok().as_deref() == Some("1");
let derive_set = std::env::var("NYASH_MACRO_DERIVE")
.ok()
.unwrap_or_else(|| "Equals,ToString".to_string());
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][derive] box={} derive_all={} set={}", name, derive_all, derive_set);
eprintln!(
"[macro][derive] box={} derive_all={} set={}",
name, derive_all, derive_set
);
}
let want_equals = derive_all || derive_set.contains("Equals");
let want_tostring = derive_all || derive_set.contains("ToString");
@ -81,19 +146,43 @@ impl MacroEngine {
let field_view: &Vec<String> = &public_fields;
if want_equals && !methods.contains_key("equals") {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][derive] equals for {} (public fields: {})", name, field_view.len());
eprintln!(
"[macro][derive] equals for {} (public fields: {})",
name,
field_view.len()
);
}
let m = build_equals_method(&name, field_view);
methods.insert("equals".to_string(), m);
}
if want_tostring && !methods.contains_key("toString") {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][derive] toString for {} (public fields: {})", name, field_view.len());
eprintln!(
"[macro][derive] toString for {} (public fields: {})",
name,
field_view.len()
);
}
let m = build_tostring_method(&name, field_view);
methods.insert("toString".to_string(), m);
}
ASTNode::BoxDeclaration { name, fields, public_fields, private_fields, methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, is_static, static_init, span }
ASTNode::BoxDeclaration {
name,
fields,
public_fields,
private_fields,
methods,
constructors,
init_fields,
weak_fields,
is_interface,
extends,
implements,
type_parameters,
is_static,
static_init,
span,
}
}
other => other,
}
@ -102,7 +191,9 @@ impl MacroEngine {
fn jsonl_trace(pass: usize, before: usize, after: usize, changed: bool, dt: std::time::Duration) {
if let Ok(path) = std::env::var("NYASH_MACRO_TRACE_JSONL") {
if path.is_empty() { return; }
if path.is_empty() {
return;
}
let rec = serde_json::json!({
"event": "macro_pass",
"pass": pass,
@ -110,17 +201,24 @@ fn jsonl_trace(pass: usize, before: usize, after: usize, changed: bool, dt: std:
"before_bytes": before,
"after_bytes": after,
"dt_us": dt.as_micros() as u64,
}).to_string();
let _ = std::fs::OpenOptions::new().create(true).append(true).open(path).and_then(|mut f| {
use std::io::Write;
writeln!(f, "{}", rec)
});
})
.to_string();
let _ = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.and_then(|mut f| {
use std::io::Write;
writeln!(f, "{}", rec)
});
}
}
fn me_field(name: &str) -> ASTNode {
ASTNode::FieldAccess {
object: Box::new(ASTNode::Me { span: Span::unknown() }),
object: Box::new(ASTNode::Me {
span: Span::unknown(),
}),
field: name.to_string(),
span: Span::unknown(),
}
@ -128,30 +226,56 @@ fn me_field(name: &str) -> ASTNode {
fn var_field(var: &str, field: &str) -> ASTNode {
ASTNode::FieldAccess {
object: Box::new(ASTNode::Variable { name: var.to_string(), span: Span::unknown() }),
object: Box::new(ASTNode::Variable {
name: var.to_string(),
span: Span::unknown(),
}),
field: field.to_string(),
span: Span::unknown(),
}
}
fn bin_add(lhs: ASTNode, rhs: ASTNode) -> ASTNode {
ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(lhs), right: Box::new(rhs), span: Span::unknown() }
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(lhs),
right: Box::new(rhs),
span: Span::unknown(),
}
}
fn bin_and(lhs: ASTNode, rhs: ASTNode) -> ASTNode {
ASTNode::BinaryOp { operator: BinaryOperator::And, left: Box::new(lhs), right: Box::new(rhs), span: Span::unknown() }
ASTNode::BinaryOp {
operator: BinaryOperator::And,
left: Box::new(lhs),
right: Box::new(rhs),
span: Span::unknown(),
}
}
fn bin_eq(lhs: ASTNode, rhs: ASTNode) -> ASTNode {
ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(lhs), right: Box::new(rhs), span: Span::unknown() }
ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(lhs),
right: Box::new(rhs),
span: Span::unknown(),
}
}
fn lit_str(s: &str) -> ASTNode { ASTNode::Literal { value: LiteralValue::String(s.to_string()), span: Span::unknown() } }
fn lit_str(s: &str) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::String(s.to_string()),
span: Span::unknown(),
}
}
fn build_equals_method(_box_name: &str, fields: &Vec<String>) -> ASTNode {
// equals(other) { return me.f1 == other.f1 && ...; }
let cond = if fields.is_empty() {
ASTNode::Literal { value: LiteralValue::Bool(true), span: Span::unknown() }
ASTNode::Literal {
value: LiteralValue::Bool(true),
span: Span::unknown(),
}
} else {
let mut it = fields.iter();
let first = it.next().unwrap();
@ -166,7 +290,10 @@ fn build_equals_method(_box_name: &str, fields: &Vec<String>) -> ASTNode {
ASTNode::FunctionDeclaration {
name: "equals".to_string(),
params: vec![param_name.clone()],
body: vec![ASTNode::Return { value: Some(Box::new(cond)), span: Span::unknown() }],
body: vec![ASTNode::Return {
value: Some(Box::new(cond)),
span: Span::unknown(),
}],
is_static: false,
is_override: false,
span: Span::unknown(),
@ -178,7 +305,9 @@ fn build_tostring_method(box_name: &str, fields: &Vec<String>) -> ASTNode {
let mut expr = lit_str(&format!("{}(", box_name));
let mut first = true;
for f in fields {
if !first { expr = bin_add(expr, lit_str(",")); }
if !first {
expr = bin_add(expr, lit_str(","));
}
first = false;
expr = bin_add(expr, me_field(f));
}
@ -186,7 +315,10 @@ fn build_tostring_method(box_name: &str, fields: &Vec<String>) -> ASTNode {
ASTNode::FunctionDeclaration {
name: "toString".to_string(),
params: vec![],
body: vec![ASTNode::Return { value: Some(Box::new(expr)), span: Span::unknown() }],
body: vec![ASTNode::Return {
value: Some(Box::new(expr)),
span: Span::unknown(),
}],
is_static: false,
is_override: false,
span: Span::unknown(),

View File

@ -1,5 +1,5 @@
use std::sync::{Mutex, OnceLock};
use nyash_rust::ASTNode;
use std::sync::{Mutex, OnceLock};
/// MacroBox API — user-extensible macro expansion units (experimental)
///
@ -33,13 +33,17 @@ pub fn register(m: &'static dyn MacroBox) {
/// 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 {
if std::env::var("NYASH_MACRO_BOX").ok().as_deref() == Some("1") { return true; }
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.
pub fn expand_all_once(ast: &ASTNode) -> ASTNode {
if !enabled() { return ast.clone(); }
if !enabled() {
return ast.clone();
}
let reg = registry();
let guard = reg.lock().expect("macro registry poisoned");
let mut cur = ast.clone();
@ -55,39 +59,127 @@ pub fn expand_all_once(ast: &ASTNode) -> ASTNode {
pub struct UppercasePrintMacro;
impl MacroBox for UppercasePrintMacro {
fn name(&self) -> &'static str { "UppercasePrintMacro" }
fn name(&self) -> &'static str {
"UppercasePrintMacro"
}
fn expand(&self, ast: &ASTNode) -> ASTNode {
use nyash_rust::ast::{ASTNode as A, LiteralValue, Span};
fn go(n: &A) -> A {
match n.clone() {
A::Program { statements, span } => A::Program { statements: statements.into_iter().map(|c| go(&c)).collect(), span },
A::Program { statements, span } => A::Program {
statements: statements.into_iter().map(|c| go(&c)).collect(),
span,
},
A::Print { expression, span } => {
match &*expression {
A::Literal { value: LiteralValue::String(s), .. } => {
A::Literal {
value: LiteralValue::String(s),
..
} => {
// Demo: if string starts with "UPPER:", uppercase the rest.
if let Some(rest) = s.strip_prefix("UPPER:") {
let up = rest.to_uppercase();
A::Print { expression: Box::new(A::Literal { value: LiteralValue::String(up), span: Span::unknown() }), span }
} else { A::Print { expression: Box::new(go(&*expression)), span } }
A::Print {
expression: Box::new(A::Literal {
value: LiteralValue::String(up),
span: Span::unknown(),
}),
span,
}
} else {
A::Print {
expression: Box::new(go(&*expression)),
span,
}
}
}
other => A::Print { expression: Box::new(go(other)), span }
other => A::Print {
expression: Box::new(go(other)),
span,
},
}
}
A::Assignment { target, value, span } => A::Assignment { target: Box::new(go(&*target)), value: Box::new(go(&*value)), span },
A::If { condition, then_body, else_body, span } => A::If {
A::Assignment {
target,
value,
span,
} => A::Assignment {
target: Box::new(go(&*target)),
value: Box::new(go(&*value)),
span,
},
A::If {
condition,
then_body,
else_body,
span,
} => A::If {
condition: Box::new(go(&*condition)),
then_body: then_body.into_iter().map(|c| go(&c)).collect(),
else_body: else_body.map(|v| v.into_iter().map(|c| go(&c)).collect()),
span,
},
A::Return { value, span } => A::Return { value: value.as_ref().map(|v| Box::new(go(v))), span },
A::FieldAccess { object, field, span } => A::FieldAccess { object: Box::new(go(&*object)), field, span },
A::MethodCall { object, method, arguments, span } => A::MethodCall { object: Box::new(go(&*object)), method, arguments: arguments.into_iter().map(|c| go(&c)).collect(), span },
A::FunctionCall { name, arguments, span } => A::FunctionCall { name, arguments: arguments.into_iter().map(|c| go(&c)).collect(), span },
A::BinaryOp { operator, left, right, span } => A::BinaryOp { operator, left: Box::new(go(&*left)), right: Box::new(go(&*right)), span },
A::UnaryOp { operator, operand, span } => A::UnaryOp { operator, operand: Box::new(go(&*operand)), span },
A::ArrayLiteral { elements, span } => A::ArrayLiteral { elements: elements.into_iter().map(|c| go(&c)).collect(), span },
A::MapLiteral { entries, span } => A::MapLiteral { entries: entries.into_iter().map(|(k,v)| (k, go(&v))).collect(), span },
A::Return { value, span } => A::Return {
value: value.as_ref().map(|v| Box::new(go(v))),
span,
},
A::FieldAccess {
object,
field,
span,
} => A::FieldAccess {
object: Box::new(go(&*object)),
field,
span,
},
A::MethodCall {
object,
method,
arguments,
span,
} => A::MethodCall {
object: Box::new(go(&*object)),
method,
arguments: arguments.into_iter().map(|c| go(&c)).collect(),
span,
},
A::FunctionCall {
name,
arguments,
span,
} => A::FunctionCall {
name,
arguments: arguments.into_iter().map(|c| go(&c)).collect(),
span,
},
A::BinaryOp {
operator,
left,
right,
span,
} => A::BinaryOp {
operator,
left: Box::new(go(&*left)),
right: Box::new(go(&*right)),
span,
},
A::UnaryOp {
operator,
operand,
span,
} => A::UnaryOp {
operator,
operand: Box::new(go(&*operand)),
span,
},
A::ArrayLiteral { elements, span } => A::ArrayLiteral {
elements: elements.into_iter().map(|c| go(&c)).collect(),
span,
},
A::MapLiteral { entries, span } => A::MapLiteral {
entries: entries.into_iter().map(|(k, v)| (k, go(&v))).collect(),
span,
},
other => other,
}
}
@ -101,13 +193,17 @@ static INIT_FLAG: OnceLock<()> = OnceLock::new();
pub fn init_builtin() {
INIT_FLAG.get_or_init(|| {
// Explicit example toggle
if std::env::var("NYASH_MACRO_BOX_EXAMPLE").ok().as_deref() == Some("1") { register(&UppercasePrintMacro); }
if std::env::var("NYASH_MACRO_BOX_EXAMPLE").ok().as_deref() == Some("1") {
register(&UppercasePrintMacro);
}
// Comma-separated names: NYASH_MACRO_BOX_ENABLE="UppercasePrintMacro,Other"
if let Ok(list) = std::env::var("NYASH_MACRO_BOX_ENABLE") {
for name in list.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
match name {
"UppercasePrintMacro" => register(&UppercasePrintMacro),
_ => { eprintln!("[macro][box] unknown MacroBox '{}', ignoring", name); }
_ => {
eprintln!("[macro][box] unknown MacroBox '{}', ignoring", name);
}
}
}
}

View File

@ -28,7 +28,9 @@ pub fn init_from_env() {
if std::env::var("NYASH_MACRO_BOX_CHILD_RUNNER").ok().is_some() {
eprintln!("[macro][compat] NYASH_MACRO_BOX_CHILD_RUNNER is deprecated; runner mode is managed automatically");
}
let Some(paths) = paths else { return; };
let Some(paths) = paths else {
return;
};
for p in paths.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
if let Err(e) = try_load_one(p) {
// Quiet by default; print only when tracing is enabled to reduce noise in normal runs
@ -47,35 +49,72 @@ fn try_load_one(path: &str) -> Result<(), String> {
let prev_sugar = std::env::var("NYASH_SYNTAX_SUGAR_LEVEL").ok();
std::env::set_var("NYASH_SYNTAX_SUGAR_LEVEL", "basic");
let ast_res = nyash_rust::parser::NyashParser::parse_from_string(&src);
if let Some(v) = prev_sugar { std::env::set_var("NYASH_SYNTAX_SUGAR_LEVEL", v); } else { std::env::remove_var("NYASH_SYNTAX_SUGAR_LEVEL"); }
if let Some(v) = prev_sugar {
std::env::set_var("NYASH_SYNTAX_SUGAR_LEVEL", v);
} else {
std::env::remove_var("NYASH_SYNTAX_SUGAR_LEVEL");
}
let ast = ast_res.map_err(|e| format!("parse error: {:?}", e))?;
// Find a BoxDeclaration with static function expand(...)
if let ASTNode::Program { statements, .. } = ast {
// Capabilities: conservative scan before registration
if let Err(msg) = caps_allow_macro_source(&ASTNode::Program { statements: statements.clone(), span: nyash_rust::ast::Span::unknown() }) {
if let Err(msg) = caps_allow_macro_source(&ASTNode::Program {
statements: statements.clone(),
span: nyash_rust::ast::Span::unknown(),
}) {
eprintln!("[macro][box_ny][caps] {} (in '{}')", msg, path);
if strict_enabled() { return Err(msg); }
if strict_enabled() {
return Err(msg);
}
return Ok(());
}
for st in &statements {
if let ASTNode::BoxDeclaration { name: box_name, methods, .. } = st {
if let Some(ASTNode::FunctionDeclaration { name: mname, body: exp_body, params, .. }) = methods.get("expand") {
if let ASTNode::BoxDeclaration {
name: box_name,
methods,
..
} = st
{
if let Some(ASTNode::FunctionDeclaration {
name: mname,
body: exp_body,
params,
..
}) = methods.get("expand")
{
if mname == "expand" {
let reg_name = derive_box_name(&box_name, methods.get("name"));
// Prefer Nyash runner route by default (self-hosting). Child-proxy only when explicitly enabled.
let use_child = std::env::var("NYASH_MACRO_BOX_CHILD").ok().map(|v| v != "0" && v != "false" && v != "off").unwrap_or(true);
let use_child = std::env::var("NYASH_MACRO_BOX_CHILD")
.ok()
.map(|v| v != "0" && v != "false" && v != "off")
.unwrap_or(true);
if use_child {
let nm = reg_name;
let file_static: &'static str = Box::leak(path.to_string().into_boxed_str());
crate::r#macro::macro_box::register(Box::leak(Box::new(NyChildMacroBox { nm, file: file_static })));
eprintln!("[macro][box_ny] registered child-proxy MacroBox '{}' for {}", nm, path);
let file_static: &'static str =
Box::leak(path.to_string().into_boxed_str());
crate::r#macro::macro_box::register(Box::leak(Box::new(
NyChildMacroBox {
nm,
file: file_static,
},
)));
eprintln!(
"[macro][box_ny] registered child-proxy MacroBox '{}' for {}",
nm, path
);
} else {
// Heuristic mapping by name first, otherwise inspect body pattern.
let mut mapped = false;
match reg_name {
"UppercasePrintMacro" => {
crate::r#macro::macro_box::register(&crate::r#macro::macro_box::UppercasePrintMacro);
eprintln!("[macro][box_ny] registered built-in '{}' from {}", reg_name, path);
crate::r#macro::macro_box::register(
&crate::r#macro::macro_box::UppercasePrintMacro,
);
eprintln!(
"[macro][box_ny] registered built-in '{}' from {}",
reg_name, path
);
mapped = true;
}
_ => {}
@ -83,14 +122,20 @@ fn try_load_one(path: &str) -> Result<(), String> {
if !mapped {
if expand_is_identity(exp_body, params) {
let nm = reg_name;
crate::r#macro::macro_box::register(Box::leak(Box::new(NyIdentityMacroBox { nm })));
crate::r#macro::macro_box::register(Box::leak(Box::new(
NyIdentityMacroBox { nm },
)));
eprintln!("[macro][box_ny] registered Ny MacroBox '{}' (identity by body) from {}", nm, path);
} else if expand_indicates_uppercase(exp_body, params) {
crate::r#macro::macro_box::register(&crate::r#macro::macro_box::UppercasePrintMacro);
crate::r#macro::macro_box::register(
&crate::r#macro::macro_box::UppercasePrintMacro,
);
eprintln!("[macro][box_ny] registered built-in 'UppercasePrintMacro' by body pattern from {}", path);
} else {
let nm = reg_name;
crate::r#macro::macro_box::register(Box::leak(Box::new(NyIdentityMacroBox { nm })));
crate::r#macro::macro_box::register(Box::leak(Box::new(
NyIdentityMacroBox { nm },
)));
eprintln!("[macro][box_ny] registered Ny MacroBox '{}' (identity: unknown body) from {}", nm, path);
}
}
@ -102,19 +147,38 @@ fn try_load_one(path: &str) -> Result<(), String> {
}
// Fallback: accept top-level `static function MacroBoxSpec.expand(json)` without a BoxDeclaration
// Default OFF for safety; can be enabled via CLI/env
let allow_top = std::env::var("NYASH_MACRO_TOPLEVEL_ALLOW").ok().map(|v| v != "0" && v != "false" && v != "off").unwrap_or(false);
let allow_top = std::env::var("NYASH_MACRO_TOPLEVEL_ALLOW")
.ok()
.map(|v| v != "0" && v != "false" && v != "off")
.unwrap_or(false);
for st in &statements {
if let ASTNode::FunctionDeclaration { is_static: true, name, .. } = st {
if let ASTNode::FunctionDeclaration {
is_static: true,
name,
..
} = st
{
if let Some((box_name, method)) = name.split_once('.') {
if method == "expand" {
let nm: &'static str = Box::leak(box_name.to_string().into_boxed_str());
let file_static: &'static str = Box::leak(path.to_string().into_boxed_str());
let use_child = std::env::var("NYASH_MACRO_BOX_CHILD").ok().map(|v| v != "0" && v != "false" && v != "off").unwrap_or(true);
let file_static: &'static str =
Box::leak(path.to_string().into_boxed_str());
let use_child = std::env::var("NYASH_MACRO_BOX_CHILD")
.ok()
.map(|v| v != "0" && v != "false" && v != "off")
.unwrap_or(true);
if use_child && allow_top {
crate::r#macro::macro_box::register(Box::leak(Box::new(NyChildMacroBox { nm, file: file_static })));
crate::r#macro::macro_box::register(Box::leak(Box::new(
NyChildMacroBox {
nm,
file: file_static,
},
)));
eprintln!("[macro][box_ny] registered child-proxy MacroBox '{}' (top-level static) for {}", nm, path);
} else {
crate::r#macro::macro_box::register(Box::leak(Box::new(NyIdentityMacroBox { nm })));
crate::r#macro::macro_box::register(Box::leak(Box::new(
NyIdentityMacroBox { nm },
)));
eprintln!("[macro][box_ny] registered identity MacroBox '{}' (top-level static) for {}", nm, path);
}
return Ok(());
@ -131,7 +195,11 @@ fn derive_box_name(default: &str, name_fn: Option<&ASTNode>) -> &'static str {
if let Some(ASTNode::FunctionDeclaration { body, .. }) = name_fn {
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 let ASTNode::Literal {
value: nyash_rust::ast::LiteralValue::String(s),
..
} = &**v
{
let owned = s.clone();
return Box::leak(owned.into_boxed_str());
}
@ -141,21 +209,33 @@ fn derive_box_name(default: &str, name_fn: Option<&ASTNode>) -> &'static str {
Box::leak(default.to_string().into_boxed_str())
}
pub(crate) struct NyIdentityMacroBox { nm: &'static str }
pub(crate) struct NyIdentityMacroBox {
nm: &'static str,
}
impl super::macro_box::MacroBox for NyIdentityMacroBox {
fn name(&self) -> &'static str { self.nm }
fn name(&self) -> &'static str {
self.nm
}
fn expand(&self, ast: &ASTNode) -> ASTNode {
if std::env::var("NYASH_MACRO_BOX_NY_IDENTITY_ROUNDTRIP").ok().as_deref() == Some("1") {
if std::env::var("NYASH_MACRO_BOX_NY_IDENTITY_ROUNDTRIP")
.ok()
.as_deref()
== Some("1")
{
let j = crate::r#macro::ast_json::ast_to_json(ast);
if let Some(a2) = crate::r#macro::ast_json::json_to_ast(&j) { return a2; }
if let Some(a2) = crate::r#macro::ast_json::json_to_ast(&j) {
return a2;
}
}
ast.clone()
}
}
fn expand_is_identity(body: &Vec<ASTNode>, params: &Vec<String>) -> bool {
if body.len() != 1 { return false; }
if body.len() != 1 {
return false;
}
if let ASTNode::Return { value: Some(v), .. } = &body[0] {
if let ASTNode::Variable { name, .. } = &**v {
return params.get(0).map(|p| p == name).unwrap_or(false);
@ -165,11 +245,15 @@ fn expand_is_identity(body: &Vec<ASTNode>, params: &Vec<String>) -> bool {
}
fn expand_indicates_uppercase(body: &Vec<ASTNode>, params: &Vec<String>) -> bool {
if body.len() != 1 { return false; }
if body.len() != 1 {
return false;
}
let p0 = params.get(0).cloned().unwrap_or_else(|| "ast".to_string());
match &body[0] {
ASTNode::Return { value: Some(v), .. } => match &**v {
ASTNode::FunctionCall { name, arguments, .. } => {
ASTNode::FunctionCall {
name, arguments, ..
} => {
if (name == "uppercase_print" || name == "upper_print") && arguments.len() == 1 {
if let ASTNode::Variable { name: an, .. } = &arguments[0] {
return an == &p0;
@ -184,32 +268,80 @@ 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, IfMatchNormalize, ForForeachNormalize, EnvTagString }
pub enum MacroBehavior {
Identity,
Uppercase,
ArrayPrependZero,
MapInsertTag,
LoopNormalize,
IfMatchNormalize,
ForForeachNormalize,
EnvTagString,
}
pub fn analyze_macro_file(path: &str) -> MacroBehavior {
let src = match std::fs::read_to_string(path) { Ok(s) => s, Err(_) => return MacroBehavior::Identity };
let ast = match nyash_rust::parser::NyashParser::parse_from_string(&src) { Ok(a) => a, Err(_) => return MacroBehavior::Identity };
let src = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(_) => return MacroBehavior::Identity,
};
let ast = match nyash_rust::parser::NyashParser::parse_from_string(&src) {
Ok(a) => a,
Err(_) => return MacroBehavior::Identity,
};
// Quick heuristics based on literals present in file
fn ast_has_literal_string(a: &ASTNode, needle: &str) -> bool {
use nyash_rust::ast::ASTNode as A;
match a {
A::Literal { value: nyash_rust::ast::LiteralValue::String(s), .. } => s.contains(needle),
A::Program { statements, .. } => statements.iter().any(|n| ast_has_literal_string(n, needle)),
A::Literal {
value: nyash_rust::ast::LiteralValue::String(s),
..
} => s.contains(needle),
A::Program { statements, .. } => {
statements.iter().any(|n| ast_has_literal_string(n, needle))
}
A::Print { expression, .. } => ast_has_literal_string(expression, needle),
A::Return { value, .. } => value.as_ref().map(|v| ast_has_literal_string(v, needle)).unwrap_or(false),
A::Assignment { target, value, .. } => ast_has_literal_string(target, needle) || ast_has_literal_string(value, needle),
A::If { condition, then_body, else_body, .. } => {
A::Return { value, .. } => value
.as_ref()
.map(|v| ast_has_literal_string(v, needle))
.unwrap_or(false),
A::Assignment { target, value, .. } => {
ast_has_literal_string(target, needle) || ast_has_literal_string(value, needle)
}
A::If {
condition,
then_body,
else_body,
..
} => {
ast_has_literal_string(condition, needle)
|| then_body.iter().any(|n| ast_has_literal_string(n, needle))
|| else_body.as_ref().map(|v| v.iter().any(|n| ast_has_literal_string(n, needle))).unwrap_or(false)
|| else_body
.as_ref()
.map(|v| v.iter().any(|n| ast_has_literal_string(n, needle)))
.unwrap_or(false)
}
A::FunctionDeclaration { body, .. } => {
body.iter().any(|n| ast_has_literal_string(n, needle))
}
A::BinaryOp { left, right, .. } => {
ast_has_literal_string(left, needle) || ast_has_literal_string(right, needle)
}
A::FunctionDeclaration { body, .. } => body.iter().any(|n| ast_has_literal_string(n, needle)),
A::BinaryOp { left, right, .. } => ast_has_literal_string(left, needle) || ast_has_literal_string(right, needle),
A::UnaryOp { operand, .. } => ast_has_literal_string(operand, needle),
A::MethodCall { object, arguments, .. } => ast_has_literal_string(object, needle) || arguments.iter().any(|n| ast_has_literal_string(n, needle)),
A::FunctionCall { arguments, .. } => arguments.iter().any(|n| ast_has_literal_string(n, needle)),
A::ArrayLiteral { elements, .. } => elements.iter().any(|n| ast_has_literal_string(n, needle)),
A::MapLiteral { entries, .. } => entries.iter().any(|(_, v)| ast_has_literal_string(v, needle)),
A::MethodCall {
object, arguments, ..
} => {
ast_has_literal_string(object, needle)
|| arguments.iter().any(|n| ast_has_literal_string(n, needle))
}
A::FunctionCall { arguments, .. } => {
arguments.iter().any(|n| ast_has_literal_string(n, needle))
}
A::ArrayLiteral { elements, .. } => {
elements.iter().any(|n| ast_has_literal_string(n, needle))
}
A::MapLiteral { entries, .. } => entries
.iter()
.any(|(_, v)| ast_has_literal_string(v, needle)),
_ => false,
}
}
@ -218,29 +350,59 @@ pub fn analyze_macro_file(path: &str) -> MacroBehavior {
match a {
A::Program { statements, .. } => statements.iter().any(|n| ast_has_method(n, method)),
A::Print { expression, .. } => ast_has_method(expression, method),
A::Return { value, .. } => value.as_ref().map(|v| ast_has_method(v, method)).unwrap_or(false),
A::Assignment { target, value, .. } => ast_has_method(target, method) || ast_has_method(value, method),
A::If { condition, then_body, else_body, .. } => ast_has_method(condition, method)
|| then_body.iter().any(|n| ast_has_method(n, method))
|| else_body.as_ref().map(|v| v.iter().any(|n| ast_has_method(n, method))).unwrap_or(false),
A::Return { value, .. } => value
.as_ref()
.map(|v| ast_has_method(v, method))
.unwrap_or(false),
A::Assignment { target, value, .. } => {
ast_has_method(target, method) || ast_has_method(value, method)
}
A::If {
condition,
then_body,
else_body,
..
} => {
ast_has_method(condition, method)
|| then_body.iter().any(|n| ast_has_method(n, method))
|| else_body
.as_ref()
.map(|v| v.iter().any(|n| ast_has_method(n, method)))
.unwrap_or(false)
}
A::FunctionDeclaration { body, .. } => body.iter().any(|n| ast_has_method(n, method)),
A::BinaryOp { left, right, .. } => ast_has_method(left, method) || ast_has_method(right, method),
A::BinaryOp { left, right, .. } => {
ast_has_method(left, method) || ast_has_method(right, method)
}
A::UnaryOp { operand, .. } => ast_has_method(operand, method),
A::MethodCall { object, method: m, arguments, .. } => m == method
|| ast_has_method(object, method)
|| arguments.iter().any(|n| ast_has_method(n, method)),
A::FunctionCall { arguments, .. } => arguments.iter().any(|n| ast_has_method(n, method)),
A::MethodCall {
object,
method: m,
arguments,
..
} => {
m == method
|| ast_has_method(object, method)
|| arguments.iter().any(|n| ast_has_method(n, method))
}
A::FunctionCall { arguments, .. } => {
arguments.iter().any(|n| ast_has_method(n, method))
}
A::ArrayLiteral { elements, .. } => elements.iter().any(|n| ast_has_method(n, method)),
A::MapLiteral { entries, .. } => entries.iter().any(|(_, v)| ast_has_method(v, method)),
_ => false,
}
}
// Detect array prepend-zero macro by pattern strings present in macro source
if ast_has_literal_string(&ast, "\"kind\":\"Array\",\"elements\":[") || ast_has_literal_string(&ast, "\"elements\":[") {
if ast_has_literal_string(&ast, "\"kind\":\"Array\",\"elements\":[")
|| ast_has_literal_string(&ast, "\"elements\":[")
{
return MacroBehavior::ArrayPrependZero;
}
// Detect map insert-tag macro by pattern strings
if ast_has_literal_string(&ast, "\"kind\":\"Map\",\"entries\":[") || ast_has_literal_string(&ast, "\"entries\":[") {
if ast_has_literal_string(&ast, "\"kind\":\"Map\",\"entries\":[")
|| ast_has_literal_string(&ast, "\"entries\":[")
{
return MacroBehavior::MapInsertTag;
}
// Detect upper-string macro by pattern or toUpperCase usage
@ -253,23 +415,47 @@ 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 {
if let ASTNode::BoxDeclaration {
name: _, methods, ..
} = st
{
// Detect LoopNormalize/IfMatchNormalize by name() returning a specific string
if let Some(ASTNode::FunctionDeclaration { name: mname, body, .. }) = methods.get("name") {
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; }
if s == "ForForeach" { return MacroBehavior::ForForeachNormalize; }
if s == "EnvTagString" { return MacroBehavior::EnvTagString; }
if let ASTNode::Literal {
value: nyash_rust::ast::LiteralValue::String(s),
..
} = &**v
{
if s == "LoopNormalize" {
return MacroBehavior::LoopNormalize;
}
if s == "IfMatchNormalize" {
return MacroBehavior::IfMatchNormalize;
}
if s == "ForForeach" {
return MacroBehavior::ForForeachNormalize;
}
if s == "EnvTagString" {
return MacroBehavior::EnvTagString;
}
}
}
}
}
}
if let Some(ASTNode::FunctionDeclaration { name: mname, body, params, .. }) = methods.get("expand") {
if let Some(ASTNode::FunctionDeclaration {
name: mname,
body,
params,
..
}) = methods.get("expand")
{
if mname == "expand" {
if expand_indicates_uppercase(body, params) {
return MacroBehavior::Uppercase;
@ -282,7 +468,10 @@ pub fn analyze_macro_file(path: &str) -> MacroBehavior {
MacroBehavior::Identity
}
struct NyChildMacroBox { nm: &'static str, file: &'static str }
struct NyChildMacroBox {
nm: &'static str,
file: &'static str,
}
fn cap_enabled(name: &str) -> bool {
match std::env::var(name).ok() {
@ -301,67 +490,148 @@ fn caps_allow_macro_source(ast: &ASTNode) -> Result<(), String> {
fn scan(n: &A, seen: &mut Vec<String>) {
match n {
A::New { class, .. } => seen.push(class.clone()),
A::Program { statements, .. } => for s in statements { scan(s, seen); },
A::FunctionDeclaration { body, .. } => for s in body { scan(s, seen); },
A::Assignment { target, value, .. } => { scan(target, seen); scan(value, seen); },
A::Return { value, .. } => if let Some(v) = value { scan(v, seen); },
A::If { condition, then_body, else_body, .. } => {
scan(condition, seen);
for s in then_body { scan(s, seen); }
if let Some(b) = else_body { for s in b { scan(s, seen); } }
A::Program { statements, .. } => {
for s in statements {
scan(s, seen);
}
}
A::FunctionDeclaration { body, .. } => {
for s in body {
scan(s, seen);
}
}
A::Assignment { target, value, .. } => {
scan(target, seen);
scan(value, seen);
}
A::Return { value, .. } => {
if let Some(v) = value {
scan(v, seen);
}
}
A::If {
condition,
then_body,
else_body,
..
} => {
scan(condition, seen);
for s in then_body {
scan(s, seen);
}
if let Some(b) = else_body {
for s in b {
scan(s, seen);
}
}
}
A::BinaryOp { left, right, .. } => {
scan(left, seen);
scan(right, seen);
}
A::BinaryOp { left, right, .. } => { scan(left, seen); scan(right, seen); }
A::UnaryOp { operand, .. } => scan(operand, seen),
A::MethodCall { object, arguments, .. } => { scan(object, seen); for a in arguments { scan(a, seen); } }
A::FunctionCall { arguments, .. } => for a in arguments { scan(a, seen); },
A::ArrayLiteral { elements, .. } => for e in elements { scan(e, seen); },
A::MapLiteral { entries, .. } => for (_, v) in entries { scan(v, seen); },
A::MethodCall {
object, arguments, ..
} => {
scan(object, seen);
for a in arguments {
scan(a, seen);
}
}
A::FunctionCall { arguments, .. } => {
for a in arguments {
scan(a, seen);
}
}
A::ArrayLiteral { elements, .. } => {
for e in elements {
scan(e, seen);
}
}
A::MapLiteral { entries, .. } => {
for (_, v) in entries {
scan(v, seen);
}
}
_ => {}
}
}
let mut boxes = Vec::new();
scan(ast, &mut boxes);
if !allow_io && boxes.iter().any(|c| c == "FileBox" || c == "PathBox" || c == "DirBox") {
if !allow_io
&& boxes
.iter()
.any(|c| c == "FileBox" || c == "PathBox" || c == "DirBox")
{
return Err("macro capability violation: IO (File/Path/Dir) denied".into());
}
if !allow_net && boxes.iter().any(|c| c.contains("HTTP") || c.contains("Http") || c == "SocketBox") {
if !allow_net
&& boxes
.iter()
.any(|c| c.contains("HTTP") || c.contains("Http") || c == "SocketBox")
{
return Err("macro capability violation: NET (HTTP/Socket) denied".into());
}
Ok(())
}
impl super::macro_box::MacroBox for NyChildMacroBox {
fn name(&self) -> &'static str { self.nm }
fn name(&self) -> &'static str {
self.nm
}
fn expand(&self, ast: &ASTNode) -> ASTNode {
// Parent-side proxy: prefer runner script (PyVM) when enabled; otherwise fallback to internal child mode.
let exe = match std::env::current_exe() {
Ok(p) => p,
Err(e) => { eprintln!("[macro-proxy] current_exe failed: {}", e); return ast.clone(); }
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(false);
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");
eprintln!(
"[macro][compat] NYASH_MACRO_BOX_CHILD_RUNNER is deprecated; prefer defaults"
);
}
let mut cmd = std::process::Command::new(exe.clone());
// Build MacroCtx JSON once (caps only, MVP)
let mctx = crate::r#macro::ctx::MacroCtx::from_env();
let ctx_json = format!("{{\"caps\":{{\"io\":{},\"net\":{},\"env\":{}}}}}", mctx.caps.io, mctx.caps.net, mctx.caps.env);
let ctx_json = format!(
"{{\"caps\":{{\"io\":{},\"net\":{},\"env\":{}}}}}",
mctx.caps.io, mctx.caps.net, mctx.caps.env
);
if use_runner {
// Synthesize a tiny runner that inlines the macro file and calls MacroBoxSpec.expand
use std::io::Write as _;
let tmp_dir = std::path::Path::new("tmp");
let _ = std::fs::create_dir_all(tmp_dir);
let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis();
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let tmp_path = tmp_dir.join(format!("macro_expand_runner_{}.hako", ts));
let mut f = match std::fs::File::create(&tmp_path) { Ok(x) => x, Err(e) => { eprintln!("[macro-proxy] create tmp runner failed: {}", e); return ast.clone(); } };
let mut f = match std::fs::File::create(&tmp_path) {
Ok(x) => x,
Err(e) => {
eprintln!("[macro-proxy] create tmp runner failed: {}", e);
return ast.clone();
}
};
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 {{\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(); }
if let Err(e) = f.write_all(script.as_bytes()) {
eprintln!("[macro-proxy] write tmp runner failed: {}", e);
return ast.clone();
}
// Run Nyash runner script under PyVM: nyash --backend vm <tmp_runner> -- <json>
cmd.arg("--backend").arg("vm").arg(tmp_path);
// Append script args after '--'
@ -372,7 +642,8 @@ impl super::macro_box::MacroBox for NyChildMacroBox {
cmd.stdin(std::process::Stdio::null());
} else {
// Internal child mode: --macro-expand-child <macro file> with stdin JSON
cmd.arg("--macro-expand-child").arg(self.file)
cmd.arg("--macro-expand-child")
.arg(self.file)
.stdin(std::process::Stdio::piped());
// Provide MacroCtx via env for internal child
cmd.env("NYASH_MACRO_CTX_JSON", ctx_json.clone());
@ -393,13 +664,21 @@ impl super::macro_box::MacroBox for NyChildMacroBox {
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);
let timeout_ms: u64 = std::env::var("NYASH_NY_COMPILER_TIMEOUT_MS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(2000);
// Spawn
let mut child = match cmd.spawn() { Ok(c) => c, Err(e) => {
eprintln!("[macro-proxy] spawn failed: {}", e);
if strict_enabled() { std::process::exit(2); }
return ast.clone();
} };
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
eprintln!("[macro-proxy] spawn failed: {}", e);
if strict_enabled() {
std::process::exit(2);
}
return ast.clone();
}
};
// Write stdin only in internal child mode
if !use_runner {
if let Some(mut sin) = child.stdin.take() {
@ -415,34 +694,60 @@ impl super::macro_box::MacroBox for NyChildMacroBox {
loop {
match child.try_wait() {
Ok(Some(_status)) => {
if let Some(mut so) = child.stdout.take() { use std::io::Read; let _ = so.read_to_string(&mut out); }
if let Some(mut so) = child.stdout.take() {
use std::io::Read;
let _ = so.read_to_string(&mut out);
}
break;
}
Ok(None) => {
if start.elapsed() >= Duration::from_millis(timeout_ms) {
let _ = child.kill(); let _ = child.wait();
let _ = child.kill();
let _ = child.wait();
eprintln!("[macro-proxy] timeout {} ms", timeout_ms);
if strict_enabled() { std::process::exit(124); }
if strict_enabled() {
std::process::exit(124);
}
return ast.clone();
}
std::thread::sleep(Duration::from_millis(5));
}
Err(e) => { eprintln!("[macro-proxy] wait error: {}", e); if strict_enabled() { std::process::exit(2); } return ast.clone(); }
Err(e) => {
eprintln!("[macro-proxy] wait error: {}", e);
if strict_enabled() {
std::process::exit(2);
}
return ast.clone();
}
}
}
// capture stderr for diagnostics and continue
// 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); }
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. stderr=\n{}", err); if strict_enabled() { std::process::exit(2); } ast.clone() }
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) => {
eprintln!("[macro-proxy] invalid JSON from child: {}\n-- child stderr --\n{}\n-- end stderr --", e, err);
if strict_enabled() { std::process::exit(2); }
if strict_enabled() {
std::process::exit(2);
}
ast.clone()
}
}

View File

@ -3,22 +3,28 @@
//! Goal: Provide minimal, typed interfaces for AST pattern matching and
//! HIR patch based expansion. Backends (MIR/JIT/LLVM) remain unchanged.
pub mod pattern;
pub mod ast_json;
pub mod ctx;
pub mod engine;
pub mod macro_box;
pub mod macro_box_ny;
pub mod ast_json;
pub mod ctx;
pub mod pattern;
use nyash_rust::ASTNode;
/// Enable/disable macro system via env gate.
pub fn enabled() -> bool {
// Default ON. Disable with NYASH_MACRO_DISABLE=1 or NYASH_MACRO_ENABLE=0/false/off.
if let Ok(v) = std::env::var("NYASH_MACRO_DISABLE") { if v == "1" { return false; } }
if let Ok(v) = std::env::var("NYASH_MACRO_DISABLE") {
if v == "1" {
return false;
}
}
if let Ok(v) = std::env::var("NYASH_MACRO_ENABLE") {
let v = v.to_ascii_lowercase();
if v == "0" || v == "false" || v == "off" { return false; }
if v == "0" || v == "false" || v == "off" {
return false;
}
return true;
}
true
@ -26,15 +32,21 @@ pub fn enabled() -> bool {
/// A hook to dump AST for `--expand` (pre/post). Expansion is no-op for now.
pub fn maybe_expand_and_dump(ast: &ASTNode, _dump_only: bool) -> ASTNode {
if !enabled() { return ast.clone(); }
if !enabled() {
return ast.clone();
}
// Initialize user macro boxes (if any, behind env gates)
self::macro_box::init_builtin();
self::macro_box_ny::init_from_env();
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { eprintln!("[macro] input AST: {:?}", ast); }
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro] input AST: {:?}", ast);
}
let mut eng = self::engine::MacroEngine::new();
let (out, _patches) = eng.expand(ast);
let out2 = maybe_inject_test_harness(&out);
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { eprintln!("[macro] output AST: {:?}", out2); }
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro] output AST: {:?}", out2);
}
out2
}
@ -44,7 +56,11 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
}
// Test call plan
#[derive(Clone)]
struct TestPlan { label: String, setup: Option<nyash_rust::ASTNode>, call: nyash_rust::ASTNode }
struct TestPlan {
label: String,
setup: Option<nyash_rust::ASTNode>,
call: nyash_rust::ASTNode,
}
// Collect tests (top-level and Box)
let mut tests: Vec<TestPlan> = Vec::new();
@ -56,9 +72,16 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
// - Detailed per-test: { "Box.method": { "args": [...], "instance": {"ctor":"new|birth","args":[...] } } }
// - Typed values inside args supported via objects (see json_to_ast below)
#[derive(Clone, Default)]
struct InstanceSpec { ctor: String, args: Vec<nyash_rust::ASTNode>, type_args: Vec<String> }
struct InstanceSpec {
ctor: String,
args: Vec<nyash_rust::ASTNode>,
type_args: Vec<String>,
}
#[derive(Clone, Default)]
struct TestArgSpec { args: Vec<nyash_rust::ASTNode>, instance: Option<InstanceSpec> }
struct TestArgSpec {
args: Vec<nyash_rust::ASTNode>,
instance: Option<InstanceSpec>,
}
fn json_err(msg: &str) {
eprintln!("[macro][test][args] {}", msg);
@ -67,76 +90,163 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
fn json_to_ast(v: &serde_json::Value) -> Result<nyash_rust::ASTNode, String> {
use nyash_rust::ast::{ASTNode as A, LiteralValue, Span};
match v {
serde_json::Value::String(st) => Ok(A::Literal { value: LiteralValue::String(st.clone()), span: Span::unknown() }),
serde_json::Value::Bool(b) => Ok(A::Literal { value: LiteralValue::Bool(*b), span: Span::unknown() }),
serde_json::Value::String(st) => Ok(A::Literal {
value: LiteralValue::String(st.clone()),
span: Span::unknown(),
}),
serde_json::Value::Bool(b) => Ok(A::Literal {
value: LiteralValue::Bool(*b),
span: Span::unknown(),
}),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(A::Literal { value: LiteralValue::Integer(i), span: Span::unknown() })
Ok(A::Literal {
value: LiteralValue::Integer(i),
span: Span::unknown(),
})
} else if let Some(f) = n.as_f64() {
Ok(A::Literal { value: LiteralValue::Float(f), span: Span::unknown() })
Ok(A::Literal {
value: LiteralValue::Float(f),
span: Span::unknown(),
})
} else {
Err("unsupported number literal".into())
}
}
serde_json::Value::Null => Ok(A::Literal { value: LiteralValue::Null, span: Span::unknown() }),
serde_json::Value::Null => Ok(A::Literal {
value: LiteralValue::Null,
span: Span::unknown(),
}),
serde_json::Value::Array(elems) => {
// Treat nested arrays as ArrayLiteral by default
let mut out = Vec::with_capacity(elems.len());
for x in elems { out.push(json_to_ast(x)?); }
Ok(A::ArrayLiteral { elements: out, span: Span::unknown() })
for x in elems {
out.push(json_to_ast(x)?);
}
Ok(A::ArrayLiteral {
elements: out,
span: Span::unknown(),
})
}
serde_json::Value::Object(obj) => {
// Typed shorthands accepted: {i:1}|{int:1}, {f:1.2}|{float:1.2}, {s:"x"}|{string:"x"}, {b:true}|{bool:true}
if let Some(v) = obj.get("i").or_else(|| obj.get("int")) { return json_to_ast(v); }
if let Some(v) = obj.get("f").or_else(|| obj.get("float")) { return json_to_ast(v); }
if let Some(v) = obj.get("s").or_else(|| obj.get("string")) { return json_to_ast(v); }
if let Some(v) = obj.get("b").or_else(|| obj.get("bool")) { return json_to_ast(v); }
if let Some(v) = obj.get("i").or_else(|| obj.get("int")) {
return json_to_ast(v);
}
if let Some(v) = obj.get("f").or_else(|| obj.get("float")) {
return json_to_ast(v);
}
if let Some(v) = obj.get("s").or_else(|| obj.get("string")) {
return json_to_ast(v);
}
if let Some(v) = obj.get("b").or_else(|| obj.get("bool")) {
return json_to_ast(v);
}
if let Some(map) = obj.get("map") {
if let Some(mo) = map.as_object() {
let mut ents: Vec<(String, nyash_rust::ASTNode)> = Vec::with_capacity(mo.len());
for (k, vv) in mo { ents.push((k.clone(), json_to_ast(vv)?)); }
return Ok(A::MapLiteral { entries: ents, span: Span::unknown() });
} else { return Err("map must be an object".into()); }
let mut ents: Vec<(String, nyash_rust::ASTNode)> =
Vec::with_capacity(mo.len());
for (k, vv) in mo {
ents.push((k.clone(), json_to_ast(vv)?));
}
return Ok(A::MapLiteral {
entries: ents,
span: Span::unknown(),
});
} else {
return Err("map must be an object".into());
}
}
if let Some(arr) = obj.get("array") {
if let Some(va) = arr.as_array() {
let mut out = Vec::with_capacity(va.len());
for x in va { out.push(json_to_ast(x)?); }
return Ok(A::ArrayLiteral { elements: out, span: Span::unknown() });
} else { return Err("array must be an array".into()); }
for x in va {
out.push(json_to_ast(x)?);
}
return Ok(A::ArrayLiteral {
elements: out,
span: Span::unknown(),
});
} else {
return Err("array must be an array".into());
}
}
if let Some(name) = obj.get("var").and_then(|v| v.as_str()) {
return Ok(A::Variable { name: name.to_string(), span: Span::unknown() });
return Ok(A::Variable {
name: name.to_string(),
span: Span::unknown(),
});
}
if let Some(name) = obj.get("call").and_then(|v| v.as_str()) {
let mut args: Vec<A> = Vec::new();
if let Some(va) = obj.get("args").and_then(|v| v.as_array()) {
for x in va { args.push(json_to_ast(x)?); }
for x in va {
args.push(json_to_ast(x)?);
}
}
return Ok(A::FunctionCall { name: name.to_string(), arguments: args, span: Span::unknown() });
return Ok(A::FunctionCall {
name: name.to_string(),
arguments: args,
span: Span::unknown(),
});
}
if let Some(method) = obj.get("method").and_then(|v| v.as_str()) {
let objv = obj.get("object").ok_or_else(|| "method requires 'object'".to_string())?;
let objv = obj
.get("object")
.ok_or_else(|| "method requires 'object'".to_string())?;
let object = json_to_ast(objv)?;
let mut args: Vec<A> = Vec::new();
if let Some(va) = obj.get("args").and_then(|v| v.as_array()) {
for x in va { args.push(json_to_ast(x)?); }
for x in va {
args.push(json_to_ast(x)?);
}
}
return Ok(A::MethodCall { object: Box::new(object), method: method.to_string(), arguments: args, span: Span::unknown() });
return Ok(A::MethodCall {
object: Box::new(object),
method: method.to_string(),
arguments: args,
span: Span::unknown(),
});
}
if let Some(bx) = obj.get("box").and_then(|v| v.as_str()) {
let mut args: Vec<A> = Vec::new();
if let Some(va) = obj.get("args").and_then(|v| v.as_array()) {
for x in va { args.push(json_to_ast(x)?); }
for x in va {
args.push(json_to_ast(x)?);
}
}
let type_args: Vec<String> = obj.get("type_args").and_then(|v| v.as_array()).map(|arr| arr.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect()).unwrap_or_default();
let type_args: Vec<String> = obj
.get("type_args")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|x| x.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let ctor = obj.get("ctor").and_then(|v| v.as_str()).unwrap_or("new");
if ctor == "new" {
return Ok(A::New { class: bx.to_string(), arguments: args, type_arguments: type_args, span: Span::unknown() });
return Ok(A::New {
class: bx.to_string(),
arguments: args,
type_arguments: type_args,
span: Span::unknown(),
});
} else if ctor == "birth" {
return Ok(A::MethodCall { object: Box::new(A::Variable { name: bx.to_string(), span: Span::unknown() }), method: "birth".into(), arguments: args, span: Span::unknown() });
return Ok(A::MethodCall {
object: Box::new(A::Variable {
name: bx.to_string(),
span: Span::unknown(),
}),
method: "birth".into(),
arguments: args,
span: Span::unknown(),
});
} else {
return Err(format!("unknown ctor '{}', expected 'new' or 'birth'", ctor));
return Err(format!(
"unknown ctor '{}', expected 'new' or 'birth'",
ctor
));
}
}
Err("unknown object mapping for AST".into())
@ -148,38 +258,90 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
match v {
serde_json::Value::Array(arr) => {
let mut out: Vec<nyash_rust::ASTNode> = Vec::new();
for a in arr { match json_to_ast(a) { Ok(n) => out.push(n), Err(e) => { json_err(&format!("args element error: {}", e)); return None; } } }
Some(TestArgSpec { args: out, instance: None })
for a in arr {
match json_to_ast(a) {
Ok(n) => out.push(n),
Err(e) => {
json_err(&format!("args element error: {}", e));
return None;
}
}
}
Some(TestArgSpec {
args: out,
instance: None,
})
}
serde_json::Value::Object(obj) => {
let mut spec = TestArgSpec::default();
if let Some(a) = obj.get("args").and_then(|v| v.as_array()) {
let mut out: Vec<nyash_rust::ASTNode> = Vec::new();
for x in a { match json_to_ast(x) { Ok(n) => out.push(n), Err(e) => { json_err(&format!("args element error: {}", e)); return None; } } }
for x in a {
match json_to_ast(x) {
Ok(n) => out.push(n),
Err(e) => {
json_err(&format!("args element error: {}", e));
return None;
}
}
}
spec.args = out;
}
if let Some(inst) = obj.get("instance").and_then(|v| v.as_object()) {
let ctor = inst.get("ctor").and_then(|v| v.as_str()).unwrap_or("new").to_string();
let type_args: Vec<String> = inst.get("type_args").and_then(|v| v.as_array()).map(|arr| arr.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect()).unwrap_or_default();
let ctor = inst
.get("ctor")
.and_then(|v| v.as_str())
.unwrap_or("new")
.to_string();
let type_args: Vec<String> = inst
.get("type_args")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|x| x.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let mut args: Vec<nyash_rust::ASTNode> = Vec::new();
if let Some(va) = inst.get("args").and_then(|v| v.as_array()) {
for x in va { match json_to_ast(x) { Ok(n) => args.push(n), Err(e) => { json_err(&format!("instance.args element error: {}", e)); return None; } } }
for x in va {
match json_to_ast(x) {
Ok(n) => args.push(n),
Err(e) => {
json_err(&format!("instance.args element error: {}", e));
return None;
}
}
}
}
spec.instance = Some(InstanceSpec { ctor, args, type_args });
spec.instance = Some(InstanceSpec {
ctor,
args,
type_args,
});
}
Some(spec)
}
_ => { json_err("test value must be array or object"); None }
_ => {
json_err("test value must be array or object");
None
}
}
}
let args_map: Option<std::collections::HashMap<String, TestArgSpec>> = (|| {
if let Ok(s) = std::env::var("NYASH_TEST_ARGS_JSON") {
if s.trim().is_empty() { return None; }
if s.trim().is_empty() {
return None;
}
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) {
let mut map = std::collections::HashMap::new();
if let Some(obj) = v.as_object() {
for (k, vv) in obj { if let Some(spec) = parse_test_arg_spec(vv) { map.insert(k.clone(), spec); } }
for (k, vv) in obj {
if let Some(spec) = parse_test_arg_spec(vv) {
map.insert(k.clone(), spec);
}
}
return Some(map);
}
}
@ -190,21 +352,48 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
for st in statements {
match st {
nyash_rust::ASTNode::FunctionDeclaration { name, params, .. } => {
if name == "main" { has_main_fn = true; _main_params_len = params.len(); }
if name == "main" {
has_main_fn = true;
_main_params_len = params.len();
}
if name.starts_with("test_") {
let label = name.clone();
// select args: JSON map > defaults > skip
let mut maybe_args: Option<Vec<nyash_rust::ASTNode>> = None;
if let Some(m) = &args_map { if let Some(v) = m.get(&label) { maybe_args = Some(v.args.clone()); } }
let args = if let Some(a) = maybe_args { a }
else if !params.is_empty() && std::env::var("NYASH_TEST_ARGS_DEFAULTS").ok().as_deref() == Some("1") {
let mut a: Vec<nyash_rust::ASTNode> = Vec::new(); for _ in params { a.push(nyash_rust::ASTNode::Literal{ value: nyash_rust::ast::LiteralValue::Integer(0), span: nyash_rust::ast::Span::unknown() }); } a
} else if params.is_empty() { Vec::new() }
else {
eprintln!("[macro][test][args] missing args for {} (need {}), skipping (set NYASH_TEST_ARGS_DEFAULTS=1 for zero defaults)", label, params.len());
continue
};
tests.push(TestPlan { label, setup: None, call: nyash_rust::ASTNode::FunctionCall { name: name.clone(), arguments: args, span: nyash_rust::ast::Span::unknown() } });
if let Some(m) = &args_map {
if let Some(v) = m.get(&label) {
maybe_args = Some(v.args.clone());
}
}
let args = if let Some(a) = maybe_args {
a
} else if !params.is_empty()
&& std::env::var("NYASH_TEST_ARGS_DEFAULTS").ok().as_deref()
== Some("1")
{
let mut a: Vec<nyash_rust::ASTNode> = Vec::new();
for _ in params {
a.push(nyash_rust::ASTNode::Literal {
value: nyash_rust::ast::LiteralValue::Integer(0),
span: nyash_rust::ast::Span::unknown(),
});
}
a
} else if params.is_empty() {
Vec::new()
} else {
eprintln!("[macro][test][args] missing args for {} (need {}), skipping (set NYASH_TEST_ARGS_DEFAULTS=1 for zero defaults)", label, params.len());
continue;
};
tests.push(TestPlan {
label,
setup: None,
call: nyash_rust::ASTNode::FunctionCall {
name: name.clone(),
arguments: args,
span: nyash_rust::ast::Span::unknown(),
},
});
}
}
_ => {}
@ -214,45 +403,101 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
// Collect Box tests: static and instance (no-arg only for instance)
if let nyash_rust::ASTNode::Program { statements, .. } = ast {
for st in statements {
if let nyash_rust::ASTNode::BoxDeclaration { name: box_name, methods, .. } = st {
if let nyash_rust::ASTNode::BoxDeclaration {
name: box_name,
methods,
..
} = st
{
for (mname, mnode) in methods {
if !mname.starts_with("test_") { continue; }
if let nyash_rust::ASTNode::FunctionDeclaration { is_static, params, .. } = mnode {
if !mname.starts_with("test_") {
continue;
}
if let nyash_rust::ASTNode::FunctionDeclaration {
is_static, params, ..
} = mnode
{
if *is_static {
// Static: BoxName.test_*()
let mut args: Vec<nyash_rust::ASTNode> = Vec::new();
if let Some(m) = &args_map { if let Some(v) = m.get(&format!("{}.{}", box_name, mname)) { args = v.args.clone(); } }
if let Some(m) = &args_map {
if let Some(v) = m.get(&format!("{}.{}", box_name, mname)) {
args = v.args.clone();
}
}
if args.is_empty() && !params.is_empty() {
if std::env::var("NYASH_TEST_ARGS_DEFAULTS").ok().as_deref() == Some("1") { for _ in params { args.push(nyash_rust::ASTNode::Literal { value: nyash_rust::ast::LiteralValue::Integer(0), span: nyash_rust::ast::Span::unknown() }); } }
else {
if std::env::var("NYASH_TEST_ARGS_DEFAULTS").ok().as_deref()
== Some("1")
{
for _ in params {
args.push(nyash_rust::ASTNode::Literal {
value: nyash_rust::ast::LiteralValue::Integer(0),
span: nyash_rust::ast::Span::unknown(),
});
}
} else {
eprintln!("[macro][test][args] missing args for {}.{} (need {}), skipping", box_name, mname, params.len());
continue;
}
}
let call = nyash_rust::ASTNode::MethodCall {
object: Box::new(nyash_rust::ASTNode::Variable { name: box_name.clone(), span: nyash_rust::ast::Span::unknown() }),
object: Box::new(nyash_rust::ASTNode::Variable {
name: box_name.clone(),
span: nyash_rust::ast::Span::unknown(),
}),
method: mname.clone(),
arguments: args,
span: nyash_rust::ast::Span::unknown(),
};
tests.push(TestPlan { label: format!("{}.{}", box_name, mname), setup: None, call });
tests.push(TestPlan {
label: format!("{}.{}", box_name, mname),
setup: None,
call,
});
} else {
// Instance: try new BoxName() then .test_*()
let inst_var = format!("__t_{}", box_name.to_lowercase());
// Instance override via JSON
let mut inst_ctor: Option<InstanceSpec> = None;
if let Some(m) = &args_map { if let Some(v) = m.get(&format!("{}.{}", box_name, mname)) { inst_ctor = v.instance.clone(); } }
if let Some(m) = &args_map {
if let Some(v) = m.get(&format!("{}.{}", box_name, mname)) {
inst_ctor = v.instance.clone();
}
}
let inst_init: nyash_rust::ASTNode = if let Some(spec) = inst_ctor {
match spec.ctor.as_str() {
"new" => nyash_rust::ASTNode::New { class: box_name.clone(), arguments: spec.args, type_arguments: spec.type_args, span: nyash_rust::ast::Span::unknown() },
"birth" => nyash_rust::ASTNode::MethodCall { object: Box::new(nyash_rust::ASTNode::Variable { name: box_name.clone(), span: nyash_rust::ast::Span::unknown() }), method: "birth".into(), arguments: spec.args, span: nyash_rust::ast::Span::unknown() },
"new" => nyash_rust::ASTNode::New {
class: box_name.clone(),
arguments: spec.args,
type_arguments: spec.type_args,
span: nyash_rust::ast::Span::unknown(),
},
"birth" => nyash_rust::ASTNode::MethodCall {
object: Box::new(nyash_rust::ASTNode::Variable {
name: box_name.clone(),
span: nyash_rust::ast::Span::unknown(),
}),
method: "birth".into(),
arguments: spec.args,
span: nyash_rust::ast::Span::unknown(),
},
other => {
eprintln!("[macro][test][args] unknown ctor '{}' for {}.{}, using new()", other, box_name, mname);
nyash_rust::ASTNode::New { class: box_name.clone(), arguments: vec![], type_arguments: vec![], span: nyash_rust::ast::Span::unknown() }
nyash_rust::ASTNode::New {
class: box_name.clone(),
arguments: vec![],
type_arguments: vec![],
span: nyash_rust::ast::Span::unknown(),
}
}
}
} else {
nyash_rust::ASTNode::New { class: box_name.clone(), arguments: vec![], type_arguments: vec![], span: nyash_rust::ast::Span::unknown() }
nyash_rust::ASTNode::New {
class: box_name.clone(),
arguments: vec![],
type_arguments: vec![],
span: nyash_rust::ast::Span::unknown(),
}
};
let setup = nyash_rust::ASTNode::Local {
variables: vec![inst_var.clone()],
@ -260,21 +505,40 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
span: nyash_rust::ast::Span::unknown(),
};
let mut args: Vec<nyash_rust::ASTNode> = Vec::new();
if let Some(m) = &args_map { if let Some(v) = m.get(&format!("{}.{}", box_name, mname)) { args = v.args.clone(); } }
if let Some(m) = &args_map {
if let Some(v) = m.get(&format!("{}.{}", box_name, mname)) {
args = v.args.clone();
}
}
if args.is_empty() && !params.is_empty() {
if std::env::var("NYASH_TEST_ARGS_DEFAULTS").ok().as_deref() == Some("1") { for _ in params { args.push(nyash_rust::ASTNode::Literal { value: nyash_rust::ast::LiteralValue::Integer(0), span: nyash_rust::ast::Span::unknown() }); } }
else {
if std::env::var("NYASH_TEST_ARGS_DEFAULTS").ok().as_deref()
== Some("1")
{
for _ in params {
args.push(nyash_rust::ASTNode::Literal {
value: nyash_rust::ast::LiteralValue::Integer(0),
span: nyash_rust::ast::Span::unknown(),
});
}
} else {
eprintln!("[macro][test][args] missing args for {}.{} (need {}), skipping", box_name, mname, params.len());
continue;
}
}
let call = nyash_rust::ASTNode::MethodCall {
object: Box::new(nyash_rust::ASTNode::Variable { name: inst_var.clone(), span: nyash_rust::ast::Span::unknown() }),
object: Box::new(nyash_rust::ASTNode::Variable {
name: inst_var.clone(),
span: nyash_rust::ast::Span::unknown(),
}),
method: mname.clone(),
arguments: args,
span: nyash_rust::ast::Span::unknown(),
};
tests.push(TestPlan { label: format!("{}.{}", box_name, mname), setup: Some(setup), call });
tests.push(TestPlan {
label: format!("{}.{}", box_name, mname),
setup: Some(setup),
call,
});
}
}
}
@ -288,7 +552,9 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
}
}
if tests.is_empty() {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { eprintln!("[macro][test] no tests found (functions starting with 'test_')"); }
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][test] no tests found (functions starting with 'test_')");
}
return ast.clone();
}
// Decide entry policy when main exists
@ -297,34 +563,128 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
let ret_policy = std::env::var("NYASH_TEST_RETURN").ok(); // Some("tests"|"original") default tests
// Build harness: top-level function main(args) { ... }
use nyash_rust::ast::{ASTNode as A, Span, LiteralValue, BinaryOperator};
use nyash_rust::ast::{ASTNode as A, BinaryOperator, LiteralValue, Span};
let mut body: Vec<A> = Vec::new();
// locals: pass=0, fail=0
body.push(A::Local { variables: vec!["pass".into(), "fail".into()], initial_values: vec![Some(Box::new(A::Literal{ value: LiteralValue::Integer(0), span: Span::unknown()})), Some(Box::new(A::Literal{ value: LiteralValue::Integer(0), span: Span::unknown()}))], span: Span::unknown() });
body.push(A::Local {
variables: vec!["pass".into(), "fail".into()],
initial_values: vec![
Some(Box::new(A::Literal {
value: LiteralValue::Integer(0),
span: Span::unknown(),
})),
Some(Box::new(A::Literal {
value: LiteralValue::Integer(0),
span: Span::unknown(),
})),
],
span: Span::unknown(),
});
for tp in &tests {
// optional setup
if let Some(set) = tp.setup.clone() { body.push(set); }
if let Some(set) = tp.setup.clone() {
body.push(set);
}
// local r = CALL
body.push(A::Local { variables: vec!["r".into()], initial_values: vec![Some(Box::new(tp.call.clone()))], span: Span::unknown() });
body.push(A::Local {
variables: vec!["r".into()],
initial_values: vec![Some(Box::new(tp.call.clone()))],
span: Span::unknown(),
});
// if r { print("PASS t"); pass = pass + 1 } else { print("FAIL t"); fail = fail + 1 }
let pass_msg = A::Literal { value: LiteralValue::String(format!("PASS {}", tp.label)), span: Span::unknown() };
let fail_msg = A::Literal { value: LiteralValue::String(format!("FAIL {}", tp.label)), span: Span::unknown() };
let pass_msg = A::Literal {
value: LiteralValue::String(format!("PASS {}", tp.label)),
span: Span::unknown(),
};
let fail_msg = A::Literal {
value: LiteralValue::String(format!("FAIL {}", tp.label)),
span: Span::unknown(),
};
let then_body = vec![
A::Print { expression: Box::new(pass_msg), span: Span::unknown() },
A::Assignment { target: Box::new(A::Variable{ name: "pass".into(), span: Span::unknown() }), value: Box::new(A::BinaryOp{ operator: BinaryOperator::Add, left: Box::new(A::Variable{ name: "pass".into(), span: Span::unknown() }), right: Box::new(A::Literal{ value: LiteralValue::Integer(1), span: Span::unknown() }), span: Span::unknown() }), span: Span::unknown() },
A::Print {
expression: Box::new(pass_msg),
span: Span::unknown(),
},
A::Assignment {
target: Box::new(A::Variable {
name: "pass".into(),
span: Span::unknown(),
}),
value: Box::new(A::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(A::Variable {
name: "pass".into(),
span: Span::unknown(),
}),
right: Box::new(A::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
let else_body = vec![
A::Print { expression: Box::new(fail_msg), span: Span::unknown() },
A::Assignment { target: Box::new(A::Variable{ name: "fail".into(), span: Span::unknown() }), value: Box::new(A::BinaryOp{ operator: BinaryOperator::Add, left: Box::new(A::Variable{ name: "fail".into(), span: Span::unknown() }), right: Box::new(A::Literal{ value: LiteralValue::Integer(1), span: Span::unknown() }), span: Span::unknown() }), span: Span::unknown() },
A::Print {
expression: Box::new(fail_msg),
span: Span::unknown(),
},
A::Assignment {
target: Box::new(A::Variable {
name: "fail".into(),
span: Span::unknown(),
}),
value: Box::new(A::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(A::Variable {
name: "fail".into(),
span: Span::unknown(),
}),
right: Box::new(A::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
body.push(A::If { condition: Box::new(A::Variable { name: "r".into(), span: Span::unknown() }), then_body, else_body: Some(else_body), span: Span::unknown() });
body.push(A::If {
condition: Box::new(A::Variable {
name: "r".into(),
span: Span::unknown(),
}),
then_body,
else_body: Some(else_body),
span: Span::unknown(),
});
}
// print summary and return fail
body.push(A::Print { expression: Box::new(A::Literal{ value: LiteralValue::String(format!("Summary: {} tests", tests.len())), span: Span::unknown() }), span: Span::unknown() });
body.push(A::Return { value: Some(Box::new(A::Variable{ name: "fail".into(), span: Span::unknown() })), span: Span::unknown() });
body.push(A::Print {
expression: Box::new(A::Literal {
value: LiteralValue::String(format!("Summary: {} tests", tests.len())),
span: Span::unknown(),
}),
span: Span::unknown(),
});
body.push(A::Return {
value: Some(Box::new(A::Variable {
name: "fail".into(),
span: Span::unknown(),
})),
span: Span::unknown(),
});
// Build harness main body as above
let make_harness_main = |body: Vec<A>| -> A {
A::FunctionDeclaration { name: "main".into(), params: vec!["args".into()], body, is_static: false, is_override: false, span: Span::unknown() }
A::FunctionDeclaration {
name: "main".into(),
params: vec!["args".into()],
body,
is_static: false,
is_override: false,
span: Span::unknown(),
}
};
// Transform AST according to policy
@ -334,27 +694,64 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
let mut orig_call_fn: Option<A> = None;
for st in statements {
match st {
A::FunctionDeclaration { name, params, body: orig_body, is_static, is_override, span: fspan } if name == "main" => {
A::FunctionDeclaration {
name,
params,
body: orig_body,
is_static,
is_override,
span: fspan,
} if name == "main" => {
if has_main_fn && (force || entry_mode.is_some()) {
// rename original main
let new_name = "__ny_orig_main".to_string();
out_stmts.push(A::FunctionDeclaration { name: new_name.clone(), params: params.clone(), body: orig_body.clone(), is_static, is_override, span: fspan });
out_stmts.push(A::FunctionDeclaration {
name: new_name.clone(),
params: params.clone(),
body: orig_body.clone(),
is_static,
is_override,
span: fspan,
});
_renamed_main = true;
if entry_mode.as_deref() == Some("wrap") {
let args_exprs = if params.len() >= 1 { vec![A::Variable { name: "args".into(), span: nyash_rust::ast::Span::unknown() }] } else { vec![] };
orig_call_fn = Some(A::FunctionCall { name: new_name, arguments: args_exprs, span: nyash_rust::ast::Span::unknown() });
let args_exprs = if params.len() >= 1 {
vec![A::Variable {
name: "args".into(),
span: nyash_rust::ast::Span::unknown(),
}]
} else {
vec![]
};
orig_call_fn = Some(A::FunctionCall {
name: new_name,
arguments: args_exprs,
span: nyash_rust::ast::Span::unknown(),
});
}
} else {
// keep as-is (no injection)
out_stmts.push(A::FunctionDeclaration { name, params, body: orig_body, is_static, is_override, span: fspan });
out_stmts.push(A::FunctionDeclaration {
name,
params,
body: orig_body,
is_static,
is_override,
span: fspan,
});
}
}
other => out_stmts.push(other),
}
}
if has_main_fn && !(force || entry_mode.is_some()) {
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") { eprintln!("[macro][test] existing main detected; skip harness (set --test-entry or NYASH_TEST_FORCE=1)"); }
return nyash_rust::ASTNode::Program { statements: out_stmts, span };
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
eprintln!("[macro][test] existing main detected; skip harness (set --test-entry or NYASH_TEST_FORCE=1)");
}
return nyash_rust::ASTNode::Program {
statements: out_stmts,
span,
};
}
// Compose harness main now
let mut body2 = body;
@ -362,9 +759,19 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
if let Some(call) = orig_call_fn.take() {
if ret_policy.as_deref() == Some("original") {
// local __ny_orig_ret = __ny_orig_main(args)
body2.push(A::Local { variables: vec!["__ny_orig_ret".into()], initial_values: vec![Some(Box::new(call))], span: nyash_rust::ast::Span::unknown() });
body2.push(A::Local {
variables: vec!["__ny_orig_ret".into()],
initial_values: vec![Some(Box::new(call))],
span: nyash_rust::ast::Span::unknown(),
});
// return __ny_orig_ret
body2.push(A::Return { value: Some(Box::new(A::Variable { name: "__ny_orig_ret".into(), span: nyash_rust::ast::Span::unknown() })), span: nyash_rust::ast::Span::unknown() });
body2.push(A::Return {
value: Some(Box::new(A::Variable {
name: "__ny_orig_ret".into(),
span: nyash_rust::ast::Span::unknown(),
})),
span: nyash_rust::ast::Span::unknown(),
});
} else {
// default: tests policy; still call original but ignore result
body2.push(call);
@ -373,7 +780,10 @@ fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
}
let harness_fn = make_harness_main(body2);
out_stmts.push(harness_fn);
return nyash_rust::ASTNode::Program { statements: out_stmts, span };
return nyash_rust::ASTNode::Program {
statements: out_stmts,
span,
};
}
ast.clone()
}

View File

@ -1,5 +1,5 @@
use std::collections::HashMap;
use nyash_rust::ASTNode;
use std::collections::HashMap;
/// Minimal pattern trait — MVP
pub trait MacroPattern {
@ -10,35 +10,71 @@ pub trait MacroPattern {
pub struct AstBuilder;
impl AstBuilder {
pub fn new() -> Self { Self }
pub fn new() -> Self {
Self
}
pub fn quote(&self, code: &str) -> ASTNode {
// MVP: parse string into AST using existing parser
match nyash_rust::parser::NyashParser::parse_from_string(code) {
Ok(ast) => ast,
Err(_) => ASTNode::Program { statements: vec![], span: nyash_rust::ast::Span::unknown() },
Err(_) => ASTNode::Program {
statements: vec![],
span: nyash_rust::ast::Span::unknown(),
},
}
}
pub fn unquote(&self, template: &ASTNode, _bindings: &HashMap<String, ASTNode>) -> ASTNode {
// Replace Variables named like "$name" with corresponding bound AST
fn is_placeholder(name: &str) -> Option<&str> {
if name.starts_with('$') && name.len() > 1 { Some(&name[1..]) } else { None }
if name.starts_with('$') && name.len() > 1 {
Some(&name[1..])
} else {
None
}
}
fn is_variadic(name: &str) -> Option<&str> {
if name.starts_with("$...") && name.len() > 4 { Some(&name[4..]) } else { None }
if name.starts_with("$...") && name.len() > 4 {
Some(&name[4..])
} else {
None
}
}
fn subst(node: &ASTNode, binds: &HashMap<String, ASTNode>) -> ASTNode {
match node.clone() {
ASTNode::Variable { name, .. } => {
if let Some(k) = is_placeholder(&name) {
if let Some(v) = binds.get(k) { return v.clone(); }
if let Some(v) = binds.get(k) {
return v.clone();
}
}
node.clone()
}
ASTNode::BinaryOp { operator, left, right, span } => ASTNode::BinaryOp {
operator, left: Box::new(subst(&left, binds)), right: Box::new(subst(&right, binds)), span
ASTNode::BinaryOp {
operator,
left,
right,
span,
} => ASTNode::BinaryOp {
operator,
left: Box::new(subst(&left, binds)),
right: Box::new(subst(&right, binds)),
span,
},
ASTNode::UnaryOp { operator, operand, span } => ASTNode::UnaryOp { operator, operand: Box::new(subst(&operand, binds)), span },
ASTNode::MethodCall { object, method, arguments, span } => {
ASTNode::UnaryOp {
operator,
operand,
span,
} => ASTNode::UnaryOp {
operator,
operand: Box::new(subst(&operand, binds)),
span,
},
ASTNode::MethodCall {
object,
method,
arguments,
span,
} => {
let mut out_args: Vec<ASTNode> = Vec::new();
let mut i = 0usize;
while i < arguments.len() {
@ -54,9 +90,18 @@ impl AstBuilder {
out_args.push(subst(&arguments[i], binds));
i += 1;
}
ASTNode::MethodCall { object: Box::new(subst(&object, binds)), method, arguments: out_args, span }
ASTNode::MethodCall {
object: Box::new(subst(&object, binds)),
method,
arguments: out_args,
span,
}
}
ASTNode::FunctionCall { name, arguments, span } => {
ASTNode::FunctionCall {
name,
arguments,
span,
} => {
let mut out_args: Vec<ASTNode> = Vec::new();
let mut i = 0usize;
while i < arguments.len() {
@ -72,7 +117,11 @@ impl AstBuilder {
out_args.push(subst(&arguments[i], binds));
i += 1;
}
ASTNode::FunctionCall { name, arguments: out_args, span }
ASTNode::FunctionCall {
name,
arguments: out_args,
span,
}
}
ASTNode::ArrayLiteral { elements, span } => {
// Splice variadic placeholder inside arrays
@ -91,21 +140,55 @@ impl AstBuilder {
out_elems.push(subst(&elements[i], binds));
i += 1;
}
ASTNode::ArrayLiteral { elements: out_elems, span }
ASTNode::ArrayLiteral {
elements: out_elems,
span,
}
}
ASTNode::MapLiteral { entries, span } => {
ASTNode::MapLiteral { entries: entries.into_iter().map(|(k, v)| (k, subst(&v, binds))).collect(), span }
}
ASTNode::FieldAccess { object, field, span } => ASTNode::FieldAccess { object: Box::new(subst(&object, binds)), field, span },
ASTNode::Assignment { target, value, span } => ASTNode::Assignment { target: Box::new(subst(&target, binds)), value: Box::new(subst(&value, binds)), span },
ASTNode::Return { value, span } => ASTNode::Return { value: value.as_ref().map(|v| Box::new(subst(v, binds))), span },
ASTNode::If { condition, then_body, else_body, span } => ASTNode::If {
ASTNode::MapLiteral { entries, span } => ASTNode::MapLiteral {
entries: entries
.into_iter()
.map(|(k, v)| (k, subst(&v, binds)))
.collect(),
span,
},
ASTNode::FieldAccess {
object,
field,
span,
} => ASTNode::FieldAccess {
object: Box::new(subst(&object, binds)),
field,
span,
},
ASTNode::Assignment {
target,
value,
span,
} => ASTNode::Assignment {
target: Box::new(subst(&target, binds)),
value: Box::new(subst(&value, binds)),
span,
},
ASTNode::Return { value, span } => ASTNode::Return {
value: value.as_ref().map(|v| Box::new(subst(v, binds))),
span,
},
ASTNode::If {
condition,
then_body,
else_body,
span,
} => ASTNode::If {
condition: Box::new(subst(&condition, binds)),
then_body: then_body.into_iter().map(|n| subst(&n, binds)).collect(),
else_body: else_body.map(|v| v.into_iter().map(|n| subst(&n, binds)).collect()),
span,
},
ASTNode::Program { statements, span } => ASTNode::Program { statements: statements.into_iter().map(|n| subst(&n, binds)).collect(), span },
ASTNode::Program { statements, span } => ASTNode::Program {
statements: statements.into_iter().map(|n| subst(&n, binds)).collect(),
span,
},
other => other,
}
}
@ -114,115 +197,326 @@ impl AstBuilder {
}
/// Simple template-based pattern that uses Variables named "$name" as bind points.
pub struct TemplatePattern { pub template: ASTNode }
pub struct TemplatePattern {
pub template: ASTNode,
}
impl TemplatePattern { pub fn new(template: ASTNode) -> Self { Self { template } } }
impl TemplatePattern {
pub fn new(template: ASTNode) -> Self {
Self { template }
}
}
impl MacroPattern for TemplatePattern {
fn match_ast(&self, node: &ASTNode) -> Option<HashMap<String, ASTNode>> {
fn is_placeholder(name: &str) -> Option<&str> {
if name.starts_with('$') && name.len() > 1 { Some(&name[1..]) } else { None }
if name.starts_with('$') && name.len() > 1 {
Some(&name[1..])
} else {
None
}
}
fn is_variadic(name: &str) -> Option<&str> {
if name.starts_with("$...") && name.len() > 4 { Some(&name[4..]) } else { None }
if name.starts_with("$...") && name.len() > 4 {
Some(&name[4..])
} else {
None
}
}
fn go(tpl: &ASTNode, tgt: &ASTNode, out: &mut HashMap<String, ASTNode>) -> bool {
match (tpl, tgt) {
(ASTNode::Variable { name, .. }, v) => {
if let Some(k) = is_placeholder(name) { out.insert(k.to_string(), v.clone()); true } else { tpl == tgt }
if let Some(k) = is_placeholder(name) {
out.insert(k.to_string(), v.clone());
true
} else {
tpl == tgt
}
}
(ASTNode::Literal { .. }, _) | (ASTNode::Me { .. }, _) | (ASTNode::This { .. }, _) => tpl == tgt,
(ASTNode::BinaryOp { operator: op1, left: l1, right: r1, .. }, ASTNode::BinaryOp { operator: op2, left: l2, right: r2, .. }) => {
op1 == op2 && go(l1, l2, out) && go(r1, r2, out)
}
(ASTNode::UnaryOp { operator: o1, operand: a1, .. }, ASTNode::UnaryOp { operator: o2, operand: a2, .. }) => {
o1 == o2 && go(a1, a2, out)
}
(ASTNode::MethodCall { object: o1, method: m1, arguments: a1, .. }, ASTNode::MethodCall { object: o2, method: m2, arguments: a2, .. }) => {
if m1 != m2 { return false; }
if !go(o1, o2, out) { return false; }
(ASTNode::Literal { .. }, _)
| (ASTNode::Me { .. }, _)
| (ASTNode::This { .. }, _) => tpl == tgt,
(
ASTNode::BinaryOp {
operator: op1,
left: l1,
right: r1,
..
},
ASTNode::BinaryOp {
operator: op2,
left: l2,
right: r2,
..
},
) => op1 == op2 && go(l1, l2, out) && go(r1, r2, out),
(
ASTNode::UnaryOp {
operator: o1,
operand: a1,
..
},
ASTNode::UnaryOp {
operator: o2,
operand: a2,
..
},
) => o1 == o2 && go(a1, a2, out),
(
ASTNode::MethodCall {
object: o1,
method: m1,
arguments: a1,
..
},
ASTNode::MethodCall {
object: o2,
method: m2,
arguments: a2,
..
},
) => {
if m1 != m2 {
return false;
}
if !go(o1, o2, out) {
return false;
}
// Support variadic anywhere in a1
let mut varpos: Option<(usize, String)> = None;
for (i, arg) in a1.iter().enumerate() {
if let ASTNode::Variable { name, .. } = arg { if let Some(vn) = is_variadic(name) { varpos = Some((i, vn.to_string())); break; } }
if let ASTNode::Variable { name, .. } = arg {
if let Some(vn) = is_variadic(name) {
varpos = Some((i, vn.to_string()));
break;
}
}
}
if let Some((k, vn)) = varpos {
let suffix_len = a1.len() - k - 1;
if a2.len() < k + suffix_len { return false; }
for (x, y) in a1[..k].iter().zip(a2.iter()) { if !go(x, y, out) { return false; } }
for (i, x) in a1[a1.len()-suffix_len..].iter().enumerate() {
let y = &a2[a2.len()-suffix_len + i];
if !go(x, y, out) { return false; }
if a2.len() < k + suffix_len {
return false;
}
let tail: Vec<ASTNode> = a2[k..a2.len()-suffix_len].to_vec();
out.insert(vn, ASTNode::Program { statements: tail, span: nyash_rust::ast::Span::unknown() });
for (x, y) in a1[..k].iter().zip(a2.iter()) {
if !go(x, y, out) {
return false;
}
}
for (i, x) in a1[a1.len() - suffix_len..].iter().enumerate() {
let y = &a2[a2.len() - suffix_len + i];
if !go(x, y, out) {
return false;
}
}
let tail: Vec<ASTNode> = a2[k..a2.len() - suffix_len].to_vec();
out.insert(
vn,
ASTNode::Program {
statements: tail,
span: nyash_rust::ast::Span::unknown(),
},
);
return true;
}
if a1.len() != a2.len() { return false; }
for (x, y) in a1.iter().zip(a2.iter()) { if !go(x, y, out) { return false; } }
if a1.len() != a2.len() {
return false;
}
for (x, y) in a1.iter().zip(a2.iter()) {
if !go(x, y, out) {
return false;
}
}
true
}
(ASTNode::FunctionCall { name: n1, arguments: a1, .. }, ASTNode::FunctionCall { name: n2, arguments: a2, .. }) => {
if n1 != n2 { return false; }
(
ASTNode::FunctionCall {
name: n1,
arguments: a1,
..
},
ASTNode::FunctionCall {
name: n2,
arguments: a2,
..
},
) => {
if n1 != n2 {
return false;
}
let mut varpos: Option<(usize, String)> = None;
for (i, arg) in a1.iter().enumerate() {
if let ASTNode::Variable { name, .. } = arg { if let Some(vn) = is_variadic(name) { varpos = Some((i, vn.to_string())); break; } }
if let ASTNode::Variable { name, .. } = arg {
if let Some(vn) = is_variadic(name) {
varpos = Some((i, vn.to_string()));
break;
}
}
}
if let Some((k, vn)) = varpos {
let suffix_len = a1.len() - k - 1;
if a2.len() < k + suffix_len { return false; }
for (x, y) in a1[..k].iter().zip(a2.iter()) { if !go(x, y, out) { return false; } }
for (i, x) in a1[a1.len()-suffix_len..].iter().enumerate() {
let y = &a2[a2.len()-suffix_len + i];
if !go(x, y, out) { return false; }
if a2.len() < k + suffix_len {
return false;
}
let tail: Vec<ASTNode> = a2[k..a2.len()-suffix_len].to_vec();
out.insert(vn, ASTNode::Program { statements: tail, span: nyash_rust::ast::Span::unknown() });
for (x, y) in a1[..k].iter().zip(a2.iter()) {
if !go(x, y, out) {
return false;
}
}
for (i, x) in a1[a1.len() - suffix_len..].iter().enumerate() {
let y = &a2[a2.len() - suffix_len + i];
if !go(x, y, out) {
return false;
}
}
let tail: Vec<ASTNode> = a2[k..a2.len() - suffix_len].to_vec();
out.insert(
vn,
ASTNode::Program {
statements: tail,
span: nyash_rust::ast::Span::unknown(),
},
);
return true;
}
if a1.len() != a2.len() { return false; }
for (x, y) in a1.iter().zip(a2.iter()) { if !go(x, y, out) { return false; } }
if a1.len() != a2.len() {
return false;
}
for (x, y) in a1.iter().zip(a2.iter()) {
if !go(x, y, out) {
return false;
}
}
true
}
(ASTNode::ArrayLiteral { elements: e1, .. }, ASTNode::ArrayLiteral { elements: e2, .. }) => {
(
ASTNode::ArrayLiteral { elements: e1, .. },
ASTNode::ArrayLiteral { elements: e2, .. },
) => {
let mut varpos: Option<(usize, String)> = None;
for (i, el) in e1.iter().enumerate() {
if let ASTNode::Variable { name, .. } = el { if let Some(vn) = is_variadic(name) { varpos = Some((i, vn.to_string())); break; } }
if let ASTNode::Variable { name, .. } = el {
if let Some(vn) = is_variadic(name) {
varpos = Some((i, vn.to_string()));
break;
}
}
}
if let Some((k, vn)) = varpos {
let suffix_len = e1.len() - k - 1;
if e2.len() < k + suffix_len { return false; }
for (x, y) in e1[..k].iter().zip(e2.iter()) { if !go(x, y, out) { return false; } }
for (i, x) in e1[e1.len()-suffix_len..].iter().enumerate() {
let y = &e2[e2.len()-suffix_len + i];
if !go(x, y, out) { return false; }
if e2.len() < k + suffix_len {
return false;
}
let tail: Vec<ASTNode> = e2[k..e2.len()-suffix_len].to_vec();
out.insert(vn, ASTNode::Program { statements: tail, span: nyash_rust::ast::Span::unknown() });
for (x, y) in e1[..k].iter().zip(e2.iter()) {
if !go(x, y, out) {
return false;
}
}
for (i, x) in e1[e1.len() - suffix_len..].iter().enumerate() {
let y = &e2[e2.len() - suffix_len + i];
if !go(x, y, out) {
return false;
}
}
let tail: Vec<ASTNode> = e2[k..e2.len() - suffix_len].to_vec();
out.insert(
vn,
ASTNode::Program {
statements: tail,
span: nyash_rust::ast::Span::unknown(),
},
);
return true;
}
if e1.len() != e2.len() { return false; }
for (x, y) in e1.iter().zip(e2.iter()) { if !go(x, y, out) { return false; } }
true
}
(ASTNode::MapLiteral { entries: m1, .. }, ASTNode::MapLiteral { entries: m2, .. }) => {
if m1.len() != m2.len() { return false; }
for ((k1, v1), (k2, v2)) in m1.iter().zip(m2.iter()) {
if k1 != k2 { return false; }
if !go(v1, v2, out) { return false; }
if e1.len() != e2.len() {
return false;
}
for (x, y) in e1.iter().zip(e2.iter()) {
if !go(x, y, out) {
return false;
}
}
true
}
(ASTNode::FieldAccess { object: o1, field: f1, .. }, ASTNode::FieldAccess { object: o2, field: f2, .. }) => f1 == f2 && go(o1, o2, out),
(ASTNode::Assignment { target: t1, value: v1, .. }, ASTNode::Assignment { target: t2, value: v2, .. }) => go(t1, t2, out) && go(v1, v2, out),
(ASTNode::Return { value: v1, .. }, ASTNode::Return { value: v2, .. }) => match (v1, v2) { (Some(a), Some(b)) => go(a, b, out), (None, None) => true, _ => false },
(ASTNode::If { condition: c1, then_body: t1, else_body: e1, .. }, ASTNode::If { condition: c2, then_body: t2, else_body: e2, .. }) => {
if !go(c1, c2, out) || t1.len() != t2.len() { return false; }
for (x, y) in t1.iter().zip(t2.iter()) { if !go(x, y, out) { return false; } }
(
ASTNode::MapLiteral { entries: m1, .. },
ASTNode::MapLiteral { entries: m2, .. },
) => {
if m1.len() != m2.len() {
return false;
}
for ((k1, v1), (k2, v2)) in m1.iter().zip(m2.iter()) {
if k1 != k2 {
return false;
}
if !go(v1, v2, out) {
return false;
}
}
true
}
(
ASTNode::FieldAccess {
object: o1,
field: f1,
..
},
ASTNode::FieldAccess {
object: o2,
field: f2,
..
},
) => f1 == f2 && go(o1, o2, out),
(
ASTNode::Assignment {
target: t1,
value: v1,
..
},
ASTNode::Assignment {
target: t2,
value: v2,
..
},
) => go(t1, t2, out) && go(v1, v2, out),
(ASTNode::Return { value: v1, .. }, ASTNode::Return { value: v2, .. }) => {
match (v1, v2) {
(Some(a), Some(b)) => go(a, b, out),
(None, None) => true,
_ => false,
}
}
(
ASTNode::If {
condition: c1,
then_body: t1,
else_body: e1,
..
},
ASTNode::If {
condition: c2,
then_body: t2,
else_body: e2,
..
},
) => {
if !go(c1, c2, out) || t1.len() != t2.len() {
return false;
}
for (x, y) in t1.iter().zip(t2.iter()) {
if !go(x, y, out) {
return false;
}
}
match (e1, e2) {
(Some(a), Some(b)) => {
if a.len() != b.len() { return false; }
for (x, y) in a.iter().zip(b.iter()) { if !go(x, y, out) { return false; } }
if a.len() != b.len() {
return false;
}
for (x, y) in a.iter().zip(b.iter()) {
if !go(x, y, out) {
return false;
}
}
true
}
(None, None) => true,
@ -233,19 +527,31 @@ impl MacroPattern for TemplatePattern {
}
}
let mut out = HashMap::new();
if go(&self.template, node, &mut out) { Some(out) } else { None }
if go(&self.template, node, &mut out) {
Some(out)
} else {
None
}
}
}
/// ORパターン: いずれかのテンプレートにマッチすれば成功
pub struct OrPattern { pub alts: Vec<TemplatePattern> }
pub struct OrPattern {
pub alts: Vec<TemplatePattern>,
}
impl OrPattern { pub fn new(alts: Vec<TemplatePattern>) -> Self { Self { alts } } }
impl OrPattern {
pub fn new(alts: Vec<TemplatePattern>) -> Self {
Self { alts }
}
}
impl MacroPattern for OrPattern {
fn match_ast(&self, node: &ASTNode) -> Option<HashMap<String, ASTNode>> {
for tp in &self.alts {
if let Some(b) = tp.match_ast(node) { return Some(b); }
if let Some(b) = tp.match_ast(node) {
return Some(b);
}
}
None
}