spawn is now a special form

This commit is contained in:
Scott Richmond 2025-07-04 17:24:54 -04:00
parent 8ce6a33573
commit bbdab93cf0
14 changed files with 95 additions and 28 deletions

View File

@ -1247,10 +1247,10 @@ fn send {
} }
} }
fn spawn! { & fn spawn! {
"Spawns a new process running the function passed in." & "Spawns a new process running the function passed in."
(f as :fn) -> base :process (:spawn, f) & (f as :fn) -> base :process (:spawn, f)
} & }
fn yield! { fn yield! {
"Forces a process to yield." "Forces a process to yield."
@ -1303,7 +1303,7 @@ fn fetch {
"Requests the contents of the URL passed in. Returns a result tuple of `(:ok, {contents})` or `(:err, {status code})`." "Requests the contents of the URL passed in. Returns a result tuple of `(:ok, {contents})` or `(:err, {status code})`."
(url) -> { (url) -> {
let pid = self () let pid = self ()
spawn! (fn () -> request_fetch! (pid, url)) spawn! request_fetch! (pid, url)
receive { receive {
(:reply, response) -> response (:reply, response) -> response
} }
@ -1312,7 +1312,7 @@ fn fetch {
fn input_reader! { fn input_reader! {
(pid as :keyword) -> { (pid as :keyword) -> {
if not (unbox (input)) if do input > unbox > not
then { then {
yield! () yield! ()
input_reader! (pid) input_reader! (pid)
@ -1328,7 +1328,7 @@ fn read_input {
"Waits until there is input in the input buffer, and returns it once there is." "Waits until there is input in the input buffer, and returns it once there is."
() -> { () -> {
let pid = self () let pid = self ()
spawn! (fn () -> input_reader! (pid)) spawn! input_reader! (pid)
receive { receive {
(:reply, response) -> response (:reply, response) -> response
} }
@ -1339,7 +1339,7 @@ fn read_input {
& completed actor functions & completed actor functions
self self
send send
spawn! & spawn!
yield! yield!
sleep! sleep!
alive? alive?

View File

@ -13,18 +13,6 @@ let ready = false
let io_interval_id = null let io_interval_id = null
let keys_down = new Set(); let keys_down = new Set();
function reset_ludus () {
outbox = []
ludus_console = ""
ludus_commands = []
ludus_result = null
code = null
running = false
ready = false
io_interval_id = null
keys_down = new Set()
}
worker.onmessage = handle_messages worker.onmessage = handle_messages
async function handle_messages (e) { async function handle_messages (e) {
@ -99,17 +87,28 @@ function io_poller () {
} }
function start_io_polling () { function start_io_polling () {
io_interval_id = setInterval(io_poller, 10) io_interval_id = setInterval(io_poller, 100)
} }
// runs a ludus script; does not return the result // runs a ludus script; does not return the result
// the result must be explicitly polled with `result` // the result must be explicitly polled with `result`
export function run (source) { export function run (source) {
if (running || ready) { if (running || ready) {
console.log("Main: received bouncy `run` call");
return "TODO: handle this? should not be running" return "TODO: handle this? should not be running"
} }
// start the vm
worker.postMessage([{verb: "Run", data: source}]) worker.postMessage([{verb: "Run", data: source}])
reset_ludus() // reset all my state
outbox = []
ludus_console = ""
ludus_commands = []
ludus_result = null
code = null
running = true
ready = false
keys_down = new Set();
// start the polling loop loop
start_io_polling() start_io_polling()
} }

View File

@ -403,7 +403,7 @@ function __wbg_get_imports() {
_assertBoolean(ret); _assertBoolean(ret);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper8065 = function() { return logError(function (arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper8087 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 354, __wbg_adapter_20); const ret = makeMutClosure(arg0, arg1, 354, __wbg_adapter_20);
return ret; return ret;
}, arguments) }; }, arguments) };

Binary file not shown.

View File

@ -18,7 +18,7 @@ export function io (out) {
resolve(JSON.stringify(e.data)) resolve(JSON.stringify(e.data))
} }
// cancel the response if it takes too long // cancel the response if it takes too long
setTimeout(() => reject("io took too long"), 500) setTimeout(() => reject("io took too long"), 1000)
}) })
} }

View File

@ -51,6 +51,7 @@ pub enum Ast {
WhenClause(Box<Spanned<Self>>, Box<Spanned<Self>>), WhenClause(Box<Spanned<Self>>, Box<Spanned<Self>>),
Match(Box<Spanned<Self>>, Vec<Spanned<Self>>), Match(Box<Spanned<Self>>, Vec<Spanned<Self>>),
Receive(Vec<Spanned<Self>>), Receive(Vec<Spanned<Self>>),
Spawn(Box<Spanned<Self>>),
MatchClause( MatchClause(
Box<Spanned<Self>>, Box<Spanned<Self>>,
Box<Option<Spanned<Self>>>, Box<Option<Spanned<Self>>>,
@ -210,6 +211,7 @@ impl Ast {
.join(" > ") .join(" > ")
) )
} }
Spawn(body) => format!("spawn! {}", body.0.show()),
Repeat(times, body) => format!("repeat {} {{\n{}\n}}", times.0.show(), body.0.show()), Repeat(times, body) => format!("repeat {} {{\n{}\n}}", times.0.show(), body.0.show()),
Splat(word) => format!("...{}", word), Splat(word) => format!("...{}", word),
Splattern(pattern) => format!("...{}", pattern.0.show()), Splattern(pattern) => format!("...{}", pattern.0.show()),
@ -380,6 +382,7 @@ impl fmt::Display for Ast {
.join(" > ") .join(" > ")
) )
} }
Spawn(body) => write!(f, "spawn: {}", body.0),
Repeat(_times, _body) => todo!(), Repeat(_times, _body) => todo!(),
Splat(word) => { Splat(word) => {
write!(f, "splat: {}", word) write!(f, "splat: {}", word)

View File

@ -43,7 +43,7 @@ impl Chunk {
| Not | Panic | EmptyString | ConcatStrings | Stringify | MatchType | Return | Not | Panic | EmptyString | ConcatStrings | Stringify | MatchType | Return
| UnconditionalMatch | Print | AppendList | ConcatList | PushList | PushDict | UnconditionalMatch | Print | AppendList | ConcatList | PushList | PushDict
| AppendDict | ConcatDict | Nothing | PushGlobal | SetUpvalue | LoadMessage | AppendDict | ConcatDict | Nothing | PushGlobal | SetUpvalue | LoadMessage
| NextMessage | MatchMessage | ClearMessage | SendMethod | LoadScrutinee => { | NextMessage | MatchMessage | ClearMessage | SendMethod | LoadScrutinee | Spawn => {
console_log!("{i:04}: {op}") console_log!("{i:04}: {op}")
} }
Constant | MatchConstant => { Constant | MatchConstant => {

View File

@ -1059,6 +1059,54 @@ impl Compiler {
self.emit_op(Op::Load); self.emit_op(Op::Load);
self.stack_depth += 1; self.stack_depth += 1;
} }
Spawn(body) => {
// we compile this identically to a function
// but it's a single, nullary fn
// no match at the beginning, just go
let name = "_spawned";
let mut compiler = Compiler::new(
body,
self.input,
self.src,
self.depth + 1,
self.chunk.env.clone(),
self.debug,
);
compiler.tail_pos = true;
compiler.visit(body);
compiler.store();
compiler.scope_depth -= 1;
while let Some(binding) = compiler.bindings.last() {
if binding.depth > compiler.scope_depth {
compiler.bindings.pop();
} else {
break;
}
}
compiler.pop_n(compiler.stack_depth);
compiler.stack_depth = 0;
compiler.emit_op(Op::Return);
let arities = vec![0];
let chunks = vec![compiler.chunk];
let lfn = crate::value::LFn::Defined {
name,
doc: None,
arities,
chunks,
splat: 0,
closed: RefCell::new(vec![]),
};
let the_fn = Value::Fn(Rc::new(lfn));
self.emit_constant(the_fn);
for upvalue in compiler.upvalues {
self.resolve_binding(upvalue);
self.emit_op(Op::SetUpvalue);
self.stack_depth -= 1;
}
self.emit_op(Op::Spawn);
}
Receive(clauses) => { Receive(clauses) => {
let tail_pos = self.tail_pos; let tail_pos = self.tail_pos;
self.emit_op(Op::ClearMessage); self.emit_op(Op::ClearMessage);

View File

@ -82,10 +82,13 @@ pub async fn send_err_to_ludus_console(msg: String) {
pub async fn do_io (msgs: Vec<MsgOut>) -> Vec<MsgIn> { pub async fn do_io (msgs: Vec<MsgOut>) -> Vec<MsgIn> {
let json = serde_json::to_string(&msgs).unwrap(); let json = serde_json::to_string(&msgs).unwrap();
let inbox = io (json).await; let inbox = io(json).await;
let inbox = match inbox { let inbox = match inbox {
Ok(msgs) => msgs, Ok(msgs) => msgs,
Err(_) => return vec![] Err(errs) => {
console_log!("error receiving messages in io; {:?}", errs);
return vec![];
}
}; };
let inbox = inbox.as_string().expect("response should be a string"); 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"); let inbox: Vec<MsgIn> = serde_json::from_str(inbox.as_str()).expect("response from js should be valid");

View File

@ -76,7 +76,7 @@ pub fn lexer(
"nil" => Token::Nil, "nil" => Token::Nil,
// todo: hard code these as type constructors // todo: hard code these as type constructors
"as" | "box" | "do" | "else" | "fn" | "if" | "let" | "loop" | "match" | "panic!" "as" | "box" | "do" | "else" | "fn" | "if" | "let" | "loop" | "match" | "panic!"
| "recur" | "repeat" | "then" | "when" | "with" | "or" | "and" | "receive" => { | "recur" | "repeat" | "then" | "when" | "with" | "or" | "and" | "receive" | "spawn!" => {
Token::Reserved(word) Token::Reserved(word)
} }
_ => Token::Word(word), _ => Token::Word(word),

View File

@ -106,6 +106,7 @@ pub enum Op {
MatchMessage, MatchMessage,
ClearMessage, ClearMessage,
SendMethod, SendMethod,
Spawn,
LoadScrutinee, LoadScrutinee,
} }
@ -208,6 +209,7 @@ impl std::fmt::Display for Op {
MatchMessage => "match_message", MatchMessage => "match_message",
ClearMessage => "clear_message", ClearMessage => "clear_message",
SendMethod => "send_method", SendMethod => "send_method",
Spawn => "spawn",
LoadScrutinee => "load_scrutinee", LoadScrutinee => "load_scrutinee",
}; };

View File

@ -512,6 +512,10 @@ where
.then(block.clone()) .then(block.clone())
.map_with(|(count, body), e| (Repeat(Box::new(count), Box::new(body)), e.span())); .map_with(|(count, body), e| (Repeat(Box::new(count), Box::new(body)), e.span()));
let spawn = just(Token::Reserved("spawn!"))
.ignore_then(nonbinding.clone())
.map_with(|body, e| (Spawn(Box::new(body)), e.span()));
let fn_guarded = tuple_pattern let fn_guarded = tuple_pattern
.clone() .clone()
.then_ignore(just(Token::Reserved("if"))) .then_ignore(just(Token::Reserved("if")))
@ -590,6 +594,7 @@ where
.or(conditional) .or(conditional)
.or(block) .or(block)
.or(repeat) .or(repeat)
.or(spawn)
.or(r#loop) .or(r#loop)
.labelled("nonbinding expression"), .labelled("nonbinding expression"),
); );

View File

@ -164,6 +164,7 @@ impl<'a> Validator<'a> {
let root = self.ast; let root = self.ast;
match root { match root {
Error => unreachable!(), Error => unreachable!(),
Spawn(body) => self.visit(body),
Word(name) | Splat(name) => { Word(name) | Splat(name) => {
if !self.resolved(name) { if !self.resolved(name) {
self.err(format!("unbound name `{name}`")) self.err(format!("unbound name `{name}`"))

View File

@ -1260,6 +1260,12 @@ impl Creature {
LoadScrutinee => { LoadScrutinee => {
self.scrutinee = Some(self.peek().clone()); self.scrutinee = Some(self.peek().clone());
} }
Spawn => {
let f = self.pop();
let proc = Creature::spawn(f, self.zoo.clone(), self.debug);
let id = self.zoo.borrow_mut().put(proc);
self.push(Value::Keyword(id));
}
} }
} }
} }