diff --git a/Cargo.toml b/Cargo.toml index cf542a9..56f5760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,6 @@ name = "rudus" version = "0.0.1" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [lib] crate-type = ["cdylib", "rlib"] @@ -19,4 +17,3 @@ wasm-bindgen-futures = "0.4.50" serde = {version = "1.0", features = ["derive"]} serde_json = "1.0" console_error_panic_hook = "0.1.7" -# talc = "4.4.3" diff --git a/may_2025_thoughts.md b/may_2025_thoughts.md index 56fe5f9..77fdf25 100644 --- a/may_2025_thoughts.md +++ b/may_2025_thoughts.md @@ -1183,6 +1183,12 @@ Happy Canada day! After a really rough evening, I seem to have the actor model not only working in Ludus, but reasonably debugged in Rust. We've got one bug to address in Firefox before I continue: * [ ] the event loop isn't returning once something is done, which makes no sense + - What seems to be happening is that the javascript behaviour is subtly different + - Current situation is that synchronous scripts work just fine + - But async scripts work ONCE, and then not again + - In FF, `do_io` doesn't return after `complete_main` in the `world` loop the second time. + - Which is to say, that last call to `io` isn't completing. + - Do I hack around this or do I try to find the source of the problem? After that: * [ ] implement other verbs beside `console`: @@ -1200,4 +1206,5 @@ After that: - [ ] do synchronous programs still work? - [ ] animations? - [ ] read inputs? + - [ ] load url text? diff --git a/pkg/ludus.js b/pkg/ludus.js index 504895f..cf4ef2f 100644 --- a/pkg/ludus.js +++ b/pkg/ludus.js @@ -10,18 +10,18 @@ worker.onmessage = async (e) => { msgs = JSON.parse(e.data) } catch { console.log(e.data) - throw Error("bad json from Ludus") + throw Error("Main: bad json from Ludus") } for (const msg of msgs) { switch (msg.verb) { case "complete": { - console.log("ludus completed with => ", msg.data) + console.log("Main: ludus completed with => ", msg.data) res = msg.data running = false break } case "console": { - console.log("ludus says => ", msg.data) + console.log("Main: ludus says => ", msg.data) break } } @@ -35,11 +35,14 @@ let io_interval_id = null const io_poller = () => { if (io_interval_id && !running) { + // flush the outbox one last time + worker.postMessage(outbox) + // cancel the poller clearInterval(io_interval_id) - return + } else { + worker.postMessage(outbox) + outbox = [] } - worker.postMessage(outbox) - outbox = [] } function poll_io () { diff --git a/pkg/rudus.d.ts b/pkg/rudus.d.ts index 48d7cb8..0e42a14 100644 --- a/pkg/rudus.d.ts +++ b/pkg/rudus.d.ts @@ -14,8 +14,8 @@ export interface InitOutput { readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_export_6: WebAssembly.Table; - readonly closure327_externref_shim: (a: number, b: number, c: any) => void; - readonly closure340_externref_shim: (a: number, b: number, c: any, d: any) => void; + readonly closure343_externref_shim: (a: number, b: number, c: any) => void; + readonly closure367_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 dd00cba..2af7198 100644 --- a/pkg/rudus.js +++ b/pkg/rudus.js @@ -17,6 +17,22 @@ function handleError(f, args) { } } +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(); }; @@ -54,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; @@ -82,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; } @@ -104,6 +122,12 @@ 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 => { @@ -210,12 +234,19 @@ export function ludus(src) { 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) { - wasm.closure327_externref_shim(arg0, arg1, arg2); + _assertNum(arg0); + _assertNum(arg1); + wasm.closure343_externref_shim(arg0, arg1, arg2); } function __wbg_adapter_52(arg0, arg1, arg2, arg3) { - wasm.closure340_externref_shim(arg0, arg1, arg2, arg3); + _assertNum(arg0); + _assertNum(arg1); + wasm.closure367_externref_shim(arg0, arg1, arg2, arg3); } async function __wbg_load(module, imports) { @@ -260,7 +291,7 @@ function __wbg_get_imports() { const ret = arg0.call(arg1, arg2); return ret; }, arguments) }; - imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) { + imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function() { return logError(function (arg0, arg1) { let deferred0_0; let deferred0_1; try { @@ -270,8 +301,8 @@ function __wbg_get_imports() { } finally { wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); } - }; - imports.wbg.__wbg_io_4b41f8089de924df = function(arg0, arg1) { + }, arguments) }; + imports.wbg.__wbg_io_4b41f8089de924df = function() { return logError(function (arg0, arg1) { let deferred0_0; let deferred0_1; try { @@ -282,8 +313,8 @@ function __wbg_get_imports() { } finally { wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); } - }; - imports.wbg.__wbg_log_86d603e98cc11395 = function(arg0, arg1) { + }, arguments) }; + imports.wbg.__wbg_log_86d603e98cc11395 = function() { return logError(function (arg0, arg1) { let deferred0_0; let deferred0_1; try { @@ -293,11 +324,11 @@ function __wbg_get_imports() { } finally { wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); } - }; - imports.wbg.__wbg_log_9e426f8e841e42d3 = function(arg0, arg1) { + }, arguments) }; + imports.wbg.__wbg_log_9e426f8e841e42d3 = function() { return logError(function (arg0, arg1) { console.log(getStringFromWasm0(arg0, arg1)); - }; - imports.wbg.__wbg_log_edeb598b620f1ba2 = function(arg0, arg1) { + }, arguments) }; + imports.wbg.__wbg_log_edeb598b620f1ba2 = function() { return logError(function (arg0, arg1) { let deferred0_0; let deferred0_1; try { @@ -307,8 +338,8 @@ function __wbg_get_imports() { } finally { wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); } - }; - imports.wbg.__wbg_new_23a2665fac83c611 = function(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) => { @@ -325,65 +356,65 @@ function __wbg_get_imports() { } finally { state0.a = state0.b = 0; } - }; - imports.wbg.__wbg_new_8a6f238a6ece86ea = function() { + }, arguments) }; + imports.wbg.__wbg_new_8a6f238a6ece86ea = function() { return logError(function () { const ret = new Error(); return ret; - }; - imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function(arg0, arg1) { + }, arguments) }; + imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function() { return logError(function (arg0, arg1) { const ret = new Function(getStringFromWasm0(arg0, arg1)); return ret; - }; - imports.wbg.__wbg_now_8dddb61fa4928554 = function() { + }, arguments) }; + imports.wbg.__wbg_now_8dddb61fa4928554 = function() { return logError(function () { const ret = Date.now(); return ret; - }; - imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function(arg0) { + }, arguments) }; + imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function() { return logError(function (arg0) { queueMicrotask(arg0); - }; - imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function(arg0) { + }, arguments) }; + imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function() { return logError(function (arg0) { const ret = arg0.queueMicrotask; return ret; - }; - imports.wbg.__wbg_random_57c118f142535bb6 = function() { + }, arguments) }; + imports.wbg.__wbg_random_57c118f142535bb6 = function() { return logError(function () { const ret = Math.random(); return ret; - }; - imports.wbg.__wbg_resolve_4851785c9c5f573d = function(arg0) { + }, arguments) }; + imports.wbg.__wbg_resolve_4851785c9c5f573d = function() { return logError(function (arg0) { const ret = Promise.resolve(arg0); return ret; - }; - imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) { + }, 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); - }; - imports.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() { + }, 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); - }; - imports.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() { + }, 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); - }; - imports.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() { + }, 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); - }; - imports.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() { + }, 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); - }; - imports.wbg.__wbg_then_44b73946d2fb3e7d = function(arg0, arg1) { + }, arguments) }; + imports.wbg.__wbg_then_44b73946d2fb3e7d = function() { return logError(function (arg0, arg1) { const ret = arg0.then(arg1); return ret; - }; - imports.wbg.__wbg_then_48b406749878a531 = function(arg0, arg1, arg2) { + }, 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) { @@ -391,12 +422,13 @@ function __wbg_get_imports() { return true; } const ret = false; + _assertBoolean(ret); return ret; }; - imports.wbg.__wbindgen_closure_wrapper974 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 328, __wbg_adapter_22); + imports.wbg.__wbindgen_closure_wrapper7649 = function() { return logError(function (arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 344, __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); @@ -416,10 +448,12 @@ function __wbg_get_imports() { }; 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) { diff --git a/pkg/rudus_bg.wasm b/pkg/rudus_bg.wasm index ca25a5f..82a74b6 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 e2bf686..9c99a54 100644 --- a/pkg/rudus_bg.wasm.d.ts +++ b/pkg/rudus_bg.wasm.d.ts @@ -9,6 +9,6 @@ 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_export_6: WebAssembly.Table; -export const closure327_externref_shim: (a: number, b: number, c: any) => void; -export const closure340_externref_shim: (a: number, b: number, c: any, d: any) => void; +export const closure343_externref_shim: (a: number, b: number, c: any) => void; +export const closure367_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 7bdd3cb..d539da4 100644 --- a/pkg/worker.js +++ b/pkg/worker.js @@ -1,39 +1,55 @@ import init, {ludus} from "./rudus.js"; +let initialized_wasm = false +onmessage = run +// exposed in rust as: +// async fn io (out: String) -> Result +// rust calls this to perform io export function io (out) { + // only send messages if we have some if (out.length > 0) postMessage(out) - return new Promise((resolve, _) => { + // make an event handler that captures and delivers messages from the main thread + // because our promise resolution isn't about calculating a value but setting a global variable, we can't asyncify it + // explicitly return a promise + return new Promise((resolve, reject) => { + // deliver the response to ludus when we get a response from the main thread onmessage = (e) => { - // console.log("Worker: from Ludus:", e.data) resolve(JSON.stringify(e.data)) } + // cancel the response if it takes too long + setTimeout(() => reject("io took too long"), 500) }) } -let loaded_wasm = false +// set as default event handler from main thread async function run(e) { - if (!loaded_wasm) { - loaded_wasm = true + // we must NEVER run `await init()` twice + if (!initialized_wasm) { + // this must come before the init call + initialized_wasm = true await init() console.log("Worker: Ludus has been initialized.") } + // the data is always an array; we only really expect one member tho let msgs = e.data for (const msg of msgs) { + // evaluate source if we get some if (msg.verb === "run" && typeof msg.data === 'string') { - // console.log("running ludus!") + // temporarily stash an empty function so we don't keep calling this one if we receive additional messages onmessage = () => {} - console.log("Worker: Beginning new Ludus run.") + // actually run the ludus--which will call `io`--and replace `run` as the event handler for ipc await ludus(msg.data) + // once we've returned from `ludus`, make this the event handler again onmessage = run } else { + // report and swallow any malformed startup messages console.log("Worker: Did not get valid startup message. Instead got:") console.log(e.data) } } } -onmessage = run diff --git a/src/io.rs b/src/io.rs index 6f1e8b0..f91af40 100644 --- a/src/io.rs +++ b/src/io.rs @@ -8,7 +8,8 @@ use std::rc::Rc; #[wasm_bindgen(module = "/pkg/worker.js")] extern "C" { - async fn io (output: String) -> JsValue; + #[wasm_bindgen(catch)] + async fn io (output: String) -> Result; } #[wasm_bindgen] @@ -117,6 +118,11 @@ impl MsgIn { 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; + // if our request dies, make sure we return back to the event loop + let inbox = match inbox { + Ok(msgs) => msgs, + Err(_) => return vec![] + }; let inbox = inbox.as_string().expect("response should be a string"); let inbox: Vec = serde_json::from_str(inbox.as_str()).expect("response from js should be valid"); if !inbox.is_empty() { diff --git a/src/lib.rs b/src/lib.rs index 6cf7881..b6ffe67 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,7 +114,6 @@ extern "C" { #[wasm_bindgen] pub async fn ludus(src: String) -> String { - log("Ludus: starting ludus run."); console_error_panic_hook::set_once(); let src = src.to_string().leak(); let (tokens, lex_errs) = lexer().parse(src).into_output_errors(); @@ -134,7 +133,6 @@ pub async fn ludus(src: String) -> String { let parsed: &'static Spanned = Box::leak(Box::new(parse_result.unwrap())); let prelude = prelude(); - let postlude = prelude.clone(); // let prelude = imbl::HashMap::new(); let mut validator = Validator::new(&parsed.0, &parsed.1, "user input", src, prelude.clone()); @@ -148,7 +146,7 @@ pub async fn ludus(src: String) -> String { let mut compiler = Compiler::new( parsed, - "sandbox", + "ludus script", src, 0, prelude.clone(), @@ -156,6 +154,7 @@ pub async fn ludus(src: String) -> String { ); compiler.compile(); + if DEBUG_SCRIPT_COMPILE { println!("=== source code ==="); println!("{src}"); @@ -173,63 +172,13 @@ pub async fn ludus(src: String) -> String { world.run().await; let result = world.result.clone().unwrap(); - let console = postlude.get("console").unwrap(); - let Value::Box(console) = console else { - unreachable!() - }; - let Value::List(ref lines) = *console.borrow() else { - unreachable!() - }; - let mut console = lines - .iter() - .map(|line| line.stringify()) - .collect::>() - .join("\n"); - - let turtle_commands = postlude.get("turtle_commands").unwrap(); - let Value::Box(commands) = turtle_commands else { - unreachable!() - }; - let commands = commands.borrow(); - let commands = commands.to_json().unwrap(); - let output = match result { Ok(val) => val.show(), - Err(panic) => { - console = format!("{console}\nLudus panicked! {panic}"); - "".to_string() - } + Err(panic) => format!("Ludus panicked! {panic}") }; if DEBUG_SCRIPT_RUN { // vm.print_stack(); } - // TODO: use serde_json to make this more robust? - format!( - "{{\"result\":\"{output}\",\"io\":{{\"stdout\":{{\"proto\":[\"text-stream\",\"0.1.0\"],\"data\":\"{console}\"}},\"turtle\":{{\"proto\":[\"turtle-graphics\",\"0.1.0\"],\"data\":{commands}}}}}}}" - ) -} - -pub fn fmt(src: &'static str) -> Result { - let (tokens, lex_errs) = lexer().parse(src).into_output_errors(); - if !lex_errs.is_empty() { - println!("{:?}", lex_errs); - return Err(format!("{:?}", lex_errs)); - } - - let tokens = tokens.unwrap(); - - let (parse_result, parse_errors) = parser() - .parse(Stream::from_iter(tokens).map((0..src.len()).into(), |(t, s)| (t, s))) - .into_output_errors(); - if !parse_errors.is_empty() { - return Err(format!("{:?}", parse_errors)); - } - - // ::sigh:: The AST should be 'static - // This simplifies lifetimes, and - // in any event, the AST should live forever - let parsed: &'static Spanned = Box::leak(Box::new(parse_result.unwrap())); - - Ok(parsed.0.show()) + output } diff --git a/src/world.rs b/src/world.rs index 7a6767a..e1c5276 100644 --- a/src/world.rs +++ b/src/world.rs @@ -212,12 +212,6 @@ impl Zoo { let mut proc = Status::Nested(proc); swap(&mut proc, &mut self.procs[*idx]); } - // Removed because, well, we shouldn't have creatures we don't know about - // And since zoo.next now cleans (and thus kills) before the world releases its active process - // We'll die if we execute this check - // else { - // unreachable!("tried to return a process the world doesn't know about"); - // } } pub fn is_available(&self) -> bool { @@ -233,6 +227,10 @@ impl Zoo { let starting_idx = self.active_idx; self.active_idx = (self.active_idx + 1) % self.procs.len(); while !self.is_available() { + // we've gone round the process queue already + // that means no process is active + // but we may have processes that are alive and asleep + // if nothing is active, yield back to the world's event loop if self.active_idx == starting_idx { return "" }