diff --git a/Cargo.toml b/Cargo.toml index 9c17cb7..8a28b5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ crate-type = ["cdylib", "rlib"] [dependencies] chumsky = "0.10.1" imbl = "3.0.0" -ran = "2.0.1" num-derive = "0.4.2" num-traits = "0.2.19" regex = "1.11.1" @@ -19,5 +18,4 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4.50" serde = {version = "1.0", features = ["derive"]} serde_json = "1.0" -tokio = {version = "1.45.1", features = ["macros", "rt-multi-thread"]} - +console_error_panic_hook = "0.1.7" diff --git a/assets/test_prelude.ld b/assets/test_prelude.ld index 71b29a9..e59380c 100644 --- a/assets/test_prelude.ld +++ b/assets/test_prelude.ld @@ -1,3 +1,7 @@ +&&& buffers: shared memory with Rust +box console = [] +box input = "" + & the very base: know something's type fn type { "Returns a keyword representing the type of the value passed in." @@ -408,8 +412,6 @@ fn to_number { (num as :string) -> base :number (num) } -box console = [] - fn print! { "Sends a text representation of Ludus values to the console." (...args) -> { @@ -1269,6 +1271,9 @@ fn sleep! { link! + console + input + abs abs add diff --git a/justfile b/justfile index daf09c8..52fd6b2 100644 --- a/justfile +++ b/justfile @@ -1,11 +1,23 @@ -wasm: +wasm: && clean-wasm-pack + # build with wasm-pack wasm-pack build --target web - rm pkg/.gitignore - cp pkg/rudus.js pkg/rudus.js.backup - echo 'import {io} from "../worker.js"' > rudus.js - cat rudus.js.backup | tail -n+2>> rudus.js - rm rudus.js.backup + +wasm-dev: && clean-wasm-pack + wasm-pack build --dev --target web + +clean-wasm-pack: + # delete cruft from wasm-pack + rm pkg/.gitignore pkg/package.json pkg/README.md rm -rf pkg/snippets + # fix imports of rudus.js + cp pkg/rudus.js pkg/rudus.js.backup + echo 'import { io } from "./worker.js"' > pkg/rudus.js + cat pkg/rudus.js.backup | tail -n+2>> pkg/rudus.js + rm pkg/rudus.js.backup + + +serve: + miniserve pkg && open http://localhost:8080/index.html default: @just --list diff --git a/pkg/.gitignore b/pkg/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/pkg/index.html b/pkg/index.html index 74f29a2..a79fb90 100644 --- a/pkg/index.html +++ b/pkg/index.html @@ -6,13 +6,7 @@ - +

Open the console. All the action's in there.

