feat: Complete internal field access identification for visibility system
- Add internal access tracking to distinguish me.field from external.field access - Interpreter correctly allows methods to access their own private fields - VM tracks object class context during method calls for visibility checks - Fix VM nested box declaration collection in collect_box_declarations - Both interpreter and VM now pass all visibility tests consistently Test results: - visibility_ok.nyash: ✅ Internal private access allowed in methods - visibility_error.nyash: ✅ External private access correctly blocked - All private fields accessible from within their own methods - Public fields remain accessible from anywhere 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
64
docs/reference/language/field-visibility-and-delegation.md
Normal file
64
docs/reference/language/field-visibility-and-delegation.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# フィールド可視性とデリゲーション設計(提案/仕様草案)
|
||||||
|
|
||||||
|
本書は Nyash 言語の「フィールド可視性」と「デリゲーション(from/override)」の設計をまとめた仕様草案です。実装は段階的に進めます。
|
||||||
|
|
||||||
|
## 1. フィールド可視性(Blocks)
|
||||||
|
- 構文
|
||||||
|
```nyash
|
||||||
|
box User {
|
||||||
|
private { age, passwordHash, internalCache }
|
||||||
|
public { name, email, displayName }
|
||||||
|
|
||||||
|
init { name, email, displayName, age }
|
||||||
|
// ... methods ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- ルール
|
||||||
|
- `private`: box 内のメソッドからのみアクセス可(外部アクセス不可)
|
||||||
|
- `public`: 外部から `obj.field` で参照可
|
||||||
|
- いずれにも属さないフィールドはエラー(明示主義)
|
||||||
|
- `me.field` は可視性に関わらず常に許可(自身の内部)
|
||||||
|
- init は現状どおり public(将来 `public/private init` の導入は別途検討)
|
||||||
|
- エラー例
|
||||||
|
- 外部から `user.age` → Compile/Interpret Error: private field access
|
||||||
|
- 親の private フィールド参照 → Error: parent private field not accessible
|
||||||
|
|
||||||
|
実装フェーズ(予定)
|
||||||
|
1. パーサ: `private { ... }` / `public { ... }` の受理、AST/宣言モデルに可視性付与
|
||||||
|
2. 解決/実行: 外部アクセス時に public のみ許可、`me.field` は常にOK
|
||||||
|
3. MIR/VM: 既存の `RefGet/RefSet` に可視性チェックフックを追加
|
||||||
|
|
||||||
|
## 2. デリゲーション(from/override)
|
||||||
|
目標: ビルトイン/プラグイン/ユーザー定義の全Boxで、同一の書き味(from/override/super呼び出し)を提供しつつ、安全性を担保する。
|
||||||
|
|
||||||
|
- 基本方針
|
||||||
|
- 継承モデルは維持(「委譲の糖衣」ではない)
|
||||||
|
- 親の内部フィールドは不可視(private は子からも見えない)
|
||||||
|
- メソッド解決規則は統一:子の override → なければ親へ解決
|
||||||
|
- 親がビルトイン/プラグインの場合、親メソッド呼び出しは `BoxCall` に lower(ローダ経由)
|
||||||
|
|
||||||
|
- 書式例
|
||||||
|
```nyash
|
||||||
|
box MyFile from FileBox {
|
||||||
|
override write(x) {
|
||||||
|
// 前処理
|
||||||
|
from FileBox.write(x)
|
||||||
|
// 後処理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 安全性
|
||||||
|
- 親の状態は親の実体としてのみ存在(フィールド継承しない)
|
||||||
|
- `from Parent.method()` は親タイプ名での明示ディスパッチ(暗黙の内包はしない)
|
||||||
|
- MIR では `BoxCall` を生成し、VM/Interpreter はローダへ委譲
|
||||||
|
|
||||||
|
実装フェーズ(予定)
|
||||||
|
1. MIR Lowering: 親がビルトイン/プラグインの `from` を常に `BoxCall` へ(現状踏襲)
|
||||||
|
2. VM/Interpreter: 既存どおりローダ委譲(Handle/BoxRef を統一処理)
|
||||||
|
|
||||||
|
## 3. 参考
|
||||||
|
- BoxRef/Handle 仕様: `docs/reference/plugin-system/boxref-behavior.md`
|
||||||
|
- nyash.toml v2.1–v2.2: `docs/reference/plugin-system/nyash-toml-v2_1-spec.md`
|
||||||
|
- 実装箇所(予定): `src/parser/declarations/box_definition.rs`, `src/core/model.rs`, `src/interpreter/expressions/access.rs`, `src/mir/*`, `src/backend/vm.rs`
|
||||||
|
|
||||||
53
docs/reference/plugin-system/boxref-behavior.md
Normal file
53
docs/reference/plugin-system/boxref-behavior.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# BoxRef/Handle Behavior (v2.1–v2.2)
|
||||||
|
|
||||||
|
本書は、プラグインBoxの引数/返り値としてのBox参照(BoxRef/Handle)の扱いと、VM/インタプリタ/ローダ/プラグインにまたがる設計上の注意点をまとめます。
|
||||||
|
|
||||||
|
## 1. TLV仕様(BID-1)
|
||||||
|
- ヘッダ: `ver:u16=1, argc:u16`
|
||||||
|
- エントリ: `tag:u8, rsv:u8=0, size:u16, payload...`
|
||||||
|
- 主要タグ:
|
||||||
|
- `6 = String(UTF-8)`, `7 = Bytes`
|
||||||
|
- `8 = Handle(BoxRef)` → payload: `type_id:u32 + instance_id:u32`(計8バイト, LE)
|
||||||
|
- `2 = I32`, `3 = I64`, `1 = Bool`, `9 = Void`
|
||||||
|
|
||||||
|
## 2. nyash.toml(v2.1〜)
|
||||||
|
- 引数宣言に `args=[{ kind="box", category="plugin" }]` を追加可能。
|
||||||
|
- 例:
|
||||||
|
```toml
|
||||||
|
[libraries."libnyash_filebox_plugin.so".FileBox.methods]
|
||||||
|
copyFrom = { method_id = 7, args = [ { kind = "box", category = "plugin" } ] }
|
||||||
|
cloneSelf = { method_id = 8 }
|
||||||
|
```
|
||||||
|
- ローダは `args` の型宣言に基づいて実引数を検証。型不一致は `InvalidArgs`。
|
||||||
|
|
||||||
|
## 3. 返り値(v2.2)
|
||||||
|
- プラグインが `tag=8` を返した場合、ローダは `type_id` を `nyash.toml` で逆引きし、
|
||||||
|
`PluginBoxV2 { box_type, type_id, invoke_fn, instance_id, fini_method_id }` を構築して返す。
|
||||||
|
|
||||||
|
## 4. VM/インタプリタの扱い
|
||||||
|
- メソッド呼び出しはローダ経由に統一(TLV/Handle処理もローダ側)。
|
||||||
|
- MIR Loweringは以下を厳守:
|
||||||
|
- ユーザー定義Boxのみ関数化(Call)最適化可。
|
||||||
|
- プラグイン/ビルトインは常に `BoxCall` を出す(VMでローダに委譲)。
|
||||||
|
- VMのBoxRefは共有ハンドルとして扱う:
|
||||||
|
- `clone_box()` ではなく `share_box()` を使用(不意のbirth回避)。
|
||||||
|
|
||||||
|
## 5. プラグイン実装の注意
|
||||||
|
- `open` のモードによっては `read` ができない(例: "w")。`copyFrom` は `file.read` が失敗したら `buffer` にフォールバックする。
|
||||||
|
- `write` 実装では、成功後に `buffer` を更新しておくと `copyFrom` のフォールバックで活きる。
|
||||||
|
- 典型メソッドID(例: FileBox)
|
||||||
|
- `0=birth`, `1=open`, `2=read`, `3=write`, `4=close`, `0xFFFFFFFF=fini`, `7=copyFrom`, `8=cloneSelf`
|
||||||
|
|
||||||
|
## 6. トラブルシュート
|
||||||
|
- rc=-4 `Invalid arguments`
|
||||||
|
- `args` 型宣言と実引数が不一致。ローダログの引数エンコードを確認(String化フォールバックが出ていないか)。
|
||||||
|
- rc=-5 `Plugin internal error`
|
||||||
|
- プラグイン内部のread/write/lock失敗など。`copyFrom` のfile→bufferフォールバック不備を疑う。
|
||||||
|
- rc=-8 `Invalid handle`
|
||||||
|
- 存在しない `instance_id` に対する呼び出し。VMで `clone_box` を使っていないか(`share_box` へ)。
|
||||||
|
|
||||||
|
## 7. 参考
|
||||||
|
- 仕様: `docs/reference/plugin-system/nyash-toml-v2_1-spec.md`
|
||||||
|
- 実装: `src/runtime/plugin_loader_v2.rs`(引数検証/Handle戻り値復元)
|
||||||
|
- 例: `docs/examples/plugin_boxref_return.nyash`
|
||||||
|
|
||||||
57
mir_error.txt
Normal file
57
mir_error.txt
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
🔍 DEBUG: Initializing v2 plugin system
|
||||||
|
[FileBox] Plugin initialized
|
||||||
|
🔌 v2 plugin system initialized from nyash.toml
|
||||||
|
📦 Registering plugin provider for FileBox
|
||||||
|
✅ v2 plugin system fully configured
|
||||||
|
🚀 Nyash MIR Compiler - Processing file: docs/examples/visibility_error.nyash 🚀
|
||||||
|
🚀 MIR Output for docs/examples/visibility_error.nyash:
|
||||||
|
; MIR Module: main
|
||||||
|
|
||||||
|
define void @User.birth/2(box<User> %0, ? %1, ? %2) effects(read) {
|
||||||
|
bb1:
|
||||||
|
0: ref_set %0.name = %1
|
||||||
|
1: ref_set %0.age = %2
|
||||||
|
2: %3 = const void
|
||||||
|
3: ret %3
|
||||||
|
}
|
||||||
|
|
||||||
|
define void @main() {
|
||||||
|
bb0:
|
||||||
|
0: safepoint
|
||||||
|
1: %0 = const "__me__"
|
||||||
|
2: %1 = new ConsoleBox()
|
||||||
|
3: call %1.birth()
|
||||||
|
4: ref_set %0.console = %1
|
||||||
|
5: %2 = const "__box_type_User"
|
||||||
|
6: %3 = const "__field_User_age"
|
||||||
|
7: %4 = const "__field_User_passwordHash"
|
||||||
|
8: %5 = const "__field_User_name"
|
||||||
|
9: %6 = const void
|
||||||
|
10: %7 = const "Alice"
|
||||||
|
11: %8 = new StringBox(%7)
|
||||||
|
12: call %8.birth(%7)
|
||||||
|
13: %9 = const 20
|
||||||
|
14: %10 = new IntegerBox(%9)
|
||||||
|
15: call %10.birth(%9)
|
||||||
|
16: %11 = new User(%8, %10)
|
||||||
|
17: call %11.birth(%8, %10)
|
||||||
|
18: %12 = const "__me__"
|
||||||
|
19: %13 = ref_get %12.console
|
||||||
|
20: %14 = const "name(public)="
|
||||||
|
21: %15 = new StringBox(%14)
|
||||||
|
22: call %15.birth(%14)
|
||||||
|
23: %16 = ref_get %11.name
|
||||||
|
24: %17 = %15 Add %16
|
||||||
|
25: %18 = call %13.log(%17)
|
||||||
|
26: %19 = const 30
|
||||||
|
27: %20 = new IntegerBox(%19)
|
||||||
|
28: call %20.birth(%19)
|
||||||
|
29: ref_set %11.age = %20
|
||||||
|
30: %21 = const "__me__"
|
||||||
|
31: %22 = ref_get %21.console
|
||||||
|
32: %23 = ref_get %11.age
|
||||||
|
33: %24 = call %22.log(%23)
|
||||||
|
34: ret %24
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
91
mir_ok.txt
Normal file
91
mir_ok.txt
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
🔍 DEBUG: Initializing v2 plugin system
|
||||||
|
[FileBox] Plugin initialized
|
||||||
|
🔌 v2 plugin system initialized from nyash.toml
|
||||||
|
📦 Registering plugin provider for FileBox
|
||||||
|
✅ v2 plugin system fully configured
|
||||||
|
🚀 Nyash MIR Compiler - Processing file: docs/examples/visibility_ok.nyash 🚀
|
||||||
|
🚀 MIR Output for docs/examples/visibility_ok.nyash:
|
||||||
|
; MIR Module: main
|
||||||
|
|
||||||
|
define void @User.birth/2(box<User> %0, ? %1, ? %2) effects(read) {
|
||||||
|
bb1:
|
||||||
|
0: ref_set %0.name = %1
|
||||||
|
1: ref_set %0.age = %2
|
||||||
|
2: %3 = const void
|
||||||
|
3: ret %3
|
||||||
|
}
|
||||||
|
|
||||||
|
define void @User.setAge/1(box<User> %0, ? %1) effects(read) {
|
||||||
|
bb3:
|
||||||
|
0: ref_set %0.age = %1
|
||||||
|
1: %2 = const void
|
||||||
|
2: ret %2
|
||||||
|
}
|
||||||
|
|
||||||
|
define ? @User.getAge/0(box<User> %0) effects(read) {
|
||||||
|
bb2:
|
||||||
|
0: %1 = ref_get %0.age
|
||||||
|
1: ret %1
|
||||||
|
}
|
||||||
|
|
||||||
|
define void @main() {
|
||||||
|
bb0:
|
||||||
|
0: safepoint
|
||||||
|
1: %0 = const "__me__"
|
||||||
|
2: %1 = new ConsoleBox()
|
||||||
|
3: call %1.birth()
|
||||||
|
4: ref_set %0.console = %1
|
||||||
|
5: %2 = const "__box_type_User"
|
||||||
|
6: %3 = const "__field_User_age"
|
||||||
|
7: %4 = const "__field_User_passwordHash"
|
||||||
|
8: %5 = const "__field_User_name"
|
||||||
|
9: %6 = const "__method_User_getAge"
|
||||||
|
10: %7 = const "__method_User_setAge"
|
||||||
|
11: %8 = const void
|
||||||
|
12: %9 = const "Alice"
|
||||||
|
13: %10 = new StringBox(%9)
|
||||||
|
14: call %10.birth(%9)
|
||||||
|
15: %11 = const 20
|
||||||
|
16: %12 = new IntegerBox(%11)
|
||||||
|
17: call %12.birth(%11)
|
||||||
|
18: %13 = new User(%10, %12)
|
||||||
|
19: call %13.birth(%10, %12)
|
||||||
|
20: %14 = const "__me__"
|
||||||
|
21: %15 = ref_get %14.console
|
||||||
|
22: %16 = const "name(public)="
|
||||||
|
23: %17 = new StringBox(%16)
|
||||||
|
24: call %17.birth(%16)
|
||||||
|
25: %18 = ref_get %13.name
|
||||||
|
26: %19 = %17 Add %18
|
||||||
|
27: %20 = call %15.log(%19)
|
||||||
|
28: %21 = const "Bob"
|
||||||
|
29: %22 = new StringBox(%21)
|
||||||
|
30: call %22.birth(%21)
|
||||||
|
31: ref_set %13.name = %22
|
||||||
|
32: %23 = const "__me__"
|
||||||
|
33: %24 = ref_get %23.console
|
||||||
|
34: %25 = const "age(private, internal)="
|
||||||
|
35: %26 = new StringBox(%25)
|
||||||
|
36: call %26.birth(%25)
|
||||||
|
37: %28 = const "User.getAge/0"
|
||||||
|
38: %27 = call %28(%13)
|
||||||
|
39: %29 = %26 Add %27
|
||||||
|
40: %30 = call %24.log(%29)
|
||||||
|
41: %31 = const 21
|
||||||
|
42: %32 = new IntegerBox(%31)
|
||||||
|
43: call %32.birth(%31)
|
||||||
|
44: %33 = const 21
|
||||||
|
45: %34 = new IntegerBox(%33)
|
||||||
|
46: call %34.birth(%33)
|
||||||
|
47: %36 = const "User.setAge/1"
|
||||||
|
48: %35 = call %36(%13, %34)
|
||||||
|
49: %37 = const "__me__"
|
||||||
|
50: %38 = ref_get %37.console
|
||||||
|
51: %39 = const "done"
|
||||||
|
52: %40 = new StringBox(%39)
|
||||||
|
53: call %40.birth(%39)
|
||||||
|
54: %41 = call %38.log(%40)
|
||||||
|
55: ret %41
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -175,6 +175,8 @@ pub struct VM {
|
|||||||
object_fields: HashMap<ValueId, HashMap<String, VMValue>>,
|
object_fields: HashMap<ValueId, HashMap<String, VMValue>>,
|
||||||
/// Class name mapping for objects (for visibility checks)
|
/// Class name mapping for objects (for visibility checks)
|
||||||
object_class: HashMap<ValueId, String>,
|
object_class: HashMap<ValueId, String>,
|
||||||
|
/// Marks ValueIds that represent internal (me/this) references within the current function
|
||||||
|
object_internal: std::collections::HashSet<ValueId>,
|
||||||
/// Loop executor for handling phi nodes and loop-specific logic
|
/// Loop executor for handling phi nodes and loop-specific logic
|
||||||
loop_executor: LoopExecutor,
|
loop_executor: LoopExecutor,
|
||||||
/// Shared runtime for box creation and declarations
|
/// Shared runtime for box creation and declarations
|
||||||
@ -208,6 +210,7 @@ impl VM {
|
|||||||
last_result: None,
|
last_result: None,
|
||||||
object_fields: HashMap::new(),
|
object_fields: HashMap::new(),
|
||||||
object_class: HashMap::new(),
|
object_class: HashMap::new(),
|
||||||
|
object_internal: std::collections::HashSet::new(),
|
||||||
loop_executor: LoopExecutor::new(),
|
loop_executor: LoopExecutor::new(),
|
||||||
runtime: NyashRuntime::new(),
|
runtime: NyashRuntime::new(),
|
||||||
scope_tracker: ScopeTracker::new(),
|
scope_tracker: ScopeTracker::new(),
|
||||||
@ -232,6 +235,7 @@ impl VM {
|
|||||||
last_result: None,
|
last_result: None,
|
||||||
object_fields: HashMap::new(),
|
object_fields: HashMap::new(),
|
||||||
object_class: HashMap::new(),
|
object_class: HashMap::new(),
|
||||||
|
object_internal: std::collections::HashSet::new(),
|
||||||
loop_executor: LoopExecutor::new(),
|
loop_executor: LoopExecutor::new(),
|
||||||
runtime,
|
runtime,
|
||||||
scope_tracker: ScopeTracker::new(),
|
scope_tracker: ScopeTracker::new(),
|
||||||
@ -300,6 +304,16 @@ impl VM {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Heuristic: map `me` (first param) to class name parsed from function name (e.g., User.method/N)
|
||||||
|
if let Some(first) = function.params.get(0) {
|
||||||
|
if let Some((class_part, _rest)) = func_name.split_once('.') {
|
||||||
|
// Record class for internal field visibility checks
|
||||||
|
self.object_class.insert(*first, class_part.to_string());
|
||||||
|
// Mark internal reference
|
||||||
|
self.object_internal.insert(*first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Execute the function
|
// Execute the function
|
||||||
let result = self.execute_function(&function);
|
let result = self.execute_function(&function);
|
||||||
|
|
||||||
@ -657,6 +671,14 @@ impl VM {
|
|||||||
// Copy instruction - duplicate the source value
|
// Copy instruction - duplicate the source value
|
||||||
let val = self.get_value(*src)?;
|
let val = self.get_value(*src)?;
|
||||||
self.set_value(*dst, val);
|
self.set_value(*dst, val);
|
||||||
|
// Propagate class mapping for references (helps track `me` copies)
|
||||||
|
if let Some(class_name) = self.object_class.get(src).cloned() {
|
||||||
|
self.object_class.insert(*dst, class_name);
|
||||||
|
}
|
||||||
|
// Propagate internal marker (me/this lineage)
|
||||||
|
if self.object_internal.contains(src) {
|
||||||
|
self.object_internal.insert(*dst);
|
||||||
|
}
|
||||||
Ok(ControlFlow::Continue)
|
Ok(ControlFlow::Continue)
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -702,13 +724,16 @@ impl VM {
|
|||||||
},
|
},
|
||||||
|
|
||||||
MirInstruction::RefGet { dst, reference, field } => {
|
MirInstruction::RefGet { dst, reference, field } => {
|
||||||
// Visibility check (if class known and visibility declared)
|
// Visibility check (if class known and visibility declared). Skip for internal refs.
|
||||||
if let Some(class_name) = self.object_class.get(reference) {
|
let is_internal = self.object_internal.contains(reference);
|
||||||
if let Ok(decls) = self.runtime.box_declarations.read() {
|
if !is_internal {
|
||||||
if let Some(decl) = decls.get(class_name) {
|
if let Some(class_name) = self.object_class.get(reference) {
|
||||||
let has_vis = !decl.public_fields.is_empty() || !decl.private_fields.is_empty();
|
if let Ok(decls) = self.runtime.box_declarations.read() {
|
||||||
if has_vis && !decl.public_fields.contains(field) {
|
if let Some(decl) = decls.get(class_name) {
|
||||||
return Err(VMError::TypeError(format!("Field '{}' is private in {}", field, 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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -733,13 +758,16 @@ impl VM {
|
|||||||
MirInstruction::RefSet { reference, field, value } => {
|
MirInstruction::RefSet { reference, field, value } => {
|
||||||
// Get the value to set
|
// Get the value to set
|
||||||
let new_value = self.get_value(*value)?;
|
let new_value = self.get_value(*value)?;
|
||||||
// Visibility check (treat all RefSet as external writes)
|
// Visibility check (Skip for internal refs; otherwise enforce public)
|
||||||
if let Some(class_name) = self.object_class.get(reference) {
|
let is_internal = self.object_internal.contains(reference);
|
||||||
if let Ok(decls) = self.runtime.box_declarations.read() {
|
if !is_internal {
|
||||||
if let Some(decl) = decls.get(class_name) {
|
if let Some(class_name) = self.object_class.get(reference) {
|
||||||
let has_vis = !decl.public_fields.is_empty() || !decl.private_fields.is_empty();
|
if let Ok(decls) = self.runtime.box_declarations.read() {
|
||||||
if has_vis && !decl.public_fields.contains(field) {
|
if let Some(decl) = decls.get(class_name) {
|
||||||
return Err(VMError::TypeError(format!("Field '{}' is private in {}", field, 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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user