quickjs-oxide 0.4.0

A safe Rust JavaScript engine targeting semantic parity with QuickJS
Documentation
use super::*;
use crate::engine::test_support::assert_script_true;
use crate::engine::test_support::test_context;

#[test]
fn global_cell_execution_observes_updates_and_property_fallback_once() {
    let (_runtime, mut context) = test_context();
    assert_eq!(
        context
            .eval("var changingCell=1; function readChanging(){return changingCell} readChanging()")
            .unwrap(),
        Value::Int(1)
    );
    assert_eq!(
        context.eval("changingCell=2; readChanging()").unwrap(),
        Value::Int(2)
    );
    assert_eq!(
        context
            .eval("changingCell={x:3}; readChanging().x")
            .unwrap(),
        Value::Int(3)
    );
    assert_eq!(context.eval("var reads=0; Object.defineProperty(globalThis,'propertyOnly',{configurable:true,get:function(){reads++;return 17}}); function readProperty(){return propertyOnly} readProperty()+reads").unwrap(), Value::Int(18));
    assert_eq!(
        context
            .eval("delete globalThis.propertyOnly; typeof propertyOnly")
            .unwrap(),
        Value::String(JsString::from_static("undefined"))
    );
    assert_eq!(context.eval("Object.defineProperty(Object.getPrototypeOf(globalThis),'propertyOnly',{configurable:true,get:function(){reads++;return 23}}); readProperty()+reads").unwrap(), Value::Int(25));
    assert_script_true(
        &mut context,
        "delete Object.getPrototypeOf(globalThis).propertyOnly; try { readProperty(); } catch(e) { e instanceof ReferenceError }",
    );
    assert_script_true(
        &mut context,
        "try { eval('typeof tdzCell; let tdzCell=1'); } catch(e) { e instanceof ReferenceError }",
    );
}

#[test]
fn detached_vm_rejects_runtime_global_execution_explicitly() {
    let error = compile_script("answer").unwrap_err();
    assert!(error.message().contains("global-environment"));

    let error = compile_script("delete answer").unwrap_err();
    assert!(error.message().contains("global-environment"));
}

#[test]
fn global_put_matches_strict_sloppy_readonly_and_setter_semantics() {
    let (runtime, context) = test_context();
    let global = context.global_object().unwrap();
    let descriptor = |value, writable| OrdinaryPropertyDescriptor {
        value: DescriptorField::Present(value),
        writable: DescriptorField::Present(writable),
        enumerable: DescriptorField::Present(true),
        configurable: DescriptorField::Present(true),
        ..OrdinaryPropertyDescriptor::new()
    };

    assert_eq!(context.eval("created = 7").unwrap(), Value::Int(7));
    assert_eq!(context.eval("created").unwrap(), Value::Int(7));

    let readonly = runtime.intern_property_key("readonly").unwrap();
    assert!(
        context
            .define_own_property(&global, &readonly, &descriptor(Value::Int(1), false))
            .unwrap()
    );
    assert_eq!(context.eval("readonly = 2").unwrap(), Value::Int(2));
    assert_eq!(context.eval("readonly").unwrap(), Value::Int(1));
    let Err(RuntimeError::Exception(Value::Object(exception))) =
        context.eval("'use strict'; readonly = 2")
    else {
        panic!("strict read-only global assignment did not throw an object");
    };
    let message = runtime.intern_property_key("message").unwrap();
    assert_eq!(
        context.get_property(&exception, &message).unwrap(),
        Value::String(JsString::from_static("'readonly' is read-only"))
    );
    assert_eq!(context.eval("readonly += 2").unwrap(), Value::Int(3));
    assert_eq!(context.eval("readonly").unwrap(), Value::Int(1));
    assert_eq!(
        context.eval("'use strict'; readonly ||= 9").unwrap(),
        Value::Int(1)
    );
    assert!(matches!(
        context.eval("'use strict'; readonly += 2"),
        Err(RuntimeError::Exception(_))
    ));
    assert!(matches!(
        context.eval("'use strict'; readonly &&= 2"),
        Err(RuntimeError::Exception(_))
    ));

    let inherited = runtime.intern_property_key("inheritedReadOnly").unwrap();
    assert!(
        context
            .define_own_property(
                &context.object_prototype().unwrap(),
                &inherited,
                &descriptor(Value::Int(5), false),
            )
            .unwrap()
    );
    assert_eq!(
        context.eval("inheritedReadOnly = 6").unwrap(),
        Value::Int(6)
    );
    assert_eq!(context.eval("inheritedReadOnly").unwrap(), Value::Int(5));
    let Err(RuntimeError::Exception(Value::Object(exception))) =
        context.eval("'use strict'; inheritedReadOnly = 6")
    else {
        panic!("strict inherited read-only assignment did not throw an object");
    };
    assert_eq!(
        context.get_property(&exception, &message).unwrap(),
        Value::String(JsString::from_static("'inheritedReadOnly' is read-only"))
    );

    let no_setter = runtime.intern_property_key("noSetter").unwrap();
    assert!(
        context
            .define_own_property(
                &global,
                &no_setter,
                &OrdinaryPropertyDescriptor {
                    get: DescriptorField::Present(AccessorValue::Undefined),
                    set: DescriptorField::Present(AccessorValue::Undefined),
                    enumerable: DescriptorField::Present(true),
                    configurable: DescriptorField::Present(true),
                    ..OrdinaryPropertyDescriptor::new()
                },
            )
            .unwrap()
    );
    assert_eq!(context.eval("noSetter = 8").unwrap(), Value::Int(8));
    let Err(RuntimeError::Exception(Value::Object(exception))) =
        context.eval("'use strict'; noSetter = 8")
    else {
        panic!("strict setter-less assignment did not throw an object");
    };
    assert_eq!(
        context.get_property(&exception, &message).unwrap(),
        Value::String(JsString::from_static("no setter for property"))
    );
    assert_eq!(context.eval("noSetter ||= 8").unwrap(), Value::Int(8));
    assert!(matches!(
        context.eval("'use strict'; noSetter ||= 8"),
        Err(RuntimeError::Exception(_))
    ));
    assert_eq!(
        context.eval("'use strict'; noSetter &&= 8").unwrap(),
        Value::Undefined
    );

    assert!(matches!(
        context.eval("'use strict'; trulyMissing = 1"),
        Err(RuntimeError::Exception(_))
    ));
    let truly_missing = runtime.intern_property_key("trulyMissing").unwrap();
    assert!(!runtime.has_own_property(&global, &truly_missing).unwrap());

    let sink = runtime.intern_property_key("sink").unwrap();
    assert!(
        context
            .define_own_property(&global, &sink, &descriptor(Value::Int(0), true))
            .unwrap()
    );
    let Value::Object(setter) = context
        .eval("(function(v) { sink = v; return 99; })")
        .unwrap()
    else {
        panic!("setter source did not produce a function");
    };
    let setter = runtime.as_callable(&setter).unwrap().unwrap();
    let target = runtime.intern_property_key("setterTarget").unwrap();
    assert!(
        context
            .define_own_property(
                &global,
                &target,
                &OrdinaryPropertyDescriptor {
                    get: DescriptorField::Present(AccessorValue::Undefined),
                    set: DescriptorField::Present(AccessorValue::Callable(setter)),
                    enumerable: DescriptorField::Present(true),
                    configurable: DescriptorField::Present(true),
                    ..OrdinaryPropertyDescriptor::new()
                },
            )
            .unwrap()
    );
    assert_eq!(context.eval("setterTarget = 42").unwrap(), Value::Int(42));
    assert_eq!(context.eval("sink").unwrap(), Value::Int(42));
    assert_eq!(context.eval("setterTarget ||= 17").unwrap(), Value::Int(17));
    assert_eq!(context.eval("sink").unwrap(), Value::Int(17));

    let Value::Object(getter) = context.eval("(function() { return this; })").unwrap() else {
        panic!("getter source did not produce a function");
    };
    let getter = runtime.as_callable(&getter).unwrap().unwrap();
    let getter_target = runtime.intern_property_key("getterTarget").unwrap();
    assert!(
        context
            .define_own_property(
                &global,
                &getter_target,
                &OrdinaryPropertyDescriptor {
                    get: DescriptorField::Present(AccessorValue::Callable(getter)),
                    set: DescriptorField::Present(AccessorValue::Undefined),
                    enumerable: DescriptorField::Present(true),
                    configurable: DescriptorField::Present(true),
                    ..OrdinaryPropertyDescriptor::new()
                },
            )
            .unwrap()
    );
    assert_eq!(
        context.eval("getterTarget").unwrap(),
        Value::Object(global.try_clone().expect("duplicate root"))
    );
    assert_eq!(
        context.eval("typeof getterTarget").unwrap(),
        Value::String(JsString::from_static("object"))
    );

    let Value::Object(throwing_getter) = context.eval("(function() { throw 17; })").unwrap() else {
        panic!("throwing getter source did not produce a function");
    };
    let throwing_getter = runtime.as_callable(&throwing_getter).unwrap().unwrap();
    let throwing_target = runtime.intern_property_key("throwingGetter").unwrap();
    assert!(
        context
            .define_own_property(
                &global,
                &throwing_target,
                &OrdinaryPropertyDescriptor {
                    get: DescriptorField::Present(AccessorValue::Callable(throwing_getter)),
                    set: DescriptorField::Present(AccessorValue::Undefined),
                    enumerable: DescriptorField::Present(true),
                    configurable: DescriptorField::Present(true),
                    ..OrdinaryPropertyDescriptor::new()
                },
            )
            .unwrap()
    );
    assert_eq!(
        context.eval("typeof throwingGetter").err(),
        Some(RuntimeError::Exception(Value::Int(17)))
    );

    assert_eq!(context.eval("compoundSide = 0").unwrap(), Value::Int(0));
    assert!(matches!(
        context.eval("missingCompound += (compoundSide = 1)"),
        Err(RuntimeError::Exception(_))
    ));
    assert_eq!(context.eval("compoundSide").unwrap(), Value::Int(0));
    assert!(matches!(
        context.eval("missingLogical ||= (compoundSide = 2)"),
        Err(RuntimeError::Exception(_))
    ));
    assert_eq!(context.eval("compoundSide").unwrap(), Value::Int(0));
}

