rubydex 0.1.0.beta12-aarch64-linux
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 +7 -0
- data/LICENSE.txt +23 -0
- data/README.md +125 -0
- data/THIRD_PARTY_LICENSES.html +4562 -0
- data/exe/rdx +47 -0
- data/ext/rubydex/declaration.c +453 -0
- data/ext/rubydex/declaration.h +23 -0
- data/ext/rubydex/definition.c +284 -0
- data/ext/rubydex/definition.h +28 -0
- data/ext/rubydex/diagnostic.c +6 -0
- data/ext/rubydex/diagnostic.h +11 -0
- data/ext/rubydex/document.c +97 -0
- data/ext/rubydex/document.h +10 -0
- data/ext/rubydex/extconf.rb +138 -0
- data/ext/rubydex/graph.c +681 -0
- data/ext/rubydex/graph.h +10 -0
- data/ext/rubydex/handle.h +44 -0
- data/ext/rubydex/location.c +22 -0
- data/ext/rubydex/location.h +15 -0
- data/ext/rubydex/reference.c +123 -0
- data/ext/rubydex/reference.h +15 -0
- data/ext/rubydex/rubydex.c +22 -0
- data/ext/rubydex/utils.c +108 -0
- data/ext/rubydex/utils.h +34 -0
- data/lib/rubydex/3.2/rubydex.so +0 -0
- data/lib/rubydex/3.3/rubydex.so +0 -0
- data/lib/rubydex/3.4/rubydex.so +0 -0
- data/lib/rubydex/4.0/rubydex.so +0 -0
- data/lib/rubydex/comment.rb +17 -0
- data/lib/rubydex/diagnostic.rb +21 -0
- data/lib/rubydex/failures.rb +15 -0
- data/lib/rubydex/graph.rb +98 -0
- data/lib/rubydex/keyword.rb +17 -0
- data/lib/rubydex/keyword_parameter.rb +13 -0
- data/lib/rubydex/librubydex_sys.so +0 -0
- data/lib/rubydex/location.rb +90 -0
- data/lib/rubydex/mixin.rb +22 -0
- data/lib/rubydex/version.rb +5 -0
- data/lib/rubydex.rb +23 -0
- data/rbi/rubydex.rbi +422 -0
- data/rust/Cargo.lock +1851 -0
- data/rust/Cargo.toml +29 -0
- data/rust/about.hbs +78 -0
- data/rust/about.toml +10 -0
- data/rust/rubydex/Cargo.toml +42 -0
- data/rust/rubydex/src/compile_assertions.rs +13 -0
- data/rust/rubydex/src/diagnostic.rs +110 -0
- data/rust/rubydex/src/errors.rs +28 -0
- data/rust/rubydex/src/indexing/local_graph.rs +224 -0
- data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -0
- data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -0
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
- data/rust/rubydex/src/indexing.rs +210 -0
- data/rust/rubydex/src/integrity.rs +279 -0
- data/rust/rubydex/src/job_queue.rs +205 -0
- data/rust/rubydex/src/lib.rs +17 -0
- data/rust/rubydex/src/listing.rs +371 -0
- data/rust/rubydex/src/main.rs +160 -0
- data/rust/rubydex/src/model/built_in.rs +83 -0
- data/rust/rubydex/src/model/comment.rs +24 -0
- data/rust/rubydex/src/model/declaration.rs +671 -0
- data/rust/rubydex/src/model/definitions.rs +1682 -0
- data/rust/rubydex/src/model/document.rs +222 -0
- data/rust/rubydex/src/model/encoding.rs +22 -0
- data/rust/rubydex/src/model/graph.rs +3754 -0
- data/rust/rubydex/src/model/id.rs +110 -0
- data/rust/rubydex/src/model/identity_maps.rs +58 -0
- data/rust/rubydex/src/model/ids.rs +60 -0
- data/rust/rubydex/src/model/keywords.rs +256 -0
- data/rust/rubydex/src/model/name.rs +298 -0
- data/rust/rubydex/src/model/references.rs +111 -0
- data/rust/rubydex/src/model/string_ref.rs +50 -0
- data/rust/rubydex/src/model/visibility.rs +41 -0
- data/rust/rubydex/src/model.rs +15 -0
- data/rust/rubydex/src/offset.rs +147 -0
- data/rust/rubydex/src/position.rs +6 -0
- data/rust/rubydex/src/query.rs +1841 -0
- data/rust/rubydex/src/resolution.rs +6517 -0
- data/rust/rubydex/src/stats/memory.rs +71 -0
- data/rust/rubydex/src/stats/orphan_report.rs +264 -0
- data/rust/rubydex/src/stats/timer.rs +127 -0
- data/rust/rubydex/src/stats.rs +11 -0
- data/rust/rubydex/src/test_utils/context.rs +226 -0
- data/rust/rubydex/src/test_utils/graph_test.rs +730 -0
- data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -0
- data/rust/rubydex/src/test_utils.rs +52 -0
- data/rust/rubydex/src/visualization/dot.rs +192 -0
- data/rust/rubydex/src/visualization.rs +6 -0
- data/rust/rubydex/tests/cli.rs +185 -0
- data/rust/rubydex-mcp/Cargo.toml +28 -0
- data/rust/rubydex-mcp/src/main.rs +48 -0
- data/rust/rubydex-mcp/src/server.rs +1145 -0
- data/rust/rubydex-mcp/src/tools.rs +49 -0
- data/rust/rubydex-mcp/tests/mcp.rs +302 -0
- data/rust/rubydex-sys/Cargo.toml +20 -0
- data/rust/rubydex-sys/build.rs +14 -0
- data/rust/rubydex-sys/cbindgen.toml +12 -0
- data/rust/rubydex-sys/src/declaration_api.rs +485 -0
- data/rust/rubydex-sys/src/definition_api.rs +443 -0
- data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
- data/rust/rubydex-sys/src/document_api.rs +85 -0
- data/rust/rubydex-sys/src/graph_api.rs +948 -0
- data/rust/rubydex-sys/src/lib.rs +79 -0
- data/rust/rubydex-sys/src/location_api.rs +79 -0
- data/rust/rubydex-sys/src/name_api.rs +135 -0
- data/rust/rubydex-sys/src/reference_api.rs +267 -0
- data/rust/rubydex-sys/src/utils.rs +70 -0
- data/rust/rustfmt.toml +2 -0
- metadata +159 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
use crate::{
|
|
2
|
+
errors::Errors,
|
|
3
|
+
indexing::{local_graph::LocalGraph, rbs_indexer::RBSIndexer, ruby_indexer::RubyIndexer},
|
|
4
|
+
job_queue::{Job, JobQueue},
|
|
5
|
+
model::graph::Graph,
|
|
6
|
+
};
|
|
7
|
+
use crossbeam_channel::{Sender, unbounded};
|
|
8
|
+
use std::{ffi::OsStr, fs, path::PathBuf, sync::Arc};
|
|
9
|
+
use url::Url;
|
|
10
|
+
|
|
11
|
+
pub mod local_graph;
|
|
12
|
+
pub mod rbs_indexer;
|
|
13
|
+
pub mod ruby_indexer;
|
|
14
|
+
|
|
15
|
+
/// The language of a source document, used to dispatch to the appropriate indexer
|
|
16
|
+
pub enum LanguageId {
|
|
17
|
+
Ruby,
|
|
18
|
+
Rbs,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl From<&OsStr> for LanguageId {
|
|
22
|
+
fn from(ext: &OsStr) -> Self {
|
|
23
|
+
if ext == "rbs" { Self::Rbs } else { Self::Ruby }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl LanguageId {
|
|
28
|
+
/// Determines the language from an LSP language ID string.
|
|
29
|
+
///
|
|
30
|
+
/// # Errors
|
|
31
|
+
///
|
|
32
|
+
/// Returns an error if the language ID is not recognized.
|
|
33
|
+
pub fn from_language_id(language_id: &str) -> Result<Self, Errors> {
|
|
34
|
+
match language_id {
|
|
35
|
+
"ruby" => Ok(Self::Ruby),
|
|
36
|
+
"rbs" => Ok(Self::Rbs),
|
|
37
|
+
_ => Err(Errors::FileError(format!("Unsupported language_id `{language_id}`"))),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Job that indexes a single file
|
|
43
|
+
pub struct IndexingJob {
|
|
44
|
+
path: PathBuf,
|
|
45
|
+
local_graph_tx: Sender<LocalGraph>,
|
|
46
|
+
errors_tx: Sender<Errors>,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
impl IndexingJob {
|
|
50
|
+
#[must_use]
|
|
51
|
+
pub fn new(path: PathBuf, local_graph_tx: Sender<LocalGraph>, errors_tx: Sender<Errors>) -> Self {
|
|
52
|
+
Self {
|
|
53
|
+
path,
|
|
54
|
+
local_graph_tx,
|
|
55
|
+
errors_tx,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn send_error(&self, error: Errors) {
|
|
60
|
+
self.errors_tx
|
|
61
|
+
.send(error)
|
|
62
|
+
.expect("errors receiver dropped before run completion");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
impl Job for IndexingJob {
|
|
67
|
+
fn run(&self) {
|
|
68
|
+
let Ok(source) = fs::read_to_string(&self.path) else {
|
|
69
|
+
self.send_error(Errors::FileError(format!(
|
|
70
|
+
"Failed to read file `{}`",
|
|
71
|
+
self.path.display()
|
|
72
|
+
)));
|
|
73
|
+
|
|
74
|
+
return;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let Ok(url) = Url::from_file_path(&self.path) else {
|
|
78
|
+
self.send_error(Errors::FileError(format!(
|
|
79
|
+
"Couldn't build URI from path `{}`",
|
|
80
|
+
self.path.display()
|
|
81
|
+
)));
|
|
82
|
+
|
|
83
|
+
return;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let language = self.path.extension().map_or(LanguageId::Ruby, LanguageId::from);
|
|
87
|
+
let local_graph = build_local_graph(url.to_string(), &source, &language);
|
|
88
|
+
|
|
89
|
+
self.local_graph_tx
|
|
90
|
+
.send(local_graph)
|
|
91
|
+
.expect("graph receiver dropped before merge");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Indexes a single source string in memory, dispatching to the appropriate indexer based on `language_id`.
|
|
96
|
+
pub fn index_source(graph: &mut Graph, uri: &str, source: &str, language_id: &LanguageId) {
|
|
97
|
+
let local_graph = build_local_graph(uri.to_string(), source, language_id);
|
|
98
|
+
graph.consume_document_changes(local_graph);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Indexes the given paths, reading the content from disk and populating the given `Graph` instance.
|
|
102
|
+
///
|
|
103
|
+
/// # Panics
|
|
104
|
+
///
|
|
105
|
+
/// Will panic if the graph cannot be wrapped in an Arc<Mutex<>>
|
|
106
|
+
pub fn index_files(graph: &mut Graph, paths: Vec<PathBuf>) -> Vec<Errors> {
|
|
107
|
+
let queue = Arc::new(JobQueue::new());
|
|
108
|
+
let (local_graphs_tx, local_graphs_rx) = unbounded();
|
|
109
|
+
let (errors_tx, errors_rx) = unbounded();
|
|
110
|
+
|
|
111
|
+
for path in paths {
|
|
112
|
+
queue.push(Box::new(IndexingJob::new(
|
|
113
|
+
path,
|
|
114
|
+
local_graphs_tx.clone(),
|
|
115
|
+
errors_tx.clone(),
|
|
116
|
+
)));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
drop(local_graphs_tx);
|
|
120
|
+
drop(errors_tx);
|
|
121
|
+
|
|
122
|
+
let handles = JobQueue::run_without_waiting(&queue);
|
|
123
|
+
|
|
124
|
+
// Merge graphs as they arrive, overlapping with indexing work on other threads.
|
|
125
|
+
while let Ok(local_graph) = local_graphs_rx.recv() {
|
|
126
|
+
graph.consume_document_changes(local_graph);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for handle in handles {
|
|
130
|
+
handle.join().expect("Worker thread panicked");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
errors_rx.iter().collect()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Indexes a source string using the appropriate indexer for the given language.
|
|
137
|
+
fn build_local_graph(uri: String, source: &str, language: &LanguageId) -> LocalGraph {
|
|
138
|
+
match language {
|
|
139
|
+
LanguageId::Ruby => {
|
|
140
|
+
let mut indexer = RubyIndexer::new(uri, source);
|
|
141
|
+
indexer.index();
|
|
142
|
+
indexer.local_graph()
|
|
143
|
+
}
|
|
144
|
+
LanguageId::Rbs => {
|
|
145
|
+
let mut indexer = RBSIndexer::new(uri, source);
|
|
146
|
+
indexer.index();
|
|
147
|
+
indexer.local_graph()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[cfg(test)]
|
|
153
|
+
mod tests {
|
|
154
|
+
use std::path::PathBuf;
|
|
155
|
+
|
|
156
|
+
use super::*;
|
|
157
|
+
use crate::test_utils::Context;
|
|
158
|
+
use std::path::Path;
|
|
159
|
+
|
|
160
|
+
#[test]
|
|
161
|
+
fn index_relative_paths() {
|
|
162
|
+
let relative_path = Path::new("foo").join("bar.rb");
|
|
163
|
+
let context = Context::new();
|
|
164
|
+
context.touch(&relative_path);
|
|
165
|
+
|
|
166
|
+
let working_directory = std::env::current_dir().unwrap();
|
|
167
|
+
let absolute_path = context.absolute_path_to("foo/bar.rb");
|
|
168
|
+
|
|
169
|
+
let mut dots = PathBuf::from("..");
|
|
170
|
+
|
|
171
|
+
for _ in 0..working_directory.components().count() - 1 {
|
|
172
|
+
dots = dots.join("..");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let relative_to_pwd = &dots.join(absolute_path);
|
|
176
|
+
|
|
177
|
+
let mut graph = Graph::new();
|
|
178
|
+
let errors = index_files(&mut graph, vec![relative_to_pwd.clone()]);
|
|
179
|
+
|
|
180
|
+
assert!(errors.is_empty());
|
|
181
|
+
assert_eq!(graph.documents().len(), 2);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#[test]
|
|
185
|
+
fn from_language_id_unknown() {
|
|
186
|
+
let result = LanguageId::from_language_id("python");
|
|
187
|
+
assert!(result.is_err());
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[test]
|
|
191
|
+
fn updating_document_from_in_memory_source() {
|
|
192
|
+
let context = Context::new();
|
|
193
|
+
let path = context.absolute_path_to("foo/bar.rb");
|
|
194
|
+
context.write(&path, "class Foo; end");
|
|
195
|
+
|
|
196
|
+
let uri = Url::from_file_path(&path).unwrap().to_string();
|
|
197
|
+
|
|
198
|
+
let mut graph = Graph::new();
|
|
199
|
+
let errors = index_files(&mut graph, vec![path]);
|
|
200
|
+
|
|
201
|
+
assert!(errors.is_empty(), "Expected no errors, got: {errors:#?}");
|
|
202
|
+
assert_eq!(6, graph.definitions().len());
|
|
203
|
+
assert_eq!(2, graph.documents().len());
|
|
204
|
+
|
|
205
|
+
index_source(&mut graph, &uri, "", &LanguageId::Ruby);
|
|
206
|
+
|
|
207
|
+
assert_eq!(5, graph.definitions().len());
|
|
208
|
+
assert_eq!(2, graph.documents().len());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
use std::fmt;
|
|
2
|
+
|
|
3
|
+
use crate::model::{
|
|
4
|
+
built_in::{BASIC_OBJECT_ID, OBJECT_ID},
|
|
5
|
+
declaration::{Declaration, Namespace},
|
|
6
|
+
graph::Graph,
|
|
7
|
+
ids::DeclarationId,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
11
|
+
pub enum IntegrityErrorKind {
|
|
12
|
+
/// A declaration's owner is not a namespace (module, class, or singleton class)
|
|
13
|
+
OwnerIsNotNamespace,
|
|
14
|
+
/// A declaration's owner does not exist in the graph
|
|
15
|
+
OwnerDoesNotExist,
|
|
16
|
+
/// A singleton class chain never resolves to a non-singleton namespace
|
|
17
|
+
SingletonClassChainDoesNotTerminate,
|
|
18
|
+
/// A non-root declaration unexpectedly owns itself
|
|
19
|
+
UnexpectedSelfOwnership,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// An integrity error found during graph validation
|
|
23
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
24
|
+
pub struct IntegrityError {
|
|
25
|
+
kind: IntegrityErrorKind,
|
|
26
|
+
declaration_name: String,
|
|
27
|
+
uris: Vec<String>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl fmt::Display for IntegrityError {
|
|
31
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
32
|
+
let message = match self.kind {
|
|
33
|
+
IntegrityErrorKind::OwnerIsNotNamespace => {
|
|
34
|
+
format!("Declaration `{}` is owned by a non-namespace", self.declaration_name)
|
|
35
|
+
}
|
|
36
|
+
IntegrityErrorKind::OwnerDoesNotExist => {
|
|
37
|
+
format!(
|
|
38
|
+
"Declaration `{}` has an owner that does not exist in the graph",
|
|
39
|
+
self.declaration_name
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
IntegrityErrorKind::SingletonClassChainDoesNotTerminate => {
|
|
43
|
+
format!(
|
|
44
|
+
"Singleton class `{}` does not eventually attach to a non-singleton namespace",
|
|
45
|
+
self.declaration_name
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
IntegrityErrorKind::UnexpectedSelfOwnership => {
|
|
49
|
+
format!("Declaration `{}` unexpectedly owns itself", self.declaration_name)
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
write!(f, "{message}. Defined in: {}", self.uris.join(", "))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl std::error::Error for IntegrityError {}
|
|
58
|
+
|
|
59
|
+
/// Checks the integrity of the graph data
|
|
60
|
+
#[must_use]
|
|
61
|
+
pub fn check_integrity(graph: &Graph) -> Vec<IntegrityError> {
|
|
62
|
+
let mut errors = Vec::new();
|
|
63
|
+
let self_owners = [*OBJECT_ID, *BASIC_OBJECT_ID];
|
|
64
|
+
|
|
65
|
+
for (id, declaration) in graph.declarations() {
|
|
66
|
+
let owner_id = declaration.owner_id();
|
|
67
|
+
|
|
68
|
+
// Check for constants that own themselves. Only `Object` and `BasicObject` own themselves and no other constant
|
|
69
|
+
if *id == *owner_id {
|
|
70
|
+
if self_owners.contains(id) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
errors.push(IntegrityError {
|
|
74
|
+
kind: IntegrityErrorKind::UnexpectedSelfOwnership,
|
|
75
|
+
declaration_name: declaration.name().to_string(),
|
|
76
|
+
uris: collect_uris(graph, declaration),
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check that the owner exists
|
|
82
|
+
let Some(owner) = graph.declarations().get(owner_id) else {
|
|
83
|
+
errors.push(IntegrityError {
|
|
84
|
+
kind: IntegrityErrorKind::OwnerDoesNotExist,
|
|
85
|
+
declaration_name: declaration.name().to_string(),
|
|
86
|
+
uris: collect_uris(graph, declaration),
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Check that the owner is a namespace
|
|
92
|
+
if owner.as_namespace().is_none() {
|
|
93
|
+
errors.push(IntegrityError {
|
|
94
|
+
kind: IntegrityErrorKind::OwnerIsNotNamespace,
|
|
95
|
+
declaration_name: declaration.name().to_string(),
|
|
96
|
+
uris: collect_uris(graph, declaration),
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check singleton class chain termination
|
|
102
|
+
if let Declaration::Namespace(Namespace::SingletonClass(_)) = declaration
|
|
103
|
+
&& !singleton_chain_terminates(graph, *owner_id)
|
|
104
|
+
{
|
|
105
|
+
errors.push(IntegrityError {
|
|
106
|
+
kind: IntegrityErrorKind::SingletonClassChainDoesNotTerminate,
|
|
107
|
+
declaration_name: declaration.name().to_string(),
|
|
108
|
+
uris: collect_uris(graph, declaration),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
errors
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Collects the URIs where a declaration is defined, sorted and deduplicated
|
|
117
|
+
fn collect_uris(graph: &Graph, declaration: &Declaration) -> Vec<String> {
|
|
118
|
+
declaration
|
|
119
|
+
.definitions()
|
|
120
|
+
.iter()
|
|
121
|
+
.map(|def_id| {
|
|
122
|
+
let definition = graph.definitions().get(def_id).unwrap();
|
|
123
|
+
let document = graph.documents().get(definition.uri_id()).unwrap();
|
|
124
|
+
document.uri().to_string()
|
|
125
|
+
})
|
|
126
|
+
.collect()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Walks the singleton class chain to verify that it eventually finds a module or class as its attached object
|
|
130
|
+
fn singleton_chain_terminates(graph: &Graph, start_owner_id: DeclarationId) -> bool {
|
|
131
|
+
const MAX_SINGLETON_DEPTH: usize = 128;
|
|
132
|
+
let mut current_id = start_owner_id;
|
|
133
|
+
|
|
134
|
+
for _ in 0..MAX_SINGLETON_DEPTH {
|
|
135
|
+
let Some(current) = graph.declarations().get(¤t_id) else {
|
|
136
|
+
return false;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
match current {
|
|
140
|
+
Declaration::Namespace(Namespace::SingletonClass(_)) => {
|
|
141
|
+
current_id = *current.owner_id();
|
|
142
|
+
}
|
|
143
|
+
Declaration::Namespace(_) => return true,
|
|
144
|
+
_ => return false,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[cfg(test)]
|
|
152
|
+
mod tests {
|
|
153
|
+
use super::*;
|
|
154
|
+
use crate::model::declaration::{ClassDeclaration, MethodDeclaration, SingletonClassDeclaration};
|
|
155
|
+
|
|
156
|
+
#[test]
|
|
157
|
+
fn test_unexpected_self_ownership() {
|
|
158
|
+
let mut graph = Graph::new();
|
|
159
|
+
|
|
160
|
+
// Object and BasicObject are exempt from self-ownership
|
|
161
|
+
graph.declarations_mut().insert(
|
|
162
|
+
*OBJECT_ID,
|
|
163
|
+
Declaration::Namespace(Namespace::Class(Box::new(ClassDeclaration::new(
|
|
164
|
+
"Object".to_string(),
|
|
165
|
+
*OBJECT_ID,
|
|
166
|
+
)))),
|
|
167
|
+
);
|
|
168
|
+
graph.declarations_mut().insert(
|
|
169
|
+
*BASIC_OBJECT_ID,
|
|
170
|
+
Declaration::Namespace(Namespace::Class(Box::new(ClassDeclaration::new(
|
|
171
|
+
"BasicObject".to_string(),
|
|
172
|
+
*BASIC_OBJECT_ID,
|
|
173
|
+
)))),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Foo owns itself — should be an error
|
|
177
|
+
let foo_id = DeclarationId::from("Foo");
|
|
178
|
+
graph.declarations_mut().insert(
|
|
179
|
+
foo_id,
|
|
180
|
+
Declaration::Namespace(Namespace::Class(Box::new(ClassDeclaration::new(
|
|
181
|
+
"Foo".to_string(),
|
|
182
|
+
foo_id,
|
|
183
|
+
)))),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
let errors = check_integrity(&graph);
|
|
187
|
+
assert_eq!(errors.len(), 1);
|
|
188
|
+
assert_eq!(errors[0].kind, IntegrityErrorKind::UnexpectedSelfOwnership);
|
|
189
|
+
assert_eq!(errors[0].declaration_name, "Foo");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#[test]
|
|
193
|
+
fn test_owner_does_not_exist() {
|
|
194
|
+
let mut graph = Graph::new();
|
|
195
|
+
let foo_id = DeclarationId::from("Foo");
|
|
196
|
+
let bogus_owner = DeclarationId::from("NonExistent");
|
|
197
|
+
|
|
198
|
+
graph.declarations_mut().insert(
|
|
199
|
+
foo_id,
|
|
200
|
+
Declaration::Namespace(Namespace::Class(Box::new(ClassDeclaration::new(
|
|
201
|
+
"Foo".to_string(),
|
|
202
|
+
bogus_owner,
|
|
203
|
+
)))),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
let errors = check_integrity(&graph);
|
|
207
|
+
assert_eq!(errors.len(), 1);
|
|
208
|
+
assert_eq!(errors[0].kind, IntegrityErrorKind::OwnerDoesNotExist);
|
|
209
|
+
assert_eq!(errors[0].declaration_name, "Foo");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#[test]
|
|
213
|
+
fn test_owner_is_not_namespace() {
|
|
214
|
+
let mut graph = Graph::new();
|
|
215
|
+
|
|
216
|
+
// Object (self-owned, exempt)
|
|
217
|
+
graph.declarations_mut().insert(
|
|
218
|
+
*OBJECT_ID,
|
|
219
|
+
Declaration::Namespace(Namespace::Class(Box::new(ClassDeclaration::new(
|
|
220
|
+
"Object".to_string(),
|
|
221
|
+
*OBJECT_ID,
|
|
222
|
+
)))),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// A method owned by Object (valid)
|
|
226
|
+
let method_id = DeclarationId::from("Object#foo");
|
|
227
|
+
graph.declarations_mut().insert(
|
|
228
|
+
method_id,
|
|
229
|
+
Declaration::Method(Box::new(MethodDeclaration::new("Object#foo".to_string(), *OBJECT_ID))),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// A class owned by the method (invalid — owner is not a namespace)
|
|
233
|
+
let bar_id = DeclarationId::from("Bar");
|
|
234
|
+
graph.declarations_mut().insert(
|
|
235
|
+
bar_id,
|
|
236
|
+
Declaration::Namespace(Namespace::Class(Box::new(ClassDeclaration::new(
|
|
237
|
+
"Bar".to_string(),
|
|
238
|
+
method_id,
|
|
239
|
+
)))),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
let errors = check_integrity(&graph);
|
|
243
|
+
assert_eq!(errors.len(), 1);
|
|
244
|
+
assert_eq!(errors[0].kind, IntegrityErrorKind::OwnerIsNotNamespace);
|
|
245
|
+
assert_eq!(errors[0].declaration_name, "Bar");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[test]
|
|
249
|
+
fn test_singleton_class_chain_does_not_terminate() {
|
|
250
|
+
let mut graph = Graph::new();
|
|
251
|
+
|
|
252
|
+
// Two singleton classes that own each other, forming a cycle
|
|
253
|
+
let s1_id = DeclarationId::from("<Class:Foo>");
|
|
254
|
+
let s2_id = DeclarationId::from("<Class:Bar>");
|
|
255
|
+
|
|
256
|
+
graph.declarations_mut().insert(
|
|
257
|
+
s1_id,
|
|
258
|
+
Declaration::Namespace(Namespace::SingletonClass(Box::new(SingletonClassDeclaration::new(
|
|
259
|
+
"<Class:Foo>".to_string(),
|
|
260
|
+
s2_id,
|
|
261
|
+
)))),
|
|
262
|
+
);
|
|
263
|
+
graph.declarations_mut().insert(
|
|
264
|
+
s2_id,
|
|
265
|
+
Declaration::Namespace(Namespace::SingletonClass(Box::new(SingletonClassDeclaration::new(
|
|
266
|
+
"<Class:Bar>".to_string(),
|
|
267
|
+
s1_id,
|
|
268
|
+
)))),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
let errors = check_integrity(&graph);
|
|
272
|
+
assert_eq!(errors.len(), 2);
|
|
273
|
+
assert!(
|
|
274
|
+
errors
|
|
275
|
+
.iter()
|
|
276
|
+
.all(|e| e.kind == IntegrityErrorKind::SingletonClassChainDoesNotTerminate)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
use std::panic::{self, AssertUnwindSafe};
|
|
2
|
+
use std::{
|
|
3
|
+
sync::Arc,
|
|
4
|
+
sync::atomic::{AtomicUsize, Ordering},
|
|
5
|
+
thread,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
use crossbeam_deque::{Injector, Steal, Stealer, Worker};
|
|
9
|
+
use crossbeam_utils::Backoff;
|
|
10
|
+
|
|
11
|
+
/// Work-stealing queue that balances jobs across worker threads.
|
|
12
|
+
///
|
|
13
|
+
/// Jobs are pushed onto the global `injector`, then workers move them into their
|
|
14
|
+
/// own local queues. Work stealing lets idle workers drain the global injector
|
|
15
|
+
/// first and then steal from peers, keeping the CPU busy without coarse locks.
|
|
16
|
+
/// `in_flight` tracks outstanding jobs so threads can tell when all work
|
|
17
|
+
/// (including work spawned by other jobs) has finished.
|
|
18
|
+
#[derive(Default)]
|
|
19
|
+
pub struct JobQueue {
|
|
20
|
+
/// Global queue feeding newly discovered jobs to workers.
|
|
21
|
+
injector: Injector<Box<dyn Job + Send + 'static>>,
|
|
22
|
+
/// Count of jobs that have been queued but not yet completed.
|
|
23
|
+
in_flight: AtomicUsize,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl JobQueue {
|
|
27
|
+
#[must_use]
|
|
28
|
+
pub fn new() -> Self {
|
|
29
|
+
Self {
|
|
30
|
+
injector: Injector::new(),
|
|
31
|
+
in_flight: AtomicUsize::new(0),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Enqueue a job for processing.
|
|
36
|
+
pub fn push(&self, job: Box<dyn Job + Send + 'static>) {
|
|
37
|
+
// Increment the count of in-flight jobs to indicate a new job has been enqueued.
|
|
38
|
+
self.in_flight.fetch_add(1, Ordering::Relaxed);
|
|
39
|
+
// Push the job onto the global injector queue for later execution by workers.
|
|
40
|
+
self.injector.push(job);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Run jobs until the queue is empty.
|
|
44
|
+
///
|
|
45
|
+
/// Accepts the shared queue so jobs can enqueue more work while the runner is processing.
|
|
46
|
+
pub fn run(queue: &Arc<JobQueue>) {
|
|
47
|
+
// Determine the number of worker threads to launch.
|
|
48
|
+
// Use the number of available logical CPUs, falling back to 4 if detection fails.
|
|
49
|
+
let worker_count = thread::available_parallelism()
|
|
50
|
+
.map(std::num::NonZeroUsize::get)
|
|
51
|
+
.unwrap_or(4);
|
|
52
|
+
|
|
53
|
+
Self::run_with_workers(queue, worker_count);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Spin up worker threads and return their handles without waiting for completion.
|
|
57
|
+
///
|
|
58
|
+
/// The caller is responsible for joining the returned handles. This allows
|
|
59
|
+
/// overlapping other work (e.g. merging results) with job processing.
|
|
60
|
+
pub fn run_without_waiting(queue: &Arc<JobQueue>) -> Vec<thread::JoinHandle<()>> {
|
|
61
|
+
let worker_count = thread::available_parallelism()
|
|
62
|
+
.map(std::num::NonZeroUsize::get)
|
|
63
|
+
.unwrap_or(4);
|
|
64
|
+
|
|
65
|
+
Self::spawn_workers(queue, worker_count)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Spin up `worker_count` threads, each with its own local queue, and block until all threads finish.
|
|
69
|
+
///
|
|
70
|
+
/// Workers steal from each other and from the global injector to keep the work balanced.
|
|
71
|
+
fn run_with_workers(queue: &Arc<JobQueue>, worker_count: usize) {
|
|
72
|
+
let handles = Self::spawn_workers(queue, worker_count);
|
|
73
|
+
|
|
74
|
+
// Wait for all worker threads to finish before returning.
|
|
75
|
+
for handle in handles {
|
|
76
|
+
handle.join().expect("Worker thread panicked");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Spin up `worker_count` threads and return their join handles.
|
|
81
|
+
fn spawn_workers(queue: &Arc<JobQueue>, worker_count: usize) -> Vec<thread::JoinHandle<()>> {
|
|
82
|
+
let mut handles = Vec::with_capacity(worker_count);
|
|
83
|
+
let mut workers = Vec::with_capacity(worker_count);
|
|
84
|
+
let mut stealers = Vec::with_capacity(worker_count);
|
|
85
|
+
|
|
86
|
+
for _ in 0..worker_count {
|
|
87
|
+
let worker = Worker::new_fifo();
|
|
88
|
+
stealers.push(worker.stealer()); // For stealing jobs from this queue
|
|
89
|
+
workers.push(worker);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Wrap all stealers in an Arc so they can be shared across all threads.
|
|
93
|
+
let stealers = Arc::new(stealers);
|
|
94
|
+
|
|
95
|
+
// Start a worker thread for each local queue.
|
|
96
|
+
for worker in workers {
|
|
97
|
+
let queue = Arc::clone(queue);
|
|
98
|
+
let stealers = Arc::clone(&stealers);
|
|
99
|
+
handles.push(thread::spawn(move || queue.worker_loop(&worker, &stealers)));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
handles
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Drain work for a single worker.
|
|
106
|
+
///
|
|
107
|
+
/// The worker prefers its local queue, then the global injector, then other
|
|
108
|
+
/// workers. If no job is immediately available, it backs off briefly and
|
|
109
|
+
/// exits once `in_flight` reaches zero (meaning no pending work anywhere).
|
|
110
|
+
fn worker_loop(
|
|
111
|
+
&self,
|
|
112
|
+
local: &Worker<Box<dyn Job + Send + 'static>>,
|
|
113
|
+
stealers: &Arc<Vec<Stealer<Box<dyn Job + Send + 'static>>>>,
|
|
114
|
+
) {
|
|
115
|
+
// Create a backoff utility for yielding when no job is immediately available
|
|
116
|
+
let backoff = Backoff::new();
|
|
117
|
+
|
|
118
|
+
// Loop until all work is done (in_flight reaches zero)
|
|
119
|
+
loop {
|
|
120
|
+
// Try to steal the next job to execute. Prioritize own local queue, then global, then peers.
|
|
121
|
+
let Some(job) = Self::steal_job(local, stealers, &self.injector) else {
|
|
122
|
+
if self.in_flight.load(Ordering::Acquire) == 0 {
|
|
123
|
+
// All work is done, exit the worker loop
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
// No work found and still pending jobs: brief pause before retrying
|
|
127
|
+
backoff.snooze();
|
|
128
|
+
continue;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Reset backoff as we've obtained a job to process
|
|
132
|
+
backoff.reset();
|
|
133
|
+
|
|
134
|
+
// Run the job and capture any panics so we can propagate them safely
|
|
135
|
+
let result = panic::catch_unwind(AssertUnwindSafe(|| job.run()));
|
|
136
|
+
|
|
137
|
+
// Job completed; decrement the in-flight job counter
|
|
138
|
+
self.in_flight.fetch_sub(1, Ordering::Relaxed);
|
|
139
|
+
|
|
140
|
+
// If the job panicked, resume the panic on this thread
|
|
141
|
+
if let Err(payload) = result {
|
|
142
|
+
panic::resume_unwind(payload);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Find the next job for a worker.
|
|
148
|
+
///
|
|
149
|
+
/// Priority: pop from the worker's local queue, steal a batch from the
|
|
150
|
+
/// global injector, then try stealing from peer workers. Returning `None`
|
|
151
|
+
/// signals the caller to back off or eventually exit if no jobs remain.
|
|
152
|
+
fn steal_job(
|
|
153
|
+
local: &Worker<Box<dyn Job + Send + 'static>>,
|
|
154
|
+
stealers: &[Stealer<Box<dyn Job + Send + 'static>>],
|
|
155
|
+
injector: &Injector<Box<dyn Job + Send + 'static>>,
|
|
156
|
+
) -> Option<Box<dyn Job + Send + 'static>> {
|
|
157
|
+
// First, try to pop a job from the worker's own local queue (fastest, lowest contention).
|
|
158
|
+
if let Some(job) = local.pop() {
|
|
159
|
+
return Some(job);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// If the local queue is empty, try to steal a batch of jobs from the global injector queue.
|
|
163
|
+
match injector.steal_batch_and_pop(local) {
|
|
164
|
+
Steal::Success(job) => return Some(job), // Successfully stole a job from the global injector.
|
|
165
|
+
Steal::Retry => return None, // Transient failure; signal to caller to back off and try again later.
|
|
166
|
+
Steal::Empty => {} // No jobs available in the global injector; continue to peers.
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// As a last resort, attempt to steal jobs from each of the peer workers' queues.
|
|
170
|
+
for stealer in stealers {
|
|
171
|
+
match stealer.steal_batch_and_pop(local) {
|
|
172
|
+
Steal::Success(job) => return Some(job), // Successfully stole a job from a peer.
|
|
173
|
+
Steal::Retry => return None, // Retry advised; caller should back off and loop.
|
|
174
|
+
Steal::Empty => {} // No jobs in this peer; try next peer.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// No jobs available from any source.
|
|
179
|
+
None
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Unit of work scheduled on a `JobQueue`.
|
|
184
|
+
///
|
|
185
|
+
/// # Example
|
|
186
|
+
///
|
|
187
|
+
/// ```
|
|
188
|
+
/// use std::sync::Arc;
|
|
189
|
+
/// use rubydex::job_queue::{Job, JobQueue};
|
|
190
|
+
///
|
|
191
|
+
/// struct PrintJob;
|
|
192
|
+
///
|
|
193
|
+
/// impl Job for PrintJob {
|
|
194
|
+
/// fn run(&self) {
|
|
195
|
+
/// println!("hello from a worker");
|
|
196
|
+
/// }
|
|
197
|
+
/// }
|
|
198
|
+
///
|
|
199
|
+
/// let queue = Arc::new(JobQueue::new());
|
|
200
|
+
/// queue.push(Box::new(PrintJob));
|
|
201
|
+
/// JobQueue::run(&queue);
|
|
202
|
+
/// ```
|
|
203
|
+
pub trait Job: Send {
|
|
204
|
+
fn run(&self);
|
|
205
|
+
}
|