release 0.1.0

This commit is contained in:
rus07tam 2026-05-06 12:21:06 +03:00
commit 30d94536a9
90 changed files with 7722 additions and 0 deletions

14
src/builtins/ast.rs Normal file
View file

@ -0,0 +1,14 @@
use crate::core::{OwaValue, Scope, except_arity};
#[must_use]
pub fn owa_pack() -> OwaValue {
OwaValue::native_macro(|_, _, args| {
let [arg] = except_arity(args)?;
Ok(arg.pack().into())
})
}
#[must_use]
pub fn scope() -> Scope {
Scope::new_with_runtime([("pack", owa_pack())])
}

97
src/builtins/cond.rs Normal file
View file

@ -0,0 +1,97 @@
use crate::{
core::{OwaError, OwaValue, Scope, except_arity, except_arity_min},
parser::ast::OwaAst,
};
#[must_use]
pub fn owa_if_eq() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let [cond_ast, then_ast, else_ast] = except_arity(args)?;
let cond_ast = cond_ast.as_call()?;
let mut result = true;
let mut prev = None;
for arg in cond_ast {
if !result {
break;
}
// Condition args are evaluated as values (control-flow here is a programming error)
let arg = interpreter.eval_value(arg, scope_id)?;
if let Some(prev) = prev {
result = prev == arg;
}
prev = Some(arg);
}
if result {
// Branch body — propagate Completion (break/continue/return pass through)
interpreter.eval(then_ast, scope_id)
} else {
interpreter.eval(else_ast, scope_id)
}
})
}
#[must_use]
pub fn owa_if_has() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let [target_ast, cond_ast, then_ast, else_ast] = except_arity(args)?;
let target_ast = interpreter.eval_value(target_ast, scope_id)?;
let items_ast = cond_ast.as_call()?;
for arg in items_ast {
let arg = interpreter.eval_value(arg, scope_id)?;
if arg == target_ast {
return interpreter.eval(then_ast, scope_id);
}
}
interpreter.eval(else_ast, scope_id)
})
}
#[must_use]
pub fn owa_match() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let ([value_ast], branches) = except_arity_min(args)?;
// Evaluate the matched value
let value = interpreter.eval_value(value_ast, scope_id)?;
let mut default = None;
for branch in branches {
let branch = branch.as_call()?;
let [pattern_ast, expr_ast] = except_arity(branch)?;
if pattern_ast == &OwaAst::sym("_") {
default = Some(expr_ast);
continue;
}
// Evaluate pattern as a value
let pattern = interpreter.eval_value(pattern_ast, scope_id)?;
if pattern == value {
// Branch body — propagate Completion
return interpreter.eval(expr_ast, scope_id);
}
}
default.map_or_else(
|| {
Err(OwaError::RuntimeError(OwaValue::Str(
"Unmatched pattern".into(),
)))
},
|default| interpreter.eval(default, scope_id),
)
})
}
#[must_use]
pub fn scope() -> Scope {
Scope::new_with_runtime([
("if_eq", owa_if_eq()),
("if_has", owa_if_has()),
("match", owa_match()),
])
}

83
src/builtins/errors.rs Normal file
View file

@ -0,0 +1,83 @@
use crate::{
core::{Completion, OwaError, OwaValue, Scope, except_arity, except_arity_min},
parser::ast::OwaAst,
};
#[must_use]
pub fn owa_throw_panic() -> OwaValue {
OwaValue::native_function(|_, _, args| {
let [message] = except_arity(args)?;
let message = message.as_str()?;
Err(OwaError::Panic(message.to_string()))
})
}
#[must_use]
pub fn owa_throw_type_error() -> OwaValue {
OwaValue::native_function(|_, _, args| {
let [expected, got] = except_arity(args)?;
let expected = expected.as_str()?;
let got = got.as_str()?;
Err(OwaError::TypeError(expected.to_string(), got.to_string()))
})
}
#[must_use]
pub fn owa_throw_arity_error() -> OwaValue {
OwaValue::native_function(|_, _, args| {
let [expected, got] = except_arity(args)?;
let expected = expected.as_int()?;
let got = got.as_int()?;
Err(OwaError::ArityError(expected as usize, got as usize))
})
}
#[must_use]
pub fn owa_throw_runtime_error() -> OwaValue {
OwaValue::native_function(|_, _, args| {
let [value] = except_arity(args)?;
Err(OwaError::RuntimeError(value.clone()))
})
}
#[must_use]
pub fn owa_try_catch() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let ([try_expr], catch_exprs) = except_arity_min(args)?;
// Normalise both signal channels first so that control-flow signals
// (e.g. `break` inside try) are never mistaken for real errors.
match Completion::from_result(interpreter.eval(try_expr, scope_id)) {
// Propagate any completion (Value, Break, Continue, Return) unchanged
Ok(completion) => Ok(completion),
// Signal errors that escaped normalisation are also propagated
Err(OwaError::Signal(f)) => Ok(f.into()),
Err(err) => {
for catch_expr in catch_exprs {
let catch_call = OwaAst::call([
catch_expr.clone(),
OwaAst::str(err.message()),
OwaAst::kw(err.type_name()),
]);
// Run catch handler; propagate control flow if it emits any
match Completion::from_result(interpreter.eval(&catch_call, scope_id))? {
Completion::Value(_) => {} // discard value
c => return Ok(c), // propagate Break/Continue/Return
}
}
Err(OwaError::Panic(format!("Unhandled error: {err}")))
}
}
})
}
#[must_use]
pub fn scope() -> Scope {
Scope::new_with_runtime([
("throw_panic", owa_throw_panic()),
("throw_type_error", owa_throw_type_error()),
("throw_arity_error", owa_throw_arity_error()),
("throw_runtime_error", owa_throw_runtime_error()),
("try", owa_try_catch()),
])
}

299
src/builtins/ffi.rs Normal file
View file

@ -0,0 +1,299 @@
use std::{
collections::HashMap,
ffi::{CStr, CString, c_char, c_void},
sync::{Mutex, OnceLock},
};
use libffi::middle::{Arg, Cif, CodePtr, Type};
use libloading::Library;
use crate::core::{OwaError, OwaValue, Scope, except_arity};
static LIBRARIES: OnceLock<Mutex<HashMap<String, Library>>> = OnceLock::new();
fn libraries() -> &'static Mutex<HashMap<String, Library>> {
LIBRARIES.get_or_init(|| Mutex::new(HashMap::new()))
}
fn runtime_err(msg: impl AsRef<str>) -> OwaError {
OwaError::RuntimeError(OwaValue::str(msg.as_ref()))
}
fn kw_to_ffi_type(kw: &str) -> Result<Type, OwaError> {
Ok(match kw {
"void" => Type::void(),
"ptr" | "str" => Type::pointer(), // both are just pointers at the ABI level
"i8" => Type::i8(),
"i16" => Type::i16(),
"i32" => Type::i32(),
"i64" => Type::i64(),
"u8" => Type::u8(),
"u16" => Type::u16(),
"u32" => Type::u32(),
"u64" => Type::u64(),
"f32" => Type::f32(),
"f64" => Type::f64(),
_ => return Err(runtime_err(format!("Unknown FFI type keyword: :{kw}"))),
})
}
// libffi expects a pointer-to-argument for each arg (void**). We box every
// value so the address stays stable regardless of Vec reallocations.
enum ArgBacking {
I8(Box<i8>),
I16(Box<i16>),
I32(Box<i32>),
I64(Box<i64>),
U8(Box<u8>),
U16(Box<u16>),
U32(Box<u32>),
U64(Box<u64>),
F32(Box<f32>),
F64(Box<f64>),
Ptr(Box<*const c_void>),
// _cstring keeps the allocation alive; ptr is a boxed *const c_char into it
Str {
_cstring: Box<CString>,
ptr: Box<*const c_char>,
},
}
impl ArgBacking {
fn to_ffi_arg(&self) -> Arg<'_> {
match self {
Self::I8(v) => Arg::new(v.as_ref()),
Self::I16(v) => Arg::new(v.as_ref()),
Self::I32(v) => Arg::new(v.as_ref()),
Self::I64(v) => Arg::new(v.as_ref()),
Self::U8(v) => Arg::new(v.as_ref()),
Self::U16(v) => Arg::new(v.as_ref()),
Self::U32(v) => Arg::new(v.as_ref()),
Self::U64(v) => Arg::new(v.as_ref()),
Self::F32(v) => Arg::new(v.as_ref()),
Self::F64(v) => Arg::new(v.as_ref()),
Self::Ptr(p) => Arg::new(p.as_ref()),
Self::Str { ptr, .. } => Arg::new(ptr.as_ref()),
}
}
}
#[allow(clippy::cast_precision_loss)]
fn prepare_backing(type_kw: &str, value: &OwaValue) -> Result<ArgBacking, OwaError> {
Ok(match type_kw {
"i8" => ArgBacking::I8(Box::new(value.as_int()? as i8)),
"i16" => ArgBacking::I16(Box::new(value.as_int()? as i16)),
"i32" => ArgBacking::I32(Box::new(value.as_int()? as i32)),
"i64" => ArgBacking::I64(Box::new(value.as_int()?)),
"u8" => ArgBacking::U8(Box::new(value.as_int()? as u8)),
"u16" => ArgBacking::U16(Box::new(value.as_int()? as u16)),
"u32" => ArgBacking::U32(Box::new(value.as_int()? as u32)),
"u64" => ArgBacking::U64(Box::new(value.as_int()? as u64)),
"f32" => {
let v = match value {
OwaValue::Float(f) => f.0 as f32,
OwaValue::Int(i) => *i as f32,
_ => {
return Err(OwaError::TypeError(
"float or int".into(),
value.type_name().into(),
));
}
};
ArgBacking::F32(Box::new(v))
}
"f64" => {
let v = match value {
OwaValue::Float(f) => f.0,
OwaValue::Int(i) => *i as f64,
_ => {
return Err(OwaError::TypeError(
"float or int".into(),
value.type_name().into(),
));
}
};
ArgBacking::F64(Box::new(v))
}
"ptr" => {
let raw = value.as_int()? as *const c_void;
ArgBacking::Ptr(Box::new(raw))
}
"str" => {
let s = value.as_str()?;
let cstring = Box::new(
CString::new(s.as_bytes())
.map_err(|e| runtime_err(format!("Invalid string for FFI: {e}")))?,
);
// box the pointer separately so libffi can dereference it once
let ptr = Box::new(cstring.as_ptr());
ArgBacking::Str {
_cstring: cstring,
ptr,
}
}
_ => return Err(runtime_err(format!("Unknown FFI type keyword: :{type_kw}"))),
})
}
// # Safety: caller must ensure func_ptr matches the cif signature
#[allow(clippy::cast_lossless, clippy::cast_possible_wrap)]
unsafe fn dispatch_call(
cif: &Cif,
func_ptr: CodePtr,
ffi_args: &[Arg<'_>],
return_type: &str,
) -> Result<OwaValue, OwaError> {
Ok(match return_type {
"void" => {
// libffi doesn't write the return slot for FFI_TYPE_VOID, so () is fine
unsafe { cif.call::<()>(func_ptr, ffi_args) };
OwaValue::int(0)
}
"i8" => OwaValue::int(unsafe { cif.call::<i8>(func_ptr, ffi_args) } as i64),
"i16" => OwaValue::int(unsafe { cif.call::<i16>(func_ptr, ffi_args) } as i64),
"i32" => OwaValue::int(unsafe { cif.call::<i32>(func_ptr, ffi_args) } as i64),
"i64" => OwaValue::int(unsafe { cif.call::<i64>(func_ptr, ffi_args) }),
"u8" => OwaValue::int(unsafe { cif.call::<u8>(func_ptr, ffi_args) } as i64),
"u16" => OwaValue::int(unsafe { cif.call::<u16>(func_ptr, ffi_args) } as i64),
"u32" => OwaValue::int(unsafe { cif.call::<u32>(func_ptr, ffi_args) } as i64),
"u64" => OwaValue::int(unsafe { cif.call::<u64>(func_ptr, ffi_args) } as i64),
"f32" => OwaValue::float(unsafe { cif.call::<f32>(func_ptr, ffi_args) } as f64),
"f64" => OwaValue::float(unsafe { cif.call::<f64>(func_ptr, ffi_args) }),
"ptr" => OwaValue::int(unsafe { cif.call::<usize>(func_ptr, ffi_args) } as i64),
"str" => {
let raw = unsafe { cif.call::<*const c_char>(func_ptr, ffi_args) };
if raw.is_null() {
OwaValue::str("")
} else {
let cstr = unsafe { CStr::from_ptr(raw) };
OwaValue::str(cstr.to_string_lossy().as_ref())
}
}
_ => {
return Err(runtime_err(format!(
"Unknown FFI return type: :{return_type}"
)));
}
})
}
#[must_use]
pub fn owa_load() -> OwaValue {
OwaValue::native_function(|_, _, args| {
let [libname] = except_arity(args)?;
let libname = libname.clone();
let libname_str = libname.as_str()?;
if libraries()
.lock()
.map_err(|_| runtime_err("Failed to acquire library registry lock"))?
.contains_key(libname_str.as_ref())
{
return Ok(libname.into());
}
// Load the library without holding the lock to avoid unnecessary contention
let lib = unsafe {
Library::new(libname_str.as_ref())
.map_err(|e| runtime_err(format!("Failed to load '{libname_str}': {e}")))?
};
libraries()
.lock()
.map_err(|_| runtime_err("Failed to acquire library registry lock"))?
.insert(libname_str.to_string(), lib);
Ok(libname.into())
})
}
#[must_use]
pub fn owa_unload() -> OwaValue {
OwaValue::native_function(|_, _, args| {
let [libname] = except_arity(args)?;
let libname = libname.clone();
let libname_str = libname.as_str()?;
libraries()
.lock()
.map_err(|_| runtime_err("Failed to acquire library registry lock"))?
.remove(libname_str.as_ref());
Ok(libname.into())
})
}
// (ffi.call libname funcname [[param-type ...] return-type] [args ...])
//
// Supported types: :void :ptr :str :i8 :i16 :i32 :i64 :u8 :u16 :u32 :u64 :f32 :f64
// :str — takes OwaValue::Str, passes const char*
// :ptr — takes OwaValue::Int as a raw address
#[must_use]
pub fn owa_call() -> OwaValue {
OwaValue::native_function(|_, _, args| {
let [libname, funcname, signature, call_args] = except_arity(args)?;
let libname = libname.as_str()?;
let funcname = funcname.as_str()?;
let signature = signature.as_vec()?;
let call_args = call_args.as_vec()?;
if signature.len() != 2 {
return Err(runtime_err(
"Signature must be [[param-type ...] return-type]",
));
}
let param_types: Vec<_> = signature[0]
.as_vec()?
.iter()
.map(OwaValue::as_kw)
.collect::<Result<_, _>>()?;
let return_type = signature[1].as_kw()?;
if param_types.len() != call_args.len() {
return Err(OwaError::ArityError(param_types.len(), call_args.len()));
}
let ffi_param_types: Vec<Type> = param_types
.iter()
.map(|kw| kw_to_ffi_type(kw.as_ref()))
.collect::<Result<_, _>>()?;
let cif = Cif::new(ffi_param_types, kw_to_ffi_type(return_type.as_ref())?);
// grab the fn pointer while holding the lock, then release before calling
let func_ptr: CodePtr = unsafe {
let libs = libraries()
.lock()
.map_err(|_| runtime_err("Failed to acquire library registry lock"))?;
let ptr = {
let sym: libloading::Symbol<unsafe extern "C" fn()> = libs
.get(libname.as_ref())
.ok_or_else(|| runtime_err(format!("Library not loaded: '{libname}'")))?
.get(funcname.as_bytes())
.map_err(|e| {
runtime_err(format!("Symbol '{funcname}' not found in '{libname}': {e}"))
})?;
// fn ptr -> usize -> *mut c_void is the portable cast in Rust
CodePtr(*sym as usize as *mut c_void)
}; // sym (and its borrow of libs) dropped here
drop(libs);
ptr
};
let backings: Vec<ArgBacking> = param_types
.iter()
.zip(call_args.iter())
.map(|(kw, val)| prepare_backing(kw.as_ref(), val))
.collect::<Result<_, _>>()?;
let ffi_args: Vec<Arg<'_>> = backings.iter().map(ArgBacking::to_ffi_arg).collect();
unsafe { dispatch_call(&cif, func_ptr, &ffi_args, return_type.as_ref()) }.map(Into::into)
})
}
#[must_use]
pub fn scope() -> Scope {
Scope::new_with_runtime([
("load", owa_load()),
("unload", owa_unload()),
("call", owa_call()),
])
}

51
src/builtins/flow.rs Normal file
View file

@ -0,0 +1,51 @@
use crate::core::{Completion, OwaError, OwaValue, Scope, special_names};
#[must_use]
pub fn owa_break() -> OwaValue {
OwaValue::native_function(|_, _, _| Ok(Completion::Break))
}
#[must_use]
pub fn owa_continue() -> OwaValue {
OwaValue::native_function(|_, _, _| Ok(Completion::Continue))
}
#[must_use]
pub fn owa_return() -> OwaValue {
OwaValue::native_function(|_, _, args| {
args.first().map_or_else(
|| Err(OwaError::ArityMinError(1, 0)),
|result| Ok(Completion::Return(result.clone())),
)
})
}
#[must_use]
pub fn owa_loop() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
'outer: loop {
for arg in args {
// Normalise both signal channels: Ok(Completion::*) from direct
// calls and Err(Signal(...)) from nested argument evaluation.
match Completion::from_result(interpreter.eval(arg, scope_id))? {
Completion::Value(_) => {}
Completion::Break => break 'outer,
Completion::Continue => continue 'outer,
Completion::Return(v) => return Ok(Completion::Value(v)),
}
}
}
Ok(OwaValue::kw(special_names::KW_NULL).into())
})
}
#[must_use]
pub fn scope() -> Scope {
Scope::new_with_runtime([
("break", owa_break()),
("continue", owa_continue()),
("return", owa_return()),
("loop", owa_loop()),
])
}

