feat: 配列/Mapリテラル糖衣構文の実装とネームスペース解決の改善計画

- ArrayLiteral/MapLiteralのAST定義追加
- パーサーで[...]と{...}構文をサポート
- MIR Builderでnew Box() + push/setへのdesugaring実装
- テストケースとスモークスクリプト追加
- CURRENT_TASK.mdにネームスペース解決Phase-1計画を追記
- 3段階解決順序(ローカル→エイリアス→プラグイン)の設計合意
This commit is contained in:
Selfhosting Dev
2025-09-16 06:13:44 +09:00
parent 18bc386bc5
commit 6ca56b0652
27 changed files with 446 additions and 50 deletions

View File

@ -201,6 +201,7 @@ pub enum ExpressionNode {
else_expr: Box<ASTNode>,
span: Span,
},
// (Stage2 sugar for literals is represented in unified ASTNode, not here)
}
/// 文ノード - 実行可能なアクション
@ -476,6 +477,16 @@ pub enum ASTNode {
else_expr: Box<ASTNode>,
span: Span,
},
/// 配列リテラル(糖衣): [e1, e2, ...]
ArrayLiteral {
elements: Vec<ASTNode>,
span: Span,
},
/// マップリテラル(糖衣): { "k": v, ... } Stage2: 文字列キー限定)
MapLiteral {
entries: Vec<(String, ASTNode)>,
span: Span,
},
/// 無名関数最小P1: 値としてのみ。呼び出しは未対応)
Lambda {
@ -708,6 +719,8 @@ impl ASTNode {
ASTNode::QMarkPropagate { .. } => "QMarkPropagate",
ASTNode::PeekExpr { .. } => "PeekExpr",
ASTNode::Lambda { .. } => "Lambda",
ASTNode::ArrayLiteral { .. } => "ArrayLiteral",
ASTNode::MapLiteral { .. } => "MapLiteral",
}
}
@ -740,6 +753,8 @@ impl ASTNode {
ASTNode::PeekExpr { .. } => ASTNodeType::Expression,
ASTNode::QMarkPropagate { .. } => ASTNodeType::Expression,
ASTNode::Lambda { .. } => ASTNodeType::Expression,
ASTNode::ArrayLiteral { .. } => ASTNodeType::Expression,
ASTNode::MapLiteral { .. } => ASTNodeType::Expression,
// Statement nodes - 実行可能なアクション
ASTNode::Program { .. } => ASTNodeType::Statement, // プログラム全体
@ -903,6 +918,12 @@ impl ASTNode {
ASTNode::Lambda { params, body, .. } => {
format!("Lambda({} params, {} statements)", params.len(), body.len())
}
ASTNode::ArrayLiteral { elements, .. } => {
format!("ArrayLiteral({} elements)", elements.len())
}
ASTNode::MapLiteral { entries, .. } => {
format!("MapLiteral({} entries)", entries.len())
}
}
}
@ -947,6 +968,8 @@ impl ASTNode {
ASTNode::PeekExpr { span, .. } => *span,
ASTNode::QMarkPropagate { span, .. } => *span,
ASTNode::Lambda { span, .. } => *span,
ASTNode::ArrayLiteral { span, .. } => *span,
ASTNode::MapLiteral { span, .. } => *span,
}
}
}

View File

