first pass at multiturtles

This commit is contained in:
Scott Richmond 2025-07-05 17:09:01 -04:00
parent f1f954de46
commit bac3c29d1d
3 changed files with 172 additions and 131 deletions

View File

@ -1014,6 +1014,108 @@ fn assert! {
else panic! "Assert failed: {msg} with {value}" else panic! "Assert failed: {msg} with {value}"
} }
&&& processes: doing more than one thing
fn self {
"Returns the current process's pid, as a keyword."
() -> base :process (:self)
}
fn send {
"Sends a message to the specified process and returns the message."
(pid as :keyword, msg) -> {
base :process (:send, pid, msg)
msg
}
}
fn spawn! {
"Spawns a process. Takes a 0-argument (nullary) function that will be executed as the new process. Returns a keyword process ID (pid) of the newly spawned process."
(f as :fn) -> base :process (:spawn, f)
}
fn yield! {
"Forces a process to yield."
() -> base :process (:yield)
}
fn alive? {
"Tells if the passed keyword is the id for a live process."
(pid as :keyword) -> base :process (:alive, pid)
}
fn link! {
"Creates a link between two processes. There are two types of links: `:report`, which sends a message to pid1 when pid2 dies; and `:panic`, 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, :report) -> base :process (:link_report, pid1, pid2)
(pid1 as :keyword, pid2 as :keyword, :panic) -> base :process (:link_panic, pid1, pid2)
}
fn flush! {
"Clears the current process's mailbox and returns all the messages."
() -> base :process (:flush)
}
fn sleep! {
"Puts the current process to sleep for at least the specified number of milliseconds."
(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 do input > unbox > not
then {
yield! ()
input_reader! (pid)
}
else {
send (pid, (:reply, unbox (input)))
store! (input, nil)
}
}
}
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
}
}
}
&&& Turtle & other graphics &&& Turtle & other graphics
& some basic colors & some basic colors
@ -1180,6 +1282,46 @@ fn loadstate! {
} }
} }
fn turtle_listener () -> {
receive {
(:forward!, steps as :number) -> add_command! (self (), (:forward, steps))
(:back!, steps as :number) -> add_command! (self (), (:back, steps))
(:left!, turns as :number) -> add_command! (self (), (:left, turns))
(:right!, turns as :number) -> add_command! (self (), (:right, turns))
(:penup!) -> add_command!(self (), (:penup))
(:pendown!) -> add_command! (self (), (:pendown))
(:pencolor!, color as :keyword) -> add_command! (self (), (:pencolor, color))
(:pencolor!, gray as :number) -> add_command! (self (), (:pencolor, (gray, gray, gray, 255)))
(:pencolor!, (r as :number, g as :number, b as :number)) -> add_command! (self (), (:pencolor, (r, g, b, 255)))
(:pencolor!, (r as :number, g as :number, b as :number, a as :number)) -> add_command! (self (), (:pencolor, (r, g, b, a)))
(:penwidth!, width as :number) -> add_command! (self (), (:penwidth, width))
(:home!) -> add_command! (self (), (:home))
(:goto!, x as :number, y as :number) -> add_command! (self (), (:goto, (x, y)))
(:goto!, (x as :number, y as :number)) -> add_command! (self (), (:goto, (x, y)))
(:show!) -> add_command! (self (), (:show))
(:hide!) -> add_command! (self (), (:hide))
(:loadstate!, state) -> {
let #{position, heading, pendown?, pencolor, penwidth, visible?} = state
add_command! (self (), (:loadstate, position, heading, visible?, pendown?, penwidth, pencolor))
}
(:pencolor, pid) -> send (pid, (:reply, do turtle_states > unbox > self () > :pencolor))
(:position, pid) -> send (pid, (:reply, do turtle_states > unbox > self () > :position))
(:penwidth, pid) -> send (pid, (:reply, do turtle_states > unbox > self () > :penwidth))
(:heading, pid) -> send (pid, (:reply, do turtle_states > unbox > self () > :heading))
does_not_understand -> {
let pid = self ()
panic! "{pid} does not understand message: {does_not_understand}"
}
}
turtle_listener ()
}
fn spawn_turtle! {
"Spawns a new turtle in a new process. Methods on the turtle process mirror those of turtle graphics functions in prelude. Returns the pid of the new turtle."
() -> spawn! (fn () -> turtle_listener ())
}
fn apply_command { fn apply_command {
"Takes a turtle state and a command and calculates a new state." "Takes a turtle state and a command and calculates a new state."
(state, command) -> { (state, command) -> {
@ -1217,9 +1359,8 @@ fn apply_command {
}} }}
} }
& position () -> (x, y)
fn position { fn position {
"Returns the turtle's current position." "Returns the turtle's current position as an `(x, y)` vector tuple."
() -> do turtle_states > unbox > :turtle_0 > :position () -> do turtle_states > unbox > :turtle_0 > :position
} }
@ -1245,6 +1386,7 @@ fn penwidth {
() -> do turtle_states > unbox > :turtle_0 > :penwidth () -> do turtle_states > unbox > :turtle_0 > :penwidth
} }
&&& fake some lispisms with tuples &&& fake some lispisms with tuples
fn cons { fn cons {
"Old-timey lisp `cons`. `Cons`tructs a tuple out of two arguments." "Old-timey lisp `cons`. `Cons`tructs a tuple out of two arguments."
@ -1266,108 +1408,6 @@ fn llist {
(...xs) -> foldr (cons, xs, nil) (...xs) -> foldr (cons, xs, nil)
} }
&&& processes
fn self {
"Returns the current process's pid, as a keyword."
() -> base :process (:self)
}
fn send {
"Sends a message to the specified process and returns the message."
(pid as :keyword, msg) -> {
base :process (:send, pid, msg)
msg
}
}
fn spawn! {
"Spawns a process. Takes a 0-argument (nullary) function that will be executed as the new process. Returns a keyword process ID (pid) of the newly spawned process."
(f as :fn) -> base :process (:spawn, f)
}
fn yield! {
"Forces a process to yield."
() -> base :process (:yield)
}
& TODO: implement these in the VM
fn alive? {
"Tells if the passed keyword is the id for a live process."
(pid as :keyword) -> base :process (:alive, pid)
}
fn link! {
"Creates a link between two processes. There are two types of links: `:report`, which sends a message to pid1 when pid2 dies; and `:panic`, 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, :report) -> base :process (:link_report, pid1, pid2)
(pid1 as :keyword, pid2 as :keyword, :panic) -> base :process (:link_panic, pid1, pid2)
}
fn flush! {
"Clears the current process's mailbox and returns all the messages."
() -> base :process (:flush)
}
fn sleep! {
"Puts the current process to sleep for at least the specified number of milliseconds."
(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 do input > unbox > not
then {
yield! ()
input_reader! (pid)
}
else {
send (pid, (:reply, unbox (input)))
store! (input, nil)
}
}
}
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 & completed actor functions
self self
@ -1380,6 +1420,7 @@ fn read_input {
& wip actor functions & wip actor functions
& link! & link!
spawn_turtle!
& shared memory w/ rust & shared memory w/ rust
& `box`es are actually way cool & `box`es are actually way cool

View File

@ -1,4 +1,4 @@
if (window) window.ludus = {run, kill, flush_stdout, stdout, p5, new_p5, svg, flush_commands, commands, result, flush_result, input, is_running, key_down, key_up, is_starting_up} if (window) window.ludus = {run, kill, flush_stdout, stdout, p5, svg, flush_commands, commands, result, flush_result, input, is_running, key_down, key_up, is_starting_up}
const worker_url = new URL("worker.js", import.meta.url) const worker_url = new URL("worker.js", import.meta.url)
const worker = new Worker(worker_url, {type: "module"}) const worker = new Worker(worker_url, {type: "module"})
@ -440,6 +440,7 @@ function svg_render_turtle (state) {
` `
} }
// TODO: update this to match the new `p5` function
export function svg (commands) { export function svg (commands) {
// console.log(commands) // console.log(commands)
const states = [turtle_init] const states = [turtle_init]
@ -509,32 +510,32 @@ function p5_render_turtle (state, calls) {
return calls return calls
} }
export function p5 (commands) { // export function p5 (commands) {
const states = [turtle_init] // const states = [turtle_init]
commands.reduce((prev_state, command) => { // commands.reduce((prev_state, command) => {
const new_state = command_to_state(prev_state, command) // const new_state = command_to_state(prev_state, command)
states.push(new_state) // states.push(new_state)
return new_state // return new_state
}, turtle_init) // }, turtle_init)
// console.log(states) // // console.log(states)
const [r, g, b, _] = resolve_color(background_color) // const [r, g, b, _] = resolve_color(background_color)
if ((r + g + b)/3 > 128) turtle_color = [0, 0, 0, 150] // if ((r + g + b)/3 > 128) turtle_color = [0, 0, 0, 150]
const p5_calls = [...p5_call_root()] // const p5_calls = [...p5_call_root()]
for (let i = 1; i < states.length; ++i) { // for (let i = 1; i < states.length; ++i) {
const prev = states[i - 1] // const prev = states[i - 1]
const curr = states[i] // const curr = states[i]
const calls = states_to_call(prev, curr) // const calls = states_to_call(prev, curr)
for (const call of calls) { // for (const call of calls) {
p5_calls.push(call) // p5_calls.push(call)
} // }
} // }
p5_calls[0] = ["background", ...resolve_color(background_color)] // p5_calls[0] = ["background", ...resolve_color(background_color)]
p5_render_turtle(states[states.length - 1], p5_calls) // p5_render_turtle(states[states.length - 1], p5_calls)
p5_calls.push(["pop"]) // p5_calls.push(["pop"])
return p5_calls // return p5_calls
} // }
export function new_p5 (commands) { export function p5 (commands) {
const all_states = {} const all_states = {}
commands.reduce((prev_state, command) => { commands.reduce((prev_state, command) => {
const [turtle_id, new_state] = command_to_state(prev_state, command) const [turtle_id, new_state] = command_to_state(prev_state, command)
@ -546,7 +547,6 @@ export function new_p5 (commands) {
if ((r + g + b)/3 > 128) turtle_color = [0, 0, 0, 150] if ((r + g + b)/3 > 128) turtle_color = [0, 0, 0, 150]
const p5_calls = [...p5_call_root()] const p5_calls = [...p5_call_root()]
for (const states of Object.values(all_states)) { for (const states of Object.values(all_states)) {
console.log(states)
for (let i = 1; i < states.length; ++i) { for (let i = 1; i < states.length; ++i) {
const prev = states[i - 1] const prev = states[i - 1]
const curr = states[i] const curr = states[i]

Binary file not shown.