slovo/compiler/src/test_runner.rs

4055 lines
148 KiB
Rust

use std::{
collections::{HashMap, HashSet},
env, fs,
io::Read,
sync::{
atomic::{AtomicI32, Ordering},
Mutex, OnceLock,
},
time::Instant,
};
use crate::{
ast::{BinaryOp, MatchPatternKind},
check::{CheckedFunction, CheckedProgram, TExpr, TExprKind, TMatchArm},
diag::Diagnostic,
std_runtime,
types::Type,
};
const MAX_TEST_CALL_DEPTH: usize = 1024;
const MAX_TEST_WHILE_ITERATIONS: usize = 1_000_000;
static MONOTONIC_START: OnceLock<Instant> = OnceLock::new();
static NEXT_TEST_FILE_HANDLE: AtomicI32 = AtomicI32::new(1);
static TEST_FILE_HANDLES: OnceLock<Mutex<HashMap<i32, fs::File>>> = OnceLock::new();
fn test_file_handles() -> &'static Mutex<HashMap<i32, fs::File>> {
TEST_FILE_HANDLES.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn check_output(program: &CheckedProgram) -> String {
let mut output = String::new();
for test in &program.tests {
output.push_str("test ");
write_test_name(&test.name, &mut output);
output.push_str(" ... checked\n");
}
output.push_str(&format!("{} test(s) checked\n", program.tests.len()));
output
}
pub struct TestRunSuccess {
pub output: String,
pub report: TestReport,
}
pub struct TestRunFailure {
pub diagnostics: Vec<Diagnostic>,
pub report: Option<TestReport>,
}
#[derive(Clone)]
pub struct TestReport {
pub total_discovered: usize,
pub selected: usize,
pub passed: usize,
pub failed: usize,
pub skipped: usize,
pub filter: Option<String>,
}
impl TestRunFailure {
pub fn before_execution(diagnostics: Vec<Diagnostic>) -> Self {
Self {
diagnostics,
report: None,
}
}
}
pub fn run(
_file: &str,
program: &CheckedProgram,
filter: Option<&str>,
) -> Result<TestRunSuccess, TestRunFailure> {
let functions = program
.functions
.iter()
.map(|function| (function.name.as_str(), function))
.collect::<HashMap<_, _>>();
let foreign_imports = program
.c_imports
.iter()
.map(|import| import.name.as_str())
.collect::<HashSet<_>>();
let mut output = String::new();
let mut errors = Vec::new();
let mut report = TestReport {
total_discovered: program.tests.len(),
selected: 0,
passed: 0,
failed: 0,
skipped: 0,
filter: filter.map(str::to_string),
};
for test in &program.tests {
if let Some(filter) = filter {
if !test.name.contains(filter) {
report.skipped += 1;
output.push_str("test ");
write_test_name(&test.name, &mut output);
output.push_str(" ... skipped\n");
continue;
}
}
report.selected += 1;
let mut locals = HashMap::new();
let test_file = test.file.as_str();
match eval_body(
test_file,
&test.body,
&mut locals,
&functions,
&foreign_imports,
0,
) {
Ok(Value::Bool(true)) => {
report.passed += 1;
output.push_str("test ");
write_test_name(&test.name, &mut output);
output.push_str(" ... ok\n");
}
Ok(Value::Bool(false)) => {
report.failed += 1;
errors.push(
Diagnostic::new(
test_file,
"TestFailed",
format!("test `{}` failed", test.name),
)
.with_span(test.span)
.expected("true")
.found("false"),
);
}
Ok(value) => {
report.failed += 1;
errors.push(
Diagnostic::new(
test_file,
"UnsupportedTestExpression",
format!(
"test `{}` produced unsupported value `{}`",
test.name,
value.ty()
),
)
.with_span(test.span),
);
}
Err(err) => {
report.failed += 1;
errors.push(err);
}
}
}
if errors.is_empty() {
output.push_str(&format!("{} test(s) passed", report.passed));
if filter.is_some() {
write_report_suffix(&report, &mut output);
}
output.push('\n');
Ok(TestRunSuccess { output, report })
} else {
Err(TestRunFailure {
diagnostics: errors,
report: Some(report),
})
}
}
fn write_report_suffix(report: &TestReport, output: &mut String) {
output.push_str(&format!(
" (total_discovered {}, selected {}, passed {}, failed {}, skipped {}",
report.total_discovered, report.selected, report.passed, report.failed, report.skipped
));
if let Some(filter) = report.filter.as_deref() {
output.push_str(", filter ");
write_test_name(filter, output);
}
output.push(')');
}
#[derive(Debug, Clone, PartialEq)]
enum Value {
I32(i32),
I64(i64),
U32(u32),
U64(u64),
F64(f64),
Bool(bool),
String(String),
Array(ArrayValue),
VecI32(Vec<i32>),
VecI64(Vec<i64>),
VecF64(Vec<f64>),
VecBool(Vec<bool>),
VecString(Vec<String>),
OptionI32 {
is_some: bool,
payload: i32,
},
OptionI64 {
is_some: bool,
payload: i64,
},
OptionU32 {
is_some: bool,
payload: u32,
},
OptionU64 {
is_some: bool,
payload: u64,
},
OptionF64 {
is_some: bool,
payload: f64,
},
OptionBool {
is_some: bool,
payload: bool,
},
OptionString {
is_some: bool,
payload: String,
},
ResultI32 {
is_ok: bool,
payload: i32,
},
ResultI64I32 {
is_ok: bool,
ok_payload: i64,
err_payload: i32,
},
ResultU32I32 {
is_ok: bool,
payload: u32,
},
ResultU64I32 {
is_ok: bool,
ok_payload: u64,
err_payload: i32,
},
ResultF64I32 {
is_ok: bool,
ok_payload: f64,
err_payload: i32,
},
ResultBoolI32 {
is_ok: bool,
ok_payload: bool,
err_payload: i32,
},
ResultStringI32 {
is_ok: bool,
ok_payload: String,
err_payload: i32,
},
Enum {
name: String,
variant: String,
discriminant: i32,
payload: Option<EnumPayloadValue>,
},
Struct {
name: String,
fields: HashMap<String, Value>,
},
Unit,
}
#[derive(Debug, Clone, PartialEq)]
enum ArrayValue {
Values {
element_ty: Type,
values: Vec<Value>,
},
}
impl ArrayValue {
fn ty(&self) -> Type {
match self {
Self::Values { element_ty, values } => {
Type::Array(Box::new(element_ty.clone()), values.len())
}
}
}
fn index_value(&self, index: usize) -> Option<Value> {
match self {
Self::Values { values, .. } => values.get(index).cloned(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
enum EnumPayloadValue {
I32(i32),
I64(i64),
U32(u32),
U64(u64),
F64(f64),
Bool(bool),
String(String),
Struct {
name: String,
fields: HashMap<String, Value>,
},
}
impl EnumPayloadValue {
fn from_value(value: Value) -> Option<Self> {
match value {
Value::I32(value) => Some(Self::I32(value)),
Value::I64(value) => Some(Self::I64(value)),
Value::U32(value) => Some(Self::U32(value)),
Value::U64(value) => Some(Self::U64(value)),
Value::F64(value) => Some(Self::F64(value)),
Value::Bool(value) => Some(Self::Bool(value)),
Value::String(value) => Some(Self::String(value)),
Value::Struct { name, fields } => Some(Self::Struct { name, fields }),
_ => None,
}
}
fn into_value(self) -> Value {
match self {
Self::I32(value) => Value::I32(value),
Self::I64(value) => Value::I64(value),
Self::U32(value) => Value::U32(value),
Self::U64(value) => Value::U64(value),
Self::F64(value) => Value::F64(value),
Self::Bool(value) => Value::Bool(value),
Self::String(value) => Value::String(value),
Self::Struct { name, fields } => Value::Struct { name, fields },
}
}
}
impl Value {
fn ty(&self) -> Type {
match self {
Self::I32(_) => Type::I32,
Self::I64(_) => Type::I64,
Self::U32(_) => Type::U32,
Self::U64(_) => Type::U64,
Self::F64(_) => Type::F64,
Self::Bool(_) => Type::Bool,
Self::String(_) => Type::String,
Self::Array(values) => values.ty(),
Self::VecI32(_) => Type::Vec(Box::new(Type::I32)),
Self::VecI64(_) => Type::Vec(Box::new(Type::I64)),
Self::VecF64(_) => Type::Vec(Box::new(Type::F64)),
Self::VecBool(_) => Type::Vec(Box::new(Type::Bool)),
Self::VecString(_) => Type::Vec(Box::new(Type::String)),
Self::OptionI32 { .. } => Type::Option(Box::new(Type::I32)),
Self::OptionI64 { .. } => Type::Option(Box::new(Type::I64)),
Self::OptionU32 { .. } => Type::Option(Box::new(Type::U32)),
Self::OptionU64 { .. } => Type::Option(Box::new(Type::U64)),
Self::OptionF64 { .. } => Type::Option(Box::new(Type::F64)),
Self::OptionBool { .. } => Type::Option(Box::new(Type::Bool)),
Self::OptionString { .. } => Type::Option(Box::new(Type::String)),
Self::ResultI32 { .. } => Type::Result(Box::new(Type::I32), Box::new(Type::I32)),
Self::ResultI64I32 { .. } => Type::Result(Box::new(Type::I64), Box::new(Type::I32)),
Self::ResultU32I32 { .. } => Type::Result(Box::new(Type::U32), Box::new(Type::I32)),
Self::ResultU64I32 { .. } => Type::Result(Box::new(Type::U64), Box::new(Type::I32)),
Self::ResultF64I32 { .. } => Type::Result(Box::new(Type::F64), Box::new(Type::I32)),
Self::ResultBoolI32 { .. } => Type::Result(Box::new(Type::Bool), Box::new(Type::I32)),
Self::ResultStringI32 { .. } => {
Type::Result(Box::new(Type::String), Box::new(Type::I32))
}
Self::Enum { name, .. } => Type::Named(name.clone()),
Self::Struct { name, .. } => Type::Named(name.clone()),
Self::Unit => Type::Unit,
}
}
fn as_i32(&self) -> Option<i32> {
match self {
Self::I32(value) => Some(*value),
_ => None,
}
}
fn as_i64(&self) -> Option<i64> {
match self {
Self::I64(value) => Some(*value),
_ => None,
}
}
fn as_u32(&self) -> Option<u32> {
match self {
Self::U32(value) => Some(*value),
_ => None,
}
}
fn as_u64(&self) -> Option<u64> {
match self {
Self::U64(value) => Some(*value),
_ => None,
}
}
fn as_f64(&self) -> Option<f64> {
match self {
Self::F64(value) => Some(*value),
_ => None,
}
}
fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(value) => Some(*value),
_ => None,
}
}
fn as_string(&self) -> Option<&str> {
match self {
Self::String(value) => Some(value),
_ => None,
}
}
fn as_vec_i32(&self) -> Option<&[i32]> {
match self {
Self::VecI32(values) => Some(values),
_ => None,
}
}
fn as_vec_i64(&self) -> Option<&[i64]> {
match self {
Self::VecI64(values) => Some(values),
_ => None,
}
}
fn as_vec_f64(&self) -> Option<&[f64]> {
match self {
Self::VecF64(values) => Some(values),
_ => None,
}
}
fn as_vec_bool(&self) -> Option<&[bool]> {
match self {
Self::VecBool(values) => Some(values),
_ => None,
}
}
fn as_vec_string(&self) -> Option<&[String]> {
match self {
Self::VecString(values) => Some(values),
_ => None,
}
}
}
fn parse_i32_result_value(value: &str) -> Value {
match parse_i32_strict_ascii(value.as_bytes()) {
Some(payload) => Value::ResultI32 {
is_ok: true,
payload,
},
None => Value::ResultI32 {
is_ok: false,
payload: 1,
},
}
}
fn parse_i32_strict_ascii(bytes: &[u8]) -> Option<i32> {
if bytes.is_empty() {
return None;
}
let mut index = 0;
let negative = bytes[index] == b'-';
if negative {
index += 1;
if index == bytes.len() {
return None;
}
}
let limit = if negative {
2_147_483_648_i64
} else {
2_147_483_647_i64
};
let mut value = 0_i64;
for &byte in &bytes[index..] {
if !byte.is_ascii_digit() {
return None;
}
let digit = i64::from(byte - b'0');
if value > (limit - digit) / 10 {
return None;
}
value = value * 10 + digit;
}
if negative {
if value == 2_147_483_648_i64 {
Some(i32::MIN)
} else {
Some(-(value as i32))
}
} else {
Some(value as i32)
}
}
fn parse_i64_result_value(value: &str) -> Value {
match parse_i64_strict_ascii(value.as_bytes()) {
Some(payload) => Value::ResultI64I32 {
is_ok: true,
ok_payload: payload,
err_payload: 0,
},
None => Value::ResultI64I32 {
is_ok: false,
ok_payload: 0,
err_payload: 1,
},
}
}
fn parse_i64_strict_ascii(bytes: &[u8]) -> Option<i64> {
if bytes.is_empty() {
return None;
}
let mut index = 0;
let negative = bytes[index] == b'-';
if negative {
index += 1;
if index == bytes.len() {
return None;
}
}
let limit = if negative {
9_223_372_036_854_775_808_u64
} else {
9_223_372_036_854_775_807_u64
};
let mut value = 0_u64;
for &byte in &bytes[index..] {
if !byte.is_ascii_digit() {
return None;
}
let digit = u64::from(byte - b'0');
if value > (limit - digit) / 10 {
return None;
}
value = value * 10 + digit;
}
if negative {
if value == 9_223_372_036_854_775_808_u64 {
Some(i64::MIN)
} else {
Some(-(value as i64))
}
} else {
Some(value as i64)
}
}
fn parse_u32_result_value(value: &str) -> Value {
match parse_u32_strict_ascii(value.as_bytes()) {
Some(payload) => Value::ResultU32I32 {
is_ok: true,
payload,
},
None => Value::ResultU32I32 {
is_ok: false,
payload: 1,
},
}
}
fn parse_u32_strict_ascii(bytes: &[u8]) -> Option<u32> {
if bytes.is_empty() {
return None;
}
let mut value = 0_u64;
for &byte in bytes {
if !byte.is_ascii_digit() {
return None;
}
let digit = u64::from(byte - b'0');
if value > ((u64::from(u32::MAX)) - digit) / 10 {
return None;
}
value = value * 10 + digit;
}
Some(value as u32)
}
fn parse_u64_result_value(value: &str) -> Value {
match parse_u64_strict_ascii(value.as_bytes()) {
Some(payload) => Value::ResultU64I32 {
is_ok: true,
ok_payload: payload,
err_payload: 0,
},
None => Value::ResultU64I32 {
is_ok: false,
ok_payload: 0,
err_payload: 1,
},
}
}
fn parse_u64_strict_ascii(bytes: &[u8]) -> Option<u64> {
if bytes.is_empty() {
return None;
}
let mut value = 0_u64;
for &byte in bytes {
if !byte.is_ascii_digit() {
return None;
}
let digit = u64::from(byte - b'0');
if value > (u64::MAX - digit) / 10 {
return None;
}
value = value * 10 + digit;
}
Some(value)
}
fn parse_f64_result_value(value: &str) -> Value {
match parse_f64_strict_ascii(value) {
Some(payload) => Value::ResultF64I32 {
is_ok: true,
ok_payload: payload,
err_payload: 0,
},
None => Value::ResultF64I32 {
is_ok: false,
ok_payload: 0.0,
err_payload: 1,
},
}
}
fn parse_bool_result_value(value: &str) -> Value {
match value {
"true" => Value::ResultBoolI32 {
is_ok: true,
ok_payload: true,
err_payload: 0,
},
"false" => Value::ResultBoolI32 {
is_ok: true,
ok_payload: false,
err_payload: 0,
},
_ => Value::ResultBoolI32 {
is_ok: false,
ok_payload: false,
err_payload: 1,
},
}
}
fn parse_f64_strict_ascii(text: &str) -> Option<f64> {
let bytes = text.as_bytes();
if !is_ascii_decimal_f64(bytes) {
return None;
}
text.parse::<f64>().ok().filter(|value| value.is_finite())
}
fn is_ascii_decimal_f64(bytes: &[u8]) -> bool {
if bytes.is_empty() {
return false;
}
let mut index = 0;
if bytes[index] == b'-' {
index += 1;
if index == bytes.len() {
return false;
}
}
let whole_digits = consume_ascii_digits(bytes, &mut index);
if whole_digits == 0 || index >= bytes.len() || bytes[index] != b'.' {
return false;
}
index += 1;
let fractional_digits = consume_ascii_digits(bytes, &mut index);
if fractional_digits == 0 {
return false;
}
index == bytes.len()
}
fn consume_ascii_digits(bytes: &[u8], index: &mut usize) -> usize {
let start = *index;
while *index < bytes.len() && bytes[*index].is_ascii_digit() {
*index += 1;
}
*index - start
}
fn format_f64_to_string(value: f64) -> String {
let mut text = format!("{value:.17}");
if let Some(dot) = text.find('.') {
while text.len() > dot + 2 && text.ends_with('0') {
text.pop();
}
}
text
}
fn quote_json_string_value(value: &str) -> String {
let mut quoted = String::with_capacity(value.len() + 2);
quoted.push('"');
for byte in value.bytes() {
match byte {
b'"' => quoted.push_str("\\\""),
b'\\' => quoted.push_str("\\\\"),
b'\n' => quoted.push_str("\\n"),
b'\t' => quoted.push_str("\\t"),
b'\r' => quoted.push_str("\\r"),
0x08 => quoted.push_str("\\b"),
0x0c => quoted.push_str("\\f"),
0x00..=0x1f => quoted.push_str(&format!("\\u{byte:04X}")),
_ => quoted.push(byte as char),
}
}
quoted.push('"');
quoted
}
fn eval_expr(
file: &str,
expr: &TExpr,
locals: &mut HashMap<String, Value>,
functions: &HashMap<&str, &CheckedFunction>,
foreign_imports: &HashSet<&str>,
depth: usize,
) -> Result<Value, Diagnostic> {
match &expr.kind {
TExprKind::Int(value) => Ok(Value::I32(*value)),
TExprKind::Int64(value) => Ok(Value::I64(*value)),
TExprKind::UInt32(value) => Ok(Value::U32(*value)),
TExprKind::UInt64(value) => Ok(Value::U64(*value)),
TExprKind::Float(value) => Ok(Value::F64(*value)),
TExprKind::Bool(value) => Ok(Value::Bool(*value)),
TExprKind::String(value) => Ok(Value::String(value.clone())),
TExprKind::EnumVariant {
enum_name,
variant,
discriminant,
payload,
} => {
let payload = match payload {
Some(payload) => {
let value =
eval_expr(file, payload, locals, functions, foreign_imports, depth)?;
let Some(value) = EnumPayloadValue::from_value(value) else {
return Err(unsupported_test_expr(
file,
expr,
"enum payloads outside the released direct scalar/string/struct families",
));
};
Some(value)
}
None => None,
};
Ok(Value::Enum {
name: enum_name.clone(),
variant: variant.clone(),
discriminant: *discriminant,
payload,
})
}
TExprKind::StructInit { name, fields } => {
let mut values = HashMap::new();
for (field, value) in fields {
values.insert(
field.clone(),
eval_expr(file, value, locals, functions, foreign_imports, depth)?,
);
}
Ok(Value::Struct {
name: name.clone(),
fields: values,
})
}
TExprKind::ArrayInit { elements } => match &expr.ty {
Type::Array(inner, _) => {
let mut values = Vec::new();
for element in elements {
let value =
eval_expr(file, element, locals, functions, foreign_imports, depth)?;
if value.ty() != **inner {
return Err(unsupported_test_expr(
file,
element,
"mismatched fixed array elements",
));
}
values.push(value);
}
Ok(Value::Array(ArrayValue::Values {
element_ty: (**inner).clone(),
values,
}))
}
_ => Err(unsupported_test_expr(
file,
expr,
"unsupported fixed array elements",
)),
},
TExprKind::OptionSome { value } => {
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
match &expr.ty {
Type::Option(inner) if **inner == Type::I32 => {
let Some(value) = value.as_i32() else {
return Err(unsupported_test_expr(file, expr, "non-i32 option payloads"));
};
Ok(Value::OptionI32 {
is_some: true,
payload: value,
})
}
Type::Option(inner) if **inner == Type::I64 => {
let Some(value) = value.as_i64() else {
return Err(unsupported_test_expr(file, expr, "non-i64 option payloads"));
};
Ok(Value::OptionI64 {
is_some: true,
payload: value,
})
}
Type::Option(inner) if **inner == Type::U32 => {
let Some(value) = value.as_u32() else {
return Err(unsupported_test_expr(file, expr, "non-u32 option payloads"));
};
Ok(Value::OptionU32 {
is_some: true,
payload: value,
})
}
Type::Option(inner) if **inner == Type::U64 => {
let Some(value) = value.as_u64() else {
return Err(unsupported_test_expr(file, expr, "non-u64 option payloads"));
};
Ok(Value::OptionU64 {
is_some: true,
payload: value,
})
}
Type::Option(inner) if **inner == Type::F64 => {
let Some(value) = value.as_f64() else {
return Err(unsupported_test_expr(file, expr, "non-f64 option payloads"));
};
Ok(Value::OptionF64 {
is_some: true,
payload: value,
})
}
Type::Option(inner) if **inner == Type::Bool => {
let Some(value) = value.as_bool() else {
return Err(unsupported_test_expr(
file,
expr,
"non-bool option payloads",
));
};
Ok(Value::OptionBool {
is_some: true,
payload: value,
})
}
Type::Option(inner) if **inner == Type::String => {
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"non-string option payloads",
));
};
Ok(Value::OptionString {
is_some: true,
payload: value.to_string(),
})
}
_ => Err(unsupported_test_expr(
file,
expr,
"unsupported option payloads",
)),
}
}
TExprKind::OptionNone => match &expr.ty {
Type::Option(inner) if **inner == Type::I32 => Ok(Value::OptionI32 {
is_some: false,
payload: 0,
}),
Type::Option(inner) if **inner == Type::I64 => Ok(Value::OptionI64 {
is_some: false,
payload: 0,
}),
Type::Option(inner) if **inner == Type::U32 => Ok(Value::OptionU32 {
is_some: false,
payload: 0,
}),
Type::Option(inner) if **inner == Type::U64 => Ok(Value::OptionU64 {
is_some: false,
payload: 0,
}),
Type::Option(inner) if **inner == Type::F64 => Ok(Value::OptionF64 {
is_some: false,
payload: 0.0,
}),
Type::Option(inner) if **inner == Type::Bool => Ok(Value::OptionBool {
is_some: false,
payload: false,
}),
Type::Option(inner) if **inner == Type::String => Ok(Value::OptionString {
is_some: false,
payload: String::new(),
}),
_ => Err(unsupported_test_expr(
file,
expr,
"unsupported option payloads",
)),
},
TExprKind::ResultOk { value } => {
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
match &expr.ty {
Type::Result(ok, err) if **ok == Type::I32 && **err == Type::I32 => {
let Some(value) = value.as_i32() else {
return Err(unsupported_test_expr(file, expr, "non-i32 result payloads"));
};
Ok(Value::ResultI32 {
is_ok: true,
payload: value,
})
}
Type::Result(ok, err) if **ok == Type::I64 && **err == Type::I32 => {
let Some(value) = value.as_i64() else {
return Err(unsupported_test_expr(file, expr, "non-i64 result payloads"));
};
Ok(Value::ResultI64I32 {
is_ok: true,
ok_payload: value,
err_payload: 0,
})
}
Type::Result(ok, err) if **ok == Type::U32 && **err == Type::I32 => {
let Some(value) = value.as_u32() else {
return Err(unsupported_test_expr(file, expr, "non-u32 result payloads"));
};
Ok(Value::ResultU32I32 {
is_ok: true,
payload: value,
})
}
Type::Result(ok, err) if **ok == Type::U64 && **err == Type::I32 => {
let Some(value) = value.as_u64() else {
return Err(unsupported_test_expr(file, expr, "non-u64 result payloads"));
};
Ok(Value::ResultU64I32 {
is_ok: true,
ok_payload: value,
err_payload: 0,
})
}
Type::Result(ok, err) if **ok == Type::F64 && **err == Type::I32 => {
let Some(value) = value.as_f64() else {
return Err(unsupported_test_expr(file, expr, "non-f64 result payloads"));
};
Ok(Value::ResultF64I32 {
is_ok: true,
ok_payload: value,
err_payload: 0,
})
}
Type::Result(ok, err) if **ok == Type::Bool && **err == Type::I32 => {
let Some(value) = value.as_bool() else {
return Err(unsupported_test_expr(
file,
expr,
"non-bool result payloads",
));
};
Ok(Value::ResultBoolI32 {
is_ok: true,
ok_payload: value,
err_payload: 0,
})
}
Type::Result(ok, err) if **ok == Type::String && **err == Type::I32 => {
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"non-string result ok payloads",
));
};
Ok(Value::ResultStringI32 {
is_ok: true,
ok_payload: value.to_string(),
err_payload: 0,
})
}
_ => Err(unsupported_test_expr(
file,
expr,
"unsupported result payloads",
)),
}
}
TExprKind::ResultErr { value } => {
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"non-i32 result err payloads",
));
};
match &expr.ty {
Type::Result(ok, err) if **ok == Type::I32 && **err == Type::I32 => {
Ok(Value::ResultI32 {
is_ok: false,
payload: value,
})
}
Type::Result(ok, err) if **ok == Type::I64 && **err == Type::I32 => {
Ok(Value::ResultI64I32 {
is_ok: false,
ok_payload: 0,
err_payload: value,
})
}
Type::Result(ok, err) if **ok == Type::U32 && **err == Type::I32 => {
Ok(Value::ResultU32I32 {
is_ok: false,
payload: value as u32,
})
}
Type::Result(ok, err) if **ok == Type::U64 && **err == Type::I32 => {
Ok(Value::ResultU64I32 {
is_ok: false,
ok_payload: 0,
err_payload: value,
})
}
Type::Result(ok, err) if **ok == Type::F64 && **err == Type::I32 => {
Ok(Value::ResultF64I32 {
is_ok: false,
ok_payload: 0.0,
err_payload: value,
})
}
Type::Result(ok, err) if **ok == Type::Bool && **err == Type::I32 => {
Ok(Value::ResultBoolI32 {
is_ok: false,
ok_payload: false,
err_payload: value,
})
}
Type::Result(ok, err) if **ok == Type::String && **err == Type::I32 => {
Ok(Value::ResultStringI32 {
is_ok: false,
ok_payload: String::new(),
err_payload: value,
})
}
_ => Err(unsupported_test_expr(
file,
expr,
"unsupported result payloads",
)),
}
}
TExprKind::OptionIsSome { value } => {
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
match value {
Value::OptionI32 { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(is_some))
}
Value::OptionI64 { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(is_some))
}
Value::OptionU32 { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(is_some))
}
Value::OptionU64 { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(is_some))
}
Value::OptionF64 { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(is_some))
}
Value::OptionBool { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(is_some))
}
Value::OptionString { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(is_some))
}
_ => Err(unsupported_test_expr(
file,
expr,
"option observation on non-option values",
)),
}
}
TExprKind::OptionIsNone { value } => {
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
match value {
Value::OptionI32 { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(!is_some))
}
Value::OptionI64 { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(!is_some))
}
Value::OptionU32 { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(!is_some))
}
Value::OptionU64 { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(!is_some))
}
Value::OptionF64 { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(!is_some))
}
Value::OptionBool { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(!is_some))
}
Value::OptionString { is_some, payload } => {
let _ = payload;
Ok(Value::Bool(!is_some))
}
_ => Err(unsupported_test_expr(
file,
expr,
"option observation on non-option values",
)),
}
}
TExprKind::OptionUnwrapSome { value } => {
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
match value {
Value::OptionI32 { is_some, payload } => {
if !is_some {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_some on none",
));
}
Ok(Value::I32(payload))
}
Value::OptionI64 { is_some, payload } => {
if !is_some {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_some on none",
));
}
Ok(Value::I64(payload))
}
Value::OptionU32 { is_some, payload } => {
if !is_some {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_some on none",
));
}
Ok(Value::U32(payload))
}
Value::OptionU64 { is_some, payload } => {
if !is_some {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_some on none",
));
}
Ok(Value::U64(payload))
}
Value::OptionF64 { is_some, payload } => {
if !is_some {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_some on none",
));
}
Ok(Value::F64(payload))
}
Value::OptionBool { is_some, payload } => {
if !is_some {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_some on none",
));
}
Ok(Value::Bool(payload))
}
Value::OptionString { is_some, payload } => {
if !is_some {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_some on none",
));
}
Ok(Value::String(payload))
}
_ => Err(unsupported_test_expr(
file,
expr,
"option payload access on non-option values",
)),
}
}
TExprKind::ResultIsOk { value, .. } => {
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
match value {
Value::ResultI32 { is_ok, payload } => {
let _ = payload;
Ok(Value::Bool(is_ok))
}
Value::ResultI64I32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = (ok_payload, err_payload);
Ok(Value::Bool(is_ok))
}
Value::ResultU32I32 { is_ok, payload } => {
let _ = payload;
Ok(Value::Bool(is_ok))
}
Value::ResultU64I32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = (ok_payload, err_payload);
Ok(Value::Bool(is_ok))
}
Value::ResultF64I32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = (ok_payload, err_payload);
Ok(Value::Bool(is_ok))
}
Value::ResultBoolI32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = (ok_payload, err_payload);
Ok(Value::Bool(is_ok))
}
Value::ResultStringI32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = (ok_payload, err_payload);
Ok(Value::Bool(is_ok))
}
_ => Err(unsupported_test_expr(
file,
expr,
"result observation on non-result values",
)),
}
}
TExprKind::ResultIsErr { value, .. } => {
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
match value {
Value::ResultI32 { is_ok, payload } => {
let _ = payload;
Ok(Value::Bool(!is_ok))
}
Value::ResultI64I32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = (ok_payload, err_payload);
Ok(Value::Bool(!is_ok))
}
Value::ResultU32I32 { is_ok, payload } => {
let _ = payload;
Ok(Value::Bool(!is_ok))
}
Value::ResultU64I32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = (ok_payload, err_payload);
Ok(Value::Bool(!is_ok))
}
Value::ResultF64I32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = (ok_payload, err_payload);
Ok(Value::Bool(!is_ok))
}
Value::ResultBoolI32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = (ok_payload, err_payload);
Ok(Value::Bool(!is_ok))
}
Value::ResultStringI32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = (ok_payload, err_payload);
Ok(Value::Bool(!is_ok))
}
_ => Err(unsupported_test_expr(
file,
expr,
"result observation on non-result values",
)),
}
}
TExprKind::ResultUnwrapOk { value, .. } => {
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
match value {
Value::ResultI32 { is_ok, payload } => {
if !is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_ok on err",
));
}
Ok(Value::I32(payload))
}
Value::ResultI64I32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = err_payload;
if !is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_ok on err",
));
}
Ok(Value::I64(ok_payload))
}
Value::ResultU32I32 { is_ok, payload } => {
if !is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_ok on err",
));
}
Ok(Value::U32(payload))
}
Value::ResultU64I32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = err_payload;
if !is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_ok on err",
));
}
Ok(Value::U64(ok_payload))
}
Value::ResultF64I32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = err_payload;
if !is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_ok on err",
));
}
Ok(Value::F64(ok_payload))
}
Value::ResultBoolI32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = err_payload;
if !is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_ok on err",
));
}
Ok(Value::Bool(ok_payload))
}
Value::ResultStringI32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = err_payload;
if !is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_ok on err",
));
}
Ok(Value::String(ok_payload))
}
_ => Err(unsupported_test_expr(
file,
expr,
"result payload access on non-result values",
)),
}
}
TExprKind::ResultUnwrapErr { value, .. } => {
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
match value {
Value::ResultI32 { is_ok, payload } => {
if is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_err on ok",
));
}
Ok(Value::I32(payload))
}
Value::ResultI64I32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = ok_payload;
if is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_err on ok",
));
}
Ok(Value::I32(err_payload))
}
Value::ResultU32I32 { is_ok, payload } => {
if is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_err on ok",
));
}
Ok(Value::I32(payload as i32))
}
Value::ResultU64I32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = ok_payload;
if is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_err on ok",
));
}
Ok(Value::I32(err_payload))
}
Value::ResultF64I32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = ok_payload;
if is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_err on ok",
));
}
Ok(Value::I32(err_payload))
}
Value::ResultBoolI32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = ok_payload;
if is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_err on ok",
));
}
Ok(Value::I32(err_payload))
}
Value::ResultStringI32 {
is_ok,
ok_payload,
err_payload,
} => {
let _ = ok_payload;
if is_ok {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: unwrap_err on ok",
));
}
Ok(Value::I32(err_payload))
}
_ => Err(unsupported_test_expr(
file,
expr,
"result payload access on non-result values",
)),
}
}
TExprKind::FieldAccess { value, field } => {
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
let Value::Struct { name, fields } = value else {
return Err(unsupported_test_expr(
file,
expr,
"field access on non-struct values",
));
};
fields.get(field).cloned().ok_or_else(|| {
Diagnostic::new(
file,
"TestRuntimeError",
format!("struct `{}` has no test field `{}`", name, field),
)
.with_span(expr.span)
})
}
TExprKind::Index { array, index } => {
let array = eval_expr(file, array, locals, functions, foreign_imports, depth)?;
let index = eval_expr(file, index, locals, functions, foreign_imports, depth)?;
let Value::Array(values) = array else {
return Err(unsupported_test_expr(
file,
expr,
"indexing non-array values",
));
};
let Some(index) = index.as_i32() else {
return Err(unsupported_test_expr(file, expr, "non-i32 array indices"));
};
let Ok(index) = usize::try_from(index) else {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: array index out of bounds",
));
};
values.index_value(index).ok_or_else(|| {
runtime_trap(file, expr, "slovo runtime error: array index out of bounds")
})
}
TExprKind::Var(name) => locals.get(name).cloned().ok_or_else(|| {
Diagnostic::new(
file,
"TestRuntimeError",
format!("unknown test local `{}`", name),
)
.with_span(expr.span)
}),
TExprKind::Local {
name, initializer, ..
} => {
let value = eval_expr(file, initializer, locals, functions, foreign_imports, depth)?;
locals.insert(name.clone(), value);
Ok(Value::Unit)
}
TExprKind::Set { name, expr: value } => {
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
if !locals.contains_key(name) {
return Err(Diagnostic::new(
file,
"TestRuntimeError",
format!("unknown test local `{}`", name),
)
.with_span(expr.span));
}
locals.insert(name.clone(), value);
Ok(Value::Unit)
}
TExprKind::Binary { op, left, right } => {
let left = eval_expr(file, left, locals, functions, foreign_imports, depth)?;
let right = eval_expr(file, right, locals, functions, foreign_imports, depth)?;
eval_binary(file, expr, *op, left, right)
}
TExprKind::If {
condition,
then_expr,
else_expr,
} => {
let condition = eval_expr(file, condition, locals, functions, foreign_imports, depth)?;
let Some(condition) = condition.as_bool() else {
return Err(Diagnostic::new(
file,
"TestRuntimeError",
"`if` condition was not bool",
)
.with_span(expr.span));
};
if condition {
eval_expr(file, then_expr, locals, functions, foreign_imports, depth)
} else {
eval_expr(file, else_expr, locals, functions, foreign_imports, depth)
}
}
TExprKind::Match { subject, arms } => {
let subject_value =
eval_expr(file, subject, locals, functions, foreign_imports, depth)?;
eval_match(
file,
expr,
subject_value,
arms,
locals,
functions,
foreign_imports,
depth,
)
}
TExprKind::While { condition, body } => {
for _ in 0..MAX_TEST_WHILE_ITERATIONS {
let condition =
eval_expr(file, condition, locals, functions, foreign_imports, depth)?;
let Some(condition) = condition.as_bool() else {
return Err(Diagnostic::new(
file,
"TestRuntimeError",
"`while` condition was not bool",
)
.with_span(expr.span));
};
if !condition {
return Ok(Value::Unit);
}
for body_expr in body {
eval_expr(file, body_expr, locals, functions, foreign_imports, depth)?;
}
}
Err(Diagnostic::new(
file,
"TestRuntimeError",
"test runner exceeded maximum while iteration count",
)
.with_span(expr.span)
.hint("check for an unbounded `while` loop in the test expression"))
}
TExprKind::Unsafe { body } => {
let outer_names = locals.keys().cloned().collect::<HashSet<_>>();
let value = eval_body(file, body, locals, functions, foreign_imports, depth)?;
locals.retain(|name, _| outer_names.contains(name));
Ok(value)
}
TExprKind::Call { name, args } => {
let runtime_symbol = std_runtime::runtime_symbol(name).unwrap_or(name);
if runtime_symbol == "std.num.i32_to_i64" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.num.i32_to_i64` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.num.i32_to_i64` on non-i32 values",
));
};
return Ok(Value::I64(i64::from(value)));
}
if runtime_symbol == "std.num.i32_to_f64" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.num.i32_to_f64` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.num.i32_to_f64` on non-i32 values",
));
};
return Ok(Value::F64(f64::from(value)));
}
if runtime_symbol == "std.num.i64_to_f64" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.num.i64_to_f64` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_i64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.num.i64_to_f64` on non-i64 values",
));
};
return Ok(Value::F64(value as f64));
}
if runtime_symbol == "std.num.i64_to_i32_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.num.i64_to_i32_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_i64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.num.i64_to_i32_result` on non-i64 values",
));
};
if i32::try_from(value).is_ok() {
return Ok(Value::ResultI32 {
is_ok: true,
payload: value as i32,
});
}
return Ok(Value::ResultI32 {
is_ok: false,
payload: 1,
});
}
if runtime_symbol == "std.num.f64_to_i32_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.num.f64_to_i32_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_f64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.num.f64_to_i32_result` on non-f64 values",
));
};
if value.is_finite()
&& value.fract() == 0.0
&& value >= f64::from(i32::MIN)
&& value <= f64::from(i32::MAX)
{
return Ok(Value::ResultI32 {
is_ok: true,
payload: value as i32,
});
}
return Ok(Value::ResultI32 {
is_ok: false,
payload: 1,
});
}
if runtime_symbol == "std.num.f64_to_i64_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.num.f64_to_i64_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_f64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.num.f64_to_i64_result` on non-f64 values",
));
};
if value.is_finite()
&& value.fract() == 0.0
&& value >= i64::MIN as f64
&& value < 9_223_372_036_854_775_808.0
{
return Ok(Value::ResultI64I32 {
is_ok: true,
ok_payload: value as i64,
err_payload: 0,
});
}
return Ok(Value::ResultI64I32 {
is_ok: false,
ok_payload: 0,
err_payload: 1,
});
}
if runtime_symbol == "__glagol_num_i32_to_string" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.num.i32_to_string` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.num.i32_to_string` on non-i32 values",
));
};
return Ok(Value::String(value.to_string()));
}
if runtime_symbol == "__glagol_num_u32_to_string" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.num.u32_to_string` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_u32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.num.u32_to_string` on non-u32 values",
));
};
return Ok(Value::String(value.to_string()));
}
if runtime_symbol == "__glagol_num_i64_to_string" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.num.i64_to_string` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_i64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.num.i64_to_string` on non-i64 values",
));
};
return Ok(Value::String(value.to_string()));
}
if runtime_symbol == "__glagol_num_u64_to_string" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.num.u64_to_string` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_u64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.num.u64_to_string` on non-u64 values",
));
};
return Ok(Value::String(value.to_string()));
}
if runtime_symbol == "__glagol_num_f64_to_string" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.num.f64_to_string` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_f64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.num.f64_to_string` on non-f64 values",
));
};
return Ok(Value::String(format_f64_to_string(value)));
}
if runtime_symbol == "print_i32" {
return Err(unsupported_test_expr(
file,
expr,
"`print_i32` calls while running tests",
));
}
if runtime_symbol == "print_f64" {
let [arg] = args.as_slice() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.io.print_f64` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
if value.as_f64().is_none() {
return Err(unsupported_test_expr(
file,
expr,
"`std.io.print_f64` on non-f64 values",
));
}
return Ok(Value::Unit);
}
if runtime_symbol == "print_i64" {
let [arg] = args.as_slice() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.io.print_i64` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
if value.as_i64().is_none() {
return Err(unsupported_test_expr(
file,
expr,
"`std.io.print_i64` on non-i64 values",
));
}
return Ok(Value::Unit);
}
if runtime_symbol == "print_u32" {
let [arg] = args.as_slice() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.io.print_u32` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
if value.as_u32().is_none() {
return Err(unsupported_test_expr(
file,
expr,
"`std.io.print_u32` on non-u32 values",
));
}
return Ok(Value::Unit);
}
if runtime_symbol == "print_u64" {
let [arg] = args.as_slice() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.io.print_u64` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
if value.as_u64().is_none() {
return Err(unsupported_test_expr(
file,
expr,
"`std.io.print_u64` on non-u64 values",
));
}
return Ok(Value::Unit);
}
if runtime_symbol == "print_string" || runtime_symbol == "print_bool" {
return Err(unsupported_test_expr(
file,
expr,
"print calls while running tests",
));
}
if runtime_symbol == "__glagol_io_eprint" {
return Err(unsupported_test_expr(
file,
expr,
"`std.io.eprint` calls while running tests",
));
}
if runtime_symbol == "__glagol_io_read_stdin_result" {
return Ok(Value::ResultStringI32 {
is_ok: true,
ok_payload: String::new(),
err_payload: 0,
});
}
if runtime_symbol == "string_len" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `string_len` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`string_len` on non-string values",
));
};
let len = i32::try_from(value.len()).map_err(|_| {
Diagnostic::new(file, "TestRuntimeError", "string length exceeded i32")
.with_span(expr.span)
})?;
return Ok(Value::I32(len));
}
if runtime_symbol == "__glagol_string_concat" {
let Some(left) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.concat` calls",
));
};
let Some(right) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.concat` calls",
));
};
let left = eval_expr(file, left, locals, functions, foreign_imports, depth)?;
let right = eval_expr(file, right, locals, functions, foreign_imports, depth)?;
let Some(left) = left.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.concat` on non-string values",
));
};
let Some(right) = right.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.concat` on non-string values",
));
};
return Ok(Value::String(format!("{}{}", left, right)));
}
if runtime_symbol == "__glagol_json_quote_string" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.json.quote_string` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.json.quote_string` on non-string values",
));
};
return Ok(Value::String(quote_json_string_value(value)));
}
if runtime_symbol == "__glagol_string_parse_i32_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.parse_i32_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.parse_i32_result` on non-string values",
));
};
return Ok(parse_i32_result_value(value));
}
if runtime_symbol == "__glagol_string_parse_i64_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.parse_i64_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.parse_i64_result` on non-string values",
));
};
return Ok(parse_i64_result_value(value));
}
if runtime_symbol == "__glagol_string_parse_u32_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.parse_u32_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.parse_u32_result` on non-string values",
));
};
return Ok(parse_u32_result_value(value));
}
if runtime_symbol == "__glagol_string_parse_u64_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.parse_u64_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.parse_u64_result` on non-string values",
));
};
return Ok(parse_u64_result_value(value));
}
if runtime_symbol == "__glagol_string_parse_f64_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.parse_f64_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.parse_f64_result` on non-string values",
));
};
return Ok(parse_f64_result_value(value));
}
if runtime_symbol == "__glagol_string_parse_bool_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.parse_bool_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.parse_bool_result` on non-string values",
));
};
return Ok(parse_bool_result_value(value));
}
if runtime_symbol == "__glagol_process_argc" {
let argc = i32::try_from(env::args().count()).map_err(|_| {
Diagnostic::new(
file,
"TestRuntimeError",
"process argument count exceeded i32",
)
.with_span(expr.span)
})?;
return Ok(Value::I32(argc));
}
if runtime_symbol == "__glagol_process_arg" {
let Some(index) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.process.arg` calls",
));
};
let index = eval_expr(file, index, locals, functions, foreign_imports, depth)?;
let Some(index) = index.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.process.arg` with non-i32 index",
));
};
let Ok(index) = usize::try_from(index) else {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: process argument index out of bounds",
));
};
return env::args().nth(index).map(Value::String).ok_or_else(|| {
runtime_trap(
file,
expr,
"slovo runtime error: process argument index out of bounds",
)
});
}
if runtime_symbol == "__glagol_process_arg_result" {
let Some(index) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.process.arg_result` calls",
));
};
let index = eval_expr(file, index, locals, functions, foreign_imports, depth)?;
let Some(index) = index.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.process.arg_result` with non-i32 index",
));
};
let value = usize::try_from(index)
.ok()
.and_then(|index| env::args().nth(index));
return Ok(match value {
Some(value) => Value::ResultStringI32 {
is_ok: true,
ok_payload: value,
err_payload: 0,
},
None => Value::ResultStringI32 {
is_ok: false,
ok_payload: String::new(),
err_payload: 1,
},
});
}
if runtime_symbol == "__glagol_env_get" {
let Some(name) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.env.get` calls",
));
};
let name = eval_expr(file, name, locals, functions, foreign_imports, depth)?;
let Some(name) = name.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.env.get` on non-string values",
));
};
return Ok(Value::String(env::var(name).unwrap_or_default()));
}
if runtime_symbol == "__glagol_env_get_result" {
let Some(name) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.env.get_result` calls",
));
};
let name = eval_expr(file, name, locals, functions, foreign_imports, depth)?;
let Some(name) = name.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.env.get_result` on non-string values",
));
};
return Ok(match env::var(name) {
Ok(value) => Value::ResultStringI32 {
is_ok: true,
ok_payload: value,
err_payload: 0,
},
Err(_) => Value::ResultStringI32 {
is_ok: false,
ok_payload: String::new(),
err_payload: 1,
},
});
}
if runtime_symbol == "__glagol_fs_read_text" {
let Some(path) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.read_text` calls",
));
};
let path = eval_expr(file, path, locals, functions, foreign_imports, depth)?;
let Some(path) = path.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.read_text` on non-string values",
));
};
return fs::read_to_string(path).map(Value::String).map_err(|_| {
runtime_trap(file, expr, "slovo runtime error: file read failed")
});
}
if runtime_symbol == "__glagol_fs_read_text_result" {
let Some(path) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.read_text_result` calls",
));
};
let path = eval_expr(file, path, locals, functions, foreign_imports, depth)?;
let Some(path) = path.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.read_text_result` on non-string values",
));
};
return Ok(match fs::read_to_string(path) {
Ok(value) => Value::ResultStringI32 {
is_ok: true,
ok_payload: value,
err_payload: 0,
},
Err(_) => Value::ResultStringI32 {
is_ok: false,
ok_payload: String::new(),
err_payload: 1,
},
});
}
if runtime_symbol == "__glagol_fs_write_text" {
let Some(path) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.write_text` calls",
));
};
let Some(text) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.write_text` calls",
));
};
let path = eval_expr(file, path, locals, functions, foreign_imports, depth)?;
let text = eval_expr(file, text, locals, functions, foreign_imports, depth)?;
let Some(path) = path.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.write_text` path on non-string values",
));
};
let Some(text) = text.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.write_text` text on non-string values",
));
};
return Ok(Value::I32(if fs::write(path, text).is_ok() {
0
} else {
1
}));
}
if runtime_symbol == "__glagol_fs_write_text_result" {
let Some(path) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.write_text_result` calls",
));
};
let Some(text) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.write_text_result` calls",
));
};
let path = eval_expr(file, path, locals, functions, foreign_imports, depth)?;
let text = eval_expr(file, text, locals, functions, foreign_imports, depth)?;
let Some(path) = path.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.write_text_result` path on non-string values",
));
};
let Some(text) = text.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.write_text_result` text on non-string values",
));
};
let status = if fs::write(path, text).is_ok() { 0 } else { 1 };
return Ok(Value::ResultI32 {
is_ok: status == 0,
payload: status,
});
}
if runtime_symbol == "__glagol_fs_exists" {
let Some(path) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.exists` calls",
));
};
let path = eval_expr(file, path, locals, functions, foreign_imports, depth)?;
let Some(path) = path.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.exists` path on non-string values",
));
};
return Ok(Value::Bool(fs::metadata(path).is_ok()));
}
if runtime_symbol == "__glagol_fs_is_file" {
let Some(path) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.is_file` calls",
));
};
let path = eval_expr(file, path, locals, functions, foreign_imports, depth)?;
let Some(path) = path.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.is_file` path on non-string values",
));
};
return Ok(Value::Bool(
fs::metadata(path)
.map(|metadata| metadata.is_file())
.unwrap_or(false),
));
}
if runtime_symbol == "__glagol_fs_is_dir" {
let Some(path) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.is_dir` calls",
));
};
let path = eval_expr(file, path, locals, functions, foreign_imports, depth)?;
let Some(path) = path.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.is_dir` path on non-string values",
));
};
return Ok(Value::Bool(
fs::metadata(path)
.map(|metadata| metadata.is_dir())
.unwrap_or(false),
));
}
if runtime_symbol == "__glagol_fs_remove_file_result" {
let Some(path) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.remove_file_result` calls",
));
};
let path = eval_expr(file, path, locals, functions, foreign_imports, depth)?;
let Some(path) = path.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.remove_file_result` path on non-string values",
));
};
let status = match fs::remove_file(path) {
Ok(_) => 0,
Err(_) => 1,
};
return Ok(Value::ResultI32 {
is_ok: status == 0,
payload: status,
});
}
if runtime_symbol == "__glagol_fs_create_dir_result" {
let Some(path) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.create_dir_result` calls",
));
};
let path = eval_expr(file, path, locals, functions, foreign_imports, depth)?;
let Some(path) = path.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.create_dir_result` path on non-string values",
));
};
let status = match fs::create_dir(path) {
Ok(_) => 0,
Err(_) => 1,
};
return Ok(Value::ResultI32 {
is_ok: status == 0,
payload: status,
});
}
if runtime_symbol == "__glagol_fs_open_text_read_result" {
let Some(path) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.open_text_read_result` calls",
));
};
let path = eval_expr(file, path, locals, functions, foreign_imports, depth)?;
let Some(path) = path.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.open_text_read_result` path on non-string values",
));
};
return Ok(match fs::File::open(path) {
Ok(file) => {
let handle = NEXT_TEST_FILE_HANDLE.fetch_add(1, Ordering::Relaxed);
test_file_handles()
.lock()
.expect("test file handle table lock poisoned")
.insert(handle, file);
Value::ResultI32 {
is_ok: true,
payload: handle,
}
}
Err(_) => Value::ResultI32 {
is_ok: false,
payload: 1,
},
});
}
if runtime_symbol == "__glagol_fs_read_open_text_result" {
let Some(handle) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.read_open_text_result` calls",
));
};
let handle = eval_expr(file, handle, locals, functions, foreign_imports, depth)?;
let Some(handle) = handle.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.read_open_text_result` handle on non-i32 values",
));
};
let mut handles = test_file_handles()
.lock()
.expect("test file handle table lock poisoned");
let Some(open_file) = handles.get_mut(&handle) else {
return Ok(Value::ResultStringI32 {
is_ok: false,
ok_payload: String::new(),
err_payload: 1,
});
};
let mut text = String::new();
return Ok(match open_file.read_to_string(&mut text) {
Ok(_) => Value::ResultStringI32 {
is_ok: true,
ok_payload: text,
err_payload: 0,
},
Err(_) => Value::ResultStringI32 {
is_ok: false,
ok_payload: String::new(),
err_payload: 1,
},
});
}
if runtime_symbol == "__glagol_fs_close_result" {
let Some(handle) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.fs.close_result` calls",
));
};
let handle = eval_expr(file, handle, locals, functions, foreign_imports, depth)?;
let Some(handle) = handle.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.fs.close_result` handle on non-i32 values",
));
};
let was_open = test_file_handles()
.lock()
.expect("test file handle table lock poisoned")
.remove(&handle)
.is_some();
return Ok(Value::ResultI32 {
is_ok: was_open,
payload: if was_open { 0 } else { 1 },
});
}
if runtime_symbol == "__glagol_net_tcp_connect_loopback_result" {
let Some(port) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.net.tcp_connect_loopback_result` calls",
));
};
let port = eval_expr(file, port, locals, functions, foreign_imports, depth)?;
let Some(_) = port.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.net.tcp_connect_loopback_result` port on non-i32 values",
));
};
return Ok(Value::ResultI32 {
is_ok: false,
payload: 1,
});
}
if runtime_symbol == "__glagol_net_tcp_listen_loopback_result" {
let Some(port) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.net.tcp_listen_loopback_result` calls",
));
};
let port = eval_expr(file, port, locals, functions, foreign_imports, depth)?;
let Some(_) = port.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.net.tcp_listen_loopback_result` port on non-i32 values",
));
};
return Ok(Value::ResultI32 {
is_ok: false,
payload: 1,
});
}
if runtime_symbol == "__glagol_net_tcp_bound_port_result" {
let Some(handle) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.net.tcp_bound_port_result` calls",
));
};
let handle = eval_expr(file, handle, locals, functions, foreign_imports, depth)?;
let Some(_) = handle.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.net.tcp_bound_port_result` handle on non-i32 values",
));
};
return Ok(Value::ResultI32 {
is_ok: false,
payload: 1,
});
}
if runtime_symbol == "__glagol_net_tcp_accept_result" {
let Some(listener) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.net.tcp_accept_result` calls",
));
};
let listener =
eval_expr(file, listener, locals, functions, foreign_imports, depth)?;
let Some(_) = listener.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.net.tcp_accept_result` listener on non-i32 values",
));
};
return Ok(Value::ResultI32 {
is_ok: false,
payload: 1,
});
}
if runtime_symbol == "__glagol_net_tcp_read_all_result" {
let Some(handle) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.net.tcp_read_all_result` calls",
));
};
let handle = eval_expr(file, handle, locals, functions, foreign_imports, depth)?;
let Some(_) = handle.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.net.tcp_read_all_result` handle on non-i32 values",
));
};
return Ok(Value::ResultStringI32 {
is_ok: false,
ok_payload: String::new(),
err_payload: 1,
});
}
if runtime_symbol == "__glagol_net_tcp_write_text_result" {
let Some(handle) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.net.tcp_write_text_result` calls",
));
};
let Some(text) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.net.tcp_write_text_result` calls",
));
};
let handle = eval_expr(file, handle, locals, functions, foreign_imports, depth)?;
let text = eval_expr(file, text, locals, functions, foreign_imports, depth)?;
let Some(_) = handle.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.net.tcp_write_text_result` handle on non-i32 values",
));
};
let Some(_) = text.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.net.tcp_write_text_result` text on non-string values",
));
};
return Ok(Value::ResultI32 {
is_ok: false,
payload: 1,
});
}
if runtime_symbol == "__glagol_net_tcp_close_result" {
let Some(handle) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.net.tcp_close_result` calls",
));
};
let handle = eval_expr(file, handle, locals, functions, foreign_imports, depth)?;
let Some(_) = handle.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.net.tcp_close_result` handle on non-i32 values",
));
};
return Ok(Value::ResultI32 {
is_ok: false,
payload: 1,
});
}
if runtime_symbol == "__glagol_vec_i32_empty" {
return Ok(Value::VecI32(Vec::new()));
}
if runtime_symbol == "__glagol_vec_i32_append" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.i32.append` calls",
));
};
let Some(element) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.i32.append` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let element = eval_expr(file, element, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.i32.append` on non-vector values",
));
};
let Some(element) = element.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.i32.append` with non-i32 elements",
));
};
let mut appended = values.to_vec();
appended.push(element);
return Ok(Value::VecI32(appended));
}
if runtime_symbol == "__glagol_vec_i32_len" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.i32.len` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.i32.len` on non-vector values",
));
};
let len = i32::try_from(values.len()).map_err(|_| {
Diagnostic::new(file, "TestRuntimeError", "vector length exceeded i32")
.with_span(expr.span)
})?;
return Ok(Value::I32(len));
}
if runtime_symbol == "__glagol_vec_i32_index" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.i32.index` calls",
));
};
let Some(index) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.i32.index` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let index = eval_expr(file, index, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.i32.index` on non-vector values",
));
};
let Some(index) = index.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.i32.index` with non-i32 index",
));
};
let Ok(index) = usize::try_from(index) else {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: vector index out of bounds",
));
};
return values.get(index).copied().map(Value::I32).ok_or_else(|| {
runtime_trap(
file,
expr,
"slovo runtime error: vector index out of bounds",
)
});
}
if runtime_symbol == "__glagol_vec_i64_empty" {
return Ok(Value::VecI64(Vec::new()));
}
if runtime_symbol == "__glagol_vec_i64_append" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.i64.append` calls",
));
};
let Some(element) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.i64.append` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let element = eval_expr(file, element, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_i64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.i64.append` on non-vector values",
));
};
let Some(element) = element.as_i64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.i64.append` with non-i64 elements",
));
};
let mut appended = values.to_vec();
appended.push(element);
return Ok(Value::VecI64(appended));
}
if runtime_symbol == "__glagol_vec_i64_len" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.i64.len` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_i64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.i64.len` on non-vector values",
));
};
let len = i32::try_from(values.len()).map_err(|_| {
Diagnostic::new(file, "TestRuntimeError", "vector length exceeded i32")
.with_span(expr.span)
})?;
return Ok(Value::I32(len));
}
if runtime_symbol == "__glagol_vec_i64_index" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.i64.index` calls",
));
};
let Some(index) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.i64.index` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let index = eval_expr(file, index, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_i64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.i64.index` on non-vector values",
));
};
let Some(index) = index.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.i64.index` with non-i32 index",
));
};
let Ok(index) = usize::try_from(index) else {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: vector index out of bounds",
));
};
return values.get(index).copied().map(Value::I64).ok_or_else(|| {
runtime_trap(
file,
expr,
"slovo runtime error: vector index out of bounds",
)
});
}
if runtime_symbol == "__glagol_vec_f64_empty" {
return Ok(Value::VecF64(Vec::new()));
}
if runtime_symbol == "__glagol_vec_f64_append" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.f64.append` calls",
));
};
let Some(element) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.f64.append` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let element = eval_expr(file, element, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_f64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.f64.append` on non-vector values",
));
};
let Some(element) = element.as_f64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.f64.append` with non-f64 elements",
));
};
let mut appended = values.to_vec();
appended.push(element);
return Ok(Value::VecF64(appended));
}
if runtime_symbol == "__glagol_vec_f64_len" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.f64.len` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_f64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.f64.len` on non-vector values",
));
};
let len = i32::try_from(values.len()).map_err(|_| {
Diagnostic::new(file, "TestRuntimeError", "vector length exceeded i32")
.with_span(expr.span)
})?;
return Ok(Value::I32(len));
}
if runtime_symbol == "__glagol_vec_f64_index" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.f64.index` calls",
));
};
let Some(index) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.f64.index` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let index = eval_expr(file, index, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_f64() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.f64.index` on non-vector values",
));
};
let Some(index) = index.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.f64.index` with non-i32 index",
));
};
let Ok(index) = usize::try_from(index) else {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: vector index out of bounds",
));
};
return values.get(index).copied().map(Value::F64).ok_or_else(|| {
runtime_trap(
file,
expr,
"slovo runtime error: vector index out of bounds",
)
});
}
if runtime_symbol == "__glagol_vec_bool_empty" {
return Ok(Value::VecBool(Vec::new()));
}
if runtime_symbol == "__glagol_vec_bool_append" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.bool.append` calls",
));
};
let Some(element) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.bool.append` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let element = eval_expr(file, element, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_bool() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.bool.append` on non-vector values",
));
};
let Some(element) = element.as_bool() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.bool.append` with non-bool elements",
));
};
let mut appended = values.to_vec();
appended.push(element);
return Ok(Value::VecBool(appended));
}
if runtime_symbol == "__glagol_vec_bool_len" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.bool.len` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_bool() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.bool.len` on non-vector values",
));
};
let len = i32::try_from(values.len()).map_err(|_| {
Diagnostic::new(file, "TestRuntimeError", "vector length exceeded i32")
.with_span(expr.span)
})?;
return Ok(Value::I32(len));
}
if runtime_symbol == "__glagol_vec_bool_index" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.bool.index` calls",
));
};
let Some(index) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.bool.index` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let index = eval_expr(file, index, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_bool() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.bool.index` on non-vector values",
));
};
let Some(index) = index.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.bool.index` with non-i32 index",
));
};
let Ok(index) = usize::try_from(index) else {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: vector index out of bounds",
));
};
return values.get(index).copied().map(Value::Bool).ok_or_else(|| {
runtime_trap(
file,
expr,
"slovo runtime error: vector index out of bounds",
)
});
}
if runtime_symbol == "__glagol_vec_string_empty" {
return Ok(Value::VecString(Vec::new()));
}
if runtime_symbol == "__glagol_vec_string_append" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.string.append` calls",
));
};
let Some(element) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.string.append` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let element = eval_expr(file, element, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.string.append` on non-vector values",
));
};
let Some(element) = element.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.string.append` with non-string elements",
));
};
let mut appended = values.to_vec();
appended.push(element.to_string());
return Ok(Value::VecString(appended));
}
if runtime_symbol == "__glagol_vec_string_len" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.string.len` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.string.len` on non-vector values",
));
};
let len = i32::try_from(values.len()).map_err(|_| {
Diagnostic::new(file, "TestRuntimeError", "vector length exceeded i32")
.with_span(expr.span)
})?;
return Ok(Value::I32(len));
}
if runtime_symbol == "__glagol_vec_string_index" {
let Some(values) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.string.index` calls",
));
};
let Some(index) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.vec.string.index` calls",
));
};
let values = eval_expr(file, values, locals, functions, foreign_imports, depth)?;
let index = eval_expr(file, index, locals, functions, foreign_imports, depth)?;
let Some(values) = values.as_vec_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.string.index` on non-vector values",
));
};
let Some(index) = index.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.vec.string.index` with non-i32 index",
));
};
let Ok(index) = usize::try_from(index) else {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: vector index out of bounds",
));
};
return values
.get(index)
.cloned()
.map(Value::String)
.ok_or_else(|| {
runtime_trap(
file,
expr,
"slovo runtime error: vector index out of bounds",
)
});
}
if runtime_symbol == "__glagol_time_monotonic_ms" {
let start = MONOTONIC_START.get_or_init(Instant::now);
let elapsed_ms = start.elapsed().as_millis();
let value = i32::try_from(elapsed_ms).unwrap_or(i32::MAX);
return Ok(Value::I32(value));
}
if runtime_symbol == "__glagol_time_sleep_ms" {
let Some(ms) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.time.sleep_ms` calls",
));
};
let ms = eval_expr(file, ms, locals, functions, foreign_imports, depth)?;
let Some(ms) = ms.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.time.sleep_ms` with non-i32 duration",
));
};
if ms < 0 {
return Err(runtime_trap(
file,
expr,
"slovo runtime error: sleep_ms negative duration",
));
}
if ms != 0 {
return Err(unsupported_test_expr(
file,
expr,
"positive `std.time.sleep_ms` calls while running tests",
));
}
return Ok(Value::Unit);
}
if runtime_symbol == "__glagol_random_i32" {
return Ok(Value::I32(0));
}
if depth >= MAX_TEST_CALL_DEPTH {
return Err(Diagnostic::new(
file,
"TestRuntimeError",
"test runner exceeded maximum call depth",
)
.with_span(expr.span)
.hint("check for unbounded recursion in the test expression"));
}
if foreign_imports.contains(name.as_str()) {
return Err(Diagnostic::new(
file,
"UnsupportedTestExpression",
format!("test runner cannot execute C import `{}`", name),
)
.with_span(expr.span)
.hint("use `glagol build --link-c <path>` for hosted C FFI smoke tests"));
}
let function = functions.get(name.as_str()).ok_or_else(|| {
Diagnostic::new(
file,
"TestRuntimeError",
format!("unknown test function `{}`", name),
)
.with_span(expr.span)
})?;
let mut call_locals = HashMap::new();
for ((param_name, _), arg) in function.params.iter().zip(args) {
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
call_locals.insert(param_name.clone(), value);
}
eval_function_body(
function.file.as_str(),
function,
&mut call_locals,
functions,
foreign_imports,
depth + 1,
)
}
}
}
fn eval_match(
file: &str,
expr: &TExpr,
subject: Value,
arms: &[TMatchArm],
locals: &mut HashMap<String, Value>,
functions: &HashMap<&str, &CheckedFunction>,
foreign_imports: &HashSet<&str>,
depth: usize,
) -> Result<Value, Diagnostic> {
let (pattern, payload) = match subject {
Value::OptionI32 { is_some, payload } => {
if is_some {
(MatchPatternKind::Some, Some(Value::I32(payload)))
} else {
(MatchPatternKind::None, None)
}
}
Value::OptionI64 { is_some, payload } => {
if is_some {
(MatchPatternKind::Some, Some(Value::I64(payload)))
} else {
(MatchPatternKind::None, None)
}
}
Value::OptionU32 { is_some, payload } => {
if is_some {
(MatchPatternKind::Some, Some(Value::U32(payload)))
} else {
(MatchPatternKind::None, None)
}
}
Value::OptionU64 { is_some, payload } => {
if is_some {
(MatchPatternKind::Some, Some(Value::U64(payload)))
} else {
(MatchPatternKind::None, None)
}
}
Value::OptionF64 { is_some, payload } => {
if is_some {
(MatchPatternKind::Some, Some(Value::F64(payload)))
} else {
(MatchPatternKind::None, None)
}
}
Value::OptionBool { is_some, payload } => {
if is_some {
(MatchPatternKind::Some, Some(Value::Bool(payload)))
} else {
(MatchPatternKind::None, None)
}
}
Value::OptionString { is_some, payload } => {
if is_some {
(MatchPatternKind::Some, Some(Value::String(payload)))
} else {
(MatchPatternKind::None, None)
}
}
Value::ResultI32 { is_ok, payload } => {
if is_ok {
(MatchPatternKind::Ok, Some(Value::I32(payload)))
} else {
(MatchPatternKind::Err, Some(Value::I32(payload)))
}
}
Value::ResultI64I32 {
is_ok,
ok_payload,
err_payload,
} => {
if is_ok {
(MatchPatternKind::Ok, Some(Value::I64(ok_payload)))
} else {
(MatchPatternKind::Err, Some(Value::I32(err_payload)))
}
}
Value::ResultU32I32 { is_ok, payload } => {
if is_ok {
(MatchPatternKind::Ok, Some(Value::U32(payload)))
} else {
(MatchPatternKind::Err, Some(Value::I32(payload as i32)))
}
}
Value::ResultU64I32 {
is_ok,
ok_payload,
err_payload,
} => {
if is_ok {
(MatchPatternKind::Ok, Some(Value::U64(ok_payload)))
} else {
(MatchPatternKind::Err, Some(Value::I32(err_payload)))
}
}
Value::ResultF64I32 {
is_ok,
ok_payload,
err_payload,
} => {
if is_ok {
(MatchPatternKind::Ok, Some(Value::F64(ok_payload)))
} else {
(MatchPatternKind::Err, Some(Value::I32(err_payload)))
}
}
Value::ResultBoolI32 {
is_ok,
ok_payload,
err_payload,
} => {
if is_ok {
(MatchPatternKind::Ok, Some(Value::Bool(ok_payload)))
} else {
(MatchPatternKind::Err, Some(Value::I32(err_payload)))
}
}
Value::ResultStringI32 {
is_ok,
ok_payload,
err_payload,
} => {
if is_ok {
(MatchPatternKind::Ok, Some(Value::String(ok_payload)))
} else {
(MatchPatternKind::Err, Some(Value::I32(err_payload)))
}
}
Value::Enum {
name,
variant,
payload,
..
} => (
MatchPatternKind::EnumVariant {
enum_name: name,
variant,
},
payload.map(EnumPayloadValue::into_value),
),
other => {
return Err(unsupported_test_expr(
file,
expr,
&format!("matching values of type `{}`", other.ty()),
));
}
};
let Some(arm) = arms.iter().find(|arm| arm.pattern == pattern) else {
return Err(Diagnostic::new(
file,
"TestRuntimeError",
"checked match did not contain the selected arm",
)
.with_span(expr.span));
};
let outer_names = locals.keys().cloned().collect::<HashSet<_>>();
if let (Some(binding), Some(payload)) = (&arm.binding, payload) {
locals.insert(binding.clone(), payload);
}
let result = eval_body(file, &arm.body, locals, functions, foreign_imports, depth);
locals.retain(|name, _| outer_names.contains(name));
result
}
fn eval_body(
file: &str,
body: &[TExpr],
locals: &mut HashMap<String, Value>,
functions: &HashMap<&str, &CheckedFunction>,
foreign_imports: &HashSet<&str>,
depth: usize,
) -> Result<Value, Diagnostic> {
let mut value = Value::Unit;
for expr in body {
value = eval_expr(file, expr, locals, functions, foreign_imports, depth)?;
}
Ok(value)
}
fn eval_function_body(
file: &str,
function: &CheckedFunction,
locals: &mut HashMap<String, Value>,
functions: &HashMap<&str, &CheckedFunction>,
foreign_imports: &HashSet<&str>,
depth: usize,
) -> Result<Value, Diagnostic> {
eval_body(
file,
&function.body,
locals,
functions,
foreign_imports,
depth,
)
}
fn eval_binary(
file: &str,
expr: &TExpr,
op: BinaryOp,
left: Value,
right: Value,
) -> Result<Value, Diagnostic> {
if let (Some(left), Some(right)) = (left.as_f64(), right.as_f64()) {
return match op {
BinaryOp::Add => Ok(Value::F64(left + right)),
BinaryOp::Sub => Ok(Value::F64(left - right)),
BinaryOp::Mul => Ok(Value::F64(left * right)),
BinaryOp::Div => Ok(Value::F64(left / right)),
BinaryOp::Rem => Err(unsupported_test_expr(
file,
expr,
"f64 remainder is not supported",
)),
BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor => Err(unsupported_test_expr(
file,
expr,
"f64 bitwise operations are not supported",
)),
BinaryOp::Eq => Ok(Value::Bool(left == right)),
BinaryOp::Lt => Ok(Value::Bool(left < right)),
BinaryOp::Gt => Ok(Value::Bool(left > right)),
BinaryOp::Le => Ok(Value::Bool(left <= right)),
BinaryOp::Ge => Ok(Value::Bool(left >= right)),
};
}
if left.as_f64().is_some() || right.as_f64().is_some() {
return Err(unsupported_test_expr(
file,
expr,
"mixed i32/i64/u32/u64/f64 binary operands",
));
}
if let (Some(left), Some(right)) = (left.as_u64(), right.as_u64()) {
return match op {
BinaryOp::Add => checked_u64(file, expr, left.checked_add(right), "addition"),
BinaryOp::Sub => checked_u64(file, expr, left.checked_sub(right), "subtraction"),
BinaryOp::Mul => checked_u64(file, expr, left.checked_mul(right), "multiplication"),
BinaryOp::Div => {
if right == 0 {
return Err(Diagnostic::new(
file,
"TestRuntimeError",
"division by zero in test",
)
.with_span(expr.span));
}
checked_u64(file, expr, left.checked_div(right), "division")
}
BinaryOp::Rem => {
if right == 0 {
return Err(Diagnostic::new(
file,
"TestRuntimeError",
"remainder by zero in test",
)
.with_span(expr.span));
}
checked_u64(file, expr, left.checked_rem(right), "remainder")
}
BinaryOp::BitAnd => Ok(Value::U64(left & right)),
BinaryOp::BitOr => Ok(Value::U64(left | right)),
BinaryOp::BitXor => Ok(Value::U64(left ^ right)),
BinaryOp::Eq => Ok(Value::Bool(left == right)),
BinaryOp::Lt => Ok(Value::Bool(left < right)),
BinaryOp::Gt => Ok(Value::Bool(left > right)),
BinaryOp::Le => Ok(Value::Bool(left <= right)),
BinaryOp::Ge => Ok(Value::Bool(left >= right)),
};
}
if left.as_u64().is_some() || right.as_u64().is_some() {
return Err(unsupported_test_expr(
file,
expr,
"mixed i32/i64/u32/u64/f64 binary operands",
));
}
if let (Some(left), Some(right)) = (left.as_i64(), right.as_i64()) {
return match op {
BinaryOp::Add => checked_i64(file, expr, left.checked_add(right), "addition"),
BinaryOp::Sub => checked_i64(file, expr, left.checked_sub(right), "subtraction"),
BinaryOp::Mul => checked_i64(file, expr, left.checked_mul(right), "multiplication"),
BinaryOp::Div => {
if right == 0 {
return Err(Diagnostic::new(
file,
"TestRuntimeError",
"division by zero in test",
)
.with_span(expr.span));
}
checked_i64(file, expr, left.checked_div(right), "division")
}
BinaryOp::Rem => {
if right == 0 {
return Err(Diagnostic::new(
file,
"TestRuntimeError",
"remainder by zero in test",
)
.with_span(expr.span));
}
checked_i64(file, expr, left.checked_rem(right), "remainder")
}
BinaryOp::BitAnd => Ok(Value::I64(left & right)),
BinaryOp::BitOr => Ok(Value::I64(left | right)),
BinaryOp::BitXor => Ok(Value::I64(left ^ right)),
BinaryOp::Eq => Ok(Value::Bool(left == right)),
BinaryOp::Lt => Ok(Value::Bool(left < right)),
BinaryOp::Gt => Ok(Value::Bool(left > right)),
BinaryOp::Le => Ok(Value::Bool(left <= right)),
BinaryOp::Ge => Ok(Value::Bool(left >= right)),
};
}
if left.as_i64().is_some() || right.as_i64().is_some() {
return Err(unsupported_test_expr(
file,
expr,
"mixed i32/i64/u32/u64/f64 binary operands",
));
}
if let (Some(left), Some(right)) = (left.as_u32(), right.as_u32()) {
return match op {
BinaryOp::Add => checked_u32(file, expr, left.checked_add(right), "addition"),
BinaryOp::Sub => checked_u32(file, expr, left.checked_sub(right), "subtraction"),
BinaryOp::Mul => checked_u32(file, expr, left.checked_mul(right), "multiplication"),
BinaryOp::Div => {
if right == 0 {
return Err(Diagnostic::new(
file,
"TestRuntimeError",
"division by zero in test",
)
.with_span(expr.span));
}
checked_u32(file, expr, left.checked_div(right), "division")
}
BinaryOp::Rem => {
if right == 0 {
return Err(Diagnostic::new(
file,
"TestRuntimeError",
"remainder by zero in test",
)
.with_span(expr.span));
}
checked_u32(file, expr, left.checked_rem(right), "remainder")
}
BinaryOp::BitAnd => Ok(Value::U32(left & right)),
BinaryOp::BitOr => Ok(Value::U32(left | right)),
BinaryOp::BitXor => Ok(Value::U32(left ^ right)),
BinaryOp::Eq => Ok(Value::Bool(left == right)),
BinaryOp::Lt => Ok(Value::Bool(left < right)),
BinaryOp::Gt => Ok(Value::Bool(left > right)),
BinaryOp::Le => Ok(Value::Bool(left <= right)),
BinaryOp::Ge => Ok(Value::Bool(left >= right)),
};
}
if left.as_u32().is_some() || right.as_u32().is_some() {
return Err(unsupported_test_expr(
file,
expr,
"mixed i32/i64/u32/u64/f64 binary operands",
));
}
if op == BinaryOp::Eq {
if let (Some(left), Some(right)) = (left.as_bool(), right.as_bool()) {
return Ok(Value::Bool(left == right));
}
if let (Some(left), Some(right)) = (left.as_string(), right.as_string()) {
return Ok(Value::Bool(left == right));
}
if let (Some(left), Some(right)) = (left.as_vec_i32(), right.as_vec_i32()) {
return Ok(Value::Bool(left == right));
}
if let (Some(left), Some(right)) = (left.as_vec_i64(), right.as_vec_i64()) {
return Ok(Value::Bool(left == right));
}
if let (Some(left), Some(right)) = (left.as_vec_f64(), right.as_vec_f64()) {
return Ok(Value::Bool(left == right));
}
if let (Some(left), Some(right)) = (left.as_vec_bool(), right.as_vec_bool()) {
return Ok(Value::Bool(left == right));
}
if let (Some(left), Some(right)) = (left.as_vec_string(), right.as_vec_string()) {
return Ok(Value::Bool(left == right));
}
if let (
Value::Enum {
name: left_name,
discriminant: left_discriminant,
payload: left_payload,
..
},
Value::Enum {
name: right_name,
discriminant: right_discriminant,
payload: right_payload,
..
},
) = (&left, &right)
{
return Ok(Value::Bool(
left_name == right_name
&& left_discriminant == right_discriminant
&& left_payload == right_payload,
));
}
}
let Some(left) = left.as_i32() else {
return Err(unsupported_test_expr(file, expr, "non-i32 binary operands"));
};
let Some(right) = right.as_i32() else {
return Err(unsupported_test_expr(file, expr, "non-i32 binary operands"));
};
match op {
BinaryOp::Add => checked_i32(file, expr, left.checked_add(right), "addition"),
BinaryOp::Sub => checked_i32(file, expr, left.checked_sub(right), "subtraction"),
BinaryOp::Mul => checked_i32(file, expr, left.checked_mul(right), "multiplication"),
BinaryOp::Div => {
if right == 0 {
return Err(
Diagnostic::new(file, "TestRuntimeError", "division by zero in test")
.with_span(expr.span),
);
}
checked_i32(file, expr, left.checked_div(right), "division")
}
BinaryOp::Rem => {
if right == 0 {
return Err(
Diagnostic::new(file, "TestRuntimeError", "remainder by zero in test")
.with_span(expr.span),
);
}
checked_i32(file, expr, left.checked_rem(right), "remainder")
}
BinaryOp::BitAnd => Ok(Value::I32(left & right)),
BinaryOp::BitOr => Ok(Value::I32(left | right)),
BinaryOp::BitXor => Ok(Value::I32(left ^ right)),
BinaryOp::Eq => Ok(Value::Bool(left == right)),
BinaryOp::Lt => Ok(Value::Bool(left < right)),
BinaryOp::Gt => Ok(Value::Bool(left > right)),
BinaryOp::Le => Ok(Value::Bool(left <= right)),
BinaryOp::Ge => Ok(Value::Bool(left >= right)),
}
}
fn runtime_trap(file: &str, expr: &TExpr, message: &str) -> Diagnostic {
Diagnostic::new(
file,
"TestRuntimeTrap",
format!("test trapped: {}", message),
)
.with_span(expr.span)
}
fn checked_i32(
file: &str,
expr: &TExpr,
value: Option<i32>,
operation: &'static str,
) -> Result<Value, Diagnostic> {
value.map(Value::I32).ok_or_else(|| {
Diagnostic::new(
file,
"TestRuntimeError",
format!("integer overflow during test {}", operation),
)
.with_span(expr.span)
})
}
fn checked_i64(
file: &str,
expr: &TExpr,
value: Option<i64>,
operation: &'static str,
) -> Result<Value, Diagnostic> {
value.map(Value::I64).ok_or_else(|| {
Diagnostic::new(
file,
"TestRuntimeError",
format!("integer overflow during test {}", operation),
)
.with_span(expr.span)
})
}
fn checked_u32(
file: &str,
expr: &TExpr,
value: Option<u32>,
operation: &'static str,
) -> Result<Value, Diagnostic> {
value.map(Value::U32).ok_or_else(|| {
Diagnostic::new(
file,
"TestRuntimeError",
format!("integer overflow during test {}", operation),
)
.with_span(expr.span)
})
}
fn checked_u64(
file: &str,
expr: &TExpr,
value: Option<u64>,
operation: &'static str,
) -> Result<Value, Diagnostic> {
value.map(Value::U64).ok_or_else(|| {
Diagnostic::new(
file,
"TestRuntimeError",
format!("integer overflow during test {}", operation),
)
.with_span(expr.span)
})
}
fn unsupported_test_expr(file: &str, expr: &TExpr, feature: &str) -> Diagnostic {
Diagnostic::new(
file,
"UnsupportedTestExpression",
format!("test runner does not support {}", feature),
)
.with_span(expr.span)
}
fn write_test_name(name: &str, output: &mut String) {
output.push('"');
for ch in name.chars() {
output.extend(ch.escape_default());
}
output.push('"');
}