Phase 286A: Macro environment variable consolidation
- src/config/env/macro_flags.rs: NYASH_MACRO_* flags centralized
- Removed duplicate ny_compiler_* functions (keep in selfhost_flags.rs)
- Fixed deprecation warning logic (return None when env var not set)
- Updated callers: src/macro/{ctx,engine,macro_box,macro_box_ny,mod}.rs
Phase 286B: Box Factory environment variable consolidation
- src/config/env/box_factory_flags.rs: NYASH_BOX_FACTORY_* flags centralized
- Updated callers: src/box_factory/mod.rs, src/runtime/plugin_loader*.rs
Phase 287: Config Catalog implementation
- src/config/env/catalog.rs: New catalog for all config modules
Fixes:
- Type mismatches: Added .unwrap_or(false) for Option<bool> returns (7 locations)
- Deprecation warnings: Fixed macro_toplevel_allow() & macro_box_child_runner() logic
- Module organization: Added module declarations in src/config/env.rs
Note: Files force-added with git add -f due to .gitignore env/ pattern
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
732 lines
29 KiB
Rust
732 lines
29 KiB
Rust
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 = crate::config::env::macro_paths()
|
|
.filter(|s| !s.trim().is_empty())
|
|
.or_else(|| {
|
|
// Back-compat: NYASH_MACRO_BOX_NY / NYASH_MACRO_BOX_NY_PATHS
|
|
if crate::config::env::macro_box_ny() {
|
|
if let Some(s) = crate::config::env::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 crate::config::env::macro_toplevel_allow().is_some() {
|
|
eprintln!("[macro][compat] NYASH_MACRO_TOPLEVEL_ALLOW is deprecated; default is OFF. Prefer CLI --macro-top-level-allow if needed");
|
|
}
|
|
if crate::config::env::macro_box_child_runner().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) {
|
|
// Quiet by default; print only when tracing is enabled to reduce noise in normal runs
|
|
let noisy = crate::config::env::macro_trace()
|
|
|| crate::config::env::macro_cli_verbose();
|
|
if noisy {
|
|
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())?;
|
|
// Enable minimal sugar for macro files during scanning (array/map literals etc.)
|
|
let prev_sugar = crate::config::env::macro_syntax_sugar_level();
|
|
std::env::set_var("NYASH_SYNTAX_SUGAR_LEVEL", "basic");
|
|
let ast_res = nyash_rust::parser::NyashParser::parse_from_string(&src);
|
|
if let Some(v) = prev_sugar {
|
|
std::env::set_var("NYASH_SYNTAX_SUGAR_LEVEL", v);
|
|
} else {
|
|
std::env::remove_var("NYASH_SYNTAX_SUGAR_LEVEL");
|
|
}
|
|
let ast = ast_res.map_err(|e| format!("parse error: {:?}", e))?;
|
|
// Find a BoxDeclaration with static function expand(...)
|
|
if let ASTNode::Program { statements, .. } = ast {
|
|
// Capabilities: conservative scan before registration
|
|
if let Err(msg) = caps_allow_macro_source(&ASTNode::Program {
|
|
statements: statements.clone(),
|
|
span: nyash_rust::ast::Span::unknown(),
|
|
}) {
|
|
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 Nyash runner route by default (self-hosting). Child-proxy only when explicitly enabled.
|
|
let use_child = crate::config::env::macro_box_child();
|
|
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 = crate::config::env::macro_toplevel_allow().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 = crate::config::env::macro_box_child();
|
|
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 crate::config::env::macro_box_ny_identity_roundtrip() {
|
|
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,
|
|
LoopNormalize,
|
|
IfMatchNormalize,
|
|
ForForeachNormalize,
|
|
EnvTagString,
|
|
}
|
|
|
|
pub fn analyze_macro_file(path: &str) -> MacroBehavior {
|
|
let src = match std::fs::read_to_string(path) {
|
|
Ok(s) => s,
|
|
Err(_) => return MacroBehavior::Identity,
|
|
};
|
|
let ast = match nyash_rust::parser::NyashParser::parse_from_string(&src) {
|
|
Ok(a) => a,
|
|
Err(_) => return MacroBehavior::Identity,
|
|
};
|
|
// 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;
|
|
}
|
|
// Detect env-tag string macro by name literal as fallback
|
|
if ast_has_literal_string(&ast, "EnvTagString") {
|
|
return MacroBehavior::EnvTagString;
|
|
}
|
|
if let ASTNode::Program { statements, .. } = ast {
|
|
for st in statements {
|
|
if let ASTNode::BoxDeclaration {
|
|
name: _, methods, ..
|
|
} = st
|
|
{
|
|
// Detect LoopNormalize/IfMatchNormalize by name() returning a specific string
|
|
if let Some(ASTNode::FunctionDeclaration {
|
|
name: mname, body, ..
|
|
}) = methods.get("name")
|
|
{
|
|
if mname == "name" {
|
|
if body.len() == 1 {
|
|
if let ASTNode::Return { value: Some(v), .. } = &body[0] {
|
|
if let ASTNode::Literal {
|
|
value: nyash_rust::ast::LiteralValue::String(s),
|
|
..
|
|
} = &**v
|
|
{
|
|
if s == "LoopNormalize" {
|
|
return MacroBehavior::LoopNormalize;
|
|
}
|
|
if s == "IfMatchNormalize" {
|
|
return MacroBehavior::IfMatchNormalize;
|
|
}
|
|
if s == "ForForeach" {
|
|
return MacroBehavior::ForForeachNormalize;
|
|
}
|
|
if s == "EnvTagString" {
|
|
return MacroBehavior::EnvTagString;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let 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 caps_allow_macro_source(ast: &ASTNode) -> Result<(), String> {
|
|
let allow_io = crate::config::env::env_flag("NYASH_MACRO_CAP_IO").unwrap_or(false);
|
|
let allow_net = crate::config::env::env_flag("NYASH_MACRO_CAP_NET").unwrap_or(false);
|
|
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();
|
|
}
|
|
};
|
|
// Prefer Nyash runner route by default for self-hosting; legacy env can force internal child with 0.
|
|
let use_runner = crate::config::env::macro_box_child_runner().unwrap_or(false);
|
|
if crate::config::env::macro_box_child_runner().is_some() {
|
|
eprintln!(
|
|
"[macro][compat] NYASH_MACRO_BOX_CHILD_RUNNER is deprecated; prefer defaults"
|
|
);
|
|
}
|
|
let mut cmd = std::process::Command::new(exe.clone());
|
|
// Build MacroCtx JSON once (caps only, MVP)
|
|
let mctx = crate::r#macro::ctx::MacroCtx::from_env();
|
|
let ctx_json = format!(
|
|
"{{\"caps\":{{\"io\":{},\"net\":{},\"env\":{}}}}}",
|
|
mctx.caps.io, mctx.caps.net, mctx.caps.env
|
|
);
|
|
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_{}.hako", ts));
|
|
let mut f = match std::fs::File::create(&tmp_path) {
|
|
Ok(x) => x,
|
|
Err(e) => {
|
|
eprintln!("[macro-proxy] create tmp runner failed: {}", e);
|
|
return ast.clone();
|
|
}
|
|
};
|
|
let macro_src = std::fs::read_to_string(self.file)
|
|
.unwrap_or_else(|_| String::from("// failed to read macro file\n"));
|
|
let script = format!(
|
|
"{}\n\nfunction main(args) {{\n if args.length() == 0 {{\n print(\"{{}}\")\n return 0\n }}\n local j, r, ctx\n j = args.get(0)\n if args.length() > 1 {{ ctx = args.get(1) }} else {{ ctx = \"{{}}\" }}\n r = MacroBoxSpec.expand(j, ctx)\n print(r)\n return 0\n}}\n",
|
|
macro_src
|
|
);
|
|
if let Err(e) = f.write_all(script.as_bytes()) {
|
|
eprintln!("[macro-proxy] write tmp runner failed: {}", e);
|
|
return ast.clone();
|
|
}
|
|
// 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 (runner takes it as script arg)
|
|
cmd.arg(ctx_json.clone());
|
|
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());
|
|
// Provide MacroCtx via env for internal child
|
|
cmd.env("NYASH_MACRO_CTX_JSON", ctx_json.clone());
|
|
}
|
|
cmd.stdout(std::process::Stdio::piped())
|
|
.stderr(std::process::Stdio::piped());
|
|
// Sandbox env (PoC): prefer PyVM; disable plugins; enable minimal syntax sugar for macros
|
|
cmd.env("NYASH_VM_USE_PY", "1");
|
|
cmd.env("NYASH_DISABLE_PLUGINS", "1");
|
|
cmd.env("NYASH_SYNTAX_SUGAR_LEVEL", "basic");
|
|
// Mark sandbox mode explicitly for PyVM capability hooks
|
|
cmd.env("NYASH_MACRO_SANDBOX", "1");
|
|
// Disable macro system inside child to avoid recursive registration/expansion
|
|
cmd.env("NYASH_MACRO_ENABLE", "0");
|
|
cmd.env_remove("NYASH_MACRO_PATHS");
|
|
cmd.env_remove("NYASH_MACRO_BOX_NY");
|
|
cmd.env_remove("NYASH_MACRO_BOX_NY_PATHS");
|
|
cmd.env_remove("NYASH_MACRO_BOX_CHILD");
|
|
cmd.env_remove("NYASH_MACRO_BOX_CHILD_RUNNER");
|
|
// Timeout
|
|
let timeout_ms = crate::config::env::ny_compiler_timeout_ms();
|
|
// 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();
|
|
}
|
|
}
|
|
}
|
|
// capture stderr for diagnostics and continue
|
|
// Capture stderr for diagnostics
|
|
let mut err = String::new();
|
|
if let Some(mut se) = child.stderr.take() {
|
|
use std::io::Read;
|
|
let _ = se.read_to_string(&mut err);
|
|
}
|
|
// Parse output JSON
|
|
match serde_json::from_str::<serde_json::Value>(&out) {
|
|
Ok(v) => match crate::r#macro::ast_json::json_to_ast(&v) {
|
|
Some(a) => a,
|
|
None => {
|
|
eprintln!(
|
|
"[macro-proxy] child JSON did not map to AST. stderr=\n{}",
|
|
err
|
|
);
|
|
if strict_enabled() {
|
|
std::process::exit(2);
|
|
}
|
|
ast.clone()
|
|
}
|
|
},
|
|
Err(e) => {
|
|
eprintln!("[macro-proxy] invalid JSON from child: {}\n-- child stderr --\n{}\n-- end stderr --", e, err);
|
|
if strict_enabled() {
|
|
std::process::exit(2);
|
|
}
|
|
ast.clone()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn strict_enabled() -> bool {
|
|
match crate::config::env::macro_strict() {
|
|
Some(v) => v,
|
|
None => true,
|
|
}
|
|
}
|