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:
Selfhosting Dev
2025-09-19 22:27:59 +09:00
parent 811e3eb3f8
commit da32455afc
192 changed files with 6454 additions and 2973 deletions

View File

@ -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,
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}
}

View File

@ -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;

View File

@ -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 {

View File

@ -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;
}

View File

@ -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() {

View File

@ -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();

View File

@ -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 {
}
}
}

View File

@ -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();

View 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());
}

View File

@ -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);

View File

@ -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 = {

View File

@ -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;

View File

@ -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);

View File

@ -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 = {

View File

@ -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();

View File

@ -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 preexpand 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) {

View 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");
}

View 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"),
}
}