mirror of
https://github.com/duhanbalci/dexpr.git
synced 2026-08-30 16:01:41 +00:00
initial commit
This commit is contained in:
@@ -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)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod expr;
|
||||
pub mod stmt;
|
||||
pub mod value;
|
||||
@@ -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>;
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 ® 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 ® 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 ® 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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user