use super::*;
use crate::ast::{Expr, Program};
use std::fmt::Write as _;
use std::sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
};
#[derive(Default)]
struct Host;
impl ExecutionHost for Host {
async fn perform(&self, op: AbilityOp) -> Result<AbilityResult, ExecutionHostError> {
match op {
AbilityOp::ResourceOperation(operation) => match operation.operation.as_str() {
"echo" => {
let value = operation
.args
.first()
.and_then(Value::as_record)
.and_then(|record| record.get("value"))
.cloned()
.unwrap_or(Value::Null);
Ok(AbilityResult::Value(value))
}
"err" => Err(ExecutionHostError::new("boom")),
other => Err(ExecutionHostError::new(format!(
"unknown module operation: {other}"
))),
},
AbilityOp::Await(handle) => match handle {
Value::Record(_) => Ok(AbilityResult::Value(Value::Null)),
_ => Err(ExecutionHostError::new("expected handle record")),
},
AbilityOp::Print(_) => Ok(AbilityResult::Unit),
AbilityOp::Submit(value) | AbilityOp::Finish(value) | AbilityOp::Fail(value) => {
Ok(AbilityResult::Value(value))
}
_ => Err(ExecutionHostError::new("unsupported host ability")),
}
}
}
#[derive(Default)]
struct RejectingAwaitHost;
impl ExecutionHost for RejectingAwaitHost {
async fn perform(&self, op: AbilityOp) -> Result<AbilityResult, ExecutionHostError> {
match op {
AbilityOp::Await(_) => Err(ExecutionHostError::new(
"unexpected generic handle await in resource operation test",
)),
other => Host.perform(other).await,
}
}
}
#[derive(Default)]
struct RecordingProcessHost {
starts: Mutex<Vec<ProcessStart>>,
events: Mutex<Vec<ProcessEvent>>,
sleeps: Mutex<Vec<Sleep>>,
signals: Mutex<Vec<ProcessSignal>>,
}
impl ExecutionHost for RecordingProcessHost {
async fn perform(&self, op: AbilityOp) -> Result<AbilityResult, ExecutionHostError> {
match op {
AbilityOp::ResourceOperation(_) => Err(ExecutionHostError::new(
"module operations are not supported by this host",
)),
AbilityOp::StartProcess(start) => {
self.starts.lock().expect("starts lock").push(*start);
let mut handle = Record::new();
handle.insert("__handle__".to_string(), Value::String("process".into()));
handle.insert("id".to_string(), Value::String("proc-1".into()));
Ok(AbilityResult::Value(Value::Record(Arc::new(handle))))
}
AbilityOp::ProcessEvent(event) => {
self.events.lock().expect("events lock").push(event);
Ok(AbilityResult::Unit)
}
AbilityOp::Sleep(sleep) => {
self.sleeps.lock().expect("sleeps lock").push(sleep);
Ok(AbilityResult::Value(Value::Null))
}
AbilityOp::WaitSignal { name } => {
assert_eq!(name, "ready");
Ok(AbilityResult::Value(Value::String("signal-payload".into())))
}
AbilityOp::SignalRun(signal) => {
self.signals.lock().expect("signals lock").push(signal);
Ok(AbilityResult::Value(Value::Null))
}
AbilityOp::Submit(value) | AbilityOp::Finish(value) | AbilityOp::Fail(value) => {
Ok(AbilityResult::Value(value))
}
_ => Err(ExecutionHostError::new("unsupported host ability")),
}
}
}
async fn exec(source: &str) -> Result<Value, RuntimeError> {
let program = crate::parse(source).expect("program should parse");
let mut state = State::new();
match execute_program(&program, &mut state, &Host).await? {
ExecutionOutcome::Finished(value) => Ok(value),
ExecutionOutcome::Continued => panic!("expected `submit` in test program"),
ExecutionOutcome::Failed(value) => panic!("unexpected process failure: {value}"),
}
}
async fn exec_outcome(source: &str) -> Result<ExecutionOutcome, RuntimeError> {
let program = crate::parse(source).expect("program should parse");
let mut state = State::new();
execute_program(&program, &mut state, &Host).await
}
fn compile_source(source: &str) -> Result<CompiledProgram, crate::ParseError> {
let program = crate::parse(source)?;
if source.contains("tools.")
&& let Ok(linked) = crate::LinkedModule::link(program.clone(), runtime_test_environment())
{
Ok(crate::compile_linked(&linked))
} else {
Ok(compile_program(&program))
}
}
fn compile_labeled_source(source: &str) -> CompiledProgram {
let program = crate::parse(source).expect("program should parse");
let surface = runtime_test_environment().with_language_features(
crate::LashlangLanguageFeatures::default().with_label_annotations(),
);
let linked = crate::LinkedModule::link(program, surface).expect("program should link");
crate::compile_linked(&linked)
}
fn compile_labeled_process_source(source: &str, process_name: &str) -> CompiledProgram {
let program = crate::parse(source).expect("program should parse");
let surface = runtime_test_environment().with_language_features(
crate::LashlangLanguageFeatures::default().with_label_annotations(),
);
let linked = crate::LinkedModule::link(program, surface).expect("program should link");
crate::compile_linked_process(&linked, process_name).expect("process should compile")
}
fn assert_resource_call_unwrap_without_handle_await(compiled: &CompiledProgram) {
let instructions = compiled_instruction_listing(compiled);
assert!(
compiled
.chunk
.code
.iter()
.any(|instruction| matches!(instruction, Instruction::ResourceCallUnwrap { .. })),
"compiled code should use ResourceCallUnwrap:\n{instructions}"
);
assert!(
!compiled.chunk.code.iter().any(|instruction| matches!(
instruction,
Instruction::AwaitHandle | Instruction::AwaitHandleUnwrap
)),
"resource operation unwrap should not emit generic handle await instructions:\n{instructions}"
);
}
fn compiled_instruction_listing(compiled: &CompiledProgram) -> String {
let mut out = String::new();
for (index, instruction) in compiled.chunk.code.iter().copied().enumerate() {
writeln!(
out,
"{index:04}: {}",
instruction_snapshot(&compiled.chunk, instruction)
)
.unwrap();
}
out
}
fn compile_program(program: &Program) -> CompiledProgram {
super::entry_points::compile_program_internal(program)
}
fn runtime_test_environment() -> crate::LashlangHostEnvironment {
let mut resources = crate::LashlangHostCatalog::new();
resources.add_module_operation(
["tools"],
"Tools",
"echo",
"echo",
crate::TypeExpr::Any,
crate::TypeExpr::Any,
);
resources.add_module_operation(
["tools"],
"Tools",
"err",
"err",
crate::TypeExpr::Any,
crate::TypeExpr::Any,
);
resources.add_module_operation(
["tools"],
"Tools",
"missing",
"missing",
crate::TypeExpr::Any,
crate::TypeExpr::Any,
);
resources.add_module_operation(
["tools"],
"Tools",
"spawn",
"spawn",
crate::TypeExpr::Any,
crate::TypeExpr::Any,
);
crate::LashlangHostEnvironment::new(resources, crate::LashlangAbilities::all())
}
async fn execute_program<H: ExecutionHost>(
program: &Program,
state: &mut State,
host: &H,
) -> Result<ExecutionOutcome, RuntimeError> {
if let Ok(linked) = crate::LinkedModule::link(program.clone(), runtime_test_environment()) {
let compiled = crate::compile_linked(&linked);
return super::execute(&compiled, state, host).await;
}
super::execute(program, state, host).await
}
async fn execute_compiled<H: ExecutionHost>(
program: &CompiledProgram,
state: &mut State,
host: &H,
) -> Result<ExecutionOutcome, RuntimeError> {
super::execute(program, state, host).await
}
async fn execute_compiled_with_projected_bindings<H: ExecutionHost>(
program: &CompiledProgram,
state: &mut State,
host: &H,
projected: &ProjectedBindings,
) -> Result<ExecutionOutcome, RuntimeError> {
let env = ExecutionEnvironment::new(host).with_projected_bindings(projected.clone());
super::execute(program, state, &env).await
}
async fn execute_compiled_with_scratch<H: ExecutionHost>(
program: &CompiledProgram,
state: &mut State,
host: &H,
scratch: &mut ExecutionScratch,
) -> Result<ExecutionOutcome, RuntimeError> {
let env = ExecutionEnvironment::new(host).with_scratch(std::mem::take(scratch));
let result = super::execute(program, state, &env).await;
*scratch = env.take_recycled_scratch().unwrap_or_default();
result
}
async fn execute_compiled_traced<H: ExecutionHost>(
program: &CompiledProgram,
state: &mut State,
host: &H,
) -> Result<ExecutionOutcome, RuntimeFailure> {
let env = ExecutionEnvironment::new(host).traced();
match super::execute(program, state, &env).await {
Ok(outcome) => Ok(outcome),
Err(error) => Err(env
.take_runtime_failure()
.unwrap_or(RuntimeFailure { error, span: None })),
}
}
async fn execute_compiled_traced_with_projected_bindings<H: ExecutionHost>(
program: &CompiledProgram,
state: &mut State,
host: &H,
projected: &ProjectedBindings,
) -> Result<ExecutionOutcome, RuntimeFailure> {
let env = ExecutionEnvironment::new(host)
.traced()
.with_projected_bindings(projected.clone());
match super::execute(program, state, &env).await {
Ok(outcome) => Ok(outcome),
Err(error) => Err(env
.take_runtime_failure()
.unwrap_or(RuntimeFailure { error, span: None })),
}
}
async fn execute_compiled_process<H: ExecutionHost>(
program: &CompiledProgram,
state: &mut State,
host: &H,
) -> Result<ExecutionOutcome, RuntimeError> {
let env = ExecutionEnvironment::new(host).process();
super::execute(program, state, &env).await
}
async fn profile_compiled<H: ExecutionHost>(
program: &CompiledProgram,
state: &mut State,
host: &H,
) -> Result<(ExecutionOutcome, ProfileReport), RuntimeError> {
let env = ExecutionEnvironment::new(host).profiled();
let outcome = super::execute(program, state, &env).await?;
let profile = env.take_profile().expect("profile should be recorded");
Ok((outcome, profile))
}
const GOLDEN_CONTRACT_SOURCE: &str = r#"
source = join(history, ",")
beta_index = find(source, "beta")
matches = grep_text(source, "beta")
counts = {}
for token in split(source, ",") {
counts[token] = counts[token] + 1
}
Payload = Type {
beta_index: int | null,
matches: list[dict],
counts: dict
}
submit validate(
{ beta_index: beta_index, matches: matches, counts: counts },
Payload
)
"#;
#[test]
fn golden_parser_ast_contract_covers_lashlang_host_environment() {
let program = crate::parse(GOLDEN_CONTRACT_SOURCE).expect("program should parse");
insta::assert_snapshot!(
"lashlang_parser_ast_contract",
serde_json::to_string_pretty(&program).expect("program should serialize")
);
}
#[test]
fn golden_compiled_bytecode_contract_covers_lashlang_host_environment() {
insta::assert_snapshot!(
"lashlang_compiled_bytecode_contract",
compiled_program_snapshot(GOLDEN_CONTRACT_SOURCE)
);
}
#[tokio::test(flavor = "current_thread")]
async fn golden_runtime_diagnostic_contract_is_exact() {
insta::assert_snapshot!(
"lashlang_runtime_diagnostic_contract",
runtime_diagnostic("x = 1\nsubmit len(true)").await
);
}
#[tokio::test(flavor = "current_thread")]
async fn golden_lashlang_diagnostic_corpus_is_exact() {
let mut projected = ProjectedBindings::new();
projected.insert(
"history",
ProjectedValue::scalar(
"history",
Value::List(vec![Value::String("entry".into())].into()),
),
);
let mut cases = Vec::new();
cases.push(diagnostic_case(
"parse_inline_if",
format_parse_diagnostic("submit if true { 1 }"),
));
cases.push(diagnostic_case(
"parse_inline_for",
format_parse_diagnostic("submit [for x in [1] { x }]"),
));
cases.push(diagnostic_case(
"parse_loop_control_outside_loop",
format_parse_diagnostic("break"),
));
cases.push(diagnostic_case(
"parse_duplicate_type_field",
format_parse_diagnostic("Payload = Type { nested: { ok: bool, ok: str } }"),
));
cases.push(diagnostic_case(
"parse_removed_parallel_keyword",
format_parse_diagnostic("parallel {\n start echo(value: 1)\n}"),
));
cases.push(diagnostic_case(
"runtime_bad_wrapper_unwrap",
runtime_diagnostic("submit ({ ok: false, error: \"boom\" })?").await,
));
cases.push(diagnostic_case(
"runtime_failed_resource_operation_unwrap",
runtime_diagnostic("submit (await tools.err({})?)").await,
));
cases.push(diagnostic_case(
"runtime_invalid_await_handle",
runtime_diagnostic("submit (await 1)?").await,
));
cases.push(diagnostic_case(
"runtime_read_only_projected_assignment",
runtime_diagnostic_with_projected("history = []\nsubmit history", &projected).await,
));
cases.push(diagnostic_case(
"runtime_invalid_validate_type_argument",
runtime_diagnostic("submit validate({ ok: true }, \"not a type\")").await,
));
cases.push(diagnostic_case(
"runtime_validation_failure",
runtime_diagnostic("submit validate({ count: \"x\" }, Type { count: int })").await,
));
cases.push(diagnostic_case(
"runtime_unknown_name",
runtime_diagnostic("submit missing").await,
));
cases.push(diagnostic_case(
"runtime_unknown_builtin",
runtime_diagnostic("submit nope()").await,
));
insta::assert_snapshot!("lashlang_diagnostic_corpus", cases.join("\n\n---\n\n"));
}
#[tokio::test(flavor = "current_thread")]
async fn golden_serialized_state_snapshot_contract_is_exact() {
let program = crate::parse(
r#"
counter = 7
Payload = Type { title: str, count: int }
submit Payload
"#,
)
.expect("program should parse");
let mut state = State::new();
let outcome = execute_program(&program, &mut state, &Host)
.await
.expect("program should execute");
assert!(matches!(outcome, ExecutionOutcome::Finished(_)));
state.globals.insert_str(
"cover",
Value::Image(ImageValue::new("img_1", "cover", 42, Some(640), Some(480))),
);
state.globals.insert_str(
"projected",
Value::Projected(ProjectedValue::custom(
"matches[0].text",
Arc::new(SnapshotGuardProjectedValue::default()),
)),
);
insta::assert_snapshot!(
"lashlang_serialized_state_snapshot_contract",
serde_json::to_string_pretty(&state.snapshot()).expect("snapshot should serialize")
);
}
fn format_parse_diagnostic(source: &str) -> String {
let err = crate::parse(source).expect_err("parse should fail");
crate::format_parse_diagnostic(source, &err)
}
async fn runtime_diagnostic(source: &str) -> String {
runtime_diagnostic_with_projected(source, &ProjectedBindings::default()).await
}
async fn runtime_diagnostic_with_projected(source: &str, projected: &ProjectedBindings) -> String {
let compiled = compile_source(source).expect("source should compile");
let mut state = State::new();
let failure =
execute_compiled_traced_with_projected_bindings(&compiled, &mut state, &Host, projected)
.await
.expect_err("runtime should fail");
crate::format_runtime_diagnostic(source, &failure.error, failure.span)
}
fn diagnostic_case(name: &str, diagnostic: String) -> String {
format!("{name}\n{diagnostic}")
}
fn compiled_program_snapshot(source: &str) -> String {
let compiled = compile_source(source).expect("source should compile");
let chunk = &compiled.chunk;
let mut out = String::new();
let stats = compiled.compile_stats();
writeln!(
out,
"compile_stats: total={} const_folded={} dynamic={} refs={}",
stats.type_literals_total,
stats.type_literals_const_folded,
stats.type_literals_dynamic,
stats.type_ref_sites
)
.unwrap();
writeln!(
out,
"slots: [{}]",
chunk
.slot_names
.iter()
.map(|name| name.text.as_ref())
.collect::<Vec<_>>()
.join(", ")
)
.unwrap();
writeln!(
out,
"names: [{}]",
chunk
.names
.iter()
.map(|name| name.text.as_ref())
.collect::<Vec<_>>()
.join(", ")
)
.unwrap();
writeln!(out, "constants:").unwrap();
for (index, value) in chunk.constants.iter().enumerate() {
writeln!(out, " c{index}: {}", compact_json(value)).unwrap();
}
writeln!(out, "code:").unwrap();
for (index, instruction) in chunk.code.iter().copied().enumerate() {
writeln!(
out,
" {index:04}: {}",
instruction_snapshot(chunk, instruction)
)
.unwrap();
}
out
}
fn instruction_snapshot(chunk: &Chunk, instruction: Instruction) -> String {
match instruction {
Instruction::PushConst(index) => {
format!(
"push_const c{index} {}",
compact_json(&chunk.constants[index])
)
}
Instruction::PushNull => "push_null".to_string(),
Instruction::PushBool(value) => format!("push_bool {value}"),
Instruction::PushNumber(value) => format!("push_number {value}"),
Instruction::LoadName(slot) => format!("load_name {slot}:{}", slot_name(chunk, slot)),
Instruction::StoreName(slot) => format!("store_name {slot}:{}", slot_name(chunk, slot)),
Instruction::StoreConst { slot, constant } => format!(
"store_const {slot}:{} c{constant} {}",
slot_name(chunk, slot),
compact_json(&chunk.constants[constant])
),
Instruction::BuildList(count) => format!("build_list {count}"),
Instruction::BuildRecord(keys) => format!("build_record {}", keys_snapshot(chunk, keys)),
Instruction::LoadField { slot, field } => format!(
"load_field {slot}:{} .{}",
slot_name(chunk, slot),
name(chunk, field)
),
Instruction::LoadFieldUnwrap { slot, field } => format!(
"load_field_unwrap {slot}:{} .{}",
slot_name(chunk, slot),
name(chunk, field)
),
Instruction::Field(field) => format!("field .{}", name(chunk, field)),
Instruction::Index => "index".to_string(),
Instruction::PathAssign { slot, path } => format!(
"path_assign {slot}:{} {}",
slot_name(chunk, slot),
assign_path_snapshot(chunk, path)
),
Instruction::ResultUnwrap => "result_unwrap".to_string(),
Instruction::Unary(op) => format!("unary {op:?}"),
Instruction::Binary(op) => format!("binary {op:?}"),
Instruction::SlotNumberBinary { slot, op, right } => format!(
"slot_number_binary {slot}:{} {op:?} {right}",
slot_name(chunk, slot)
),
Instruction::SlotNumberCompare { slot, op, right } => format!(
"slot_number_compare {slot}:{} {op:?} {right}",
slot_name(chunk, slot)
),
Instruction::SlotNumberBinaryCompare {
slot,
binary_op,
binary_right,
compare_op,
compare_right,
} => format!(
"slot_number_binary_compare {slot}:{} {binary_op:?} {binary_right} {compare_op:?} {compare_right}",
slot_name(chunk, slot)
),
Instruction::ToBool => "to_bool".to_string(),
Instruction::Jump(target) => format!("jump {target}"),
Instruction::JumpIfFalse(target) => format!("jump_if_false {target}"),
Instruction::JumpIfCompareFalse { op, target } => {
format!("jump_if_compare_false {op:?} {target}")
}
Instruction::JumpIfSlotNumberCompareFalse {
slot,
op,
right,
target,
} => format!(
"jump_if_slot_number_compare_false {slot}:{} {op:?} {right} {target}",
slot_name(chunk, slot)
),
Instruction::JumpIfSlotNumberBinaryCompareFalse {
slot,
binary_op,
binary_right,
compare_op,
compare_right,
target,
} => format!(
"jump_if_slot_number_binary_compare_false {slot}:{} {binary_op:?} {binary_right} {compare_op:?} {compare_right} {target}",
slot_name(chunk, slot)
),
Instruction::JumpIfTrue(target) => format!("jump_if_true {target}"),
Instruction::ResourceCall { operation, argc } => {
format!("resource_call {} argc={argc}", name_text(chunk, operation))
}
Instruction::ResourceCallUnwrap { operation, argc } => {
format!(
"resource_call_unwrap {} argc={argc}",
name_text(chunk, operation)
)
}
Instruction::StartProcess { process, keys } => format!(
"start_process {} {}",
name_text(chunk, process),
keys_snapshot(chunk, keys)
),
Instruction::AwaitHandle => "await_handle".to_string(),
Instruction::SleepFor => "sleep_for".to_string(),
Instruction::SleepUntil => "sleep_until".to_string(),
Instruction::ProcessWaitSignal { name } => {
format!("process_wait_signal {}", name_text(chunk, name))
}
Instruction::ProcessSignalRun { name } => {
format!("process_signal_run {}", name_text(chunk, name))
}
Instruction::AwaitHandleUnwrap => "await_handle_unwrap".to_string(),
Instruction::CancelHandle => "cancel_handle".to_string(),
Instruction::Intrinsic(op) => intrinsic_snapshot(chunk, op),
Instruction::AddAssign(slot) => format!("add_assign {slot}:{}", slot_name(chunk, slot)),
Instruction::AddAssignNumber { slot, right } => {
format!(
"add_assign_number {slot}:{} {right}",
slot_name(chunk, slot)
)
}
Instruction::AddAssignSlot { slot, right } => format!(
"add_assign_slot {slot}:{} {right}:{}",
slot_name(chunk, slot),
slot_name(chunk, right)
),
Instruction::AddAssignIndexNumber { slot, right } => format!(
"add_assign_index_number {slot}:{} {right}",
slot_name(chunk, slot)
),
Instruction::AddAssignIndexSlotNumber { slot, index, right } => format!(
"add_assign_index_slot_number {slot}:{} {index}:{} {right}",
slot_name(chunk, slot),
slot_name(chunk, index)
),
Instruction::AppendAssign(slot) => {
format!("append_assign {slot}:{}", slot_name(chunk, slot))
}
Instruction::Print => "print".to_string(),
Instruction::Submit => "submit".to_string(),
Instruction::ProcessYield => "process_yield".to_string(),
Instruction::ProcessWake => "process_wake".to_string(),
Instruction::ProcessFinish => "process_finish".to_string(),
Instruction::ProcessFail => "process_fail".to_string(),
Instruction::ObserveStep => "observe_step".to_string(),
Instruction::Pop => "pop".to_string(),
Instruction::BeginIter(slot) => format!("begin_iter {slot}:{}", slot_name(chunk, slot)),
Instruction::BeginRangeIter { binding, argc } => {
format!(
"begin_range_iter {binding}:{} argc={argc}",
slot_name(chunk, binding)
)
}
Instruction::IterNext { jump_to } => format!("iter_next {jump_to}"),
Instruction::EndIter => "end_iter".to_string(),
Instruction::ResolveTypeRef(slot) => {
format!("resolve_type_ref {slot}:{}", slot_name(chunk, slot))
}
Instruction::WrapTypeLiteral => "wrap_type_literal".to_string(),
Instruction::WrapHostDescriptor(type_name) => {
format!("wrap_host_descriptor {}", name_text(chunk, type_name))
}
}
}
fn compact_json(value: &Value) -> String {
serde_json::to_string(value).expect("value should serialize")
}
fn slot_name(chunk: &Chunk, index: usize) -> &str {
chunk.slot_names[index].text.as_ref()
}
fn name_text(chunk: &Chunk, index: usize) -> &str {
chunk.names[index].text.as_ref()
}
fn name(chunk: &Chunk, index: usize) -> &str {
name_text(chunk, index)
}
fn keys_snapshot(chunk: &Chunk, index: usize) -> String {
let keys = &chunk.key_lists[index];
let names = keys
.iter()
.map(|key| name_text(chunk, *key))
.collect::<Vec<_>>();
format!("[{}]", names.join(", "))
}
fn assign_path_snapshot(chunk: &Chunk, index: usize) -> String {
let path = &chunk.assign_paths[index];
let mut rendered = String::new();
for step in path.steps.iter() {
match step {
CompiledAssignPathStep::Field(field) => {
write!(rendered, ".{}", name_text(chunk, *field)).unwrap();
}
CompiledAssignPathStep::Index => rendered.push_str("[dynamic]"),
}
}
rendered
}
fn format_template_snapshot(chunk: &Chunk, index: usize) -> String {
let template = &chunk.format_templates[index];
let parts = template
.parts
.iter()
.map(|part| match part {
CompiledFormatPart::Literal(value) => format!("{value:?}"),
CompiledFormatPart::Arg(index) => format!("arg{index}"),
})
.collect::<Vec<_>>()
.join(" + ");
format!(
"argc={} min={} parts={parts}",
template.argc, template.min_capacity
)
}
fn intrinsic_snapshot(chunk: &Chunk, op: IntrinsicOp) -> String {
let argc = op.argc();
match op {
IntrinsicOp::Len => format!("intrinsic len argc={argc}"),
IntrinsicOp::Empty => format!("intrinsic empty argc={argc}"),
IntrinsicOp::Keys => format!("intrinsic keys argc={argc}"),
IntrinsicOp::Values => format!("intrinsic values argc={argc}"),
IntrinsicOp::Contains => format!("intrinsic contains argc={argc}"),
IntrinsicOp::Find(_) => format!("intrinsic find argc={argc}"),
IntrinsicOp::GrepText => format!("intrinsic grep_text argc={argc}"),
IntrinsicOp::StartsWith => format!("intrinsic starts_with argc={argc}"),
IntrinsicOp::EndsWith => format!("intrinsic ends_with argc={argc}"),
IntrinsicOp::Split => format!("intrinsic split argc={argc}"),
IntrinsicOp::Join => format!("intrinsic join argc={argc}"),
IntrinsicOp::Trim => format!("intrinsic trim argc={argc}"),
IntrinsicOp::Slice => format!("intrinsic slice argc={argc}"),
IntrinsicOp::ToString => format!("intrinsic to_string argc={argc}"),
IntrinsicOp::ToInt => format!("intrinsic to_int argc={argc}"),
IntrinsicOp::ToFloat => format!("intrinsic to_float argc={argc}"),
IntrinsicOp::JsonParse => format!("intrinsic json_parse argc={argc}"),
IntrinsicOp::Format(_) => format!("intrinsic format argc={argc}"),
IntrinsicOp::Validate => format!("intrinsic validate argc={argc}"),
IntrinsicOp::Range(_) => format!("intrinsic range argc={argc}"),
IntrinsicOp::CeilDiv => format!("intrinsic ceil_div argc={argc}"),
IntrinsicOp::FloorDiv => format!("intrinsic floor_div argc={argc}"),
IntrinsicOp::Push => format!("intrinsic push argc={argc}"),
IntrinsicOp::InvalidArity { name, .. } => {
format!(
"intrinsic invalid_arity({}) argc={argc}",
name_text(chunk, name)
)
}
IntrinsicOp::Unknown { name, .. } => {
format!("intrinsic unknown({}) argc={argc}", name_text(chunk, name))
}
IntrinsicOp::ValidateCompiled(schema) => {
format!("intrinsic validate_compiled schema#{schema}")
}
IntrinsicOp::PushAssign(slot) => {
format!("intrinsic push_assign {slot}:{}", slot_name(chunk, slot))
}
IntrinsicOp::FormatCompiled(template) => {
format!(
"intrinsic format_compiled {}",
format_template_snapshot(chunk, template)
)
}
IntrinsicOp::FormatCompiledSlotNumber { template, slot } => format!(
"intrinsic format_compiled_slot_number {} {slot}:{}",
format_template_snapshot(chunk, template),
slot_name(chunk, slot)
),
IntrinsicOp::FormatCompiledSlotNumberBinary {
template,
slot,
op,
right,
} => format!(
"intrinsic format_compiled_slot_number_binary {} {slot}:{} {op:?} {right}",
format_template_snapshot(chunk, template),
slot_name(chunk, slot)
),
}
}
#[tokio::test(flavor = "current_thread")]
async fn value_helpers_and_display_cover_all_variants() {
let mut record = Record::default();
record.insert("k".to_string(), Value::Number(1.0));
assert_eq!(Value::Null.to_string(), "null");
assert_eq!(Value::Bool(true).to_string(), "true");
assert_eq!(Value::Number(1.5).to_string(), "1.5");
assert_eq!(Value::String("x".to_string().into()).to_string(), "x");
assert_eq!(
Value::List(vec![Value::Bool(true)].into()).to_string(),
"[true]"
);
assert_eq!(
Value::Record(record.clone().into()).as_record().unwrap()["k"],
Value::Number(1.0)
);
assert!(Value::String("x".to_string().into()).as_record().is_none());
assert!(Value::Record(record.into()).to_string().contains("\"k\":1"));
}
#[tokio::test(flavor = "current_thread")]
async fn compiler_folds_constant_list_and_record_literals() {
let program = crate::parse(
r#"
items = [{ label: "a", weight: 1 }, { label: "b", weight: 2 }]
submit items
"#,
)
.expect("program should parse");
let compiled = compile_program(&program);
assert!(
compiled
.chunk
.code
.iter()
.any(|instruction| matches!(instruction, Instruction::PushConst(_)))
);
assert!(
!compiled
.chunk
.code
.iter()
.any(|instruction| matches!(instruction, Instruction::BuildRecord(_)))
);
let mut state = State::new();
let outcome = execute_compiled(&compiled, &mut state, &Host)
.await
.expect("program should run");
let ExecutionOutcome::Finished(Value::List(items)) = outcome else {
panic!("expected folded list result");
};
assert_eq!(items.len(), 2);
}
#[tokio::test(flavor = "current_thread")]
async fn compiler_propagates_safe_straight_line_constants() {
let program = crate::parse(
r#"
items = [1, 2, 3]
indexes = range(0, len(items))
extended = push(indexes, len(items))
submit extended
"#,
)
.expect("program should parse");
let compiled = compile_program(&program);
assert!(
!compiled.chunk.code.iter().any(|instruction| matches!(
instruction,
Instruction::Intrinsic(IntrinsicOp::Len | IntrinsicOp::Range(_) | IntrinsicOp::Push)
)),
"straight-line constant builtins should fold out of the runtime instruction stream"
);
let mut state = State::new();
let outcome = execute_compiled(&compiled, &mut state, &Host)
.await
.expect("program should run");
assert_eq!(
outcome,
ExecutionOutcome::Finished(Value::List(
vec![
Value::Number(0.0),
Value::Number(1.0),
Value::Number(2.0),
Value::Number(3.0),
]
.into()
))
);
}
#[tokio::test(flavor = "current_thread")]
async fn compiler_keeps_assignment_hot_paths_specialized() {
let source = r#"
items = []
total = 0
step = await tools.echo({ value: 3 })?
items = push(items, 1)
total = total + 2
total = total + step
submit { items: items, total: total }
"#;
let compiled = compile_source(source).expect("program should compile");
assert!(
compiled.chunk.code.iter().any(|instruction| {
matches!(
instruction,
Instruction::Intrinsic(IntrinsicOp::PushAssign(_))
)
}),
"`x = push(x, item)` should compile to the in-place push-assign opcode"
);
assert!(
compiled
.chunk
.code
.iter()
.any(|instruction| matches!(instruction, Instruction::AddAssignNumber { .. })),
"`x = x + constant_number` should compile to numeric add-assign"
);
assert!(
compiled
.chunk
.code
.iter()
.any(|instruction| matches!(instruction, Instruction::AddAssignSlot { .. })),
"`x = x + y` should compile to slot add-assign"
);
assert!(
!compiled
.chunk
.code
.iter()
.any(|instruction| matches!(instruction, Instruction::Intrinsic(IntrinsicOp::Push))),
"the assignment form should not route through generic push"
);
assert!(
!compiled
.chunk
.code
.iter()
.any(|instruction| matches!(instruction, Instruction::AddAssign(_))),
"numeric assignment forms should not route through generic add-assign"
);
let mut state = State::new();
let outcome = execute_compiled(&compiled, &mut state, &Host)
.await
.expect("program should run");
let ExecutionOutcome::Finished(Value::Record(record)) = outcome else {
panic!("expected record result");
};
assert_eq!(record["total"], Value::Number(5.0));
assert_eq!(
record["items"],
Value::List(vec![Value::Number(1.0)].into())
);
}
include!("tests/compiler_cases.rs");
include!("tests/projection_cases.rs");
include!("tests/async_and_cache_cases.rs");