#[test]
fn unresolved_name_compiles_to_one_global_then_parent_global_relays() {
    let script = compile_unlinked_script(
        "(function() { return function() { return function() { return relayName; }; }; })",
    )
    .unwrap();
    let mut function = &script;
    for depth in 0..4 {
        let descriptor = function
            .closure_variables()
            .first()
            .expect("every function on the unresolved-name path needs a closure slot");
        assert_eq!(
            descriptor.source,
            if depth == 0 {
                ClosureSource::Global
            } else {
                ClosureSource::ParentGlobal(0)
            }
        );
        let ClosureVariableName::Constant(name_index) = descriptor.name else {
            panic!("unlinked global relay did not retain a name constant");
        };
        assert!(matches!(
            function.constants()[name_index as usize].as_primitive(),
            Some(crate::engine::value::PrimitiveValue::String(name)) if name == &JsString::from_static("relayName")
        ));
        if depth == 3 {
            assert!(matches!(
                function.code(),
                [Instruction::GetVar(0), Instruction::Return, ..]
            ));
            break;
        }
        function = function
            .constants()
            .iter()
            .find_map(|constant| constant.as_child())
            .expect("global relay path lost its nested child");
    }
}

#[test]
fn late_global_property_delete_reconnect_and_cross_realm_use_the_defining_realm() {
    let runtime = Runtime::new();
    let defining = runtime.new_context().expect("create context");
    let caller = runtime.new_context().expect("create context");
    let Value::Object(reader) = defining
        .eval("(function() { return lateRealmValue; })")
        .unwrap()
    else {
        panic!("late global reader source did not produce a function");
    };
    let reader = runtime.as_callable(&reader).unwrap().unwrap();
    let key = runtime.intern_property_key("lateRealmValue").unwrap();
    let descriptor = |value| OrdinaryPropertyDescriptor {
        value: DescriptorField::Present(Value::Int(value)),
        writable: DescriptorField::Present(true),
        enumerable: DescriptorField::Present(true),
        configurable: DescriptorField::Present(true),
        ..OrdinaryPropertyDescriptor::new()
    };
    assert!(
        caller
            .define_own_property(&caller.global_object().unwrap(), &key, &descriptor(9),)
            .unwrap()
    );
    let defining_global = defining.global_object().unwrap();
    assert!(
        defining
            .define_own_property(&defining_global, &key, &descriptor(1))
            .unwrap()
    );
    runtime.run_gc().unwrap();
    assert_eq!(
        caller.call(&reader, Value::Undefined, &[]).unwrap(),
        Value::Int(1)
    );

    assert!(runtime.delete_property(&defining_global, &key).unwrap());
    runtime.run_gc().unwrap();
    let Err(RuntimeError::Exception(Value::Object(exception))) =
        caller.call(&reader, Value::Undefined, &[])
    else {
        panic!("missing defining-realm global did not throw an object");
    };
    let reference_error = runtime.intern_property_key("ReferenceError").unwrap();
    let prototype = runtime.intern_property_key("prototype").unwrap();
    let Value::Object(reference_error) = defining
        .get_property(&defining_global, &reference_error)
        .unwrap()
    else {
        panic!("defining realm ReferenceError was not an object");
    };
    let Value::Object(reference_error_prototype) =
        defining.get_property(&reference_error, &prototype).unwrap()
    else {
        panic!("defining realm ReferenceError.prototype was not an object");
    };
    assert_eq!(
        runtime.get_prototype_of(&exception).unwrap(),
        Some(reference_error_prototype)
    );
    assert!(
        defining
            .define_own_property(&defining_global, &key, &descriptor(2))
            .unwrap()
    );
    runtime.run_gc().unwrap();
    assert_eq!(
        caller.call(&reader, Value::Undefined, &[]).unwrap(),
        Value::Int(2)
    );
}