use std::collections::{HashMap, HashSet};
use std::sync::Arc;
#[cfg(not(feature = "send-values"))]
use std::rc::Rc;
use tishlang_core::VmRef;
use tishlang_ast::{BinOp, UnaryOp};
use tishlang_builtins::array as arr_builtins;
use tishlang_builtins::construct as construct_builtin;
use tishlang_builtins::globals as globals_builtins;
use tishlang_builtins::math as math_builtins;
use tishlang_builtins::string as str_builtins;
use tishlang_bytecode::{u8_to_binop, u8_to_unaryop, Chunk, Constant, Opcode, NO_REST_PARAM};
use tishlang_core::{
merge_object_data, object_get, object_has, object_set, NativeFn, ObjectData, ObjectMap, Value,
};
#[cfg(feature = "send-values")]
#[inline]
fn make_native_fn<F>(f: F) -> NativeFn
where
F: Fn(&[Value]) -> Value + Send + Sync + 'static,
{
Arc::new(f)
}
#[cfg(not(feature = "send-values"))]
#[inline]
fn make_native_fn<F>(f: F) -> NativeFn
where
F: Fn(&[Value]) -> Value + 'static,
{
Rc::new(f)
}
type ArrayMethodFn = NativeFn;
#[cfg_attr(
not(any(
feature = "fs",
feature = "http",
feature = "promise",
feature = "timers",
feature = "process",
feature = "ws"
)),
allow(dead_code)
)]
#[inline]
fn value_object_from_map(m: ObjectMap) -> Value {
Value::Object(VmRef::new(ObjectData::from_strings(m)))
}
#[cfg(any(
feature = "fs",
feature = "http",
feature = "promise",
feature = "timers",
feature = "process",
feature = "ws"
))]
#[inline]
fn cap_allows(enabled: &HashSet<String>, name: &str) -> bool {
enabled.contains("full") || enabled.contains(name)
}
pub fn all_compiled_capabilities() -> HashSet<String> {
#[allow(unused_mut)]
let mut s = HashSet::new();
#[cfg(feature = "http")]
s.insert("http".to_string());
#[cfg(feature = "promise")]
s.insert("promise".to_string());
#[cfg(feature = "timers")]
s.insert("timers".to_string());
#[cfg(feature = "fs")]
s.insert("fs".to_string());
#[cfg(feature = "process")]
s.insert("process".to_string());
#[cfg(feature = "regex")]
s.insert("regex".to_string());
#[cfg(feature = "ws")]
s.insert("ws".to_string());
s
}
#[cfg_attr(
not(any(
feature = "fs",
feature = "http",
feature = "promise",
feature = "timers",
feature = "process",
feature = "ws"
)),
allow(unused_variables)
)]
fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str) -> Option<Value> {
#[cfg(feature = "fs")]
if spec == "tish:fs" && cap_allows(enabled, "fs") {
return match export_name {
"readFile" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::read_file(args)
})),
"writeFile" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::write_file(args)
})),
"fileExists" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::file_exists(args)
})),
"isDir" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::is_dir(args)
})),
"readDir" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::read_dir(args)
})),
"mkdir" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::mkdir(args)
})),
_ => None,
};
}
#[cfg(feature = "http")]
if spec == "tish:http" && cap_allows(enabled, "http") {
return match export_name {
"await" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::await_promise(args.first().cloned().unwrap_or(Value::Null))
})),
"fetch" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::fetch_promise(args.to_vec())
})),
"fetchAll" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::fetch_all_promise(args.to_vec())
})),
"Promise" => Some(tishlang_runtime::promise_object()),
"serve" => Some(Value::native(|args: &[Value]| {
let raw = args.get(1).cloned().unwrap_or(Value::Null);
let handler_value = match raw {
Value::Function(_) => raw,
Value::Object(ref obj) => {
let obj_ref = obj.borrow();
if let Some(Value::Function(on_worker)) =
obj_ref.strings.get(&std::sync::Arc::from("onWorker")).cloned()
{
let args_for_init = [Value::Number(0.0)];
on_worker(&args_for_init)
} else if let Some(h) =
obj_ref.strings.get(&std::sync::Arc::from("handler")).cloned()
{
h
} else {
Value::Null
}
}
_ => Value::Null,
};
if let Value::Function(f) = handler_value {
tishlang_runtime::http_serve(args, move |req_args| f(req_args))
} else {
Value::Null
}
})),
_ => None,
};
}
#[cfg(all(feature = "promise", not(feature = "http")))]
if spec == "tish:http" && cap_allows(enabled, "promise") {
return match export_name {
"Promise" => Some(tishlang_runtime::promise_object()),
"await" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::await_promise(args.first().cloned().unwrap_or(Value::Null))
})),
_ => None,
};
}
#[cfg(feature = "timers")]
if spec == "tish:timers" && cap_allows(enabled, "timers") {
return match export_name {
"setTimeout" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::timer_set_timeout(args)
})),
"setInterval" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::timer_set_interval(args)
})),
"clearTimeout" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::timer_clear_timeout(args)
})),
"clearInterval" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::timer_clear_interval(args)
})),
_ => None,
};
}
#[cfg(feature = "process")]
if spec == "tish:process" && cap_allows(enabled, "process") {
return match export_name {
"exit" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::process_exit(args)
})),
"cwd" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::process_cwd(args)
})),
"exec" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::process_exec(args)
})),
"argv" => Some(Value::Array(VmRef::new(
std::env::args().map(|s| Value::String(s.into())).collect(),
))),
"env" => Some(value_object_from_map(
std::env::vars()
.map(|(k, v)| (Arc::from(k.as_str()), Value::String(v.into())))
.collect(),
)),
"process" => {
let mut m = ObjectMap::default();
m.insert(
"exit".into(),
Value::native(|args: &[Value]| tishlang_runtime::process_exit(args)),
);
m.insert(
"cwd".into(),
Value::native(|args: &[Value]| tishlang_runtime::process_cwd(args)),
);
m.insert(
"exec".into(),
Value::native(|args: &[Value]| tishlang_runtime::process_exec(args)),
);
m.insert(
"argv".into(),
Value::Array(VmRef::new(
std::env::args().map(|s| Value::String(s.into())).collect(),
)),
);
m.insert(
"env".into(),
value_object_from_map(
std::env::vars()
.map(|(k, v)| (Arc::from(k.as_str()), Value::String(v.into())))
.collect(),
),
);
Some(value_object_from_map(m))
}
_ => None,
};
}
#[cfg(feature = "ws")]
if spec == "tish:ws" && cap_allows(enabled, "ws") {
return match export_name {
"WebSocket" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::web_socket_client(args)
})),
"Server" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::web_socket_server_construct(args)
})),
"wsSend" => Some(Value::native(|args: &[Value]| {
Value::Bool(tishlang_runtime::ws_send_native(
args.first().unwrap_or(&Value::Null),
&args
.get(1)
.map(|v| v.to_display_string())
.unwrap_or_default(),
))
})),
"wsBroadcast" => Some(Value::native(|args: &[Value]| {
tishlang_runtime::ws_broadcast_native(args)
})),
_ => None,
};
}
None
}
#[cfg(not(feature = "wasm"))]
fn vm_log(s: &str) {
println!("{}", s);
}
#[cfg(not(feature = "wasm"))]
fn vm_log_err(s: &str) {
eprintln!("{}", s);
}
#[cfg(feature = "wasm")]
fn vm_log(s: &str) {
#[wasm_bindgen::prelude::wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
log(s);
}
#[cfg(feature = "wasm")]
fn vm_log_err(s: &str) {
#[wasm_bindgen::prelude::wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn error(s: &str);
}
error(s);
}
#[allow(unused_variables)]
fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
let mut g = ObjectMap::default();
let mut console = ObjectMap::default();
console.insert(
"debug".into(),
Value::native(|args: &[Value]| {
let s =
tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
vm_log(&s);
Value::Null
}),
);
console.insert(
"log".into(),
Value::native(|args: &[Value]| {
let s =
tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
vm_log(&s);
Value::Null
}),
);
console.insert(
"info".into(),
Value::native(|args: &[Value]| {
let s =
tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
vm_log(&s);
Value::Null
}),
);
console.insert(
"warn".into(),
Value::native(|args: &[Value]| {
let s =
tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
vm_log_err(&s);
Value::Null
}),
);
console.insert(
"error".into(),
Value::native(|args: &[Value]| {
let s =
tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
vm_log_err(&s);
Value::Null
}),
);
g.insert("console".into(), value_object_from_map(console));
let mut math = ObjectMap::default();
math.insert(
"abs".into(),
Value::native(|args: &[Value]| {
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
Value::Number(n.abs())
}),
);
math.insert(
"sqrt".into(),
Value::native(|args: &[Value]| {
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
Value::Number(n.sqrt())
}),
);
math.insert(
"floor".into(),
Value::native(|args: &[Value]| {
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
Value::Number(n.floor())
}),
);
math.insert(
"ceil".into(),
Value::native(|args: &[Value]| {
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
Value::Number(n.ceil())
}),
);
math.insert(
"round".into(),
Value::native(|args: &[Value]| {
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
Value::Number(n.round())
}),
);
math.insert(
"random".into(),
Value::native(|_| Value::Number(rand::random::<f64>())),
);
math.insert(
"min".into(),
Value::native(|args: &[Value]| {
let nums: Vec<f64> = args.iter().filter_map(|v| v.as_number()).collect();
Value::Number(nums.into_iter().fold(f64::NAN, |a, b| a.min(b)))
}),
);
math.insert(
"max".into(),
Value::native(|args: &[Value]| {
let nums: Vec<f64> = args.iter().filter_map(|v| v.as_number()).collect();
Value::Number(nums.into_iter().fold(f64::NAN, |a, b| a.max(b)))
}),
);
math.insert(
"pow".into(),
Value::native(|args: &[Value]| math_builtins::pow(args)),
);
math.insert(
"sin".into(),
Value::native(|args: &[Value]| math_builtins::sin(args)),
);
math.insert(
"cos".into(),
Value::native(|args: &[Value]| math_builtins::cos(args)),
);
math.insert(
"tan".into(),
Value::native(|args: &[Value]| math_builtins::tan(args)),
);
math.insert(
"log".into(),
Value::native(|args: &[Value]| math_builtins::log(args)),
);
math.insert(
"exp".into(),
Value::native(|args: &[Value]| math_builtins::exp(args)),
);
math.insert(
"sign".into(),
Value::native(|args: &[Value]| math_builtins::sign(args)),
);
math.insert(
"trunc".into(),
Value::native(|args: &[Value]| math_builtins::trunc(args)),
);
math.insert(
"atan2".into(),
Value::native(|args: &[Value]| {
let y = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
let x = args.get(1).and_then(|v| v.as_number()).unwrap_or(f64::NAN);
Value::Number(y.atan2(x))
}),
);
math.insert(
"atan".into(),
Value::native(|args: &[Value]| {
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
Value::Number(n.atan())
}),
);
math.insert(
"asin".into(),
Value::native(|args: &[Value]| {
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
Value::Number(n.asin())
}),
);
math.insert(
"acos".into(),
Value::native(|args: &[Value]| {
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
Value::Number(n.acos())
}),
);
math.insert(
"hypot".into(),
Value::native(|args: &[Value]| {
let nums: Vec<f64> = args.iter().filter_map(|v| v.as_number()).collect();
let sum_sq: f64 = nums.iter().map(|n| n * n).sum();
Value::Number(sum_sq.sqrt())
}),
);
math.insert("PI".into(), Value::Number(std::f64::consts::PI));
math.insert("E".into(), Value::Number(std::f64::consts::E));
g.insert("Math".into(), value_object_from_map(math));
let mut json = ObjectMap::default();
json.insert(
"parse".into(),
Value::native(|args: &[Value]| {
let s = args
.first()
.map(|v| v.to_display_string())
.unwrap_or_default();
tishlang_core::json_parse(&s).unwrap_or(Value::Null)
}),
);
json.insert(
"stringify".into(),
Value::native(|args: &[Value]| {
let v = args.first().unwrap_or(&Value::Null);
Value::String(tishlang_core::json_stringify(v).into())
}),
);
g.insert("JSON".into(), value_object_from_map(json));
g.insert(
"parseInt".into(),
Value::native(|args: &[Value]| globals_builtins::parse_int(args)),
);
g.insert(
"parseFloat".into(),
Value::native(|args: &[Value]| globals_builtins::parse_float(args)),
);
g.insert(
"encodeURI".into(),
Value::native(|args: &[Value]| globals_builtins::encode_uri(args)),
);
g.insert(
"decodeURI".into(),
Value::native(|args: &[Value]| globals_builtins::decode_uri(args)),
);
g.insert(
"htmlEscape".into(),
Value::native(|args: &[Value]| {
tishlang_builtins::string::escape_html(args.first().unwrap_or(&Value::Null))
}),
);
g.insert(
"Boolean".into(),
Value::native(|args: &[Value]| globals_builtins::boolean(args)),
);
g.insert(
"isFinite".into(),
Value::native(|args: &[Value]| globals_builtins::is_finite(args)),
);
g.insert(
"isNaN".into(),
Value::native(|args: &[Value]| globals_builtins::is_nan(args)),
);
g.insert("Infinity".into(), Value::Number(f64::INFINITY));
g.insert("NaN".into(), Value::Number(f64::NAN));
g.insert(
"typeof".into(),
Value::native(|args: &[Value]| {
let v = args.first().unwrap_or(&Value::Null);
Value::String(v.type_name().into())
}),
);
g.insert(
"Symbol".into(),
tishlang_builtins::symbol::symbol_object(),
);
let mut date = ObjectMap::default();
date.insert(
"now".into(),
Value::native(|_args: &[Value]| {
let ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as f64;
Value::Number(ms)
}),
);
g.insert("Date".into(), value_object_from_map(date));
g.insert(
"Uint8Array".into(),
construct_builtin::uint8_array_constructor_value(),
);
g.insert(
"AudioContext".into(),
construct_builtin::audio_context_constructor_value(),
);
let mut object_methods = ObjectMap::default();
object_methods.insert(
"assign".into(),
Value::native(|args: &[Value]| globals_builtins::object_assign(args)),
);
object_methods.insert(
"fromEntries".into(),
Value::native(|args: &[Value]| globals_builtins::object_from_entries(args)),
);
object_methods.insert(
"keys".into(),
Value::native(|args: &[Value]| globals_builtins::object_keys(args)),
);
object_methods.insert(
"values".into(),
Value::native(|args: &[Value]| globals_builtins::object_values(args)),
);
object_methods.insert(
"entries".into(),
Value::native(|args: &[Value]| globals_builtins::object_entries(args)),
);
g.insert("Object".into(), value_object_from_map(object_methods));
let mut array_static = ObjectMap::default();
array_static.insert(
"isArray".into(),
Value::native(|args: &[Value]| globals_builtins::array_is_array(args)),
);
g.insert("Array".into(), value_object_from_map(array_static));
let string_convert_fn = Value::native(|args: &[Value]| globals_builtins::string_convert(args));
let mut string_static = ObjectMap::default();
string_static.insert(
"fromCharCode".into(),
Value::native(|args: &[Value]| globals_builtins::string_from_char_code(args)),
);
string_static.insert(Arc::from("__call"), string_convert_fn);
g.insert("String".into(), value_object_from_map(string_static));
g.insert("h".into(), Value::native(|_args: &[Value]| Value::Null));
g.insert(
"Fragment".into(),
value_object_from_map(ObjectMap::default()),
);
g.insert(
"createRoot".into(),
Value::native(|_args: &[Value]| {
let mut render_obj = ObjectMap::default();
render_obj.insert(
"render".into(),
Value::native(|_args: &[Value]| Value::Null),
);
value_object_from_map(render_obj)
}),
);
g.insert(
"useState".into(),
Value::native(|args: &[Value]| {
let init = args.first().cloned().unwrap_or(Value::Null);
let arr = vec![init, Value::native(|_| Value::Null)];
Value::Array(VmRef::new(arr))
}),
);
let mut document_obj = ObjectMap::default();
document_obj.insert("body".into(), Value::Null);
g.insert("document".into(), value_object_from_map(document_obj));
#[cfg(feature = "process")]
if cap_allows(enabled, "process") {
let mut process_obj = ObjectMap::default();
process_obj.insert(
"exit".into(),
Value::native(|args: &[Value]| tishlang_runtime::process_exit(args)),
);
process_obj.insert(
"cwd".into(),
Value::native(|args: &[Value]| tishlang_runtime::process_cwd(args)),
);
process_obj.insert(
"exec".into(),
Value::native(|args: &[Value]| tishlang_runtime::process_exec(args)),
);
process_obj.insert(
"argv".into(),
Value::Array(VmRef::new(
std::env::args().map(|s| Value::String(s.into())).collect(),
)),
);
process_obj.insert(
"env".into(),
value_object_from_map(
std::env::vars()
.map(|(k, v)| (Arc::from(k.as_str()), Value::String(v.into())))
.collect(),
),
);
g.insert("process".into(), value_object_from_map(process_obj));
}
#[cfg(feature = "timers")]
if cap_allows(enabled, "timers") {
g.insert(
"setTimeout".into(),
Value::native(|args: &[Value]| tishlang_runtime::timer_set_timeout(args)),
);
g.insert(
"clearTimeout".into(),
Value::native(|args: &[Value]| tishlang_runtime::timer_clear_timeout(args)),
);
g.insert(
"setInterval".into(),
Value::native(|args: &[Value]| tishlang_runtime::timer_set_interval(args)),
);
g.insert(
"clearInterval".into(),
Value::native(|args: &[Value]| tishlang_runtime::timer_clear_interval(args)),
);
}
#[cfg(feature = "http")]
if cap_allows(enabled, "http") {
g.insert(
"fetch".into(),
Value::native(|args: &[Value]| tishlang_runtime::fetch_promise(args.to_vec())),
);
g.insert(
"fetchAll".into(),
Value::native(|args: &[Value]| tishlang_runtime::fetch_all_promise(args.to_vec())),
);
g.insert(
"registerStaticRoute".into(),
Value::native(|args: &[Value]| {
let path = match args.first() {
Some(Value::String(s)) => s.to_string(),
_ => return Value::Null,
};
let body = match args.get(1) {
Some(Value::String(s)) => s.as_bytes().to_vec(),
_ => return Value::Null,
};
let ct = match args.get(2) {
Some(Value::String(s)) => s.to_string(),
_ => "application/octet-stream".to_string(),
};
tishlang_runtime::register_static_route(&path, &body, &ct);
Value::Null
}),
);
g.insert(
"serve".into(),
Value::native(|args: &[Value]| {
let raw = args.get(1).cloned().unwrap_or(Value::Null);
let handler_value = match raw {
Value::Function(_) => raw,
Value::Object(ref obj) => {
let obj_ref = obj.borrow();
if let Some(Value::Function(on_worker)) =
obj_ref.strings.get(&std::sync::Arc::from("onWorker")).cloned()
{
let args_for_init = [Value::Number(0.0)];
on_worker(&args_for_init)
} else if let Some(h) =
obj_ref.strings.get(&std::sync::Arc::from("handler")).cloned()
{
h
} else {
Value::Null
}
}
_ => Value::Null,
};
if let Value::Function(f) = handler_value {
tishlang_runtime::http_serve(args, move |req_args| f(req_args))
} else {
Value::Null
}
}),
);
}
#[cfg(any(feature = "http", feature = "promise"))]
if cap_allows(enabled, "http") || cap_allows(enabled, "promise") {
g.insert("Promise".into(), tishlang_runtime::promise_object());
}
g
}
type ScopeMap = VmRef<ObjectMap>;
#[derive(Clone, Debug, Default)]
pub struct VmRunOptions {
pub repl_mode: bool,
pub capabilities: HashSet<String>,
}
pub struct Vm {
stack: Vec<Value>,
scope: ObjectMap,
enclosing: Option<ScopeMap>,
globals: VmRef<ObjectMap>,
capabilities: Arc<HashSet<String>>,
native_modules: VmRef<HashMap<String, VmRef<ObjectMap>>>,
}
impl Vm {
pub fn new() -> Self {
Self::with_capabilities_arc(Arc::new(all_compiled_capabilities()))
}
pub fn with_capabilities(capabilities: HashSet<String>) -> Self {
Self::with_capabilities_arc(Arc::new(capabilities))
}
fn with_capabilities_arc(capabilities: Arc<HashSet<String>>) -> Self {
Self {
stack: Vec::new(),
scope: ObjectMap::default(),
enclosing: None,
globals: VmRef::new(init_globals(capabilities.as_ref())),
capabilities,
native_modules: VmRef::new(HashMap::new()),
}
}
pub fn register_native_module(&mut self, spec: impl Into<String>, exports: ObjectMap) {
self.native_modules
.borrow_mut()
.insert(spec.into(), VmRef::new(exports));
}
pub fn get_global(&self, name: &str) -> Option<Value> {
self.globals.borrow().get(name).cloned()
}
pub fn set_global(&mut self, name: Arc<str>, value: Value) {
self.globals.borrow_mut().insert(name, value);
}
pub fn global_names(&self) -> Vec<String> {
self.globals
.borrow()
.keys()
.map(|k| k.as_ref().to_string())
.collect()
}
fn read_u16(code: &[u8], ip: &mut usize) -> u16 {
let a = code[*ip] as u16;
let b = code[*ip + 1] as u16;
*ip += 2;
(a << 8) | b
}
fn read_i16(code: &[u8], ip: &mut usize) -> i16 {
Self::read_u16(code, ip) as i16
}
fn unwind_throw(
try_handlers: &mut Vec<(usize, usize)>,
stack: &mut Vec<Value>,
ip: &mut usize,
v: Value,
) -> Result<(), String> {
let (catch_ip, stack_len) = try_handlers
.pop()
.ok_or_else(|| format!("Uncaught throw: {}", v.to_display_string()))?;
stack.truncate(stack_len);
stack.push(v);
*ip = catch_ip;
Ok(())
}
pub fn run(&mut self, chunk: &Chunk) -> Result<Value, String> {
self.run_with_options(chunk, false)
}
pub fn run_with_options(&mut self, chunk: &Chunk, repl_mode: bool) -> Result<Value, String> {
self.run_chunk(chunk, &chunk.nested, &[], repl_mode)
}
fn run_chunk(
&mut self,
chunk: &Chunk,
nested: &[Chunk],
args: &[Value],
repl_mode: bool,
) -> Result<Value, String> {
let code = &chunk.code;
let constants = &chunk.constants;
let names = &chunk.names;
let mut ip = 0;
let local_scope: ScopeMap = VmRef::new(ObjectMap::default());
{
let mut ls = local_scope.borrow_mut();
let param_count = chunk.param_count as usize;
if chunk.rest_param_index != NO_REST_PARAM {
let ri = chunk.rest_param_index as usize;
for (i, name) in chunk.names.iter().take(param_count).enumerate() {
if i < ri {
let v = args.get(i).cloned().unwrap_or(Value::Null);
ls.insert(Arc::clone(name), v);
} else if i == ri {
let rest_arr: Vec<Value> = args.iter().skip(ri).cloned().collect();
ls.insert(Arc::clone(name), Value::Array(VmRef::new(rest_arr)));
}
}
} else {
for (i, name) in chunk.names.iter().take(param_count).enumerate() {
if let Some(v) = args.get(i) {
ls.insert(Arc::clone(name), v.clone());
}
}
}
}
let mut try_handlers: Vec<(usize, usize)> = vec![];
let mut block_undo_stack: Vec<Vec<(Arc<str>, Option<Value>)>> = vec![];
loop {
if ip >= code.len() {
break;
}
let op = code[ip];
ip += 1;
if op == Opcode::Nop as u8 {
continue;
}
let opcode = Opcode::from_u8(op).ok_or_else(|| format!("Unknown opcode: {}", op))?;
match opcode {
Opcode::Nop => {}
Opcode::LoadConst => {
let idx = Self::read_u16(code, &mut ip);
let c = constants
.get(idx as usize)
.ok_or_else(|| format!("Constant index out of bounds: {}", idx))?;
let v = match c {
Constant::Number(n) => Value::Number(*n),
Constant::String(s) => Value::String(Arc::clone(s)),
Constant::Bool(b) => Value::Bool(*b),
Constant::Null => Value::Null,
Constant::Closure(nested_idx) => {
let inner = nested
.get(*nested_idx)
.ok_or_else(|| "Nested chunk index out of bounds".to_string())?;
let inner_clone = inner.clone();
let globals = self.globals.clone();
let enclosing = Some(local_scope.clone());
let capabilities = Arc::clone(&self.capabilities);
let native_modules = self.native_modules.clone();
Value::native(move |args: &[Value]| {
let mut vm = Vm {
stack: Vec::new(),
scope: ObjectMap::default(),
enclosing: enclosing.clone(),
globals: globals.clone(),
capabilities: Arc::clone(&capabilities),
native_modules: native_modules.clone(),
};
vm.run_chunk(&inner_clone, &inner_clone.nested, args, false)
.unwrap_or(Value::Null)
})
}
};
self.stack.push(v);
}
Opcode::LoadVar => {
let idx = Self::read_u16(code, &mut ip);
let name = names
.get(idx as usize)
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
let v = local_scope
.borrow()
.get(name.as_ref())
.cloned()
.or_else(|| {
self.enclosing
.as_ref()
.and_then(|e| e.borrow().get(name.as_ref()).cloned())
})
.or_else(|| self.scope.get(name.as_ref()).cloned())
.or_else(|| self.globals.borrow().get(name.as_ref()).cloned())
.ok_or_else(|| format!("Undefined variable: {}", name))?;
self.stack.push(v);
}
Opcode::StoreVar => {
let idx = Self::read_u16(code, &mut ip);
let name = names
.get(idx as usize)
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
let v = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
if local_scope.borrow().contains_key(name.as_ref()) {
local_scope.borrow_mut().insert(Arc::clone(name), v);
} else if self
.enclosing
.as_ref()
.map(|e| e.borrow().contains_key(name.as_ref()))
.unwrap_or(false)
{
let en = self.enclosing.as_ref().unwrap();
en.borrow_mut().insert(Arc::clone(name), v);
} else if self.scope.contains_key(name.as_ref()) {
self.scope.insert(Arc::clone(name), v);
} else if self.globals.borrow().contains_key(name.as_ref()) {
self.globals.borrow_mut().insert(Arc::clone(name), v);
} else {
if self.enclosing.is_none() {
self.globals.borrow_mut().insert(Arc::clone(name), v);
} else {
local_scope.borrow_mut().insert(Arc::clone(name), v);
}
}
}
Opcode::DeclareVar => {
let idx = Self::read_u16(code, &mut ip);
let name = names
.get(idx as usize)
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
let v = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
if let Some(frame) = block_undo_stack.last_mut() {
let old = local_scope.borrow().get(name.as_ref()).cloned();
frame.push((Arc::clone(name), old));
}
if repl_mode && self.enclosing.is_none() && block_undo_stack.is_empty() {
self.globals
.borrow_mut()
.insert(Arc::clone(name), v.clone());
}
local_scope.borrow_mut().insert(Arc::clone(name), v);
}
Opcode::DeclareVarPlain => {
let idx = Self::read_u16(code, &mut ip);
let name = names
.get(idx as usize)
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
let v = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
if repl_mode && self.enclosing.is_none() && block_undo_stack.is_empty() {
self.globals
.borrow_mut()
.insert(Arc::clone(name), v.clone());
}
local_scope.borrow_mut().insert(Arc::clone(name), v);
}
Opcode::EnterBlock => {
block_undo_stack.push(Vec::new());
}
Opcode::ExitBlock => {
let frame = block_undo_stack
.pop()
.ok_or_else(|| "ExitBlock without matching EnterBlock".to_string())?;
for (name, old) in frame.into_iter().rev() {
let mut ls = local_scope.borrow_mut();
match old {
Some(prev) => {
ls.insert(name, prev);
}
None => {
ls.remove(name.as_ref());
}
}
}
}
Opcode::LoadGlobal => {
let idx = Self::read_u16(code, &mut ip);
let name = names
.get(idx as usize)
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
let v = self
.globals
.borrow()
.get(name.as_ref())
.cloned()
.ok_or_else(|| format!("Undefined global: {}", name))?;
self.stack.push(v);
}
Opcode::StoreGlobal => {
let idx = Self::read_u16(code, &mut ip);
let name = names
.get(idx as usize)
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
let v = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
self.globals.borrow_mut().insert(Arc::clone(name), v);
}
Opcode::Pop => {
self.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
}
Opcode::PopN => {
let n = Self::read_u16(code, &mut ip) as usize;
for _ in 0..n {
self.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
}
}
Opcode::Dup => {
let v = self
.stack
.last()
.ok_or_else(|| "Stack underflow".to_string())?
.clone();
self.stack.push(v);
}
Opcode::Call => {
let argc = Self::read_u16(code, &mut ip) as usize;
let mut args = Vec::with_capacity(argc);
for _ in 0..argc {
args.push(
self.stack
.pop()
.ok_or_else(|| "Stack underflow in call".to_string())?,
);
}
args.reverse();
let callee = self
.stack
.pop()
.ok_or_else(|| "Stack underflow: no callee".to_string())?;
let f = match &callee {
Value::Function(f) => f.clone(),
Value::Object(o) => {
if let Some(Value::Function(call_fn)) =
o.borrow().strings.get("__call")
{
call_fn.clone()
} else {
return Err(format!(
"Call of non-function: {}",
callee.type_name()
));
}
}
_ => {
return Err(format!("Call of non-function: {}", callee.type_name()));
}
};
let result = f(&args);
self.stack.push(result);
}
Opcode::CallSpread => {
let callee = self
.stack
.pop()
.ok_or_else(|| "Stack underflow: no callee in CallSpread".to_string())?;
let args_array = self
.stack
.pop()
.ok_or_else(|| "Stack underflow in CallSpread".to_string())?;
let args: Vec<Value> = match &args_array {
Value::Array(a) => a.borrow().clone(),
_ => {
return Err(format!(
"CallSpread: args must be array, got {}",
args_array.to_display_string()
));
}
};
let f = match &callee {
Value::Function(f) => f.clone(),
Value::Object(o) => {
if let Some(Value::Function(call_fn)) =
o.borrow().strings.get("__call")
{
call_fn.clone()
} else {
return Err(format!(
"Call of non-function: {}",
callee.type_name()
));
}
}
_ => {
return Err(format!("Call of non-function: {}", callee.type_name()));
}
};
let result = f(&args);
self.stack.push(result);
}
Opcode::Construct => {
let argc = Self::read_u16(code, &mut ip) as usize;
let mut args = Vec::with_capacity(argc);
for _ in 0..argc {
args.push(
self.stack
.pop()
.ok_or_else(|| "Stack underflow in construct".to_string())?,
);
}
args.reverse();
let callee = self
.stack
.pop()
.ok_or_else(|| "Stack underflow: no callee for construct".to_string())?;
let result = construct_builtin::construct(&callee, &args);
self.stack.push(result);
}
Opcode::ConstructSpread => {
let callee = self
.stack
.pop()
.ok_or_else(|| "Stack underflow: callee in ConstructSpread".to_string())?;
let args_array = self
.stack
.pop()
.ok_or_else(|| "Stack underflow in ConstructSpread".to_string())?;
let args: Vec<Value> = match &args_array {
Value::Array(a) => a.borrow().clone(),
_ => {
return Err(format!(
"ConstructSpread: args must be array, got {}",
args_array.to_display_string()
));
}
};
let result = construct_builtin::construct(&callee, &args);
self.stack.push(result);
}
Opcode::Return => {
let v = self.stack.pop().unwrap_or(Value::Null);
return Ok(v);
}
Opcode::Jump => {
let offset = Self::read_i16(code, &mut ip) as isize;
ip = (ip as isize + offset).max(0) as usize;
}
Opcode::JumpIfFalse => {
let offset = Self::read_i16(code, &mut ip) as isize;
let v = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
if !v.is_truthy() {
ip = (ip as isize + offset).max(0) as usize;
}
}
Opcode::JumpBack => {
let dist = Self::read_u16(code, &mut ip) as usize;
ip = ip.saturating_sub(dist);
}
Opcode::BinOp => {
let op_u8 = Self::read_u16(code, &mut ip) as u8;
let r = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let l = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let op =
u8_to_binop(op_u8).ok_or_else(|| format!("Unknown binop: {}", op_u8))?;
let result = eval_binop(op, &l, &r)?;
self.stack.push(result);
}
Opcode::UnaryOp => {
let op_u8 = Self::read_u16(code, &mut ip) as u8;
let o = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let op = u8_to_unaryop(op_u8)
.ok_or_else(|| format!("Unknown unary op: {}", op_u8))?;
let result = eval_unary(op, &o)?;
self.stack.push(result);
}
Opcode::GetMember => {
let idx = Self::read_u16(code, &mut ip);
let key = names
.get(idx as usize)
.ok_or_else(|| "Name index out of bounds".to_string())?;
let obj = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let v = get_member(&obj, key)?;
self.stack.push(v);
}
Opcode::GetMemberOptional => {
let idx = Self::read_u16(code, &mut ip);
let key = names
.get(idx as usize)
.ok_or_else(|| "Name index out of bounds".to_string())?;
let obj = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let v = get_member(&obj, key).unwrap_or(Value::Null);
self.stack.push(v);
}
Opcode::SetMember => {
let idx = Self::read_u16(code, &mut ip);
let key = names
.get(idx as usize)
.ok_or_else(|| "Name index out of bounds".to_string())?;
let val = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let obj = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
set_member(&obj, key, val.clone())?;
self.stack.push(val); }
Opcode::GetIndex => {
let idx_val = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let obj = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let v = get_index(&obj, &idx_val)?;
self.stack.push(v);
}
Opcode::SetIndex => {
let dup_val = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let val = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let idx_val = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let obj = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
set_index(&obj, &idx_val, val.clone())?;
self.stack.push(dup_val); }
Opcode::NewArray => {
let n = Self::read_u16(code, &mut ip) as usize;
let mut elems = Vec::with_capacity(n);
for _ in 0..n {
elems.push(
self.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?,
);
}
elems.reverse();
self.stack.push(Value::Array(VmRef::new(elems)));
}
Opcode::NewObject => {
let n = Self::read_u16(code, &mut ip) as usize;
let mut map = ObjectMap::with_capacity(n.max(1));
for _ in 0..n {
let val = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let key_val = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let key = key_val.to_display_string().into();
map.insert(key, val);
}
self.stack.push(value_object_from_map(map));
}
Opcode::EnterTry => {
let offset = Self::read_u16(code, &mut ip) as usize;
let catch_ip = ip + offset;
try_handlers.push((catch_ip, self.stack.len()));
}
Opcode::ExitTry => {
try_handlers.pop();
}
Opcode::ConcatArray => {
let right = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let left = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let (mut a, b) = (
match &left {
Value::Array(arr) => arr.borrow().clone(),
_ => {
return Err(format!(
"ConcatArray: left must be array, got {}",
left.to_display_string()
));
}
},
match &right {
Value::Array(arr) => arr.borrow().clone(),
_ => {
return Err(format!(
"ConcatArray: right must be array, got {}",
right.to_display_string()
));
}
},
);
a.extend(b);
self.stack.push(Value::Array(VmRef::new(a)));
}
Opcode::MergeObject => {
let right = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let left = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
match (&left, &right) {
(Value::Object(l), Value::Object(r)) => {
let merged = merge_object_data(l, r);
self.stack.push(Value::Object(VmRef::new(merged)));
}
_ => {
return Err(format!(
"MergeObject: expected two objects, got {} and {}",
left.to_display_string(),
right.to_display_string()
));
}
}
}
Opcode::ArraySortNumeric => {
let operand = Self::read_u16(code, &mut ip);
let asc = operand == 0;
let arr = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let result = if asc {
arr_builtins::sort_numeric_asc(&arr)
} else {
arr_builtins::sort_numeric_desc(&arr)
};
self.stack.push(result);
}
Opcode::ArraySortByProperty => {
let prop_idx = Self::read_u16(code, &mut ip);
let asc = Self::read_u16(code, &mut ip) == 0;
let arr = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let prop = constants
.get(prop_idx as usize)
.and_then(|c| {
if let Constant::String(s) = c {
Some(s.as_ref())
} else {
None
}
})
.unwrap_or("");
let result = arr_builtins::sort_by_property_numeric(&arr, prop, asc);
self.stack.push(result);
}
Opcode::ArrayMapIdentity => {
let arr = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let result = match &arr {
Value::Array(a) => Value::Array(VmRef::new(a.borrow().clone())),
_ => Value::Null,
};
self.stack.push(result);
}
Opcode::ArrayMapBinOp => {
let binop_u8 = code[ip];
ip += 1;
let const_idx = Self::read_u16(code, &mut ip);
let param_left = code[ip] == 0; ip += 1;
let binop = u8_to_binop(binop_u8)
.ok_or_else(|| format!("Unknown binop in ArrayMapBinOp: {}", binop_u8))?;
let arr = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let const_val = constants
.get(const_idx as usize)
.map(|c| c.to_value())
.unwrap_or(Value::Null);
let result = if let Value::Array(a) = &arr {
let arr_borrow = a.borrow();
let mapped: Vec<Value> = arr_borrow
.iter()
.map(|v| {
let l: Value = if param_left {
(*v).clone()
} else {
const_val.clone()
};
let r: Value = if param_left {
const_val.clone()
} else {
(*v).clone()
};
eval_binop(binop, &l, &r).unwrap_or(Value::Null)
})
.collect();
Value::Array(VmRef::new(mapped))
} else {
Value::Null
};
self.stack.push(result);
}
Opcode::ArrayFilterBinOp => {
let binop_u8 = code[ip];
ip += 1;
let const_idx = Self::read_u16(code, &mut ip);
let param_left = code[ip] == 0; ip += 1;
let binop = u8_to_binop(binop_u8).ok_or_else(|| {
format!("Unknown binop in ArrayFilterBinOp: {}", binop_u8)
})?;
let arr = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
let const_val = constants
.get(const_idx as usize)
.map(|c| c.to_value())
.unwrap_or(Value::Null);
let result = if let Value::Array(a) = &arr {
let arr_borrow = a.borrow();
let filtered: Vec<Value> = arr_borrow
.iter()
.filter(|v| {
let l: Value = if param_left {
(*v).clone()
} else {
const_val.clone()
};
let r: Value = if param_left {
const_val.clone()
} else {
(*v).clone()
};
let b = eval_binop(binop, &l, &r).unwrap_or(Value::Null);
b.is_truthy()
})
.cloned()
.collect();
Value::Array(VmRef::new(filtered))
} else {
Value::Null
};
self.stack.push(result);
}
Opcode::Throw => {
let v = self
.stack
.pop()
.ok_or_else(|| "Stack underflow".to_string())?;
Self::unwind_throw(&mut try_handlers, &mut self.stack, &mut ip, v)?;
}
Opcode::AwaitPromise => {
let v = self
.stack
.pop()
.ok_or_else(|| "Stack underflow in AwaitPromise".to_string())?;
#[cfg(any(feature = "http", feature = "promise"))]
{
use tishlang_core::Value as V;
match v {
V::Promise(p) => match p.block_until_settled() {
Ok(val) => self.stack.push(val),
Err(rej) => {
Self::unwind_throw(
&mut try_handlers,
&mut self.stack,
&mut ip,
rej,
)?;
}
},
other => self.stack.push(tishlang_runtime::await_promise(other)),
}
}
#[cfg(not(any(feature = "http", feature = "promise")))]
{
self.stack.push(v);
}
}
Opcode::LoadNativeExport => {
let spec_idx = Self::read_u16(code, &mut ip);
let export_idx = Self::read_u16(code, &mut ip);
let spec = match constants.get(spec_idx as usize) {
Some(Constant::String(s)) => s.as_ref(),
_ => {
return Err(
"LoadNativeExport: spec constant out of bounds or not string"
.to_string(),
);
}
};
let export_name = match constants.get(export_idx as usize) {
Some(Constant::String(s)) => s.as_ref(),
_ => {
return Err("LoadNativeExport: export_name constant out of bounds or not string".to_string());
}
};
let from_registry: Option<Value> = if spec.starts_with("cargo:") {
let regs = self.native_modules.borrow();
regs.get(spec)
.and_then(|m| m.borrow().get(&Arc::from(export_name)).cloned())
} else {
None
};
let v = from_registry
.or_else(|| get_builtin_export(self.capabilities.as_ref(), spec, export_name))
.ok_or_else(|| {
if spec.starts_with("cargo:") {
format!(
"cargo:{} is not registered on the bytecode VM. Embedders must call Vm::register_native_module before run(). Spec: {} export: {}",
spec.trim_start_matches("cargo:"),
spec,
export_name,
)
} else {
format!(
"Built-in module '{}' does not export '{}' or capability not enabled for this run. Use e.g. tish run --feature fs (or full). The tish binary must also be built with that capability linked in.",
spec, export_name
)
}
})?;
self.stack.push(v);
}
Opcode::Closure | Opcode::LoadThis => {
return Err(format!("Unhandled opcode: {:?}", opcode));
}
}
}
#[cfg(feature = "timers")]
if cap_allows(self.capabilities.as_ref(), "timers") {
tishlang_runtime::drain_timers();
}
Ok(self.stack.pop().unwrap_or(Value::Null))
}
}
impl Default for Vm {
fn default() -> Self {
Self::new()
}
}
fn estimate_string_concat_len(v: &Value) -> usize {
match v {
Value::String(s) => s.len(),
Value::Number(_) => 24,
Value::Bool(_) => 5,
Value::Null => 4,
_ => 32,
}
}
fn append_value_for_string_concat(out: &mut String, v: &Value) {
use std::fmt::Write;
match v {
Value::Number(n) => {
if n.is_nan() {
out.push_str("NaN");
} else if *n == f64::INFINITY {
out.push_str("Infinity");
} else if *n == f64::NEG_INFINITY {
out.push_str("-Infinity");
} else {
let _ = write!(out, "{n}");
}
}
Value::String(s) => out.push_str(s.as_ref()),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Null => out.push_str("null"),
_ => out.push_str(&v.to_display_string()),
}
}
fn eval_binop(op: BinOp, l: &Value, r: &Value) -> Result<Value, String> {
use tishlang_ast::BinOp::*;
use tishlang_core::Value::*;
let ln = l.as_number().unwrap_or(f64::NAN);
let rn = r.as_number().unwrap_or(f64::NAN);
match op {
Add => {
if matches!(l, Value::String(_)) || matches!(r, Value::String(_)) {
let cap = estimate_string_concat_len(l) + estimate_string_concat_len(r);
let mut buf = std::string::String::with_capacity(cap);
append_value_for_string_concat(&mut buf, l);
append_value_for_string_concat(&mut buf, r);
Ok(String(buf.into()))
} else {
Ok(Number(ln + rn))
}
}
Sub => Ok(Number(ln - rn)),
Mul => Ok(Number(ln * rn)),
Div => Ok(Number(if rn == 0.0 { f64::NAN } else { ln / rn })),
Mod => Ok(Number(if rn == 0.0 { f64::NAN } else { ln % rn })),
Pow => Ok(Number(ln.powf(rn))),
Eq => Ok(Bool(l.strict_eq(r))),
Ne => Ok(Bool(!l.strict_eq(r))),
StrictEq => Ok(Bool(l.strict_eq(r))),
StrictNe => Ok(Bool(!l.strict_eq(r))),
Lt => Ok(Bool(ln < rn)),
Le => Ok(Bool(ln <= rn)),
Gt => Ok(Bool(ln > rn)),
Ge => Ok(Bool(ln >= rn)),
And => Ok(Bool(l.is_truthy() && r.is_truthy())),
Or => Ok(Bool(l.is_truthy() || r.is_truthy())),
BitAnd => Ok(Number((ln as i32 & rn as i32) as f64)),
BitOr => Ok(Number((ln as i32 | rn as i32) as f64)),
BitXor => Ok(Number((ln as i32 ^ rn as i32) as f64)),
Shl => Ok(Number(((ln as i32) << (rn as i32)) as f64)),
Shr => Ok(Number(((ln as i32) >> (rn as i32)) as f64)),
In => Ok(Bool(match r {
Value::Object(_) => object_has(r, l),
Value::Array(a) => {
let key_s: Arc<str> = match l {
Value::String(s) => Arc::clone(s),
Value::Number(n) => n.to_string().into(),
_ => l.to_display_string().into(),
};
if key_s.as_ref() == "length" {
true
} else if let Ok(idx) = key_s.parse::<usize>() {
idx < a.borrow().len()
} else {
false
}
}
_ => false,
})),
}
}
fn eval_unary(op: UnaryOp, o: &Value) -> Result<Value, String> {
use tishlang_ast::UnaryOp::*;
use tishlang_core::Value::*;
match op {
Not => Ok(Bool(!o.is_truthy())),
Neg => Ok(Number(-o.as_number().unwrap_or(f64::NAN))),
Pos => Ok(Number(o.as_number().unwrap_or(f64::NAN))),
BitNot => Ok(Number(!(o.as_number().unwrap_or(0.0) as i32) as f64)),
Void => Ok(Null),
}
}
fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
match obj {
Value::Object(m) => {
let map = m.borrow();
map.strings
.get(key.as_ref())
.cloned()
.ok_or_else(|| format!("Property '{}' not found", key))
}
Value::Array(a) => {
let key_s = key.as_ref();
if let Ok(idx) = key_s.parse::<usize>() {
let arr = a.borrow();
return arr
.get(idx)
.cloned()
.ok_or_else(|| "Index out of bounds".to_string());
}
if key_s == "length" {
return Ok(Value::Number(a.borrow().len() as f64));
}
let a_clone = a.clone();
let method: ArrayMethodFn = match key_s {
"push" => make_native_fn(move |args: &[Value]| {
arr_builtins::push(&Value::Array(a_clone.clone()), args)
}),
"pop" => make_native_fn(move |_args: &[Value]| {
arr_builtins::pop(&Value::Array(a_clone.clone()))
}),
"shift" => make_native_fn(move |_args: &[Value]| {
arr_builtins::shift(&Value::Array(a_clone.clone()))
}),
"unshift" => make_native_fn(move |args: &[Value]| {
arr_builtins::unshift(&Value::Array(a_clone.clone()), args)
}),
"reverse" => make_native_fn(move |_args: &[Value]| {
arr_builtins::reverse(&Value::Array(a_clone.clone()))
}),
"shuffle" => make_native_fn(move |_args: &[Value]| {
arr_builtins::shuffle(&Value::Array(a_clone.clone()))
}),
"slice" => make_native_fn(move |args: &[Value]| {
let start = args.first().unwrap_or(&Value::Null);
let end = args.get(1).unwrap_or(&Value::Null);
arr_builtins::slice(&Value::Array(a_clone.clone()), start, end)
}),
"concat" => make_native_fn(move |args: &[Value]| {
arr_builtins::concat(&Value::Array(a_clone.clone()), args)
}),
"join" => make_native_fn(move |args: &[Value]| {
let sep = args.first().unwrap_or(&Value::Null);
arr_builtins::join(&Value::Array(a_clone.clone()), sep)
}),
"indexOf" => make_native_fn(move |args: &[Value]| {
let search = args.first().unwrap_or(&Value::Null);
arr_builtins::index_of(&Value::Array(a_clone.clone()), search)
}),
"includes" => make_native_fn(move |args: &[Value]| {
let search = args.first().unwrap_or(&Value::Null);
let from = args.get(1);
arr_builtins::includes(&Value::Array(a_clone.clone()), search, from)
}),
"map" => make_native_fn(move |args: &[Value]| {
let cb = args.first().cloned().unwrap_or(Value::Null);
arr_builtins::map(&Value::Array(a_clone.clone()), &cb)
}),
"filter" => make_native_fn(move |args: &[Value]| {
let cb = args.first().cloned().unwrap_or(Value::Null);
arr_builtins::filter(&Value::Array(a_clone.clone()), &cb)
}),
"reduce" => make_native_fn(move |args: &[Value]| {
let cb = args.first().cloned().unwrap_or(Value::Null);
let init = args.get(1).cloned().unwrap_or(Value::Null);
arr_builtins::reduce(&Value::Array(a_clone.clone()), &cb, &init)
}),
"forEach" => make_native_fn(move |args: &[Value]| {
let cb = args.first().cloned().unwrap_or(Value::Null);
arr_builtins::for_each(&Value::Array(a_clone.clone()), &cb)
}),
"find" => make_native_fn(move |args: &[Value]| {
let cb = args.first().cloned().unwrap_or(Value::Null);
arr_builtins::find(&Value::Array(a_clone.clone()), &cb)
}),
"findIndex" => make_native_fn(move |args: &[Value]| {
let cb = args.first().cloned().unwrap_or(Value::Null);
arr_builtins::find_index(&Value::Array(a_clone.clone()), &cb)
}),
"some" => make_native_fn(move |args: &[Value]| {
let cb = args.first().cloned().unwrap_or(Value::Null);
arr_builtins::some(&Value::Array(a_clone.clone()), &cb)
}),
"every" => make_native_fn(move |args: &[Value]| {
let cb = args.first().cloned().unwrap_or(Value::Null);
arr_builtins::every(&Value::Array(a_clone.clone()), &cb)
}),
"flat" => make_native_fn(move |args: &[Value]| {
let depth = args.first().unwrap_or(&Value::Number(1.0));
arr_builtins::flat(&Value::Array(a_clone.clone()), depth)
}),
"flatMap" => make_native_fn(move |args: &[Value]| {
let cb = args.first().cloned().unwrap_or(Value::Null);
arr_builtins::flat_map(&Value::Array(a_clone.clone()), &cb)
}),
"sort" => make_native_fn(move |args: &[Value]| {
let cmp = args.first();
if let Some(Value::Function(_)) = cmp {
arr_builtins::sort_with_comparator(
&Value::Array(a_clone.clone()),
cmp.unwrap(),
)
} else {
arr_builtins::sort_default(&Value::Array(a_clone.clone()))
}
}),
"splice" => make_native_fn(move |args: &[Value]| {
let start = args.first().unwrap_or(&Value::Null);
let delete_count = args.get(1).map(|v| v as &Value);
let items: Vec<Value> = args.get(2..).unwrap_or(&[]).to_vec();
arr_builtins::splice(
&Value::Array(a_clone.clone()),
start,
delete_count,
&items,
)
}),
_ => return Err(format!("Property '{}' not found", key)),
};
Ok(Value::Function(method))
}
Value::String(s) => {
let key_s = key.as_ref();
if let Ok(idx) = key_s.parse::<usize>() {
return match s.chars().nth(idx) {
Some(c) => Ok(Value::String(Arc::from(c.to_string()))),
None => Err("Index out of bounds".to_string()),
};
}
if key_s == "length" {
return Ok(Value::Number(s.chars().count() as f64));
}
let s_clone: Arc<str> = Arc::clone(s);
let method: ArrayMethodFn = match key_s {
"indexOf" => make_native_fn(move |args: &[Value]| {
let search = args.first().unwrap_or(&Value::Null);
let from = args.get(1);
str_builtins::index_of(&Value::String(Arc::clone(&s_clone)), search, from)
}),
"lastIndexOf" => make_native_fn(move |args: &[Value]| {
let search = args.first().unwrap_or(&Value::Null);
let position = args.get(1).cloned().unwrap_or(Value::Number(f64::INFINITY));
str_builtins::last_index_of(
&Value::String(Arc::clone(&s_clone)),
search,
&position,
)
}),
"includes" => make_native_fn(move |args: &[Value]| {
let search = args.first().unwrap_or(&Value::Null);
let from = args.get(1);
str_builtins::includes(&Value::String(Arc::clone(&s_clone)), search, from)
}),
"slice" => make_native_fn(move |args: &[Value]| {
let start = args.first().unwrap_or(&Value::Null);
let end = args.get(1).unwrap_or(&Value::Null);
str_builtins::slice(&Value::String(Arc::clone(&s_clone)), start, end)
}),
"substring" => make_native_fn(move |args: &[Value]| {
let start = args.first().unwrap_or(&Value::Null);
let end = args.get(1).unwrap_or(&Value::Null);
str_builtins::substring(&Value::String(Arc::clone(&s_clone)), start, end)
}),
"split" => make_native_fn(move |args: &[Value]| {
let sep = args.first().unwrap_or(&Value::Null);
str_builtins::split(&Value::String(Arc::clone(&s_clone)), sep)
}),
"trim" => make_native_fn(move |_args: &[Value]| {
str_builtins::trim(&Value::String(Arc::clone(&s_clone)))
}),
"toUpperCase" => make_native_fn(move |_args: &[Value]| {
str_builtins::to_upper_case(&Value::String(Arc::clone(&s_clone)))
}),
"toLowerCase" => make_native_fn(move |_args: &[Value]| {
str_builtins::to_lower_case(&Value::String(Arc::clone(&s_clone)))
}),
"startsWith" => make_native_fn(move |args: &[Value]| {
let search = args.first().unwrap_or(&Value::Null);
str_builtins::starts_with(&Value::String(Arc::clone(&s_clone)), search)
}),
"endsWith" => make_native_fn(move |args: &[Value]| {
let search = args.first().unwrap_or(&Value::Null);
str_builtins::ends_with(&Value::String(Arc::clone(&s_clone)), search)
}),
"replace" => make_native_fn(move |args: &[Value]| {
let search = args.first().unwrap_or(&Value::Null);
let replacement = args.get(1).unwrap_or(&Value::Null);
str_builtins::replace(&Value::String(Arc::clone(&s_clone)), search, replacement)
}),
"replaceAll" => make_native_fn(move |args: &[Value]| {
let search = args.first().unwrap_or(&Value::Null);
let replacement = args.get(1).unwrap_or(&Value::Null);
str_builtins::replace_all(
&Value::String(Arc::clone(&s_clone)),
search,
replacement,
)
}),
"charAt" => make_native_fn(move |args: &[Value]| {
let idx = args.first().unwrap_or(&Value::Null);
str_builtins::char_at(&Value::String(Arc::clone(&s_clone)), idx)
}),
"charCodeAt" => make_native_fn(move |args: &[Value]| {
let idx = args.first().unwrap_or(&Value::Null);
str_builtins::char_code_at(&Value::String(Arc::clone(&s_clone)), idx)
}),
"repeat" => make_native_fn(move |args: &[Value]| {
let count = args.first().unwrap_or(&Value::Null);
str_builtins::repeat(&Value::String(Arc::clone(&s_clone)), count)
}),
"padStart" => make_native_fn(move |args: &[Value]| {
let target_len = args.first().unwrap_or(&Value::Null);
let pad = args.get(1).unwrap_or(&Value::Null);
str_builtins::pad_start(&Value::String(Arc::clone(&s_clone)), target_len, pad)
}),
"padEnd" => make_native_fn(move |args: &[Value]| {
let target_len = args.first().unwrap_or(&Value::Null);
let pad = args.get(1).unwrap_or(&Value::Null);
str_builtins::pad_end(&Value::String(Arc::clone(&s_clone)), target_len, pad)
}),
_ => return Err(format!("Property '{}' not found", key)),
};
Ok(Value::Function(method))
}
#[cfg(any(feature = "http", feature = "promise"))]
Value::Promise(p) => match key.as_ref() {
"then" => {
let pc = Arc::clone(p);
Ok(Value::native(move |args| {
tishlang_runtime::promise_instance_then(&pc, args)
}))
}
"catch" => {
let pc = Arc::clone(p);
Ok(Value::native(move |args| {
tishlang_runtime::promise_instance_catch(&pc, args)
}))
}
_ => Err(format!("Property '{}' not found", key)),
},
_ => Err(format!(
"Cannot read property '{}' of {}",
key,
obj.type_name()
)),
}
}
fn set_member(obj: &Value, key: &Arc<str>, val: Value) -> Result<(), String> {
match obj {
Value::Object(m) => {
m.borrow_mut().strings.insert(Arc::clone(key), val);
Ok(())
}
Value::Array(a) => {
let idx: usize = key.as_ref().parse().unwrap_or(0);
let mut arr = a.borrow_mut();
if idx < arr.len() {
arr[idx] = val;
} else {
arr.resize(idx + 1, Value::Null);
arr[idx] = val;
}
Ok(())
}
_ => Err(format!("Cannot set property of {}", obj.type_name())),
}
}
fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
match obj {
Value::Array(a) => {
let i = match idx {
Value::Number(n) => *n as usize,
_ => {
return Err(format!(
"Array index must be number, got {}",
idx.type_name()
));
}
};
Ok(a
.borrow()
.get(i)
.cloned()
.unwrap_or(Value::Null))
}
Value::String(s) => {
let i = match idx {
Value::Number(n) => {
let n = *n;
if n < 0.0 || n.fract() != 0.0 {
return Err(format!(
"String index must be non-negative integer, got {}",
n
));
}
let i = n as usize;
let len = s.chars().count();
if i >= len {
return Err("Index out of bounds".to_string());
}
i
}
_ => {
return Err(format!(
"String index must be number, got {}",
idx.type_name()
));
}
};
match s.chars().nth(i) {
Some(c) => Ok(Value::String(Arc::from(c.to_string()))),
None => Err("Index out of bounds".to_string()),
}
}
Value::Object(_) => object_get(obj, idx).ok_or_else(|| {
format!(
"Property '{}' not found",
idx.to_display_string()
)
}),
#[cfg(any(feature = "http", feature = "promise"))]
Value::Promise(_) => {
let key_arc: std::sync::Arc<str> = match idx {
Value::String(s) => std::sync::Arc::clone(s),
_ => {
return Err(format!(
"Promise bracket access requires a string key, got {}",
idx.type_name()
));
}
};
get_member(obj, &key_arc)
},
_ => Err(format!(
"Cannot read property '{}' of {}",
idx.to_display_string(),
obj.type_name()
)),
}
}
fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
match obj {
Value::Array(a) => {
let i = match idx {
Value::Number(n) => *n as usize,
_ => {
return Err(format!(
"Array index must be number, got {}",
idx.type_name()
));
}
};
let mut arr = a.borrow_mut();
while arr.len() <= i {
arr.push(Value::Null);
}
arr[i] = val;
Ok(())
}
Value::Object(_) => object_set(obj, idx, val),
_ => Err(format!("Cannot set property of {}", obj.type_name())),
}
}
pub fn run(chunk: &Chunk) -> Result<Value, String> {
let mut vm = Vm::new();
vm.run_with_options(chunk, false)
}
pub fn run_with_options(chunk: &Chunk, opts: VmRunOptions) -> Result<Value, String> {
let mut vm = Vm::with_capabilities(opts.capabilities);
vm.run_with_options(chunk, opts.repl_mode)
}