Compare commits

...

2 Commits

Author SHA1 Message Date
Scott Richmond
173fdb913c also add the new io file 2025-06-30 12:49:07 -04:00
Scott Richmond
bc49ece0cf stub out first pass of io system 2025-06-30 12:48:50 -04:00
11 changed files with 298 additions and 48 deletions

View File

@ -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"]}

View File

@ -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

View File

@ -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.

View File

@ -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]
}
}
}
}

View File

@ -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.")

View File

@ -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!

106
src/io.rs Normal file
View File

@ -0,0 +1,106 @@
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
use crate::value::Value;
use crate::vm::Panic;
use imbl::Vector;
use std::rc::Rc;
#[wasm_bindgen(module = "/pkg/worker.js")]
extern "C" {
async fn io (output: String) -> JsValue;
}
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
type FinalValue = Result<Value, Panic>;
fn make_json_payload(verb: &'static str, data: String) -> String {
format!("{{\"verb\":\"{verb}\",\"data\":{data}}}")
}
#[derive(Debug, Clone, PartialEq)]
pub enum MsgOut {
Console(Lines),
Commands(Commands),
SlurpRequest(Url),
Complete(Result<Value, Panic>),
}
impl std::fmt::Display for MsgOut {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.to_json())
}
}
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()),
Err(_) => make_json_payload("complete", "\"null\"".to_string())
},
MsgOut::Commands(commands) => {
let commands = commands.as_list();
let vals_json = commands.iter().map(|v| v.to_json().unwrap()).collect::<Vec<_>>().join(",");
let vals_json = format!("[{vals_json}]");
make_json_payload("commands", vals_json)
}
MsgOut::SlurpRequest(value) => {
// TODO: do parsing here?
// Right now, defer to fetch
let url = value.to_json().unwrap();
make_json_payload("slurp", url)
}
MsgOut::Console(lines) => {
let lines = lines.as_list();
let json_lines = lines.iter().map(|line| line.stringify()).collect::<Vec<_>>().join("\n");
let json_lines = format!("\"{json_lines}\"");
make_json_payload("console", json_lines)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "verb", content = "data")]
pub enum MsgIn {
Input(String),
SlurpResponse(String, String, String),
Kill,
Keyboard(Vec<String>),
}
impl MsgIn {
pub fn to_value(self) -> Value {
match self {
MsgIn::Input(str) => Value::string(str),
MsgIn::SlurpResponse(url, status, string) => {
let url = Value::string(url);
let status = Value::keyword(status);
let string = Value::string(string);
let result_tuple = Value::tuple(vec![status, string]);
Value::tuple(vec![url, result_tuple])
}
MsgIn::Kill => Value::Nothing,
MsgIn::Keyboard(downkeys) => {
let mut vector = Vector::new();
for key in downkeys {
vector.push_back(Value::String(Rc::new(key)));
}
Value::List(Box::new(vector))
}
}
}
}
pub async fn do_io (msgs: Vec<MsgOut>) -> Vec<MsgIn> {
let outbox = format!("[{}]", msgs.iter().map(|msg| msg.to_json()).collect::<Vec<_>>().join(","));
let inbox = io (outbox).await;
let inbox = inbox.as_string().expect("response should be a string");
let inbox: Vec<MsgIn> = serde_json::from_str(inbox.as_str()).expect("response from js should be valid");
inbox
}

View File

@ -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<Ast> = 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();

View File

@ -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}");
}

View File

@ -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<Value> {
match self {
Value::List(ref inner) => inner,
_ => unreachable!("expected value to be list"),
}
}
pub fn as_box(&self) -> Rc<RefCell<Value>> {
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 {
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 {
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"),
}
}
}

View File

@ -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]
fn flush_console(console: &Value) -> Option<MsgOut> {
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()))
}
// Okay, some more thinking
// The world and process can't have mutable references to one another
// They will each need an Rc<RefCell<PostOffice>>
// 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.