quickjs-oxide 0.4.0

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

#[test]
fn direct_function_body_declarations_hoist_into_argument_and_local_bindings() {
    for (source, expected) in [
        (
            "(function(){return before();function before(){return 1}})()",
            Value::Int(1),
        ),
        (
            "(function(parameter){function local(){return later}function parameter(){return 10}function local(){return later+1}let later=2;return parameter()+local()})(0)",
            Value::Int(13),
        ),
        (
            "(function(){function arguments(){return 4}return arguments()})()",
            Value::Int(4),
        ),
        (
            "(function(){var arguments;function arguments(){return 6}return arguments()})()",
            Value::Int(6),
        ),
        (
            "(function(){function arguments(){return 7}var arguments;return arguments()})()",
            Value::Int(7),
        ),
        (
            "(function(){var arguments=function(){return 9};function arguments(){return 8}return arguments()})()",
            Value::Int(9),
        ),
        (
            "(function named(){function named(){return 5}return named()})()",
            Value::Int(5),
        ),
        (
            "(function(){'use strict';function mutable(){mutable=6;return mutable}return mutable()})()",
            Value::Int(6),
        ),
        (
            "(function(){if(false)return 0;function branch(){return 2}var count=0;while(count<1)count++;return branch()+count})()",
            Value::Int(3),
        ),
    ] {
        assert_eq!(evaluate_in_context(source), expected, "{source}");
    }

    let script = compile_unlinked_script(
        "(function(first,second){var local;function local(){}function second(){}function first(){}function local(){return 1}})",
    )
    .unwrap();
    let outer = script.constants()[0].as_child().unwrap();
    assert!(matches!(outer.code()[0], Instruction::FClosure(2)));
    assert!(matches!(outer.code()[1], Instruction::PutArg(0)));
    assert!(matches!(outer.code()[2], Instruction::FClosure(1)));
    assert!(matches!(outer.code()[3], Instruction::PutArg(1)));
    assert!(matches!(outer.code()[4], Instruction::FClosure(3)));
    assert!(matches!(outer.code()[5], Instruction::PutLocal(0)));
    for child in outer
        .constants()
        .iter()
        .filter_map(|value| value.as_child())
    {
        assert_eq!(child.metadata().function_name_local, None);
    }
}

#[test]
fn direct_function_body_declaration_conflicts_match_quickjs_order() {
    for (source, message) in [
        (
            "(function(){function conflict(){};let conflict})",
            "invalid redefinition of a variable",
        ),
        (
            "(function(){let conflict;function conflict(){}})",
            "invalid redefinition of lexical identifier",
        ),
        (
            "(function(conflict){function conflict(){};let conflict})",
            "invalid redefinition of parameter name",
        ),
        (
            "(function(){'use strict';function eval(){}})",
            "invalid function name in strict code",
        ),
    ] {
        assert_eq!(
            compile_unlinked_script(source).unwrap_err().message(),
            message,
            "{source}"
        );
    }
}

#[test]
fn scoped_function_declarations_keep_entry_and_annex_closures_distinct() {
    for (source, expected) in [
        (
            "(function(){var inside;{function f(){return f}inside=f}return (inside!==f)+'|'+(inside()===inside)+'|'+(f()===inside)})()",
            "true|true|true",
        ),
        (
            "(function(){var inside;{function f(){return 1}function f(){return 2}inside=f}return inside()+'|'+f()+'|'+(inside===f)})()",
            "2|1|false",
        ),
        (
            "(function(){'use strict';var inside;{function f(){return 3}inside=f}return typeof f+'|'+inside()})()",
            "undefined|3",
        ),
        (
            "(function(parameter){var inside;{function parameter(){return 4}inside=parameter}return parameter+'|'+inside()})(1)",
            "1|4",
        ),
        (
            "(function(){var g;{f=8;function f(){}g=f}return g+'|'+typeof f+'|'+f.name})()",
            "8|function|f",
        ),
        (
            "(function(){var a,b,i=0;while(i<2){let x=i;function f(){return x}if(i===0)a=f;else b=f;i++}return (a!==b)+'|'+a()+'|'+b()})()",
            "true|0|1",
        ),
        (
            "(function(){var trace=typeof f;switch(0){case (trace+='|'+typeof f,0):function f(){return 5}}return trace+'|'+f()})()",
            "undefined|function|5",
        ),
        (
            "(function(){var original=function self(){{function self(){return 6}}return self};var replacement=original();return (replacement!==original)+'|'+replacement()})()",
            "true|6",
        ),
    ] {
        assert_eq!(
            evaluate_in_context(source),
            Value::String(JsString::from_static(expected)),
            "{source}"
        );
    }

    let script = compile_unlinked_script(
        "(function(){{function duplicate(){return 1}function duplicate(){return 2}}return duplicate()})",
    )
    .unwrap();
    let outer = script.constants()[0].as_child().unwrap();
    let closures = outer
        .code()
        .iter()
        .filter_map(|instruction| match instruction {
            Instruction::FClosure(constant) => Some(*constant),
            _ => None,
        })
        .collect::<Vec<_>>();
    assert_eq!(closures, [1, 0, 0, 1]);
    assert!(outer.code().windows(4).any(|window| matches!(
        window,
        [
            Instruction::FClosure(0),
            Instruction::Dup,
            Instruction::PutLocal(_),
            Instruction::Drop,
        ]
    )));
    assert!(!outer.code().windows(3).any(|window| matches!(
        window,
        [
            Instruction::FClosure(1),
            Instruction::Dup,
            Instruction::PutLocal(_),
        ]
    )));

    let arguments_script =
        compile_unlinked_script("(function(){{function arguments(){return 3}}return 1})").unwrap();
    let arguments_outer = arguments_script.constants()[0].as_child().unwrap();
    assert_eq!(arguments_outer.local_definitions().len(), 1);
    assert_eq!(
        arguments_outer.local_definitions()[0].name.as_ref(),
        Some(&JsString::from_static("arguments"))
    );
    assert!(arguments_outer.local_definitions()[0].is_lexical);
    assert!(
        !arguments_outer
            .code()
            .iter()
            .any(|instruction| matches!(instruction, Instruction::Dup)),
        "implicit arguments name incorrectly created an Annex root write"
    );

    let shadow_script = compile_unlinked_script(
        "(function(){let shadow=12;{function shadow(){return 13}}return shadow})",
    )
    .unwrap();
    let shadow_outer = shadow_script.constants()[0].as_child().unwrap();
    assert_eq!(shadow_outer.local_definitions().len(), 2);
    assert!(
        shadow_outer
            .local_definitions()
            .iter()
            .all(|definition| definition.is_lexical)
    );
    assert!(
        !shadow_outer
            .code()
            .iter()
            .any(|instruction| matches!(instruction, Instruction::Dup)),
        "prior enclosing lexical incorrectly allowed an Annex root write"
    );
}

