Compare commits

...

5 Commits

Author SHA1 Message Date
Scott Richmond
e5467e9e7e wasm release 2025-07-01 19:08:13 -04:00
Scott Richmond
bba3e1e800 thoughts 2025-07-01 19:07:16 -04:00
Scott Richmond
b7ff0eda80 get reading input up and running 2025-07-01 19:04:38 -04:00
Scott Richmond
5b2fd5e2d7 get fetch up & running 2025-07-01 18:52:03 -04:00
Scott Richmond
b12d0e00aa get input working 2025-07-01 16:59:42 -04:00
12 changed files with 186 additions and 60 deletions

View File

@ -1,6 +1,10 @@
&&& buffers: shared memory with Rust &&& buffers: shared memory with Rust
& use types that are all either empty or any
box console = [] box console = []
box input = "" box input = ""
box fetch_outbox = ""
box fetch_inbox = ()
box keys_down = []
& the very base: know something's type & the very base: know something's type
fn type { fn type {
@ -1244,7 +1248,7 @@ fn alive? {
} }
fn link! { fn link! {
"Creates a 'hard link' between two processes: if either one dies, they both do." "Creates a link between two processes. There are two types of links: `:report`, which sends a message to pid1 when pid2 dies; and `:enforce`, which causes a panic in one when the other dies. The default is `:report`."
(pid1 as :keyword, pid2 as :keyword) -> link! (pid1, pid2, :report) (pid1 as :keyword, pid2 as :keyword) -> link! (pid1, pid2, :report)
(pid1 as :keyword, pid2 as :keyword, :report) -> base :process (:link_report, pid1, pid2) (pid1 as :keyword, pid2 as :keyword, :report) -> base :process (:link_report, pid1, pid2)
(pid1 as :keyword, pid2 as :keyword, :enforce) -> base :process (:link_enforce, pid1, pid2) (pid1 as :keyword, pid2 as :keyword, :enforce) -> base :process (:link_enforce, pid1, pid2)
@ -1260,7 +1264,63 @@ fn sleep! {
(ms as :number) -> base :process (:sleep, ms) (ms as :number) -> base :process (:sleep, ms)
} }
& TODO: make this more robust, to handle multiple pending requests w/o data races
fn request_fetch! {
(pid as :keyword, url as :string) -> {
store! (fetch_outbox, url)
request_fetch! (pid)
}
(pid as :keyword) -> {
if empty? (unbox (fetch_inbox))
then {
yield! ()
request_fetch! (pid)
}
else {
send (pid, (:reply, unbox (fetch_inbox)))
store! (fetch_inbox, ())
}
}
}
fn fetch {
"Requests the contents of the URL passed in. Returns a result tuple of `(:ok, {contents})` or `(:err, {status code})`."
(url) -> {
let pid = self ()
spawn! (fn () -> request_fetch! (pid, url))
receive {
(:reply, response) -> response
}
}
}
fn input_reader! {
(pid as :keyword) -> {
if empty? (unbox (input))
then {
yield! ()
input_reader! (pid)
}
else {
send (pid, (:reply, unbox (input)))
store! (input, "")
}
}
}
fn read_input {
"Waits until there is input in the input buffer, and returns it once there is."
() -> {
let pid = self ()
spawn! (fn () -> input_reader! (pid))
receive {
(:reply, response) -> response
}
}
}
#{ #{
& completed actor functions
self self
send send
spawn! spawn!
@ -1269,11 +1329,20 @@ fn sleep! {
alive? alive?
flush! flush!
link! & wip actor functions
& link!
& shared memory w/ rust
console console
input input
fetch_outbox
fetch_inbox
keys_down
& a fetch fn
fetch
read_input
abs abs
abs abs
add add

View File

@ -1193,17 +1193,17 @@ We've got one bug to address in Firefox before I continue:
After that: After that:
* [ ] implement other verbs beside `console`: * [ ] implement other verbs beside `console`:
- [x] `command` - [x] `command`
- [ ] `input` - [x] `input`
* [ ] js->rust->ludus buffer (in Rust code) * [x] js->rust->ludus buffer (in Rust code)
* [ ] ludus abstractions around this buffer (in Ludus code) * [x] ludus abstractions around this buffer (in Ludus code)
- [ ] `fetch`--request & response - [x] `fetch`--request & response
* [ ] request: ludus->rust->js->net * [x] request: ludus->rust->js->net
* [ ] response: js->rust->ludus * [x] response: js->rust->ludus
- [ ] `keyboard` - [ ] `keyboard`
* [ ] still working on how to represent this * [ ] still working on how to represent this
* [ ] hook this up to `web.ludus.dev` * [ ] hook this up to `web.ludus.dev`
* [ ] do some integration testing * [ ] do some integration testing
- [ ] do synchronous programs still work? - [x] do synchronous programs still work?
- [ ] animations? - [ ] animations?
- [ ] read inputs? - [ ] read inputs?
- [ ] load url text? - [ ] load url text?

View File

@ -13,7 +13,7 @@ let io_interval_id = null
worker.onmessage = handle_messages worker.onmessage = handle_messages
function handle_messages (e) { async function handle_messages (e) {
let msgs let msgs
try { try {
msgs = JSON.parse(e.data) msgs = JSON.parse(e.data)
@ -23,25 +23,32 @@ function handle_messages (e) {
} }
for (const msg of msgs) { for (const msg of msgs) {
switch (msg.verb) { switch (msg.verb) {
case "complete": { case "Complete": {
console.log("Main: ludus completed with => ", msg.data) console.log("Main: ludus completed with => ", msg.data)
ludus_result = msg.data ludus_result = msg.data
running = false running = false
break break
} }
// TODO: do more than report these // TODO: do more than report these
case "console": { case "Console": {
console.log("Main: ludus says => ", msg.data) console.log("Main: ludus says => ", msg.data)
ludus_console = ludus_console + msg.data ludus_console = ludus_console + msg.data
break break
} }
case "commands": { case "Commands": {
console.log("Main: ludus commands => ", msg.data) console.log("Main: ludus commands => ", msg.data)
for (const command of msg.data) { for (const command of msg.data) {
ludus_commands.push(command) ludus_commands.push(command)
} }
break break
} }
case "Fetch": {
console.log("Main: ludus requests => ", msg.data)
const res = await fetch(msg.data, {mode: "cors"})
const text = await res.text()
console.log("Main: js responds => ", text)
outbox.push({verb: "Fetch", data: [msg.data, res.status, text]})
}
} }
} }
} }
@ -67,8 +74,9 @@ function start_io_polling () {
export function run (source) { export function run (source) {
if (running) "TODO: handle this? should not be running" if (running) "TODO: handle this? should not be running"
running = true running = true
result = null
code = source code = source
outbox = [{verb: "run", data: source}] outbox = [{verb: "Run", data: source}]
start_io_polling() start_io_polling()
} }
@ -80,12 +88,12 @@ export function is_running() {
// kills a ludus script // kills a ludus script
export function kill () { export function kill () {
running = false running = false
outbox.push({verb: "kill"}) outbox.push({verb: "Kill"})
} }
// sends text into ludus (status: not working) // sends text into ludus (status: not working)
export function input (text) { export function input (text) {
outbox.push({verb: "input", data: text}) outbox.push({verb: "Input", data: text})
} }
// returns the contents of the ludus console and resets the console // returns the contents of the ludus console and resets the console

4
pkg/rudus.d.ts vendored
View File

@ -14,8 +14,8 @@ export interface InitOutput {
readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_export_6: WebAssembly.Table; readonly __wbindgen_export_6: WebAssembly.Table;
readonly closure323_externref_shim: (a: number, b: number, c: any) => void; readonly closure328_externref_shim: (a: number, b: number, c: any) => void;
readonly closure336_externref_shim: (a: number, b: number, c: any, d: any) => void; readonly closure341_externref_shim: (a: number, b: number, c: any, d: any) => void;
readonly __wbindgen_start: () => void; readonly __wbindgen_start: () => void;
} }

View File

@ -146,11 +146,11 @@ export function ludus(src) {
} }
function __wbg_adapter_20(arg0, arg1, arg2) { function __wbg_adapter_20(arg0, arg1, arg2) {
wasm.closure323_externref_shim(arg0, arg1, arg2); wasm.closure328_externref_shim(arg0, arg1, arg2);
} }
function __wbg_adapter_48(arg0, arg1, arg2, arg3) { function __wbg_adapter_50(arg0, arg1, arg2, arg3) {
wasm.closure336_externref_shim(arg0, arg1, arg2, arg3); wasm.closure341_externref_shim(arg0, arg1, arg2, arg3);
} }
async function __wbg_load(module, imports) { async function __wbg_load(module, imports) {
@ -229,6 +229,9 @@ function __wbg_get_imports() {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
} }
}; };
imports.wbg.__wbg_log_9e426f8e841e42d3 = function(arg0, arg1) {
console.log(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbg_log_edeb598b620f1ba2 = function(arg0, arg1) { imports.wbg.__wbg_log_edeb598b620f1ba2 = function(arg0, arg1) {
let deferred0_0; let deferred0_0;
let deferred0_1; let deferred0_1;
@ -247,7 +250,7 @@ function __wbg_get_imports() {
const a = state0.a; const a = state0.a;
state0.a = 0; state0.a = 0;
try { try {
return __wbg_adapter_48(a, state0.b, arg0, arg1); return __wbg_adapter_50(a, state0.b, arg0, arg1);
} finally { } finally {
state0.a = a; state0.a = a;
} }
@ -325,8 +328,8 @@ function __wbg_get_imports() {
const ret = false; const ret = false;
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper969 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper985 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 324, __wbg_adapter_20); const ret = makeMutClosure(arg0, arg1, 329, __wbg_adapter_20);
return ret; return ret;
}; };
imports.wbg.__wbindgen_init_externref_table = function() { imports.wbg.__wbindgen_init_externref_table = function() {

Binary file not shown.

View File

@ -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_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_export_6: WebAssembly.Table; export const __wbindgen_export_6: WebAssembly.Table;
export const closure323_externref_shim: (a: number, b: number, c: any) => void; export const closure328_externref_shim: (a: number, b: number, c: any) => void;
export const closure336_externref_shim: (a: number, b: number, c: any, d: any) => void; export const closure341_externref_shim: (a: number, b: number, c: any, d: any) => void;
export const __wbindgen_start: () => void; export const __wbindgen_start: () => void;

View File

@ -36,7 +36,7 @@ async function run(e) {
let msgs = e.data let msgs = e.data
for (const msg of msgs) { for (const msg of msgs) {
// evaluate source if we get some // evaluate source if we get some
if (msg.verb === "run" && typeof msg.data === 'string') { if (msg.verb === "Run" && typeof msg.data === 'string') {
// temporarily stash an empty function so we don't keep calling this one if we receive additional messages // temporarily stash an empty function so we don't keep calling this one if we receive additional messages
onmessage = () => {} onmessage = () => {}
// actually run the ludus--which will call `io`--and replace `run` as the event handler for ipc // actually run the ludus--which will call `io`--and replace `run` as the event handler for ipc

View File

@ -31,7 +31,7 @@ fn make_json_payload(verb: &'static str, data: String) -> String {
pub enum MsgOut { pub enum MsgOut {
Console(Lines), Console(Lines),
Commands(Commands), Commands(Commands),
SlurpRequest(Url), Fetch(Url),
Complete(FinalValue), Complete(FinalValue),
} }
@ -46,28 +46,27 @@ impl MsgOut {
match self { match self {
MsgOut::Complete(value) => match value { MsgOut::Complete(value) => match value {
Ok(value) => { Ok(value) => {
log(format!("value is: {}", value.show())); make_json_payload("Complete", serde_json::to_string(&value.show()).unwrap())
make_json_payload("complete", serde_json::to_string(&value.show()).unwrap())
}, },
Err(_) => make_json_payload("complete", "\"null\"".to_string()) Err(_) => make_json_payload("Complete", "\"null\"".to_string())
}, },
MsgOut::Commands(commands) => { MsgOut::Commands(commands) => {
let commands = commands.as_list(); let commands = commands.as_list();
let vals_json = commands.iter().map(|v| v.to_json().unwrap()).collect::<Vec<_>>().join(","); let vals_json = commands.iter().map(|v| v.to_json().unwrap()).collect::<Vec<_>>().join(",");
let vals_json = format!("[{vals_json}]"); let vals_json = format!("[{vals_json}]");
make_json_payload("commands", vals_json) make_json_payload("Commands", vals_json)
} }
MsgOut::SlurpRequest(value) => { MsgOut::Fetch(value) => {
// TODO: do parsing here? // TODO: do parsing here?
// Right now, defer to fetch // Right now, defer to fetch
let url = value.to_json().unwrap(); let url = value.to_json().unwrap();
make_json_payload("slurp", url) make_json_payload("Fetch", url)
} }
MsgOut::Console(lines) => { MsgOut::Console(lines) => {
let lines = lines.as_list(); let lines = lines.as_list();
let json_lines = lines.iter().map(|line| line.stringify()).collect::<Vec<_>>().join("\\n"); let json_lines = lines.iter().map(|line| line.stringify()).collect::<Vec<_>>().join("\\n");
let json_lines = format!("\"{json_lines}\""); let json_lines = format!("\"{json_lines}\"");
make_json_payload("console", json_lines) make_json_payload("Console", json_lines)
} }
} }
} }
@ -77,7 +76,7 @@ impl MsgOut {
#[serde(tag = "verb", content = "data")] #[serde(tag = "verb", content = "data")]
pub enum MsgIn { pub enum MsgIn {
Input(String), Input(String),
SlurpResponse(String, String, String), Fetch(String, f64, String),
Kill, Kill,
Keyboard(Vec<String>), Keyboard(Vec<String>),
} }
@ -85,8 +84,9 @@ pub enum MsgIn {
impl std::fmt::Display for MsgIn { impl std::fmt::Display for MsgIn {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self { match self {
MsgIn::Input(str) => write!(f, "input: {str}"), MsgIn::Input(str) => write!(f, "Input: {str}"),
MsgIn::Kill => write!(f, "kill"), MsgIn::Kill => write!(f, "Kill"),
MsgIn::Fetch(url, code, text) => write!(f, "Fetch: {url} :: {code} ::\n{text}"),
_ => todo!() _ => todo!()
} }
} }
@ -96,11 +96,15 @@ impl MsgIn {
pub fn to_value(self) -> Value { pub fn to_value(self) -> Value {
match self { match self {
MsgIn::Input(str) => Value::string(str), MsgIn::Input(str) => Value::string(str),
MsgIn::SlurpResponse(url, status, string) => { MsgIn::Fetch(url, status_f64, string) => {
let url = Value::string(url); let url = Value::string(url);
let status = Value::keyword(status); let status = Value::Number(status_f64);
let string = Value::string(string); let text = Value::string(string);
let result_tuple = Value::tuple(vec![status, string]); let result_tuple = if status_f64 == 200.0 {
Value::tuple(vec![Value::keyword("ok".to_string()), text])
} else {
Value::tuple(vec![Value::keyword("err".to_string()), status])
};
Value::tuple(vec![url, result_tuple]) Value::tuple(vec![url, result_tuple])
} }
MsgIn::Kill => Value::Nothing, MsgIn::Kill => Value::Nothing,
@ -124,6 +128,7 @@ pub async fn do_io (msgs: Vec<MsgOut>) -> Vec<MsgIn> {
Err(_) => return vec![] Err(_) => return vec![]
}; };
let inbox = inbox.as_string().expect("response should be a string"); let inbox = inbox.as_string().expect("response should be a string");
// log(format!("got a message: {inbox}"));
let inbox: Vec<MsgIn> = serde_json::from_str(inbox.as_str()).expect("response from js should be valid"); let inbox: Vec<MsgIn> = serde_json::from_str(inbox.as_str()).expect("response from js should be valid");
if !inbox.is_empty() { if !inbox.is_empty() {
log("ludus received messages".to_string()); log("ludus received messages".to_string());

View File

@ -60,9 +60,9 @@ fn prelude() -> HashMap<&'static str, Value> {
.into_output_errors(); .into_output_errors();
if !parse_errors.is_empty() { if !parse_errors.is_empty() {
println!("ERROR PARSING PRELUDE:"); log("ERROR PARSING PRELUDE:");
println!("{:?}", parse_errors); log(format!("{:?}", parse_errors).as_str());
panic!(); panic!("parsing errors in prelude");
} }
let parsed = parsed.unwrap(); let parsed = parsed.unwrap();
@ -77,8 +77,9 @@ fn prelude() -> HashMap<&'static str, Value> {
validator.validate(); validator.validate();
if !validator.errors.is_empty() { if !validator.errors.is_empty() {
println!("VALIDATION ERRORS IN PRLUDE:"); log("VALIDATION ERRORS IN PRLUDE:");
report_invalidation(validator.errors); // report_invalidation(validator.errors);
log(format!("{:?}", validator.errors).as_str());
panic!("validator errors in prelude"); panic!("validator errors in prelude");
} }

View File

@ -398,6 +398,21 @@ impl Value {
} }
} }
pub fn as_string(&self) -> Rc<String> {
match self {
Value::String(str) => str.clone(),
Value::Interned(str) => Rc::new(str.to_string()),
_ => unreachable!("expected value to be a string"),
}
}
pub fn as_tuple(&self) -> Rc<Vec<Value>> {
match self {
Value::Tuple(members) => members.clone(),
_ => unreachable!("expected value to be a tuple"),
}
}
pub fn string(str: String) -> Value { pub fn string(str: String) -> Value {
Value::String(Rc::new(str)) Value::String(Rc::new(str))
} }

View File

@ -254,8 +254,8 @@ impl Zoo {
pub struct Buffers { pub struct Buffers {
console: Value, console: Value,
commands: Value, commands: Value,
// fetch_outbox: Value, fetch_out: Value,
// fetch_inbox: Value, fetch_in: Value,
input: Value, input: Value,
} }
@ -264,8 +264,8 @@ impl Buffers {
Buffers { Buffers {
console: prelude.get("console").unwrap().clone(), console: prelude.get("console").unwrap().clone(),
commands: prelude.get("turtle_commands").unwrap().clone(), commands: prelude.get("turtle_commands").unwrap().clone(),
// fetch_outbox: prelude.get("fetch_outbox").unwrap().clone(), fetch_out: prelude.get("fetch_outbox").unwrap().clone(),
// fetch_inbox: prelude.get("fetch_inbox").unwrap().clone(), fetch_in: prelude.get("fetch_inbox").unwrap().clone(),
input: prelude.get("input").unwrap().clone(), input: prelude.get("input").unwrap().clone(),
} }
} }
@ -282,12 +282,13 @@ impl Buffers {
self.commands.as_box() self.commands.as_box()
} }
// pub fn fetch_outbox (&self) -> Rc<RefCell<Value>> { pub fn fetch_out (&self) -> Rc<RefCell<Value>> {
// self.fetch_outbox.as_box() self.fetch_out.as_box()
// } }
// pub fn fetch_inbox (&self) -> Rc<RefCell<Value>> {
// self.fetch_inbox.as_box() pub fn fetch_in (&self) -> Rc<RefCell<Value>> {
// } self.fetch_in.as_box()
}
} }
@ -369,16 +370,34 @@ impl World {
if let Some(commands) = self.flush_commands() { if let Some(commands) = self.flush_commands() {
outbox.push(commands); outbox.push(commands);
} }
if let Some(fetch) = self.make_fetch_happen() {
outbox.push(fetch);
}
outbox outbox
} }
fn make_fetch_happen(&self) -> Option<MsgOut> {
let out = self.buffers.fetch_out();
let working = RefCell::new(Value::Interned(""));
out.swap(&working);
let working = working.borrow();
if working.as_string().is_empty() {
None
} else {
Some(MsgOut::Fetch(working.clone()))
}
}
fn flush_console(&self) -> Option<MsgOut> { fn flush_console(&self) -> Option<MsgOut> {
let console = self.buffers.console(); let console = self.buffers.console();
let working_copy = RefCell::new(Value::new_list()); let working_copy = RefCell::new(Value::new_list());
console.swap(&working_copy); console.swap(&working_copy);
let working_value = working_copy.borrow(); let working_value = working_copy.borrow();
if working_value.as_list().is_empty() { return None; } if working_value.as_list().is_empty() {
Some(MsgOut::Console(working_value.clone())) None
} else {
Some(MsgOut::Console(working_value.clone()))
}
} }
fn flush_commands(&self) -> Option<MsgOut> { fn flush_commands(&self) -> Option<MsgOut> {
@ -422,11 +441,17 @@ impl World {
input.swap(&working); input.swap(&working);
} }
fn fetch_reply(&mut self, reply: Value) {
let inbox_rc = self.buffers.fetch_in();
inbox_rc.replace(reply);
}
fn fill_buffers(&mut self, inbox: Vec<MsgIn>) { fn fill_buffers(&mut self, inbox: Vec<MsgIn>) {
for msg in inbox { for msg in inbox {
match msg { match msg {
MsgIn::Input(str) => self.fill_input(str), MsgIn::Input(str) => self.fill_input(str),
MsgIn::Kill => self.kill_signal = true, MsgIn::Kill => self.kill_signal = true,
MsgIn::Fetch(..) => self.fetch_reply(msg.to_value()),
_ => todo!() _ => todo!()
} }
} }