This commit is contained in:
2026-04-05 23:05:31 +03:00
parent 75ab9bec9f
commit 7582c5aee7
28 changed files with 2439 additions and 1213 deletions

1
Cargo.lock generated
View File

@@ -315,6 +315,7 @@ dependencies = [
"rust_decimal",
"rust_decimal_macros",
"rustc-hash",
"serde",
"serde_json",
"smallvec",
"smol_str",

View File

@@ -2,6 +2,9 @@
name = "dexpr"
version = "0.1.0"
edition = "2021"
description = "Embeddable expression evaluator and bytecode VM"
license = "MIT"
exclude = ["editor/", "wasm/", "docs/", ".vscode/", "benches/", "scripts/", "CLAUDE.md", "flamegraph.svg", "profile.json.gz", "gen.js", "*.dexpr", "*.txt", "src/main.rs"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -35,6 +38,7 @@ serde_json = "1"
[dev-dependencies]
criterion = { version = "0.8.2", features = ["html_reports"] }
serde = { version = "1", features = ["derive"] }
[[bench]]
name = "my_benchmark"

View File

@@ -7,7 +7,7 @@ use rust_decimal_macros::dec;
pub fn criterion_benchmark(c: &mut Criterion) {
// 1. Parser Benchmark
c.bench_function("parser_long", |b| {
let input = include_str!("../src/bench_long.dexpr");
let input = include_str!("../examples/bench_long.dexpr");
b.iter(|| {
let _ = parser::program(input).unwrap();
})
@@ -15,7 +15,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
// 2. Compiler Benchmark
c.bench_function("compiler_long", |b| {
let input = include_str!("../src/bench_long.dexpr");
let input = include_str!("../examples/bench_long.dexpr");
let ast = parser::program(input).unwrap();
b.iter(|| {
let mut compiler = Compiler::new();
@@ -27,7 +27,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
// basic_long.dexpr benchmark
c.bench_function("vm_basic_long", |b| {
let input = include_str!("../src/basic_long.dexpr");
let input = include_str!("../examples/basic_long.dexpr");
let ast = parser::program(input).unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).unwrap();
@@ -40,7 +40,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
// Long code benchmark (using bench_long.dexpr)
c.bench_function("vm_long", |b| {
let input = include_str!("../src/bench_long.dexpr");
let input = include_str!("../examples/bench_long.dexpr");
let ast = parser::program(input).unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).unwrap();

View File

@@ -96,8 +96,9 @@ Parse ile birlikte pozisyon bilgisi de toplar ve `DebugInfo` üretir.
2. Sağ operandı register'a derle
3. Sonuç register'ı ayır
4. Uygun opcode'u emit et (Add, Sub, Mul, vs.)
5. **Özel durum:** String + String → `Concat` kullanılır
6. Operand register'ları serbest bırak
5. Operand register'ları serbest bırak
> **Not:** String birleştirme derleme zamanında ayırt edilmez. `Op::Add` her zaman `OpCodeByte::Add` emit eder; string birleştirme ve otomatik tip dönüşümü VM tarafından çalışma zamanında (runtime) ele alınır.
### UnaryOp (Tekli Operasyon)
1. Operandı register'a derle

View File

@@ -85,6 +85,25 @@ Bytecode komut setini (instruction set) tanımlar. Her opcode bir `u8` değerine
---
## Built-in Fonksiyon ID'leri
`default_fn` modülü, built-in fonksiyonlar için sabit ID'ler tanımlar. `CallExternal` opcode'u bu ID'leri kullanarak built-in fonksiyonları çağırır.
| Sabit | ID | Fonksiyon |
|-------|----|-----------|
| `ABS` | `1` | Mutlak değer |
| `MIN` | `2` | Minimum değer |
| `MAX` | `3` | Maksimum değer |
| `FLOOR` | `4` | Aşağı yuvarlama |
| `CEIL` | `5` | Yukarı yuvarlama |
| `ROUND` | `6` | Yuvarlama |
| `SQRT` | `7` | Karekök |
| `LEN` | `8` | Uzunluk |
| `TO_STRING` | `9` | String'e dönüştür |
| `TO_NUMBER` | `10` | Sayıya dönüştür |
---
## Hızlı Lookup Tablosu
`LOOKUP[256]` statik dizisi, O(1) karmaşıklıkta byte-to-opcode dönüşümü sağlar. `from_byte(u8)` metodu bu tabloyu kullanır.

View File

@@ -11,7 +11,9 @@ Register tabanlı sanal makine. Bytecode'u çalıştırır, 8 register ve global
| Dosya | İçerik |
|-------|--------|
| `vm/mod.rs` | Modül export'ları |
| `vm/vm.rs` | Ana VM implementasyonu |
| `vm/vm.rs` | Ana VM implementasyonu (core çalıştırma döngüsü) |
| `vm/methods.rs` | Metod dispatch (String, StringList, NumberList, Object metodları) |
| `vm/builtins.rs` | Built-in fonksiyon implementasyonları (abs, min, max, floor, ceil, round, sqrt, len, toString, toNumber) |
| `vm/error.rs` | Hata türleri (VMError) |
| `vm/debug_info.rs` | Bytecode offset → kaynak konum eşleştirme |
@@ -120,6 +122,7 @@ struct VM<'a> {
### Aritmetik
- **`binary_op(f, name)`** — Genel handler: iki operand register'ı oku, fonksiyonu uygula, sonucu kaydet
- Sıfıra bölme kontrolü yapılır
- **`Add` opcode:** Sayısal toplama yanında string birleştirmeyi de destekler. Otomatik tip dönüşümü (auto-coercion) yapılır: String+String, String+Number, Number+String, String+Boolean kombinasyonları birleştirme olarak çalışır
- **`handle_neg()`** — Sadece Number tipinde tekli negatif
### Karşılaştırma
@@ -133,7 +136,7 @@ struct VM<'a> {
- **`handle_jump_if_false()`** — Register `Boolean(false)` ise atla
### String, Nesne ve Metodlar
- **`handle_concat()`** — İki String register'ını birleştir
- **`handle_concat()`** — İki register'ı birleştir (karışık tip dönüşümü destekler: String, Number, Boolean otomatik olarak String'e dönüştürülür)
- **`handle_get_property()`** — Object register'ından alan oku, alan yoksa `Null` döndür
- **`handle_set_property()`** — Object register'ında alan değerini ayarla
- **`handle_method_call()`** — Nesne register'ı, metod adı, argümanlar
@@ -153,6 +156,18 @@ struct VM<'a> {
### Built-in
- **`handle_log()`** — Register değerini stdout'a yazdır
- **`rand(min, max)`** — min ile max arasında rastgele tamsayı üret (varsayılan harici fonksiyon)
- **`abs(n)`** — Mutlak değer
- **`min(a, b, ...)`** — Verilen değerlerin minimumu
- **`max(a, b, ...)`** — Verilen değerlerin maksimumu
- **`floor(n)`** — Aşağı yuvarlama
- **`ceil(n)`** — Yukarı yuvarlama
- **`round(n[, places])`** — Yuvarlama (opsiyonel ondalık basamak sayısı)
- **`sqrt(n)`** — Karekök
- **`len(v)`** — Değerin uzunluğu (String, List, Object)
- **`toString(v)`** — Değeri String'e dönüştür
- **`toNumber(v)`** — Değeri Number'a dönüştür
> **Not:** Built-in fonksiyon implementasyonları `vm/builtins.rs` dosyasında, sabit ID tanımları `src/opcodes.rs` içindeki `default_fn` modülündedir.
---

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 48 KiB

74
gen.js
View File

@@ -1,74 +0,0 @@
function randomBetween(min, max) {
min = Math.round(min);
max = Math.round(max);
return Math.floor(Math.random() * (max - min + 1) + min);
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
function generateRandomArithmeticExpression() {
const operators = ['+', '-', '*', '/']; // Array of arithmetic operators
const maxDecimalPlaces = 5;
const numOperands = randomBetween(10, 20);
const numParentheses = randomBetween(4, 8);
// Generate random numbers with two decimal places
let nums = [];
for (let i = 0; i < numOperands; i++) {
nums.push((Math.random() * 100).toFixed(randomBetween(1, maxDecimalPlaces)));
}
let res = [];
shuffleArray(nums);
let numRemainingOperands = nums.length;
let openParentheses = 0;
for (let i = 0; i < nums.length; i++) {
// Open a parenthesis if there are enough operands remaining
if (openParentheses < numParentheses && numRemainingOperands > 1 && Math.random() < 0.5) {
res.push('(');
openParentheses++;
}
res.push(nums[i]);
// Close a parenthesis if there are enough operands preceding it
if (openParentheses > 0 && numRemainingOperands > 2 && Math.random() < 0.5) {
res.push(')');
openParentheses--;
}
if (i < nums.length - 1) {
res.push(operators[randomBetween(0, 3)]);
}
numRemainingOperands--;
}
// Close any remaining open parentheses
while (openParentheses > 0) {
res.push(')');
openParentheses--;
}
return res.join('');
}
let res = '';
for (let i = 0; i < 100; i++) {
let expr = generateRandomArithmeticExpression();
let val = eval(expr);
// res += `("${expr}", "${val}"),\n`;
res += `"${expr}",\n`;
}
console.log(res);

View File

@@ -12,6 +12,12 @@ bench:
run:
cargo run --release
# --- Publish ---
# Publish to Gitea cargo registry
publish:
cargo publish --registry gitea --allow-dirty
# --- WASM ---
# Build wasm package (web target)

Binary file not shown.

View File

@@ -223,6 +223,7 @@ pub fn disassemble_bytecode(bytecode: &[u8]) -> Vec<String> {
Ok(reg) => format!("{:04x}: SetResult r{}", start_position, reg),
Err(_) => format!("{:04x}: SetResult (truncated)", start_position),
},
OpCodeByte::ClearResult => format!("{:04x}: ClearResult", start_position),
OpCodeByte::End => format!("{:04x}: End", start_position),
};

View File

@@ -170,6 +170,7 @@ impl Compiler {
let expr_reg = self.compile_expr(expr)?;
self.emit_store_global(name, expr_reg);
self.free_register(expr_reg);
self.emit_byte(OpCodeByte::ClearResult.to_byte());
Ok(())
}
@@ -229,6 +230,7 @@ impl Compiler {
// Store root back to global
self.emit_store_global(root, root_reg);
self.free_register(root_reg);
self.emit_byte(OpCodeByte::ClearResult.to_byte());
Ok(())
}
@@ -319,13 +321,7 @@ impl Compiler {
let result_reg = self.allocate_register()?;
let opcode = match op {
Op::Add => {
if self.is_string_concatenation(left, right) {
OpCodeByte::Concat
} else {
OpCodeByte::Add
}
}
Op::Add => OpCodeByte::Add,
Op::Sub => OpCodeByte::Sub,
Op::Mul => OpCodeByte::Mul,
Op::Div => OpCodeByte::Div,
@@ -359,12 +355,6 @@ impl Compiler {
Ok(result_reg)
}
/// Check if binary operation is string concatenation
fn is_string_concatenation(&self, left: &Expr, right: &Expr) -> bool {
matches!(left, Expr::Value(Value::String(_)))
|| matches!(right, Expr::Value(Value::String(_)))
}
/// Compile a unary operation
fn compile_unary_op(&mut self, op: &Op, operand: &Expr) -> Result<u8, CompileError> {
let operand_reg = self.compile_expr(operand)?;

View File

@@ -2,7 +2,7 @@ use dexpr::{ast::value::Value, compiler::Compiler, parser, vm::VM};
use rust_decimal_macros::dec;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let input = include_str!("basic_long.dexpr");
let input = include_str!("../examples/basic_long.dexpr");
let ast = parser::program(input)?;

View File

@@ -5,10 +5,31 @@ pub struct Register(pub u8);
/// Default (built-in) function IDs for CallDefault opcode
pub mod default_fn {
pub const RAND: u8 = 0;
// Future: ABS = 1, MIN = 2, MAX = 3, FLOOR = 4, CEIL = 5, ROUND = 6, ...
pub const ABS: u8 = 1;
pub const MIN: u8 = 2;
pub const MAX: u8 = 3;
pub const FLOOR: u8 = 4;
pub const CEIL: u8 = 5;
pub const ROUND: u8 = 6;
pub const SQRT: u8 = 7;
pub const LEN: u8 = 8;
pub const TO_STRING: u8 = 9;
pub const TO_NUMBER: u8 = 10;
/// Lookup table: function name ID
pub const NAMES: &[(&str, u8)] = &[("rand", RAND)];
/// Lookup table: function name <EFBFBD><EFBFBD><EFBFBD> ID
pub const NAMES: &[(&str, u8)] = &[
("rand", RAND),
("abs", ABS),
("min", MIN),
("max", MAX),
("floor", FLOOR),
("ceil", CEIL),
("round", ROUND),
("sqrt", SQRT),
("len", LEN),
("toString", TO_STRING),
("toNumber", TO_NUMBER),
];
/// Get function name by ID
pub fn name(id: u8) -> Option<&'static str> {
@@ -78,6 +99,7 @@ pub enum OpCodeByte {
// Result
SetResult = 0xB0, // Set expression result (for return value)
ClearResult = 0xB1, // Clear expression result (assignment resets last result)
// End marker
End = 0xFF, // End of program
@@ -128,6 +150,7 @@ impl OpCodeByte {
0xA1 => Some(OpCodeByte::CallExternal),
0xA2 => Some(OpCodeByte::CallDefault),
0xB0 => Some(OpCodeByte::SetResult),
0xB1 => Some(OpCodeByte::ClearResult),
0xFF => Some(OpCodeByte::End),
_ => None,
};
@@ -178,6 +201,7 @@ impl OpCodeByte {
OpCodeByte::CallExternal => "CallExternal",
OpCodeByte::CallDefault => "Rand",
OpCodeByte::SetResult => "SetResult",
OpCodeByte::ClearResult => "ClearResult",
OpCodeByte::End => "End",
}
}

View File

@@ -1,23 +0,0 @@
fib:
push rbp
movrr rbp, rsp
movsr rbp, r1
cmpsi rbp, 0
jg .L0
movri eax, 0
ret
.L0:
cmpsi rbp, 2
jg .L1
movri eax, 1
ret
.L1:
movrs r1, rbp
main:
add rsp, 16
movsi 16(byte) rbp, 10
movrs r1, rbp
call fib

229
src/vm/builtins.rs Normal file
View File

@@ -0,0 +1,229 @@
use crate::ast::value::Value;
use crate::opcodes::default_fn;
use rust_decimal::{prelude::ToPrimitive, Decimal, MathematicalOps};
use super::error::VMError;
use super::vm::VM;
impl<'a> VM<'a> {
/// Dispatch a built-in (default) function call by ID
pub(super) fn dispatch_builtin(
&mut self,
dest: usize,
fn_id: u8,
arg_regs: &[usize],
) -> Result<(), VMError> {
match fn_id {
default_fn::RAND => self.builtin_rand(dest, arg_regs),
default_fn::ABS => self.builtin_abs(dest, arg_regs),
default_fn::MIN => self.builtin_min(dest, arg_regs),
default_fn::MAX => self.builtin_max(dest, arg_regs),
default_fn::FLOOR => self.builtin_floor(dest, arg_regs),
default_fn::CEIL => self.builtin_ceil(dest, arg_regs),
default_fn::ROUND => self.builtin_round(dest, arg_regs),
default_fn::SQRT => self.builtin_sqrt(dest, arg_regs),
default_fn::LEN => self.builtin_len(dest, arg_regs),
default_fn::TO_STRING => self.builtin_to_string(dest, arg_regs),
default_fn::TO_NUMBER => self.builtin_to_number(dest, arg_regs),
_ => {
let name = default_fn::name(fn_id)
.map(|s| s.to_string())
.unwrap_or_else(|| format!("unknown({})", fn_id));
Err(VMError::RuntimeError(format!(
"Unknown default function: {}",
name
)))
}
}
}
fn builtin_rand(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
use rand::RngExt;
if arg_regs.len() < 2 {
return Err(VMError::RuntimeError(
"rand() requires two arguments (min, max)".to_string(),
));
}
match (&self.registers[arg_regs[0]], &self.registers[arg_regs[1]]) {
(Value::Number(min), Value::Number(max)) => {
let min_i64 = min.to_i64().ok_or_else(|| {
VMError::RuntimeError("rand() min must be an integer".to_string())
})?;
let max_i64 = max.to_i64().ok_or_else(|| {
VMError::RuntimeError("rand() max must be an integer".to_string())
})?;
if min_i64 > max_i64 {
return Err(VMError::RuntimeError(
"rand() min must be <= max".to_string(),
));
}
let mut rng = rand::rng();
let result = rng.random_range(min_i64..=max_i64);
self.registers[dest] = Value::Number(Decimal::from(result));
Ok(())
}
_ => Err(VMError::RuntimeError(
"rand() requires number arguments".to_string(),
)),
}
}
fn builtin_abs(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("abs", arg_regs, 1)?;
let n = extract_number("abs", &self.registers[arg_regs[0]])?;
self.registers[dest] = Value::Number(n.abs());
Ok(())
}
fn builtin_min(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
if arg_regs.is_empty() {
return Err(VMError::RuntimeError(
"min() requires at least one argument".to_string(),
));
}
let mut result = extract_number("min", &self.registers[arg_regs[0]])?;
for &reg in &arg_regs[1..] {
let n = extract_number("min", &self.registers[reg])?;
if n < result {
result = n;
}
}
self.registers[dest] = Value::Number(result);
Ok(())
}
fn builtin_max(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
if arg_regs.is_empty() {
return Err(VMError::RuntimeError(
"max() requires at least one argument".to_string(),
));
}
let mut result = extract_number("max", &self.registers[arg_regs[0]])?;
for &reg in &arg_regs[1..] {
let n = extract_number("max", &self.registers[reg])?;
if n > result {
result = n;
}
}
self.registers[dest] = Value::Number(result);
Ok(())
}
fn builtin_floor(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("floor", arg_regs, 1)?;
let n = extract_number("floor", &self.registers[arg_regs[0]])?;
self.registers[dest] = Value::Number(n.floor());
Ok(())
}
fn builtin_ceil(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("ceil", arg_regs, 1)?;
let n = extract_number("ceil", &self.registers[arg_regs[0]])?;
self.registers[dest] = Value::Number(n.ceil());
Ok(())
}
fn builtin_round(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("round", arg_regs, 1)?;
let n = extract_number("round", &self.registers[arg_regs[0]])?;
// Optional second argument: decimal places (default 0)
let places = if arg_regs.len() > 1 {
extract_number("round", &self.registers[arg_regs[1]])?
.to_u32()
.unwrap_or(0)
} else {
0
};
self.registers[dest] = Value::Number(n.round_dp(places));
Ok(())
}
fn builtin_sqrt(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("sqrt", arg_regs, 1)?;
let n = extract_number("sqrt", &self.registers[arg_regs[0]])?;
if n.is_sign_negative() {
return Err(VMError::RuntimeError(
"sqrt() argument must be non-negative".to_string(),
));
}
let result = n.sqrt().ok_or_else(|| {
VMError::RuntimeError("sqrt() failed to compute".to_string())
})?;
self.registers[dest] = Value::Number(result);
Ok(())
}
fn builtin_len(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("len", arg_regs, 1)?;
let len = match &self.registers[arg_regs[0]] {
Value::String(s) => Decimal::from(s.len()),
Value::StringList(l) => Decimal::from(l.len()),
Value::NumberList(l) => Decimal::from(l.len()),
Value::Object(m) => Decimal::from(m.len()),
other => {
return Err(VMError::RuntimeError(format!(
"len() not supported for type {}",
other.type_name()
)));
}
};
self.registers[dest] = Value::Number(len);
Ok(())
}
fn builtin_to_string(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("toString", arg_regs, 1)?;
let s = super::vm::value_to_string(&self.registers[arg_regs[0]]);
self.registers[dest] = Value::String(s.into_owned().into());
Ok(())
}
fn builtin_to_number(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("toNumber", arg_regs, 1)?;
let result = match &self.registers[arg_regs[0]] {
Value::Number(n) => *n,
Value::String(s) => s.parse::<Decimal>().map_err(|_| {
VMError::RuntimeError(format!("toNumber() cannot parse '{}'", s))
})?,
Value::Boolean(b) => {
if *b {
Decimal::from(1)
} else {
Decimal::from(0)
}
}
other => {
return Err(VMError::RuntimeError(format!(
"toNumber() not supported for type {}",
other.type_name()
)));
}
};
self.registers[dest] = Value::Number(result);
Ok(())
}
}
/// Helper: check minimum argument count
fn require_args(name: &str, arg_regs: &[usize], min: usize) -> Result<(), VMError> {
if arg_regs.len() < min {
Err(VMError::RuntimeError(format!(
"{}() requires at least {} argument(s)",
name, min
)))
} else {
Ok(())
}
}
/// Helper: extract a Decimal from a Value or return an error
fn extract_number(name: &str, val: &Value) -> Result<Decimal, VMError> {
match val {
Value::Number(n) => Ok(*n),
other => Err(VMError::RuntimeError(format!(
"{}() requires a number argument, got {}",
name,
other.type_name()
))),
}
}

608
src/vm/methods.rs Normal file
View File

@@ -0,0 +1,608 @@
use crate::ast::value::Value;
use rust_decimal::{prelude::ToPrimitive, Decimal};
use smol_str::{SmolStr, StrExt};
use super::error::VMError;
use super::vm::VM;
impl<'a> VM<'a> {
/// Dispatch a method call on a value
pub(super) fn dispatch_method(
&mut self,
dest: usize,
obj: usize,
method: &SmolStr,
args: &[Value],
) -> Result<(), VMError> {
match &self.registers[obj] {
Value::String(_) => self.dispatch_string_method(dest, obj, method, args),
Value::StringList(_) => self.dispatch_string_list_method(dest, obj, method, args),
Value::NumberList(_) => self.dispatch_number_list_method(dest, obj, method, args),
Value::Object(_) => self.dispatch_object_method(dest, obj, method, args),
_ => {
// Try external methods for any type
let obj_val = &self.registers[obj];
let type_name: SmolStr = obj_val.type_name().into();
let key = (type_name.clone(), method.clone());
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
let result = ext_method(obj_val, args).map_err(VMError::RuntimeError)?;
self.registers[dest] = result;
Ok(())
} else {
Err(VMError::MethodNotFound {
type_name: obj_val.type_name(),
method: method.clone(),
})
}
}
}
}
fn dispatch_string_method(
&mut self,
dest: usize,
obj: usize,
method: &SmolStr,
args: &[Value],
) -> Result<(), VMError> {
let s = match &self.registers[obj] {
Value::String(s) => s.clone(),
_ => unreachable!(),
};
match method.as_str() {
"upper" => {
self.registers[dest] = Value::String(s.to_uppercase_smolstr());
}
"lower" => {
self.registers[dest] = Value::String(s.to_lowercase_smolstr());
}
"trim" => {
self.registers[dest] = Value::String(SmolStr::new(s.trim()));
}
"trimStart" => {
self.registers[dest] = Value::String(SmolStr::new(s.trim_start()));
}
"trimEnd" => {
self.registers[dest] = Value::String(SmolStr::new(s.trim_end()));
}
"split" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"split() requires a delimiter argument".to_string(),
));
}
match &args[0] {
Value::String(delim) => {
let parts: Vec<SmolStr> = s.split(delim.as_str()).map(SmolStr::new).collect();
self.registers[dest] = Value::StringList(parts);
}
_ => {
return Err(VMError::RuntimeError(
"split() requires a string delimiter".to_string(),
));
}
}
}
"replace" => {
if args.len() < 2 {
return Err(VMError::RuntimeError(
"replace() requires two arguments (old, new)".to_string(),
));
}
match (&args[0], &args[1]) {
(Value::String(old), Value::String(new)) => {
let result = SmolStr::new(s.replace(old.as_str(), new.as_str()));
self.registers[dest] = Value::String(result);
}
_ => {
return Err(VMError::RuntimeError(
"replace() requires string arguments".to_string(),
));
}
}
}
"startsWith" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"startsWith() requires a prefix argument".to_string(),
));
}
match &args[0] {
Value::String(prefix) => {
self.registers[dest] = Value::Boolean(s.starts_with(prefix.as_str()));
}
_ => {
return Err(VMError::RuntimeError(
"startsWith() requires a string prefix".to_string(),
));
}
}
}
"endsWith" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"endsWith() requires a suffix argument".to_string(),
));
}
match &args[0] {
Value::String(suffix) => {
self.registers[dest] = Value::Boolean(s.ends_with(suffix.as_str()));
}
_ => {
return Err(VMError::RuntimeError(
"endsWith() requires a string suffix".to_string(),
));
}
}
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"contains() requires a substring argument".to_string(),
));
}
match &args[0] {
Value::String(substr) => {
self.registers[dest] = Value::Boolean(s.contains(substr.as_str()));
}
_ => {
return Err(VMError::RuntimeError(
"contains() requires a string substring".to_string(),
));
}
}
}
"length" => {
self.registers[dest] = Value::Number(Decimal::from(s.len()));
}
"charAt" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"charAt() requires an index argument".to_string(),
));
}
match &args[0] {
Value::Number(idx) => {
let index = idx.to_u64().unwrap_or(u64::MAX) as usize;
match s.chars().nth(index) {
Some(c) => {
self.registers[dest] = Value::String(SmolStr::new(c.to_string()));
}
None => {
self.registers[dest] = Value::Null;
}
}
}
_ => {
return Err(VMError::RuntimeError(
"charAt() requires a number index".to_string(),
));
}
}
}
"substring" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"substring() requires at least a start index".to_string(),
));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0);
let chars: Vec<char> = s.chars().collect();
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(chars.len()),
_ => chars.len(),
}
} else {
chars.len()
};
if start >= chars.len() || start >= end {
self.registers[dest] = Value::String(SmolStr::new(""));
} else {
let end = end.min(chars.len());
let result: String = chars[start..end].iter().collect();
self.registers[dest] = Value::String(SmolStr::new(result));
}
}
_ => {
return Err(VMError::RuntimeError(
"substring() requires a number start index".to_string(),
));
}
}
}
_ => {
let key = (SmolStr::new_static("String"), method.clone());
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
let obj_val = &self.registers[obj];
let result = ext_method(obj_val, args).map_err(VMError::RuntimeError)?;
self.registers[dest] = result;
} else {
return Err(VMError::MethodNotFound {
type_name: "String",
method: method.clone(),
});
}
}
}
Ok(())
}
fn dispatch_string_list_method(
&mut self,
dest: usize,
obj: usize,
method: &SmolStr,
args: &[Value],
) -> Result<(), VMError> {
let list = match &self.registers[obj] {
Value::StringList(l) => l.clone(),
_ => unreachable!(),
};
match method.as_str() {
"length" | "len" => {
self.registers[dest] = Value::Number(Decimal::from(list.len()));
}
"isEmpty" => {
self.registers[dest] = Value::Boolean(list.is_empty());
}
"first" => {
self.registers[dest] = list
.first()
.map(|s| Value::String(s.clone()))
.unwrap_or(Value::Null);
}
"last" => {
self.registers[dest] = list
.last()
.map(|s| Value::String(s.clone()))
.unwrap_or(Value::Null);
}
"get" => {
if args.is_empty() {
return Err(VMError::RuntimeError("get() requires an index".to_string()));
}
match &args[0] {
Value::Number(idx) => {
let index = idx.to_usize().unwrap_or(usize::MAX);
self.registers[dest] = list
.get(index)
.map(|s| Value::String(s.clone()))
.unwrap_or(Value::Null);
}
_ => return Err(VMError::RuntimeError("get() requires a number index".to_string())),
}
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError("contains() requires an argument".to_string()));
}
match &args[0] {
Value::String(s) => {
self.registers[dest] = Value::Boolean(list.contains(s));
}
_ => return Err(VMError::RuntimeError("contains() requires a string argument".to_string())),
}
}
"indexOf" => {
if args.is_empty() {
return Err(VMError::RuntimeError("indexOf() requires an argument".to_string()));
}
match &args[0] {
Value::String(s) => {
let idx = list.iter().position(|item| item == s);
self.registers[dest] = idx
.map(|i| Value::Number(Decimal::from(i)))
.unwrap_or(Value::Number(Decimal::from(-1)));
}
_ => return Err(VMError::RuntimeError("indexOf() requires a string argument".to_string())),
}
}
"slice" => {
if args.is_empty() {
return Err(VMError::RuntimeError("slice() requires at least a start index".to_string()));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0).min(list.len());
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(list.len()).min(list.len()),
_ => list.len(),
}
} else {
list.len()
};
self.registers[dest] = Value::StringList(list[start..end].to_vec());
}
_ => return Err(VMError::RuntimeError("slice() requires a number index".to_string())),
}
}
"reverse" => {
let mut reversed = list;
reversed.reverse();
self.registers[dest] = Value::StringList(reversed);
}
"sort" => {
let mut sorted = list;
sorted.sort();
self.registers[dest] = Value::StringList(sorted);
}
"join" => {
let delim = if args.is_empty() {
""
} else {
match &args[0] {
Value::String(s) => s.as_str(),
_ => return Err(VMError::RuntimeError("join() requires a string delimiter".to_string())),
}
};
let result: String = list.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(delim);
self.registers[dest] = Value::String(SmolStr::new(result));
}
_ => {
let key = (SmolStr::new_static("StringList"), method.clone());
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
let obj_val = &self.registers[obj];
let result = ext_method(obj_val, args).map_err(VMError::RuntimeError)?;
self.registers[dest] = result;
} else {
return Err(VMError::MethodNotFound {
type_name: "StringList",
method: method.clone(),
});
}
}
}
Ok(())
}
fn dispatch_number_list_method(
&mut self,
dest: usize,
obj: usize,
method: &SmolStr,
args: &[Value],
) -> Result<(), VMError> {
let list = match &self.registers[obj] {
Value::NumberList(l) => l.clone(),
_ => unreachable!(),
};
match method.as_str() {
"length" | "len" => {
self.registers[dest] = Value::Number(Decimal::from(list.len()));
}
"isEmpty" => {
self.registers[dest] = Value::Boolean(list.is_empty());
}
"first" => {
self.registers[dest] = list
.first()
.map(|n| Value::Number(*n))
.unwrap_or(Value::Null);
}
"last" => {
self.registers[dest] = list
.last()
.map(|n| Value::Number(*n))
.unwrap_or(Value::Null);
}
"get" => {
if args.is_empty() {
return Err(VMError::RuntimeError("get() requires an index".to_string()));
}
match &args[0] {
Value::Number(idx) => {
let index = idx.to_usize().unwrap_or(usize::MAX);
self.registers[dest] = list
.get(index)
.map(|n| Value::Number(*n))
.unwrap_or(Value::Null);
}
_ => return Err(VMError::RuntimeError("get() requires a number index".to_string())),
}
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError("contains() requires an argument".to_string()));
}
match &args[0] {
Value::Number(n) => {
self.registers[dest] = Value::Boolean(list.contains(n));
}
_ => return Err(VMError::RuntimeError("contains() requires a number argument".to_string())),
}
}
"indexOf" => {
if args.is_empty() {
return Err(VMError::RuntimeError("indexOf() requires an argument".to_string()));
}
match &args[0] {
Value::Number(n) => {
let idx = list.iter().position(|item| item == n);
self.registers[dest] = idx
.map(|i| Value::Number(Decimal::from(i)))
.unwrap_or(Value::Number(Decimal::from(-1)));
}
_ => return Err(VMError::RuntimeError("indexOf() requires a number argument".to_string())),
}
}
"slice" => {
if args.is_empty() {
return Err(VMError::RuntimeError("slice() requires at least a start index".to_string()));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0).min(list.len());
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(list.len()).min(list.len()),
_ => list.len(),
}
} else {
list.len()
};
self.registers[dest] = Value::NumberList(list[start..end].to_vec());
}
_ => return Err(VMError::RuntimeError("slice() requires a number index".to_string())),
}
}
"reverse" => {
let mut reversed = list;
reversed.reverse();
self.registers[dest] = Value::NumberList(reversed);
}
"sort" => {
let mut sorted = list;
sorted.sort();
self.registers[dest] = Value::NumberList(sorted);
}
"sum" => {
let sum: Decimal = list.iter().sum();
self.registers[dest] = Value::Number(sum);
}
"avg" => {
if list.is_empty() {
self.registers[dest] = Value::Null;
} else {
let sum: Decimal = list.iter().sum();
let avg = sum / Decimal::from(list.len());
self.registers[dest] = Value::Number(avg);
}
}
"min" => {
self.registers[dest] = list
.iter()
.min()
.map(|n| Value::Number(*n))
.unwrap_or(Value::Null);
}
"max" => {
self.registers[dest] = list
.iter()
.max()
.map(|n| Value::Number(*n))
.unwrap_or(Value::Null);
}
_ => {
let key = (SmolStr::new_static("NumberList"), method.clone());
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
let obj_val = &self.registers[obj];
let result = ext_method(obj_val, args).map_err(VMError::RuntimeError)?;
self.registers[dest] = result;
} else {
return Err(VMError::MethodNotFound {
type_name: "NumberList",
method: method.clone(),
});
}
}
}
Ok(())
}
fn dispatch_object_method(
&mut self,
dest: usize,
obj: usize,
method: &SmolStr,
args: &[Value],
) -> Result<(), VMError> {
let map = match &self.registers[obj] {
Value::Object(m) => m.clone(),
_ => unreachable!(),
};
match method.as_str() {
"keys" => {
let keys: Vec<SmolStr> = map.keys().cloned().collect();
self.registers[dest] = Value::StringList(keys);
}
"values" => {
let vals: Vec<Value> = map.values().cloned().collect();
if vals.is_empty() {
self.registers[dest] = Value::StringList(Vec::new());
} else if vals.iter().all(|v| matches!(v, Value::String(_))) {
let strings: Vec<SmolStr> = vals
.into_iter()
.map(|v| match v {
Value::String(s) => s,
_ => unreachable!(),
})
.collect();
self.registers[dest] = Value::StringList(strings);
} else if vals.iter().all(|v| matches!(v, Value::Number(_))) {
let numbers: Vec<Decimal> = vals
.into_iter()
.map(|v| match v {
Value::Number(n) => n,
_ => unreachable!(),
})
.collect();
self.registers[dest] = Value::NumberList(numbers);
} else {
return Err(VMError::RuntimeError(
"values() only works when all values are the same type (String or Number)".to_string(),
));
}
}
"length" | "len" => {
self.registers[dest] = Value::Number(Decimal::from(map.len()));
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"contains() requires a key argument".to_string(),
));
}
match &args[0] {
Value::String(key) => {
self.registers[dest] = Value::Boolean(map.contains_key(key));
}
_ => {
return Err(VMError::RuntimeError(
"contains() requires a string key".to_string(),
))
}
}
}
"get" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"get() requires a key argument".to_string(),
));
}
match &args[0] {
Value::String(key) => {
self.registers[dest] = map.get(key).cloned().unwrap_or(Value::Null);
}
_ => {
return Err(VMError::RuntimeError(
"get() requires a string key".to_string(),
))
}
}
}
_ => {
let key = (SmolStr::new_static("Object"), method.clone());
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
let obj_val = &self.registers[obj];
let result = ext_method(obj_val, args).map_err(VMError::RuntimeError)?;
self.registers[dest] = result;
} else {
return Err(VMError::MethodNotFound {
type_name: "Object",
method: method.clone(),
});
}
}
}
Ok(())
}
}

