Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ca02496
♻️ refactor(mq-lang): remove tarn feature flag and tree-walking evalu…
harehare Sep 8, 2026
d47af1e
⚡ perf(tarn): reduce dynamic call allocations
harehare Sep 9, 2026
7f0cf97
perf(lang): reduce parser allocations
harehare Sep 9, 2026
786eef7
⚡ perf(tarn): drive calls through an explicit frame-stack trampoline
harehare Sep 9, 2026
f592c93
perf(tarn): optimize frame-local recursion
harehare Sep 9, 2026
714c036
perf(tarn): avoid cloning root chunks per frame
harehare Sep 9, 2026
d29a8a0
perf(tarn): specialize fixed-arity direct calls
harehare Sep 9, 2026
644bf72
perf(tarn): specialize common exact calls
harehare Sep 9, 2026
5326b87
perf(tarn): specialize common exact call frames
harehare Sep 9, 2026
2bd32be
⚡ perf(vm): reduce interpreter and markdown traversal overhead
harehare Sep 9, 2026
b63b897
⚡ perf(vm): avoid re-cloning updated markdown children
harehare Sep 9, 2026
28001bb
⚡ perf(vm): fuse local comparisons into their conditional jump
harehare Sep 9, 2026
722ea02
⚡ perf(vm): switch Dict's backing map from BTreeMap to IndexMap
harehare Sep 10, 2026
5a16334
perf(mq-markdown): avoid cloning whole node in node_values()
harehare Sep 10, 2026
4301655
feat(debug): add Tarn VM opcode profiler
harehare Sep 10, 2026
1976e02
⚡ perf(cli): reuse queries in parallel workers
harehare Sep 10, 2026
e45ce51
⚡ perf(vm): fuse local constant updates
harehare Sep 10, 2026
4aa2740
⚡ perf(vm): fuse local updates
harehare Sep 10, 2026
1285037
🐛 fix(vm): clear discarded frames' operand stack during unwind
harehare Sep 10, 2026
05cf66e
test(mq-lang): snapshot TOON stringification
harehare Sep 10, 2026
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: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ flate2 = "1.1"
futures = "0.3"
glob = "0.3.3"
httpmock = "0.8.2"
indexmap = "2.14.0"
itertools = "0.15.0"
js-sys = "0.3.77"
libc = "0.2"
Expand Down
6 changes: 0 additions & 6 deletions crates/mq-check/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1335,10 +1335,6 @@ fn register_markdown(ctx: &mut InferenceContext) {
/// Variable/symbol management functions
fn register_variable(ctx: &mut InferenceContext) {
register_nullary(ctx, "all_symbols", Type::array(Type::Symbol));
// Deprecated: tree-walker only (see `mq-lang`'s `runtime::builtin`), scheduled for
// removal in the next release.
register_unary(ctx, "get_variable", Type::String, Type::String);
register_binary(ctx, "set_variable", Type::String, Type::String, Type::None);
register_unary(ctx, "intern", Type::String, Type::Symbol);
}

Expand Down Expand Up @@ -2367,8 +2363,6 @@ mod tests {
#[case::error_func("error(\"message\")", true)]
#[case::halt_func("halt(1)", true)]
#[case::all_symbols("all_symbols()", true)]
#[case::get_variable("get_variable(\"key\")", true)]
#[case::set_variable("set_variable(\"key\", \"value\")", true)]
#[case::intern("intern(\"symbol\")", true)]
#[case::is_debug_mode("is_debug_mode()", true)]
#[case::breakpoint("breakpoint()", true)]
Expand Down
3 changes: 0 additions & 3 deletions crates/mq-crawler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ readme = "README.md"
repository.workspace = true
version.workspace = true

[features]
tarn = ["mq-lang/tarn"]

[dependencies]
base64 = {workspace = true}
chromiumoxide = {workspace = true}
Expand Down
3 changes: 0 additions & 3 deletions crates/mq-dap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ readme = "README.md"
repository.workspace = true
version.workspace = true

[features]
tarn = ["mq-lang/tarn"]

[dependencies]
crossbeam-channel = {workspace = true}
dap = {workspace = true}
Expand Down
150 changes: 45 additions & 105 deletions crates/mq-dap/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,14 +299,9 @@ impl MqAdapter {
))) as Box<dyn std::error::Error>);
};

#[cfg(feature = "tarn")]
let result = self
.engine
.eval_debug_expression(code, context.current_value.clone(), &context.vm_bindings());
#[cfg(not(feature = "tarn"))]
let result = self
.engine
.eval_debug_expression(code, context.current_value.clone(), &context.env);

result.map_err(|e| {
let error_msg = format!("Evaluation error: {}", e);
Expand All @@ -315,7 +310,6 @@ impl MqAdapter {
})
}