#[test]
fn scoped_function_conflicts_and_global_annex_order_match_quickjs() {
    for (source, message) in [
        (
            "(function(){{let conflict;function conflict(}})",
            "invalid redefinition of lexical identifier",
        ),
        (
            "(function(){'use strict';{function duplicate(){}function duplicate(}})",
            "invalid redefinition of lexical identifier",
        ),
        (
            "(function(){{var conflict;function conflict(){}}})",
            "invalid redefinition of a variable",
        ),
        (
            "(function(){{function conflict(){}var conflict;}})",
            "invalid redefinition of lexical identifier",
        ),
    ] {
        assert_eq!(
            compile_unlinked_script(source).unwrap_err().message(),
            message,
            "{source}"
        );
    }

    let (runtime, context) = test_context();
    let Err(RuntimeError::Exception(Value::Object(error))) =
        context.eval("{function lateGlobalLexical(){}}let lateGlobalLexical;")
    else {
        panic!("Annex B global lexical collision did not throw an Error object");
    };
    let message = runtime.intern_property_key("message").unwrap();
    assert_eq!(
        context.get_property(&error, &message).unwrap(),
        Value::String(JsString::from_static(
            "lateGlobalLexical is not initialized"
        ))
    );

    assert_eq!(
        context
            .eval(
                "let priorGlobalLexical=7;{function priorGlobalLexical(){return 8}}\
                 typeof globalThis.priorGlobalLexical+'|'+priorGlobalLexical"
            )
            .unwrap(),
        Value::String(JsString::from_static("undefined|7"))
    );
}

#[test]
fn annex_b_single_and_labelled_declarations_preserve_scope_shape() {
    let program = compile_unlinked_script(
        "programLabel:function programLabel(){return programLabel};programLabel",
    )
    .unwrap();
    assert_eq!(
        program.local_definitions().len(),
        1,
        "Program labelled functions must not allocate a lexical local"
    );
    assert!(program.code().windows(4).any(|window| matches!(
        window,
        [
            Instruction::FClosure(_),
            Instruction::Dup,
            Instruction::PutVar(_),
            Instruction::PutVar(_),
        ]
    )));

    let body = compile_unlinked_script(
        "(function(){bodyLabel:function bodyLabel(){return 3};return bodyLabel})",
    )
    .unwrap();
    let body = body.constants()[0].as_child().unwrap();
    assert_eq!(body.local_definitions().len(), 2);
    assert!(body.local_definitions()[0].is_lexical);
    assert!(!body.local_definitions()[1].is_lexical);
    assert!(body.code().windows(4).any(|window| matches!(
        window,
        [
            Instruction::FClosure(_),
            Instruction::Dup,
            Instruction::PutLocal(_),
            Instruction::Drop,
        ]
    )));

    let parameter = compile_unlinked_script(
        "(function(parameter){label:function parameter(){return 4};return parameter})",
    )
    .unwrap();
    let parameter = parameter.constants()[0].as_child().unwrap();
    assert_eq!(parameter.local_definitions().len(), 1);
    assert!(parameter.local_definitions()[0].is_lexical);
    assert!(
        !parameter
            .code()
            .iter()
            .any(|instruction| matches!(instruction, Instruction::Dup)),
        "same-name parameter incorrectly received an Annex root write"
    );

    let duplicate = compile_unlinked_script(
        "(function(){if(true)function duplicate(){return 1}else function duplicate(){return 2};return duplicate})",
    )
    .unwrap();
    let duplicate = duplicate.constants()[0].as_child().unwrap();
    assert_eq!(
        duplicate
            .local_definitions()
            .iter()
            .filter(|definition| definition.is_lexical)
            .count(),
        2
    );
    assert_eq!(
        duplicate
            .code()
            .iter()
            .filter(|instruction| matches!(instruction, Instruction::Dup))
            .count(),
        1,
        "only the first same-scope if declaration is Annex-eligible"
    );

    for source in [
        "var prior;label:function prior(){}",
        "function prior(){}label:function prior(){}",
        "let prior;label:function prior(){}",
    ] {
        assert_eq!(
            compile_unlinked_script(source).unwrap_err().message(),
            "invalid redefinition of global identifier",
            "{source}"
        );
    }
    compile_unlinked_script("{var nested;}label:function nested(){}")
        .expect("a nested first var must not block the Program label exception");
}