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:
@ -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,
|
||||
})
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
618
src/macro/mod.rs
618
src/macro/mod.rs
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user