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
145 changes: 28 additions & 117 deletions crates/plotnik-compiler/src/compile/compile_tests.rs
Original file line number Diff line number Diff line change
@@ -1,162 +1,73 @@
//! Integration tests for the compilation pipeline.

use std::cell::RefCell;

use super::*;
use crate::{emit::StringTableBuilder, query::QueryBuilder};

/// Helper to compile a query with default context.
fn compile_query(query: &crate::query::QueryAnalyzed) -> CompileResult {
let strings = RefCell::new(StringTableBuilder::new());
let ctx = CompileCtx {
interner: query.interner(),
type_ctx: query.type_context(),
symbol_table: &query.symbol_table,
strings: &strings,
node_types: None,
node_fields: None,
};
Compiler::compile(&ctx).unwrap()
}
use crate::shot_bytecode;

#[test]
fn compile_simple_named_node() {
let query = QueryBuilder::one_liner("Test = (identifier)")
.parse()
.unwrap()
.analyze();

let result = compile_query(&query);

// Should have at least one instruction
assert!(!result.instructions.is_empty());
// Should have one entrypoint
assert_eq!(result.def_entries.len(), 1);
shot_bytecode!("Test = (identifier)");
}

#[test]
fn compile_alternation() {
let query = QueryBuilder::one_liner("Test = [(identifier) (number)]")
.parse()
.unwrap()
.analyze();

let result = compile_query(&query);

assert!(!result.instructions.is_empty());
shot_bytecode!("Test = [(identifier) (number)]");
}

#[test]
fn compile_sequence() {
let query = QueryBuilder::one_liner("Test = {(comment) (function)}")
.parse()
.unwrap()
.analyze();

let result = compile_query(&query);

assert!(!result.instructions.is_empty());
shot_bytecode!("Test = {(comment) (identifier)}");
}

#[test]
fn compile_quantified() {
let query = QueryBuilder::one_liner("Test = (identifier)*")
.parse()
.unwrap()
.analyze();

let result = compile_query(&query);

assert!(!result.instructions.is_empty());
shot_bytecode!("Test = (identifier)*");
}

#[test]
fn compile_capture() {
let query = QueryBuilder::one_liner("Test = (identifier) @id")
.parse()
.unwrap()
.analyze();

let result = compile_query(&query);

assert!(!result.instructions.is_empty());
shot_bytecode!("Test = (identifier) @id");
}

#[test]
fn compile_nested() {
let query = QueryBuilder::one_liner("Test = (call_expression function: (identifier) @fn)")
.parse()
.unwrap()
.analyze();

let result = compile_query(&query);

assert!(!result.instructions.is_empty());
shot_bytecode!("Test = (call_expression function: (identifier) @fn)");
}

#[test]
fn compile_large_tagged_alternation() {
// Regression test: alternations with 30+ branches should compile
// by splitting epsilon transitions into a cascade.
let branches: String = (0..30)
.map(|i| format!("A{i}: (identifier) @x{i}"))
.collect::<Vec<_>>()
.join(" ");
let query_str = format!("Q = [{branches}]");

let query = QueryBuilder::one_liner(&query_str)
.parse()
.unwrap()
.analyze();

let result = compile_query(&query);

assert!(!result.instructions.is_empty());
shot_bytecode!(r#"
Q = [
A0: (identifier) @x0 A1: (identifier) @x1 A2: (identifier) @x2
A3: (identifier) @x3 A4: (identifier) @x4 A5: (identifier) @x5
A6: (identifier) @x6 A7: (identifier) @x7 A8: (identifier) @x8
A9: (identifier) @x9 A10: (identifier) @x10 A11: (identifier) @x11
A12: (identifier) @x12 A13: (identifier) @x13 A14: (identifier) @x14
A15: (identifier) @x15 A16: (identifier) @x16 A17: (identifier) @x17
A18: (identifier) @x18 A19: (identifier) @x19 A20: (identifier) @x20
A21: (identifier) @x21 A22: (identifier) @x22 A23: (identifier) @x23
A24: (identifier) @x24 A25: (identifier) @x25 A26: (identifier) @x26
A27: (identifier) @x27 A28: (identifier) @x28 A29: (identifier) @x29
]
"#);
}

#[test]
fn compile_unlabeled_alternation_5_branches_with_captures() {
// Regression test: unlabeled alternation with 5+ branches where each has
// a unique capture requires 8+ pre-effects (4 nulls + 4 sets per branch).
// This exceeds the 3-bit limit (max 7) and must cascade via epsilon chain.
let query = QueryBuilder::one_liner(
"Q = [(identifier) @a (number) @b (string) @c (binary_expression) @d (call_expression) @e]",
)
.parse()
.unwrap()
.analyze();

let result = compile_query(&query);

assert!(!result.instructions.is_empty());

// Verify that effects cascade created extra epsilon instructions.
// With 5 branches, each branch needs 8 pre-effects (4 missing captures × 2 effects).
// This requires at least one cascade step per branch.
let epsilon_count = result
.instructions
.iter()
.filter(|i| matches!(i, crate::bytecode::InstructionIR::Match(m) if m.is_epsilon()))
.count();

// Should have more epsilon transitions than without cascade
// (5 branches + cascade steps for overflow effects)
assert!(epsilon_count >= 5, "expected cascade epsilon steps");
shot_bytecode!(
"Q = [(identifier) @a (number) @b (string) @c (binary_expression) @d (call_expression) @e]"
);
}

#[test]
fn compile_unlabeled_alternation_8_branches_with_captures() {
// Even more extreme: 8 branches means 14 pre-effects per branch (7 nulls + 7 sets).
// This requires 2 cascade steps per branch.
let query = QueryBuilder::one_liner(
"Q = [(identifier) @a (number) @b (string) @c (binary_expression) @d \
(call_expression) @e (member_expression) @f (array) @g (object) @h]",
)
.parse()
.unwrap()
.analyze();

let result = compile_query(&query);

assert!(!result.instructions.is_empty());
shot_bytecode!(r#"
Q = [(identifier) @a (number) @b (string) @c (binary_expression) @d
(call_expression) @e (member_expression) @f (array) @g (object) @h]
"#);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
source: crates/plotnik-compiler/src/compile/compile_tests.rs
---
Test = [(identifier) (number)]
---
[strings]
S0 "Beauty will save the world"
S1 "Test"
S2 "identifier"
S3 "number"

[type_defs]
T0 = <Void>

[type_members]

[type_names]
N0: S1 → T0 ; Test

[entrypoints]
Test = 06 :: T0

[transitions]
_ObjWrap:
00 ε [Obj] 02
02 Trampoline 03
03 ε [EndObj] 05
05 ▶

Test:
06 ε 09, 10
08 ▶
09 ! (identifier) 08
10 ! (number) 08
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
source: crates/plotnik-compiler/src/compile/compile_tests.rs
---
Test = (identifier) @id
---
[strings]
S0 "Beauty will save the world"
S1 "id"
S2 "Test"
S3 "identifier"

[type_defs]
T0 = <Node>
T1 = Struct M0:1 ; { id }

[type_members]
M0: S1 → T0 ; id: <Node>

[type_names]
N0: S2 → T1 ; Test

[entrypoints]
Test = 6 :: T1

[transitions]
_ObjWrap:
0 ε [Obj] 2
2 Trampoline 3
3 ε [EndObj] 5
5 ▶

Test:
6 ! (identifier) [Node Set(M0)] 8
8 ▶
Loading