This commit is contained in:
2026-08-18 15:16:14 +03:00
parent f4d089e5e5
commit ce46aad145
21 changed files with 1161 additions and 514 deletions
+5 -2
View File
@@ -1,6 +1,6 @@
use crate::ast::value::Value;
use crate::opcodes::default_fn;
use rust_decimal::{prelude::ToPrimitive, Decimal, MathematicalOps};
use rust_decimal::{prelude::ToPrimitive, Decimal, MathematicalOps, RoundingStrategy};
use super::error::VMError;
use super::vm::VM;
@@ -134,7 +134,10 @@ impl<'a> VM<'a> {
} else {
0
};
self.registers[dest] = Value::Number(n.round_dp(places));
// Half away from zero (2.5 → 3, -2.5 → -3), not banker's rounding.
self.registers[dest] = Value::Number(
n.round_dp_with_strategy(places, RoundingStrategy::MidpointAwayFromZero),
);
Ok(())
}
+32 -1
View File
@@ -135,7 +135,7 @@ impl<'a> VM<'a> {
)),
}
}
"length" => Ok(Value::Number(Decimal::from(s.len()))),
"length" => Ok(Value::Number(Decimal::from(s.chars().count()))),
"charAt" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
@@ -733,6 +733,37 @@ impl<'a> VM<'a> {
_ => Err(VMError::RuntimeError("sort() requires a string field name".to_string())),
}
}
// Numeric aggregates: a List holding only Numbers behaves like a
// NumberList (an empty projection like `items.amount` yields an empty
// List, so `items.amount.sum()` must still work).
"sum" | "avg" | "min" | "max" => {
let mut nums: Vec<Decimal> = Vec::with_capacity(list.len());
for item in list.iter() {
match item {
Value::Number(n) => nums.push(*n),
other => {
return Err(VMError::RuntimeError(format!(
"{}() requires a list of numbers, found {}",
method,
other.type_name()
)));
}
}
}
match method {
"sum" => Ok(Value::Number(nums.iter().sum())),
"avg" => {
if nums.is_empty() {
Ok(Value::Null)
} else {
let sum: Decimal = nums.iter().sum();
Ok(Value::Number(sum / Decimal::from(nums.len())))
}
}
"min" => Ok(nums.iter().min().map(|n| Value::Number(*n)).unwrap_or(Value::Null)),
_ => Ok(nums.iter().max().map(|n| Value::Number(*n)).unwrap_or(Value::Null)),
}
}
_ => {
let key = (SmolStr::new_static("List"), SmolStr::from(method));
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
+43
View File
@@ -221,6 +221,7 @@ impl<'a> VM<'a> {
OpCodeByte::Not => self.handle_not(),
OpCodeByte::Jump => self.handle_jump(),
OpCodeByte::JumpIfFalse => self.handle_jump_if_false(),
OpCodeByte::JumpIfTrue => self.handle_jump_if_true(),
OpCodeByte::Concat => self.handle_concat(),
OpCodeByte::GetProperty => self.handle_get_property(),
OpCodeByte::SetProperty => self.handle_set_property(),
@@ -654,6 +655,32 @@ impl<'a> VM<'a> {
Ok(())
}
/// Handle JumpIfTrue opcode - conditional jump (used for `||` short-circuit)
#[inline]
fn handle_jump_if_true(&mut self) -> Result<(), VMError> {
let cond_reg = self.read_register_checked()?;
let addr = self.read_jump_address()?;
match &self.registers[cond_reg] {
Value::Boolean(condition) => {
if *condition {
self.set_position(addr)?;
log_debug!(self, "JumpIfTrue to {} (condition=true)", addr);
} else {
log_debug!(self, "JumpIfTrue not taken (condition=false)");
}
}
v => {
return Err(VMError::TypeMismatch {
expected: "Boolean".to_string(),
got: v.type_name().to_string(),
});
}
}
Ok(())
}
// ============================================================================
// Opcode Handlers - String Operations
// ============================================================================
@@ -687,6 +714,22 @@ impl<'a> VM<'a> {
let value = map.get(prop).cloned().unwrap_or(Value::Null);
self.registers[dest] = value;
}
// `.length` as a property (sugar for `.length()`) on strings and lists.
Value::String(s) if prop == "length" => {
self.registers[dest] = Value::Number(Decimal::from(s.chars().count()));
}
Value::NumberList(list) if prop == "length" => {
self.registers[dest] = Value::Number(Decimal::from(list.len()));
}
Value::StringList(list) if prop == "length" => {
self.registers[dest] = Value::Number(Decimal::from(list.len()));
}
Value::List(list)
if prop == "length" && !matches!(list.first(), Some(Value::Object(_))) =>
{
// Only when this can't be a projection of an Object field named "length"
self.registers[dest] = Value::Number(Decimal::from(list.len()));
}
Value::List(list) => {
// Property projection: list.field → extract field from each Object element
let mut values: Vec<Value> = Vec::with_capacity(list.len());