mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 15:57:49 +00:00
$.line and $.stack counted lines in the run's expanded source. In code that comes after an #include they gave lines that match no file, and this included code in a file $.evalFile ran and code a named engine kept from an earlier run. Both now map each frame through the run's source maps, as uncaught errors already were. $.stack writes each frame as name@file:line, outermost first, with the run's name as the file of a script given as source. Code that eval or Function compiled keeps the line of the source they compiled, in [eval]; code whose place a named engine no longer keeps is at line 0 of [unknown]. The 64-frame limit, [global] and [anonymous] are unchanged. Docs: docs/mcp.md, docs/target-app-parity.md.
268 lines
15 KiB
Rust
268 lines
15 KiB
Rust
//! The script extensions: `localize`, `toSource`, host properties with setters and the items that
|
|
//! read the interpreter's own state.
|
|
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
use super::*;
|
|
|
|
fn realm() -> Realm {
|
|
let mut r = Realm::new();
|
|
r.add_script_extensions().unwrap();
|
|
r
|
|
}
|
|
|
|
fn text(r: &mut Realm, source: &str) -> String {
|
|
match r.run(source) {
|
|
Ok(JsValue::String(s)) => s,
|
|
other => panic!("{source}: {other:?}"),
|
|
}
|
|
}
|
|
|
|
fn thrown(r: &mut Realm, source: &str) -> String {
|
|
match r.run(source) {
|
|
Err(e) if e.kind == RunErrorKind::Thrown => e.message,
|
|
other => panic!("{source}: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn localize_looks_up_the_locale_with_its_platform_then_its_language_then_english() {
|
|
let mut r = realm();
|
|
let l = r.localization();
|
|
l.set_locale("de_DE");
|
|
l.set_platform(Some("Win"));
|
|
assert_eq!(l.keys(), ["de_DE_Win", "de_DE", "de_Win", "de", "en"]);
|
|
let all = "({en: 'e', de: 'd', de_Win: 'dw', de_DE: 'dd', de_DE_Win: 'ddw'})";
|
|
assert_eq!(text(&mut r, &format!("localize({all})")), "ddw");
|
|
assert_eq!(text(&mut r, "localize({en: 'e', de: 'd', de_Win: 'dw', de_DE: 'dd'})"), "dd");
|
|
assert_eq!(text(&mut r, "localize({en: 'e', de: 'd', de_Win: 'dw'})"), "dw");
|
|
assert_eq!(text(&mut r, "localize({en: 'e', de: 'd', DE: 'x'})"), "d", "names are case sensitive");
|
|
assert_eq!(text(&mut r, "localize({en: 'e', fr: 'f'})"), "e");
|
|
l.set_platform(None);
|
|
assert_eq!(text(&mut r, &format!("localize({all})")), "dd");
|
|
l.set_locale("en");
|
|
assert_eq!(l.keys(), ["en"]);
|
|
}
|
|
|
|
#[test]
|
|
fn localize_replaces_numbered_values_once_and_leaves_the_rest() {
|
|
let mut r = realm();
|
|
assert_eq!(text(&mut r, "localize({en: '%2 before %1, %1 again'}, 'a', 'b')"), "b before a, a again");
|
|
assert_eq!(text(&mut r, "localize('%1%% %3 %0 %', 'x', '%1')"), "x%% %3 %0 %");
|
|
assert_eq!(text(&mut r, "localize('%2', 'x', '%1')"), "%1", "a value is not replaced again");
|
|
assert_eq!(text(&mut r, "localize('%1', {toString: function () { return 'o'; }})"), "o");
|
|
assert_eq!(text(&mut r, "var n = 0; var v = {toString: function () { n++; return 'v'; }}; localize('%1%1', v) + n"), "vv1");
|
|
}
|
|
|
|
#[test]
|
|
fn localize_gives_back_an_object_without_the_locale_and_runs_no_getter() {
|
|
let mut r = realm();
|
|
r.localization().set_locale("fr_FR");
|
|
assert_eq!(r.run("var o = {de: 'd'}; localize(o) === o").unwrap(), JsValue::Bool(true));
|
|
let source = "var hit = false; var o = {get fr() { hit = true; return 'g'; }, en: 'e'}; localize(o) + ',' + hit";
|
|
assert_eq!(text(&mut r, source), "e,false");
|
|
assert_eq!(text(&mut r, "localize({fr: 1, en: 'e'})"), "e", "a value that is not a string is passed over");
|
|
for bad in ["localize()", "localize(1)", "localize(null)"] {
|
|
assert!(thrown(&mut r, bad).starts_with("TypeError"), "{bad}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn to_string_localizes_an_object_only_while_localization_is_on() {
|
|
let mut r = realm();
|
|
r.localization().set_locale("ja");
|
|
let source = "String({en: 'Hello', ja: 'こんにちは'}) + '|' + String({x: 1})";
|
|
assert_eq!(text(&mut r, source), "[object Object]|[object Object]");
|
|
r.localization().set_enabled(true);
|
|
assert_eq!(text(&mut r, source), "こんにちは|[object Object]");
|
|
}
|
|
|
|
#[test]
|
|
fn to_source_writes_plain_data_as_an_expression_that_makes_it_again() {
|
|
let mut r = realm();
|
|
let source = r#"toSource({a: [1, "x\n\"", null, true, -0, 1.5e300], "b c": {d: undefined}, e: {}, f: []})"#;
|
|
let want = r#"({"a":[1, "x\n\"", null, true, -0, 1.5e+300], "b c":({"d":(void 0)}), "e":({}), "f":[]})"#;
|
|
assert_eq!(text(&mut r, source), want);
|
|
assert_eq!(text(&mut r, "[1, , 3].toSource() + '|' + [1, ,].toSource() + '|' + [,].toSource()"), "[1, , 3]|[1, ,]|[,]");
|
|
assert_eq!(
|
|
text(&mut r, "toSource(NaN) + toSource(-Infinity) + toSource() + toSource('\\u2028\\ud800\\x7f')"),
|
|
"NaN-Infinity(void 0)\"\\u2028\\uD800\\u007F\""
|
|
);
|
|
assert_eq!(text(&mut r, "(5).toSource() + 'a'.toSource() + true.toSource()"), "5\"a\"true");
|
|
// Parsed back, the text makes the same value: holes, -0 and undefined included.
|
|
let round = r#"var v = {a: [1, , "q\u0001'\\"], b: {"0": -0, "": undefined, "x y": [[]]}};
|
|
var w = eval(toSource(v));
|
|
[JSON.stringify(w) === JSON.stringify(v), w.a.length, 1 in w.a, 1 / w.b[0], "" in w.b].join()"#;
|
|
assert_eq!(text(&mut r, round), "true,3,false,-Infinity,true");
|
|
let shared = "var s = {k: 1}; toSource([s, s])";
|
|
assert_eq!(text(&mut r, shared), r#"[({"k":1}), ({"k":1})]"#, "a value seen twice that is not its own ancestor is written twice");
|
|
}
|
|
|
|
#[test]
|
|
fn to_source_refuses_what_is_not_plain_data() {
|
|
let mut r = realm();
|
|
for (source, kind) in [
|
|
("var o = {}; o.o = o; toSource(o)", "TypeError"),
|
|
("var a = [1]; a.push([a]); toSource(a)", "TypeError"),
|
|
("toSource({f: function () {}})", "TypeError"),
|
|
("toSource({get g() { return 1; }})", "TypeError"),
|
|
("toSource(new Date(0))", "TypeError"),
|
|
("toSource([/x/])", "TypeError"),
|
|
("toSource(new Error('e'))", "TypeError"),
|
|
("toSource(new String('s'))", "TypeError"),
|
|
("var a = [1]; a.extra = 2; toSource(a)", "TypeError"),
|
|
("toSource(Object.create(Array.prototype))", "TypeError"),
|
|
("var o = {}; Object.defineProperty(o, 'hidden', {value: 1}); toSource(o)", "TypeError"),
|
|
("var a = [0]; Object.defineProperty(a, '0', {value: 1, enumerable: false}); toSource(a)", "TypeError"),
|
|
("toSource(Object.create({a: 1}))", "TypeError"),
|
|
("toSource(Object.create(null))", "TypeError"),
|
|
("function F() { this.a = 1; } toSource(new F())", "TypeError"),
|
|
("var d = []; for (var i = 0; i < 70; i++) d = [d]; toSource(d)", "RangeError"),
|
|
("var a = []; a.length = 2000000; toSource(a)", "RangeError"),
|
|
] {
|
|
assert!(thrown(&mut r, source).starts_with(kind), "{source}");
|
|
}
|
|
assert!(thrown(&mut r, "toSource({get g() { throw 'ran'; }})").starts_with("TypeError"), "the getter does not run");
|
|
}
|
|
|
|
#[test]
|
|
fn to_source_refuses_flags_and_extensibility_a_literal_does_not_make() {
|
|
let mut r = realm();
|
|
let property = "TypeError: toSource: a property that is not writable or configurable cannot be written as source";
|
|
let extensible = "TypeError: toSource: a non-extensible object cannot be written as source";
|
|
let length = "TypeError: toSource: an array with a read-only length cannot be written as source";
|
|
for (source, want) in [
|
|
("var o = {}; Object.defineProperty(o, 'a', {value: 1, enumerable: true, configurable: true}); toSource(o)", property),
|
|
("var o = {}; Object.defineProperty(o, 'a', {value: 1, enumerable: true, writable: true}); toSource(o)", property),
|
|
("var a = [1, 2]; Object.defineProperty(a, '1', {writable: false}); toSource(a)", property),
|
|
("var a = [1, 2]; Object.defineProperty(a, '0', {configurable: false}); a.toSource()", property),
|
|
("var o = {}; Object.defineProperty(o, 'a', {value: 1, enumerable: true}); toSource({x: [{b: o}]})", property),
|
|
("toSource(Object.preventExtensions({}))", extensible),
|
|
("toSource(Object.preventExtensions([]))", extensible),
|
|
("toSource(Object.seal({a: 1}))", extensible),
|
|
("toSource(Object.freeze({a: 1}))", extensible),
|
|
("toSource(Object.seal([1]))", extensible),
|
|
("toSource(Object.freeze([1]))", extensible),
|
|
("toSource([1, {b: Object.freeze({})}])", extensible),
|
|
("var a = []; Object.defineProperty(a, 'length', {writable: false}); toSource(a)", length),
|
|
("var a = [1, , 3]; Object.defineProperty(a, 'length', {writable: false}); toSource({a: a})", length),
|
|
] {
|
|
assert_eq!(thrown(&mut r, source), want, "{source}");
|
|
}
|
|
// The refused states are ones the literal would lose: evaluated, it is open again.
|
|
let lost = "var o = Object.freeze({a: 1}); var w = eval('({\"a\":1})');
|
|
[Object.isExtensible(o), Object.isExtensible(w), Object.getOwnPropertyDescriptor(w, 'a').writable].join()";
|
|
assert_eq!(text(&mut r, lost), "false,true,true");
|
|
}
|
|
|
|
#[test]
|
|
fn to_source_writes_properties_defined_with_every_flag_on() {
|
|
let mut r = realm();
|
|
let source = "var o = {}; Object.defineProperty(o, 'a', {value: 1, writable: true, enumerable: true, configurable: true});
|
|
var a = [0]; Object.defineProperty(a, '0', {value: 2, writable: true, enumerable: true, configurable: true});
|
|
toSource({o: o, a: a, e: {}, f: [], s: [1, , 3]})";
|
|
assert_eq!(text(&mut r, source), r#"({"o":({"a":1}), "a":[2], "e":({}), "f":[], "s":[1, , 3]})"#);
|
|
}
|
|
|
|
#[test]
|
|
fn to_source_of_a_string_with_many_escapes_throws_a_range_error_before_the_heap_limit() {
|
|
let mut r = realm();
|
|
// A run's heap in the web build. 2^23 control characters, each written as six, pass a quarter
|
|
// of it, which is all one string may take.
|
|
r.set_limits(Limits { heap_bytes: 128 << 20, ..Limits::default() });
|
|
let source = "var s = '\\x01'; for (var i = 0; i < 23; i++) s = s + s;
|
|
try { toSource(s); 'no error'; } catch (e) { e.name + ': ' + e.message; }";
|
|
assert_eq!(text(&mut r, source), "RangeError: toSource: the source is too long");
|
|
assert_eq!(text(&mut r, "toSource('a\\x01a\\x01a\\x01a\\x01').length + ''"), "30", "short strings are written whole");
|
|
}
|
|
|
|
#[test]
|
|
fn to_source_stops_at_the_fuel_limit() {
|
|
let mut r = realm();
|
|
r.set_limits(Limits { fuel: 200_000, ..Limits::default() });
|
|
let e = r.run("var a = []; for (var i = 0; i < 1000; i++) a.push(i); while (true) toSource(a);").unwrap_err();
|
|
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Fuel));
|
|
}
|
|
|
|
#[test]
|
|
fn script_items_read_the_global_object_the_last_error_the_line_and_the_stack() {
|
|
let mut r = realm();
|
|
let items = ["global", "error", "line", "stack"]
|
|
.into_iter()
|
|
.zip([ScriptItem::Global, ScriptItem::LastError, ScriptItem::Line, ScriptItem::Stack])
|
|
.map(|(n, i)| (n.to_string(), HostItem::Script(i)))
|
|
.collect();
|
|
r.define_host_object("h", items).unwrap();
|
|
assert_eq!(r.run("var later = 2; h.global === this && h.global.later === 2").unwrap(), JsValue::Bool(true));
|
|
let source = "var before = h.error; try { null.x; } catch (e) {} var caught = h.error; try { throw 'text'; } catch (e) {}
|
|
[before === null, caught instanceof TypeError, h.error === caught].join()";
|
|
assert_eq!(text(&mut r, source), "true,true,true");
|
|
let source = "try { try { throw new RangeError('first'); } finally { try { throw new TypeError('second'); } catch (e) {} } } catch (e) {}
|
|
h.error.message";
|
|
assert_eq!(text(&mut r, source), "second", "an exception a finally block passes on is not thrown again");
|
|
let source = "try { [1].map(function () { try { throw new RangeError('first'); } finally { try { throw new TypeError('second'); } catch (e) {} } }); } catch (e) {}
|
|
h.error.message";
|
|
assert_eq!(text(&mut r, source), "second", "nor is one that leaves a built-in's callback");
|
|
let source = "var kept; try { throw new RangeError('first'); } catch (e) { kept = e; } try { throw new TypeError('second'); } catch (e) {}
|
|
try { throw kept; } catch (e) {} h.error.message";
|
|
assert_eq!(text(&mut r, source), "first", "throwing a caught error again makes it the last one");
|
|
assert_eq!(r.run("h.error").unwrap(), JsValue::Null, "each run starts without one");
|
|
assert_eq!(r.run("\n\n h.line").unwrap(), JsValue::Number(3.0));
|
|
let source = "function outer() {\n return inner();\n}\nfunction inner() {\n return h.stack;\n}\nvar f = function () { return outer(); };\nf()";
|
|
assert_eq!(text(&mut r, source), "[global]:8\n[anonymous]:7\nouter:2\ninner:5\n");
|
|
let source = "function maker() {\n function named() {\n return h.stack;\n }\n return named;\n}\nvar later = maker();\nlater()";
|
|
assert_eq!(text(&mut r, source), "[global]:8\nnamed:3\n", "a declared function keeps its name after its declaring function returns");
|
|
let deep = "function d(n) { return n ? d(n - 1) : h.stack; } var s = d(100).split('\\n'); s.length + ',' + s[0]";
|
|
let lines = text(&mut r, deep);
|
|
assert_eq!(lines, "66,…", "64 frames, the line that some are left out, and the last line's end");
|
|
assert!(thrown(&mut r, "'use strict'; h.line = 1").starts_with("TypeError"), "the items cannot be assigned");
|
|
}
|
|
|
|
#[test]
|
|
fn the_line_and_the_stack_give_the_place_the_source_locator_maps_each_frame_to() {
|
|
let mut r = realm();
|
|
let items = [("line", ScriptItem::Line), ("stack", ScriptItem::Stack)].into_iter().map(|(n, i)| (n.to_string(), HostItem::Script(i))).collect();
|
|
r.define_host_object("h", items).unwrap();
|
|
let asked: Rc<RefCell<Vec<(u32, u32)>>> = Rc::default();
|
|
let seen = asked.clone();
|
|
r.set_source_locator(Rc::new(move |tag, line| {
|
|
seen.borrow_mut().push((tag, line));
|
|
let file = if tag == UNKNOWN_SOURCE { "[eval]".to_string() } else { format!("f{tag}") };
|
|
(file, line + 100)
|
|
}));
|
|
let source = "function inner() {\n return h.stack;\n}\n\ninner()";
|
|
let stack = text(&mut r, source);
|
|
let tag = r.script_tag();
|
|
assert_eq!(stack, format!("[global]@f{tag}:105\ninner@f{tag}:102\n"));
|
|
assert_eq!(*asked.borrow(), [(tag, 5), (tag, 2)], "each frame's tag and its line in the compiled source");
|
|
assert_eq!(r.run("\n\n h.line").unwrap(), JsValue::Number(103.0));
|
|
let tag = r.script_tag();
|
|
let stack = text(&mut r, "\neval('\\n\\nh.stack')");
|
|
assert_eq!(stack, format!("[global]@f{}:102\n[global]@[eval]:103\n", tag + 1), "code eval compiles has no tag of its own");
|
|
let deep = "function d(n) { return n ? d(n - 1) : h.stack; } var s = d(100).split('\\n'); s.length + ',' + s[0] + ',' + s[1]";
|
|
let tag = r.script_tag() + 1;
|
|
assert_eq!(text(&mut r, deep), format!("66,…,d@f{tag}:101"), "the frames past 64 are still left out");
|
|
}
|
|
|
|
#[test]
|
|
fn a_host_property_reads_its_getter_and_gives_assignments_to_its_setter() {
|
|
let mut r = Realm::new();
|
|
let seen: Rc<RefCell<Vec<Data>>> = Rc::default();
|
|
let log = seen.clone();
|
|
let get: HostFunction = Rc::new(move |_| Ok(Data::String("got".into())));
|
|
let set: HostFunction = Rc::new(move |args| match args.first() {
|
|
Some(Data::Number(n)) if *n < 0.0 => Err(HostError::Range("no negatives".into())),
|
|
Some(d) => {
|
|
log.borrow_mut().push(d.clone());
|
|
Ok(Data::Undefined)
|
|
}
|
|
None => Ok(Data::Undefined),
|
|
});
|
|
r.define_host_object("h", vec![("p".into(), HostItem::Property { get, set })]).unwrap();
|
|
assert_eq!(r.run("h.p = 5; h.p = {a: [1]}; h.p").unwrap(), JsValue::String("got".into()));
|
|
assert_eq!(*seen.borrow(), [Data::Number(5.0), Data::Object(vec![("a".into(), Data::Array(vec![Data::Number(1.0)]))])]);
|
|
let e = r.run("try { h.p = -1; } catch (e) { e instanceof RangeError && e.message }").unwrap();
|
|
assert_eq!(e, JsValue::String("no negatives".into()));
|
|
}
|