moejs

package module
v0.1.0-alpha.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

README

moejs

moejs

A pure-Go JavaScript runtime built for running many small plugin sandboxes fast.

简体中文 | English

moejs is an ECMAScript engine written in Go, with no cgo and no assembly. It was built to run the JavaScript task plugins of new-api, and its host API is shaped by that job: compile a plugin module once, load it into many small runtimes, call its hook functions with JSON-shaped Go values or JSON bytes, and read the results back as Go values or JSON bytes.

On that workload the whole host path around a hook call (arguments in, the call, the result decoded into the host's struct) takes about 6 µs, 2.5 times faster than with Sobek and with a sixth of its allocations, a runtime is created in about 1 µs, and a live runtime retains less than a third of Sobek's memory (see Performance).

  • Pure Go. Cross-compiles anywhere Go does; no C toolchain, no cgo call overhead, and the Go scheduler and race detector see everything.
  • A host API without needless conversions. Hooks are resolved once, arguments are lazy views of Go values or parsed straight from JSON bytes, results come back as Go values or as JSON bytes, and errors are returned as values.
  • Cheap runtimes. Intrinsics are built once per process, frozen and shared by every runtime, so creating one costs a global object and a register stack.
  • Safe to interrupt. A runaway script stops at the next loop back-edge or call, long-running builtins check the flag as they work, and the runtime is reusable afterwards.
  • Fails loudly. An unsupported feature is a compile-time or runtime error that names it, never a silent misbehaviour.

Status

moejs runs classic scripts, strict or sloppy, and ES modules, alone or as graphs of modules that import each other. Its results on the test262 conformance suite, overall and by directory, are in bench/test262/RESULTS.md, which the test262 runner regenerates.

Supported, among others:

  • Language: let/const, arrow functions, classes (fields, private names and methods, #x in o, static blocks, super, new.target, subclassing builtins), destructuring, spread and rest, default parameters, template and tagged template literals, optional chaining, ??, getters and setters, Symbol and the iterator protocol (for-of, spread, destructuring), generators, async functions and await, async generators and for await, import and export between modules (live bindings, cycles, namespace objects, export * as, string export names), top-level await across a module graph, dynamic import() in modules and scripts, import.meta, labelled statements, exceptions, direct and indirect eval, and the Function, GeneratorFunction, AsyncFunction and AsyncGeneratorFunction constructors.
  • Sloppy mode (scripts without "use strict"): this coercion, implicit globals, silently failing assignments and deletes, with (including Symbol.unscopables), the mapped arguments object and arguments.callee, and the Annex B syntax and semantics of scripts: block-level functions, labelled function declarations, catch parameter redeclaration, for (var x = init in o), calls as assignment targets, legacy octal literals and escapes, and HTML-like comments.
  • Builtins: Object, Function, Array (including the ES2023 methods and Array.fromAsync), String (including normalize on Unicode 17), Number, Boolean, Symbol, BigInt, Math (including sumPrecise), JSON, Proxy, Reflect, Map and Set (including the ES2025 set methods), WeakMap, WeakSet, WeakRef, ArrayBuffer (resizable, with transfer), SharedArrayBuffer, DataView (including getFloat16/setFloat16), the typed arrays (including Float16Array), Atomics, the Error family with AggregateError, Error.isError and V8-format stack, structuredClone, TextEncoder/TextDecoder (UTF-8), atob/btoa, the URI functions, Object.groupBy/Map.groupBy, Promise (including Promise.try), queueMicrotask, globalThis.
  • Date: all of ES2024 plus the Annex B methods, V8's Date.parse, and time zones from Go's tz database, settable per runtime.
  • RegExp: every flag (dgimsuvy), lookahead and lookbehind, backreferences, named and duplicate named groups, pattern modifiers and all Unicode 17 \p{…} properties.

Not supported yet: Intl, iterator helpers, FinalizationRegistry, timers, import attributes and JSON modules. TODO.md has the full list and the known wrong results.

Install

go get github.com/Calcium-Ion/moejs

The module requires Go 1.25 or later and has no dependencies outside the standard library (testify is used by tests only).

Usage

package main

import (
	"errors"
	"fmt"
	"time"

	"github.com/Calcium-Ion/moejs"
)

const source = `
export function buildRequest(input) {
  return {
    method: "POST",
    url: "https://api.example.com/v1/tasks",
    headers: { authorization: "Bearer " + utils.env("API_KEY") },
    body: { prompt: input.prompt.trim(), n: input.n ?? 1 },
  };
}
export function spin() { for (;;) {} }
`

func main() {
	// Compile once per process; a Module is immutable and shared.
	mod, err := moejs.Compile("plugin.js", source)
	if err != nil {
		panic(err)
	}
	build, err := mod.Hook("buildRequest")
	if err != nil {
		panic(err)
	}

	// One runtime per sandbox: host functions, then the module.
	env := map[string]string{"API_KEY": "test-key"}
	rt := moejs.NewRuntime(moejs.Options{})
	err = rt.SetGlobal("utils", map[string]any{
		"env": moejs.NativeFunc(func(r *moejs.Realm, _ moejs.Value, args []moejs.Value) (moejs.Value, error) {
			name, err := r.ToString(moejs.Arg(args, 0))
			if err != nil {
				return moejs.Undefined(), err
			}
			v, ok := env[name.GoString()]
			if !ok {
				// A Go error throws an Error with this message.
				return moejs.Undefined(), fmt.Errorf("%s is not set", name.GoString())
			}
			return moejs.String(v), nil
		}),
	})
	if err != nil {
		panic(err)
	}
	if err := rt.Load(mod); err != nil {
		panic(err)
	}

	// JSON in, JSON out.
	input, err := rt.ParseJSON([]byte(`{"prompt": " a cat "}`))
	if err != nil {
		panic(err)
	}
	res, err := rt.Call(build, input)
	if err != nil {
		panic(err)
	}
	out, err := rt.AppendJSON(nil, res)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(out))
	// {"method":"POST","url":"https://api.example.com/v1/tasks","headers":{"authorization":"Bearer test-key"},"body":{"prompt":"a cat","n":1}}

	// Go values in, Go values out.
	input, err = rt.FromGo(map[string]any{"prompt": "a dog", "n": 2})
	if err != nil {
		panic(err)
	}
	if res, err = rt.Call(build, input); err != nil {
		panic(err)
	}
	req, err := rt.ToGo(res)
	if err != nil {
		panic(err)
	}
	fmt.Println(req.(map[string]any)["body"]) // map[n:2 prompt:a dog]

	// A throw is an *moejs.Exception.
	_, err = rt.Call(build, moejs.Null())
	var exc *moejs.Exception
	fmt.Println(errors.As(err, &exc), exc.Name(), exc.Message())
	// true TypeError Cannot read properties of null (reading 'prompt')

	// Interrupts stop a runaway hook from another goroutine.
	spin, _ := mod.Hook("spin")
	timer := time.AfterFunc(50*time.Millisecond, func() { rt.Interrupt("timeout") })
	defer timer.Stop()
	_, err = rt.Call(spin)
	var interrupted *moejs.InterruptedError
	fmt.Println(errors.As(err, &interrupted), interrupted.Value) // true timeout
	rt.ClearInterrupt()
}

A Module is immutable and can be loaded by any number of runtimes concurrently, so a host compiles each plugin once and keeps a pool of runtimes per plugin. A Runtime belongs to one goroutine at a time; only Interrupt and ClearInterrupt may be called from others. Before it returns a runtime to the pool, the host calls rt.ReleaseCallData(), so the idle runtime does not keep the last request's arguments alive; what the module stored of them stays valid.

Host API

  • Hooks. Module.Hook(export, members...) resolves an exported function, or one below an exported object (mod.Hook("protocols", "openai", "decodeRequest")), once. Bindings stay live: each Call reads the export and walks the members, as own properties, in the runtime at hand. A path that leads nowhere is ErrHookNotFound, one that leads to a value that is not a function ErrNotCallable; Has reports either as false.

  • Module graphs. A module that imports others is linked before runtimes load it. moejs.Link(entry, resolve) asks the host's Resolver for the module each specifier names, once per module and specifier (moejs never reads files or the network), links the graph once and returns the Module to load: immutable and shared like any other, with Hook, Export and Exports addressing the entry's exports, re-exported names included. The resolver's referrer is the importing *Module, as a Referrer: the sealed interface of the code that requests a module, a *Module or a *Script. Each runtime only instantiates and evaluates the graph, each module of it once, and a module several graphs share is compiled once when the resolver returns the same *Module. A failed resolution is a *ResolveError, and an import that does not resolve to one binding a *SyntaxError, both at the importing module's position; Load refuses a module that imports and was not linked. A module that imports nothing needs no Link and pays nothing for graphs.

    entry, err := moejs.Compile("plugin.js", source)
    mod, err := moejs.Link(entry, func(referrer moejs.Referrer, specifier string) (*moejs.Module, error) {
    	return host.module(specifier) // compiled once per process
    })
    err = rt.Load(mod) // per runtime
    
  • Dynamic import. Options.Importer is the host's side of import() and import.meta: its Resolve is a Resolver, whose referrer is the importing *Module or the *Script that RunScript ran, and its optional Meta fills a module's import.meta, a null-prototype object made on first use. import(specifier) asks Resolve at once and returns a promise that settles with the module's namespace, or rejects with what the resolution, the linking or the evaluation failed with; without an Importer it rejects with a TypeError. The module is the identity: a runtime evaluates a module once, whether it was imported statically or dynamically, and the graph of a module import() loads is linked once per Importer, which any number of runtimes may share, so each runtime only instantiates and evaluates it. Jobs and interrupts behave as for Load and Call. Code without import() or import.meta compiles and runs exactly as before.

    imp := &moejs.Importer{Resolve: func(referrer moejs.Referrer, specifier string) (*moejs.Module, error) {
    	return host.module(specifier)
    }}
    rt := moejs.NewRuntime(moejs.Options{Importer: imp}) // one imp for every runtime
    
  • Values in. FromGo converts nil, booleans, numbers, strings, json.Number, Value, NativeFunc and the JSON-shaped containers (map[string]any, []any, map[string]string, []string, ...) lazily, one level at a time when JavaScript first reads it, so a hook pays only for the parts of its arguments it touches. JavaScript writes do not reach the Go value, and the host must not modify a value while JavaScript may still read it. Keys of objects from Go maps enumerate in sorted order. A []byte becomes an ArrayBuffer over the same bytes, which JavaScript writes do reach. Structs and other types are an error: marshal them and use ParseJSON. Function(name, length, fn) wraps a NativeFunc with the name and length JavaScript sees.

  • Values out. ToGo exports integral numbers as int64 and other numbers as float64, arrays as []any, objects as map[string]any of their own enumerable properties, bigints as *big.Int, which FromGo also accepts, and an ArrayBuffer, typed array or DataView as a copy of the bytes it holds or views, a []byte. AppendJSON is JSON.stringify written to a byte slice; unlike json.Marshal of ToGo's result, it omits undefined members, writes NaN and ±Infinity as null, keeps insertion order and calls toJSON. Get(v, key) reads one property, running getters, and Export(name) the current value of an export of the loaded module.

  • Scripts. CompileScript(name, source) compiles a classic script into an immutable *Script, sloppy unless it starts with a "use strict" directive; Runtime.RunScript runs it in the runtime's global environment and returns its completion value. Its var and function declarations become properties of the global object, and its let, const and class declarations global bindings that later scripts and the loaded modules see. A declaration that conflicts with an existing global binding throws before anything runs. SetGlobal writes the global object, so after a script's let x the global lexical binding shadows a SetGlobal("x", …) (ECMA-262 9.1.1.4.1).

  • Eval. Importing moejs installs the compiler behind eval, the Function constructors and Realm.EvalScript(name, source), which a native function can call on the *Realm it receives to run a classic script from a string (test262's $262.evalScript). A direct eval sees the caller's bindings, including a module's imports; the evaluated code and the functions it creates report the evaluating script or module as their source in stack traces, and their import() passes it as the Resolver's referrer (nil for the code of an indirect eval or a constructor called from a script or module that uses neither import(), import.meta nor a direct eval; see TODO.md). Code that uses neither eval nor with compiles exactly as before and pays nothing.

  • Limits on dynamic code. Options.MaxDynamicSource caps the source text that eval, the Function constructors and Realm.EvalScript compile: 1 MiB of UTF-8 by default, no limit when negative. Longer text throws a RangeError before it is parsed. Compiling takes time and memory linear in the length of the text, so the cap bounds both, and an interrupt stops a compile in progress. Options.DisableDynamicCode turns dynamic code off for a runtime: all of them throw an EvalError instead of compiling. Neither applies to Compile and CompileScript.

  • Promises. NewPromise gives a host function a promise to return and Go functions that settle it later. PromiseResult reads a promise's state and result, and SetPromiseRejectionTracker reports rejections no handler caught, as Sobek's tracker does.

  • Errors. A throw is an *Exception. Name() and Message() read the thrown value's name and message data properties (the thrown value itself for a primitive) and never run JavaScript. A host function that returns a Go error throws an Error with the error's text as its message, and the *Exception unwraps to the Go error. An interrupt is an *InterruptedError, a bad module or script a *SyntaxError with its position, and a Go panic inside a call (a panicking host function) an *InternalError; the runtime stays usable. Runtime.StackTrace(exc) returns a thrown Error's V8-format stack, also without running JavaScript.

  • Shared, frozen intrinsics by default. Runtimes share one deeply frozen set of builtins, the Hardened JavaScript (SES lockdown()) model: writing to Array.prototype throws a TypeError in strict code and, as for any frozen object, fails silently in sloppy code, so plugins cannot pollute each other's prototypes. Options{MutableIntrinsics: true} builds a mutable copy per runtime for embeddings that patch builtins, at about 25 times the creation cost.

  • Time zone per runtime. Options.TimeZone sets the local zone of Date (nil means time.Local).

Like Sobek, moejs converts decimal text with Go's strconv: a decimal number with more than 800 significant digits before its point or exponent, or with an exponent of 100000 or more that a long run of zeros offsets, can convert to the wrong value (Number("1" + "0".repeat(900) + "e-900") is 1e-101, not 1), in Number, parseFloat, JSON.parse, ParseJSON and source literals.

Design

  • 16-byte values. A Value is an unsafe.Pointer and a uint64. Numbers, booleans, undefined and null never allocate, and the pointer word always holds a real pointer or nil, so the value is safe for Go's garbage collector.
  • Shapes and inline caches. Objects share an immutable shape transition tree by property insertion order, property access instructions carry inline cache slots, and one epoch counter validates prototype chains. Objects converted from Go maps share shapes by key set, so plugin code reading ctx.xxx stays monomorphic across calls.
  • Register bytecode VM. Fixed-width 32-bit instructions, one contiguous register stack per runtime, calls without allocation, and exceptions unwound through handler tables instead of Go panic/recover.
  • Strings are ASCII (a zero-copy Go string), UTF-16 or a rope that flattens on demand; builtin property names are static atoms.
  • No locks on the hot path. Property access, calls, strings, host conversion and regular expression matching take no locks, so runtimes on different goroutines only meet in the garbage collector.
  • Regular expressions run on Go's regexp (RE2, linear time) when the translation is exact, and on a pure-Go backtracking engine otherwise, with a bounded stack and an interrupt check every 4096 steps.

Performance

All numbers are medians of 5 runs on 2026-09-24: Apple M5 Pro (6 super + 12 performance cores, 64 GiB), go1.26.6 darwin/arm64, GOMAXPROCS=18. The machine was not idle (load average 4–7), so treat differences under about 10% as noise. The baselines are Sobek v0.0.0-20260708062710 (pure Go), quickjs-go v0.7.7 (QuickJS, cgo) and v8go v0.9.0 (V8, cgo).

The workload is new-api's 10 task plugins (6,358 lines) and 269 hook calls recorded on Sobek, 47 of which throw. Every measurement is taken from the Go caller's side and includes converting the arguments and the result; the cgo engines exchange JSON text, which is their real cost. Every iteration checks the result.

Hook calls (all 269 cases in a loop, one goroutine, mean per call):

Engine Time Bytes Allocations
moejs 3.91 µs 4.9 KB 30
Sobek 8.64 µs 11.4 KB 163
v8go 14.37 µs 5.4 KB 104
quickjs-go 60.41 µs 7.7 KB 118

Host path (BenchmarkHostFlow: the same 269 calls with everything new-api's host does around them, mean per call). With Sobek's API the host deep-copies every argument, because Sobek wraps Go maps live, and decodes a result through Export, json.Marshal and json.Unmarshal into its struct. With moejs it passes its maps as they are and unmarshals the bytes of AppendJSON. The second row starts from each argument's JSON bytes, as the host holds stored task data (json.Unmarshal + ToValue for Sobek, ParseJSON for moejs):

Arguments moejs Sobek
Go values 6.09 µs / 6.3 KB / 44 allocs 15.32 µs / 17.1 KB / 248
JSON bytes 8.03 µs / 10.3 KB / 96 19.53 µs / 18.6 KB / 304

Runtimes (a new runtime with new-api's host globals, then evaluating a plugin module in it; memory is the Go heap retained per live runtime):

moejs Sobek quickjs-go v8go
New runtime 0.90 µs / 27 allocs 1.59 µs / 47 210 µs / 135 609 µs / 54
+ largest plugin (alibaba) 51 µs / 479 197 µs / 4,499 1,717 µs ¹ 1,309 µs ¹
+ smallest plugin (sora) 5.0 µs / 81 24.1 µs / 672 556 µs ¹ 722 µs ¹
Retained, alibaba, 512 runtimes 79 KiB 258 KiB 339 KiB ² 785 KiB ²
Retained, sora, 64 runtimes 11.5 KiB 48 KiB

¹ Includes compiling the script, which the cgo engines do per context. ² The engine's own heap (QuickJS malloc_size, V8 used heap size).

Compiling the largest plugin takes 1.5 ms in moejs and 1.6 ms in Sobek, once per process. With Options{MutableIntrinsics: true} a new runtime costs 22.6 µs and a live alibaba runtime retains 231 KiB.

Micro-benchmarks (moejs vs Sobek, 100 iterations per operation):

Case moejs Sobek Speed-up Allocations (moejs / Sobek)
Property read, monomorphic 4.2 µs 10.6 µs 2.5x 9 / 96
Property read, polymorphic 4.4 µs 7.5 µs 1.7x 8 / 11
Function call 4.1 µs 7.0 µs 1.7x 9 / 89
Closure 13.3 µs 38.0 µs 2.9x 211 / 1,094
Array push + for-of 5.4 µs 43.2 µs 8.1x 15 / 719
String concatenation 14.0 µs 22.3 µs 1.6x 399 / 712
String methods 135 µs 478 µs 3.5x 909 / 11,810
JSON.parse 4.2 ms 27.6 ms 6.6x 66k / 807k
JSON.stringify 3.5 ms 10.4 ms 3.0x 1,415 / 237k
Object.keys + Object.assign 141 µs 492 µs 3.5x 609 / 17,302
RegExp test + replace 123 µs 309 µs 2.5x 2,210 / 9,503
new Error + throw + catch 19.1 µs 45.1 µs 2.4x 300 / 1,393

The benchmarks live in bench/ and run new-api's plugins, which bench/testdata/plugins/fetch.sh downloads at a pinned commit. To reproduce them:

bench/testdata/plugins/fetch.sh
cd bench
go test -run xxx -bench 'Benchmark(HookSuite|HostFlow|NewRuntime|Instantiate|Compile|Micro)$' -benchmem -count 5 .
go test -run TestFootprint -v .

bench/scripts/run_all.sh runs the full set, including the parallel throughput and per-phase benchmarks.

PGO. default.pgo is recorded from the whole hook workload (go run ./cmd/pgo in bench/ regenerates it). Go applies a default.pgo automatically only in the main package's directory, so an embedding program passes -pgo=<path to moejs>/default.pgo or copies the profile into its own main package.

Testing

# Test inputs that are not part of the repository: new-api's plugins and the
# pinned test262 revision. Tests that need them skip until they are fetched.
bench/testdata/plugins/fetch.sh
bench/test262/fetch.sh

# Unit, audit and fuzz-corpus tests of the engine.
go test ./...

# Differential tests against Sobek, the expression corpus, the benchmarks and
# test262. bench/ is a separate module so that Sobek and the cgo engines never
# become dependencies of moejs; its V8 and QuickJS baselines need cgo.
cd bench && go test -timeout 30m ./...

bench/test262/README.md describes the test262 rules, and bench/test262/RESULTS.md has the results per directory.

Acknowledgements

moejs learned from these projects; no code was copied from them.

  • goja and Sobek: the Go interop conventions and the Export rules; Sobek is the reference of the differential tests.
  • QuickJS: the 16-byte value layout, atoms, shape transitions and compact builtin tables.
  • V8: hidden classes, inline caches, prototype validity (reduced to one counter), the Ignition register interpreter, Date.parse and the Error.prototype.stack format.
  • Lua 5.x: the fixed-width register instruction encoding.
  • JavaScriptCore and SpiderMonkey: NaN-boxing.
  • esbuild: the engineering of a fast JavaScript parser in Go.
  • Hardened JavaScript / SES: the lockdown() model behind shared frozen intrinsics.
  • quickjs-go and v8go: the cgo baselines of the benchmarks.
  • test262: the conformance suite.
  • new-api: the plugin host whose pkg/jsplugin defines the API moejs has to support.

License

moejs is licensed under the Apache License 2.0.

The benchmarks run new-api's task plugins, which are licensed under AGPL-3.0 and are not included in this repository; bench/testdata/plugins/fetch.sh downloads them.

Documentation

Overview

Package moejs embeds the moejs JavaScript engine in a Go host that runs ES modules as plugins: compile a module once, evaluate it in any number of runtimes, call its exported functions with Go data and read their results back as Go data or JSON bytes.

mod, err := moejs.Compile("plugin.js", source)
hook, err := mod.Hook("protocols", "openai", "decodeRequest")
rt := moejs.NewRuntime(moejs.Options{})
err = rt.SetGlobal("utils", map[string]any{"now": moejs.NativeFunc(now)})
err = rt.Load(mod)
arg, err := rt.ParseJSON(body)
res, err := rt.Call(hook, arg)
out, err := rt.AppendJSON(nil, res)

Values are engine values (package engine) under their own names, so host functions and the host share one representation and nothing is wrapped on the way in or out. Errors are returned, never panicked: a JavaScript throw is an *Exception, an interrupt an *InterruptedError, bad source a *SyntaxError.

The jobs JavaScript queues (promise reactions, queueMicrotask callbacks) run before the method that ran it returns, getters and proxy traps included (Load, Call, Has, Get, ToGo, AppendJSON, SetGlobal, the settlers of NewPromise): the first exception a callback throws is the method's error when it succeeded otherwise, and an interrupt drops the jobs left.

A module that imports others is linked once with the modules it imports, which the host resolves (moejs never reads files or the network):

mod, err := moejs.Link(entry, func(referrer moejs.Referrer, specifier string) (*moejs.Module, error) {
	return host.modules[specifier], nil // compiled once, by the host
})

A runtime that loads the linked module evaluates each module of its graph once; its hooks and exports are the entry's, re-exported names included. import() and import.meta are the host's too, through the Importer of Options, which resolves with the same kind of resolver:

rt := moejs.NewRuntime(moejs.Options{Importer: &moejs.Importer{Resolve: resolve}})

A module import() loads joins the runtime's modules: one the runtime evaluated before is not evaluated again.

A Module is immutable and may be loaded by many runtimes concurrently. A Runtime is one global environment with one loaded module; it must be used from one goroutine at a time, except Interrupt and ClearInterrupt.

Index

Examples

Constants

View Source
const (
	PromisePending   = engine.PromisePending
	PromiseFulfilled = engine.PromiseFulfilled
	PromiseRejected  = engine.PromiseRejected
)

Promise states.

View Source
const (
	// PromiseRejectionReject: the promise was rejected with no handler.
	PromiseRejectionReject = engine.PromiseRejectionReject
	// PromiseRejectionHandle: the first handler was added to the promise,
	// rejected earlier with none.
	PromiseRejectionHandle = engine.PromiseRejectionHandle
)

Rejection tracker operations.

Variables

View Source
var (
	Undefined = engine.Undefined
	Null      = engine.Null
	Bool      = engine.Bool
	Number    = engine.NumberValue
	Int       = engine.Int64Value
)

Value constructors, for host functions. None allocates except String.

View Source
var (
	// ErrHookNotFound is returned when a hook's export or one of its members
	// is missing, undefined or null.
	ErrHookNotFound = errors.New("moejs: hook not found")
	// ErrNotCallable is returned when a hook names a value that is not a
	// function.
	ErrNotCallable = errors.New("moejs: hook is not a function")
	// ErrModulePending is Load's error for a module with top-level await
	// whose evaluation still awaits once no job is left.
	ErrModulePending = engine.ErrModulePending
)
View Source
var Arg = engine.Arg

Arg returns args[i], or undefined when there are fewer arguments.

View Source
var ErrNoResolver = engine.ErrNoResolver

ErrNoResolver is the Err of the *ResolveError of Link for a module that imports when the resolver is nil.

Functions

func PromiseResult

func PromiseResult(p Value) (state PromiseState, result Value, ok bool)

PromiseResult reports the state of the promise p and its result: the fulfillment value or the rejection reason, undefined while pending. ok is false when p is not a promise. No user code runs, so a promise a Call returned is read as the jobs that Call ran left it.

Types

type Exception

type Exception = engine.Exception

Exception is a thrown JavaScript value.

type Hook

type Hook struct {
	// contains filtered or unexported fields
}

Hook is a resolved path to a function in a module (Module.Hook), which Call finds in a runtime that loaded the module. A Hook of an export the module declares itself also works in a runtime that loaded the Module Link returned for it; a re-exported name needs the Hook of that Module. The zero Hook names nothing.

func (Hook) Name

func (h Hook) Name() string

Name returns the path joined with dots ("protocols.openai.decodeRequest").

type Importer

type Importer struct {
	// Resolve returns the module import(specifier) names in the code of
	// referrer: the *Module whose code imports (Link's entry or a module a
	// resolver returned for its graph; Compile's module for a module Load
	// evaluated alone), or the *Script RunScript ran; nil for code of
	// neither. Eval code and the code of the Function constructors import
	// for the script or module whose code evaluates them when that code
	// uses import() or import.meta or holds a direct eval, and with a nil
	// referrer otherwise, as when a job calls eval or a constructor
	// directly, with no code of a script or module running
	// (Promise.resolve(s).then(eval)). It runs within import(), and its
	// error rejects import()'s promise: an *Exception with its value, a
	// *SyntaxError with a SyntaxError, an *InterruptedError stops the
	// running code, any other error rejects with an Error. The modules a
	// returned module imports are resolved through Resolve too, as Link
	// does.
	Resolve Resolver
	// Meta, when set, fills the import.meta object of module m when a
	// runtime first evaluates import.meta in m's code. The object has a null
	// prototype and no properties. Its error is thrown by the import.meta
	// expression, and the next evaluation calls Meta again.
	Meta func(r *Realm, m *Module, meta *Object) error
	// contains filtered or unexported fields
}

Importer is the host's side of import() and import.meta in the runtimes whose Options name it. One Importer may serve any number of runtimes, concurrently; its fields must not change once a runtime uses it, and it must not be copied then. Without an Importer, import() rejects with a TypeError and import.meta is an empty object.

import(specifier) asks Resolve for the module at once and links and evaluates it from a job: its promise settles with the module's namespace or what the resolution, the linking or the evaluation failed with. The module is the identity, as for Link: a runtime evaluates one module once, whether its code imports it statically or dynamically, so a module the runtime evaluated before is not evaluated again. The graph of a module is linked once per Importer (the graph of a module Link returned is its own), so each runtime only instantiates and evaluates it.

A module whose evaluation an interrupt stopped stays failed in the runtime: one whose top level the interrupt stopped, before or after an await; one whose next step (resuming its top level, or running it once the modules it imports evaluated) the interrupt dropped with the runtime's queued jobs; and one that imports such a module. After ClearInterrupt, an import() whose graph reaches it rejects with an Error whose message is `Cannot import "<specifier>": its evaluation was interrupted`, with the specifier import() was given. The Error holds neither the interrupt's value nor a Go error: thrown out of a hook, its *Exception unwraps to nil, where the Error of a Resolve error unwraps to that error. An import() in flight when the interrupt came stays pending, and so does a module whose top level awaits a promise the interrupt left pending, as other code awaiting it does.

Resolve and Meta are called from every goroutine that uses the Importer, concurrently. Each call runs within the call of the runtime that evaluates the import() or import.meta, or runs the job that links the module import() loaded, and Resolve may call back into that runtime.

What the Importer and its runtimes keep grows with the distinct code they see. The Importer holds every module with imports that import() loaded, with its graph, for as long as it lives. A runtime holds a record of every distinct script and module whose code uses import() or import.meta or holds a direct eval and that it ran, with the *Script or *Module, for as long as it lives, however many times the code runs, and an entry for each piece of code it compiled from a string in them (eval code and the code of the Function constructors). A host that generates modules or scripts at run time must bound how many one Importer loads and one runtime runs.

type InternalError

type InternalError struct {
	Value any    // the recovered panic value
	Stack []byte // the goroutine stack at the panic
}

InternalError is a Go panic that escaped the engine: an engine bug or a host function that panicked. The runtime's call state is restored, but the host should drop the runtime.

func (*InternalError) Error

func (e *InternalError) Error() string

func (*InternalError) Unwrap

func (e *InternalError) Unwrap() error

Unwrap returns the panic value when it is an error.

type InterruptedError

type InterruptedError = engine.InterruptedError

InterruptedError is returned when Interrupt stopped running code.

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module is a compiled ES module. It is immutable: any number of runtimes may load it, concurrently.

func Compile

func Compile(name, source string) (*Module, error)

Compile parses and compiles an ES module. Bad source, including features the engine does not support yet, is a *SyntaxError. A module that imports others is loaded once Link linked it with them.

func Link(entry *Module, resolve Resolver) (*Module, error)

Link resolves the modules entry imports, directly or not, asking resolve once per module and specifier, links them and returns entry linked with its graph: the Module runtimes load, whose Hook and Exports also reach the names entry re-exports. Linking happens once; the Module is immutable, and each runtime that loads it only instantiates and evaluates the graph. The Hooks of entry's own exports stay valid for the linked Module. An import that does not resolve to one binding is a *SyntaxError at the import in the importing module, and so is an indirect export; a failed resolution is a *ResolveError. Link runs no JavaScript: a panic of resolve propagates. A module that imports nothing and uses neither import() nor import.meta is returned as is.

func (*Module) Exports

func (m *Module) Exports() []string

Exports returns the module's export names, sorted: for a module Link returned, the names it re-exports too, without those its star exports leave ambiguous.

func (*Module) Hook

func (m *Module) Hook(export string, members ...string) (Hook, error)

Hook names an exported function, or a function below an exported object reached through own properties (members "openai", "decodeRequest" of export "protocols"). The export slot and the property keys are resolved here, once; the bindings stay live, so each Call reads the export and walks the members in the runtime at hand. An export the module does not declare, or re-export once linked, is ErrHookNotFound.

func (*Module) Name

func (m *Module) Name() string

Name returns the name given to Compile.

func (*Module) Requests

func (m *Module) Requests() []string

Requests returns the specifiers the module imports from, in the order they first appear in its source, nil when it imports none.

type NativeFunc

type NativeFunc = engine.NativeFunc

NativeFunc is a host function. Returning an error throws: an *Exception or *InterruptedError as is, any other error as an Error whose message is err.Error() and which unwraps to err.

type Object

type Object = engine.Object

Object is a JavaScript object.

type Options

type Options struct {
	// MutableIntrinsics gives the runtime its own mutable copy of the
	// builtins (prototypes, constructors, Math, JSON). By default they are
	// built once per process, deeply frozen and shared by every runtime, and
	// writing to them throws a TypeError.
	MutableIntrinsics bool
	// TimeZone is the local time zone of Date; nil means time.Local.
	TimeZone *time.Location
	// Importer resolves the modules of import() and fills import.meta; nil
	// means import() rejects with a TypeError.
	Importer *Importer
	// MaxDynamicSource is the length in UTF-8 bytes of the longest source
	// text eval, the Function constructors and Realm.EvalScript compile:
	// longer text throws a RangeError. Zero
	// means engine.DefaultMaxDynamicSource (1 MiB), a negative value no
	// limit (engine.RealmOptions). Compile and CompileScript have no limit.
	MaxDynamicSource int
	// DisableDynamicCode makes eval of a string, the Function constructors
	// and Realm.EvalScript throw an EvalError instead of compiling anything,
	// for hosts whose code needs none (engine.RealmOptions). Compile and
	// CompileScript are not affected.
	DisableDynamicCode bool
}

Options configures NewRuntime.

type PromiseRejectionOperation

type PromiseRejectionOperation = engine.PromiseRejectionOperation

PromiseRejectionOperation is what a rejection tracker is told (see SetPromiseRejectionTracker).

type PromiseState

type PromiseState = engine.PromiseState

PromiseState is the state of a promise.

type Realm

type Realm = engine.Realm

Realm is the engine state a Runtime owns; host functions receive it.

type Referrer

type Referrer interface {
	// Name returns the name the code was compiled with.
	Name() string
	// contains filtered or unexported methods
}

A Referrer is the code that requests a module: a *Module or a *Script.

type ResolveError

type ResolveError struct {
	File      string
	Line      int // 1-based
	Column    int // 1-based, in code points
	Specifier string
	Err       error
}

ResolveError is Link's error when the resolver fails for an import, an export-from or a star export of a module, at File, Line and Column of that entry: Err is the resolver's error, ErrNoResolver, or an error when the resolver returned no module. Load returns one too when the graph resolves the request of a module the runtime instantiated before (from import()) to another module than the runtime did; an import() of such a graph rejects with an Error of it.

func (*ResolveError) Error

func (e *ResolveError) Error() string

Error formats as `file:line:col: cannot resolve module "specifier": err`.

func (*ResolveError) Unwrap

func (e *ResolveError) Unwrap() error

type Resolver

type Resolver func(referrer Referrer, specifier string) (*Module, error)

Resolver returns the module that specifier names in referrer (HostLoadImportedModule). Link asks it for the requests of the modules of a graph: the referrer is the importing *Module, Link's entry or a module the resolver returned. An Importer asks it for import() too, where the referrer can also be a *Script, or nil. moejs never reads files or the network: the host maps every specifier to a module it compiled. The module is the identity: a runtime evaluates one module once, so a resolver returns the same *Module for the same module every time, which also compiles a module shared by several graphs once. A module Link returned stands for the module it linked: its graph is not used, and Link resolves its requests again.

type Runtime

type Runtime struct {
	// contains filtered or unexported fields
}

Runtime is one JavaScript global environment with at most one loaded module. It must be used from one goroutine at a time; only Interrupt and ClearInterrupt may be called concurrently.

func NewRuntime

func NewRuntime(opts Options) *Runtime

NewRuntime creates a runtime.

func (*Runtime) AppendJSON

func (rt *Runtime) AppendJSON(dst []byte, v Value) (out []byte, err error)

AppendJSON appends JSON.stringify(v) to dst as UTF-8. A value with no JSON form (undefined, a function, a symbol) appends null. A proxy is serialized as JSON.stringify does it: its traps run.

func (*Runtime) Call

func (rt *Runtime) Call(h Hook, args ...Value) (res Value, err error)

Call invokes the function h names with this = undefined. A path that does not lead to a value is ErrHookNotFound, one that leads to a value that is not a function ErrNotCallable; a throw is an *Exception, an interrupt an *InterruptedError. The hooks of a module whose top level failed return Load's error.

func (*Runtime) ClearInterrupt

func (rt *Runtime) ClearInterrupt()

ClearInterrupt drops a pending interrupt.

func (*Runtime) Export

func (rt *Runtime) Export(name string) (v Value, ok bool)

Export returns the current value of the loaded module's export name, a name it re-exports included. ok is false when there is no such export or the binding is not initialized yet, and, once the module's top level failed (Load), for a function or a module namespace object, whose code could find the module's other bindings uninitialized without the ReferenceError.

func (*Runtime) FromGo

func (rt *Runtime) FromGo(v any) (Value, error)

FromGo converts a Go value: nil, bool, the integer and float kinds, string, json.Number, *big.Int (a bigint), Value, NativeFunc, and the JSON-shaped containers map[string]any, map[string]string, map[string][]string, []any, []string and []map[string]any. Containers convert lazily, one level when first touched, so a large argument the hook reads little of costs little; the Go value must not change while the result is in use, and JavaScript writes never reach it. Maps enumerate their keys sorted. A []byte becomes an ArrayBuffer over the same bytes, not a copy: JavaScript writes reach them, and the host must not modify them while JavaScript may read them. Other types (structs, named map types) are an error: marshal them and use ParseJSON.

func (*Runtime) Function

func (rt *Runtime) Function(name string, length int, fn NativeFunc) Value

Function creates a host function with a name and a length, the values of its `name` and `length` properties.

func (*Runtime) Get

func (rt *Runtime) Get(v Value, key string) (res Value, err error)

Get reads property key of v, running a getter and walking the prototype chain; undefined and null have no properties and read as undefined.

func (*Runtime) Has

func (rt *Runtime) Has(h Hook) (ok bool, err error)

Has reports whether h names a function in this runtime now. A getter on the path that throws is returned as the error, and so is Load's for the hooks of a module whose top level failed.

func (*Runtime) Interrupt

func (rt *Runtime) Interrupt(v any)

Interrupt stops running code: the pending Call (or Load, ToGo, ...) returns an *InterruptedError carrying v. It may be called from any goroutine. An interrupt that arrives while nothing runs stops the next Call, whatever the function it names, or Load, and any other method once it runs JavaScript, so a host calls ClearInterrupt before reusing the runtime.

func (*Runtime) Load

func (rt *Runtime) Load(m *Module) (err error)

Load evaluates m's top level in this runtime. A runtime loads one module once, and a Load made while the top level runs is refused too. A module that imports others loads once Link linked it: Load then evaluates the modules of its graph first, each once, in the order of their imports; a failure of one of them is a failure of m's top level, which then does not run. In a runtime with an Importer, import() of m, or of a module of its graph, finds the instance Load evaluated. When the top level throws, is interrupted or panics (an *InternalError) before it ends or first awaits, the error is returned and the module stays loaded: Export reads the bindings initialized before the failure other than functions and module namespaces (none when an interrupt stopped the top level before it started), and Call and Has return the error for the module's hooks: a function could find the other bindings uninitialized. The jobs the top level queued run after it, when Export and the module's hooks find its bindings; while the top level itself runs they find none. A module with top-level await evaluates asynchronously: its top level resumes from those jobs, with the bindings found, and Load returns once none is left, with what the evaluation rejected with (an interrupt of those jobs wins), or ErrModulePending when it still awaits; an exported function called while it awaits throws a ReferenceError for a binding it has not initialized yet.

func (*Runtime) Module

func (rt *Runtime) Module() *Module

Module returns the loaded module, or nil.

func (*Runtime) NewPromise

func (rt *Runtime) NewPromise() (p Value, resolve, reject func(v Value) error)

NewPromise creates a pending promise, for a host function to return, and the functions that settle it. resolve does what the promise's resolve function does in JavaScript: it adopts a thenable and fulfills with any other value; reject rejects. The first call of either decides and later calls do nothing. Both may be called after the host function returned, from the goroutine that uses the runtime: called outside a Call, they run the jobs they queue before returning, as the end of a Call does, and return what a Call would for those (an *InterruptedError, or the first exception a queueMicrotask callback threw).

func (*Runtime) ParseJSON

func (rt *Runtime) ParseJSON(b []byte) (Value, error)

ParseJSON is JSON.parse of b.

func (*Runtime) Realm

func (rt *Runtime) Realm() *Realm

Realm returns the runtime's engine state, for host functions and tests that need the engine API directly.

func (*Runtime) ReleaseCallData

func (rt *Runtime) ReleaseCallData()

ReleaseCallData drops the runtime's references to the host values of finished calls, so a pooled runtime does not keep the last request alive. A host that pools runtimes calls it after the request's last use of the runtime (Call, ToGo, Get, AppendJSON: each can run JavaScript), before it returns the runtime to its pool; Call does not, so that a request running several hooks converts its data as one, and hosts that do not pool pay nothing. Values already returned stay valid: their nodes keep their own storage, so ToGo and Get of them work after it. Between calls only; a no-op inside one (from a host function).

What JavaScript keeps stays alive with what it references: a string of a FromGo argument the module stores keeps every string converted in its period (up to 512), and an object it stores keeps its chunk and, through the root placeholder in it, the whole argument; a substring of an ASCII string shares its bytes, so storing a slice of a large one keeps all of it. The names the runtime keeps for its caches (property names, RegExp patterns) are its own copies, whether they come from map keys, a parsed JSON text or a slice of a string.

func (*Runtime) RunScript

func (rt *Runtime) RunScript(s *Script) (res Value, err error)

RunScript runs s in the runtime's global environment and returns its completion value. Its var and function declarations become properties of the global object; its let, const and class declarations are global bindings that later scripts and the loaded module (before or after Load) see too. A declaration that conflicts with an existing global binding throws before anything runs. The jobs the script queued run before RunScript returns. A throw is an *Exception, an interrupt an *InterruptedError.

func (*Runtime) SetGlobal

func (rt *Runtime) SetGlobal(name string, v any) (err error)

SetGlobal assigns the global variable name. v is converted by FromGo, so a host installs a namespace of functions as a map[string]any with NativeFunc values. It writes the global object, whose property a script's global let, const or class declaration of the same name shadows (ECMA-262 9.1.1.4.1).

func (*Runtime) SetPromiseRejectionTracker

func (rt *Runtime) SetPromiseRejectionTracker(f func(p Value, op PromiseRejectionOperation))

SetPromiseRejectionTracker registers f to be called when a promise is rejected with no handler (PromiseRejectionReject) and when a promise so rejected gets its first handler (PromiseRejectionHandle), ECMAScript's HostPromiseRejectionTracker. A promise told Reject and not Handle when a Call returns has an unhandled rejection. f runs synchronously, in the reject or then that caused the operation. nil removes the tracker.

func (*Runtime) StackTrace

func (rt *Runtime) StackTrace(exc *Exception) string

StackTrace returns the `stack` of an Error thrown in this runtime: "Name: message" and one " at ..." line per frame. It is "" when the thrown value is not an Error or its stack was replaced by a non-string. No user code runs.

func (*Runtime) ToGo

func (rt *Runtime) ToGo(v Value) (out any, err error)

ToGo exports v: undefined and null become nil, booleans bool, strings string, integral numbers in int64 range int64 (except -0), other numbers float64, arrays []any, other objects map[string]any of their own enumerable string-keyed properties, Date time.Time, bigint *big.Int, and functions and symbols the engine value itself (*Object, *engine.Symbol). An ArrayBuffer or SharedArrayBuffer exports a copy of its bytes as a []byte, and a typed array or DataView a copy of the bytes it views (so a Uint16Array of 2 elements gives 4 bytes, little-endian); a detached buffer and a view out of its buffer's bounds give a nil []byte, and a zero-length buffer or view a non-nil empty one. A proxy exports through its traps (see engine.Realm.ToGo): []any when its target is an array, the map of its enumerable keys otherwise, the *Object when it is callable. A getter or trap that throws, or a revoked proxy, is returned as the error.

type Script

type Script struct {
	// contains filtered or unexported fields
}

Script is a compiled classic script. It is immutable: any number of runtimes may run it, concurrently, and a runtime may run it more than once.

func CompileScript

func CompileScript(name, source string) (*Script, error)

CompileScript parses and compiles a classic script. A script is sloppy mode code unless it starts with a "use strict" directive. Bad source, including features the engine does not support yet, is a *SyntaxError.

func (*Script) Name

func (s *Script) Name() string

Name returns the name given to CompileScript.

type SyntaxError

type SyntaxError struct {
	File    string
	Line    int // 1-based
	Column  int // 1-based, in code points
	Message string
}

SyntaxError is a parse or early error in module source.

func (*SyntaxError) Error

func (e *SyntaxError) Error() string

Error formats as "file:line:col: SyntaxError: message".

type Value

type Value = engine.Value

Value is a JavaScript value.

func String

func String(s string) Value

String converts a Go string.

Directories

Path Synopsis
Package bytecode defines the moejs instruction set and the compiled-function template shared between the compiler and the engine's interpreter.
Package bytecode defines the moejs instruction set and the compiled-function template shared between the compiler and the engine's interpreter.
Package compiler translates the annotated AST produced by package syntax into bytecode.Function templates.
Package compiler translates the annotated AST produced by package syntax into bytecode.Function templates.
internal
gen/decomp command
Command decomp generates engine/decomp_tables.go, the character decomposition data of String.prototype.localeCompare: the Decomposition_Mapping field of UnicodeData.txt (canonical and compatibility mappings, one level each, with the compatibility tag) and the non-zero Canonical_Combining_Class values.
Command decomp generates engine/decomp_tables.go, the character decomposition data of String.prototype.localeCompare: the Decomposition_Mapping field of UnicodeData.txt (canonical and compatibility mappings, one level each, with the compatibility tag) and the non-zero Canonical_Combining_Class values.
gen/ucd command
Command ucd generates internal/regexpsyntax/unicode_tables.go, the Unicode Character Database tables of the regular-expression engine: General_Category, Script and Script_Extensions values, the binary properties of ECMA-262 table 67, the properties of strings of the v flag, simple case folding and the non-u Canonicalize mapping.
Command ucd generates internal/regexpsyntax/unicode_tables.go, the Unicode Character Database tables of the regular-expression engine: General_Category, Script and Script_Extensions values, the binary properties of ECMA-262 table 67, the properties of strings of the v flag, simple case folding and the non-u Canonicalize mapping.
regexpsyntax
Package regexpsyntax parses ECMAScript regular expression patterns.
Package regexpsyntax parses ECMAScript regular expression patterns.
Package syntax implements the moejs front end: a hand-written lexer, a recursive-descent parser producing a compact AST, early errors, and a scope-resolution pass whose annotations the compiler consumes directly.
Package syntax implements the moejs front end: a hand-written lexer, a recursive-descent parser producing a compact AST, early errors, and a scope-resolution pass whose annotations the compiler consumes directly.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL