freeze: macro platform complete; default ON with profiles; env consolidation; docs + smokes\n\n- Profiles: --profile {lite|dev|ci|strict} (dev-like default for macros)\n- Macro paths: prefer NYASH_MACRO_PATHS (legacy envs deprecated with warnings)\n- Selfhost pre-expand: auto mode, PyVM-only, add smokes (array/map)\n- Docs: user-macros updated; new macro-profiles guide; AGENTS freeze note; CURRENT_TASK freeze\n- Compat: non-breaking; legacy envs print deprecation notices\n
This commit is contained in:
151
src/cli.rs
151
src/cli.rs
@ -75,6 +75,10 @@ pub struct CliConfig {
|
||||
pub emit_exe: Option<String>,
|
||||
pub emit_exe_nyrt: Option<String>,
|
||||
pub emit_exe_libs: Option<String>,
|
||||
// Macro child (sandbox) mode
|
||||
pub macro_expand_child: Option<String>,
|
||||
// Dump expanded AST as JSON and exit
|
||||
pub dump_expanded_ast_json: bool,
|
||||
}
|
||||
|
||||
/// Grouped views (Phase 1: non-breaking). These structs provide a categorized
|
||||
@ -273,6 +277,18 @@ impl CliConfig {
|
||||
.value_name("FILE")
|
||||
.index(1)
|
||||
)
|
||||
.arg(
|
||||
Arg::new("macro-expand-child")
|
||||
.long("macro-expand-child")
|
||||
.value_name("FILE")
|
||||
.help("Macro sandbox child: read AST JSON v0 from stdin, expand using Nyash macro file, write AST JSON v0 to stdout (PoC)")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("dump-expanded-ast-json")
|
||||
.long("dump-expanded-ast-json")
|
||||
.help("Dump AST after macro expansion as JSON v0 and exit")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
)
|
||||
.arg(
|
||||
Arg::new("gc")
|
||||
.long("gc")
|
||||
@ -354,6 +370,66 @@ impl CliConfig {
|
||||
.help("Dump parsed AST and exit")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
)
|
||||
.arg(
|
||||
Arg::new("profile")
|
||||
.long("profile")
|
||||
.value_name("{lite|dev|ci|strict}")
|
||||
.help("Set execution profile: lite (macros OFF), dev (macros ON), ci (macros ON+strict), strict (macros ON+strict). Default run behaves like dev for macros.")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("expand")
|
||||
.long("expand")
|
||||
.help("Macro: enable macro engine and dump expansion traces (sets NYASH_MACRO_ENABLE=1, NYASH_MACRO_TRACE=1)")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
)
|
||||
.arg(
|
||||
Arg::new("macro-preexpand")
|
||||
.long("macro-preexpand")
|
||||
.help("Self-host: pre-expand macros before MIR compile (sets NYASH_MACRO_SELFHOST_PRE_EXPAND=1). Requires NYASH_USE_NY_COMPILER=1 and NYASH_VM_USE_PY=1.")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
)
|
||||
.arg(
|
||||
Arg::new("macro-preexpand-auto")
|
||||
.long("macro-preexpand-auto")
|
||||
.help("Self-host: pre-expand macros in auto mode (sets NYASH_MACRO_SELFHOST_PRE_EXPAND=auto). Requires NYASH_USE_NY_COMPILER=1 and NYASH_VM_USE_PY=1.")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
)
|
||||
.arg(
|
||||
Arg::new("macro-top-level-allow")
|
||||
.long("macro-top-level-allow")
|
||||
.help("Allow top-level static MacroBoxSpec.expand(json[,ctx]) without BoxDeclaration (sets NYASH_MACRO_TOPLEVEL_ALLOW=1)")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
)
|
||||
.arg(
|
||||
Arg::new("macro-profile")
|
||||
.long("macro-profile")
|
||||
.value_name("{dev|ci-fast|strict}")
|
||||
.help("Convenience: configure macro envs for dev/ci-fast/strict. Non-breaking; can be overridden by explicit envs/flags.")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("run-tests")
|
||||
.long("run-tests")
|
||||
.help("Run tests: enable macro engine and inject test harness (functions starting with 'test_')")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
)
|
||||
.arg(
|
||||
Arg::new("test-filter")
|
||||
.long("test-filter")
|
||||
.value_name("SUBSTR")
|
||||
.help("Only run tests whose name contains SUBSTR (with --run-tests)")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("test-entry")
|
||||
.long("test-entry")
|
||||
.value_name("{wrap|override}")
|
||||
.help("When --run-tests and a main exists: wrap (run tests then call original) or override (replace main with test harness). Default: keep original (no harness). Use with --run-tests.")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("test-return")
|
||||
.long("test-return")
|
||||
.value_name("{tests|original}")
|
||||
.help("When --run-tests with --test-entry wrap: choose harness return policy (tests: return failures count; original: return original main()'s result)")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("dump-mir")
|
||||
.long("dump-mir")
|
||||
@ -611,7 +687,7 @@ impl CliConfig {
|
||||
if let Some(a) = matches.get_one::<String>("ny-compiler-args") {
|
||||
std::env::set_var("NYASH_NY_COMPILER_CHILD_ARGS", a);
|
||||
}
|
||||
Self {
|
||||
let cfg = Self {
|
||||
file: matches.get_one::<String>("file").cloned(),
|
||||
debug_fuel: parse_debug_fuel(matches.get_one::<String>("debug-fuel").unwrap()),
|
||||
dump_ast: matches.get_flag("dump-ast"),
|
||||
@ -676,7 +752,78 @@ impl CliConfig {
|
||||
emit_exe: matches.get_one::<String>("emit-exe").cloned(),
|
||||
emit_exe_nyrt: matches.get_one::<String>("emit-exe-nyrt").cloned(),
|
||||
emit_exe_libs: matches.get_one::<String>("emit-exe-libs").cloned(),
|
||||
macro_expand_child: matches.get_one::<String>("macro-expand-child").cloned(),
|
||||
dump_expanded_ast_json: matches.get_flag("dump-expanded-ast-json"),
|
||||
};
|
||||
// Macro debug gate
|
||||
if matches.get_flag("expand") {
|
||||
std::env::set_var("NYASH_MACRO_ENABLE", "1");
|
||||
std::env::set_var("NYASH_MACRO_TRACE", "1");
|
||||
}
|
||||
// Profile mapping (non-breaking; users can override afterwards)
|
||||
if let Some(p) = matches.get_one::<String>("profile") {
|
||||
match p.as_str() {
|
||||
"lite" => {
|
||||
std::env::set_var("NYASH_MACRO_ENABLE", "0");
|
||||
std::env::set_var("NYASH_MACRO_STRICT", "0");
|
||||
std::env::set_var("NYASH_MACRO_TRACE", "0");
|
||||
}
|
||||
"dev" => {
|
||||
std::env::set_var("NYASH_MACRO_ENABLE", "1");
|
||||
std::env::set_var("NYASH_MACRO_STRICT", "1");
|
||||
std::env::set_var("NYASH_MACRO_TRACE", "0");
|
||||
}
|
||||
"ci" | "strict" => {
|
||||
std::env::set_var("NYASH_MACRO_ENABLE", "1");
|
||||
std::env::set_var("NYASH_MACRO_STRICT", "1");
|
||||
std::env::set_var("NYASH_MACRO_TRACE", "0");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if matches.get_flag("run-tests") {
|
||||
std::env::set_var("NYASH_MACRO_ENABLE", "1");
|
||||
std::env::set_var("NYASH_TEST_RUN", "1");
|
||||
if let Some(f) = matches.get_one::<String>("test-filter") {
|
||||
std::env::set_var("NYASH_TEST_FILTER", f);
|
||||
}
|
||||
if let Some(entry) = matches.get_one::<String>("test-entry") {
|
||||
let v = entry.as_str();
|
||||
if v == "wrap" || v == "override" {
|
||||
std::env::set_var("NYASH_TEST_ENTRY", v);
|
||||
}
|
||||
}
|
||||
if let Some(ret) = matches.get_one::<String>("test-return") {
|
||||
let v = ret.as_str();
|
||||
if v == "tests" || v == "original" {
|
||||
std::env::set_var("NYASH_TEST_RETURN", v);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Self-host macro pre-expand gate (CLI convenience)
|
||||
if matches.get_flag("macro-preexpand") {
|
||||
std::env::set_var("NYASH_MACRO_SELFHOST_PRE_EXPAND", "1");
|
||||
}
|
||||
if matches.get_flag("macro-preexpand-auto") {
|
||||
std::env::set_var("NYASH_MACRO_SELFHOST_PRE_EXPAND", "auto");
|
||||
}
|
||||
if matches.get_flag("macro-top-level-allow") {
|
||||
std::env::set_var("NYASH_MACRO_TOPLEVEL_ALLOW", "1");
|
||||
}
|
||||
if let Some(p) = matches.get_one::<String>("macro-profile") {
|
||||
let p = p.as_str();
|
||||
match p {
|
||||
"dev" | "ci-fast" | "strict" => {
|
||||
// Minimal, non-invasive mapping; users can still override.
|
||||
std::env::set_var("NYASH_MACRO_ENABLE", "1");
|
||||
std::env::set_var("NYASH_MACRO_STRICT", "1");
|
||||
std::env::set_var("NYASH_MACRO_TOPLEVEL_ALLOW", "0");
|
||||
std::env::set_var("NYASH_MACRO_SELFHOST_PRE_EXPAND", "auto");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
cfg
|
||||
}
|
||||
}
|
||||
|
||||
@ -734,6 +881,8 @@ impl Default for CliConfig {
|
||||
emit_exe: None,
|
||||
emit_exe_nyrt: None,
|
||||
emit_exe_libs: None,
|
||||
macro_expand_child: None,
|
||||
dump_expanded_ast_json: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from typing import Dict, Any, List
|
||||
from llvmlite import ir
|
||||
from trace import debug as trace_debug
|
||||
from trace import phi_json as trace_phi_json
|
||||
|
||||
|
||||
def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, Any]], order: List[int], loop_plan: Dict[str, Any] | None):
|
||||
@ -225,13 +226,9 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
|
||||
snap = dict(vmap_cur)
|
||||
try:
|
||||
keys = sorted(list(snap.keys()))
|
||||
from phi_wiring.common import trace as trace_phi_json
|
||||
try:
|
||||
trace_phi_json({"phi": "snapshot", "block": int(bid), "keys": [int(k) for k in keys[:20]]})
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
keys = list(snap.keys())
|
||||
trace_phi_json({"phi": "snapshot", "block": int(bid), "keys": [int(k) for k in keys[:20]]})
|
||||
for vid in created_ids:
|
||||
if vid in vmap_cur:
|
||||
builder.def_blocks.setdefault(vid, set()).add(block_data.get("id", 0))
|
||||
@ -240,4 +237,3 @@ def lower_blocks(builder, func: ir.Function, block_by_id: Dict[int, Dict[str, An
|
||||
delattr(builder, '_current_vmap')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@ -33,12 +33,7 @@ from phi_wiring import setup_phi_placeholders as _setup_phi_placeholders, finali
|
||||
from phi_wiring import ensure_phi as _ensure_phi
|
||||
from trace import debug as trace_debug
|
||||
from trace import phi as trace_phi
|
||||
try:
|
||||
# Structured JSON trace for PHI wiring (shared with phi_wiring)
|
||||
from phi_wiring.common import trace as trace_phi_json
|
||||
except Exception:
|
||||
def trace_phi_json(_msg):
|
||||
pass
|
||||
from trace import phi_json as trace_phi_json
|
||||
from prepass.loops import detect_simple_while
|
||||
from prepass.if_merge import plan_ret_phi_predeclare
|
||||
from build_ctx import BuildCtx
|
||||
|
||||
@ -51,3 +51,27 @@ def phi(msg) -> None:
|
||||
def values(msg: str) -> None:
|
||||
if _enabled('NYASH_LLVM_TRACE_VALUES'):
|
||||
_write(msg)
|
||||
|
||||
def phi_json(msg):
|
||||
"""Safe JSON-style PHI trace delegator.
|
||||
|
||||
- Gated by NYASH_LLVM_TRACE_PHI=1 (same gate as phi())
|
||||
- Delegates to phi_wiring.common.trace if available; otherwise no-op
|
||||
- Accepts arbitrary Python objects and forwards as-is
|
||||
"""
|
||||
if not _enabled('NYASH_LLVM_TRACE_PHI'):
|
||||
return
|
||||
try:
|
||||
from phi_wiring.common import trace as _trace_phi_json # type: ignore
|
||||
_trace_phi_json(msg)
|
||||
except Exception:
|
||||
# Fallback: stringify and route via plain phi
|
||||
try:
|
||||
if not isinstance(msg, (str, bytes)):
|
||||
try:
|
||||
msg = json.dumps(msg, ensure_ascii=False, separators=(",", ":"))
|
||||
except Exception:
|
||||
msg = str(msg)
|
||||
phi(msg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
190
src/macro/ast_json.rs
Normal file
190
src/macro/ast_json.rs
Normal file
@ -0,0 +1,190 @@
|
||||
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() {
|
||||
ASTNode::Program { statements, .. } => json!({
|
||||
"kind": "Program",
|
||||
"statements": statements.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>()
|
||||
}),
|
||||
ASTNode::Print { expression, .. } => json!({
|
||||
"kind": "Print",
|
||||
"expression": ast_to_json(&expression),
|
||||
}),
|
||||
ASTNode::Return { value, .. } => json!({
|
||||
"kind": "Return",
|
||||
"value": value.as_ref().map(|v| ast_to_json(v)),
|
||||
}),
|
||||
ASTNode::Assignment { target, value, .. } => json!({
|
||||
"kind": "Assignment",
|
||||
"target": ast_to_json(&target),
|
||||
"value": ast_to_json(&value),
|
||||
}),
|
||||
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::FunctionDeclaration { name, params, body, is_static, is_override, .. } => json!({
|
||||
"kind": "FunctionDeclaration",
|
||||
"name": name,
|
||||
"params": params,
|
||||
"body": body.into_iter().map(|s| ast_to_json(&s)).collect::<Vec<_>>(),
|
||||
"static": is_static,
|
||||
"override": is_override,
|
||||
}),
|
||||
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!({
|
||||
"kind":"BinaryOp",
|
||||
"op": bin_to_str(&operator),
|
||||
"left": ast_to_json(&left),
|
||||
"right": ast_to_json(&right),
|
||||
}),
|
||||
ASTNode::UnaryOp { operator, operand, .. } => json!({
|
||||
"kind":"UnaryOp",
|
||||
"op": un_to_str(&operator),
|
||||
"operand": ast_to_json(&operand),
|
||||
}),
|
||||
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!({
|
||||
"kind":"FunctionCall",
|
||||
"name": name,
|
||||
"arguments": arguments.into_iter().map(|a| ast_to_json(&a)).collect::<Vec<_>>()
|
||||
}),
|
||||
ASTNode::ArrayLiteral { elements, .. } => json!({
|
||||
"kind":"Array",
|
||||
"elements": elements.into_iter().map(|e| ast_to_json(&e)).collect::<Vec<_>>()
|
||||
}),
|
||||
ASTNode::MapLiteral { entries, .. } => json!({
|
||||
"kind":"Map",
|
||||
"entries": entries.into_iter().map(|(k,v)| json!({"k":k,"v":ast_to_json(&v)})).collect::<Vec<_>>()
|
||||
}),
|
||||
other => json!({"kind":"Unsupported","debug": format!("{:?}", other)}),
|
||||
}
|
||||
}
|
||||
|
||||
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() }
|
||||
}
|
||||
"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() },
|
||||
"Assignment" => ASTNode::Assignment { target: Box::new(json_to_ast(v.get("target")?)?), value: Box::new(json_to_ast(v.get("value")?)?), 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(),
|
||||
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() },
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn lit_to_json(v: &LiteralValue) -> Value {
|
||||
match v {
|
||||
LiteralValue::String(s) => json!({"type":"string","value":s}),
|
||||
LiteralValue::Integer(i) => json!({"type":"int","value":i}),
|
||||
LiteralValue::Float(f) => json!({"type":"float","value":f}),
|
||||
LiteralValue::Bool(b) => json!({"type":"bool","value":b}),
|
||||
LiteralValue::Null => json!({"type":"null"}),
|
||||
LiteralValue::Void => json!({"type":"void"}),
|
||||
}
|
||||
}
|
||||
|
||||
fn json_to_lit(v: &Value) -> Option<LiteralValue> {
|
||||
let t = v.get("type")?.as_str()?;
|
||||
Some(match t {
|
||||
"string" => LiteralValue::String(v.get("value")?.as_str()?.to_string()),
|
||||
"int" => LiteralValue::Integer(v.get("value")?.as_i64()?),
|
||||
"float" => LiteralValue::Float(v.get("value")?.as_f64()?),
|
||||
"bool" => LiteralValue::Bool(v.get("value")?.as_bool()?),
|
||||
"null" => LiteralValue::Null,
|
||||
"void" => LiteralValue::Void,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn bin_to_str(op: &BinaryOperator) -> &'static str {
|
||||
match op {
|
||||
BinaryOperator::Add => "+",
|
||||
BinaryOperator::Subtract => "-",
|
||||
BinaryOperator::Multiply => "*",
|
||||
BinaryOperator::Divide => "/",
|
||||
BinaryOperator::Modulo => "%",
|
||||
BinaryOperator::BitAnd => "&",
|
||||
BinaryOperator::BitOr => "|",
|
||||
BinaryOperator::BitXor => "^",
|
||||
BinaryOperator::Shl => "<<",
|
||||
BinaryOperator::Shr => ">>",
|
||||
BinaryOperator::Equal => "==",
|
||||
BinaryOperator::NotEqual => "!=",
|
||||
BinaryOperator::Less => "<",
|
||||
BinaryOperator::Greater => ">",
|
||||
BinaryOperator::LessEqual => "<=",
|
||||
BinaryOperator::GreaterEqual => ">=",
|
||||
BinaryOperator::And => "&&",
|
||||
BinaryOperator::Or => "||",
|
||||
}
|
||||
}
|
||||
|
||||
fn str_to_bin(s: &str) -> Option<BinaryOperator> {
|
||||
Some(match s {
|
||||
"+" => BinaryOperator::Add,
|
||||
"-" => BinaryOperator::Subtract,
|
||||
"*" => BinaryOperator::Multiply,
|
||||
"/" => BinaryOperator::Divide,
|
||||
"%" => BinaryOperator::Modulo,
|
||||
"&" => BinaryOperator::BitAnd,
|
||||
"|" => BinaryOperator::BitOr,
|
||||
"^" => BinaryOperator::BitXor,
|
||||
"<<" => BinaryOperator::Shl,
|
||||
">>" => BinaryOperator::Shr,
|
||||
"==" => BinaryOperator::Equal,
|
||||
"!=" => BinaryOperator::NotEqual,
|
||||
"<" => BinaryOperator::Less,
|
||||
">" => BinaryOperator::Greater,
|
||||
"<=" => BinaryOperator::LessEqual,
|
||||
">=" => BinaryOperator::GreaterEqual,
|
||||
"&&" => BinaryOperator::And,
|
||||
"||" => BinaryOperator::Or,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn un_to_str(op: &UnaryOperator) -> &'static str {
|
||||
match op {
|
||||
UnaryOperator::Minus => "-",
|
||||
UnaryOperator::Not => "not",
|
||||
}
|
||||
}
|
||||
|
||||
fn str_to_un(s: &str) -> Option<UnaryOperator> {
|
||||
Some(match s {
|
||||
"-" => UnaryOperator::Minus,
|
||||
"not" => UnaryOperator::Not,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
52
src/macro/ctx.rs
Normal file
52
src/macro/ctx.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MacroCaps {
|
||||
pub io: bool,
|
||||
pub net: bool,
|
||||
pub env: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MacroCtx {
|
||||
pub caps: MacroCaps,
|
||||
}
|
||||
|
||||
impl MacroCtx {
|
||||
pub fn from_env() -> Self {
|
||||
fn on(name: &str) -> bool {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.map(|v| {
|
||||
let v = v.to_ascii_lowercase();
|
||||
v == "1" || v == "true" || v == "on"
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
MacroCtx {
|
||||
caps: MacroCaps {
|
||||
io: on("NYASH_MACRO_CAP_IO"),
|
||||
net: on("NYASH_MACRO_CAP_NET"),
|
||||
env: on("NYASH_MACRO_CAP_ENV"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
std::env::var(key).ok()
|
||||
}
|
||||
}
|
||||
|
||||
static GENSYM_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
pub fn gensym(prefix: &str) -> String {
|
||||
let n = GENSYM_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
format!("{}_{}", prefix, n)
|
||||
}
|
||||
194
src/macro/engine.rs
Normal file
194
src/macro/engine.rs
Normal file
@ -0,0 +1,194 @@
|
||||
use nyash_rust::ast::Span;
|
||||
use nyash_rust::{ASTNode, ast::LiteralValue, ast::BinaryOperator};
|
||||
use std::time::Instant;
|
||||
|
||||
/// HIR Patch description (MVP placeholder)
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct HirPatch {
|
||||
// In MVP, we keep it opaque; later will host add/replace nodes
|
||||
}
|
||||
|
||||
pub struct MacroEngine {
|
||||
max_passes: usize,
|
||||
cycle_window: usize,
|
||||
trace: bool,
|
||||
}
|
||||
|
||||
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 trace = std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1");
|
||||
Self { max_passes, cycle_window, trace }
|
||||
}
|
||||
|
||||
/// Expand all macros with depth/cycle guards and return patched AST.
|
||||
pub fn expand(&mut self, ast: &ASTNode) -> (ASTNode, Vec<HirPatch>) {
|
||||
let patches = Vec::new();
|
||||
let mut cur = ast.clone();
|
||||
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 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 dt = t0.elapsed();
|
||||
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); }
|
||||
// cycle detection in small window
|
||||
if history.iter().any(|h| *h == next) {
|
||||
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(); }
|
||||
cur = next;
|
||||
}
|
||||
eprintln!("[macro][engine] max passes ({}) exceeded — stopping expansion", self.max_passes);
|
||||
(cur, patches)
|
||||
}
|
||||
|
||||
fn expand_node(&mut self, node: &ASTNode) -> ASTNode {
|
||||
match node.clone() {
|
||||
ASTNode::Program { statements, span } => {
|
||||
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 }
|
||||
}
|
||||
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());
|
||||
}
|
||||
// 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());
|
||||
if std::env::var("NYASH_MACRO_TRACE").ok().as_deref() == Some("1") {
|
||||
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");
|
||||
// Philosophy-2: respect box independence — operate on public interface only
|
||||
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());
|
||||
}
|
||||
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());
|
||||
}
|
||||
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 }
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
let rec = serde_json::json!({
|
||||
"event": "macro_pass",
|
||||
"pass": pass,
|
||||
"changed": changed,
|
||||
"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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn me_field(name: &str) -> ASTNode {
|
||||
ASTNode::FieldAccess {
|
||||
object: Box::new(ASTNode::Me { span: Span::unknown() }),
|
||||
field: name.to_string(),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn var_field(var: &str, field: &str) -> ASTNode {
|
||||
ASTNode::FieldAccess {
|
||||
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() }
|
||||
}
|
||||
|
||||
fn bin_and(lhs: ASTNode, rhs: ASTNode) -> ASTNode {
|
||||
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() }
|
||||
}
|
||||
|
||||
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() }
|
||||
} else {
|
||||
let mut it = fields.iter();
|
||||
let first = it.next().unwrap();
|
||||
let mut expr = bin_eq(me_field(first), var_field("__ny_other", first));
|
||||
for f in it {
|
||||
expr = bin_and(expr, bin_eq(me_field(f), var_field("__ny_other", f)));
|
||||
}
|
||||
expr
|
||||
};
|
||||
// Hygiene: use gensym-like param to avoid collisions
|
||||
let param_name = "__ny_other".to_string();
|
||||
ASTNode::FunctionDeclaration {
|
||||
name: "equals".to_string(),
|
||||
params: vec![param_name.clone()],
|
||||
body: vec![ASTNode::Return { value: Some(Box::new(cond)), span: Span::unknown() }],
|
||||
is_static: false,
|
||||
is_override: false,
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tostring_method(box_name: &str, fields: &Vec<String>) -> ASTNode {
|
||||
// toString() { return "Name(" + me.f1 + "," + me.f2 + ")" }
|
||||
let mut expr = lit_str(&format!("{}(", box_name));
|
||||
let mut first = true;
|
||||
for f in fields {
|
||||
if !first { expr = bin_add(expr, lit_str(",")); }
|
||||
first = false;
|
||||
expr = bin_add(expr, me_field(f));
|
||||
}
|
||||
expr = bin_add(expr, lit_str(")"));
|
||||
ASTNode::FunctionDeclaration {
|
||||
name: "toString".to_string(),
|
||||
params: vec![],
|
||||
body: vec![ASTNode::Return { value: Some(Box::new(expr)), span: Span::unknown() }],
|
||||
is_static: false,
|
||||
is_override: false,
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
111
src/macro/macro_box.rs
Normal file
111
src/macro/macro_box.rs
Normal file
@ -0,0 +1,111 @@
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use nyash_rust::ASTNode;
|
||||
|
||||
/// MacroBox API — user-extensible macro expansion units (experimental)
|
||||
///
|
||||
/// Philosophy:
|
||||
/// - Deterministic, side-effect free, no IO. Pure AST -> AST transforms.
|
||||
/// - Prefer operating on public interfaces (Box public fields/methods) and avoid
|
||||
/// coupling to private internals.
|
||||
pub trait MacroBox: Send + Sync {
|
||||
fn name(&self) -> &'static str;
|
||||
fn expand(&self, ast: &ASTNode) -> ASTNode;
|
||||
}
|
||||
|
||||
static REG: OnceLock<Mutex<Vec<&'static dyn MacroBox>>> = OnceLock::new();
|
||||
|
||||
fn registry() -> &'static Mutex<Vec<&'static dyn MacroBox>> {
|
||||
REG.get_or_init(|| Mutex::new(Vec::new()))
|
||||
}
|
||||
|
||||
/// Register a MacroBox. Intended to be called at startup/init paths.
|
||||
pub fn register(m: &'static dyn MacroBox) {
|
||||
let reg = registry();
|
||||
let mut guard = reg.lock().expect("macro registry poisoned");
|
||||
// avoid duplicates
|
||||
if !guard.iter().any(|e| e.name() == m.name()) {
|
||||
guard.push(m);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gate for MacroBox execution (default OFF).
|
||||
pub fn enabled() -> bool {
|
||||
std::env::var("NYASH_MACRO_BOX").ok().as_deref() == Some("1")
|
||||
}
|
||||
|
||||
/// Expand AST by applying all registered MacroBoxes in order once.
|
||||
pub fn expand_all_once(ast: &ASTNode) -> ASTNode {
|
||||
if !enabled() { return ast.clone(); }
|
||||
let reg = registry();
|
||||
let guard = reg.lock().expect("macro registry poisoned");
|
||||
let mut cur = ast.clone();
|
||||
for m in guard.iter() {
|
||||
let out = m.expand(&cur);
|
||||
cur = out;
|
||||
}
|
||||
cur
|
||||
}
|
||||
|
||||
// ---- Built-in example (optional) ----
|
||||
|
||||
pub struct UppercasePrintMacro;
|
||||
|
||||
impl MacroBox for 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::Print { expression, span } => {
|
||||
match &*expression {
|
||||
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 } }
|
||||
}
|
||||
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 {
|
||||
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 },
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
go(ast)
|
||||
}
|
||||
}
|
||||
|
||||
static INIT_FLAG: OnceLock<()> = OnceLock::new();
|
||||
|
||||
/// Initialize built-in demo MacroBoxes when enabled by env flags.
|
||||
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); }
|
||||
// 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); }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
407
src/macro/macro_box_ny.rs
Normal file
407
src/macro/macro_box_ny.rs
Normal file
@ -0,0 +1,407 @@
|
||||
use nyash_rust::ASTNode;
|
||||
|
||||
/// Load MacroBoxes written in Nyash.
|
||||
/// Preferred env: NYASH_MACRO_PATHS=comma,separated,paths
|
||||
/// Backward compat: NYASH_MACRO_BOX_NY=1 + NYASH_MACRO_BOX_NY_PATHS
|
||||
pub fn init_from_env() {
|
||||
// Preferred: NYASH_MACRO_PATHS
|
||||
let paths = match std::env::var("NYASH_MACRO_PATHS") {
|
||||
Ok(s) if !s.trim().is_empty() => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
.or_else(|| {
|
||||
// Back-compat: NYASH_MACRO_BOX_NY / NYASH_MACRO_BOX_NY_PATHS
|
||||
if std::env::var("NYASH_MACRO_BOX_NY").ok().as_deref() == Some("1") {
|
||||
if let Ok(s) = std::env::var("NYASH_MACRO_BOX_NY_PATHS") {
|
||||
if !s.trim().is_empty() {
|
||||
eprintln!("[macro][compat] NYASH_MACRO_BOX_NY*_ vars are deprecated; use NYASH_MACRO_PATHS");
|
||||
return Some(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
// Soft deprecations for legacy envs
|
||||
if std::env::var("NYASH_MACRO_TOPLEVEL_ALLOW").ok().is_some() {
|
||||
eprintln!("[macro][compat] NYASH_MACRO_TOPLEVEL_ALLOW is deprecated; default is OFF. Prefer CLI --macro-top-level-allow if needed");
|
||||
}
|
||||
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; };
|
||||
for p in paths.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
|
||||
if let Err(e) = try_load_one(p) {
|
||||
eprintln!("[macro][box_ny] failed to load '{}': {}", p, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_load_one(path: &str) -> Result<(), String> {
|
||||
let src = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
|
||||
let ast = nyash_rust::parser::NyashParser::parse_from_string(&src)
|
||||
.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() }) {
|
||||
eprintln!("[macro][box_ny][caps] {} (in '{}')", msg, path);
|
||||
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 mname == "expand" {
|
||||
let reg_name = derive_box_name(&box_name, methods.get("name"));
|
||||
// Prefer child-proxy registration when enabled (default ON)
|
||||
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);
|
||||
} 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);
|
||||
mapped = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
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 })));
|
||||
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);
|
||||
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 })));
|
||||
eprintln!("[macro][box_ny] registered Ny MacroBox '{}' (identity: unknown body) from {}", nm, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
for st in &statements {
|
||||
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);
|
||||
if use_child && allow_top {
|
||||
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 })));
|
||||
eprintln!("[macro][box_ny] registered identity MacroBox '{}' (top-level static) for {}", nm, path);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err("no Box with static expand(ast) found".into())
|
||||
}
|
||||
|
||||
fn derive_box_name(default: &str, name_fn: Option<&ASTNode>) -> &'static str {
|
||||
// If name() { return "X" } pattern is detected, use it; else box name
|
||||
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 {
|
||||
let owned = s.clone();
|
||||
return Box::leak(owned.into_boxed_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box::leak(default.to_string().into_boxed_str())
|
||||
}
|
||||
|
||||
pub(crate) struct NyIdentityMacroBox { nm: &'static str }
|
||||
|
||||
impl super::macro_box::MacroBox for NyIdentityMacroBox {
|
||||
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") {
|
||||
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; }
|
||||
}
|
||||
ast.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_is_identity(body: &Vec<ASTNode>, params: &Vec<String>) -> bool {
|
||||
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);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn expand_indicates_uppercase(body: &Vec<ASTNode>, params: &Vec<String>) -> bool {
|
||||
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, .. } => {
|
||||
if (name == "uppercase_print" || name == "upper_print") && arguments.len() == 1 {
|
||||
if let ASTNode::Variable { name: an, .. } = &arguments[0] {
|
||||
return an == &p0;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MacroBehavior { Identity, Uppercase, ArrayPrependZero, MapInsertTag }
|
||||
|
||||
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 };
|
||||
// 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::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, .. } => {
|
||||
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)
|
||||
}
|
||||
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)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
fn ast_has_method(a: &ASTNode, method: &str) -> bool {
|
||||
use nyash_rust::ast::ASTNode as A;
|
||||
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::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::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::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\":[") {
|
||||
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\":[") {
|
||||
return MacroBehavior::MapInsertTag;
|
||||
}
|
||||
// Detect upper-string macro by pattern or toUpperCase usage
|
||||
if ast_has_literal_string(&ast, "\"value\":\"UPPER:") || ast_has_method(&ast, "toUpperCase") {
|
||||
return MacroBehavior::Uppercase;
|
||||
}
|
||||
if let ASTNode::Program { statements, .. } = ast {
|
||||
for st in statements {
|
||||
if let ASTNode::BoxDeclaration { name: _, methods, .. } = st {
|
||||
if let Some(ASTNode::FunctionDeclaration { name: mname, body, params, .. }) = methods.get("expand") {
|
||||
if mname == "expand" {
|
||||
if expand_indicates_uppercase(body, params) {
|
||||
return MacroBehavior::Uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MacroBehavior::Identity
|
||||
}
|
||||
|
||||
struct NyChildMacroBox { nm: &'static str, file: &'static str }
|
||||
|
||||
fn cap_enabled(name: &str) -> bool {
|
||||
match std::env::var(name).ok() {
|
||||
Some(v) => {
|
||||
let v = v.to_ascii_lowercase();
|
||||
v == "1" || v == "true" || v == "on"
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn caps_allow_macro_source(ast: &ASTNode) -> Result<(), String> {
|
||||
let allow_io = cap_enabled("NYASH_MACRO_CAP_IO");
|
||||
let allow_net = cap_enabled("NYASH_MACRO_CAP_NET");
|
||||
use nyash_rust::ast::ASTNode as A;
|
||||
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::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); },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut boxes = Vec::new();
|
||||
scan(ast, &mut boxes);
|
||||
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") {
|
||||
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 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(); }
|
||||
};
|
||||
let use_runner = std::env::var("NYASH_MACRO_BOX_CHILD_RUNNER").ok().map(|v| v == "1").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");
|
||||
}
|
||||
let mut cmd = std::process::Command::new(exe.clone());
|
||||
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 tmp_path = tmp_dir.join(format!("macro_expand_runner_{}.nyash", 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 macro_src = std::fs::read_to_string(self.file)
|
||||
.unwrap_or_else(|_| String::from("// failed to read macro file\n"));
|
||||
let script = format!(
|
||||
"{}\n\nfunction main(args) {{\n if args.length() == 0 {{ print(\\\"{{}}\\\"); return 0 }}\n local j, r, ctx\n j = args.get(0)\n if args.length() > 1 {{ ctx = args.get(1) }} else {{ ctx = \\\"{{}}\\\" }}\n try {{\n r = MacroBoxSpec.expand(j, ctx)\n }} catch (e) {{\n r = MacroBoxSpec.expand(j)\n }}\n print(r)\n return 0\n}}\n",
|
||||
macro_src
|
||||
);
|
||||
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 '--'
|
||||
let j = crate::r#macro::ast_json::ast_to_json(ast).to_string();
|
||||
cmd.arg("--").arg(j);
|
||||
// Provide MacroCtx as JSON (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);
|
||||
cmd.arg(ctx_json);
|
||||
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)
|
||||
.stdin(std::process::Stdio::piped());
|
||||
}
|
||||
cmd.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped());
|
||||
// Sandbox env (PoC): prefer PyVM; disable plugins
|
||||
cmd.env("NYASH_VM_USE_PY", "1");
|
||||
cmd.env("NYASH_DISABLE_PLUGINS", "1");
|
||||
// Timeout
|
||||
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();
|
||||
} };
|
||||
// Write stdin only in internal child mode
|
||||
if !use_runner {
|
||||
if let Some(mut sin) = child.stdin.take() {
|
||||
let j = crate::r#macro::ast_json::ast_to_json(ast).to_string();
|
||||
use std::io::Write;
|
||||
let _ = sin.write_all(j.as_bytes());
|
||||
}
|
||||
}
|
||||
// Wait with timeout
|
||||
use std::time::{Duration, Instant};
|
||||
let start = Instant::now();
|
||||
let mut out = String::new();
|
||||
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); }
|
||||
break;
|
||||
}
|
||||
Ok(None) => {
|
||||
if start.elapsed() >= Duration::from_millis(timeout_ms) {
|
||||
let _ = child.kill(); let _ = child.wait();
|
||||
eprintln!("[macro-proxy] timeout {} ms", timeout_ms);
|
||||
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(); }
|
||||
}
|
||||
}
|
||||
// Parse output JSON
|
||||
match serde_json::from_str::<serde_json::Value>(&out) {
|
||||
Ok(v) => match crate::r#macro::ast_json::json_to_ast(&v) { Some(a) => a, None => { eprintln!("[macro-proxy] child JSON did not map to AST"); if strict_enabled() { std::process::exit(2); } ast.clone() } },
|
||||
Err(e) => { eprintln!("[macro-proxy] invalid JSON from child: {}", e); if strict_enabled() { std::process::exit(2); } ast.clone() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strict_enabled() -> bool {
|
||||
match std::env::var("NYASH_MACRO_STRICT").ok() {
|
||||
Some(v) => {
|
||||
let v = v.to_ascii_lowercase();
|
||||
!(v == "0" || v == "false" || v == "off")
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
379
src/macro/mod.rs
Normal file
379
src/macro/mod.rs
Normal file
@ -0,0 +1,379 @@
|
||||
//! Macro System scaffolding (Phase 16 – MVP)
|
||||
//!
|
||||
//! 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 engine;
|
||||
pub mod macro_box;
|
||||
pub mod macro_box_ny;
|
||||
pub mod ast_json;
|
||||
pub mod ctx;
|
||||
|
||||
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_ENABLE") {
|
||||
let v = v.to_ascii_lowercase();
|
||||
if v == "0" || v == "false" || v == "off" { return false; }
|
||||
return true;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// 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(); }
|
||||
// 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); }
|
||||
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); }
|
||||
out2
|
||||
}
|
||||
|
||||
fn maybe_inject_test_harness(ast: &ASTNode) -> ASTNode {
|
||||
if std::env::var("NYASH_TEST_RUN").ok().as_deref() != Some("1") {
|
||||
return ast.clone();
|
||||
}
|
||||
// Test call plan
|
||||
#[derive(Clone)]
|
||||
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();
|
||||
let mut has_main_fn = false;
|
||||
let mut _main_params_len: usize = 0;
|
||||
|
||||
// Optional JSON args:
|
||||
// - Simple: { "test_name": [1, "s", true], "Box.method": [ ... ] }
|
||||
// - 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> }
|
||||
#[derive(Clone, Default)]
|
||||
struct TestArgSpec { args: Vec<nyash_rust::ASTNode>, instance: Option<InstanceSpec> }
|
||||
|
||||
fn json_err(msg: &str) {
|
||||
eprintln!("[macro][test][args] {}", msg);
|
||||
}
|
||||
|
||||
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::Number(n) => {
|
||||
if let Some(i) = n.as_i64() {
|
||||
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() })
|
||||
} else {
|
||||
Err("unsupported number literal".into())
|
||||
}
|
||||
}
|
||||
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() })
|
||||
}
|
||||
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(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()); }
|
||||
}
|
||||
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()); }
|
||||
}
|
||||
if let Some(name) = obj.get("var").and_then(|v| v.as_str()) {
|
||||
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)?); }
|
||||
}
|
||||
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 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)?); }
|
||||
}
|
||||
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)?); }
|
||||
}
|
||||
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() });
|
||||
} 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() });
|
||||
} else {
|
||||
return Err(format!("unknown ctor '{}', expected 'new' or 'birth'", ctor));
|
||||
}
|
||||
}
|
||||
Err("unknown object mapping for AST".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_test_arg_spec(v: &serde_json::Value) -> Option<TestArgSpec> {
|
||||
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 })
|
||||
}
|
||||
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; } } }
|
||||
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 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; } } }
|
||||
}
|
||||
spec.instance = Some(InstanceSpec { ctor, args, type_args });
|
||||
}
|
||||
Some(spec)
|
||||
}
|
||||
_ => { 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 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); } }
|
||||
return Some(map);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})();
|
||||
if let nyash_rust::ASTNode::Program { statements, .. } = ast {
|
||||
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.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() } });
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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 {
|
||||
for (mname, mnode) in methods {
|
||||
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 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 {
|
||||
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() }),
|
||||
method: mname.clone(),
|
||||
arguments: args,
|
||||
span: nyash_rust::ast::Span::unknown(),
|
||||
};
|
||||
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(); } }
|
||||
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() },
|
||||
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() }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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()],
|
||||
initial_values: vec![Some(Box::new(inst_init))],
|
||||
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 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 {
|
||||
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() }),
|
||||
method: mname.clone(),
|
||||
arguments: args,
|
||||
span: nyash_rust::ast::Span::unknown(),
|
||||
};
|
||||
tests.push(TestPlan { label: format!("{}.{}", box_name, mname), setup: Some(setup), call });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Filter
|
||||
if let Ok(substr) = std::env::var("NYASH_TEST_FILTER") {
|
||||
if !substr.is_empty() {
|
||||
tests.retain(|tp| tp.label.contains(&substr));
|
||||
}
|
||||
}
|
||||
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_')"); }
|
||||
return ast.clone();
|
||||
}
|
||||
// Decide entry policy when main exists
|
||||
let force = std::env::var("NYASH_TEST_FORCE").ok().as_deref() == Some("1");
|
||||
let entry_mode = std::env::var("NYASH_TEST_ENTRY").ok(); // Some("wrap"|"override")
|
||||
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};
|
||||
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() });
|
||||
for tp in &tests {
|
||||
// optional setup
|
||||
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() });
|
||||
// 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 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() },
|
||||
];
|
||||
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() },
|
||||
];
|
||||
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() });
|
||||
// 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() }
|
||||
};
|
||||
|
||||
// Transform AST according to policy
|
||||
if let nyash_rust::ASTNode::Program { statements, span } = ast.clone() {
|
||||
let mut out_stmts: Vec<A> = Vec::with_capacity(statements.len() + 1);
|
||||
let mut _renamed_main = false;
|
||||
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" => {
|
||||
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 });
|
||||
_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() });
|
||||
}
|
||||
} else {
|
||||
// keep as-is (no injection)
|
||||
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 };
|
||||
}
|
||||
// Compose harness main now
|
||||
let mut body2 = body;
|
||||
// Summary is already included in body. Append call/return per policy.
|
||||
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() });
|
||||
// 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() });
|
||||
} else {
|
||||
// default: tests policy; still call original but ignore result
|
||||
body2.push(call);
|
||||
// return fail already appended earlier
|
||||
}
|
||||
}
|
||||
let harness_fn = make_harness_main(body2);
|
||||
out_stmts.push(harness_fn);
|
||||
return nyash_rust::ASTNode::Program { statements: out_stmts, span };
|
||||
}
|
||||
ast.clone()
|
||||
}
|
||||
252
src/macro/pattern.rs
Normal file
252
src/macro/pattern.rs
Normal file
@ -0,0 +1,252 @@
|
||||
use std::collections::HashMap;
|
||||
use nyash_rust::ASTNode;
|
||||
|
||||
/// Minimal pattern trait — MVP
|
||||
pub trait MacroPattern {
|
||||
fn match_ast(&self, node: &ASTNode) -> Option<HashMap<String, ASTNode>>;
|
||||
}
|
||||
|
||||
/// Quote/Unquote placeholders — MVP stubs
|
||||
pub struct AstBuilder;
|
||||
|
||||
impl AstBuilder {
|
||||
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() },
|
||||
}
|
||||
}
|
||||
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 }
|
||||
}
|
||||
fn is_variadic(name: &str) -> Option<&str> {
|
||||
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(); }
|
||||
}
|
||||
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::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() {
|
||||
if let ASTNode::Variable { name, .. } = &arguments[i] {
|
||||
if let Some(vn) = is_variadic(name) {
|
||||
if let Some(ASTNode::Program { statements, .. }) = binds.get(vn) {
|
||||
out_args.extend(statements.clone());
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
out_args.push(subst(&arguments[i], binds));
|
||||
i += 1;
|
||||
}
|
||||
ASTNode::MethodCall { object: Box::new(subst(&object, binds)), method, arguments: out_args, span }
|
||||
}
|
||||
ASTNode::FunctionCall { name, arguments, span } => {
|
||||
let mut out_args: Vec<ASTNode> = Vec::new();
|
||||
let mut i = 0usize;
|
||||
while i < arguments.len() {
|
||||
if let ASTNode::Variable { name, .. } = &arguments[i] {
|
||||
if let Some(vn) = is_variadic(name) {
|
||||
if let Some(ASTNode::Program { statements, .. }) = binds.get(vn) {
|
||||
out_args.extend(statements.clone());
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
out_args.push(subst(&arguments[i], binds));
|
||||
i += 1;
|
||||
}
|
||||
ASTNode::FunctionCall { name, arguments: out_args, span }
|
||||
}
|
||||
ASTNode::ArrayLiteral { elements, span } => {
|
||||
// Splice variadic placeholder inside arrays
|
||||
let mut out_elems: Vec<ASTNode> = Vec::new();
|
||||
let mut i = 0usize;
|
||||
while i < elements.len() {
|
||||
if let ASTNode::Variable { name, .. } = &elements[i] {
|
||||
if let Some(vn) = is_variadic(name) {
|
||||
if let Some(ASTNode::Program { statements, .. }) = binds.get(vn) {
|
||||
out_elems.extend(statements.clone());
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
out_elems.push(subst(&elements[i], binds));
|
||||
i += 1;
|
||||
}
|
||||
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 {
|
||||
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 },
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
subst(template, _bindings)
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple template-based pattern that uses Variables named "$name" as bind points.
|
||||
pub struct TemplatePattern { pub template: ASTNode }
|
||||
|
||||
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 }
|
||||
}
|
||||
fn is_variadic(name: &str) -> Option<&str> {
|
||||
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 }
|
||||
}
|
||||
(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 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; }
|
||||
}
|
||||
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; } }
|
||||
true
|
||||
}
|
||||
(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 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; }
|
||||
}
|
||||
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; } }
|
||||
true
|
||||
}
|
||||
(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 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; }
|
||||
}
|
||||
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; }
|
||||
}
|
||||
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; } }
|
||||
true
|
||||
}
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
_ => tpl == tgt,
|
||||
}
|
||||
}
|
||||
let mut out = HashMap::new();
|
||||
if go(&self.template, node, &mut out) { Some(out) } else { None }
|
||||
}
|
||||
}
|
||||
|
||||
/// ORパターン: いずれかのテンプレートにマッチすれば成功
|
||||
pub struct OrPattern { pub alts: Vec<TemplatePattern> }
|
||||
|
||||
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); }
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@ -50,6 +50,7 @@ pub mod semantics; // mirror library semantics module for crate path consistency
|
||||
// 🚀 Refactored modules for better organization
|
||||
pub mod cli;
|
||||
pub mod runner;
|
||||
pub mod r#macro;
|
||||
|
||||
// BID-FFI / Plugin System (prototype)
|
||||
pub mod bid;
|
||||
|
||||
@ -13,6 +13,27 @@ use crate::tokenizer::TokenType;
|
||||
use std::collections::HashMap;
|
||||
|
||||
impl NyashParser {
|
||||
/// Forbid user-defined methods named exactly as the box (constructor-like names).
|
||||
/// Nyash constructors are explicit: init/pack/birth. A method with the same name
|
||||
/// as the box is likely a mistaken constructor attempt; reject for clarity.
|
||||
fn validate_no_ctor_like_name(
|
||||
&mut self,
|
||||
box_name: &str,
|
||||
methods: &HashMap<String, ASTNode>,
|
||||
) -> Result<(), ParseError> {
|
||||
if methods.contains_key(box_name) {
|
||||
let line = self.current_token().line;
|
||||
return Err(ParseError::UnexpectedToken {
|
||||
found: self.current_token().token_type.clone(),
|
||||
expected: format!(
|
||||
"method name must not match box name '{}'; use init/pack/birth for constructors",
|
||||
box_name
|
||||
),
|
||||
line,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Validate that birth_once properties do not have cyclic dependencies via me.<prop> references
|
||||
fn validate_birth_once_cycles(
|
||||
&mut self,
|
||||
@ -592,6 +613,8 @@ impl NyashParser {
|
||||
}
|
||||
|
||||
self.consume(TokenType::RBRACE)?;
|
||||
// 🚫 Disallow method named same as the box (constructor-like confusion)
|
||||
self.validate_no_ctor_like_name(&name, &methods)?;
|
||||
|
||||
// 🔥 Override validation
|
||||
for parent in &extends {
|
||||
|
||||
@ -60,7 +60,29 @@ pub(crate) fn execute_file_with_backend(runner: &NyashRunner, filename: &str) {
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
println!("{:#?}", ast);
|
||||
// Optional macro expansion dump (no-op expansion for now)
|
||||
let ast2 = if crate::r#macro::enabled() {
|
||||
crate::r#macro::maybe_expand_and_dump(&ast, true)
|
||||
} else {
|
||||
ast.clone()
|
||||
};
|
||||
println!("{:#?}", ast2);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dump expanded AST as JSON v0 and exit
|
||||
if runner.config.dump_expanded_ast_json {
|
||||
let code = match fs::read_to_string(filename) {
|
||||
Ok(content) => content,
|
||||
Err(e) => { eprintln!("❌ Error reading file {}: {}", filename, e); process::exit(1); }
|
||||
};
|
||||
let ast = match NyashParser::parse_from_string(&code) {
|
||||
Ok(ast) => ast,
|
||||
Err(e) => { eprintln!("❌ Parse error: {}", e); process::exit(1); }
|
||||
};
|
||||
let expanded = if crate::r#macro::enabled() { crate::r#macro::maybe_expand_and_dump(&ast, false) } else { ast };
|
||||
let j = crate::r#macro::ast_json::ast_to_json(&expanded);
|
||||
println!("{}", j.to_string());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -54,6 +54,11 @@ impl NyashRunner {
|
||||
|
||||
/// Run Nyash based on the configuration
|
||||
pub fn run(&self) {
|
||||
// Macro sandbox child mode: --macro-expand-child <file>
|
||||
if let Some(ref macro_file) = self.config.macro_expand_child {
|
||||
crate::runner::modes::macro_child::run_macro_child(macro_file);
|
||||
return;
|
||||
}
|
||||
// Build system (MVP): nyash --build <nyash.toml>
|
||||
let groups = self.config.as_groups();
|
||||
if let Some(cfg_path) = groups.build.path.clone() {
|
||||
|
||||
@ -137,7 +137,8 @@ impl NyashRunner {
|
||||
std::env::set_var("NYASH_JIT_NATIVE_F64", "1");
|
||||
let start = std::time::Instant::now();
|
||||
for _ in 0..iters {
|
||||
if let Ok(ast) = NyashParser::parse_from_string(code) {
|
||||
if let Ok(ast0) = NyashParser::parse_from_string(code) {
|
||||
let ast = crate::r#macro::maybe_expand_and_dump(&ast0, false);
|
||||
let mut interp = NyashInterpreter::new();
|
||||
let _ = interp.execute(ast);
|
||||
}
|
||||
@ -155,7 +156,8 @@ impl NyashRunner {
|
||||
fn bench_vm(&self, code: &str, iters: u32) -> std::time::Duration {
|
||||
let start = std::time::Instant::now();
|
||||
for _ in 0..iters {
|
||||
if let Ok(ast) = NyashParser::parse_from_string(code) {
|
||||
if let Ok(ast0) = NyashParser::parse_from_string(code) {
|
||||
let ast = crate::r#macro::maybe_expand_and_dump(&ast0, false);
|
||||
let mut mc = MirCompiler::new();
|
||||
if let Ok(cr) = mc.compile(ast) {
|
||||
let mut vm = VM::new();
|
||||
@ -186,7 +188,8 @@ impl NyashRunner {
|
||||
}
|
||||
let start = std::time::Instant::now();
|
||||
for _ in 0..iters {
|
||||
if let Ok(ast) = NyashParser::parse_from_string(code) {
|
||||
if let Ok(ast0) = NyashParser::parse_from_string(code) {
|
||||
let ast = crate::r#macro::maybe_expand_and_dump(&ast0, false);
|
||||
let mut mc = MirCompiler::new();
|
||||
if let Ok(cr) = mc.compile(ast) {
|
||||
let mut vm = VM::new();
|
||||
@ -208,8 +211,8 @@ impl NyashRunner {
|
||||
fn verify_outputs_match(&self, code: &str) -> Result<(), String> {
|
||||
// VM
|
||||
let vm_out = {
|
||||
let ast =
|
||||
NyashParser::parse_from_string(code).map_err(|e| format!("vm parse: {}", e))?;
|
||||
let ast0 = NyashParser::parse_from_string(code).map_err(|e| format!("vm parse: {}", e))?;
|
||||
let ast = crate::r#macro::maybe_expand_and_dump(&ast0, false);
|
||||
let mut mc = MirCompiler::new();
|
||||
let cr = mc.compile(ast).map_err(|e| format!("vm compile: {}", e))?;
|
||||
let mut vm = VM::new();
|
||||
@ -222,8 +225,8 @@ impl NyashRunner {
|
||||
let jit_out = {
|
||||
std::env::set_var("NYASH_JIT_EXEC", "1");
|
||||
std::env::set_var("NYASH_JIT_THRESHOLD", "1");
|
||||
let ast =
|
||||
NyashParser::parse_from_string(code).map_err(|e| format!("jit parse: {}", e))?;
|
||||
let ast0 = NyashParser::parse_from_string(code).map_err(|e| format!("jit parse: {}", e))?;
|
||||
let ast = crate::r#macro::maybe_expand_and_dump(&ast0, false);
|
||||
let mut mc = MirCompiler::new();
|
||||
let cr = mc.compile(ast).map_err(|e| format!("jit compile: {}", e))?;
|
||||
let mut vm = VM::new();
|
||||
|
||||
@ -15,6 +15,7 @@ impl NyashRunner {
|
||||
Ok(ast) => ast,
|
||||
Err(e) => { eprintln!("❌ Parse error: {}", e); process::exit(1); }
|
||||
};
|
||||
let ast = crate::r#macro::maybe_expand_and_dump(&ast, false);
|
||||
// AST → MIR
|
||||
let mut mir_compiler = MirCompiler::new();
|
||||
let compile_result = match mir_compiler.compile(ast) {
|
||||
@ -42,4 +43,3 @@ impl NyashRunner {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,8 @@ impl NyashRunner {
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
// Macro expansion (env-gated)
|
||||
let ast = crate::r#macro::maybe_expand_and_dump(&ast, false);
|
||||
|
||||
// Compile to MIR
|
||||
let mut mir_compiler = MirCompiler::new();
|
||||
|
||||
101
src/runner/modes/macro_child.rs
Normal file
101
src/runner/modes/macro_child.rs
Normal file
@ -0,0 +1,101 @@
|
||||
use serde_json::Value;
|
||||
|
||||
fn transform_array_prepend_zero(ast: &nyash_rust::ASTNode) -> nyash_rust::ASTNode {
|
||||
use nyash_rust::ast::{ASTNode as A, LiteralValue, Span};
|
||||
match ast {
|
||||
A::ArrayLiteral { elements, .. } => {
|
||||
// Idempotent: only prepend if first element is not int 0
|
||||
let mut new_elems: Vec<A> = Vec::with_capacity(elements.len() + 1);
|
||||
let already_zero = elements.get(0).and_then(|n| if let A::Literal { value: LiteralValue::Integer(0), .. } = n { Some(()) } else { None }).is_some();
|
||||
if already_zero {
|
||||
for e in elements { new_elems.push(transform_array_prepend_zero(e)); }
|
||||
} else {
|
||||
new_elems.push(A::Literal { value: LiteralValue::Integer(0), span: Span::unknown() });
|
||||
for e in elements { new_elems.push(transform_array_prepend_zero(e)); }
|
||||
}
|
||||
A::ArrayLiteral { elements: new_elems, span: Span::unknown() }
|
||||
}
|
||||
A::Program { statements, .. } => A::Program { statements: statements.iter().map(transform_array_prepend_zero).collect(), span: Span::unknown() },
|
||||
A::Print { expression, .. } => A::Print { expression: Box::new(transform_array_prepend_zero(expression)), span: Span::unknown() },
|
||||
A::Return { value, .. } => A::Return { value: value.as_ref().map(|v| Box::new(transform_array_prepend_zero(v))), span: Span::unknown() },
|
||||
A::Assignment { target, value, .. } => A::Assignment { target: Box::new(transform_array_prepend_zero(target)), value: Box::new(transform_array_prepend_zero(value)), span: Span::unknown() },
|
||||
A::If { condition, then_body, else_body, .. } => A::If {
|
||||
condition: Box::new(transform_array_prepend_zero(condition)),
|
||||
then_body: then_body.iter().map(transform_array_prepend_zero).collect(),
|
||||
else_body: else_body.as_ref().map(|v| v.iter().map(transform_array_prepend_zero).collect()),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
A::BinaryOp { operator, left, right, .. } => A::BinaryOp { operator: operator.clone(), left: Box::new(transform_array_prepend_zero(left)), right: Box::new(transform_array_prepend_zero(right)), span: Span::unknown() },
|
||||
A::UnaryOp { operator, operand, .. } => A::UnaryOp { operator: operator.clone(), operand: Box::new(transform_array_prepend_zero(operand)), span: Span::unknown() },
|
||||
A::MethodCall { object, method, arguments, .. } => A::MethodCall { object: Box::new(transform_array_prepend_zero(object)), method: method.clone(), arguments: arguments.iter().map(transform_array_prepend_zero).collect(), span: Span::unknown() },
|
||||
A::FunctionCall { name, arguments, .. } => A::FunctionCall { name: name.clone(), arguments: arguments.iter().map(transform_array_prepend_zero).collect(), span: Span::unknown() },
|
||||
A::MapLiteral { entries, .. } => A::MapLiteral { entries: entries.iter().map(|(k,v)| (k.clone(), transform_array_prepend_zero(v))).collect(), span: Span::unknown() },
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_map_insert_tag(ast: &nyash_rust::ASTNode) -> nyash_rust::ASTNode {
|
||||
use nyash_rust::ast::{ASTNode as A, LiteralValue, Span};
|
||||
match ast {
|
||||
A::MapLiteral { entries, .. } => {
|
||||
// Idempotent: only insert if first key is not "__macro"
|
||||
let mut new_entries: Vec<(String, A)> = Vec::with_capacity(entries.len() + 1);
|
||||
let already_tagged = entries.get(0).map(|(k, _)| k == "__macro").unwrap_or(false);
|
||||
if already_tagged {
|
||||
for (k, v) in entries { new_entries.push((k.clone(), transform_map_insert_tag(v))); }
|
||||
} else {
|
||||
new_entries.push(("__macro".to_string(), A::Literal { value: LiteralValue::String("on".to_string()), span: Span::unknown() }));
|
||||
for (k, v) in entries { new_entries.push((k.clone(), transform_map_insert_tag(v))); }
|
||||
}
|
||||
A::MapLiteral { entries: new_entries, span: Span::unknown() }
|
||||
}
|
||||
A::Program { statements, .. } => A::Program { statements: statements.iter().map(transform_map_insert_tag).collect(), span: Span::unknown() },
|
||||
A::Print { expression, .. } => A::Print { expression: Box::new(transform_map_insert_tag(expression)), span: Span::unknown() },
|
||||
A::Return { value, .. } => A::Return { value: value.as_ref().map(|v| Box::new(transform_map_insert_tag(v))), span: Span::unknown() },
|
||||
A::Assignment { target, value, .. } => A::Assignment { target: Box::new(transform_map_insert_tag(target)), value: Box::new(transform_map_insert_tag(value)), span: Span::unknown() },
|
||||
A::If { condition, then_body, else_body, .. } => A::If {
|
||||
condition: Box::new(transform_map_insert_tag(condition)),
|
||||
then_body: then_body.iter().map(transform_map_insert_tag).collect(),
|
||||
else_body: else_body.as_ref().map(|v| v.iter().map(transform_map_insert_tag).collect()),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
A::BinaryOp { operator, left, right, .. } => A::BinaryOp { operator: operator.clone(), left: Box::new(transform_map_insert_tag(left)), right: Box::new(transform_map_insert_tag(right)), span: Span::unknown() },
|
||||
A::UnaryOp { operator, operand, .. } => A::UnaryOp { operator: operator.clone(), operand: Box::new(transform_map_insert_tag(operand)), span: Span::unknown() },
|
||||
A::MethodCall { object, method, arguments, .. } => A::MethodCall { object: Box::new(transform_map_insert_tag(object)), method: method.clone(), arguments: arguments.iter().map(transform_map_insert_tag).collect(), span: Span::unknown() },
|
||||
A::FunctionCall { name, arguments, .. } => A::FunctionCall { name: name.clone(), arguments: arguments.iter().map(transform_map_insert_tag).collect(), span: Span::unknown() },
|
||||
A::ArrayLiteral { elements, .. } => A::ArrayLiteral { elements: elements.iter().map(transform_map_insert_tag).collect(), span: Span::unknown() },
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_macro_child(macro_file: &str) {
|
||||
// Read stdin all
|
||||
use std::io::Read;
|
||||
let mut input = String::new();
|
||||
if let Err(e) = std::io::stdin().read_to_string(&mut input) {
|
||||
eprintln!("[macro-child] read stdin error: {}", e);
|
||||
std::process::exit(2);
|
||||
}
|
||||
let v: Value = match serde_json::from_str(&input) {
|
||||
Ok(x) => x,
|
||||
Err(e) => { eprintln!("[macro-child] invalid JSON: {}", e); std::process::exit(3); }
|
||||
};
|
||||
let ast = match crate::r#macro::ast_json::json_to_ast(&v) {
|
||||
Some(a) => a,
|
||||
None => { eprintln!("[macro-child] unsupported AST JSON v0"); std::process::exit(4); }
|
||||
};
|
||||
// Analyze macro behavior (PoC)
|
||||
let behavior = crate::r#macro::macro_box_ny::analyze_macro_file(macro_file);
|
||||
let out_ast = match behavior {
|
||||
crate::r#macro::macro_box_ny::MacroBehavior::Identity => ast.clone(),
|
||||
crate::r#macro::macro_box_ny::MacroBehavior::Uppercase => {
|
||||
// Apply built-in Uppercase transformation
|
||||
let m = crate::r#macro::macro_box::UppercasePrintMacro;
|
||||
crate::r#macro::macro_box::MacroBox::expand(&m, &ast)
|
||||
}
|
||||
crate::r#macro::macro_box_ny::MacroBehavior::ArrayPrependZero => transform_array_prepend_zero(&ast),
|
||||
crate::r#macro::macro_box_ny::MacroBehavior::MapInsertTag => transform_map_insert_tag(&ast),
|
||||
};
|
||||
let out_json = crate::r#macro::ast_json::ast_to_json(&out_ast);
|
||||
println!("{}", out_json.to_string());
|
||||
}
|
||||
@ -25,6 +25,8 @@ impl NyashRunner {
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
// Macro expansion (env-gated)
|
||||
let ast = crate::r#macro::maybe_expand_and_dump(&ast, false);
|
||||
|
||||
// Compile to MIR (opt passes configurable)
|
||||
let mut mir_compiler = MirCompiler::with_options(!self.config.no_optimize);
|
||||
|
||||
@ -17,6 +17,7 @@ impl NyashRunner {
|
||||
Ok(ast) => ast,
|
||||
Err(e) => { eprintln!("❌ Parse error: {}", e); process::exit(1); }
|
||||
};
|
||||
let ast = crate::r#macro::maybe_expand_and_dump(&ast, false);
|
||||
|
||||
// Prepare runtime and collect Box declarations for user-defined types
|
||||
let runtime = {
|
||||
|
||||
@ -7,6 +7,7 @@ pub mod mir;
|
||||
#[cfg(feature = "vm-legacy")]
|
||||
pub mod vm;
|
||||
pub mod pyvm;
|
||||
pub mod macro_child;
|
||||
|
||||
// Shared helpers extracted from common.rs (in progress)
|
||||
pub mod common_util;
|
||||
|
||||
@ -21,6 +21,7 @@ pub fn execute_pyvm_only(_runner: &NyashRunner, filename: &str) {
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
let ast = crate::r#macro::maybe_expand_and_dump(&ast, false);
|
||||
|
||||
// Compile to MIR (respect default optimizer setting)
|
||||
let mut mir_compiler = MirCompiler::with_options(true);
|
||||
|
||||
@ -117,6 +117,7 @@ impl NyashRunner {
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
let ast = crate::r#macro::maybe_expand_and_dump(&ast, false);
|
||||
|
||||
// Prepare runtime and collect Box declarations for VM user-defined types
|
||||
let runtime = {
|
||||
|
||||
@ -19,6 +19,7 @@ impl NyashRunner {
|
||||
Ok(ast) => ast,
|
||||
Err(e) => { eprintln!("❌ Parse error: {}", e); process::exit(1); }
|
||||
};
|
||||
let ast = crate::r#macro::maybe_expand_and_dump(&ast, false);
|
||||
|
||||
// Compile to MIR
|
||||
let mut mir_compiler = MirCompiler::new();
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
use super::*;
|
||||
use nyash_rust::{mir::MirCompiler, parser::NyashParser};
|
||||
use std::{fs, process};
|
||||
|
||||
impl NyashRunner {
|
||||
@ -37,6 +38,56 @@ impl NyashRunner {
|
||||
eprintln!("[ny-compiler] mkdir tmp failed: {}", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Optional macro pre‑expand path for selfhost
|
||||
// Default: auto when macro engine is enabled (safe: PyVM only)
|
||||
// Gate: NYASH_MACRO_SELFHOST_PRE_EXPAND={1|auto|0}
|
||||
{
|
||||
let preenv = std::env::var("NYASH_MACRO_SELFHOST_PRE_EXPAND")
|
||||
.ok()
|
||||
.or_else(|| if crate::r#macro::enabled() { Some("auto".to_string()) } else { None });
|
||||
let do_pre = match preenv.as_deref() {
|
||||
Some("1") => true,
|
||||
Some("auto") => crate::r#macro::enabled() && crate::config::env::vm_use_py(),
|
||||
_ => false,
|
||||
};
|
||||
if do_pre && crate::r#macro::enabled() {
|
||||
crate::cli_v!("[ny-compiler] selfhost macro pre-expand: engaging (mode={:?})", preenv);
|
||||
match NyashParser::parse_from_string(code_ref.as_ref()) {
|
||||
Ok(ast0) => {
|
||||
let ast = crate::r#macro::maybe_expand_and_dump(&ast0, false);
|
||||
// Compile to MIR and execute (respect VM/PyVM policy similar to vm mode)
|
||||
let mut mir_compiler = MirCompiler::with_options(true);
|
||||
match mir_compiler.compile(ast) {
|
||||
Ok(result) => {
|
||||
let prefer_pyvm = crate::config::env::vm_use_py();
|
||||
if prefer_pyvm {
|
||||
if let Ok(code) = crate::runner::modes::common_util::pyvm::run_pyvm_harness_lib(&result.module, "selfhost-preexpand") {
|
||||
println!("Result: {}", code);
|
||||
std::process::exit(code);
|
||||
} else {
|
||||
eprintln!("❌ PyVM error (selfhost-preexpand)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
// For now, only PyVM path is supported in pre-expand mode; fall back otherwise.
|
||||
crate::cli_v!("[ny-compiler] pre-expand path requires NYASH_VM_USE_PY=1; falling back to default selfhost");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[ny-compiler] pre-expand compile error: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[ny-compiler] pre-expand parse error: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let tmp_path = tmp_dir.join("ny_parser_input.ny");
|
||||
if !use_tmp_only {
|
||||
match std::fs::File::create(&tmp_path) {
|
||||
|
||||
34
src/tests/macro_derive_test.rs
Normal file
34
src/tests/macro_derive_test.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use nyash_rust::parser::NyashParser;
|
||||
|
||||
#[test]
|
||||
fn macro_derive_injects_equals_and_tostring() {
|
||||
// Enable macro engine and default derives
|
||||
std::env::set_var("NYASH_MACRO_ENABLE", "1");
|
||||
std::env::set_var("NYASH_MACRO_TRACE", "0");
|
||||
std::env::remove_var("NYASH_MACRO_DERIVE");
|
||||
|
||||
let code = r#"
|
||||
box UserBox {
|
||||
name: StringBox
|
||||
age: IntegerBox
|
||||
}
|
||||
"#;
|
||||
let ast = NyashParser::parse_from_string(code).expect("parse ok");
|
||||
let ast2 = crate::r#macro::maybe_expand_and_dump(&ast, false);
|
||||
|
||||
// Find UserBox and check methods
|
||||
let mut found = false;
|
||||
if let nyash_rust::ASTNode::Program { statements, .. } = ast2 {
|
||||
for st in statements {
|
||||
if let nyash_rust::ASTNode::BoxDeclaration { name, methods, .. } = st {
|
||||
if name == "UserBox" {
|
||||
assert!(methods.contains_key("equals"), "equals method should be generated");
|
||||
assert!(methods.contains_key("toString"), "toString method should be generated");
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(found, "UserBox declaration not found after expansion");
|
||||
}
|
||||
|
||||
49
src/tests/macro_pattern_test.rs
Normal file
49
src/tests/macro_pattern_test.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use nyash_rust::ast::{ASTNode, BinaryOperator, Span};
|
||||
use crate::r#macro::pattern::{TemplatePattern, AstBuilder, MacroPattern};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn template_pattern_matches_and_unquotes() {
|
||||
// Build a template: ($x + 1) == $y (Binary(Equal, Binary(Add, $x, 1), $y))
|
||||
let tpl = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Equal,
|
||||
left: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable { name: "$x".into(), span: Span::unknown() }),
|
||||
right: Box::new(ASTNode::Literal { value: nyash_rust::ast::LiteralValue::Integer(1), span: Span::unknown() }),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Variable { name: "$y".into(), span: Span::unknown() }),
|
||||
span: Span::unknown(),
|
||||
};
|
||||
let pat = TemplatePattern::new(tpl);
|
||||
|
||||
// Target: (a + 1) == b
|
||||
let target = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Equal,
|
||||
left: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable { name: "a".into(), span: Span::unknown() }),
|
||||
right: Box::new(ASTNode::Literal { value: nyash_rust::ast::LiteralValue::Integer(1), span: Span::unknown() }),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Variable { name: "b".into(), span: Span::unknown() }),
|
||||
span: Span::unknown(),
|
||||
};
|
||||
let binds = pat.match_ast(&target).expect("pattern match");
|
||||
assert!(binds.contains_key("x"));
|
||||
assert!(binds.contains_key("y"));
|
||||
|
||||
// Unquote a template: return $y
|
||||
let builder = AstBuilder::new();
|
||||
let tpl2 = ASTNode::Return { value: Some(Box::new(ASTNode::Variable { name: "$y".into(), span: Span::unknown() })), span: Span::unknown() };
|
||||
let out = builder.unquote(&tpl2, &binds);
|
||||
match out {
|
||||
ASTNode::Return { value: Some(v), .. } => match *v {
|
||||
ASTNode::Variable { name, .. } => assert_eq!(name, "b"),
|
||||
_ => panic!("expected variable"),
|
||||
}
|
||||
_ => panic!("expected return"),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user