Phase 10.7/10.5c: include cycle detection (VM/Interpreter), minimal pyc IR→Nyash, String unification bridge (VM partial), add core plugins: RegexBox/EncodingBox/TOMLBox/PathBox + examples; wire nyash.toml; begin String interop for internal vs plugin boxes; update CURRENT_TASK.md

This commit is contained in:
Moe Charm
2025-08-30 23:47:08 +09:00
parent c13d9c045e
commit 4ae92cfb56
39 changed files with 3217 additions and 69 deletions

View File

@ -1215,6 +1215,42 @@ impl VM {
return Ok(ControlFlow::Continue);
}
}
// Fallback: support common methods on internal StringBox without requiring PluginBox receiver
if let VMValue::BoxRef(ref bx) = recv {
if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
match method {
"length" => {
if let Some(dst_id) = dst { self.set_value(dst_id, VMValue::Integer(sb.value.len() as i64)); }
return Ok(ControlFlow::Continue);
}
"is_empty" | "isEmpty" => {
if let Some(dst_id) = dst { self.set_value(dst_id, VMValue::Bool(sb.value.is_empty())); }
return Ok(ControlFlow::Continue);
}
"charCodeAt" => {
let idx_v = if let Some(a0) = args.get(0) { self.get_value(*a0)? } else { VMValue::Integer(0) };
let idx = match idx_v { VMValue::Integer(i) => i.max(0) as usize, _ => 0 };
let code = sb.value.chars().nth(idx).map(|c| c as u32 as i64).unwrap_or(0);
if let Some(dst_id) = dst { self.set_value(dst_id, VMValue::Integer(code)); }
return Ok(ControlFlow::Continue);
}
"concat" => {
let rhs_v = if let Some(a0) = args.get(0) { self.get_value(*a0)? } else { VMValue::String(String::new()) };
let rhs_s = match rhs_v {
VMValue::String(s) => s,
VMValue::BoxRef(br) => br.to_string_box().value,
_ => rhs_v.to_string(),
};
let mut new_s = sb.value.clone();
new_s.push_str(&rhs_s);
let out = Box::new(crate::box_trait::StringBox::new(new_s));
if let Some(dst_id) = dst { self.set_value(dst_id, VMValue::BoxRef(std::sync::Arc::from(out as Box<dyn crate::box_trait::NyashBox>))); }
return Ok(ControlFlow::Continue);
}
_ => {}
}
}
}
Err(VMError::InvalidInstruction(format!("PluginInvoke requires PluginBox receiver; method={} got {:?}", method, recv)))
}
}

View File

