Skip to content

WebR - Expreimental #93

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ This section goal is to avoid confusion around topics discussed in this document

Also commonly referred as *runtime* or *engine*, we consider an **interpreter** any "_piece of software_" able to parse, understand, and ultimately execute, a *Programming Language* through this project.

We also explicitly use that "_piece of software_" as the interpreter name it refers to. We currently bundle references to four interpreters:
We also explicitly use that "_piece of software_" as the interpreter name it refers to. We currently bundle references to the following interpreters:

* [pyodide](https://pyodide.org/en/stable/index.html) is the name of the interpreter that runs likely the most complete version of latest *Python*, enabling dozen official modules at run time, also offering a great *JS* integration in its core
* [micropython](https://micropython.org/) is the name of the interpreter that runs a small subset of the *Python* standard library and is optimized to run in constrained environments such as *Mobile* phones, or even *Desktop*, thanks to its tiny size and an extremely fast bootstrap
* [ruby-wasm-wasi](https://github.com/ruby/ruby.wasm) is the name of the (currently *experimental*) interpreter that adds *Ruby* to the list of programming languages currently supported
* [wasmoon](https://github.com/ceifa/wasmoon) is the name of the interpreter that runs *Lua* on the browser and that, among the previous two interpreters, is fully compatible with all core features
* [webr](https://docs.r-wasm.org/webr/latest/) is the name of the (currently *experimental*) interpreter that adds *R* to the list of programming languages currently supported

`<script>` tags specify which *interpreter* to use via the `type` attribute. This is typically the full name of the interpreter:

Expand All @@ -56,6 +57,10 @@ We also explicitly use that "_piece of software_" as the interpreter name it ref
<script type="wasmoon">
print(_VERSION)
</script>

<script type="webr">
print(R.version.string)
</script>
```

ℹ️ - Please note we decided on purpose to not use the generic programming language name instead of its interpreter project name to avoid being too exclusive for alternative projects that would like to target that very same Programming Language (i.e. note *pyodide* & *micropython* not using *python* indeed as interpreter name).
Expand Down Expand Up @@ -772,10 +777,14 @@ Please note that if a worker is created explicitly, there won't be any element,
| micropython | • | • | • | • | • | • |
| ruby-wasm-wasi | • | • | • | ! | | |
| wasmoon | • | • | • | ! | • | |
| webr | r | • | re | | | |

* **run** allows code to run synchronously and optionally return value
* **runAsync** allows code to run asynchronously and optionally return value
* **runEvent** allows events to be invoked and receive the `event` object
* **registerJSModule** allows `from polyscript import Xworker` or registration of arbitrary modules for *custom types*. It currently fallback to globally defined reference (the module name) whenever it's not possible to register a module (i.e. `polyscriptXWorker` in Lua or `$polyscript.XWorker` in Ruby).
* **writeFile** it's used to save *fetch* config files into virtual FS (usually the one provided by Emscripten). It is then possible to import those files as module within the evaluated code.
* **transform** allows `xworker.sync` related invokes to pass as argument internal objects without issues, simplifying as example the dance needed with *pyodide* and the `ffi.PyProxy` interface, automatically using `.toJs()` for better DX.

* issue **r**: the runtime exposes the `run` utility but this is *not synchronous*
* issue **re**: the event or its listener somehow run but it's not possible to `stopPropagation()` or do other regular *event* operations even on the main thread
4 changes: 2 additions & 2 deletions docs/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/index.js.map

Large diffs are not rendered by default.

69 changes: 69 additions & 0 deletions esm/interpreter/webr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { create } from 'gc-hook';
import { dedent } from '../utils.js';
import { fetchFiles, fetchJSModules, fetchPaths } from './_utils.js';
import { io, stdio } from './_io.js';

const type = 'webr';
const r = new WeakMap();

// REQUIRES INTEGRATION TEST
/* c8 ignore start */
const run = async (interpreter, code) => {
const { shelter, destroy, io } = r.get(interpreter);
const { output, result } = await shelter.captureR(dedent(code));
for (const { type, data } of output) io[type](data);
// this is a double proxy but it's OK as the consumer
// of the result here needs to invoke explicitly a conversion
// or trust the `(await p.toJs()).values` returns what's expected.
return create(result, destroy, { token: false });
};

export default {
type,
experimental: true,
module: (version = '0.3.2') =>
`https://cdn.jsdelivr.net/npm/webr@${version}/dist/webr.mjs`,
async engine(module, config) {
const { get } = stdio();
const interpreter = new module.WebR();
await get(interpreter.init().then(() => interpreter));
const shelter = await new interpreter.Shelter();
r.set(interpreter, {
module,
shelter,
destroy: shelter.destroy.bind(shelter),
io: io.get(interpreter),
});
if (config.files) await fetchFiles(this, interpreter, config.files);
if (config.fetch) await fetchPaths(this, interpreter, config.fetch);
if (config.js_modules) await fetchJSModules(config.js_modules);
return interpreter;
},
// Fallback to globally defined module fields (i.e. $xworker)
registerJSModule(_, name) {
console.warn(`Experimental interpreter: module ${name} is not supported (yet)`);
// TODO: as complex JS objects / modules are not allowed
// it's not clear how we can bind anything or import a module
// in a context that doesn't understand methods from JS
// https://docs.r-wasm.org/webr/latest/convert-js-to-r.html#constructing-r-objects-from-javascript-objects
},
run,
runAsync: run,
async runEvent(interpreter, code, event) {
// TODO: WebR cannot convert exoteric objects or any literal
// to an easy to reason about data/frame ... that convertion
// is reserved for the future:
// https://docs.r-wasm.org/webr/latest/convert-js-to-r.html#constructing-r-objects-from-javascript-objects
await interpreter.evalRVoid(`${code}(event)`, {
env: { event: { type: [ event.type ] } }
});
},
transform: (_, value) => {
console.log('transforming', value);
return value;
},
writeFile: () => {
// MAYBE ???
},
};
/* c8 ignore stop */
3 changes: 2 additions & 1 deletion esm/interpreters.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,6 @@ import micropython from './interpreter/micropython.js';
import pyodide from './interpreter/pyodide.js';
import ruby_wasm_wasi from './interpreter/ruby-wasm-wasi.js';
import wasmoon from './interpreter/wasmoon.js';
for (const interpreter of [micropython, pyodide, ruby_wasm_wasi, wasmoon])
import webr from './interpreter/webr.js';
for (const interpreter of [micropython, pyodide, ruby_wasm_wasi, wasmoon, webr])
register(interpreter);
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@
"size:module": "echo module is $(cat dist/index.js | brotli | wc -c) bytes once compressed",
"size:worker": "echo worker is $(cat esm/worker/xworker.js | brotli | wc -c) bytes once compressed",
"ts": "rm -rf types && tsc -p .",
"update:interpreters": "npm run version:pyodide && npm run version:wasmoon && npm run version:micropython && npm run version:ruby-wasm-wasi && node rollup/update_versions.cjs && npm run build && npm run test",
"update:interpreters": "npm run version:pyodide && npm run version:wasmoon && npm run version:webr && npm run version:micropython && npm run version:ruby-wasm-wasi && node rollup/update_versions.cjs && npm run build && npm run test",
"version:micropython": "npm view @micropython/micropython-webassembly-pyscript version>versions/micropython",
"version:pyodide": "npm view pyodide version>versions/pyodide",
"version:ruby-wasm-wasi": "git ls-remote --tags --refs --sort='v:refname' https://github.com/ruby/ruby.wasm.git | grep 'tags/[[:digit:]]\\.' | tail -n1 | sed 's/.*\\///'>versions/ruby-wasm-wasi",
"version:wasmoon": "npm view wasmoon version>versions/wasmoon"
"version:wasmoon": "npm view wasmoon version>versions/wasmoon",
"version:webr": "npm view webr version>versions/webr"
},
"keywords": [
"polyscript",
Expand Down Expand Up @@ -89,6 +90,6 @@
"to-json-callback": "^0.1.1"
},
"worker": {
"blob": "sha256-x9FnCN9Oz1l5i3eTcOPg2IrqbdU7VfskuTHKznW4/L0="
"blob": "sha256-LfRMRHNR4OuZRpfqszG9crCkgf5MX6IPDaoCdHjnzls="
}
}
2 changes: 1 addition & 1 deletion test/integration.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>polyscript integration tests</title>
</head>
<body><ul><li><strong>micropython</strong><ul><li><a href="/test/integration/interpreter/micropython/bootstrap.html">bootstrap</a></li><li><a href="/test/integration/interpreter/micropython/config-json.html">config-json</a></li><li><a href="/test/integration/interpreter/micropython/config-object.html">config-object</a></li><li><a href="/test/integration/interpreter/micropython/current-script.html">current-script</a></li><li><a href="/test/integration/interpreter/micropython/custom-hooks.html">custom-hooks</a></li><li><a href="/test/integration/interpreter/micropython/fetch.html">fetch</a></li><li><a href="/test/integration/interpreter/micropython/interpreter-local.html">interpreter-local</a></li><li><a href="/test/integration/interpreter/micropython/mip.html">mip</a></li><li><a href="/test/integration/interpreter/micropython/no-type.html">no-type</a></li><li><a href="/test/integration/interpreter/micropython/ready-done.html">ready-done</a></li><li><a href="/test/integration/interpreter/micropython/worker-attribute.html">worker-attribute</a></li><li><a href="/test/integration/interpreter/micropython/worker-bad.html">worker-bad</a></li><li><a href="/test/integration/interpreter/micropython/worker-empty-attribute.html">worker-empty-attribute</a></li><li><a href="/test/integration/interpreter/micropython/worker-error.html">worker-error</a></li><li><a href="/test/integration/interpreter/micropython/worker-lua.html">worker-lua</a></li><li><a href="/test/integration/interpreter/micropython/worker-tag.html">worker-tag</a></li><li><a href="/test/integration/interpreter/micropython/worker-window.html">worker-window</a></li><li><a href="/test/integration/interpreter/micropython/worker.html">worker</a></li></ul><li><strong>pyodide</strong><ul><li><a href="/test/integration/interpreter/pyodide/bootstrap.html">bootstrap</a></li><li><a href="/test/integration/interpreter/pyodide/button.html">button</a></li><li><a href="/test/integration/interpreter/pyodide/config-json.html">config-json</a></li><li><a href="/test/integration/interpreter/pyodide/fetch.html">fetch</a></li><li><a href="/test/integration/interpreter/pyodide/sync.html">sync</a></li><li><a href="/test/integration/interpreter/pyodide/worker-error.html">worker-error</a></li><li><a href="/test/integration/interpreter/pyodide/worker-transform.html">worker-transform</a></li><li><a href="/test/integration/interpreter/pyodide/worker.html">worker</a></li></ul><li><strong>ruby-wasm-wasi</strong><ul><li><a href="/test/integration/interpreter/ruby-wasm-wasi/bootstrap.html">bootstrap</a></li></ul><li><strong>wasmoon</strong><ul><li><a href="/test/integration/interpreter/wasmoon/bootstrap.html">bootstrap</a></li><li><a href="/test/integration/interpreter/wasmoon/worker.html">worker</a></li></ul></ul></body>
<body><ul><li><strong>micropython</strong><ul><li><a href="/test/integration/interpreter/micropython/bootstrap.html">bootstrap</a></li><li><a href="/test/integration/interpreter/micropython/config-json.html">config-json</a></li><li><a href="/test/integration/interpreter/micropython/config-object.html">config-object</a></li><li><a href="/test/integration/interpreter/micropython/current-script.html">current-script</a></li><li><a href="/test/integration/interpreter/micropython/custom-hooks.html">custom-hooks</a></li><li><a href="/test/integration/interpreter/micropython/fetch.html">fetch</a></li><li><a href="/test/integration/interpreter/micropython/interpreter-local.html">interpreter-local</a></li><li><a href="/test/integration/interpreter/micropython/mip.html">mip</a></li><li><a href="/test/integration/interpreter/micropython/no-type.html">no-type</a></li><li><a href="/test/integration/interpreter/micropython/ready-done.html">ready-done</a></li><li><a href="/test/integration/interpreter/micropython/worker-attribute.html">worker-attribute</a></li><li><a href="/test/integration/interpreter/micropython/worker-bad.html">worker-bad</a></li><li><a href="/test/integration/interpreter/micropython/worker-empty-attribute.html">worker-empty-attribute</a></li><li><a href="/test/integration/interpreter/micropython/worker-error.html">worker-error</a></li><li><a href="/test/integration/interpreter/micropython/worker-lua.html">worker-lua</a></li><li><a href="/test/integration/interpreter/micropython/worker-tag.html">worker-tag</a></li><li><a href="/test/integration/interpreter/micropython/worker-window.html">worker-window</a></li><li><a href="/test/integration/interpreter/micropython/worker.html">worker</a></li></ul><li><strong>pyodide</strong><ul><li><a href="/test/integration/interpreter/pyodide/bootstrap.html">bootstrap</a></li><li><a href="/test/integration/interpreter/pyodide/button.html">button</a></li><li><a href="/test/integration/interpreter/pyodide/config-json.html">config-json</a></li><li><a href="/test/integration/interpreter/pyodide/fetch.html">fetch</a></li><li><a href="/test/integration/interpreter/pyodide/sync.html">sync</a></li><li><a href="/test/integration/interpreter/pyodide/worker-error.html">worker-error</a></li><li><a href="/test/integration/interpreter/pyodide/worker-transform.html">worker-transform</a></li><li><a href="/test/integration/interpreter/pyodide/worker.html">worker</a></li></ul><li><strong>ruby-wasm-wasi</strong><ul><li><a href="/test/integration/interpreter/ruby-wasm-wasi/bootstrap.html">bootstrap</a></li></ul><li><strong>wasmoon</strong><ul><li><a href="/test/integration/interpreter/wasmoon/bootstrap.html">bootstrap</a></li><li><a href="/test/integration/interpreter/wasmoon/worker.html">worker</a></li></ul><li><strong>webr</strong><ul><li><a href="/test/integration/interpreter/webr/just-click.html">just-click</a></li></ul></ul></body>
</html>
12 changes: 12 additions & 0 deletions test/integration/_shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ exports.shared = {
await expect(result.trim()).toBe('OK');
},

justClick: ({ expect }, baseURL) => async ({ page }) => {
// Test that a config passed as object works out of the box.
const logs = [];
page.on('console', msg => logs.push(msg.text()));
await page.goto(`${baseURL}/just-click.html`);
await page.waitForSelector('html.ready');
await page.getByRole('button').click();
// this is ugly ... reaction time really slow on listeners (100 is safe)
await new Promise($ => setTimeout($, 100));
await expect(/\bOK\b/.test(logs.at(-1))).toBe(true);
},

worker: ({ expect }, url) => async ({ page }) => {
const logs = [];
page.on('console', msg => logs.push(msg.text()));
Expand Down
20 changes: 20 additions & 0 deletions test/integration/interpreter/webr/just-click.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module">
import { init } from '../utils.js';
init('webr');
</script>
</head>
<body>
<script type="webr">
show_OK <- function(event) {
if (event["type"] == "click")
print("OK")
}
</script>
<button webr-click="show_OK"></button>
</body>
</html>
9 changes: 9 additions & 0 deletions test/integration/webr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

const { shared } = require('./_shared.js');

module.exports = (playwright, baseURL) => {
const { test } = playwright;

test('WebR just click', shared.justClick(playwright, baseURL));
};
19 changes: 19 additions & 0 deletions test/webr.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>WebR</title>
<link rel="stylesheet" href="style.css">
<script type="module" src="/dist/index.js"></script>
</head>
<body>
<script type="webr">
print_version <- function(event) {
if (event["type"] == "click")
print(R.version.string)
}
</script>
<button webr-click="print_version">webr version</button>
</body>
</html>
1 change: 1 addition & 0 deletions versions/webr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.3.2
Loading