We want to provide access to the tree-sitter tree (among other relevant metadata) to JavaScript rule execution.
What is your solution?
This PR does two main things:
1. Introduce code structure and patterns for Rust <> v8 interop
The code structure has three components:
struct js::{ArbitraryName} - A thin layer over a JavaScript object (already exists).
Serializes/deserializes the JavaScript object it represents (which is physically adjacent as {ArbitraryName}.js).
Exposes APIs to manipulate instances of the JavaScript object so that the JavaScript end is opaque to the caller (an example caller is a Bridge (#3)).
Tests any relevant {ArbitraryName}.js behavior that isn't straightforward (for example, use of deno ops)
struct ddsa_lib::{ArbitraryName} - The Rust representation of an "ArbitraryName" and its data (new via this PR).
Doesn't know/care that it has a complementary v8 representation.
Provides an API to manipulate Rust data contained by the struct.
Tests any relevant logic about updating Rust data.
struct {ArbitraryNameBridge} The orchestrator of Rust <> V8 (new via this PR).
Exposes the public API that the upcoming ddsa::JsRuntime will consume.
Under the hood, a bridge is responsible for keeping #1 and #2 in sync, as indicated by the "Linked" abstraction.
It will only be possible to mutate data via a bridge API
With this architecture, #3 is the brains behind the the Rust <> v8 interop, and #1 and #2 are ignorant about how they are being used.
2. Implement "Context"
Root - Metadata solely for the file: a pointer to the tree-sitter free, filename, and file contents.
The bridge design was chosen because we don't allocate new "Context" v8 objects for every execution (like a more straightforward, but less performant solution would do). And so because this means we have to use a v8 global object (i.e. one that has been leaked and isn't tracked by the v8 garbage collector), we need to keep state in sync and ensure no side effects from one execution to the other. A nice design for this is to have #1 and #2 be "dumb" structs that are operated in conjunction by a bridge and hidden by the public API.
Technical notes
I introduced marker structs Class and Instance because I often found myself getting confused because the ddsa_lib::{ArbitraryName} and js::{ArbitraryName} share the same struct name, and in the IDE, the module isn't always shown. Additionally, within JavaScript, we leak some variables into v8 globals that are either JavaScript classes or JavaScript class instances. Rather than try to do something like ArbitraryNameInstance or ArbitraryNameClass, I decided to solve both problems with a zero-width marker struct: we now have ddsa_lib::{ArbitraryName} and js::{ArbitraryName}<Class> or js::{ArbitraryName}<Instance>. While it may tangentially improve type safety, the impetus here was for readability.
Alternatives considered
What the reviewer should know
With this PR, all the plumbing to access the tree-sitter tree via JS is implemented.
The additions here are "inert" in the sense that none of this code is currently executed. The plan is to have a separate PR that completely switches over from stella to ddsa (after all ddsa code is merged)
The unit tests are very targeted around core logic, not overall "business logic". Business logic tests (like performing a scan and getting expected results) will happen in the upcoming ddsa_lib::JsRuntime's unit tests.
What problem are you trying to solve?
We want to provide access to the tree-sitter tree (among other relevant metadata) to JavaScript rule execution.
What is your solution?
This PR does two main things:
1. Introduce code structure and patterns for Rust <> v8 interop
The code structure has three components:
struct js::{ArbitraryName}
- A thin layer over a JavaScript object (already exists).{ArbitraryName}.js
).#3
)).{ArbitraryName}.js
behavior that isn't straightforward (for example, use of deno ops)struct ddsa_lib::{ArbitraryName}
- The Rust representation of an "ArbitraryName" and its data (new via this PR).struct {ArbitraryNameBridge}
The orchestrator of Rust <> V8 (new via this PR).ddsa::JsRuntime
will consume.#1
and#2
in sync, as indicated by the "Linked" abstraction.It will only be possible to mutate data via a bridge API With this architecture,
#3
is the brains behind the the Rust <> v8 interop, and#1
and#2
are ignorant about how they are being used.2. Implement "Context"
.tf
file).FileContextGo
will be demonstrated in the next PR.Resulting file organization
Design constraints
The bridge design was chosen because we don't allocate new "Context" v8 objects for every execution (like a more straightforward, but less performant solution would do). And so because this means we have to use a v8 global object (i.e. one that has been leaked and isn't tracked by the v8 garbage collector), we need to keep state in sync and ensure no side effects from one execution to the other. A nice design for this is to have
#1
and#2
be "dumb" structs that are operated in conjunction by a bridge and hidden by the public API.Technical notes
Class
andInstance
because I often found myself getting confused because theddsa_lib::{ArbitraryName}
andjs::{ArbitraryName}
share the same struct name, and in the IDE, the module isn't always shown. Additionally, within JavaScript, we leak some variables into v8 globals that are either JavaScript classes or JavaScript class instances. Rather than try to do something likeArbitraryNameInstance
orArbitraryNameClass
, I decided to solve both problems with a zero-width marker struct: we now haveddsa_lib::{ArbitraryName}
andjs::{ArbitraryName}<Class>
orjs::{ArbitraryName}<Instance>
. While it may tangentially improve type safety, the impetus here was for readability.Alternatives considered
What the reviewer should know
ddsa_lib::JsRuntime
's unit tests.