View File

@@ -1,5 +1,7 @@
mod builtins;
mod debug_info;
pub mod error;
mod methods;
mod vm;
pub use debug_info::DebugInfo;

View File

@@ -1,9 +1,9 @@
use crate::{ast::value::Value, bytecode::BytecodeReader, opcodes::OpCodeByte};
use bumpalo::Bump;
use micromap::Map;
use rust_decimal::{prelude::ToPrimitive, Decimal, MathematicalOps};
use rust_decimal::{Decimal, MathematicalOps};
use rustc_hash::FxHashMap;
use smol_str::{SmolStr, StrExt};
use smol_str::SmolStr;
/// Type alias for external (host) functions
pub type ExternalFn = Box<dyn Fn(&[Value]) -> Result<Value, String>>;
@@ -33,36 +33,27 @@ macro_rules! log_debug {
/// Virtual Machine for executing dExpr bytecode
pub struct VM<'a> {
bytecode: &'a [u8], // Bytecode to execute
reader: BytecodeReader<'a>, // Bytecode reader
pc: usize, // Program counter
bytecode: &'a [u8],
pub(super) reader: BytecodeReader<'a>,
pc: usize,
// Registers for computation
registers: [Value; MAX_REGISTERS],
pub(super) registers: [Value; MAX_REGISTERS],
// Global variables
globals: Map<SmolStr, Value, 64>,
// Last expression result (returned by execute)
last_result: Value,
// External (host) functions — lazily allocated
external_functions: Option<FxHashMap<SmolStr, ExternalFn>>,
pub(super) external_functions: Option<FxHashMap<SmolStr, ExternalFn>>,
// External (host) methods per type — lazily allocated
external_methods: Option<FxHashMap<(SmolStr, SmolStr), ExternalMethod>>,
pub(super) external_methods: Option<FxHashMap<(SmolStr, SmolStr), ExternalMethod>>,
// Heap for complex data types
heap: Bump,
// Debug info for error messages
debug_info: Option<&'a DebugInfo>,
// Debug flag
#[cfg(debug_assertions)]
debug: bool,
pub(super) debug: bool,
// Profiling counts
#[cfg(debug_assertions)]
opcode_counts: [usize; 256],
}
@@ -195,7 +186,7 @@ impl<'a> VM<'a> {
OpCodeByte::StoreLocal => self.handle_store_local(),
OpCodeByte::LoadGlobal => self.handle_load_global(),
OpCodeByte::StoreGlobal => self.handle_store_global(),
OpCodeByte::Add => self.binary_op(|a, b| Ok(a + b), "add"),
OpCodeByte::Add => self.handle_add(),
OpCodeByte::Sub => self.binary_op(|a, b| Ok(a - b), "subtract"),
OpCodeByte::Mul => self.binary_op(|a, b| Ok(a * b), "multiply"),
OpCodeByte::Div => self.binary_op(
@@ -240,6 +231,11 @@ impl<'a> VM<'a> {
OpCodeByte::CallExternal => self.handle_call_external(),
OpCodeByte::CallDefault => self.handle_call_default(),
OpCodeByte::SetResult => self.handle_set_result(),
OpCodeByte::ClearResult => {
self.last_result = Value::Null;
log_debug!(self, "ClearResult");
Ok(())
}
OpCodeByte::End => {
log_debug!(self, "End of program");
#[cfg(debug_assertions)]
@@ -464,6 +460,46 @@ impl<'a> VM<'a> {
Ok(())
}
// ============================================================================
// Opcode Handlers - Add (Number + String coercion)
// ============================================================================
/// Handle Add opcode - number addition or string concatenation with auto-coercion
#[inline]
fn handle_add(&mut self) -> Result<(), VMError> {
let dest = self.read_register_checked()?;
let a = self.read_register_checked()?;
let b = self.read_register_checked()?;
match (&self.registers[a], &self.registers[b]) {
(Value::Number(a_num), Value::Number(b_num)) => {
self.registers[dest] = Value::Number(*a_num + *b_num);
}
(Value::String(a_str), Value::String(b_str)) => {
let result = format!("{}{}", a_str, b_str);
self.registers[dest] = Value::String(result.into());
}
(Value::String(a_str), other) => {
let result = format!("{}{}", a_str, value_to_string(other));
self.registers[dest] = Value::String(result.into());
}
(other, Value::String(b_str)) => {
let result = format!("{}{}", value_to_string(other), b_str);
self.registers[dest] = Value::String(result.into());
}
(a_val, b_val) => {
return Err(VMError::InvalidOperation {
operation: "add",
left_type: a_val.type_name(),
right_type: b_val.type_name(),
});
}
}
log_debug!(self, "Add r{} = r{} + r{}", dest, a, b);
Ok(())
}
// ============================================================================
// Opcode Handlers - Boolean Operations
// ============================================================================
@@ -614,26 +650,19 @@ impl<'a> VM<'a> {
// Opcode Handlers - String Operations
// ============================================================================
/// Handle Concat opcode - string concatenation
/// Handle Concat opcode - string concatenation with auto-coercion
#[inline]
fn handle_concat(&mut self) -> Result<(), VMError> {
let dest = self.read_register_checked()?;
let a = self.read_register_checked()?;
let b = self.read_register_checked()?;
match (&self.registers[a], &self.registers[b]) {
(Value::String(a_str), Value::String(b_str)) => {
let result = format!("{}{}", a_str, b_str);
let result = format!(
"{}{}",
value_to_string(&self.registers[a]),
value_to_string(&self.registers[b])
);
self.registers[dest] = Value::String(result.into());
}
(a_val, b_val) => {
return Err(VMError::InvalidOperation {
operation: "concat",
left_type: a_val.type_name(),
right_type: b_val.type_name(),
});
}
}
log_debug!(self, "Concat r{} = r{} + r{}", dest, a, b);
Ok(())
@@ -703,524 +732,7 @@ impl<'a> VM<'a> {
args.push(self.registers[reg].clone());
}
// Dispatch method call
match &self.registers[obj] {
Value::String(s) => match method.as_str() {
"upper" => {
let result = s.to_uppercase_smolstr();
self.registers[dest] = Value::String(result);
}
"lower" => {
let result = s.to_lowercase_smolstr();
self.registers[dest] = Value::String(result);
}
"trim" => {
let result = SmolStr::new(s.trim());
self.registers[dest] = Value::String(result);
}
"trimStart" => {
let result = SmolStr::new(s.trim_start());
self.registers[dest] = Value::String(result);
}
"trimEnd" => {
let result = SmolStr::new(s.trim_end());
self.registers[dest] = Value::String(result);
}
"split" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"split() requires a delimiter argument".to_string(),
));
}
match &args[0] {
Value::String(delim) => {
let parts: Vec<SmolStr> = s.split(delim.as_str()).map(SmolStr::new).collect();
self.registers[dest] = Value::StringList(parts);
}
_ => {
return Err(VMError::RuntimeError(
"split() requires a string delimiter".to_string(),
));
}
}
}
"replace" => {
if args.len() < 2 {
return Err(VMError::RuntimeError(
"replace() requires two arguments (old, new)".to_string(),
));
}
match (&args[0], &args[1]) {
(Value::String(old), Value::String(new)) => {
let result = SmolStr::new(s.replace(old.as_str(), new.as_str()));
self.registers[dest] = Value::String(result);
}
_ => {
return Err(VMError::RuntimeError(
"replace() requires string arguments".to_string(),
));
}
}
}
"startsWith" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"startsWith() requires a prefix argument".to_string(),
));
}
match &args[0] {
Value::String(prefix) => {
let result = s.starts_with(prefix.as_str());
self.registers[dest] = Value::Boolean(result);
}
_ => {
return Err(VMError::RuntimeError(
"startsWith() requires a string prefix".to_string(),
));
}
}
}
"endsWith" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"endsWith() requires a suffix argument".to_string(),
));
}
match &args[0] {
Value::String(suffix) => {
let result = s.ends_with(suffix.as_str());
self.registers[dest] = Value::Boolean(result);
}
_ => {
return Err(VMError::RuntimeError(
"endsWith() requires a string suffix".to_string(),
));
}
}
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"contains() requires a substring argument".to_string(),
));
}
match &args[0] {
Value::String(substr) => {
let result = s.contains(substr.as_str());
self.registers[dest] = Value::Boolean(result);
}
_ => {
return Err(VMError::RuntimeError(
"contains() requires a string substring".to_string(),
));
}
}
}
"length" => {
let len = Decimal::from(s.len());
self.registers[dest] = Value::Number(len);
}
"charAt" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"charAt() requires an index argument".to_string(),
));
}
match &args[0] {
Value::Number(idx) => {
let index = idx.to_u64().unwrap_or(u64::MAX) as usize;
match s.chars().nth(index) {
Some(c) => {
self.registers[dest] = Value::String(SmolStr::new(c.to_string()));
}
None => {
self.registers[dest] = Value::Null;
}
}
}
_ => {
return Err(VMError::RuntimeError(
"charAt() requires a number index".to_string(),
));
}
}
}
"substring" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"substring() requires at least a start index".to_string(),
));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0);
let chars: Vec<char> = s.chars().collect();
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(chars.len()),
_ => chars.len(),
}
} else {
chars.len()
};
if start >= chars.len() || start >= end {
self.registers[dest] = Value::String(SmolStr::new(""));
} else {
let end = end.min(chars.len());
let result: String = chars[start..end].iter().collect();
self.registers[dest] = Value::String(SmolStr::new(result));
}
}
_ => {
return Err(VMError::RuntimeError(
"substring() requires a number start index".to_string(),
));
}
}
}
_ => {
let key = (SmolStr::new_static("String"), method.clone());
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
let obj_val = &self.registers[obj];
let result = ext_method(obj_val, &args).map_err(VMError::RuntimeError)?;
self.registers[dest] = result;
} else {
return Err(VMError::MethodNotFound {
type_name: "String",
method,
});
}
}
},
Value::StringList(list) => match method.as_str() {
"length" | "len" => {
self.registers[dest] = Value::Number(Decimal::from(list.len()));
}
"isEmpty" => {
self.registers[dest] = Value::Boolean(list.is_empty());
}
"first" => {
self.registers[dest] = list
.first()
.map(|s| Value::String(s.clone()))
.unwrap_or(Value::Null);
}
"last" => {
self.registers[dest] = list
.last()
.map(|s| Value::String(s.clone()))
.unwrap_or(Value::Null);
}
"get" => {
if args.is_empty() {
return Err(VMError::RuntimeError("get() requires an index".to_string()));
}
match &args[0] {
Value::Number(idx) => {
let index = idx.to_usize().unwrap_or(usize::MAX);
self.registers[dest] = list
.get(index)
.map(|s| Value::String(s.clone()))
.unwrap_or(Value::Null);
}
_ => return Err(VMError::RuntimeError("get() requires a number index".to_string())),
}
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError("contains() requires an argument".to_string()));
}
match &args[0] {
Value::String(s) => {
self.registers[dest] = Value::Boolean(list.contains(s));
}
_ => return Err(VMError::RuntimeError("contains() requires a string argument".to_string())),
}
}
"indexOf" => {
if args.is_empty() {
return Err(VMError::RuntimeError("indexOf() requires an argument".to_string()));
}
match &args[0] {
Value::String(s) => {
let idx = list.iter().position(|item| item == s);
self.registers[dest] = idx
.map(|i| Value::Number(Decimal::from(i)))
.unwrap_or(Value::Number(Decimal::from(-1)));
}
_ => return Err(VMError::RuntimeError("indexOf() requires a string argument".to_string())),
}
}
"slice" => {
if args.is_empty() {
return Err(VMError::RuntimeError("slice() requires at least a start index".to_string()));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0).min(list.len());
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(list.len()).min(list.len()),
_ => list.len(),
}
} else {
list.len()
};
self.registers[dest] = Value::StringList(list[start..end].to_vec());
}
_ => return Err(VMError::RuntimeError("slice() requires a number index".to_string())),
}
}
"reverse" => {
let mut reversed = list.clone();
reversed.reverse();
self.registers[dest] = Value::StringList(reversed);
}
"sort" => {
let mut sorted = list.clone();
sorted.sort();
self.registers[dest] = Value::StringList(sorted);
}
"join" => {
let delim = if args.is_empty() {
""
} else {
match &args[0] {
Value::String(s) => s.as_str(),
_ => return Err(VMError::RuntimeError("join() requires a string delimiter".to_string())),
}
};
let result: String = list.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(delim);
self.registers[dest] = Value::String(SmolStr::new(result));
}
_ => {
let key = (SmolStr::new_static("StringList"), method.clone());
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
let obj_val = &self.registers[obj];
let result = ext_method(obj_val, &args).map_err(VMError::RuntimeError)?;
self.registers[dest] = result;
} else {
return Err(VMError::MethodNotFound {
type_name: "StringList",
method,
});
}
}
},
Value::NumberList(list) => match method.as_str() {
"length" | "len" => {
self.registers[dest] = Value::Number(Decimal::from(list.len()));
}
"isEmpty" => {
self.registers[dest] = Value::Boolean(list.is_empty());
}
"first" => {
self.registers[dest] = list
.first()
.map(|n| Value::Number(*n))
.unwrap_or(Value::Null);
}
"last" => {
self.registers[dest] = list
.last()
.map(|n| Value::Number(*n))
.unwrap_or(Value::Null);
}
"get" => {
if args.is_empty() {
return Err(VMError::RuntimeError("get() requires an index".to_string()));
}
match &args[0] {
Value::Number(idx) => {
let index = idx.to_usize().unwrap_or(usize::MAX);
self.registers[dest] = list
.get(index)
.map(|n| Value::Number(*n))
.unwrap_or(Value::Null);
}
_ => return Err(VMError::RuntimeError("get() requires a number index".to_string())),
}
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError("contains() requires an argument".to_string()));
}
match &args[0] {
Value::Number(n) => {
self.registers[dest] = Value::Boolean(list.contains(n));
}
_ => return Err(VMError::RuntimeError("contains() requires a number argument".to_string())),
}
}
"indexOf" => {
if args.is_empty() {
return Err(VMError::RuntimeError("indexOf() requires an argument".to_string()));
}
match &args[0] {
Value::Number(n) => {
let idx = list.iter().position(|item| item == n);
self.registers[dest] = idx
.map(|i| Value::Number(Decimal::from(i)))
.unwrap_or(Value::Number(Decimal::from(-1)));
}
_ => return Err(VMError::RuntimeError("indexOf() requires a number argument".to_string())),
}
}
"slice" => {
if args.is_empty() {
return Err(VMError::RuntimeError("slice() requires at least a start index".to_string()));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0).min(list.len());
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(list.len()).min(list.len()),
_ => list.len(),
}
} else {
list.len()
};
self.registers[dest] = Value::NumberList(list[start..end].to_vec());
}
_ => return Err(VMError::RuntimeError("slice() requires a number index".to_string())),
}
}
"reverse" => {
let mut reversed = list.clone();
reversed.reverse();
self.registers[dest] = Value::NumberList(reversed);
}
"sort" => {
let mut sorted = list.clone();
sorted.sort();
self.registers[dest] = Value::NumberList(sorted);
}
"sum" => {
let sum: Decimal = list.iter().sum();
self.registers[dest] = Value::Number(sum);
}
"avg" => {
if list.is_empty() {
self.registers[dest] = Value::Null;
} else {
let sum: Decimal = list.iter().sum();
let avg = sum / Decimal::from(list.len());
self.registers[dest] = Value::Number(avg);
}
}
"min" => {
self.registers[dest] = list
.iter()
.min()
.map(|n| Value::Number(*n))
.unwrap_or(Value::Null);
}
"max" => {
self.registers[dest] = list
.iter()
.max()
.map(|n| Value::Number(*n))
.unwrap_or(Value::Null);
}
_ => {
let key = (SmolStr::new_static("NumberList"), method.clone());
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
let obj_val = &self.registers[obj];
let result = ext_method(obj_val, &args).map_err(VMError::RuntimeError)?;
self.registers[dest] = result;
} else {
return Err(VMError::MethodNotFound {
type_name: "NumberList",
method,
});
}
}
},
Value::Object(map) => match method.as_str() {
"keys" => {
let keys: Vec<SmolStr> = map.keys().cloned().collect();
self.registers[dest] = Value::StringList(keys);
}
"values" => {
// Returns a StringList if all values are strings, NumberList if all numbers, otherwise error
let vals: Vec<Value> = map.values().cloned().collect();
if vals.is_empty() {
self.registers[dest] = Value::StringList(Vec::new());
} else if vals.iter().all(|v| matches!(v, Value::String(_))) {
let strings: Vec<SmolStr> = vals.into_iter().map(|v| match v {
Value::String(s) => s,
_ => unreachable!(),
}).collect();
self.registers[dest] = Value::StringList(strings);
} else if vals.iter().all(|v| matches!(v, Value::Number(_))) {
let numbers: Vec<Decimal> = vals.into_iter().map(|v| match v {
Value::Number(n) => n,
_ => unreachable!(),
}).collect();
self.registers[dest] = Value::NumberList(numbers);
} else {
return Err(VMError::RuntimeError(
"values() only works when all values are the same type (String or Number)".to_string(),
));
}
}
"length" | "len" => {
self.registers[dest] = Value::Number(Decimal::from(map.len()));
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError("contains() requires a key argument".to_string()));
}
match &args[0] {
Value::String(key) => {
self.registers[dest] = Value::Boolean(map.contains_key(key));
}
_ => return Err(VMError::RuntimeError("contains() requires a string key".to_string())),
}
}
"get" => {
if args.is_empty() {
return Err(VMError::RuntimeError("get() requires a key argument".to_string()));
}
match &args[0] {
Value::String(key) => {
self.registers[dest] = map.get(key).cloned().unwrap_or(Value::Null);
}
_ => return Err(VMError::RuntimeError("get() requires a string key".to_string())),
}
}
_ => {
let key = (SmolStr::new_static("Object"), method.clone());
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
let obj_val = &self.registers[obj];
let result = ext_method(obj_val, &args).map_err(VMError::RuntimeError)?;
self.registers[dest] = result;
} else {
return Err(VMError::MethodNotFound {
type_name: "Object",
method,
});
}
}
},
_ => {
// Try external methods for any type
let obj_val = &self.registers[obj];
let type_name: SmolStr = obj_val.type_name().into();
let key = (type_name.clone(), method.clone());
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
let result = ext_method(obj_val, &args).map_err(VMError::RuntimeError)?;
self.registers[dest] = result;
} else {
return Err(VMError::MethodNotFound {
type_name: obj_val.type_name(),
method,
});
}
}
}
self.dispatch_method(dest, obj, &method, &args)?;
log_debug!(self, "MethodCall r{} = r{}.{}(...)", dest, obj, method);
Ok(())
@@ -1232,8 +744,6 @@ impl<'a> VM<'a> {
/// Handle CallDefault opcode - call a default (built-in) function by ID
fn handle_call_default(&mut self) -> Result<(), VMError> {
use crate::opcodes::default_fn;
let dest = self.read_register_checked()?;
let fn_id = self.reader.read_byte().map_err(VMError::BytecodeError)?;
let arg_count = self.reader.read_byte().map_err(VMError::BytecodeError)? as usize;
@@ -1243,37 +753,7 @@ impl<'a> VM<'a> {
arg_regs.push(self.read_register_checked()?);
}
match fn_id {
default_fn::RAND => {
use rand::RngExt;
if arg_regs.len() < 2 {
return Err(VMError::RuntimeError("rand() requires two arguments (min, max)".to_string()));
}
match (&self.registers[arg_regs[0]], &self.registers[arg_regs[1]]) {
(Value::Number(min), Value::Number(max)) => {
let min_i64 = min.to_i64().ok_or_else(|| {
VMError::RuntimeError("rand() min must be an integer".to_string())
})?;
let max_i64 = max.to_i64().ok_or_else(|| {
VMError::RuntimeError("rand() max must be an integer".to_string())
})?;
if min_i64 > max_i64 {
return Err(VMError::RuntimeError("rand() min must be <= max".to_string()));
}
let mut rng = rand::rng();
let result = rng.random_range(min_i64..=max_i64);
self.registers[dest] = Value::Number(Decimal::from(result));
}
_ => return Err(VMError::RuntimeError("rand() requires number arguments".to_string())),
}
}
_ => {
let name = default_fn::name(fn_id)
.map(|s| s.to_string())
.unwrap_or_else(|| format!("unknown({})", fn_id));
return Err(VMError::RuntimeError(format!("Unknown default function: {}", name)));
}
}
self.dispatch_builtin(dest, fn_id, &arg_regs)?;
log_debug!(self, "CallDefault r{} = fn#{}(...)", dest, fn_id);
Ok(())
@@ -1415,3 +895,14 @@ impl<'a> VM<'a> {
Ok(())
}
}
/// Convert a Value to its string representation for concatenation (no quotes around strings)
pub(super) fn value_to_string(val: &Value) -> std::borrow::Cow<'_, str> {
match val {
Value::String(s) => std::borrow::Cow::Borrowed(s.as_str()),
Value::Number(n) => std::borrow::Cow::Owned(n.to_string()),
Value::Boolean(b) => std::borrow::Cow::Borrowed(if *b { "true" } else { "false" }),
Value::Null => std::borrow::Cow::Borrowed("null"),
other => std::borrow::Cow::Owned(format!("{}", other)),
}
}

