docs: Add field visibility analysis and MIR BoxCall documentation

## Field Visibility Analysis Results
- Confirmed init{} fields are **public** in current Nyash implementation
- No access modifier (private/public) system currently implemented
- All fields accessible via me.fieldname syntax
- Documented findings for future reference

## MIR Documentation Enhancements
- Created comprehensive mir-dumper-guide.md for reading MIR dumps
- Enhanced mir-26-specification.md with BoxCall vs regular Call examples
- Added clear identification patterns:
  * BoxCall: `call %value.method(args)` (plugins/builtins)
  * Regular Call: `call %func(%me, args)` (user-defined boxes)

## VM Backend BoxRef Handling Improvements
- Fixed BoxRef method dispatch using share_box() instead of clone_box()
- Prevents unintended constructor calls during method resolution
- Maintains proper instance identity throughout VM execution

## MIR Builder User-Defined Box Tracking
- Added user_defined_boxes HashSet to track declared user boxes
- Improved method lowering decisions for user-defined vs builtin boxes
- Enhanced AST→MIR conversion accuracy for method calls

## Plugin Tester Lifecycle Enhancements
- Added comprehensive FileBox lifecycle testing (open/write/close)
- Enhanced cloneSelf() and copyFrom() testing with proper Handle parsing
- Added TLV encoding helpers for strings and bytes
- Improved error reporting and step-by-step validation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Moe Charm
2025-08-21 01:18:25 +09:00
parent cc2a820af7
commit 2200312f09
5 changed files with 183 additions and 49 deletions

View File

