expressir 2.4.3 → 2.4.4
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/.cargo/config.toml +3 -0
- data/.github/workflows/rust-ext.yml +41 -0
- data/.github/workflows/verify_remarks.yml +33 -0
- data/.gitignore +6 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_todo.yml +1 -0
- data/Cargo.toml +7 -0
- data/README.adoc +65 -0
- data/Rakefile +12 -0
- data/TODO.parity-ee/00-overview.md +58 -0
- data/TODO.parity-ee/01-annex-g-rule-extraction.md +41 -0
- data/TODO.parity-ee/02-eeng-algorithm-inventory.md +8 -0
- data/TODO.parity-ee/03-eeng-oracle-harness.md +33 -0
- data/TODO.parity-ee/04-shtolo-concatenate.md +44 -0
- data/TODO.parity-ee/05-shtolo-longform-flatten.md +53 -0
- data/TODO.parity-ee/06-interface-scheduling-parity.md +47 -0
- data/TODO.parity-ee/07-semantic-checks-port.md +35 -0
- data/TODO.parity-ee/08-pretty-roundtrip-gate.md +27 -0
- data/TODO.parity-ee/09-smrl-index-and-listing.md +22 -0
- data/TODO.parity-ee/10-interface-dot-graph.md +21 -0
- data/TODO.parity-ee/11-import-eeng-tests.md +79 -0
- data/TODO.parity-ee/parity-matrix.md +108 -0
- data/exe/expressir +6 -0
- data/ext/expressir_core/Cargo.toml +24 -0
- data/ext/expressir_core/extconf.rb +30 -0
- data/ext/expressir_core/src/lib.rs +446 -0
- data/lib/expressir/cli.rb +49 -4
- data/lib/expressir/commands/check.rb +56 -0
- data/lib/expressir/commands/expand.rb +26 -0
- data/lib/expressir/commands/fix.rb +33 -0
- data/lib/expressir/commands/flatten.rb +29 -0
- data/lib/expressir/commands/parity_inputs.rb +85 -0
- data/lib/expressir/commands/validate.rb +22 -0
- data/lib/expressir/commands.rb +5 -0
- data/lib/expressir/express/builder_registry.rb +11 -2
- data/lib/expressir/express/builders/entity_decl_builder.rb +17 -7
- data/lib/expressir/express/builders/subtype_constraint_builder.rb +3 -1
- data/lib/expressir/express/builders/type_builder.rb +8 -0
- data/lib/expressir/express/builders/unique_rule_builder.rb +8 -1
- data/lib/expressir/express/checker.rb +458 -0
- data/lib/expressir/express/concatenator.rb +79 -0
- data/lib/expressir/express/core.rb +66 -0
- data/lib/expressir/express/formatter.rb +25 -3
- data/lib/expressir/express/formatters/data_types_formatter.rb +2 -2
- data/lib/expressir/express/formatters/declarations_formatter.rb +1 -0
- data/lib/expressir/express/formatters/remark_formatter.rb +30 -6
- data/lib/expressir/express/formatters/statements_formatter.rb +7 -1
- data/lib/expressir/express/interface_dot.rb +105 -0
- data/lib/expressir/express/listing.rb +174 -0
- data/lib/expressir/express/model_traversal.rb +42 -0
- data/lib/expressir/express/node_position_index.rb +18 -4
- data/lib/expressir/express/parser.rb +242 -4
- data/lib/expressir/express/pretty_gate.rb +61 -0
- data/lib/expressir/express/refs_overlay.rb +47 -0
- data/lib/expressir/express/remark_attacher.rb +122 -10
- data/lib/expressir/express/remark_overlay.rb +86 -0
- data/lib/expressir/express/schema_plain_source_formatter.rb +11 -0
- data/lib/expressir/express/schema_source_formatter.rb +5 -5
- data/lib/expressir/express/self_schema_reference.rb +126 -0
- data/lib/expressir/express/shtolo.rb +522 -0
- data/lib/expressir/express.rb +13 -0
- data/lib/expressir/model/concerns.rb +15 -1
- data/lib/expressir/model/declarations/entity.rb +6 -1
- data/lib/expressir/model/declarations/interface.rb +13 -0
- data/lib/expressir/model/declarations/schema.rb +74 -23
- data/lib/expressir/model/indexes/item_graph.rb +135 -0
- data/lib/expressir/model/indexes.rb +1 -0
- data/lib/expressir/model/model_element.rb +30 -1
- data/lib/expressir/model/remark_info.rb +20 -1
- data/lib/expressir/model/remark_placement.rb +15 -2
- data/lib/expressir/model/repository.rb +8 -2
- data/lib/expressir/model.rb +2 -1
- data/lib/expressir/version.rb +1 -1
- data/lib/tasks/verify_remarks.rake +9 -0
- data/rakelib/verify_remarks.rake +16 -0
- metadata +42 -2
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
//! Ruby bridge for expressir-core: exposes the Rust parse + model
|
|
2
|
+
//! extraction pipeline to the expressir gem.
|
|
3
|
+
//!
|
|
4
|
+
//! Two entry points:
|
|
5
|
+
//! - `parse_to_model_hash` — the wire JSON string (tooling, parity runs)
|
|
6
|
+
//! - `parse_to_model` — the hydrated model, built by walking the wire
|
|
7
|
+
//! JSON definition once and constructing each node through
|
|
8
|
+
//! `Serializable.instantiate` (lutaml-model's fast bulk constructor).
|
|
9
|
+
//! Skips the JSON string, `JSON.parse`, and the generic from_hash
|
|
10
|
+
//! mapping machinery.
|
|
11
|
+
|
|
12
|
+
use std::collections::HashMap;
|
|
13
|
+
use std::sync::mpsc::Receiver;
|
|
14
|
+
use std::sync::Mutex;
|
|
15
|
+
|
|
16
|
+
use expressir_rs::batch::{self, BatchOutcome};
|
|
17
|
+
use expressir_rs::compiled_set::{CompiledFile, CompiledSet};
|
|
18
|
+
use expressir_rs::ParsedTree;
|
|
19
|
+
use magnus::{function, method};
|
|
20
|
+
use magnus::prelude::*;
|
|
21
|
+
use magnus::typed_data::DataTypeBuilder;
|
|
22
|
+
use magnus::value::Lazy;
|
|
23
|
+
use magnus::IntoValue;
|
|
24
|
+
use magnus::{DataType, DataTypeFunctions, Error, RArray, RClass, RHash, RModule, Ruby, TypedData};
|
|
25
|
+
use serde_json::Value as Wire;
|
|
26
|
+
|
|
27
|
+
fn parse_error(ruby: &Ruby, e: expressir_rs::model_json::ModelJsonError) -> Error {
|
|
28
|
+
Error::new(
|
|
29
|
+
ruby.exception_runtime_error(),
|
|
30
|
+
format!("expressir-core: parse error: {e:?}"),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn model_json(ruby: &Ruby, source: String, path: String) -> Result<String, Error> {
|
|
35
|
+
let tree = ParsedTree::parse(&source).map_err(|e| {
|
|
36
|
+
Error::new(
|
|
37
|
+
ruby.exception_runtime_error(),
|
|
38
|
+
format!("expressir-core: parse error: {e:?}"),
|
|
39
|
+
)
|
|
40
|
+
})?;
|
|
41
|
+
let value = tree.to_model_json(&path).map_err(|e| parse_error(ruby, e))?;
|
|
42
|
+
serde_json::to_string(&value).map_err(|e| {
|
|
43
|
+
Error::new(
|
|
44
|
+
ruby.exception_runtime_error(),
|
|
45
|
+
format!("expressir-core: serialize error: {e}"),
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Resolves `Expressir::Model::*` classes by their wire `_class` name,
|
|
51
|
+
/// caching each lookup (the class set is closed).
|
|
52
|
+
#[derive(Default)]
|
|
53
|
+
struct ClassCache {
|
|
54
|
+
classes: HashMap<String, RClass>,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl ClassCache {
|
|
58
|
+
fn resolve(&mut self, ruby: &Ruby, name: &str) -> Result<RClass, Error> {
|
|
59
|
+
if let Some(klass) = self.classes.get(name) {
|
|
60
|
+
return Ok(*klass);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let short = name.strip_prefix("Expressir::Model::").ok_or_else(|| {
|
|
64
|
+
Error::new(
|
|
65
|
+
ruby.exception_runtime_error(),
|
|
66
|
+
format!("expressir-core: unknown model class {name:?}"),
|
|
67
|
+
)
|
|
68
|
+
})?;
|
|
69
|
+
let mut current = ruby
|
|
70
|
+
.class_object()
|
|
71
|
+
.const_get::<_, RModule>("Expressir")?
|
|
72
|
+
.const_get::<_, RModule>("Model")?;
|
|
73
|
+
let segments: Vec<&str> = short.split("::").collect();
|
|
74
|
+
let last = segments.len() - 1;
|
|
75
|
+
for (i, segment) in segments.iter().enumerate() {
|
|
76
|
+
let value = current
|
|
77
|
+
.const_get::<_, magnus::Value>(segment.to_string())
|
|
78
|
+
.map_err(|e| {
|
|
79
|
+
Error::new(
|
|
80
|
+
ruby.exception_runtime_error(),
|
|
81
|
+
format!("expressir-core: unknown model class {name:?}: {e}"),
|
|
82
|
+
)
|
|
83
|
+
})?;
|
|
84
|
+
if i == last {
|
|
85
|
+
let klass = RClass::from_value(value).ok_or_else(|| {
|
|
86
|
+
Error::new(
|
|
87
|
+
ruby.exception_runtime_error(),
|
|
88
|
+
format!("expressir-core: {name:?} is not a class"),
|
|
89
|
+
)
|
|
90
|
+
})?;
|
|
91
|
+
self.classes.insert(name.to_string(), klass);
|
|
92
|
+
return Ok(klass);
|
|
93
|
+
}
|
|
94
|
+
current = RModule::from_value(value).ok_or_else(|| {
|
|
95
|
+
Error::new(
|
|
96
|
+
ruby.exception_runtime_error(),
|
|
97
|
+
format!("expressir-core: {name:?} is not a module"),
|
|
98
|
+
)
|
|
99
|
+
})?;
|
|
100
|
+
}
|
|
101
|
+
unreachable!()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Live handle to a running batch compile: the outcomes channel plus
|
|
106
|
+
/// the per-stream class cache and the wire values retained for
|
|
107
|
+
/// artifact assembly. `next` blocks until a worker delivers; workers
|
|
108
|
+
/// are Rust threads, so they progress while Ruby waits.
|
|
109
|
+
#[derive(DataTypeFunctions)]
|
|
110
|
+
struct BatchStream {
|
|
111
|
+
rx: Receiver<BatchOutcome>,
|
|
112
|
+
cache: Mutex<ClassCache>,
|
|
113
|
+
wires: Mutex<Vec<(String, Wire)>>,
|
|
114
|
+
/// Wire paths in job order — the artifact layout must be
|
|
115
|
+
/// deterministic regardless of completion order.
|
|
116
|
+
order: Mutex<Vec<String>>,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// SAFETY: BatchStream never crosses threads itself; Send is required by
|
|
120
|
+
// magnus to expose methods, and every access happens on the Ruby thread
|
|
121
|
+
// that owns the stream.
|
|
122
|
+
unsafe impl Send for BatchStream {}
|
|
123
|
+
|
|
124
|
+
unsafe impl TypedData for BatchStream {
|
|
125
|
+
fn class(ruby: &Ruby) -> RClass {
|
|
126
|
+
static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
|
|
127
|
+
let class = ruby
|
|
128
|
+
.class_object()
|
|
129
|
+
.const_get::<_, RModule>("Expressir")
|
|
130
|
+
.and_then(|m| m.const_get::<_, RModule>("Core"))
|
|
131
|
+
.and_then(|m| m.const_get::<_, RClass>("BatchStream"))
|
|
132
|
+
.expect("Expressir::Core::BatchStream defined in init");
|
|
133
|
+
class.undef_default_alloc_func();
|
|
134
|
+
class
|
|
135
|
+
});
|
|
136
|
+
ruby.get_inner(&CLASS)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn data_type() -> &'static DataType {
|
|
140
|
+
static DATA_TYPE: DataType =
|
|
141
|
+
DataTypeBuilder::<BatchStream>::new(c"Expressir::Core::BatchStream").build();
|
|
142
|
+
&DATA_TYPE
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn batch_start(ruby: &Ruby, jobs: RArray, workers: usize) -> Result<BatchStream, Error> {
|
|
147
|
+
let mut parsed: Vec<(String, String)> = Vec::with_capacity(jobs.len());
|
|
148
|
+
for index in 0..jobs.len() {
|
|
149
|
+
let job = jobs.entry(index as isize)?;
|
|
150
|
+
let pair = RArray::from_value(job).ok_or_else(|| {
|
|
151
|
+
Error::new(
|
|
152
|
+
ruby.exception_arg_error(),
|
|
153
|
+
"jobs must be [read_path, wire_path] pairs",
|
|
154
|
+
)
|
|
155
|
+
})?;
|
|
156
|
+
let read_path: String = pair.entry(0)?;
|
|
157
|
+
let wire_path: String = pair.entry(1)?;
|
|
158
|
+
parsed.push((read_path, wire_path));
|
|
159
|
+
}
|
|
160
|
+
let order = parsed.iter().map(|(_, wire)| wire.clone()).collect();
|
|
161
|
+
let rx = batch::parse_batch(parsed, workers);
|
|
162
|
+
Ok(BatchStream {
|
|
163
|
+
rx,
|
|
164
|
+
cache: Mutex::new(ClassCache::default()),
|
|
165
|
+
wires: Mutex::new(Vec::new()),
|
|
166
|
+
order: Mutex::new(order),
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Write the compiled-set artifact from this stream's retained wires:
|
|
171
|
+
/// `read_paths` maps wire_path → physical source for digesting.
|
|
172
|
+
fn batch_write_set(
|
|
173
|
+
ruby: &Ruby,
|
|
174
|
+
stream: &BatchStream,
|
|
175
|
+
out_path: String,
|
|
176
|
+
read_paths: RHash,
|
|
177
|
+
expressir_version: String,
|
|
178
|
+
) -> Result<magnus::Value, Error> {
|
|
179
|
+
let wires = stream.wires.lock().expect("wires").clone();
|
|
180
|
+
let order = stream.order.lock().expect("order").clone();
|
|
181
|
+
let position: HashMap<String, usize> =
|
|
182
|
+
order.iter().enumerate().map(|(i, p)| (p.clone(), i)).collect();
|
|
183
|
+
let mut wires = wires;
|
|
184
|
+
wires.sort_by_key(|(wire_path, _)| position.get(wire_path).copied().unwrap_or(usize::MAX));
|
|
185
|
+
|
|
186
|
+
let mut files = Vec::with_capacity(wires.len());
|
|
187
|
+
for (wire_path, wire) in wires {
|
|
188
|
+
let read_path: Option<String> = read_paths
|
|
189
|
+
.fetch(ruby.str_new(&wire_path))
|
|
190
|
+
.map_err(|e| Error::new(ruby.exception_arg_error(), e.to_string()))?;
|
|
191
|
+
let source_sha = read_path
|
|
192
|
+
.as_deref()
|
|
193
|
+
.map(compiled_set_file_sha)
|
|
194
|
+
.unwrap_or_default();
|
|
195
|
+
let wire_json = serde_json::to_string(&wire).map_err(|e| {
|
|
196
|
+
Error::new(
|
|
197
|
+
ruby.exception_runtime_error(),
|
|
198
|
+
format!("wire encode {wire_path}: {e}"),
|
|
199
|
+
)
|
|
200
|
+
})?;
|
|
201
|
+
files.push(CompiledFile {
|
|
202
|
+
wire_path,
|
|
203
|
+
source_sha,
|
|
204
|
+
wire_json,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
let set = CompiledSet::build(files, &expressir_version, &expressir_rs::grammar_digest());
|
|
208
|
+
set.write_to(&out_path).map_err(|e| {
|
|
209
|
+
Error::new(
|
|
210
|
+
ruby.exception_io_error(),
|
|
211
|
+
format!("compiled set {out_path}: {e}"),
|
|
212
|
+
)
|
|
213
|
+
})?;
|
|
214
|
+
Ok(ruby.str_new(&set.header.set_digest).as_value())
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fn compiled_set_file_sha(path: &str) -> String {
|
|
218
|
+
expressir_rs::compiled_set::file_sha256(path)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// `[wire_path, model, nil]` / `[wire_path, nil, message]`, or nil when
|
|
222
|
+
/// the queue has drained.
|
|
223
|
+
fn batch_next(ruby: &Ruby, stream: &BatchStream) -> Result<magnus::Value, Error> {
|
|
224
|
+
let outcome = stream.rx.recv();
|
|
225
|
+
let Ok(outcome) = outcome else {
|
|
226
|
+
return Ok(ruby.qnil().as_value());
|
|
227
|
+
};
|
|
228
|
+
let BatchOutcome { path, result } = outcome;
|
|
229
|
+
let triple = RArray::with_capacity(3);
|
|
230
|
+
triple.push(ruby.str_new(&path))?;
|
|
231
|
+
match result {
|
|
232
|
+
Ok(wire) => {
|
|
233
|
+
stream
|
|
234
|
+
.wires
|
|
235
|
+
.lock()
|
|
236
|
+
.expect("wires")
|
|
237
|
+
.push((path.clone(), wire.clone()));
|
|
238
|
+
let mut cache = stream.cache.lock().expect("class cache");
|
|
239
|
+
let model = value_to_ruby(ruby, &mut cache, &wire)?;
|
|
240
|
+
triple.push(model)?;
|
|
241
|
+
triple.push(ruby.qnil())?;
|
|
242
|
+
}
|
|
243
|
+
Err(message) => {
|
|
244
|
+
triple.push(ruby.qnil())?;
|
|
245
|
+
triple.push(ruby.str_new(&message))?;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
Ok(triple.as_value())
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// Reader over a compiled-set artifact: ordered, lazy — each `next`
|
|
252
|
+
/// hydrates one file's wire model through the shared class cache.
|
|
253
|
+
#[derive(DataTypeFunctions)]
|
|
254
|
+
struct SetReader {
|
|
255
|
+
set: CompiledSet,
|
|
256
|
+
index: Mutex<usize>,
|
|
257
|
+
cache: Mutex<ClassCache>,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// SAFETY: only ever touched from the owning Ruby thread.
|
|
261
|
+
unsafe impl Send for SetReader {}
|
|
262
|
+
|
|
263
|
+
unsafe impl TypedData for SetReader {
|
|
264
|
+
fn class(ruby: &Ruby) -> RClass {
|
|
265
|
+
static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
|
|
266
|
+
let class = ruby
|
|
267
|
+
.class_object()
|
|
268
|
+
.const_get::<_, RModule>("Expressir")
|
|
269
|
+
.and_then(|m| m.const_get::<_, RModule>("Core"))
|
|
270
|
+
.and_then(|m| m.const_get::<_, RClass>("Set"))
|
|
271
|
+
.expect("Expressir::Core::Set defined in init");
|
|
272
|
+
class.undef_default_alloc_func();
|
|
273
|
+
class
|
|
274
|
+
});
|
|
275
|
+
ruby.get_inner(&CLASS)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fn data_type() -> &'static DataType {
|
|
279
|
+
static DATA_TYPE: DataType =
|
|
280
|
+
DataTypeBuilder::<SetReader>::new(c"Expressir::Core::Set").build();
|
|
281
|
+
&DATA_TYPE
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fn set_open(_ruby: &Ruby, path: String) -> Result<SetReader, Error> {
|
|
286
|
+
let set = CompiledSet::read_from(&path).map_err(|e| {
|
|
287
|
+
Error::new(
|
|
288
|
+
_ruby.exception_runtime_error(),
|
|
289
|
+
format!("compiled set {path}: {e}"),
|
|
290
|
+
)
|
|
291
|
+
})?;
|
|
292
|
+
Ok(SetReader {
|
|
293
|
+
set,
|
|
294
|
+
index: Mutex::new(0),
|
|
295
|
+
cache: Mutex::new(ClassCache::default()),
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
fn set_digest(ruby: &Ruby, reader: &SetReader) -> Result<magnus::Value, Error> {
|
|
300
|
+
Ok(ruby.str_new(&reader.set.header.set_digest).as_value())
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn set_count(_ruby: &Ruby, reader: &SetReader) -> usize {
|
|
304
|
+
reader.set.files.len()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// `[wire_path, model]` for the next file in artifact order, nil at
|
|
308
|
+
/// the end.
|
|
309
|
+
fn set_next(ruby: &Ruby, reader: &SetReader) -> Result<magnus::Value, Error> {
|
|
310
|
+
let mut index = reader.index.lock().expect("index");
|
|
311
|
+
if *index >= reader.set.files.len() {
|
|
312
|
+
return Ok(ruby.qnil().as_value());
|
|
313
|
+
}
|
|
314
|
+
let file = &reader.set.files[*index];
|
|
315
|
+
*index += 1;
|
|
316
|
+
let wire = file.wire().map_err(|e| {
|
|
317
|
+
Error::new(ruby.exception_runtime_error(), format!("{}: {e}", file.wire_path))
|
|
318
|
+
})?;
|
|
319
|
+
let mut cache = reader.cache.lock().expect("class cache");
|
|
320
|
+
let model = value_to_ruby(ruby, &mut cache, &wire)?;
|
|
321
|
+
let pair = RArray::with_capacity(2);
|
|
322
|
+
pair.push(ruby.str_new(&file.wire_path))?;
|
|
323
|
+
pair.push(model)?;
|
|
324
|
+
Ok(pair.as_value())
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/// Whether the artifact still matches the given {wire_path => read_path}
|
|
328
|
+
/// sources; nil when a source is unreadable.
|
|
329
|
+
fn set_matches_sources(ruby: &Ruby, reader: &SetReader, read_paths: RHash) -> Result<magnus::Value, Error> {
|
|
330
|
+
let lookup = |wire_path: &str| -> Option<String> {
|
|
331
|
+
read_paths
|
|
332
|
+
.fetch(ruby.str_new(wire_path))
|
|
333
|
+
.ok()
|
|
334
|
+
.and_then(|v| String::try_convert(v).ok())
|
|
335
|
+
};
|
|
336
|
+
match reader.set.matches_sources(&lookup) {
|
|
337
|
+
Some(ok) => Ok(ok.into_value()),
|
|
338
|
+
None => Ok(ruby.qnil().as_value()),
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// Write a compiled set from Ruby-provided entries —
|
|
343
|
+
/// `[wire_path, source_sha, wire_json]` triples (the post-remark wire
|
|
344
|
+
/// of finalized models, so warm loads skip remark attachment).
|
|
345
|
+
fn set_write(
|
|
346
|
+
ruby: &Ruby,
|
|
347
|
+
out_path: String,
|
|
348
|
+
entries: RArray,
|
|
349
|
+
expressir_version: String,
|
|
350
|
+
) -> Result<magnus::Value, Error> {
|
|
351
|
+
let mut files = Vec::with_capacity(entries.len());
|
|
352
|
+
for index in 0..entries.len() {
|
|
353
|
+
let triple = RArray::from_value(entries.entry(index as isize)?).ok_or_else(|| {
|
|
354
|
+
Error::new(
|
|
355
|
+
ruby.exception_arg_error(),
|
|
356
|
+
"entries must be [wire_path, source_sha, wire_json] triples",
|
|
357
|
+
)
|
|
358
|
+
})?;
|
|
359
|
+
let wire_path: String = triple.entry(0)?;
|
|
360
|
+
let source_sha: String = triple.entry(1)?;
|
|
361
|
+
let wire_json: String = triple.entry(2)?;
|
|
362
|
+
files.push(CompiledFile {
|
|
363
|
+
wire_path,
|
|
364
|
+
source_sha,
|
|
365
|
+
wire_json,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
let set = CompiledSet::build(files, &expressir_version, &expressir_rs::grammar_digest());
|
|
369
|
+
set.write_to(&out_path).map_err(|e| {
|
|
370
|
+
Error::new(
|
|
371
|
+
ruby.exception_io_error(),
|
|
372
|
+
format!("compiled set {out_path}: {e}"),
|
|
373
|
+
)
|
|
374
|
+
})?;
|
|
375
|
+
Ok(ruby.str_new(&set.header.set_digest).as_value())
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
fn value_to_ruby(ruby: &Ruby, cache: &mut ClassCache, wire: &Wire) -> Result<magnus::Value, Error> {
|
|
379
|
+
match wire {
|
|
380
|
+
Wire::Null => Ok(ruby.qnil().as_value()),
|
|
381
|
+
Wire::Bool(b) => Ok(b.into_value()),
|
|
382
|
+
Wire::Number(n) => {
|
|
383
|
+
if let Some(i) = n.as_i64() {
|
|
384
|
+
Ok(ruby.integer_from_i64(i).into_value())
|
|
385
|
+
} else {
|
|
386
|
+
Ok(ruby.float_from_f64(n.as_f64().unwrap_or(0.0)).into_value())
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
Wire::String(s) => Ok(ruby.str_new(s).as_value()),
|
|
390
|
+
Wire::Array(items) => {
|
|
391
|
+
let array = ruby.ary_new_capa(items.len());
|
|
392
|
+
for item in items {
|
|
393
|
+
array.push(value_to_ruby(ruby, cache, item)?)?;
|
|
394
|
+
}
|
|
395
|
+
Ok(array.as_value())
|
|
396
|
+
}
|
|
397
|
+
Wire::Object(map) => {
|
|
398
|
+
let class_name = map.get("_class").and_then(Wire::as_str).ok_or_else(|| {
|
|
399
|
+
Error::new(
|
|
400
|
+
ruby.exception_runtime_error(),
|
|
401
|
+
"expressir-core: model node without _class",
|
|
402
|
+
)
|
|
403
|
+
})?;
|
|
404
|
+
let klass = cache.resolve(ruby, class_name)?;
|
|
405
|
+
let attrs = RHash::new();
|
|
406
|
+
for (key, item) in map {
|
|
407
|
+
attrs.aset(ruby.sym_new(key.as_str()), value_to_ruby(ruby, cache, item)?)?;
|
|
408
|
+
}
|
|
409
|
+
klass.funcall::<_, _, magnus::Value>("instantiate", (attrs,))
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
fn model_object(ruby: &Ruby, source: String, path: String) -> Result<magnus::Value, Error> {
|
|
415
|
+
let tree = ParsedTree::parse(&source).map_err(|e| {
|
|
416
|
+
Error::new(
|
|
417
|
+
ruby.exception_runtime_error(),
|
|
418
|
+
format!("expressir-core: parse error: {e:?}"),
|
|
419
|
+
)
|
|
420
|
+
})?;
|
|
421
|
+
let wire = tree.to_model_json(&path).map_err(|e| parse_error(ruby, e))?;
|
|
422
|
+
let mut cache = ClassCache::default();
|
|
423
|
+
value_to_ruby(ruby, &mut cache, &wire)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#[magnus::init]
|
|
427
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
428
|
+
let module = ruby.define_module("Expressir")?.define_module("Core")?;
|
|
429
|
+
module.define_singleton_method("parse_to_model_hash", function!(model_json, 2))?;
|
|
430
|
+
module.define_singleton_method("parse_to_model", function!(model_object, 2))?;
|
|
431
|
+
module.define_singleton_method("write_set", function!(set_write, 3))?;
|
|
432
|
+
let batch_class = module.define_class("BatchStream", ruby.class_object())?;
|
|
433
|
+
batch_class.define_singleton_method("start", function!(batch_start, 2))?;
|
|
434
|
+
batch_class.define_method("next", method!(batch_next, 0))?;
|
|
435
|
+
batch_class.define_method(
|
|
436
|
+
"write_set",
|
|
437
|
+
method!(batch_write_set, 3),
|
|
438
|
+
)?;
|
|
439
|
+
let set_class = module.define_class("Set", ruby.class_object())?;
|
|
440
|
+
set_class.define_singleton_method("open", function!(set_open, 1))?;
|
|
441
|
+
set_class.define_method("digest", method!(set_digest, 0))?;
|
|
442
|
+
set_class.define_method("count", method!(set_count, 0))?;
|
|
443
|
+
set_class.define_method("next", method!(set_next, 0))?;
|
|
444
|
+
set_class.define_method("matches_sources", method!(set_matches_sources, 1))?;
|
|
445
|
+
Ok(())
|
|
446
|
+
}
|
data/lib/expressir/cli.rb
CHANGED
|
@@ -18,17 +18,62 @@ module Expressir
|
|
|
18
18
|
method_option :provenance, type: :boolean,
|
|
19
19
|
desc: "Include provenance information (ELF profile only)",
|
|
20
20
|
default: true
|
|
21
|
-
def format(path)
|
|
21
|
+
def format(path, _thor_options = nil)
|
|
22
22
|
Expressir::Commands::Format.new(options).run(path)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
desc "clean PATH", "Strip remarks and prettify EXPRESS schema at PATH"
|
|
26
26
|
method_option :output, type: :string,
|
|
27
27
|
desc: "Output file path (defaults to stdout)"
|
|
28
|
-
def clean(path)
|
|
28
|
+
def clean(path, _thor_options = nil)
|
|
29
29
|
Expressir::Commands::Clean.new(options).run(path)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
desc "fix PATH",
|
|
33
|
+
"Rewrite self-schema-qualified references (#125) in the EXPRESS schema at PATH"
|
|
34
|
+
method_option :output, type: :string,
|
|
35
|
+
aliases: "-o",
|
|
36
|
+
desc: "Output file path (defaults to stdout)"
|
|
37
|
+
def fix(path, _thor_options = nil)
|
|
38
|
+
Expressir::Commands::Fix.new(options).run(path)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc "expand PATH",
|
|
42
|
+
"Concatenate the interface closure of PATH into one .exp artifact"
|
|
43
|
+
method_option :output, type: :string,
|
|
44
|
+
aliases: "-o",
|
|
45
|
+
desc: "Output file path (defaults to stdout)"
|
|
46
|
+
method_option :manifest, type: :string,
|
|
47
|
+
desc: "ELF schema manifest YAML defining where schemas live " \
|
|
48
|
+
"(primary resolution mode)"
|
|
49
|
+
method_option :stepmod, type: :string,
|
|
50
|
+
desc: "STEPmod checkout root — explicit opt-in to directory-" \
|
|
51
|
+
"convention resolution as the fallback"
|
|
52
|
+
def expand(path, _thor_options = nil)
|
|
53
|
+
Expressir::Commands::Expand.new(options).run(path)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
desc "flatten PATH",
|
|
57
|
+
"Flatten the interface closure of PATH into an ISO 10303-11:1994 longform schema"
|
|
58
|
+
method_option :output, type: :string,
|
|
59
|
+
aliases: "-o",
|
|
60
|
+
desc: "Output file path (defaults to stdout)"
|
|
61
|
+
method_option :longform_name, type: :string,
|
|
62
|
+
desc: "Longform SCHEMA id (default: <root>_lf)"
|
|
63
|
+
method_option :extenders, type: :string,
|
|
64
|
+
desc: "Extensible type handling: 'all' (default, fold every " \
|
|
65
|
+
"extender in the closure per Annex G.2.3/G.2.4) or " \
|
|
66
|
+
"'none' (leave extensible types as declared)"
|
|
67
|
+
method_option :manifest, type: :string,
|
|
68
|
+
desc: "ELF schema manifest YAML defining where schemas live " \
|
|
69
|
+
"(primary resolution mode)"
|
|
70
|
+
method_option :stepmod, type: :string,
|
|
71
|
+
desc: "STEPmod checkout root — explicit opt-in to directory-" \
|
|
72
|
+
"convention resolution as the fallback"
|
|
73
|
+
def flatten(path, _thor_options = nil)
|
|
74
|
+
Expressir::Commands::Flatten.new(options).run(path)
|
|
75
|
+
end
|
|
76
|
+
|
|
32
77
|
desc "benchmark FILE_OR_YAML",
|
|
33
78
|
"Benchmark schema loading performance for a file or list of files from YAML"
|
|
34
79
|
method_option :ips, type: :boolean,
|
|
@@ -37,7 +82,7 @@ module Expressir
|
|
|
37
82
|
method_option :save, type: :boolean, desc: "Save benchmark results to file"
|
|
38
83
|
method_option :format, type: :string,
|
|
39
84
|
desc: "Output format (json, csv, default)"
|
|
40
|
-
def benchmark(path)
|
|
85
|
+
def benchmark(path, _thor_options = nil)
|
|
41
86
|
Expressir::Commands::Benchmark.new(options).run(path)
|
|
42
87
|
end
|
|
43
88
|
|
|
@@ -50,7 +95,7 @@ module Expressir
|
|
|
50
95
|
desc: "Output format (json, csv, default)"
|
|
51
96
|
method_option :cache_path, type: :string,
|
|
52
97
|
desc: "Path to store the cache file"
|
|
53
|
-
def benchmark_cache(path)
|
|
98
|
+
def benchmark_cache(path, _thor_options = nil)
|
|
54
99
|
Expressir::Commands::BenchmarkCache.new(options).run(path)
|
|
55
100
|
end
|
|
56
101
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Commands
|
|
5
|
+
# `validate check` — semantic checks modeled on eeng check-p11 note
|
|
6
|
+
# subset (expressir#282). Exits 1 when any :error notes are emitted.
|
|
7
|
+
# Loads the interface closure of each input file so USE FROM targets
|
|
8
|
+
# are visible — matches Concatenator/Shtolo expectations.
|
|
9
|
+
class Check < Base
|
|
10
|
+
def run(*paths)
|
|
11
|
+
files = Array(paths).flat_map do |p|
|
|
12
|
+
if File.directory?(p)
|
|
13
|
+
Dir["#{p}/**/*.exp"]
|
|
14
|
+
else
|
|
15
|
+
ParityInputs.closure_paths(
|
|
16
|
+
p, manifest: options[:manifest], stepmod: options[:stepmod]
|
|
17
|
+
).select { |f| File.exist?(f) }
|
|
18
|
+
end
|
|
19
|
+
end.uniq
|
|
20
|
+
repository = Expressir::Express::Parser.from_files(files)
|
|
21
|
+
# A file that failed to parse is a nil entry: it must fail the run,
|
|
22
|
+
# not pass silently as a "clean" schema (#413).
|
|
23
|
+
failed = repository.files.count(&:nil?)
|
|
24
|
+
if failed.positive?
|
|
25
|
+
raise Thor::Error, "#{failed} file(s) failed to parse"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
result = Expressir::Express::Checker.new(repository).check
|
|
29
|
+
|
|
30
|
+
if options[:json]
|
|
31
|
+
say JSON.generate(
|
|
32
|
+
valid: result.valid?,
|
|
33
|
+
errors: serialize(result.errors),
|
|
34
|
+
warnings: serialize(result.warnings),
|
|
35
|
+
)
|
|
36
|
+
else
|
|
37
|
+
result.notes.each { |n| say "[#{n.severity}] #{n.id}: #{n.message}" }
|
|
38
|
+
say "#{result.errors.size} error(s), #{result.warnings.size} warning(s)"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
return if result.valid?
|
|
42
|
+
|
|
43
|
+
raise Thor::Error, "schema check produced #{result.errors.size} error(s)"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def serialize(notes)
|
|
49
|
+
notes.map do |n|
|
|
50
|
+
{ id: n.id.to_s, severity: n.severity.to_s, schema: n.schema,
|
|
51
|
+
message: n.message }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Expressir
|
|
2
|
+
module Commands
|
|
3
|
+
# `expand` — flattens an interface closure into a single concatenated
|
|
4
|
+
# .exp artifact (expressir#247). Equivalent to eeng's
|
|
5
|
+
# `--concat_schema` over the root schema's closure.
|
|
6
|
+
class Expand < Base
|
|
7
|
+
def run(path)
|
|
8
|
+
root, repository = ParityInputs.root_schema(
|
|
9
|
+
path, manifest: options[:manifest], stepmod: options[:stepmod]
|
|
10
|
+
)
|
|
11
|
+
exit_with_error "schema for #{path} not found in closure" unless root
|
|
12
|
+
|
|
13
|
+
closure = Expressir::Express::Concatenator.closure(root, repository)
|
|
14
|
+
|
|
15
|
+
if options[:output]
|
|
16
|
+
File.open(options[:output], "w") do |io|
|
|
17
|
+
Expressir::Express::Concatenator.write(io, closure)
|
|
18
|
+
end
|
|
19
|
+
say "Concatenated schema written to #{options[:output]}"
|
|
20
|
+
else
|
|
21
|
+
Expressir::Express::Concatenator.write($stdout, closure)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Expressir
|
|
2
|
+
module Commands
|
|
3
|
+
# `fix` — rewrites self-schema-qualified references (#125): string
|
|
4
|
+
# literals like `'THIS_SCHEMA.ITEM'` lose the current-schema prefix
|
|
5
|
+
# when the item is local, or gain the item's true defining schema
|
|
6
|
+
# otherwise. Outputs the corrected EXPRESS file.
|
|
7
|
+
class Fix < Base
|
|
8
|
+
def run(path)
|
|
9
|
+
# Load the interface closure (same-dir by default, or --manifest /
|
|
10
|
+
# --stepmod) so SelfSchemaReference can resolve where each item
|
|
11
|
+
# truly comes from.
|
|
12
|
+
files = ParityInputs.closure_paths(
|
|
13
|
+
path, manifest: options[:manifest], stepmod: options[:stepmod]
|
|
14
|
+
).select { |f| File.exist?(f) }
|
|
15
|
+
repository = Expressir::Express::Parser.from_files(
|
|
16
|
+
files, skip_references: true
|
|
17
|
+
)
|
|
18
|
+
total = repository.schemas.sum do |schema|
|
|
19
|
+
Expressir::Express::SelfSchemaReference.fix!(schema)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
text = Expressir::Express::Formatter.format(repository)
|
|
23
|
+
if options[:output]
|
|
24
|
+
File.write(options[:output], text)
|
|
25
|
+
say "#{total} self-schema reference(s) fixed; written to " \
|
|
26
|
+
"#{options[:output]}"
|
|
27
|
+
else
|
|
28
|
+
$stdout.write(text)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Expressir
|
|
2
|
+
module Commands
|
|
3
|
+
# `flatten` — converts a STEPmod-style multi-schema root into a single
|
|
4
|
+
# ISO 10303-11:1994 longform schema (SHTOLO, expressir#32).
|
|
5
|
+
# Equivalent to eeng's `--flat -mode _longform`.
|
|
6
|
+
class Flatten < Base
|
|
7
|
+
def run(path)
|
|
8
|
+
root, repository = ParityInputs.root_schema(
|
|
9
|
+
path, manifest: options[:manifest], stepmod: options[:stepmod]
|
|
10
|
+
)
|
|
11
|
+
exit_with_error "schema for #{path} not found in closure" unless root
|
|
12
|
+
|
|
13
|
+
longform_name = options[:longform_name] || "#{root.id}_lf"
|
|
14
|
+
extenders = options[:extenders] == "none" ? :none : :all
|
|
15
|
+
flat = Expressir::Express::Shtolo.new(root, repository,
|
|
16
|
+
longform_name: longform_name,
|
|
17
|
+
extenders: extenders).flatten.schema
|
|
18
|
+
text = Expressir::Express::Formatter.format(flat)
|
|
19
|
+
|
|
20
|
+
if options[:output]
|
|
21
|
+
File.write(options[:output], text)
|
|
22
|
+
say "Longform schema written to #{options[:output]}"
|
|
23
|
+
else
|
|
24
|
+
$stdout.write(text)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|