4055 lines
148 KiB
Rust
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('"');
|
|
}
|