method-ray 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/lib/methodray/version.rb +1 -1
- data/rust/Cargo.toml +1 -1
- data/rust/src/analyzer/blocks.rs +4 -3
- data/rust/src/analyzer/definitions.rs +4 -3
- data/rust/src/analyzer/dispatch.rs +98 -11
- data/rust/src/analyzer/mod.rs +29 -0
- data/rust/src/analyzer/parameters.rs +6 -4
- data/rust/src/env/global_env.rs +33 -2
- data/rust/src/env/method_registry.rs +132 -15
- data/rust/src/env/scope.rs +130 -160
- data/rust/src/graph/box.rs +87 -80
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b5b60b4408b5e547cb40911fb0eb68d4dce182bc32fdf1ce3e6c8cd25f24975
|
|
4
|
+
data.tar.gz: 44f0b585ad8f2c83568d0df520711b2c1d09affd6fded8e35fbc3f27b082f073
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a7988edfb76b4fe4d1e03b8558104cc378299ebf8651b79dfc743e0e48b9cd4a04d235be1ca054f3ad172f9b7a8b0689e4bf99250935c02ab5cedb0737f1340
|
|
7
|
+
data.tar.gz: b6d94d5ce791e5b8317a880e986d60618a017434bc5823412dba3c8a04dcf3fb25089dff2a86e3662ecb7c2cdd13790cd32d5b18bc73960cf488ad8e8849a35b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.7] - 2026-03-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Kernel/Object methods loaded from RBS to reduce false positives ([#39](https://github.com/dak2/method-ray/pull/39))
|
|
13
|
+
- Object/Kernel fallback chain for method resolution ([#40](https://github.com/dak2/method-ray/pull/40))
|
|
14
|
+
- Constant namespace resolution for ConstantReadNode in nested scopes ([#41](https://github.com/dak2/method-ray/pull/41))
|
|
15
|
+
- Cargo test added to CI workflow ([#38](https://github.com/dak2/method-ray/pull/38))
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Extract `bytes_to_name` helper to consolidate 17 UTF-8 conversion sites ([#42](https://github.com/dak2/method-ray/pull/42))
|
|
20
|
+
- Refactor MethodCallBox by extracting helper methods ([#43](https://github.com/dak2/method-ray/pull/43))
|
|
21
|
+
|
|
8
22
|
## [0.1.6] - 2026-02-23
|
|
9
23
|
|
|
10
24
|
### Fixed
|
|
@@ -95,6 +109,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
95
109
|
- Initial release
|
|
96
110
|
- `methodray check` - Static type checking for Ruby files
|
|
97
111
|
|
|
112
|
+
[0.1.7]: https://github.com/dak2/method-ray/releases/tag/v0.1.7
|
|
98
113
|
[0.1.6]: https://github.com/dak2/method-ray/releases/tag/v0.1.6
|
|
99
114
|
[0.1.5]: https://github.com/dak2/method-ray/releases/tag/v0.1.5
|
|
100
115
|
[0.1.4]: https://github.com/dak2/method-ray/releases/tag/v0.1.4
|
data/lib/methodray/version.rb
CHANGED
data/rust/Cargo.toml
CHANGED
data/rust/src/analyzer/blocks.rs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
use crate::env::{GlobalEnv, LocalEnv, ScopeKind};
|
|
9
9
|
use crate::graph::{ChangeSet, VertexId};
|
|
10
10
|
|
|
11
|
+
use super::bytes_to_name;
|
|
11
12
|
use super::parameters::{install_optional_parameter, install_required_parameter, install_rest_parameter};
|
|
12
13
|
|
|
13
14
|
/// Process block node
|
|
@@ -68,7 +69,7 @@ fn install_block_parameters_with_vtxs(
|
|
|
68
69
|
// Required parameters (most common in blocks)
|
|
69
70
|
for node in params.requireds().iter() {
|
|
70
71
|
if let Some(req_param) = node.as_required_parameter_node() {
|
|
71
|
-
let name =
|
|
72
|
+
let name = bytes_to_name(req_param.name().as_slice());
|
|
72
73
|
let vtx = install_block_parameter(genv, lenv, name);
|
|
73
74
|
vtxs.push(vtx);
|
|
74
75
|
}
|
|
@@ -77,7 +78,7 @@ fn install_block_parameters_with_vtxs(
|
|
|
77
78
|
// Optional parameters: { |x = 1| ... }
|
|
78
79
|
for node in params.optionals().iter() {
|
|
79
80
|
if let Some(opt_param) = node.as_optional_parameter_node() {
|
|
80
|
-
let name =
|
|
81
|
+
let name = bytes_to_name(opt_param.name().as_slice());
|
|
81
82
|
let default_value = opt_param.value();
|
|
82
83
|
|
|
83
84
|
if let Some(default_vtx) =
|
|
@@ -97,7 +98,7 @@ fn install_block_parameters_with_vtxs(
|
|
|
97
98
|
if let Some(rest_node) = params.rest() {
|
|
98
99
|
if let Some(rest_param) = rest_node.as_rest_parameter_node() {
|
|
99
100
|
if let Some(name_id) = rest_param.name() {
|
|
100
|
-
let name =
|
|
101
|
+
let name = bytes_to_name(name_id.as_slice());
|
|
101
102
|
let vtx = install_rest_parameter(genv, lenv, name);
|
|
102
103
|
vtxs.push(vtx);
|
|
103
104
|
}
|
|
@@ -11,6 +11,7 @@ use crate::graph::{ChangeSet, VertexId};
|
|
|
11
11
|
use crate::types::Type;
|
|
12
12
|
use ruby_prism::Node;
|
|
13
13
|
|
|
14
|
+
use super::bytes_to_name;
|
|
14
15
|
use super::install::install_statements;
|
|
15
16
|
use super::parameters::install_parameters;
|
|
16
17
|
|
|
@@ -64,7 +65,7 @@ pub(crate) fn process_def_node(
|
|
|
64
65
|
source: &str,
|
|
65
66
|
def_node: &ruby_prism::DefNode,
|
|
66
67
|
) -> Option<VertexId> {
|
|
67
|
-
let method_name =
|
|
68
|
+
let method_name = bytes_to_name(def_node.name().as_slice());
|
|
68
69
|
|
|
69
70
|
// Check if this is a class method (def self.foo)
|
|
70
71
|
let is_class_method = def_node
|
|
@@ -163,7 +164,7 @@ fn extract_module_name(module_node: &ruby_prism::ModuleNode) -> String {
|
|
|
163
164
|
pub(crate) fn extract_constant_path(node: &Node) -> Option<String> {
|
|
164
165
|
// Simple constant read: `User`
|
|
165
166
|
if let Some(constant_read) = node.as_constant_read_node() {
|
|
166
|
-
return Some(
|
|
167
|
+
return Some(bytes_to_name(constant_read.name().as_slice()));
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
// Constant path: `Api::User` or `Api::V1::User`
|
|
@@ -171,7 +172,7 @@ pub(crate) fn extract_constant_path(node: &Node) -> Option<String> {
|
|
|
171
172
|
// name() returns Option<ConstantId>, use as_slice() to get &[u8]
|
|
172
173
|
let name = constant_path
|
|
173
174
|
.name()
|
|
174
|
-
.map(|id|
|
|
175
|
+
.map(|id| bytes_to_name(id.as_slice()))?;
|
|
175
176
|
|
|
176
177
|
// Get parent path if exists
|
|
177
178
|
if let Some(parent_node) = constant_path.parent() {
|
|
@@ -9,6 +9,7 @@ use crate::source_map::SourceLocation;
|
|
|
9
9
|
use crate::types::Type;
|
|
10
10
|
use ruby_prism::Node;
|
|
11
11
|
|
|
12
|
+
use super::bytes_to_name;
|
|
12
13
|
use super::calls::install_method_call;
|
|
13
14
|
use super::variables::{
|
|
14
15
|
install_ivar_read, install_ivar_write, install_local_var_read, install_local_var_write,
|
|
@@ -68,7 +69,7 @@ pub enum NeedsChildKind<'a> {
|
|
|
68
69
|
pub fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &Node) -> DispatchResult {
|
|
69
70
|
// Instance variable read: @name
|
|
70
71
|
if let Some(ivar_read) = node.as_instance_variable_read_node() {
|
|
71
|
-
let ivar_name =
|
|
72
|
+
let ivar_name = bytes_to_name(ivar_read.name().as_slice());
|
|
72
73
|
return match install_ivar_read(genv, &ivar_name) {
|
|
73
74
|
Some(vtx) => DispatchResult::Vertex(vtx),
|
|
74
75
|
None => DispatchResult::NotHandled,
|
|
@@ -82,17 +83,19 @@ pub fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &Node) -
|
|
|
82
83
|
|
|
83
84
|
// Local variable read: x
|
|
84
85
|
if let Some(read_node) = node.as_local_variable_read_node() {
|
|
85
|
-
let var_name =
|
|
86
|
+
let var_name = bytes_to_name(read_node.name().as_slice());
|
|
86
87
|
return match install_local_var_read(lenv, &var_name) {
|
|
87
88
|
Some(vtx) => DispatchResult::Vertex(vtx),
|
|
88
89
|
None => DispatchResult::NotHandled,
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
// ConstantReadNode: User → Type::Singleton("User")
|
|
93
|
+
// ConstantReadNode: User → Type::Singleton("User") or Type::Singleton("Api::User")
|
|
93
94
|
if let Some(const_read) = node.as_constant_read_node() {
|
|
94
|
-
let name =
|
|
95
|
-
let
|
|
95
|
+
let name = bytes_to_name(const_read.name().as_slice());
|
|
96
|
+
let resolved_name = genv.scope_manager.lookup_constant(&name)
|
|
97
|
+
.unwrap_or(name);
|
|
98
|
+
let vtx = genv.new_source(Type::singleton(&resolved_name));
|
|
96
99
|
return DispatchResult::Vertex(vtx);
|
|
97
100
|
}
|
|
98
101
|
|
|
@@ -116,7 +119,7 @@ fn extract_symbol_names(call_node: &ruby_prism::CallNode) -> Vec<String> {
|
|
|
116
119
|
.iter()
|
|
117
120
|
.filter_map(|arg| {
|
|
118
121
|
arg.as_symbol_node().map(|sym| {
|
|
119
|
-
|
|
122
|
+
bytes_to_name(sym.unescaped())
|
|
120
123
|
})
|
|
121
124
|
})
|
|
122
125
|
.collect()
|
|
@@ -128,7 +131,7 @@ fn extract_symbol_names(call_node: &ruby_prism::CallNode) -> Vec<String> {
|
|
|
128
131
|
pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsChildKind<'a>> {
|
|
129
132
|
// Instance variable write: @name = value
|
|
130
133
|
if let Some(ivar_write) = node.as_instance_variable_write_node() {
|
|
131
|
-
let ivar_name =
|
|
134
|
+
let ivar_name = bytes_to_name(ivar_write.name().as_slice());
|
|
132
135
|
return Some(NeedsChildKind::IvarWrite {
|
|
133
136
|
ivar_name,
|
|
134
137
|
value: ivar_write.value(),
|
|
@@ -137,7 +140,7 @@ pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsCh
|
|
|
137
140
|
|
|
138
141
|
// Local variable write: x = value
|
|
139
142
|
if let Some(write_node) = node.as_local_variable_write_node() {
|
|
140
|
-
let var_name =
|
|
143
|
+
let var_name = bytes_to_name(write_node.name().as_slice());
|
|
141
144
|
return Some(NeedsChildKind::LocalVarWrite {
|
|
142
145
|
var_name,
|
|
143
146
|
value: write_node.value(),
|
|
@@ -146,7 +149,7 @@ pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsCh
|
|
|
146
149
|
|
|
147
150
|
// Method call: x.upcase, x.each { |i| ... }, or name (implicit self)
|
|
148
151
|
if let Some(call_node) = node.as_call_node() {
|
|
149
|
-
let method_name =
|
|
152
|
+
let method_name = bytes_to_name(call_node.name().as_slice());
|
|
150
153
|
let block = call_node.block();
|
|
151
154
|
let arguments: Vec<Node<'a>> = call_node
|
|
152
155
|
.arguments()
|
|
@@ -463,8 +466,8 @@ end
|
|
|
463
466
|
|
|
464
467
|
// Utils.run should be registered
|
|
465
468
|
let info = genv
|
|
466
|
-
.resolve_method(&Type::
|
|
467
|
-
.expect("Utils
|
|
469
|
+
.resolve_method(&Type::singleton("Utils"), "run")
|
|
470
|
+
.expect("Utils.run should be registered");
|
|
468
471
|
assert!(info.return_vertex.is_some());
|
|
469
472
|
}
|
|
470
473
|
|
|
@@ -893,4 +896,88 @@ Api::User.new.name
|
|
|
893
896
|
genv.type_errors
|
|
894
897
|
);
|
|
895
898
|
}
|
|
899
|
+
|
|
900
|
+
// Test 23: ConstantReadNode inside module resolves to qualified name
|
|
901
|
+
#[test]
|
|
902
|
+
fn test_constant_read_inside_module_resolves_qualified() {
|
|
903
|
+
let source = r#"
|
|
904
|
+
module Api
|
|
905
|
+
class User
|
|
906
|
+
def name
|
|
907
|
+
"Alice"
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
class Service
|
|
912
|
+
def run
|
|
913
|
+
User.new.name
|
|
914
|
+
end
|
|
915
|
+
end
|
|
916
|
+
end
|
|
917
|
+
"#;
|
|
918
|
+
let genv = analyze(source);
|
|
919
|
+
assert!(
|
|
920
|
+
genv.type_errors.is_empty(),
|
|
921
|
+
"User.new inside module Api should resolve to Api::User: {:?}",
|
|
922
|
+
genv.type_errors
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Test 24: ConstantReadNode in deeply nested modules
|
|
927
|
+
#[test]
|
|
928
|
+
fn test_constant_read_deeply_nested() {
|
|
929
|
+
let source = r#"
|
|
930
|
+
module Api
|
|
931
|
+
module V1
|
|
932
|
+
class User
|
|
933
|
+
def name
|
|
934
|
+
"Alice"
|
|
935
|
+
end
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
class Service
|
|
939
|
+
def run
|
|
940
|
+
User.new.name
|
|
941
|
+
end
|
|
942
|
+
end
|
|
943
|
+
end
|
|
944
|
+
end
|
|
945
|
+
"#;
|
|
946
|
+
let genv = analyze(source);
|
|
947
|
+
assert!(
|
|
948
|
+
genv.type_errors.is_empty(),
|
|
949
|
+
"User.new inside Api::V1 should resolve to Api::V1::User: {:?}",
|
|
950
|
+
genv.type_errors
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Test 25: Same constant name in different modules
|
|
955
|
+
#[test]
|
|
956
|
+
fn test_constant_read_same_name_different_modules() {
|
|
957
|
+
let source = r#"
|
|
958
|
+
module Api
|
|
959
|
+
class User
|
|
960
|
+
def name; "Api User"; end
|
|
961
|
+
end
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
module Admin
|
|
965
|
+
class User
|
|
966
|
+
def name; "Admin User"; end
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
class Service
|
|
970
|
+
def run
|
|
971
|
+
User.new.name
|
|
972
|
+
end
|
|
973
|
+
end
|
|
974
|
+
end
|
|
975
|
+
"#;
|
|
976
|
+
let genv = analyze(source);
|
|
977
|
+
assert!(
|
|
978
|
+
genv.type_errors.is_empty(),
|
|
979
|
+
"User.new inside Admin should resolve to Admin::User: {:?}",
|
|
980
|
+
genv.type_errors
|
|
981
|
+
);
|
|
982
|
+
}
|
|
896
983
|
}
|
data/rust/src/analyzer/mod.rs
CHANGED
|
@@ -13,3 +13,32 @@ mod returns;
|
|
|
13
13
|
mod variables;
|
|
14
14
|
|
|
15
15
|
pub use install::AstInstaller;
|
|
16
|
+
|
|
17
|
+
/// Convert ruby-prism identifier bytes to a String (lossy).
|
|
18
|
+
///
|
|
19
|
+
/// ruby-prism returns identifiers (method names, variable names, constant names,
|
|
20
|
+
/// parameter names) as `&[u8]`. This helper provides a single conversion point
|
|
21
|
+
/// used throughout the analyzer.
|
|
22
|
+
///
|
|
23
|
+
/// Note: Uses `from_utf8_lossy` — invalid UTF-8 bytes are replaced with U+FFFD.
|
|
24
|
+
/// ruby-prism identifiers are expected to be valid UTF-8, so this should not
|
|
25
|
+
/// occur in practice. Do NOT use this function for arbitrary byte data such as
|
|
26
|
+
/// string literal contents.
|
|
27
|
+
pub(crate) fn bytes_to_name(bytes: &[u8]) -> String {
|
|
28
|
+
String::from_utf8_lossy(bytes).to_string()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[cfg(test)]
|
|
32
|
+
mod tests {
|
|
33
|
+
use super::bytes_to_name;
|
|
34
|
+
|
|
35
|
+
#[test]
|
|
36
|
+
fn test_bytes_to_name_valid_utf8() {
|
|
37
|
+
assert_eq!(bytes_to_name(b"hello"), "hello");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[test]
|
|
41
|
+
fn test_bytes_to_name_invalid_utf8_replaced() {
|
|
42
|
+
assert_eq!(bytes_to_name(b"hello\xff"), "hello\u{FFFD}");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -9,6 +9,8 @@ use crate::env::{GlobalEnv, LocalEnv};
|
|
|
9
9
|
use crate::graph::{ChangeSet, VertexId};
|
|
10
10
|
use crate::types::Type;
|
|
11
11
|
|
|
12
|
+
use super::bytes_to_name;
|
|
13
|
+
|
|
12
14
|
/// Install a required parameter as a local variable
|
|
13
15
|
///
|
|
14
16
|
/// Required parameters start with Bot (untyped) type since we don't know
|
|
@@ -129,7 +131,7 @@ pub(crate) fn install_parameters(
|
|
|
129
131
|
// Required parameters: def foo(a, b)
|
|
130
132
|
for node in params_node.requireds().iter() {
|
|
131
133
|
if let Some(req_param) = node.as_required_parameter_node() {
|
|
132
|
-
let name =
|
|
134
|
+
let name = bytes_to_name(req_param.name().as_slice());
|
|
133
135
|
let vtx = install_required_parameter(genv, lenv, name);
|
|
134
136
|
param_vtxs.push(vtx);
|
|
135
137
|
}
|
|
@@ -138,7 +140,7 @@ pub(crate) fn install_parameters(
|
|
|
138
140
|
// Optional parameters: def foo(a = 1, b = "hello")
|
|
139
141
|
for node in params_node.optionals().iter() {
|
|
140
142
|
if let Some(opt_param) = node.as_optional_parameter_node() {
|
|
141
|
-
let name =
|
|
143
|
+
let name = bytes_to_name(opt_param.name().as_slice());
|
|
142
144
|
let default_value = opt_param.value();
|
|
143
145
|
|
|
144
146
|
let vtx = if let Some(default_vtx) =
|
|
@@ -157,7 +159,7 @@ pub(crate) fn install_parameters(
|
|
|
157
159
|
if let Some(rest_node) = params_node.rest() {
|
|
158
160
|
if let Some(rest_param) = rest_node.as_rest_parameter_node() {
|
|
159
161
|
if let Some(name_id) = rest_param.name() {
|
|
160
|
-
let name =
|
|
162
|
+
let name = bytes_to_name(name_id.as_slice());
|
|
161
163
|
install_rest_parameter(genv, lenv, name);
|
|
162
164
|
}
|
|
163
165
|
}
|
|
@@ -168,7 +170,7 @@ pub(crate) fn install_parameters(
|
|
|
168
170
|
if let Some(kwrest_node) = params_node.keyword_rest() {
|
|
169
171
|
if let Some(kwrest_param) = kwrest_node.as_keyword_rest_parameter_node() {
|
|
170
172
|
if let Some(name_id) = kwrest_param.name() {
|
|
171
|
-
let name =
|
|
173
|
+
let name = bytes_to_name(name_id.as_slice());
|
|
172
174
|
install_keyword_rest_parameter(genv, lenv, name);
|
|
173
175
|
}
|
|
174
176
|
}
|
data/rust/src/env/global_env.rs
CHANGED
|
@@ -71,6 +71,21 @@ impl GlobalEnv {
|
|
|
71
71
|
self.vertex_manager.get_source(id)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/// Get the types associated with a vertex ID (handles both Vertex and Source)
|
|
75
|
+
///
|
|
76
|
+
/// Returns `None` if neither a Vertex nor Source exists for this ID.
|
|
77
|
+
/// Returns `Some(vec![])` if a Vertex exists but has no types yet (e.g., unresolved block parameters).
|
|
78
|
+
/// Returns `Some(vec![ty])` if a Source exists (always exactly one type).
|
|
79
|
+
pub fn get_receiver_types(&self, id: VertexId) -> Option<Vec<Type>> {
|
|
80
|
+
if let Some(vertex) = self.get_vertex(id) {
|
|
81
|
+
Some(vertex.types.keys().cloned().collect())
|
|
82
|
+
} else if let Some(source) = self.get_source(id) {
|
|
83
|
+
Some(vec![source.ty.clone()])
|
|
84
|
+
} else {
|
|
85
|
+
None
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
/// Add edge (immediate type propagation)
|
|
75
90
|
pub fn add_edge(&mut self, src: VertexId, dst: VertexId) {
|
|
76
91
|
self.vertex_manager.add_edge(src, dst);
|
|
@@ -189,20 +204,36 @@ impl GlobalEnv {
|
|
|
189
204
|
|
|
190
205
|
// ===== Scope Management =====
|
|
191
206
|
|
|
207
|
+
/// Register a constant (simple name → qualified name) in the parent scope
|
|
208
|
+
fn register_constant_in_parent(&mut self, scope_id: ScopeId, name: &str) {
|
|
209
|
+
if name.contains("::") { return; }
|
|
210
|
+
let qualified = self.scope_manager.current_qualified_name()
|
|
211
|
+
.unwrap_or_else(|| name.to_string());
|
|
212
|
+
if let Some(parent_id) = self.scope_manager.get_scope(scope_id)
|
|
213
|
+
.and_then(|s| s.parent)
|
|
214
|
+
{
|
|
215
|
+
if let Some(parent_scope) = self.scope_manager.get_scope_mut(parent_id) {
|
|
216
|
+
parent_scope.constants.insert(name.to_string(), qualified);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
192
221
|
/// Enter a class scope
|
|
193
222
|
pub fn enter_class(&mut self, name: String) -> ScopeId {
|
|
194
223
|
let scope_id = self.scope_manager.new_scope(ScopeKind::Class {
|
|
195
|
-
name,
|
|
224
|
+
name: name.clone(),
|
|
196
225
|
superclass: None,
|
|
197
226
|
});
|
|
198
227
|
self.scope_manager.enter_scope(scope_id);
|
|
228
|
+
self.register_constant_in_parent(scope_id, &name);
|
|
199
229
|
scope_id
|
|
200
230
|
}
|
|
201
231
|
|
|
202
232
|
/// Enter a module scope
|
|
203
233
|
pub fn enter_module(&mut self, name: String) -> ScopeId {
|
|
204
|
-
let scope_id = self.scope_manager.new_scope(ScopeKind::Module { name });
|
|
234
|
+
let scope_id = self.scope_manager.new_scope(ScopeKind::Module { name: name.clone() });
|
|
205
235
|
self.scope_manager.enter_scope(scope_id);
|
|
236
|
+
self.register_constant_in_parent(scope_id, &name);
|
|
206
237
|
scope_id
|
|
207
238
|
}
|
|
208
239
|
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
use crate::graph::VertexId;
|
|
4
4
|
use crate::types::Type;
|
|
5
|
+
use smallvec::SmallVec;
|
|
5
6
|
use std::collections::HashMap;
|
|
6
7
|
|
|
8
|
+
const OBJECT_CLASS: &str = "Object";
|
|
9
|
+
const KERNEL_MODULE: &str = "Kernel";
|
|
10
|
+
|
|
7
11
|
/// Method information
|
|
8
12
|
#[derive(Debug, Clone)]
|
|
9
13
|
pub struct MethodInfo {
|
|
@@ -70,26 +74,40 @@ impl MethodRegistry {
|
|
|
70
74
|
);
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
///
|
|
77
|
+
/// Build the method resolution order (MRO) fallback chain for a receiver type.
|
|
74
78
|
///
|
|
75
|
-
///
|
|
76
|
-
///
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return Some(info);
|
|
84
|
-
}
|
|
79
|
+
/// Returns a list of types to search in order:
|
|
80
|
+
/// 1. Exact receiver type
|
|
81
|
+
/// 2. Generic → base class (e.g., Array[Integer] → Array)
|
|
82
|
+
/// 3. Object (for Instance/Generic types only)
|
|
83
|
+
/// 4. Kernel (for Instance/Generic types only)
|
|
84
|
+
fn fallback_chain(recv_ty: &Type) -> SmallVec<[Type; 4]> {
|
|
85
|
+
let mut chain = SmallVec::new();
|
|
86
|
+
chain.push(recv_ty.clone());
|
|
85
87
|
|
|
86
|
-
// For generic types, fall back to base class
|
|
87
88
|
if let Type::Generic { name, .. } = recv_ty {
|
|
88
|
-
|
|
89
|
-
return self.methods.get(&(base_type, method_name.to_string()));
|
|
89
|
+
chain.push(Type::Instance { name: name.clone() });
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
// NOTE: Kernel is a module, not a class. Represented as Type::Instance
|
|
93
|
+
// due to lack of Type::Module variant.
|
|
94
|
+
if matches!(recv_ty, Type::Instance { .. } | Type::Generic { .. }) {
|
|
95
|
+
chain.push(Type::instance(OBJECT_CLASS));
|
|
96
|
+
chain.push(Type::instance(KERNEL_MODULE));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
chain
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Resolve a method for a receiver type
|
|
103
|
+
///
|
|
104
|
+
/// Searches the MRO fallback chain: exact type → base class (for generics) → Object → Kernel.
|
|
105
|
+
/// For non-instance types (Singleton, Nil, Union, Bot), only exact match is attempted.
|
|
106
|
+
pub fn resolve(&self, recv_ty: &Type, method_name: &str) -> Option<&MethodInfo> {
|
|
107
|
+
let method_key = method_name.to_string();
|
|
108
|
+
Self::fallback_chain(recv_ty)
|
|
109
|
+
.into_iter()
|
|
110
|
+
.find_map(|ty| self.methods.get(&(ty, method_key.clone())))
|
|
93
111
|
}
|
|
94
112
|
}
|
|
95
113
|
|
|
@@ -142,4 +160,103 @@ mod tests {
|
|
|
142
160
|
assert_eq!(pvs[0], VertexId(20));
|
|
143
161
|
assert_eq!(pvs[1], VertexId(21));
|
|
144
162
|
}
|
|
163
|
+
|
|
164
|
+
// --- Object/Kernel fallback ---
|
|
165
|
+
|
|
166
|
+
#[test]
|
|
167
|
+
fn test_resolve_falls_back_to_object() {
|
|
168
|
+
let mut registry = MethodRegistry::new();
|
|
169
|
+
registry.register(Type::instance("Object"), "nil?", Type::instance("TrueClass"));
|
|
170
|
+
let info = registry.resolve(&Type::instance("CustomClass"), "nil?").unwrap();
|
|
171
|
+
assert_eq!(info.return_type.base_class_name(), Some("TrueClass"));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[test]
|
|
175
|
+
fn test_resolve_falls_back_to_kernel() {
|
|
176
|
+
let mut registry = MethodRegistry::new();
|
|
177
|
+
registry.register(Type::instance("Kernel"), "puts", Type::Nil);
|
|
178
|
+
let info = registry.resolve(&Type::instance("MyApp"), "puts").unwrap();
|
|
179
|
+
assert_eq!(info.return_type, Type::Nil);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[test]
|
|
183
|
+
fn test_resolve_object_before_kernel() {
|
|
184
|
+
let mut registry = MethodRegistry::new();
|
|
185
|
+
registry.register(Type::instance("Object"), "to_s", Type::string());
|
|
186
|
+
registry.register(Type::instance("Kernel"), "to_s", Type::integer());
|
|
187
|
+
let info = registry.resolve(&Type::instance("Anything"), "to_s").unwrap();
|
|
188
|
+
assert_eq!(info.return_type.base_class_name(), Some("String"));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn test_resolve_exact_match_over_fallback() {
|
|
193
|
+
let mut registry = MethodRegistry::new();
|
|
194
|
+
registry.register(Type::string(), "length", Type::integer());
|
|
195
|
+
registry.register(Type::instance("Object"), "length", Type::string());
|
|
196
|
+
let info = registry.resolve(&Type::string(), "length").unwrap();
|
|
197
|
+
assert_eq!(info.return_type.base_class_name(), Some("Integer"));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- Types that skip fallback ---
|
|
201
|
+
|
|
202
|
+
#[test]
|
|
203
|
+
fn test_singleton_type_skips_fallback() {
|
|
204
|
+
let mut registry = MethodRegistry::new();
|
|
205
|
+
registry.register(Type::instance("Kernel"), "puts", Type::Nil);
|
|
206
|
+
assert!(registry.resolve(&Type::singleton("User"), "puts").is_none());
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#[test]
|
|
210
|
+
fn test_nil_type_skips_fallback() {
|
|
211
|
+
let mut registry = MethodRegistry::new();
|
|
212
|
+
registry.register(Type::instance("Kernel"), "puts", Type::Nil);
|
|
213
|
+
assert!(registry.resolve(&Type::Nil, "puts").is_none());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[test]
|
|
217
|
+
fn test_union_type_skips_fallback() {
|
|
218
|
+
let mut registry = MethodRegistry::new();
|
|
219
|
+
registry.register(Type::instance("Kernel"), "puts", Type::Nil);
|
|
220
|
+
let union_ty = Type::Union(vec![Type::string(), Type::integer()]);
|
|
221
|
+
assert!(registry.resolve(&union_ty, "puts").is_none());
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[test]
|
|
225
|
+
fn test_bot_type_skips_fallback() {
|
|
226
|
+
let mut registry = MethodRegistry::new();
|
|
227
|
+
registry.register(Type::instance("Kernel"), "puts", Type::Nil);
|
|
228
|
+
assert!(registry.resolve(&Type::Bot, "puts").is_none());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// --- Generic type fallback chain ---
|
|
232
|
+
|
|
233
|
+
#[test]
|
|
234
|
+
fn test_resolve_generic_falls_back_to_kernel() {
|
|
235
|
+
let mut registry = MethodRegistry::new();
|
|
236
|
+
registry.register(Type::instance("Kernel"), "puts", Type::Nil);
|
|
237
|
+
let generic_type = Type::array_of(Type::integer());
|
|
238
|
+
let info = registry.resolve(&generic_type, "puts").unwrap();
|
|
239
|
+
assert_eq!(info.return_type, Type::Nil);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#[test]
|
|
243
|
+
fn test_resolve_generic_full_chain() {
|
|
244
|
+
// Verify the 4-step fallback: Generic[T] → Base → Object → Kernel
|
|
245
|
+
let mut registry = MethodRegistry::new();
|
|
246
|
+
registry.register(Type::instance("Kernel"), "object_id", Type::integer());
|
|
247
|
+
let generic_type = Type::array_of(Type::string());
|
|
248
|
+
// Array[String] → Array (none) → Object (none) → Kernel (exists)
|
|
249
|
+
let info = registry.resolve(&generic_type, "object_id").unwrap();
|
|
250
|
+
assert_eq!(info.return_type.base_class_name(), Some("Integer"));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// --- Namespaced class fallback ---
|
|
254
|
+
|
|
255
|
+
#[test]
|
|
256
|
+
fn test_resolve_namespaced_class_falls_back_to_object() {
|
|
257
|
+
let mut registry = MethodRegistry::new();
|
|
258
|
+
registry.register(Type::instance("Object"), "class", Type::string());
|
|
259
|
+
let info = registry.resolve(&Type::instance("Api::V1::User"), "class").unwrap();
|
|
260
|
+
assert_eq!(info.return_type.base_class_name(), Some("String"));
|
|
261
|
+
}
|
|
145
262
|
}
|
data/rust/src/env/scope.rs
CHANGED
|
@@ -41,6 +41,9 @@ pub struct Scope {
|
|
|
41
41
|
|
|
42
42
|
/// Class variables (class scope only)
|
|
43
43
|
pub class_vars: HashMap<String, VertexId>,
|
|
44
|
+
|
|
45
|
+
/// Constants (simple name → qualified name)
|
|
46
|
+
pub constants: HashMap<String, String>,
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
#[allow(dead_code)]
|
|
@@ -53,6 +56,7 @@ impl Scope {
|
|
|
53
56
|
local_vars: HashMap::new(),
|
|
54
57
|
instance_vars: HashMap::new(),
|
|
55
58
|
class_vars: HashMap::new(),
|
|
59
|
+
constants: HashMap::new(),
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
|
|
@@ -135,6 +139,18 @@ impl ScopeManager {
|
|
|
135
139
|
self.scopes.get_mut(&self.current_scope).unwrap()
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
/// Walk scopes from current scope up to the top-level, yielding each scope
|
|
143
|
+
fn walk_scopes(&self) -> impl Iterator<Item = &Scope> + '_ {
|
|
144
|
+
let scopes = &self.scopes;
|
|
145
|
+
let mut current = Some(self.current_scope);
|
|
146
|
+
std::iter::from_fn(move || {
|
|
147
|
+
let scope_id = current?;
|
|
148
|
+
let scope = scopes.get(&scope_id)?;
|
|
149
|
+
current = scope.parent;
|
|
150
|
+
Some(scope)
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
138
154
|
/// Get scope by ID
|
|
139
155
|
pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> {
|
|
140
156
|
self.scopes.get(&id)
|
|
@@ -147,103 +163,54 @@ impl ScopeManager {
|
|
|
147
163
|
|
|
148
164
|
/// Lookup variable in current scope or parent scopes
|
|
149
165
|
pub fn lookup_var(&self, name: &str) -> Option<VertexId> {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
while let Some(scope_id) = current {
|
|
153
|
-
if let Some(scope) = self.scopes.get(&scope_id) {
|
|
154
|
-
if let Some(vtx) = scope.get_local_var(name) {
|
|
155
|
-
return Some(vtx);
|
|
156
|
-
}
|
|
157
|
-
current = scope.parent;
|
|
158
|
-
} else {
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
166
|
+
self.walk_scopes().find_map(|scope| scope.get_local_var(name))
|
|
167
|
+
}
|
|
162
168
|
|
|
163
|
-
|
|
169
|
+
/// Lookup constant in current scope or parent scopes (simple name → qualified name)
|
|
170
|
+
pub fn lookup_constant(&self, simple_name: &str) -> Option<String> {
|
|
171
|
+
self.walk_scopes()
|
|
172
|
+
.find_map(|scope| scope.constants.get(simple_name).cloned())
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
/// Lookup instance variable in enclosing class scope
|
|
167
176
|
pub fn lookup_instance_var(&self, name: &str) -> Option<VertexId> {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if let Some(scope) = self.scopes.get(&scope_id) {
|
|
172
|
-
// Walk up to class scope
|
|
173
|
-
match &scope.kind {
|
|
174
|
-
ScopeKind::Class { .. } => {
|
|
175
|
-
return scope.get_instance_var(name);
|
|
176
|
-
}
|
|
177
|
-
_ => {
|
|
178
|
-
current = scope.parent;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
None
|
|
177
|
+
self.walk_scopes()
|
|
178
|
+
.find(|scope| matches!(&scope.kind, ScopeKind::Class { .. }))
|
|
179
|
+
.and_then(|scope| scope.get_instance_var(name))
|
|
187
180
|
}
|
|
188
181
|
|
|
189
182
|
/// Set instance variable in enclosing class scope
|
|
190
183
|
pub fn set_instance_var_in_class(&mut self, name: String, vtx: VertexId) {
|
|
191
|
-
let
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
ScopeKind::Class { .. } => {
|
|
198
|
-
if let Some(class_scope) = self.scopes.get_mut(&scope_id) {
|
|
199
|
-
class_scope.set_instance_var(name, vtx);
|
|
200
|
-
}
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
_ => {
|
|
204
|
-
current = scope.parent;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
} else {
|
|
208
|
-
break;
|
|
184
|
+
let class_scope_id = self.walk_scopes()
|
|
185
|
+
.find(|scope| matches!(&scope.kind, ScopeKind::Class { .. }))
|
|
186
|
+
.map(|scope| scope.id);
|
|
187
|
+
if let Some(scope_id) = class_scope_id {
|
|
188
|
+
if let Some(scope) = self.scopes.get_mut(&scope_id) {
|
|
189
|
+
scope.set_instance_var(name, vtx);
|
|
209
190
|
}
|
|
210
191
|
}
|
|
211
192
|
}
|
|
212
193
|
|
|
213
194
|
/// Get current class name (simple name, not qualified)
|
|
214
195
|
pub fn current_class_name(&self) -> Option<String> {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if let Some(scope) = self.scopes.get(&scope_id) {
|
|
219
|
-
if let ScopeKind::Class { name, .. } = &scope.kind {
|
|
220
|
-
return Some(name.clone());
|
|
221
|
-
}
|
|
222
|
-
current = scope.parent;
|
|
196
|
+
self.walk_scopes().find_map(|scope| {
|
|
197
|
+
if let ScopeKind::Class { name, .. } = &scope.kind {
|
|
198
|
+
Some(name.clone())
|
|
223
199
|
} else {
|
|
224
|
-
|
|
200
|
+
None
|
|
225
201
|
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
None
|
|
202
|
+
})
|
|
229
203
|
}
|
|
230
204
|
|
|
231
205
|
/// Get current module name (simple name, not qualified)
|
|
232
206
|
pub fn current_module_name(&self) -> Option<String> {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if let Some(scope) = self.scopes.get(&scope_id) {
|
|
237
|
-
if let ScopeKind::Module { name } = &scope.kind {
|
|
238
|
-
return Some(name.clone());
|
|
239
|
-
}
|
|
240
|
-
current = scope.parent;
|
|
207
|
+
self.walk_scopes().find_map(|scope| {
|
|
208
|
+
if let ScopeKind::Module { name } = &scope.kind {
|
|
209
|
+
Some(name.clone())
|
|
241
210
|
} else {
|
|
242
|
-
|
|
211
|
+
None
|
|
243
212
|
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
None
|
|
213
|
+
})
|
|
247
214
|
}
|
|
248
215
|
|
|
249
216
|
/// Get current fully qualified name by traversing all parent class/module scopes
|
|
@@ -260,36 +227,12 @@ impl ScopeManager {
|
|
|
260
227
|
/// ```
|
|
261
228
|
/// When inside `greet`, this returns `Some("Api::V1::User")`
|
|
262
229
|
pub fn current_qualified_name(&self) -> Option<String> {
|
|
263
|
-
let mut path_segments: Vec
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
match &scope.kind {
|
|
270
|
-
ScopeKind::Class { name, .. } => {
|
|
271
|
-
// If the name already contains ::, it's a qualified name from AST
|
|
272
|
-
// (e.g., `class Api::User` defined at top level)
|
|
273
|
-
if name.contains("::") {
|
|
274
|
-
path_segments.push(name.clone());
|
|
275
|
-
} else {
|
|
276
|
-
path_segments.push(name.clone());
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
ScopeKind::Module { name } => {
|
|
280
|
-
if name.contains("::") {
|
|
281
|
-
path_segments.push(name.clone());
|
|
282
|
-
} else {
|
|
283
|
-
path_segments.push(name.clone());
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
_ => {}
|
|
287
|
-
}
|
|
288
|
-
current = scope.parent;
|
|
289
|
-
} else {
|
|
290
|
-
break;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
230
|
+
let mut path_segments: Vec<&str> = self.walk_scopes()
|
|
231
|
+
.filter_map(|scope| match &scope.kind {
|
|
232
|
+
ScopeKind::Class { name, .. } | ScopeKind::Module { name } => Some(name.as_str()),
|
|
233
|
+
_ => None,
|
|
234
|
+
})
|
|
235
|
+
.collect();
|
|
293
236
|
|
|
294
237
|
if path_segments.is_empty() {
|
|
295
238
|
return None;
|
|
@@ -297,76 +240,35 @@ impl ScopeManager {
|
|
|
297
240
|
|
|
298
241
|
// Reverse to get from outermost to innermost
|
|
299
242
|
path_segments.reverse();
|
|
300
|
-
|
|
301
|
-
// Join all segments, handling cases where segments may already contain ::
|
|
302
|
-
let mut result = String::new();
|
|
303
|
-
for segment in path_segments {
|
|
304
|
-
if !result.is_empty() {
|
|
305
|
-
result.push_str("::");
|
|
306
|
-
}
|
|
307
|
-
result.push_str(&segment);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
Some(result)
|
|
243
|
+
Some(path_segments.join("::"))
|
|
311
244
|
}
|
|
312
245
|
|
|
313
246
|
/// Get return_vertex from the nearest enclosing method scope
|
|
314
247
|
pub fn current_method_return_vertex(&self) -> Option<VertexId> {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if let ScopeKind::Method { return_vertex, .. } = &scope.kind {
|
|
319
|
-
return *return_vertex;
|
|
320
|
-
}
|
|
321
|
-
current = scope.parent;
|
|
248
|
+
self.walk_scopes().find_map(|scope| {
|
|
249
|
+
if let ScopeKind::Method { return_vertex, .. } = &scope.kind {
|
|
250
|
+
*return_vertex
|
|
322
251
|
} else {
|
|
323
|
-
|
|
252
|
+
None
|
|
324
253
|
}
|
|
325
|
-
}
|
|
326
|
-
None
|
|
254
|
+
})
|
|
327
255
|
}
|
|
328
256
|
|
|
329
257
|
/// Lookup instance variable in enclosing module scope
|
|
330
258
|
pub fn lookup_instance_var_in_module(&self, name: &str) -> Option<VertexId> {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if let Some(scope) = self.scopes.get(&scope_id) {
|
|
335
|
-
match &scope.kind {
|
|
336
|
-
ScopeKind::Module { .. } => {
|
|
337
|
-
return scope.get_instance_var(name);
|
|
338
|
-
}
|
|
339
|
-
_ => {
|
|
340
|
-
current = scope.parent;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
} else {
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
None
|
|
259
|
+
self.walk_scopes()
|
|
260
|
+
.find(|scope| matches!(&scope.kind, ScopeKind::Module { .. }))
|
|
261
|
+
.and_then(|scope| scope.get_instance_var(name))
|
|
349
262
|
}
|
|
350
263
|
|
|
351
264
|
/// Set instance variable in enclosing module scope
|
|
352
265
|
pub fn set_instance_var_in_module(&mut self, name: String, vtx: VertexId) {
|
|
353
|
-
let
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if let Some(module_scope) = self.scopes.get_mut(&scope_id) {
|
|
360
|
-
module_scope.set_instance_var(name, vtx);
|
|
361
|
-
}
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
_ => {
|
|
365
|
-
current = scope.parent;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
} else {
|
|
369
|
-
break;
|
|
266
|
+
let module_scope_id = self.walk_scopes()
|
|
267
|
+
.find(|scope| matches!(&scope.kind, ScopeKind::Module { .. }))
|
|
268
|
+
.map(|scope| scope.id);
|
|
269
|
+
if let Some(scope_id) = module_scope_id {
|
|
270
|
+
if let Some(scope) = self.scopes.get_mut(&scope_id) {
|
|
271
|
+
scope.set_instance_var(name, vtx);
|
|
370
272
|
}
|
|
371
273
|
}
|
|
372
274
|
}
|
|
@@ -614,4 +516,72 @@ mod tests {
|
|
|
614
516
|
// At top level, no class/module
|
|
615
517
|
assert_eq!(sm.current_qualified_name(), None);
|
|
616
518
|
}
|
|
519
|
+
|
|
520
|
+
#[test]
|
|
521
|
+
fn test_constant_registration_and_lookup() {
|
|
522
|
+
let mut sm = ScopeManager::new();
|
|
523
|
+
|
|
524
|
+
// module Api
|
|
525
|
+
sm.current_scope_mut().constants.insert("Api".to_string(), "Api".to_string());
|
|
526
|
+
let api_id = sm.new_scope(ScopeKind::Module { name: "Api".to_string() });
|
|
527
|
+
sm.enter_scope(api_id);
|
|
528
|
+
|
|
529
|
+
// class User (inside Api) — register in parent scope (Api)
|
|
530
|
+
sm.current_scope_mut().constants.insert("User".to_string(), "Api::User".to_string());
|
|
531
|
+
let user_id = sm.new_scope(ScopeKind::Class {
|
|
532
|
+
name: "User".to_string(),
|
|
533
|
+
superclass: None,
|
|
534
|
+
});
|
|
535
|
+
sm.enter_scope(user_id);
|
|
536
|
+
|
|
537
|
+
assert_eq!(sm.lookup_constant("User"), Some("Api::User".to_string()));
|
|
538
|
+
assert_eq!(sm.lookup_constant("Api"), Some("Api".to_string()));
|
|
539
|
+
assert_eq!(sm.lookup_constant("Unknown"), None);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
#[test]
|
|
543
|
+
fn test_constant_lookup_from_method_scope() {
|
|
544
|
+
let mut sm = ScopeManager::new();
|
|
545
|
+
|
|
546
|
+
sm.current_scope_mut().constants.insert("Api".to_string(), "Api".to_string());
|
|
547
|
+
let api_id = sm.new_scope(ScopeKind::Module { name: "Api".to_string() });
|
|
548
|
+
sm.enter_scope(api_id);
|
|
549
|
+
|
|
550
|
+
sm.current_scope_mut().constants.insert("User".to_string(), "Api::User".to_string());
|
|
551
|
+
let user_id = sm.new_scope(ScopeKind::Class {
|
|
552
|
+
name: "User".to_string(),
|
|
553
|
+
superclass: None,
|
|
554
|
+
});
|
|
555
|
+
sm.enter_scope(user_id);
|
|
556
|
+
|
|
557
|
+
let method_id = sm.new_scope(ScopeKind::Method {
|
|
558
|
+
name: "greet".to_string(),
|
|
559
|
+
receiver_type: None,
|
|
560
|
+
return_vertex: None,
|
|
561
|
+
});
|
|
562
|
+
sm.enter_scope(method_id);
|
|
563
|
+
|
|
564
|
+
// Should find constant by traversing parent scopes from method scope
|
|
565
|
+
assert_eq!(sm.lookup_constant("User"), Some("Api::User".to_string()));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
#[test]
|
|
569
|
+
fn test_constant_same_name_different_namespaces() {
|
|
570
|
+
let mut sm = ScopeManager::new();
|
|
571
|
+
|
|
572
|
+
// module Api
|
|
573
|
+
let api_id = sm.new_scope(ScopeKind::Module { name: "Api".to_string() });
|
|
574
|
+
sm.enter_scope(api_id);
|
|
575
|
+
sm.current_scope_mut().constants.insert("User".to_string(), "Api::User".to_string());
|
|
576
|
+
|
|
577
|
+
sm.exit_scope();
|
|
578
|
+
|
|
579
|
+
// module Admin
|
|
580
|
+
let admin_id = sm.new_scope(ScopeKind::Module { name: "Admin".to_string() });
|
|
581
|
+
sm.enter_scope(admin_id);
|
|
582
|
+
sm.current_scope_mut().constants.insert("User".to_string(), "Admin::User".to_string());
|
|
583
|
+
|
|
584
|
+
// Inside Admin scope, User should resolve to Admin::User
|
|
585
|
+
assert_eq!(sm.lookup_constant("User"), Some("Admin::User".to_string()));
|
|
586
|
+
}
|
|
617
587
|
}
|
data/rust/src/graph/box.rs
CHANGED
|
@@ -16,6 +16,18 @@ pub trait BoxTrait: Send + Sync {
|
|
|
16
16
|
fn ret(&self) -> VertexId;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/// Propagate argument types to parameter vertices by adding edges
|
|
20
|
+
/// from each argument vertex to the corresponding parameter vertex.
|
|
21
|
+
fn propagate_arguments(
|
|
22
|
+
arg_vtxs: &[VertexId],
|
|
23
|
+
param_vtxs: Option<&[VertexId]>,
|
|
24
|
+
changes: &mut ChangeSet,
|
|
25
|
+
) {
|
|
26
|
+
for (arg_vtx, param_vtx) in arg_vtxs.iter().zip(param_vtxs.unwrap_or_default()) {
|
|
27
|
+
changes.add_edge(*arg_vtx, *param_vtx);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
/// Box representing a method call
|
|
20
32
|
#[allow(dead_code)]
|
|
21
33
|
pub struct MethodCallBox {
|
|
@@ -51,6 +63,77 @@ impl MethodCallBox {
|
|
|
51
63
|
reschedule_count: 0,
|
|
52
64
|
}
|
|
53
65
|
}
|
|
66
|
+
|
|
67
|
+
/// Reschedule this box for re-execution if the limit hasn't been reached.
|
|
68
|
+
/// Handles cases where the receiver has no types yet (e.g., block parameters
|
|
69
|
+
/// that get typed by a later box). If max reschedules are reached, the box
|
|
70
|
+
/// is silently dropped (receiver type remains unknown).
|
|
71
|
+
fn try_reschedule(&mut self, changes: &mut ChangeSet) {
|
|
72
|
+
if self.reschedule_count < MAX_RESCHEDULE_COUNT {
|
|
73
|
+
self.reschedule_count += 1;
|
|
74
|
+
changes.reschedule(self.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn process_recv_type(
|
|
79
|
+
&self,
|
|
80
|
+
recv_ty: &Type,
|
|
81
|
+
genv: &mut GlobalEnv,
|
|
82
|
+
changes: &mut ChangeSet,
|
|
83
|
+
) {
|
|
84
|
+
if let Some(method_info) = genv.resolve_method(recv_ty, &self.method_name) {
|
|
85
|
+
if let Some(return_vtx) = method_info.return_vertex {
|
|
86
|
+
// User-defined method: connect body's return vertex to call site
|
|
87
|
+
changes.add_edge(return_vtx, self.ret);
|
|
88
|
+
propagate_arguments(
|
|
89
|
+
&self.arg_vtxs,
|
|
90
|
+
method_info.param_vertices.as_deref(),
|
|
91
|
+
changes,
|
|
92
|
+
);
|
|
93
|
+
} else {
|
|
94
|
+
// Builtin/RBS method: create source with fixed return type
|
|
95
|
+
let ret_src_id = genv.new_source(method_info.return_type.clone());
|
|
96
|
+
changes.add_edge(ret_src_id, self.ret);
|
|
97
|
+
}
|
|
98
|
+
} else if self.method_name == "new" {
|
|
99
|
+
self.handle_new_call(recv_ty, genv, changes);
|
|
100
|
+
} else if !matches!(recv_ty, Type::Singleton { .. }) {
|
|
101
|
+
// Singleton types with unresolved methods are silently skipped;
|
|
102
|
+
// these are typically RBS class methods not yet supported.
|
|
103
|
+
self.report_type_error(recv_ty, genv);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Handle `.new` calls: singleton(Foo)#new produces instance(Foo),
|
|
108
|
+
/// and propagates arguments to the `initialize` method's parameters.
|
|
109
|
+
fn handle_new_call(
|
|
110
|
+
&self,
|
|
111
|
+
recv_ty: &Type,
|
|
112
|
+
genv: &mut GlobalEnv,
|
|
113
|
+
changes: &mut ChangeSet,
|
|
114
|
+
) {
|
|
115
|
+
if let Type::Singleton { name } = recv_ty {
|
|
116
|
+
let instance_type = Type::instance(name.full_name());
|
|
117
|
+
|
|
118
|
+
let ret_src = genv.new_source(instance_type.clone());
|
|
119
|
+
changes.add_edge(ret_src, self.ret);
|
|
120
|
+
|
|
121
|
+
let init_params = genv
|
|
122
|
+
.resolve_method(&instance_type, "initialize")
|
|
123
|
+
.and_then(|info| info.param_vertices.as_deref());
|
|
124
|
+
propagate_arguments(&self.arg_vtxs, init_params, changes);
|
|
125
|
+
} else {
|
|
126
|
+
self.report_type_error(recv_ty, genv);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fn report_type_error(&self, recv_ty: &Type, genv: &mut GlobalEnv) {
|
|
131
|
+
genv.record_type_error(
|
|
132
|
+
recv_ty.clone(),
|
|
133
|
+
self.method_name.clone(),
|
|
134
|
+
self.location.clone(),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
54
137
|
}
|
|
55
138
|
|
|
56
139
|
impl BoxTrait for MethodCallBox {
|
|
@@ -63,88 +146,17 @@ impl BoxTrait for MethodCallBox {
|
|
|
63
146
|
}
|
|
64
147
|
|
|
65
148
|
fn run(&mut self, genv: &mut GlobalEnv, changes: &mut ChangeSet) {
|
|
66
|
-
|
|
67
|
-
let recv_types: Vec<Type> = if let Some(recv_vertex) = genv.get_vertex(self.recv) {
|
|
68
|
-
// Vertex case: may have multiple types
|
|
69
|
-
recv_vertex.types.keys().cloned().collect()
|
|
70
|
-
} else if let Some(recv_source) = genv.get_source(self.recv) {
|
|
71
|
-
// Source case: has one fixed type (e.g., literals)
|
|
72
|
-
vec![recv_source.ty.clone()]
|
|
73
|
-
} else {
|
|
74
|
-
// Receiver not found
|
|
149
|
+
let Some(recv_types) = genv.get_receiver_types(self.recv) else {
|
|
75
150
|
return;
|
|
76
151
|
};
|
|
77
152
|
|
|
78
|
-
// If receiver has no types yet, reschedule this box for later
|
|
79
|
-
// This handles cases like block parameters that are typed later
|
|
80
153
|
if recv_types.is_empty() {
|
|
81
|
-
|
|
82
|
-
self.reschedule_count += 1;
|
|
83
|
-
changes.reschedule(self.id);
|
|
84
|
-
}
|
|
85
|
-
// If max reschedules reached, just skip (receiver type is unknown)
|
|
154
|
+
self.try_reschedule(changes);
|
|
86
155
|
return;
|
|
87
156
|
}
|
|
88
157
|
|
|
89
158
|
for recv_ty in recv_types {
|
|
90
|
-
|
|
91
|
-
if let Some(method_info) = genv.resolve_method(&recv_ty, &self.method_name) {
|
|
92
|
-
if let Some(return_vtx) = method_info.return_vertex {
|
|
93
|
-
// User-defined: edge from body's last expr → call site return
|
|
94
|
-
changes.add_edge(return_vtx, self.ret);
|
|
95
|
-
|
|
96
|
-
// Propagate argument types to parameter vertices
|
|
97
|
-
if let Some(param_vtxs) = &method_info.param_vertices {
|
|
98
|
-
for (i, param_vtx) in param_vtxs.iter().enumerate() {
|
|
99
|
-
if let Some(arg_vtx) = self.arg_vtxs.get(i) {
|
|
100
|
-
changes.add_edge(*arg_vtx, *param_vtx);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
// RBS/builtin: create Source with fixed return type
|
|
106
|
-
let ret_src_id = genv.new_source(method_info.return_type.clone());
|
|
107
|
-
changes.add_edge(ret_src_id, self.ret);
|
|
108
|
-
}
|
|
109
|
-
} else if self.method_name == "new" {
|
|
110
|
-
if let Type::Singleton { name } = &recv_ty {
|
|
111
|
-
// singleton(User)#new → instance(User)
|
|
112
|
-
let instance_type = Type::instance(name.full_name());
|
|
113
|
-
let ret_src = genv.new_source(instance_type.clone());
|
|
114
|
-
changes.add_edge(ret_src, self.ret);
|
|
115
|
-
|
|
116
|
-
// Propagate arguments to initialize parameters
|
|
117
|
-
if let Some(init_info) = genv.resolve_method(&instance_type, "initialize") {
|
|
118
|
-
if let Some(param_vtxs) = &init_info.param_vertices {
|
|
119
|
-
for (i, param_vtx) in param_vtxs.iter().enumerate() {
|
|
120
|
-
if let Some(arg_vtx) = self.arg_vtxs.get(i) {
|
|
121
|
-
changes.add_edge(*arg_vtx, *param_vtx);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
// Non-singleton .new: record error
|
|
129
|
-
genv.record_type_error(
|
|
130
|
-
recv_ty.clone(),
|
|
131
|
-
self.method_name.clone(),
|
|
132
|
-
self.location.clone(),
|
|
133
|
-
);
|
|
134
|
-
} else if matches!(&recv_ty, Type::Singleton { .. }) {
|
|
135
|
-
// Skip error for unknown class methods on Singleton types.
|
|
136
|
-
// User-defined class methods (def self.foo) are resolved by
|
|
137
|
-
// resolve_method above. Only unresolved methods reach here
|
|
138
|
-
// (e.g., RBS class methods not yet supported).
|
|
139
|
-
continue;
|
|
140
|
-
} else {
|
|
141
|
-
// Record type error for diagnostic reporting
|
|
142
|
-
genv.record_type_error(
|
|
143
|
-
recv_ty.clone(),
|
|
144
|
-
self.method_name.clone(),
|
|
145
|
-
self.location.clone(),
|
|
146
|
-
);
|
|
147
|
-
}
|
|
159
|
+
self.process_recv_type(&recv_ty, genv, changes);
|
|
148
160
|
}
|
|
149
161
|
}
|
|
150
162
|
}
|
|
@@ -241,12 +253,7 @@ impl BoxTrait for BlockParameterTypeBox {
|
|
|
241
253
|
}
|
|
242
254
|
|
|
243
255
|
fn run(&mut self, genv: &mut GlobalEnv, changes: &mut ChangeSet) {
|
|
244
|
-
|
|
245
|
-
let recv_types: Vec<Type> = if let Some(recv_vertex) = genv.get_vertex(self.recv_vtx) {
|
|
246
|
-
recv_vertex.types.keys().cloned().collect()
|
|
247
|
-
} else if let Some(recv_source) = genv.get_source(self.recv_vtx) {
|
|
248
|
-
vec![recv_source.ty.clone()]
|
|
249
|
-
} else {
|
|
256
|
+
let Some(recv_types) = genv.get_receiver_types(self.recv_vtx) else {
|
|
250
257
|
return;
|
|
251
258
|
};
|
|
252
259
|
|