Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions crates/plotnik-lib/src/bytecode/dump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use super::ids::TypeId;
use super::instructions::StepId;
use super::ir::NodeTypeIR;
use super::module::{Instruction, Module};
use super::nav::Nav;
use super::type_meta::{TypeData, TypeKind};
use super::{Call, Match, Return, Trampoline};

Expand Down Expand Up @@ -364,6 +365,30 @@ fn dump_entrypoints(out: &mut String, module: &Module, ctx: &DumpContext) {
out.push('\n');
}

/// Check if an instruction is padding (all-zeros Match8).
///
/// Padding slots contain zero bytes which decode as terminal epsilon Match8
/// with Any node type, no field constraint, and next=0.
fn is_padding(instr: &Instruction) -> bool {
match instr {
Instruction::Match(m) => {
m.is_match8()
&& m.nav == Nav::Epsilon
&& matches!(m.node_type, NodeTypeIR::Any)
&& m.node_field.is_none()
&& m.is_terminal()
}
_ => false,
}
}

/// Format a single padding step line.
///
/// Output: ` 07 ... ` (step number and " ... " in symbol column)
fn format_padding_step(step: u16, step_width: usize) -> String {
LineBuilder::new(step_width).instruction_prefix(step, Symbol::PADDING)
}

fn dump_code(out: &mut String, module: &Module, ctx: &DumpContext) {
let c = &ctx.colors;
let header = module.header();
Expand All @@ -386,6 +411,14 @@ fn dump_code(out: &mut String, module: &Module, ctx: &DumpContext) {
}

let instr = module.decode_step(step);

// Check for padding (all-zeros Match8 instruction)
if is_padding(&instr) {
writeln!(out, "{}", format_padding_step(step, step_width)).unwrap();
step += 1;
continue;
}

let line = format_instruction(step, &instr, module, ctx, step_width);
out.push_str(&line);
out.push('\n');
Expand Down
3 changes: 3 additions & 0 deletions crates/plotnik-lib/src/bytecode/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ impl Symbol {
/// Epsilon symbol for unconditional transitions.
pub const EPSILON: Symbol = Symbol::new(" ", "ε", " ");

/// Padding indicator (centered "..." in 5-char column).
pub const PADDING: Symbol = Symbol::new(" ", "...", " ");

/// Format as a 5-character string.
pub fn format(&self) -> String {
format!("{}{}{}", self.left, self.center, self.right)
Expand Down
6 changes: 6 additions & 0 deletions crates/plotnik-lib/src/bytecode/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ impl<'a> Match<'a> {
self.nav == Nav::Epsilon
}

/// Check if this is a Match8 (8-byte fast-path instruction).
#[inline]
pub fn is_match8(&self) -> bool {
self.is_match8
}

/// Number of successors.
#[inline]
pub fn succ_count(&self) -> usize {
Expand Down
29 changes: 17 additions & 12 deletions crates/plotnik-lib/src/emit/layout.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Cache-aligned instruction layout.
//!
//! Uses Pettis-Hansen inspired greedy chain extraction to place
//! hot paths contiguously and avoid cache line straddling.
//! Extracts linear chains from the control flow graph and places them
//! contiguously. Pads instructions to prevent cache line straddling.

use std::collections::{BTreeMap, HashSet};

Expand Down Expand Up @@ -161,18 +161,23 @@ fn assign_step_ids(
};
let size = instr.size();

// Cache line alignment for large instructions
if size >= 48 {
let line_offset = current_offset % CACHE_LINE;
if line_offset + size > CACHE_LINE {
// Would straddle cache line - pad to next line
let padding_bytes = CACHE_LINE - line_offset;
let padding_steps = (padding_bytes / STEP_SIZE) as u16;
current_step += padding_steps;
current_offset += padding_bytes;
}
// Pad if instruction would straddle cache line boundary
let line_offset = current_offset % CACHE_LINE;
if line_offset + size > CACHE_LINE {
let padding_bytes = CACHE_LINE - line_offset;
let padding_steps = (padding_bytes / STEP_SIZE) as u16;
current_step += padding_steps;
current_offset += padding_bytes;
}

// Invariant: instruction must not straddle cache line
assert!(
current_offset % CACHE_LINE + size <= CACHE_LINE,
"instruction at offset {} with size {} straddles 64-byte cache line",
current_offset,
size
);

mapping.insert(label, current_step);
let step_count = (size / STEP_SIZE) as u16;
current_step += step_count;
Expand Down
159 changes: 123 additions & 36 deletions crates/plotnik-lib/src/emit/layout_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,45 +113,132 @@ fn layout_branch() {
}

#[test]
fn layout_large_instruction_cache_alignment() {
// Large instruction (Match48 = 48 bytes = 6 steps) near cache line boundary
// Start at step 5 (offset 40), would straddle - should pad
let large_match = MatchIR::at(Label(1))
.nav(Nav::Down)
.node_type(NodeTypeIR::Named(NonZeroU16::new(10)))
.pre_effect(EffectIR::start_obj())
.pre_effect(EffectIR::start_obj())
.pre_effect(EffectIR::start_obj())
.post_effect(EffectIR::node())
.post_effect(EffectIR::end_obj())
.post_effect(EffectIR::end_obj())
.post_effect(EffectIR::end_obj())
.next_many(vec![
Label(100),
Label(101),
Label(102),
Label(103),
Label(104),
Label(105),
Label(106),
Label(107),
]);

// Verify it's large enough to trigger alignment
assert!(large_match.size() >= 48);
fn layout_match16_cache_alignment() {
// Match16 (16 bytes, 2 steps) at offset 56 would straddle (56+16=72 > 64)
// Place 7 Match8s (7 steps = 56 bytes) before Match16
// Expected: Match16 gets padded to step 8 (offset 64)
let mut instructions = Vec::new();

// 7 Match8 instructions in a chain: Label(0) -> Label(1) -> ... -> Label(6) -> Label(7)
for i in 0..7 {
instructions.push(
MatchIR::at(Label(i))
.nav(Nav::Down)
.next(Label(i + 1))
.into(),
);
}

// Match16 at Label(7): needs 2+ successors to become Match16
instructions.push(
MatchIR::at(Label(7))
.nav(Nav::Down)
.next_many(vec![Label(100), Label(101)])
.into(),
);

let instructions = vec![
// Small instruction first
MatchIR::epsilon(Label(0), Label(1)).into(),
large_match.into(),
];
let result = CacheAligned::layout(&instructions, &[Label(0)]);

// Labels 0-6 should be at steps 0-6 (no padding needed)
for i in 0..7 {
assert_eq!(
result.label_to_step.get(&Label(i)),
Some(&(i as u16)),
"Label({i}) should be at step {i}"
);
}

// Label(7) would be at step 7 (offset 56) without padding
// But Match16 at offset 56 straddles (56+16=72 > 64), so it must be padded
// After padding: step 8 (offset 64)
let step7 = *result.label_to_step.get(&Label(7)).unwrap();
assert_eq!(step7, 8, "Match16 should be padded to step 8 (offset 64)");

// Total steps: 8 (padding at step 7) + 2 (Match16) = 10
assert_eq!(result.total_steps, 10);
}

#[test]
fn layout_match8_no_padding_needed() {
// Match8 (8 bytes) never straddles: max offset 56, 56+8=64 <= 64
// Place 7 Match8s, then another Match8 - should NOT need padding
let mut instructions = Vec::new();

for i in 0..8 {
if i < 7 {
instructions.push(
MatchIR::at(Label(i))
.nav(Nav::Down)
.next(Label(i + 1))
.into(),
);
} else {
instructions.push(MatchIR::terminal(Label(i)).nav(Nav::Down).into());
}
}

let result = CacheAligned::layout(&instructions, &[Label(0)]);

// Label 0 at step 0 (offset 0)
assert_eq!(result.label_to_step.get(&Label(0)), Some(&0u16));
// All 8 Match8s should be contiguous: steps 0-7
for i in 0..8 {
assert_eq!(
result.label_to_step.get(&Label(i)),
Some(&(i as u16)),
"Label({i}) should be at step {i} (no padding)"
);
}

// Total steps: 8 (no padding)
assert_eq!(result.total_steps, 8);
}

#[test]
fn layout_match32_cache_alignment() {
// Match32 (32 bytes, 4 steps) at offset 40 would straddle (40+32=72 > 64)
// Place 5 Match8s (5 steps = 40 bytes) before Match32
// Expected: Match32 gets padded to step 8 (offset 64)
let mut instructions: Vec<crate::bytecode::InstructionIR> = Vec::new();

// 5 Match8 instructions: Label(0) -> ... -> Label(4) -> Label(5)
for i in 0..5 {
instructions.push(
MatchIR::at(Label(i))
.nav(Nav::Down)
.next(Label(i + 1))
.into(),
);
}

// Match32 at Label(5): needs enough payload to become Match32 (9-12 slots)
// 3 pre + 3 post + 4 successors = 10 slots -> Match32
instructions.push(
MatchIR::at(Label(5))
.nav(Nav::Down)
.pre_effect(EffectIR::start_obj())
.pre_effect(EffectIR::start_obj())
.pre_effect(EffectIR::start_obj())
.post_effect(EffectIR::end_obj())
.post_effect(EffectIR::end_obj())
.post_effect(EffectIR::end_obj())
.next_many(vec![Label(100), Label(101), Label(102), Label(103)])
.into(),
);

// Verify it's Match32 (32 bytes)
assert_eq!(instructions.last().unwrap().size(), 32);

let result = CacheAligned::layout(&instructions, &[Label(0)]);

// Labels 0-4 at steps 0-4
for i in 0..5 {
assert_eq!(result.label_to_step.get(&Label(i)), Some(&(i as u16)));
}

// Label(5) would be at step 5 (offset 40) without padding
// Match32 at offset 40 straddles (40+32=72 > 64), so padded to step 8
let step5 = *result.label_to_step.get(&Label(5)).unwrap();
assert_eq!(step5, 8, "Match32 should be padded to step 8 (offset 64)");

// Label 1 should be aligned - either at step 1 or padded to cache line
let step1 = *result.label_to_step.get(&Label(1)).unwrap();
assert!(step1 >= 1);
// Total steps: 8 (3 padding steps at 5,6,7) + 4 (Match32) = 12
assert_eq!(result.total_steps, 12);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ _ObjWrap:
05 ▶

Test:
06 ε 07
07 ε 10, 12
09 ▶
10 ! (identifier) [Node Set(M0)] 09
12 ! (number) [Node Set(M0)] 09
06 ε 08
07 ...
08 ε 11, 13
10 ▶
11 ! (identifier) [Node Set(M0)] 10
13 ! (number) [Node Set(M0)] 10
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ _ObjWrap:
05 ▶

Test:
06 ε 07
07 ε 12, 15
09 ▶
10 ε [Set(M4)] 09
12 ! [Enum(M2)] (identifier) [Node Set(M0) EndEnum] 10
15 ! [Enum(M3)] (number) [Node Set(M1) EndEnum] 10
06 ε 08
07 ...
08 ε 13, 16
10 ▶
11 ε [Set(M4)] 10
13 ! [Enum(M2)] (identifier) [Node Set(M0) EndEnum] 11
16 ! [Enum(M3)] (number) [Node Set(M1) EndEnum] 11
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,26 @@ Test:
06 ε 07
07 ! (object) 08
08 ε [Arr] 10
10 ε 39, 20
10 ε 42, 21
12 ε [EndArr Set(M5)] 14
14 △ _ 19
15 ε [EndObj Push] 17
17 ε 45, 12
19 ▶
20 ε [EndArr Set(M5)] 19
22 ε [Set(M4)] 15
24 ! [Enum(M2)] (pair) [Node Set(M0) EndEnum] 22
27 ! [Enum(M3)] (shorthand_property_identifier) [Node Set(M1) EndEnum] 22
30 ε 24, 27
32 ε [Obj] 30
34 ▷ _ 37
35 ε 34, 12
37 ε 32, 35
39 ▽ _ 37
40 ▷ _ 43
41 ε 40, 12
43 ε 32, 41
45 ▷ _ 43
14 △ _ 20
15 ...
16 ε [EndObj Push] 18
18 ε 48, 12
20 ▶
21 ε [EndArr Set(M5)] 20
23 ...
24 ε [Set(M4)] 16
26 ! [Enum(M2)] (pair) [Node Set(M0) EndEnum] 24
29 ! [Enum(M3)] (shorthand_property_identifier) [Node Set(M1) EndEnum] 24
32 ε 26, 29
34 ε [Obj] 32
36 ▷ _ 40
37 ε 36, 12
39 ...
40 ε 34, 37
42 ▽ _ 40
43 ▷ _ 46
44 ε 43, 12
46 ε 34, 44
48 ▷ _ 46
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@ _ObjWrap:
05 ▶

Test:
06 ε 07
07 ε 10, 13
09 ▶
10 ! [Enum(M2)] (identifier) [Node Set(M0) EndEnum] 09
13 ! [Enum(M3)] (number) [Node Set(M1) EndEnum] 09
06 ε 08
07 ...
08 ε 11, 16
10 ▶
11 ! [Enum(M2)] (identifier) [Node Set(M0) EndEnum] 10
14 ...
15 ...
16 ! [Enum(M3)] (number) [Node Set(M1) EndEnum] 10
Loading