From 9d40e9137a54de20965e2fd1b6953b2c88bda827 Mon Sep 17 00:00:00 2001 From: Selfhosting Dev Date: Fri, 19 Sep 2025 22:47:12 +0900 Subject: [PATCH] macro(ast-json): add Loop/Break/Continue to AST JSON v0; add loop_normalize_macro (MVP identity) and loopform smoke; docs update --- .../examples/loop_normalize_macro.nyash | 13 +++++++++++++ docs/reference/ir/ast-json-v0.md | 5 ++++- src/macro/ast_json.rs | 14 ++++++++++++++ .../smoke/macro/loopform_identity_smoke.sh | 19 +++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 apps/macros/examples/loop_normalize_macro.nyash create mode 100644 tools/test/smoke/macro/loopform_identity_smoke.sh diff --git a/apps/macros/examples/loop_normalize_macro.nyash b/apps/macros/examples/loop_normalize_macro.nyash new file mode 100644 index 00000000..75d933a3 --- /dev/null +++ b/apps/macros/examples/loop_normalize_macro.nyash @@ -0,0 +1,13 @@ +// loop_normalize_macro.nyash +// MVP: identity expansion with (json, ctx) signature. +// Next steps: normalize `loop(cond){ body }` into carrier-based LoopForm. + +static box MacroBoxSpec { + static function name() { return "LoopNormalize" } + + static function expand(json, ctx) { + // For MVP, return input unchanged. + return json + } +} + diff --git a/docs/reference/ir/ast-json-v0.md b/docs/reference/ir/ast-json-v0.md index 9da7fc01..0e361a3b 100644 --- a/docs/reference/ir/ast-json-v0.md +++ b/docs/reference/ir/ast-json-v0.md @@ -7,10 +7,13 @@ Top-level - Nested nodes referenced inline; no IDs. - Span is omitted in v0 (unknown). Future versions may include `span` with file/line/col. -Kinds (subset for Phase 2) +Kinds (subset for Phase 2+) - Program: { kind: "Program", statements: [Node] } +- Loop: { kind: "Loop", condition: Node, body: [Node] } - Print: { kind: "Print", expression: Node } - Return: { kind: "Return", value: Node|null } +- Break: { kind: "Break" } +- Continue: { kind: "Continue" } - Assignment: { kind: "Assignment", target: Node, value: Node } - If: { kind: "If", condition: Node, then: [Node], else: [Node]|null } - FunctionDeclaration: { kind: "FunctionDeclaration", name: string, params: [string], body: [Node], static: bool, override: bool } diff --git a/src/macro/ast_json.rs b/src/macro/ast_json.rs index 3e4f0e88..c90ff0c3 100644 --- a/src/macro/ast_json.rs +++ b/src/macro/ast_json.rs @@ -7,6 +7,11 @@ pub fn ast_to_json(ast: &ASTNode) -> Value { "kind": "Program", "statements": statements.into_iter().map(|s| ast_to_json(&s)).collect::>() }), + ASTNode::Loop { condition, body, .. } => json!({ + "kind": "Loop", + "condition": ast_to_json(&condition), + "body": body.into_iter().map(|s| ast_to_json(&s)).collect::>() + }), ASTNode::Print { expression, .. } => json!({ "kind": "Print", "expression": ast_to_json(&expression), @@ -15,6 +20,8 @@ pub fn ast_to_json(ast: &ASTNode) -> Value { "kind": "Return", "value": value.as_ref().map(|v| ast_to_json(v)), }), + ASTNode::Break { .. } => json!({"kind":"Break"}), + ASTNode::Continue { .. } => json!({"kind":"Continue"}), ASTNode::Assignment { target, value, .. } => json!({ "kind": "Assignment", "target": ast_to_json(&target), @@ -77,8 +84,15 @@ pub fn json_to_ast(v: &Value) -> Option { let stmts = v.get("statements")?.as_array()?.iter().filter_map(json_to_ast).collect::>(); ASTNode::Program { statements: stmts, span: Span::unknown() } } + "Loop" => ASTNode::Loop { + condition: Box::new(json_to_ast(v.get("condition")?)?), + body: v.get("body")?.as_array()?.iter().filter_map(json_to_ast).collect::>(), + span: Span::unknown(), + }, "Print" => ASTNode::Print { expression: Box::new(json_to_ast(v.get("expression")?)?), span: Span::unknown() }, "Return" => ASTNode::Return { value: v.get("value").and_then(json_to_ast).map(Box::new), span: Span::unknown() }, + "Break" => ASTNode::Break { span: Span::unknown() }, + "Continue" => ASTNode::Continue { span: Span::unknown() }, "Assignment" => ASTNode::Assignment { target: Box::new(json_to_ast(v.get("target")?)?), value: Box::new(json_to_ast(v.get("value")?)?), span: Span::unknown() }, "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::>(), else_body: v.get("else").and_then(|a| a.as_array().map(|arr| arr.iter().filter_map(json_to_ast).collect::>())), span: Span::unknown() }, "FunctionDeclaration" => ASTNode::FunctionDeclaration { diff --git a/tools/test/smoke/macro/loopform_identity_smoke.sh b/tools/test/smoke/macro/loopform_identity_smoke.sh new file mode 100644 index 00000000..5e631d44 --- /dev/null +++ b/tools/test/smoke/macro/loopform_identity_smoke.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +root=$(cd "$(dirname "$0")"/../../../.. && pwd) +bin="$root/target/release/nyash" +src="apps/tests/loop_min_while.nyash" + +if [ ! -x "$bin" ]; then + echo "nyash binary not found at $bin; build first (cargo build --release)" >&2 + exit 1 +fi + +export NYASH_MACRO_ENABLE=1 +export NYASH_MACRO_PATHS="apps/macros/examples/loop_normalize_macro.nyash" + +# Prefer PyVM run to align with macro pipeline +"$bin" --backend vm "$src" >/dev/null +echo "[OK] loopform identity smoke passed" +