@ -213,6 +213,33 @@ impl VM {
li.type_name(), ri.type_name(), li.to_string_box().value, ri.to_string_box().value
);
}
// String-like comparison: internal StringBox or Plugin StringBox
fn boxref_to_string(b: &dyn crate::box_trait::NyashBox) -> Option<String> {
if let Some(sb) = b.as_any().downcast_ref::<crate::box_trait::StringBox>() {
return Some(sb.value.clone());
}
if let Some(pb) = b.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
if pb.box_type == "StringBox" {
let host = crate::runtime::get_global_plugin_host();
let s_opt: Option<String> = {
if let Ok(ro) = host.read() {
if let Ok(val_opt) = ro.invoke_instance_method("StringBox", "toUtf8", pb.inner.instance_id, &[]) {
if let Some(vb) = val_opt {
if let Some(sbb) = vb.as_any().downcast_ref::<crate::box_trait::StringBox>() {
Some(sbb.value.clone())
} else { None }
} else { None }
} else { None }
} else { None }
};
if s_opt.is_some() { return s_opt; }
}
}
None
}
if let (Some(ls), Some(rs)) = (boxref_to_string(li.as_ref()), boxref_to_string(ri.as_ref())) {
return Ok(match op { CompareOp::Eq => ls == rs, CompareOp::Ne => ls != rs, CompareOp::Lt => ls < rs, CompareOp::Le => ls <= rs, CompareOp::Gt => ls > rs, CompareOp::Ge => ls >= rs });
}
// Try integer comparisons via downcast or parse fallback
let l_opt = li.as_any().downcast_ref::<crate::box_trait::IntegerBox>().map(|x| x.value)
.or_else(|| li.as_any().downcast_ref::<crate::boxes::integer_box::IntegerBox>().map(|x| x.value))
@ -225,6 +252,43 @@ impl VM {
}
Err(VMError::TypeError(format!("[BoxRef-BoxRef] Unsupported comparison: {:?} on {:?} and {:?}", op, left, right)))
}
// Mixed String vs BoxRef (string-like)
(VMValue::String(ls), VMValue::BoxRef(ri)) => {
let rs_opt = if let Some(sb) = ri.as_any().downcast_ref::<crate::box_trait::StringBox>() { Some(sb.value.clone()) } else {
if let Some(pb) = ri.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
if pb.box_type == "StringBox" {
let host = crate::runtime::get_global_plugin_host();
let tmp = if let Ok(ro) = host.read() {
if let Ok(val_opt) = ro.invoke_instance_method("StringBox", "toUtf8", pb.inner.instance_id, &[]) {
if let Some(vb) = val_opt {
if let Some(sbb) = vb.as_any().downcast_ref::<crate::box_trait::StringBox>() { Some(sbb.value.clone()) } else { None }
} else { None }
} else { None }
} else { None };
tmp
} else { None }
} else { None }
};
if let Some(rs) = rs_opt { return Ok(match op { CompareOp::Eq => *ls == rs, CompareOp::Ne => *ls != rs, CompareOp::Lt => *ls < rs, CompareOp::Le => *ls <= rs, CompareOp::Gt => *ls > rs, CompareOp::Ge => *ls >= rs }); }
Err(VMError::TypeError(format!("[String-BoxRef] Unsupported comparison: {:?} on {:?} and {:?}", op, left, right)))
}
(VMValue::BoxRef(li), VMValue::String(rs)) => {
let ls_opt = if let Some(sb) = li.as_any().downcast_ref::<crate::box_trait::StringBox>() { Some(sb.value.clone()) } else {
if let Some(pb) = li.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>() {
if pb.box_type == "StringBox" {
let host = crate::runtime::get_global_plugin_host();
let tmp = if let Ok(ro) = host.read() {
if let Ok(val_opt) = ro.invoke_instance_method("StringBox", "toUtf8", pb.inner.instance_id, &[]) {
if let Some(vb) = val_opt { if let Some(sbb) = vb.as_any().downcast_ref::<crate::box_trait::StringBox>() { Some(sbb.value.clone()) } else { None } } else { None }
} else { None }
} else { None };
tmp
} else { None }
} else { None }
};
if let Some(ls) = ls_opt { return Ok(match op { CompareOp::Eq => ls == *rs, CompareOp::Ne => ls != *rs, CompareOp::Lt => ls < *rs, CompareOp::Le => ls <= *rs, CompareOp::Gt => ls > *rs, CompareOp::Ge => ls >= *rs }); }
Err(VMError::TypeError(format!("[BoxRef-String] Unsupported comparison: {:?} on {:?} and {:?}", op, left, right)))
}
// Mixed Integer (BoxRef vs Integer)
(VMValue::BoxRef(li), VMValue::Integer(r)) => {
let l_opt = li.as_any().downcast_ref::<crate::box_trait::IntegerBox>().map(|x| x.value)

View File

@ -57,9 +57,24 @@ impl NyashInterpreter {
} else if std::path::Path::new(&canonical_path).extension().is_none() {
canonical_path.push_str(".nyash");
}
// 循環検出(ロード中スタック)
{
let mut stack = self.shared.include_stack.lock().unwrap();
if let Some(pos) = stack.iter().position(|p| p == &canonical_path) {
// 検出: A -> ... -> B -> A
let mut chain: Vec<String> = stack[pos..].to_vec();
chain.push(canonical_path.clone());
let msg = format!("include cycle detected: {}",
chain.join(" -> "));
return Err(RuntimeError::InvalidOperation { message: msg });
}
stack.push(canonical_path.clone());
}
// 重複読み込みチェック
if self.shared.included_files.lock().unwrap().contains(&canonical_path) {
// スタックから外して早期終了
self.shared.include_stack.lock().unwrap().pop();
return Ok(()); // 既に読み込み済み
}
@ -76,10 +91,14 @@ impl NyashInterpreter {
})?;
// 重複防止リストに追加
self.shared.included_files.lock().unwrap().insert(canonical_path);
self.shared.included_files.lock().unwrap().insert(canonical_path.clone());
// 現在の環境で実行
self.execute(ast)?;
let exec_res = self.execute(ast);
// スタックを外す
self.shared.include_stack.lock().unwrap().pop();
// 実行結果を伝播
exec_res?;
Ok(())
}
@ -96,6 +115,18 @@ impl NyashInterpreter {
canonical_path.push_str(".nyash");
}
// 循環検出(ロード中スタック)
{
let mut stack = self.shared.include_stack.lock().unwrap();
if let Some(pos) = stack.iter().position(|p| p == &canonical_path) {
let mut chain: Vec<String> = stack[pos..].to_vec();
chain.push(canonical_path.clone());
let msg = format!("include cycle detected: {}", chain.join(" -> "));
return Err(RuntimeError::InvalidOperation { message: msg });
}
stack.push(canonical_path.clone());
}
// ファイル読み込みstatic box名検出用
let content = std::fs::read_to_string(&canonical_path)
.map_err(|e| RuntimeError::InvalidOperation {
@ -131,8 +162,14 @@ impl NyashInterpreter {
set.contains(&canonical_path)
};
if !already {
self.shared.included_files.lock().unwrap().insert(canonical_path);
self.execute(ast)?;
self.shared.included_files.lock().unwrap().insert(canonical_path.clone());
let exec_res = self.execute(ast);
// スタックを外す
self.shared.include_stack.lock().unwrap().pop();
exec_res?;
} else {
// スタックを外す(既に読み込み済みのため)
self.shared.include_stack.lock().unwrap().pop();
}
// static boxを初期化・取得して返す

View File

@ -20,6 +20,9 @@ pub struct SharedState {
/// 読み込み済みファイル(重複防止)
pub included_files: Arc<Mutex<HashSet<String>>>,
/// includeロード中スタック循環検出用: A -> B -> A を検出)
pub include_stack: Arc<Mutex<Vec<String>>>,
}
impl SharedState {
@ -37,6 +40,7 @@ impl SharedState {
static_functions: Arc::new(RwLock::new(HashMap::new())),
static_box_definitions: Arc::new(RwLock::new(HashMap::new())),
included_files: Arc::new(Mutex::new(HashSet::new())),
include_stack: Arc::new(Mutex::new(Vec::new())),
}
}
}

View File

@ -96,6 +96,11 @@ pub struct MirBuilder {
pub(super) value_types: HashMap<ValueId, super::MirType>,
/// Current static box name when lowering a static box body (e.g., "Main")
current_static_box: Option<String>,
/// Include guards: currently loading file canonical paths
include_loading: HashSet<String>,
/// Include visited cache: canonical path -> box name
include_box_map: HashMap<String, String>,
}
impl MirBuilder {
@ -128,6 +133,8 @@ impl MirBuilder {
field_origin_class: HashMap::new(),
value_types: HashMap::new(),
current_static_box: None,
include_loading: HashSet::new(),
include_box_map: HashMap::new(),
}
}
@ -507,6 +514,15 @@ impl MirBuilder {
} else if std::path::Path::new(&path).extension().is_none() {
path.push_str(".nyash");
}
// Cycle detection
if self.include_loading.contains(&path) {
return Err(format!("Circular include detected: {}", path));
}
// Cache hit: build only the instance
if let Some(name) = self.include_box_map.get(&path).cloned() {
return self.build_new_expression(name, vec![]);
}
self.include_loading.insert(path.clone());
let content = fs::read_to_string(&path)
.map_err(|e| format!("Include read error '{}': {}", filename, e))?;
// Parse to AST
@ -524,6 +540,9 @@ impl MirBuilder {
let bname = box_name.ok_or_else(|| format!("Include target '{}' has no static box", filename))?;
// Lower included AST into current MIR (register types/methods)
let _ = self.build_expression(included_ast)?;
// Mark caches
self.include_loading.remove(&path);
self.include_box_map.insert(path.clone(), bname.clone());
// Return a new instance of included box (no args)
self.build_new_expression(bname, vec![])
},

View File

@ -126,10 +126,12 @@ impl NyashRunner {
format!("./{}", filename)
}
fn walk(node: &ASTNode, runtime: &NyashRuntime) {
use std::collections::{HashSet, VecDeque};
fn walk_with_state(node: &ASTNode, runtime: &NyashRuntime, stack: &mut Vec<String>, visited: &mut HashSet<String>) {
match node {
ASTNode::Program { statements, .. } => { for st in statements { walk(st, runtime); } }
ASTNode::FunctionDeclaration { body, .. } => { for st in body { walk(st, runtime); } }
ASTNode::Program { statements, .. } => { for st in statements { walk_with_state(st, runtime, stack, visited); } }
ASTNode::FunctionDeclaration { body, .. } => { for st in body { walk_with_state(st, runtime, stack, visited); } }
ASTNode::Include { filename, .. } => {
let mut path = resolve_include_path(filename);
if std::path::Path::new(&path).is_dir() {
@ -137,49 +139,62 @@ impl NyashRunner {
} else if std::path::Path::new(&path).extension().is_none() {
path.push_str(".nyash");
}
// Cycle detection using stack
if let Some(pos) = stack.iter().position(|p| p == &path) {
let mut chain = stack[pos..].to_vec();
chain.push(path.clone());
eprintln!("include cycle detected (collector): {}", chain.join(" -> "));
return; // Skip to avoid infinite recursion
}
if visited.contains(&path) {
return; // Already processed
}
stack.push(path.clone());
if let Ok(content) = std::fs::read_to_string(&path) {
if let Ok(inc_ast) = NyashParser::parse_from_string(&content) {
walk(&inc_ast, runtime);
walk_with_state(&inc_ast, runtime, stack, visited);
visited.insert(path);
}
}
stack.pop();
}
ASTNode::Assignment { target, value, .. } => {
walk(target, runtime); walk(value, runtime);
walk_with_state(target, runtime, stack, visited); walk_with_state(value, runtime, stack, visited);
}
ASTNode::Return { value, .. } => { if let Some(v) = value { walk(v, runtime); } }
ASTNode::Print { expression, .. } => { walk(expression, runtime); }
ASTNode::Return { value, .. } => { if let Some(v) = value { walk_with_state(v, runtime, stack, visited); } }
ASTNode::Print { expression, .. } => { walk_with_state(expression, runtime, stack, visited); }
ASTNode::If { condition, then_body, else_body, .. } => {
walk(condition, runtime);
for st in then_body { walk(st, runtime); }
if let Some(eb) = else_body { for st in eb { walk(st, runtime); } }
walk_with_state(condition, runtime, stack, visited);
for st in then_body { walk_with_state(st, runtime, stack, visited); }
if let Some(eb) = else_body { for st in eb { walk_with_state(st, runtime, stack, visited); } }
}
ASTNode::Loop { condition, body, .. } => {
walk(condition, runtime); for st in body { walk(st, runtime); }
walk_with_state(condition, runtime, stack, visited); for st in body { walk_with_state(st, runtime, stack, visited); }
}
ASTNode::TryCatch { try_body, catch_clauses, finally_body, .. } => {
for st in try_body { walk(st, runtime); }
for cc in catch_clauses { for st in &cc.body { walk(st, runtime); } }
if let Some(fb) = finally_body { for st in fb { walk(st, runtime); } }
for st in try_body { walk_with_state(st, runtime, stack, visited); }
for cc in catch_clauses { for st in &cc.body { walk_with_state(st, runtime, stack, visited); } }
if let Some(fb) = finally_body { for st in fb { walk_with_state(st, runtime, stack, visited); } }
}
ASTNode::Throw { expression, .. } => { walk(expression, runtime); }
ASTNode::Throw { expression, .. } => { walk_with_state(expression, runtime, stack, visited); }
ASTNode::Local { initial_values, .. } => {
for iv in initial_values { if let Some(v) = iv { walk(v, runtime); } }
for iv in initial_values { if let Some(v) = iv { walk_with_state(v, runtime, stack, visited); } }
}
ASTNode::Outbox { initial_values, .. } => {
for iv in initial_values { if let Some(v) = iv { walk(v, runtime); } }
for iv in initial_values { if let Some(v) = iv { walk_with_state(v, runtime, stack, visited); } }
}
ASTNode::FunctionCall { arguments, .. } => { for a in arguments { walk(a, runtime); } }
ASTNode::MethodCall { object, arguments, .. } => { walk(object, runtime); for a in arguments { walk(a, runtime); } }
ASTNode::FieldAccess { object, .. } => { walk(object, runtime); }
ASTNode::New { arguments, .. } => { for a in arguments { walk(a, runtime); } }
ASTNode::BinaryOp { left, right, .. } => { walk(left, runtime); walk(right, runtime); }
ASTNode::UnaryOp { operand, .. } => { walk(operand, runtime); }
ASTNode::AwaitExpression { expression, .. } => { walk(expression, runtime); }
ASTNode::Arrow { sender, receiver, .. } => { walk(sender, runtime); walk(receiver, runtime); }
ASTNode::Nowait { expression, .. } => { walk(expression, runtime); }
ASTNode::FunctionCall { arguments, .. } => { for a in arguments { walk_with_state(a, runtime, stack, visited); } }
ASTNode::MethodCall { object, arguments, .. } => { walk_with_state(object, runtime, stack, visited); for a in arguments { walk_with_state(a, runtime, stack, visited); } }
ASTNode::FieldAccess { object, .. } => { walk_with_state(object, runtime, stack, visited); }
ASTNode::New { arguments, .. } => { for a in arguments { walk_with_state(a, runtime, stack, visited); } }
ASTNode::BinaryOp { left, right, .. } => { walk_with_state(left, runtime, stack, visited); walk_with_state(right, runtime, stack, visited); }
ASTNode::UnaryOp { operand, .. } => { walk_with_state(operand, runtime, stack, visited); }
ASTNode::AwaitExpression { expression, .. } => { walk_with_state(expression, runtime, stack, visited); }
ASTNode::Arrow { sender, receiver, .. } => { walk_with_state(sender, runtime, stack, visited); walk_with_state(receiver, runtime, stack, visited); }
ASTNode::Nowait { expression, .. } => { walk_with_state(expression, runtime, stack, visited); }
ASTNode::BoxDeclaration { name, fields, public_fields, private_fields, methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, .. } => {
for (_mname, mnode) in methods { walk(mnode, runtime); }
for (_ckey, cnode) in constructors { walk(cnode, runtime); }
for (_mname, mnode) in methods { walk_with_state(mnode, runtime, stack, visited); }
for (_ckey, cnode) in constructors { walk_with_state(cnode, runtime, stack, visited); }
let decl = CoreBoxDecl {
name: name.clone(),
fields: fields.clone(),
@ -199,6 +214,8 @@ impl NyashRunner {
_ => {}
}
}
walk(ast, runtime);
let mut stack: Vec<String> = Vec::new();
let mut visited: HashSet<String> = HashSet::new();
walk_with_state(ast, runtime, &mut stack, &mut visited);
}
}