14
src/builtins/map.rs Normal file
View file

@ -0,0 +1,14 @@
use crate::core::{OwaValue, Scope};
#[must_use]
pub fn owa_map() -> OwaValue {
OwaValue::native_function(|_, _, args| {
let value = OwaValue::map(args.iter().cloned());
Ok(value.into())
})
}
#[must_use]
pub fn scope() -> Scope {
Scope::new_with_runtime([("new", owa_map())])
}

83
src/builtins/math.rs Normal file
View file

@ -0,0 +1,83 @@
#![allow(clippy::cast_precision_loss)]
use crate::core::{OwaError, OwaValue, Scope, except_arity};
macro_rules! math_binary_op {
($fn_name:ident, $int_op:tt, $float_op:tt) => {
#[must_use]
pub fn $fn_name() -> OwaValue {
OwaValue::native_function(|_, _, args: &[OwaValue]| {
match args {
[arg] => {
match (arg) {
OwaValue::Int(a) => Ok(OwaValue::Int(0 $int_op a).into()),
OwaValue::Float(a) => Ok(OwaValue::float(0.0 $float_op a.as_ref()).into()),
_ => Err(OwaError::TypeError("number".into(), arg.type_name().into())),
}
},
[first, rest @ ..] => {
let mut first = first.clone();
for arg in rest {
first = match (&first, arg) {
(OwaValue::Int(a), OwaValue::Int(b)) => OwaValue::Int(*a $int_op b),
(OwaValue::Float(a), OwaValue::Float(b)) => OwaValue::Float(*a $float_op b),
(OwaValue::Int(a), OwaValue::Float(b)) => OwaValue::float((*a as f64) $float_op b.as_ref()),
(OwaValue::Float(a), OwaValue::Int(b)) => OwaValue::Float(*a $float_op (*b as f64)),
_ => return Err(OwaError::TypeError("number".into(), arg.type_name().into())),
}
};
Ok(first.into())
},
_ => Err(OwaError::ArityMinError(1, args.len())),
}
})
}
};
}
macro_rules! math_binary_op_method {
($fn_name:ident, $int_method:ident, $float_method:ident) => {
#[must_use]
pub fn $fn_name() -> OwaValue {
OwaValue::native_function(|_, _, args: &[OwaValue]| {
let [value1, value2] = except_arity(args)?;
match (value1, value2) {
(OwaValue::Int(a), OwaValue::Int(b)) => {
Ok(OwaValue::Int(a.pow((*b).try_into().unwrap_or(0))).into())
}
(OwaValue::Float(a), OwaValue::Float(b)) => {
Ok(OwaValue::float(a.powf(**b)).into())
}
(OwaValue::Int(a), OwaValue::Float(b)) => {
Ok(OwaValue::float((*a as f64).powf(**b)).into())
}
(OwaValue::Float(a), OwaValue::Int(b)) => {
Ok(OwaValue::float(a.powf(*b as f64)).into())
}
_ => Err(OwaError::TypeError(
"number".into(),
value1.type_name().into(),
)),
}
})
}
};
}
math_binary_op!(owa_add, +, +);
math_binary_op!(owa_sub, -, -);
math_binary_op!(owa_mul, *, *);
math_binary_op!(owa_div, /, /);
math_binary_op!(owa_mod, %, %);
math_binary_op_method!(owa_pow, pow, powf);
#[must_use]
pub fn scope() -> Scope {
Scope::new_with_runtime([
("add", owa_add()),
("sub", owa_sub()),
("mul", owa_mul()),
("div", owa_div()),
("mod", owa_mod()),
("pow", owa_pow()),
])
}

324
src/builtins/mod.rs Normal file
View file

@ -0,0 +1,324 @@
use std::sync::Arc;
use rpds::VectorSync;
use crate::{
core::{Completion, Interpreter, OwaError, OwaValue, Scope, except_arity, except_arity_min},
parser::ast::OwaAst,
};
pub mod ast;
pub mod cond;
pub mod errors;
pub mod ffi;
pub mod flow;
pub mod map;
pub mod math;
pub mod platform;
pub mod set;
pub mod str;
pub mod vec;
#[must_use]
pub fn owa_apply() -> OwaValue {
OwaValue::native_function(|interpreter, _, args| {
let ([first], rest) = except_arity_min(args)?;
let calle = first.as_callable()?;
let mut flat_args: Vec<OwaValue> = Vec::with_capacity(args.len());
for arg in rest {
match arg {
OwaValue::Vec(vec) => flat_args.extend(vec.iter().cloned()),
_ => flat_args.push(arg.clone()),
}
}
calle.call(interpreter, flat_args.as_slice())
})
}
#[must_use]
pub fn owa_cmp() -> OwaValue {
OwaValue::native_function(|_, _, args| {
let [left, right] = except_arity(args)?;
let result = match left.cmp(right) {
std::cmp::Ordering::Less => OwaValue::int(-1),
std::cmp::Ordering::Equal => OwaValue::int(0),
std::cmp::Ordering::Greater => OwaValue::int(1),
};
Ok(result.into())
})
}
/// Returns a tuple of names and values
///
/// # Errors
///
/// Returns [`OwaError::TypeError`] if `names_ast` or `values_ast` are not the expected types
pub fn get_names_and_values(
interpreter: &mut Interpreter,
scope_id: usize,
names_ast: &OwaAst,
values_ast: &OwaAst,
) -> Result<(Vec<String>, Vec<OwaValue>), OwaError> {
let names = match names_ast {
OwaAst::Symbol(v) | OwaAst::Str(v) | OwaAst::Keyword(v) => vec![v.to_string()],
OwaAst::Vec(v) => v
.iter()
.map(super::parser::ast::OwaAst::to_sym)
.collect::<Result<_, _>>()?,
_ => {
let result = interpreter.eval_value(names_ast, scope_id)?;
let result = result.to_sym()?;
vec![result.to_string()]
}
};
let values = if names.len() == 1 {
vec![interpreter.eval_value(values_ast, scope_id)?]
} else {
let result = interpreter.eval_value(values_ast, scope_id)?;
let result = result.as_vec()?;
result.iter().cloned().collect()
};
Ok((names, values))
}
#[must_use]
pub fn owa_def() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let [key, value_ast] = except_arity(args)?;
let (names, values) = get_names_and_values(interpreter, scope_id, key, value_ast)?;
for (name, value) in names.iter().zip(values.iter()) {
interpreter.define_runtime(scope_id, name, value.clone())?;
}
if names.len() == 1 && values.len() == 1 {
Ok(values[0].clone().into())
} else {
Ok(OwaValue::vec(values).into())
}
})
}
#[must_use]
pub fn owa_exec() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let [variables_ast, bootstrap_ast, target] = except_arity(args)?;
let variables = interpreter.eval_value(variables_ast, scope_id)?;
let variables = variables.as_map()?;
let target = interpreter.eval_value(target, scope_id)?.as_str()?;
// create temp scope
let temp_scope_id = interpreter.inherit_scope(scope_id)?;
// load shared
for (key, value) in variables {
let key = &key.to_sym()?;
let value = value.clone();
interpreter.define_runtime(temp_scope_id, key, value)?;
}
// eval bootstrap
interpreter.eval_value(bootstrap_ast, temp_scope_id)?;
// isolate scope
let mut temp_scope = interpreter.get_scope_cloned(temp_scope_id)?;
temp_scope.parent = None;
temp_scope.token = std::sync::Arc::new(());
temp_scope.child_count = 0;
let isolated_scope_id = interpreter.allocate_scope(temp_scope)?;
// release temp scope (no longer needed after cloning)
interpreter.try_release_scope(temp_scope_id);
// eval
interpreter.run(target.as_ref(), isolated_scope_id)?;
let result = interpreter.get_scope_as_map(isolated_scope_id)?;
Ok(result.into())
})
}
#[must_use]
pub fn owa_include() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let [raw_filename] = except_arity(args)?;
let raw_filename = interpreter.eval_value(raw_filename, scope_id)?;
let raw_filename = raw_filename.as_str()?;
interpreter
.run(raw_filename.as_ref(), scope_id)
.map(Into::into)
})
}
#[must_use]
pub fn owa_lambda() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let [params, body] = except_arity(args)?;
let params = params.as_vec()?;
let params = params
.iter()
.map(OwaAst::as_sym)
.collect::<Result<VectorSync<_>, _>>()?;
let token = interpreter.get_scope_token(scope_id)?;
let value = OwaValue::new_lambda(body.clone(), params, scope_id, token);
Ok(value.into())
})
}
#[must_use]
pub fn owa_lookup() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let ([key], rest) = except_arity_min(args)?;
let key = key.as_sym().or_else(|_| {
let result = interpreter
.eval_value(&args[0], scope_id)
.map(|v| v.unparse())?;
Ok(Arc::from(result.as_str()))
})?;
let default = rest.first();
let value = interpreter.lookup_runtime(scope_id, &key);
match value {
Ok(v) => Ok(v.clone().into()),
Err(e) => default.map_or_else(
|| Err(e),
|default| interpreter.eval_value(default, scope_id).map(Into::into),
),
}
})
}
#[must_use]
pub fn owa_macro() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let [params, body] = except_arity(args)?;
let params = params.as_vec()?;
let params = params
.iter()
.map(OwaAst::as_sym)
.collect::<Result<VectorSync<_>, _>>()?;
let token = interpreter.get_scope_token(scope_id)?;
let value = OwaValue::new_macro(body.clone(), params, scope_id, token);
Ok(value.into())
})
}
#[must_use]
pub fn owa_trace() -> OwaValue {
OwaValue::native_function(|_, _, args| {
for arg in args {
match arg {
OwaValue::Str(v) => print!("{v}"),
_ => print!("{}", arg.unparse()),
}
}
println!();
Ok(OwaValue::str("").into())
})
}
#[must_use]
pub fn owa_typeof() -> OwaValue {
OwaValue::native_function(|_, _, args| {
let [value] = except_arity(args)?;
Ok(OwaValue::kw(value.type_name()).into())
})
}
#[must_use]
pub fn owa_scope() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let inner_scope_id = interpreter.inherit_scope(scope_id)?;
let _ = interpreter.eval_collection::<_, Vec<_>>(args, inner_scope_id)?;
let inner_map = interpreter.get_scope_as_map(inner_scope_id)?;
interpreter.try_release_scope(inner_scope_id);
Ok(inner_map.into())
})
}
#[must_use]
pub fn owa_set() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let [key, value_ast] = except_arity(args)?;
let (names, values) = get_names_and_values(interpreter, scope_id, key, value_ast)?;
for (name, value) in names.iter().zip(values.iter()) {
interpreter.set_runtime(scope_id, name, value.clone())?;
}
if names.len() == 1 && values.len() == 1 {
Ok(values[0].clone().into())
} else {
Ok(OwaValue::vec(values).into())
}
})
}
#[must_use]
pub fn owa_unset() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let mut result = Vec::with_capacity(args.len());
for arg in args {
let value = interpreter.lookup_runtime(scope_id, &arg.to_str())?.clone();
interpreter.unset_runtime(scope_id, &arg.to_str())?;
result.push(value);
}
Ok(OwaValue::vec(result).into())
})
}
#[must_use]
pub fn owa_seq() -> OwaValue {
OwaValue::native_macro(|interpreter, scope_id, args| {
let mut last = None;
for arg in args {
// Normalise both signal channels: Ok(Completion::*) from direct calls
// and Err(Signal(...)) from nested argument evaluation (e.g. inside
// a `namespace` / `scope` that uses eval_collection).
match Completion::from_result(interpreter.eval(arg, scope_id))? {
Completion::Value(v) => last = Some(v),
// Return is caught here — it exits the seq with the returned value
Completion::Return(v) => return Ok(Completion::Value(v)),
// Break/Continue propagate outward (they belong to an enclosing loop)
c => return Ok(c),
}
}
last.ok_or_else(|| OwaError::ArityMinError(1, args.len()))
.map(Completion::Value)
})
}
#[must_use]
pub fn scope() -> Scope {
Scope::new_with_runtime([
("apply", owa_apply()),
("cmp", owa_cmp()),
("def", owa_def()),
("exec", owa_exec()),
("lambda", owa_lambda()),
("lookup", owa_lookup()),
("macro", owa_macro()),
("trace", owa_trace()),
("typeof", owa_typeof()),
("include", owa_include()),
("scope", owa_scope()),
("set!", owa_set()),
("unset!", owa_unset()),
("seq", owa_seq()),
("ast", ast::scope().local_runtime_to_map()),
("cond", cond::scope().local_runtime_to_map()),
("errors", errors::scope().local_runtime_to_map()),
("ffi", ffi::scope().local_runtime_to_map()),
("flow", flow::scope().local_runtime_to_map()),
("map", map::scope().local_runtime_to_map()),
("math", math::scope().local_runtime_to_map()),
("platform", platform::scope().local_runtime_to_map()),
("set", set::scope().local_runtime_to_map()),
("str", str::scope().local_runtime_to_map()),
("vec", vec::scope().local_runtime_to_map()),
])
}

