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 = OnceLock::new(); static NEXT_TEST_FILE_HANDLE: AtomicI32 = AtomicI32::new(1); static TEST_FILE_HANDLES: OnceLock>> = OnceLock::new(); fn test_file_handles() -> &'static Mutex> { 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, pub report: Option, } #[derive(Clone)] pub struct TestReport { pub total_discovered: usize, pub selected: usize, pub passed: usize, pub failed: usize, pub skipped: usize, pub filter: Option, } impl TestRunFailure { pub fn before_execution(diagnostics: Vec) -> Self { Self { diagnostics, report: None, } } } pub fn run( _file: &str, program: &CheckedProgram, filter: Option<&str>, ) -> Result { let functions = program .functions .iter() .map(|function| (function.name.as_str(), function)) .collect::>(); let foreign_imports = program .c_imports .iter() .map(|import| import.name.as_str()) .collect::>(); 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), VecI64(Vec), VecF64(Vec), VecBool(Vec), VecString(Vec), 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, }, Struct { name: String, fields: HashMap, }, Unit, } #[derive(Debug, Clone, PartialEq)] enum ArrayValue { Values { element_ty: Type, values: Vec, }, } 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 { 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, }, } impl EnumPayloadValue { fn from_value(value: Value) -> Option { 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 { match self { Self::I32(value) => Some(*value), _ => None, } } fn as_i64(&self) -> Option { match self { Self::I64(value) => Some(*value), _ => None, } } fn as_u32(&self) -> Option { match self { Self::U32(value) => Some(*value), _ => None, } } fn as_u64(&self) -> Option { match self { Self::U64(value) => Some(*value), _ => None, } } fn as_f64(&self) -> Option { match self { Self::F64(value) => Some(*value), _ => None, } } fn as_bool(&self) -> Option { 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 { 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 { 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 { 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 { 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 { let bytes = text.as_bytes(); if !is_ascii_decimal_f64(bytes) { return None; } text.parse::().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, functions: &HashMap<&str, &CheckedFunction>, foreign_imports: &HashSet<&str>, depth: usize, ) -> Result { 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::>(); 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 ` 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, functions: &HashMap<&str, &CheckedFunction>, foreign_imports: &HashSet<&str>, depth: usize, ) -> Result { 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::>(); 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, functions: &HashMap<&str, &CheckedFunction>, foreign_imports: &HashSet<&str>, depth: usize, ) -> Result { 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, functions: &HashMap<&str, &CheckedFunction>, foreign_imports: &HashSet<&str>, depth: usize, ) -> Result { eval_body( file, &function.body, locals, functions, foreign_imports, depth, ) } fn eval_binary( file: &str, expr: &TExpr, op: BinaryOp, left: Value, right: Value, ) -> Result { 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, operation: &'static str, ) -> Result { 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, operation: &'static str, ) -> Result { 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, operation: &'static str, ) -> Result { 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, operation: &'static str, ) -> Result { 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('"'); }