Scripts
editRun a script
realm.Execute runs a script and returns its completion value, the value of its last statement:
using var engine = new JsEngine();
var realm = engine.CreateRealm();
// The result of a script is the value of its last statement
var total = realm.Execute("let total = 0; for (const price of [10, 20, 12.5]) { total += price; } total", "prices.js");
Assert.Equal(42.5, total.AsNumber());
// The scripts of a realm share their global variables and functions
realm.Execute("var counter = 1; function increment() { return ++counter; }");
realm.Execute("increment();");
Assert.Equal(2, realm.Execute("counter").AsNumber());- The second argument is the URL or the file name of the script. It appears in the stack traces and in the locations of the errors: give one to every script.
- Declarations have no value: the result of
var x = 1;alone isundefined. End the script with an expression to return a value. - The
varandfunctiondeclarations of a script are properties of the global object; itslet,constandclassdeclarations are global bindings, also visible to the next scripts of the realm. - Execute returns when the script ends. The callbacks of the promises it creates run later, from RunJobs: see Async code and promises.
To read the result, see Values.
realm.Evaluate(code) runs code as an indirect eval would, and also returns its completion value.
Reuse a compiled script
JsPreparedScript.Parse parses and compiles a script once. The result is immutable: it runs in any realm of any engine, on any thread. Use it for the scripts that run many times, such as the same user script for each request:
// Parse and compile once, run many times, in any engine and on any thread
var script = JsPreparedScript.Parse("function fib(n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); } fib(input)", "fib.js");
var results = new double[4];
Parallel.For(0, results.Length, i =>
{
// An engine runs on one thread at a time: each thread uses its own engine
using var engine = new JsEngine();
var realm = engine.CreateRealm();
using (engine.Enter())
{
realm.GlobalObject.SetProperty("input", 10 + i);
}
results[i] = realm.Execute(script).AsNumber();
});
Assert.Equal([55, 89, 144, 233], results);Parse reports the syntax errors of the whole script. The body of a nested function is compiled on its first call, once for all the engines.
realm.Execute(string) parses and compiles its code on every call, and keeps no cache. When the same code runs often, parse it once and keep the JsPreparedScript yourself, for instance in a field or in a dictionary of the scripts of your application. Bound the dictionary when the scripts come from your users: the engine does not evict anything.
A prepared script holds code, not state. Each run evaluates it again, in the realm that runs it:
var counter = JsPreparedScript.Parse("var count = (count ?? 0) + 1; count", "counter.js");
using var engine = new JsEngine();
var first = engine.CreateRealm();
var second = engine.CreateRealm();
// Each run evaluates the script again, in the realm that runs it: its global declarations go to that realm
Assert.Equal(1, first.Execute(counter).AsNumber());
Assert.Equal(2, first.Execute(counter).AsNumber());
Assert.Equal(1, second.Execute(counter).AsNumber());
// A realm cannot declare the same let, const or class twice: run such a script once per realm
var settings = JsPreparedScript.Parse("const limit = 10; limit", "settings.js");
Assert.Equal(10, first.Execute(settings).AsNumber());
var exception = Assert.Throws<JsException>(() => first.Execute(settings));
Assert.StartsWith("SyntaxError", exception.Message, StringComparison.Ordinal);
// The URL given to Parse is the location of the errors of every run
var failing = JsPreparedScript.Parse("let total = 0;\nthrow new Error('Invalid total');", "https://example.com/total.js");
exception = Assert.Throws<JsException>(() => second.Execute(failing));
Assert.Equal(new JsSourceLocation("https://example.com/total.js", 2, 1), exception.Location);- The global declarations of a run go to the realm that runs it, as for
realm.Execute(string). Running the script again in the same realm sees the variables of the previous runs. - A realm cannot declare the same
let,constorclasstwice: a second run of a script that declares one throws aSyntaxError. Wrap the code in a function, or run it in a new realm, to run it several times. - The URL given to Parse is the location of the errors and of the stack traces of every run; it cannot be changed per run.
- The script can be shared between threads, but each engine stays on one thread at a time: see Threads.
A prepared script keeps only the compiled code: each engine keeps what it learns while it runs the script (the inline caches of the property accesses). Keeping a prepared script alive does not keep the engines that ran it alive, and the engines that run the same script do not slow each other down.
When the script is a part of a larger document, such as an inline script of an HTML page, pass its position in the document (lineOffset and columnOffset): the locations of the errors are then positions in the document.
Check the syntax of a script
JsPreparedScript.TryParse reports a syntax error without throwing, with its position. Use it to validate a script before you save it:
// TryParse reports a syntax error without throwing
if (JsPreparedScript.TryParse("let total = 1;\nlet = ;", "user.js", lineOffset: 0, columnOffset: 0, out var script, out var error))
{
Assert.Fail("The script is not valid: " + script.Url);
}
Assert.Equal("user.js", error.Url);
Assert.Equal(2, error.Line);
Assert.Equal(7, error.Column);- Line and Column are 1-based; the column counts UTF-16 code units.
- JsPreparedScript.Parse throws the same error as a JsSyntaxErrorException.
realm.Execute(string)does not throw a JsSyntaxErrorException: a script that is not valid throws a JsException whose value is aSyntaxError, as in JavaScript.