diff --git a/pkg/ludus.js b/pkg/ludus.js index b26f229..e964c7d 100644 --- a/pkg/ludus.js +++ b/pkg/ludus.js @@ -1,18 +1,72 @@ -import init, {ludus} from "./rudus.js"; +if (window) window.ludus = {run, kill, flush_console, p5, svg, turtle_commands, result, input} +const worker = new Worker("worker.js", {type: "module"}) -let res = null +let outbox = [] -let code = null - -export function run (source) { - code = source - const output = ludus(source) - res = JSON.parse(output) - return res +worker.onmessage = async (e) => { + let msgs + try { + msgs = JSON.parse(e.data) + } catch { + console.log(e.data) + throw Error("bad json from Ludus") + } + for (const msg of msgs) { + console.log("Main: message received from worker:", msg); + switch (msg.verb) { + case "complete": { + console.log("completed ludus run!") + console.log("with", msg.data) + res = msg.data + running = false + break + } + case "console": { + console.log("console msg from msg.data") + console.log(msg.data) + break + } + } + } } -export function stdout () { +let res = null +let code = null +let running = false +let io_interval_id = null + +const io_poller = () => { + if (io_interval_id && !running) { + clearInterval(io_interval_id) + return + } + worker.postMessage(outbox) + outbox = [] +} + +function poll_io () { + io_interval_id = setInterval(io_poller, 10) +} + +export function run (source) { + if (running) "TODO: handle this? should not be running" + running = true + code = source + outbox.push({verb: "run", data: source}) + poll_io() +} + +export function kill () { + running = false + outbox.push({verb: "kill"}) +} + +export function input (text) { + outbox.push({verb: "input", data: text}) +} + +export function flush_console () { if (!res) return "" return res.io.stdout.data } @@ -375,25 +429,4 @@ export function p5 (commands) { return p5_calls } -window.ludus = {run, console, p5, svg, stdout, turtle_commands, result} -await init() - -const worker = new Worker("worker.js", {type: "module"}) - -let outbox = {} - -setInterval(() => { - worker.postMessage(outbox) - outbox = {} -}) - -worker.onmessage = async (msgs) => { - for (const msg of msgs) { - switch (msg[0]) { - case "stdout": { - stdout = msg[1] - } - } - } -} diff --git a/pkg/rudus.d.ts b/pkg/rudus.d.ts index fc664bf..12ab49a 100644 --- a/pkg/rudus.d.ts +++ b/pkg/rudus.d.ts @@ -1,16 +1,21 @@ /* tslint:disable */ /* eslint-disable */ -export function ludus(src: string): string; +export function ludus(src: string): Promise; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { readonly memory: WebAssembly.Memory; - readonly ludus: (a: number, b: number) => [number, number]; - readonly __wbindgen_export_0: WebAssembly.Table; + readonly ludus: (a: number, b: number) => any; + readonly __wbindgen_exn_store: (a: number) => void; + readonly __externref_table_alloc: () => number; + readonly __wbindgen_export_2: WebAssembly.Table; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; - readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __wbindgen_export_6: WebAssembly.Table; + readonly closure347_externref_shim: (a: number, b: number, c: any) => void; + readonly closure371_externref_shim: (a: number, b: number, c: any, d: any) => void; readonly __wbindgen_start: () => void; } diff --git a/pkg/rudus.js b/pkg/rudus.js index 52f2363..ccdb85c 100644 --- a/pkg/rudus.js +++ b/pkg/rudus.js @@ -1,6 +1,41 @@ +import { io } from "./worker.js" + let wasm; -let WASM_VECTOR_LEN = 0; +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_export_2.set(idx, obj); + return idx; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + const idx = addToExternrefTable0(e); + wasm.__wbindgen_exn_store(idx); + } +} + +function logError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + let error = (function () { + try { + return e instanceof Error ? `${e.message}\n\nStack:\n${e.stack}` : e.toString(); + } catch(_) { + return ""; + } + }()); + console.error("wasm-bindgen: imported JS function that was not marked as `catch` threw an error:", error); + throw e; + } +} + +const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); + +if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; let cachedUint8ArrayMemory0 = null; @@ -11,6 +46,13 @@ function getUint8ArrayMemory0() { return cachedUint8ArrayMemory0; } +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +let WASM_VECTOR_LEN = 0; + const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' @@ -28,6 +70,8 @@ const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' function passStringToWasm0(arg, malloc, realloc) { + if (typeof(arg) !== 'string') throw new Error(`expected a string argument, found ${typeof(arg)}`); + if (realloc === undefined) { const buf = cachedTextEncoder.encode(arg); const ptr = malloc(buf.length, 1) >>> 0; @@ -56,7 +100,7 @@ function passStringToWasm0(arg, malloc, realloc) { ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); const ret = encodeString(arg, view); - + if (ret.read !== arg.length) throw new Error('failed to pass whole string'); offset += ret.written; ptr = realloc(ptr, len, offset, 1) >>> 0; } @@ -65,31 +109,144 @@ function passStringToWasm0(arg, malloc, realloc) { return ptr; } -const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); +let cachedDataViewMemory0 = null; -if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} -function getStringFromWasm0(ptr, len) { - ptr = ptr >>> 0; - return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +function isLikeNone(x) { + return x === undefined || x === null; +} + +function _assertBoolean(n) { + if (typeof(n) !== 'boolean') { + throw new Error(`expected a boolean argument, found ${typeof(n)}`); + } +} + +const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(state => { + wasm.__wbindgen_export_6.get(state.dtor)(state.a, state.b) +}); + +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + if (--state.cnt === 0) { + wasm.__wbindgen_export_6.get(state.dtor)(a, state.b); + CLOSURE_DTORS.unregister(state); + } else { + state.a = a; + } + } + }; + real.original = state; + CLOSURE_DTORS.register(real, state, state); + return real; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches && builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; } /** * @param {string} src - * @returns {string} + * @returns {Promise} */ export function ludus(src) { - let deferred2_0; - let deferred2_1; - try { - const ptr0 = passStringToWasm0(src, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.ludus(ptr0, len0); - deferred2_0 = ret[0]; - deferred2_1 = ret[1]; - return getStringFromWasm0(ret[0], ret[1]); - } finally { - wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); - } + const ptr0 = passStringToWasm0(src, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.ludus(ptr0, len0); + return ret; +} + +function _assertNum(n) { + if (typeof(n) !== 'number') throw new Error(`expected a number argument, found ${typeof(n)}`); +} +function __wbg_adapter_22(arg0, arg1, arg2) { + _assertNum(arg0); + _assertNum(arg1); + wasm.closure347_externref_shim(arg0, arg1, arg2); +} + +function __wbg_adapter_50(arg0, arg1, arg2, arg3) { + _assertNum(arg0); + _assertNum(arg1); + wasm.closure371_externref_shim(arg0, arg1, arg2, arg3); } async function __wbg_load(module, imports) { @@ -126,8 +283,150 @@ async function __wbg_load(module, imports) { function __wbg_get_imports() { const imports = {}; imports.wbg = {}; + imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) { + const ret = arg0.call(arg1); + return ret; + }, arguments) }; + imports.wbg.__wbg_call_7cccdd69e0791ae2 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.call(arg1, arg2); + return ret; + }, arguments) }; + imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function() { return logError(function (arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } + }, arguments) }; + imports.wbg.__wbg_io_4b41f8089de924df = function() { return logError(function (arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + const ret = io(getStringFromWasm0(arg0, arg1)); + return ret; + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } + }, arguments) }; + imports.wbg.__wbg_log_86d603e98cc11395 = function() { return logError(function (arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.log(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } + }, arguments) }; + imports.wbg.__wbg_log_9e426f8e841e42d3 = function() { return logError(function (arg0, arg1) { + console.log(getStringFromWasm0(arg0, arg1)); + }, arguments) }; + imports.wbg.__wbg_new_23a2665fac83c611 = function() { return logError(function (arg0, arg1) { + try { + var state0 = {a: arg0, b: arg1}; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return __wbg_adapter_50(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + const ret = new Promise(cb0); + return ret; + } finally { + state0.a = state0.b = 0; + } + }, arguments) }; + imports.wbg.__wbg_new_8a6f238a6ece86ea = function() { return logError(function () { + const ret = new Error(); + return ret; + }, arguments) }; + imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function() { return logError(function (arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return ret; + }, arguments) }; + imports.wbg.__wbg_now_8dddb61fa4928554 = function() { return logError(function () { + const ret = Date.now(); + return ret; + }, arguments) }; + imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function() { return logError(function (arg0) { + queueMicrotask(arg0); + }, arguments) }; + imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function() { return logError(function (arg0) { + const ret = arg0.queueMicrotask; + return ret; + }, arguments) }; + imports.wbg.__wbg_random_57c118f142535bb6 = function() { return logError(function () { + const ret = Math.random(); + return ret; + }, arguments) }; + imports.wbg.__wbg_resolve_4851785c9c5f573d = function() { return logError(function (arg0) { + const ret = Promise.resolve(arg0); + return ret; + }, arguments) }; + imports.wbg.__wbg_stack_0ed75d68575b0f3c = function() { return logError(function (arg0, arg1) { + const ret = arg1.stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, arguments) }; + imports.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() { return logError(function () { + const ret = typeof global === 'undefined' ? null : global; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, arguments) }; + imports.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() { return logError(function () { + const ret = typeof globalThis === 'undefined' ? null : globalThis; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, arguments) }; + imports.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() { return logError(function () { + const ret = typeof self === 'undefined' ? null : self; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, arguments) }; + imports.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() { return logError(function () { + const ret = typeof window === 'undefined' ? null : window; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, arguments) }; + imports.wbg.__wbg_then_44b73946d2fb3e7d = function() { return logError(function (arg0, arg1) { + const ret = arg0.then(arg1); + return ret; + }, arguments) }; + imports.wbg.__wbg_then_48b406749878a531 = function() { return logError(function (arg0, arg1, arg2) { + const ret = arg0.then(arg1, arg2); + return ret; + }, arguments) }; + imports.wbg.__wbindgen_cb_drop = function(arg0) { + const obj = arg0.original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + const ret = false; + _assertBoolean(ret); + return ret; + }; + imports.wbg.__wbindgen_closure_wrapper7663 = function() { return logError(function (arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 348, __wbg_adapter_22); + return ret; + }, arguments) }; + imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { + const ret = debugString(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; imports.wbg.__wbindgen_init_externref_table = function() { - const table = wasm.__wbindgen_export_0; + const table = wasm.__wbindgen_export_2; const offset = table.grow(4); table.set(0, undefined); table.set(offset + 0, undefined); @@ -136,6 +435,31 @@ function __wbg_get_imports() { table.set(offset + 3, false); ; }; + imports.wbg.__wbindgen_is_function = function(arg0) { + const ret = typeof(arg0) === 'function'; + _assertBoolean(ret); + return ret; + }; + imports.wbg.__wbindgen_is_undefined = function(arg0) { + const ret = arg0 === undefined; + _assertBoolean(ret); + return ret; + }; + imports.wbg.__wbindgen_string_get = function(arg0, arg1) { + const obj = arg1; + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; return imports; } @@ -147,6 +471,7 @@ function __wbg_init_memory(imports, memory) { function __wbg_finalize_init(instance, module) { wasm = instance.exports; __wbg_init.__wbindgen_wasm_module = module; + cachedDataViewMemory0 = null; cachedUint8ArrayMemory0 = null; diff --git a/pkg/rudus_bg.wasm b/pkg/rudus_bg.wasm index 94f0652..1897f95 100644 Binary files a/pkg/rudus_bg.wasm and b/pkg/rudus_bg.wasm differ diff --git a/pkg/rudus_bg.wasm.d.ts b/pkg/rudus_bg.wasm.d.ts index 0d287b6..9a87573 100644 --- a/pkg/rudus_bg.wasm.d.ts +++ b/pkg/rudus_bg.wasm.d.ts @@ -1,9 +1,14 @@ /* tslint:disable */ /* eslint-disable */ export const memory: WebAssembly.Memory; -export const ludus: (a: number, b: number) => [number, number]; -export const __wbindgen_export_0: WebAssembly.Table; +export const ludus: (a: number, b: number) => any; +export const __wbindgen_exn_store: (a: number) => void; +export const __externref_table_alloc: () => number; +export const __wbindgen_export_2: WebAssembly.Table; +export const __wbindgen_free: (a: number, b: number, c: number) => void; export const __wbindgen_malloc: (a: number, b: number) => number; export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; -export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_export_6: WebAssembly.Table; +export const closure347_externref_shim: (a: number, b: number, c: any) => void; +export const closure371_externref_shim: (a: number, b: number, c: any, d: any) => void; export const __wbindgen_start: () => void; diff --git a/pkg/worker.js b/pkg/worker.js index fcc4212..3631a53 100644 --- a/pkg/worker.js +++ b/pkg/worker.js @@ -1,15 +1,40 @@ -import init from "./rudus.js"; +import init, {ludus} from "./rudus.js"; console.log("Worker: starting Ludus VM.") export function io (out) { - if (Object.keys(out).length > 0) postMessage(out) + if (out.length > 0) postMessage(out) return new Promise((resolve, _) => { - onmessage = (e) => resolve(e.data) + onmessage = (e) => { + console.log(e.data) + resolve(JSON.stringify(e.data)) + } }) } -await init() +let loaded_wasm = false + +async function run(e) { + if (!loaded_wasm) { + await init() + loaded_wasm = true + } + let msgs = e.data + for (const msg of msgs) { + if (msg.verb === "run" && typeof msg.data === 'string') { + console.log("running ludus!") + onmessage = () => {} + let result = await ludus(msg.data) + console.log(result) + onmessage = run + } else { + console.log("Did not get valid startup message. Instead got:") + console.log(e.data) + } + } +} + +onmessage = run console.log("Worker: Ludus VM is running.") diff --git a/sandbox.ld b/sandbox.ld index 4052cc3..08f6528 100644 --- a/sandbox.ld +++ b/sandbox.ld @@ -1,31 +1 @@ -fn agent (val) -> receive { - (:set, new) -> agent (new) - (:get, pid) -> { - send (pid, (:response, val)) - agent (val) - } - (:update, f) -> agent (f (val)) -} - -fn agent/set (pid, val) -> { - send (pid, (:set, val)) - val -} - -fn agent/get (pid) -> { - send (pid, (:get, self ())) - receive { - (:response, val) -> val - } -} - -fn agent/update (pid, f) -> { - send (pid, (:update, f)) - agent/get (pid) -} - -let myagent = spawn! (fn () -> agent (42)) - -print! ("incrementing agent value to", agent/update (myagent, inc)) - -:done! +:foobar diff --git a/src/base.rs b/src/base.rs index d3ed96e..b553a6b 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1,7 +1,7 @@ use crate::value::*; use imbl::*; -use ran::ran_f64; use std::rc::Rc; +use wasm_bindgen::prelude::*; #[derive(Clone, Debug)] pub enum BaseFn { @@ -481,8 +481,14 @@ pub fn floor(x: &Value) -> Value { } } -pub fn random() -> Value { - Value::Number(ran_f64()) +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = Math)] + fn random() -> f64; +} + +pub fn base_random() -> Value { + Value::Number(random()) } pub fn round(x: &Value) -> Value { @@ -610,7 +616,10 @@ pub fn make_base() -> Value { ("pi", Value::Number(std::f64::consts::PI)), ("print!", Value::BaseFn(BaseFn::Unary("print!", print))), ("process", Value::Process), - ("random", Value::BaseFn(BaseFn::Nullary("random", random))), + ( + "random", + Value::BaseFn(BaseFn::Nullary("random", base_random)), + ), ("range", Value::BaseFn(BaseFn::Binary("range", range))), ("rest", Value::BaseFn(BaseFn::Unary("rest", rest))), ("round", Value::BaseFn(BaseFn::Unary("round", round))), diff --git a/src/io.rs b/src/io.rs index 3a406ff..0a36535 100644 --- a/src/io.rs +++ b/src/io.rs @@ -11,6 +11,12 @@ extern "C" { async fn io (output: String) -> JsValue; } +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console)] + fn log(s: String); +} + type Lines = Value; // expect a list of values type Commands = Value; // expect a list of values type Url = Value; // expect a string representing a URL @@ -25,7 +31,7 @@ pub enum MsgOut { Console(Lines), Commands(Commands), SlurpRequest(Url), - Complete(Result), + Complete(FinalValue), } impl std::fmt::Display for MsgOut { @@ -38,7 +44,10 @@ impl MsgOut { pub fn to_json(&self) -> String { match self { MsgOut::Complete(value) => match value { - Ok(value) => make_json_payload("complete", value.to_json().unwrap()), + Ok(value) => { + log(format!("value is: {}", value.show())); + make_json_payload("complete", serde_json::to_string(&value.show()).unwrap()) + }, Err(_) => make_json_payload("complete", "\"null\"".to_string()) }, MsgOut::Commands(commands) => { @@ -55,7 +64,7 @@ impl MsgOut { } MsgOut::Console(lines) => { let lines = lines.as_list(); - let json_lines = lines.iter().map(|line| line.stringify()).collect::>().join("\n"); + let json_lines = lines.iter().map(|line| line.stringify()).collect::>().join("\\n"); let json_lines = format!("\"{json_lines}\""); make_json_payload("console", json_lines) } @@ -72,6 +81,16 @@ pub enum MsgIn { Keyboard(Vec), } +impl std::fmt::Display for MsgIn { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + MsgIn::Input(str) => write!(f, "input: {str}"), + MsgIn::Kill => write!(f, "kill"), + _ => todo!() + } + } +} + impl MsgIn { pub fn to_value(self) -> Value { match self { @@ -99,8 +118,14 @@ pub async fn do_io (msgs: Vec) -> Vec { let outbox = format!("[{}]", msgs.iter().map(|msg| msg.to_json()).collect::>().join(",")); let inbox = io (outbox).await; let inbox = inbox.as_string().expect("response should be a string"); + log(format!("response is: {inbox}")); let inbox: Vec = serde_json::from_str(inbox.as_str()).expect("response from js should be valid"); + if !inbox.is_empty() { + log("got messages in ludus!".to_string()); + for msg in inbox.iter() { + log(format!("{}", msg)); + } + } inbox - } diff --git a/src/lib.rs b/src/lib.rs index 9a57659..e4e1cea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ use chumsky::{input::Stream, prelude::*}; use imbl::HashMap; use wasm_bindgen::prelude::*; +use std::rc::Rc; +use std::cell::RefCell; const DEBUG_SCRIPT_COMPILE: bool = false; const DEBUG_SCRIPT_RUN: bool = false; @@ -15,7 +17,7 @@ use crate::ast::Ast; mod base; mod world; -use crate::world::World; +use crate::world::{World, Zoo}; mod spans; use crate::spans::Spanned; @@ -42,10 +44,11 @@ mod value; use value::Value; mod vm; +use vm::Creature; const PRELUDE: &str = include_str!("../assets/test_prelude.ld"); -async fn prelude() -> HashMap<&'static str, Value> { +fn prelude() -> HashMap<&'static str, Value> { let tokens = lexer().parse(PRELUDE).into_output_errors().0.unwrap(); let (parsed, parse_errors) = parser() .parse(Stream::from_iter(tokens).map((0..PRELUDE.len()).into(), |(t, s)| (t, s))) @@ -71,7 +74,7 @@ async fn prelude() -> HashMap<&'static str, Value> { if !validator.errors.is_empty() { println!("VALIDATION ERRORS IN PRLUDE:"); report_invalidation(validator.errors); - panic!(); + panic!("validator errors in prelude"); } let parsed: &'static Spanned = Box::leak(Box::new(parsed)); @@ -88,25 +91,37 @@ async fn prelude() -> HashMap<&'static str, Value> { compiler.compile(); let chunk = compiler.chunk; - let mut world = World::new(chunk, DEBUG_PRELUDE_RUN); - let stub_console = Value::r#box(Value::new_list()); - world.run(stub_console).await; - let prelude = world.result.unwrap().unwrap(); + log("compiled prelude"); + let stub_zoo = Rc::new(RefCell::new(Zoo::new())); + let mut prld_sync = Creature::new(chunk, stub_zoo, DEBUG_PRELUDE_RUN); + prld_sync.interpret(); + log("run prelude synchronously"); + let prelude = prld_sync.result.unwrap().unwrap(); match prelude { Value::Dict(hashmap) => *hashmap, _ => unreachable!(), } } +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); +} + #[wasm_bindgen] pub async fn ludus(src: String) -> String { + console_error_panic_hook::set_once(); + log("successfully entered ludus fn in Rust"); let src = src.to_string().leak(); + log(src); let (tokens, lex_errs) = lexer().parse(src).into_output_errors(); if !lex_errs.is_empty() { return format!("{:?}", lex_errs); } let tokens = tokens.unwrap(); + log("successfully tokenized source"); let (parse_result, parse_errors) = parser() .parse(Stream::from_iter(tokens).map((0..src.len()).into(), |(t, s)| (t, s))) @@ -119,8 +134,10 @@ pub async fn ludus(src: String) -> String { // This simplifies lifetimes, and // in any event, the AST should live forever let parsed: &'static Spanned = Box::leak(Box::new(parse_result.unwrap())); + log("successfully parsed source"); - let prelude = prelude().await; + let prelude = prelude(); + log("successfully loaded prelude"); let postlude = prelude.clone(); // let prelude = imbl::HashMap::new(); @@ -146,6 +163,7 @@ pub async fn ludus(src: String) -> String { // compiler.bind("base"); compiler.compile(); + log("successfully compiled source"); if DEBUG_SCRIPT_COMPILE { println!("=== source code ==="); println!("{src}"); @@ -159,13 +177,16 @@ pub async fn ludus(src: String) -> String { let vm_chunk = compiler.chunk; - let mut world = World::new(vm_chunk, DEBUG_SCRIPT_RUN); + let mut world = World::new(vm_chunk, prelude.clone(), DEBUG_SCRIPT_RUN); let console = prelude .get("console") .expect("prelude must have a console") .clone(); - world.run(console).await; + log("loaded world and console"); + world.run().await; let result = world.result.clone().unwrap(); + log("ran script"); + log(format!("{:?}", result).as_str()); let console = postlude.get("console").unwrap(); let Value::Box(console) = console else { diff --git a/src/main.rs b/src/main.rs index 8fe07b6..210846e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,9 @@ use rudus::ludus; use std::env; use std::fs; -#[tokio::main] -pub async fn main() { +pub fn main() { env::set_var("RUST_BACKTRACE", "1"); let src = fs::read_to_string("sandbox.ld").unwrap(); - let json = ludus(src).await; - println!("{json}"); + let json = ludus(src); + // println!("{json}"); } diff --git a/src/value.rs b/src/value.rs index 5258141..9618c67 100644 --- a/src/value.rs +++ b/src/value.rs @@ -259,7 +259,9 @@ impl Value { pub fn to_json(&self) -> Option { use Value::*; match self { - True | False | String(..) | Interned(..) | Number(..) => Some(self.show()), + True | False | Number(..) => Some(self.show()), + String(string) => Some(serde_json::to_string(string.as_ref()).unwrap()), + Interned(str) => Some(serde_json::to_string(str).unwrap()), Keyword(str) => Some(format!("\"{str}\"")), List(members) => { let mut joined = "".to_string(); diff --git a/src/vm.rs b/src/vm.rs index e6e0b9b..8ad7e86 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -339,7 +339,7 @@ impl Creature { let Value::Number(ms) = args[1] else { unreachable!() }; - self.zoo.as_ref().borrow_mut().sleep(self.pid, ms as usize); + self.zoo.as_ref().borrow_mut().sleep(self.pid, ms); self.r#yield = true; self.push(Value::Keyword("ok")); } diff --git a/src/world.rs b/src/world.rs index c7fc3eb..d0c7426 100644 --- a/src/world.rs +++ b/src/world.rs @@ -2,14 +2,26 @@ use crate::chunk::Chunk; use crate::value::Value; use crate::vm::{Creature, Panic}; use crate::io::{MsgOut, MsgIn, do_io}; -use ran::ran_u8; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::mem::swap; use std::rc::Rc; -use std::time::{Duration, Instant}; +use wasm_bindgen::prelude::*; -const ANIMALS: [&str; 24] = [ +// Grab some JS stuff +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); + + #[wasm_bindgen(js_namespace = Math)] + fn random() -> f64; + + #[wasm_bindgen(js_namespace = Date)] + fn now() -> f64; +} + +const ANIMALS: [&str; 32] = [ "tortoise", "hare", "squirrel", @@ -31,9 +43,17 @@ const ANIMALS: [&str; 24] = [ "zebra", "hyena", "giraffe", - "leopard", - "lion", "hippopotamus", + "capybara", + "python", + "gopher", + "crab", + "trout", + "osprey", + "lemur", + "wobbegong", + "walrus", + "opossum", ]; #[derive(Debug, Clone, PartialEq)] @@ -70,7 +90,7 @@ pub struct Zoo { ids: HashMap<&'static str, usize>, dead: HashSet<&'static str>, kill_list: Vec<&'static str>, - sleeping: HashMap<&'static str, (Instant, Duration)>, + sleeping: HashMap<&'static str, f64>, active_idx: usize, active_id: &'static str, } @@ -90,20 +110,27 @@ impl Zoo { } fn random_id(&self) -> String { - let rand = ran_u8() as usize % 24; + log("generating random id"); + let rand_idx = (random() * 32.0) as usize; + log("random number!"); let idx = self.procs.len(); - format!("{}_{idx}", ANIMALS[rand]) + log("procs len"); + format!("{}_{idx}", ANIMALS[rand_idx]) } fn new_id(&self) -> &'static str { + log("creating new id"); let mut new = self.random_id(); + log("got new ramdom id"); while self.dead.iter().any(|old| *old == new) { new = self.random_id(); } + log(format!("got new id: {}", new).as_str()); new.leak() } pub fn put(&mut self, mut proc: Creature) -> &'static str { + log("putting creature"); if self.empty.is_empty() { let id = self.new_id(); let idx = self.procs.len(); @@ -113,7 +140,7 @@ impl Zoo { id } else { let idx = self.empty.pop().unwrap(); - let rand = ran_u8() as usize % 24; + let rand = (random() * 32.0) as usize; let id = format!("{}_{idx}", ANIMALS[rand]).leak(); proc.pid = id; self.ids.insert(id, idx); @@ -126,9 +153,9 @@ impl Zoo { self.kill_list.push(id); } - pub fn sleep(&mut self, id: &'static str, ms: usize) { + pub fn sleep(&mut self, id: &'static str, ms: f64) { self.sleeping - .insert(id, (Instant::now(), Duration::from_millis(ms as u64))); + .insert(id, now() + ms); } pub fn is_alive(&self, id: &'static str) -> bool { @@ -161,7 +188,7 @@ impl Zoo { } self.sleeping - .retain(|_, (instant, duration)| instant.elapsed() < *duration); + .retain(|_, wakeup_time| now() < *wakeup_time); println!( "currently sleeping processes: {}", @@ -232,25 +259,72 @@ impl Zoo { } } +#[derive(Debug, Clone, PartialEq)] +pub struct Buffers { + console: Value, + // commands: Value, + // fetch_outbox: Value, + // fetch_inbox: Value, + input: Value, +} + +impl Buffers { + pub fn new (prelude: imbl::HashMap<&'static str, Value>) -> Buffers { + Buffers { + console: prelude.get("console").unwrap().clone(), + // commands: prelude.get("commands").unwrap().clone(), + // fetch_outbox: prelude.get("fetch_outbox").unwrap().clone(), + // fetch_inbox: prelude.get("fetch_inbox").unwrap().clone(), + input: prelude.get("input").unwrap().clone(), + } + } + + pub fn console (&self) -> Rc> { + self.console.as_box() + } + + pub fn input (&self) -> Rc> { + self.input.as_box() + } + + // pub fn commands (&self) -> Rc> { + // self.commands.as_box() + // } + // pub fn fetch_outbox (&self) -> Rc> { + // self.fetch_outbox.as_box() + // } + // pub fn fetch_inbox (&self) -> Rc> { + // self.fetch_inbox.as_box() + // } + +} + #[derive(Debug, Clone, PartialEq)] pub struct World { zoo: Rc>, active: Option, main: &'static str, pub result: Option>, + buffers: Buffers, + last_io: f64, + kill_signal: bool, } impl World { - pub fn new(chunk: Chunk, debug: bool) -> World { + pub fn new(chunk: Chunk, prelude: imbl::HashMap<&'static str, Value>, debug: bool) -> World { let zoo = Rc::new(RefCell::new(Zoo::new())); let main = Creature::new(chunk, zoo.clone(), debug); - let id = zoo.as_ref().borrow_mut().put(main); - + let id = zoo.borrow_mut().put(main); + let buffers = Buffers::new(prelude); + World { zoo, active: None, main: id, result: None, + buffers, + last_io: 0.0, + kill_signal: false, } } @@ -266,111 +340,100 @@ impl World { swap(&mut new_active_opt, &mut self.active); } - pub fn activate_main(&mut self) { - let main = self.zoo.as_ref().borrow_mut().catch(self.main); + fn activate_main(&mut self) { + let main = self.zoo.borrow_mut().catch(self.main); self.active = Some(main); } - pub fn active_id(&mut self) -> &'static str { + fn active_id(&mut self) -> &'static str { self.active.as_ref().unwrap().pid } - pub fn kill_active(&mut self) { + fn kill_active(&mut self) { let id = self.active_id(); self.zoo.as_ref().borrow_mut().kill(id); } - pub fn active_result(&mut self) -> &Option> { + fn active_result(&mut self) -> &Option> { &self.active.as_ref().unwrap().result } - // TODO: add memory io places to this signature - // * console - // * input - // * commands - // * slurp - pub async fn run( - &mut self, - console: Value, - // input: Value, - // commands: Value, - // slurp_out: Value, - // slurp_in: Value, - ) { + fn flush_buffers(&mut self) -> Vec { + let mut outbox = vec![]; + if let Some(console) = self.flush_console() { + outbox.push(console); + } + outbox + } + + fn flush_console(&self) -> Option { + let console = self.buffers.console(); + let working_copy = RefCell::new(Value::new_list()); + console.swap(&working_copy); + let working_value = working_copy.borrow(); + if working_value.as_list().is_empty() { return None; } + Some(MsgOut::Console(working_value.clone())) + } + + fn complete_main(&mut self) -> Vec { + let mut outbox = self.flush_buffers(); + // TODO: if we have a panic, actually add the panic message to the console + let result = self.active_result().clone().unwrap(); + self.result = Some(result.clone()); + outbox.push(MsgOut::Complete(result)); + outbox + } + + fn interpret_active(&mut self) { + self.active.as_mut().unwrap().interpret(); + } + + async fn maybe_do_io(&mut self) { + if self.last_io + 10.0 > now () { + let outbox = self.flush_buffers(); + let inbox = do_io(outbox).await; + self.fill_buffers(inbox); + } + self.last_io = now(); + } + + fn fill_input(&mut self, str: String) { + let value = Value::string(str); + let working = RefCell::new(value); + let input = self.buffers.input(); + input.swap(&working); + } + + fn fill_buffers(&mut self, inbox: Vec) { + for msg in inbox { + match msg { + MsgIn::Input(str) => self.fill_input(str), + MsgIn::Kill => self.kill_signal = true, + _ => todo!() + } + } + } + + pub async fn run(&mut self) { self.activate_main(); - // let Value::Box(input) = input else {unreachable!()}; - // let Value::Box(commands) = commands else {unreachable!()}; - // let Value::Box(slurp) = slurp else { - // unreachable!()}; - let mut last_io = Instant::now(); - let mut kill_signal = false; loop { - if kill_signal { - // TODO: send a last message to the console - println!("received KILL signal"); + if self.kill_signal { + let outbox = self.flush_buffers(); + do_io(outbox).await; return; } - println!( - "entering world loop; active process is {}", - self.active_id() - ); - self.active.as_mut().unwrap().interpret(); - println!("yielded from {}", self.active_id()); - match self.active_result() { - None => (), - Some(_) => { - if self.active_id() == self.main { - let result = self.active_result().clone().unwrap(); - self.result = Some(result.clone()); - - //TODO: capture any remaining console or command values - do_io(vec![MsgOut::Complete(result)]); - return; - } - println!( - "process {} died with {:?}", - self.active_id(), - self.active_result().clone() - ); - self.kill_active(); + self.interpret_active(); + if self.active_result().is_some() { + if self.active_id() == self.main { + let outbox = self.complete_main(); + do_io(outbox).await; + return; } + self.kill_active(); } - println!("getting next process"); self.next(); - // TODO:: if enough time has elapsed (how much?) run i/o - // 10 ms is 100hz, so that's a nice refresh rate - if Instant::now().duration_since(last_io) > Duration::from_millis(10) { - // gather io - // compile it into messages - // serialize it - let mut outbox = vec![]; - if let Some(console) = flush_console(&console) { - outbox.push(console); - }; - // TODO: slurp - // TODO: commands - // send it - // await the response - let inbox = do_io(outbox).await; - // unpack the response into messages - for msg in inbox { - match msg { - MsgIn::Kill => kill_signal = true, - _ => todo!() - } - } - // update - last_io = Instant::now(); - } + self.maybe_do_io().await; } } } -fn flush_console(console: &Value) -> Option { - let console = console.as_box(); - let working_copy = RefCell::new(Value::new_list()); - console.swap(&working_copy); - let working_value = working_copy.borrow(); - if working_value.as_list().is_empty() { return None; } - Some(MsgOut::Console(working_value.clone())) -}