19
src/builtins/platform.rs Normal file
View file

@ -0,0 +1,19 @@
use crate::core::{OwaValue, Scope};
#[must_use]
pub fn owa_argv() -> OwaValue {
OwaValue::native_function(|_, _, _| {
let args = std::env::args().map(OwaValue::str);
Ok(OwaValue::vec(args).into())
})
}
#[must_use]
pub fn scope() -> Scope {
Scope::new_with_runtime([
("os-name", OwaValue::kw(std::env::consts::OS)),
("os-family", OwaValue::kw(std::env::consts::FAMILY)),
("arch", OwaValue::str(std::env::consts::ARCH)),
("argv", owa_argv()),
])
}

47
src/builtins/set.rs Normal file
View file

@ -0,0 +1,47 @@
use crate::core::{Completion, OwaValue, Scope, except_arity};
#[must_use]
pub fn owa_set_from() -> OwaValue {
OwaValue::native_function(|_, _, args: &[OwaValue]| {
let mut elements = Vec::new();
for arg in args {
match arg {
OwaValue::Vec(values) => elements.extend(values.iter().cloned()),
OwaValue::Set(values) => elements.extend(values.iter().cloned()),
OwaValue::Map(values) => elements.extend(values.keys().cloned()),
OwaValue::Str(v) => {
elements.extend(v.chars().map(|v| OwaValue::str(v.to_string())));
}
_ => elements.push(arg.clone()),
}
}
Ok(OwaValue::set(elements).into())
})
}
#[must_use]
pub fn owa_fold() -> OwaValue {
OwaValue::native_function(|interpreter, _, args| {
let [lambda, initial, set] = except_arity(args)?;
let callable = lambda.as_callable()?;
let set = set.as_set()?;
let mut initial = initial.clone();
for value in set {
let call_args = [initial.clone(), value.clone()];
match Completion::from_result(callable.call(interpreter, &call_args))? {
Completion::Value(v) => initial = v,
Completion::Break => break,
Completion::Continue => continue,
Completion::Return(v) => return Ok(Completion::Value(v)),
}
}
Ok(initial.into())
})
}
#[must_use]
pub fn scope() -> Scope {
Scope::new_with_runtime([("from", owa_set_from()), ("fold", owa_fold())])
}

48
src/builtins/str.rs Normal file
View file

@ -0,0 +1,48 @@
use crate::core::{OwaValue, Scope};
#[must_use]
pub fn owa_to_str() -> OwaValue {
OwaValue::native_function(|_, _, args: &[OwaValue]| {
let value = OwaValue::str(
args.iter()
.map(|v| match v {
OwaValue::Str(v) => v.to_string(),
_ => v.unparse(),
})
.collect::<String>(),
);
Ok(value.into())
})
}
#[must_use]
pub fn owa_to_kw() -> OwaValue {
OwaValue::native_function(|_, _, args: &[OwaValue]| {
let value = OwaValue::kw(
args.iter()
.map(|v| match v {
OwaValue::Str(v) | OwaValue::Keyword(v) => v.to_string(),
_ => v.unparse(),
})
.collect::<String>(),
);
Ok(value.into())
})
}
#[must_use]
pub fn owa_serialize() -> OwaValue {
OwaValue::native_function(|_, _, args| {
let value = OwaValue::str(args.iter().map(OwaValue::unparse).collect::<String>());
Ok(value.into())
})
}
#[must_use]
pub fn scope() -> Scope {
Scope::new_with_runtime([
("new", owa_to_str()),
("kw", owa_to_kw()),
("serialize", owa_serialize()),
])
}

66
src/builtins/vec.rs Normal file
View file

@ -0,0 +1,66 @@
use crate::core::{Completion, OwaValue, Scope, except_arity};
#[must_use]
pub fn owa_vec_new() -> OwaValue {
OwaValue::native_function(|_, _, args: &[OwaValue]| {
let value = OwaValue::vec(args.iter().cloned());
Ok(value.into())
})
}
#[must_use]
pub fn owa_vec_from() -> OwaValue {
OwaValue::native_function(|_, _, args: &[OwaValue]| {
let mut result = Vec::with_capacity(args.len());
for arg in args {
match arg {
OwaValue::Vec(values) => result.extend(values.iter().cloned()),
OwaValue::Set(values) => result.extend(values.iter().cloned()),
OwaValue::Map(values) => {
result.extend(
values
.iter()
.map(|(k, v)| OwaValue::vec([k.clone(), v.clone()])),
);
}
OwaValue::Str(v) => result.extend(v.chars().map(|v| OwaValue::str(v.to_string()))),
_ => result.push(arg.clone()),
}
}
Ok(OwaValue::vec(result).into())
})
}
#[must_use]
pub fn owa_fold() -> OwaValue {
OwaValue::native_function(|interpreter, _, args| {
let [lambda, initial, vector] = except_arity(args)?;
let callable = lambda.as_callable()?;
let vector = vector.as_vec()?;
let mut acc = initial.clone();
let mut call_args = [OwaValue::Int(0), OwaValue::Int(0), OwaValue::Int(0)];
for (idx, value) in vector.iter().enumerate() {
call_args[0] = std::mem::replace(&mut acc, OwaValue::Int(0));
call_args[1] = value.clone();
call_args[2] = OwaValue::int(idx);
match Completion::from_result(callable.call(interpreter, &call_args))? {
Completion::Value(v) => acc = v,
Completion::Break => break,
Completion::Continue => continue,
Completion::Return(v) => return Ok(Completion::Value(v)),
}
}
Ok(acc.into())
})
}
#[must_use]
pub fn scope() -> Scope {
Scope::new_with_runtime([
("new", owa_vec_new()),
("from", owa_vec_from()),
("fold", owa_fold()),
])
}

234
src/core/callable.rs Normal file
View file

@ -0,0 +1,234 @@
#![allow(unpredictable_function_pointer_comparisons)]
use std::{hash::Hash, sync::Arc};
use educe::Educe;
use rpds::VectorSync;
use tracing::debug;
use crate::{
core::{Completion, Interpreter, OwaError, OwaValue, ScopeId, special_names},
parser::ast::{Expander, OwaAst},
vec_unparse,
};
pub type OwaNativeFunction =
fn(&mut Interpreter, ScopeId, &[OwaValue]) -> Result<Completion, OwaError>;
pub type OwaNativeMacro = fn(&mut Interpreter, ScopeId, &[OwaAst]) -> Result<Completion, OwaError>;
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum OwaCallableCell {
NativeLambda(OwaNativeFunction),
NativeMacro(OwaNativeMacro),
Lambda(Arc<OwaAst>),
Macro(Arc<OwaAst>),
}
#[derive(Educe)]
#[educe(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct OwaCallable {
#[educe(Debug(ignore), Hash(ignore), PartialEq(ignore), PartialOrd(ignore))]
pub scope_id: ScopeId,
#[educe(Debug(ignore), Hash(ignore), PartialEq(ignore), PartialOrd(ignore))]
pub scope_token: Arc<()>,
pub params: VectorSync<Arc<str>>,
pub body: OwaCallableCell,
}
impl OwaCallable {
fn call_macro<'a, I>(
&self,
interpreter: &mut Interpreter,
args: I,
scope_id: ScopeId,
) -> Result<Completion, OwaError>
where
I: IntoIterator<Item = &'a OwaAst>,
{
// remove annotations
let args = args.into_iter().map(|a| a.unannotate()).collect::<Vec<_>>();
// check arity
if args.len() < self.params.len() {
return Err(OwaError::ArityError(self.params.len(), args.len()));
}
// create call scope
self.load_expand_args(interpreter, scope_id, &args)?;
match &self.body {
OwaCallableCell::NativeMacro(native) => native(interpreter, scope_id, &args),
OwaCallableCell::Macro(body) => {
let expanded = Expander::new(interpreter, scope_id).expand(body, false, false);
debug!("expanded: {expanded:?}");
interpreter.eval(&expanded, scope_id)
}
_ => Err(OwaError::TypeError("macro".into(), self.type_name().into())),
}
}
/// Calls this callable with the given pre-evaluated arguments.
///
/// # Errors
///
/// Returns [`OwaError::ArityError`] if too few arguments are provided.
/// Returns [`OwaError::TypeError`] if the body is not a lambda variant.
/// Propagates any error returned by the lambda body or native function.
pub fn call(
&self,
interpreter: &mut Interpreter,
args: &[OwaValue],
) -> Result<Completion, OwaError> {
// check arity
if args.len() < self.params.len() {
return Err(OwaError::ArityError(self.params.len(), args.len()));
}
// create scope
let call_scope_id = interpreter.inherit_scope(self.scope_id)?;
self.load_args(interpreter, call_scope_id, args)?;
// eval
let result = match &self.body {
OwaCallableCell::Lambda(body) => interpreter.eval(body, call_scope_id),
OwaCallableCell::NativeLambda(native) => native(interpreter, call_scope_id, args),
_ => Err(OwaError::TypeError(
"lambda".into(),
self.type_name().into(),
)),
};
interpreter.try_release_scope(call_scope_id);
result
}
/// Evaluates the callable with the given raw AST arguments.
///
/// # Errors
///
/// Returns [`OwaError`] if argument evaluation or the underlying call fails.
pub fn eval<'a, I>(
&self,
interpreter: &mut Interpreter,
raw_args: I,
scope_id: ScopeId,
) -> Result<Completion, OwaError>
where
I: IntoIterator<Item = &'a OwaAst>,
{
match &self.body {
OwaCallableCell::NativeMacro(_) | OwaCallableCell::Macro(_) => {
self.call_macro(interpreter, raw_args, scope_id)
}
OwaCallableCell::NativeLambda(_) | OwaCallableCell::Lambda(_) => {
let args: Vec<OwaValue> = interpreter.eval_collection(raw_args, scope_id)?;
self.call(interpreter, &args)
}
}
}
/// Defines all call arguments (`this`, `%%`, `%&`, named params, indexed params)
/// in the given scope.
///
/// # Errors
///
/// Returns [`OwaError::Panic`] if any variable is already defined in the scope.
#[allow(clippy::needless_pass_by_value)]
fn load_args(
&self,
interpreter: &mut Interpreter,
scope_id: ScopeId,
args: &[OwaValue],
) -> Result<(), OwaError> {
// define self
interpreter.define_runtime(
scope_id,
special_names::THIS_CALLABLE,
OwaValue::Callable(self.clone()),
)?;
// define special arguments
interpreter.define_runtime(
scope_id,
special_names::ALL_ARGS,
OwaValue::vec(args.iter().cloned()),
)?;
interpreter.define_runtime(
scope_id,
special_names::REST_ARGS,
OwaValue::vec(args.iter().skip(self.params.len()).cloned()),
)?;
// define named arguments
for (idx, param) in self.params.iter().enumerate() {
interpreter.define_runtime(scope_id, param, args[idx].clone())?;
}
// define indexed arguments
for (idx, arg) in args.iter().enumerate() {
interpreter.define_runtime(scope_id, &format!("%{}", idx + 1), arg.clone())?;
}
Ok(())
}
#[allow(clippy::needless_pass_by_value)]
fn load_expand_args(
&self,
interpreter: &mut Interpreter,
scope_id: ScopeId,
args: &[OwaAst],
) -> Result<(), OwaError> {
// define special arguments
interpreter.set_expand(
scope_id,
special_names::ALL_ARGS,
OwaAst::vec(args.iter().cloned()),
)?;
interpreter.set_expand(
scope_id,
special_names::REST_ARGS,
OwaAst::vec(args.iter().skip(self.params.len()).cloned()),
)?;
// define named arguments
for (idx, param) in self.params.iter().enumerate() {
interpreter.set_expand(scope_id, param, args[idx].clone())?;
}
// define indexed arguments
for (idx, arg) in args.iter().enumerate() {
interpreter.set_expand(scope_id, &format!("%{}", idx + 1), arg.clone())?;
}
Ok(())
}
#[must_use]
pub fn get_param_index(&self, name: &str) -> Option<usize> {
self.params.iter().position(|p| p.clone().as_ref() == name)
}
#[must_use]
pub const fn type_name(&self) -> &'static str {
match self.body {
OwaCallableCell::NativeLambda(_) | OwaCallableCell::Lambda(_) => "lambda",
OwaCallableCell::NativeMacro(_) | OwaCallableCell::Macro(_) => "macro",
}
}
#[must_use]
pub fn unparse(&self) -> String {
match &self.body {
OwaCallableCell::Lambda(v) => match v.as_ref() {
OwaAst::Call(items) => format!("#{}", vec_unparse!(items.iter())),
_ => format!("#({})", v.unparse()),
},
OwaCallableCell::NativeLambda(_) => "#(!native.lambda!)".to_string(),
OwaCallableCell::Macro(v) => match v.as_ref() {
OwaAst::Call(items) => format!("#{}", vec_unparse!(items.iter())),
_ => format!("@({})", v.unparse()),
},
OwaCallableCell::NativeMacro(_) => "#(!native.macro!)".to_string(),
}
}
}