#[cfg(feature = "tarn")]
fn eval_single_value(&mut self, code: &str) -> DynResult<mq_lang::RuntimeValue> {
self.eval(code)?.values().first().cloned().ok_or_else(|| {
Box::new(MqAdapterError::EvaluationError(Cow::Borrowed(
Expand Down Expand Up @@ -522,90 +516,55 @@ impl MqAdapter {
}
Command::SetVariable(args) => {
debug!(?args, "Received SetVariables request");
#[cfg(feature = "tarn")]
{
let name = args.name.clone();
let response_value = args.value.clone();
let value = self.eval_single_value(&response_value)?;
let Some(context) = self.current_debug_context.as_ref() else {
return Err(Box::new(MqAdapterError::EvaluationError(Cow::Borrowed(
"Current context not found",
))));
};
let prefer_upvalue = args.variables_reference == 1;
if !context.set_vm_variable(&name, value, prefer_upvalue) {
return Err(Box::new(MqAdapterError::EvaluationError(Cow::Owned(format!(
"Variable `{}` is not visible in this VM scope",
name
)))));
}
let rsp = req.success(ResponseBody::SetVariable(SetVariableResponse {
value: response_value,
indexed_variables: None,
named_variables: None,
type_field: None,
variables_reference: None,
}));
server.respond(rsp)?;
}
#[cfg(not(feature = "tarn"))]
{
self.eval(format!("let {} = {}", args.name, args.value).as_str())?;

let value = args.value.clone();
let rsp = req.success(ResponseBody::SetVariable(SetVariableResponse {
value,
indexed_variables: None,
named_variables: None,
type_field: None,
variables_reference: None,
}));
server.respond(rsp)?;
let name = args.name.clone();
let response_value = args.value.clone();
let value = self.eval_single_value(&response_value)?;
let Some(context) = self.current_debug_context.as_ref() else {
return Err(Box::new(MqAdapterError::EvaluationError(Cow::Borrowed(
"Current context not found",
))));
};
let prefer_upvalue = args.variables_reference == 1;
if !context.set_vm_variable(&name, value, prefer_upvalue) {
return Err(Box::new(MqAdapterError::EvaluationError(Cow::Owned(format!(
"Variable `{}` is not visible in this VM scope",
name
)))));
}
let rsp = req.success(ResponseBody::SetVariable(SetVariableResponse {
value: response_value,
indexed_variables: None,
named_variables: None,
type_field: None,
variables_reference: None,
}));
server.respond(rsp)?;
}
Command::SetExpression(args) => {
debug!(?args, "Received SetExpression request");
#[cfg(feature = "tarn")]
{
let expression = args.expression.clone();
let response_value = args.value.clone();
let value = self.eval_single_value(&response_value)?;
let Some(context) = self.current_debug_context.as_ref() else {
return Err(Box::new(MqAdapterError::EvaluationError(Cow::Borrowed(
"Current context not found",
))));
};
if !context.set_vm_expression(&expression, value) {
return Err(Box::new(MqAdapterError::EvaluationError(Cow::Owned(format!(
"Expression `{}` is not a visible VM variable",
expression
)))));
}
let rsp = req.success(ResponseBody::SetExpression(SetExpressionResponse {
value: response_value,
type_field: None,
presentation_hint: None,
variables_reference: None,
named_variables: None,
indexed_variables: None,
}));
server.respond(rsp)?;
}
#[cfg(not(feature = "tarn"))]
{
self.eval(format!("let {} = {}", args.expression, args.value).as_str())?;

let value = args.value.clone();
let rsp = req.success(ResponseBody::SetExpression(SetExpressionResponse {
value,
type_field: None,
presentation_hint: None,
variables_reference: None,
named_variables: None,
indexed_variables: None,
}));
server.respond(rsp)?;
let expression = args.expression.clone();
let response_value = args.value.clone();
let value = self.eval_single_value(&response_value)?;
let Some(context) = self.current_debug_context.as_ref() else {
return Err(Box::new(MqAdapterError::EvaluationError(Cow::Borrowed(
"Current context not found",
))));
};
if !context.set_vm_expression(&expression, value) {
return Err(Box::new(MqAdapterError::EvaluationError(Cow::Owned(format!(
"Expression `{}` is not a visible VM variable",
expression
)))));
}
let rsp = req.success(ResponseBody::SetExpression(SetExpressionResponse {
value: response_value,
type_field: None,
presentation_hint: None,
variables_reference: None,
named_variables: None,
indexed_variables: None,
}));
server.respond(rsp)?;
}
Command::Continue(_) => {
debug!("Received Continue request");
Expand Down Expand Up @@ -736,7 +695,6 @@ mod tests {
use dap::server::Server;
use mq_lang::Shared;
use std::io::{BufReader, BufWriter, Cursor};
#[cfg(feature = "tarn")]
use std::time::Duration;

#[test]
Expand Down Expand Up @@ -1115,27 +1073,10 @@ mod tests {
assert!(result.is_ok());
}

#[cfg(not(feature = "tarn"))]
#[test]
fn test_eval_resolves_debug_context_bindings() {
let mut adapter = MqAdapter::new();
let context = mq_lang::DebugContext::default();
context
.env
.write()
.unwrap()
.define("x".into(), mq_lang::RuntimeValue::Number(41.into()));
adapter.current_debug_context = Some(context);

let result = adapter.eval("x + 1").unwrap();
assert_eq!(result[0], mq_lang::RuntimeValue::Number(42.into()));
}

/// A real stopped VM frame must expose its bindings to DAP variable and evaluate requests.
///
/// This runs through the configured `DapHandlerWrapper`, rather than constructing a
/// `DebugContext` by hand, so it protects the VM debugger boundary → DAP adapter path.
/// The same test also runs without the `tarn` feature as the tree-walker reference.
#[test]
fn test_stopped_frame_exposes_live_bindings_to_dap() {
let mut adapter = MqAdapter::new();
Expand Down Expand Up @@ -1195,7 +1136,6 @@ mod tests {
);
}

#[cfg(feature = "tarn")]
#[rstest::rstest]
#[case::top_level_global_set_variable(("let x = 1 |\nx + 1", 2, 1, "x", "1", "41", false, 42))]
#[case::top_level_set_expression(("let x = 1 |\nx + 1", 2, 1, "x", "1", "41", true, 42))]
Expand Down Expand Up @@ -1651,7 +1591,7 @@ mod tests {
let mut context = mq_lang::DebugContext::default();

context.call_stack.push(Shared::new(mq_lang::AstNode {
expr: Shared::new(mq_lang::AstExpr::Literal(mq_lang::AstLiteral::Number(42.into()))),
expr: mq_lang::AstExpr::Literal(mq_lang::AstLiteral::Number(42.into())),
token_id: 0u32.into(),
}));
adapter.current_debug_context = Some(context);
Expand Down
4 changes: 3 additions & 1 deletion crates/mq-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1339,7 +1339,9 @@ mod tests {
let engine = mq_create();
mq_set_max_call_stack_depth(engine, 2);

let code = make_c_string("def rec(): rec(); rec()");
// The recursive call must not be in tail position: tail calls deliberately reuse a
// frame, while this test verifies enforcement of the configured stack-depth limit.
let code = make_c_string("def rec(): 1 + rec(); | rec()");
let input = make_c_string("test");
let format = make_c_string("text");
let result = unsafe { mq_eval(engine, code, input, format) };
Expand Down
2 changes: 1 addition & 1 deletion crates/mq-ffi/test_mq.c
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ void test_set_max_call_stack_depth() {
mq_context_t *engine = mq_create();
mq_set_max_call_stack_depth(engine, 2);

struct mq_result_t result = mq_eval(engine, "def rec(): rec(); rec()", "test", "text");
struct mq_result_t result = mq_eval(engine, "def rec(): 1 + rec(); rec()", "test", "text");
assert_not_null(result.error_msg, "Should have error due to stack depth limit");
mq_free_result(result);

Expand Down
15 changes: 7 additions & 8 deletions crates/mq-lang/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ chrono = {workspace = true}
dirs = {workspace = true}
glob = {workspace = true}
ignore = {workspace = true, optional = true}
indexmap = {workspace = true}
itertools = {workspace = true}
miette = {workspace = true}
mq-markdown = {workspace = true, features = ["json", "obsidian"]}
Expand Down Expand Up @@ -66,17 +67,14 @@ web-time = {workspace = true}

[features]
ast-json = ["smallvec/serde", "smol_str/serde"]
# Routes `Engine::eval`/`eval_compiled` through Tarn, the bytecode VM (`src/tarn.rs`),
# instead of the tree-walking evaluator (`src/eval.rs`). Opt-in for now while Tarn is
# validated against real usage; the tree-walker stays the default until it's dropped.
tarn = []
# Captures VM operand-stack snapshots at debugger statement boundaries. Intended for
# `mq-dbg` diagnosis; disabled in regular Tarn builds to avoid snapshot allocations.
debug-trace = ["tarn"]
# `mq-dbg` diagnosis; disabled by default to avoid snapshot allocations.
debug-trace = []
# Enables execution-count profiling for Tarn bytecode instructions. This is intended for
# development diagnostics and is deliberately excluded from normal builds.
vm-profile = []
cst = ["dep:ropey"]
css-selector = ["dep:ego-tree", "dep:scraper"]
# The tree-walker debugger is independent of Tarn. VM operand-stack tracing remains opt-in
# through `debug-trace`, which enables Tarn for debugger frontends that need it.
debugger = ["sync"]
default = ["std", "html-to-markdown"]
file-io = ["dep:ignore"]
Expand All @@ -92,6 +90,7 @@ tiktoken = ["dep:tiktoken-rs"]

[dev-dependencies]
divan = {workspace = true}
mimalloc = {workspace = true, features = ["v3"]}
proptest = {workspace = true}
rstest = {workspace = true}
scopeguard = {workspace = true}
Expand Down
Loading
Loading