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
2 changes: 1 addition & 1 deletion crates/plotnik-cli/src/commands/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ pub fn run(args: ExecArgs) {
let colors = Colors::new(args.color);

// Debug-only: verify output matches declared type
debug_verify_type(&value, &module, colors);
debug_verify_type(&value, entrypoint.result_type, &module, colors);

let output = value.format(args.pretty, colors);
println!("{}", output);
Expand Down
2 changes: 1 addition & 1 deletion crates/plotnik-cli/src/commands/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ pub fn run(args: TraceArgs) {
let value = materializer.materialize(effects.as_slice(), entrypoint.result_type);

// Debug-only: verify output matches declared type
debug_verify_type(&value, &module, colors);
debug_verify_type(&value, entrypoint.result_type, &module, colors);

let output = value.format(true, colors);
println!("{}", output);
Expand Down
47 changes: 30 additions & 17 deletions crates/plotnik-lib/src/engine/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,12 @@ use super::Value;
///
/// Panics with a pretty diagnostic if the value doesn't match the expected type.
/// This is a no-op in release builds.
///
/// `expected_type` should be the `result_type` from the entrypoint that was executed.
#[cfg(debug_assertions)]
pub fn debug_verify_type(value: &Value, module: &Module, colors: Colors) {
pub fn debug_verify_type(value: &Value, expected_type: QTypeId, module: &Module, colors: Colors) {
let types = module.types();
let strings = module.strings();
let entrypoints = module.entrypoints();

// Get the first entrypoint's result type for verification
if entrypoints.is_empty() {
return;
}
let entrypoint = entrypoints.get(0);
let expected_type = entrypoint.result_type;

let mut errors = Vec::new();
verify_type(
Expand All @@ -37,14 +31,20 @@ pub fn debug_verify_type(value: &Value, module: &Module, colors: Colors) {
&mut errors,
);
if !errors.is_empty() {
panic_with_mismatch(value, &errors, module, colors);
panic_with_mismatch(value, expected_type, &errors, module, colors);
}
}

/// No-op in release builds.
#[cfg(not(debug_assertions))]
#[inline(always)]
pub fn debug_verify_type(_value: &Value, _module: &Module, _colors: Colors) {}
pub fn debug_verify_type(
_value: &Value,
_expected_type: QTypeId,
_module: &Module,
_colors: Colors,
) {
}

/// Recursive type verification. Collects mismatch paths into `errors`.
#[cfg(debug_assertions)]
Expand Down Expand Up @@ -316,17 +316,30 @@ fn centered_header(label: &str, width: usize) -> String {

/// Panic with a pretty diagnostic showing the type mismatch.
#[cfg(debug_assertions)]
fn panic_with_mismatch(value: &Value, errors: &[String], module: &Module, colors: Colors) -> ! {
fn panic_with_mismatch(
value: &Value,
expected_type: QTypeId,
errors: &[String],
module: &Module,
colors: Colors,
) -> ! {
const WIDTH: usize = 80;
let separator = "=".repeat(WIDTH);

let entrypoints = module.entrypoints();
let strings = module.strings();
let type_name = if !entrypoints.is_empty() {
strings.get(entrypoints.get(0).name)
} else {
"unknown"
};

// Find the entrypoint name by matching result_type
let type_name = (0..entrypoints.len())
.find_map(|i| {
let e = entrypoints.get(i);
if e.result_type == expected_type {
Some(strings.get(e.name))
} else {
None
}
})
.unwrap_or("unknown");

let config = Config {
export: true,
Expand Down
78 changes: 40 additions & 38 deletions crates/plotnik-lib/src/engine/verify_tests.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
//! Tests for debug type verification.

use crate::Colors;
use crate::QueryBuilder;
use crate::bytecode::Module;
use crate::bytecode::{Module, QTypeId};
use crate::emit::emit_linked;
use crate::engine::value::{NodeHandle, Value};
use crate::Colors;
use crate::QueryBuilder;

use super::debug_verify_type;

/// Build a module from a query string.
fn build_module(query: &str) -> Module {
/// Build a module from a query string and return with its first entrypoint's result type.
fn build_module(query: &str) -> (Module, QTypeId) {
let lang = plotnik_langs::javascript();
let query_obj = QueryBuilder::one_liner(query)
.parse()
Expand All @@ -18,7 +18,9 @@ fn build_module(query: &str) -> Module {
.link(&lang);
assert!(query_obj.is_valid(), "query should be valid");
let bytecode = emit_linked(&query_obj).expect("emit failed");
Module::from_bytes(bytecode).expect("decode failed")
let module = Module::from_bytes(bytecode).expect("decode failed");
let expected_type = module.entrypoints().get(0).result_type;
(module, expected_type)
}

fn make_node() -> Value {
Expand All @@ -31,51 +33,51 @@ fn make_node() -> Value {

#[test]
fn verify_valid_node() {
let module = build_module("Q = (identifier) @id");
let (module, expected_type) = build_module("Q = (identifier) @id");
let value = Value::Object(vec![("id".to_string(), make_node())]);

// Should not panic
debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
fn verify_valid_optional_present() {
let module = build_module("Q = (identifier)? @id");
let (module, expected_type) = build_module("Q = (identifier)? @id");
let value = Value::Object(vec![("id".to_string(), make_node())]);

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
fn verify_valid_optional_null() {
let module = build_module("Q = (identifier)? @id");
let (module, expected_type) = build_module("Q = (identifier)? @id");
let value = Value::Object(vec![("id".to_string(), Value::Null)]);

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
fn verify_valid_array() {
let module = build_module("Q = (identifier)* @ids");
let (module, expected_type) = build_module("Q = (identifier)* @ids");
let value = Value::Object(vec![(
"ids".to_string(),
Value::Array(vec![make_node(), make_node()]),
)]);

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
fn verify_valid_empty_array() {
let module = build_module("Q = (identifier)* @ids");
let (module, expected_type) = build_module("Q = (identifier)* @ids");
let value = Value::Object(vec![("ids".to_string(), Value::Array(vec![]))]);

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
fn verify_valid_enum() {
let module = build_module("Q = [A: (identifier) @x B: (number) @y]");
let (module, expected_type) = build_module("Q = [A: (identifier) @x B: (number) @y]");
let value = Value::Tagged {
tag: "A".to_string(),
data: Some(Box::new(Value::Object(vec![(
Expand All @@ -84,123 +86,123 @@ fn verify_valid_enum() {
)]))),
};

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
fn verify_valid_enum_void_variant() {
let module = build_module("Q = [A: (identifier) @x B: (number)]");
let (module, expected_type) = build_module("Q = [A: (identifier) @x B: (number)]");
let value = Value::Tagged {
tag: "B".to_string(),
data: None,
};

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
fn verify_valid_string() {
let module = build_module("Q = (identifier) @id :: string");
let (module, expected_type) = build_module("Q = (identifier) @id :: string");
let value = Value::Object(vec![("id".to_string(), Value::String("foo".to_string()))]);

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
#[should_panic(expected = "TYPE MISMATCH")]
fn verify_invalid_node_is_string() {
let module = build_module("Q = (identifier) @id");
let (module, expected_type) = build_module("Q = (identifier) @id");

// id should be Node, but we provide string
let value = Value::Object(vec![("id".to_string(), Value::String("wrong".to_string()))]);

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
#[should_panic(expected = "TYPE MISMATCH")]
fn verify_invalid_missing_required_field() {
let module = build_module("Q = {(identifier) @a (number) @b}");
let (module, expected_type) = build_module("Q = {(identifier) @a (number) @b}");

// Missing field 'b'
let value = Value::Object(vec![("a".to_string(), make_node())]);

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
#[should_panic(expected = "TYPE MISMATCH")]
fn verify_invalid_array_element_wrong_type() {
let module = build_module("Q = (identifier)* @ids");
let (module, expected_type) = build_module("Q = (identifier)* @ids");

// Array element is string instead of Node
let value = Value::Object(vec![(
"ids".to_string(),
Value::Array(vec![make_node(), Value::String("oops".to_string())]),
)]);

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
#[should_panic(expected = "TYPE MISMATCH")]
fn verify_invalid_non_empty_array_is_empty() {
let module = build_module("Q = (identifier)+ @ids");
let (module, expected_type) = build_module("Q = (identifier)+ @ids");

// Non-empty array but we provide empty
let value = Value::Object(vec![("ids".to_string(), Value::Array(vec![]))]);

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
#[should_panic(expected = "TYPE MISMATCH")]
fn verify_invalid_enum_unknown_variant() {
let module = build_module("Q = [A: (identifier) @x B: (number) @y]");
let (module, expected_type) = build_module("Q = [A: (identifier) @x B: (number) @y]");

let value = Value::Tagged {
tag: "C".to_string(), // Unknown variant
data: None,
};

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
#[should_panic(expected = "TYPE MISMATCH")]
fn verify_invalid_enum_void_with_data() {
let module = build_module("Q = [A: (identifier) @x B: (number)]");
let (module, expected_type) = build_module("Q = [A: (identifier) @x B: (number)]");

// Void variant B has data when it shouldn't
let value = Value::Tagged {
tag: "B".to_string(),
data: Some(Box::new(Value::Object(vec![]))),
};

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
#[should_panic(expected = "TYPE MISMATCH")]
fn verify_invalid_enum_non_void_missing_data() {
let module = build_module("Q = [A: (identifier) @x B: (number) @y]");
let (module, expected_type) = build_module("Q = [A: (identifier) @x B: (number) @y]");

// Non-void variant A missing data
let value = Value::Tagged {
tag: "A".to_string(),
data: None,
};

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}

#[test]
#[should_panic(expected = "TYPE MISMATCH")]
fn verify_invalid_expected_object_got_array() {
let module = build_module("Q = (identifier) @id");
let (module, expected_type) = build_module("Q = (identifier) @id");

// Expected object, got array
let value = Value::Array(vec![make_node()]);

debug_verify_type(&value, &module, Colors::OFF);
debug_verify_type(&value, expected_type, &module, Colors::OFF);
}