207
src/core/error.rs Normal file
View file

@ -0,0 +1,207 @@
use crate::core::OwaValue;
/// Payload carried by `break`, `continue`, and `return` signals.
///
/// This is kept separate from [`OwaError`] so that control flow can travel
/// through two independent channels:
///
/// * **`Ok(Completion::*)`** — produced directly by the `break`/`continue`/
/// `return` native functions and propagated through `eval`/`callable::call`.
/// * **`Err(OwaError::Signal(...))`** — produced when a signal passes through
/// a value-evaluation context (e.g. argument lists via `eval_collection`).
/// This mirrors the old viral-error propagation so that constructs like
/// `(for x v (continue))` still work correctly even though the `continue`
/// originates inside a nested lambda.
///
/// Both channels are unified by [`Completion::from_result`].
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum FlowOperation {
Break,
Continue,
Return(OwaValue),
}
/// Represents the outcome of evaluating an expression.
///
/// Normal expressions produce `Value`. Control-flow constructs (`break`,
/// `continue`, `return`) produce the corresponding variant. These are NOT
/// errors — they are intentional signals that loops and sequences catch.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Completion {
/// The expression evaluated to a value normally.
Value(OwaValue),
/// A `break` signal was emitted.
Break,
/// A `continue` signal was emitted.
Continue,
/// A `return` signal was emitted with its return value.
Return(OwaValue),
}
impl Completion {
/// Normalises a result by converting `OwaError::Signal` errors back into
/// `Completion` variants.
///
/// Control-flow signals may travel via two channels:
/// * `Ok(Completion::Break/Continue/Return)` — from a direct lambda call.
/// * `Err(OwaError::Signal(...))` — from nested argument evaluation.
///
/// Call this function wherever both channels must be handled (loops, seq).
///
/// # Errors
///
/// * `Ok(Completion::Value(v))` — the expression evaluated to a value normally.
/// * `Ok(Completion::Break/Continue/Return)` — a control-flow signal was emitted.
/// * `Err(OwaError::Panic(_))` — a `break` signal was emitted outside a loop.
pub fn from_result(result: Result<Self, OwaError>) -> Result<Self, OwaError> {
match result {
Err(OwaError::Signal(f)) => Ok(f.into()),
other => other,
}
}
/// Unwraps the inner value, converting control-flow signals into
/// user-visible panic errors.
///
/// Use this only when control flow is genuinely unexpected (e.g. at the
/// top level of a file). Inside loops / sequences use [`from_result`] +
/// a pattern match instead.
///
/// # Errors
///
/// Returns [`OwaError::Panic`] if this is `Break`, `Continue`, or `Return`.
pub fn into_value(self) -> Result<OwaValue, OwaError> {
match self {
Self::Value(v) => Ok(v),
Self::Break => Err(OwaError::Panic("Unexpected break outside loop".into())),
Self::Continue => Err(OwaError::Panic("Unexpected continue outside loop".into())),
Self::Return(v) => Err(OwaError::Panic(format!(
"Unexpected return: {}",
v.unparse()
))),
}
}
}
impl From<OwaValue> for Completion {
fn from(v: OwaValue) -> Self {
Self::Value(v)
}
}
impl From<FlowOperation> for Completion {
fn from(f: FlowOperation) -> Self {
match f {
FlowOperation::Break => Self::Break,
FlowOperation::Continue => Self::Continue,
FlowOperation::Return(v) => Self::Return(v),
}
}
}
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum OwaError {
TypeError(String, String), // expected type A, got type B
ArityError(usize, usize), // expected N, got M
ArityMaxError(usize, usize), // expected max N args, got M
ArityMinError(usize, usize), // expected min N args, got M
InvalidMemberAccess(String, &'static str), // name A; type B
Panic(String),
EmptyCall,
RuntimeError(OwaValue),
UnboundVariable(String),
/// Internal: a control-flow signal propagating through value-evaluation
/// contexts (e.g. argument lists). Never shown to the user as a top-level
/// error; always caught by the nearest loop / sequence construct.
Signal(FlowOperation),
}
impl OwaError {
#[must_use]
pub const fn type_name(&self) -> &'static str {
match self {
Self::TypeError(_, _) => "TypeError",
Self::ArityError(_, _) => "ArityError",
Self::ArityMaxError(_, _) => "ArityMaxError",
Self::ArityMinError(_, _) => "ArityMinError",
Self::InvalidMemberAccess(_, _) => "InvalidMemberAccess",
Self::Panic(_) => "Panic",
Self::EmptyCall => "EmptyCall",
Self::RuntimeError(_) => "RuntimeError",
Self::UnboundVariable(_) => "UnboundVariable",
Self::Signal(_) => "Signal",
}
}
#[must_use]
pub fn message(&self) -> String {
match self {
Self::TypeError(expected, got) => {
format!("expected type {expected}, got type {got}")
}
Self::ArityError(expected, got) => {
format!("expected {expected} args, got {got}")
}
Self::ArityMaxError(max, got) => {
format!("expected max {max} args, got {got}")
}
Self::ArityMinError(min, got) => {
format!("expected min {min} args, got {got}")
}
Self::InvalidMemberAccess(name, type_name) => {
format!("cannot access member '{name}' on type {type_name}")
}
Self::Panic(message) => message.to_owned(),
Self::EmptyCall => "empty call".to_string(),
Self::RuntimeError(value) => format!("runtime error: {}", value.unparse()),
Self::UnboundVariable(name) => format!("unbound variable {name}"),
Self::Signal(FlowOperation::Break) => "break outside loop".to_string(),
Self::Signal(FlowOperation::Continue) => "continue outside loop".to_string(),
Self::Signal(FlowOperation::Return(v)) => {
format!("unexpected return: {}", v.unparse())
}
}
}
}
impl std::fmt::Display for OwaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.type_name(), self.message())
}
}
/// Collects `args` into a fixed-size array of length `N`.
///
/// # Errors
///
/// Returns [`OwaError::ArityError`] if the number of items in `args` is not exactly `N`.
pub fn except_arity<T: std::fmt::Debug, C, const N: usize>(args: C) -> Result<[T; N], OwaError>
where
C: IntoIterator<Item = T>,
{
let args = args.into_iter().collect::<Vec<_>>();
let len = args.len();
args.try_into().map_err(|_| OwaError::ArityError(N, len))
}
/// Collects `args` into a fixed-size array of length `N` or more,
/// returning the fixed-size array and any remaining items as a [`Vec`].
///
/// # Errors
///
/// Returns [`OwaError::ArityMinError`] if the number of items in `args` is less than `N`.
pub fn except_arity_min<T: std::fmt::Debug, C, const N: usize>(
args: C,
) -> Result<([T; N], Vec<T>), OwaError>
where
C: IntoIterator<Item = T>,
{
let mut args = args.into_iter().collect::<Vec<_>>();
let len = args.len();
if len < N {
return Err(OwaError::ArityMinError(N, len));
}
let rest = args.split_off(N);
let fixed: [T; N] = args.try_into().map_err(|_| OwaError::ArityError(N, len))?;
Ok((fixed, rest))
}

492
src/core/interpreter.rs Normal file
View file

