release 0.1.0
This commit is contained in:
commit
30d94536a9
90 changed files with 7722 additions and 0 deletions
14
src/builtins/ast.rs
Normal file
14
src/builtins/ast.rs
Normal 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
97
src/builtins/cond.rs
Normal 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
83
src/builtins/errors.rs
Normal 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
299
src/builtins/ffi.rs
Normal 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
51
src/builtins/flow.rs
Normal 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
14
src/builtins/map.rs
Normal 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
83
src/builtins/math.rs
Normal 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
324
src/builtins/mod.rs
Normal 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
19
src/builtins/platform.rs
Normal 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
47
src/builtins/set.rs
Normal 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
48
src/builtins/str.rs
Normal 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
66
src/builtins/vec.rs
Normal 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
234
src/core/callable.rs
Normal 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
207
src/core/error.rs
Normal 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
492
src/core/interpreter.rs
Normal 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
12
src/core/mod.rs
Normal 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
77
src/core/scope.rs
Normal 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
23
src/core/special_names.rs
Normal 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
347
src/core/value.rs
Normal 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
18
src/lib.rs
Normal 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
51
src/main.rs
Normal 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
37
src/parser/annotation.rs
Normal 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
353
src/parser/ast.rs
Normal 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
29
src/parser/call.rs
Normal 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
14
src/parser/keyword.rs
Normal 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
30
src/parser/lambda.rs
Normal 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
39
src/parser/map.rs
Normal 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
163
src/parser/mod.rs
Normal 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
170
src/parser/number.rs
Normal 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
24
src/parser/quote.rs
Normal 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
29
src/parser/set.rs
Normal 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
96
src/parser/string.rs
Normal 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
22
src/parser/symbol.rs
Normal 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
27
src/parser/unquote.rs
Normal 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
29
src/parser/vector.rs
Normal 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"),]),]),
|
||||
])
|
||||
))
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue