Skip to content

Commit 3006491

Browse files
committed
refactor: Extract bytecode lowering into dedicated pass
1 parent e9ae693 commit 3006491

7 files changed

Lines changed: 405 additions & 86 deletions

File tree

‎crates/plotnik-compiler/src/bytecode/ir.rs‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -606,11 +606,20 @@ impl MatchIR {
606606
// counts layout: pre(3) | neg(3) | post(3) | succ(5) | has_pred(1) | reserved(1)
607607
assert!(
608608
pre_count <= 7,
609-
"pre_effects overflow: {pre_count} > 7 (use emit_match_with_cascade)"
609+
"pre_effects overflow: {pre_count} > 7 (lowering should have cascaded)"
610+
);
611+
assert!(
612+
neg_count <= 7,
613+
"neg_fields overflow: {neg_count} > 7 (lowering should have cascaded)"
614+
);
615+
assert!(
616+
post_count <= 7,
617+
"post_effects overflow: {post_count} > 7 (lowering should have cascaded)"
618+
);
619+
assert!(
620+
succ_count <= 31,
621+
"successors overflow: {succ_count} > 31 (lowering should have cascaded)"
610622
);
611-
assert!(neg_count <= 7, "neg_fields overflow: {neg_count} > 7");
612-
assert!(post_count <= 7, "post_effects overflow: {post_count} > 7");
613-
assert!(succ_count <= 31, "successors overflow: {succ_count} > 31");
614623

615624
let counts = ((pre_count as u16) << 13)
616625
| ((neg_count as u16) << 10)

‎crates/plotnik-compiler/src/compile/compiler.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use plotnik_bytecode::Nav;
1515
use super::capture::CaptureEffects;
1616
use super::dce::remove_unreachable;
1717
use super::epsilon_elim::eliminate_epsilons;
18+
use super::lower::lower;
1819
use super::error::{CompileError, CompileResult};
1920
use super::scope::StructScope;
2021
use super::verify::debug_verify_ir_fingerprint;
@@ -86,6 +87,9 @@ impl<'a> Compiler<'a> {
8687
// Remove unreachable instructions (bypassed epsilons, etc.)
8788
remove_unreachable(&mut result);
8889

90+
// Lower to bytecode-compatible form (cascade overflows)
91+
lower(&mut result);
92+
8993
Ok(result)
9094
}
9195

‎crates/plotnik-compiler/src/compile/expressions.rs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ impl Compiler<'_> {
4949
if let Some(p) = predicate {
5050
m = m.predicate(p);
5151
}
52-
return self.emit_match_with_cascade(m);
52+
return self.emit_match(m);
5353
}
5454

5555
// Determine Up navigation based on trailing anchor
@@ -123,7 +123,7 @@ impl Compiler<'_> {
123123
if let Some(p) = predicate {
124124
entry_match = entry_match.predicate(p);
125125
}
126-
self.emit_match_with_cascade(entry_match);
126+
self.emit_match(entry_match);
127127

128128
entry
129129
}
@@ -201,7 +201,7 @@ impl Compiler<'_> {
201201
if let Some(p) = predicate {
202202
entry_match = entry_match.predicate(p);
203203
}
204-
self.emit_match_with_cascade(entry_match);
204+
self.emit_match(entry_match);
205205

206206
entry
207207
}
@@ -236,7 +236,7 @@ impl Compiler<'_> {
236236
None => NodeTypeIR::Any, // `_` wildcard matches any node
237237
};
238238