@ -0,0 +1,492 @@
use std::collections::HashMap;
use std::sync::Arc;
use rpds::{RedBlackTreeMapSync, RedBlackTreeSetSync, VectorSync};
use tracing::trace;
use crate::{
core::{Completion, FlowOperation, OwaError, OwaValue, Scope, ScopeId, special_names},
parser::{ast::OwaAst, parse_file},
};
#[derive(Debug, Clone, Default)]
pub struct Interpreter {
scopes: Vec<Option<Scope>>,
free_list: Vec<ScopeId>,
pub max_scopes_count: Option<usize>,
}
impl Interpreter {
pub const fn new(max_scopes_count: Option<usize>) -> Self {
Self {
scopes: Vec::new(),
free_list: Vec::new(),
max_scopes_count,
}
}
/// Resolves, parses and evaluates an OWA source file relative to the current
/// `__dir__` runtime variable.
///
/// # Errors
///
/// Returns [`OwaError::UnboundVariable`] if `__dir__` is not set in the scope.
/// Returns [`OwaError::Panic`] if the file cannot be found, read, or parsed.
/// Propagates any [`OwaError`] produced while evaluating the file.
pub fn run<S>(&mut self, filename: S, scope_id: ScopeId) -> Result<OwaValue, OwaError>
where
S: Into<String>,
{
// resolve
let olddir = self.lookup_runtime(scope_id, special_names::DIR)?.clone();
let filename: String = filename.into();
let filename_path = std::path::Path::new(olddir.as_str()?.as_ref()).join(&filename);
trace!("run {}", filename_path.display());
let filename_resolved = resolve_owa_path(&filename_path)
.ok_or_else(|| OwaError::Panic(format!("Could not find file {filename}")))?;
// parse
let ast = parse_file(&filename_resolved).map_err(OwaError::Panic)?;
// update __dir__
let dir = std::path::Path::new(&filename_resolved)
.parent()
.and_then(|p| p.to_str());
if let Some(dir) = dir {
self.set_runtime(scope_id, special_names::DIR, OwaValue::str(dir))?;
}
// eval — normalise both signal channels, then catch Return at the top level
let result = match Completion::from_result(self.eval(&ast, scope_id))? {
Completion::Value(v) | Completion::Return(v) => v,
Completion::Break => {
return Err(OwaError::Panic("Unexpected break at top level".into()));
}
Completion::Continue => {
return Err(OwaError::Panic("Unexpected continue at top level".into()));
}
};
// restore __dir__
self.set_runtime(scope_id, special_names::DIR, olddir)?;
Ok(result)
}
/// Evaluates a single AST node and returns a [`Completion`].
///
/// Returns `Completion::Value` for normal expressions and
/// `Completion::Break`/`Continue`/`Return` for control-flow signals.
///
/// # Errors
///
/// Propagates any [`OwaError`] produced during evaluation.
pub fn eval(&mut self, value: &OwaAst, scope_id: ScopeId) -> Result<Completion, OwaError> {
trace!("scopes count: {}", self.scopes.len());
match value {
OwaAst::Symbol(v) => self.eval_symbol(v, scope_id).map(Completion::Value),
OwaAst::Keyword(v) => Ok(OwaValue::kw(v).into()),
OwaAst::Str(v) => Ok(OwaValue::str(v).into()),
OwaAst::Float(v) => Ok(OwaValue::Float(*v).into()),
OwaAst::Int(v) => Ok(OwaValue::Int(*v).into()),
OwaAst::Vec(v) => self.eval_vector(v, scope_id).map(Completion::Value),
OwaAst::Call(v) => self.eval_call(v, scope_id),
OwaAst::Set(v) => self.eval_set(v, scope_id).map(Completion::Value),
OwaAst::Map(v) => self.eval_map(v, scope_id).map(Completion::Value),
OwaAst::Quote(v) => Ok(OwaValue::quote(v.clone()).into()),
OwaAst::Unquote(v) => self.eval(v.as_ref(), scope_id),
OwaAst::Lambda(v) => {
let call = OwaAst::call(v.iter().cloned());
let token = self.get_scope_token(scope_id)?;
let lambda = OwaValue::new_lambda(call, VectorSync::default(), scope_id, token);
Ok(lambda.into())
}
OwaAst::Annotation(v) => self.eval(&v.value, scope_id),
}
}
/// Evaluates a single AST node and unwraps it to an [`OwaValue`].
///
/// Control-flow signals (`Break`, `Continue`, `Return`) are converted to
/// [`OwaError::Signal`] so they propagate virally through argument
/// evaluation until the nearest loop / sequence catches them via
/// [`Completion::from_result`].
///
/// # Errors
///
/// Returns [`OwaError::Signal`] if the result is a control-flow signal.
/// Propagates any other [`OwaError`] produced during evaluation.
pub fn eval_value(&mut self, value: &OwaAst, scope_id: ScopeId) -> Result<OwaValue, OwaError> {
match self.eval(value, scope_id)? {
Completion::Value(v) => Ok(v),
Completion::Break => Err(OwaError::Signal(FlowOperation::Break)),
Completion::Continue => Err(OwaError::Signal(FlowOperation::Continue)),
Completion::Return(v) => Err(OwaError::Signal(FlowOperation::Return(v))),
}
}
fn eval_symbol(&self, symbol: &str, scope_id: ScopeId) -> Result<OwaValue, OwaError> {
// specials
if is_special_symbol(symbol) {
match symbol {
special_names::SCOPE => return self.get_scope_as_map(scope_id),
_ => (),
}
}
// normal
self.lookup_runtime(scope_id, symbol).cloned()
}
fn eval_vector(
&mut self,
value: &VectorSync<OwaAst>,
scope_id: ScopeId,
) -> Result<OwaValue, OwaError> {
self.eval_collection(value, scope_id).map(OwaValue::Vec)
}
fn eval_call(
&mut self,
value: &VectorSync<OwaAst>,
scope_id: ScopeId,
) -> Result<Completion, OwaError> {
let raw_calle = value.first().ok_or(OwaError::EmptyCall)?;
let raw_calle = self.eval_value(raw_calle, scope_id)?;
let callable = raw_calle.as_callable()?;
callable.eval(self, value.iter().skip(1), scope_id)
}
fn eval_set(
&mut self,
value: &RedBlackTreeSetSync<OwaAst>,
scope_id: ScopeId,
) -> Result<OwaValue, OwaError> {
self.eval_collection(value.iter(), scope_id)
.map(OwaValue::Set)
}
fn eval_map(
&mut self,
value: &RedBlackTreeMapSync<OwaAst, OwaAst>,
scope_id: ScopeId,
) -> Result<OwaValue, OwaError> {
let iter = value.iter().flat_map(|(k, v)| [k, v]);
self.eval_collection::<_, Vec<_>>(iter, scope_id)
.map(OwaValue::map)
}
/// Evaluates every item in the iterator as a value and collects the results into `B`.
///
/// Control-flow signals are forwarded as [`OwaError::Signal`] so they
/// propagate virally through argument-evaluation contexts and are later
/// caught by the nearest loop / sequence via [`Completion::from_result`].
///
/// # Errors
///
/// Returns the first [`OwaError`] (including `Signal`) encountered.
pub fn eval_collection<'a, C, B>(&mut self, items: C, scope_id: ScopeId) -> Result<B, OwaError>
where
C: IntoIterator<Item = &'a OwaAst>,
B: FromIterator<OwaValue>,
{
let mut result = Vec::new();
for item in items {
match self.eval(item, scope_id)? {
Completion::Value(v) => result.push(v),
Completion::Break => return Err(OwaError::Signal(FlowOperation::Break)),
Completion::Continue => return Err(OwaError::Signal(FlowOperation::Continue)),
Completion::Return(v) => return Err(OwaError::Signal(FlowOperation::Return(v))),
}
}
Ok(result.into_iter().collect())
}
/// Looks up `name` by walking the scope parent chain without dot-path splitting.
///
/// # Errors
///
/// Returns [`OwaError::Panic`] if any scope ID in the chain is invalid.
/// Returns [`OwaError::UnboundVariable`] if `name` is not found in any scope.
pub fn lookup_expand(&self, scope: ScopeId, name: &str) -> Result<&OwaAst, OwaError> {
let scope = self.get_scope(scope)?;
if let Some(v) = scope.expand.get(name) {
return Ok(v);
}
Err(OwaError::UnboundVariable(name.to_string()))
}
/// Defines a new macro argument `name` in the given scope.
///
/// # Errors
///
/// Returns [`OwaError::Panic`] if scope ID is invalid.
pub fn set_expand(
&mut self,
scope: ScopeId,
name: &str,
value: OwaAst,
) -> Result<(), OwaError> {
let scope = self.get_scope_mut(scope)?;
scope.expand.insert(name.to_string(), value);
Ok(())
}
/// Looks up `name` by walking the scope parent chain without dot-path splitting.
///
/// # Errors
///
/// Returns [`OwaError::Panic`] if any scope ID in the chain is invalid.
/// Returns [`OwaError::UnboundVariable`] if `name` is not found in any scope.
pub fn lookup_runtime_direct(&self, scope: ScopeId, name: &str) -> Result<&OwaValue, OwaError> {
let mut scope_id = Some(scope);
while let Some(s) = scope_id {
let scope = self.get_scope(s)?;
if let Some(v) = scope.runtime.get(name) {
return Ok(v);
}
scope_id = scope.parent;
}
Err(OwaError::UnboundVariable(name.to_string()))
}
/// Looks up a (potentially dot-separated) `name` in the scope chain.
///
/// # Errors
///
/// Returns [`OwaError::UnboundVariable`] if the name (or any path segment) is not found.
/// Returns [`OwaError::InvalidMemberAccess`] if a non-map value is accessed as a map.
/// Returns [`OwaError::Panic`] if a scope ID is invalid.
pub fn lookup_runtime(&self, scope_id: ScopeId, name: &str) -> Result<&OwaValue, OwaError> {
// fast path
if !name.contains('.') {
return self.lookup_runtime_direct(scope_id, name);
}
let mut parts = name.splitn(2, '.');
let first_part = parts
.next()
.ok_or_else(|| OwaError::UnboundVariable(name.to_string()))?;
let rest = parts.next();
let mut current = self.lookup_runtime_direct(scope_id, first_part)?;
if let Some(rest) = rest {
for part in rest.split('.') {
let OwaValue::Map(map) = current else {
return Err(OwaError::InvalidMemberAccess(
name.to_string(),
current.type_name(),
));
};
let key = OwaValue::kw(part);
current = map
.get(&key)
.ok_or_else(|| OwaError::UnboundVariable(name.to_string()))?;
}
}
Ok(current)
}
/// Defines a new variable `name` in the given scope.
///
/// # Errors
///
/// Returns [`OwaError::Panic`] if `name` is already defined in that scope
/// or if the scope ID is invalid.
pub fn define_runtime(
&mut self,
scope: ScopeId,
name: &str,
value: OwaValue,
) -> Result<(), OwaError> {
let scope = self.get_scope_mut(scope)?;
if scope.runtime.contains_key(name) {
let msg = format!("Variable {name} is already defined in this scope");
Err(OwaError::Panic(msg))
} else {
scope.runtime.insert(name.to_string(), value);
Ok(())
}
}
/// Updates the value of an existing variable `name`, searching from `scope`
/// upward through the parent chain.
///
/// # Errors
///
/// Returns [`OwaError::UnboundVariable`] if `name` is not found in any scope.
/// Returns [`OwaError::Panic`] if a scope ID is invalid.
pub fn set_runtime(
&mut self,
scope: ScopeId,
name: &str,
value: OwaValue,
) -> Result<(), OwaError> {
let target = self.search_runtime(scope, name)?;
let scope_mut = self.get_scope_mut(target)?;
if let Some(slot) = scope_mut.runtime.get_mut(name) {
*slot = value;
} else {
scope_mut.runtime.insert(name.to_string(), value);
}
Ok(())
}
/// Removes the variable `name` from the scope where it is first found.
///
/// # Errors
///
/// Returns [`OwaError::UnboundVariable`] if `name` is not found in any scope.
/// Returns [`OwaError::Panic`] if a scope ID is invalid.
pub fn unset_runtime(&mut self, scope: ScopeId, name: &str) -> Result<(), OwaError> {
let target = self.search_runtime(scope, name)?;
let scope_mut = self.get_scope_mut(target)?;
scope_mut.runtime.remove(name);
Ok(())
}
fn search_runtime(&self, scope: ScopeId, name: &str) -> Result<usize, OwaError> {
let mut cur = Some(scope);
let mut target = None;
while let Some(s) = cur {
let scope_ref = self.get_scope(s)?;
if scope_ref.runtime.contains_key(name) {
target = Some(s);
break;
}
cur = scope_ref.parent;
}
target.ok_or_else(|| OwaError::UnboundVariable(name.to_string()))
}
pub fn allocate_scope(&mut self, scope: Scope) -> Result<ScopeId, OwaError> {
if let Some(free_id) = self.free_list.pop() {
self.scopes[free_id] = Some(scope);
return Ok(free_id);
}
if let Some(max) = self.max_scopes_count
&& self.scopes.len() >= max
{
return Err(OwaError::Panic("scope limit exceeded".to_string()));
}
let scope_id = self.scopes.len();
self.scopes.push(Some(scope));
Ok(scope_id)
}
pub fn inherit_scope(&mut self, parent: ScopeId) -> Result<ScopeId, OwaError> {
self.get_scope_mut(parent)?.child_count += 1;
self.allocate_scope(Scope::new(Some(parent)))
}
pub fn get_scope_token(&self, scope_id: ScopeId) -> Result<Arc<()>, OwaError> {
Ok(self.get_scope(scope_id)?.token.clone())
}
pub fn try_release_scope(&mut self, scope_id: ScopeId) {
let Some(scope) = self.scopes.get(scope_id).and_then(|s| s.as_ref()) else {
return;
};
if scope.child_count > 0 {
return;
}
if Arc::strong_count(&scope.token) > 1 {
return;
}
let parent = match scope.parent {
Some(p) => p,
None => return,
};
self.scopes[scope_id] = None;
self.free_list.push(scope_id);
if let Some(parent_scope) = self.scopes[parent].as_mut() {
parent_scope.child_count = parent_scope.child_count.saturating_sub(1);
}
}
/// Returns the parent scope ID of the given scope, if any.
///
/// # Errors
///
/// Returns [`OwaError::Panic`] if `scope_id` is invalid.
pub fn get_scope_parent(&self, scope_id: ScopeId) -> Result<Option<ScopeId>, OwaError> {
let scope = self.get_scope(scope_id)?;
Ok(scope.parent)
}
/// Collects all variables visible from `scope_id` (including parent scopes)
/// into a map value.
///
/// # Errors
///
/// Returns [`OwaError::Panic`] if any scope ID in the chain is invalid.
/// Returns [`OwaError::TypeError`] if a parent scope map cannot be read as a map.
pub fn get_scope_as_map(&self, scope_id: ScopeId) -> Result<OwaValue, OwaError> {
let mut chain = Vec::new();
let mut cur = Some(scope_id);
while let Some(id) = cur {
let scope = self.get_scope(id)?;
chain.push(id);
cur = scope.parent;
}
let mut map = HashMap::new();
for &id in chain.iter().rev() {
let scope = self.get_scope(id)?;
for (k, v) in &scope.runtime {
map.insert(OwaValue::kw(k), v.clone());
}
}
map.insert(OwaValue::kw(special_names::IS_SCOPE), OwaValue::int(1));
Ok(OwaValue::Map(RedBlackTreeMapSync::from_iter(map)))
}
/// Returns a clone of the scope with the given ID.
///
/// # Errors
///
/// Returns [`OwaError::Panic`] if `scope_id` does not correspond to a live scope.
pub fn get_scope_cloned(&self, scope_id: ScopeId) -> Result<Scope, OwaError> {
let scope = self.scopes.get(scope_id).and_then(|inner| inner.as_ref());
scope
.cloned()
.ok_or_else(|| OwaError::Panic(format!("Attempt to access invalid scope {scope_id}")))
}
fn get_scope(&self, scope_id: ScopeId) -> Result<&Scope, OwaError> {
let scope = self.scopes.get(scope_id).and_then(|inner| inner.as_ref());
scope.ok_or_else(|| OwaError::Panic(format!("Attempt to access invalid scope {scope_id}")))
}
fn get_scope_mut(&mut self, scope_id: ScopeId) -> Result<&mut Scope, OwaError> {
let scope = self.scopes.get_mut(scope_id).and_then(|x| x.as_mut());
scope.ok_or_else(|| OwaError::Panic(format!("Attempt to access invalid scope {scope_id}")))
}
}
fn resolve_owa_path(base: &std::path::Path) -> Option<String> {
[
base.to_path_buf(),
base.with_extension("owa"),
base.join("main.owa"),
]
.into_iter()
.find(|p| p.is_file())
.and_then(|v| v.to_str().map(std::string::ToString::to_string))
}
fn is_special_symbol(s: &str) -> bool {
s.starts_with("__") && s.ends_with("__")
}

12
src/core/mod.rs Normal file
View file

@ -0,0 +1,12 @@
mod callable;
mod error;
mod interpreter;
mod scope;
pub mod special_names;
mod value;
pub use callable::*;
pub use error::*;
pub use interpreter::*;
pub use scope::*;
pub use value::*;

77
src/core/scope.rs Normal file
View file

@ -0,0 +1,77 @@
use std::collections::HashMap;
use std::sync::Arc;
use crate::{core::OwaValue, parser::ast::OwaAst};
pub type ScopeId = usize;
#[derive(Debug, Clone)]
pub struct Scope {
pub parent: Option<ScopeId>,
pub runtime: HashMap<String, OwaValue>,
pub expand: HashMap<String, OwaAst>,
pub child_count: usize,
pub token: Arc<()>,
}
impl Default for Scope {
fn default() -> Self {
Self {
parent: None,
runtime: HashMap::new(),
expand: HashMap::new(),
child_count: 0,
token: Arc::new(()),
}
}
}
impl PartialEq for Scope {
fn eq(&self, other: &Self) -> bool {
self.parent == other.parent && self.runtime == other.runtime && self.expand == other.expand
}
}
impl Eq for Scope {}
impl Scope {
#[must_use]
pub fn new(parent: Option<ScopeId>) -> Self {
Self {
parent,
runtime: HashMap::new(),
expand: HashMap::new(),
child_count: 0,
token: Arc::new(()),
}
}
#[must_use]
pub fn new_with_runtime<K, V, I>(runtime: I) -> Self
where
K: Into<String>,
V: Into<OwaValue>,
I: IntoIterator<Item = (K, V)>,
{
Self {
parent: None,
runtime: runtime
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
expand: HashMap::new(),
child_count: 0,
token: Arc::new(()),
}
}
#[must_use]
pub fn local_runtime_to_map(&self) -> OwaValue {
OwaValue::Map(
self.runtime
.iter()
.map(|(k, v)| (OwaValue::kw(k), v.clone()))
.collect(),
)
}
}

23
src/core/special_names.rs Normal file
View file

@ -0,0 +1,23 @@
/// Self-reference binding available inside every lambda/macro call body.
pub const THIS_CALLABLE: &str = "this";
/// Binding containing all arguments passed to the current callable.
pub const ALL_ARGS: &str = "%%";
/// Binding containing arguments beyond the declared named parameters.
pub const REST_ARGS: &str = "%&";
/// Runtime variable holding the resolved directory of the currently executing file.
pub const DIR: &str = "__dir__";
/// Special symbol that evaluates to the current scope as a map value.
pub const SCOPE: &str = "__scope__";
/// Map key inserted into every scope-map to identify it as a scope object.
pub const IS_SCOPE: &str = "__is_scope__";
/// Map key that makes a map value callable (callable-object protocol).
pub const CALL: &str = "__call__";
/// Keyword representing the null / absent value.
pub const KW_NULL: &str = "null";

347
src/core/value.rs Normal file
View file

