Skip to content

Commit 73c44b0

Browse files
committed
Compile with WASI
1 parent 3c4b3b5 commit 73c44b0

File tree

18 files changed

+750
-2
lines changed

18 files changed

+750
-2
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: JavaScript Bindings
2+
3+
on:
4+
push:
5+
paths:
6+
- ".github/workflows/javascript-bindings.yml"
7+
- "include/"
8+
- "src/"
9+
- "*akefile*"
10+
branches:
11+
- main
12+
pull_request:
13+
14+
jobs:
15+
build:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v3
19+
20+
- name: Set up Ruby
21+
uses: ruby/setup-ruby@v1
22+
with:
23+
ruby-version: head
24+
bundler-cache: true
25+
26+
- name: rake templates
27+
run: bundle exec rake templates
28+
29+
- name: Set up WASI-SDK
30+
run: |
31+
wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-20/wasi-sdk-20.0-linux.tar.gz
32+
tar xvf wasi-sdk-20.0-linux.tar.gz
33+
34+
- name: Build the project
35+
run: make wasm WASI_SDK_PATH=$(pwd)/wasi-sdk-20.0
36+
37+
- uses: actions/setup-node@v3
38+
with:
39+
node-version: 20.x
40+
41+
- name: Run the tests
42+
run: npm test
43+
working-directory: javascript
44+
45+
- uses: actions/upload-artifact@v3
46+
with:
47+
name: prism.wasm
48+
path: javascript/src/prism.wasm

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ a.out
2525
/ext/prism/api_node.c
2626
/fuzz/output/
2727
/include/prism/ast.h
28+
/javascript/node_modules/
29+
/javascript/package-lock.json
30+
/javascript/src/deserialize.js
31+
/javascript/src/nodes.js
32+
/javascript/src/prism.wasm
33+
/javascript/types/
2834
/java/org/prism/AbstractNodeVisitor.java
2935
/java/org/prism/Loader.java
3036
/java/org/prism/Nodes.java

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ SOEXT := $(shell ruby -e 'puts RbConfig::CONFIG["SOEXT"]')
1313
CPPFLAGS := -Iinclude
1414
CFLAGS := -g -O2 -std=c99 -Wall -Werror -Wextra -Wpedantic -Wundef -Wconversion -fPIC -fvisibility=hidden
1515
CC := cc
16+
WASI_SDK_PATH := /opt/wasi-sdk
1617

1718
HEADERS := $(shell find include -name '*.h')
1819
SOURCES := $(shell find src -name '*.c')
@@ -23,6 +24,7 @@ all: shared static
2324

2425
shared: build/librubyparser.$(SOEXT)
2526
static: build/librubyparser.a
27+
wasm: javascript/src/prism.wasm
2628

2729
build/librubyparser.$(SOEXT): $(SHARED_OBJECTS)
2830
$(ECHO) "linking $@"
@@ -32,6 +34,10 @@ build/librubyparser.a: $(STATIC_OBJECTS)
3234
$(ECHO) "building $@"
3335
$(Q) $(AR) $(ARFLAGS) $@ $(STATIC_OBJECTS) $(Q1:0=>/dev/null)
3436

37+
javascript/src/prism.wasm: Makefile $(SOURCES) $(HEADERS)
38+
$(ECHO) "building $@"
39+
$(Q) $(WASI_SDK_PATH)/bin/clang --sysroot=$(WASI_SDK_PATH)/share/wasi-sysroot/ $(DEBUG_FLAGS) -DPRISM_EXPORT_SYMBOLS -D_WASI_EMULATED_MMAN -lwasi-emulated-mman $(CPPFLAGS) $(CFLAGS) -Wl,--export-all -Wl,--no-entry -mexec-model=reactor -o $@ $(SOURCES)
40+
3541
build/shared/%.o: src/%.c Makefile $(HEADERS)
3642
$(ECHO) "compiling $@"
3743
$(Q) mkdir -p $(@D)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ See the [CONTRIBUTING.md](CONTRIBUTING.md) file for more information. We additio
8585
* [Encoding](docs/encoding.md)
8686
* [Fuzzing](docs/fuzzing.md)
8787
* [Heredocs](docs/heredocs.md)
88+
* [JavaScript](docs/javascript.md)
8889
* [Mapping](docs/mapping.md)
8990
* [Ripper](docs/ripper.md)
9091
* [Ruby API](docs/ruby_api.md)

docs/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ A lot of code in prism's repository is templated from a single configuration fil
44

55
* `ext/prism/api_node.c` - for defining how to build Ruby objects for the nodes out of C structs
66
* `include/prism/ast.h` - for defining the C structs that represent the nodes
7+
* `javascript/src/deserialize.js` - for defining how to deserialize the nodes in JavaScript
8+
* `javascript/src/nodes.js` - for defining the nodes in JavaScript
79
* `java/org/prism/AbstractNodeVisitor.java` - for defining the visitor interface for the nodes in Java
810
* `java/org/prism/Loader.java` - for defining how to deserialize the nodes in Java
911
* `java/org/prism/Nodes.java` - for defining the nodes in Java

