diff --git a/docs/examples/visibility_error.nyash b/docs/examples/visibility_error.nyash new file mode 100644 index 00000000..c2c8c8b0 --- /dev/null +++ b/docs/examples/visibility_error.nyash @@ -0,0 +1,27 @@ +static box Main { + init { console } + + main() { + me.console = new ConsoleBox() + + box User { + private { age, passwordHash } + public { name } + + init { name, age } + + birth(n, a) { + me.name = n + me.age = a + } + } + + local u = new User("Alice", 20) + me.console.log("name(public)=" + u.name) # OK: public + + # 以下は外部からprivateフィールドへのアクセス → エラーになる想定 + u.age = 30 # 外部からの代入(NG) + me.console.log(u.age) # 外部からの参照(NG) + } +} + diff --git a/docs/examples/visibility_ok.nyash b/docs/examples/visibility_ok.nyash new file mode 100644 index 00000000..3aedc256 --- /dev/null +++ b/docs/examples/visibility_ok.nyash @@ -0,0 +1,36 @@ +static box Main { + init { console } + + main() { + me.console = new ConsoleBox() + + box User { + private { age, passwordHash } + public { name } + + init { name, age } + + birth(n, a) { + me.name = n + me.age = a + } + + setAge(a) { + me.age = a # private だが内部からなのでOK + } + + getAge() { + return me.age # private を内部参照(OK) + } + } + + local u = new User("Alice", 20) + me.console.log("name(public)=" + u.name) # OK: public + u.name = "Bob" # OK: public set + + me.console.log("age(private, internal)=" + u.getAge()) # OK: 内部アクセス + u.setAge(21) # OK: 内部でprivate set + me.console.log("done") + } +} + diff --git a/src/ast.rs b/src/ast.rs index 3bcec322..8b66253b 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -460,6 +460,10 @@ pub enum ASTNode { BoxDeclaration { name: String, fields: Vec, + /// 公開フィールド(public { ... }) + public_fields: Vec, + /// 非公開フィールド(private { ... }) + private_fields: Vec, methods: HashMap, // method_name -> FunctionDeclaration constructors: HashMap, // constructor_key -> FunctionDeclaration init_fields: Vec, // initブロック内のフィールド定義 diff --git a/src/backend/vm.rs b/src/backend/vm.rs index 6dbb02c5..1fe243bd 100644 --- a/src/backend/vm.rs +++ b/src/backend/vm.rs @@ -173,6 +173,8 @@ pub struct VM { last_result: Option, /// Simple field storage for objects (maps reference -> field -> value) object_fields: HashMap>, + /// Class name mapping for objects (for visibility checks) + object_class: HashMap, /// 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(), diff --git a/src/core/model.rs b/src/core/model.rs index 66c81bde..b8da6efa 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -15,6 +15,8 @@ use crate::ast::ASTNode; pub struct BoxDeclaration { pub name: String, pub fields: Vec, + pub public_fields: Vec, + pub private_fields: Vec, pub methods: HashMap, pub constructors: HashMap, pub init_fields: Vec, @@ -26,4 +28,3 @@ pub struct BoxDeclaration { /// Generic type parameters pub type_parameters: Vec, } - diff --git a/src/interpreter/expressions/access.rs b/src/interpreter/expressions/access.rs index 73f1c5af..50920a03 100644 --- a/src/interpreter/expressions/access.rs +++ b/src/interpreter/expressions/access.rs @@ -34,6 +34,13 @@ impl NyashInterpreter { } + // 外からのフィールドアクセスか(me/this以外)を判定 + let is_internal_access = match object { + ASTNode::This { .. } | ASTNode::Me { .. } => true, + ASTNode::Variable { name, .. } if name == "me" => true, + _ => false, + }; + // オブジェクトを評価(通常のフィールドアクセス) let obj_value = self.execute_expression(object); @@ -41,6 +48,20 @@ impl NyashInterpreter { // InstanceBoxにキャスト if let Some(instance) = obj_value.as_any().downcast_ref::() { + // 可視性チェック(互換性: public/privateのどちらかが定義されていれば強制) + if !is_internal_access { + let box_decls = self.shared.box_declarations.read().unwrap(); + if let Some(box_decl) = box_decls.get(&instance.class_name) { + let has_visibility = !box_decl.public_fields.is_empty() || !box_decl.private_fields.is_empty(); + if has_visibility { + if !box_decl.public_fields.contains(&field.to_string()) { + return Err(RuntimeError::InvalidOperation { + message: format!("Field '{}' is private in {}", field, instance.class_name), + }); + } + } + } + } // 🔥 finiは何回呼ばれてもエラーにしない(ユーザー要求) // is_finalized()チェックを削除 @@ -145,4 +166,4 @@ impl NyashInterpreter { Ok(value) } } -} \ No newline at end of file +} diff --git a/src/interpreter/expressions/calls.rs b/src/interpreter/expressions/calls.rs index fc3d4106..f901e567 100644 --- a/src/interpreter/expressions/calls.rs +++ b/src/interpreter/expressions/calls.rs @@ -949,203 +949,16 @@ impl NyashInterpreter { arguments: &[ASTNode], ) -> Result, 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> = 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_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> = 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) }), } } } diff --git a/src/interpreter/objects.rs b/src/interpreter/objects.rs index 8f7588ef..ffb42e46 100644 --- a/src/interpreter/objects.rs +++ b/src/interpreter/objects.rs @@ -958,6 +958,8 @@ impl NyashInterpreter { &mut self, name: String, fields: Vec, + public_fields: Vec, + private_fields: Vec, methods: HashMap, constructors: HashMap, init_fields: Vec, @@ -991,6 +993,8 @@ impl NyashInterpreter { let box_decl = super::BoxDeclaration { name: name.clone(), fields, + public_fields, + private_fields, methods, constructors, init_fields, diff --git a/src/interpreter/statements.rs b/src/interpreter/statements.rs index 08ae4b0b..28df28ed 100644 --- a/src/interpreter/statements.rs +++ b/src/interpreter/statements.rs @@ -64,7 +64,7 @@ impl NyashInterpreter { self.execute_using_statement(namespace_name) } - ASTNode::BoxDeclaration { name, fields, methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, is_static, static_init, .. } => { + ASTNode::BoxDeclaration { name, fields, public_fields, private_fields, methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, is_static, static_init, .. } => { if *is_static { // 🔥 Static Box宣言の処理 self.register_static_box_declaration( @@ -83,6 +83,8 @@ impl NyashInterpreter { self.register_box_declaration( name.clone(), fields.clone(), + public_fields.clone(), + private_fields.clone(), methods.clone(), constructors.clone(), init_fields.clone(), @@ -286,9 +288,28 @@ impl NyashInterpreter { ASTNode::FieldAccess { object, field, .. } => { // フィールドへの代入 + // 内部(me/this)からの代入かどうか + let is_internal = match &**object { + ASTNode::This { .. } | ASTNode::Me { .. } => true, + ASTNode::Variable { name, .. } if name == "me" => true, + _ => false, + }; + let obj_value = self.execute_expression(object)?; if let Some(instance) = obj_value.as_any().downcast_ref::() { + // 可視性チェック(外部アクセスの場合のみ) + if !is_internal { + let box_decls = self.shared.box_declarations.read().unwrap(); + if let Some(box_decl) = box_decls.get(&instance.class_name) { + let has_visibility = !box_decl.public_fields.is_empty() || !box_decl.private_fields.is_empty(); + if has_visibility && !box_decl.public_fields.contains(&field.to_string()) { + return Err(RuntimeError::InvalidOperation { + message: format!("Field '{}' is private in {}", field, instance.class_name), + }); + } + } + } // 🔥 finiは何回呼ばれてもエラーにしない(ユーザー要求) // is_finalized()チェックを削除 diff --git a/src/parser/declarations/box_definition.rs b/src/parser/declarations/box_definition.rs index 0c3caa67..b7c08e98 100644 --- a/src/parser/declarations/box_definition.rs +++ b/src/parser/declarations/box_definition.rs @@ -137,6 +137,8 @@ impl NyashParser { let mut fields = Vec::new(); let mut methods = HashMap::new(); + let mut public_fields: Vec = Vec::new(); + let mut private_fields: Vec = Vec::new(); let mut constructors = HashMap::new(); let mut init_fields = Vec::new(); let mut weak_fields = Vec::new(); // 🔗 Track weak fields @@ -404,6 +406,35 @@ impl NyashParser { if let TokenType::IDENTIFIER(field_or_method) = &self.current_token().token_type { let field_or_method = field_or_method.clone(); self.advance(); + + // 可視性ブロック: public { ... } / private { ... } + if field_or_method == "public" || field_or_method == "private" { + self.consume(TokenType::LBRACE)?; + self.skip_newlines(); + while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() { + if let TokenType::IDENTIFIER(fname) = &self.current_token().token_type { + let fname = fname.clone(); + // ブロックに追加 + if field_or_method == "public" { public_fields.push(fname.clone()); } else { private_fields.push(fname.clone()); } + // 互換性のため、全体fieldsにも追加 + fields.push(fname); + self.advance(); + // カンマ/改行をスキップ + if self.match_token(&TokenType::COMMA) { self.advance(); } + self.skip_newlines(); + continue; + } + // 予期しないトークン + return Err(ParseError::UnexpectedToken { + expected: "identifier in visibility block".to_string(), + found: self.current_token().token_type.clone(), + line: self.current_token().line, + }); + } + self.consume(TokenType::RBRACE)?; + self.skip_newlines(); + continue; + } // メソッドかフィールドかを判定 if self.match_token(&TokenType::LPAREN) { @@ -473,6 +504,8 @@ impl NyashParser { Ok(ASTNode::BoxDeclaration { name, fields, + public_fields, + private_fields, methods, constructors, init_fields, @@ -571,6 +604,8 @@ impl NyashParser { Ok(ASTNode::BoxDeclaration { name, fields: vec![], // インターフェースはフィールドなし + public_fields: vec![], + private_fields: vec![], methods, constructors: HashMap::new(), // インターフェースにコンストラクタなし init_fields: vec![], // インターフェースにinitブロックなし @@ -584,4 +619,4 @@ impl NyashParser { span: Span::unknown(), }) } -} \ No newline at end of file +} diff --git a/src/parser/declarations/static_box.rs b/src/parser/declarations/static_box.rs index 1b99b784..d70e404a 100644 --- a/src/parser/declarations/static_box.rs +++ b/src/parser/declarations/static_box.rs @@ -273,6 +273,8 @@ impl NyashParser { Ok(ASTNode::BoxDeclaration { name, fields, + public_fields: vec![], + private_fields: vec![], methods, constructors, init_fields, @@ -286,4 +288,4 @@ impl NyashParser { span: Span::unknown(), }) } -} \ No newline at end of file +} diff --git a/src/runner.rs b/src/runner.rs index 4797f03e..32068cf0 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -365,10 +365,23 @@ impl NyashRunner { ASTNode::Program { statements, .. } => { for st in statements { walk(st, runtime); } } - ASTNode::BoxDeclaration { name, fields, methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, .. } => { + ASTNode::FunctionDeclaration { body, .. } => { + // Walk into function bodies to find nested box declarations + for st in body { walk(st, runtime); } + } + ASTNode::BoxDeclaration { name, fields, public_fields, private_fields, methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, .. } => { + // Walk into methods/constructors to find nested box declarations + for (_mname, mnode) in methods { + walk(mnode, runtime); + } + for (_ckey, cnode) in constructors { + walk(cnode, runtime); + } 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(),