@ -0,0 +1,347 @@
use num_traits::ToPrimitive;
use ordered_float::OrderedFloat;
use rpds::{RedBlackTreeMapSync, RedBlackTreeSetSync, VectorSync};
use std::{hash::Hash, sync::Arc};
use crate::core::{
OwaCallable, OwaCallableCell, OwaError, OwaNativeFunction, OwaNativeMacro, ScopeId,
special_names,
};
use crate::parser::ast::OwaAst;
/// Formats an iterator of items-with-`unparse()` into a space-separated string.
#[macro_export]
macro_rules! vec_unparse {
($iter:expr) => {{
$iter
.map(|v| v.unparse())
.collect::<Vec<String>>()
.join(" ")
}};
}
/// Generates constructor and accessor methods shared by `OwaValue` and `OwaAst`.
///
/// The `kw()` constructor strips all characters listed in the parser's
/// `INDENT_IGNORE` set so that keyword identifiers are always clean.
#[macro_export]
macro_rules! owa_base_impl {
() => {
pub fn kw<S: AsRef<str>>(s: S) -> Self {
Self::Keyword(Arc::from(s.as_ref()))
}
/// Returns the inner string if this value is a `Keyword`.
///
/// # Errors
///
/// Returns [`OwaError::TypeError`] if this is not a `Keyword` variant.
pub fn as_kw(&self) -> Result<Arc<str>, OwaError> {
match self {
Self::Keyword(s) => Ok(s.clone()),
_ => Err(OwaError::TypeError(
"keyword".into(),
self.type_name().into(),
)),
}
}
pub fn str<S: AsRef<str>>(s: S) -> Self {
Self::Str(Arc::from(s.as_ref()))
}
/// Returns the inner string if this value is a `Str`.
///
/// # Errors
///
/// Returns [`OwaError::TypeError`] if this is not a `Str` variant.
pub fn as_str(&self) -> Result<Arc<str>, OwaError> {
match self {
Self::Str(s) => Ok(s.clone()),
_ => Err(OwaError::TypeError("str".into(), self.type_name().into())),
}
}
#[must_use]
pub fn to_sym(&self) -> Result<String, OwaError> {
match self {
Self::Str(s) => Ok(s.as_ref().into()),
Self::Keyword(s) => Ok(s.as_ref().into()),
_ => Err(OwaError::TypeError(
"str/keyword".into(),
self.type_name().into(),
)),
}
}
pub fn int<T: ToPrimitive>(i: T) -> Self {
Self::Int(i.to_i64().unwrap_or(0))
}
/// Returns the inner integer if this value is an `Int`.
///
/// # Errors
///
/// Returns [`OwaError::TypeError`] if this is not an `Int` variant.
pub fn as_int(&self) -> Result<i64, OwaError> {
match self {
Self::Int(i) => Ok(*i),
_ => Err(OwaError::TypeError("int".into(), self.type_name().into())),
}
}
pub const fn float(f: f64) -> Self {
Self::Float(OrderedFloat(f))
}
/// Returns the inner float if this value is a `Float`.
///
/// # Errors
///
/// Returns [`OwaError::TypeError`] if this is not a `Float` variant.
pub fn as_float(&self) -> Result<OrderedFloat<f64>, OwaError> {
match self {
Self::Float(f) => Ok(*f),
_ => Err(OwaError::TypeError("float".into(), self.type_name().into())),
}
}
pub fn map<C>(coll: C) -> Self
where
C: IntoIterator<Item = Self>,
{
let mut map = RedBlackTreeMapSync::default();
let mut iter = coll.into_iter();
while let (Some(k), Some(v)) = (iter.next(), iter.next()) {
map = map.insert(k, v);
}
Self::Map(map)
}
/// Returns a reference to the inner map if this value is a `Map`.
///
/// # Errors
///
/// Returns [`OwaError::TypeError`] if this is not a `Map` variant.
pub fn as_map(&self) -> Result<&RedBlackTreeMapSync<Self, Self>, OwaError> {
match self {
Self::Map(m) => Ok(m),
_ => Err(OwaError::TypeError("map".into(), self.type_name().into())),
}
}
#[must_use]
pub fn vec<C>(coll: C) -> Self
where
C: IntoIterator<Item = Self>,
{
Self::Vec(VectorSync::from_iter(coll.into_iter()))
}
#[must_use]
pub fn to_vec(self) -> Self {
Self::Vec(VectorSync::from_iter(vec![self]))
}
/// Returns a reference to the inner vector if this value is a `Vec`.
///
/// # Errors
///
/// Returns [`OwaError::TypeError`] if this is not a `Vec` variant.
pub fn as_vec(&self) -> Result<&VectorSync<Self>, OwaError> {
match self {
Self::Vec(v) => Ok(v),
_ => Err(OwaError::TypeError("vec".into(), self.type_name().into())),
}
}
/// Returns a reference to the inner set if this value is a `Set`.
///
/// # Errors
///
/// Returns [`OwaError::TypeError`] if this is not a `Set` variant.
pub fn as_set(&self) -> Result<&RedBlackTreeSetSync<Self>, OwaError> {
match self {
Self::Set(s) => Ok(s),
_ => Err(OwaError::TypeError("set".into(), self.type_name().into())),
}
}
#[must_use]
pub fn set<C>(coll: C) -> Self
where
C: IntoIterator<Item = Self>,
{
Self::Set(RedBlackTreeSetSync::from_iter(coll.into_iter()))
}
#[must_use]
pub fn to_set(self) -> Self {
Self::Set(RedBlackTreeSetSync::from_iter(vec![self]))
}
};
}
#[derive(Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum OwaValue {
Keyword(Arc<str>), // :ident
Str(Arc<str>), // "..."
Float(OrderedFloat<f64>), // 1.0
Int(i64), // 1
Vec(VectorSync<Self>), // [...]
Set(RedBlackTreeSetSync<Self>), // {...}
Map(RedBlackTreeMapSync<Self, Self>), // #{...}
Quote(Arc<OwaAst>), // '...
Callable(OwaCallable),
}
impl OwaValue {
owa_base_impl!();
#[must_use]
pub fn new_macro(
body: OwaAst,
params: VectorSync<Arc<str>>,
scope_id: ScopeId,
scope_token: Arc<()>,
) -> Self {
Self::Callable(OwaCallable {
scope_id,
scope_token,
params,
body: OwaCallableCell::Macro(Arc::new(body)),
})
}
#[must_use]
pub fn new_lambda(
body: OwaAst,
params: VectorSync<Arc<str>>,
scope_id: ScopeId,
scope_token: Arc<()>,
) -> Self {
Self::Callable(OwaCallable {
scope_id,
scope_token,
params,
body: OwaCallableCell::Lambda(Arc::new(body)),
})
}
/// Returns the callable wrapped by this value, following `__call__` map keys.
///
/// # Errors
///
/// Returns [`OwaError::TypeError`] if the value (or the resolved `__call__`
/// chain) is not a `Callable` variant.
pub fn as_callable(&self) -> Result<&OwaCallable, OwaError> {
let mut calle = self;
// magic __call__
while let Self::Map(items) = calle {
let item = items.get(&Self::kw(special_names::CALL));
match item {
Some(v) => calle = v,
None => break,
}
}
match calle {
Self::Callable(lambda) => Ok(lambda),
_ => Err(OwaError::TypeError(
"lambda or macro".into(),
calle.type_name().into(),
)),
}
}
pub fn native_function(function: OwaNativeFunction) -> Self {
Self::native(OwaCallableCell::NativeLambda(function))
}
pub fn native_macro(r#macro: OwaNativeMacro) -> Self {
Self::native(OwaCallableCell::NativeMacro(r#macro))
}
fn native(body: OwaCallableCell) -> Self {
Self::Callable(OwaCallable {
scope_id: 0,
scope_token: Arc::new(()),
params: VectorSync::from_iter(vec![]),
body,
})
}
#[must_use]
pub const fn quote(value: Arc<OwaAst>) -> Self {
Self::Quote(value)
}
/// Returns the inner AST node if this value is a `Quote`.
///
/// # Errors
///
/// Returns [`OwaError::TypeError`] if this is not a `Quote` variant.
pub fn as_quote(&self) -> Result<Arc<OwaAst>, OwaError> {
match self {
Self::Quote(q) => Ok(q.clone()),
_ => Err(OwaError::TypeError("quote".into(), self.type_name().into())),
}
}
#[must_use]
#[allow(clippy::missing_const_for_fn)]
pub fn type_name(&self) -> &'static str {
match self {
Self::Keyword(_) => "keyword",
Self::Str(_) => "str",
Self::Float(_) => "float",
Self::Int(_) => "int",
Self::Vec(_) => "vec",
Self::Set(_) => "set",
Self::Map(_) => "map",
Self::Quote(_) => "quote",
Self::Callable(lambda) => lambda.type_name(),
}
}
#[must_use]
pub fn unparse(&self) -> String {
match self {
Self::Keyword(s) => format!(":{s}"),
Self::Str(s) => format!("\"{s}\""),
Self::Float(f) => {
let s = format!("{f}");
if s.contains('.') { s } else { format!("{s}.0") }
}
Self::Int(i) => i.to_string(),
Self::Vec(v) => {
format!("[{}]", vec_unparse!(v.iter()))
}
Self::Set(v) => {
format!("#{{{}}}", vec_unparse!(v.iter()))
}
Self::Map(v) => format!(
"{{{}}}",
vec_unparse!(v.iter().flat_map(|(k, v)| vec![k, v]))
),
Self::Quote(v) => format!("'{}", v.unparse()),
Self::Callable(v) => v.unparse(),
}
}
}
impl std::fmt::Debug for OwaValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.unparse())
}
}
impl From<&str> for OwaValue {
fn from(val: &str) -> Self {
Self::Str(Arc::from(val))
}
}

18
src/lib.rs Normal file
View file

@ -0,0 +1,18 @@
#![warn(
clippy::all,
clippy::pedantic,
clippy::nursery,
clippy::cargo,
clippy::perf
)]
#![allow(
clippy::multiple_crate_versions, // idk how to fix this
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::needless_continue
)]
#![cfg_attr(test, allow(clippy::needless_pass_by_value))]
pub mod builtins;
pub mod core;
pub mod parser;

51
src/main.rs Normal file
View file

@ -0,0 +1,51 @@
use owa_rs::{
builtins,
core::{Interpreter, OwaValue, Scope, special_names},
};
use tracing::error;
fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
// flags
let argv = std::env::args().collect::<Vec<String>>();
let argv = argv.iter().map(|s| s.as_str()).collect::<Vec<_>>();
let no_owu = argv.contains(&"--no-owu");
// builtins
let builtins = [("builtins", builtins::scope().local_runtime_to_map())];
// init root scope
let mut interpreter = Interpreter::new(None);
let scope = Scope::new_with_runtime(builtins);
let scope_id = interpreter.allocate_scope(scope).unwrap();
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
interpreter
.define_runtime(
scope_id,
special_names::DIR,
OwaValue::str(cwd.to_str().unwrap_or(".")),
)
.unwrap();
// eval target
let target_path = if no_owu {
let target = argv.iter().find(|s| s.ends_with(".owa"));
if let Some(target) = target {
target
} else {
error!("No owa file specified");
std::process::exit(1);
}
} else {
"modules/owu/main.owa"
};
interpreter
.run(target_path, scope_id)
.unwrap_or_else(|err| {
error!("Failed to load owu: {}", err.message());
std::process::exit(1)
});
Ok(())
}

37
src/parser/annotation.rs Normal file
View file

@ -0,0 +1,37 @@
use nom::{
IResult, Parser as _, bytes::complete::tag, character::multispace0, combinator::cut,
sequence::delimited,
};
use crate::parser::{OwaAst, parse_value, ws};
pub(super) fn parse_annotation(input: &str) -> IResult<&str, OwaAst> {
delimited(
ws(tag("<")),
cut(parse_value),
cut((multispace0(), tag(">"))),
)
.parse(input)
}
#[test]
fn test_annotation() {
assert_eq!(
parse_annotation("<(:a :b :c)>"),
Ok((
"",
OwaAst::call([OwaAst::kw("a"), OwaAst::kw("b"), OwaAst::kw("c")])
))
);
assert_eq!(
parse_annotation("<(:a [:b :c] [(:d)])>garbage"),
Ok((
"garbage",
OwaAst::call([
OwaAst::kw("a"),
OwaAst::vec([OwaAst::kw("b"), OwaAst::kw("c"),]),
OwaAst::vec([OwaAst::call([OwaAst::kw("d")]),]),
])
))
);
}

353
src/parser/ast.rs Normal file
View file

