feat: Implement field visibility (public/private) system
## Major Features Added
### Field Visibility System
- Added `private { ... }` and `public { ... }` blocks in box declarations
- Default visibility is now handled explicitly (fields must be in either block)
- Visibility checks enforced at both interpreter and VM levels
### Parser Enhancements
- Extended AST with public_fields and private_fields vectors
- Added parsing for visibility blocks in box definitions
- Maintained backward compatibility with existing `init { ... }` syntax
### Interpreter Implementation
- Added visibility checks in field access (get_field/set_field)
- External access to private fields now throws appropriate errors
- Internal access (within methods) always allowed
### VM Implementation
- Extended VM with object_class tracking for visibility checks
- RefGet/RefSet instructions now enforce field visibility
- Fixed nested box declaration collection (boxes defined inside methods)
### Test Examples Added
- docs/examples/visibility_ok.nyash - demonstrates correct usage
- docs/examples/visibility_error.nyash - tests private field access errors
## Technical Details
### Error Messages
- Interpreter: "Field 'X' is private in Y"
- VM: Same error message for consistency
### Current Limitations
- All RefGet/RefSet treated as external access in VM (internal flag future work)
- Legacy `init { ... }` fields treated as having unspecified visibility
## Test Results
✅ Interpreter: Both test cases pass correctly
✅ VM: Both test cases pass correctly after nested declaration fix
This implements the foundation for proper encapsulation in Nyash,
following the "explicit is better than implicit" philosophy.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -949,203 +949,16 @@ impl NyashInterpreter {
|
||||
arguments: &[ASTNode],
|
||||
) -> Result<Box<dyn NyashBox>, RuntimeError> {
|
||||
eprintln!("🔍 execute_plugin_box_v2_method called: {}.{}", plugin_box.box_type, method);
|
||||
// Route via loader for proper TLV/Handle handling (early return)
|
||||
{
|
||||
let mut arg_values: Vec<Box<dyn NyashBox>> = Vec::new();
|
||||
for arg in arguments {
|
||||
arg_values.push(self.execute_expression(arg)?);
|
||||
}
|
||||
let loader_guard = crate::runtime::plugin_loader_v2::get_global_loader_v2();
|
||||
let loader = loader_guard.read().map_err(|_| RuntimeError::RuntimeFailure { message: "Plugin loader lock poisoned".into() })?;
|
||||
match loader.invoke_instance_method(&plugin_box.box_type, method, plugin_box.instance_id, &arg_values) {
|
||||
Ok(Some(result_box)) => return Ok(result_box),
|
||||
Ok(None) => return Ok(Box::new(VoidBox::new())),
|
||||
Err(e) => return Err(RuntimeError::RuntimeFailure { message: format!("Plugin method {} failed: {:?}", method, e) }),
|
||||
}
|
||||
}
|
||||
|
||||
// Get global loader to access configuration
|
||||
let loader = crate::runtime::plugin_loader_v2::get_global_loader_v2();
|
||||
let loader = loader.read().unwrap();
|
||||
|
||||
// Get method_id from configuration
|
||||
let method_id = if let Some(config) = &loader.config {
|
||||
// Find library that provides this box type
|
||||
let (lib_name, _) = config.find_library_for_box(&plugin_box.box_type)
|
||||
.ok_or_else(|| RuntimeError::InvalidOperation {
|
||||
message: format!("No plugin provides box type: {}", plugin_box.box_type)
|
||||
})?;
|
||||
|
||||
// Get method_id from toml
|
||||
if let Ok(toml_content) = std::fs::read_to_string("nyash.toml") {
|
||||
if let Ok(toml_value) = toml::from_str::<toml::Value>(&toml_content) {
|
||||
if let Some(box_config) = config.get_box_config(lib_name, &plugin_box.box_type, &toml_value) {
|
||||
if let Some(method_config) = box_config.methods.get(method) {
|
||||
eprintln!("🔍 Found method {} with id: {}", method, method_config.method_id);
|
||||
method_config.method_id
|
||||
} else {
|
||||
return Err(RuntimeError::InvalidOperation {
|
||||
message: format!("Unknown method '{}' for {}", method, plugin_box.box_type)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return Err(RuntimeError::InvalidOperation {
|
||||
message: format!("No configuration for box type: {}", plugin_box.box_type)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return Err(RuntimeError::InvalidOperation {
|
||||
message: "Failed to parse nyash.toml".into()
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return Err(RuntimeError::InvalidOperation {
|
||||
message: "Failed to read nyash.toml".into()
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return Err(RuntimeError::InvalidOperation {
|
||||
message: "No configuration loaded".into()
|
||||
});
|
||||
};
|
||||
|
||||
// Evaluate arguments
|
||||
let mut arg_values = Vec::new();
|
||||
let mut arg_values: Vec<Box<dyn NyashBox>> = Vec::new();
|
||||
for arg in arguments {
|
||||
arg_values.push(self.execute_expression(arg)?);
|
||||
}
|
||||
|
||||
// Encode arguments using TLV (plugin's expected format)
|
||||
let mut tlv_data = Vec::new();
|
||||
|
||||
// Header: version(2 bytes) + argc(2 bytes)
|
||||
tlv_data.extend_from_slice(&1u16.to_le_bytes()); // version = 1
|
||||
tlv_data.extend_from_slice(&(arg_values.len() as u16).to_le_bytes()); // argc
|
||||
|
||||
// Encode each argument
|
||||
for arg in arg_values.iter() {
|
||||
// For now, convert all arguments to strings
|
||||
let arg_str = arg.to_string_box().value;
|
||||
let arg_bytes = arg_str.as_bytes();
|
||||
|
||||
// TLV entry: tag(1) + reserved(1) + size(2) + data
|
||||
tlv_data.push(6); // tag = 6 (String)
|
||||
tlv_data.push(0); // reserved
|
||||
tlv_data.extend_from_slice(&(arg_bytes.len() as u16).to_le_bytes()); // size
|
||||
tlv_data.extend_from_slice(arg_bytes); // data
|
||||
}
|
||||
|
||||
// Prepare output buffer
|
||||
let mut output_buffer = vec![0u8; 4096]; // 4KB buffer
|
||||
let mut output_len = output_buffer.len();
|
||||
|
||||
eprintln!("🔍 Calling plugin invoke_fn: type_id={}, method_id={}, instance_id={}",
|
||||
plugin_box.type_id, method_id, plugin_box.instance_id);
|
||||
|
||||
// Call plugin method
|
||||
let result = unsafe {
|
||||
(plugin_box.invoke_fn)(
|
||||
plugin_box.type_id, // type_id from PluginBoxV2
|
||||
method_id, // method_id
|
||||
plugin_box.instance_id, // instance_id
|
||||
tlv_data.as_ptr(), // arguments
|
||||
tlv_data.len(), // arguments length
|
||||
output_buffer.as_mut_ptr(), // output buffer
|
||||
&mut output_len, // output length
|
||||
)
|
||||
};
|
||||
|
||||
eprintln!("🔍 Plugin method returned: {}", result);
|
||||
|
||||
if result != 0 {
|
||||
return Err(RuntimeError::RuntimeFailure {
|
||||
message: format!("Plugin method {} failed with code: {}", method, result)
|
||||
});
|
||||
}
|
||||
|
||||
// Parse TLV output dynamically
|
||||
if output_len >= 4 {
|
||||
// Parse TLV header
|
||||
let version = u16::from_le_bytes([output_buffer[0], output_buffer[1]]);
|
||||
let argc = u16::from_le_bytes([output_buffer[2], output_buffer[3]]);
|
||||
|
||||
eprintln!("🔍 TLV response: version={}, argc={}", version, argc);
|
||||
|
||||
if version == 1 && argc > 0 && output_len >= 8 {
|
||||
// Parse first TLV entry
|
||||
let tag = output_buffer[4];
|
||||
let _reserved = output_buffer[5];
|
||||
let size = u16::from_le_bytes([output_buffer[6], output_buffer[7]]) as usize;
|
||||
|
||||
eprintln!("🔍 TLV entry: tag={}, size={}", tag, size);
|
||||
|
||||
if output_len >= 8 + size {
|
||||
match tag {
|
||||
2 => {
|
||||
// I32 type
|
||||
if size == 4 {
|
||||
let value = i32::from_le_bytes([
|
||||
output_buffer[8], output_buffer[9],
|
||||
output_buffer[10], output_buffer[11]
|
||||
]);
|
||||
Ok(Box::new(IntegerBox::new(value as i64)))
|
||||
} else {
|
||||
Ok(Box::new(StringBox::new("ok")))
|
||||
}
|
||||
}
|
||||
6 | 7 => {
|
||||
// String or Bytes type
|
||||
let data = &output_buffer[8..8+size];
|
||||
let string = String::from_utf8_lossy(data).to_string();
|
||||
Ok(Box::new(StringBox::new(string)))
|
||||
}
|
||||
8 => {
|
||||
// Handle type - contains type_id and instance_id
|
||||
if size == 8 {
|
||||
let type_id = u32::from_le_bytes([
|
||||
output_buffer[8], output_buffer[9],
|
||||
output_buffer[10], output_buffer[11]
|
||||
]);
|
||||
let instance_id = u32::from_le_bytes([
|
||||
output_buffer[12], output_buffer[13],
|
||||
output_buffer[14], output_buffer[15]
|
||||
]);
|
||||
eprintln!("🔍 Received Handle: type_id={}, instance_id={}", type_id, instance_id);
|
||||
|
||||
// Create a new PluginBoxV2 instance with the returned handle
|
||||
let new_plugin_box = PluginBoxV2 {
|
||||
box_type: plugin_box.box_type.clone(),
|
||||
type_id: plugin_box.type_id,
|
||||
invoke_fn: plugin_box.invoke_fn,
|
||||
instance_id: instance_id,
|
||||
fini_method_id: plugin_box.fini_method_id,
|
||||
};
|
||||
Ok(Box::new(new_plugin_box))
|
||||
} else {
|
||||
eprintln!("🔍 Invalid Handle size: {} (expected 8)", size);
|
||||
Ok(Box::new(VoidBox::new()))
|
||||
}
|
||||
}
|
||||
9 => {
|
||||
// Void type
|
||||
Ok(Box::new(StringBox::new("ok")))
|
||||
}
|
||||
_ => {
|
||||
// Unknown type, treat as string
|
||||
eprintln!("🔍 Unknown TLV tag: {}", tag);
|
||||
Ok(Box::new(StringBox::new("ok")))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(Box::new(StringBox::new("ok")))
|
||||
}
|
||||
} else {
|
||||
// No output, return void
|
||||
Ok(Box::new(VoidBox::new()))
|
||||
}
|
||||
} else {
|
||||
// No output, return void
|
||||
Ok(Box::new(VoidBox::new()))
|
||||
let loader_guard = crate::runtime::plugin_loader_v2::get_global_loader_v2();
|
||||
let loader = loader_guard.read().map_err(|_| RuntimeError::RuntimeFailure { message: "Plugin loader lock poisoned".into() })?;
|
||||
match loader.invoke_instance_method(&plugin_box.box_type, method, plugin_box.instance_id, &arg_values) {
|
||||
Ok(Some(result_box)) => Ok(result_box),
|
||||
Ok(None) => Ok(Box::new(VoidBox::new())),
|
||||
Err(e) => Err(RuntimeError::RuntimeFailure { message: format!("Plugin method {} failed: {:?}", method, e) }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user