239-
self.emit_match_with_cascade(
239+
self.emit_match(
240240
MatchIR::epsilon(entry, exit)
241241
.nav(nav)
242242
.node_type(node_type)
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
//! Lowering pass: transforms unconstrained IR into bytecode-compatible form.
2+
//!
3+
//! This pass handles bytecode encoding constraints by splitting oversized
4+
//! instructions into cascading epsilon chains:
5+
//! - pre_effects > 7 → epsilon chain before match
6+
//! - post_effects > 7 → epsilon chain after match
7+
//! - neg_fields > 7 → epsilon chain for overflow checks
8+
//! - successors > 28 → cascading epsilon branches
9+
10+
use plotnik_bytecode::{MAX_MATCH_PAYLOAD_SLOTS, MAX_PRE_EFFECTS};
11+
12+
use crate::bytecode::{EffectIR, InstructionIR, Label, MatchIR};
13+
use crate::compile::CompileResult;
14+
15+
const MAX_POST_EFFECTS: usize = 7;
16+
const MAX_NEG_FIELDS: usize = 7;
17+
18+
enum PostChain {
19+
NegFields(Vec<u16>),
20+
PostEffects(Vec<EffectIR>),
21+
Successors(Vec<Label>),
22+
}
23+
24+
/// Lower IR to bytecode-compatible form.
25+
///
26+
/// Transforms instructions that exceed bytecode encoding limits into
27+
/// equivalent cascades of smaller instructions.
28+
pub fn lower(result: &mut CompileResult) {
29+
let mut next_label = result
30+
.instructions
31+
.iter()
32+
.map(|i| i.label().0)
33+
.max()
34+
.unwrap_or(0)
35+
+ 1;
36+
37+
let mut new_instructions = Vec::with_capacity(result.instructions.len());
38+
39+
for instr in result.instructions.drain(..) {
40+
let InstructionIR::Match(m) = instr else {
41+
new_instructions.push(instr);
42+
continue;
43+
};
44+
lower_match(m, &mut new_instructions, &mut next_label);
45+
}
46+
47+
result.instructions = new_instructions;
48+
}
49+
50+
fn fresh_label(next: &mut u32) -> Label {
51+
let l = Label(*next);
52+
*next += 1;
53+
l
54+
}
55+
56+
fn lower_match(mut m: MatchIR, out: &mut Vec<InstructionIR>, next_label: &mut u32) {
57+
// Step 1: Handle pre_effects overflow → epsilon chain before the match
58+
if m.pre_effects.len() > MAX_PRE_EFFECTS {
59+
let all_pre = std::mem::take(&mut m.pre_effects);
60+
let entry = m.label;
61+
m.label = fresh_label(next_label);
62+
emit_effects_chain(entry, m.label, all_pre, out, next_label);
63+
}
64+
65+
// Collect overflow chains to emit after the match
66+
let mut post_chains: Vec<PostChain> = Vec::new();
67+
68+
if m.neg_fields.len() > MAX_NEG_FIELDS {
69+
let overflow: Vec<_> = m.neg_fields.drain(MAX_NEG_FIELDS..).collect();
70+
post_chains.push(PostChain::NegFields(overflow));
71+
}
72+
73+
if m.post_effects.len() > MAX_POST_EFFECTS {
74+
let overflow: Vec<_> = m.post_effects.drain(MAX_POST_EFFECTS..).collect();
75+
post_chains.push(PostChain::PostEffects(overflow));
76+
}
77+
78+
if m.successors.len() > MAX_MATCH_PAYLOAD_SLOTS {
79+
let overflow: Vec<_> = m.successors.drain(MAX_MATCH_PAYLOAD_SLOTS - 1..).collect();
80+
post_chains.push(PostChain::Successors(overflow));
81+
}
82+
83+
if post_chains.is_empty() {
84+
out.push(m.into());
85+
return;
86+
}
87+
88+
// Build chain: match → chain1 → chain2 → ... → final_succs
89+
let mut current_succs = std::mem::take(&mut m.successors);
90+
91+
for chain in post_chains.into_iter().rev() {
92+
let chain_entry = fresh_label(next_label);
93+
match chain {
94+
PostChain::NegFields(neg_fields) => {
95+
emit_neg_fields_chain(chain_entry, current_succs, neg_fields, out, next_label);
96+
}
97+
PostChain::PostEffects(effects) => {
98+
emit_effects_chain_to_succs(chain_entry, current_succs, effects, out, next_label);
99+
}
100+
PostChain::Successors(succs) => {
101+
emit_successors_cascade(chain_entry, succs, out, next_label);
102+
current_succs = vec![chain_entry];
103+
continue;
104+
}
105+
}
106+
current_succs = vec![chain_entry];
107+
}
108+
109+
m.successors = current_succs;
110+
out.push(m.into());
111+
}
112+
113+
fn emit_effects_chain(
114+
entry: Label,
115+
exit: Label,
116+
mut effects: Vec<EffectIR>,
117+
out: &mut Vec<InstructionIR>,
118+
next_label: &mut u32,
119+
) {
120+
if effects.is_empty() {
121+
out.push(MatchIR::epsilon(entry, exit).into());
122+
return;
123+
}
124+
125+
if effects.len() <= MAX_PRE_EFFECTS {
126+
out.push(MatchIR::epsilon(entry, exit).pre_effects(effects).into());
127+
return;
128+
}
129+
130+
let first_batch: Vec<_> = effects.drain(..MAX_PRE_EFFECTS).collect();
131+
let intermediate = fresh_label(next_label);
132+
out.push(
133+
MatchIR::epsilon(entry, intermediate)
134+
.pre_effects(first_batch)
135+
.into(),
136+
);
137+
emit_effects_chain(intermediate, exit, effects, out, next_label);
138+
}
139+
140+
fn emit_neg_fields_chain(
141+
entry: Label,
142+
final_succs: Vec<Label>,
143+
mut neg_fields: Vec<u16>,
144+
out: &mut Vec<InstructionIR>,
145+
next_label: &mut u32,
146+
) {
147+
if neg_fields.len() <= MAX_NEG_FIELDS {
148+
let mut m = MatchIR::at(entry).neg_fields(neg_fields);
149+
m.successors = final_succs;
150+
out.push(m.into());
151+
return;
152+
}
153+
154+
let first_batch: Vec<_> = neg_fields.drain(..MAX_NEG_FIELDS).collect();
155+
let intermediate = fresh_label(next_label);
156+
out.push(
157+
MatchIR::at(entry)
158+
.neg_fields(first_batch)
159+
.next(intermediate)
160+
.into(),
161+
);
162+
emit_neg_fields_chain(intermediate, final_succs, neg_fields, out, next_label);
163+
}
164+
165+
fn emit_effects_chain_to_succs(
166+
entry: Label,
167+
final_succs: Vec<Label>,
168+
mut effects: Vec<EffectIR>,
169+
out: &mut Vec<InstructionIR>,
170+
next_label: &mut u32,
171+
) {
172+
if effects.len() <= MAX_POST_EFFECTS {
173+
let mut m = MatchIR::at(entry).post_effects(effects);
174+
m.successors = final_succs;
175+
out.push(m.into());
176+
return;
177+
}
178+
179+
let first_batch: Vec<_> = effects.drain(..MAX_POST_EFFECTS).collect();
180+
let intermediate = fresh_label(next_label);
181+
out.push(
182+
MatchIR::at(entry)
183+
.post_effects(first_batch)
184+
.next(intermediate)
185+
.into(),
186+
);
187+
emit_effects_chain_to_succs(intermediate, final_succs, effects, out, next_label);
188+
}
189+
190+
fn emit_successors_cascade(
191+
entry: Label,
192+
mut succs: Vec<Label>,
193+
out: &mut Vec<InstructionIR>,
194+
next_label: &mut u32,
195+
) {
196+
if succs.len() <= MAX_MATCH_PAYLOAD_SLOTS {
197+
out.push(MatchIR::at(entry).next_many(succs).into());
198+
return;
199+
}
200+
201+
let overflow: Vec<_> = succs.drain(MAX_MATCH_PAYLOAD_SLOTS - 1..).collect();
202+
let intermediate = fresh_label(next_label);
203+
succs.push(intermediate);
204+
out.push(MatchIR::at(entry).next_many(succs).into());
205+
emit_successors_cascade(intermediate, overflow, out, next_label);
206+
}

0 commit comments

Comments
 (0)