mirror of
https://github.com/duhanbalci/dexpr.git
synced 2026-08-30 16:01:41 +00:00
0.4.1
This commit is contained in:
+243
-235
@@ -1,235 +1,243 @@
|
||||
use crate::bytecode::BytecodeReader;
|
||||
use crate::opcodes::OpCodeByte;
|
||||
|
||||
/// A utility function to disassemble bytecode for debugging
|
||||
pub fn disassemble_bytecode(bytecode: &[u8]) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
let mut reader = BytecodeReader::new(bytecode);
|
||||
|
||||
while reader.remaining() > 0 {
|
||||
let start_position = reader.position();
|
||||
let opcode_byte = match reader.read_byte() {
|
||||
Ok(b) => b,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
let opcode = match OpCodeByte::from_byte(opcode_byte) {
|
||||
Some(op) => op,
|
||||
None => {
|
||||
result.push(format!(
|
||||
"{:04x}: Unknown opcode: 0x{:02x}",
|
||||
start_position, opcode_byte
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let instruction = match opcode {
|
||||
OpCodeByte::LoadConst => {
|
||||
let reg = reader.read_byte();
|
||||
let value = reader.read_value();
|
||||
match (reg, value) {
|
||||
(Ok(r), Ok(v)) => format!("{:04x}: LoadConst r{}, {}", start_position, r, v),
|
||||
_ => format!("{:04x}: LoadConst (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::Move => {
|
||||
let dest = reader.read_byte();
|
||||
let src = reader.read_byte();
|
||||
match (dest, src) {
|
||||
(Ok(d), Ok(s)) => format!("{:04x}: Move r{} = r{}", start_position, d, s),
|
||||
_ => format!("{:04x}: Move (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::LoadLocal => {
|
||||
let reg = reader.read_byte();
|
||||
let offset = reader.read_byte();
|
||||
match (reg, offset) {
|
||||
(Ok(r), Ok(o)) => format!("{:04x}: LoadLocal r{}, offset={}", start_position, r, o),
|
||||
_ => format!("{:04x}: LoadLocal (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::StoreLocal => {
|
||||
let offset = reader.read_byte();
|
||||
let reg = reader.read_byte();
|
||||
match (offset, reg) {
|
||||
(Ok(o), Ok(r)) => format!("{:04x}: StoreLocal offset={}, r{}", start_position, o, r),
|
||||
_ => format!("{:04x}: StoreLocal (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::LoadGlobal => {
|
||||
let reg = reader.read_byte();
|
||||
let name = reader.read_string();
|
||||
match (reg, name) {
|
||||
(Ok(r), Ok(n)) => format!("{:04x}: LoadGlobal r{}, \"{}\"", start_position, r, n),
|
||||
_ => format!("{:04x}: LoadGlobal (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::StoreGlobal => {
|
||||
let name = reader.read_string();
|
||||
let reg = reader.read_byte();
|
||||
match (name, reg) {
|
||||
(Ok(n), Ok(r)) => format!("{:04x}: StoreGlobal \"{}\", r{}", start_position, n, r),
|
||||
_ => format!("{:04x}: StoreGlobal (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::Add
|
||||
| OpCodeByte::Sub
|
||||
| OpCodeByte::Mul
|
||||
| OpCodeByte::Div
|
||||
| OpCodeByte::Mod
|
||||
| OpCodeByte::Pow
|
||||
| OpCodeByte::Lt
|
||||
| OpCodeByte::Lte
|
||||
| OpCodeByte::Gt
|
||||
| OpCodeByte::Gte
|
||||
| OpCodeByte::Eq
|
||||
| OpCodeByte::Neq
|
||||
| OpCodeByte::And
|
||||
| OpCodeByte::Or
|
||||
| OpCodeByte::Contains
|
||||
| OpCodeByte::Concat => {
|
||||
let res = reader.read_byte();
|
||||
let left = reader.read_byte();
|
||||
let right = reader.read_byte();
|
||||
match (res, left, right) {
|
||||
(Ok(r), Ok(l), Ok(rg)) => {
|
||||
format!("{:04x}: {:?} r{}, r{}, r{}", start_position, opcode, r, l, rg)
|
||||
}
|
||||
_ => format!("{:04x}: {:?} (truncated)", start_position, opcode),
|
||||
}
|
||||
}
|
||||
OpCodeByte::Neg | OpCodeByte::Not => {
|
||||
let res = reader.read_byte();
|
||||
let operand = reader.read_byte();
|
||||
match (res, operand) {
|
||||
(Ok(r), Ok(o)) => format!("{:04x}: {:?} r{}, r{}", start_position, opcode, r, o),
|
||||
_ => format!("{:04x}: {:?} (truncated)", start_position, opcode),
|
||||
}
|
||||
}
|
||||
OpCodeByte::Jump => match reader.read_u32() {
|
||||
Ok(addr) => format!("{:04x}: Jump -> 0x{:04x}", start_position, addr),
|
||||
Err(_) => format!("{:04x}: Jump (truncated)", start_position),
|
||||
},
|
||||
OpCodeByte::JumpIfFalse => {
|
||||
let reg = reader.read_byte();
|
||||
let addr = reader.read_u32();
|
||||
match (reg, addr) {
|
||||
(Ok(r), Ok(a)) => format!("{:04x}: JumpIfFalse r{} -> 0x{:04x}", start_position, r, a),
|
||||
_ => format!("{:04x}: JumpIfFalse (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::MethodCall => {
|
||||
let res = reader.read_byte();
|
||||
let obj = reader.read_byte();
|
||||
let method = reader.read_string();
|
||||
let arg_count = reader.read_byte();
|
||||
match (res, obj, method, arg_count) {
|
||||
(Ok(r), Ok(o), Ok(m), Ok(count)) => {
|
||||
let mut arg_regs = Vec::new();
|
||||
let mut truncated = false;
|
||||
for _ in 0..count {
|
||||
match reader.read_byte() {
|
||||
Ok(reg) => arg_regs.push(format!("r{}", reg)),
|
||||
Err(_) => {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if truncated {
|
||||
format!(
|
||||
"{:04x}: MethodCall r{} = r{}.{}(truncated args)",
|
||||
start_position, r, o, m
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{:04x}: MethodCall r{} = r{}.{}({})",
|
||||
start_position,
|
||||
r,
|
||||
o,
|
||||
m,
|
||||
arg_regs.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => format!("{:04x}: MethodCall (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::Log => match reader.read_byte() {
|
||||
Ok(reg) => format!("{:04x}: Log r{}", start_position, reg),
|
||||
Err(_) => format!("{:04x}: Log (truncated)", start_position),
|
||||
},
|
||||
OpCodeByte::CallDefault => {
|
||||
let res = reader.read_byte();
|
||||
let fn_id = reader.read_byte();
|
||||
let arg_count = reader.read_byte();
|
||||
match (res, fn_id, arg_count) {
|
||||
(Ok(r), Ok(id), Ok(count)) => {
|
||||
let fn_name = crate::opcodes::default_fn::name(id).unwrap_or("?");
|
||||
let mut arg_regs = Vec::new();
|
||||
for _ in 0..count {
|
||||
if let Ok(reg) = reader.read_byte() {
|
||||
arg_regs.push(format!("r{}", reg));
|
||||
}
|
||||
}
|
||||
format!(
|
||||
"{:04x}: CallDefault r{} = {}({})",
|
||||
start_position, r, fn_name, arg_regs.join(", ")
|
||||
)
|
||||
}
|
||||
_ => format!("{:04x}: CallDefault (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::CallExternal => {
|
||||
let res = reader.read_byte();
|
||||
let name = reader.read_string();
|
||||
let arg_count = reader.read_byte();
|
||||
match (res, name, arg_count) {
|
||||
(Ok(r), Ok(n), Ok(count)) => {
|
||||
let mut arg_regs = Vec::new();
|
||||
for _ in 0..count {
|
||||
if let Ok(reg) = reader.read_byte() {
|
||||
arg_regs.push(format!("r{}", reg));
|
||||
}
|
||||
}
|
||||
format!(
|
||||
"{:04x}: CallExternal r{} = {}({})",
|
||||
start_position, r, n, arg_regs.join(", ")
|
||||
)
|
||||
}
|
||||
_ => format!("{:04x}: CallExternal (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::GetProperty => {
|
||||
let dest = reader.read_byte();
|
||||
let obj = reader.read_byte();
|
||||
let prop = reader.read_string();
|
||||
match (dest, obj, prop) {
|
||||
(Ok(d), Ok(o), Ok(p)) => format!("{:04x}: GetProperty r{} = r{}.{}", start_position, d, o, p),
|
||||
_ => format!("{:04x}: GetProperty (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::SetProperty => {
|
||||
let obj = reader.read_byte();
|
||||
let prop = reader.read_string();
|
||||
let val = reader.read_byte();
|
||||
match (obj, prop, val) {
|
||||
(Ok(o), Ok(p), Ok(v)) => format!("{:04x}: SetProperty r{}.{} = r{}", start_position, o, p, v),
|
||||
_ => format!("{:04x}: SetProperty (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::SetResult => match reader.read_byte() {
|
||||
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),
|
||||
};
|
||||
|
||||
result.push(instruction);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
use crate::bytecode::BytecodeReader;
|
||||
use crate::opcodes::OpCodeByte;
|
||||
|
||||
/// A utility function to disassemble bytecode for debugging
|
||||
pub fn disassemble_bytecode(bytecode: &[u8]) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
let mut reader = BytecodeReader::new(bytecode);
|
||||
|
||||
while reader.remaining() > 0 {
|
||||
let start_position = reader.position();
|
||||
let opcode_byte = match reader.read_byte() {
|
||||
Ok(b) => b,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
let opcode = match OpCodeByte::from_byte(opcode_byte) {
|
||||
Some(op) => op,
|
||||
None => {
|
||||
result.push(format!(
|
||||
"{:04x}: Unknown opcode: 0x{:02x}",
|
||||
start_position, opcode_byte
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let instruction = match opcode {
|
||||
OpCodeByte::LoadConst => {
|
||||
let reg = reader.read_byte();
|
||||
let value = reader.read_value();
|
||||
match (reg, value) {
|
||||
(Ok(r), Ok(v)) => format!("{:04x}: LoadConst r{}, {}", start_position, r, v),
|
||||
_ => format!("{:04x}: LoadConst (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::Move => {
|
||||
let dest = reader.read_byte();
|
||||
let src = reader.read_byte();
|
||||
match (dest, src) {
|
||||
(Ok(d), Ok(s)) => format!("{:04x}: Move r{} = r{}", start_position, d, s),
|
||||
_ => format!("{:04x}: Move (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::LoadLocal => {
|
||||
let reg = reader.read_byte();
|
||||
let offset = reader.read_byte();
|
||||
match (reg, offset) {
|
||||
(Ok(r), Ok(o)) => format!("{:04x}: LoadLocal r{}, offset={}", start_position, r, o),
|
||||
_ => format!("{:04x}: LoadLocal (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::StoreLocal => {
|
||||
let offset = reader.read_byte();
|
||||
let reg = reader.read_byte();
|
||||
match (offset, reg) {
|
||||
(Ok(o), Ok(r)) => format!("{:04x}: StoreLocal offset={}, r{}", start_position, o, r),
|
||||
_ => format!("{:04x}: StoreLocal (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::LoadGlobal => {
|
||||
let reg = reader.read_byte();
|
||||
let name = reader.read_string();
|
||||
match (reg, name) {
|
||||
(Ok(r), Ok(n)) => format!("{:04x}: LoadGlobal r{}, \"{}\"", start_position, r, n),
|
||||
_ => format!("{:04x}: LoadGlobal (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::StoreGlobal => {
|
||||
let name = reader.read_string();
|
||||
let reg = reader.read_byte();
|
||||
match (name, reg) {
|
||||
(Ok(n), Ok(r)) => format!("{:04x}: StoreGlobal \"{}\", r{}", start_position, n, r),
|
||||
_ => format!("{:04x}: StoreGlobal (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::Add
|
||||
| OpCodeByte::Sub
|
||||
| OpCodeByte::Mul
|
||||
| OpCodeByte::Div
|
||||
| OpCodeByte::Mod
|
||||
| OpCodeByte::Pow
|
||||
| OpCodeByte::Lt
|
||||
| OpCodeByte::Lte
|
||||
| OpCodeByte::Gt
|
||||
| OpCodeByte::Gte
|
||||
| OpCodeByte::Eq
|
||||
| OpCodeByte::Neq
|
||||
| OpCodeByte::And
|
||||
| OpCodeByte::Or
|
||||
| OpCodeByte::Contains
|
||||
| OpCodeByte::Concat => {
|
||||
let res = reader.read_byte();
|
||||
let left = reader.read_byte();
|
||||
let right = reader.read_byte();
|
||||
match (res, left, right) {
|
||||
(Ok(r), Ok(l), Ok(rg)) => {
|
||||
format!("{:04x}: {:?} r{}, r{}, r{}", start_position, opcode, r, l, rg)
|
||||
}
|
||||
_ => format!("{:04x}: {:?} (truncated)", start_position, opcode),
|
||||
}
|
||||
}
|
||||
OpCodeByte::Neg | OpCodeByte::Not => {
|
||||
let res = reader.read_byte();
|
||||
let operand = reader.read_byte();
|
||||
match (res, operand) {
|
||||
(Ok(r), Ok(o)) => format!("{:04x}: {:?} r{}, r{}", start_position, opcode, r, o),
|
||||
_ => format!("{:04x}: {:?} (truncated)", start_position, opcode),
|
||||
}
|
||||
}
|
||||
OpCodeByte::Jump => match reader.read_u32() {
|
||||
Ok(addr) => format!("{:04x}: Jump -> 0x{:04x}", start_position, addr),
|
||||
Err(_) => format!("{:04x}: Jump (truncated)", start_position),
|
||||
},
|
||||
OpCodeByte::JumpIfFalse => {
|
||||
let reg = reader.read_byte();
|
||||
let addr = reader.read_u32();
|
||||
match (reg, addr) {
|
||||
(Ok(r), Ok(a)) => format!("{:04x}: JumpIfFalse r{} -> 0x{:04x}", start_position, r, a),
|
||||
_ => format!("{:04x}: JumpIfFalse (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::JumpIfTrue => {
|
||||
let reg = reader.read_byte();
|
||||
let addr = reader.read_u32();
|
||||
match (reg, addr) {
|
||||
(Ok(r), Ok(a)) => format!("{:04x}: JumpIfTrue r{} -> 0x{:04x}", start_position, r, a),
|
||||
_ => format!("{:04x}: JumpIfTrue (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::MethodCall => {
|
||||
let res = reader.read_byte();
|
||||
let obj = reader.read_byte();
|
||||
let method = reader.read_string();
|
||||
let arg_count = reader.read_byte();
|
||||
match (res, obj, method, arg_count) {
|
||||
(Ok(r), Ok(o), Ok(m), Ok(count)) => {
|
||||
let mut arg_regs = Vec::new();
|
||||
let mut truncated = false;
|
||||
for _ in 0..count {
|
||||
match reader.read_byte() {
|
||||
Ok(reg) => arg_regs.push(format!("r{}", reg)),
|
||||
Err(_) => {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if truncated {
|
||||
format!(
|
||||
"{:04x}: MethodCall r{} = r{}.{}(truncated args)",
|
||||
start_position, r, o, m
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{:04x}: MethodCall r{} = r{}.{}({})",
|
||||
start_position,
|
||||
r,
|
||||
o,
|
||||
m,
|
||||
arg_regs.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => format!("{:04x}: MethodCall (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::Log => match reader.read_byte() {
|
||||
Ok(reg) => format!("{:04x}: Log r{}", start_position, reg),
|
||||
Err(_) => format!("{:04x}: Log (truncated)", start_position),
|
||||
},
|
||||
OpCodeByte::CallDefault => {
|
||||
let res = reader.read_byte();
|
||||
let fn_id = reader.read_byte();
|
||||
let arg_count = reader.read_byte();
|
||||
match (res, fn_id, arg_count) {
|
||||
(Ok(r), Ok(id), Ok(count)) => {
|
||||
let fn_name = crate::opcodes::default_fn::name(id).unwrap_or("?");
|
||||
let mut arg_regs = Vec::new();
|
||||
for _ in 0..count {
|
||||
if let Ok(reg) = reader.read_byte() {
|
||||
arg_regs.push(format!("r{}", reg));
|
||||
}
|
||||
}
|
||||
format!(
|
||||
"{:04x}: CallDefault r{} = {}({})",
|
||||
start_position, r, fn_name, arg_regs.join(", ")
|
||||
)
|
||||
}
|
||||
_ => format!("{:04x}: CallDefault (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::CallExternal => {
|
||||
let res = reader.read_byte();
|
||||
let name = reader.read_string();
|
||||
let arg_count = reader.read_byte();
|
||||
match (res, name, arg_count) {
|
||||
(Ok(r), Ok(n), Ok(count)) => {
|
||||
let mut arg_regs = Vec::new();
|
||||
for _ in 0..count {
|
||||
if let Ok(reg) = reader.read_byte() {
|
||||
arg_regs.push(format!("r{}", reg));
|
||||
}
|
||||
}
|
||||
format!(
|
||||
"{:04x}: CallExternal r{} = {}({})",
|
||||
start_position, r, n, arg_regs.join(", ")
|
||||
)
|
||||
}
|
||||
_ => format!("{:04x}: CallExternal (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::GetProperty => {
|
||||
let dest = reader.read_byte();
|
||||
let obj = reader.read_byte();
|
||||
let prop = reader.read_string();
|
||||
match (dest, obj, prop) {
|
||||
(Ok(d), Ok(o), Ok(p)) => format!("{:04x}: GetProperty r{} = r{}.{}", start_position, d, o, p),
|
||||
_ => format!("{:04x}: GetProperty (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::SetProperty => {
|
||||
let obj = reader.read_byte();
|
||||
let prop = reader.read_string();
|
||||
let val = reader.read_byte();
|
||||
match (obj, prop, val) {
|
||||
(Ok(o), Ok(p), Ok(v)) => format!("{:04x}: SetProperty r{}.{} = r{}", start_position, o, p, v),
|
||||
_ => format!("{:04x}: SetProperty (truncated)", start_position),
|
||||
}
|
||||
}
|
||||
OpCodeByte::SetResult => match reader.read_byte() {
|
||||
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),
|
||||
};
|
||||
|
||||
result.push(instruction);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
@@ -316,6 +316,10 @@ impl Compiler {
|
||||
op: &Op,
|
||||
right: &Expr,
|
||||
) -> Result<u8, CompileError> {
|
||||
if matches!(op, Op::And | Op::Or) {
|
||||
return self.compile_logical_op(left, op, right);
|
||||
}
|
||||
|
||||
let left_reg = self.compile_expr(left)?;
|
||||
let right_reg = self.compile_expr(right)?;
|
||||
let result_reg = self.allocate_register()?;
|
||||
@@ -355,6 +359,49 @@ impl Compiler {
|
||||
Ok(result_reg)
|
||||
}
|
||||
|
||||
/// Compile `&&` / `||` with short-circuit evaluation.
|
||||
///
|
||||
/// Layout (`&&`):
|
||||
/// ```text
|
||||
/// <left> -> rL
|
||||
/// JumpIfFalse rL, END ; left false → result is rL (false), skip right
|
||||
/// <right> -> rR
|
||||
/// Move rL, rR ; (omitted when rR == rL)
|
||||
/// END:
|
||||
/// ```
|
||||
/// `||` is identical with `JumpIfTrue`. The result always lives in `rL`.
|
||||
/// The left register is released before compiling the right side because its
|
||||
/// value is only needed on the jump path, where nothing else executes; this
|
||||
/// lets the right side usually land in the same register and skip the Move.
|
||||
fn compile_logical_op(&mut self, left: &Expr, op: &Op, right: &Expr) -> Result<u8, CompileError> {
|
||||
let left_reg = self.compile_expr(left)?;
|
||||
let end_label = self.create_label();
|
||||
|
||||
let jump_op = if matches!(op, Op::And) {
|
||||
OpCodeByte::JumpIfFalse
|
||||
} else {
|
||||
OpCodeByte::JumpIfTrue
|
||||
};
|
||||
self.emit_byte(jump_op.to_byte());
|
||||
self.emit_byte(left_reg);
|
||||
self.emit_jump_address(end_label);
|
||||
|
||||
self.free_register(left_reg);
|
||||
let right_reg = self.compile_expr(right)?;
|
||||
if right_reg != left_reg {
|
||||
self.emit_byte(OpCodeByte::Move.to_byte());
|
||||
self.emit_byte(left_reg);
|
||||
self.emit_byte(right_reg);
|
||||
self.free_register(right_reg);
|
||||
}
|
||||
// Re-claim left_reg as the result register (it may have been reused/freed
|
||||
// while compiling the right side).
|
||||
self.used_registers[left_reg as usize] = true;
|
||||
|
||||
self.set_label(end_label);
|
||||
Ok(left_reg)
|
||||
}
|
||||
|
||||
/// Compile a unary operation
|
||||
fn compile_unary_op(&mut self, op: &Op, operand: &Expr) -> Result<u8, CompileError> {
|
||||
let operand_reg = self.compile_expr(operand)?;
|
||||
|
||||
@@ -305,6 +305,10 @@ fn builtin_methods() -> Vec<(&'static str, Vec<MethodInfo>)> {
|
||||
MethodInfo { name: "filter", signature: "(field: String, value?: any) -> List", doc: Some("Filter by field value or truthy field") },
|
||||
MethodInfo { name: "find", signature: "(field: String, value?: any) -> any", doc: Some("Find first element matching field condition") },
|
||||
MethodInfo { name: "sort", signature: "(field: String) -> List", doc: Some("Sort by field value") },
|
||||
MethodInfo { name: "sum", signature: "() -> Number", doc: Some("Sum of numeric elements (0 when empty)") },
|
||||
MethodInfo { name: "avg", signature: "() -> Number", doc: Some("Average of numeric elements") },
|
||||
MethodInfo { name: "min", signature: "() -> Number", doc: Some("Smallest numeric element") },
|
||||
MethodInfo { name: "max", signature: "() -> Number", doc: Some("Largest numeric element") },
|
||||
]),
|
||||
("StringList", vec![
|
||||
MethodInfo { name: "length", signature: "() -> Number", doc: None },
|
||||
|
||||
+211
-208
@@ -1,208 +1,211 @@
|
||||
/// Register identifier
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Register(pub u8);
|
||||
|
||||
/// Default (built-in) function IDs for CallDefault opcode
|
||||
pub mod default_fn {
|
||||
pub const RAND: u8 = 0;
|
||||
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),
|
||||
("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> {
|
||||
NAMES.iter().find(|(_, i)| *i == id).map(|(n, _)| *n)
|
||||
}
|
||||
|
||||
/// Get function ID by name
|
||||
pub fn id(name: &str) -> Option<u8> {
|
||||
NAMES.iter().find(|(n, _)| *n == name).map(|(_, i)| *i)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytecode opcodes
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OpCodeByte {
|
||||
// Register operations
|
||||
LoadConst = 0x10, // Load constant to register
|
||||
Move = 0x11, // Move value between registers
|
||||
|
||||
// Memory operations
|
||||
LoadLocal = 0x20, // Load local variable to register
|
||||
StoreLocal = 0x21, // Store register to local variable
|
||||
LoadGlobal = 0x22, // Load global variable to register
|
||||
StoreGlobal = 0x23, // Store register to global variable
|
||||
|
||||
// Arithmetic operations
|
||||
Add = 0x30, // Addition
|
||||
Sub = 0x31, // Subtraction
|
||||
Mul = 0x32, // Multiplication
|
||||
Div = 0x33, // Division
|
||||
Neg = 0x34, // Negation
|
||||
Mod = 0x35, // Modulo
|
||||
Pow = 0x36, // Power
|
||||
|
||||
// Comparison operations
|
||||
Lt = 0x40, // Less than
|
||||
Lte = 0x41, // Less than or equal
|
||||
Gt = 0x42, // Greater than
|
||||
Gte = 0x43, // Greater than or equal
|
||||
Eq = 0x44, // Equal
|
||||
Neq = 0x45, // Not equal
|
||||
|
||||
// Boolean operations
|
||||
And = 0x50, // Logical AND
|
||||
Or = 0x51, // Logical OR
|
||||
Not = 0x52, // Logical NOT
|
||||
|
||||
// Membership test
|
||||
Contains = 0x53, // Check if value is in list/string
|
||||
|
||||
// Control flow
|
||||
Jump = 0x60, // Should read 4-byte address
|
||||
JumpIfFalse = 0x61, // Should read register + 4-byte address
|
||||
|
||||
// String operations
|
||||
Concat = 0x80, // String concatenation
|
||||
|
||||
// Property & method calls
|
||||
GetProperty = 0x91, // Get object property: dest, obj, name
|
||||
SetProperty = 0x92, // Set object property: obj, name, value
|
||||
MethodCall = 0x90, // Call method on object
|
||||
|
||||
// Built-in functions
|
||||
Log = 0xA0, // Print a value
|
||||
CallExternal = 0xA1, // Call external (host) function
|
||||
CallDefault = 0xA2, // Call default (built-in) function by ID
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
impl OpCodeByte {
|
||||
/// Convert opcode to byte
|
||||
pub fn to_byte(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
|
||||
/// Static lookup table for fast byte to opcode conversion
|
||||
const LOOKUP: [Option<OpCodeByte>; 256] = {
|
||||
let mut table = [None; 256];
|
||||
let mut i = 0;
|
||||
while i < 256 {
|
||||
table[i] = match i as u8 {
|
||||
0x10 => Some(OpCodeByte::LoadConst),
|
||||
0x11 => Some(OpCodeByte::Move),
|
||||
0x20 => Some(OpCodeByte::LoadLocal),
|
||||
0x21 => Some(OpCodeByte::StoreLocal),
|
||||
0x22 => Some(OpCodeByte::LoadGlobal),
|
||||
0x23 => Some(OpCodeByte::StoreGlobal),
|
||||
0x30 => Some(OpCodeByte::Add),
|
||||
0x31 => Some(OpCodeByte::Sub),
|
||||
0x32 => Some(OpCodeByte::Mul),
|
||||
0x33 => Some(OpCodeByte::Div),
|
||||
0x34 => Some(OpCodeByte::Neg),
|
||||
0x35 => Some(OpCodeByte::Mod),
|
||||
0x36 => Some(OpCodeByte::Pow),
|
||||
0x40 => Some(OpCodeByte::Lt),
|
||||
0x41 => Some(OpCodeByte::Lte),
|
||||
0x42 => Some(OpCodeByte::Gt),
|
||||
0x43 => Some(OpCodeByte::Gte),
|
||||
0x44 => Some(OpCodeByte::Eq),
|
||||
0x45 => Some(OpCodeByte::Neq),
|
||||
0x50 => Some(OpCodeByte::And),
|
||||
0x51 => Some(OpCodeByte::Or),
|
||||
0x52 => Some(OpCodeByte::Not),
|
||||
0x53 => Some(OpCodeByte::Contains),
|
||||
0x60 => Some(OpCodeByte::Jump),
|
||||
0x61 => Some(OpCodeByte::JumpIfFalse),
|
||||
0x80 => Some(OpCodeByte::Concat),
|
||||
0x90 => Some(OpCodeByte::MethodCall),
|
||||
0x91 => Some(OpCodeByte::GetProperty),
|
||||
0x92 => Some(OpCodeByte::SetProperty),
|
||||
0xA0 => Some(OpCodeByte::Log),
|
||||
0xA1 => Some(OpCodeByte::CallExternal),
|
||||
0xA2 => Some(OpCodeByte::CallDefault),
|
||||
0xB0 => Some(OpCodeByte::SetResult),
|
||||
0xB1 => Some(OpCodeByte::ClearResult),
|
||||
0xFF => Some(OpCodeByte::End),
|
||||
_ => None,
|
||||
};
|
||||
i += 1;
|
||||
}
|
||||
table
|
||||
};
|
||||
|
||||
/// Convert byte to opcode
|
||||
#[inline(always)]
|
||||
pub fn from_byte(byte: u8) -> Option<Self> {
|
||||
Self::LOOKUP[byte as usize]
|
||||
}
|
||||
|
||||
/// Get opcode name
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
OpCodeByte::LoadConst => "LoadConst",
|
||||
OpCodeByte::Move => "Move",
|
||||
OpCodeByte::LoadLocal => "LoadLocal",
|
||||
OpCodeByte::StoreLocal => "StoreLocal",
|
||||
OpCodeByte::LoadGlobal => "LoadGlobal",
|
||||
OpCodeByte::StoreGlobal => "StoreGlobal",
|
||||
OpCodeByte::Add => "Add",
|
||||
OpCodeByte::Sub => "Sub",
|
||||
OpCodeByte::Mul => "Mul",
|
||||
OpCodeByte::Div => "Div",
|
||||
OpCodeByte::Neg => "Neg",
|
||||
OpCodeByte::Mod => "Mod",
|
||||
OpCodeByte::Pow => "Pow",
|
||||
OpCodeByte::Lt => "Lt",
|
||||
OpCodeByte::Lte => "Lte",
|
||||
OpCodeByte::Gt => "Gt",
|
||||
OpCodeByte::Gte => "Gte",
|
||||
OpCodeByte::Eq => "Eq",
|
||||
OpCodeByte::Neq => "Neq",
|
||||
OpCodeByte::And => "And",
|
||||
OpCodeByte::Or => "Or",
|
||||
OpCodeByte::Not => "Not",
|
||||
OpCodeByte::Contains => "Contains",
|
||||
OpCodeByte::Jump => "Jump",
|
||||
OpCodeByte::JumpIfFalse => "JumpIfFalse",
|
||||
OpCodeByte::Concat => "Concat",
|
||||
OpCodeByte::MethodCall => "MethodCall",
|
||||
OpCodeByte::GetProperty => "GetProperty",
|
||||
OpCodeByte::SetProperty => "SetProperty",
|
||||
OpCodeByte::Log => "Log",
|
||||
OpCodeByte::CallExternal => "CallExternal",
|
||||
OpCodeByte::CallDefault => "Rand",
|
||||
OpCodeByte::SetResult => "SetResult",
|
||||
OpCodeByte::ClearResult => "ClearResult",
|
||||
OpCodeByte::End => "End",
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Register identifier
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Register(pub u8);
|
||||
|
||||
/// Default (built-in) function IDs for CallDefault opcode
|
||||
pub mod default_fn {
|
||||
pub const RAND: u8 = 0;
|
||||
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),
|
||||
("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> {
|
||||
NAMES.iter().find(|(_, i)| *i == id).map(|(n, _)| *n)
|
||||
}
|
||||
|
||||
/// Get function ID by name
|
||||
pub fn id(name: &str) -> Option<u8> {
|
||||
NAMES.iter().find(|(n, _)| *n == name).map(|(_, i)| *i)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytecode opcodes
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OpCodeByte {
|
||||
// Register operations
|
||||
LoadConst = 0x10, // Load constant to register
|
||||
Move = 0x11, // Move value between registers
|
||||
|
||||
// Memory operations
|
||||
LoadLocal = 0x20, // Load local variable to register
|
||||
StoreLocal = 0x21, // Store register to local variable
|
||||
LoadGlobal = 0x22, // Load global variable to register
|
||||
StoreGlobal = 0x23, // Store register to global variable
|
||||
|
||||
// Arithmetic operations
|
||||
Add = 0x30, // Addition
|
||||
Sub = 0x31, // Subtraction
|
||||
Mul = 0x32, // Multiplication
|
||||
Div = 0x33, // Division
|
||||
Neg = 0x34, // Negation
|
||||
Mod = 0x35, // Modulo
|
||||
Pow = 0x36, // Power
|
||||
|
||||
// Comparison operations
|
||||
Lt = 0x40, // Less than
|
||||
Lte = 0x41, // Less than or equal
|
||||
Gt = 0x42, // Greater than
|
||||
Gte = 0x43, // Greater than or equal
|
||||
Eq = 0x44, // Equal
|
||||
Neq = 0x45, // Not equal
|
||||
|
||||
// Boolean operations
|
||||
And = 0x50, // Logical AND
|
||||
Or = 0x51, // Logical OR
|
||||
Not = 0x52, // Logical NOT
|
||||
|
||||
// Membership test
|
||||
Contains = 0x53, // Check if value is in list/string
|
||||
|
||||
// Control flow
|
||||
Jump = 0x60, // Should read 4-byte address
|
||||
JumpIfFalse = 0x61, // Should read register + 4-byte address
|
||||
JumpIfTrue = 0x62, // Should read register + 4-byte address
|
||||
|
||||
// String operations
|
||||
Concat = 0x80, // String concatenation
|
||||
|
||||
// Property & method calls
|
||||
GetProperty = 0x91, // Get object property: dest, obj, name
|
||||
SetProperty = 0x92, // Set object property: obj, name, value
|
||||
MethodCall = 0x90, // Call method on object
|
||||
|
||||
// Built-in functions
|
||||
Log = 0xA0, // Print a value
|
||||
CallExternal = 0xA1, // Call external (host) function
|
||||
CallDefault = 0xA2, // Call default (built-in) function by ID
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
impl OpCodeByte {
|
||||
/// Convert opcode to byte
|
||||
pub fn to_byte(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
|
||||
/// Static lookup table for fast byte to opcode conversion
|
||||
const LOOKUP: [Option<OpCodeByte>; 256] = {
|
||||
let mut table = [None; 256];
|
||||
let mut i = 0;
|
||||
while i < 256 {
|
||||
table[i] = match i as u8 {
|
||||
0x10 => Some(OpCodeByte::LoadConst),
|
||||
0x11 => Some(OpCodeByte::Move),
|
||||
0x20 => Some(OpCodeByte::LoadLocal),
|
||||
0x21 => Some(OpCodeByte::StoreLocal),
|
||||
0x22 => Some(OpCodeByte::LoadGlobal),
|
||||
0x23 => Some(OpCodeByte::StoreGlobal),
|
||||
0x30 => Some(OpCodeByte::Add),
|
||||
0x31 => Some(OpCodeByte::Sub),
|
||||
0x32 => Some(OpCodeByte::Mul),
|
||||
0x33 => Some(OpCodeByte::Div),
|
||||
0x34 => Some(OpCodeByte::Neg),
|
||||
0x35 => Some(OpCodeByte::Mod),
|
||||
0x36 => Some(OpCodeByte::Pow),
|
||||
0x40 => Some(OpCodeByte::Lt),
|
||||
0x41 => Some(OpCodeByte::Lte),
|
||||
0x42 => Some(OpCodeByte::Gt),
|
||||
0x43 => Some(OpCodeByte::Gte),
|
||||
0x44 => Some(OpCodeByte::Eq),
|
||||
0x45 => Some(OpCodeByte::Neq),
|
||||
0x50 => Some(OpCodeByte::And),
|
||||
0x51 => Some(OpCodeByte::Or),
|
||||
0x52 => Some(OpCodeByte::Not),
|
||||
0x53 => Some(OpCodeByte::Contains),
|
||||
0x60 => Some(OpCodeByte::Jump),
|
||||
0x61 => Some(OpCodeByte::JumpIfFalse),
|
||||
0x62 => Some(OpCodeByte::JumpIfTrue),
|
||||
0x80 => Some(OpCodeByte::Concat),
|
||||
0x90 => Some(OpCodeByte::MethodCall),
|
||||
0x91 => Some(OpCodeByte::GetProperty),
|
||||
0x92 => Some(OpCodeByte::SetProperty),
|
||||
0xA0 => Some(OpCodeByte::Log),
|
||||
0xA1 => Some(OpCodeByte::CallExternal),
|
||||
0xA2 => Some(OpCodeByte::CallDefault),
|
||||
0xB0 => Some(OpCodeByte::SetResult),
|
||||
0xB1 => Some(OpCodeByte::ClearResult),
|
||||
0xFF => Some(OpCodeByte::End),
|
||||
_ => None,
|
||||
};
|
||||
i += 1;
|
||||
}
|
||||
table
|
||||
};
|
||||
|
||||
/// Convert byte to opcode
|
||||
#[inline(always)]
|
||||
pub fn from_byte(byte: u8) -> Option<Self> {
|
||||
Self::LOOKUP[byte as usize]
|
||||
}
|
||||
|
||||
/// Get opcode name
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
OpCodeByte::LoadConst => "LoadConst",
|
||||
OpCodeByte::Move => "Move",
|
||||
OpCodeByte::LoadLocal => "LoadLocal",
|
||||
OpCodeByte::StoreLocal => "StoreLocal",
|
||||
OpCodeByte::LoadGlobal => "LoadGlobal",
|
||||
OpCodeByte::StoreGlobal => "StoreGlobal",
|
||||
OpCodeByte::Add => "Add",
|
||||
OpCodeByte::Sub => "Sub",
|
||||
OpCodeByte::Mul => "Mul",
|
||||
OpCodeByte::Div => "Div",
|
||||
OpCodeByte::Neg => "Neg",
|
||||
OpCodeByte::Mod => "Mod",
|
||||
OpCodeByte::Pow => "Pow",
|
||||
OpCodeByte::Lt => "Lt",
|
||||
OpCodeByte::Lte => "Lte",
|
||||
OpCodeByte::Gt => "Gt",
|
||||
OpCodeByte::Gte => "Gte",
|
||||
OpCodeByte::Eq => "Eq",
|
||||
OpCodeByte::Neq => "Neq",
|
||||
OpCodeByte::And => "And",
|
||||
OpCodeByte::Or => "Or",
|
||||
OpCodeByte::Not => "Not",
|
||||
OpCodeByte::Contains => "Contains",
|
||||
OpCodeByte::Jump => "Jump",
|
||||
OpCodeByte::JumpIfFalse => "JumpIfFalse",
|
||||
OpCodeByte::JumpIfTrue => "JumpIfTrue",
|
||||
OpCodeByte::Concat => "Concat",
|
||||
OpCodeByte::MethodCall => "MethodCall",
|
||||
OpCodeByte::GetProperty => "GetProperty",
|
||||
OpCodeByte::SetProperty => "SetProperty",
|
||||
OpCodeByte::Log => "Log",
|
||||
OpCodeByte::CallExternal => "CallExternal",
|
||||
OpCodeByte::CallDefault => "Rand",
|
||||
OpCodeByte::SetResult => "SetResult",
|
||||
OpCodeByte::ClearResult => "ClearResult",
|
||||
OpCodeByte::End => "End",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+77
-48
@@ -41,8 +41,8 @@ pub grammar parser() for str {
|
||||
= binary_op()
|
||||
|
||||
pub rule mul_div() -> Expr =
|
||||
left:power() mul_div_right:(
|
||||
_ op:$("*" / "/" / "%") _ right:power()
|
||||
left:unary() mul_div_right:(
|
||||
_ op:$("*" / "/" / "%") _ right:unary()
|
||||
{ (op, right) }
|
||||
)* {
|
||||
let mut result = left;
|
||||
@@ -57,31 +57,39 @@ pub grammar parser() for str {
|
||||
result
|
||||
}
|
||||
|
||||
/// Unary operators bind looser than postfix (`.prop`, `.method()`) and `**`,
|
||||
/// so `!o.active` == `!(o.active)` and `-2 ** 2` == `-(2 ** 2)`.
|
||||
pub rule unary() -> Expr
|
||||
= "-" _ e:unary() { Expr::UnaryOp(Op::Neg, Box::new(e)) }
|
||||
/ "!" _ e:unary() { Expr::UnaryOp(Op::Not, Box::new(e)) }
|
||||
/ power()
|
||||
|
||||
pub rule power() -> Expr =
|
||||
base:postfix() _ "**" _ exp:power() { Expr::BinaryOp(Box::new(base), Op::Pow, Box::new(exp)) }
|
||||
base:postfix() _ "**" _ exp:unary() { Expr::BinaryOp(Box::new(base), Op::Pow, Box::new(exp)) }
|
||||
/ a:postfix() { a }
|
||||
|
||||
|
||||
pub rule binary_op() -> Expr = precedence!{
|
||||
i:identifier() _ "(" args:((_ e:expression() _ {e}) ** ",") ")" { Expr::FunctionCall(i, args) }
|
||||
--
|
||||
x:@ _ "&&" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::And, Box::new(y)) }
|
||||
x:@ _ "||" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Or, Box::new(y)) }
|
||||
x:(@) _ "||" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Or, Box::new(y)) }
|
||||
--
|
||||
x:@ _ "==" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Eq, Box::new(y)) }
|
||||
x:@ _ "!=" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Neq, Box::new(y)) }
|
||||
x:@ _ "<" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Lt, Box::new(y)) }
|
||||
x:@ _ "<=" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Lte, Box::new(y)) }
|
||||
x:@ _ ">" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Gt, Box::new(y)) }
|
||||
x:@ _ ">=" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Gte, Box::new(y)) }
|
||||
x:@ _ "in" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::In, Box::new(y)) }
|
||||
x:(@) _ "&&" _ y:@ { Expr::BinaryOp(Box::new(x), Op::And, Box::new(y)) }
|
||||
--
|
||||
x:@ _ "+" _ y:(@) { Expr::BinaryOp(Box::new(x),Op::Add, Box::new(y)) }
|
||||
x:@ _ "-" _ y:(@) { Expr::BinaryOp(Box::new(x),Op::Sub, Box::new(y)) }
|
||||
x:(@) _ "==" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Eq, Box::new(y)) }
|
||||
x:(@) _ "!=" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Neq, Box::new(y)) }
|
||||
x:(@) _ "<=" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Lte, Box::new(y)) }
|
||||
x:(@) _ ">=" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Gte, Box::new(y)) }
|
||||
x:(@) _ "<" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Lt, Box::new(y)) }
|
||||
x:(@) _ ">" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Gt, Box::new(y)) }
|
||||
x:(@) _ "in" _ y:@ { Expr::BinaryOp(Box::new(x), Op::In, Box::new(y)) }
|
||||
--
|
||||
x:(@) _ "+" _ y:@ { Expr::BinaryOp(Box::new(x),Op::Add, Box::new(y)) }
|
||||
x:(@) _ "-" _ y:@ { Expr::BinaryOp(Box::new(x),Op::Sub, Box::new(y)) }
|
||||
--
|
||||
x:mul_div() { x }
|
||||
--
|
||||
p:postfix() { p }
|
||||
p:unary() { p }
|
||||
}
|
||||
|
||||
/// Postfix operations: property access and method calls with chaining
|
||||
@@ -107,8 +115,6 @@ pub grammar parser() for str {
|
||||
/ i:number() { Expr::Value(Value::Number(i)) }
|
||||
/ i:boolean_literal() { Expr::Value(i) }
|
||||
/ "(" e:expression() ")" { e }
|
||||
/ "-" e:atom() { Expr::UnaryOp(Op::Neg, Box::new(e)) }
|
||||
/ "!" e:atom() { Expr::UnaryOp(Op::Not, Box::new(e)) }
|
||||
|
||||
pub rule string() -> SmolStr
|
||||
= "\"" s:$(([^'"'] / "\\\"")*) "\"" {
|
||||
@@ -197,40 +203,63 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_binary_op() {
|
||||
let res = parser::binary_op("1 + 2 * 3 - 4 / 5");
|
||||
if let Err(e) = &res {
|
||||
println!("{}", e);
|
||||
}
|
||||
if let Ok(expr) = res {
|
||||
match expr {
|
||||
Expr::BinaryOp(left, Op::Add, right) => {
|
||||
assert!(matches!(*left, Expr::Value(_)));
|
||||
match *right {
|
||||
Expr::BinaryOp(left2, Op::Sub, right2) => {
|
||||
// Check 2 * 3
|
||||
match *left2 {
|
||||
Expr::BinaryOp(left3, Op::Mul, right3) => {
|
||||
assert!(matches!(*left3, Expr::Value(_)));
|
||||
assert!(matches!(*right3, Expr::Value(_)));
|
||||
}
|
||||
_ => panic!("Expected multiplication"),
|
||||
}
|
||||
// Check 4 / 5
|
||||
match *right2 {
|
||||
Expr::BinaryOp(left3, Op::Div, right3) => {
|
||||
assert!(matches!(*left3, Expr::Value(_)));
|
||||
assert!(matches!(*right3, Expr::Value(_)));
|
||||
}
|
||||
_ => panic!("Expected division"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected subtraction"),
|
||||
// Left-associative: ((1 + (2 * 3)) - (4 / 5))
|
||||
let expr = parser::binary_op("1 + 2 * 3 - 4 / 5").expect("Failed to parse expression");
|
||||
match expr {
|
||||
Expr::BinaryOp(left, Op::Sub, right) => {
|
||||
// right: 4 / 5
|
||||
match *right {
|
||||
Expr::BinaryOp(l, Op::Div, r) => {
|
||||
assert!(matches!(*l, Expr::Value(_)));
|
||||
assert!(matches!(*r, Expr::Value(_)));
|
||||
}
|
||||
_ => panic!("Expected division"),
|
||||
}
|
||||
// left: 1 + (2 * 3)
|
||||
match *left {
|
||||
Expr::BinaryOp(l, Op::Add, r) => {
|
||||
assert!(matches!(*l, Expr::Value(_)));
|
||||
match *r {
|
||||
Expr::BinaryOp(l2, Op::Mul, r2) => {
|
||||
assert!(matches!(*l2, Expr::Value(_)));
|
||||
assert!(matches!(*r2, Expr::Value(_)));
|
||||
}
|
||||
_ => panic!("Expected multiplication"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected addition"),
|
||||
}
|
||||
_ => panic!("Expected addition at top level"),
|
||||
}
|
||||
} else {
|
||||
panic!("Failed to parse expression");
|
||||
_ => panic!("Expected subtraction at top level"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_left_associativity() {
|
||||
// 10 - 3 - 2 => (10 - 3) - 2
|
||||
match parser::binary_op("10 - 3 - 2").unwrap() {
|
||||
Expr::BinaryOp(left, Op::Sub, right) => {
|
||||
assert!(matches!(*right, Expr::Value(_)));
|
||||
assert!(matches!(*left, Expr::BinaryOp(_, Op::Sub, _)));
|
||||
}
|
||||
_ => panic!("Expected subtraction at top level"),
|
||||
}
|
||||
// a && b || c => (a && b) || c
|
||||
match parser::binary_op("true && false || true").unwrap() {
|
||||
Expr::BinaryOp(left, Op::Or, _) => {
|
||||
assert!(matches!(*left, Expr::BinaryOp(_, Op::And, _)));
|
||||
}
|
||||
_ => panic!("Expected || at top level"),
|
||||
}
|
||||
// !o.a => !(o.a)
|
||||
match parser::binary_op("!o.a").unwrap() {
|
||||
Expr::UnaryOp(Op::Not, inner) => assert!(matches!(*inner, Expr::PropertyAccess(_, _))),
|
||||
_ => panic!("Expected unary not at top level"),
|
||||
}
|
||||
// -2 ** 2 => -(2 ** 2)
|
||||
match parser::binary_op("-2 ** 2").unwrap() {
|
||||
Expr::UnaryOp(Op::Neg, inner) => assert!(matches!(*inner, Expr::BinaryOp(_, Op::Pow, _))),
|
||||
_ => panic!("Expected unary neg at top level"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -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
@@ -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)) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user