143
tests/data_driven_tests.rs Normal file
View File

@@ -0,0 +1,143 @@
use dexpr::{ast::value::Value, compiler::Compiler, parser, vm::VM};
use indexmap::IndexMap;
use rust_decimal::Decimal;
use smol_str::SmolStr;
use std::collections::HashMap;
use std::str::FromStr;
#[derive(serde::Deserialize)]
struct TestCase {
name: String,
code: String,
#[serde(default)]
globals: HashMap<String, ValueDef>,
expected: ValueDef,
}
#[derive(serde::Deserialize)]
struct ValueDef {
#[serde(rename = "type")]
typ: String,
#[serde(default)]
value: Option<serde_json::Value>,
}
fn value_def_to_value(def: &ValueDef) -> Value {
match def.typ.as_str() {
"null" => Value::Null,
"number" => {
let s = def.value.as_ref().unwrap().as_str().unwrap();
Value::Number(Decimal::from_str(s).unwrap())
}
"string" => {
let s = def.value.as_ref().unwrap().as_str().unwrap();
Value::String(s.into())
}
"boolean" => {
let b = def.value.as_ref().unwrap().as_bool().unwrap();
Value::Boolean(b)
}
"object" => {
fn json_obj_to_value(obj: &serde_json::Map<String, serde_json::Value>) -> Value {
let mut map = IndexMap::new();
for (k, v) in obj {
let val = match v {
serde_json::Value::String(s) => {
if let Ok(d) = Decimal::from_str(s) {
Value::Number(d)
} else {
Value::String(SmolStr::from(s.as_str()))
}
}
serde_json::Value::Bool(b) => Value::Boolean(*b),
serde_json::Value::Object(nested) => json_obj_to_value(nested),
_ => Value::String(SmolStr::from(v.to_string())),
};
map.insert(SmolStr::from(k.as_str()), val);
}
Value::Object(map)
}
let obj = def.value.as_ref().unwrap().as_object().unwrap();
json_obj_to_value(obj)
}
other => panic!("Unknown type: {other}"),
}
}
#[test]
fn test_all_cases() {
let json = include_str!("test_cases.json");
let cases: Vec<TestCase> =
serde_json::from_str(json).expect("Failed to parse test_cases.json");
let mut failures = Vec::new();
let total = cases.len();
for case in &cases {
let ast = match parser::program(&case.code) {
Ok(ast) => ast,
Err(e) => {
failures.push(format!(
"FAIL: {}\n code: {}\n error: parse failed: {e}",
case.name,
case.code.replace('\n', "\\n"),
));
continue;
}
};
let mut compiler = Compiler::new();
let bytecode = match compiler.compile(ast) {
Ok(bc) => bc,
Err(e) => {
failures.push(format!(
"FAIL: {}\n code: {}\n error: compile failed: {e}",
case.name,
case.code.replace('\n', "\\n"),
));
continue;
}
};
let mut vm = VM::new(&bytecode);
for (name, def) in &case.globals {
vm.set_global(name, value_def_to_value(def));
}
let result = match vm.execute() {
Ok(v) => v,
Err(e) => {
failures.push(format!(
"FAIL: {}\n code: {}\n error: execute failed: {e}",
case.name,
case.code.replace('\n', "\\n"),
));
continue;
}
};
let expected = value_def_to_value(&case.expected);
if result != expected {
failures.push(format!(
"FAIL: {}\n code: {}\n expected: {:?}\n got: {:?}",
case.name,
case.code.replace('\n', "\\n"),
expected,
result
));
}
}
if !failures.is_empty() {
panic!(
"\n{} / {} test cases failed:\n\n{}\n",
failures.len(),
total,
failures.join("\n\n")
);
}
eprintln!("All {total} test cases passed.");
}

View File

@@ -959,27 +959,19 @@ result = x % y"#;
}
#[test]
fn test_error_location_type_mismatch() {
fn test_string_number_auto_coercion() {
// string + number now auto-coerces to string concatenation
let code = r#"x = "hello"
y = 5
result = x + y"#;
x + y"#;
let ast = parser::program(code).unwrap();
let mut compiler = Compiler::new();
let (bytecode, debug_info) = compiler
.compile_from_source(code)
.expect("Failed to compile");
let bytecode = compiler.compile(ast).unwrap();
let mut vm = VM::new(&bytecode);
vm.set_debug_info(&debug_info);
let result = vm.execute().unwrap();
let err = vm.execute().unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("line 3"),
"Error should contain line 3, got: {}",
err_msg
);
assert_eq!(result, Value::String("hello5".into()));
}
#[test]

1288
tests/test_cases.json Normal file

File diff suppressed because it is too large Load Diff