docs/javascript.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# JavaScript
2+
3+
Prism provides bindings to JavaScript out of the box.
4+
5+
## Node
6+
7+
To use the package from node, install the `@ruby/prism` dependency:
8+
9+
```sh
10+
npm install @ruby/prism
11+
```
12+
13+
Then import the package:
14+
15+
```js
16+
import { loadPrism } from "@ruby/prism";
17+
```
18+
19+
Then call the load function to get a parse function:
20+
21+
```js
22+
const parse = await loadPrism();
23+
```
24+
25+
## Browser
26+
27+
To use the package from the browser, you will need to do some additional work. The [javascript/example.html](javascript/example.html) file shows an example of running Prism in the browser. You will need to instantiate the WebAssembly module yourself and then pass it to the `parsePrism` function.
28+
29+
First, get a shim for WASI since not all browsers support it yet.
30+
31+
```js
32+
import { WASI } from "https://unpkg.com/@bjorn3/browser_wasi_shim@latest/dist/index.js";
33+
```
34+
35+
Next, import the `parsePrism` function from `@ruby/prism`, either through a CDN or by bundling it with your application.
36+
37+
```js
38+
import { parsePrism } from "https://unpkg.com/@ruby/prism@latest/src/parsePrism.js";
39+
```
40+
41+
Next, fetch and instantiate the WebAssembly module. You can access it through a CDN or by bundling it with your application.
42+
43+
```js
44+
const wasm = await WebAssembly.compileStreaming(fetch("https://unpkg.com/@ruby/prism@latest/src/prism.wasm"));
45+
```
46+
47+
Next, instantiate the module and initialize WASI.
48+
49+
```js
50+
const wasi = new WASI([], [], []);
51+
const instance = await WebAssembly.instantiate(wasm, { wasi_snapshot_preview1: wasi.wasiImport });
52+
wasi.initialize(instance);
53+
```
54+
55+
Finally, you can create a function that will parse a string of Ruby code.
56+
57+
```js
58+
function parse(source) {
59+
return parsePrism(instance.exports, source);
60+
}
61+
```
62+
63+
## API
64+
65+
Now that we have access to a `parse` function, we can use it to parse Ruby code:
66+
67+
```js
68+
const parseResult = parse("1 + 2");
69+
```
70+
71+
A ParseResult object is very similar to the Prism::ParseResult object from Ruby. It has the same properties: `value`, `comments`, `magicComments`, `errors`, and `warnings`. Here we can serialize the AST to JSON.
72+
73+
```js
74+
console.log(JSON.stringify(parseResult.value, null, 2));
75+
```
76+
77+
## Building
78+
79+
To build the WASM package yourself, first obtain a copy of `wasi-sdk`. You can retrieve this here: <https://github.com/WebAssembly/wasi-sdk>. Next, run:
80+
81+
```sh
82+
make wasm WASI_SDK_PATH=path/to/wasi-sdk
83+
```
84+
85+
This will generate `javascript/src/prism.wasm`. From there, you can run the tests to verify everything was generated correctly.
86+
87+
```sh
88+
cd javascript
89+
node test
90+
```

javascript/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# @ruby/prism
2+
3+
JavaScript bindings for Ruby's [prism](https://github.com/ruby/prism) parser.

javascript/example.html

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>@ruby/prism</title>
5+
</head>
6+
<body style="margin: 0;">
7+
<div style="display: grid; grid-template-columns: 1fr 1fr;">
8+
<div>
9+
<textarea id="input" style="box-sizing: border-box; width: 100%; height: 100vh; resize: none; vertical-align: top;"></textarea>
10+
</div>
11+
<div style="height: 100vh; overflow-y: scroll;">
12+
<code><pre id="output" style="margin: 0; padding: 1em;"></pre></code>
13+
</div>
14+
</div>
15+
<script type="module">
16+
import { WASI } from "https://unpkg.com/@bjorn3/browser_wasi_shim@latest/dist/index.js";
17+
import { parsePrism } from "https://unpkg.com/@ruby/prism@latest/src/parsePrism.js";
18+
19+
const wasm = await WebAssembly.compileStreaming(fetch("https://unpkg.com/@ruby/prism@latest/src/prism.wasm"));
20+
const wasi = new WASI([], [], []);
21+
22+
const instance = await WebAssembly.instantiate(wasm, { wasi_snapshot_preview1: wasi.wasiImport });
23+
wasi.initialize(instance);
24+
25+
let timeout = null;
26+
const input = document.getElementById("input");
27+
const output = document.getElementById("output");
28+
29+
input.addEventListener("input", function (event) {
30+
if (timeout) clearTimeout(timeout);
31+
32+
timeout = setTimeout(function () {
33+
const result = parsePrism(instance.exports, event.target.value);
34+
output.textContent = JSON.stringify(result, null, 2);
35+
}, 250);
36+
});
37+
</script>
38+
</body>
39+
</html>

javascript/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "@ruby/prism",
3+
"version": "0.15.2",
4+
"description": "Prism Ruby parser",
5+
"type": "module",
6+
"main": "src/index.js",
7+
"types": "types/index.d.ts",
8+
"scripts": {
9+
"prepublishOnly": "npm run type",
10+
"test": "node test.js",
11+
"type": "tsc --allowJs -d --emitDeclarationOnly --outDir types src/index.js"
12+
},
13+
"author": "Shopify <ruby@shopify.com>",
14+
"license": "MIT"
15+
}

javascript/src/index.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { WASI } from "wasi";
2+
import { readFile } from "node:fs/promises";
3+
import { fileURLToPath } from "node:url";
4+
5+
import { ParseResult } from "./deserialize.js";
6+
import { parsePrism } from "./parsePrism.js";
7+
8+
/**
9+
* Load the prism wasm module and return a parse function.
10+
*
11+
* @returns {Promise<(source: string) => ParseResult>}
12+
*/
13+
export async function loadPrism() {
14+
const wasm = await WebAssembly.compile(await readFile(fileURLToPath(new URL("prism.wasm", import.meta.url))));
15+
const wasi = new WASI({ version: "preview1" });
16+
17+
const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
18+
wasi.initialize(instance);
19+
20+
return function (source) {
21+
return parsePrism(instance.exports, source);
22+
}
23+
}

0 commit comments

Comments
 (0)