shirobai 2026.0709.0000 → 2026.0713.0000
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.lock +3 -2
- data/Cargo.toml +7 -0
- data/crates/shirobai-core/Cargo.toml +10 -1
- data/crates/shirobai-core/src/bin/bundle_profile.rs +1 -0
- data/crates/shirobai-core/src/bin/pm_lex_probe.rs +246 -0
- data/crates/shirobai-core/src/lib.rs +5 -0
- data/crates/shirobai-core/src/pm_lex.rs +300 -0
- data/crates/shirobai-core/src/pm_lex_token_names.rs +173 -0
- data/crates/shirobai-core/src/rules/abc_size.rs +229 -43
- data/crates/shirobai-core/src/rules/aligner.rs +502 -0
- data/crates/shirobai-core/src/rules/argument_alignment.rs +10 -1
- data/crates/shirobai-core/src/rules/ascii_identifiers.rs +232 -0
- data/crates/shirobai-core/src/rules/block_alignment.rs +29 -7
- data/crates/shirobai-core/src/rules/bundle.rs +475 -17
- data/crates/shirobai-core/src/rules/code_length.rs +2 -3
- data/crates/shirobai-core/src/rules/complexity.rs +134 -8
- data/crates/shirobai-core/src/rules/dot_position.rs +2 -3
- data/crates/shirobai-core/src/rules/duplicate_magic_comment.rs +192 -3
- data/crates/shirobai-core/src/rules/duplicate_methods.rs +2 -3
- data/crates/shirobai-core/src/rules/else_alignment.rs +35 -26
- data/crates/shirobai-core/src/rules/empty_line_between_defs.rs +92 -18
- data/crates/shirobai-core/src/rules/empty_lines_around_arguments.rs +12 -5
- data/crates/shirobai-core/src/rules/end_of_line.rs +148 -0
- data/crates/shirobai-core/src/rules/extra_spacing.rs +721 -0
- data/crates/shirobai-core/src/rules/file_null.rs +32 -11
- data/crates/shirobai-core/src/rules/first_argument_indentation.rs +15 -8
- data/crates/shirobai-core/src/rules/first_array_element_indentation.rs +2 -3
- data/crates/shirobai-core/src/rules/first_hash_element_indentation.rs +2 -3
- data/crates/shirobai-core/src/rules/hash_syntax.rs +21 -13
- data/crates/shirobai-core/src/rules/hash_transform_keys.rs +10 -0
- data/crates/shirobai-core/src/rules/if_unless_modifier.rs +2 -3
- data/crates/shirobai-core/src/rules/indentation_consistency.rs +19 -2
- data/crates/shirobai-core/src/rules/indentation_width.rs +86 -7
- data/crates/shirobai-core/src/rules/initial_indentation.rs +170 -0
- data/crates/shirobai-core/src/rules/line_length_breakable.rs +329 -38
- data/crates/shirobai-core/src/rules/magic_comment_format.rs +128 -0
- data/crates/shirobai-core/src/rules/method_length.rs +16 -14
- data/crates/shirobai-core/src/rules/mod.rs +13 -0
- data/crates/shirobai-core/src/rules/multiline_method_call_brace_layout.rs +6 -1
- data/crates/shirobai-core/src/rules/multiline_method_call_indentation.rs +23 -7
- data/crates/shirobai-core/src/rules/ordered_magic_comments.rs +211 -0
- data/crates/shirobai-core/src/rules/parse_cache.rs +208 -61
- data/crates/shirobai-core/src/rules/percent_literal_delimiters.rs +21 -7
- data/crates/shirobai-core/src/rules/rails_app.rs +206 -0
- data/crates/shirobai-core/src/rules/redundant_self.rs +18 -7
- data/crates/shirobai-core/src/rules/rescue_ensure_alignment.rs +139 -0
- data/crates/shirobai-core/src/rules/rspec_dispatcher.rs +504 -38
- data/crates/shirobai-core/src/rules/rspec_empty_line.rs +15 -565
- data/crates/shirobai-core/src/rules/send_range.rs +94 -0
- data/crates/shirobai-core/src/rules/space_around_equals_in_parameter_default.rs +202 -0
- data/crates/shirobai-core/src/rules/space_around_operators.rs +1228 -0
- data/crates/shirobai-core/src/rules/space_inside_string_interpolation.rs +314 -0
- data/crates/shirobai-core/src/rules/tokens.rs +894 -0
- data/crates/shirobai-core/src/rules/unreachable_code.rs +24 -7
- data/crates/shirobai-core/src/rules/variable_number.rs +30 -1
- data/crates/shirobai-core/src/rules/void.rs +11 -7
- data/crates/shirobai-core/tests/pm_lex_canary.rs +130 -0
- data/ext/shirobai/Cargo.toml +1 -1
- data/ext/shirobai/extconf.rb +6 -0
- data/ext/shirobai/src/lib.rs +355 -13
- data/lib/shirobai/cop/layout/argument_alignment.rb +21 -12
- data/lib/shirobai/cop/layout/array_alignment.rb +10 -10
- data/lib/shirobai/cop/layout/end_of_line.rb +90 -0
- data/lib/shirobai/cop/layout/extra_spacing.rb +124 -0
- data/lib/shirobai/cop/layout/first_argument_indentation.rb +21 -10
- data/lib/shirobai/cop/layout/indentation_consistency.rb +9 -9
- data/lib/shirobai/cop/layout/indentation_width.rb +13 -8
- data/lib/shirobai/cop/layout/initial_indentation.rb +80 -0
- data/lib/shirobai/cop/layout/line_continuation_spacing.rb +157 -0
- data/lib/shirobai/cop/layout/line_length.rb +12 -4
- data/lib/shirobai/cop/layout/rescue_ensure_alignment.rb +250 -0
- data/lib/shirobai/cop/layout/space_around_equals_in_parameter_default.rb +79 -0
- data/lib/shirobai/cop/layout/space_around_operators.rb +121 -0
- data/lib/shirobai/cop/layout/space_inside_string_interpolation.rb +81 -0
- data/lib/shirobai/cop/lint/ordered_magic_comments.rb +62 -0
- data/lib/shirobai/cop/metrics/abc_size.rb +10 -5
- data/lib/shirobai/cop/naming/ascii_identifiers.rb +63 -0
- data/lib/shirobai/cop/style/arguments_forwarding.rb +1 -1
- data/lib/shirobai/cop/style/empty_literal.rb +184 -0
- data/lib/shirobai/cop/style/magic_comment_format.rb +229 -0
- data/lib/shirobai/cop/style/mutable_constant.rb +286 -0
- data/lib/shirobai/cop/style/semicolon.rb +11 -9
- data/lib/shirobai/dispatch.rb +53 -4
- data/lib/shirobai/inject.rb +95 -0
- data/lib/shirobai/version.rb +1 -1
- data/lib/shirobai.rb +11 -1
- metadata +49 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac67035ad74e8850bceadc1fe62046339127dba699f7976ce45bbea34646273f
|
|
4
|
+
data.tar.gz: 3453f727a31a8fdf6d12376e6af577a09f503d80b7e0dfe2e8c9df6a10af3260
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ebe11c9d5e1638871f75f24b5139605694acb3af3387eb442a5c96c1a09aba51afa21c09c5097a23979ca4bb887897fdb4f8ac0005d4cde5efb6ae30d3f64299
|
|
7
|
+
data.tar.gz: 8807b3a11cd85ba0366c723076db229d96966eda367a0a0413e5a01908975ac8af4102e5b05acc498a204e62be38fc3ab8396ce9e09ef5e80bc66b812a0975d5
|
data/Cargo.lock
CHANGED
|
@@ -356,7 +356,7 @@ checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
|
|
|
356
356
|
|
|
357
357
|
[[package]]
|
|
358
358
|
name = "shirobai"
|
|
359
|
-
version = "
|
|
359
|
+
version = "2026.713.0"
|
|
360
360
|
dependencies = [
|
|
361
361
|
"magnus",
|
|
362
362
|
"shirobai-core",
|
|
@@ -364,10 +364,11 @@ dependencies = [
|
|
|
364
364
|
|
|
365
365
|
[[package]]
|
|
366
366
|
name = "shirobai-core"
|
|
367
|
-
version = "
|
|
367
|
+
version = "2026.713.0"
|
|
368
368
|
dependencies = [
|
|
369
369
|
"libc",
|
|
370
370
|
"ruby-prism",
|
|
371
|
+
"ruby-prism-sys",
|
|
371
372
|
"self_cell",
|
|
372
373
|
"unicode-width",
|
|
373
374
|
]
|
data/Cargo.toml
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
members = ["ext/shirobai", "crates/shirobai-core"]
|
|
3
3
|
resolver = "3"
|
|
4
4
|
|
|
5
|
+
# Single source of the crate version. bump-version.yml rewrites this line to
|
|
6
|
+
# the semver form of the gem CalVer (leading zeros stripped: gem 2026.0709.0000
|
|
7
|
+
# is crate 2026.709.0 -- cargo rejects leading zeros in numeric segments, and
|
|
8
|
+
# Gem::Version normalizes to the same integers, so the two stay value-equal).
|
|
9
|
+
[workspace.package]
|
|
10
|
+
version = "2026.713.0"
|
|
11
|
+
|
|
5
12
|
# Release tuning. The hot path crosses crate boundaries (ext cdylib -> core
|
|
6
13
|
# check_* -> ruby-prism); without LTO the cross-crate calls are not inlined and
|
|
7
14
|
# codegen-units=16 (the release default) further fragments optimization. Fat LTO
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "shirobai-core"
|
|
3
|
-
version =
|
|
3
|
+
version.workspace = true
|
|
4
4
|
edition = "2024"
|
|
5
5
|
|
|
6
6
|
[dependencies]
|
|
7
7
|
ruby-prism = "1.9.0"
|
|
8
|
+
# Direct dep on the -sys crate to reach pm_parser_init / pm_parse / lex_callback
|
|
9
|
+
# from pm_lex. ruby-prism 1.9.0 already depends on this exact version, so cargo
|
|
10
|
+
# unifies it into a single build (no second prism compile).
|
|
11
|
+
ruby-prism-sys = "=1.9.0"
|
|
8
12
|
self_cell = "1"
|
|
9
13
|
unicode-width = "0.2"
|
|
10
14
|
|
|
@@ -18,3 +22,8 @@ path = "src/bin/walk_profile.rs"
|
|
|
18
22
|
[[bin]]
|
|
19
23
|
name = "analysis_profile"
|
|
20
24
|
path = "src/bin/analysis_profile.rs"
|
|
25
|
+
|
|
26
|
+
# pm_lex probe: token dump / canary / profile harness.
|
|
27
|
+
[[bin]]
|
|
28
|
+
name = "pm_lex_probe"
|
|
29
|
+
path = "src/bin/pm_lex_probe.rs"
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
//! pm_lex probe harness (measurement / verification only). Three subcommands:
|
|
2
|
+
//!
|
|
3
|
+
//! dump-tokens <corpus> <out_file>
|
|
4
|
+
//! Per .rb file: `# <path>` then one `name\tstart\tlen\tstate` line per
|
|
5
|
+
//! token. Diff against the Ruby `Prism.lex` oracle (oracle_lex.rb).
|
|
6
|
+
//!
|
|
7
|
+
//! canary <corpus>
|
|
8
|
+
//! For every .rb file compare the AST reached via the transmute path
|
|
9
|
+
//! (parse_with_lex) against a normal ruby_prism::parse(). Reports files,
|
|
10
|
+
//! AST fingerprint mismatches, and comment-count mismatches. This is the
|
|
11
|
+
//! ad-hoc, whole-corpus version of the tests/pm_lex_canary.rs CI gate.
|
|
12
|
+
//!
|
|
13
|
+
//! profile <parse|lex|lex_reuse|reparse> <corpus> [iters]
|
|
14
|
+
//! CLOCK_PROCESS_CPUTIME_ID median over iters (default 5). Modes:
|
|
15
|
+
//! parse - ruby_prism::parse only (AST, no tokens)
|
|
16
|
+
//! lex - parse_with_lex (AST + a fresh token Vec per file)
|
|
17
|
+
//! lex_reuse - parse_with_lex_into (AST + one reused token buffer)
|
|
18
|
+
//! reparse - two full parses (reproduces the pm_serialize_lex tax:
|
|
19
|
+
//! AST from one parse, tokens from a second full parse)
|
|
20
|
+
|
|
21
|
+
use std::io::Write;
|
|
22
|
+
use std::path::{Path, PathBuf};
|
|
23
|
+
|
|
24
|
+
use ruby_prism::{Node, Visit};
|
|
25
|
+
use shirobai_core::pm_lex::{RawToken, parse_with_lex, parse_with_lex_into};
|
|
26
|
+
|
|
27
|
+
#[cfg(unix)]
|
|
28
|
+
fn cpu_time_secs() -> f64 {
|
|
29
|
+
let mut ts = libc::timespec {
|
|
30
|
+
tv_sec: 0,
|
|
31
|
+
tv_nsec: 0,
|
|
32
|
+
};
|
|
33
|
+
// SAFETY: ts is a valid timespec.
|
|
34
|
+
unsafe { libc::clock_gettime(libc::CLOCK_PROCESS_CPUTIME_ID, &mut ts) };
|
|
35
|
+
ts.tv_sec as f64 + ts.tv_nsec as f64 / 1e9
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn collect_rb_files(dir: &Path, out: &mut Vec<PathBuf>) {
|
|
39
|
+
let Ok(entries) = std::fs::read_dir(dir) else {
|
|
40
|
+
return;
|
|
41
|
+
};
|
|
42
|
+
for entry in entries.flatten() {
|
|
43
|
+
let path = entry.path();
|
|
44
|
+
if path.is_dir() {
|
|
45
|
+
collect_rb_files(&path, out);
|
|
46
|
+
} else if path.extension().is_some_and(|e| e == "rb") {
|
|
47
|
+
out.push(path);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn sorted_rb_files(corpus: &Path) -> Vec<PathBuf> {
|
|
53
|
+
let mut files = Vec::new();
|
|
54
|
+
collect_rb_files(corpus, &mut files);
|
|
55
|
+
files.sort();
|
|
56
|
+
files
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Pre-order (start,end) offset fingerprint of an AST via the Visit trait.
|
|
60
|
+
struct Fingerprint(Vec<(usize, usize)>);
|
|
61
|
+
impl<'pr> Visit<'pr> for Fingerprint {
|
|
62
|
+
fn visit_branch_node_enter(&mut self, node: Node<'pr>) {
|
|
63
|
+
let l = node.location();
|
|
64
|
+
self.0.push((l.start_offset(), l.end_offset()));
|
|
65
|
+
}
|
|
66
|
+
fn visit_leaf_node_enter(&mut self, node: Node<'pr>) {
|
|
67
|
+
let l = node.location();
|
|
68
|
+
self.0.push((l.start_offset(), l.end_offset()));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
fn fingerprint(root: &Node<'_>) -> Vec<(usize, usize)> {
|
|
72
|
+
let mut fp = Fingerprint(Vec::new());
|
|
73
|
+
fp.visit(root);
|
|
74
|
+
fp.0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn dump_tokens(corpus: &Path, out_file: &Path) {
|
|
78
|
+
let files = sorted_rb_files(corpus);
|
|
79
|
+
let mut out = std::io::BufWriter::new(std::fs::File::create(out_file).expect("create out"));
|
|
80
|
+
for path in &files {
|
|
81
|
+
let Ok(src) = std::fs::read(path) else {
|
|
82
|
+
continue;
|
|
83
|
+
};
|
|
84
|
+
let rel = path.strip_prefix(corpus).unwrap_or(path);
|
|
85
|
+
writeln!(out, "# {}", rel.display()).unwrap();
|
|
86
|
+
let (_result, tokens) = parse_with_lex(&src);
|
|
87
|
+
for t in &tokens {
|
|
88
|
+
writeln!(
|
|
89
|
+
out,
|
|
90
|
+
"{}\t{}\t{}\t{}",
|
|
91
|
+
t.type_name(),
|
|
92
|
+
t.start_offset,
|
|
93
|
+
t.length,
|
|
94
|
+
t.lex_state
|
|
95
|
+
)
|
|
96
|
+
.unwrap();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
eprintln!("dumped {} files -> {}", files.len(), out_file.display());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn canary(corpus: &Path) {
|
|
103
|
+
let files = sorted_rb_files(corpus);
|
|
104
|
+
let mut n_ok = 0usize;
|
|
105
|
+
let mut ast_mismatch = 0usize;
|
|
106
|
+
let mut comment_mismatch = 0usize;
|
|
107
|
+
let mut first_bad: Vec<String> = Vec::new();
|
|
108
|
+
for path in &files {
|
|
109
|
+
let Ok(src) = std::fs::read(path) else {
|
|
110
|
+
continue;
|
|
111
|
+
};
|
|
112
|
+
let normal = ruby_prism::parse(&src);
|
|
113
|
+
let fp_normal = fingerprint(&normal.node());
|
|
114
|
+
let n_comments_normal = normal.comments().count();
|
|
115
|
+
drop(normal);
|
|
116
|
+
|
|
117
|
+
let (spiked, _tokens) = parse_with_lex(&src);
|
|
118
|
+
let fp_spiked = fingerprint(&spiked.node());
|
|
119
|
+
let n_comments_spiked = spiked.comments().count();
|
|
120
|
+
drop(spiked); // exercise Drop of a transmuted ParseResult.
|
|
121
|
+
|
|
122
|
+
let ast_ok = fp_normal == fp_spiked;
|
|
123
|
+
let com_ok = n_comments_normal == n_comments_spiked;
|
|
124
|
+
if ast_ok && com_ok {
|
|
125
|
+
n_ok += 1;
|
|
126
|
+
} else {
|
|
127
|
+
if !ast_ok {
|
|
128
|
+
ast_mismatch += 1;
|
|
129
|
+
}
|
|
130
|
+
if !com_ok {
|
|
131
|
+
comment_mismatch += 1;
|
|
132
|
+
}
|
|
133
|
+
if first_bad.len() < 10 {
|
|
134
|
+
first_bad.push(format!(
|
|
135
|
+
"{} ast_ok={} nodes {}/{} comments {}/{}",
|
|
136
|
+
path.display(),
|
|
137
|
+
ast_ok,
|
|
138
|
+
fp_normal.len(),
|
|
139
|
+
fp_spiked.len(),
|
|
140
|
+
n_comments_normal,
|
|
141
|
+
n_comments_spiked
|
|
142
|
+
));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
println!("canary files={}", files.len());
|
|
147
|
+
println!(" parity_ok = {n_ok}");
|
|
148
|
+
println!(" ast_fingerprint_bad = {ast_mismatch}");
|
|
149
|
+
println!(" comment_count_bad = {comment_mismatch}");
|
|
150
|
+
for b in &first_bad {
|
|
151
|
+
println!(" BAD {b}");
|
|
152
|
+
}
|
|
153
|
+
let parity = if files.is_empty() {
|
|
154
|
+
0.0
|
|
155
|
+
} else {
|
|
156
|
+
100.0 * n_ok as f64 / files.len() as f64
|
|
157
|
+
};
|
|
158
|
+
println!(" parity = {parity:.2}%");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn profile(mode: &str, corpus: &Path, iters: usize) {
|
|
162
|
+
let files = sorted_rb_files(corpus);
|
|
163
|
+
let sources: Vec<Vec<u8>> = files.iter().filter_map(|p| std::fs::read(p).ok()).collect();
|
|
164
|
+
eprintln!("files: {}", sources.len());
|
|
165
|
+
|
|
166
|
+
let mut buf: Vec<RawToken> = Vec::new();
|
|
167
|
+
let run = |mode: &str, buf: &mut Vec<RawToken>| -> usize {
|
|
168
|
+
let mut sink = 0usize;
|
|
169
|
+
for s in &sources {
|
|
170
|
+
match mode {
|
|
171
|
+
"parse" => {
|
|
172
|
+
let r = ruby_prism::parse(s);
|
|
173
|
+
sink = sink.wrapping_add(r.node().location().end_offset());
|
|
174
|
+
}
|
|
175
|
+
"lex" => {
|
|
176
|
+
let (r, tokens) = parse_with_lex(s);
|
|
177
|
+
sink = sink.wrapping_add(r.node().location().end_offset());
|
|
178
|
+
sink = sink.wrapping_add(tokens.len());
|
|
179
|
+
}
|
|
180
|
+
"lex_reuse" => {
|
|
181
|
+
// Reuse one token buffer across files (retains capacity):
|
|
182
|
+
// isolates pure callback+push cost from per-file heap alloc.
|
|
183
|
+
let r = parse_with_lex_into(s, buf);
|
|
184
|
+
sink = sink.wrapping_add(r.node().location().end_offset());
|
|
185
|
+
sink = sink.wrapping_add(buf.len());
|
|
186
|
+
}
|
|
187
|
+
"reparse" => {
|
|
188
|
+
// AST from one parse; tokens would come from a *second*
|
|
189
|
+
// full parse (the pm_serialize_lex tax being reproduced).
|
|
190
|
+
let r = ruby_prism::parse(s);
|
|
191
|
+
sink = sink.wrapping_add(r.node().location().end_offset());
|
|
192
|
+
let r2 = ruby_prism::parse(s);
|
|
193
|
+
sink = sink.wrapping_add(r2.node().location().end_offset());
|
|
194
|
+
}
|
|
195
|
+
other => {
|
|
196
|
+
eprintln!("unknown profile mode: {other}");
|
|
197
|
+
std::process::exit(2);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
sink
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Warm-up (untimed).
|
|
205
|
+
let warm = run(mode, &mut buf);
|
|
206
|
+
let mut times: Vec<f64> = (0..iters)
|
|
207
|
+
.map(|_| {
|
|
208
|
+
let t0 = cpu_time_secs();
|
|
209
|
+
let sink = run(mode, &mut buf);
|
|
210
|
+
let dt = cpu_time_secs() - t0;
|
|
211
|
+
eprintln!(" iter {mode}: {dt:.4}s (sink {sink})");
|
|
212
|
+
dt
|
|
213
|
+
})
|
|
214
|
+
.collect();
|
|
215
|
+
times.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
216
|
+
let median = times[times.len() / 2];
|
|
217
|
+
eprintln!("warm sink {warm}");
|
|
218
|
+
println!("{mode}\t{median:.4}");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fn main() {
|
|
222
|
+
let args: Vec<String> = std::env::args().collect();
|
|
223
|
+
match args.get(1).map(String::as_str) {
|
|
224
|
+
Some("dump-tokens") => {
|
|
225
|
+
let corpus = PathBuf::from(args.get(2).expect("corpus dir"));
|
|
226
|
+
let out = PathBuf::from(args.get(3).expect("out file"));
|
|
227
|
+
dump_tokens(&corpus, &out);
|
|
228
|
+
}
|
|
229
|
+
Some("canary") => {
|
|
230
|
+
let corpus = PathBuf::from(args.get(2).expect("corpus dir"));
|
|
231
|
+
canary(&corpus);
|
|
232
|
+
}
|
|
233
|
+
Some("profile") => {
|
|
234
|
+
let mode = args.get(2).expect("mode").clone();
|
|
235
|
+
let corpus = PathBuf::from(args.get(3).expect("corpus dir"));
|
|
236
|
+
let iters: usize = args.get(4).map_or(5, |v| v.parse().expect("iters"));
|
|
237
|
+
profile(&mode, &corpus, iters);
|
|
238
|
+
}
|
|
239
|
+
_ => {
|
|
240
|
+
eprintln!(
|
|
241
|
+
"usage:\n pm_lex_probe dump-tokens <corpus> <out_file>\n pm_lex_probe canary <corpus>\n pm_lex_probe profile <parse|lex|lex_reuse|reparse> <corpus> [iters]"
|
|
242
|
+
);
|
|
243
|
+
std::process::exit(2);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
//! Single-pass parse + lex: raw prism parse that also collects the token stream.
|
|
2
|
+
//!
|
|
3
|
+
//! Goal: get the same in-memory AST that `ruby_prism::parse()` produces AND the
|
|
4
|
+
//! token stream from the *same single parse*, without prism's `pm_serialize_lex`
|
|
5
|
+
//! tax (a second full parse just to get tokens).
|
|
6
|
+
//!
|
|
7
|
+
//! How: `ruby_prism::parse()` hard-wires `pm_parser_init` -> `pm_parse` with no
|
|
8
|
+
//! seam to install a `lex_callback`. We replicate that body ourselves with the
|
|
9
|
+
//! raw `ruby_prism_sys` symbols, set `parser.lex_callback` to a token-collecting
|
|
10
|
+
//! C callback in between, then hand the result back to safe `ruby_prism` code by
|
|
11
|
+
//! `transmute`-ing a layout-identical look-alike into `ruby_prism::ParseResult`
|
|
12
|
+
//! (whose fields are private and whose only constructor, `parse()`, cannot take
|
|
13
|
+
//! a callback).
|
|
14
|
+
//!
|
|
15
|
+
//! The transmute is a load-bearing bet on `ParseResult`'s field layout. Two
|
|
16
|
+
//! things guard it: [`assert_parse_result_layout`] is a `const` static assert on
|
|
17
|
+
//! size + alignment (a compile error if either drifts), and the canary tests
|
|
18
|
+
//! (here and in `tests/pm_lex_canary.rs`) compare the transmute-path AST against
|
|
19
|
+
//! a normal `parse()` on a large corpus, catching any residual field-order
|
|
20
|
+
//! change on a rustc update. A constructor that takes a `lex_callback` is being
|
|
21
|
+
//! proposed upstream to prism; once it lands this module can drop the transmute.
|
|
22
|
+
|
|
23
|
+
use std::mem::MaybeUninit;
|
|
24
|
+
use std::os::raw::c_void;
|
|
25
|
+
use std::ptr::NonNull;
|
|
26
|
+
|
|
27
|
+
use ruby_prism::ParseResult;
|
|
28
|
+
use ruby_prism_sys::{
|
|
29
|
+
pm_lex_callback_t, pm_node_t, pm_parse, pm_parser_init, pm_parser_t, pm_token_t,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
pub use crate::pm_lex_token_names::PM_TOKEN_NAMES;
|
|
33
|
+
|
|
34
|
+
/// One lexer token, collected in-line during the parse.
|
|
35
|
+
/// Byte offsets are from the start of the source (prism is byte-oriented).
|
|
36
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
37
|
+
pub struct RawToken {
|
|
38
|
+
/// prism `pm_token_type` integer. Name via [`RawToken::type_name`].
|
|
39
|
+
pub token_type: u32,
|
|
40
|
+
pub start_offset: usize,
|
|
41
|
+
pub length: usize,
|
|
42
|
+
/// `parser.lex_state` sampled at callback time (matches `Prism.lex`).
|
|
43
|
+
pub lex_state: u32,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
impl RawToken {
|
|
47
|
+
/// Ruby `Prism::Token#type` symbol name for this token type.
|
|
48
|
+
#[must_use]
|
|
49
|
+
pub fn type_name(&self) -> &'static str {
|
|
50
|
+
PM_TOKEN_NAMES
|
|
51
|
+
.get(self.token_type as usize)
|
|
52
|
+
.copied()
|
|
53
|
+
.unwrap_or("")
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// A look-alike of `ruby_prism::ParseResult<'pr>`.
|
|
58
|
+
///
|
|
59
|
+
/// `ParseResult`'s fields are private, so we cannot name them; we mirror their
|
|
60
|
+
/// declared order and types here and `transmute`. `ParseResult` is `repr(Rust)`
|
|
61
|
+
/// (no `repr(C)`), so field order is not guaranteed by the language — this only
|
|
62
|
+
/// works because two structs with identical field types get the same layout
|
|
63
|
+
/// under the current rustc, and we pin size+alignment with a static assert. If a
|
|
64
|
+
/// future rustc reorders fields, the canary tests break loudly.
|
|
65
|
+
///
|
|
66
|
+
/// Fields are only ever read through the `transmute` into `ParseResult`, so the
|
|
67
|
+
/// compiler cannot see the reads — hence `dead_code` is allowed here.
|
|
68
|
+
#[repr(Rust)]
|
|
69
|
+
#[allow(dead_code)]
|
|
70
|
+
struct ParseResultLookAlike<'pr> {
|
|
71
|
+
source: &'pr [u8],
|
|
72
|
+
parser: NonNull<pm_parser_t>,
|
|
73
|
+
node: NonNull<pm_node_t>,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Compile-time guard: our look-alike must match `ParseResult` byte-for-byte in
|
|
77
|
+
/// size and alignment. (Field *order* is the residual, canary-tested bet.)
|
|
78
|
+
pub const fn assert_parse_result_layout() {
|
|
79
|
+
use std::mem::{align_of, size_of};
|
|
80
|
+
assert!(
|
|
81
|
+
size_of::<ParseResultLookAlike<'_>>() == size_of::<ParseResult<'_>>(),
|
|
82
|
+
"ParseResult size drifted from look-alike; transmute is unsafe"
|
|
83
|
+
);
|
|
84
|
+
assert!(
|
|
85
|
+
align_of::<ParseResultLookAlike<'_>>() == align_of::<ParseResult<'_>>(),
|
|
86
|
+
"ParseResult alignment drifted from look-alike; transmute is unsafe"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const _: () = assert_parse_result_layout();
|
|
90
|
+
|
|
91
|
+
/// The C callback prism invokes for every lexed token. `data` is a
|
|
92
|
+
/// `*mut Vec<RawToken>`. Reads `parser.lex_state` at call time, mirroring how
|
|
93
|
+
/// `Prism.lex` records lex state.
|
|
94
|
+
///
|
|
95
|
+
/// # Safety
|
|
96
|
+
/// `data` must point to a live `Vec<RawToken>`; `parser`/`token` are valid for
|
|
97
|
+
/// the duration of the call (prism guarantees this while lexing).
|
|
98
|
+
unsafe extern "C" fn collect_token(
|
|
99
|
+
data: *mut c_void,
|
|
100
|
+
parser: *mut pm_parser_t,
|
|
101
|
+
token: *mut pm_token_t,
|
|
102
|
+
) {
|
|
103
|
+
// SAFETY: contract documented above; all derefs are of live prism/caller
|
|
104
|
+
// memory for the span of this call.
|
|
105
|
+
unsafe {
|
|
106
|
+
let tokens = &mut *(data.cast::<Vec<RawToken>>());
|
|
107
|
+
let tok = &*token;
|
|
108
|
+
let source_start = (*parser).start;
|
|
109
|
+
let start_offset = tok.start.offset_from(source_start) as usize;
|
|
110
|
+
let length = tok.end.offset_from(tok.start) as usize;
|
|
111
|
+
let lex_state = (*parser).lex_state;
|
|
112
|
+
tokens.push(RawToken {
|
|
113
|
+
token_type: tok.type_,
|
|
114
|
+
start_offset,
|
|
115
|
+
length,
|
|
116
|
+
lex_state,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Like [`parse_with_lex`] but appends tokens into a caller-owned buffer
|
|
122
|
+
/// (cleared first, capacity retained). Lets a caller amortize the token Vec
|
|
123
|
+
/// allocation across files, isolating the pure callback+push cost from
|
|
124
|
+
/// per-file heap allocation. Returns the `ParseResult`.
|
|
125
|
+
///
|
|
126
|
+
/// # Safety
|
|
127
|
+
/// Uses raw prism FFI and a `transmute` into `ParseResult`. Sound as long as
|
|
128
|
+
/// `ParseResultLookAlike` matches `ParseResult`'s layout (guarded by the static
|
|
129
|
+
/// assert + canary tests) and prism's C ABI is stable.
|
|
130
|
+
#[must_use]
|
|
131
|
+
pub fn parse_with_lex_into<'s>(
|
|
132
|
+
source: &'s [u8],
|
|
133
|
+
out: &mut Vec<RawToken>,
|
|
134
|
+
) -> ParseResult<'s> {
|
|
135
|
+
out.clear();
|
|
136
|
+
let mut callback = pm_lex_callback_t {
|
|
137
|
+
data: (out as *mut Vec<RawToken>).cast::<c_void>(),
|
|
138
|
+
callback: Some(collect_token),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// SAFETY: replicate ruby_prism::parse()'s body verbatim, plus lex_callback.
|
|
142
|
+
let look_alike = unsafe {
|
|
143
|
+
let uninit = Box::new(MaybeUninit::<pm_parser_t>::uninit());
|
|
144
|
+
let uninit = Box::into_raw(uninit);
|
|
145
|
+
|
|
146
|
+
pm_parser_init(
|
|
147
|
+
(*uninit).as_mut_ptr(),
|
|
148
|
+
source.as_ptr(),
|
|
149
|
+
source.len(),
|
|
150
|
+
std::ptr::null(),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
let parser = (*uninit).assume_init_mut();
|
|
154
|
+
let parser = NonNull::new_unchecked(parser);
|
|
155
|
+
|
|
156
|
+
(*parser.as_ptr()).lex_callback = &mut callback as *mut pm_lex_callback_t;
|
|
157
|
+
|
|
158
|
+
let node = pm_parse(parser.as_ptr());
|
|
159
|
+
let node = NonNull::new_unchecked(node);
|
|
160
|
+
|
|
161
|
+
(*parser.as_ptr()).lex_callback = std::ptr::null_mut();
|
|
162
|
+
|
|
163
|
+
ParseResultLookAlike {
|
|
164
|
+
source,
|
|
165
|
+
parser,
|
|
166
|
+
node,
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// SAFETY: identical layout (static assert on size/align; canary on fields).
|
|
171
|
+
unsafe { std::mem::transmute(look_alike) }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Parse `source` and collect its token stream in the *same* parse.
|
|
175
|
+
///
|
|
176
|
+
/// Returns a real `ruby_prism::ParseResult` (safe to walk / drop like any other)
|
|
177
|
+
/// plus the collected tokens. Mirrors `ruby_prism::parse()` exactly, only adding
|
|
178
|
+
/// the `lex_callback` wiring in between init and parse. Allocates a fresh token
|
|
179
|
+
/// Vec per call; use [`parse_with_lex_into`] to reuse one buffer across files.
|
|
180
|
+
///
|
|
181
|
+
/// # Safety
|
|
182
|
+
/// Same contract as [`parse_with_lex_into`].
|
|
183
|
+
#[must_use]
|
|
184
|
+
pub fn parse_with_lex(source: &[u8]) -> (ParseResult<'_>, Vec<RawToken>) {
|
|
185
|
+
// Tokens Vec lives on the heap in a stable slot that outlives `pm_parse`.
|
|
186
|
+
let mut tokens: Box<Vec<RawToken>> = Box::default();
|
|
187
|
+
let mut callback = pm_lex_callback_t {
|
|
188
|
+
data: (&mut *tokens as *mut Vec<RawToken>).cast::<c_void>(),
|
|
189
|
+
callback: Some(collect_token),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// SAFETY: replicate ruby_prism::parse()'s body verbatim, plus lex_callback.
|
|
193
|
+
let look_alike = unsafe {
|
|
194
|
+
let uninit = Box::new(MaybeUninit::<pm_parser_t>::uninit());
|
|
195
|
+
let uninit = Box::into_raw(uninit);
|
|
196
|
+
|
|
197
|
+
pm_parser_init(
|
|
198
|
+
(*uninit).as_mut_ptr(),
|
|
199
|
+
source.as_ptr(),
|
|
200
|
+
source.len(),
|
|
201
|
+
std::ptr::null(),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
let parser = (*uninit).assume_init_mut();
|
|
205
|
+
let parser = NonNull::new_unchecked(parser);
|
|
206
|
+
|
|
207
|
+
// Install the collecting callback before parsing.
|
|
208
|
+
(*parser.as_ptr()).lex_callback = &mut callback as *mut pm_lex_callback_t;
|
|
209
|
+
|
|
210
|
+
let node = pm_parse(parser.as_ptr());
|
|
211
|
+
let node = NonNull::new_unchecked(node);
|
|
212
|
+
|
|
213
|
+
// Detach the callback: nothing reads it post-parse, and `callback`
|
|
214
|
+
// (and `tokens`) get moved/dropped independently of the parser.
|
|
215
|
+
(*parser.as_ptr()).lex_callback = std::ptr::null_mut();
|
|
216
|
+
|
|
217
|
+
ParseResultLookAlike {
|
|
218
|
+
source,
|
|
219
|
+
parser,
|
|
220
|
+
node,
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// The load-bearing transmute. Look-alike -> real ParseResult.
|
|
225
|
+
// SAFETY: identical layout (static assert on size/align; canary on fields).
|
|
226
|
+
let result: ParseResult<'_> = unsafe { std::mem::transmute(look_alike) };
|
|
227
|
+
|
|
228
|
+
(result, *tokens)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#[cfg(test)]
|
|
232
|
+
mod tests {
|
|
233
|
+
use super::*;
|
|
234
|
+
use ruby_prism::{Node, Visit};
|
|
235
|
+
|
|
236
|
+
// Pre-order (start,end) offset fingerprint of an AST via the Visit trait.
|
|
237
|
+
struct Fingerprint(Vec<(usize, usize)>);
|
|
238
|
+
impl<'pr> Visit<'pr> for Fingerprint {
|
|
239
|
+
fn visit_branch_node_enter(&mut self, node: Node<'pr>) {
|
|
240
|
+
let l = node.location();
|
|
241
|
+
self.0.push((l.start_offset(), l.end_offset()));
|
|
242
|
+
}
|
|
243
|
+
fn visit_leaf_node_enter(&mut self, node: Node<'pr>) {
|
|
244
|
+
let l = node.location();
|
|
245
|
+
self.0.push((l.start_offset(), l.end_offset()));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
fn fingerprint(root: &Node<'_>) -> Vec<(usize, usize)> {
|
|
249
|
+
let mut fp = Fingerprint(Vec::new());
|
|
250
|
+
fp.visit(root);
|
|
251
|
+
fp.0
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[test]
|
|
255
|
+
fn layout_static_assert_holds() {
|
|
256
|
+
assert_parse_result_layout();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#[test]
|
|
260
|
+
fn collects_tokens_matching_a_known_snippet() {
|
|
261
|
+
let src = b"def foo(a)\n a + 1\nend\n";
|
|
262
|
+
let (_result, tokens) = parse_with_lex(src);
|
|
263
|
+
let named: Vec<(&str, usize, usize, u32)> = tokens
|
|
264
|
+
.iter()
|
|
265
|
+
.map(|t| (t.type_name(), t.start_offset, t.length, t.lex_state))
|
|
266
|
+
.collect();
|
|
267
|
+
// From `Prism.lex` on the same source.
|
|
268
|
+
assert_eq!(named[0], ("KEYWORD_DEF", 0, 3, 128));
|
|
269
|
+
assert_eq!(named[1], ("IDENTIFIER", 4, 3, 8));
|
|
270
|
+
assert_eq!(named[2], ("PARENTHESIS_LEFT", 7, 1, 1025));
|
|
271
|
+
// Last token is always EOF.
|
|
272
|
+
assert_eq!(tokens.last().unwrap().type_name(), "EOF");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[test]
|
|
276
|
+
fn transmute_path_ast_matches_normal_parse() {
|
|
277
|
+
let srcs: &[&[u8]] = &[
|
|
278
|
+
b"x = 1\n",
|
|
279
|
+
b"def m(a, b)\n a.foo { |c| c + b }\nend\n",
|
|
280
|
+
b"class C < D\n attr_reader :x\n # comment\nend\n",
|
|
281
|
+
b"[1, 2, 3].map { _1 * 2 }.select(&:even?)\n",
|
|
282
|
+
"\u{3042} = 'multibyte'\n".as_bytes(), // non-ASCII
|
|
283
|
+
];
|
|
284
|
+
for src in srcs {
|
|
285
|
+
let normal = ruby_prism::parse(src);
|
|
286
|
+
let (spiked, _tokens) = parse_with_lex(src);
|
|
287
|
+
assert_eq!(
|
|
288
|
+
fingerprint(&normal.node()),
|
|
289
|
+
fingerprint(&spiked.node()),
|
|
290
|
+
"AST fingerprint diverged for {:?}",
|
|
291
|
+
String::from_utf8_lossy(src)
|
|
292
|
+
);
|
|
293
|
+
// Comments must also survive the transmute path.
|
|
294
|
+
let n_comments_normal = normal.comments().count();
|
|
295
|
+
let n_comments_spiked = spiked.comments().count();
|
|
296
|
+
assert_eq!(n_comments_normal, n_comments_spiked);
|
|
297
|
+
// Drop of `spiked` (a transmuted ParseResult) must not crash.
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|