@ -87,6 +87,21 @@ This document specifies the official 26-instruction set for Nyash MIR (Machine I
%dst = call %box.method(%arg1, %arg2, ...) %dst = call %box.method(%arg1, %arg2, ...)
``` ```
Effect: context-dependent Effect: context-dependent
**重要な識別方法:**
- BoxCall形式: `call %値.メソッド名(引数)` - 値(%7などに対してメソッドを直接呼ぶ
- 通常のCall形式: `call %関数値(%me, 引数)` - 関数値を呼び、第1引数にmeを渡す
例:
```mir
; BoxCallプラグイン/ビルトインBoxのメソッド
%17 = call %7.open(%14, %16)
%22 = call %7.write(%21)
; 通常のCallユーザー定義Boxのメソッド
%func = const "UserBox.method/2"
%result = call %func(%me, %arg1)
```
13. **ExternCall** - Call external function 13. **ExternCall** - Call external function
```mir ```mir

View File

@ -0,0 +1,59 @@
# MIR Dumper Output Guide
MIRダンプ出力を正しく読み解くためのガイドです。
## BoxCall vs 通常のCall の見分け方
### BoxCall形式プラグイン/ビルトインBoxのメソッド
```mir
%8 = call %7.cloneSelf()
%17 = call %7.open(%14, %16)
%22 = call %7.write(%21)
%23 = call %8.copyFrom(%7)
```
**特徴:**
- `call %値.メソッド名(引数)` の形式
- 値(%7, %8などに対して直接メソッドを呼ぶ
- プラグインBoxやビルトインBoxで使用される
### 通常のCall形式ユーザー定義Boxのメソッド
```mir
%func = const "UserBox.calculate/2"
%result = call %func(%me, %arg1)
```
**特徴:**
- 事前に `const "クラス名.メソッド名/引数数"` で関数値を取得
- `call %関数値(%me, 引数...)` の形式で呼び出し
- 第1引数は常に `%me`self相当
## 実例での比較
### plugin_boxref_return.nyashのMIRダンプ
```mir
11: %7 = new FileBox()
12: call %7.birth()
13: %8 = call %7.cloneSelf() ← BoxCallプラグインメソッド
26: %17 = call %7.open(%14, %16) ← BoxCallプラグインメソッド
33: %22 = call %7.write(%21) ← BoxCallプラグインメソッド
34: %23 = call %8.copyFrom(%7) ← BoxCallプラグインメソッド
```
これらはすべてBoxCall形式で、プラグインのFileBoxメソッドを直接呼び出しています。
### ユーザー定義Boxの場合仮想例
```mir
; ユーザー定義Box "Calculator" のメソッド呼び出し
%calc_func = const "Calculator.add/2"
%result = call %calc_func(%me, %10, %20)
```
この場合は、MIR関数として事前にlower済みのメソッドを呼び出しています。
## まとめ
- **`call %値.メソッド()`** → BoxCallプラグイン/ビルトイン)
- **`call %関数値(%me, ...)`** → 通常のCallユーザー定義Box
MIRダンプを見る際は、この形式の違いに注目することで、どのタイプのメソッド呼び出しかを判断できます。

View File

@ -90,8 +90,8 @@ impl VMValue {
VMValue::String(s) => Box::new(StringBox::new(s)), VMValue::String(s) => Box::new(StringBox::new(s)),
VMValue::Future(f) => Box::new(f.clone()), VMValue::Future(f) => Box::new(f.clone()),
VMValue::Void => Box::new(VoidBox::new()), VMValue::Void => Box::new(VoidBox::new()),
// Phase 9.78a: BoxRef returns cloned Box // BoxRef returns a shared handle (do NOT birth a new instance)
VMValue::BoxRef(arc_box) => arc_box.clone_box(), VMValue::BoxRef(arc_box) => arc_box.share_box(),
} }
} }
@ -504,7 +504,8 @@ impl VM {
// Handle BoxRef for proper method dispatch // Handle BoxRef for proper method dispatch
let box_nyash = match &box_vm_value { let box_nyash = match &box_vm_value {
VMValue::BoxRef(arc_box) => arc_box.clone_box(), // Use shared handle to avoid unintended constructor calls
VMValue::BoxRef(arc_box) => arc_box.share_box(),
_ => box_vm_value.to_nyash_box(), _ => box_vm_value.to_nyash_box(),
}; };
@ -607,8 +608,8 @@ impl VM {
}; };
match created { match created {
Ok(b) => { Ok(b) => {
// Register for scope-based finalization (clone for registration) // Register for scope-based finalization (share; keep same instance)
let reg_arc = std::sync::Arc::from(b.clone_box()); let reg_arc = std::sync::Arc::from(b.share_box());
self.scope_tracker.register_box(reg_arc); self.scope_tracker.register_box(reg_arc);
// Store value in VM // Store value in VM
self.set_value(*dst, VMValue::from_nyash_box(b)); self.set_value(*dst, VMValue::from_nyash_box(b));

View File

@ -11,6 +11,7 @@ use super::{
}; };
use crate::ast::{ASTNode, LiteralValue, BinaryOperator}; use crate::ast::{ASTNode, LiteralValue, BinaryOperator};
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::HashSet;
/// MIR builder for converting AST to SSA form /// MIR builder for converting AST to SSA form
pub struct MirBuilder { pub struct MirBuilder {
@ -39,6 +40,9 @@ pub struct MirBuilder {
/// Origin tracking for simple optimizations (e.g., object.method after new) /// Origin tracking for simple optimizations (e.g., object.method after new)
/// Maps a ValueId to the class name if it was produced by NewBox of that class /// Maps a ValueId to the class name if it was produced by NewBox of that class
pub(super) value_origin_newbox: HashMap<ValueId, String>, pub(super) value_origin_newbox: HashMap<ValueId, String>,
/// Names of user-defined boxes declared in the current module
pub(super) user_defined_boxes: HashSet<String>,
} }
impl MirBuilder { impl MirBuilder {
@ -53,6 +57,7 @@ impl MirBuilder {
variable_map: HashMap::new(), variable_map: HashMap::new(),
pending_phis: Vec::new(), pending_phis: Vec::new(),
value_origin_newbox: HashMap::new(), value_origin_newbox: HashMap::new(),
user_defined_boxes: HashSet::new(),
} }
} }
@ -299,6 +304,8 @@ impl MirBuilder {
self.build_static_main_box(methods.clone()) self.build_static_main_box(methods.clone())
} else { } else {
// Support user-defined boxes - handle as statement, return void // Support user-defined boxes - handle as statement, return void
// Track as user-defined (eligible for method lowering)
self.user_defined_boxes.insert(name.clone());
self.build_box_declaration(name.clone(), methods.clone(), fields.clone())?; self.build_box_declaration(name.clone(), methods.clone(), fields.clone())?;
// Phase 2: Lower constructors (birth/N) into MIR functions // Phase 2: Lower constructors (birth/N) into MIR functions
@ -993,7 +1000,8 @@ impl MirBuilder {
if let ASTNode::New { class, .. } = object { if let ASTNode::New { class, .. } = object {
// Build function name and only lower to Call if the function exists (user-defined) // Build function name and only lower to Call if the function exists (user-defined)
let func_name = format!("{}.{}{}", class, method, format!("/{}", arg_values.len())); let func_name = format!("{}.{}{}", class, method, format!("/{}", arg_values.len()));
let can_lower = if let Some(ref module) = self.current_module { module.functions.contains_key(&func_name) } else { false }; let can_lower = self.user_defined_boxes.contains(&class)
&& if let Some(ref module) = self.current_module { module.functions.contains_key(&func_name) } else { false };
if can_lower { if can_lower {
let func_val = self.value_gen.next(); let func_val = self.value_gen.next();
self.emit_instruction(MirInstruction::Const { dst: func_val, value: ConstValue::String(func_name) })?; self.emit_instruction(MirInstruction::Const { dst: func_val, value: ConstValue::String(func_name) })?;
@ -1013,7 +1021,8 @@ impl MirBuilder {
// If the object originates from a NewBox in this function, we can lower to Call as well // If the object originates from a NewBox in this function, we can lower to Call as well
if let Some(class_name) = self.value_origin_newbox.get(&object_value).cloned() { if let Some(class_name) = self.value_origin_newbox.get(&object_value).cloned() {
let func_name = format!("{}.{}{}", class_name, method, format!("/{}", arg_values.len())); let func_name = format!("{}.{}{}", class_name, method, format!("/{}", arg_values.len()));
let can_lower = if let Some(ref module) = self.current_module { module.functions.contains_key(&func_name) } else { false }; let can_lower = self.user_defined_boxes.contains(&class_name)
&& if let Some(ref module) = self.current_module { module.functions.contains_key(&func_name) } else { false };
if can_lower { if can_lower {
let func_val = self.value_gen.next(); let func_val = self.value_gen.next();
self.emit_instruction(MirInstruction::Const { dst: func_val, value: ConstValue::String(func_name) })?; self.emit_instruction(MirInstruction::Const { dst: func_val, value: ConstValue::String(func_name) })?;

View File

@ -100,6 +100,33 @@ fn tlv_encode_one_handle(type_id: u32, instance_id: u32) -> Vec<u8> {
buf buf
} }
fn tlv_encode_two_strings(a: &str, b: &str) -> Vec<u8> {
let ab = a.as_bytes();
let bb = b.as_bytes();
let mut buf = Vec::with_capacity(4 + 2 * (4 + ab.len().min(u16::MAX as usize)));
buf.extend_from_slice(&1u16.to_le_bytes()); // ver
buf.extend_from_slice(&2u16.to_le_bytes()); // argc=2
// first string
buf.push(6u8); buf.push(0u8);
buf.extend_from_slice(&((ab.len().min(u16::MAX as usize) as u16).to_le_bytes()));
buf.extend_from_slice(ab);
// second string
buf.push(6u8); buf.push(0u8);
buf.extend_from_slice(&((bb.len().min(u16::MAX as usize) as u16).to_le_bytes()));
buf.extend_from_slice(bb);
buf
}
fn tlv_encode_bytes(data: &[u8]) -> Vec<u8> {
let mut buf = Vec::with_capacity(4 + 4 + data.len());
buf.extend_from_slice(&1u16.to_le_bytes()); // ver
buf.extend_from_slice(&1u16.to_le_bytes()); // argc=1
buf.push(7u8); buf.push(0u8);
buf.extend_from_slice(&((data.len().min(u16::MAX as usize) as u16).to_le_bytes()));
buf.extend_from_slice(data);
buf
}
fn tlv_decode_u32(data: &[u8]) -> Result<u32, String> { fn tlv_decode_u32(data: &[u8]) -> Result<u32, String> {
if data.len() >= 4 { if data.len() >= 4 {
Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])) Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]]))
@ -330,50 +357,73 @@ fn test_lifecycle_v2(config_path: &PathBuf, box_type: &str) {
println!("{}: Birth successful, instance_id = {}", "".green(), instance_id); println!("{}: Birth successful, instance_id = {}", "".green(), instance_id);
// Optional: If method 'copyFrom' exists, create another instance and pass it as Box arg // 1b. Open the first instance (if open exists)
if box_config.methods.contains_key("copyFrom") { if let Some(open_def) = box_config.methods.get("open") {
println!("\n{}", "1b. Testing method with Box arg: copyFrom(other) ...".cyan()); println!("\n{}", "1b. Opening src FileBox (id=instance_id) ...".cyan());
let args_open = tlv_encode_two_strings("test_lifecycle.txt", "w");
let mut out = vec![0u8; 1024];
let mut out_len = out.len();
let rc = invoke_fn(box_config.type_id, open_def.method_id, instance_id, args_open.as_ptr(), args_open.len(), out.as_mut_ptr(), &mut out_len);
if rc == 0 { println!("{}: open ok", "".green()); } else { eprintln!("{}: open rc={}", "WARN".yellow(), rc); }
}
// Birth another instance to serve as argument handle // 1c. Write some bytes (if write exists)
let args2 = tlv_encode_empty(); if let Some(write_def) = box_config.methods.get("write") {
let mut out2 = vec![0u8; 1024]; println!("\n{}", "1c. Writing to src FileBox ...".cyan());
let mut out2_len = out2.len(); let args_write = tlv_encode_bytes(b"hello nyash");
let rc2 = invoke_fn( let mut out = vec![0u8; 1024];
box_config.type_id, let mut out_len = out.len();
0, let rc = invoke_fn(box_config.type_id, write_def.method_id, instance_id, args_write.as_ptr(), args_write.len(), out.as_mut_ptr(), &mut out_len);
0, if rc == 0 { println!("{}: write ok", "".green()); } else { eprintln!("{}: write rc={}", "WARN".yellow(), rc); }
args2.as_ptr(), }
args2.len(),
out2.as_mut_ptr(), // 1d. Create destination instance via cloneSelf() if available; else birth
&mut out2_len, let mut dst_id = None;
); if let Some(clone_def) = box_config.methods.get("cloneSelf") {
if rc2 == 0 { println!("\n{}", "1d. Cloning via cloneSelf() ...".cyan());
if let Ok(other_id) = tlv_decode_u32(&out2[..out2_len]) { let args0 = tlv_encode_empty();
// Encode one Box handle as argument let mut out = vec![0u8; 1024];
let arg_buf = tlv_encode_one_handle(box_config.type_id, other_id); let mut out_len = out.len();
let mut ret = vec![0u8; 1024]; let rc = invoke_fn(box_config.type_id, clone_def.method_id, instance_id, args0.as_ptr(), args0.len(), out.as_mut_ptr(), &mut out_len);
let mut ret_len = ret.len(); if rc == 0 && out_len >= 16 && out[4] == 8 { // Handle
let method_id = box_config.methods.get("copyFrom").unwrap().method_id; // parse handle payload at bytes 8..16
let rc_call = invoke_fn( let t = u32::from_le_bytes([out[8],out[9],out[10],out[11]]);
box_config.type_id, let i = u32::from_le_bytes([out[12],out[13],out[14],out[15]]);
method_id, if t == box_config.type_id { dst_id = Some(i); println!("{}: cloneSelf returned id={}", "".green(), i); }
instance_id, } else { eprintln!("{}: cloneSelf rc={}", "WARN".yellow(), rc); }
arg_buf.as_ptr(), }
arg_buf.len(), if dst_id.is_none() {
ret.as_mut_ptr(), println!("\n{}", "1d. Cloning fallback via birth() ...".cyan());
&mut ret_len, let args0 = tlv_encode_empty();
); let mut out = vec![0u8; 1024];
if rc_call == 0 { let mut out_len = out.len();
println!("{}: copyFrom call succeeded (arg=BoxRef)", "".green()); let rc = invoke_fn(box_config.type_id, 0, 0, args0.as_ptr(), args0.len(), out.as_mut_ptr(), &mut out_len);
} else { if rc == 0 { dst_id = tlv_decode_u32(&out[..out_len]).ok(); }
eprintln!("{}: copyFrom call failed (rc={})", "WARN".yellow(), rc_call); if let Some(i) = dst_id { println!("{}: birth dst id={}", "".green(), i); } else { eprintln!("{}: birth dst failed rc={}", "WARN".yellow(), rc); }
} }
} else {
eprintln!("{}: Failed to decode other instance_id", "WARN".yellow()); // 1e. copyFrom(dst <- src)
} if let (Some(copy_def), Some(dst)) = (box_config.methods.get("copyFrom"), dst_id) {
} else { println!("\n{}", "1e. Testing copyFrom(dst <- src) ...".cyan());
eprintln!("{}: Failed to create other instance for copyFrom (rc={})", "WARN".yellow(), rc2); let arg_buf = tlv_encode_one_handle(box_config.type_id, instance_id);
let mut out = vec![0u8; 1024];
let mut out_len = out.len();
let rc = invoke_fn(box_config.type_id, copy_def.method_id, dst, arg_buf.as_ptr(), arg_buf.len(), out.as_mut_ptr(), &mut out_len);
if rc == 0 { println!("{}: copyFrom ok", "".green()); } else { eprintln!("{}: copyFrom rc={}", "WARN".yellow(), rc); }
}
// 1f. close both
if let Some(close_def) = box_config.methods.get("close") {
println!("\n{}", "1f. Closing both instances ...".cyan());
let args0 = tlv_encode_empty();
let mut out = vec![0u8; 64];
let mut out_len = out.len();
let _ = invoke_fn(box_config.type_id, close_def.method_id, instance_id, args0.as_ptr(), args0.len(), out.as_mut_ptr(), &mut out_len);
if let Some(dst) = dst_id {
out_len = out.len();
let _ = invoke_fn(box_config.type_id, close_def.method_id, dst, args0.as_ptr(), args0.len(), out.as_mut_ptr(), &mut out_len);
} }
println!("{}: close done", "".green());
} }
// Optional: If method 'cloneSelf' exists, call it and verify Handle return // Optional: If method 'cloneSelf' exists, call it and verify Handle return