From bc49ece0cf1de46cf243a85e060d5cefdd21a907 Mon Sep 17 00:00:00 2001 From: Scott Richmond Date: Mon, 30 Jun 2025 12:48:50 -0400 Subject: [PATCH] stub out first pass of io system --- Cargo.toml | 10 ++--- justfile | 1 + may_2025_thoughts.md | 9 +++++ pkg/ludus.js | 23 ++++++++++- pkg/worker.js | 15 ++++++++ sandbox.ld | 2 + src/lib.rs | 27 +++++++++---- src/main.rs | 5 ++- src/value.rs | 58 +++++++++++++++++++++++++--- src/world.rs | 90 +++++++++++++++++++++++++++++++------------- 10 files changed, 192 insertions(+), 48 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 271287d..9c17cb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,8 @@ num-derive = "0.4.2" num-traits = "0.2.19" regex = "1.11.1" wasm-bindgen = "0.2" -# struct_scalpel = "0.1.1" -# rust-embed = "8.5.0" -# boxing = "0.1.2" -# ordered-float = "4.5.0" -# index_vec = "0.1.4" +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"]} + diff --git a/justfile b/justfile index 7a56278..daf09c8 100644 --- a/justfile +++ b/justfile @@ -5,6 +5,7 @@ wasm: echo 'import {io} from "../worker.js"' > rudus.js cat rudus.js.backup | tail -n+2>> rudus.js rm rudus.js.backup + rm -rf pkg/snippets default: @just --list diff --git a/may_2025_thoughts.md b/may_2025_thoughts.md index 34abce4..780e809 100644 --- a/may_2025_thoughts.md +++ b/may_2025_thoughts.md @@ -1153,9 +1153,11 @@ That leaves the following list: * [ ] improve build process for rudus+wasm_pack - [ ] delete generated .gitignore - [ ] edit first line of rudus.js to import the local `ludus.js` + - On this, made a justfile, but I needed to like actually try the build and figure out what happens. I got carried away touching the js. Too many things at once. * [ ] design & implement asynchronous i/o+runtime - [ ] use `box`es for i/o: they can be reified in rust: making actors available is rather more complex (i.e. require message passing between the ludus and rust) * We also then don't have to have prelude run in the vm; that's good + * We... maybe or maybe don't need processes in prelude, since we need to read and write from the boxes; we might be able to do that with closures and functions that call `spawn!` themselves - [ ] start with ludus->rust->js pipeline * [ ] console * [ ] turtle graphics @@ -1166,3 +1168,10 @@ That leaves the following list: * [ ] keypresses - [ ] then ludus->rust->js->rust->ludus * [ ] slurp + - For the above, I've started hammering out a situation. I ought to have followed my instinct here: do a little at a time. I ended up doing all the things in one place all at once. + - What I've done is work on a bespoke `to_json` method for values; and using serde deserialization to read a string delivered from js. I think this is easier and more straightforward than using `wasm_bindgen`. Or easier; I have no idea what the plumbing looks like. + - Just to catch myself up, some additional decisions & thoughts: + * No need to send a run event: we'll just start things with with a call to `run`, which we expose to JS. + * One thing I hadn't quite grokked before is that we need to have a way of running the i/o events. Perhaps the simplest way to do this is to just to do it every so often, regardless of how long the ludus event loop is taking. That way even if things are getting weird in the VM, i/o still happens regularly. + * The return to a `slurp` call is interesting. + * I think the thing to do is to write to a slurp buffer/box as well. diff --git a/pkg/ludus.js b/pkg/ludus.js index f7b23f1..b26f229 100644 --- a/pkg/ludus.js +++ b/pkg/ludus.js @@ -1,6 +1,5 @@ import init, {ludus} from "./rudus.js"; -await init(); let res = null @@ -376,3 +375,25 @@ 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/worker.js b/pkg/worker.js index e69de29..fcc4212 100644 --- a/pkg/worker.js +++ b/pkg/worker.js @@ -0,0 +1,15 @@ +import init from "./rudus.js"; + +console.log("Worker: starting Ludus VM.") + +export function io (out) { + if (Object.keys(out).length > 0) postMessage(out) + return new Promise((resolve, _) => { + onmessage = (e) => resolve(e.data) + }) +} + +await init() + +console.log("Worker: Ludus VM is running.") + diff --git a/sandbox.ld b/sandbox.ld index afdd359..4052cc3 100644 --- a/sandbox.ld +++ b/sandbox.ld @@ -27,3 +27,5 @@ fn agent/update (pid, f) -> { let myagent = spawn! (fn () -> agent (42)) print! ("incrementing agent value to", agent/update (myagent, inc)) + +:done! diff --git a/src/lib.rs b/src/lib.rs index f5beeb2..9a57659 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,8 @@ const DEBUG_SCRIPT_RUN: bool = false; const DEBUG_PRELUDE_COMPILE: bool = false; const DEBUG_PRELUDE_RUN: bool = false; +mod io; + mod ast; use crate::ast::Ast; @@ -40,11 +42,10 @@ mod value; use value::Value; mod vm; -use vm::Creature; const PRELUDE: &str = include_str!("../assets/test_prelude.ld"); -fn prelude() -> HashMap<&'static str, Value> { +async 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))) @@ -88,7 +89,8 @@ fn prelude() -> HashMap<&'static str, Value> { let chunk = compiler.chunk; let mut world = World::new(chunk, DEBUG_PRELUDE_RUN); - world.run(); + let stub_console = Value::r#box(Value::new_list()); + world.run(stub_console).await; let prelude = world.result.unwrap().unwrap(); match prelude { Value::Dict(hashmap) => *hashmap, @@ -97,7 +99,7 @@ fn prelude() -> HashMap<&'static str, Value> { } #[wasm_bindgen] -pub fn ludus(src: String) -> String { +pub async fn ludus(src: String) -> String { let src = src.to_string().leak(); let (tokens, lex_errs) = lexer().parse(src).into_output_errors(); if !lex_errs.is_empty() { @@ -118,7 +120,7 @@ pub fn ludus(src: String) -> String { // in any event, the AST should live forever let parsed: &'static Spanned = Box::leak(Box::new(parse_result.unwrap())); - let prelude = prelude(); + let prelude = prelude().await; let postlude = prelude.clone(); // let prelude = imbl::HashMap::new(); @@ -131,7 +133,14 @@ pub fn ludus(src: String) -> String { return "Ludus found some validation errors.".to_string(); } - let mut compiler = Compiler::new(parsed, "sandbox", src, 0, prelude, DEBUG_SCRIPT_COMPILE); + let mut compiler = Compiler::new( + parsed, + "sandbox", + src, + 0, + prelude.clone(), + DEBUG_SCRIPT_COMPILE, + ); // let base = base::make_base(); // compiler.emit_constant(base); // compiler.bind("base"); @@ -151,7 +160,11 @@ pub fn ludus(src: String) -> String { let vm_chunk = compiler.chunk; let mut world = World::new(vm_chunk, DEBUG_SCRIPT_RUN); - world.run(); + let console = prelude + .get("console") + .expect("prelude must have a console") + .clone(); + world.run(console).await; let result = world.result.clone().unwrap(); let console = postlude.get("console").unwrap(); diff --git a/src/main.rs b/src/main.rs index b2bbc6b..8fe07b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,10 @@ use rudus::ludus; use std::env; use std::fs; -pub fn main() { +#[tokio::main] +pub async fn main() { env::set_var("RUST_BACKTRACE", "1"); let src = fs::read_to_string("sandbox.ld").unwrap(); - let json = ludus(src); + let json = ludus(src).await; println!("{json}"); } diff --git a/src/value.rs b/src/value.rs index 4d868c3..5258141 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,7 +1,5 @@ use crate::base::BaseFn; use crate::chunk::Chunk; -// use crate::parser::Ast; -// use crate::spans::Spanned; use imbl::{HashMap, Vector}; use std::cell::RefCell; use std::rc::Rc; @@ -250,8 +248,8 @@ impl Value { BaseFn(_) => format!("{self}"), Nothing => "_".to_string(), }; - if out.len() > 20 { - out.truncate(20); + if out.len() > 80 { + out.truncate(77); format!("{out}...") } else { out @@ -379,8 +377,56 @@ impl Value { pub fn as_fn(&self) -> &LFn { match self { - Value::Fn(inner) => inner.as_ref(), - _ => unreachable!(), + Value::Fn(ref inner) => inner, + _ => unreachable!("expected value to be fn"), + } + } + + pub fn as_list(&self) -> &Vector { + match self { + Value::List(ref inner) => inner, + _ => unreachable!("expected value to be list"), + } + } + + pub fn as_box(&self) -> Rc> { + match self { + Value::Box(inner) => inner.clone(), + _ => unreachable!("expected value to be a box"), + } + } + + pub fn string(str: String) -> Value { + Value::String(Rc::new(str)) + } + + pub fn keyword(str: String) -> Value { + Value::Keyword(str.leak()) + } + + pub fn list(list: Vector) -> Value { + Value::List(Box::new(list)) + } + + pub fn new_list() -> Value { + Value::list(Vector::new()) + } + + pub fn r#box(value: Value) -> Value { + Value::Box(Rc::new(RefCell::new(value))) + } + + pub fn tuple(vec: Vec) -> Value { + Value::Tuple(Rc::new(vec)) + } + + pub fn get_shared_box(&self, name: &'static str) -> Value { + match self { + Value::Dict(dict) => dict + .get(name) + .expect("expected dict to have requested value") + .clone(), + _ => unreachable!("expected dict"), } } } diff --git a/src/world.rs b/src/world.rs index 763c90a..c7fc3eb 100644 --- a/src/world.rs +++ b/src/world.rs @@ -1,6 +1,7 @@ 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}; @@ -283,9 +284,32 @@ impl World { &self.active.as_ref().unwrap().result } - pub fn run(&mut self) { + // 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, + ) { 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"); + return; + } println!( "entering world loop; active process is {}", self.active_id() @@ -296,7 +320,11 @@ impl World { None => (), Some(_) => { if self.active_id() == self.main { - self.result = self.active_result().clone(); + 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!( @@ -309,32 +337,40 @@ impl World { } println!("getting next process"); self.next(); - // self.clean_up(); + // 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(); + } } } - - // TODO: - // * [ ] Maybe I need to write this from the bottom up? - // What do processes need to do? - // - [ ] send a message to another process - // - [ ] tell the world to spawn a new process, get the pid back - // - [ ] receive its messages (always until something matches, or sleep if nothing matches) - // - [ ] delete a message from the mbx if it's a match (by idx) - // - [ ] yield - // - [ ] panic - // - [ ] complete - // Thus the other side of this looks like: - // * [x] Spawn a process - // * [x] } -// Okay, some more thinking -// The world and process can't have mutable references to one another -// They will each need an Rc> -// All the message passing and world/proc communication will happen through there -// And ownership goes World -> Process A -> World -> Process B - -// Both the world and a process will have an endless `loop`. -// But I already have three terms: Zoo, Creature, and World -// That should be enough indirection? -// To solve tomorrow. +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())) +}