initial commit

This commit is contained in:
2026-04-05 16:08:59 +03:00
commit 75ab9bec9f
1117 changed files with 789034 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
use super::value::Value;
use smol_str::SmolStr;
/// Source code location for error reporting
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Span {
pub line: u32,
pub column: u32,
}
impl Span {
pub fn new(line: u32, column: u32) -> Self {
Self { line, column }
}
}
impl std::fmt::Display for Span {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "line {}, column {}", self.line, self.column)
}
}
/// Wrapper that associates a value with its source location
#[derive(Debug, Clone, PartialEq)]
pub struct Spanned<T> {
pub node: T,
pub span: Span,
}
impl<T> Spanned<T> {
pub fn new(node: T, span: Span) -> Self {
Self { node, span }
}
pub fn dummy(node: T) -> Self {
Self {
node,
span: Span::default(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Value(Value),
Variable(SmolStr),
BinaryOp(Box<Expr>, Op, Box<Expr>),
UnaryOp(Op, Box<Expr>),
FunctionCall(SmolStr, Vec<Expr>),
MethodCall(Box<Expr>, SmolStr, Vec<Expr>),
PropertyAccess(Box<Expr>, SmolStr),
}
/// Expression with source location
pub type SpannedExpr = Spanned<Expr>;
#[derive(Debug, Clone, PartialEq)]
pub enum Op {
Add,
Sub,
Mul,
Div,
Mod, // Modulo
Pow, // Power
Lt,
Lte,
Gt,
Gte,
Eq,
Neq,
Neg,
And,
Or,
Not,
In, // Membership test (value in list/string)
}
+3
View File
@@ -0,0 +1,3 @@
pub mod expr;
pub mod stmt;
pub mod value;
+16
View File
@@ -0,0 +1,16 @@
use smol_str::SmolStr;
use super::expr::{Expr, Spanned};
#[derive(Debug, PartialEq, Clone)]
pub enum Stmt {
Assignment(SmolStr, Box<Expr>),
/// Property assignment: root variable, field path, value
/// e.g. `a.b.c = 5` → PropertyAssignment("a", ["b", "c"], 5)
PropertyAssignment(SmolStr, Vec<SmolStr>, Box<Expr>),
ExprStmt(Box<Expr>),
If(Box<Expr>, Vec<Stmt>, Option<Vec<Stmt>>),
}
/// Statement with source location
pub type SpannedStmt = Spanned<Stmt>;
+440
View File
@@ -0,0 +1,440 @@
use indexmap::IndexMap;
use rust_decimal::Decimal;
use smol_str::SmolStr;
use std::fmt;
/// Value type for the dExpr language
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Value {
#[default]
Null,
Number(Decimal),
String(SmolStr),
Boolean(bool),
NumberList(Vec<Decimal>),
StringList(Vec<SmolStr>),
Object(IndexMap<SmolStr, Value>),
}
/// Type tag constants for serialization
pub const TYPE_NULL: u8 = 0x00;
pub const TYPE_NUMBER: u8 = 0x01;
pub const TYPE_STRING: u8 = 0x02;
pub const TYPE_BOOLEAN: u8 = 0x03;
pub const TYPE_NUMBER_LIST: u8 = 0x04;
pub const TYPE_STRING_LIST: u8 = 0x05;
pub const TYPE_OBJECT: u8 = 0x06;
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::Null => write!(f, "null"),
Value::Number(n) => write!(f, "{}", n),
Value::String(s) => write!(f, "\"{}\"", s),
Value::Boolean(b) => write!(f, "{}", b),
Value::NumberList(list) => {
write!(f, "[")?;
for (i, val) in list.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", val)?;
}
write!(f, "]")
}
Value::StringList(list) => {
write!(f, "[")?;
for (i, val) in list.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "\"{}\"", val)?;
}
write!(f, "]")
}
Value::Object(map) => {
write!(f, "{{")?;
for (i, (key, val)) in map.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}: {}", key, val)?;
}
write!(f, "}}")
}
}
}
}
impl Value {
/// Get the type tag for bytecode serialization
pub fn type_tag(&self) -> u8 {
match self {
Value::Null => TYPE_NULL,
Value::Number(_) => TYPE_NUMBER,
Value::String(_) => TYPE_STRING,
Value::Boolean(_) => TYPE_BOOLEAN,
Value::NumberList(_) => TYPE_NUMBER_LIST,
Value::StringList(_) => TYPE_STRING_LIST,
Value::Object(_) => TYPE_OBJECT,
}
}
/// Get the type name as a string (for error messages)
pub fn type_name(&self) -> &'static str {
match self {
Value::Null => "Null",
Value::Number(_) => "Number",
Value::String(_) => "String",
Value::Boolean(_) => "Boolean",
Value::NumberList(_) => "NumberList",
Value::StringList(_) => "StringList",
Value::Object(_) => "Object",
}
}
/// Serialize the value to bytes for bytecode
pub fn serialize(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.push(self.type_tag());
match self {
Value::Null => {
// No additional data for null
}
Value::Number(n) => {
bytes.extend_from_slice(&n.serialize());
}
Value::String(s) => {
// String length (2 bytes)
bytes.push((s.len() >> 8) as u8);
bytes.push(s.len() as u8);
// String data
bytes.extend_from_slice(s.as_bytes());
}
Value::Boolean(b) => {
bytes.push(if *b { 1 } else { 0 });
}
Value::NumberList(list) => {
// List length (2 bytes)
bytes.push((list.len() >> 8) as u8);
bytes.push(list.len() as u8);
// List items
for n in list {
bytes.extend_from_slice(&n.serialize());
}
}
Value::StringList(list) => {
// List length (2 bytes)
bytes.push((list.len() >> 8) as u8);
bytes.push(list.len() as u8);
// List items
for s in list {
// String length (2 bytes)
bytes.push((s.len() >> 8) as u8);
bytes.push(s.len() as u8);
// String data
bytes.extend_from_slice(s.as_bytes());
}
}
Value::Object(map) => {
// Entry count (2 bytes)
bytes.push((map.len() >> 8) as u8);
bytes.push(map.len() as u8);
// Entries: key (string) + value (recursive)
for (key, val) in map {
bytes.push((key.len() >> 8) as u8);
bytes.push(key.len() as u8);
bytes.extend_from_slice(key.as_bytes());
bytes.extend_from_slice(&val.serialize());
}
}
}
bytes
}
pub fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
}
impl From<Decimal> for Value {
fn from(n: Decimal) -> Self {
Value::Number(n)
}
}
impl From<i64> for Value {
fn from(n: i64) -> Self {
Value::Number(Decimal::from(n))
}
}
impl From<i32> for Value {
fn from(n: i32) -> Self {
Value::Number(Decimal::from(n))
}
}
impl From<f64> for Value {
fn from(n: f64) -> Self {
Value::Number(Decimal::try_from(n).unwrap_or_default())
}
}
impl From<bool> for Value {
fn from(b: bool) -> Self {
Value::Boolean(b)
}
}
impl From<&str> for Value {
fn from(s: &str) -> Self {
Value::String(SmolStr::new(s))
}
}
impl From<String> for Value {
fn from(s: String) -> Self {
Value::String(SmolStr::new(&s))
}
}
impl From<SmolStr> for Value {
fn from(s: SmolStr) -> Self {
Value::String(s)
}
}
impl From<Vec<Decimal>> for Value {
fn from(v: Vec<Decimal>) -> Self {
Value::NumberList(v)
}
}
impl From<Vec<SmolStr>> for Value {
fn from(v: Vec<SmolStr>) -> Self {
Value::StringList(v)
}
}
impl From<IndexMap<SmolStr, Value>> for Value {
fn from(m: IndexMap<SmolStr, Value>) -> Self {
Value::Object(m)
}
}
impl Value {
/// Deserialize a value from bytes
pub fn deserialize(bytes: &[u8]) -> Result<(Value, usize), String> {
if bytes.is_empty() {
return Err("Empty buffer".to_string());
}
let type_tag = bytes[0];
let mut pos = 1;
match type_tag {
TYPE_NULL => Ok((Value::Null, pos)),
TYPE_NUMBER => {
if bytes.len() < pos + 16 {
return Err("Insufficient bytes for Number".to_string());
}
let mut decimal_bytes = [0u8; 16];
decimal_bytes.copy_from_slice(&bytes[pos..pos + 16]);
pos += 16;
Ok((Value::Number(Decimal::deserialize(decimal_bytes)), pos))
}
TYPE_STRING => {
if bytes.len() < pos + 2 {
return Err("Insufficient bytes for String length".to_string());
}
let len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
pos += 2;
if bytes.len() < pos + len {
return Err("Insufficient bytes for String data".to_string());
}
let s = match std::str::from_utf8(&bytes[pos..pos + len]) {
Ok(s) => s,
Err(_) => return Err("Invalid UTF-8 in String".to_string()),
};
pos += len;
Ok((Value::String(s.into()), pos))
}
TYPE_BOOLEAN => {
if bytes.len() < pos + 1 {
return Err("Insufficient bytes for Boolean".to_string());
}
let b = bytes[pos] != 0;
pos += 1;
Ok((Value::Boolean(b), pos))
}
TYPE_NUMBER_LIST => {
if bytes.len() < pos + 2 {
return Err("Insufficient bytes for NumberList length".to_string());
}
let len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
pos += 2;
let total_bytes = len * 16;
if bytes.len() < pos + total_bytes {
return Err("Insufficient bytes for NumberList items".to_string());
}
let mut list = Vec::with_capacity(len);
for _ in 0..len {
let mut decimal_bytes = [0u8; 16];
decimal_bytes.copy_from_slice(&bytes[pos..pos + 16]);
pos += 16;
list.push(Decimal::deserialize(decimal_bytes));
}
Ok((Value::NumberList(list), pos))
}
TYPE_STRING_LIST => {
if bytes.len() < pos + 2 {
return Err("Insufficient bytes for StringList length".to_string());
}
let len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
pos += 2;
let mut list = Vec::with_capacity(len);
for _ in 0..len {
if bytes.len() < pos + 2 {
return Err("Insufficient bytes for StringList item length".to_string());
}
let str_len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
pos += 2;
if bytes.len() < pos + str_len {
return Err("Insufficient bytes for StringList item data".to_string());
}
let s = match std::str::from_utf8(&bytes[pos..pos + str_len]) {
Ok(s) => s,
Err(_) => return Err("Invalid UTF-8 in StringList item".to_string()),
};
pos += str_len;
list.push(s.into());
}
Ok((Value::StringList(list), pos))
}
TYPE_OBJECT => {
if bytes.len() < pos + 2 {
return Err("Insufficient bytes for Object length".to_string());
}
let len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
pos += 2;
let mut map = IndexMap::with_capacity(len);
for _ in 0..len {
// Read key
if bytes.len() < pos + 2 {
return Err("Insufficient bytes for Object key length".to_string());
}
let key_len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
pos += 2;
if bytes.len() < pos + key_len {
return Err("Insufficient bytes for Object key data".to_string());
}
let key = match std::str::from_utf8(&bytes[pos..pos + key_len]) {
Ok(s) => s,
Err(_) => return Err("Invalid UTF-8 in Object key".to_string()),
};
pos += key_len;
// Read value (recursive)
let (val, val_bytes) = Value::deserialize(&bytes[pos..])?;
pos += val_bytes;
map.insert(key.into(), val);
}
Ok((Value::Object(map), pos))
}
_ => Err(format!("Unknown type tag: {}", type_tag)),
}
}
/// Create a Value from a JSON string.
///
/// Mapping:
/// - `null` → `Null`
/// - `true`/`false` → `Boolean`
/// - number → `Number` (Decimal)
/// - string → `String`
/// - array of numbers → `NumberList`
/// - array of strings → `StringList`
/// - object → `Object` (recursive)
///
/// ```
/// use dexpr::ast::value::Value;
/// use rust_decimal_macros::dec;
///
/// let val = Value::from_json(r#"{"name": "Alice", "age": 30}"#).unwrap();
/// if let Value::Object(map) = &val {
/// assert_eq!(map.get("name").unwrap(), &Value::String("Alice".into()));
/// assert_eq!(map.get("age").unwrap(), &Value::Number(dec!(30)));
/// }
/// ```
pub fn from_json(json: &str) -> Result<Value, String> {
let v: serde_json::Value = serde_json::from_str(json)
.map_err(|e| format!("JSON parse error: {}", e))?;
Self::from_json_value(&v)
}
/// Convert a serde_json::Value to a dexpr Value.
pub fn from_json_value(v: &serde_json::Value) -> Result<Value, String> {
match v {
serde_json::Value::Null => Ok(Value::Null),
serde_json::Value::Bool(b) => Ok(Value::Boolean(*b)),
serde_json::Value::Number(n) => {
// Try integer first, then float
if let Some(i) = n.as_i64() {
Ok(Value::Number(Decimal::from(i)))
} else if let Some(f) = n.as_f64() {
Decimal::try_from(f)
.map(Value::Number)
.map_err(|e| format!("Cannot convert {} to Decimal: {}", f, e))
} else {
Err(format!("Unsupported JSON number: {}", n))
}
}
serde_json::Value::String(s) => Ok(Value::String(SmolStr::new(s))),
serde_json::Value::Array(arr) => {
if arr.is_empty() {
return Ok(Value::StringList(Vec::new()));
}
// Check if all elements are the same type
let first = &arr[0];
if first.is_number() && arr.iter().all(|v| v.is_number()) {
let mut nums = Vec::with_capacity(arr.len());
for item in arr {
if let Value::Number(n) = Self::from_json_value(item)? {
nums.push(n);
}
}
Ok(Value::NumberList(nums))
} else if first.is_string() && arr.iter().all(|v| v.is_string()) {
let strings: Vec<SmolStr> = arr.iter()
.filter_map(|v| v.as_str().map(SmolStr::new))
.collect();
Ok(Value::StringList(strings))
} else {
Err("Arrays must contain all numbers or all strings".to_string())
}
}
serde_json::Value::Object(obj) => {
let mut map = IndexMap::with_capacity(obj.len());
for (k, v) in obj {
map.insert(SmolStr::new(k), Self::from_json_value(v)?);
}
Ok(Value::Object(map))
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
a = 10.2
b = a + 5.5
c = 2.4
d = b + c
f = (3+3)*2/3
t = 3+3
g = t*2/3 + test
+28
View File
@@ -0,0 +1,28 @@
a = 10.2
b = a + 5.5
c = 2.4
d = b + c
"Merhaba" + " duhan".upper()
(3+3)*2/3
t = 3+3
t*2/3
if true then
"true"
else
"false"
end
aaa = false
if false && true then
"11"
else if 2 > 1 && 2 >= 2 && !aaa then
"22"
else if true then
"33"
end
+10
View File
@@ -0,0 +1,10 @@
a = 10.2
b = a + 5.5
c = 2.4
d = b + c
f = (3+3)*2/3
t = 3+3
g = t*2/3
+10
View File
@@ -0,0 +1,10 @@
a = 10.2
b = a + 5.5
c = 2.4
d = b + c
f = (3+3)*2/3
t = 3+3
g = t*2/3
+177
View File
@@ -0,0 +1,177 @@
use smol_str::SmolStr;
use crate::ast::value::Value;
/// Bytecode writer for generating VM bytecode
#[derive(Debug, Clone)]
pub struct BytecodeWriter {
buffer: Vec<u8>,
}
impl BytecodeWriter {
/// Create a new bytecode writer
pub fn new() -> Self {
Self { buffer: Vec::new() }
}
/// Write a single byte
pub fn write_byte(&mut self, byte: u8) {
self.buffer.push(byte);
}
/// Write a 16-bit unsigned integer
pub fn write_u16(&mut self, value: u16) {
self.buffer.push((value >> 8) as u8);
self.buffer.push(value as u8);
}
/// Write a 32-bit unsigned integer
pub fn write_u32(&mut self, value: u32) {
self.buffer.push((value >> 24) as u8);
self.buffer.push((value >> 16) as u8);
self.buffer.push((value >> 8) as u8);
self.buffer.push(value as u8);
}
/// Write a register
pub fn write_register(&mut self, reg: u8) {
self.buffer.push(reg);
}
/// Write a string
pub fn write_string(&mut self, s: &SmolStr) {
// String length (2 bytes)
self.write_u16(s.len() as u16);
// String data
self.buffer.extend_from_slice(s.as_bytes());
}
/// Write a value
pub fn write_value(&mut self, value: &Value) {
self.buffer.extend_from_slice(&value.serialize());
}
/// Get the current position
pub fn position(&self) -> usize {
self.buffer.len()
}
/// Get the bytecode buffer
pub fn bytecode(&self) -> &[u8] {
&self.buffer
}
/// Consume the writer and return the bytecode
pub fn into_bytecode(self) -> Vec<u8> {
self.buffer
}
}
/// Bytecode reader for parsing VM bytecode
pub struct BytecodeReader<'a> {
bytecode: &'a [u8],
position: usize,
}
impl<'a> BytecodeReader<'a> {
/// Create a new bytecode reader
pub fn new(bytecode: &'a [u8]) -> Self {
Self {
bytecode,
position: 0,
}
}
/// Read a single byte
#[inline(always)]
pub fn read_byte(&mut self) -> Result<u8, String> {
if self.position >= self.bytecode.len() {
return Err("Unexpected end of bytecode".to_string());
}
let byte = self.bytecode[self.position];
self.position += 1;
Ok(byte)
}
/// Read a 16-bit unsigned integer
pub fn read_u16(&mut self) -> Result<u16, String> {
if self.position + 1 >= self.bytecode.len() {
return Err("Unexpected end of bytecode".to_string());
}
let value =
((self.bytecode[self.position] as u16) << 8) | (self.bytecode[self.position + 1] as u16);
self.position += 2;
Ok(value)
}
/// Read a 32-bit unsigned integer
pub fn read_u32(&mut self) -> Result<u32, String> {
if self.position + 3 >= self.bytecode.len() {
return Err("Unexpected end of bytecode".to_string());
}
let value = ((self.bytecode[self.position] as u32) << 24)
| ((self.bytecode[self.position + 1] as u32) << 16)
| ((self.bytecode[self.position + 2] as u32) << 8)
| (self.bytecode[self.position + 3] as u32);
self.position += 4;
Ok(value)
}
/// Read a register
#[inline(always)]
pub fn read_register(&mut self) -> Result<u8, String> {
self.read_byte()
}
/// Read a string
#[inline(always)]
pub fn read_string(&mut self) -> Result<SmolStr, String> {
let length = self.read_u16()? as usize;
if self.position + length > self.bytecode.len() {
return Err("Unexpected end of bytecode".to_string());
}
let s = std::str::from_utf8(&self.bytecode[self.position..self.position + length])
.map_err(|_| "Invalid UTF-8 string".to_string())?;
self.position += length;
Ok(s.into())
}
/// Read a value
pub fn read_value(&mut self) -> Result<Value, String> {
if self.position >= self.bytecode.len() {
return Err("Unexpected end of bytecode".to_string());
}
let (value, bytes_read) = Value::deserialize(&self.bytecode[self.position..])?;
self.position += bytes_read;
Ok(value)
}
/// Get the current position
pub fn position(&self) -> usize {
self.position
}
/// Set the position
pub fn set_position(&mut self, position: usize) -> Result<(), String> {
if position > self.bytecode.len() {
return Err("Position out of range".to_string());
}
self.position = position;
Ok(())
}
/// Get the remaining bytes
#[inline(always)]
pub fn remaining(&self) -> usize {
self.bytecode.len() - self.position
}
}
+234
View File
@@ -0,0 +1,234 @@
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::End => format!("{:04x}: End", start_position),
};
result.push(instruction);
}
result
}
+605
View File
@@ -0,0 +1,605 @@
use crate::ast::expr::Span;
use crate::ast::value::Value;
use crate::ast::{
expr::{Expr, Op},
stmt::Stmt,
};
use crate::bytecode::BytecodeWriter;
use crate::opcodes::OpCodeByte;
use crate::parser::offset_to_span;
use crate::vm::DebugInfo;
use smol_str::SmolStr;
use thiserror::Error;
/// Maximum number of registers
pub const MAX_REGISTERS: u8 = 8;
/// Compile-time error
#[derive(Error, Debug)]
pub enum CompileError {
#[error("Undefined function: {0}")]
UndefinedFunction(SmolStr),
#[error("Register limit exceeded")]
RegisterLimitExceeded,
#[error("Invalid expression: {0}")]
InvalidExpression(String),
#[error("Invalid statement: {0}")]
InvalidStatement(String),
#[error("Bytecode error: {0}")]
BytecodeError(String),
}
/// Compiler for dExpr language
pub struct Compiler {
writer: BytecodeWriter,
used_registers: Vec<bool>,
#[cfg(debug_assertions)]
debug: bool,
// Jump address resolution
pending_jumps: Vec<(usize, usize)>,
labels: HashMap<usize, usize>,
next_label: usize,
// Debug info generation
debug_info: DebugInfo,
current_span: Span,
}
use std::collections::HashMap;
impl Compiler {
/// Create a new compiler
pub fn new() -> Self {
Self {
writer: BytecodeWriter::new(),
used_registers: vec![false; MAX_REGISTERS as usize],
#[cfg(debug_assertions)]
debug: false,
pending_jumps: Vec::new(),
labels: HashMap::new(),
next_label: 0,
debug_info: DebugInfo::new(),
current_span: Span::default(),
}
}
/// Set debug mode
#[cfg(debug_assertions)]
pub fn set_debug(&mut self, debug: bool) {
self.debug = debug;
}
/// Set debug mode (no-op in release)
#[cfg(not(debug_assertions))]
pub fn set_debug(&mut self, _debug: bool) {}
/// Update current source span and emit debug info
fn set_span(&mut self, span: Span) {
if span != self.current_span {
self.current_span = span;
let offset = self.writer.position() as u32;
self.debug_info.add_entry(offset, span);
}
}
/// Get the generated debug info
pub fn debug_info(&self) -> &DebugInfo {
&self.debug_info
}
/// Take the debug info out of the compiler
pub fn take_debug_info(&mut self) -> DebugInfo {
std::mem::take(&mut self.debug_info)
}
/// Compile AST to bytecode
pub fn compile(&mut self, statements: Vec<Stmt>) -> Result<Vec<u8>, CompileError> {
self.reset_compiler_state();
for stmt in &statements {
self.compile_stmt(stmt)?;
}
self.emit_byte(OpCodeByte::End.to_byte());
self.resolve_jumps()?;
Ok(self.writer.clone().into_bytecode())
}
/// Compile source code with debug info for error messages
/// Returns (bytecode, debug_info)
pub fn compile_from_source(
&mut self,
source: &str,
) -> Result<(Vec<u8>, DebugInfo), CompileError> {
use crate::parser;
// Parse with position info
let stmts_with_pos = parser::program_with_spans(source)
.map_err(|e| CompileError::InvalidStatement(e.to_string()))?;
self.reset_compiler_state();
// Compile statements with span info
for (offset, stmt) in &stmts_with_pos {
self.set_span(offset_to_span(source, *offset));
self.compile_stmt(stmt)?;
}
self.emit_byte(OpCodeByte::End.to_byte());
self.resolve_jumps()?;
let bytecode = self.writer.clone().into_bytecode();
Ok((bytecode, self.take_debug_info()))
}
/// Reset all compiler state for a new compilation
fn reset_compiler_state(&mut self) {
self.writer = BytecodeWriter::new();
self.used_registers = vec![false; MAX_REGISTERS as usize];
self.pending_jumps.clear();
self.labels.clear();
self.next_label = 0;
self.debug_info = DebugInfo::new();
self.current_span = Span::default();
}
/// Compile a statement
fn compile_stmt(&mut self, stmt: &Stmt) -> Result<(), CompileError> {
match stmt {
Stmt::Assignment(name, expr) => self.compile_assignment(name, expr),
Stmt::PropertyAssignment(root, path, expr) => self.compile_property_assignment(root, path, expr),
Stmt::ExprStmt(expr) => self.compile_expr_stmt(expr),
Stmt::If(condition, then_branch, else_branch) => {
self.compile_if_statement(condition, then_branch, else_branch)
}
}
}
/// Compile an assignment statement
fn compile_assignment(
&mut self,
name: &SmolStr,
expr: &Expr,
) -> Result<(), CompileError> {
let expr_reg = self.compile_expr(expr)?;
self.emit_store_global(name, expr_reg);
self.free_register(expr_reg);
Ok(())
}
/// Emit store to global variable instruction
fn emit_store_global(&mut self, name: &SmolStr, reg: u8) {
self.emit_byte(OpCodeByte::StoreGlobal.to_byte());
self.emit_string(name);
self.emit_byte(reg);
}
/// Compile a property assignment: `a.b.c = expr`
/// Strategy: load root, get intermediates, set deepest, then set back up the chain, store root.
fn compile_property_assignment(
&mut self,
root: &SmolStr,
path: &[SmolStr],
expr: &Expr,
) -> Result<(), CompileError> {
// Load root object
let root_reg = self.allocate_register()?;
self.emit_byte(OpCodeByte::LoadGlobal.to_byte());
self.emit_byte(root_reg);
self.emit_string(root);
// Get intermediate objects along the path (except last)
let mut chain_regs = vec![root_reg];
for field in &path[..path.len() - 1] {
let next_reg = self.allocate_register()?;
self.emit_byte(OpCodeByte::GetProperty.to_byte());
self.emit_byte(next_reg);
self.emit_byte(*chain_regs.last().unwrap());
self.emit_string(field);
chain_regs.push(next_reg);
}
// Compile the value expression
let val_reg = self.compile_expr(expr)?;
// Set the deepest property
let last_field = &path[path.len() - 1];
self.emit_byte(OpCodeByte::SetProperty.to_byte());
self.emit_byte(*chain_regs.last().unwrap());
self.emit_string(last_field);
self.emit_byte(val_reg);
self.free_register(val_reg);
// Write back up the chain
for i in (1..chain_regs.len()).rev() {
let field = &path[i - 1];
self.emit_byte(OpCodeByte::SetProperty.to_byte());
self.emit_byte(chain_regs[i - 1]);
self.emit_string(field);
self.emit_byte(chain_regs[i]);
self.free_register(chain_regs[i]);
}
// Store root back to global
self.emit_store_global(root, root_reg);
self.free_register(root_reg);
Ok(())
}
/// Compile an expression statement (expression without assignment)
fn compile_expr_stmt(&mut self, expr: &Expr) -> Result<(), CompileError> {
let expr_reg = self.compile_expr(expr)?;
self.emit_byte(OpCodeByte::SetResult.to_byte());
self.emit_byte(expr_reg);
self.free_register(expr_reg);
Ok(())
}
/// Compile an if statement
fn compile_if_statement(
&mut self,
condition: &Expr,
then_branch: &[Stmt],
else_branch: &Option<Vec<Stmt>>,
) -> Result<(), CompileError> {
let cond_reg = self.compile_expr(condition)?;
let else_label = self.create_label();
let end_label = self.create_label();
// Jump to else branch if condition is false
self.emit_byte(OpCodeByte::JumpIfFalse.to_byte());
self.emit_byte(cond_reg);
self.emit_jump_address(else_label);
self.free_register(cond_reg);
// Compile then branch
for stmt in then_branch {
self.compile_stmt(stmt)?;
}
// Jump to end after then branch
self.emit_jump(end_label);
// Compile else branch if it exists
self.set_label(else_label);
if let Some(else_stmts) = else_branch {
for stmt in else_stmts {
self.compile_stmt(stmt)?;
}
}
self.set_label(end_label);
Ok(())
}
/// Compile an expression
fn compile_expr(&mut self, expr: &Expr) -> Result<u8, CompileError> {
match expr {
Expr::Value(value) => self.compile_value(value),
Expr::Variable(name) => self.compile_variable(name),
Expr::BinaryOp(left, op, right) => self.compile_binary_op(left, op, right),
Expr::UnaryOp(op, operand) => self.compile_unary_op(op, operand),
Expr::FunctionCall(name, args) => self.compile_function_call(name, args),
Expr::MethodCall(obj, method, args) => self.compile_method_call(obj, method, args),
Expr::PropertyAccess(obj, prop) => self.compile_property_access(obj, prop),
}
}
/// Compile a constant value
fn compile_value(&mut self, value: &Value) -> Result<u8, CompileError> {
let reg = self.allocate_register()?;
self.emit_load_const(reg, value.clone());
Ok(reg)
}
/// Compile a variable reference
fn compile_variable(&mut self, name: &SmolStr) -> Result<u8, CompileError> {
let reg = self.allocate_register()?;
self.emit_byte(OpCodeByte::LoadGlobal.to_byte());
self.emit_byte(reg);
self.emit_string(name);
Ok(reg)
}
/// Compile a binary operation
fn compile_binary_op(
&mut self,
left: &Expr,
op: &Op,
right: &Expr,
) -> Result<u8, CompileError> {
let left_reg = self.compile_expr(left)?;
let right_reg = self.compile_expr(right)?;
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::Sub => OpCodeByte::Sub,
Op::Mul => OpCodeByte::Mul,
Op::Div => OpCodeByte::Div,
Op::Mod => OpCodeByte::Mod,
Op::Pow => OpCodeByte::Pow,
Op::Lt => OpCodeByte::Lt,
Op::Lte => OpCodeByte::Lte,
Op::Gt => OpCodeByte::Gt,
Op::Gte => OpCodeByte::Gte,
Op::Eq => OpCodeByte::Eq,
Op::Neq => OpCodeByte::Neq,
Op::And => OpCodeByte::And,
Op::Or => OpCodeByte::Or,
Op::In => OpCodeByte::Contains,
_ => {
return Err(CompileError::InvalidExpression(format!(
"Unsupported binary operator: {:?}",
op
)));
}
};
self.emit_byte(opcode.to_byte());
self.emit_byte(result_reg);
self.emit_byte(left_reg);
self.emit_byte(right_reg);
self.free_register(left_reg);
self.free_register(right_reg);
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)?;
let result_reg = self.allocate_register()?;
let opcode = match op {
Op::Neg => OpCodeByte::Neg,
Op::Not => OpCodeByte::Not,
_ => {
return Err(CompileError::InvalidExpression(format!(
"Unsupported unary operator: {:?}",
op
)));
}
};
self.emit_byte(opcode.to_byte());
self.emit_byte(result_reg);
self.emit_byte(operand_reg);
self.free_register(operand_reg);
Ok(result_reg)
}
/// Compile a function call (built-in or external)
fn compile_function_call(
&mut self,
name: &SmolStr,
args: &[Expr],
) -> Result<u8, CompileError> {
let arg_regs = self.compile_arguments(args)?;
let result_reg = self.allocate_register()?;
if name == "log" {
self.compile_builtin_log(&arg_regs, result_reg)?;
} else if let Some(fn_id) = crate::opcodes::default_fn::id(name) {
// Default (built-in) function — emit CallDefault with function ID
self.emit_byte(OpCodeByte::CallDefault.to_byte());
self.emit_byte(result_reg);
self.emit_byte(fn_id);
self.emit_byte(arg_regs.len() as u8);
for &reg in &arg_regs {
self.emit_byte(reg);
}
} else {
// Emit CallExternal — resolved by VM at runtime
self.emit_byte(OpCodeByte::CallExternal.to_byte());
self.emit_byte(result_reg);
self.emit_string(name);
self.emit_byte(arg_regs.len() as u8);
for &reg in &arg_regs {
self.emit_byte(reg);
}
}
// Free argument registers
for reg in arg_regs {
self.free_register(reg);
}
Ok(result_reg)
}
/// Compile function arguments and return their register numbers
fn compile_arguments(&mut self, args: &[Expr]) -> Result<Vec<u8>, CompileError> {
let mut arg_regs = Vec::with_capacity(args.len());
for arg in args {
let reg = self.compile_expr(arg)?;
arg_regs.push(reg);
}
Ok(arg_regs)
}
/// Compile built-in log function
fn compile_builtin_log(
&mut self,
arg_regs: &[u8],
result_reg: u8,
) -> Result<(), CompileError> {
if let Some(&arg_reg) = arg_regs.first() {
self.emit_byte(OpCodeByte::Log.to_byte());
self.emit_byte(arg_reg);
self.emit_load_const(result_reg, Value::Null);
Ok(())
} else {
Err(CompileError::InvalidExpression(
"log requires an argument".to_string(),
))
}
}
/// Compile a property access: `obj.field`
fn compile_property_access(
&mut self,
obj: &Expr,
prop: &SmolStr,
) -> Result<u8, CompileError> {
let obj_reg = self.compile_expr(obj)?;
let result_reg = self.allocate_register()?;
self.emit_byte(OpCodeByte::GetProperty.to_byte());
self.emit_byte(result_reg);
self.emit_byte(obj_reg);
self.emit_string(prop);
self.free_register(obj_reg);
Ok(result_reg)
}
/// Compile a method call
fn compile_method_call(
&mut self,
obj: &Expr,
method: &SmolStr,
args: &[Expr],
) -> Result<u8, CompileError> {
let obj_reg = self.compile_expr(obj)?;
let arg_regs = self.compile_arguments(args)?;
let result_reg = self.allocate_register()?;
// Emit method call instruction
self.emit_byte(OpCodeByte::MethodCall.to_byte());
self.emit_byte(result_reg);
self.emit_byte(obj_reg);
self.emit_string(method);
self.emit_byte(arg_regs.len() as u8);
// Emit argument registers
for &reg in &arg_regs {
self.emit_byte(reg);
}
// Free object and argument registers
self.free_register(obj_reg);
for reg in arg_regs {
self.free_register(reg);
}
Ok(result_reg)
}
/// Allocate a register
fn allocate_register(&mut self) -> Result<u8, CompileError> {
for i in 0..MAX_REGISTERS {
if !self.used_registers[i as usize] {
self.used_registers[i as usize] = true;
return Ok(i);
}
}
Err(CompileError::RegisterLimitExceeded)
}
/// Free a register
fn free_register(&mut self, reg: u8) {
if reg < MAX_REGISTERS {
self.used_registers[reg as usize] = false;
}
}
/// Create a new label
fn create_label(&mut self) -> usize {
let label = self.next_label;
self.next_label += 1;
label
}
/// Set a label position
fn set_label(&mut self, label: usize) {
let pos = self.writer.position();
self.labels.insert(label, pos);
}
/// Emit a jump address placeholder to be resolved later
fn emit_jump_address(&mut self, label: usize) -> usize {
let pos = self.writer.position();
self.emit_u32(0); // Placeholder
self.pending_jumps.push((pos, label));
pos
}
/// Emit a jump instruction (opcode + address)
fn emit_jump(&mut self, label: usize) {
self.emit_byte(OpCodeByte::Jump.to_byte());
self.emit_jump_address(label);
}
/// Resolve pending jumps by filling in jump addresses
fn resolve_jumps(&mut self) -> Result<(), CompileError> {
let bytecode = self.writer.bytecode();
let mut result = bytecode.to_vec();
for (jump_pos, label) in &self.pending_jumps {
let target_pos = self
.labels
.get(label)
.ok_or_else(|| CompileError::BytecodeError(format!("Undefined label: {}", label)))?;
self.write_u32_at_position(&mut result, *jump_pos, *target_pos as u32);
}
// Replace bytecode with resolved jumps
self.writer = BytecodeWriter::new();
for byte in result {
self.emit_byte(byte);
}
Ok(())
}
/// Write a u32 value at a specific position in bytecode (big-endian)
fn write_u32_at_position(&self, bytecode: &mut [u8], pos: usize, value: u32) {
bytecode[pos] = (value >> 24) as u8;
bytecode[pos + 1] = (value >> 16) as u8;
bytecode[pos + 2] = (value >> 8) as u8;
bytecode[pos + 3] = value as u8;
}
/// Emit a byte
fn emit_byte(&mut self, byte: u8) {
self.writer.write_byte(byte);
}
/// Emit a 32-bit integer
fn emit_u32(&mut self, value: u32) {
self.writer.write_u32(value);
}
/// Emit a string
fn emit_string(&mut self, s: &SmolStr) {
self.writer.write_string(s);
}
/// Emit a load constant instruction
fn emit_load_const(&mut self, reg: u8, value: Value) {
self.emit_byte(OpCodeByte::LoadConst.to_byte());
self.emit_byte(reg);
self.writer.write_value(&value);
}
}
+293
View File
@@ -0,0 +1,293 @@
//! Language metadata for editor integration.
//!
//! Generates JSON describing built-in functions, methods per type,
//! and host-registered extensions. The frontend editor library
//! (`codemirror-lang-dexpr`) consumes this to provide type-aware autocomplete.
//!
//! # Usage
//! ```rust
//! use dexpr::language_info::LanguageInfo;
//! use dexpr::ast::value::Value;
//! use indexmap::IndexMap;
//! use smol_str::SmolStr;
//! use rust_decimal_macros::dec;
//!
//! let mut info = LanguageInfo::builtin();
//!
//! // Add host-registered functions
//! info.add_function("getRate", "(code: String) -> Number", Some("Get exchange rate"));
//!
//! // Add host-registered methods
//! info.add_method("String", "toTitleCase", "() -> String", None);
//!
//! // Add external variables — type inferred from Value
//! let mut customer = IndexMap::new();
//! customer.insert(SmolStr::new("name"), Value::String("Alice".into()));
//! customer.insert(SmolStr::new("age"), Value::Number(dec!(30)));
//! info.add_value("customer", &Value::Object(customer), None);
//! info.add_value("price", &Value::Number(dec!(100)), None);
//!
//! let json = info.to_json();
//! // Send `json` to frontend
//! ```
use crate::ast::value::Value;
/// Function metadata
pub struct FunctionInfo {
pub name: &'static str,
pub signature: &'static str,
pub doc: Option<&'static str>,
}
/// Method metadata
pub struct MethodInfo {
pub name: &'static str,
pub signature: &'static str,
pub doc: Option<&'static str>,
}
/// Field metadata for Object type variables
pub struct FieldInfo {
pub name: String,
pub type_name: String,
}
/// Variable metadata
pub struct VariableInfo {
pub name: String,
pub type_name: String,
pub doc: Option<String>,
pub fields: Option<Vec<FieldInfo>>,
}
/// Collected language metadata for editor autocomplete
pub struct LanguageInfo {
pub functions: Vec<FunctionInfo>,
pub methods: Vec<(&'static str, Vec<MethodInfo>)>,
pub variables: Vec<VariableInfo>,
}
impl LanguageInfo {
/// Create metadata with all built-in functions and methods
pub fn builtin() -> Self {
Self {
functions: builtin_functions(),
methods: builtin_methods(),
variables: Vec::new(),
}
}
/// Add a host-registered function
pub fn add_function(&mut self, name: &'static str, signature: &'static str, doc: Option<&'static str>) {
self.functions.push(FunctionInfo { name, signature, doc });
}
/// Add a host-registered method on a type
pub fn add_method(&mut self, type_name: &'static str, name: &'static str, signature: &'static str, doc: Option<&'static str>) {
if let Some(entry) = self.methods.iter_mut().find(|(t, _)| *t == type_name) {
entry.1.push(MethodInfo { name, signature, doc });
} else {
self.methods.push((type_name, vec![MethodInfo { name, signature, doc }]));
}
}
/// Add an external variable
pub fn add_variable(&mut self, name: impl Into<String>, type_name: impl Into<String>, doc: Option<String>) {
self.variables.push(VariableInfo {
name: name.into(),
type_name: type_name.into(),
doc,
fields: None,
});
}
/// Add a variable by inspecting a Value — type and Object fields are derived automatically.
///
/// This is the recommended way to register variables for editor autocomplete.
/// It mirrors what you pass to `vm.set_global()`.
///
/// ```ignore
/// vm.set_global("customer", customer.clone());
/// info.add_value("customer", &customer, None);
/// ```
pub fn add_value(&mut self, name: impl Into<String>, value: &Value, doc: Option<String>) {
let type_name = value.type_name().to_string();
let fields = match value {
Value::Object(map) => {
Some(map.iter().map(|(k, v)| FieldInfo {
name: k.to_string(),
type_name: v.type_name().to_string(),
}).collect())
}
_ => None,
};
self.variables.push(VariableInfo {
name: name.into(),
type_name,
doc,
fields,
});
}
/// Add an Object variable with manually specified field types.
/// Use `add_value` instead when you have the actual Value.
pub fn add_object_variable(&mut self, name: impl Into<String>, fields: Vec<(&str, &str)>, doc: Option<String>) {
self.variables.push(VariableInfo {
name: name.into(),
type_name: "Object".to_string(),
doc,
fields: Some(fields.into_iter().map(|(n, t)| FieldInfo {
name: n.to_string(),
type_name: t.to_string(),
}).collect()),
});
}
/// Serialize to JSON string for the frontend editor
pub fn to_json(&self) -> String {
let mut out = String::with_capacity(2048);
out.push_str("{\n \"functions\": [");
for (i, f) in self.functions.iter().enumerate() {
if i > 0 { out.push(','); }
out.push_str("\n {\"name\":\"");
out.push_str(f.name);
out.push_str("\",\"signature\":\"");
out.push_str(f.signature);
out.push('"');
if let Some(doc) = f.doc {
out.push_str(",\"doc\":\"");
out.push_str(&escape_json(doc));
out.push('"');
}
out.push('}');
}
out.push_str("\n ],\n \"methods\": {");
for (i, (type_name, methods)) in self.methods.iter().enumerate() {
if i > 0 { out.push(','); }
out.push_str("\n \"");
out.push_str(type_name);
out.push_str("\": [");
for (j, m) in methods.iter().enumerate() {
if j > 0 { out.push(','); }
out.push_str("\n {\"name\":\"");
out.push_str(m.name);
out.push_str("\",\"signature\":\"");
out.push_str(m.signature);
out.push('"');
if let Some(doc) = m.doc {
out.push_str(",\"doc\":\"");
out.push_str(&escape_json(doc));
out.push('"');
}
out.push('}');
}
out.push_str("\n ]");
}
out.push_str("\n },\n \"variables\": [");
for (i, v) in self.variables.iter().enumerate() {
if i > 0 { out.push(','); }
out.push_str("\n {\"name\":\"");
out.push_str(&escape_json(&v.name));
out.push_str("\",\"type\":\"");
out.push_str(&v.type_name);
out.push('"');
if let Some(doc) = &v.doc {
out.push_str(",\"doc\":\"");
out.push_str(&escape_json(doc));
out.push('"');
}
if let Some(fields) = &v.fields {
out.push_str(",\"fields\":[");
for (j, f) in fields.iter().enumerate() {
if j > 0 { out.push(','); }
out.push_str("{\"name\":\"");
out.push_str(&escape_json(&f.name));
out.push_str("\",\"type\":\"");
out.push_str(&f.type_name);
out.push_str("\"}");
}
out.push(']');
}
out.push('}');
}
out.push_str("\n ]\n}");
out
}
}
fn escape_json(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
fn builtin_functions() -> Vec<FunctionInfo> {
vec![
FunctionInfo { name: "log", signature: "(...args) -> null", doc: Some("Print values to output") },
FunctionInfo { name: "rand", signature: "(min, max) -> Number", doc: Some("Random integer between min and max (inclusive)") },
]
}
fn builtin_methods() -> Vec<(&'static str, Vec<MethodInfo>)> {
vec![
("String", vec![
MethodInfo { name: "upper", signature: "() -> String", doc: None },
MethodInfo { name: "lower", signature: "() -> String", doc: None },
MethodInfo { name: "trim", signature: "() -> String", doc: None },
MethodInfo { name: "trimStart", signature: "() -> String", doc: None },
MethodInfo { name: "trimEnd", signature: "() -> String", doc: None },
MethodInfo { name: "split", signature: "(delim: String) -> StringList", doc: None },
MethodInfo { name: "replace", signature: "(old: String, new: String) -> String", doc: None },
MethodInfo { name: "contains", signature: "(substr: String) -> Boolean", doc: None },
MethodInfo { name: "startsWith", signature: "(prefix: String) -> Boolean", doc: None },
MethodInfo { name: "endsWith", signature: "(suffix: String) -> Boolean", doc: None },
MethodInfo { name: "length", signature: "() -> Number", doc: None },
MethodInfo { name: "charAt", signature: "(index: Number) -> String", doc: None },
MethodInfo { name: "substring", signature: "(start: Number, end?: Number) -> String", doc: None },
]),
("Number", vec![]),
("Boolean", vec![]),
("NumberList", vec![
MethodInfo { name: "length", signature: "() -> Number", doc: None },
MethodInfo { name: "len", signature: "() -> Number", doc: None },
MethodInfo { name: "isEmpty", signature: "() -> Boolean", doc: None },
MethodInfo { name: "first", signature: "() -> Number", doc: None },
MethodInfo { name: "last", signature: "() -> Number", doc: None },
MethodInfo { name: "get", signature: "(index: Number) -> Number", doc: None },
MethodInfo { name: "contains", signature: "(value: Number) -> Boolean", doc: None },
MethodInfo { name: "indexOf", signature: "(value: Number) -> Number", doc: None },
MethodInfo { name: "slice", signature: "(start: Number, end?: Number) -> NumberList", doc: None },
MethodInfo { name: "reverse", signature: "() -> NumberList", doc: None },
MethodInfo { name: "sort", signature: "() -> NumberList", doc: None },
MethodInfo { name: "sum", signature: "() -> Number", doc: None },
MethodInfo { name: "avg", signature: "() -> Number", doc: None },
MethodInfo { name: "min", signature: "() -> Number", doc: None },
MethodInfo { name: "max", signature: "() -> Number", doc: None },
]),
("Object", vec![
MethodInfo { name: "keys", signature: "() -> StringList", doc: Some("Get all keys") },
MethodInfo { name: "values", signature: "() -> StringList | NumberList", doc: Some("Get all values (must be same type)") },
MethodInfo { name: "length", signature: "() -> Number", doc: Some("Number of entries") },
MethodInfo { name: "len", signature: "() -> Number", doc: None },
MethodInfo { name: "contains", signature: "(key: String) -> Boolean", doc: Some("Check if key exists") },
MethodInfo { name: "get", signature: "(key: String) -> any", doc: Some("Get value by key") },
]),
("StringList", vec![
MethodInfo { name: "length", signature: "() -> Number", doc: None },
MethodInfo { name: "len", signature: "() -> Number", doc: None },
MethodInfo { name: "isEmpty", signature: "() -> Boolean", doc: None },
MethodInfo { name: "first", signature: "() -> String", doc: None },
MethodInfo { name: "last", signature: "() -> String", doc: None },
MethodInfo { name: "get", signature: "(index: Number) -> String", doc: None },
MethodInfo { name: "contains", signature: "(value: String) -> Boolean", doc: None },
MethodInfo { name: "indexOf", signature: "(value: String) -> Number", doc: None },
MethodInfo { name: "slice", signature: "(start: Number, end?: Number) -> StringList", doc: None },
MethodInfo { name: "reverse", signature: "() -> StringList", doc: None },
MethodInfo { name: "sort", signature: "() -> StringList", doc: None },
MethodInfo { name: "join", signature: "(delim?: String) -> String", doc: None },
]),
]
}
+14
View File
@@ -0,0 +1,14 @@
pub mod parser;
pub mod compiler;
pub mod vm;
pub mod opcodes;
pub mod ast;
pub mod bytecode;
pub mod bytecode_dump;
pub mod language_info;
// Re-export dependency types used in public API
pub use rust_decimal::Decimal;
pub use rust_decimal_macros::dec;
pub use smol_str::SmolStr;
pub use indexmap::IndexMap;
+38
View File
@@ -0,0 +1,38 @@
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 ast = parser::program(input)?;
let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast)?;
let num = dec!(3);
let mut vm = VM::new(&bytecode);
vm.set_global("test", Value::Number(num));
let res = vm.execute();
if res.is_err() {
println!("Error: {:?}", res.unwrap_err());
}
let code = r#"x = 10
y = 0
result = x / y"#;
let mut compiler = Compiler::new();
let (bytecode, debug_info) = compiler
.compile_from_source(code)
.expect("Failed to compile");
let mut vm = VM::new(&bytecode);
vm.set_debug_info(&debug_info);
let err = vm.execute().unwrap_err();
let err_msg = err.to_string();
println!("Error message:\n{}", err_msg);
Ok(())
}
+184
View File
@@ -0,0 +1,184 @@
/// 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;
// Future: ABS = 1, MIN = 2, MAX = 3, FLOOR = 4, CEIL = 5, ROUND = 6, ...
/// Lookup table: function name → ID
pub const NAMES: &[(&str, u8)] = &[("rand", RAND)];
/// 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)
// 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),
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::End => "End",
}
}
}
+430
View File
@@ -0,0 +1,430 @@
use rust_decimal::Decimal;
use smol_str::SmolStr;
use std::str::FromStr;
use crate::ast::{
expr::{Expr, Op},
stmt::Stmt,
value::Value,
};
peg::parser!(
pub grammar parser() for str {
pub rule program() -> Vec<Stmt>
= s:statement()* { s }
/// Parse program with source location info for each statement
pub rule program_with_spans() -> Vec<(usize, Stmt)>
= s:statement_with_pos()* { s }
/// Statement with position info (byte offset)
rule statement_with_pos() -> (usize, Stmt)
= whitespace()?
pos:position!()
s:(
assignment()
/ if_stmt()
/ expr_stmt()
)
whitespace()? { (pos, s) }
pub rule statement() -> Stmt
= whitespace()?
s:(
assignment()
/ if_stmt()
/ expr_stmt()
)
whitespace()? { s }
pub rule expression() -> Expr
= binary_op()
pub rule mul_div() -> Expr =
left:power() mul_div_right:(
_ op:$("*" / "/" / "%") _ right:power()
{ (op, right) }
)* {
let mut result = left;
for (op, right) in mul_div_right {
result = match op {
"*" => Expr::BinaryOp(Box::new(result), Op::Mul, Box::new(right)),
"/" => Expr::BinaryOp(Box::new(result), Op::Div, Box::new(right)),
"%" => Expr::BinaryOp(Box::new(result), Op::Mod, Box::new(right)),
_ => unreachable!()
};
}
result
}
pub rule power() -> Expr =
base:postfix() _ "**" _ exp:power() { 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::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::Add, Box::new(y)) }
x:@ _ "-" _ y:(@) { Expr::BinaryOp(Box::new(x),Op::Sub, Box::new(y)) }
--
x:mul_div() { x }
--
p:postfix() { p }
}
/// Postfix operations: property access and method calls with chaining
rule postfix() -> Expr
= base:atom() chain:(
"." m:identifier() "(" args:((_ e:expression() _ {e}) ** ",") ")" { (m, Some(args)) }
/ "." p:identifier() { (p, None) }
)* {
let mut result = base;
for (name, args) in chain {
if let Some(args) = args {
result = Expr::MethodCall(Box::new(result), name, args);
} else {
result = Expr::PropertyAccess(Box::new(result), name);
}
}
result
}
rule atom() -> Expr
= i:identifier() { Expr::Variable(i) }
/ i:string() { Expr::Value(Value::String(i)) }
/ 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:$(([^'"'] / "\\\"")*) "\"" {
s.replace("\\\"", "\"").into()
}
/ "'" s:$(([^'\''] / "\\''")*) "'" {
s.replace("\\'", "'").into()
}
rule boolean_literal() -> Value
= "true" { Value::Boolean(true) }
/ "false" { Value::Boolean(false) }
pub rule expr_stmt() -> Stmt
= e:expression() { Stmt::ExprStmt(Box::new(e)) }
pub rule if_stmt() -> Stmt
= "if" _ cond:expression() whitespace()? "then" whitespace()?
then_body:statement()* whitespace()?
else_part:else_clause()?
"end" whitespace()? {
Stmt::If(Box::new(cond), then_body, else_part)
}
pub rule else_clause() -> Vec<Stmt>
= "else if" whitespace()? cond:expression() whitespace()? "then" whitespace()?
then_body:statement()* whitespace()?
else_part:else_clause()? whitespace()? {
vec![Stmt::If(Box::new(cond), then_body, else_part)]
}
/ "else" whitespace()? else_body:statement()* whitespace()? {
else_body
}
pub rule assignment() -> Stmt
= i:identifier() path:("." p:identifier() { p })+ _ "=" _ value:expression() {
Stmt::PropertyAssignment(i, path, Box::new(value))
}
/ i:identifier() _ op:compound_op() _ value:expression() {
// Desugar compound assignment: x += 1 becomes x = x + 1
let var_expr = Expr::Variable(i.clone());
let combined = Expr::BinaryOp(Box::new(var_expr), op, Box::new(value));
Stmt::Assignment(i, Box::new(combined))
}
/ i:identifier() _ "=" _ value:expression() { Stmt::Assignment(i, Box::new(value)) }
rule compound_op() -> Op
= "+=" { Op::Add }
/ "-=" { Op::Sub }
/ "*=" { Op::Mul }
/ "/=" { Op::Div }
/ "%=" { Op::Mod }
rule keyword()
= ("if" / "then" / "else" / "end" / "true" / "false" / "in") !['a'..='z' | 'A'..='Z' | '0'..='9' | '_']
rule identifier() -> SmolStr
= !keyword() s:$(['a'..='z' | 'A'..='Z' | '_']['a'..='z' | 'A'..='Z' | '0'..='9' | '_']*)
{ s.into() }
rule number() -> Decimal
= n:$(['0'..='9']+ ("." ['0'..='9']+)?) {?
Decimal::from_str(n).map_err(|_| "invalid decimal")
}
rule whitespace()
= ([' ' | '\t' | '\n' | '\r'] / comment())+
rule comment()
= "//" [^'\n']* "\n"?
/ "/*" (!"*/" [_])* "*/"
rule _() = quiet!{([' ' | '\t'] / comment())*}
// rule string_lit() -> Expr
// = "\"" s:$([^'"']*) "\""
// { Expr::String(s.to_string()) }
}
);
#[cfg(test)]
mod tests {
use super::*;
#[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"),
}
}
_ => panic!("Expected addition at top level"),
}
} else {
panic!("Failed to parse expression");
}
}
#[test]
fn test_function_call() {
assert!(matches!(
parser::expression("add(1, 2)"),
Ok(Expr::FunctionCall(_, _))
));
}
#[test]
fn test_var_decl() {
let input = "x = 1";
let res = parser::assignment(input);
assert!(matches!(res, Ok(Stmt::Assignment(_, _))));
}
#[test]
fn test_simple_arithmetic() {
let input = "x = 1 + 2 * 3";
let result = parser::program(input).unwrap();
assert_eq!(result.len(), 1);
if let Stmt::Assignment(name, expr) = &result[0] {
assert_eq!(name, "x");
if let Expr::BinaryOp(left, op, right) = expr.as_ref() {
assert!(matches!(op, Op::Add));
assert!(matches!(**left, Expr::Value(Value::Number(_))));
if let Expr::BinaryOp(mul_left, mul_op, mul_right) = right.as_ref() {
assert!(matches!(mul_op, Op::Mul));
assert!(matches!(**mul_left, Expr::Value(Value::Number(_))));
assert!(matches!(**mul_right, Expr::Value(Value::Number(_))));
} else {
panic!("Expected multiplication operation");
}
} else {
panic!("Expected binary operation");
}
} else {
panic!("Expected assignment statement");
}
}
#[test]
fn test_if_statement() {
let input = "if x < 10 then y = x else y = 0 end";
let result = parser::program(input).unwrap();
assert_eq!(result.len(), 1);
if let Stmt::If(condition, then_branch, else_branch) = &result[0] {
// Check condition
if let Expr::BinaryOp(left, op, right) = condition.as_ref() {
assert!(matches!(op, Op::Lt));
assert!(matches!(**left, Expr::Variable(_)));
assert!(matches!(**right, Expr::Value(Value::Number(_))));
} else {
panic!("Expected binary operation in condition");
}
// Check then branch
assert_eq!(then_branch.len(), 1);
assert!(matches!(&then_branch[0], Stmt::Assignment(_, _)));
// Check else branch
assert!(else_branch.is_some());
let else_branch = else_branch.as_ref().unwrap();
assert_eq!(else_branch.len(), 1);
assert!(matches!(&else_branch[0], Stmt::Assignment(_, _)));
} else {
panic!("Expected if statement");
}
}
#[test]
fn test_nested_function_calls() {
let input = "result = max(min(a, b), abs(c))";
let result = parser::program(input).unwrap();
assert_eq!(result.len(), 1);
if let Stmt::Assignment(name, expr) = &result[0] {
assert_eq!(name, "result");
if let Expr::FunctionCall(func_name, args) = expr.as_ref() {
assert_eq!(func_name, "max");
assert_eq!(args.len(), 2);
// Check first argument (min call)
if let Expr::FunctionCall(inner_func, inner_args) = &args[0] {
assert_eq!(inner_func, "min");
assert_eq!(inner_args.len(), 2);
} else {
panic!("Expected min function call");
}
// Check second argument (abs call)
if let Expr::FunctionCall(inner_func, inner_args) = &args[1] {
assert_eq!(inner_func, "abs");
assert_eq!(inner_args.len(), 1);
} else {
panic!("Expected abs function call");
}
} else {
panic!("Expected function call");
}
}
}
#[test]
fn test_decimal_numbers() {
let input = "x = 123.456";
let result = parser::program(input).unwrap();
if let Stmt::Assignment(_, expr) = &result[0] {
if let Expr::Value(Value::Number(n)) = expr.as_ref() {
assert_eq!(*n, Decimal::from_str("123.456").unwrap());
} else {
panic!("Expected decimal number");
}
}
}
#[test]
fn test_complex_nested_if() {
let input = r#"
if x > 0 then
if y > 0 then
result = x + y
else
result = x - y
end
else
result = 0
end
"#;
let result = parser::program(input).unwrap();
assert_eq!(result.len(), 1);
if let Stmt::If(_, then_branch, else_branch) = &result[0] {
// Check that then_branch contains another if statement
assert_eq!(then_branch.len(), 1);
assert!(matches!(&then_branch[0], Stmt::If(_, _, _)));
// Check else branch
assert!(else_branch.is_some());
let else_branch = else_branch.as_ref().unwrap();
assert_eq!(else_branch.len(), 1);
assert!(matches!(&else_branch[0], Stmt::Assignment(_, _)));
}
}
#[test]
fn test_syntax_errors() {
// Missing 'end' keyword
assert!(parser::program("if x < 10 then y = x").is_err());
// Invalid expression
assert!(parser::program("x = 1 + * 2").is_err());
}
#[test]
fn test_whitespace_handling() {
let input1 = "x=1+2";
let input2 = "x = 1 + 2";
let input3 = "x = 1 + 2";
let result1 = parser::program(input1).unwrap();
let result2 = parser::program(input2).unwrap();
let result3 = parser::program(input3).unwrap();
// All should produce equivalent ASTs
assert_eq!(result1, result2);
assert_eq!(result2, result3);
}
#[test]
fn test_compound_assignment_parsing() {
let input = "x += 5";
let result = parser::program(input);
println!("Result: {:?}", result);
let result = result.unwrap();
assert_eq!(result.len(), 1);
if let Stmt::Assignment(name, expr) = &result[0] {
assert_eq!(name, "x");
// Should be desugared to x + 5
if let Expr::BinaryOp(left, op, right) = expr.as_ref() {
assert!(matches!(op, Op::Add));
// left should be Variable("x")
assert!(matches!(**left, Expr::Variable(_)));
// right should be Number(5)
assert!(matches!(**right, Expr::Value(Value::Number(_))));
} else {
panic!("Expected BinaryOp after desugaring, got {:?}", expr);
}
} else {
panic!("Expected Assignment, got {:?}", result[0]);
}
}
}
+29
View File
@@ -0,0 +1,29 @@
mod grammar;
use crate::ast::expr::Span;
pub use grammar::parser::program;
pub use grammar::parser::program_with_spans;
/// Convert a byte offset in source code to line and column numbers
/// Lines and columns are 1-indexed
pub fn offset_to_span(source: &str, offset: usize) -> Span {
let mut line = 1u32;
let mut col = 1u32;
let mut current_offset = 0;
for ch in source.chars() {
if current_offset >= offset {
break;
}
if ch == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
current_offset += ch.len_utf8();
}
Span::new(line, col)
}
+28
View File
@@ -0,0 +1,28 @@
a = 10.2
b = a + 5.5
c = 2.4
log(b + c)
log("Merhaba" + " duhan".upper())
log((3+3)*2/3)
t = 3+3
log(t*2/3)
if true then
log("true")
else
log("false")
end
aaa = false
if false && true then
log("11")
else if 2 > 1 && 2 >= 2 && !aaa then
log("22")
else if true then
log("33")
end
+28
View File
@@ -0,0 +1,28 @@
a = 10.2
b = a + 5.5
c = 2.4
b + c
"Merhaba" + " duhan".upper()
(3+3)*2/3
t = 3+3
t*2/3
if true then
"true"
else
"false"
end
aaa = false
if false && true then
"11"
else if 2 > 1 && 2 >= 2 && !aaa then
"22"
else if true then
"33"
end
+23
View File
@@ -0,0 +1,23 @@
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
+95
View File
@@ -0,0 +1,95 @@
use crate::ast::expr::Span;
/// Debug information that maps bytecode offsets to source locations.
/// Uses a run-length encoded format: each entry covers instructions from
/// its offset until the next entry's offset.
#[derive(Debug, Clone, Default)]
pub struct DebugInfo {
/// Sorted list of (bytecode_offset, span) pairs
entries: Vec<(u32, Span)>,
}
impl DebugInfo {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
/// Add a mapping from a bytecode offset to a source span.
/// Entries must be added in increasing offset order.
pub fn add_entry(&mut self, offset: u32, span: Span) {
// Only add if different from the last entry's span
if let Some((_, last_span)) = self.entries.last() {
if *last_span == span {
return;
}
}
self.entries.push((offset, span));
}
/// Look up the source span for a given bytecode offset.
/// Returns None if no debug info is available.
pub fn get_span(&self, offset: u32) -> Option<Span> {
if self.entries.is_empty() {
return None;
}
// Binary search for the largest offset <= target
match self.entries.binary_search_by_key(&offset, |(off, _)| *off) {
Ok(idx) => Some(self.entries[idx].1),
Err(idx) => {
if idx == 0 {
// Before the first entry
None
} else {
// Use the previous entry
Some(self.entries[idx - 1].1)
}
}
}
}
/// Check if debug info is available
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// Get the number of entries
pub fn len(&self) -> usize {
self.entries.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_debug_info_lookup() {
let mut info = DebugInfo::new();
info.add_entry(0, Span::new(1, 1));
info.add_entry(10, Span::new(2, 5));
info.add_entry(20, Span::new(3, 10));
// Exact matches
assert_eq!(info.get_span(0), Some(Span::new(1, 1)));
assert_eq!(info.get_span(10), Some(Span::new(2, 5)));
assert_eq!(info.get_span(20), Some(Span::new(3, 10)));
// In-between values use previous entry
assert_eq!(info.get_span(5), Some(Span::new(1, 1)));
assert_eq!(info.get_span(15), Some(Span::new(2, 5)));
assert_eq!(info.get_span(100), Some(Span::new(3, 10)));
}
#[test]
fn test_duplicate_spans_not_added() {
let mut info = DebugInfo::new();
info.add_entry(0, Span::new(1, 1));
info.add_entry(5, Span::new(1, 1)); // Same span, should not add
info.add_entry(10, Span::new(2, 1));
assert_eq!(info.len(), 2);
}
}
+64
View File
@@ -0,0 +1,64 @@
use crate::ast::expr::Span;
use smol_str::SmolStr;
use thiserror::Error;
/// Errors that can occur during VM execution
#[derive(Debug, Error)]
pub enum VMError {
/// Error when types don't match the operation's expectations
#[error("Type error: expected {expected}, got {got}")]
TypeMismatch { expected: String, got: String },
/// Error when a variable is not defined
#[error("Undefined variable: {0}")]
UndefinedVariable(SmolStr),
/// Error when dividing by zero
#[error("Division by zero")]
DivisionByZero,
/// Error in bytecode format or execution
#[error("Bytecode error: {0}")]
BytecodeError(String),
/// Error when a method is not found for a type
#[error("Method '{method}' not found for type '{type_name}'")]
MethodNotFound {
type_name: &'static str,
method: SmolStr,
},
/// Generic runtime error
#[error("Runtime error: {0}")]
RuntimeError(String),
/// Error when an invalid operation is performed
#[error("Type error: cannot {operation} {left_type} and {right_type}")]
InvalidOperation {
operation: &'static str,
left_type: &'static str,
right_type: &'static str,
},
/// Error with source location information
#[error("Error at {span}: {message}")]
WithLocation { span: Span, message: String },
}
impl VMError {
/// Wrap this error with source location information
pub fn with_span(self, span: Span) -> Self {
// Don't double-wrap location errors
if matches!(self, VMError::WithLocation { .. }) {
return self;
}
// Only wrap if we have a valid span (non-zero)
if span.line == 0 && span.column == 0 {
return self;
}
VMError::WithLocation {
span,
message: self.to_string(),
}
}
}
+7
View File
@@ -0,0 +1,7 @@
mod debug_info;
pub mod error;
mod vm;
pub use debug_info::DebugInfo;
pub use error::VMError;
pub use vm::{ExternalFn, ExternalMethod, VM};
+1417
View File
File diff suppressed because it is too large Load Diff