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:
@ -173,6 +173,8 @@ pub struct VM {
|
||||
last_result: Option<VMValue>,
|
||||
/// Simple field storage for objects (maps reference -> field -> value)
|
||||
object_fields: HashMap<ValueId, HashMap<String, VMValue>>,
|
||||
/// Class name mapping for objects (for visibility checks)
|
||||
object_class: HashMap<ValueId, String>,
|
||||
/// Loop executor for handling phi nodes and loop-specific logic
|
||||
loop_executor: LoopExecutor,
|
||||
/// Shared runtime for box creation and declarations
|
||||
@ -205,6 +207,7 @@ impl VM {
|
||||
pc: 0,
|
||||
last_result: None,
|
||||
object_fields: HashMap::new(),
|
||||
object_class: HashMap::new(),
|
||||
loop_executor: LoopExecutor::new(),
|
||||
runtime: NyashRuntime::new(),
|
||||
scope_tracker: ScopeTracker::new(),
|
||||
@ -228,6 +231,7 @@ impl VM {
|
||||
pc: 0,
|
||||
last_result: None,
|
||||
object_fields: HashMap::new(),
|
||||
object_class: HashMap::new(),
|
||||
loop_executor: LoopExecutor::new(),
|
||||
runtime,
|
||||
scope_tracker: ScopeTracker::new(),
|
||||
@ -611,6 +615,8 @@ impl VM {
|
||||
// Register for scope-based finalization (share; keep same instance)
|
||||
let reg_arc = std::sync::Arc::from(b.share_box());
|
||||
self.scope_tracker.register_box(reg_arc);
|
||||
// Record class name for visibility checks
|
||||
self.object_class.insert(*dst, box_type.clone());
|
||||
// Store value in VM
|
||||
self.set_value(*dst, VMValue::from_nyash_box(b));
|
||||
Ok(ControlFlow::Continue)
|
||||
@ -696,6 +702,17 @@ impl VM {
|
||||
},
|
||||
|
||||
MirInstruction::RefGet { dst, reference, field } => {
|
||||
// Visibility check (if class known and visibility declared)
|
||||
if let Some(class_name) = self.object_class.get(reference) {
|
||||
if let Ok(decls) = self.runtime.box_declarations.read() {
|
||||
if let Some(decl) = decls.get(class_name) {
|
||||
let has_vis = !decl.public_fields.is_empty() || !decl.private_fields.is_empty();
|
||||
if has_vis && !decl.public_fields.contains(field) {
|
||||
return Err(VMError::TypeError(format!("Field '{}' is private in {}", field, class_name)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Get field value from object
|
||||
let field_value = if let Some(fields) = self.object_fields.get(reference) {
|
||||
if let Some(value) = fields.get(field) {
|
||||
@ -716,6 +733,17 @@ impl VM {
|
||||
MirInstruction::RefSet { reference, field, value } => {
|
||||
// Get the value to set
|
||||
let new_value = self.get_value(*value)?;
|
||||
// Visibility check (treat all RefSet as external writes)
|
||||
if let Some(class_name) = self.object_class.get(reference) {
|
||||
if let Ok(decls) = self.runtime.box_declarations.read() {
|
||||
if let Some(decl) = decls.get(class_name) {
|
||||
let has_vis = !decl.public_fields.is_empty() || !decl.private_fields.is_empty();
|
||||
if has_vis && !decl.public_fields.contains(field) {
|
||||
return Err(VMError::TypeError(format!("Field '{}' is private in {}", field, class_name)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure object has field storage
|
||||
if !self.object_fields.contains_key(reference) {
|
||||
@ -1127,10 +1155,12 @@ mod tests {
|
||||
crate::ast::ASTNode::Program { statements, .. } => {
|
||||
for st in statements { walk(st, runtime); }
|
||||
}
|
||||
crate::ast::ASTNode::BoxDeclaration { name, fields, methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, .. } => {
|
||||
crate::ast::ASTNode::BoxDeclaration { name, fields, public_fields, private_fields, methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, .. } => {
|
||||
let decl = CoreBoxDecl {
|
||||
name: name.clone(),
|
||||
fields: fields.clone(),
|
||||
public_fields: public_fields.clone(),
|
||||
private_fields: private_fields.clone(),
|
||||
methods: methods.clone(),
|
||||
constructors: constructors.clone(),
|
||||
init_fields: init_fields.clone(),
|
||||
|
||||
Reference in New Issue
Block a user