@ -135,9 +135,9 @@ impl VM {
/// Execute a single function
pub(super) fn execute_function(&mut self, function: &MirFunction) -> Result<VMValue, VMError> {
use crate::box_trait::{StringBox, IntegerBox, BoolBox, VoidBox};
// unused: local downcasts not required here
use crate::runtime::global_hooks;
use crate::instance_v2::InstanceBox;
// use crate::instance_v2::InstanceBox; // not used in this path
use super::control_flow;
self.current_function = Some(function.signature.name.clone());

View File

@ -1,4 +1,3 @@
use crate::box_trait::NyashBox;
use crate::mir::ValueId;
use crate::backend::vm::ControlFlow;
use crate::backend::{VM, VMError, VMValue};
@ -7,7 +6,7 @@ impl VM {
/// Small helpers to reduce duplication in vtable stub paths.
#[inline]
fn vmvalue_to_box(val: &VMValue) -> Box<dyn crate::box_trait::NyashBox> {
use crate::box_trait::{NyashBox, StringBox as SBox, IntegerBox as IBox, BoolBox as BBox};
use crate::box_trait::{StringBox as SBox, IntegerBox as IBox, BoolBox as BBox};
match val {
VMValue::Integer(i) => Box::new(IBox::new(*i)),
VMValue::String(s) => Box::new(SBox::new(s)),
@ -900,3 +899,4 @@ impl VM {
Ok(None)
}
}
use crate::box_trait::NyashBox;

View File

@ -1,5 +1,5 @@
use crate::mir::{ConstValue, BinaryOp, CompareOp, UnaryOp, ValueId, BasicBlockId, TypeOpKind, MirType};
use crate::box_trait::{NyashBox, BoolBox, VoidBox};
use crate::box_trait::{BoolBox, VoidBox};
use crate::boxes::ArrayBox;
use std::sync::Arc;
use crate::backend::vm::ControlFlow;

View File

@ -12,7 +12,7 @@ use crate::mir::{BasicBlockId, ValueId};
use crate::runtime::NyashRuntime;
use crate::scope_tracker::ScopeTracker;
use std::collections::HashMap;
use std::time::Instant;
// use std::time::Instant; // not used in this module
impl VM {
fn jit_threshold_from_env() -> u32 {

View File

@ -47,7 +47,7 @@ impl NyashInterpreter {
}
"MathBox" => {
if let Ok(reg) = self.runtime.box_registry.lock() {
if let Ok(b) = reg.create_box("MathBox", &[]) {
if let Ok(_b) = reg.create_box("MathBox", &[]) {
// Note: execute_math_method expects builtin MathBox; plugin path should route via VM/BoxCall in new pipeline.
// Here we simply return void; method paths should prefer plugin invoke in VM.
return Ok(Box::new(VoidBox::new()));

View File

@ -1,5 +1,5 @@
use crate::mir::{MirFunction, MirInstruction, ConstValue, BinaryOp, CompareOp, ValueId};
use super::builder::{IRBuilder, BinOpKind, CmpKind};
use crate::mir::{MirFunction, MirInstruction, ConstValue, ValueId};
use super::builder::{IRBuilder, BinOpKind};
mod analysis;
mod cfg;

View File

@ -1,9 +1,8 @@
use std::collections::{HashMap, HashSet, BTreeSet};
use std::collections::HashSet;
use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
use super::super::builder::IRBuilder;
use super::super::core_ops; // ensure module link remains
// removed unused imports
use super::LowerCore;
impl LowerCore {
@ -114,4 +113,3 @@ impl LowerCore {
}
}
}

View File

@ -127,6 +127,9 @@ class PyVM:
# incoming: prefer [[vid, pred_bid]], but accept [pred_bid, vid] robustly
incoming = inst.get("incoming", [])
chosen: Any = None
dbg = os.environ.get('NYASH_PYVM_DEBUG_PHI') == '1'
if dbg:
print(f"[pyvm.phi] prev={prev} incoming={incoming}")
for pair in incoming:
if not isinstance(pair, (list, tuple)) or len(pair) < 2:
continue
@ -134,10 +137,8 @@ class PyVM:
# Case 1: [vid, pred]
if prev is not None and int(b) == int(prev) and int(a) in regs:
chosen = regs.get(int(a))
break
# Case 2: [pred, vid]
if prev is not None and int(a) == int(prev) and int(b) in regs:
chosen = regs.get(int(b))
if dbg:
print(f"[pyvm.phi] case1 match: use v{a} from pred {b} -> {chosen}")
break
if chosen is None and incoming:
# Fallback to first element that resolves to a known vid
@ -147,8 +148,9 @@ class PyVM:
a, b = pair[0], pair[1]
if int(a) in regs:
chosen = regs.get(int(a)); break
if int(b) in regs:
chosen = regs.get(int(b)); break
# Do not try to resolve by assuming [pred, vid] — avoid false matches
if dbg:
print(f"[pyvm.phi] chosen={chosen}")
self._set(regs, inst.get("dst"), chosen)
i += 1
continue

View File

@ -487,6 +487,17 @@ impl MirBuilder {
self.build_new_expression(class.clone(), arguments.clone())
},
ASTNode::ArrayLiteral { elements, .. } => {
// Lower: new ArrayBox(); for each elem: .push(elem)
let arr_id = self.value_gen.next();
self.emit_instruction(MirInstruction::NewBox { dst: arr_id, box_type: "ArrayBox".to_string(), args: vec![] })?;
for e in elements {
let v = self.build_expression(e)?;
self.emit_instruction(MirInstruction::BoxCall { dst: None, box_val: arr_id, method: "push".to_string(), method_id: None, args: vec![v], effects: super::EffectMask::MUT })?;
}
Ok(arr_id)
},
// Phase 7: Async operations
ASTNode::Nowait { variable, expression, .. } => {
self.build_nowait_statement(variable.clone(), *expression.clone())

View File

@ -114,6 +114,28 @@ impl super::MirBuilder {
ASTNode::New { class, arguments, .. } =>
self.build_new_expression(class.clone(), arguments.clone()),
ASTNode::ArrayLiteral { elements, .. } => {
let arr_id = self.value_gen.next();
self.emit_instruction(MirInstruction::NewBox { dst: arr_id, box_type: "ArrayBox".to_string(), args: vec![] })?;
for e in elements {
let v = self.build_expression_impl(e)?;
self.emit_instruction(MirInstruction::BoxCall { dst: None, box_val: arr_id, method: "push".to_string(), method_id: None, args: vec![v], effects: super::EffectMask::MUT })?;
}
Ok(arr_id)
}
ASTNode::MapLiteral { entries, .. } => {
let map_id = self.value_gen.next();
self.emit_instruction(MirInstruction::NewBox { dst: map_id, box_type: "MapBox".to_string(), args: vec![] })?;
for (k, expr) in entries {
// const string key
let k_id = self.value_gen.next();
self.emit_instruction(MirInstruction::Const { dst: k_id, value: ConstValue::String(k) })?;
let v_id = self.build_expression_impl(expr)?;
self.emit_instruction(MirInstruction::BoxCall { dst: None, box_val: map_id, method: "set".to_string(), method_id: None, args: vec![k_id, v_id], effects: super::EffectMask::MUT })?;
}
Ok(map_id)
}
ASTNode::Nowait { variable, expression, .. } =>
self.build_nowait_statement(variable.clone(), *expression.clone()),

View File

@ -42,6 +42,29 @@ pub struct LoopBuilder<'a> {
continue_snapshots: Vec<(BasicBlockId, HashMap<String, ValueId>)>,
}
// Local copy: detect a variable name assigned within an AST fragment
fn extract_assigned_var_local(ast: &ASTNode) -> Option<String> {
match ast {
ASTNode::Assignment { target, .. } => {
if let ASTNode::Variable { name, .. } = target.as_ref() { Some(name.clone()) } else { None }
}
ASTNode::Program { statements, .. } => statements.last().and_then(|st| extract_assigned_var_local(st)),
ASTNode::If { then_body, else_body, .. } => {
let then_prog = ASTNode::Program { statements: then_body.clone(), span: crate::ast::Span::unknown() };
let tvar = extract_assigned_var_local(&then_prog);
let evar = else_body.as_ref().and_then(|eb| {
let ep = ASTNode::Program { statements: eb.clone(), span: crate::ast::Span::unknown() };
extract_assigned_var_local(&ep)
});
match (tvar, evar) {
(Some(tv), Some(ev)) if tv == ev => Some(tv),
_ => None,
}
}
_ => None,
}
}
impl<'a> LoopBuilder<'a> {
/// 新しいループビルダーを作成
pub fn new(parent: &'a mut super::builder::MirBuilder) -> Self {
@ -325,6 +348,10 @@ impl<'a> LoopBuilder<'a> {
let merge_bb = self.new_block();
self.emit_branch(cond_val, then_bb, else_bb)?;
// Capture pre-if variable map (used for phi normalization)
let pre_if_var_map = self.get_current_variable_map();
let pre_then_var_value = pre_if_var_map.clone();
// then
self.set_current_block(then_bb)?;
for s in then_body.iter().cloned() {
@ -338,6 +365,7 @@ impl<'a> LoopBuilder<'a> {
};
if terminated { break; }
}
let then_var_map_end = self.get_current_variable_map();
// Only jump to merge if not already terminated (e.g., continue/break)
{
let cur_id = self.current_block()?;
@ -351,7 +379,8 @@ impl<'a> LoopBuilder<'a> {
// else
self.set_current_block(else_bb)?;
if let Some(es) = else_body {
let mut else_var_map_end_opt: Option<std::collections::HashMap<String, super::ValueId>> = None;
if let Some(es) = else_body.clone() {
for s in es.into_iter() {
let _ = self.build_statement(s)?;
let cur_id = self.current_block()?;
@ -362,6 +391,7 @@ impl<'a> LoopBuilder<'a> {
};
if terminated { break; }
}
else_var_map_end_opt = Some(self.get_current_variable_map());
}
{
let cur_id = self.current_block()?;
@ -375,6 +405,29 @@ impl<'a> LoopBuilder<'a> {
// Continue at merge
self.set_current_block(merge_bb)?;
// If both branches assign the same variable, emit phi and bind it
let then_prog = ASTNode::Program { statements: then_body.clone(), span: crate::ast::Span::unknown() };
let assigned_then = extract_assigned_var_local(&then_prog);
let assigned_else = else_body.as_ref().and_then(|es| {
let ep = ASTNode::Program { statements: es.clone(), span: crate::ast::Span::unknown() };
extract_assigned_var_local(&ep)
});
if let Some(var_name) = assigned_then {
let else_assigns_same = assigned_else.as_ref().map(|s| s == &var_name).unwrap_or(false);
let then_value_for_var = then_var_map_end.get(&var_name).copied();
let else_value_for_var = if else_assigns_same {
else_var_map_end_opt.as_ref().and_then(|m| m.get(&var_name).copied())
} else {
pre_then_var_value.get(&var_name).copied()
};
if let (Some(tv), Some(ev)) = (then_value_for_var, else_value_for_var) {
let phi_id = self.new_value();
self.emit_phi_at_block_start(merge_bb, phi_id, vec![(then_bb, tv), (else_bb, ev)])?;
// Reset to pre-if map and bind the phi result
self.parent_builder.variable_map = pre_if_var_map.clone();
self.parent_builder.variable_map.insert(var_name, phi_id);
}
}
let void_id = self.new_value();
self.emit_const(void_id, ConstValue::Void)?;
Ok(void_id)

View File

@ -631,9 +631,71 @@ impl NyashParser {
Ok(expr)
}
/// 基本式をパース: リテラル、変数、括弧、this、new
/// 基本式をパース: リテラル、変数、括弧、this、new、配列リテラル(糖衣)
fn parse_primary(&mut self) -> Result<ASTNode, ParseError> {
match &self.current_token().token_type {
TokenType::LBRACK => {
// Array literal: [e1, e2, ...] (sugar)
let sugar_on = crate::parser::sugar_gate::is_enabled()
|| std::env::var("NYASH_ENABLE_ARRAY_LITERAL").ok().as_deref() == Some("1");
if !sugar_on {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "enable NYASH_SYNTAX_SUGAR_LEVEL=basic|full or NYASH_ENABLE_ARRAY_LITERAL=1".to_string(),
line,
});
}
self.advance(); // consume '['
let mut elems: Vec<ASTNode> = Vec::new();
while !self.match_token(&TokenType::RBRACK) && !self.is_at_end() {
must_advance!(self, _unused, "array literal element parsing");
let el = self.parse_expression()?;
elems.push(el);
if self.match_token(&TokenType::COMMA) { self.advance(); }
}
self.consume(TokenType::RBRACK)?;
Ok(ASTNode::ArrayLiteral { elements: elems, span: Span::unknown() })
}
TokenType::LBRACE => {
// Map literal (Stage2, string keys only)
let sugar_on = crate::parser::sugar_gate::is_enabled()
|| std::env::var("NYASH_ENABLE_MAP_LITERAL").ok().as_deref() == Some("1");
if !sugar_on {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "enable NYASH_SYNTAX_SUGAR_LEVEL=basic|full or NYASH_ENABLE_MAP_LITERAL=1".to_string(),
line,
});
}
self.advance(); // consume '{'
let mut entries: Vec<(String, ASTNode)> = Vec::new();
let sugar_level = std::env::var("NYASH_SYNTAX_SUGAR_LEVEL").ok();
let ident_key_on = std::env::var("NYASH_ENABLE_MAP_IDENT_KEY").ok().as_deref() == Some("1")
|| sugar_level.as_deref() == Some("full");
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
// Key: string literal (Stage2) or identifier key sugar (Stage3; gated)
let key = match &self.current_token().token_type {
TokenType::STRING(s) => { let v = s.clone(); self.advance(); v }
TokenType::IDENTIFIER(id) if ident_key_on => { let v = id.clone(); self.advance(); v }
_ => {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: if ident_key_on { "string or identifier key in map literal".to_string() } else { "string key in map literal".to_string() },
line,
});
}
};
self.consume(TokenType::COLON)?;
let value_expr = self.parse_expression()?;
entries.push((key, value_expr));
if self.match_token(&TokenType::COMMA) { self.advance(); }
}
self.consume(TokenType::RBRACE)?;
Ok(ASTNode::MapLiteral { entries, span: Span::unknown() })
}
TokenType::INCLUDE => {
// Allow include as an expression: include "path"
self.parse_include()

View File

@ -86,6 +86,12 @@ pub(crate) fn execute_file_with_backend(runner: &NyashRunner, filename: &str) {
}
runner.execute_mir_mode(filename);
}
"vm" => {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
println!("🚀 Nyash VM Backend - Executing file: {} 🚀", filename);
}
runner.execute_vm_mode(filename);
}
#[cfg(feature = "cranelift-jit")]
"jit-direct" => {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {

View File

@ -6,20 +6,8 @@
*/
use nyash_rust::cli::CliConfig;
use nyash_rust::{
box_trait::{StringBox, IntegerBox, BoolBox, VoidBox, AddBox},
tokenizer::{NyashTokenizer},
ast::ASTNode,
parser::NyashParser,
interpreter::NyashInterpreter,
mir::{MirCompiler, MirPrinter, MirInstruction},
backend::VM,
};
use nyash_rust::runtime::{NyashRuntime, NyashRuntimeBuilder};
use nyash_rust::interpreter::SharedState;
use nyash_rust::box_factory::user_defined::UserDefinedBoxFactory;
use nyash_rust::core::model::BoxDeclaration as CoreBoxDecl;
use std::sync::Arc;
// prune heavy unused imports here; modules import what they need locally
// pruned unused runtime imports in this module
#[cfg(feature = "wasm-backend")]
use nyash_rust::backend::{wasm::WasmBackend, aot::AotBackend};

View File

@ -183,7 +183,7 @@ impl NyashRunner {
// Prefer MIR signature when available, but fall back to runtime coercions to keep VM/JIT consistent.
let (ety, sval) = if let Some(func) = compile_result.module.functions.get("main") {
use nyash_rust::mir::MirType;
use nyash_rust::box_trait::{NyashBox, IntegerBox, BoolBox, StringBox};
use nyash_rust::box_trait::{IntegerBox, BoolBox, StringBox};
use nyash_rust::boxes::FloatBox;
match &func.signature.return_type {
MirType::Float => {
@ -272,7 +272,7 @@ impl NyashRunner {
format!("./{}", filename)
}
use std::collections::{HashSet, VecDeque};
use std::collections::HashSet;
fn walk_with_state(node: &ASTNode, runtime: &NyashRuntime, stack: &mut Vec<String>, visited: &mut HashSet<String>) {
match node {

View File

@ -1,6 +1,6 @@
use super::types::{PluginBoxV2, PluginHandleInner, NyashTypeBoxFfi, LoadedPluginV2};
use super::types::{PluginBoxV2, PluginHandleInner, LoadedPluginV2};
use crate::bid::{BidResult, BidError};
use crate::box_trait::{NyashBox, BoxCore, StringBox, IntegerBox};
use crate::box_trait::NyashBox;
use crate::config::nyash_toml_v2::{NyashConfigV2, LibraryDefinition};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};

View File

@ -94,6 +94,8 @@ pub enum TokenType {
DoubleColon, // :: (Parent::method) - P1用定義のみ
LPAREN, // (
RPAREN, // )
LBRACK, // [
RBRACK, // ]
LBRACE, // {
RBRACE, // }
COMMA, // ,
@ -395,6 +397,14 @@ impl NyashTokenizer {
self.advance();
Ok(Token::new(TokenType::RPAREN, start_line, start_column))
}
Some('[') => {
self.advance();
Ok(Token::new(TokenType::LBRACK, start_line, start_column))
}
Some(']') => {
self.advance();
Ok(Token::new(TokenType::RBRACK, start_line, start_column))
}
Some('{') => {
self.advance();
Ok(Token::new(TokenType::LBRACE, start_line, start_column))
@ -407,14 +417,7 @@ impl NyashTokenizer {
self.advance();
Ok(Token::new(TokenType::COMMA, start_line, start_column))
}
Some('?') => {
self.advance();
Ok(Token::new(TokenType::QUESTION, start_line, start_column))
}
Some(':') => {
self.advance();
Ok(Token::new(TokenType::COLON, start_line, start_column))
}
// '?' and ':' are handled earlier (including variants); avoid duplicate arms
Some('\n') => {
self.advance();
Ok(Token::new(TokenType::NEWLINE, start_line, start_column))