diff --git a/.github/workflows/biome.yml b/.github/workflows/biome.yml
new file mode 100644
index 0000000000000..88744f16ca7d6
--- /dev/null
+++ b/.github/workflows/biome.yml
@@ -0,0 +1,16 @@
+name: JavaScript code lint and formatting with Biome
+
+on: [push, pull_request]
+
+jobs:
+ eslint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Setup Biome
+ uses: biomejs/setup-biome@v2
+ with:
+ version: 1.5.3
+ - name: Run Biome
+ run: biome ci --indent-style=space --indent-width=4 tests/ ports/webassembly
diff --git a/ports/webassembly/Makefile b/ports/webassembly/Makefile
index c5ee80fe0046f..d0a8aa99243ca 100644
--- a/ports/webassembly/Makefile
+++ b/ports/webassembly/Makefile
@@ -1,57 +1,153 @@
-include ../../py/mkenv.mk
+################################################################################
+# Initial setup of Makefile environment.
+
+# Select the variant to build for:
+ifdef VARIANT_DIR
+# Custom variant path - remove trailing slash and get the final component of
+# the path as the variant name.
+VARIANT ?= $(notdir $(VARIANT_DIR:/=))
+else
+# If not given on the command line, then default to standard.
+VARIANT ?= standard
+VARIANT_DIR ?= variants/$(VARIANT)
+endif
+
+ifeq ($(wildcard $(VARIANT_DIR)/.),)
+$(error Invalid VARIANT specified: $(VARIANT_DIR))
+endif
+
+# If the build directory is not given, make it reflect the variant name.
+BUILD ?= build-$(VARIANT)
-CROSS = 0
+include ../../py/mkenv.mk
+include $(VARIANT_DIR)/mpconfigvariant.mk
+# Qstr definitions (must come before including py.mk).
QSTR_DEFS = qstrdefsport.h
+# Include py core make definitions.
include $(TOP)/py/py.mk
include $(TOP)/extmod/extmod.mk
+################################################################################
+# Project specific settings and compiler/linker flags.
+
CC = emcc
LD = emcc
+NODE ?= node
+TERSER ?= npx terser
INC += -I.
INC += -I$(TOP)
INC += -I$(BUILD)
+INC += -I$(VARIANT_DIR)
CFLAGS += -std=c99 -Wall -Werror -Wdouble-promotion -Wfloat-conversion
CFLAGS += -Os -DNDEBUG
CFLAGS += $(INC)
+EXPORTED_FUNCTIONS_EXTRA += ,\
+ _mp_js_do_exec,\
+ _mp_js_do_exec_async,\
+ _mp_js_do_import,\
+ _mp_js_register_js_module,\
+ _proxy_c_init,\
+ _proxy_c_to_js_call,\
+ _proxy_c_to_js_delete_attr,\
+ _proxy_c_to_js_dir,\
+ _proxy_c_to_js_get_array,\
+ _proxy_c_to_js_get_dict,\
+ _proxy_c_to_js_get_type,\
+ _proxy_c_to_js_has_attr,\
+ _proxy_c_to_js_lookup_attr,\
+ _proxy_c_to_js_resume,\
+ _proxy_c_to_js_store_attr,\
+ _proxy_convert_mp_to_js_obj_cside
+
+EXPORTED_RUNTIME_METHODS_EXTRA += ,\
+ PATH,\
+ PATH_FS,\
+ UTF8ToString,\
+ getValue,\
+ lengthBytesUTF8,\
+ setValue,\
+ stringToUTF8
+
+JSFLAGS += -s EXPORTED_FUNCTIONS="\
+ _free,\
+ _malloc,\
+ _mp_js_init,\
+ _mp_js_repl_init,\
+ _mp_js_repl_process_char,\
+ _mp_hal_get_interrupt_char,\
+ _mp_sched_keyboard_interrupt$(EXPORTED_FUNCTIONS_EXTRA)"
+JSFLAGS += -s EXPORTED_RUNTIME_METHODS="\
+ ccall,\
+ cwrap,\
+ FS$(EXPORTED_RUNTIME_METHODS_EXTRA)"
+JSFLAGS += --js-library library.js
+JSFLAGS += -s SUPPORT_LONGJMP=emscripten
+JSFLAGS += -s MODULARIZE -s EXPORT_NAME=_createMicroPythonModule
+
+################################################################################
+# Source files and libraries.
+
SRC_SHARED = $(addprefix shared/,\
runtime/interrupt_char.c \
runtime/stdout_helpers.c \
runtime/pyexec.c \
readline/readline.c \
+ timeutils/timeutils.c \
)
-SRC_C = \
+SRC_C += \
+ lexer_dedent.c \
main.c \
+ modjs.c \
+ modjsffi.c \
mphalport.c \
+ objjsproxy.c \
+ proxy_c.c \
+# List of sources for qstr extraction.
SRC_QSTR += $(SRC_C) $(SRC_SHARED)
+SRC_JS += \
+ api.js \
+ objpyproxy.js \
+ proxy_js.js \
+
OBJ += $(PY_O)
OBJ += $(addprefix $(BUILD)/, $(SRC_SHARED:.c=.o))
OBJ += $(addprefix $(BUILD)/, $(SRC_C:.c=.o))
-JSFLAGS += -s ASYNCIFY
-JSFLAGS += -s EXPORTED_FUNCTIONS="['_mp_js_init', '_mp_js_init_repl', '_mp_js_do_str', '_mp_js_process_char', '_mp_hal_get_interrupt_char', '_mp_sched_keyboard_interrupt']"
-JSFLAGS += -s EXPORTED_RUNTIME_METHODS="['ccall', 'cwrap', 'FS']"
-JSFLAGS += --js-library library.js
+################################################################################
+# Main targets.
+
+.PHONY: all repl min test test_min
+
+all: $(BUILD)/micropython.mjs
+
+$(BUILD)/micropython.mjs: $(OBJ) library.js $(SRC_JS)
+ $(ECHO) "LINK $@"
+ $(Q)emcc $(LDFLAGS) -o $@ $(OBJ) $(JSFLAGS)
+ $(Q)cat $(SRC_JS) >> $@
+
+$(BUILD)/micropython.min.mjs: $(BUILD)/micropython.mjs
+ $(TERSER) $< --compress --module -o $@
+
+repl: $(BUILD)/micropython.mjs
+ $(NODE) $<
-all: $(BUILD)/micropython.js
+min: $(BUILD)/micropython.min.mjs
-$(BUILD)/micropython.js: $(OBJ) library.js wrapper.js
- $(ECHO) "LINK $(BUILD)/firmware.js"
- $(Q)emcc $(LDFLAGS) -o $(BUILD)/firmware.js $(OBJ) $(JSFLAGS)
- cat wrapper.js $(BUILD)/firmware.js > $@
+test: $(BUILD)/micropython.mjs $(TOP)/tests/run-tests.py
+ cd $(TOP)/tests && MICROPY_MICROPYTHON_MJS=../ports/webassembly/$< ./run-tests.py --target webassembly
-min: $(BUILD)/micropython.js
- uglifyjs $< -c -o $(BUILD)/micropython.min.js
+test_min: $(BUILD)/micropython.min.mjs $(TOP)/tests/run-tests.py
+ cd $(TOP)/tests && MICROPY_MICROPYTHON_MJS=../ports/webassembly/$< ./run-tests.py --target webassembly
-test: $(BUILD)/micropython.js $(TOP)/tests/run-tests.py
- $(eval DIRNAME=ports/$(notdir $(CURDIR)))
- cd $(TOP)/tests && MICROPY_MICROPYTHON=../ports/webassembly/node_run.sh ./run-tests.py -j1
+################################################################################
+# Remaining make rules.
include $(TOP)/py/mkrules.mk
diff --git a/ports/webassembly/README.md b/ports/webassembly/README.md
index abd2864a56cb3..af395778a748e 100644
--- a/ports/webassembly/README.md
+++ b/ports/webassembly/README.md
@@ -6,22 +6,22 @@ MicroPython for [WebAssembly](https://webassembly.org/).
Dependencies
------------
-Building webassembly port bears the same requirements as the standard
-MicroPython ports with the addition of Emscripten (and uglify-js for the
-minified file).
+Building the webassembly port bears the same requirements as the standard
+MicroPython ports with the addition of Emscripten, and optionally terser for
+the minified file.
-The output includes `micropython.js` (a JavaScript wrapper for the
-MicroPython runtime) and `firmware.wasm` (actual MicroPython compiled to
+The output includes `micropython.mjs` (a JavaScript wrapper for the
+MicroPython runtime) and `micropython.wasm` (actual MicroPython compiled to
WASM).
Build instructions
------------------
-In order to build micropython.js, run:
+In order to build `micropython.mjs`, run:
$ make
-To generate the minified file micropython.min.js, run:
+To generate the minified file `micropython.min.mjs`, run:
$ make min
@@ -30,55 +30,90 @@ Running with Node.js
Access the repl with:
- $ node build/micropython.js
+ $ make repl
-Stack size may be modified using:
+This is the same as running:
- $ node build/micropython.js -X stack=64K
+ $ node build-standard/micropython.mjs
-Where stack size may be represented in Bytes, KiB or MiB.
+The initial MicroPython GC heap size may be modified using:
+
+ $ node build-standard/micropython.mjs -X heapsize=64k
+
+Where stack size may be represented in bytes, or have a `k` or `m` suffix.
MicroPython scripts may be executed using:
- $ node build/micropython.js hello.py
+ $ node build-standard/micropython.mjs hello.py
-Alternatively micropython.js may by accessed by other javascript programs in node
+Alternatively `micropython.mjs` may by accessed by other JavaScript programs in node
using the require command and the general API outlined below. For example:
```javascript
-var mp_js = require('./build/micropython.js');
+const mp_mjs = await import("micropython.mjs");
+const mp = await mp_mjs.loadMicroPython();
+
+mp.runPython("print('hello world')");
+```
-mp_js_init(64 * 1024);
-await mp_js_do_str("print('hello world')\n");
+Or without await notation:
+
+```javascript
+import("micropython.mjs").then((mp_mjs) => {
+ mp_mjs.loadMicroPython().then((mp) => {
+ mp.runPython("print('hello world')");
+ });
+});
```
Running with HTML
-----------------
-The prerequisite for browser operation of micropython.js is to listen to the
-`micropython-print` event, which is passed data when MicroPython code prints
-something to stdout. The following code demonstrates basic functionality:
+The following code demonstrates the simplest way to load `micropython.mjs` in a
+browser, create an interpreter context, and run some Python code:
+
+```html
+
+
+
+
+
+
+
+
+
+```
+
+The output in the above example will go to the JavaScript console. It's possible
+to instead capture the output and print it somewhere else, for example in an
+HTML element. The following example shows how to do this, and also demonstrates
+the use of top-level await and the `js` module:
```html
-
+
-
@@ -98,31 +133,41 @@ Run the test suite using:
API
---
-The following functions have been exposed to javascript.
+The following functions have been exposed to JavaScript through the interpreter
+context, created and returned by `loadMicroPython()`.
-```
-mp_js_init(stack_size)
-```
+- `PyProxy`: the type of the object that proxies Python objects.
-Initialize MicroPython with the given stack size in bytes. This must be
-called before attempting to interact with MicroPython.
+- `FS`: the Emscripten filesystem object.
-```
-await mp_js_do_str(code)
-```
+- `globals`: an object exposing the globals from the Python `__main__` module,
+ with methods `get(key)`, `set(key, value)` and `delete(key)`.
-Execute the input code. `code` must be a `string`.
+- `registerJsModule(name, module)`: register a JavaScript object as importable
+ from Python with the given name.
-```
-mp_js_init_repl()
-```
+- `pyimport`: import a Python module and return it.
-Initialize MicroPython repl. Must be called before entering characters into
-the repl.
+- `runPython(code)`: execute Python code and return the result.
-```
-await mp_js_process_char(char)
-```
+- `runPythonAsync(code)`: execute Python code and return the result, allowing for
+ top-level await expressions (this call must be await'ed on the JavaScript side).
+
+- `replInit()`: initialise the REPL.
+
+- `replProcessChar(chr)`: process an incoming character at the REPL.
+
+- `replProcessCharWithAsyncify(chr)`: process an incoming character at the REPL,
+ for use when ASYNCIFY is enabled.
+
+Proxying
+--------
+
+A Python `dict` instance is proxied such that:
+
+ for (const key in dict) {
+ print(key, dict[key]);
+ }
-Input character into MicroPython repl. `char` must be of type `number`. This
-will execute MicroPython code when necessary.
+works as expected on the JavaScript side and iterates through the keys of the
+Python `dict`.
diff --git a/ports/webassembly/api.js b/ports/webassembly/api.js
new file mode 100644
index 0000000000000..7d1832af4f2ba
--- /dev/null
+++ b/ports/webassembly/api.js
@@ -0,0 +1,269 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+// Options:
+// - heapsize: size in bytes of the MicroPython GC heap.
+// - url: location to load `micropython.mjs`.
+// - stdin: function to return input characters.
+// - stdout: function that takes one argument, and is passed lines of stdout
+// output as they are produced. By default this is handled by Emscripten
+// and in a browser goes to console, in node goes to process.stdout.write.
+// - stderr: same behaviour as stdout but for error output.
+// - linebuffer: whether to buffer line-by-line to stdout/stderr.
+export async function loadMicroPython(options) {
+ const { heapsize, url, stdin, stdout, stderr, linebuffer } = Object.assign(
+ { heapsize: 1024 * 1024, linebuffer: true },
+ options,
+ );
+ const Module = {};
+ Module.locateFile = (path, scriptDirectory) =>
+ url || scriptDirectory + path;
+ Module._textDecoder = new TextDecoder();
+ if (stdin !== undefined) {
+ Module.stdin = stdin;
+ }
+ if (stdout !== undefined) {
+ if (linebuffer) {
+ Module._stdoutBuffer = [];
+ Module.stdout = (c) => {
+ if (c === 10) {
+ stdout(
+ Module._textDecoder.decode(
+ new Uint8Array(Module._stdoutBuffer),
+ ),
+ );
+ Module._stdoutBuffer = [];
+ } else {
+ Module._stdoutBuffer.push(c);
+ }
+ };
+ } else {
+ Module.stdout = (c) => stdout(new Uint8Array([c]));
+ }
+ }
+ if (stderr !== undefined) {
+ if (linebuffer) {
+ Module._stderrBuffer = [];
+ Module.stderr = (c) => {
+ if (c === 10) {
+ stderr(
+ Module._textDecoder.decode(
+ new Uint8Array(Module._stderrBuffer),
+ ),
+ );
+ Module._stderrBuffer = [];
+ } else {
+ Module._stderrBuffer.push(c);
+ }
+ };
+ } else {
+ Module.stderr = (c) => stderr(new Uint8Array([c]));
+ }
+ }
+ const moduleLoaded = new Promise((r) => {
+ Module.postRun = r;
+ });
+ _createMicroPythonModule(Module);
+ await moduleLoaded;
+ globalThis.Module = Module;
+ proxy_js_init();
+ const pyimport = (name) => {
+ const value = Module._malloc(3 * 4);
+ Module.ccall(
+ "mp_js_do_import",
+ "null",
+ ["string", "pointer"],
+ [name, value],
+ );
+ return proxy_convert_mp_to_js_obj_jsside_with_free(value);
+ };
+ Module.ccall("mp_js_init", "null", ["number"], [heapsize]);
+ Module.ccall("proxy_c_init", "null", [], []);
+ return {
+ _module: Module,
+ PyProxy: PyProxy,
+ FS: Module.FS,
+ globals: {
+ __dict__: pyimport("__main__").__dict__,
+ get(key) {
+ return this.__dict__[key];
+ },
+ set(key, value) {
+ this.__dict__[key] = value;
+ },
+ delete(key) {
+ delete this.__dict__[key];
+ },
+ },
+ registerJsModule(name, module) {
+ const value = Module._malloc(3 * 4);
+ proxy_convert_js_to_mp_obj_jsside(module, value);
+ Module.ccall(
+ "mp_js_register_js_module",
+ "null",
+ ["string", "pointer"],
+ [name, value],
+ );
+ Module._free(value);
+ },
+ pyimport: pyimport,
+ runPython(code) {
+ const value = Module._malloc(3 * 4);
+ Module.ccall(
+ "mp_js_do_exec",
+ "number",
+ ["string", "pointer"],
+ [code, value],
+ );
+ return proxy_convert_mp_to_js_obj_jsside_with_free(value);
+ },
+ runPythonAsync(code) {
+ const value = Module._malloc(3 * 4);
+ Module.ccall(
+ "mp_js_do_exec_async",
+ "number",
+ ["string", "pointer"],
+ [code, value],
+ );
+ return proxy_convert_mp_to_js_obj_jsside_with_free(value);
+ },
+ replInit() {
+ Module.ccall("mp_js_repl_init", "null", ["null"]);
+ },
+ replProcessChar(chr) {
+ return Module.ccall(
+ "mp_js_repl_process_char",
+ "number",
+ ["number"],
+ [chr],
+ );
+ },
+ // Needed if the GC/asyncify is enabled.
+ async replProcessCharWithAsyncify(chr) {
+ return Module.ccall(
+ "mp_js_repl_process_char",
+ "number",
+ ["number"],
+ [chr],
+ { async: true },
+ );
+ },
+ };
+}
+
+globalThis.loadMicroPython = loadMicroPython;
+
+async function runCLI() {
+ const fs = await import("fs");
+ let heap_size = 128 * 1024;
+ let contents = "";
+ let repl = true;
+
+ for (let i = 2; i < process.argv.length; i++) {
+ if (process.argv[i] === "-X" && i < process.argv.length - 1) {
+ if (process.argv[i + 1].includes("heapsize=")) {
+ heap_size = parseInt(process.argv[i + 1].split("heapsize=")[1]);
+ const suffix = process.argv[i + 1].substr(-1).toLowerCase();
+ if (suffix === "k") {
+ heap_size *= 1024;
+ } else if (suffix === "m") {
+ heap_size *= 1024 * 1024;
+ }
+ ++i;
+ }
+ } else {
+ contents += fs.readFileSync(process.argv[i], "utf8");
+ repl = false;
+ }
+ }
+
+ if (process.stdin.isTTY === false) {
+ contents = fs.readFileSync(0, "utf8");
+ repl = false;
+ }
+
+ const mp = await loadMicroPython({
+ heapsize: heap_size,
+ stdout: (data) => process.stdout.write(data),
+ linebuffer: false,
+ });
+
+ if (repl) {
+ mp.replInit();
+ process.stdin.setRawMode(true);
+ process.stdin.on("data", (data) => {
+ for (let i = 0; i < data.length; i++) {
+ mp.replProcessCharWithAsyncify(data[i]).then((result) => {
+ if (result) {
+ process.exit();
+ }
+ });
+ }
+ });
+ } else {
+ try {
+ mp.runPython(contents);
+ } catch (error) {
+ if (error.name === "PythonError") {
+ if (error.type === "SystemExit") {
+ // SystemExit, this is a valid exception to successfully end a script.
+ } else {
+ // An unhandled Python exception, print in out.
+ console.error(error.message);
+ }
+ } else {
+ // A non-Python exception. Re-raise it.
+ throw error;
+ }
+ }
+ }
+}
+
+// Check if Node is running (equivalent to ENVIRONMENT_IS_NODE).
+if (
+ typeof process === "object" &&
+ typeof process.versions === "object" &&
+ typeof process.versions.node === "string"
+) {
+ // Check if this module is ron from the command line.
+ //
+ // See https://stackoverflow.com/questions/6398196/detect-if-called-through-require-or-directly-by-command-line/66309132#66309132
+ //
+ // Note:
+ // - `resolve()` is used to handle symlinks
+ // - `includes()` is used to handle cases where the file extension was omitted when passed to node
+
+ const path = await import("path");
+ const url = await import("url");
+
+ const pathToThisFile = path.resolve(url.fileURLToPath(import.meta.url));
+ const pathPassedToNode = path.resolve(process.argv[1]);
+ const isThisFileBeingRunViaCLI = pathToThisFile.includes(pathPassedToNode);
+
+ if (isThisFileBeingRunViaCLI) {
+ runCLI();
+ }
+}
diff --git a/ports/webassembly/lexer_dedent.c b/ports/webassembly/lexer_dedent.c
new file mode 100644
index 0000000000000..555caea89692b
--- /dev/null
+++ b/ports/webassembly/lexer_dedent.c
@@ -0,0 +1,105 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "lexer_dedent.h"
+
+typedef struct _mp_reader_mem_dedent_t {
+ size_t free_len; // if >0 mem is freed on close by: m_free(beg, free_len)
+ const byte *beg;
+ const byte *cur;
+ const byte *end;
+ size_t dedent_prefix;
+} mp_reader_mem_dedent_t;
+
+// Work out the amount of common whitespace among all non-empty lines.
+static size_t dedent(const byte *text, size_t len) {
+ size_t min_prefix = -1;
+ size_t cur_prefix = 0;
+ bool start_of_line = true;
+ for (const byte *t = text; t < text + len; ++t) {
+ if (*t == '\n') {
+ start_of_line = true;
+ cur_prefix = 0;
+ } else if (start_of_line) {
+ if (unichar_isspace(*t)) {
+ ++cur_prefix;
+ } else {
+ if (cur_prefix < min_prefix) {
+ min_prefix = cur_prefix;
+ if (min_prefix == 0) {
+ return min_prefix;
+ }
+ }
+ start_of_line = false;
+ }
+ }
+ }
+ return min_prefix;
+}
+
+static mp_uint_t mp_reader_mem_dedent_readbyte(void *data) {
+ mp_reader_mem_dedent_t *reader = (mp_reader_mem_dedent_t *)data;
+ if (reader->cur < reader->end) {
+ byte c = *reader->cur++;
+ if (c == '\n') {
+ for (size_t i = 0; i < reader->dedent_prefix; ++i) {
+ if (*reader->cur == '\n') {
+ break;
+ }
+ ++reader->cur;
+ }
+ }
+ return c;
+ } else {
+ return MP_READER_EOF;
+ }
+}
+
+static void mp_reader_mem_dedent_close(void *data) {
+ mp_reader_mem_dedent_t *reader = (mp_reader_mem_dedent_t *)data;
+ if (reader->free_len > 0) {
+ m_del(char, (char *)reader->beg, reader->free_len);
+ }
+ m_del_obj(mp_reader_mem_dedent_t, reader);
+}
+
+static void mp_reader_new_mem_dedent(mp_reader_t *reader, const byte *buf, size_t len, size_t free_len) {
+ mp_reader_mem_dedent_t *rm = m_new_obj(mp_reader_mem_dedent_t);
+ rm->free_len = free_len;
+ rm->beg = buf;
+ rm->cur = buf;
+ rm->end = buf + len;
+ rm->dedent_prefix = dedent(buf, len);
+ reader->data = rm;
+ reader->readbyte = mp_reader_mem_dedent_readbyte;
+ reader->close = mp_reader_mem_dedent_close;
+}
+
+mp_lexer_t *mp_lexer_new_from_str_len_dedent(qstr src_name, const char *str, size_t len, size_t free_len) {
+ mp_reader_t reader;
+ mp_reader_new_mem_dedent(&reader, (const byte *)str, len, free_len);
+ return mp_lexer_new(src_name, reader);
+}
diff --git a/ports/webassembly/lexer_dedent.h b/ports/webassembly/lexer_dedent.h
new file mode 100644
index 0000000000000..a8cc2526b4f53
--- /dev/null
+++ b/ports/webassembly/lexer_dedent.h
@@ -0,0 +1,36 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+#ifndef MICROPY_INCLUDED_WEBASSEMBLY_LEXER_DEDENT_H
+#define MICROPY_INCLUDED_WEBASSEMBLY_LEXER_DEDENT_H
+
+#include "py/lexer.h"
+
+// This function creates a new "dedenting lexer" which automatically dedents the input
+// source code if every non-empty line in that source starts with a common whitespace
+// prefix. It does this dedenting inplace as the memory is read.
+mp_lexer_t *mp_lexer_new_from_str_len_dedent(qstr src_name, const char *str, size_t len, size_t free_len);
+
+#endif // MICROPY_INCLUDED_WEBASSEMBLY_LEXER_DEDENT_H
diff --git a/ports/webassembly/library.h b/ports/webassembly/library.h
index 47982e064007a..04b408d71a25a 100644
--- a/ports/webassembly/library.h
+++ b/ports/webassembly/library.h
@@ -29,3 +29,5 @@
extern void mp_js_write(const char *str, mp_uint_t len);
extern int mp_js_ticks_ms(void);
extern void mp_js_hook(void);
+extern double mp_js_time_ms(void);
+extern uint32_t mp_js_random_u32(void);
diff --git a/ports/webassembly/library.js b/ports/webassembly/library.js
index 009b0a4dcc20c..3f6c9cb61f189 100644
--- a/ports/webassembly/library.js
+++ b/ports/webassembly/library.js
@@ -25,41 +25,51 @@
*/
mergeInto(LibraryManager.library, {
- mp_js_write: function(ptr, len) {
- const buffer = HEAPU8.subarray(ptr, ptr + len)
- if (ENVIRONMENT_IS_NODE) {
- process.stdout.write(buffer);
- } else {
- const printEvent = new CustomEvent('micropython-print', { detail: buffer });
- document.dispatchEvent(printEvent);
- }
- },
+ // This string will be emitted directly into the output file by Emscripten.
+ mp_js_ticks_ms__postset: "var MP_JS_EPOCH = Date.now()",
- mp_js_ticks_ms: function() {
- return Date.now() - MP_JS_EPOCH;
- },
+ mp_js_ticks_ms: () => Date.now() - MP_JS_EPOCH,
- mp_js_hook: function() {
+ mp_js_hook: () => {
if (ENVIRONMENT_IS_NODE) {
- var mp_interrupt_char = Module.ccall('mp_hal_get_interrupt_char', 'number', ['number'], ['null']);
- var fs = require('fs');
+ const mp_interrupt_char = Module.ccall(
+ "mp_hal_get_interrupt_char",
+ "number",
+ ["number"],
+ ["null"],
+ );
+ const fs = require("fs");
- var buf = Buffer.alloc(1);
+ const buf = Buffer.alloc(1);
try {
- var n = fs.readSync(process.stdin.fd, buf, 0, 1);
+ const n = fs.readSync(process.stdin.fd, buf, 0, 1);
if (n > 0) {
- if (buf[0] == mp_interrupt_char) {
- Module.ccall('mp_sched_keyboard_interrupt', 'null', ['null'], ['null']);
+ if (buf[0] === mp_interrupt_char) {
+ Module.ccall(
+ "mp_sched_keyboard_interrupt",
+ "null",
+ ["null"],
+ ["null"],
+ );
} else {
process.stdout.write(String.fromCharCode(buf[0]));
}
}
} catch (e) {
- if (e.code === 'EAGAIN') {
+ if (e.code === "EAGAIN") {
} else {
throw e;
}
}
}
},
+
+ mp_js_time_ms: () => Date.now(),
+
+ // Node prior to v19 did not expose "crypto" as a global, so make sure it exists.
+ mp_js_random_u32__postset:
+ "if (globalThis.crypto === undefined) { globalThis.crypto = require('crypto'); }",
+
+ mp_js_random_u32: () =>
+ globalThis.crypto.getRandomValues(new Uint32Array(1))[0],
});
diff --git a/ports/webassembly/main.c b/ports/webassembly/main.c
index 0aacf1ee095be..441c6d67e350a 100644
--- a/ports/webassembly/main.c
+++ b/ports/webassembly/main.c
@@ -40,47 +40,9 @@
#include "shared/runtime/pyexec.h"
#include "emscripten.h"
+#include "lexer_dedent.h"
#include "library.h"
-
-#if MICROPY_ENABLE_COMPILER
-int do_str(const char *src, mp_parse_input_kind_t input_kind) {
- int ret = 0;
- nlr_buf_t nlr;
- if (nlr_push(&nlr) == 0) {
- mp_lexer_t *lex = mp_lexer_new_from_str_len(MP_QSTR__lt_stdin_gt_, src, strlen(src), 0);
- qstr source_name = lex->source_name;
- mp_parse_tree_t parse_tree = mp_parse(lex, input_kind);
- mp_obj_t module_fun = mp_compile(&parse_tree, source_name, false);
- mp_call_function_0(module_fun);
- nlr_pop();
- } else {
- // uncaught exception
- if (mp_obj_is_subclass_fast(mp_obj_get_type((mp_obj_t)nlr.ret_val), &mp_type_SystemExit)) {
- mp_obj_t exit_val = mp_obj_exception_get_value(MP_OBJ_FROM_PTR(nlr.ret_val));
- if (exit_val != mp_const_none) {
- mp_int_t int_val;
- if (mp_obj_get_int_maybe(exit_val, &int_val)) {
- ret = int_val & 255;
- } else {
- ret = 1;
- }
- }
- } else {
- mp_obj_print_exception(&mp_plat_print, (mp_obj_t)nlr.ret_val);
- ret = 1;
- }
- }
- return ret;
-}
-#endif
-
-int mp_js_do_str(const char *code) {
- return do_str(code, MP_PARSE_FILE_INPUT);
-}
-
-int mp_js_process_char(int c) {
- return pyexec_event_repl_process_char(c);
-}
+#include "proxy_c.h"
void mp_js_init(int heap_size) {
#if MICROPY_ENABLE_GC
@@ -105,13 +67,91 @@ void mp_js_init(int heap_size) {
mp_vfs_mount(2, args, (mp_map_t *)&mp_const_empty_map);
MP_STATE_VM(vfs_cur) = MP_STATE_VM(vfs_mount_table);
}
+ mp_obj_list_append(mp_sys_path, MP_OBJ_NEW_QSTR(MP_QSTR__slash_lib));
#endif
}
-void mp_js_init_repl() {
+void mp_js_register_js_module(const char *name, uint32_t *value) {
+ mp_obj_t module_name = MP_OBJ_NEW_QSTR(qstr_from_str(name));
+ mp_obj_t module = proxy_convert_js_to_mp_obj_cside(value);
+ mp_map_t *mp_loaded_modules_map = &MP_STATE_VM(mp_loaded_modules_dict).map;
+ mp_map_lookup(mp_loaded_modules_map, module_name, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND)->value = module;
+}
+
+void mp_js_do_import(const char *name, uint32_t *out) {
+ nlr_buf_t nlr;
+ if (nlr_push(&nlr) == 0) {
+ mp_obj_t ret = mp_import_name(qstr_from_str(name), mp_const_none, MP_OBJ_NEW_SMALL_INT(0));
+ // Return the leaf of the import, eg for "a.b.c" return "c".
+ const char *m = name;
+ const char *n = name;
+ for (;; ++n) {
+ if (*n == '\0' || *n == '.') {
+ if (m != name) {
+ ret = mp_load_attr(ret, qstr_from_strn(m, n - m));
+ }
+ m = n + 1;
+ if (*n == '\0') {
+ break;
+ }
+ }
+ }
+ nlr_pop();
+ proxy_convert_mp_to_js_obj_cside(ret, out);
+ } else {
+ // uncaught exception
+ proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out);
+ }
+}
+
+void mp_js_do_exec(const char *src, uint32_t *out) {
+ // Collect at the top-level, where there are no root pointers from stack/registers.
+ gc_collect_start();
+ gc_collect_end();
+
+ mp_parse_input_kind_t input_kind = MP_PARSE_FILE_INPUT;
+ nlr_buf_t nlr;
+ if (nlr_push(&nlr) == 0) {
+ mp_lexer_t *lex = mp_lexer_new_from_str_len_dedent(MP_QSTR__lt_stdin_gt_, src, strlen(src), 0);
+ qstr source_name = lex->source_name;
+ mp_parse_tree_t parse_tree = mp_parse(lex, input_kind);
+ mp_obj_t module_fun = mp_compile(&parse_tree, source_name, false);
+ mp_obj_t ret = mp_call_function_0(module_fun);
+ nlr_pop();
+ proxy_convert_mp_to_js_obj_cside(ret, out);
+ } else {
+ // uncaught exception
+ proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out);
+ }
+}
+
+void mp_js_do_exec_async(const char *src, uint32_t *out) {
+ mp_compile_allow_top_level_await = true;
+ mp_js_do_exec(src, out);
+ mp_compile_allow_top_level_await = false;
+}
+
+void mp_js_repl_init(void) {
pyexec_event_repl_init();
}
+int mp_js_repl_process_char(int c) {
+ return pyexec_event_repl_process_char(c);
+}
+
+#if MICROPY_GC_SPLIT_HEAP_AUTO
+
+// The largest new region that is available to become Python heap.
+size_t gc_get_max_new_split(void) {
+ return 128 * 1024 * 1024;
+}
+
+// Don't collect anything. Instead require the heap to grow.
+void gc_collect(void) {
+}
+
+#else
+
static void gc_scan_func(void *begin, void *end) {
gc_collect_root((void **)begin, (void **)end - (void **)begin + 1);
}
@@ -123,6 +163,8 @@ void gc_collect(void) {
gc_collect_end();
}
+#endif
+
#if !MICROPY_VFS
mp_lexer_t *mp_lexer_new_from_file(qstr filename) {
mp_raise_OSError(MP_ENOENT);
diff --git a/ports/webassembly/modjs.c b/ports/webassembly/modjs.c
new file mode 100644
index 0000000000000..bed09086ab7cf
--- /dev/null
+++ b/ports/webassembly/modjs.c
@@ -0,0 +1,55 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "py/objmodule.h"
+#include "py/runtime.h"
+#include "proxy_c.h"
+
+#if MICROPY_PY_JS
+
+/******************************************************************************/
+// js module
+
+void mp_module_js_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+ mp_obj_jsproxy_t global_this;
+ global_this.ref = 0;
+ mp_obj_jsproxy_attr(MP_OBJ_FROM_PTR(&global_this), attr, dest);
+}
+
+static const mp_rom_map_elem_t mp_module_js_globals_table[] = {
+ { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_js) },
+};
+static MP_DEFINE_CONST_DICT(mp_module_js_globals, mp_module_js_globals_table);
+
+const mp_obj_module_t mp_module_js = {
+ .base = { &mp_type_module },
+ .globals = (mp_obj_dict_t *)&mp_module_js_globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_js, mp_module_js);
+MP_REGISTER_MODULE_DELEGATION(mp_module_js, mp_module_js_attr);
+
+#endif // MICROPY_PY_JS
diff --git a/ports/webassembly/modjsffi.c b/ports/webassembly/modjsffi.c
new file mode 100644
index 0000000000000..d4e61e368f009
--- /dev/null
+++ b/ports/webassembly/modjsffi.c
@@ -0,0 +1,80 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "emscripten.h"
+#include "py/objmodule.h"
+#include "py/runtime.h"
+#include "proxy_c.h"
+
+#if MICROPY_PY_JSFFI
+
+/******************************************************************************/
+// jsffi module
+
+EM_JS(void, proxy_convert_mp_to_js_then_js_to_mp_obj_jsside, (uint32_t * out), {
+ const ret = proxy_convert_mp_to_js_obj_jsside(out);
+ proxy_convert_js_to_mp_obj_jsside_force_double_proxy(ret, out);
+});
+
+static mp_obj_t mp_jsffi_create_proxy(mp_obj_t arg) {
+ uint32_t out[3];
+ proxy_convert_mp_to_js_obj_cside(arg, out);
+ proxy_convert_mp_to_js_then_js_to_mp_obj_jsside(out);
+ return proxy_convert_js_to_mp_obj_cside(out);
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_jsffi_create_proxy_obj, mp_jsffi_create_proxy);
+
+EM_JS(void, proxy_convert_mp_to_js_then_js_to_js_then_js_to_mp_obj_jsside, (uint32_t * out), {
+ const ret = proxy_convert_mp_to_js_obj_jsside(out);
+ const js_obj = PyProxy.toJs(ret);
+ proxy_convert_js_to_mp_obj_jsside(js_obj, out);
+});
+
+static mp_obj_t mp_jsffi_to_js(mp_obj_t arg) {
+ uint32_t out[3];
+ proxy_convert_mp_to_js_obj_cside(arg, out);
+ proxy_convert_mp_to_js_then_js_to_js_then_js_to_mp_obj_jsside(out);
+ return proxy_convert_js_to_mp_obj_cside(out);
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(mp_jsffi_to_js_obj, mp_jsffi_to_js);
+
+static const mp_rom_map_elem_t mp_module_jsffi_globals_table[] = {
+ { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_jsffi) },
+
+ { MP_ROM_QSTR(MP_QSTR_JsProxy), MP_ROM_PTR(&mp_type_jsproxy) },
+ { MP_ROM_QSTR(MP_QSTR_create_proxy), MP_ROM_PTR(&mp_jsffi_create_proxy_obj) },
+ { MP_ROM_QSTR(MP_QSTR_to_js), MP_ROM_PTR(&mp_jsffi_to_js_obj) },
+};
+static MP_DEFINE_CONST_DICT(mp_module_jsffi_globals, mp_module_jsffi_globals_table);
+
+const mp_obj_module_t mp_module_jsffi = {
+ .base = { &mp_type_module },
+ .globals = (mp_obj_dict_t *)&mp_module_jsffi_globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_jsffi, mp_module_jsffi);
+
+#endif // MICROPY_PY_JSFFI
diff --git a/ports/webassembly/modtime.c b/ports/webassembly/modtime.c
new file mode 100644
index 0000000000000..1b1e63d4ddf3a
--- /dev/null
+++ b/ports/webassembly/modtime.c
@@ -0,0 +1,51 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "py/obj.h"
+#include "shared/timeutils/timeutils.h"
+#include "library.h"
+
+// Return the localtime as an 8-tuple.
+static mp_obj_t mp_time_localtime_get(void) {
+ timeutils_struct_time_t tm;
+ timeutils_seconds_since_epoch_to_struct_time(mp_hal_time_ms() / 1000, &tm);
+ mp_obj_t tuple[8] = {
+ mp_obj_new_int(tm.tm_year),
+ mp_obj_new_int(tm.tm_mon),
+ mp_obj_new_int(tm.tm_mday),
+ mp_obj_new_int(tm.tm_hour),
+ mp_obj_new_int(tm.tm_min),
+ mp_obj_new_int(tm.tm_sec),
+ mp_obj_new_int(tm.tm_wday),
+ mp_obj_new_int(tm.tm_yday),
+ };
+ return mp_obj_new_tuple(8, tuple);
+}
+
+// Returns the number of seconds, as a float, since the Epoch.
+static mp_obj_t mp_time_time_get(void) {
+ return mp_obj_new_float((mp_float_t)mp_hal_time_ms() / 1000);
+}
diff --git a/ports/webassembly/mpconfigport.h b/ports/webassembly/mpconfigport.h
index e0d1af0cce2bd..ae5dfa6fa50b8 100644
--- a/ports/webassembly/mpconfigport.h
+++ b/ports/webassembly/mpconfigport.h
@@ -25,18 +25,21 @@
* THE SOFTWARE.
*/
+// Options to control how MicroPython is built for this port, overriding
+// defaults in py/mpconfig.h.
+
#include
+#include // for malloc, for MICROPY_GC_SPLIT_HEAP_AUTO
-// options to control how MicroPython is built
+// Variant-specific definitions.
+#include "mpconfigvariant.h"
+#ifndef MICROPY_CONFIG_ROM_LEVEL
#define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_EXTRA_FEATURES)
-
-// You can disable the built-in MicroPython compiler by setting the following
-// config option to 0. If you do this then you won't get a REPL prompt, but you
-// will still be able to execute pre-compiled scripts, compiled with mpy-cross.
-#define MICROPY_ENABLE_COMPILER (1)
+#endif
#define MICROPY_ALLOC_PATH_MAX (256)
+#define MICROPY_COMP_ALLOW_TOP_LEVEL_AWAIT (1)
#define MICROPY_READER_VFS (MICROPY_VFS)
#define MICROPY_ENABLE_GC (1)
#define MICROPY_ENABLE_PYSTACK (1)
@@ -46,15 +49,29 @@
#define MICROPY_LONGINT_IMPL (MICROPY_LONGINT_IMPL_MPZ)
#define MICROPY_ENABLE_DOC_STRING (1)
#define MICROPY_WARNINGS (1)
+#define MICROPY_ERROR_PRINTER (&mp_stderr_print)
#define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_DOUBLE)
#define MICROPY_USE_INTERNAL_ERRNO (1)
#define MICROPY_USE_INTERNAL_PRINTF (0)
+
+#define MICROPY_EPOCH_IS_1970 (1)
+#define MICROPY_PY_RANDOM_SEED_INIT_FUNC (mp_js_random_u32())
+#define MICROPY_PY_TIME_GMTIME_LOCALTIME_MKTIME (1)
+#define MICROPY_PY_TIME_TIME_TIME_NS (1)
+#define MICROPY_PY_TIME_INCLUDEFILE "ports/webassembly/modtime.c"
#ifndef MICROPY_VFS
#define MICROPY_VFS (1)
#endif
#define MICROPY_VFS_POSIX (MICROPY_VFS)
#define MICROPY_PY_SYS_PLATFORM "webassembly"
-#define MICROPY_PY_SYS_STDFILES (0)
+
+#ifndef MICROPY_PY_JS
+#define MICROPY_PY_JS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#endif
+
+#ifndef MICROPY_PY_JSFFI
+#define MICROPY_PY_JSFFI (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
+#endif
#define MICROPY_EVENT_POLL_HOOK \
do { \
@@ -62,6 +79,13 @@
mp_handle_pending(true); \
} while (0);
+// Whether the VM will periodically call mp_js_hook(), which checks for
+// interrupt characters on stdin (or equivalent input).
+#ifndef MICROPY_VARIANT_ENABLE_JS_HOOK
+#define MICROPY_VARIANT_ENABLE_JS_HOOK (0)
+#endif
+
+#if MICROPY_VARIANT_ENABLE_JS_HOOK
#define MICROPY_VM_HOOK_COUNT (10)
#define MICROPY_VM_HOOK_INIT static uint vm_hook_divisor = MICROPY_VM_HOOK_COUNT;
#define MICROPY_VM_HOOK_POLL if (--vm_hook_divisor == 0) { \
@@ -71,6 +95,7 @@
}
#define MICROPY_VM_HOOK_LOOP MICROPY_VM_HOOK_POLL
#define MICROPY_VM_HOOK_RETURN MICROPY_VM_HOOK_POLL
+#endif
// type definitions for the specific machine
@@ -95,3 +120,7 @@ typedef long mp_off_t;
// _GNU_SOURCE must be defined to get definitions of DT_xxx symbols from dirent.h.
#define _GNU_SOURCE
#endif
+
+extern const struct _mp_print_t mp_stderr_print;
+
+uint32_t mp_js_random_u32(void);
diff --git a/ports/webassembly/mphalport.c b/ports/webassembly/mphalport.c
index f91a509013a3c..9ab47762e3e97 100644
--- a/ports/webassembly/mphalport.c
+++ b/ports/webassembly/mphalport.c
@@ -24,12 +24,19 @@
* THE SOFTWARE.
*/
+#include
#include "library.h"
#include "mphalport.h"
+static void stderr_print_strn(void *env, const char *str, size_t len) {
+ (void)env;
+ write(2, str, len);
+}
+
+const mp_print_t mp_stderr_print = {NULL, stderr_print_strn};
+
mp_uint_t mp_hal_stdout_tx_strn(const char *str, size_t len) {
- mp_js_write(str, len);
- return len;
+ return write(1, str, len);
}
void mp_hal_delay_ms(mp_uint_t ms) {
@@ -56,6 +63,15 @@ mp_uint_t mp_hal_ticks_cpu(void) {
return 0;
}
+uint64_t mp_hal_time_ms(void) {
+ double mm = mp_js_time_ms();
+ return (uint64_t)mm;
+}
+
+uint64_t mp_hal_time_ns(void) {
+ return mp_hal_time_ms() * 1000000ULL;
+}
+
extern int mp_interrupt_char;
int mp_hal_get_interrupt_char(void) {
diff --git a/ports/webassembly/mphalport.h b/ports/webassembly/mphalport.h
index 1b3179698644b..a90de8ec5fb0e 100644
--- a/ports/webassembly/mphalport.h
+++ b/ports/webassembly/mphalport.h
@@ -35,6 +35,7 @@ void mp_hal_delay_us(mp_uint_t us);
mp_uint_t mp_hal_ticks_ms(void);
mp_uint_t mp_hal_ticks_us(void);
mp_uint_t mp_hal_ticks_cpu(void);
+uint64_t mp_hal_time_ms(void);
int mp_hal_get_interrupt_char(void);
diff --git a/ports/webassembly/objjsproxy.c b/ports/webassembly/objjsproxy.c
new file mode 100644
index 0000000000000..5e2aeb6a36e4b
--- /dev/null
+++ b/ports/webassembly/objjsproxy.c
@@ -0,0 +1,487 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include
+#include
+
+#include "emscripten.h"
+#include "py/objmodule.h"
+#include "py/runtime.h"
+#include "proxy_c.h"
+
+EM_JS(bool, has_attr, (int jsref, const char *str), {
+ const base = proxy_js_ref[jsref];
+ const attr = UTF8ToString(str);
+ if (attr in base) {
+ return true;
+ } else {
+ return false;
+ }
+});
+
+// *FORMAT-OFF*
+EM_JS(bool, lookup_attr, (int jsref, const char *str, uint32_t * out), {
+ const base = proxy_js_ref[jsref];
+ const attr = UTF8ToString(str);
+ if (attr in base) {
+ let value = base[attr];
+ if (typeof value == "function") {
+ if (base !== globalThis) {
+ value = value.bind(base);
+ }
+ }
+ proxy_convert_js_to_mp_obj_jsside(value, out);
+ return true;
+ } else {
+ return false;
+ }
+});
+// *FORMAT-ON*
+
+EM_JS(void, store_attr, (int jsref, const char *attr_ptr, uint32_t * value_ref), {
+ const attr = UTF8ToString(attr_ptr);
+ const value = proxy_convert_mp_to_js_obj_jsside(value_ref);
+ proxy_js_ref[jsref][attr] = value;
+});
+
+EM_JS(void, call0, (int f_ref, uint32_t * out), {
+ // Because of JavaScript "this" semantics, we must extract the target function
+ // to a variable before calling it, so "this" is bound to the correct value.
+ //
+ // In detail:
+ // In JavaScript, proxy_js_ref[f_ref] acts like a function call
+ // proxy_js_ref.at(f_ref), and "this" will be bound to proxy_js_ref if
+ // there is a chain of calls, such as proxy_js_ref.at(f_ref)().
+ // But proxy_js_ref is not "this" in the context of the call, so we
+ // must extract the function to an independent variable and then call
+ // that variable, so that "this" is correct (it will be "undefined").
+
+ const f = proxy_js_ref[f_ref];
+ const ret = f();
+ proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(int, call1, (int f_ref, uint32_t * a0, uint32_t * out), {
+ const a0_js = proxy_convert_mp_to_js_obj_jsside(a0);
+ const f = proxy_js_ref[f_ref];
+ const ret = f(a0_js);
+ proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(int, call2, (int f_ref, uint32_t * a0, uint32_t * a1, uint32_t * out), {
+ const a0_js = proxy_convert_mp_to_js_obj_jsside(a0);
+ const a1_js = proxy_convert_mp_to_js_obj_jsside(a1);
+ const f = proxy_js_ref[f_ref];
+ const ret = f(a0_js, a1_js);
+ proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(int, calln, (int f_ref, uint32_t n_args, uint32_t * value, uint32_t * out), {
+ const f = proxy_js_ref[f_ref];
+ const a = [];
+ for (let i = 0; i < n_args; ++i) {
+ const v = proxy_convert_mp_to_js_obj_jsside(value + i * 3 * 4);
+ a.push(v);
+ }
+ const ret = f(... a);
+ proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(void, call0_kwarg, (int f_ref, uint32_t n_kw, uint32_t * key, uint32_t * value, uint32_t * out), {
+ const f = proxy_js_ref[f_ref];
+ const a = {};
+ for (let i = 0; i < n_kw; ++i) {
+ const k = UTF8ToString(getValue(key + i * 4, "i32"));
+ const v = proxy_convert_mp_to_js_obj_jsside(value + i * 3 * 4);
+ a[k] = v;
+ }
+ const ret = f(a);
+ proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(void, call1_kwarg, (int f_ref, uint32_t * arg0, uint32_t n_kw, uint32_t * key, uint32_t * value, uint32_t * out), {
+ const f = proxy_js_ref[f_ref];
+ const a0 = proxy_convert_mp_to_js_obj_jsside(arg0);
+ const a = {};
+ for (let i = 0; i < n_kw; ++i) {
+ const k = UTF8ToString(getValue(key + i * 4, "i32"));
+ const v = proxy_convert_mp_to_js_obj_jsside(value + i * 3 * 4);
+ a[k] = v;
+ }
+ const ret = f(a0, a);
+ proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(void, js_reflect_construct, (int f_ref, uint32_t n_args, uint32_t * args, uint32_t * out), {
+ const f = proxy_js_ref[f_ref];
+ const as = [];
+ for (let i = 0; i < n_args; ++i) {
+ as.push(proxy_convert_mp_to_js_obj_jsside(args + i * 4));
+ }
+ const ret = Reflect.construct(f, as);
+ proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(int, js_get_len, (int f_ref), {
+ return proxy_js_ref[f_ref].length;
+});
+
+EM_JS(void, js_subscr_int, (int f_ref, int idx, uint32_t * out), {
+ const f = proxy_js_ref[f_ref];
+ const ret = f[idx];
+ proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(void, js_subscr_load, (int f_ref, uint32_t * index_ref, uint32_t * out), {
+ const target = proxy_js_ref[f_ref];
+ const index = python_index_semantics(target, proxy_convert_mp_to_js_obj_jsside(index_ref));
+ const ret = target[index];
+ proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+
+EM_JS(void, js_subscr_store, (int f_ref, uint32_t * idx, uint32_t * value), {
+ const f = proxy_js_ref[f_ref];
+ f[proxy_convert_mp_to_js_obj_jsside(idx)] = proxy_convert_mp_to_js_obj_jsside(value);
+});
+
+static void jsproxy_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
+ mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
+ mp_printf(print, "", self->ref);
+}
+
+static mp_obj_t jsproxy_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
+ mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
+
+ if (n_kw == 0) {
+ mp_arg_check_num(n_args, n_kw, 0, MP_OBJ_FUN_ARGS_MAX, false);
+ } else {
+ mp_arg_check_num(n_args, n_kw, 0, 1, true);
+ uint32_t key[n_kw];
+ uint32_t value[PVN * n_kw];
+ for (int i = 0; i < n_kw; ++i) {
+ key[i] = (uintptr_t)mp_obj_str_get_str(args[n_args + i * 2]);
+ proxy_convert_mp_to_js_obj_cside(args[n_args + i * 2 + 1], &value[i * PVN]);
+ }
+ uint32_t out[3];
+ if (n_args == 0) {
+ call0_kwarg(self->ref, n_kw, key, value, out);
+ } else {
+ // n_args == 1
+ uint32_t arg0[PVN];
+ proxy_convert_mp_to_js_obj_cside(args[0], arg0);
+ call1_kwarg(self->ref, arg0, n_kw, key, value, out);
+ }
+ return proxy_convert_js_to_mp_obj_cside(out);
+ }
+
+ if (n_args == 0) {
+ uint32_t out[3];
+ call0(self->ref, out);
+ return proxy_convert_js_to_mp_obj_cside(out);
+ } else if (n_args == 1) {
+ uint32_t arg0[PVN];
+ uint32_t out[PVN];
+ proxy_convert_mp_to_js_obj_cside(args[0], arg0);
+ call1(self->ref, arg0, out);
+ return proxy_convert_js_to_mp_obj_cside(out);
+ } else if (n_args == 2) {
+ uint32_t arg0[PVN];
+ proxy_convert_mp_to_js_obj_cside(args[0], arg0);
+ uint32_t arg1[PVN];
+ proxy_convert_mp_to_js_obj_cside(args[1], arg1);
+ uint32_t out[3];
+ call2(self->ref, arg0, arg1, out);
+ return proxy_convert_js_to_mp_obj_cside(out);
+ } else {
+ uint32_t value[PVN * n_args];
+ for (int i = 0; i < n_args; ++i) {
+ proxy_convert_mp_to_js_obj_cside(args[i], &value[i * PVN]);
+ }
+ uint32_t out[3];
+ calln(self->ref, n_args, value, out);
+ return proxy_convert_js_to_mp_obj_cside(out);
+ }
+}
+
+static mp_obj_t jsproxy_reflect_construct(size_t n_args, const mp_obj_t *args) {
+ int arg0 = mp_obj_jsproxy_get_ref(args[0]);
+ n_args -= 1;
+ args += 1;
+ uint32_t args_conv[n_args];
+ for (unsigned int i = 0; i < n_args; ++i) {
+ proxy_convert_mp_to_js_obj_cside(args[i], &args_conv[i * PVN]);
+ }
+ uint32_t out[3];
+ js_reflect_construct(arg0, n_args, args_conv, out);
+ return proxy_convert_js_to_mp_obj_cside(out);
+}
+static MP_DEFINE_CONST_FUN_OBJ_VAR(jsproxy_reflect_construct_obj, 1, jsproxy_reflect_construct);
+
+static mp_obj_t jsproxy_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
+ mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
+ if (value == MP_OBJ_SENTINEL) {
+ // Load subscript.
+ uint32_t idx[PVN], out[PVN];
+ proxy_convert_mp_to_js_obj_cside(index, idx);
+ js_subscr_load(self->ref, idx, out);
+ return proxy_convert_js_to_mp_obj_cside(out);
+ } else if (value == MP_OBJ_NULL) {
+ // Delete subscript.
+ return MP_OBJ_NULL; // not supported
+ } else {
+ // Store subscript.
+ uint32_t idx[PVN], val[PVN];
+ proxy_convert_mp_to_js_obj_cside(index, idx);
+ proxy_convert_mp_to_js_obj_cside(value, val);
+ js_subscr_store(self->ref, idx, val);
+ return mp_const_none;
+ }
+}
+
+void mp_obj_jsproxy_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+ mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
+ if (dest[0] == MP_OBJ_NULL) {
+ // Load attribute.
+ uint32_t out[PVN];
+ if (lookup_attr(self->ref, qstr_str(attr), out)) {
+ dest[0] = proxy_convert_js_to_mp_obj_cside(out);
+ } else if (attr == MP_QSTR_new) {
+ // Special case to handle construction of JS objects.
+ // JS objects don't have a ".new" attribute, doing "Obj.new" is a Pyodide idiom for "new Obj".
+ // It translates to the JavaScript "Reflect.construct(Obj, Array(...args))".
+ dest[0] = MP_OBJ_FROM_PTR(&jsproxy_reflect_construct_obj);
+ dest[1] = self_in;
+ }
+ } else if (dest[1] == MP_OBJ_NULL) {
+ // Delete attribute.
+ } else {
+ // Store attribute.
+ uint32_t value[PVN];
+ proxy_convert_mp_to_js_obj_cside(dest[1], value);
+ store_attr(self->ref, qstr_str(attr), value);
+ dest[0] = MP_OBJ_NULL;
+ }
+}
+
+/******************************************************************************/
+// jsproxy iterator
+
+typedef struct _jsproxy_it_t {
+ mp_obj_base_t base;
+ mp_fun_1_t iternext;
+ int ref;
+ uint16_t cur;
+ uint16_t len;
+} jsproxy_it_t;
+
+static mp_obj_t jsproxy_it_iternext(mp_obj_t self_in) {
+ jsproxy_it_t *self = MP_OBJ_TO_PTR(self_in);
+ if (self->cur < self->len) {
+ uint32_t out[3];
+ js_subscr_int(self->ref, self->cur, out);
+ self->cur += 1;
+ return proxy_convert_js_to_mp_obj_cside(out);
+ } else {
+ return MP_OBJ_STOP_ITERATION;
+ }
+}
+
+static mp_obj_t jsproxy_new_it(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
+ assert(sizeof(jsproxy_it_t) <= sizeof(mp_obj_iter_buf_t));
+ mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
+ jsproxy_it_t *o = (jsproxy_it_t *)iter_buf;
+ o->base.type = &mp_type_polymorph_iter;
+ o->iternext = jsproxy_it_iternext;
+ o->ref = self->ref;
+ o->cur = 0;
+ o->len = js_get_len(self->ref);
+ return MP_OBJ_FROM_PTR(o);
+}
+
+/******************************************************************************/
+// jsproxy generator
+
+enum {
+ JSOBJ_GEN_STATE_WAITING,
+ JSOBJ_GEN_STATE_COMPLETED,
+ JSOBJ_GEN_STATE_EXHAUSTED,
+};
+
+typedef struct _jsproxy_gen_t {
+ mp_obj_base_t base;
+ mp_obj_t thenable;
+ int state;
+} jsproxy_gen_t;
+
+mp_vm_return_kind_t jsproxy_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_obj_t throw_value, mp_obj_t *ret_val) {
+ jsproxy_gen_t *self = MP_OBJ_TO_PTR(self_in);
+ switch (self->state) {
+ case JSOBJ_GEN_STATE_WAITING:
+ self->state = JSOBJ_GEN_STATE_COMPLETED;
+ *ret_val = self->thenable;
+ return MP_VM_RETURN_YIELD;
+
+ case JSOBJ_GEN_STATE_COMPLETED:
+ self->state = JSOBJ_GEN_STATE_EXHAUSTED;
+ *ret_val = send_value;
+ return MP_VM_RETURN_NORMAL;
+
+ case JSOBJ_GEN_STATE_EXHAUSTED:
+ default:
+ // Trying to resume an already stopped generator.
+ // This is an optimised "raise StopIteration(None)".
+ *ret_val = mp_const_none;
+ return MP_VM_RETURN_NORMAL;
+ }
+}
+
+static mp_obj_t jsproxy_gen_resume_and_raise(mp_obj_t self_in, mp_obj_t send_value, mp_obj_t throw_value, bool raise_stop_iteration) {
+ mp_obj_t ret;
+ switch (jsproxy_gen_resume(self_in, send_value, throw_value, &ret)) {
+ case MP_VM_RETURN_NORMAL:
+ default:
+ // A normal return is a StopIteration, either raise it or return
+ // MP_OBJ_STOP_ITERATION as an optimisation.
+ if (ret == mp_const_none) {
+ ret = MP_OBJ_NULL;
+ }
+ if (raise_stop_iteration) {
+ mp_raise_StopIteration(ret);
+ } else {
+ return mp_make_stop_iteration(ret);
+ }
+
+ case MP_VM_RETURN_YIELD:
+ return ret;
+
+ case MP_VM_RETURN_EXCEPTION:
+ nlr_raise(ret);
+ }
+}
+
+static mp_obj_t jsproxy_gen_instance_iternext(mp_obj_t self_in) {
+ return jsproxy_gen_resume_and_raise(self_in, mp_const_none, MP_OBJ_NULL, false);
+}
+
+static mp_obj_t jsproxy_gen_instance_send(mp_obj_t self_in, mp_obj_t send_value) {
+ return jsproxy_gen_resume_and_raise(self_in, send_value, MP_OBJ_NULL, true);
+}
+static MP_DEFINE_CONST_FUN_OBJ_2(jsproxy_gen_instance_send_obj, jsproxy_gen_instance_send);
+
+static mp_obj_t jsproxy_gen_instance_throw(size_t n_args, const mp_obj_t *args) {
+ // The signature of this function is: throw(type[, value[, traceback]])
+ // CPython will pass all given arguments through the call chain and process them
+ // at the point they are used (native generators will handle them differently to
+ // user-defined generators with a throw() method). To save passing multiple
+ // values, MicroPython instead does partial processing here to reduce it down to
+ // one argument and passes that through:
+ // - if only args[1] is given, or args[2] is given but is None, args[1] is
+ // passed through (in the standard case it is an exception class or instance)
+ // - if args[2] is given and not None it is passed through (in the standard
+ // case it would be an exception instance and args[1] its corresponding class)
+ // - args[3] is always ignored
+
+ mp_obj_t exc = args[1];
+ if (n_args > 2 && args[2] != mp_const_none) {
+ exc = args[2];
+ }
+
+ return jsproxy_gen_resume_and_raise(args[0], mp_const_none, exc, true);
+}
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(jsproxy_gen_instance_throw_obj, 2, 4, jsproxy_gen_instance_throw);
+
+static mp_obj_t jsproxy_gen_instance_close(mp_obj_t self_in) {
+ mp_obj_t ret;
+ switch (jsproxy_gen_resume(self_in, mp_const_none, MP_OBJ_FROM_PTR(&mp_const_GeneratorExit_obj), &ret)) {
+ case MP_VM_RETURN_YIELD:
+ mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("generator ignored GeneratorExit"));
+
+ // Swallow GeneratorExit (== successful close), and re-raise any other
+ case MP_VM_RETURN_EXCEPTION:
+ // ret should always be an instance of an exception class
+ if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(mp_obj_get_type(ret)), MP_OBJ_FROM_PTR(&mp_type_GeneratorExit))) {
+ return mp_const_none;
+ }
+ nlr_raise(ret);
+
+ default:
+ // The only choice left is MP_VM_RETURN_NORMAL which is successful close
+ return mp_const_none;
+ }
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(jsproxy_gen_instance_close_obj, jsproxy_gen_instance_close);
+
+static const mp_rom_map_elem_t jsproxy_gen_instance_locals_dict_table[] = {
+ { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&jsproxy_gen_instance_close_obj) },
+ { MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&jsproxy_gen_instance_send_obj) },
+ { MP_ROM_QSTR(MP_QSTR_throw), MP_ROM_PTR(&jsproxy_gen_instance_throw_obj) },
+};
+static MP_DEFINE_CONST_DICT(jsproxy_gen_instance_locals_dict, jsproxy_gen_instance_locals_dict_table);
+
+MP_DEFINE_CONST_OBJ_TYPE(
+ mp_type_jsproxy_gen,
+ MP_QSTR_generator,
+ MP_TYPE_FLAG_ITER_IS_ITERNEXT,
+ iter, jsproxy_gen_instance_iternext,
+ locals_dict, &jsproxy_gen_instance_locals_dict
+ );
+
+static mp_obj_t jsproxy_new_gen(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
+ assert(sizeof(jsproxy_gen_t) <= sizeof(mp_obj_iter_buf_t));
+ jsproxy_gen_t *o = (jsproxy_gen_t *)iter_buf;
+ o->base.type = &mp_type_jsproxy_gen;
+ o->thenable = self_in;
+ o->state = JSOBJ_GEN_STATE_WAITING;
+ return MP_OBJ_FROM_PTR(o);
+}
+
+/******************************************************************************/
+
+static mp_obj_t jsproxy_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
+ mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
+ if (has_attr(self->ref, "then")) {
+ return jsproxy_new_gen(self_in, iter_buf);
+ } else {
+ return jsproxy_new_it(self_in, iter_buf);
+ }
+}
+
+MP_DEFINE_CONST_OBJ_TYPE(
+ mp_type_jsproxy,
+ MP_QSTR_JsProxy,
+ MP_TYPE_FLAG_ITER_IS_GETITER,
+ print, jsproxy_print,
+ call, jsproxy_call,
+ attr, mp_obj_jsproxy_attr,
+ subscr, jsproxy_subscr,
+ iter, jsproxy_getiter
+ );
+
+mp_obj_t mp_obj_new_jsproxy(int ref) {
+ mp_obj_jsproxy_t *o = mp_obj_malloc(mp_obj_jsproxy_t, &mp_type_jsproxy);
+ o->ref = ref;
+ return MP_OBJ_FROM_PTR(o);
+}
diff --git a/ports/webassembly/objpyproxy.js b/ports/webassembly/objpyproxy.js
new file mode 100644
index 0000000000000..9ba06283ea2b0
--- /dev/null
+++ b/ports/webassembly/objpyproxy.js
@@ -0,0 +1,214 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+class PyProxy {
+ constructor(ref) {
+ this._ref = ref;
+ }
+
+ // Convert js_obj -- which is possibly a PyProxy -- to a JavaScript object.
+ static toJs(js_obj) {
+ if (!(js_obj instanceof PyProxy)) {
+ return js_obj;
+ }
+
+ const type = Module.ccall(
+ "proxy_c_to_js_get_type",
+ "number",
+ ["number"],
+ [js_obj._ref],
+ );
+
+ if (type === 1 || type === 2) {
+ // List or tuple.
+ const array_ref = Module._malloc(2 * 4);
+ const item = Module._malloc(3 * 4);
+ Module.ccall(
+ "proxy_c_to_js_get_array",
+ "null",
+ ["number", "pointer"],
+ [js_obj._ref, array_ref],
+ );
+ const len = Module.getValue(array_ref, "i32");
+ const items_ptr = Module.getValue(array_ref + 4, "i32");
+ const js_array = [];
+ for (let i = 0; i < len; ++i) {
+ Module.ccall(
+ "proxy_convert_mp_to_js_obj_cside",
+ "null",
+ ["pointer", "pointer"],
+ [Module.getValue(items_ptr + i * 4, "i32"), item],
+ );
+ const js_item = proxy_convert_mp_to_js_obj_jsside(item);
+ js_array.push(PyProxy.toJs(js_item));
+ }
+ Module._free(array_ref);
+ Module._free(item);
+ return js_array;
+ }
+
+ if (type === 3) {
+ // Dict.
+ const map_ref = Module._malloc(2 * 4);
+ const item = Module._malloc(3 * 4);
+ Module.ccall(
+ "proxy_c_to_js_get_dict",
+ "null",
+ ["number", "pointer"],
+ [js_obj._ref, map_ref],
+ );
+ const alloc = Module.getValue(map_ref, "i32");
+ const table_ptr = Module.getValue(map_ref + 4, "i32");
+ const js_dict = {};
+ for (let i = 0; i < alloc; ++i) {
+ const mp_key = Module.getValue(table_ptr + i * 8, "i32");
+ if (mp_key > 8) {
+ // Convert key to JS object.
+ Module.ccall(
+ "proxy_convert_mp_to_js_obj_cside",
+ "null",
+ ["pointer", "pointer"],
+ [mp_key, item],
+ );
+ const js_key = proxy_convert_mp_to_js_obj_jsside(item);
+
+ // Convert value to JS object.
+ const mp_value = Module.getValue(
+ table_ptr + i * 8 + 4,
+ "i32",
+ );
+ Module.ccall(
+ "proxy_convert_mp_to_js_obj_cside",
+ "null",
+ ["pointer", "pointer"],
+ [mp_value, item],
+ );
+ const js_value = proxy_convert_mp_to_js_obj_jsside(item);
+
+ // Populate JS dict.
+ js_dict[js_key] = PyProxy.toJs(js_value);
+ }
+ }
+ Module._free(map_ref);
+ Module._free(item);
+ return js_dict;
+ }
+
+ // Cannot convert to JS, leave as a PyProxy.
+ return js_obj;
+ }
+}
+
+// This handler's goal is to allow minimal introspection
+// of Python references from the JS world/utilities.
+const py_proxy_handler = {
+ isExtensible() {
+ return true;
+ },
+ ownKeys(target) {
+ const value = Module._malloc(3 * 4);
+ Module.ccall(
+ "proxy_c_to_js_dir",
+ "null",
+ ["number", "pointer"],
+ [target._ref, value],
+ );
+ const dir = proxy_convert_mp_to_js_obj_jsside_with_free(value);
+ return PyProxy.toJs(dir).filter((attr) => !attr.startsWith("__"));
+ },
+ getOwnPropertyDescriptor(target, prop) {
+ return {
+ value: target[prop],
+ enumerable: true,
+ writable: true,
+ configurable: true,
+ };
+ },
+ has(target, prop) {
+ return Module.ccall(
+ "proxy_c_to_js_has_attr",
+ "number",
+ ["number", "string"],
+ [target._ref, prop],
+ );
+ },
+ get(target, prop) {
+ if (prop === "_ref") {
+ return target._ref;
+ }
+ if (prop === "then") {
+ return null;
+ }
+ const value = Module._malloc(3 * 4);
+ Module.ccall(
+ "proxy_c_to_js_lookup_attr",
+ "number",
+ ["number", "string", "pointer"],
+ [target._ref, prop, value],
+ );
+ return proxy_convert_mp_to_js_obj_jsside_with_free(value);
+ },
+ set(target, prop, value) {
+ const value_conv = Module._malloc(3 * 4);
+ proxy_convert_js_to_mp_obj_jsside(value, value_conv);
+ const ret = Module.ccall(
+ "proxy_c_to_js_store_attr",
+ "number",
+ ["number", "string", "number"],
+ [target._ref, prop, value_conv],
+ );
+ Module._free(value_conv);
+ return ret;
+ },
+ deleteProperty(target, prop) {
+ return Module.ccall(
+ "proxy_c_to_js_delete_attr",
+ "number",
+ ["number", "string"],
+ [target._ref, prop],
+ );
+ },
+};
+
+// PyProxy of a Python generator, that implements the thenable interface.
+class PyProxyThenable {
+ constructor(ref) {
+ this._ref = ref;
+ }
+
+ then(resolve, reject) {
+ const values = Module._malloc(3 * 3 * 4);
+ proxy_convert_js_to_mp_obj_jsside(resolve, values + 3 * 4);
+ proxy_convert_js_to_mp_obj_jsside(reject, values + 2 * 3 * 4);
+ Module.ccall(
+ "proxy_c_to_js_resume",
+ "null",
+ ["number", "pointer"],
+ [this._ref, values],
+ );
+ return proxy_convert_mp_to_js_obj_jsside_with_free(values);
+ }
+}
diff --git a/ports/webassembly/proxy_c.c b/ports/webassembly/proxy_c.c
new file mode 100644
index 0000000000000..1e4573ce0ba55
--- /dev/null
+++ b/ports/webassembly/proxy_c.c
@@ -0,0 +1,360 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include
+#include
+
+#include "emscripten.h"
+#include "py/builtin.h"
+#include "py/runtime.h"
+#include "proxy_c.h"
+
+// These constants should match the constants in proxy_js.js.
+
+enum {
+ PROXY_KIND_MP_EXCEPTION = -1,
+ PROXY_KIND_MP_NULL = 0,
+ PROXY_KIND_MP_NONE = 1,
+ PROXY_KIND_MP_BOOL = 2,
+ PROXY_KIND_MP_INT = 3,
+ PROXY_KIND_MP_FLOAT = 4,
+ PROXY_KIND_MP_STR = 5,
+ PROXY_KIND_MP_CALLABLE = 6,
+ PROXY_KIND_MP_GENERATOR = 7,
+ PROXY_KIND_MP_OBJECT = 8,
+ PROXY_KIND_MP_JSPROXY = 9,
+};
+
+enum {
+ PROXY_KIND_JS_NULL = 1,
+ PROXY_KIND_JS_BOOLEAN = 2,
+ PROXY_KIND_JS_INTEGER = 3,
+ PROXY_KIND_JS_DOUBLE = 4,
+ PROXY_KIND_JS_STRING = 5,
+ PROXY_KIND_JS_OBJECT = 6,
+ PROXY_KIND_JS_PYPROXY = 7,
+};
+
+void proxy_c_init(void) {
+ MP_STATE_PORT(proxy_c_ref) = mp_obj_new_list(0, NULL);
+ mp_obj_list_append(MP_STATE_PORT(proxy_c_ref), MP_OBJ_NULL);
+}
+
+MP_REGISTER_ROOT_POINTER(mp_obj_t proxy_c_ref);
+
+static inline mp_obj_t proxy_c_get_obj(uint32_t c_ref) {
+ return ((mp_obj_list_t *)MP_OBJ_TO_PTR(MP_STATE_PORT(proxy_c_ref)))->items[c_ref];
+}
+
+mp_obj_t proxy_convert_js_to_mp_obj_cside(uint32_t *value) {
+ if (value[0] == PROXY_KIND_JS_NULL) {
+ return mp_const_none;
+ } else if (value[0] == PROXY_KIND_JS_BOOLEAN) {
+ return mp_obj_new_bool(value[1]);
+ } else if (value[0] == PROXY_KIND_JS_INTEGER) {
+ return mp_obj_new_int(value[1]);
+ } else if (value[0] == PROXY_KIND_JS_DOUBLE) {
+ return mp_obj_new_float_from_d(*(double *)&value[1]);
+ } else if (value[0] == PROXY_KIND_JS_STRING) {
+ mp_obj_t s = mp_obj_new_str((void *)value[2], value[1]);
+ free((void *)value[2]);
+ return s;
+ } else if (value[0] == PROXY_KIND_JS_PYPROXY) {
+ return proxy_c_get_obj(value[1]);
+ } else {
+ // PROXY_KIND_JS_OBJECT
+ return mp_obj_new_jsproxy(value[1]);
+ }
+}
+
+void proxy_convert_mp_to_js_obj_cside(mp_obj_t obj, uint32_t *out) {
+ uint32_t kind;
+ if (obj == MP_OBJ_NULL) {
+ kind = PROXY_KIND_MP_NULL;
+ } else if (obj == mp_const_none) {
+ kind = PROXY_KIND_MP_NONE;
+ } else if (mp_obj_is_bool(obj)) {
+ kind = PROXY_KIND_MP_BOOL;
+ out[1] = mp_obj_is_true(obj);
+ } else if (mp_obj_is_int(obj)) {
+ kind = PROXY_KIND_MP_INT;
+ out[1] = mp_obj_get_int_truncated(obj); // TODO support big int
+ } else if (mp_obj_is_float(obj)) {
+ kind = PROXY_KIND_MP_FLOAT;
+ *(double *)&out[1] = mp_obj_get_float(obj);
+ } else if (mp_obj_is_str(obj)) {
+ kind = PROXY_KIND_MP_STR;
+ size_t len;
+ const char *str = mp_obj_str_get_data(obj, &len);
+ out[1] = len;
+ out[2] = (uintptr_t)str;
+ } else if (mp_obj_is_jsproxy(obj)) {
+ kind = PROXY_KIND_MP_JSPROXY;
+ out[1] = mp_obj_jsproxy_get_ref(obj);
+ } else {
+ if (mp_obj_is_callable(obj)) {
+ kind = PROXY_KIND_MP_CALLABLE;
+ } else if (mp_obj_is_type(obj, &mp_type_gen_instance)) {
+ kind = PROXY_KIND_MP_GENERATOR;
+ } else {
+ kind = PROXY_KIND_MP_OBJECT;
+ }
+ size_t id = ((mp_obj_list_t *)MP_OBJ_TO_PTR(MP_STATE_PORT(proxy_c_ref)))->len;
+ mp_obj_list_append(MP_STATE_PORT(proxy_c_ref), obj);
+ out[1] = id;
+ }
+ out[0] = kind;
+}
+
+void proxy_convert_mp_to_js_exc_cside(void *exc, uint32_t *out) {
+ out[0] = PROXY_KIND_MP_EXCEPTION;
+ vstr_t vstr;
+ mp_print_t print;
+ vstr_init_print(&vstr, 64, &print);
+ vstr_add_str(&vstr, qstr_str(mp_obj_get_type(MP_OBJ_FROM_PTR(exc))->name));
+ vstr_add_char(&vstr, '\x04');
+ mp_obj_print_exception(&print, MP_OBJ_FROM_PTR(exc));
+ char *s = malloc(vstr_len(&vstr) + 1);
+ memcpy(s, vstr_str(&vstr), vstr_len(&vstr));
+ out[1] = vstr_len(&vstr);
+ out[2] = (uintptr_t)s;
+ vstr_clear(&vstr);
+}
+
+void proxy_c_to_js_call(uint32_t c_ref, uint32_t n_args, uint32_t *args_value, uint32_t *out) {
+ nlr_buf_t nlr;
+ if (nlr_push(&nlr) == 0) {
+ mp_obj_t args[4] = { mp_const_none, mp_const_none, mp_const_none, mp_const_none };
+ for (size_t i = 0; i < n_args; ++i) {
+ args[i] = proxy_convert_js_to_mp_obj_cside(args_value + i * 3);
+ }
+ mp_obj_t obj = proxy_c_get_obj(c_ref);
+ mp_obj_t member = mp_call_function_n_kw(obj, n_args, 0, args);
+ nlr_pop();
+ proxy_convert_mp_to_js_obj_cside(member, out);
+ } else {
+ // uncaught exception
+ proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out);
+ }
+}
+
+void proxy_c_to_js_dir(uint32_t c_ref, uint32_t *out) {
+ nlr_buf_t nlr;
+ if (nlr_push(&nlr) == 0) {
+ mp_obj_t obj = proxy_c_get_obj(c_ref);
+ mp_obj_t dir;
+ if (mp_obj_is_dict_or_ordereddict(obj)) {
+ mp_map_t *map = mp_obj_dict_get_map(obj);
+ dir = mp_obj_new_list(0, NULL);
+ for (size_t i = 0; i < map->alloc; i++) {
+ if (mp_map_slot_is_filled(map, i)) {
+ mp_obj_list_append(dir, map->table[i].key);
+ }
+ }
+ } else {
+ mp_obj_t args[1] = { obj };
+ dir = mp_builtin_dir_obj.fun.var(1, args);
+ }
+ nlr_pop();
+ return proxy_convert_mp_to_js_obj_cside(dir, out);
+ } else {
+ // uncaught exception
+ return proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out);
+ }
+}
+
+bool proxy_c_to_js_has_attr(uint32_t c_ref, const char *attr_in) {
+ mp_obj_t obj = proxy_c_get_obj(c_ref);
+ qstr attr = qstr_from_str(attr_in);
+ if (mp_obj_is_dict_or_ordereddict(obj)) {
+ mp_map_t *map = mp_obj_dict_get_map(obj);
+ mp_map_elem_t *elem = mp_map_lookup(map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP);
+ return elem != NULL;
+ } else {
+ mp_obj_t dest[2];
+ mp_load_method_protected(obj, attr, dest, true);
+ if (dest[0] != MP_OBJ_NULL) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void proxy_c_to_js_lookup_attr(uint32_t c_ref, const char *attr_in, uint32_t *out) {
+ nlr_buf_t nlr;
+ if (nlr_push(&nlr) == 0) {
+ mp_obj_t obj = proxy_c_get_obj(c_ref);
+ qstr attr = qstr_from_str(attr_in);
+ mp_obj_t member;
+ if (mp_obj_is_dict_or_ordereddict(obj)) {
+ member = mp_obj_dict_get(obj, MP_OBJ_NEW_QSTR(attr));
+ } else {
+ member = mp_load_attr(obj, attr);
+ }
+ nlr_pop();
+ return proxy_convert_mp_to_js_obj_cside(member, out);
+ } else {
+ // uncaught exception
+ return proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out);
+ }
+}
+
+static bool proxy_c_to_js_store_helper(uint32_t c_ref, const char *attr_in, mp_obj_t value) {
+ mp_obj_t obj = proxy_c_get_obj(c_ref);
+ qstr attr = qstr_from_str(attr_in);
+
+ nlr_buf_t nlr;
+ if (nlr_push(&nlr) == 0) {
+ if (mp_obj_is_dict_or_ordereddict(obj)) {
+ if (value == MP_OBJ_NULL) {
+ mp_obj_dict_delete(obj, MP_OBJ_NEW_QSTR(attr));
+ } else {
+ mp_obj_dict_store(obj, MP_OBJ_NEW_QSTR(attr), value);
+ }
+ } else {
+ mp_store_attr(obj, attr, value);
+ }
+ nlr_pop();
+ return true;
+ } else {
+ // uncaught exception
+ return false;
+ }
+}
+
+bool proxy_c_to_js_store_attr(uint32_t c_ref, const char *attr_in, uint32_t *value_in) {
+ mp_obj_t value = proxy_convert_js_to_mp_obj_cside(value_in);
+ return proxy_c_to_js_store_helper(c_ref, attr_in, value);
+}
+
+bool proxy_c_to_js_delete_attr(uint32_t c_ref, const char *attr_in) {
+ return proxy_c_to_js_store_helper(c_ref, attr_in, MP_OBJ_NULL);
+}
+
+uint32_t proxy_c_to_js_get_type(uint32_t c_ref) {
+ mp_obj_t obj = proxy_c_get_obj(c_ref);
+ const mp_obj_type_t *type = mp_obj_get_type(obj);
+ if (type == &mp_type_tuple) {
+ return 1;
+ } else if (type == &mp_type_list) {
+ return 2;
+ } else if (type == &mp_type_dict) {
+ return 3;
+ } else {
+ return 4;
+ }
+}
+
+void proxy_c_to_js_get_array(uint32_t c_ref, uint32_t *out) {
+ mp_obj_t obj = proxy_c_get_obj(c_ref);
+ size_t len;
+ mp_obj_t *items;
+ mp_obj_get_array(obj, &len, &items);
+ out[0] = len;
+ out[1] = (uintptr_t)items;
+}
+
+void proxy_c_to_js_get_dict(uint32_t c_ref, uint32_t *out) {
+ mp_obj_t obj = proxy_c_get_obj(c_ref);
+ mp_map_t *map = mp_obj_dict_get_map(obj);
+ out[0] = map->alloc;
+ out[1] = (uintptr_t)map->table;
+}
+
+/******************************************************************************/
+// Bridge Python generator to JavaScript thenable.
+
+static const mp_obj_fun_builtin_var_t resume_obj;
+
+EM_JS(void, js_then_resolve, (uint32_t * resolve, uint32_t * reject), {
+ const resolve_js = proxy_convert_mp_to_js_obj_jsside(resolve);
+ const reject_js = proxy_convert_mp_to_js_obj_jsside(reject);
+ resolve_js(null);
+});
+
+EM_JS(void, js_then_reject, (uint32_t * resolve, uint32_t * reject), {
+ const resolve_js = proxy_convert_mp_to_js_obj_jsside(resolve);
+ const reject_js = proxy_convert_mp_to_js_obj_jsside(reject);
+ reject_js(null);
+});
+
+// *FORMAT-OFF*
+EM_JS(void, js_then_continue, (int jsref, uint32_t * py_resume, uint32_t * resolve, uint32_t * reject, uint32_t * out), {
+ const py_resume_js = proxy_convert_mp_to_js_obj_jsside(py_resume);
+ const resolve_js = proxy_convert_mp_to_js_obj_jsside(resolve);
+ const reject_js = proxy_convert_mp_to_js_obj_jsside(reject);
+ const ret = proxy_js_ref[jsref].then((x) => {py_resume_js(x, resolve_js, reject_js);}, reject_js);
+ proxy_convert_js_to_mp_obj_jsside(ret, out);
+});
+// *FORMAT-ON*
+
+static mp_obj_t proxy_resume_execute(mp_obj_t self_in, mp_obj_t value, mp_obj_t resolve, mp_obj_t reject) {
+ mp_obj_t ret_value;
+ mp_vm_return_kind_t ret_kind = mp_resume(self_in, value, MP_OBJ_NULL, &ret_value);
+
+ uint32_t out_resolve[PVN];
+ uint32_t out_reject[PVN];
+ proxy_convert_mp_to_js_obj_cside(resolve, out_resolve);
+ proxy_convert_mp_to_js_obj_cside(reject, out_reject);
+
+ if (ret_kind == MP_VM_RETURN_NORMAL) {
+ js_then_resolve(out_resolve, out_reject);
+ return mp_const_none;
+ } else if (ret_kind == MP_VM_RETURN_YIELD) {
+ // ret_value should be a JS thenable
+ mp_obj_t py_resume = mp_obj_new_bound_meth(MP_OBJ_FROM_PTR(&resume_obj), self_in);
+ int ref = mp_obj_jsproxy_get_ref(ret_value);
+ uint32_t out_py_resume[PVN];
+ proxy_convert_mp_to_js_obj_cside(py_resume, out_py_resume);
+ uint32_t out[PVN];
+ js_then_continue(ref, out_py_resume, out_resolve, out_reject, out);
+ return proxy_convert_js_to_mp_obj_cside(out);
+ } else {
+ // MP_VM_RETURN_EXCEPTION;
+ js_then_reject(out_resolve, out_reject);
+ nlr_raise(ret_value);
+ }
+}
+
+static mp_obj_t resume_fun(size_t n_args, const mp_obj_t *args) {
+ return proxy_resume_execute(args[0], args[1], args[2], args[3]);
+}
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(resume_obj, 4, 4, resume_fun);
+
+void proxy_c_to_js_resume(uint32_t c_ref, uint32_t *args) {
+ nlr_buf_t nlr;
+ if (nlr_push(&nlr) == 0) {
+ mp_obj_t obj = proxy_c_get_obj(c_ref);
+ mp_obj_t resolve = proxy_convert_js_to_mp_obj_cside(args + 1 * 3);
+ mp_obj_t reject = proxy_convert_js_to_mp_obj_cside(args + 2 * 3);
+ mp_obj_t ret = proxy_resume_execute(obj, mp_const_none, resolve, reject);
+ nlr_pop();
+ return proxy_convert_mp_to_js_obj_cside(ret, args);
+ } else {
+ // uncaught exception
+ return proxy_convert_mp_to_js_exc_cside(nlr.ret_val, args);
+ }
+}
diff --git a/ports/webassembly/proxy_c.h b/ports/webassembly/proxy_c.h
new file mode 100644
index 0000000000000..3e68d2504958c
--- /dev/null
+++ b/ports/webassembly/proxy_c.h
@@ -0,0 +1,58 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+#ifndef MICROPY_INCLUDED_WEBASSEMBLY_PROXY_C_H
+#define MICROPY_INCLUDED_WEBASSEMBLY_PROXY_C_H
+
+#include "py/obj.h"
+
+// proxy value number of items
+#define PVN (3)
+
+typedef struct _mp_obj_jsproxy_t {
+ mp_obj_base_t base;
+ int ref;
+} mp_obj_jsproxy_t;
+
+extern const mp_obj_type_t mp_type_jsproxy;
+
+void proxy_c_init(void);
+mp_obj_t proxy_convert_js_to_mp_obj_cside(uint32_t *value);
+void proxy_convert_mp_to_js_obj_cside(mp_obj_t obj, uint32_t *out);
+void proxy_convert_mp_to_js_exc_cside(void *exc, uint32_t *out);
+
+mp_obj_t mp_obj_new_jsproxy(int ref);
+void mp_obj_jsproxy_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest);
+
+static inline bool mp_obj_is_jsproxy(mp_obj_t o) {
+ return mp_obj_get_type(o) == &mp_type_jsproxy;
+}
+
+static inline int mp_obj_jsproxy_get_ref(mp_obj_t o) {
+ mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(o);
+ return self->ref;
+}
+
+#endif // MICROPY_INCLUDED_WEBASSEMBLY_PROXY_C_H
diff --git a/ports/webassembly/proxy_js.js b/ports/webassembly/proxy_js.js
new file mode 100644
index 0000000000000..7a0a1bbe8932d
--- /dev/null
+++ b/ports/webassembly/proxy_js.js
@@ -0,0 +1,228 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2023-2024 Damien P. George
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+// These constants should match the constants in proxy_c.c.
+
+const PROXY_KIND_MP_EXCEPTION = -1;
+const PROXY_KIND_MP_NULL = 0;
+const PROXY_KIND_MP_NONE = 1;
+const PROXY_KIND_MP_BOOL = 2;
+const PROXY_KIND_MP_INT = 3;
+const PROXY_KIND_MP_FLOAT = 4;
+const PROXY_KIND_MP_STR = 5;
+const PROXY_KIND_MP_CALLABLE = 6;
+const PROXY_KIND_MP_GENERATOR = 7;
+const PROXY_KIND_MP_OBJECT = 8;
+const PROXY_KIND_MP_JSPROXY = 9;
+
+const PROXY_KIND_JS_NULL = 1;
+const PROXY_KIND_JS_BOOLEAN = 2;
+const PROXY_KIND_JS_INTEGER = 3;
+const PROXY_KIND_JS_DOUBLE = 4;
+const PROXY_KIND_JS_STRING = 5;
+const PROXY_KIND_JS_OBJECT = 6;
+const PROXY_KIND_JS_PYPROXY = 7;
+
+class PythonError extends Error {
+ constructor(exc_type, exc_details) {
+ super(exc_details);
+ this.name = "PythonError";
+ this.type = exc_type;
+ }
+}
+
+function proxy_js_init() {
+ globalThis.proxy_js_ref = [globalThis];
+}
+
+function proxy_call_python(target, argumentsList) {
+ let args = 0;
+
+ // Strip trailing "undefined" arguments.
+ while (
+ argumentsList.length > 0 &&
+ argumentsList[argumentsList.length - 1] === undefined
+ ) {
+ argumentsList.pop();
+ }
+
+ if (argumentsList.length > 0) {
+ // TODO use stackAlloc/stackRestore?
+ args = Module._malloc(argumentsList.length * 3 * 4);
+ for (const i in argumentsList) {
+ proxy_convert_js_to_mp_obj_jsside(
+ argumentsList[i],
+ args + i * 3 * 4,
+ );
+ }
+ }
+ const value = Module._malloc(3 * 4);
+ Module.ccall(
+ "proxy_c_to_js_call",
+ "null",
+ ["number", "number", "number", "pointer"],
+ [target, argumentsList.length, args, value],
+ );
+ if (argumentsList.length > 0) {
+ Module._free(args);
+ }
+ return proxy_convert_mp_to_js_obj_jsside_with_free(value);
+}
+
+function proxy_convert_js_to_mp_obj_jsside(js_obj, out) {
+ let kind;
+ if (js_obj === null) {
+ kind = PROXY_KIND_JS_NULL;
+ } else if (typeof js_obj === "boolean") {
+ kind = PROXY_KIND_JS_BOOLEAN;
+ Module.setValue(out + 4, js_obj, "i32");
+ } else if (typeof js_obj === "number") {
+ if (Number.isInteger(js_obj)) {
+ kind = PROXY_KIND_JS_INTEGER;
+ Module.setValue(out + 4, js_obj, "i32");
+ } else {
+ kind = PROXY_KIND_JS_DOUBLE;
+ // double must be stored to an address that's a multiple of 8
+ const temp = (out + 4) & ~7;
+ Module.setValue(temp, js_obj, "double");
+ const double_lo = Module.getValue(temp, "i32");
+ const double_hi = Module.getValue(temp + 4, "i32");
+ Module.setValue(out + 4, double_lo, "i32");
+ Module.setValue(out + 8, double_hi, "i32");
+ }
+ } else if (typeof js_obj === "string") {
+ kind = PROXY_KIND_JS_STRING;
+ const len = Module.lengthBytesUTF8(js_obj);
+ const buf = Module._malloc(len + 1);
+ Module.stringToUTF8(js_obj, buf, len + 1);
+ Module.setValue(out + 4, len, "i32");
+ Module.setValue(out + 8, buf, "i32");
+ } else if (js_obj instanceof PyProxy) {
+ kind = PROXY_KIND_JS_PYPROXY;
+ Module.setValue(out + 4, js_obj._ref, "i32");
+ } else if (js_obj instanceof PyProxyThenable) {
+ kind = PROXY_KIND_JS_PYPROXY;
+ Module.setValue(out + 4, js_obj._ref, "i32");
+ } else {
+ kind = PROXY_KIND_JS_OBJECT;
+ const id = proxy_js_ref.length;
+ proxy_js_ref[id] = js_obj;
+ Module.setValue(out + 4, id, "i32");
+ }
+ Module.setValue(out + 0, kind, "i32");
+}
+
+function proxy_convert_js_to_mp_obj_jsside_force_double_proxy(js_obj, out) {
+ if (js_obj instanceof PyProxy) {
+ const kind = PROXY_KIND_JS_OBJECT;
+ const id = proxy_js_ref.length;
+ proxy_js_ref[id] = js_obj;
+ Module.setValue(out + 4, id, "i32");
+ Module.setValue(out + 0, kind, "i32");
+ } else {
+ proxy_convert_js_to_mp_obj_jsside(js_obj, out);
+ }
+}
+
+function proxy_convert_mp_to_js_obj_jsside(value) {
+ const kind = Module.getValue(value, "i32");
+ let obj;
+ if (kind === PROXY_KIND_MP_EXCEPTION) {
+ // Exception
+ const str_len = Module.getValue(value + 4, "i32");
+ const str_ptr = Module.getValue(value + 8, "i32");
+ const str = Module.UTF8ToString(str_ptr, str_len);
+ Module._free(str_ptr);
+ const str_split = str.split("\x04");
+ throw new PythonError(str_split[0], str_split[1]);
+ }
+ if (kind === PROXY_KIND_MP_NULL) {
+ // MP_OBJ_NULL
+ throw new Error("NULL object");
+ }
+ if (kind === PROXY_KIND_MP_NONE) {
+ // None
+ obj = null;
+ } else if (kind === PROXY_KIND_MP_BOOL) {
+ // bool
+ obj = Module.getValue(value + 4, "i32") ? true : false;
+ } else if (kind === PROXY_KIND_MP_INT) {
+ // int
+ obj = Module.getValue(value + 4, "i32");
+ } else if (kind === PROXY_KIND_MP_FLOAT) {
+ // float
+ // double must be loaded from an address that's a multiple of 8
+ const temp = (value + 4) & ~7;
+ const double_lo = Module.getValue(value + 4, "i32");
+ const double_hi = Module.getValue(value + 8, "i32");
+ Module.setValue(temp, double_lo, "i32");
+ Module.setValue(temp + 4, double_hi, "i32");
+ obj = Module.getValue(temp, "double");
+ } else if (kind === PROXY_KIND_MP_STR) {
+ // str
+ const str_len = Module.getValue(value + 4, "i32");
+ const str_ptr = Module.getValue(value + 8, "i32");
+ obj = Module.UTF8ToString(str_ptr, str_len);
+ } else if (kind === PROXY_KIND_MP_JSPROXY) {
+ // js proxy
+ const id = Module.getValue(value + 4, "i32");
+ obj = proxy_js_ref[id];
+ } else {
+ // obj
+ const id = Module.getValue(value + 4, "i32");
+ if (kind === PROXY_KIND_MP_CALLABLE) {
+ obj = (...args) => {
+ return proxy_call_python(id, args);
+ };
+ } else if (kind === PROXY_KIND_MP_GENERATOR) {
+ obj = new PyProxyThenable(id);
+ } else {
+ // PROXY_KIND_MP_OBJECT
+ const target = new PyProxy(id);
+ obj = new Proxy(target, py_proxy_handler);
+ }
+ }
+ return obj;
+}
+
+function proxy_convert_mp_to_js_obj_jsside_with_free(value) {
+ const ret = proxy_convert_mp_to_js_obj_jsside(value);
+ Module._free(value);
+ return ret;
+}
+
+function python_index_semantics(target, index_in) {
+ let index = index_in;
+ if (typeof index === "number") {
+ if (index < 0) {
+ index += target.length;
+ }
+ if (index < 0 || index >= target.length) {
+ throw new PythonError("IndexError", "index out of range");
+ }
+ }
+ return index;
+}
diff --git a/ports/webassembly/qstrdefsport.h b/ports/webassembly/qstrdefsport.h
index 00d3e2ae3c555..472d05f4375f6 100644
--- a/ports/webassembly/qstrdefsport.h
+++ b/ports/webassembly/qstrdefsport.h
@@ -1,2 +1,3 @@
// qstrs specific to this port
// *FORMAT-OFF*
+Q(/lib)
diff --git a/ports/webassembly/variants/pyscript/manifest.py b/ports/webassembly/variants/pyscript/manifest.py
new file mode 100644
index 0000000000000..0646e1d897f0c
--- /dev/null
+++ b/ports/webassembly/variants/pyscript/manifest.py
@@ -0,0 +1,27 @@
+require("abc")
+require("base64")
+require("collections")
+require("collections-defaultdict")
+require("copy")
+require("datetime")
+require("fnmatch")
+require("functools")
+require("gzip")
+require("hmac")
+require("html")
+require("inspect")
+require("io")
+require("itertools")
+require("locale")
+require("logging")
+require("operator")
+require("os")
+require("os-path")
+require("pathlib")
+require("stat")
+require("tarfile")
+require("tarfile-write")
+require("time")
+require("unittest")
+require("uu")
+require("zlib")
diff --git a/ports/webassembly/variants/pyscript/mpconfigvariant.h b/ports/webassembly/variants/pyscript/mpconfigvariant.h
new file mode 100644
index 0000000000000..ed8e812803533
--- /dev/null
+++ b/ports/webassembly/variants/pyscript/mpconfigvariant.h
@@ -0,0 +1,3 @@
+#define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_FULL_FEATURES)
+#define MICROPY_GC_SPLIT_HEAP (1)
+#define MICROPY_GC_SPLIT_HEAP_AUTO (1)
diff --git a/ports/webassembly/variants/pyscript/mpconfigvariant.mk b/ports/webassembly/variants/pyscript/mpconfigvariant.mk
new file mode 100644
index 0000000000000..016b96a99af0a
--- /dev/null
+++ b/ports/webassembly/variants/pyscript/mpconfigvariant.mk
@@ -0,0 +1,3 @@
+JSFLAGS += -s ALLOW_MEMORY_GROWTH
+
+FROZEN_MANIFEST ?= variants/pyscript/manifest.py
diff --git a/ports/webassembly/variants/standard/mpconfigvariant.h b/ports/webassembly/variants/standard/mpconfigvariant.h
new file mode 100644
index 0000000000000..7be62ea7f4f3f
--- /dev/null
+++ b/ports/webassembly/variants/standard/mpconfigvariant.h
@@ -0,0 +1 @@
+#define MICROPY_VARIANT_ENABLE_JS_HOOK (1)
diff --git a/ports/webassembly/variants/standard/mpconfigvariant.mk b/ports/webassembly/variants/standard/mpconfigvariant.mk
new file mode 100644
index 0000000000000..62ee161907de2
--- /dev/null
+++ b/ports/webassembly/variants/standard/mpconfigvariant.mk
@@ -0,0 +1 @@
+JSFLAGS += -s ASYNCIFY
diff --git a/ports/webassembly/wrapper.js b/ports/webassembly/wrapper.js
deleted file mode 100644
index e63abfffe4ae9..0000000000000
--- a/ports/webassembly/wrapper.js
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * This file is part of the MicroPython project, http://micropython.org/
- *
- * The MIT License (MIT)
- *
- * Copyright (c) 2017, 2018 Rami Ali
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-var Module = {};
-
-var mainProgram = function()
-{
- mp_js_init = Module.cwrap('mp_js_init', 'null', ['number']);
- mp_js_do_str = Module.cwrap('mp_js_do_str', 'number', ['string'], {async: true});
- mp_js_init_repl = Module.cwrap('mp_js_init_repl', 'null', ['null']);
- mp_js_process_char = Module.cwrap('mp_js_process_char', 'number', ['number'], {async: true});
-
- MP_JS_EPOCH = Date.now();
-
- if (typeof window === 'undefined' && require.main === module) {
- var fs = require('fs');
- var heap_size = 128 * 1024;
- var contents = '';
- var repl = true;
-
- for (var i = 0; i < process.argv.length; i++) {
- if (process.argv[i] === '-X' && i < process.argv.length - 1) {
- if (process.argv[i + 1].includes('heapsize=')) {
- heap_size = parseInt(process.argv[i + 1].split('heapsize=')[1]);
- if (process.argv[i + 1].substr(-1).toLowerCase() === 'k') {
- heap_size *= 1024;
- } else if (process.argv[i + 1].substr(-1).toLowerCase() === 'm') {
- heap_size *= 1024 * 1024;
- }
- }
- } else if (process.argv[i].includes('.py')) {
- contents += fs.readFileSync(process.argv[i], 'utf8');
- repl = false;;
- }
- }
-
- if (process.stdin.isTTY === false) {
- contents = fs.readFileSync(0, 'utf8');
- repl = 0;
- }
-
- mp_js_init(heap_size);
-
- if (repl) {
- mp_js_init_repl();
- process.stdin.setRawMode(true);
- process.stdin.on('data', function (data) {
- for (var i = 0; i < data.length; i++) {
- mp_js_process_char(data[i]).then(result => {
- if (result) {
- process.exit()
- }
- })
- }
- });
- } else {
- mp_js_do_str(contents).then(exitCode => {
- process.exitCode = exitCode
- })
- }
- }
-}
-
-Module["onRuntimeInitialized"] = mainProgram;
diff --git a/py/compile.c b/py/compile.c
index 7a359e662e731..62757de3c083c 100644
--- a/py/compile.c
+++ b/py/compile.c
@@ -196,6 +196,10 @@ typedef struct _compiler_t {
mp_emit_common_t emit_common;
} compiler_t;
+#if MICROPY_COMP_ALLOW_TOP_LEVEL_AWAIT
+bool mp_compile_allow_top_level_await = false;
+#endif
+
/******************************************************************************/
// mp_emit_common_t helper functions
// These are defined here so they can be inlined, to reduce code size.
@@ -2759,8 +2763,13 @@ static void compile_yield_expr(compiler_t *comp, mp_parse_node_struct_t *pns) {
#if MICROPY_PY_ASYNC_AWAIT
static void compile_atom_expr_await(compiler_t *comp, mp_parse_node_struct_t *pns) {
if (comp->scope_cur->kind != SCOPE_FUNCTION && comp->scope_cur->kind != SCOPE_LAMBDA) {
- compile_syntax_error(comp, (mp_parse_node_t)pns, MP_ERROR_TEXT("'await' outside function"));
- return;
+ #if MICROPY_COMP_ALLOW_TOP_LEVEL_AWAIT
+ if (!mp_compile_allow_top_level_await)
+ #endif
+ {
+ compile_syntax_error(comp, (mp_parse_node_t)pns, MP_ERROR_TEXT("'await' outside function"));
+ return;
+ }
}
compile_atom_expr_normal(comp, pns);
compile_yield_from(comp);
diff --git a/py/compile.h b/py/compile.h
index 5e0fd8b31c4a8..f9970a521d644 100644
--- a/py/compile.h
+++ b/py/compile.h
@@ -30,6 +30,11 @@
#include "py/parse.h"
#include "py/emitglue.h"
+#if MICROPY_COMP_ALLOW_TOP_LEVEL_AWAIT
+// set to `true` to allow top-level await expressions
+extern bool mp_compile_allow_top_level_await;
+#endif
+
// the compiler will raise an exception if an error occurred
// the compiler will clear the parse tree before it returns
// mp_globals_get() will be used for the context
diff --git a/py/mpconfig.h b/py/mpconfig.h
index d9cff930d1485..af2480266bcab 100644
--- a/py/mpconfig.h
+++ b/py/mpconfig.h
@@ -446,6 +446,11 @@
#define MICROPY_DYNAMIC_COMPILER (0)
#endif
+// Whether the compiler allows compiling top-level await expressions
+#ifndef MICROPY_COMP_ALLOW_TOP_LEVEL_AWAIT
+#define MICROPY_COMP_ALLOW_TOP_LEVEL_AWAIT (0)
+#endif
+
// Whether to enable constant folding; eg 1+2 rewritten as 3
#ifndef MICROPY_COMP_CONST_FOLDING
#define MICROPY_COMP_CONST_FOLDING (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
diff --git a/tests/ports/webassembly/basic.js b/tests/ports/webassembly/basic.js
new file mode 100644
index 0000000000000..19b1881faf5ca
--- /dev/null
+++ b/tests/ports/webassembly/basic.js
@@ -0,0 +1,8 @@
+import(process.argv[2]).then((mp) => {
+ mp.loadMicroPython().then((py) => {
+ py.runPython("1");
+ py.runPython("print('hello')");
+ py.runPython("import sys; print(f'hello from {sys.platform}')");
+ py.runPython("import collections; print(collections.OrderedDict)");
+ });
+});
diff --git a/tests/ports/webassembly/basic.js.exp b/tests/ports/webassembly/basic.js.exp
new file mode 100644
index 0000000000000..6459b2492cf86
--- /dev/null
+++ b/tests/ports/webassembly/basic.js.exp
@@ -0,0 +1,3 @@
+hello
+hello from webassembly
+
diff --git a/tests/ports/webassembly/filesystem.mjs b/tests/ports/webassembly/filesystem.mjs
new file mode 100644
index 0000000000000..e9e2920a16105
--- /dev/null
+++ b/tests/ports/webassembly/filesystem.mjs
@@ -0,0 +1,9 @@
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.FS.mkdir("/lib/");
+mp.FS.writeFile("/lib/testmod.py", "x = 1; print(__name__, x)");
+mp.runPython("import testmod");
+
+mp.runPython("import sys; sys.modules.clear()");
+const testmod = mp.pyimport("testmod");
+console.log("testmod:", testmod, testmod.x);
diff --git a/tests/ports/webassembly/filesystem.mjs.exp b/tests/ports/webassembly/filesystem.mjs.exp
new file mode 100644
index 0000000000000..25a48b10832c5
--- /dev/null
+++ b/tests/ports/webassembly/filesystem.mjs.exp
@@ -0,0 +1,3 @@
+testmod 1
+testmod 1
+testmod: PyProxy { _ref: 3 } 1
diff --git a/tests/ports/webassembly/float.mjs b/tests/ports/webassembly/float.mjs
new file mode 100644
index 0000000000000..53bb8b1c4d0ad
--- /dev/null
+++ b/tests/ports/webassembly/float.mjs
@@ -0,0 +1,43 @@
+// Test passing floats between JavaScript and Python.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+globalThis.a = 1 / 2;
+globalThis.b = Infinity;
+globalThis.c = NaN;
+
+mp.runPython(`
+import js
+
+# Test retrieving floats from JS.
+print(js.a)
+print(js.b)
+print(js.c)
+
+# Test calling JS which returns a float.
+r = js.Math.random()
+print(type(r), 0 < r < 1)
+
+x = 1 / 2
+y = float("inf")
+z = float("nan")
+
+# Test passing floats to a JS function.
+js.console.log(x)
+js.console.log(x, y)
+js.console.log(x, y, z)
+`);
+
+// Test retrieving floats from Python.
+console.log(mp.globals.get("x"));
+console.log(mp.globals.get("y"));
+console.log(mp.globals.get("z"));
+
+// Test passing floats to a Python function.
+const mp_print = mp.pyimport("builtins").print;
+mp_print(globalThis.a);
+mp_print(globalThis.a, globalThis.b);
+mp_print(globalThis.a, globalThis.b, globalThis.c);
+
+// Test calling Python which returns a float.
+console.log(mp.pyimport("math").sqrt(0.16));
diff --git a/tests/ports/webassembly/float.mjs.exp b/tests/ports/webassembly/float.mjs.exp
new file mode 100644
index 0000000000000..57eff74acd8af
--- /dev/null
+++ b/tests/ports/webassembly/float.mjs.exp
@@ -0,0 +1,14 @@
+0.5
+inf
+nan
+ True
+0.5
+0.5 Infinity
+0.5 Infinity NaN
+0.5
+Infinity
+NaN
+0.5
+0.5 inf
+0.5 inf nan
+0.4
diff --git a/tests/ports/webassembly/fun_call.mjs b/tests/ports/webassembly/fun_call.mjs
new file mode 100644
index 0000000000000..295745d2e2e34
--- /dev/null
+++ b/tests/ports/webassembly/fun_call.mjs
@@ -0,0 +1,17 @@
+// Test calling JavaScript functions from Python.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+globalThis.f = (a, b, c, d, e) => {
+ console.log(a, b, c, d, e);
+};
+mp.runPython(`
+import js
+js.f()
+js.f(1)
+js.f(1, 2)
+js.f(1, 2, 3)
+js.f(1, 2, 3, 4)
+js.f(1, 2, 3, 4, 5)
+js.f(1, 2, 3, 4, 5, 6)
+`);
diff --git a/tests/ports/webassembly/fun_call.mjs.exp b/tests/ports/webassembly/fun_call.mjs.exp
new file mode 100644
index 0000000000000..e9ed5f6ddf9de
--- /dev/null
+++ b/tests/ports/webassembly/fun_call.mjs.exp
@@ -0,0 +1,7 @@
+undefined undefined undefined undefined undefined
+1 undefined undefined undefined undefined
+1 2 undefined undefined undefined
+1 2 3 undefined undefined
+1 2 3 4 undefined
+1 2 3 4 5
+1 2 3 4 5
diff --git a/tests/ports/webassembly/globals.mjs b/tests/ports/webassembly/globals.mjs
new file mode 100644
index 0000000000000..3d5ecb4167878
--- /dev/null
+++ b/tests/ports/webassembly/globals.mjs
@@ -0,0 +1,13 @@
+// Test accessing Python globals dict via mp.globals.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython("x = 1");
+console.log(mp.globals.get("x"));
+
+mp.globals.set("y", 2);
+mp.runPython("print(y)");
+
+mp.runPython("print('y' in globals())");
+mp.globals.delete("y");
+mp.runPython("print('y' in globals())");
diff --git a/tests/ports/webassembly/globals.mjs.exp b/tests/ports/webassembly/globals.mjs.exp
new file mode 100644
index 0000000000000..a118c13fecba9
--- /dev/null
+++ b/tests/ports/webassembly/globals.mjs.exp
@@ -0,0 +1,4 @@
+1
+2
+True
+False
diff --git a/tests/ports/webassembly/heap_expand.mjs b/tests/ports/webassembly/heap_expand.mjs
new file mode 100644
index 0000000000000..2cf7c07b0b823
--- /dev/null
+++ b/tests/ports/webassembly/heap_expand.mjs
@@ -0,0 +1,15 @@
+// Test expanding the MicroPython GC heap.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+import gc
+bs = []
+for i in range(24):
+ b = bytearray(1 << i)
+ bs.append(b)
+ gc.collect()
+ print(gc.mem_free())
+for b in bs:
+ print(len(b))
+`);
diff --git a/tests/ports/webassembly/heap_expand.mjs.exp b/tests/ports/webassembly/heap_expand.mjs.exp
new file mode 100644
index 0000000000000..ee14908409d86
--- /dev/null
+++ b/tests/ports/webassembly/heap_expand.mjs.exp
@@ -0,0 +1,48 @@
+135241360
+135241328
+135241296
+135241264
+135241216
+135241168
+135241088
+135240944
+135240640
+135240112
+135239072
+135237008
+135232896
+135224688
+135208288
+135175504
+135109888
+134978800
+134716640
+135216848
+136217216
+138218032
+142219616
+150222864
+1
+2
+4
+8
+16
+32
+64
+128
+256
+512
+1024
+2048
+4096
+8192
+16384
+32768
+65536
+131072
+262144
+524288
+1048576
+2097152
+4194304
+8388608
diff --git a/tests/ports/webassembly/jsffi_create_proxy.mjs b/tests/ports/webassembly/jsffi_create_proxy.mjs
new file mode 100644
index 0000000000000..5f04bf44d7e21
--- /dev/null
+++ b/tests/ports/webassembly/jsffi_create_proxy.mjs
@@ -0,0 +1,15 @@
+// Test jsffi.create_proxy().
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+import jsffi
+x = jsffi.create_proxy(1)
+print(x)
+y = jsffi.create_proxy([2])
+print(y)
+`);
+console.log(mp.globals.get("x"));
+console.log(mp.PyProxy.toJs(mp.globals.get("x")));
+console.log(mp.globals.get("y"));
+console.log(mp.PyProxy.toJs(mp.globals.get("y")));
diff --git a/tests/ports/webassembly/jsffi_create_proxy.mjs.exp b/tests/ports/webassembly/jsffi_create_proxy.mjs.exp
new file mode 100644
index 0000000000000..a3b38a78b1132
--- /dev/null
+++ b/tests/ports/webassembly/jsffi_create_proxy.mjs.exp
@@ -0,0 +1,6 @@
+1
+
+1
+1
+PyProxy { _ref: 3 }
+[ 2 ]
diff --git a/tests/ports/webassembly/jsffi_to_js.mjs b/tests/ports/webassembly/jsffi_to_js.mjs
new file mode 100644
index 0000000000000..714af6b629937
--- /dev/null
+++ b/tests/ports/webassembly/jsffi_to_js.mjs
@@ -0,0 +1,28 @@
+// Test jsffi.to_js().
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+import jsffi
+x = jsffi.to_js(1)
+print(x)
+y = jsffi.to_js([2])
+print(y)
+z = jsffi.to_js({"three":3})
+print(z)
+`);
+
+const x = mp.globals.get("x");
+const y = mp.globals.get("y");
+const z = mp.globals.get("z");
+
+console.log(Array.isArray(x));
+console.log(x);
+
+console.log(Array.isArray(y));
+console.log(y);
+console.log(Reflect.ownKeys(y));
+
+console.log(Array.isArray(z));
+console.log(z);
+console.log(Reflect.ownKeys(z));
diff --git a/tests/ports/webassembly/jsffi_to_js.mjs.exp b/tests/ports/webassembly/jsffi_to_js.mjs.exp
new file mode 100644
index 0000000000000..399dd0aa8c9cb
--- /dev/null
+++ b/tests/ports/webassembly/jsffi_to_js.mjs.exp
@@ -0,0 +1,11 @@
+1
+
+
+false
+1
+true
+[ 2 ]
+[ '0', 'length' ]
+false
+{ three: 3 }
+[ 'three' ]
diff --git a/tests/ports/webassembly/override_new.mjs b/tests/ports/webassembly/override_new.mjs
new file mode 100644
index 0000000000000..f5d64d7d11379
--- /dev/null
+++ b/tests/ports/webassembly/override_new.mjs
@@ -0,0 +1,33 @@
+// Test overriding .new() on a JavaScript class.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+globalThis.MyClass1 = class {
+ new() {
+ console.log("MyClass1 new");
+ return 1;
+ }
+};
+
+globalThis.MyClass2 = class {
+ static new() {
+ console.log("MyClass2 static new");
+ return 2;
+ }
+ new() {
+ console.log("MyClass2 new");
+ return 3;
+ }
+};
+
+globalThis.myClass2Instance = new globalThis.MyClass2();
+
+mp.runPython(`
+ import js
+
+ print(type(js.MyClass1.new()))
+ print(js.MyClass1.new().new())
+
+ print(js.MyClass2.new())
+ print(js.myClass2Instance.new())
+`);
diff --git a/tests/ports/webassembly/override_new.mjs.exp b/tests/ports/webassembly/override_new.mjs.exp
new file mode 100644
index 0000000000000..2efb669714ed4
--- /dev/null
+++ b/tests/ports/webassembly/override_new.mjs.exp
@@ -0,0 +1,7 @@
+
+MyClass1 new
+1
+MyClass2 static new
+2
+MyClass2 new
+3
diff --git a/tests/ports/webassembly/promise_with_resolvers.mjs b/tests/ports/webassembly/promise_with_resolvers.mjs
new file mode 100644
index 0000000000000..a2c6d509a6095
--- /dev/null
+++ b/tests/ports/webassembly/promise_with_resolvers.mjs
@@ -0,0 +1,23 @@
+// Test polyfill of a method on a built-in.
+
+// Implement Promise.withResolvers, and make sure it has a unique name so
+// the test below is guaranteed to use this version.
+Promise.withResolversCustom = function withResolversCustom() {
+ let a;
+ let b;
+ const c = new this((resolve, reject) => {
+ a = resolve;
+ b = reject;
+ });
+ return { resolve: a, reject: b, promise: c };
+};
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+ from js import Promise
+
+ deferred = Promise.withResolversCustom()
+ deferred.promise.then(print)
+ deferred.resolve('OK')
+`);
diff --git a/tests/ports/webassembly/promise_with_resolvers.mjs.exp b/tests/ports/webassembly/promise_with_resolvers.mjs.exp
new file mode 100644
index 0000000000000..d86bac9de59ab
--- /dev/null
+++ b/tests/ports/webassembly/promise_with_resolvers.mjs.exp
@@ -0,0 +1 @@
+OK
diff --git a/tests/ports/webassembly/py_proxy_delete.mjs b/tests/ports/webassembly/py_proxy_delete.mjs
new file mode 100644
index 0000000000000..2afea0b9f2221
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_delete.mjs
@@ -0,0 +1,30 @@
+// Test `delete .` on the JavaScript side, which tests PyProxy.deleteProperty.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+class A:
+ pass
+x = A()
+x.foo = 1
+y = []
+`);
+
+const x = mp.globals.get("x");
+const y = mp.globals.get("y");
+
+// Should pass.
+// biome-ignore lint/performance/noDelete: test delete statement
+delete x.foo;
+
+mp.runPython(`
+print(hasattr(x, "foo"))
+`);
+
+// Should fail, can't delete attributes on MicroPython lists.
+try {
+ // biome-ignore lint/performance/noDelete: test delete statement
+ delete y.sort;
+} catch (error) {
+ console.log(error.message);
+}
diff --git a/tests/ports/webassembly/py_proxy_delete.mjs.exp b/tests/ports/webassembly/py_proxy_delete.mjs.exp
new file mode 100644
index 0000000000000..8eb9ad1501419
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_delete.mjs.exp
@@ -0,0 +1,2 @@
+False
+'deleteProperty' on proxy: trap returned falsish for property 'sort'
diff --git a/tests/ports/webassembly/py_proxy_dict.mjs b/tests/ports/webassembly/py_proxy_dict.mjs
new file mode 100644
index 0000000000000..201f179ef377b
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_dict.mjs
@@ -0,0 +1,34 @@
+// Test passing a Python dict into JavaScript, it should act like a JS object.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+x = {"a": 1, "b": 2}
+`);
+
+const x = mp.globals.get("x");
+
+// Test has, get, keys/iteration.
+console.log("a" in x, "b" in x, "c" in x);
+console.log(x.a, x.b);
+for (const k in x) {
+ console.log(k, x[k]);
+}
+console.log(Object.keys(x));
+console.log(Reflect.ownKeys(x));
+
+// Test set.
+x.c = 3;
+console.log(Object.keys(x));
+
+// Test delete.
+// biome-ignore lint/performance/noDelete: test delete statement
+delete x.b;
+console.log(Object.keys(x));
+
+// Make sure changes on the JavaScript side are reflected in Python.
+mp.runPython(`
+print(x["a"])
+print("b" in x)
+print(x["c"])
+`);
diff --git a/tests/ports/webassembly/py_proxy_dict.mjs.exp b/tests/ports/webassembly/py_proxy_dict.mjs.exp
new file mode 100644
index 0000000000000..f0e15034bd71c
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_dict.mjs.exp
@@ -0,0 +1,11 @@
+true true false
+1 2
+a 1
+b 2
+[ 'a', 'b' ]
+[ 'a', 'b' ]
+[ 'a', 'c', 'b' ]
+[ 'a', 'c' ]
+1
+False
+3
diff --git a/tests/ports/webassembly/py_proxy_has.mjs b/tests/ports/webassembly/py_proxy_has.mjs
new file mode 100644
index 0000000000000..8881776fdbe51
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_has.mjs
@@ -0,0 +1,11 @@
+// Test ` in ` on the JavaScript side, which tests PyProxy.has.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+x = []
+`);
+
+const x = mp.globals.get("x");
+console.log("no_exist" in x);
+console.log("sort" in x);
diff --git a/tests/ports/webassembly/py_proxy_has.mjs.exp b/tests/ports/webassembly/py_proxy_has.mjs.exp
new file mode 100644
index 0000000000000..1d474d5255713
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_has.mjs.exp
@@ -0,0 +1,2 @@
+false
+true
diff --git a/tests/ports/webassembly/py_proxy_own_keys.mjs b/tests/ports/webassembly/py_proxy_own_keys.mjs
new file mode 100644
index 0000000000000..bbf5b4f01c056
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_own_keys.mjs
@@ -0,0 +1,11 @@
+// Test `Reflect.ownKeys()` on the JavaScript side, which tests PyProxy.ownKeys.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+x = []
+y = {"a": 1}
+`);
+
+console.log(Reflect.ownKeys(mp.globals.get("x")));
+console.log(Reflect.ownKeys(mp.globals.get("y")));
diff --git a/tests/ports/webassembly/py_proxy_own_keys.mjs.exp b/tests/ports/webassembly/py_proxy_own_keys.mjs.exp
new file mode 100644
index 0000000000000..313d06d4d75da
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_own_keys.mjs.exp
@@ -0,0 +1,9 @@
+[
+ 'append', 'clear',
+ 'copy', 'count',
+ 'extend', 'index',
+ 'insert', 'pop',
+ 'remove', 'reverse',
+ 'sort'
+]
+[ 'a' ]
diff --git a/tests/ports/webassembly/py_proxy_set.mjs b/tests/ports/webassembly/py_proxy_set.mjs
new file mode 100644
index 0000000000000..30360a847d806
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_set.mjs
@@ -0,0 +1,27 @@
+// Test `. = ` on the JavaScript side, which tests PyProxy.set.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+class A:
+ pass
+x = A()
+y = []
+`);
+
+const x = mp.globals.get("x");
+const y = mp.globals.get("y");
+
+// Should pass.
+x.foo = 1;
+
+mp.runPython(`
+print(x.foo)
+`);
+
+// Should fail, can't set attributes on MicroPython lists.
+try {
+ y.bar = 1;
+} catch (error) {
+ console.log(error.message);
+}
diff --git a/tests/ports/webassembly/py_proxy_set.mjs.exp b/tests/ports/webassembly/py_proxy_set.mjs.exp
new file mode 100644
index 0000000000000..e1d995156341e
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_set.mjs.exp
@@ -0,0 +1,2 @@
+1
+'set' on proxy: trap returned falsish for property 'bar'
diff --git a/tests/ports/webassembly/py_proxy_to_js.mjs b/tests/ports/webassembly/py_proxy_to_js.mjs
new file mode 100644
index 0000000000000..f9fce606f1fdf
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_to_js.mjs
@@ -0,0 +1,20 @@
+// Test PyProxy.toJs().
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+mp.runPython(`
+a = 1
+b = (1, 2, 3)
+c = [None, True, 1.2]
+d = {"one": 1, "tuple": b, "list": c}
+`);
+
+const py_a = mp.globals.get("a");
+const py_b = mp.globals.get("b");
+const py_c = mp.globals.get("c");
+const py_d = mp.globals.get("d");
+
+console.log(py_a instanceof mp.PyProxy, mp.PyProxy.toJs(py_a));
+console.log(py_b instanceof mp.PyProxy, mp.PyProxy.toJs(py_b));
+console.log(py_c instanceof mp.PyProxy, mp.PyProxy.toJs(py_c));
+console.log(py_d instanceof mp.PyProxy, mp.PyProxy.toJs(py_d));
diff --git a/tests/ports/webassembly/py_proxy_to_js.mjs.exp b/tests/ports/webassembly/py_proxy_to_js.mjs.exp
new file mode 100644
index 0000000000000..279df7bdfbb84
--- /dev/null
+++ b/tests/ports/webassembly/py_proxy_to_js.mjs.exp
@@ -0,0 +1,4 @@
+false 1
+true [ 1, 2, 3 ]
+true [ null, true, 1.2 ]
+true { tuple: [ 1, 2, 3 ], one: 1, list: [ null, true, 1.2 ] }
diff --git a/tests/ports/webassembly/register_js_module.js b/tests/ports/webassembly/register_js_module.js
new file mode 100644
index 0000000000000..b512f2c0dd465
--- /dev/null
+++ b/tests/ports/webassembly/register_js_module.js
@@ -0,0 +1,6 @@
+import(process.argv[2]).then((mp) => {
+ mp.loadMicroPython().then((py) => {
+ py.registerJsModule("js_module", { y: 2 });
+ py.runPython("import js_module; print(js_module); print(js_module.y)");
+ });
+});
diff --git a/tests/ports/webassembly/register_js_module.js.exp b/tests/ports/webassembly/register_js_module.js.exp
new file mode 100644
index 0000000000000..bb45f4ce00285
--- /dev/null
+++ b/tests/ports/webassembly/register_js_module.js.exp
@@ -0,0 +1,2 @@
+
+2
diff --git a/tests/ports/webassembly/run_python_async.mjs b/tests/ports/webassembly/run_python_async.mjs
new file mode 100644
index 0000000000000..44f2a90ab175d
--- /dev/null
+++ b/tests/ports/webassembly/run_python_async.mjs
@@ -0,0 +1,115 @@
+// Test runPythonAsync() and top-level await in Python.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+/**********************************************************/
+// Using only promise objects, no await's.
+
+console.log("= TEST 1 ==========");
+
+globalThis.p = new Promise((resolve, reject) => {
+ resolve(123);
+});
+
+console.log(1);
+
+mp.runPython(`
+import js
+print(js.p)
+print("py 1")
+print(js.p.then(lambda x: print("resolved", x)))
+print("py 2")
+`);
+
+console.log(2);
+
+// Let the promise resolve.
+await globalThis.p;
+
+console.log(3);
+
+/**********************************************************/
+// Using setTimeout to resolve the promise.
+
+console.log("= TEST 2 ==========");
+
+globalThis.p = new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve(123);
+ console.log("setTimeout resolved");
+ }, 100);
+});
+
+console.log(1);
+
+mp.runPython(`
+import js
+print(js.p)
+print("py 1")
+print(js.p.then(lambda x: print("resolved", x)))
+print("py 2")
+`);
+
+console.log(2);
+
+// Let the promise resolve.
+await globalThis.p;
+
+console.log(3);
+
+/**********************************************************/
+// Using setTimeout and await within Python.
+
+console.log("= TEST 3 ==========");
+
+globalThis.p = new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve(123);
+ console.log("setTimeout resolved");
+ }, 100);
+});
+
+console.log(1);
+
+const ret3 = await mp.runPythonAsync(`
+import js
+print("py 1")
+print("resolved value:", await js.p)
+print("py 2")
+`);
+
+console.log(2, ret3);
+
+/**********************************************************/
+// Multiple setTimeout's and await's within Python.
+
+console.log("= TEST 4 ==========");
+
+globalThis.p1 = new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve(123);
+ console.log("setTimeout A resolved");
+ }, 100);
+});
+
+globalThis.p2 = new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve(456);
+ console.log("setTimeout B resolved");
+ }, 200);
+});
+
+console.log(1);
+
+const ret4 = await mp.runPythonAsync(`
+import js
+print("py 1")
+print("resolved value:", await js.p1)
+print("py 2")
+print("resolved value:", await js.p1)
+print("py 3")
+print("resolved value:", await js.p2)
+print("py 4")
+`);
+
+console.log(2, ret4);
diff --git a/tests/ports/webassembly/run_python_async.mjs.exp b/tests/ports/webassembly/run_python_async.mjs.exp
new file mode 100644
index 0000000000000..f441bc5cf1db6
--- /dev/null
+++ b/tests/ports/webassembly/run_python_async.mjs.exp
@@ -0,0 +1,38 @@
+= TEST 1 ==========
+1
+
+py 1
+
+py 2
+2
+resolved 123
+3
+= TEST 2 ==========
+1
+
+py 1
+
+py 2
+2
+setTimeout resolved
+resolved 123
+3
+= TEST 3 ==========
+1
+py 1
+setTimeout resolved
+resolved value: 123
+py 2
+2 null
+= TEST 4 ==========
+1
+py 1
+setTimeout A resolved
+resolved value: 123
+py 2
+resolved value: 123
+py 3
+setTimeout B resolved
+resolved value: 456
+py 4
+2 null
diff --git a/tests/ports/webassembly/run_python_async2.mjs b/tests/ports/webassembly/run_python_async2.mjs
new file mode 100644
index 0000000000000..87067e6e8c403
--- /dev/null
+++ b/tests/ports/webassembly/run_python_async2.mjs
@@ -0,0 +1,42 @@
+// Test runPythonAsync() and top-level await in Python, with multi-level awaits.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+// simulate a 2-step resolution of the string "OK".
+await mp.runPythonAsync(`
+import js
+
+def _timeout(resolve, _):
+ js.setTimeout(resolve, 100)
+
+def _fetch():
+ return js.Promise.new(_timeout)
+
+async def _text(promise):
+ if not promise._response:
+ print("_text await start")
+ await promise
+ print("_text awaited end")
+ ret = await promise._response.text()
+ return ret
+
+class _Response:
+ async def text(self):
+ print('_Response.text start')
+ await js.Promise.new(_timeout)
+ print('_Response.text end')
+ return "OK"
+
+def _response(promise):
+ promise._response = _Response()
+ return promise._response
+
+def fetch(url):
+ promise = _fetch().then(lambda *_: _response(promise))
+ promise._response = None
+ promise.text = lambda: _text(promise)
+ return promise
+
+print(await fetch("config.json").text())
+print(await (await fetch("config.json")).text())
+`);
diff --git a/tests/ports/webassembly/run_python_async2.mjs.exp b/tests/ports/webassembly/run_python_async2.mjs.exp
new file mode 100644
index 0000000000000..60d68c5d3b55c
--- /dev/null
+++ b/tests/ports/webassembly/run_python_async2.mjs.exp
@@ -0,0 +1,8 @@
+_text await start
+_text awaited end
+_Response.text start
+_Response.text end
+OK
+_Response.text start
+_Response.text end
+OK
diff --git a/tests/ports/webassembly/this_behaviour.mjs b/tests/ports/webassembly/this_behaviour.mjs
new file mode 100644
index 0000000000000..6411b6ce63485
--- /dev/null
+++ b/tests/ports/webassembly/this_behaviour.mjs
@@ -0,0 +1,24 @@
+// Test "this" behaviour.
+
+const mp = await (await import(process.argv[2])).loadMicroPython();
+
+// "this" should be undefined.
+globalThis.func0 = function () {
+ console.log("func0", this);
+};
+mp.runPython("import js; js.func0()");
+
+globalThis.func1 = function (a) {
+ console.log("func1", a, this);
+};
+mp.runPython("import js; js.func1(123)");
+
+globalThis.func2 = function (a, b) {
+ console.log("func2", a, b, this);
+};
+mp.runPython("import js; js.func2(123, 456)");
+
+globalThis.func3 = function (a, b, c) {
+ console.log("func3", a, b, c, this);
+};
+mp.runPython("import js; js.func3(123, 456, 789)");
diff --git a/tests/ports/webassembly/this_behaviour.mjs.exp b/tests/ports/webassembly/this_behaviour.mjs.exp
new file mode 100644
index 0000000000000..4762026ab2027
--- /dev/null
+++ b/tests/ports/webassembly/this_behaviour.mjs.exp
@@ -0,0 +1,4 @@
+func0 undefined
+func1 123 undefined
+func2 123 456 undefined
+func3 123 456 789 undefined
diff --git a/tests/ports/webassembly/various.js b/tests/ports/webassembly/various.js
new file mode 100644
index 0000000000000..e2fa9362c4f3d
--- /dev/null
+++ b/tests/ports/webassembly/various.js
@@ -0,0 +1,38 @@
+import(process.argv[2]).then((mp) => {
+ mp.loadMicroPython().then((py) => {
+ globalThis.jsadd = (x, y) => {
+ return x + y;
+ };
+ py.runPython("import js; print(js); print(js.jsadd(4, 9))");
+
+ py.runPython(
+ "def set_timeout_callback():\n print('set_timeout_callback')",
+ );
+ py.runPython("import js; js.setTimeout(set_timeout_callback, 100)");
+
+ py.runPython("obj = js.Object(a=1)");
+ console.log("main", py.pyimport("__main__").obj);
+
+ console.log("=======");
+ py.runPython(`
+ from js import Array, Promise, Reflect
+
+ def callback(resolve, reject):
+ resolve('OK1')
+
+ p = Reflect.construct(Promise, Array(callback))
+ p.then(print)
+ `);
+
+ console.log("=======");
+ py.runPython(`
+ from js import Promise
+
+ def callback(resolve, reject):
+ resolve('OK2')
+
+ p = Promise.new(callback)
+ p.then(print)
+ `);
+ });
+});
diff --git a/tests/ports/webassembly/various.js.exp b/tests/ports/webassembly/various.js.exp
new file mode 100644
index 0000000000000..502ab2cccf000
--- /dev/null
+++ b/tests/ports/webassembly/various.js.exp
@@ -0,0 +1,8 @@
+
+13
+main { a: 1 }
+=======
+=======
+OK1
+OK2
+set_timeout_callback
diff --git a/tests/run-tests.py b/tests/run-tests.py
index 70279d379df60..4f55cdd39842e 100755
--- a/tests/run-tests.py
+++ b/tests/run-tests.py
@@ -308,7 +308,9 @@ def send_get(what):
else:
# run via pyboard interface
- had_crash, output_mupy = run_script_on_remote_target(pyb, args, test_file, is_special)
+ had_crash, output_mupy = pyb.run_script_on_remote_target(
+ args, test_file_abspath, is_special
+ )
# canonical form for all ports/platforms is to use \n for end-of-line
output_mupy = output_mupy.replace(b"\r\n", b"\n")
@@ -393,6 +395,51 @@ def value(self):
return self._value
+class PyboardNodeRunner:
+ def __init__(self):
+ mjs = os.getenv("MICROPY_MICROPYTHON_MJS")
+ if mjs is None:
+ mjs = base_path("../ports/webassembly/build-standard/micropython.mjs")
+ else:
+ mjs = os.path.abspath(mjs)
+ self.micropython_mjs = mjs
+
+ def close(self):
+ pass
+
+ def run_script_on_remote_target(self, args, test_file, is_special):
+ cwd = os.path.dirname(test_file)
+
+ # Create system command list.
+ cmdlist = ["node"]
+ if test_file.endswith(".py"):
+ # Run a Python script indirectly via "node micropython.mjs ".
+ cmdlist.append(self.micropython_mjs)
+ if args.heapsize is not None:
+ cmdlist.extend(["-X", "heapsize=" + args.heapsize])
+ cmdlist.append(test_file)
+ else:
+ # Run a js/mjs script directly with Node, passing in the path to micropython.mjs.
+ cmdlist.append(test_file)
+ cmdlist.append(self.micropython_mjs)
+
+ # Run the script.
+ try:
+ had_crash = False
+ output_mupy = subprocess.check_output(
+ cmdlist, stderr=subprocess.STDOUT, timeout=TEST_TIMEOUT, cwd=cwd
+ )
+ except subprocess.CalledProcessError as er:
+ had_crash = True
+ output_mupy = er.output + b"CRASH"
+ except subprocess.TimeoutExpired as er:
+ had_crash = True
+ output_mupy = (er.output or b"") + b"TIMEOUT"
+
+ # Return the results.
+ return had_crash, output_mupy
+
+
def run_tests(pyb, tests, args, result_dir, num_threads=1):
test_count = ThreadSafeCounter()
testcase_count = ThreadSafeCounter()
@@ -631,6 +678,20 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1):
) # RA fsp rtc function doesn't support nano sec info
elif args.target == "qemu-arm":
skip_tests.add("misc/print_exception.py") # requires sys stdfiles
+ elif args.target == "webassembly":
+ skip_tests.add("basics/string_format_modulo.py") # can't print nulls to stdout
+ skip_tests.add("basics/string_strip.py") # can't print nulls to stdout
+ skip_tests.add("extmod/binascii_a2b_base64.py")
+ skip_tests.add("extmod/re_stack_overflow.py")
+ skip_tests.add("extmod/time_res.py")
+ skip_tests.add("extmod/vfs_posix.py")
+ skip_tests.add("extmod/vfs_posix_enoent.py")
+ skip_tests.add("extmod/vfs_posix_paths.py")
+ skip_tests.add("extmod/vfs_userfs.py")
+ skip_tests.add("micropython/emg_exc.py")
+ skip_tests.add("micropython/extreme_exc.py")
+ skip_tests.add("micropython/heapalloc_exc_compressed_emg_exc.py")
+ skip_tests.add("micropython/import_mpy_invalid.py")
# Some tests are known to fail on 64-bit machines
if pyb is None and platform.architecture()[0] == "64bit":
@@ -977,6 +1038,7 @@ def main():
LOCAL_TARGETS = (
"unix",
"qemu-arm",
+ "webassembly",
)
EXTERNAL_TARGETS = (
"pyboard",
@@ -997,6 +1059,8 @@ def main():
args.mpy_cross_flags = "-march=host"
elif args.target == "qemu-arm":
args.mpy_cross_flags = "-march=armv7m"
+ if args.target == "webassembly":
+ pyb = PyboardNodeRunner()
elif args.target in EXTERNAL_TARGETS:
global pyboard
sys.path.append(base_path("../tools"))
@@ -1015,6 +1079,7 @@ def main():
args.mpy_cross_flags = "-march=armv7m"
pyb = pyboard.Pyboard(args.device, args.baudrate, args.user, args.password)
+ pyboard.Pyboard.run_script_on_remote_target = run_script_on_remote_target
pyb.enter_raw_repl()
else:
raise ValueError("target must be one of %s" % ", ".join(LOCAL_TARGETS + EXTERNAL_TARGETS))
@@ -1032,6 +1097,10 @@ def main():
else:
tests = []
elif len(args.files) == 0:
+ test_extensions = ("*.py",)
+ if args.target == "webassembly":
+ test_extensions += ("*.js", "*.mjs")
+
if args.test_dirs is None:
test_dirs = (
"basics",
@@ -1072,12 +1141,16 @@ def main():
"inlineasm",
"ports/qemu-arm",
)
+ elif args.target == "webassembly":
+ test_dirs += ("float", "ports/webassembly")
else:
# run tests from these directories
test_dirs = args.test_dirs
tests = sorted(
test_file
- for test_files in (glob("{}/*.py".format(dir)) for dir in test_dirs)
+ for test_files in (
+ glob(os.path.join(dir, ext)) for dir in test_dirs for ext in test_extensions
+ )
for test_file in test_files
)
else:
diff --git a/tools/ci.sh b/tools/ci.sh
index f94f23893b710..ad5e79c4ad07c 100755
--- a/tools/ci.sh
+++ b/tools/ci.sh
@@ -178,18 +178,19 @@ function ci_esp8266_build {
# ports/webassembly
function ci_webassembly_setup {
+ npm install terser
git clone https://github.com/emscripten-core/emsdk.git
(cd emsdk && ./emsdk install latest && ./emsdk activate latest)
}
function ci_webassembly_build {
source emsdk/emsdk_env.sh
- make ${MAKEOPTS} -C ports/webassembly
+ make ${MAKEOPTS} -C ports/webassembly VARIANT=pyscript submodules
+ make ${MAKEOPTS} -C ports/webassembly VARIANT=pyscript
}
function ci_webassembly_run_tests {
- # This port is very slow at running, so only run a few of the tests.
- (cd tests && MICROPY_MICROPYTHON=../ports/webassembly/node_run.sh ./run-tests.py -j1 basics/builtin_*.py)
+ make -C ports/webassembly VARIANT=pyscript test_min
}
########################################################################################
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy