use std::rc::Rc;
use crate::error::StatorResult;
use crate::gc::trace::{Trace, Tracer};
use crate::objects::value::JsValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LanguageMode {
Sloppy,
Strict,
}
#[derive(Debug, Clone)]
pub struct BytecodeRef(pub Vec<u8>);
#[derive(Debug, Clone)]
pub struct SharedFunctionInfo {
name: String,
bytecode: Option<BytecodeRef>,
param_count: u32,
language_mode: LanguageMode,
}
impl SharedFunctionInfo {
pub fn new(name: impl Into<String>, param_count: u32, language_mode: LanguageMode) -> Self {
Self {
name: name.into(),
bytecode: None,
param_count,
language_mode,
}
}
pub fn with_bytecode(
name: impl Into<String>,
param_count: u32,
language_mode: LanguageMode,
bytecode: BytecodeRef,
) -> Self {
Self {
name: name.into(),
bytecode: Some(bytecode),
param_count,
language_mode,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn param_count(&self) -> u32 {
self.param_count
}
pub fn language_mode(&self) -> LanguageMode {
self.language_mode
}
pub fn bytecode(&self) -> Option<&BytecodeRef> {
self.bytecode.as_ref()
}
pub fn set_bytecode(&mut self, bytecode: BytecodeRef) {
self.bytecode = Some(bytecode);
}
}
#[derive(Debug, Clone, Default)]
pub struct Context {
bindings: Vec<(String, JsValue)>,
}
impl Context {
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, name: impl Into<String>, value: JsValue) {
let name = name.into();
if let Some(entry) = self.bindings.iter_mut().find(|(k, _)| k == &name) {
entry.1 = value;
} else {
self.bindings.push((name, value));
}
}
pub fn get(&self, name: &str) -> Option<&JsValue> {
self.bindings
.iter()
.rev()
.find(|(k, _)| k == name)
.map(|(_, v)| v)
}
pub fn len(&self) -> usize {
self.bindings.len()
}
pub fn is_empty(&self) -> bool {
self.bindings.is_empty()
}
}
pub type NativeFn = fn(&[JsValue]) -> StatorResult<JsValue>;
pub enum FunctionKind {
Normal,
Bound {
target: Rc<JsFunction>,
bound_this: JsValue,
bound_args: Vec<JsValue>,
},
Native(NativeFn),
}
impl std::fmt::Debug for FunctionKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Normal => write!(f, "Normal"),
Self::Bound {
target,
bound_this,
bound_args,
} => f
.debug_struct("Bound")
.field("target", target)
.field("bound_this", bound_this)
.field("bound_args", bound_args)
.finish(),
Self::Native(_) => write!(f, "Native(<fn>)"),
}
}
}
#[derive(Debug)]
pub struct JsFunction {
shared: Rc<SharedFunctionInfo>,
context: Context,
kind: FunctionKind,
}
impl JsFunction {
pub fn new(shared: SharedFunctionInfo) -> Self {
Self {
shared: Rc::new(shared),
context: Context::new(),
kind: FunctionKind::Normal,
}
}
pub fn new_with_context(shared: SharedFunctionInfo, context: Context) -> Self {
Self {
shared: Rc::new(shared),
context,
kind: FunctionKind::Normal,
}
}
pub fn new_native(shared: SharedFunctionInfo, native: NativeFn) -> Self {
Self {
shared: Rc::new(shared),
context: Context::new(),
kind: FunctionKind::Native(native),
}
}
pub fn new_bound(
target: Rc<JsFunction>,
bound_this: JsValue,
bound_args: Vec<JsValue>,
) -> Self {
let shared = Rc::clone(&target.shared);
Self {
shared,
context: Context::new(),
kind: FunctionKind::Bound {
target,
bound_this,
bound_args,
},
}
}
pub fn name(&self) -> &str {
self.shared.name()
}
pub fn param_count(&self) -> u32 {
self.shared.param_count()
}
pub fn language_mode(&self) -> LanguageMode {
self.shared.language_mode()
}
pub fn shared_info(&self) -> &SharedFunctionInfo {
&self.shared
}
pub fn context(&self) -> &Context {
&self.context
}
pub fn kind(&self) -> &FunctionKind {
&self.kind
}
pub fn is_normal(&self) -> bool {
matches!(self.kind, FunctionKind::Normal)
}
pub fn is_native(&self) -> bool {
matches!(self.kind, FunctionKind::Native(_))
}
pub fn is_bound(&self) -> bool {
matches!(self.kind, FunctionKind::Bound { .. })
}
pub fn call_native(&self, args: &[JsValue]) -> Option<StatorResult<JsValue>> {
if let FunctionKind::Native(f) = self.kind {
Some(f(args))
} else {
None
}
}
}
impl Trace for JsFunction {
fn trace(&self, tracer: &mut Tracer) {
for (_, v) in &self.context.bindings {
v.trace(tracer);
}
if let FunctionKind::Bound {
target,
bound_this,
bound_args,
} = &self.kind
{
target.trace(tracer);
bound_this.trace(tracer);
for v in bound_args {
v.trace(tracer);
}
}
}
}
#[cfg(test)]
mod tests {
use std::rc::Rc;
use super::*;
use crate::error::StatorError;
#[test]
fn test_shared_function_info_name() {
let sfi = SharedFunctionInfo::new("greet", 1, LanguageMode::Sloppy);
assert_eq!(sfi.name(), "greet");
}
#[test]
fn test_shared_function_info_anonymous_empty_name() {
let sfi = SharedFunctionInfo::new("", 0, LanguageMode::Sloppy);
assert_eq!(sfi.name(), "");
}
#[test]
fn test_shared_function_info_param_count() {
let sfi = SharedFunctionInfo::new("f", 3, LanguageMode::Strict);
assert_eq!(sfi.param_count(), 3);
}
#[test]
fn test_shared_function_info_language_mode() {
let sfi_sloppy = SharedFunctionInfo::new("f", 0, LanguageMode::Sloppy);
assert_eq!(sfi_sloppy.language_mode(), LanguageMode::Sloppy);
let sfi_strict = SharedFunctionInfo::new("f", 0, LanguageMode::Strict);
assert_eq!(sfi_strict.language_mode(), LanguageMode::Strict);
}
#[test]
fn test_shared_function_info_no_bytecode_by_default() {
let sfi = SharedFunctionInfo::new("f", 0, LanguageMode::Sloppy);
assert!(sfi.bytecode().is_none());
}
#[test]
fn test_shared_function_info_with_bytecode() {
let bytecode = BytecodeRef(vec![0x01, 0x02, 0x03]);
let sfi = SharedFunctionInfo::with_bytecode("f", 0, LanguageMode::Sloppy, bytecode.clone());
assert!(sfi.bytecode().is_some());
assert_eq!(sfi.bytecode().unwrap().0, bytecode.0);
}
#[test]
fn test_shared_function_info_set_bytecode() {
let mut sfi = SharedFunctionInfo::new("f", 0, LanguageMode::Sloppy);
assert!(sfi.bytecode().is_none());
sfi.set_bytecode(BytecodeRef(vec![0xde, 0xad]));
assert_eq!(sfi.bytecode().unwrap().0, &[0xde, 0xad]);
}
#[test]
fn test_context_new_is_empty() {
let ctx = Context::new();
assert!(ctx.is_empty());
assert_eq!(ctx.len(), 0);
}
#[test]
fn test_context_set_and_get() {
let mut ctx = Context::new();
ctx.set("x", JsValue::Smi(42));
assert_eq!(ctx.get("x"), Some(&JsValue::Smi(42)));
}
#[test]
fn test_context_set_overwrites_existing() {
let mut ctx = Context::new();
ctx.set("x", JsValue::Smi(1));
ctx.set("x", JsValue::Smi(2));
assert_eq!(ctx.get("x"), Some(&JsValue::Smi(2)));
assert_eq!(ctx.len(), 1);
}
#[test]
fn test_context_get_missing_returns_none() {
let ctx = Context::new();
assert_eq!(ctx.get("missing"), None);
}
#[test]
fn test_context_multiple_bindings() {
let mut ctx = Context::new();
ctx.set("a", JsValue::Smi(10));
ctx.set("b", JsValue::Boolean(true));
assert_eq!(ctx.get("a"), Some(&JsValue::Smi(10)));
assert_eq!(ctx.get("b"), Some(&JsValue::Boolean(true)));
assert_eq!(ctx.len(), 2);
}
#[test]
fn test_js_function_new_name_and_param_count() {
let sfi = SharedFunctionInfo::new("hello", 2, LanguageMode::Sloppy);
let f = JsFunction::new(sfi);
assert_eq!(f.name(), "hello");
assert_eq!(f.param_count(), 2);
}
#[test]
fn test_js_function_new_is_normal() {
let sfi = SharedFunctionInfo::new("f", 0, LanguageMode::Sloppy);
let f = JsFunction::new(sfi);
assert!(f.is_normal());
assert!(!f.is_native());
assert!(!f.is_bound());
}
#[test]
fn test_js_function_language_mode() {
let sfi = SharedFunctionInfo::new("f", 0, LanguageMode::Strict);
let f = JsFunction::new(sfi);
assert_eq!(f.language_mode(), LanguageMode::Strict);
}
#[test]
fn test_js_function_with_context_captures_bindings() {
let sfi = SharedFunctionInfo::new("closure", 0, LanguageMode::Sloppy);
let mut ctx = Context::new();
ctx.set("captured", JsValue::Smi(99));
let f = JsFunction::new_with_context(sfi, ctx);
assert_eq!(f.context().get("captured"), Some(&JsValue::Smi(99)));
}
#[test]
fn test_js_function_empty_context_by_default() {
let sfi = SharedFunctionInfo::new("f", 0, LanguageMode::Sloppy);
let f = JsFunction::new(sfi);
assert!(f.context().is_empty());
}
fn native_add(args: &[JsValue]) -> StatorResult<JsValue> {
let a = args.get(1).cloned().unwrap_or(JsValue::Smi(0));
let b = args.get(2).cloned().unwrap_or(JsValue::Smi(0));
match (a, b) {
(JsValue::Smi(x), JsValue::Smi(y)) => Ok(JsValue::Smi(x + y)),
_ => Err(StatorError::TypeError("expected Smi".to_string())),
}
}
#[test]
fn test_wrap_native_fn_is_native() {
let sfi = SharedFunctionInfo::new("add", 2, LanguageMode::Sloppy);
let f = JsFunction::new_native(sfi, native_add);
assert!(f.is_native());
assert!(!f.is_normal());
assert!(!f.is_bound());
}
#[test]
fn test_wrap_native_fn_call_info() {
let sfi = SharedFunctionInfo::new("add", 2, LanguageMode::Sloppy);
let f = JsFunction::new_native(sfi, native_add);
assert_eq!(f.name(), "add");
assert_eq!(f.param_count(), 2);
assert_eq!(f.language_mode(), LanguageMode::Sloppy);
}
#[test]
fn test_wrap_native_fn_call_native_returns_value() {
let sfi = SharedFunctionInfo::new("add", 2, LanguageMode::Sloppy);
let f = JsFunction::new_native(sfi, native_add);
let result = f
.call_native(&[JsValue::Undefined, JsValue::Smi(3), JsValue::Smi(4)])
.expect("should be a native fn");
assert_eq!(result.unwrap(), JsValue::Smi(7));
}
#[test]
fn test_call_native_on_normal_fn_returns_none() {
let sfi = SharedFunctionInfo::new("f", 0, LanguageMode::Sloppy);
let f = JsFunction::new(sfi);
assert!(f.call_native(&[]).is_none());
}
#[test]
fn test_native_fn_context_is_empty() {
let sfi = SharedFunctionInfo::new("native", 0, LanguageMode::Sloppy);
let f = JsFunction::new_native(sfi, |_| Ok(JsValue::Undefined));
assert!(f.context().is_empty());
}
#[test]
fn test_bound_function_is_bound() {
let sfi = SharedFunctionInfo::new("f", 1, LanguageMode::Sloppy);
let target = Rc::new(JsFunction::new(sfi));
let bound = JsFunction::new_bound(Rc::clone(&target), JsValue::Null, vec![JsValue::Smi(1)]);
assert!(bound.is_bound());
assert!(!bound.is_native());
assert!(!bound.is_normal());
}
#[test]
fn test_bound_function_inherits_name() {
let sfi = SharedFunctionInfo::new("original", 2, LanguageMode::Strict);
let target = Rc::new(JsFunction::new(sfi));
let bound = JsFunction::new_bound(Rc::clone(&target), JsValue::Null, vec![]);
assert_eq!(bound.name(), "original");
assert_eq!(bound.param_count(), 2);
}
#[test]
fn test_bound_function_stores_bound_this_and_args() {
let sfi = SharedFunctionInfo::new("f", 1, LanguageMode::Sloppy);
let target = Rc::new(JsFunction::new(sfi));
let bound_this = JsValue::Smi(42);
let bound_args = vec![JsValue::Boolean(true)];
let bound =
JsFunction::new_bound(Rc::clone(&target), bound_this.clone(), bound_args.clone());
if let FunctionKind::Bound {
bound_this: bt,
bound_args: ba,
..
} = bound.kind()
{
assert_eq!(*bt, bound_this);
assert_eq!(*ba, bound_args);
} else {
panic!("expected Bound kind");
}
}
#[test]
fn test_bound_function_call_native_returns_none() {
let sfi = SharedFunctionInfo::new("f", 0, LanguageMode::Sloppy);
let target = Rc::new(JsFunction::new(sfi));
let bound = JsFunction::new_bound(target, JsValue::Null, vec![]);
assert!(bound.call_native(&[]).is_none());
}
#[test]
fn test_shared_info_accessor() {
let sfi = SharedFunctionInfo::new("test", 5, LanguageMode::Strict);
let f = JsFunction::new(sfi);
assert_eq!(f.shared_info().name(), "test");
assert_eq!(f.shared_info().param_count(), 5);
assert_eq!(f.shared_info().language_mode(), LanguageMode::Strict);
}
}