@ -0,0 +1,353 @@
use super::to_indent;
use crate::{
core::{Interpreter, OwaError, OwaValue, special_names},
owa_base_impl, vec_unparse,
};
use num_traits::ToPrimitive;
use ordered_float::OrderedFloat;
use rpds::{RedBlackTreeMapSync, RedBlackTreeSetSync, VectorSync};
use std::sync::Arc;
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[warn(unpredictable_function_pointer_comparisons)]
pub struct OwaAnnotation {
pub annotation: Arc<OwaAst>,
pub value: Arc<OwaAst>,
}
#[derive(Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[warn(unpredictable_function_pointer_comparisons)]
#[non_exhaustive]
pub enum OwaAst {
Annotation(OwaAnnotation), // <...>...
Call(VectorSync<Self>), // (...)
Float(OrderedFloat<f64>), // 1.0
Int(i64), // 1
Keyword(Arc<str>), // :ident
Lambda(VectorSync<Self>), // #(...)
Map(RedBlackTreeMapSync<Self, Self>), // #{...}
Quote(Arc<Self>), // '...
Set(RedBlackTreeSetSync<Self>), // {...}
Str(Arc<str>), // "..."
Symbol(Arc<str>), // ident
Unquote(Arc<Self>), // $...
Vec(VectorSync<Self>), // [...]
}
impl OwaAst {
owa_base_impl!();
#[must_use]
pub fn call<C>(coll: C) -> Self
where
C: IntoIterator<Item = Self>,
{
Self::Call(VectorSync::from_iter(coll))
}
#[must_use]
pub fn to_call(self) -> Self {
Self::Call(VectorSync::from_iter([self]))
}
/// Returns a reference to the inner vector if this node is a [`OwaAst::Call`].
///
/// # Errors
///
/// Returns [`OwaError::TypeError`] if this node is not the `Call` variant.
pub fn as_call(&self) -> Result<&VectorSync<Self>, OwaError> {
match self {
Self::Call(v) => Ok(v),
_ => Err(OwaError::TypeError("call".into(), self.type_name().into())),
}
}
pub fn new_lambda<B>(body: B) -> Self
where
B: IntoIterator<Item = Self>,
{
Self::Lambda(VectorSync::from_iter(body))
}
pub fn sym<S: AsRef<str>>(s: S) -> Self {
let s = to_indent(s);
Self::Symbol(Arc::from(s))
}
/// Returns the inner symbol string if this node is a [`OwaAst::Symbol`].
///
/// # Errors
///
/// Returns [`OwaError::TypeError`] if this node is not the `Symbol` variant.
pub fn as_sym(&self) -> Result<Arc<str>, OwaError> {
match self {
Self::Symbol(s) => Ok(s.clone()),
_ => Err(OwaError::TypeError(
"symbol".into(),
self.type_name().into(),
)),
}
}
#[must_use]
pub fn new_quote(v: Self) -> Self {
Self::Quote(Arc::from(v))
}
#[must_use]
pub fn to_quote(self) -> Self {
Self::Quote(Arc::from(self))
}
#[must_use]
pub fn new_unquote(v: Self) -> Self {
Self::Unquote(Arc::from(v))
}
#[must_use]
pub fn to_unquote(self) -> Self {
Self::Unquote(Arc::from(self))
}
#[must_use]
pub fn to_annotation(self, ann: Self) -> Self {
Self::Annotation(OwaAnnotation {
annotation: Arc::from(ann),
value: Arc::from(self),
})
}
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
Self::Symbol(_) => "symbol",
Self::Keyword(_) => "keyword",
Self::Str(_) => "str",
Self::Float(_) => "float",
Self::Int(_) => "int",
Self::Vec(_) => "vec",
Self::Call(_) => "call",
Self::Set(_) => "set",
Self::Map(_) => "map",
Self::Quote(_) => "quote",
Self::Unquote(_) => "unquote",
Self::Lambda(_) => "lambda",
Self::Annotation(v) => v.value.type_name(),
}
}
#[must_use]
pub fn unparse(&self) -> String {
match self {
Self::Symbol(v) => v.to_string(),
Self::Keyword(v) => format!(":{v}"),
Self::Str(v) => format!("\"{v}\""),
Self::Float(v) => {
let s = format!("{v}");
if s.contains('.') { s } else { format!("{s}.0") }
}
Self::Int(v) => v.to_string(),
Self::Vec(v) => {
format!("[{}]", vec_unparse!(v.iter()))
}
Self::Call(v) => format!("({})", vec_unparse!(v.iter())),
Self::Set(v) => {
format!("#{{{}}}", vec_unparse!(v.iter()))
}
Self::Map(v) => format!("{{{}}}", vec_unparse!(v.iter().flat_map(|(k, v)| [k, v]))),
Self::Quote(v) => format!("'{}", v.unparse()),
Self::Unquote(v) => format!("${}", v.unparse()),
Self::Lambda(v) => format!("#({})", vec_unparse!(v.iter())),
Self::Annotation(ann) => {
format!("<{}>{}", ann.annotation.unparse(), ann.value.unparse())
}
}
}
#[must_use]
pub fn to_str(&self) -> String {
match self {
Self::Symbol(s) => s.to_string(),
Self::Str(s) => s.as_ref().to_string(),
_ => self.unparse(),
}
}
#[must_use]
pub fn unannotate(&self) -> Self {
match self {
Self::Annotation(ann) => ann.value.unannotate(),
Self::Vec(v) => Self::Vec(v.iter().map(Self::unannotate).collect()),
Self::Call(v) => Self::Call(v.iter().map(Self::unannotate).collect()),
Self::Set(v) => Self::Set(v.iter().map(Self::unannotate).collect()),
Self::Map(v) => Self::map(
v.iter()
.flat_map(|(k, v)| [Self::unannotate(k), Self::unannotate(v)]),
),
Self::Lambda(v) => Self::Lambda(v.iter().map(Self::unannotate).collect()),
Self::Quote(v) => Self::Quote(Arc::from(v.unannotate())),
Self::Unquote(v) => Self::Unquote(Arc::from(v.unannotate())),
_ => self.clone(),
}
}
#[must_use]
pub fn pack(&self) -> OwaValue {
let type_name = OwaValue::kw(self.type_name());
let data = match self {
Self::Symbol(v) => OwaValue::str(v),
Self::Keyword(v) => OwaValue::kw(v),
Self::Str(v) => OwaValue::str(v),
Self::Float(v) => OwaValue::float(v.into_inner()),
Self::Int(v) => OwaValue::Int(*v),
Self::Vec(v) => OwaValue::vec(v.iter().map(Self::pack)),
Self::Call(v) => OwaValue::vec(v.iter().map(Self::pack)),
Self::Set(v) => OwaValue::set(v.iter().map(Self::pack)),
Self::Map(v) => OwaValue::map(v.iter().flat_map(|(k, v)| [k.pack(), v.pack()])),
Self::Quote(v) => OwaValue::quote(v.clone()),
Self::Unquote(v) => v.pack(),
Self::Lambda(v) => OwaValue::vec(v.iter().map(Self::pack)),
Self::Annotation(v) => OwaValue::map(vec![
OwaValue::kw("value"),
v.value.as_ref().pack(),
OwaValue::kw("annotation"),
v.annotation.as_ref().pack(),
]),
};
OwaValue::map(vec![
OwaValue::kw("type"),
type_name,
OwaValue::kw("data"),
data,
])
}
}
pub struct Expander<'a> {
interpreter: &'a mut Interpreter,
scope_id: usize,
}
impl<'a> Expander<'a> {
pub const fn new(interpreter: &'a mut Interpreter, scope_id: usize) -> Self {
Self {
interpreter,
scope_id,
}
}
#[must_use]
pub fn expand(&self, ast: &OwaAst, in_unquote: bool, in_quote: bool) -> OwaAst {
self.expand_priv(ast, in_unquote, in_quote)
.pop()
.unwrap_or_else(|| ast.clone())
}
#[must_use]
fn resolve(&self, sym: &str) -> Vec<OwaAst> {
let result = match sym {
special_names::ALL_ARGS | special_names::REST_ARGS => {
let val = self.interpreter.lookup_expand(self.scope_id, sym);
val.and_then(|v| v.as_vec())
.map(|v| v.iter().cloned().collect::<Vec<_>>())
}
_ => {
let val = self.interpreter.lookup_expand(self.scope_id, sym);
val.map(|v| vec![v.clone()])
}
};
result.unwrap_or_else(|_| vec![OwaAst::sym(sym)])
}
#[must_use]
fn expand_priv(&self, ast: &OwaAst, in_unquote: bool, in_quote: bool) -> Vec<OwaAst> {
match ast {
OwaAst::Symbol(s) => {
if !in_unquote {
return vec![ast.clone()];
}
self.resolve(s.as_ref())
}
OwaAst::Vec(v) => {
let transformed = v.iter().map(|v| self.expand_priv(v, in_unquote, in_quote));
vec![OwaAst::vec(transformed.flatten())]
}
OwaAst::Call(v) => {
let transformed = v.iter().map(|v| self.expand_priv(v, in_unquote, in_quote));
vec![OwaAst::call(transformed.flatten())]
}
OwaAst::Set(v) => {
let transformed = v.iter().map(|v| self.expand_priv(v, in_unquote, in_quote));
vec![OwaAst::set(transformed.flatten())]
}
OwaAst::Map(v) => {
let transformed = v.iter().flat_map(|(k, v)| {
[
self.expand_priv(k, in_unquote, in_quote),
self.expand_priv(v, in_unquote, in_quote),
]
});
vec![OwaAst::map(transformed.flatten())]
}
OwaAst::Lambda(v) => {
let transformed = v.iter().map(|v| self.expand_priv(v, in_unquote, in_quote));
vec![OwaAst::new_lambda(transformed.flatten())]
}
OwaAst::Quote(v) => vec![if in_quote {
ast.clone()
} else {
v.as_ref().clone()
}],
OwaAst::Unquote(v) => {
if in_unquote {
vec![ast.clone()]
} else {
self.expand_priv(v, true, in_quote)
}
}
_ => vec![ast.clone()],
}
}
}
impl std::fmt::Debug for OwaAst {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.unparse())
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[test_case(OwaAst::sym("foo"), "foo")]
#[test_case(OwaAst::kw("foo"), ":foo")]
#[test_case(OwaAst::str("foo"), "\"foo\"")]
#[test_case(OwaAst::float(1.0), "1.0")]
#[test_case(OwaAst::float(1.2), "1.2")]
#[test_case(OwaAst::float(1.234), "1.234")]
#[test_case(
OwaAst::vec([OwaAst::int(1), OwaAst::str("foo"), OwaAst::kw("a")]),
"[1 \"foo\" :a]"
)]
#[test_case(
OwaAst::call([OwaAst::int(1), OwaAst::float(1.0), OwaAst::kw("b"), OwaAst::str("foo")]),
"(1 1.0 :b \"foo\")"
)]
#[test_case(
OwaAst::set([OwaAst::int(1), OwaAst::float(1.0), OwaAst::kw("b"), OwaAst::str("foo")]),
"#{1.0 1 :b \"foo\"}"
)]
#[test_case(
OwaAst::map([
OwaAst::int(1), OwaAst::float(1.0),
OwaAst::kw("b"), OwaAst::str("foo"),
OwaAst::str("foo"), OwaAst::kw("b")
]),
"{1 1.0 :b \"foo\" \"foo\" :b}"
)]
fn unparse_tests(value: OwaAst, expected: &str) {
assert_eq!(value.unparse(), expected);
}
}

29
src/parser/call.rs Normal file
View file

@ -0,0 +1,29 @@
use nom::{IResult, Parser as _, combinator::map};
use crate::parser::{OwaAst, collection};
pub(super) fn parse_call(input: &str) -> IResult<&str, OwaAst> {
map(collection("(", ")"), OwaAst::call).parse(input)
}
#[test]
fn test_call() {
assert_eq!(
parse_call("(:a :b :c)"),
Ok((
"",
OwaAst::call([OwaAst::kw("a"), OwaAst::kw("b"), OwaAst::kw("c")])
))
);
assert_eq!(
parse_call("(:a [:b :c] [(:d)])garbage"),
Ok((
"garbage",
OwaAst::call([
OwaAst::kw("a"),
OwaAst::vec([OwaAst::kw("b"), OwaAst::kw("c"),]),
OwaAst::vec([OwaAst::call([OwaAst::kw("d")]),]),
])
))
);
}

14
src/parser/keyword.rs Normal file
View file

@ -0,0 +1,14 @@
use nom::{IResult, Parser as _, bytes::complete::tag, combinator::map, sequence::preceded};
use crate::parser::{OwaAst, identifier};
pub(super) fn parse_keyword(input: &str) -> IResult<&str, OwaAst> {
map(preceded(tag(":"), identifier), OwaAst::kw).parse(input)
}
#[test]
fn test_keyword() {
assert_eq!(parse_keyword(":foo"), Ok(("", OwaAst::kw("foo"))));
assert_eq!(parse_keyword(":foo1232"), Ok(("", OwaAst::kw("foo1232"))));
assert_eq!(parse_keyword(":foo:s"), Ok((":s", OwaAst::kw("foo"))));
}

30
src/parser/lambda.rs Normal file
View file

@ -0,0 +1,30 @@
use nom::{IResult, Parser as _, combinator::map};
use crate::parser::{OwaAst, collection};
pub(super) fn parse_lambda(input: &str) -> IResult<&str, OwaAst> {
map(collection("#(", ")"), OwaAst::new_lambda).parse(input)
}
#[test]
fn test_call() {
assert_eq!(
parse_lambda("#(:a :b :c)"),
Ok((
"",
OwaAst::new_lambda([OwaAst::kw("a"), OwaAst::kw("b"), OwaAst::kw("c")])
))
);
assert_eq!(
parse_lambda("#(:a [:b :c] [(:d)] #(:e))garbage"),
Ok((
"garbage",
OwaAst::new_lambda([
OwaAst::kw("a"),
OwaAst::vec([OwaAst::kw("b"), OwaAst::kw("c")]),
OwaAst::vec([OwaAst::call([OwaAst::kw("d")])]),
OwaAst::new_lambda([OwaAst::kw("e")])
])
))
);
}

39
src/parser/map.rs Normal file
View file

@ -0,0 +1,39 @@
use nom::{IResult, Parser as _, combinator::map};
use crate::parser::{OwaAst, collection};
pub(super) fn parse_map(input: &str) -> IResult<&str, OwaAst> {
map(collection("{", "}"), OwaAst::map).parse(input)
}
#[test]
fn test_call() {
assert_eq!(
parse_map("{:a :b :c}"),
Ok(("", OwaAst::map([OwaAst::kw("a"), OwaAst::kw("b")])))
);
assert_eq!(
parse_map("{:a :b :c :d}"),
Ok((
"",
OwaAst::map([
OwaAst::kw("a"),
OwaAst::kw("b"),
OwaAst::kw("c"),
OwaAst::kw("d")
])
))
);
assert_eq!(
parse_map("{:a #{:b :c} [(:d)] :e}garbage"),
Ok((
"garbage",
OwaAst::map([
OwaAst::kw("a"),
OwaAst::set([OwaAst::kw("b"), OwaAst::kw("c")]),
OwaAst::vec([OwaAst::call([OwaAst::kw("d"),]),]),
OwaAst::kw("e")
])
))
);
}

163
src/parser/mod.rs Normal file
View file

@ -0,0 +1,163 @@
use nom::{
IResult, Parser,
branch::alt,
bytes::complete::{is_not, tag},
character::complete::multispace1,
combinator::{cut, map, recognize},
error::Error,
multi::{many0, many1},
sequence::{delimited, pair, preceded},
};
mod annotation;
pub mod ast;
mod call;
mod keyword;
mod lambda;
mod map;
mod number;
mod quote;
mod set;
mod string;
mod symbol;
mod unquote;
mod vector;
use annotation::parse_annotation;
use ast::OwaAst;
use call::parse_call;
use keyword::parse_keyword;
use map::parse_map;
use number::{parse_float, parse_integer};
use quote::parse_quote;
use set::parse_set;
use string::parse_string;
use symbol::parse_symbol;
use unquote::parse_unquote;
use vector::parse_vector;
use crate::parser::lambda::parse_lambda;
pub(crate) const INDENT_IGNORE: &str = " \t\n\r,;:{}[]()<>$#\"'";
pub(crate) fn to_indent<S: AsRef<str>>(s: S) -> String {
s.as_ref()
.chars()
.filter(|c| !INDENT_IGNORE.contains(*c))
.collect()
}
fn identifier(input: &str) -> IResult<&str, &str> {
recognize(many1(is_not(INDENT_IGNORE))).parse(input)
}
fn parse_comment(input: &str) -> IResult<&str, ()> {
map(preceded(tag(";;"), is_not("\n")), |_| ()).parse(input)
}
fn skip_ws(input: &str) -> IResult<&str, ()> {
map(
many0(alt((map(multispace1, |_| ()), parse_comment))),
|_| (),
)
.parse(input)
}
fn collection<'a>(
start: &str,
end: &str,
) -> impl Parser<&'a str, Output = Vec<OwaAst>, Error = Error<&'a str>> {
delimited(
ws(tag(start)),
cut(many0(parse_value)),
cut((skip_ws, tag(end))),
)
}
fn ws<'a, O, F>(inner: F) -> impl Parser<&'a str, Output = O, Error = Error<&'a str>>
where
F: Parser<&'a str, Output = O, Error = Error<&'a str>>,
{
delimited(skip_ws, inner, skip_ws)
}
fn parse_atom(input: &str) -> IResult<&str, OwaAst> {
preceded(
skip_ws,
alt((
parse_float, // must be before parse_integer
parse_integer,
parse_string,
parse_keyword, // must be before parse_symbol
parse_symbol,
parse_lambda, // must be before parse_call
parse_call,
parse_set, // must be before parse_map
parse_map,
parse_vector,
parse_unquote,
parse_quote,
)),
)
.parse(input)
}
/// Parses a single OWA value (with optional leading annotations) from `input`.
///
/// # Errors
///
/// Returns a [`nom::Err`] if the input does not contain a valid OWA expression.
pub fn parse_value(input: &str) -> IResult<&str, OwaAst> {
map(
pair(many0(parse_annotation), parse_atom),
|(annotations, value)| {
let mut result = value;
for annotation in annotations {
result = result.to_annotation(annotation);
}
result
},
)
.parse(input)
}
/// Reads a file from disk and parses its contents as a single OWA value.
///
/// # Errors
///
/// Returns an error string if the file cannot be read or if its contents
/// cannot be parsed as a valid OWA expression.
pub fn parse_file(raw_filename: &str) -> Result<OwaAst, String> {
let content = std::fs::read_to_string(raw_filename)
.map_err(|err| format!("Failed to read file {raw_filename}: {err}"))?;
match parse_value(&content) {
Ok((_, value)) => Ok(value),
Err(err) => Err(format!("Failed to parse file {raw_filename}: {err:?}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[test_case("(1 2 3)", OwaAst::call([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]))]
#[test_case(r#"("hi" 1 "bye")"#, OwaAst::call([OwaAst::str("hi"), OwaAst::int(1), OwaAst::str("bye")]))]
#[test_case("[1 2 3]", OwaAst::vec([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]))]
#[test_case(r#"["hello world!" 1 "bye"]"#, OwaAst::vec([OwaAst::str("hello world!"), OwaAst::int(1), OwaAst::str("bye")]))]
#[test_case(";; comment\n(1 2 3)", OwaAst::call([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]))]
#[test_case("(1 ;; comment\n 2 3)", OwaAst::call([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]))]
#[test_case("(1 2 ;; comment\n 3)", OwaAst::call([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]))]
#[test_case("(1 2 3) ;; trailing comment", OwaAst::call([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]))]
fn parse_tests(input: &str, expected: OwaAst) {
assert_eq!(parse_value(input).unwrap().1, expected);
}
#[test]
fn parse_comment_test() {
let input = ";; this is a comment\n(1 2 3)";
let expected = OwaAst::call([OwaAst::int(1), OwaAst::int(2), OwaAst::int(3)]);
assert_eq!(parse_value(input).unwrap().1, expected);
}
}

170
src/parser/number.rs Normal file
View file

@ -0,0 +1,170 @@
use nom::{
IResult, Parser as _,
branch::alt,
bytes::complete::tag,
character::complete::{char, one_of},
combinator::{map_res, opt, recognize},
multi::{many0, many1},
sequence::{preceded, terminated},
};
use crate::parser::OwaAst;
fn hexadecimal(input: &str) -> IResult<&str, isize> {
map_res(
preceded(
alt((tag("0x"), tag("0X"))),
recognize(many1(terminated(
one_of("0123456789abcdefABCDEF"),
many0(char('_')),
))),
),
|out: &str| isize::from_str_radix(&str::replace(out, "_", ""), 16),
)
.parse(input)
}
fn octal(input: &str) -> IResult<&str, isize> {
map_res(
preceded(
alt((tag("0o"), tag("0O"))),
recognize(many1(terminated(one_of("01234567"), many0(char('_'))))),
),
|out: &str| isize::from_str_radix(&str::replace(out, "_", ""), 8),
)
.parse(input)
}
fn binary(input: &str) -> IResult<&str, isize> {
map_res(
preceded(
alt((tag("0b"), tag("0B"))),
recognize(many1(terminated(one_of("01"), many0(char('_'))))),
),
|out: &str| isize::from_str_radix(&str::replace(out, "_", ""), 2),
)
.parse(input)
}
fn decimal(input: &str) -> IResult<&str, isize> {
map_res(
recognize((
opt(one_of("+-")),
many1(terminated(one_of("0123456789"), many0(char('_')))),
)),
|out: &str| str::replace(out, "_", "").parse::<isize>(),
)
.parse(input)
}
fn decimal_str(input: &str) -> IResult<&str, &str> {
recognize(many1(terminated(one_of("0123456789"), many0(char('_'))))).parse(input)
}
pub fn parse_integer(input: &str) -> IResult<&str, OwaAst> {
let (input, value) = (alt((hexadecimal, octal, binary, decimal))).parse(input)?;
Ok((input, OwaAst::int(value)))
}
pub fn parse_float(input: &str) -> IResult<&str, OwaAst> {
let result = map_res(
alt((
// Case one: .42
recognize((
opt(one_of("+-")),
char('.'),
decimal_str,
opt((one_of("eE"), opt(one_of("+-")), decimal_str)),
)), // Case two: 42e42 and 42.42e42
recognize((
opt(one_of("+-")),
decimal_str,
opt(preceded(char('.'), decimal_str)),
one_of("eE"),
opt(one_of("+-")),
decimal_str,
)), // Case three: 42. and 42.42
recognize((opt(one_of("+-")), decimal_str, char('.'), opt(decimal_str))),
)),
|out: &str| -> Result<OwaAst, std::num::ParseFloatError> {
let result = out.parse::<f64>()?;
Ok(OwaAst::float(result))
},
)
.parse(input)?;
Ok(result)
}
#[test]
fn test_integer() {
// decimal
assert_eq!(parse_integer("12garbage"), Ok(("garbage", OwaAst::int(12))));
assert_eq!(parse_integer("12"), Ok(("", OwaAst::int(12))));
assert_eq!(parse_integer("-12garbage"), Ok(("garbage", OwaAst::int(-12))));
// assert_eq!(integer("-12"), Ok(("", OwaAst::int(-12))));
// hexadecimal
assert_eq!(parse_integer("0x12"), Ok(("", OwaAst::int(0x12))));
assert_eq!(
parse_integer("0x12garbage"),
Ok(("garbage", OwaAst::int(0x12)))
);
// assert_eq!(integer("-0x12"), Ok(("", OwaAst::int(-0x12))));
// assert_eq!(
// integer("-0x12garbage"),
// Ok(("garbage", OwaAst::int(-0x12)))
// );
// octal
assert_eq!(parse_integer("0o12"), Ok(("", OwaAst::int(0o12))));
assert_eq!(
parse_integer("0o12garbage"),
Ok(("garbage", OwaAst::int(0o12)))
);
// assert_eq!(integer("-0o12"), Ok(("", OwaAst::int(-0o12))));
// assert_eq!(
// integer("-0o12garbage"),
// Ok(("garbage", OwaAst::int(-0o12)))
// );
// binary
assert_eq!(parse_integer("0b10"), Ok(("", OwaAst::int(0b10))));
assert_eq!(
parse_integer("0b10garbage"),
Ok(("garbage", OwaAst::int(0b10)))
);
// assert_eq!(integer("-0b10"), Ok(("", OwaAst::int(-0b10))));
// assert_eq!(
// integer("-0b10garbage"),
// Ok(("garbage", OwaAst::int(-0b10)))
// );
}
#[test]
fn test_float() {
// normal
assert_eq!(parse_float("12."), Ok(("", OwaAst::float(12.0))));
assert_eq!(parse_float("12.0"), Ok(("", OwaAst::float(12.0))));
assert_eq!(parse_float("-12.0"), Ok(("", OwaAst::float(-12.0))));
assert_eq!(parse_float("12.garbage"), Ok(("garbage", OwaAst::float(12.0))));
assert_eq!(
parse_float("12.0garbage"),
Ok(("garbage", OwaAst::float(12.0)))
);
assert_eq!(
parse_float("-12.0garbage"),
Ok(("garbage", OwaAst::float(-12.0)))
);
// with exponent
assert_eq!(parse_float("12.0e12"), Ok(("", OwaAst::float(12.0e12))));
assert_eq!(
parse_float("12.0e12garbage"),
Ok(("garbage", OwaAst::float(12.0e12)))
);
assert_eq!(parse_float("-12.0e12"), Ok(("", OwaAst::float(-12.0e12))));
assert_eq!(
parse_float("-12.0e12garbage"),
Ok(("garbage", OwaAst::float(-12.0e12)))
);
}

24
src/parser/quote.rs Normal file
View file

@ -0,0 +1,24 @@
use nom::{IResult, Parser as _, bytes::complete::tag, combinator::map, sequence::preceded};
use crate::parser::{OwaAst, parse_value};
pub fn parse_quote(input: &str) -> IResult<&str, OwaAst> {
map(preceded(tag("'"), parse_value), OwaAst::new_quote).parse(input)
}
#[test]
fn test_keyword() {
assert_eq!(parse_quote("'foo"), Ok(("", OwaAst::sym("foo").to_quote())));
assert_eq!(
parse_quote("':foo1232"),
Ok(("", OwaAst::kw("foo1232").to_quote()))
);
assert_eq!(
parse_quote("'\"foo\":s"),
Ok((":s", OwaAst::str("foo").to_quote()))
);
assert_eq!(
parse_quote("''[\"foo\"]:s"),
Ok((":s", OwaAst::str("foo").to_vec().to_quote().to_quote()))
);
}

29
src/parser/set.rs Normal file
View file

@ -0,0 +1,29 @@
use nom::{IResult, Parser as _, combinator::map};
use crate::parser::{OwaAst, collection};
pub fn parse_set(input: &str) -> IResult<&str, OwaAst> {
map(collection("#{", "}"), OwaAst::set).parse(input)
}
#[test]
fn test_set() {
assert_eq!(
parse_set("#{:a :b :c}"),
Ok((
"",
OwaAst::set([OwaAst::kw("a"), OwaAst::kw("b"), OwaAst::kw("c")])
))
);
assert_eq!(
parse_set("#{:a [:b #{\"c\"}] [(:d)]}garbage"),
Ok((
"garbage",
OwaAst::set([
OwaAst::kw("a"),
OwaAst::vec([OwaAst::kw("b"), OwaAst::set([OwaAst::str("c")]),]),
OwaAst::vec([OwaAst::call([OwaAst::kw("d"),]),])
])
))
);
}

96
src/parser/string.rs Normal file
View file

@ -0,0 +1,96 @@
use nom::combinator::opt;
use nom::{IResult, Parser};
use nom::{
bytes::complete::{escaped, tag},
character::complete::{none_of, one_of},
combinator::map_res,
sequence::delimited,
};
use crate::parser::OwaAst;
fn unescape_string(s: &str) -> Result<String, String> {
let mut result = String::new();
let mut chars = s.chars();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('t') => result.push('\t'),
Some('b') => result.push('\x08'),
Some('f') => result.push('\x0C'),
Some('\\') => result.push('\\'),
Some('"') => result.push('"'),
Some(c) => {
return Err(format!("Unknown escape sequence: \\{c}"));
}
None => {
return Err("Unexpected end of string after backslash".to_string());
}
}
} else {
result.push(ch);
}
}
Ok(result)
}
pub fn parse_string(i: &str) -> IResult<&str, OwaAst> {
let (input, value) = delimited(
tag("\""),
opt(map_res(
escaped(none_of("\"\\"), '\\', one_of("\"\\nrtbf")),
unescape_string,
)),
tag("\""),
)
.parse(i)?;
Ok((input, OwaAst::str(value.unwrap_or_else(String::new))))
}
#[test]
fn test_symbol() {
assert_eq!(parse_string("\"foo\""), Ok(("", OwaAst::str("foo"))));
assert_eq!(
parse_string("\"foo1232\""),
Ok(("", OwaAst::str("foo1232")))
);
assert_eq!(
parse_string("\"foo\"garbage"),
Ok(("garbage", OwaAst::str("foo")))
);
assert_eq!(
parse_string("\"foo\\\"bar\"garbage"),
Ok(("garbage", OwaAst::str("foo\"bar")))
);
// Test newline escape
assert_eq!(
parse_string("\"hello\\nworld\""),
Ok(("", OwaAst::str("hello\nworld")))
);
// Test tab escape
assert_eq!(
parse_string("\"tab\\there\""),
Ok(("", OwaAst::str("tab\there")))
);
// Test backslash escape
assert_eq!(
parse_string("\"path\\\\file\""),
Ok(("", OwaAst::str("path\\file")))
);
// Test carriage return
assert_eq!(
parse_string("\"line\\rend\""),
Ok(("", OwaAst::str("line\rend")))
);
// Test space
assert_eq!(
parse_string("\"hello world\""),
Ok(("", OwaAst::str("hello world")))
);
}

22
src/parser/symbol.rs Normal file
View file

@ -0,0 +1,22 @@
use nom::{IResult, Parser as _, combinator::map};
use crate::parser::{OwaAst, identifier};
pub(super) fn parse_symbol(input: &str) -> IResult<&str, OwaAst> {
map(identifier, OwaAst::sym).parse(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symbol() {
assert_eq!(parse_symbol("foo"), Ok(("", OwaAst::sym("foo"))));
assert_eq!(parse_symbol("foo1232"), Ok(("", OwaAst::sym("foo1232"))));
assert_eq!(
parse_symbol("foo:garbage"),
Ok((":garbage", OwaAst::sym("foo")))
);
}
}

27
src/parser/unquote.rs Normal file
View file

@ -0,0 +1,27 @@
use nom::{IResult, Parser as _, bytes::complete::tag, combinator::map, sequence::preceded};
use crate::parser::{OwaAst, parse_value};
pub fn parse_unquote(input: &str) -> IResult<&str, OwaAst> {
map(preceded(tag("$"), parse_value), OwaAst::new_unquote).parse(input)
}
#[test]
fn test_keyword() {
assert_eq!(
parse_unquote("$foo"),
Ok(("", OwaAst::sym("foo").to_unquote()))
);
assert_eq!(
parse_unquote("$:foo1232"),
Ok(("", OwaAst::kw("foo1232").to_unquote()))
);
assert_eq!(
parse_unquote("$\"foo\":s"),
Ok((":s", OwaAst::str("foo").to_unquote()))
);
assert_eq!(
parse_unquote("$$[\"foo\"]:s"),
Ok((":s", OwaAst::str("foo").to_vec().to_unquote().to_unquote()))
);
}

29
src/parser/vector.rs Normal file
View file

@ -0,0 +1,29 @@
use nom::{IResult, Parser as _, combinator::map};
use crate::parser::{OwaAst, collection};
pub fn parse_vector(input: &str) -> IResult<&str, OwaAst> {
map(collection("[", "]"), OwaAst::vec).parse(input)
}
#[test]
fn test_vector() {
assert_eq!(
parse_vector("[:a :b :c]"),
Ok((
"",
OwaAst::vec([OwaAst::kw("a"), OwaAst::kw("b"), OwaAst::kw("c"),])
))
);
assert_eq!(
parse_vector("[:a [:b :c] [[:d]]]garbage"),
Ok((
"garbage",
OwaAst::vec([
OwaAst::kw("a"),
OwaAst::vec([OwaAst::kw("b"), OwaAst::kw("c"),]),
OwaAst::vec([OwaAst::vec([OwaAst::kw("d"),]),]),
])
))
);
}