Compare commits

...

82 Commits

Author SHA1 Message Date
Scott Richmond
d52faeff41 asdf 2025-07-04 15:57:16 -04:00
Scott Richmond
ed6976fe35 asdf 2025-07-04 15:37:39 -04:00
Scott Richmond
22ac3cb0fe release build 2025-07-04 15:30:26 -04:00
Scott Richmond
8851002a90 add is_starting_up 2025-07-04 15:29:44 -04:00
Scott Richmond
cd80e65528 release build 2025-07-04 15:19:32 -04:00
Scott Richmond
f853e02f00 fix slice_n 2025-07-04 15:18:49 -04:00
Scott Richmond
dc11d6cc58 release build 2025-07-04 15:10:51 -04:00
Scott Richmond
7cffa43c3e globalize key_down and key_up 2025-07-04 15:09:02 -04:00
Scott Richmond
294d7d6be2 release build 2025-07-04 14:44:50 -04:00
Scott Richmond
2808c0b709 add slice_n to prelude 2025-07-04 14:44:09 -04:00
Scott Richmond
55483d54a2 release build 2025-07-04 14:11:01 -04:00
Scott Richmond
3b8d3ff5e3 consolidate js functions 2025-07-04 14:10:27 -04:00
Scott Richmond
9228e060bb keep working on panics: tracebacks sort of work? 2025-07-04 14:10:03 -04:00
Scott Richmond
050a0f987d also put the new panic mod under version control 2025-07-04 01:23:31 -04:00
Scott Richmond
0d8b42662b working on panics 2025-07-04 01:23:16 -04:00
Scott Richmond
f97f6670bd pretty good parsing errors 2025-07-03 23:23:14 -04:00
Scott Richmond
d6a004d9ac scanning errors are now nice 2025-07-03 20:45:55 -04:00
Scott Richmond
c6709bb2e8 use serde to serialize the things 2025-07-03 20:22:11 -04:00
Scott Richmond
9f9f59b33b string keys on dicts now fully work 2025-07-03 15:30:51 -04:00
Scott Richmond
659fdd3506 add string keys to dicts 2025-07-03 12:41:00 -04:00
Scott Richmond
d334e483a5 work on errors 2025-07-02 23:47:02 -04:00
Scott Richmond
2ffff9edd9 properly scan escape chars 2025-07-02 20:54:21 -04:00
Scott Richmond
28d6dc24f0 make an attempt at fixing string escaping 2025-07-02 19:44:12 -04:00
Scott Richmond
0cd682de21 method syntax sugar achieved 2025-07-02 19:29:49 -04:00
Scott Richmond
12389ae371 do and panic are now simple forms 2025-07-02 17:29:09 -04:00
Scott Richmond
bf204696a5 release build 2025-07-02 16:56:59 -04:00
Scott Richmond
6bdb9779d8 don't discard initial messages 2025-07-02 16:56:30 -04:00
Scott Richmond
2f4ab41a62 add log to input 2025-07-02 16:20:22 -04:00
Scott Richmond
1316c8228f release build 2025-07-02 16:05:49 -04:00
Scott Richmond
dcf550ba2f wasm->build 2025-07-02 16:05:38 -04:00
Scott Richmond
df5c745ce9 fix complete reset 2025-07-02 16:05:06 -04:00
Scott Richmond
1435e753e8 move default to the top 2025-07-02 15:47:33 -04:00
Scott Richmond
f6ad3b6966 clean up justfile 2025-07-02 15:43:44 -04:00
Scott Richmond
5a778d9a55 try again w/ justfile 2025-07-02 15:37:56 -04:00
Scott Richmond
62ad321a88 finish release recipe? 2025-07-02 15:35:19 -04:00
Scott Richmond
14a41dc1bd justinging 2025-07-02 15:34:36 -04:00
Scott Richmond
d9b095c3f3 keep justing 2025-07-02 15:26:19 -04:00
Scott Richmond
44739adfe5 keep working on justfile 2025-07-02 15:19:54 -04:00
Scott Richmond
624c0bd2f8 start work on release recipe 2025-07-02 15:04:54 -04:00
Scott Richmond
1158821aff build 2025-07-02 14:52:22 -04:00
Scott Richmond
cfe8009861 ready handshake for better message passing 2025-07-02 14:51:42 -04:00
Scott Richmond
33b7f78038 build 2025-07-02 13:49:36 -04:00
Scott Richmond
116a5b2ed9 prevent rust panic on kill signal 2025-07-02 13:44:26 -04:00
Scott Richmond
9414dc64d9 actually (?!) fix drunk turtle problem 2025-07-01 20:10:24 -04:00
Scott Richmond
197cbfc795 try again 2025-07-01 20:07:02 -04:00
Scott Richmond
f3801b3c37 maybe fix drunk turtle bug? 2025-07-01 19:55:49 -04:00
Scott Richmond
f8983d24a4 another wasm release 2025-07-01 19:20:33 -04:00
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
Scott Richmond
808368d2b9 update worker url resolution 2025-07-01 16:30:17 -04:00
Scott Richmond
88ff5886bb fix worker path 2025-07-01 16:07:01 -04:00
Scott Richmond
1ec60b9362 get commands wired up, probs 2025-07-01 14:35:36 -04:00
Scott Richmond
400bd5864b fix FF event loop bug 2025-07-01 12:54:11 -04:00
Scott Richmond
991705e734 add thoughts 2025-07-01 11:10:50 -04:00
Scott Richmond
989e217917 stash changes 2025-07-01 10:42:34 -04:00
Scott Richmond
4e7557cbcc fix truly heinous memory bug 2025-07-01 01:30:10 -04:00
Scott Richmond
2f3f362f49 hook the things up and discover a possible stop-the-world bug 2025-07-01 00:43:01 -04:00
Scott Richmond
4eceb62ce5 integration work continues 2025-06-30 18:59:59 -04:00
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
Scott Richmond
5478e5e40e use a hashset instead of vec for dead ids 2025-06-29 18:14:06 -04:00
Scott Richmond
f6cbe3f800 start working on packaging better 2025-06-29 18:13:49 -04:00
Scott Richmond
c62b5c903d update chumsky, lose ariadne, update parser to conform to new chumsky 2025-06-29 18:08:44 -04:00
Scott Richmond
de6cb5380d add a justfile, some project management 2025-06-29 17:47:08 -04:00
Scott Richmond
4dd47dd56c save work 2025-06-29 11:38:45 -04:00
Scott Richmond
f710beff46 actually get receive working???? 2025-06-28 16:40:31 -04:00
Scott Richmond
f873be7668 some notes 2025-06-27 20:54:48 -04:00
Scott Richmond
48342ba4ea make progress, I guess 2025-06-27 20:41:29 -04:00
Scott Richmond
db52bc2687 parser housekeeping; add receive to lexer and parser 2025-06-27 19:15:59 -04:00
Scott Richmond
a175ee7a41 move Ast into its own module 2025-06-27 19:05:17 -04:00
Scott Richmond
759fc63cae ugh. spin my wheels a lot. decide to start work on the receive special form 2025-06-27 18:48:27 -04:00
Scott Richmond
8923581eed add sleep, which was unexpectedly titchy! 2025-06-27 14:27:42 -04:00
Scott Richmond
90505f89fe make some new process functions 2025-06-27 12:27:54 -04:00
Scott Richmond
00ebac17ce some notes for tomorrow's work 2025-06-26 23:28:17 -04:00
Scott Richmond
888f5b62da send messages, motherfucker! 2025-06-26 20:30:40 -04:00
Scott Richmond
c144702b98 add a process value 2025-06-26 17:17:41 -04:00
Scott Richmond
801e5bcc01 devise a way of communicating between ludus and processes 2025-06-26 17:15:00 -04:00
Scott Richmond
b35657e698 refactor to have a world run a process 2025-06-26 16:11:35 -04:00
Scott Richmond
b5528ced8f start work on actor model 2025-06-26 01:28:33 -04:00
34 changed files with 3947 additions and 1976 deletions

View File

@ -3,22 +3,19 @@ name = "rudus"
version = "0.0.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
ariadne = { git = "https://github.com/zesterer/ariadne" }
chumsky = { git = "https://github.com/zesterer/chumsky", features = ["label"] }
chumsky = "0.10.1"
imbl = "3.0.0"
ran = "2.0.1"
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"
console_error_panic_hook = "0.1.7"
struct_scalpel = "0.1.1"
serde-wasm-bindgen = "0.6.5"

31
assets/agent.ld Normal file
View File

@ -0,0 +1,31 @@
fn agent (val) -> receive {
(:set, new) -> agent (new)
(:get, pid) -> {
send (pid, (:response, val))
agent (val)
}
(:update, f) -> agent (f (val))
}
fn agent/set (pid, val) -> {
send (pid, (:set, val))
val
}
fn agent/get (pid) -> {
send (pid, (:get, self ()))
receive {
(:response, val) -> val
}
}
fn agent/update (pid, f) -> {
send (pid, (:update, f))
agent/get (pid)
}
let myagent = spawn! (fn () -> agent (42))
print! ("incrementing agent value to", agent/update (myagent, inc))
:done!

View File

@ -1,3 +1,11 @@
&&& buffers: shared memory with Rust
& use types that are all either empty or any
box console = []
box input = nil
box fetch_outbox = ""
box fetch_inbox = ()
box keys_down = []
& the very base: know something's type
fn type {
"Returns a keyword representing the type of the value passed in."
@ -370,8 +378,9 @@ fn chars/safe {
fn ws? {
"Tells if a string is a whitespace character."
(" ") -> true
("\n") -> true
("\t") -> true
("\n") -> true
("\r") -> true
(_) -> false
}
@ -408,12 +417,11 @@ fn to_number {
(num as :string) -> base :number (num)
}
box console = []
fn print! {
"Sends a text representation of Ludus values to the console."
(...args) -> {
let line = do args > map (string, _) > join (_, " ")
& base :print! (args)
update! (console, append (_, line))
:ok
}
@ -834,6 +842,12 @@ fn slice {
(str as :string, start as :number, end as :number) -> base :str_slice (str, start, end)
}
fn slice_n {
"Returns a slice of a list or a string, representing a sub-list or sub-string."
(xs as :list, start as :number, n as :number) -> slice (xs, start, add (start, n))
(str as :string, start as :number, n as :number) -> slice (str, start, add (start, n))
}
fn butlast {
"Returns a list, omitting the last element."
(xs as :list) -> slice (xs, 0, dec (count (xs)))
@ -871,6 +885,12 @@ fn get {
nil -> default
val -> val
}
(k as :string) -> get (k, _)
(k as :string, d as :dict) -> base :get (d, k)
(k as :string, d as :dict, default) -> match base :get (d, k) with {
nil -> default
val -> val
}
}
fn update {
@ -1027,11 +1047,14 @@ let turtle_init = #{
& turtle_commands is a list of commands, expressed as tuples
box turtle_commands = []
box turtle_state = turtle_init
box command_id = 0
fn apply_command
fn add_command! (command) -> {
update! (turtle_commands, append (_, command))
let idx = unbox (command_id)
update! (command_id, inc)
update! (turtle_commands, append (_, (:turtle_0, idx, command)))
let prev = unbox (turtle_state)
let curr = apply_command (prev, command)
store! (turtle_state, curr)
@ -1211,7 +1234,131 @@ fn penwidth {
box state = nil
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 new process running the function passed in."
(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 `: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, :report) -> base :process (:link_report, pid1, pid2)
(pid1 as :keyword, pid2 as :keyword, :enforce) -> base :process (:link_enforce, 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 not (unbox (input))
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
self
send
spawn!
yield!
sleep!
alive?
flush!
& wip actor functions
& link!
& shared memory w/ rust
console
input
fetch_outbox
fetch_inbox
keys_down
& a fetch fn
fetch
read_input
abs
abs
add
@ -1343,6 +1490,7 @@ box state = nil
showturtle!
sin
slice
slice_n
some
some?
split

40
justfile Normal file
View File

@ -0,0 +1,40 @@
default:
@just --list
# build optimized wasm
build: && clean-wasm-pack
# build with wasm-pack
wasm-pack build --target web
# build dev wasm
dev: && clean-wasm-pack
wasm-pack build --dev --target web
# clean up after wasm-pack
clean-wasm-pack:
# delete cruft from wasm-pack
rm pkg/.gitignore pkg/package.json pkg/README.md
rm -rf pkg/snippets
# fix imports of rudus.js
cp pkg/rudus.js pkg/rudus.js.backup
echo 'import { io } from "./worker.js"' > pkg/rudus.js
cat pkg/rudus.js.backup | tail -n+2>> pkg/rudus.js
rm pkg/rudus.js.backup
from_branch := `git branch --show-current`
git_status := `git status -s`
# publish this branch into release
release:
echo {{ if git_status == "" {"git status ok"} else {error("please commit changes first")} }}
just build
-git commit -am "release build"
git checkout release
git merge {{from_branch}}
git push
git checkout {{from_branch}}
# serve the pkg directory
serve:
live-server pkg

View File

@ -121,7 +121,7 @@ A few thoughts:
* Function calls should be different from tuple pattern matching. Tuples are currently (and maybe forever?) allocated on the heap. Function calls should *not* have to pass through the heap. The good news: `Arguments` is already a different AST node type than `Tuple`; we'll want an `ArgumentsPattern` pattern node type that's different from (and thus compiled differently than) `TuplePattern`. They'll be similar--the matching logic is the same, after all--but the arguments will be on the stack already, and won't need to be unpacked in the same way.
- One difficulty will be matching against different arities? But actually, we should compile these arities as different functions.
- Given splats, can we actually compile functions into different arities? Consider the following:
```
```ludus
fn foo {
(x) -> & arity 1
(y, z) -> & arity 2
@ -142,12 +142,12 @@ A few thoughts:
* That will get me a lot of the way there. What's left after that which might be challenging?
- [x] string interpolation
- [x] splats
- [ ] splatterns
- [x] splatterns
- [x] string patterns
- [x] partial application
- [ ] tail calls
- [ ] stack traces in panics
- [ ] actually good lexing, parsing, and validation errors. I got some of the way there in the fall, but everything needs to be "good enough."
- [x] tail calls
- [-] stack traces in panics
- [-] actually good lexing, parsing, and validation errors. I got some of the way there in the fall, but everything needs to be "good enough."
* After that, we're in integration hell: taking this thing and putting it together for Computer Class 1. Other things that I want (e.g., `test` forms) are for later on.
* There's then a whole host of things I'll need to get done for CC2:
- some kind of actual parsing strategy (that's good enough for "Dissociated Press"/Markov chains)
@ -161,7 +161,7 @@ The `loop` compilation is _almost_ the same as a function body. That said, the t
A few possibilities:
* Probably the best option: enforce a new requirement that splat patterns in function clauses *must* be longer than any explicit arity of the function. So, taking the above:
```
```ludus
fn foo {
(x) -> & arity 1
(y, z) -> & arity 2
@ -171,7 +171,7 @@ A few possibilities:
```
This would give you a validation error that splats must be longer than any other arity.
Similarly, we could enforce this:
```
```ludus
fn foo {
(x) -> & arity 1
(x, y) -> & arity 2
@ -243,41 +243,41 @@ To reiterate the punch list that *I would have needed for Computer Class 1*:
* [x] jump instructions need 16 bits of operand
- Whew, that took longer than I expected
* [x] splatterns
- [ ] validator should ensure splatterns are the longest patterns in a form
* [ ] improve validator
- [ ] Tuples may not be longer than n members
- [ ] Loops may not have splatterns
- [ ] Identify others
- [-] validator should ensure splatterns are the longest patterns in a form
* [-] improve validator
- [-] Tuples may not be longer than n members
- [-] Loops may not have splatterns
- [-] Identify others
* [x] add guards to loop forms
* [x] check loop forms against function calls: do they still work the way we want them to?
* [x] tail call elimination
* [x] stack traces in panics
* [ ] actually good error messages
- [ ] parsing
- [ ] my memory is that validator messages are already good?
- [ ] panics, esp. no match panics
* [ ] getting to prelude
- [ ] `base` should load into Prelude
- [ ] prelude should run properly
- [ ] prelude should be loaded into every context
* [ ] packaging things up
- [ ] add a `to_json` method for values
- [ ] teach Rudus to speak our protocols (stdout and turtle graphics)
- [ ] there should be a Rust function that takes Ludus source and returns valid Ludus status json
- [ ] compile Rust to WASM
- [ ] wire Rust-based WASM into JS
- [ ] FINALLY, test Rudus against Ludus test cases
* [-] actually good error messages
- [-] parsing
- [-] my memory is that validator messages are already good?
- [-] panics, esp. no match panics
* [-] getting to prelude
- [-] `base` should load into Prelude
- [-] prelude should run properly
- [-] prelude should be loaded into every context
* [-] packaging things up
- [-] add a `to_json` method for values
- [-] teach Rudus to speak our protocols (stdout and turtle graphics)
- [-] there should be a Rust function that takes Ludus source and returns valid Ludus status json
- [-] compile Rust to WASM
- [-] wire Rust-based WASM into JS
- [-] FINALLY, test Rudus against Ludus test cases
So this is the work of the week of June 16, maybe?
Just trying to get a sense of what needs to happen for CC2:
* [ ] Actor model (objects, Spacewar!)
* [ ] Animation hooked into the web frontend (Spacewar!)
* [ ] Text input (Spacewar!)
- [ ] Makey makey for alternate input?
* [ ] Saving and loading data into Ludus (perceptrons, dissociated press)
* [ ] Finding corpuses for Dissociated Press
* [x] Actor model (objects, Spacewar!)
* [-] Animation hooked into the web frontend (Spacewar!)
* [-] Text input (Spacewar!)
- [-] Makey makey for alternate input?
* [-] Saving and loading data into Ludus (perceptrons, dissociated press)
* [-] Finding corpuses for Dissociated Press
### Final touches on semantics, or lots of bugs
#### 2025-06-19
@ -309,38 +309,38 @@ So this is my near-term TODO:
- [x] `base` should load into Prelude
- [x] write a mock prelude with a few key functions from real prelude
- [x] a prelude should be loaded into every context
- [ ] the full prelude should run properly
* [ ] packaging things up
- [ ] add a `to_json` method for values
- [ ] teach Rudus to speak our protocols (stdout and turtle graphics)
- [ ] there should be a Rust function that takes Ludus source and returns valid Ludus status json
- [ ] compile Rust to WASM
- [ ] wire Rust-based WASM into JS
- [ ] FINALLY, test Rudus against Ludus test cases
- [?] the full prelude should run properly
* [x] packaging things up
- [x] add a `to_json` method for values
- [x] teach Rudus to speak our protocols (stdout and turtle graphics)
- [x] there should be a Rust function that takes Ludus source and returns valid Ludus status json
- [x] compile Rust to WASM
- [x] wire Rust-based WASM into JS
- [-] FINALLY, test Rudus against Ludus test cases
And then: quality of life improvements:
* [ ] refactor messes
- [ ] The compiler should abstract over some of the very titchy bytecode instruction code
- [ ] Pull apart some gargantuan modules into smaller chunks: e.g., `Op` and `Chunk` should be their own modules
- [ ] Identify code smells
- [ ] Fix some of them
* [ ] improve validator
- [ ] Tuples may not be longer than n members
- [ ] Loops may not have splatterns
- [ ] Identify others
- [ ] Splats in functions must be the same arity, and greater than any explicit arity
* [ ] actually good error messages
- [ ] parsing
- [ ] my memory is that validator messages are already good?
- [ ] panics, esp. no match panics
* [ ] panics should be able to refernce the line number where they fail
* [ ] that suggests that we need a mapping from bytecodes to AST nodes
* [-] refactor messes
- [x] The compiler should abstract over some of the very titchy bytecode instruction code
- [x] Pull apart some gargantuan modules into smaller chunks: e.g., `Op` and `Chunk` should be their own modules
- [x] Identify code smells
- [x] Fix some of them
* [-] improve validator
- [-] Tuples may not be longer than n members
- [-] Loops may not have splatterns
- [-] Identify others
- [-] Splats in functions must be the same arity, and greater than any explicit arity
* [-] actually good error messages
- [-] parsing
- [-] my memory is that validator messages are already good?
- [-] panics, esp. no match panics
* [-] panics should be able to refernce the line number where they fail
* [-] that suggests that we need a mapping from bytecodes to AST nodes
* The way I had been planning on doing this is having a vec that moves in lockstep with bytecode that's just references to ast nodes, which are `'static`, so that shouldn't be too bad. But this is per-chunk, which means we need a reference to that vec in the VM. My sense is that what we want is actually a separate data structure that holds the AST nodes--we'll only need them in the sad path, which can be slow.
### Bugs discovered while trying to compile prelude
#### 2025-06-20
Consider the following code:
```
```ludus
fn one {
(x as :number) -> {
fn two () -> :number
@ -381,15 +381,15 @@ So here's a short punch list of things to do in that register:
* [x] Hook validator back in to both source AND prelude code
- [x] Validator should know about the environment for global/prelude function
- [x] Run validator on current prelude to fix current known errors
* [ ] Do what it takes to compile this interpreter into Ludus's JS environment
- [ ] JSONify Ludus values
- [ ] Write a function that's source code to JSON result
- [ ] Expose this to a WASM compiler
- [ ] Patch this into a JS file
- [ ] Automate this build process
* [ ] Start testing against the cases in `ludus-test`
* [ ] Systematically debug prelude
- [ ] Bring it in function by function, testing each in turn
* [x] Do what it takes to compile this interpreter into Ludus's JS environment
- [x] JSONify Ludus values
- [x] Write a function that's source code to JSON result
- [x] Expose this to a WASM compiler
- [x] Patch this into a JS file
- [-] Automate this build process
* [-] Start testing against the cases in `ludus-test`
* [-] Systematically debug prelude
- [-] Bring it in function by function, testing each in turn
***
I've started working on systematically going through the Prelude.
@ -400,7 +400,7 @@ What we need to have happen is that if a function is closing over values _inside
I think I need to consult Uncle Bob Nystrom to get a sense of what to do here.
***
So I found the minimal test case:
```
```ludus
let foo = {
let thing = :thing
let bar = :bar
@ -445,7 +445,7 @@ To the best of my ability to tell, `if` has proper stack behaviour.
So the question is what's happening in the interaction between the `jump_if_false` instruction and `recur`.
To wit, the following code works just fine:
```
```ludus
fn not {
(false) -> true
(nil) -> true
@ -463,7 +463,7 @@ loop ([1, 2, 3]) with {
```
But the following code does not:
```
```ludus
let test = 2
loop ([1, 2, 3]) with {
([]) -> false
@ -486,19 +486,19 @@ I may be surprised, though.
Currently fixing little bugs in prelude.
Here's a list of things that need doing:
* [ ] Escape characters in strings: \n, \t, and \{, \}.
* [ ] `doc!` needs to print the patterns of a function.
* [ ] I need to return to the question of whether/how strings are ordered; do we use `at`, or do we want `char_at`? etc.
* [ ] Original implementation of `butlast` is breaking stack discipline; I don't know why. It ends up returning from evaluating one of the arguments straight into a `load` instruction. Something about tail calls and ternary synthetic expressions and base functions. (For now, I can call `slice` instead of `base :slice` and it works.)
* [-] Escape characters in strings: \n, \t, and \{, \}.
* [-] `doc!` needs to print the patterns of a function.
* [-] I need to return to the question of whether/how strings are ordered; do we use `at`, or do we want `char_at`? etc.
* [-] Original implementation of `butlast` is breaking stack discipline; I don't know why. It ends up returning from evaluating one of the arguments straight into a `load` instruction. Something about tail calls and ternary synthetic expressions and base functions. (For now, I can call `slice` instead of `base :slice` and it works.)
- Original version of `update` also had this same problem with `assoc`; fixed it by calling the Ludus, rather than Rust, function.
- I need this fixed for optimization reasons.
- I _think_ I just fixed this by fixing tail position tracking in collections
- [ ] test this
- [-] test this
- I did not fix it.
* [x] Dict patterns are giving me stack discipline grief. Why is stack discipline so hard?
* [ ] This is in the service of getting turtle graphics working
* [x] This is in the service of getting turtle graphics working
* Other forms in the language need help:
* [ ] repeat needs its stack discipline updated, it currently crashes the compiler
* xx] repeat needs its stack discipline updated, it currently crashes the compiler
### More closure problems
#### 2025-06-23
@ -522,11 +522,7 @@ SOLUTION: test to see if the function has been forward-declared, and if it has,
NEW PROBLEM: a lot of instructions in the VM don't properly offset from the call frame's stack base, which leads to weirdness when doing things inside function calls.
NEW SOLUTION: create a function that does the offset properly, and replace everywhere we directly access the stack.
<<<<<<< HEAD
<<<<<<< Updated upstream
This is the thing I am about to do
||||||| Stash base
This is the thing I am about to do.
### I think the interpreter, uh, works?
#### 2025-06-24
@ -742,13 +738,13 @@ println!("line {line_no}: {}", lines[line_no - 1]);
#### 2025-06-25
* Web workers
* My javascript wrapper needs to execute WASM in its own thread (ugh)
- [ ] is this a thing that can be done easily in a platform-independent way (node vs. bun vs. browser)?
- [-] is this a thing that can be done easily in a platform-independent way (node vs. bun vs. browser)?
* Top priorities:
- [ ] Get a node package out
- [ ] Stand up actors + threads, etc.
- [ ] How to model keyboard input from p5?
* [ ] Model after the p5 keyboard input API
* [ ] ludus keyboard API: `key_is_down(), key_pressed(), key_released()`, key code values (use a dict)
- [-] Get a node package out
- [-] Stand up actors + threads, etc.
- [-] How to model keyboard input from p5?
* [-] Model after the p5 keyboard input API
* [-] ludus keyboard API: `key_is_down(), key_pressed(), key_released()`, key code values (use a dict)
- Assets:
* We don't (for now) need to worry about serialization formats, since we're not doing perceptrons
* We do need to read from URLs, which need in a *.ludus.dev.
@ -774,4 +770,441 @@ See https://github.com/vitejs/vite/discussions/12826.
Web, in some ways, is even more straightforward.
It produces an ESM that just works in the browser.
And
Put the rest of my thinking up in an issue on alea.
### Actor/Model
#### 2025-06-26
Okay, even though we're not fully hooked up with wasm yet, I've started working on the actor model.
I believe I have things broadly right.
The thing that I'm thinking about right now is how to connect ludus to the world and to processes.
It's not clear how to expose that plumbing.
The most obvious way to do this would be to make all the process stuff special forms.
But first, before I get there, this is what Elixir does.
* `receive` is a special form. It is strictly equivalent to `match`, but with the caveats that (a) if there are no messages to receive, the process yields, and (b) if there's no match for the first message, it matches against the second, and so on (keeping messages in the mailbox), and (c) if no messages match, the process yields as well.
Everything else is functions:
* `spawn/1` takes a function to execute
* `send/3` takes a pid and a message (the third argument is for options, which we won't have in Ludus), returning a pid
* `self/0` returns the current processes' pid
* `exit/2` takes a pid and a message, and kills the process
* `sleep/1` takes a duration in milliseconds and sleeps the process for a time. This isn't strictly speaking necessary, but it is a nice-to-have.
In Ludus, `receive` will need to be a special form.
We could make `self` a reserved word, emitting the instruction to get the current pid.
`spawn` I like as a special form: whatever the expression that comes after spawn is just deferred into the new process.
So in place of Elixir's `spawn(fn -> :something end)` in Ludus, we'd have `spawn :something`.
`send`, `exit`, and `sleep` are a little more interesting (difficult).
Each could be like `and` and `or`: special forms that _look_ like functions, but really aren't--implemented in the compiler.
Alternately, I could attempt to find a way to expose the vm to base.
That seems, frankly, rather more daunting in Rust.
But we could also just build out syntax?
For example, I had proposed the following desugaring:
`foo ::bar (baz)` -> `send (foo, (:bar, baz))`
I don't mind the sugar, but it feels like it's actually really important conceptually for Ludus that everything really is functions.
(Then again, the sugar feels weird, because `foo` is just a name bound to a keyword, so it's possible to do: `:foo ::bar (baz)`, which makes sense and is weird.).
The only disadvantage with making `send` a special form is that you can't pass it as a higher-order function.
`and` and `or` must be special forms, and don't make sense as higher-order functions because they have a different evaluation order than regular functions.
But that's not true of all the things here.
So how do we connect Ludus function calls to handles in the rust interpreter?
In place of base functions, we need something like messages to the process, that aren't mailbox processes but something else.
So, like, a special sort of Ludus value, only available in base, that represents the VM.
Call it `base :process`.
We call it like a function, but the VM instead responds to the various prompts.
Thus we can use it to communicate with the process.
`receive` will be tricky.
But everything else? Seems pretty straightforward.
#### Some time later...
I've implemented what I decribe above. It works! I'm low-key astonished.
It perfectly handles an infinitely recurring process! What the fuck.
Anyway, things left to do:
* [x] `receive` forms are the big one: they require threading through the whole interpreter
* [x] implement the missing process functions at the end of prelude
* [-] research how Elixir/Erlang's abstractions over processes work (I'm especially interested in how to make synchronous-looking calls); take a look especially at https://medium.com/qixxit-development/build-your-own-genserver-in-49-lines-of-code-1a9db07b6f13
* [-] write some examples just using these simple tools (not even GenServer, etc.) to see how they work, and to start building curriculum
* [-] develop a design for how to deal with asynchronous io with js
```ludus
fn agent/get (pid) -> {
send (pid, (:get, self ()))
receive {
(:response, value) -> value
}
}
fn agent/store (pid, x) -> {
send (pid, (:store, x))
:ok
}
fn agent/update (pix, f) -> {
send (pid, (:update, f))
}
fn agent (state) -> receive {
(:get, pid) -> {
send (pid, (:response, state))
agent (state)
}
(:update, pid, f) -> {
agent (f (state))
}
(:store, pid, x) -> {
agent (x)
}
}
```
Two things that pop out to me:
* The way this works is just to yield immediately. This actually makes a lot of sense. If we put them next to one another, there's no risk that there'll be backlogged `(:response, x)` messages in the mbx, right? But that makes me a little queasy.
* The way `gen_server` works is pretty deep inversion of control; you effectively write callbacks for the `gen_server` to call. I'm not sure that's how we want to do things in Ludus; it's a handy pattern, and easy. But it's not simple. But also worth investigating. In any event, it's the foundation of all the other process patterns Elixir has developed. I need an intuiation around it.
### Rethinking reception
#### 2025-06-27
So one thing that's stuck with me is that in Elixir, ~`receive` isn't a special form: it's a function that takes a block~ (**EDIT**: it is indeed a special form in Elixir, and it has to be on in Ludus.).
It may be a macro, but it's still mostly normalish, and doesn't invovle compiler shenanigans.
So, this is what I want to write:
```ludus
fn receiver () -> receive {
:foo -> :bar
:bar -> :baz
:baz -> :quux
}
```
There's a way to model this without the magic of receive.
Imagine instead a function that just receives a message and matches against it:
```ludus
fn receive_msg (msg) -> match msg with {
:foo -> :bar
:bar -> :baz
:baz -> :quux
}
```
But if there's no matching message clause, we get a panic.
And panics stop the world.
Meanwhile, we need to know whether the message matched.
So this desugaring goes a little further:
```ludus
fn receive_msg (msg) -> match msg with {
:foo -> :bar
:bar -> :baz
:baz -> :quux
_ -> :does_not_understand
}
```
This way we avoid a panic when there's no matching message.
There's an easy wrapping function which looks like this:
```ludus
fn receive (receiver) -> {
let my_msgs = msgs ()
loop (my_msgs, 0) with {
([], _) -> yield! ()
(xs, i) -> match receiver(first (xs)) with {
:does_not_understand -> recur (rest (xs), inc (i))
x -> {
flush_n! (i)
x
}
}
}
}
receive (receive_msg)
```
There's a thing I both like and don't like about this.
The fact that we use a magic keyword, `:does_not_understand`, means it's actually easy to hook into the behaviour of not understanding.
I don't know if we should panic on a process receiving a message it doesn't understand.
Maybe we do that for now?
Crash early and often; thanks Erlang.
And so we do have to worry about the last clause.
At an implementation level, it's worth noting that we're not optimizing for scanning through the messages--we'll scan through any messages we don't understand every time we call `receive`.
That's probably fine!
So now the thing, without sugar, is just:
```ludus
fn agent (x) -> receive (fn (msg) {
(:get, pid) -> {
send (pid, (:response, x))
agent (x)
}
(:set, y) -> agent(y)
(:update, f) -> agent (f (x))
_ -> :does_not_understand
})
```
So I don't need any sugar to make `receive` work?
And it doesn't even need to hook into the vm?
What?
#### some time later
It turns out I do.
The problem is that the `flush_i` instruction never gets called from Ludus in a thing like an agent, because of the recursive call.
So the flushing would need to happen in the receiver.
A few things that are wrong right now:
* [-] `loop`/`recur` is still giving me a very headache. It breaks stack discipline to avoid packing tuples into a heap-allocated vec. There may be a way to fix this in the current compiler scheme around how I do and don't handle arguments--make recur stupider and don't bother loading anything. A different solution would be to desugar loop into an anonymous function call. A final solution would be just to use a heap-allocated tuple.
Minimal failing case:
```ludus
match (nil) with {
(x) -> match x with {
(y) -> recur (y)
}
}
```
* [x] The amount of sugar needed to get to `receive` without making a special form is too great.
In particular, function arities need to be changed, flushes need to be inserted, anonymous lambdas need to be created (which can't be multi-clause), etc.
_This is now implemented._
~* [-] There was another bug that I was going to write down and fix, but I forgot what it was. Something with processes.~
* [-] I remembered: I got some weird behaviour when `MAX_REDUCTIONS` was set to 100; I've increased it to 1000, but now need to test what happens when we yield because of reductions.
* [-] Also: the `butlast` bug is still outstanding: `base :slice` causes a panic in that function, but not the call to the Ludus function. Still have to investigate that one.
* [x] In testing this, it's looking like `match` is misbehaving; none of the matches that *should* happen in my fully sugarless `receive` testing are matching how they ought.
- That is not what was happening. The mailbox
I haven't got to a minimal case, but here's what's not working:
```ludus
fn receive (receiver) -> {
fn looper {
([], _) -> yield! ()
(xs, i) -> {
print!("looping through messages:", xs)
match receiver (first (xs), i) with {
:does_not_understand -> looper (rest (xs), inc (i))
x -> x
}}
}
print! ("receiving in", self (), "with messages", msgs())
looper (msgs (), 0)
}
fn agent (x) -> receive (fn (msg, i) -> {
print!("received msg in agent: ", msg)
match msg with {
(:get, pid) -> {
flush_i! (i)
print!("getted from {pid}")
send (pid, (:response, x))
agent (x)
}
(:set, y) -> {flush_i!(i); print!("setted! {y}"); agent (y)}
(:update, f) -> {flush_i!(i);print!("updated: {f}"); agent (f (x))}
y -> {print!("no agent reception match!!!! {y}");:does_not_understand}
}
})
fn agent/get (pid) -> {
send (pid, (:get, self ()))
yield! ()
receive (fn (msg, i) -> match msg with {
(:response, x) -> {flush_i! (i); x}
})
}
fn agent/set (pid, val) -> send (pid, (:set, val))
fn agent/update (pid, f) -> send (pid, (:update, f))
let counter = spawn! (fn () -> agent (0))
agent/set (counter, 12)
agent/update (counter, inc)
agent/update (counter, mult(_, 3))
agent/get (counter)
```
_I haven't been able to recreate this, and the code above is subtle enough that the Ludus may be behaving as expected; the following works as expected:_
```ludus
fn receive (receiver) -> {
print! ("receiving in", self (), "with msgs", msgs())
if empty? (msgs ())
then {yield! (); receive (receiver)}
else do msgs () > first > receiver
}
fn foo? (val) -> receive (fn (msg) -> match report!("scrutinee is", msg) with {
(:report) -> {
print! ("LUDUS SAYS ==> value is {val}")
flush! ()
foo? (val)
}
(:set, x) -> {
print! ("LUDUS SAYS ==> foo! was {val}, now is {x}")
flush! ()
foo? (x)
}
(:get, pid) -> {
print! ("LUDUS SAYS ==> value is {val}")
send (pid, (:response, val))
flush! ()
foo? (val)
}
x -> print! ("LUDUS SAYS ==> no match, got {x}")
})
let foo = spawn! (fn () -> foo? (42))
print! (foo)
send (foo, (:set, 23))
yield! ()
send (foo, (:get, self ()))
yield! ()
fn id (x) -> x
receive(id)
```
#### some time later
I've started work on `receive`.
I've got all the stuff wired up, and it seems to all work (and was pretty straightforward!).
EXCEPT: I've got a difficult off-by-one error.
The problem is being in a receive in a tight loop/tail call, where the ip doesn't advance past the tail call back to the top of the function.
Jumping back to the beginning of the loop advances the message counter by one.
Basically, after the first time we've matched, we keep skipping item 0.
So: what I need to do is to figure out the right order of operations for.
This is just stepwise logic, and some titchy state management.
So the thing that's worth noting is that entering the receive afresh with the whole message queue and entering it with the next message in the queue are _different_ behaviours. This may involve mucking with the instruction pointer on a yield.
This is subtle but will give me the feeling of "oh, why didn't I see that immediately" as soon as I get it.
### Step-by-step
#### 2025-06-28
Here's some pseudobytecode to get us to where we need to be:
010 reset the message counter
020 load current message
025 if no more messages, jump to 010 THEN yield
030 test the message against a pattern
040 if no match jump to 090
050 reset message counter
060 delete current message
070 execute body
# this may be the last instruction executed
# recursive tail calls will jump to 010
080 jump to 100
085 increase the message counter
090 jump to 025 (not really in bytecode; this will be unrolled)
100 receive end
#### a short time later
Well, that worked! The real issue was the jump back if we're out of messages.
That leaves the following list:
* [a] research how Elixir/Erlang's abstractions over processes work (I'm especially interested in how to make synchronous-looking calls); take a look especially at https://medium.com/qixxit-development/build-your-own-genserver-in-49-lines-of-code-1a9db07b6f13
* [ ] write some examples just using these simple tools (not even GenServer, etc.) to see how they work, and to start building curriculum
* [a] develop a design for how to deal with asynchronous io with js
* [a] I got some weird behaviour when `MAX_REDUCTIONS` was set to 100; I've increased it to 1000, but now need to test what happens when we yield because of reductions.
* [a] Also: the `butlast` bug is still outstanding: `base :slice` causes a panic in that function, but not the call to the Ludus function. Still have to investigate that one.
- Original version of `update` also had this same problem with `assoc`; fixed it by calling the Ludus, rather than Rust, function.
* [a] `loop`/`recur` is still giving me a very headache. It breaks stack discipline to avoid packing tuples into a heap-allocated vec. There may be a way to fix this in the current compiler scheme around how I do and don't handle arguments--make recur stupider and don't bother loading anything. A different solution would be to desugar loop into an anonymous function call. A final solution would be just to use a heap-allocated tuple.
* My javascript wrapper needs to execute WASM in its own thread (ugh)
- [x] is this a thing that can be done easily in a platform-independent way (node vs. bun vs. browser)?
- No, no it is not. I will need to build a separate node version for using at the command line (and, like, for testing with our test harness.)
* Top priorities:
- [-] Get a node package out
- [x] Stand up actors + threads, etc.
- [ ] How to model keyboard input from p5?
* [ ] Model after the p5 keyboard input API
* [ ] ludus keyboard API: `key_down?(), key_pressed?(), key_released?()`, key code values (use a dict)
* [a] Escape characters in strings: \n, \t, and \{, \}.
* [a] `doc!` needs to print the patterns of a function.
* [a] I need to return to the question of whether/how strings are ordered; do we use `at`, or do we want `char_at`? etc.
- MNL and I decided: yes, stings are indexable
- [ ] implement `slice` and `at` and others for strings
* [x] Automate this build process
* [ ] Start testing against the cases in `ludus-test`
* [ ] Systematically debug prelude
* [ ] Bring it in function by function, testing each in turn
* [x] Animation hooked into the web frontend (Spacewar!)
* [ ] Text input (Spacewar!)
* [ ] ~Makey makey for alternate input?~
* [x] Saving and loading data into Ludus (perceptrons, dissociated press)
* [ ] Finding corpuses for Dissociated Press
* [a] improve validator
- [a] Tuples may not be longer than n members
- [a] Loops may not have splatterns
- [ ] Identify others
- [a] Splats in functions must be the same arity, and greater than any explicit arity
* [a] actually good error messages
- [a] parsing
- [ ] my memory is that validator messages are already good?
- [a] panics, esp. no match panics
* [ ] panics should be able to refernce the line number where they fail
* [ ] that suggests that we need a mapping from bytecodes to AST nodes
* The way I had been planning on doing this is having a vec that moves in lockstep with bytecode that's just references to ast nodes, which are `'static`, so that shouldn't be too bad. But this is per-chunk, which means we need a reference to that vec in the VM. My sense is that what we want is actually a separate data structure that holds the AST nodes--we'll only need them in the sad path, which can be slow.
### Next steps in integration hell
#### 2025-06-29
* [x] improve build process for rudus+wasm_pack
- [x] delete generated .gitignore
- [x] 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.
* [x] design & implement asynchronous i/o+runtime
- [x] 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
- [x] start with ludus->rust->js pipeline
* [x] console
* [ ] turtle graphics
* [x] completion
- [ ] then js->rust->ludus
* [x] kill
* [x] text input
* [ ] keypresses
- [x] then ludus->rust->js->rust->ludus
* [x] 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.
### Finishing integration?
#### 2025-07-01
Happy Canada day!
After a really rough evening, I seem to have the actor model not only working in Ludus, but reasonably debugged in Rust.
We've got one bug to address in Firefox before I continue:
* [x] the event loop isn't returning once something is done, which makes no sense
- What seems to be happening is that the javascript behaviour is subtly different
- Current situation is that synchronous scripts work just fine
- But async scripts work ONCE, and then not again
- In FF, `do_io` doesn't return after `complete_main` in the `world` loop the second time.
- Which is to say, that last call to `io` isn't completing.
- Do I hack around this or do I try to find the source of the problem?
After that:
* [ ] implement other verbs beside `console`:
- [x] `command`
- [x] `input`
* [x] js->rust->ludus buffer (in Rust code)
* [x] ludus abstractions around this buffer (in Ludus code)
- [x] `fetch`--request & response
* [x] request: ludus->rust->js->net
* [x] response: js->rust->ludus
- [ ] `keyboard`
* [ ] still working on how to represent this
* [x] hook this up to `web.ludus.dev`
* [x] do some integration testing
- [x] do synchronous programs still work?
- [x] animations?
- [x] read inputs?
- [x] load url text?

View File

@ -1,3 +0,0 @@
# rudus
A Rust implementation of Ludus.

View File

@ -6,13 +6,7 @@
</head>
<body>
<script type="module">
import {run} from "./ludus.js";
window.ludus = run;
console.log(run(":foobar"));
</script>
<script src="./ludus.js" type="module"></script>
<p>
Open the console. All the action's in there.
</p>

View File

@ -1,32 +1,188 @@
import init, {ludus} from "./rudus.js";
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}
await init();
let res = null
const worker_url = new URL("worker.js", import.meta.url)
const worker = new Worker(worker_url, {type: "module"})
let outbox = []
let ludus_console = ""
let ludus_commands = []
let ludus_result = null
let code = null
let running = false
let ready = false
let io_interval_id = null
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
async function handle_messages (e) {
let msgs
try {
msgs = JSON.parse(e.data)
} catch {
console.log(e.data)
throw Error("Main: bad json from Ludus")
}
for (const msg of msgs) {
switch (msg.verb) {
case "Complete": {
console.log("Main: ludus completed with => ", msg.data)
ludus_result = msg.data
running = false
ready = false
outbox = []
break
}
case "Error": {
console.log("Main: ludus errored with => ", msg.data)
ludus_result = msg.data
running = false
ready = false
outbox = []
break
}
// TODO: do more than report these
case "Console": {
let new_lines = msg.data.join("\n");
ludus_console = ludus_console + new_lines
console.log("Main: ludus says => ", new_lines)
break
}
case "Commands": {
console.log("Main: ludus commands => ", msg.data)
for (const command of msg.data) {
// attempt to solve out-of-order command bug
ludus_commands[command[1]] = command
}
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]})
}
case "Ready": {
console.log("Main: ludus is ready")
ready = true
}
}
}
}
function io_poller () {
if (io_interval_id && !running) {
// flush the outbox one last time
// (presumably, with the kill message)
worker.postMessage(outbox)
// cancel the poller
clearInterval(io_interval_id)
outbox = []
}
if (ready && running) {
worker.postMessage(outbox)
outbox = []
}
}
function start_io_polling () {
io_interval_id = setInterval(io_poller, 10)
}
// runs a ludus script; does not return the result
// the result must be explicitly polled with `result`
export function run (source) {
code = source
const output = ludus(source)
res = JSON.parse(output)
return res
if (running || ready) {
return "TODO: handle this? should not be running"
}
worker.postMessage([{verb: "Run", data: source}])
reset_ludus()
start_io_polling()
}
export function is_starting_up() {
return running && !ready
}
// tells if the ludus script is still running
export function is_running() {
return running && ready
}
// kills a ludus script
export function kill () {
running = false
outbox.push({verb: "Kill"})
console.log("Main: Killed Ludus")
}
// sends text into ludus (status: not working)
export function input (text) {
console.log("Main: calling `input` with ", text)
outbox.push({verb: "Input", data: text})
}
// returns the contents of the ludus console and resets the console
export function flush_stdout () {
let out = ludus_console
ludus_console = ""
return out
}
// returns the contents of the ludus console, retaining them
export function stdout () {
if (!res) return ""
return res.io.stdout.data
return ludus_console
}
export function turtle_commands () {
if (!res) return []
return res.io.turtle.data
// returns the array of turtle commands
export function commands () {
return ludus_commands
}
// returns the array of turtle commands and clears it
export function flush_commands () {
let out = ludus_commands
ludus_commands = []
return out
}
// returns the ludus result
// this is effectively Option<String>:
// null if no result has been returned, or
// a string representation of the result
export function result () {
return res
return ludus_result
}
export function flush_result () {
let out = ludus_result
ludus_result = null
return out
}
export function key_down (str) {
if (is_running()) keys_down.add(str)
}
export function key_up (str) {
if (is_running()) keys_down.delete(str)
}
//////////// turtle plumbing below
// TODO: refactor this out into modules
const turtle_init = {
position: [0, 0],
heading: 0,
@ -81,8 +237,9 @@ function unit_of (heading) {
return [Math.cos(radians), Math.sin(radians)]
}
function command_to_state (prev_state, curr_command) {
const verb = curr_command[0]
function command_to_state (prev_state, command) {
const [_target, _id, curr_command] = command
const [verb] = curr_command
switch (verb) {
case "goto": {
const [_, x, y] = curr_command
@ -299,7 +456,7 @@ export function svg (commands) {
return accum
}, {maxX: 0, maxY: 0, minX: 0, minY: 0})
const [r, g, b, a] = resolve_color(background_color)
const [r, g, b] = resolve_color(background_color)
if ((r+g+b)/3 > 128) turtle_color = [0, 0, 0, 150]
const view_width = (maxX - minX) * 1.2
const view_height = (maxY - minY) * 1.2
@ -376,3 +533,4 @@ export function p5 (commands) {
return p5_calls
}

View File

@ -1,15 +0,0 @@
{
"name": "rudus",
"type": "module",
"version": "0.0.1",
"files": [
"rudus_bg.wasm",
"rudus.js",
"rudus.d.ts"
],
"main": "rudus.js",
"types": "rudus.d.ts",
"sideEffects": [
"./snippets/*"
]
}

13
pkg/rudus.d.ts vendored
View File

@ -1,16 +1,21 @@
/* tslint:disable */
/* eslint-disable */
export function ludus(src: string): string;
export function ludus(src: string): Promise<void>;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly ludus: (a: number, b: number) => [number, number];
readonly __wbindgen_export_0: WebAssembly.Table;
readonly ludus: (a: number, b: number) => any;
readonly __wbindgen_exn_store: (a: number) => void;
readonly __externref_table_alloc: () => number;
readonly __wbindgen_export_2: WebAssembly.Table;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_export_6: WebAssembly.Table;
readonly closure338_externref_shim: (a: number, b: number, c: any) => void;
readonly closure351_externref_shim: (a: number, b: number, c: any, d: any) => void;
readonly __wbindgen_start: () => void;
}

View File

@ -1,6 +1,25 @@
import { io } from "./worker.js"
let wasm;
let WASM_VECTOR_LEN = 0;
function addToExternrefTable0(obj) {
const idx = wasm.__externref_table_alloc();
wasm.__wbindgen_export_2.set(idx, obj);
return idx;
}
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
const idx = addToExternrefTable0(e);
wasm.__wbindgen_exn_store(idx);
}
}
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
let cachedUint8ArrayMemory0 = null;
@ -11,6 +30,13 @@ function getUint8ArrayMemory0() {
return cachedUint8ArrayMemory0;
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
let WASM_VECTOR_LEN = 0;
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
@ -65,31 +91,66 @@ function passStringToWasm0(arg, malloc, realloc) {
return ptr;
}
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
let cachedDataViewMemory0 = null;
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
function getDataViewMemory0() {
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
}
return cachedDataViewMemory0;
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
function isLikeNone(x) {
return x === undefined || x === null;
}
const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(state => {
wasm.__wbindgen_export_6.get(state.dtor)(state.a, state.b)
});
function makeMutClosure(arg0, arg1, dtor, f) {
const state = { a: arg0, b: arg1, cnt: 1, dtor };
const real = (...args) => {
// First up with a closure we increment the internal reference
// count. This ensures that the Rust closure environment won't
// be deallocated while we're invoking it.
state.cnt++;
const a = state.a;
state.a = 0;
try {
return f(a, state.b, ...args);
} finally {
if (--state.cnt === 0) {
wasm.__wbindgen_export_6.get(state.dtor)(a, state.b);
CLOSURE_DTORS.unregister(state);
} else {
state.a = a;
}
}
};
real.original = state;
CLOSURE_DTORS.register(real, state, state);
return real;
}
/**
* @param {string} src
* @returns {string}
* @returns {Promise<void>}
*/
export function ludus(src) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(src, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.ludus(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
const ptr0 = passStringToWasm0(src, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.ludus(ptr0, len0);
return ret;
}
function __wbg_adapter_18(arg0, arg1, arg2) {
wasm.closure338_externref_shim(arg0, arg1, arg2);
}
function __wbg_adapter_44(arg0, arg1, arg2, arg3) {
wasm.closure351_externref_shim(arg0, arg1, arg2, arg3);
}
async function __wbg_load(module, imports) {
@ -126,8 +187,131 @@ async function __wbg_load(module, imports) {
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) {
const ret = arg0.call(arg1);
return ret;
}, arguments) };
imports.wbg.__wbg_call_7cccdd69e0791ae2 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = arg0.call(arg1, arg2);
return ret;
}, arguments) };
imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) {
let deferred0_0;
let deferred0_1;
try {
deferred0_0 = arg0;
deferred0_1 = arg1;
console.error(getStringFromWasm0(arg0, arg1));
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
};
imports.wbg.__wbg_io_5a3c8ea72d8c6ea3 = function() { return handleError(function (arg0, arg1) {
let deferred0_0;
let deferred0_1;
try {
deferred0_0 = arg0;
deferred0_1 = arg1;
const ret = io(getStringFromWasm0(arg0, arg1));
return ret;
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
}, arguments) };
imports.wbg.__wbg_log_11652c6a56eeddfb = function(arg0, arg1) {
console.log(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbg_new_23a2665fac83c611 = function(arg0, arg1) {
try {
var state0 = {a: arg0, b: arg1};
var cb0 = (arg0, arg1) => {
const a = state0.a;
state0.a = 0;
try {
return __wbg_adapter_44(a, state0.b, arg0, arg1);
} finally {
state0.a = a;
}
};
const ret = new Promise(cb0);
return ret;
} finally {
state0.a = state0.b = 0;
}
};
imports.wbg.__wbg_new_8a6f238a6ece86ea = function() {
const ret = new Error();
return ret;
};
imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function(arg0, arg1) {
const ret = new Function(getStringFromWasm0(arg0, arg1));
return ret;
};
imports.wbg.__wbg_now_8dddb61fa4928554 = function() {
const ret = Date.now();
return ret;
};
imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function(arg0) {
queueMicrotask(arg0);
};
imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function(arg0) {
const ret = arg0.queueMicrotask;
return ret;
};
imports.wbg.__wbg_random_57c118f142535bb6 = function() {
const ret = Math.random();
return ret;
};
imports.wbg.__wbg_resolve_4851785c9c5f573d = function(arg0) {
const ret = Promise.resolve(arg0);
return ret;
};
imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) {
const ret = arg1.stack;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() {
const ret = typeof global === 'undefined' ? null : global;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
};
imports.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() {
const ret = typeof globalThis === 'undefined' ? null : globalThis;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
};
imports.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() {
const ret = typeof self === 'undefined' ? null : self;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
};
imports.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() {
const ret = typeof window === 'undefined' ? null : window;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
};
imports.wbg.__wbg_then_44b73946d2fb3e7d = function(arg0, arg1) {
const ret = arg0.then(arg1);
return ret;
};
imports.wbg.__wbg_then_48b406749878a531 = function(arg0, arg1, arg2) {
const ret = arg0.then(arg1, arg2);
return ret;
};
imports.wbg.__wbindgen_cb_drop = function(arg0) {
const obj = arg0.original;
if (obj.cnt-- == 1) {
obj.a = 0;
return true;
}
const ret = false;
return ret;
};
imports.wbg.__wbindgen_closure_wrapper1034 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 339, __wbg_adapter_18);
return ret;
};
imports.wbg.__wbindgen_init_externref_table = function() {
const table = wasm.__wbindgen_export_0;
const table = wasm.__wbindgen_export_2;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
@ -136,6 +320,25 @@ function __wbg_get_imports() {
table.set(offset + 3, false);
;
};
imports.wbg.__wbindgen_is_function = function(arg0) {
const ret = typeof(arg0) === 'function';
return ret;
};
imports.wbg.__wbindgen_is_undefined = function(arg0) {
const ret = arg0 === undefined;
return ret;
};
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
const obj = arg1;
const ret = typeof(obj) === 'string' ? obj : undefined;
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
return imports;
}
@ -147,6 +350,7 @@ function __wbg_init_memory(imports, memory) {
function __wbg_finalize_init(instance, module) {
wasm = instance.exports;
__wbg_init.__wbindgen_wasm_module = module;
cachedDataViewMemory0 = null;
cachedUint8ArrayMemory0 = null;

Binary file not shown.

View File

@ -1,9 +1,14 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const ludus: (a: number, b: number) => [number, number];
export const __wbindgen_export_0: WebAssembly.Table;
export const ludus: (a: number, b: number) => any;
export const __wbindgen_exn_store: (a: number) => void;
export const __externref_table_alloc: () => number;
export const __wbindgen_export_2: WebAssembly.Table;
export const __wbindgen_free: (a: number, b: number, c: number) => void;
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_free: (a: number, b: number, c: number) => void;
export const __wbindgen_export_6: WebAssembly.Table;
export const closure338_externref_shim: (a: number, b: number, c: any) => void;
export const closure351_externref_shim: (a: number, b: number, c: any, d: any) => void;
export const __wbindgen_start: () => void;

View File

@ -1,5 +0,0 @@
import * as mod from "./ludus.js";
console.log(mod.run(`
:foobar
`));

55
pkg/worker.js Normal file
View File

@ -0,0 +1,55 @@
import init, {ludus} from "./rudus.js";
let initialized_wasm = false
onmessage = run
// exposed in rust as:
// async fn io (out: String) -> Result<JsValue, JsValue>
// rust calls this to perform io
export function io (out) {
// only send messages if we have some
if (out.length > 0) postMessage(out)
// make an event handler that captures and delivers messages from the main thread
// because our promise resolution isn't about calculating a value but setting a global variable, we can't asyncify it
// explicitly return a promise
return new Promise((resolve, reject) => {
// deliver the response to ludus when we get a response from the main thread
onmessage = (e) => {
resolve(JSON.stringify(e.data))
}
// cancel the response if it takes too long
setTimeout(() => reject("io took too long"), 500)
})
}
// set as default event handler from main thread
async function run(e) {
// we must NEVER run `await init()` twice
if (!initialized_wasm) {
// this must come before the init call
initialized_wasm = true
await init()
console.log("Worker: Ludus has been initialized.")
}
// the data is always an array; we only really expect one member tho
let msgs = e.data
for (const msg of msgs) {
// evaluate source if we get some
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
onmessage = () => {}
// actually run the ludus--which will call `io`--and replace `run` as the event handler for ipc
await ludus(msg.data)
// once we've returned from `ludus`, make this the event handler again
onmessage = run
} else {
// report and swallow any malformed startup messages
console.log("Worker: Did not get valid startup message. Instead got:")
console.log(e.data)
}
}
}

View File

@ -1,4 +1,34 @@
repeat 1 {
fd! (100)
rt! (0.25)
fn inputter () -> {
if do input > unbox > empty?
then {
yield! ()
inputter ()
}
else receive {
(:get, pid) -> send (pid, (:reply, unbox (input)))
(:flush, pid) -> {
send (pid, (:reply, unbox (input)))
store! (input, "")
}
(:clear) -> store! (input, "")
}
}
fn clear_input () -> store! (input, "")
fn read_input () -> {
let reader = spawn! (inputter)
send (reader, (:get, self ()))
receive {
(:reply, msg) -> msg
}
}
fn flush_input () -> {
let reader = spawn! (inputter)
send (reader, (:flush, self ()))
receive {
(:reply, msg) -> msg
}
}

File diff suppressed because it is too large Load Diff

470
src/ast.rs Normal file
View File

@ -0,0 +1,470 @@
use crate::spans::*;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StringPart {
Data(String),
Word(&'static str),
Inline(String),
}
impl fmt::Display for StringPart {
fn fmt(self: &StringPart, f: &mut fmt::Formatter) -> fmt::Result {
let rep = match self {
StringPart::Word(s) => format!("{{{s}}}"),
StringPart::Data(s) => s.to_string(),
StringPart::Inline(s) => s.to_string(),
};
write!(f, "{}", rep)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Ast {
// a special Error node
// may come in handy?
Error,
And,
Or,
// expression nodes
Placeholder,
Nil,
Boolean(bool),
Number(f64),
Keyword(&'static str),
Method(&'static str, Box<Spanned<Self>>),
Word(&'static str),
String(&'static str),
Interpolated(Vec<Spanned<StringPart>>),
Block(Vec<Spanned<Self>>),
If(Box<Spanned<Self>>, Box<Spanned<Self>>, Box<Spanned<Self>>),
Tuple(Vec<Spanned<Self>>),
Arguments(Vec<Spanned<Self>>),
List(Vec<Spanned<Self>>),
Dict(Vec<Spanned<Self>>),
Let(Box<Spanned<Self>>, Box<Spanned<Self>>),
LBox(&'static str, Box<Spanned<Self>>),
Synthetic(Box<Spanned<Self>>, Box<Spanned<Self>>, Vec<Spanned<Self>>),
When(Vec<Spanned<Self>>),
WhenClause(Box<Spanned<Self>>, Box<Spanned<Self>>),
Match(Box<Spanned<Self>>, Vec<Spanned<Self>>),
Receive(Vec<Spanned<Self>>),
MatchClause(
Box<Spanned<Self>>,
Box<Option<Spanned<Self>>>,
Box<Spanned<Self>>,
),
Fn(&'static str, Box<Spanned<Ast>>, Option<&'static str>),
FnBody(Vec<Spanned<Ast>>),
FnDeclaration(&'static str),
Panic(Box<Spanned<Self>>),
Do(Vec<Spanned<Self>>),
Repeat(Box<Spanned<Self>>, Box<Spanned<Self>>),
Splat(&'static str),
StringPair(&'static str, Box<Spanned<Self>>),
KeywordPair(&'static str, Box<Spanned<Self>>),
Loop(Box<Spanned<Self>>, Vec<Spanned<Self>>),
Recur(Vec<Spanned<Self>>),
// pattern nodes
NilPattern,
BooleanPattern(bool),
NumberPattern(f64),
StringPattern(&'static str),
InterpolatedPattern(Vec<Spanned<StringPart>>),
KeywordPattern(&'static str),
WordPattern(&'static str),
AsPattern(&'static str, &'static str),
Splattern(Box<Spanned<Self>>),
PlaceholderPattern,
TuplePattern(Vec<Spanned<Self>>),
ListPattern(Vec<Spanned<Self>>),
StrPairPattern(&'static str, Box<Spanned<Self>>),
KeyPairPattern(&'static str, Box<Spanned<Self>>),
DictPattern(Vec<Spanned<Self>>),
}
impl Ast {
pub fn show(&self) -> String {
use Ast::*;
match self {
And => "and".to_string(),
Or => "or".to_string(),
Error => unreachable!(),
Nil | NilPattern => "nil".to_string(),
String(s) | StringPattern(s) => format!("\"{s}\""),
Interpolated(strs) | InterpolatedPattern(strs) => {
let mut out = "".to_string();
out = format!("\"{out}");
for (part, _) in strs {
out = format!("{out}{part}");
}
format!("{out}\"")
}
Boolean(b) | BooleanPattern(b) => b.to_string(),
Number(n) | NumberPattern(n) => n.to_string(),
Keyword(k) | KeywordPattern(k) => format!(":{k}"),
Method(m, args) => format!("::{m} {}", args.0),
Word(w) | WordPattern(w) => w.to_string(),
Block(lines) => {
let mut out = "{\n".to_string();
for (line, _) in lines {
out = format!("{out}\n {}", line.show());
}
format!("{out}\n}}")
}
If(cond, then, r#else) => format!(
"if {}\n then {}\n else {}",
cond.0.show(),
then.0.show(),
r#else.0.show()
),
Let(pattern, expression) => {
format!("let {} = {}", pattern.0.show(), expression.0.show())
}
Dict(entries) | DictPattern(entries) => {
format!(
"#{{{}}}",
entries
.iter()
.map(|(pair, _)| pair.show())
.collect::<Vec<_>>()
.join(", ")
)
}
List(members) | ListPattern(members) => format!(
"[{}]",
members
.iter()
.map(|(member, _)| member.show())
.collect::<Vec<_>>()
.join(", ")
),
Arguments(members) => format!(
"({})",
members
.iter()
.map(|(member, _)| member.show())
.collect::<Vec<_>>()
.join(", ")
),
Tuple(members) | TuplePattern(members) => format!(
"({})",
members
.iter()
.map(|(member, _)| member.show())
.collect::<Vec<_>>()
.join(", ")
),
Synthetic(root, first, rest) => format!(
"{} {} {}",
root.0.show(),
first.0.show(),
rest.iter()
.map(|(term, _)| term.show())
.collect::<Vec<_>>()
.join(" ")
),
When(clauses) | Receive(clauses) => format!(
"when {{\n {}\n}}",
clauses
.iter()
.map(|(clause, _)| clause.show())
.collect::<Vec<_>>()
.join("\n ")
),
Placeholder | PlaceholderPattern => "_".to_string(),
LBox(name, rhs) => format!("box {name} = {}", rhs.0.show()),
Match(scrutinee, clauses) => format!(
"match {} with {{\n {}\n}}",
scrutinee.0.show(),
clauses
.iter()
.map(|(clause, _)| clause.show())
.collect::<Vec<_>>()
.join("\n ")
),
FnBody(clauses) => clauses
.iter()
.map(|(clause, _)| clause.show())
.collect::<Vec<_>>()
.join("\n "),
Fn(name, body, doc) => {
let mut out = format!("fn {name} {{\n");
if let Some(doc) = doc {
out = format!("{out} {doc}\n");
}
format!("{out} {}\n}}", body.0.show())
}
FnDeclaration(name) => format!("fn {name}"),
Panic(expr) => format!("panic! {}", expr.0.show()),
Do(terms) => {
format!(
"do {}",
terms
.iter()
.map(|(term, _)| term.show())
.collect::<Vec<_>>()
.join(" > ")
)
}
Repeat(times, body) => format!("repeat {} {{\n{}\n}}", times.0.show(), body.0.show()),
Splat(word) => format!("...{}", word),
Splattern(pattern) => format!("...{}", pattern.0.show()),
AsPattern(word, type_keyword) => format!("{word} as :{type_keyword}"),
KeywordPair(key, value) | KeyPairPattern(key, value) => {
format!(":{key} {}", value.0.show())
}
StringPair(key, value) | StrPairPattern(key, value) => {
format!("\"{key}\" {}", value.0.show())
}
Loop(init, body) => format!(
"loop {} with {{\n {}\n}}",
init.0.show(),
body.iter()
.map(|(clause, _)| clause.show())
.collect::<Vec<_>>()
.join("\n ")
),
Recur(args) => format!(
"recur ({})",
args.iter()
.map(|(arg, _)| arg.show())
.collect::<Vec<_>>()
.join(", ")
),
MatchClause(pattern, guard, body) => {
let mut out = pattern.0.show();
if let Some(guard) = guard.as_ref() {
out = format!("{out} if {}", guard.0.show());
}
format!("{out} -> {}", body.0.show())
}
WhenClause(cond, body) => format!("{} -> {}", cond.0.show(), body.0.show()),
}
}
}
impl fmt::Display for Ast {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use Ast::*;
match self {
And => write!(f, "And"),
Or => write!(f, "Or"),
Error => write!(f, "Error"),
Nil => write!(f, "nil"),
String(s) => write!(f, "String: \"{}\"", s),
Interpolated(strs) => {
write!(
f,
"Interpolated: \"{}\"",
strs.iter()
.map(|(s, _)| s.to_string())
.collect::<Vec<_>>()
.join("")
)
}
Boolean(b) => write!(f, "Boolean: {}", b),
Number(n) => write!(f, "Number: {}", n),
Keyword(k) => write!(f, "Keyword: :{}", k),
Method(m, args) => write!(f, "Method: ::{m} ({})", args.0),
Word(w) => write!(f, "Word: {}", w),
Block(b) => write!(
f,
"Block: <{}>",
b.iter()
.map(|(line, _)| line.to_string())
.collect::<Vec<_>>()
.join("\n")
),
If(cond, then_branch, else_branch) => write!(
f,
"If: {} Then: {} Else: {}",
cond.0, then_branch.0, else_branch.0
),
Let(pattern, expression) => {
write!(f, "Let: {} = {}", pattern.0, expression.0)
}
Dict(entries) => write!(
f,
"#{{{}}}",
entries
.iter()
.map(|pair| pair.0.to_string())
.collect::<Vec<_>>()
.join(", ")
),
List(l) => write!(
f,
"List: [{}]",
l.iter()
.map(|(line, _)| line.to_string())
.collect::<Vec<_>>()
.join("\n")
),
Arguments(a) => write!(
f,
"Arguments: ({})",
a.iter()
.map(|(line, _)| line.to_string())
.collect::<Vec<_>>()
.join("\n")
),
Tuple(t) => write!(
f,
"Tuple: ({})",
t.iter()
.map(|(line, _)| line.to_string())
.collect::<Vec<_>>()
.join("\n")
),
Synthetic(root, first, rest) => write!(
f,
"Synth: [{}, {}, {}]",
root.0,
first.0,
rest.iter()
.map(|(term, _)| term.to_string())
.collect::<Vec<_>>()
.join("\n")
),
When(clauses) | Receive(clauses) => write!(
f,
"When: [{}]",
clauses
.iter()
.map(|clause| clause.0.to_string())
.collect::<Vec<_>>()
.join("\n")
),
Placeholder => write!(f, "Placeholder"),
LBox(_name, _rhs) => todo!(),
Match(value, clauses) => {
write!(
f,
"match: {} with {}",
&value.0.to_string(),
clauses
.iter()
.map(|clause| clause.0.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
FnBody(clauses) => {
write!(
f,
"{}",
clauses
.iter()
.map(|clause| clause.0.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
Fn(name, body, ..) => {
write!(f, "fn: {name}\n{}", body.0)
}
FnDeclaration(_name) => todo!(),
Panic(_expr) => todo!(),
Do(terms) => {
write!(
f,
"do: {}",
terms
.iter()
.map(|(term, _)| term.to_string())
.collect::<Vec<_>>()
.join(" > ")
)
}
Repeat(_times, _body) => todo!(),
Splat(word) => {
write!(f, "splat: {}", word)
}
KeywordPair(k, v) | KeyPairPattern(k, v) => {
write!(f, "key_pair: {} {}", k, v.0)
}
StringPair(k, v) | StrPairPattern(k, v) => {
write!(f, "str_pair: {k} {}", v.0)
}
Loop(init, body) => {
write!(
f,
"loop: {} with {}",
init.0,
body.iter()
.map(|clause| clause.0.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
Recur(args) => {
write!(
f,
"recur: {}",
args.iter()
.map(|(arg, _)| arg.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
MatchClause(pattern, guard, body) => {
write!(
f,
"match clause: {} if {:?} -> {}",
pattern.0, guard, body.0
)
}
WhenClause(cond, body) => {
write!(f, "when clause: {} -> {}", cond.0, body.0)
}
NilPattern => write!(f, "nil"),
BooleanPattern(b) => write!(f, "{}", b),
NumberPattern(n) => write!(f, "{}", n),
StringPattern(s) => write!(f, "{}", s),
KeywordPattern(k) => write!(f, ":{}", k),
WordPattern(w) => write!(f, "{}", w),
AsPattern(w, t) => write!(f, "{} as :{}", w, t),
Splattern(p) => write!(f, "...{}", p.0),
PlaceholderPattern => write!(f, "_"),
TuplePattern(t) => write!(
f,
"({})",
t.iter()
.map(|x| x.0.to_string())
.collect::<Vec<_>>()
.join(", ")
),
ListPattern(l) => write!(
f,
"({})",
l.iter()
.map(|x| x.0.to_string())
.collect::<Vec<_>>()
.join(", ")
),
DictPattern(entries) => write!(
f,
"#{{{}}}",
entries
.iter()
.map(|(pair, _)| pair.to_string())
.collect::<Vec<_>>()
.join(", ")
),
InterpolatedPattern(strprts) => write!(
f,
"interpolated: \"{}\"",
strprts
.iter()
.map(|part| part.0.to_string())
.collect::<Vec<_>>()
.join("")
),
}
}
}

View File

@ -1,6 +1,6 @@
use crate::js::*;
use crate::value::*;
use imbl::*;
use ran::ran_f64;
use std::rc::Rc;
#[derive(Clone, Debug)]
@ -60,7 +60,15 @@ pub fn doc(f: &Value) -> Value {
pub fn assoc(dict: &Value, key: &Value, value: &Value) -> Value {
match (dict, key) {
(Value::Dict(d), Value::Keyword(k)) => Value::Dict(Box::new(d.update(k, value.clone()))),
(Value::Dict(d), Value::Keyword(k)) => {
Value::Dict(Box::new(d.update(Key::Keyword(k), value.clone())))
}
(Value::Dict(d), Value::Interned(k)) => {
Value::Dict(Box::new(d.update(Key::Interned(k), value.clone())))
}
(Value::Dict(d), Value::String(s)) => {
Value::Dict(Box::new(d.update(Key::String(s.clone()), value.clone())))
}
_ => unreachable!("internal Ludus error calling assoc with ({dict}, {key}, {value})"),
}
}
@ -175,7 +183,17 @@ pub fn dissoc(dict: &Value, key: &Value) -> Value {
match (dict, key) {
(Value::Dict(dict), Value::Keyword(key)) => {
let mut new = dict.clone();
new.remove(key);
new.remove(&Key::Keyword(key));
Value::Dict(new)
}
(Value::Dict(dict), Value::Interned(key)) => {
let mut new = dict.clone();
new.remove(&Key::Interned(key));
Value::Dict(new)
}
(Value::Dict(dict), Value::String(key)) => {
let mut new = dict.clone();
new.remove(&Key::String(key.clone()));
Value::Dict(new)
}
_ => unreachable!("internal Ludus error"),
@ -220,7 +238,15 @@ pub fn at(ordered: &Value, i: &Value) -> Value {
pub fn get(dict: &Value, key: &Value) -> Value {
match (dict, key) {
(Value::Dict(dict), Value::Keyword(key)) => match dict.get(key) {
(Value::Dict(dict), Value::Keyword(key)) => match dict.get(&Key::Keyword(key)) {
Some(x) => x.clone(),
None => Value::Nil,
},
(Value::Dict(dict), Value::Interned(key)) => match dict.get(&Key::Interned(key)) {
Some(x) => x.clone(),
None => Value::Nil,
},
(Value::Dict(dict), Value::String(key)) => match dict.get(&Key::String(key.clone())) {
Some(x) => x.clone(),
None => Value::Nil,
},
@ -242,7 +268,6 @@ pub fn last(ordered: &Value) -> Value {
}
}
// TODO: fix this: x is a list of all the args passed to Ludus's print!
pub fn print(x: &Value) -> Value {
let Value::List(args) = x else {
unreachable!("internal Ludus error")
@ -252,7 +277,8 @@ pub fn print(x: &Value) -> Value {
.map(|val| format!("{val}"))
.collect::<Vec<_>>()
.join(" ");
println!("{out}");
// println!("{out}");
console_log!("{out}");
Value::Keyword("ok")
}
@ -338,7 +364,7 @@ pub fn list(x: &Value) -> Value {
let kvs = d.iter();
let mut list = vector![];
for (key, value) in kvs {
let kv = Value::Tuple(Rc::new(vec![Value::Keyword(key), value.clone()]));
let kv = Value::Tuple(Rc::new(vec![key.to_value(), value.clone()]));
list.push_back(kv);
}
Value::List(Box::new(list))
@ -382,6 +408,7 @@ pub fn r#type(x: &Value) -> Value {
Value::Box(_) => Value::Keyword("box"),
Value::BaseFn(_) => Value::Keyword("fn"),
Value::Partial(_) => Value::Keyword("fn"),
Value::Process => Value::Keyword("process"),
Value::Nothing => unreachable!(),
}
}
@ -480,8 +507,8 @@ pub fn floor(x: &Value) -> Value {
}
}
pub fn random() -> Value {
Value::Number(ran_f64())
pub fn base_random() -> Value {
Value::Number(random())
}
pub fn round(x: &Value) -> Value {
@ -573,60 +600,137 @@ pub fn r#mod(x: &Value, y: &Value) -> Value {
pub fn make_base() -> Value {
let members = vec![
("add", Value::BaseFn(BaseFn::Binary("add", add))),
("append", Value::BaseFn(BaseFn::Binary("append", append))),
("assoc", Value::BaseFn(BaseFn::Ternary("assoc", assoc))),
("at", Value::BaseFn(BaseFn::Binary("at", at))),
("atan_2", Value::BaseFn(BaseFn::Binary("atan_2", atan_2))),
("bool", Value::BaseFn(BaseFn::Unary("bool", r#bool))),
("ceil", Value::BaseFn(BaseFn::Unary("ceil", ceil))),
("chars", Value::BaseFn(BaseFn::Unary("chars", chars))),
("concat", Value::BaseFn(BaseFn::Binary("concat", concat))),
("cos", Value::BaseFn(BaseFn::Unary("cos", cos))),
("count", Value::BaseFn(BaseFn::Unary("count", count))),
("dec", Value::BaseFn(BaseFn::Unary("dec", dec))),
("dissoc", Value::BaseFn(BaseFn::Binary("dissoc", dissoc))),
("div", Value::BaseFn(BaseFn::Binary("div", div))),
("doc!", Value::BaseFn(BaseFn::Unary("doc!", doc))),
("add", Value::BaseFn(Box::new(BaseFn::Binary("add", add)))),
(
"append",
Value::BaseFn(Box::new(BaseFn::Binary("append", append))),
),
(
"assoc",
Value::BaseFn(Box::new(BaseFn::Ternary("assoc", assoc))),
),
("at", Value::BaseFn(Box::new(BaseFn::Binary("at", at)))),
(
"atan_2",
Value::BaseFn(Box::new(BaseFn::Binary("atan_2", atan_2))),
),
(
"bool",
Value::BaseFn(Box::new(BaseFn::Unary("bool", r#bool))),
),
("ceil", Value::BaseFn(Box::new(BaseFn::Unary("ceil", ceil)))),
(
"chars",
Value::BaseFn(Box::new(BaseFn::Unary("chars", chars))),
),
(
"concat",
Value::BaseFn(Box::new(BaseFn::Binary("concat", concat))),
),
("cos", Value::BaseFn(Box::new(BaseFn::Unary("cos", cos)))),
(
"count",
Value::BaseFn(Box::new(BaseFn::Unary("count", count))),
),
("dec", Value::BaseFn(Box::new(BaseFn::Unary("dec", dec)))),
(
"dissoc",
Value::BaseFn(Box::new(BaseFn::Binary("dissoc", dissoc))),
),
("div", Value::BaseFn(Box::new(BaseFn::Binary("div", div)))),
("doc!", Value::BaseFn(Box::new(BaseFn::Unary("doc!", doc)))),
(
"downcase",
Value::BaseFn(BaseFn::Unary("downcase", downcase)),
Value::BaseFn(Box::new(BaseFn::Unary("downcase", downcase))),
),
("eq?", Value::BaseFn(Box::new(BaseFn::Binary("eq?", eq)))),
(
"first",
Value::BaseFn(Box::new(BaseFn::Unary("first", first))),
),
(
"floor",
Value::BaseFn(Box::new(BaseFn::Unary("floor", floor))),
),
("get", Value::BaseFn(Box::new(BaseFn::Binary("get", get)))),
("gt?", Value::BaseFn(Box::new(BaseFn::Binary("gt?", gt)))),
("gte?", Value::BaseFn(Box::new(BaseFn::Binary("gte?", gte)))),
("inc", Value::BaseFn(Box::new(BaseFn::Unary("inc", inc)))),
("last", Value::BaseFn(Box::new(BaseFn::Unary("last", last)))),
("list", Value::BaseFn(Box::new(BaseFn::Unary("list", list)))),
("lt?", Value::BaseFn(Box::new(BaseFn::Binary("lt?", lt)))),
("lte?", Value::BaseFn(Box::new(BaseFn::Binary("lte?", lte)))),
("mod", Value::BaseFn(Box::new(BaseFn::Binary("mod", r#mod)))),
(
"mult",
Value::BaseFn(Box::new(BaseFn::Binary("mult", mult))),
),
(
"number",
Value::BaseFn(Box::new(BaseFn::Unary("number", number))),
),
("eq?", Value::BaseFn(BaseFn::Binary("eq?", eq))),
("first", Value::BaseFn(BaseFn::Unary("first", first))),
("floor", Value::BaseFn(BaseFn::Unary("floor", floor))),
("get", Value::BaseFn(BaseFn::Binary("get", get))),
("gt?", Value::BaseFn(BaseFn::Binary("gt?", gt))),
("gte?", Value::BaseFn(BaseFn::Binary("gte?", gte))),
("inc", Value::BaseFn(BaseFn::Unary("inc", inc))),
("last", Value::BaseFn(BaseFn::Unary("last", last))),
("list", Value::BaseFn(BaseFn::Unary("list", list))),
("lt?", Value::BaseFn(BaseFn::Binary("lt?", lt))),
("lte?", Value::BaseFn(BaseFn::Binary("lte?", lte))),
("mod", Value::BaseFn(BaseFn::Binary("mod", r#mod))),
("mult", Value::BaseFn(BaseFn::Binary("mult", mult))),
("number", Value::BaseFn(BaseFn::Unary("number", number))),
("pi", Value::Number(std::f64::consts::PI)),
("print!", Value::BaseFn(BaseFn::Unary("print!", print))),
("random", Value::BaseFn(BaseFn::Nullary("random", random))),
("range", Value::BaseFn(BaseFn::Binary("range", range))),
("rest", Value::BaseFn(BaseFn::Unary("rest", rest))),
("round", Value::BaseFn(BaseFn::Unary("round", round))),
("show", Value::BaseFn(BaseFn::Unary("show", show))),
("sin", Value::BaseFn(BaseFn::Unary("sin", sin))),
("slice", Value::BaseFn(BaseFn::Ternary("slice", slice))),
("split", Value::BaseFn(BaseFn::Binary("split", split))),
("sqrt", Value::BaseFn(BaseFn::Unary("sqrt", sqrt))),
(
"print!",
Value::BaseFn(Box::new(BaseFn::Unary("print!", print))),
),
("process", Value::Process),
(
"random",
Value::BaseFn(Box::new(BaseFn::Nullary("random", base_random))),
),
(
"range",
Value::BaseFn(Box::new(BaseFn::Binary("range", range))),
),
("rest", Value::BaseFn(Box::new(BaseFn::Unary("rest", rest)))),
(
"round",
Value::BaseFn(Box::new(BaseFn::Unary("round", round))),
),
("show", Value::BaseFn(Box::new(BaseFn::Unary("show", show)))),
("sin", Value::BaseFn(Box::new(BaseFn::Unary("sin", sin)))),
(
"slice",
Value::BaseFn(Box::new(BaseFn::Ternary("slice", slice))),
),
(
"split",
Value::BaseFn(Box::new(BaseFn::Binary("split", split))),
),
("sqrt", Value::BaseFn(Box::new(BaseFn::Unary("sqrt", sqrt)))),
("sqrt_2", Value::Number(std::f64::consts::SQRT_2)),
("store!", Value::BaseFn(BaseFn::Binary("store!", store))),
("sub", Value::BaseFn(BaseFn::Binary("sub", sub))),
("tan", Value::BaseFn(BaseFn::Unary("tan", tan))),
("trim", Value::BaseFn(BaseFn::Unary("trim", trim))),
("triml", Value::BaseFn(BaseFn::Unary("triml", triml))),
("trimr", Value::BaseFn(BaseFn::Unary("trimr", trimr))),
("type", Value::BaseFn(BaseFn::Unary("type", r#type))),
("unbox", Value::BaseFn(BaseFn::Unary("unbox", unbox))),
("upcase", Value::BaseFn(BaseFn::Unary("upcase", upcase))),
(
"store!",
Value::BaseFn(Box::new(BaseFn::Binary("store!", store))),
),
("sub", Value::BaseFn(Box::new(BaseFn::Binary("sub", sub)))),
("tan", Value::BaseFn(Box::new(BaseFn::Unary("tan", tan)))),
("trim", Value::BaseFn(Box::new(BaseFn::Unary("trim", trim)))),
(
"triml",
Value::BaseFn(Box::new(BaseFn::Unary("triml", triml))),
),
(
"trimr",
Value::BaseFn(Box::new(BaseFn::Unary("trimr", trimr))),
),
(
"type",
Value::BaseFn(Box::new(BaseFn::Unary("type", r#type))),
),
(
"unbox",
Value::BaseFn(Box::new(BaseFn::Unary("unbox", unbox))),
),
(
"upcase",
Value::BaseFn(Box::new(BaseFn::Unary("upcase", upcase))),
),
];
let members = members
.iter()
.map(|(name, bfn)| (Key::Keyword(name), bfn.clone()))
.collect::<Vec<_>>();
Value::Dict(Box::new(HashMap::from(members)))
}

View File

@ -1,12 +1,14 @@
use crate::js::*;
use crate::op::Op;
use crate::value::Value;
use crate::value::{Key, Value};
use chumsky::prelude::SimpleSpan;
use imbl::HashMap;
use num_traits::FromPrimitive;
use regex::Regex;
#[derive(Clone, Debug)]
pub struct StrPattern {
pub words: Vec<String>,
pub words: Vec<&'static str>,
pub re: Regex,
}
@ -16,8 +18,17 @@ pub struct Chunk {
pub bytecode: Vec<u8>,
pub keywords: Vec<&'static str>,
pub string_patterns: Vec<StrPattern>,
pub env: HashMap<&'static str, Value>,
pub env: HashMap<Key, Value>,
pub msgs: Vec<String>,
pub spans: Vec<SimpleSpan>,
pub src: &'static str,
pub input: &'static str,
}
impl std::fmt::Display for Chunk {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Chunk.")
}
}
impl Chunk {
@ -26,26 +37,27 @@ impl Chunk {
use Op::*;
match op {
Pop | Store | Stash | Load | Nil | True | False | MatchNil | MatchTrue | MatchFalse
| PanicIfNoMatch | ResetMatch | GetKey | PanicNoWhen | PanicNoMatch | TypeOf
| Duplicate | Decrement | ToInt | Noop | LoadTuple | LoadList | Eq | Add | Sub
| Mult | Div | Unbox | BoxStore | Assert | Get | At | Not | Panic | EmptyString
| ConcatStrings | Stringify | MatchType | Return | UnconditionalMatch | Print
| AppendList | ConcatList | PushList | PushDict | AppendDict | ConcatDict | Nothing
| PushGlobal | SetUpvalue => {
println!("{i:04}: {op}")
| ResetMatch | GetKey | PanicWhenFallthrough | PanicNoMatch | PanicNoFnMatch
| PanicNoLetMatch | TypeOf | Duplicate | Decrement | ToInt | Noop | LoadTuple
| LoadList | Eq | Add | Sub | Mult | Div | Unbox | BoxStore | Assert | Get | At
| Not | Panic | EmptyString | ConcatStrings | Stringify | MatchType | Return
| UnconditionalMatch | Print | AppendList | ConcatList | PushList | PushDict
| AppendDict | ConcatDict | Nothing | PushGlobal | SetUpvalue | LoadMessage
| NextMessage | MatchMessage | ClearMessage | SendMethod | LoadScrutinee => {
console_log!("{i:04}: {op}")
}
Constant | MatchConstant => {
let high = self.bytecode[*i + 1];
let low = self.bytecode[*i + 2];
let idx = ((high as usize) << 8) + low as usize;
let value = &self.constants[idx].show();
println!("{i:04}: {:16} {idx:05}: {value}", op.to_string());
console_log!("{i:04}: {:16} {idx:05}: {value}", op.to_string());
*i += 2;
}
Msg => {
let msg_idx = self.bytecode[*i + 1];
let msg = &self.msgs[msg_idx as usize];
println!("{i:04}: {msg}");
console_log!("{i:04}: {msg}");
*i += 1;
}
PushBinding | MatchTuple | MatchSplattedTuple | LoadSplattedTuple | MatchList
@ -53,7 +65,7 @@ impl Chunk {
| DropDictEntry | LoadDictValue | PushTuple | PushBox | MatchDepth | PopN | StoreN
| Call | GetUpvalue | Partial | MatchString | PushStringMatches | TailCall | LoadN => {
let next = self.bytecode[*i + 1];
println!("{i:04}: {:16} {next:03}", op.to_string());
console_log!("{i:04}: {:16} {next:03}", op.to_string());
*i += 1;
}
Jump | JumpIfFalse | JumpIfTrue | JumpIfNoMatch | JumpIfMatch | JumpBack
@ -61,26 +73,18 @@ impl Chunk {
let high = self.bytecode[*i + 1];
let low = self.bytecode[*i + 2];
let len = ((high as u16) << 8) + low as u16;
println!("{i:04}: {:16} {len:05}", op.to_string());
console_log!("{i:04}: {:16} {len:05}", op.to_string());
*i += 2;
}
}
}
pub fn dissasemble(&self) {
println!("IDX | CODE | INFO");
console_log!("IDX | CODE | INFO");
let mut i = 0;
while i < self.bytecode.len() {
self.dissasemble_instr(&mut i);
i += 1;
}
}
// pub fn kw_from(&self, kw: &str) -> Option<Value> {
// self.kw_index_from(kw).map(Value::Keyword)
// }
// pub fn kw_index_from(&self, kw: &str) -> Option<usize> {
// self.keywords.iter().position(|s| *s == kw)
// }
}

View File

@ -1,7 +1,6 @@
use crate::ast::{Ast, StringPart};
use crate::chunk::{Chunk, StrPattern};
use crate::op::Op;
use crate::parser::Ast;
use crate::parser::StringPart;
use crate::spans::Spanned;
use crate::value::*;
use chumsky::prelude::SimpleSpan;
@ -41,7 +40,7 @@ impl LoopInfo {
}
}
fn get_builtin(name: &str, arity: usize) -> Option<Op> {
fn get_builtin(_name: &str, _arity: usize) -> Option<Op> {
// match (name, arity) {
// ("type", 1) => Some(Op::TypeOf),
// ("eq?", 2) => Some(Op::Eq),
@ -69,12 +68,10 @@ pub struct Compiler {
pub scope_depth: isize,
pub match_depth: usize,
pub stack_depth: usize,
pub spans: Vec<SimpleSpan>,
pub nodes: Vec<&'static Ast>,
pub ast: &'static Ast,
pub span: SimpleSpan,
pub src: &'static str,
pub name: &'static str,
pub input: &'static str,
pub depth: usize,
pub upvalues: Vec<&'static str>,
loop_info: Vec<LoopInfo>,
@ -99,10 +96,10 @@ fn has_placeholder(args: &[Spanned<Ast>]) -> bool {
impl Compiler {
pub fn new(
ast: &'static Spanned<Ast>,
name: &'static str,
input: &'static str,
src: &'static str,
depth: usize,
env: imbl::HashMap<&'static str, Value>,
env: imbl::HashMap<Key, Value>,
debug: bool,
) -> Compiler {
let chunk = Chunk {
@ -112,6 +109,9 @@ impl Compiler {
string_patterns: vec![],
env,
msgs: vec![],
src,
input,
spans: vec![],
};
Compiler {
chunk,
@ -120,14 +120,12 @@ impl Compiler {
scope_depth: -1,
match_depth: 0,
stack_depth: 0,
spans: vec![],
nodes: vec![],
ast: &ast.0,
span: ast.1,
loop_info: vec![],
upvalues: vec![],
src,
name,
input,
tail_pos: false,
debug,
}
@ -148,8 +146,8 @@ impl Compiler {
let low = len as u8;
let high = (len >> 8) as u8;
self.emit_op(op);
self.chunk.bytecode.push(high);
self.chunk.bytecode.push(low);
self.emit_byte(high as usize);
self.emit_byte(low as usize);
}
fn stub_jump(&mut self, op: Op) -> usize {
@ -189,8 +187,8 @@ impl Compiler {
self.emit_op(Op::Constant);
let low = const_idx as u8;
let high = (const_idx >> 8) as u8;
self.chunk.bytecode.push(high);
self.chunk.bytecode.push(low);
self.emit_byte(high as usize);
self.emit_byte(low as usize);
self.stack_depth += 1;
}
@ -216,18 +214,18 @@ impl Compiler {
self.emit_op(Op::MatchConstant);
let low = const_idx as u8;
let high = (const_idx >> 8) as u8;
self.chunk.bytecode.push(high);
self.chunk.bytecode.push(low);
self.emit_byte(high as usize);
self.emit_byte(low as usize);
}
fn emit_op(&mut self, op: Op) {
self.chunk.bytecode.push(op as u8);
self.spans.push(self.span);
self.chunk.spans.push(self.span);
}
fn emit_byte(&mut self, byte: usize) {
self.chunk.bytecode.push(byte as u8);
self.spans.push(self.span);
self.chunk.spans.push(self.span);
}
fn len(&self) -> usize {
@ -235,7 +233,7 @@ impl Compiler {
}
pub fn bind(&mut self, name: &'static str) {
self.msg(format!("binding `{name}` in {}", self.name));
self.msg(format!("binding `{name}` in {}", self.input));
self.msg(format!(
"stack depth: {}; match depth: {}",
self.stack_depth, self.match_depth
@ -276,7 +274,7 @@ impl Compiler {
fn resolve_binding(&mut self, name: &'static str) {
self.msg(format!(
"resolving binding `{name}` in {}\nlocals: {}",
self.name,
self.input,
self.bindings
.iter()
.map(|binding| format!("{binding}"))
@ -454,13 +452,13 @@ impl Compiler {
// return the evaluated rhs instead of whatever is last on the stack
// we do this by pretending it's a binding
(Let(patt, expr), _) => {
// self.match_depth = 0;
self.visit(expr);
let expr_pos = self.stack_depth - 1;
self.report_ast("let binding: matching".to_string(), patt);
self.reset_match();
self.emit_op(Op::LoadScrutinee);
self.visit(patt);
self.emit_op(Op::PanicIfNoMatch);
self.emit_op(Op::PanicNoLetMatch);
self.emit_op(Op::PushBinding);
self.emit_byte(expr_pos);
self.stack_depth += 1;
@ -510,14 +508,13 @@ impl Compiler {
}
Let(patt, expr) => {
self.report_depth("before let binding");
// self.match_depth = 0;
// self.emit_op(Op::ResetMatch);
self.visit(expr);
self.report_depth("after let expr");
self.report_ast("let binding: matching".to_string(), patt);
self.reset_match();
self.emit_op(Op::LoadScrutinee);
self.visit(patt);
self.emit_op(Op::PanicIfNoMatch);
self.emit_op(Op::PanicNoLetMatch);
self.report_depth("after let binding");
}
WordPattern(name) => {
@ -704,10 +701,12 @@ impl Compiler {
let match_depth = self.match_depth;
self.match_depth = 0;
for pair in pairs.iter().take(pairs_len) {
let (PairPattern(key, pattern), _) = pair else {
unreachable!()
let (key, pattern) = match &pair.0 {
KeyPairPattern(key, pattern) => (Value::Keyword(key), pattern),
StrPairPattern(key, pattern) => (Value::Interned(key), pattern),
_ => unreachable!("expected key to be keyword or string"),
};
self.emit_constant(Value::Keyword(key));
self.emit_constant(key);
self.emit_op(Op::LoadDictValue);
self.emit_byte(dict_stack_pos);
self.visit(pattern);
@ -722,7 +721,7 @@ impl Compiler {
self.stack_depth += 1;
for pair in pairs.iter().take(pairs_len) {
let (PairPattern(key, _), _) = pair else {
let (KeyPairPattern(key, _), _) = pair else {
unreachable!()
};
self.emit_constant(Value::Keyword(key));
@ -751,7 +750,7 @@ impl Compiler {
self.patch_jump(jump_idx, self.len() - jump_idx - 3);
}
Splattern(patt) => self.visit(patt),
InterpolatedPattern(parts, _) => {
InterpolatedPattern(parts) => {
// println!("An interpolated pattern of {} parts", parts.len());
let mut pattern = "".to_string();
let mut words = vec![];
@ -759,7 +758,7 @@ impl Compiler {
match part {
StringPart::Word(word) => {
// println!("wordpart: {word}");
words.push(word.clone());
words.push(*word);
pattern.push_str("(.*)");
}
StringPart::Data(data) => {
@ -786,9 +785,8 @@ impl Compiler {
self.emit_byte(pattern_idx);
for word in moar_words {
let name: &'static str = std::string::String::leak(word);
let binding = Binding {
name,
name: word,
depth: self.scope_depth,
stack_pos: self.stack_depth,
};
@ -798,7 +796,7 @@ impl Compiler {
self.patch_jump(jnm_idx, self.len() - jnm_idx - 3);
}
PairPattern(_, _) => unreachable!(),
KeyPairPattern(..) | StrPairPattern(..) => unreachable!(),
Tuple(members) => {
self.tail_pos = false;
for member in members {
@ -843,7 +841,12 @@ impl Compiler {
}
}
}
Pair(key, value) => {
StringPair(key, value) => {
self.tail_pos = false;
self.emit_constant(Value::Interned(key));
self.visit(value);
}
KeywordPair(key, value) => {
self.tail_pos = false;
self.emit_constant(Value::Keyword(key));
self.visit(value);
@ -861,6 +864,14 @@ impl Compiler {
self.stack_depth -= 1;
self.report_depth("after keyword access");
}
(Keyword(_), Method(str, args)) | (Word(_), Method(str, args)) => {
self.visit(first);
self.emit_constant(Value::Keyword(str));
self.visit(args);
self.emit_op(Op::SendMethod);
// target, method, args -> result
self.stack_depth -= 2;
}
(Keyword(_), Arguments(args)) => {
self.visit(&args[0]);
self.visit(first);
@ -955,8 +966,16 @@ impl Compiler {
Keyword(str) => {
self.emit_constant(Value::Keyword(str));
self.emit_op(Op::GetKey);
// target, keyword -> value
self.stack_depth -= 1;
}
Method(str, args) => {
self.emit_constant(Value::Keyword(str));
self.visit(args);
self.emit_op(Op::SendMethod);
// target, method, args -> result
self.stack_depth -= 2;
}
Arguments(args) => {
self.store();
let arity = args.len();
@ -991,7 +1010,7 @@ impl Compiler {
jump_idxes.push(self.stub_jump(Op::Jump));
self.patch_jump(jif_jump_idx, self.len() - jif_jump_idx - 3);
}
self.emit_op(Op::PanicNoWhen);
self.emit_op(Op::PanicWhenFallthrough);
for idx in jump_idxes {
self.patch_jump(idx, self.len() - idx - 3);
}
@ -1002,6 +1021,7 @@ impl Compiler {
let tail_pos = self.tail_pos;
self.tail_pos = false;
self.visit(scrutinee.as_ref());
self.emit_op(Op::LoadScrutinee);
let stack_depth = self.stack_depth;
let mut jump_idxes = vec![];
let mut clauses = clauses.iter();
@ -1039,6 +1059,52 @@ impl Compiler {
self.emit_op(Op::Load);
self.stack_depth += 1;
}
Receive(clauses) => {
let tail_pos = self.tail_pos;
self.emit_op(Op::ClearMessage);
let receive_begin = self.len();
self.emit_op(Op::LoadMessage);
self.stack_depth += 1;
let stack_depth = self.stack_depth;
let mut jump_idxes = vec![];
let mut clauses = clauses.iter();
while let Some((MatchClause(pattern, guard, body), _)) = clauses.next() {
self.tail_pos = false;
let mut no_match_jumps = vec![];
self.enter_scope();
self.reset_match();
self.visit(pattern);
no_match_jumps.push(self.stub_jump(Op::JumpIfNoMatch));
if guard.is_some() {
let guard_expr: &'static Spanned<Ast> =
Box::leak(Box::new(guard.clone().unwrap()));
self.visit(guard_expr);
no_match_jumps.push(self.stub_jump(Op::JumpIfFalse));
}
self.emit_op(Op::MatchMessage);
self.tail_pos = tail_pos;
self.visit(body);
self.store();
self.leave_scope();
self.pop_n(self.stack_depth - stack_depth);
jump_idxes.push(self.stub_jump(Op::Jump));
for idx in no_match_jumps {
self.patch_jump(idx, self.len() - idx - 3);
}
}
// TODO: get the next message
self.emit_op(Op::NextMessage);
// TODO: jump back to the "get a message" instruction
let jump_back = self.stub_jump(Op::JumpBack);
self.patch_jump(jump_back, self.len() - receive_begin - 3);
for idx in jump_idxes {
self.patch_jump(idx, self.len() - idx - 3);
}
self.pop_n(self.stack_depth - stack_depth);
self.emit_op(Op::Load);
self.stack_depth += 1;
}
MatchClause(..) => unreachable!(),
Fn(name, body, doc) => {
let is_anon = name.is_empty();
@ -1094,7 +1160,7 @@ impl Compiler {
None => {
let mut compiler = Compiler::new(
clause,
name,
self.input,
self.src,
self.depth + 1,
self.chunk.env.clone(),
@ -1259,7 +1325,7 @@ impl Compiler {
let jump_back = self.stub_jump(Op::JumpBack);
// set jump points
self.patch_jump(jump_back, self.len() - repeat_begin - 2);
self.patch_jump(jiz_idx, self.len() - repeat_begin - 4);
self.patch_jump(jiz_idx, self.len() - jiz_idx - 3);
self.pop();
self.emit_constant(Value::Nil);
self.tail_pos = tail_pos;
@ -1430,12 +1496,12 @@ impl Compiler {
Placeholder => {
self.emit_op(Op::Nothing);
}
And | Or | Arguments(..) => unreachable!(),
And | Or | Arguments(..) | Method(..) => unreachable!(),
}
}
pub fn disassemble(&self) {
println!("=== chunk: {} ===", self.name);
println!("=== chunk: {} ===", self.input);
self.chunk.dissasemble();
}
}

View File

@ -1,52 +1,143 @@
// use crate::process::{LErr, Trace};
use crate::js::*;
use crate::lexer::Token;
use crate::panic::{Panic, PanicMsg};
use crate::validator::VErr;
use ariadne::{sources, Color, Label, Report, ReportKind};
use crate::vm::CallFrame;
use chumsky::error::RichPattern;
use chumsky::prelude::*;
// pub fn report_panic(err: LErr) {
// let mut srcs = HashSet::new();
// let mut stack = vec![];
// let mut order = 1;
// for entry in err.trace.iter().rev() {
// let Trace {
// callee,
// caller,
// function,
// arguments,
// input,
// src,
// } = entry;
// let (_, first_span) = callee;
// let (_, second_span) = caller;
// let Value::Fn(f) = function else {
// unreachable!()
// };
// let fn_name = f.borrow().name.clone();
// let i = first_span.start;
// let j = second_span.end;
// let label = Label::new((entry.input, i..j))
// .with_color(Color::Yellow)
// .with_message(format!("({order}) calling `{fn_name}` with `{arguments}`"));
// order += 1;
// stack.push(label);
// srcs.insert((*input, *src));
// }
// Report::build(ReportKind::Error, (err.input, err.span.into_range()))
// .with_message(format!("Ludus panicked! {}", err.msg))
// .with_label(Label::new((err.input, err.span.into_range())).with_color(Color::Red))
// .with_labels(stack)
// .with_note(err.extra)
// .finish()
// .print(sources(srcs.iter().copied()))
// .unwrap();
// }
const SEPARATOR: &str = "\n\n";
pub fn report_invalidation(errs: Vec<VErr>) {
for err in errs {
Report::build(ReportKind::Error, (err.input, err.span.into_range()))
.with_message(err.msg.to_string())
.with_label(Label::new((err.input, err.span.into_range())).with_color(Color::Cyan))
.finish()
.print(sources(vec![(err.input, err.src)]))
.unwrap();
}
fn line_number(src: &'static str, span: SimpleSpan) -> usize {
src.chars().take(span.start).filter(|c| *c == '\n').count()
}
fn get_line(src: &'static str, line: usize) -> String {
src.split("\n").nth(line).unwrap().to_string()
}
pub fn lexing(errs: Vec<Rich<'static, char>>, src: &'static str, input: &'static str) -> String {
let mut msgs = vec!["Ludus found some errors.".to_string()];
for err in errs {
let mut msg = vec![];
let line_number = line_number(src, *err.span());
let line = get_line(src, line_number);
let char = src.chars().nth(err.span().start).unwrap();
msg.push(format!("Syntax error: unexpected {char}"));
msg.push(format!(" on line {} in {}", line_number + 1, input));
msg.push(format!(" >>> {line}"));
msgs.push(msg.join("\n"));
}
msgs.join(SEPARATOR)
}
pub fn validation(errs: Vec<VErr>) -> String {
let mut msgs = vec!["Ludus found some errors.".to_string()];
for err in errs {
let mut msg = vec![];
let line_number = line_number(err.src, *err.span);
let line = get_line(err.src, line_number);
msg.push(format!("Validation error: {}", err.msg));
msg.push(format!(" on line {} in {}", line_number + 1, err.input));
msg.push(format!(" >>> {line}"));
msgs.push(msg.join("\n"));
}
msgs.join(SEPARATOR)
}
pub fn parsing(errs: Vec<Rich<'static, Token>>, src: &'static str, input: &'static str) -> String {
let mut msgs = vec!["Ludus found some errors.".to_string()];
for err in errs {
let mut msg = vec![];
let line_number = line_number(src, *err.span());
let line = get_line(src, line_number);
let details = parsing_message(err);
msg.push(format!("Syntax error: {}", details));
msg.push(format!(" on line {} in {}", line_number + 1, input));
msg.push(format!(" >>> {line}"));
msgs.push(msg.join("\n"))
}
msgs.join(SEPARATOR)
}
fn parsing_message(err: Rich<'static, Token>) -> String {
let found = match err.found() {
Some(token) => token.show(),
None => "end of input".to_string(),
};
let expected = err.expected();
let mut expecteds = vec![];
for pattern in expected {
let shown = match pattern {
RichPattern::Token(t) => t.show(),
RichPattern::Label(s) => s.to_string(),
RichPattern::Identifier(s) => s.clone(),
RichPattern::Any => "any".to_string(),
RichPattern::SomethingElse => "something else".to_string(),
RichPattern::EndOfInput => "eof".to_string(),
};
expecteds.push(shown);
}
let expecteds = if expecteds.iter().any(|e| e == &"else".to_string()) {
vec!["else".to_string()]
} else {
expecteds
};
let expecteds = if expecteds.iter().any(|e| e == &"then".to_string()) {
vec!["then".to_string()]
} else {
expecteds
};
let expecteds = expecteds.join(" | ");
format!("Ludus did not expect to see: {found}\n expected: {expecteds}")
}
pub fn panic(panic: Panic) -> String {
// console_log!("Ludus panicked!: {panic}");
// panic.call_stack.last().unwrap().chunk().dissasemble();
// console_log!("{:?}", panic.call_stack.last().unwrap().chunk().spans);
let mut msgs = vec!["Ludus panicked!".to_string()];
let msg = match panic.msg {
PanicMsg::Generic(ref s) => s,
_ => &"no match".to_string(),
};
msgs.push(msg.clone());
msgs.push(traceback(&panic));
msgs.join("\n")
}
fn traceback(panic: &Panic) -> String {
let mut traceback = vec![];
for frame in panic.call_stack.iter().rev() {
traceback.push(frame_info(frame));
}
traceback.join("\n")
}
fn frame_info(frame: &CallFrame) -> String {
let span = frame.chunk().spans[if frame.ip == 0 {
frame.ip
} else {
frame.ip - 1
}];
let line_number = line_number(frame.chunk().src, span);
let line = get_line(frame.chunk().src, line_number);
let line = line.trim_start();
let name = frame.function.as_fn().name();
let input = frame.chunk().input;
format!(
" in {name} on line {} in {input}\n >>> {line}",
line_number + 1
)
}
/////// Some thoughts
// We're putting the information we need on the function and the chunk.
// In the compiler, on functions, build up a vec of strings that are the patterns the function can match against
// The pattern asts have a `show` method.
// And with the additional members on Chunk, we should have everything we need for a pretty fn no match message
// Let no match is no problem, either. We should have no concerns pulling the line with the span start and string
// We don't need to reproduce the pattern, since it will be right there in the code
// As for match forms, we'll just use "no match" and print the value

100
src/io.rs Normal file
View File

@ -0,0 +1,100 @@
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
use crate::value::Value;
use crate::js::*;
use imbl::Vector;
use std::rc::Rc;
const OK: Value = Value::Keyword("ok");
const ERR: Value = Value::Keyword("err");
#[wasm_bindgen(module = "/pkg/worker.js")]
extern "C" {
#[wasm_bindgen(catch)]
async fn io (output: String) -> Result<JsValue, JsValue>;
}
type Url = Value; // expect a string
type Commands = Value; // expect a list of command tuples
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(tag = "verb", content = "data")]
pub enum MsgOut {
Console(Value),
Commands(Commands),
Fetch(Url),
Complete(Value),
Error(String),
Ready
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "verb", content = "data")]
pub enum MsgIn {
Input(String),
Fetch(String, f64, String),
Kill,
Keyboard(Vec<String>),
}
impl std::fmt::Display for MsgIn {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
MsgIn::Input(str) => write!(f, "Input: {str}"),
MsgIn::Kill => write!(f, "Kill"),
MsgIn::Fetch(url, code, text) => write!(f, "Fetch: {url} :: {code} ::\n{text}"),
_ => todo!()
}
}
}
impl MsgIn {
pub fn into_value(self) -> Value {
match self {
MsgIn::Input(str) => Value::string(str),
MsgIn::Fetch(url, status_f64, string) => {
let url = Value::string(url);
let status = Value::Number(status_f64);
let text = Value::string(string);
let result_tuple = if status_f64 == 200.0 {
Value::tuple(vec![OK, text])
} else {
Value::tuple(vec![ERR, status])
};
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 send_err_to_ludus_console(msg: String) {
console_log!("{msg}");
do_io(vec![MsgOut::Ready, MsgOut::Error(msg)]).await;
}
pub async fn do_io (msgs: Vec<MsgOut>) -> Vec<MsgIn> {
let json = serde_json::to_string(&msgs).unwrap();
let inbox = io (json).await;
let inbox = match inbox {
Ok(msgs) => msgs,
Err(_) => return vec![]
};
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");
if !inbox.is_empty() {
console_log!("ludus received messages");
for msg in inbox.iter() {
console_log!("{}", msg);
}
}
inbox
}

19
src/js.rs Normal file
View File

@ -0,0 +1,19 @@
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
pub fn log(a: &str);
#[wasm_bindgen(js_namespace = Math)]
pub fn random() -> f64;
#[wasm_bindgen(js_namespace = Date)]
pub fn now() -> f64;
}
macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
pub(crate) use console_log;

View File

@ -2,7 +2,7 @@ use crate::spans::*;
use chumsky::prelude::*;
use std::fmt;
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, PartialEq, Debug)]
pub enum Token {
Nil,
Number(f64),
@ -13,6 +13,7 @@ pub enum Token {
// todo: hard code these types
Reserved(&'static str),
Punctuation(&'static str),
Method(&'static str),
}
impl fmt::Display for Token {
@ -26,6 +27,25 @@ impl fmt::Display for Token {
Token::Reserved(r) => write!(f, "[Reserved {}]", r),
Token::Nil => write!(f, "[nil]"),
Token::Punctuation(p) => write!(f, "[Punctuation {}]", p),
Token::Method(m) => write!(f, "[Method {m}]"),
}
}
}
impl Token {
pub fn show(&self) -> String {
match self {
Token::Number(n) => format!("{n}"),
Token::Boolean(b) => format!("{b}"),
Token::Keyword(k) => format!(":{k}"),
Token::Method(m) => format!("::{m}"),
Token::Nil => "nil".to_string(),
Token::String(s) => format!("\"{s}\""),
Token::Reserved(s) | Token::Word(s) => s.to_string(),
Token::Punctuation(s) => {
let out = if *s == "\n" { "newline" } else { s };
out.to_string()
}
}
}
}
@ -56,16 +76,33 @@ pub fn lexer(
"nil" => Token::Nil,
// todo: hard code these as type constructors
"as" | "box" | "do" | "else" | "fn" | "if" | "let" | "loop" | "match" | "panic!"
| "recur" | "repeat" | "then" | "when" | "with" | "or" | "and" => Token::Reserved(word),
| "recur" | "repeat" | "then" | "when" | "with" | "or" | "and" | "receive" => {
Token::Reserved(word)
}
_ => Token::Word(word),
});
let method = just("::").ignore_then(word).map(Token::Method);
let keyword = just(':').ignore_then(word).map(Token::Keyword);
let string = just('"')
.ignore_then(none_of("\"").repeated().to_slice())
.then_ignore(just('"'))
.map(Token::String);
let escape = just('\\')
.then(choice((
just('\\').to('\\'),
just('n').to('\n'),
just('t').to('\t'),
just('r').to('\r'),
just('"').to('"'), // TODO: figure out why this isn't working
)))
.ignored();
let string = none_of('"')
.ignored()
.or(escape)
.repeated()
.to_slice()
.map(Token::String)
.delimited_by(just('"'), just('"'));
// todo: hard code these as type constructors
let punctuation = one_of(",=[]{}()>;\n_")
@ -79,6 +116,7 @@ pub fn lexer(
let token = number
.or(reserved_or_word)
.or(keyword)
.or(method)
.or(string)
.or(punctuation);
@ -96,3 +134,37 @@ pub fn lexer(
.repeated()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_lexes_nil() {
let spanned_toks = lexer().parse("nil").into_output_errors().0.unwrap();
let (token, _) = spanned_toks[0].clone();
assert_eq!(token, Token::Nil);
}
#[test]
fn it_lexes_strings() {
let spanned_toks = lexer()
.parse("\"foo bar baz\"")
.into_output_errors()
.0
.unwrap();
let (token, _) = spanned_toks[0].clone();
assert_eq!(token, Token::String("foo bar baz"));
}
#[test]
fn it_lexes_strings_w_escaped_quotes() {
let spanned_toks = lexer()
.parse("\"foo \\\"bar baz\"")
.into_output_errors()
.0
.unwrap();
let (token, _) = spanned_toks[0].clone();
assert_eq!(token, Token::String("foo \"bar baz"));
}
}

View File

@ -1,14 +1,30 @@
use chumsky::{input::Stream, prelude::*};
use imbl::HashMap;
use wasm_bindgen::prelude::*;
use std::rc::Rc;
use std::cell::RefCell;
const DEBUG_SCRIPT_COMPILE: bool = false;
const DEBUG_SCRIPT_RUN: bool = false;
const DEBUG_PRELUDE_COMPILE: bool = false;
const DEBUG_PRELUDE_RUN: bool = false;
// #[cfg(target_family = "wasm")]
// #[global_allocator]
// static ALLOCATOR: talc::TalckWasm = unsafe { talc::TalckWasm::new_global() };
mod io;
use io::send_err_to_ludus_console;
mod ast;
use crate::ast::Ast;
mod base;
mod world;
use crate::world::{World, Zoo};
mod spans;
use crate::spans::Spanned;
@ -16,13 +32,18 @@ mod lexer;
use crate::lexer::lexer;
mod parser;
use crate::parser::{parser, Ast};
use crate::parser::parser;
mod validator;
use crate::validator::Validator;
mod errors;
use crate::errors::report_invalidation;
use crate::errors::{lexing, parsing, validation};
mod panic;
mod js;
use crate::js::*;
mod chunk;
mod op;
@ -30,24 +51,24 @@ mod op;
mod compiler;
use crate::compiler::Compiler;
mod value;
use value::Value;
pub mod value;
use value::{Value, Key};
mod vm;
use vm::Vm;
use vm::Creature;
const PRELUDE: &str = include_str!("../assets/test_prelude.ld");
fn prelude() -> HashMap<&'static str, Value> {
fn prelude() -> HashMap<Key, 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)))
.into_output_errors();
if !parse_errors.is_empty() {
println!("ERROR PARSING PRELUDE:");
println!("{:?}", parse_errors);
panic!();
console_log!("ERROR PARSING PRELUDE:");
console_log!("{:?}", parse_errors);
panic!("parsing errors in prelude");
}
let parsed = parsed.unwrap();
@ -55,16 +76,17 @@ fn prelude() -> HashMap<&'static str, Value> {
let base = base::make_base();
let mut base_env = imbl::HashMap::new();
base_env.insert("base", base.clone());
base_env.insert(Key::Keyword("base"), base.clone());
let mut validator = Validator::new(ast, span, "prelude", PRELUDE, base_env);
validator.validate();
if !validator.errors.is_empty() {
println!("VALIDATION ERRORS IN PRLUDE:");
report_invalidation(validator.errors);
panic!();
console_log!("VALIDATION ERRORS IN PRLUDE:");
// report_invalidation(validator.errors);
console_log!("{:?}", validator.errors);
panic!("validator errors in prelude");
}
let parsed: &'static Spanned<Ast> = Box::leak(Box::new(parsed));
@ -81,8 +103,10 @@ fn prelude() -> HashMap<&'static str, Value> {
compiler.compile();
let chunk = compiler.chunk;
let mut vm = Vm::new(chunk, DEBUG_PRELUDE_RUN);
let prelude = vm.run().clone().unwrap();
let stub_zoo = Rc::new(RefCell::new(Zoo::new()));
let mut prld_sync = Creature::new(chunk, stub_zoo, DEBUG_PRELUDE_RUN);
prld_sync.interpret();
let prelude = prld_sync.result.unwrap().unwrap();
match prelude {
Value::Dict(hashmap) => *hashmap,
_ => unreachable!(),
@ -90,11 +114,17 @@ fn prelude() -> HashMap<&'static str, Value> {
}
#[wasm_bindgen]
pub fn ludus(src: String) -> String {
pub async fn ludus(src: String) {
// instrument wasm to report rust panics
console_error_panic_hook::set_once();
// leak the source so it lives FOREVER
let src = src.to_string().leak();
// lex the source
let (tokens, lex_errs) = lexer().parse(src).into_output_errors();
if !lex_errs.is_empty() {
return format!("{:?}", lex_errs);
send_err_to_ludus_console(lexing(lex_errs, src, "user script")).await;
return;
}
let tokens = tokens.unwrap();
@ -103,16 +133,13 @@ pub fn ludus(src: String) -> String {
.parse(Stream::from_iter(tokens).map((0..src.len()).into(), |(t, s)| (t, s)))
.into_output_errors();
if !parse_errors.is_empty() {
return format!("{:?}", parse_errors);
send_err_to_ludus_console(parsing(parse_errors, src, "user script")).await;
return;
}
// ::sigh:: The AST should be 'static
// This simplifies lifetimes, and
// in any event, the AST should live forever
let parsed: &'static Spanned<Ast> = Box::leak(Box::new(parse_result.unwrap()));
let prelude = prelude();
let postlude = prelude.clone();
// let prelude = imbl::HashMap::new();
let mut validator = Validator::new(&parsed.0, &parsed.1, "user input", src, prelude.clone());
@ -120,16 +147,21 @@ pub fn ludus(src: String) -> String {
// TODO: validator should generate a string, not print to the console
if !validator.errors.is_empty() {
report_invalidation(validator.errors);
return "Ludus found some validation errors.".to_string();
send_err_to_ludus_console(validation(validator.errors)).await;
return;
}
let mut compiler = Compiler::new(parsed, "sandbox", src, 0, prelude, DEBUG_SCRIPT_COMPILE);
// let base = base::make_base();
// compiler.emit_constant(base);
// compiler.bind("base");
let mut compiler = Compiler::new(
parsed,
"user script",
src,
0,
prelude.clone(),
DEBUG_SCRIPT_COMPILE,
);
compiler.compile();
if DEBUG_SCRIPT_COMPILE {
println!("=== source code ===");
println!("{src}");
@ -143,67 +175,17 @@ pub fn ludus(src: String) -> String {
let vm_chunk = compiler.chunk;
let mut vm = Vm::new(vm_chunk, DEBUG_SCRIPT_RUN);
let result = vm.run();
let mut world = World::new(vm_chunk, prelude.clone(), DEBUG_SCRIPT_RUN);
world.run().await;
let console = postlude.get("console").unwrap();
let Value::Box(console) = console else {
unreachable!()
};
let Value::List(ref lines) = *console.borrow() else {
unreachable!()
};
let mut console = lines
.iter()
.map(|line| line.stringify())
.collect::<Vec<_>>()
.join("\n");
// TODO: actually do something useful on a panic
// match result {
// Some(Ok(val)) => val.show(),
// Some(Err(panic)) => format!("Ludus panicked! {panic}"),
// None => "Ludus run terminated by user".to_string()
// };
// if DEBUG_SCRIPT_RUN {
// // vm.print_stack();
// }
let turtle_commands = postlude.get("turtle_commands").unwrap();
let Value::Box(commands) = turtle_commands else {
unreachable!()
};
let commands = commands.borrow();
dbg!(&commands);
let commands = commands.to_json().unwrap();
let output = match result {
Ok(val) => val.show(),
Err(panic) => {
console = format!("{console}\nLudus panicked! {panic}");
"".to_string()
}
};
if DEBUG_SCRIPT_RUN {
vm.print_stack();
}
// TODO: use serde_json to make this more robust?
format!(
"{{\"result\":\"{output}\",\"io\":{{\"stdout\":{{\"proto\":[\"text-stream\",\"0.1.0\"],\"data\":\"{console}\"}},\"turtle\":{{\"proto\":[\"turtle-graphics\",\"0.1.0\"],\"data\":{commands}}}}}}}"
)
}
pub fn fmt(src: &'static str) -> Result<String, String> {
let (tokens, lex_errs) = lexer().parse(src).into_output_errors();
if !lex_errs.is_empty() {
println!("{:?}", lex_errs);
return Err(format!("{:?}", lex_errs));
}
let tokens = tokens.unwrap();
let (parse_result, parse_errors) = parser()
.parse(Stream::from_iter(tokens).map((0..src.len()).into(), |(t, s)| (t, s)))
.into_output_errors();
if !parse_errors.is_empty() {
return Err(format!("{:?}", parse_errors));
}
// ::sigh:: The AST should be 'static
// This simplifies lifetimes, and
// in any event, the AST should live forever
let parsed: &'static Spanned<Ast> = Box::leak(Box::new(parse_result.unwrap()));
Ok(parsed.0.show())
}

View File

@ -1,10 +1,6 @@
use rudus::ludus;
use std::env;
use std::fs;
pub fn main() {
env::set_var("RUST_BACKTRACE", "1");
let src = fs::read_to_string("sandbox.ld").unwrap();
let json = ludus(src);
println!("{json}");
println!("Hello, world.")
}

View File

@ -25,7 +25,6 @@ pub enum Op {
MatchNil,
MatchTrue,
MatchFalse,
PanicIfNoMatch,
MatchConstant,
MatchString,
PushStringMatches,
@ -51,10 +50,12 @@ pub enum Op {
DropDictEntry,
PushBox,
GetKey,
PanicNoWhen,
PanicWhenFallthrough,
JumpIfNoMatch,
JumpIfMatch,
PanicNoMatch,
PanicNoLetMatch,
PanicNoFnMatch,
TypeOf,
JumpBack,
JumpIfZero,
@ -82,13 +83,6 @@ pub enum Op {
Assert,
Get,
At,
Not,
Print,
SetUpvalue,
GetUpvalue,
Msg,
// Inc,
// Dec,
// Gt,
@ -96,37 +90,24 @@ pub enum Op {
// Lt,
// Lte,
// Mod,
// Round,
// Ceil,
// Floor,
// Random,
// First,
// Rest
// Sqrt,
// Append,
Not,
Print,
SetUpvalue,
GetUpvalue,
// Assoc,
// Concat,
// Conj,
// Count,
// Disj,
// Dissoc,
// Range,
// Rest,
// Slice,
Msg,
// "atan_2" math/atan2
// "chars" chars
// "cos" math/cos
// "doc" doc
// "downcase" string/ascii-lower
// "pi" math/pi
// "show" show
// "sin" math/sin
// "split" string/split
// "str_slice" string/slice
// "tan" math/tan
// "trim" string/trim
// "triml" string/triml
// "trimr" string/trimr
// "upcase" string/ascii-upper
LoadMessage,
NextMessage,
MatchMessage,
ClearMessage,
SendMethod,
LoadScrutinee,
}
impl std::fmt::Display for Op {
@ -157,7 +138,6 @@ impl std::fmt::Display for Op {
MatchTrue => "match_true",
MatchFalse => "match_false",
ResetMatch => "reset_match",
PanicIfNoMatch => "panic_if_no_match",
MatchConstant => "match_constant",
MatchString => "match_string",
PushStringMatches => "push_string_matches",
@ -183,10 +163,12 @@ impl std::fmt::Display for Op {
DropDictEntry => "drop_dict_entry",
PushBox => "push_box",
GetKey => "get_key",
PanicNoWhen => "panic_no_when",
PanicWhenFallthrough => "panic_no_when",
JumpIfNoMatch => "jump_if_no_match",
JumpIfMatch => "jump_if_match",
PanicNoMatch => "panic_no_match",
PanicNoFnMatch => "panic_no_fn_match",
PanicNoLetMatch => "panic_no_let_match",
TypeOf => "type_of",
JumpBack => "jump_back",
JumpIfZero => "jump_if_zero",
@ -220,6 +202,14 @@ impl std::fmt::Display for Op {
SetUpvalue => "set_upvalue",
GetUpvalue => "get_upvalue",
LoadMessage => "load_message",
NextMessage => "next_message",
MatchMessage => "match_message",
ClearMessage => "clear_message",
SendMethod => "send_method",
LoadScrutinee => "load_scrutinee",
};
write!(f, "{rep}")
}

47
src/panic.rs Normal file
View File

@ -0,0 +1,47 @@
use crate::errors::panic;
use crate::value::Value;
use crate::vm::CallFrame;
#[derive(Debug, Clone, PartialEq)]
pub enum PanicMsg {
NoLetMatch,
NoFnMatch,
NoMatch,
Generic(String),
}
impl std::fmt::Display for PanicMsg {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use PanicMsg::*;
match self {
NoLetMatch => write!(f, "no match in `let`"),
NoFnMatch => write!(f, "no match calling fn"),
NoMatch => write!(f, "no match in `match` form"),
Generic(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Panic {
pub msg: PanicMsg,
pub scrutinee: Option<Value>,
pub call_stack: Vec<CallFrame>,
}
fn frame_dump(frame: &CallFrame) -> String {
let dump = format!("stack name: {}\nspans: {:?}", frame, frame.chunk().spans);
dump
}
impl std::fmt::Display for Panic {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let stub_trace = self
.call_stack
.iter()
.map(frame_dump)
.collect::<Vec<_>>()
.join("\n");
write!(f, "Panic: {}\n{stub_trace}", self.msg)
}
}

View File

@ -2,467 +2,13 @@
// TODO: remove StringMatcher cruft
// TODO: good error messages?
use crate::ast::{Ast, StringPart};
use crate::lexer::*;
use crate::spans::*;
use chumsky::{input::ValueInput, prelude::*, recursive::Recursive};
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StringPart {
Data(String),
Word(String),
Inline(String),
}
impl fmt::Display for StringPart {
fn fmt(self: &StringPart, f: &mut fmt::Formatter) -> fmt::Result {
let rep = match self {
StringPart::Word(s) => format!("{{{s}}}"),
StringPart::Data(s) => s.to_string(),
StringPart::Inline(s) => s.to_string(),
};
write!(f, "{}", rep)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Ast {
// a special Error node
// may come in handy?
Error,
And,
Or,
// expression nodes
Placeholder,
Nil,
Boolean(bool),
Number(f64),
Keyword(&'static str),
Word(&'static str),
String(&'static str),
Interpolated(Vec<Spanned<StringPart>>),
Block(Vec<Spanned<Self>>),
If(Box<Spanned<Self>>, Box<Spanned<Self>>, Box<Spanned<Self>>),
Tuple(Vec<Spanned<Self>>),
Arguments(Vec<Spanned<Self>>),
List(Vec<Spanned<Self>>),
Dict(Vec<Spanned<Self>>),
Let(Box<Spanned<Self>>, Box<Spanned<Self>>),
LBox(&'static str, Box<Spanned<Self>>),
Synthetic(Box<Spanned<Self>>, Box<Spanned<Self>>, Vec<Spanned<Self>>),
When(Vec<Spanned<Self>>),
WhenClause(Box<Spanned<Self>>, Box<Spanned<Self>>),
Match(Box<Spanned<Self>>, Vec<Spanned<Self>>),
MatchClause(
Box<Spanned<Self>>,
Box<Option<Spanned<Self>>>,
Box<Spanned<Self>>,
),
Fn(&'static str, Box<Spanned<Ast>>, Option<&'static str>),
FnBody(Vec<Spanned<Ast>>),
FnDeclaration(&'static str),
Panic(Box<Spanned<Self>>),
Do(Vec<Spanned<Self>>),
Repeat(Box<Spanned<Self>>, Box<Spanned<Self>>),
Splat(&'static str),
Pair(&'static str, Box<Spanned<Self>>),
Loop(Box<Spanned<Self>>, Vec<Spanned<Self>>),
Recur(Vec<Spanned<Self>>),
// pattern nodes
NilPattern,
BooleanPattern(bool),
NumberPattern(f64),
StringPattern(&'static str),
InterpolatedPattern(Vec<Spanned<StringPart>>, StringMatcher),
KeywordPattern(&'static str),
WordPattern(&'static str),
AsPattern(&'static str, &'static str),
Splattern(Box<Spanned<Self>>),
PlaceholderPattern,
TuplePattern(Vec<Spanned<Self>>),
ListPattern(Vec<Spanned<Self>>),
PairPattern(&'static str, Box<Spanned<Self>>),
DictPattern(Vec<Spanned<Self>>),
}
impl Ast {
pub fn show(&self) -> String {
use Ast::*;
match self {
And => "and".to_string(),
Or => "or".to_string(),
Error => unreachable!(),
Nil | NilPattern => "nil".to_string(),
String(s) | StringPattern(s) => format!("\"{s}\""),
Interpolated(strs) | InterpolatedPattern(strs, _) => {
let mut out = "".to_string();
out = format!("\"{out}");
for (part, _) in strs {
out = format!("{out}{part}");
}
format!("{out}\"")
}
Boolean(b) | BooleanPattern(b) => b.to_string(),
Number(n) | NumberPattern(n) => n.to_string(),
Keyword(k) | KeywordPattern(k) => format!(":{k}"),
Word(w) | WordPattern(w) => w.to_string(),
Block(lines) => {
let mut out = "{\n".to_string();
for (line, _) in lines {
out = format!("{out}\n {}", line.show());
}
format!("{out}\n}}")
}
If(cond, then, r#else) => format!(
"if {}\n then {}\n else {}",
cond.0.show(),
then.0.show(),
r#else.0.show()
),
Let(pattern, expression) => {
format!("let {} = {}", pattern.0.show(), expression.0.show())
}
Dict(entries) | DictPattern(entries) => {
format!(
"#{{{}}}",
entries
.iter()
.map(|(pair, _)| pair.show())
.collect::<Vec<_>>()
.join(", ")
)
}
List(members) | ListPattern(members) => format!(
"[{}]",
members
.iter()
.map(|(member, _)| member.show())
.collect::<Vec<_>>()
.join(", ")
),
Arguments(members) => format!(
"({})",
members
.iter()
.map(|(member, _)| member.show())
.collect::<Vec<_>>()
.join(", ")
),
Tuple(members) | TuplePattern(members) => format!(
"({})",
members
.iter()
.map(|(member, _)| member.show())
.collect::<Vec<_>>()
.join(", ")
),
Synthetic(root, first, rest) => format!(
"{} {} {}",
root.0.show(),
first.0.show(),
rest.iter()
.map(|(term, _)| term.show())
.collect::<Vec<_>>()
.join(" ")
),
When(clauses) => format!(
"when {{\n {}\n}}",
clauses
.iter()
.map(|(clause, _)| clause.show())
.collect::<Vec<_>>()
.join("\n ")
),
Placeholder | PlaceholderPattern => "_".to_string(),
LBox(name, rhs) => format!("box {name} = {}", rhs.0.show()),
Match(scrutinee, clauses) => format!(
"match {} with {{\n {}\n}}",
scrutinee.0.show(),
clauses
.iter()
.map(|(clause, _)| clause.show())
.collect::<Vec<_>>()
.join("\n ")
),
FnBody(clauses) => clauses
.iter()
.map(|(clause, _)| clause.show())
.collect::<Vec<_>>()
.join("\n "),
Fn(name, body, doc) => {
let mut out = format!("fn {name} {{\n");
if let Some(doc) = doc {
out = format!("{out} {doc}\n");
}
format!("{out} {}\n}}", body.0.show())
}
FnDeclaration(name) => format!("fn {name}"),
Panic(expr) => format!("panic! {}", expr.0.show()),
Do(terms) => {
format!(
"do {}",
terms
.iter()
.map(|(term, _)| term.show())
.collect::<Vec<_>>()
.join(" > ")
)
}
Repeat(times, body) => format!("repeat {} {{\n{}\n}}", times.0.show(), body.0.show()),
Splat(word) => format!("...{}", word),
Splattern(pattern) => format!("...{}", pattern.0.show()),
AsPattern(word, type_keyword) => format!("{word} as :{type_keyword}"),
Pair(key, value) | PairPattern(key, value) => format!(":{key} {}", value.0.show()),
Loop(init, body) => format!(
"loop {} with {{\n {}\n}}",
init.0.show(),
body.iter()
.map(|(clause, _)| clause.show())
.collect::<Vec<_>>()
.join("\n ")
),
Recur(args) => format!(
"recur ({})",
args.iter()
.map(|(arg, _)| arg.show())
.collect::<Vec<_>>()
.join(", ")
),
MatchClause(pattern, guard, body) => {
let mut out = pattern.0.show();
if let Some(guard) = guard.as_ref() {
out = format!("{out} if {}", guard.0.show());
}
format!("{out} -> {}", body.0.show())
}
WhenClause(cond, body) => format!("{} -> {}", cond.0.show(), body.0.show()),
}
}
}
impl fmt::Display for Ast {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use Ast::*;
match self {
And => write!(f, "And"),
Or => write!(f, "Or"),
Error => write!(f, "Error"),
Nil => write!(f, "nil"),
String(s) => write!(f, "String: \"{}\"", s),
Interpolated(strs) => {
write!(
f,
"Interpolated: \"{}\"",
strs.iter()
.map(|(s, _)| s.to_string())
.collect::<Vec<_>>()
.join("")
)
}
Boolean(b) => write!(f, "Boolean: {}", b),
Number(n) => write!(f, "Number: {}", n),
Keyword(k) => write!(f, "Keyword: :{}", k),
Word(w) => write!(f, "Word: {}", w),
Block(b) => write!(
f,
"Block: <{}>",
b.iter()
.map(|(line, _)| line.to_string())
.collect::<Vec<_>>()
.join("\n")
),
If(cond, then_branch, else_branch) => write!(
f,
"If: {} Then: {} Else: {}",
cond.0, then_branch.0, else_branch.0
),
Let(pattern, expression) => {
write!(f, "Let: {} = {}", pattern.0, expression.0)
}
Dict(entries) => write!(
f,
"#{{{}}}",
entries
.iter()
.map(|pair| pair.0.to_string())
.collect::<Vec<_>>()
.join(", ")
),
List(l) => write!(
f,
"List: [{}]",
l.iter()
.map(|(line, _)| line.to_string())
.collect::<Vec<_>>()
.join("\n")
),
Arguments(a) => write!(
f,
"Arguments: ({})",
a.iter()
.map(|(line, _)| line.to_string())
.collect::<Vec<_>>()
.join("\n")
),
Tuple(t) => write!(
f,
"Tuple: ({})",
t.iter()
.map(|(line, _)| line.to_string())
.collect::<Vec<_>>()
.join("\n")
),
Synthetic(root, first, rest) => write!(
f,
"Synth: [{}, {}, {}]",
root.0,
first.0,
rest.iter()
.map(|(term, _)| term.to_string())
.collect::<Vec<_>>()
.join("\n")
),
When(clauses) => write!(
f,
"When: [{}]",
clauses
.iter()
.map(|clause| clause.0.to_string())
.collect::<Vec<_>>()
.join("\n")
),
Placeholder => write!(f, "Placeholder"),
LBox(_name, _rhs) => todo!(),
Match(value, clauses) => {
write!(
f,
"match: {} with {}",
&value.0.to_string(),
clauses
.iter()
.map(|clause| clause.0.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
FnBody(clauses) => {
write!(
f,
"{}",
clauses
.iter()
.map(|clause| clause.0.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
Fn(name, body, ..) => {
write!(f, "fn: {name}\n{}", body.0)
}
FnDeclaration(_name) => todo!(),
Panic(_expr) => todo!(),
Do(terms) => {
write!(
f,
"do: {}",
terms
.iter()
.map(|(term, _)| term.to_string())
.collect::<Vec<_>>()
.join(" > ")
)
}
Repeat(_times, _body) => todo!(),
Splat(word) => {
write!(f, "splat: {}", word)
}
Pair(k, v) => {
write!(f, "pair: {} {}", k, v.0)
}
Loop(init, body) => {
write!(
f,
"loop: {} with {}",
init.0,
body.iter()
.map(|clause| clause.0.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
Recur(args) => {
write!(
f,
"recur: {}",
args.iter()
.map(|(arg, _)| arg.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
MatchClause(pattern, guard, body) => {
write!(
f,
"match clause: {} if {:?} -> {}",
pattern.0, guard, body.0
)
}
WhenClause(cond, body) => {
write!(f, "when clause: {} -> {}", cond.0, body.0)
}
NilPattern => write!(f, "nil"),
BooleanPattern(b) => write!(f, "{}", b),
NumberPattern(n) => write!(f, "{}", n),
StringPattern(s) => write!(f, "{}", s),
KeywordPattern(k) => write!(f, ":{}", k),
WordPattern(w) => write!(f, "{}", w),
AsPattern(w, t) => write!(f, "{} as :{}", w, t),
Splattern(p) => write!(f, "...{}", p.0),
PlaceholderPattern => write!(f, "_"),
TuplePattern(t) => write!(
f,
"({})",
t.iter()
.map(|x| x.0.to_string())
.collect::<Vec<_>>()
.join(", ")
),
ListPattern(l) => write!(
f,
"({})",
l.iter()
.map(|x| x.0.to_string())
.collect::<Vec<_>>()
.join(", ")
),
DictPattern(entries) => write!(
f,
"#{{{}}}",
entries
.iter()
.map(|(pair, _)| pair.to_string())
.collect::<Vec<_>>()
.join(", ")
),
PairPattern(key, value) => write!(f, ":{} {}", key, value.0),
InterpolatedPattern(strprts, _) => write!(
f,
"interpolated: \"{}\"",
strprts
.iter()
.map(|part| part.0.to_string())
.collect::<Vec<_>>()
.join("")
),
}
}
}
pub struct StringMatcher(pub Box<dyn Fn(String) -> Option<Vec<(String, String)>>>);
pub struct StringMatcher();
impl PartialEq for StringMatcher {
fn eq(&self, _other: &StringMatcher) -> bool {
@ -516,7 +62,7 @@ fn parse_string(s: &'static str, span: SimpleSpan) -> Result<Vec<Spanned<StringP
if !current_part.is_empty() {
parts.push((
StringPart::Data(current_part),
SimpleSpan::new(start, start + i),
SimpleSpan::new(span.context(), start..start + i),
));
};
current_part = String::new();
@ -533,8 +79,8 @@ fn parse_string(s: &'static str, span: SimpleSpan) -> Result<Vec<Spanned<StringP
'}' => {
if is_word {
parts.push((
StringPart::Word(current_part.clone()),
SimpleSpan::new(start, start + i),
StringPart::Word(current_part.leak()),
SimpleSpan::new(span.context(), start..start + i),
));
current_part = String::new();
start = i;
@ -563,67 +109,19 @@ fn parse_string(s: &'static str, span: SimpleSpan) -> Result<Vec<Spanned<StringP
if current_part == s {
parts.push((
StringPart::Inline(current_part),
SimpleSpan::new(start, span.end),
SimpleSpan::new(span.context(), start..span.end),
))
} else if !current_part.is_empty() {
let part_len = current_part.len();
parts.push((
StringPart::Data(current_part),
SimpleSpan::new(start, part_len),
SimpleSpan::new(span.context(), start..part_len),
))
}
Ok(parts)
}
pub fn compile_string_pattern(parts: Vec<Spanned<StringPart>>) -> StringMatcher {
StringMatcher(Box::new(move |scrutinee| {
let mut last_match = 0;
let mut parts_iter = parts.iter();
let mut matches = vec![];
while let Some((part, _)) = parts_iter.next() {
match part {
StringPart::Data(string) => match scrutinee.find(string.as_str()) {
Some(i) => {
// if i = 0, we're at the beginning
if i == 0 && last_match == 0 {
last_match = i + string.len();
continue;
}
// in theory, we only hit this branch if the first part is Data
unreachable!("internal Ludus error: bad string pattern")
}
None => return None,
},
StringPart::Word(word) => {
let to_test = scrutinee.get(last_match..scrutinee.len()).unwrap();
match parts_iter.next() {
None => matches.push((word.clone(), to_test.to_string())),
Some(part) => {
let (StringPart::Data(part), _) = part else {
unreachable!("internal Ludus error: bad string pattern")
};
match to_test.find(part) {
None => return None,
Some(i) => {
matches.push((
word.clone(),
to_test.get(last_match..i).unwrap().to_string(),
));
last_match = i + part.len();
continue;
}
}
}
}
}
_ => unreachable!("internal Ludus error"),
}
}
Some(matches)
}))
}
pub fn parser<I>(
) -> impl Parser<'static, I, Spanned<Ast>, extra::Err<Rich<'static, Token, Span>>> + Clone
where
@ -643,13 +141,15 @@ where
just(Token::Punctuation(","))
.or(just(Token::Punctuation("\n")))
.then(separators.clone().repeated())
});
})
.labelled("separator");
let terminators = recursive(|terminators| {
just(Token::Punctuation(";"))
.or(just(Token::Punctuation("\n")))
.then(terminators.clone().repeated())
});
})
.labelled("terminator");
let placeholder_pattern =
select! {Token::Punctuation("_") => PlaceholderPattern}.map_with(|p, e| (p, e.span()));
@ -669,10 +169,7 @@ where
match parsed {
Ok(parts) => match parts[0] {
(StringPart::Inline(_), _) => Ok((StringPattern(s), e.span())),
_ => Ok((
InterpolatedPattern(parts.clone(), compile_string_pattern(parts)),
e.span(),
)),
_ => Ok((InterpolatedPattern(parts.clone()), e.span())),
},
Err(msg) => Err(Rich::custom(e.span(), msg)),
}
@ -712,22 +209,29 @@ where
.allow_trailing()
.collect()
.delimited_by(just(Token::Punctuation("[")), just(Token::Punctuation("]")))
.map_with(|list, e| (ListPattern(list), e.span()));
.map_with(|list, e| (ListPattern(list), e.span()))
.labelled("list pattern");
let pair_pattern = select! {Token::Keyword(k) => k}
let key_pair_pattern = select! {Token::Keyword(k) => k}
.then(pattern.clone())
.map_with(|(key, patt), e| (PairPattern(key, Box::new(patt)), e.span()));
.map_with(|(key, patt), e| (KeyPairPattern(key, Box::new(patt)), e.span()));
let shorthand_pattern = select! {Token::Word(w) => w}.map_with(|w, e| {
(
PairPattern(w, Box::new((WordPattern(w), e.span()))),
KeyPairPattern(w, Box::new((WordPattern(w), e.span()))),
e.span(),
)
});
let dict_pattern = pair_pattern
let str_pair_pattern = select! {Token::String(s) => s}
.then(pattern.clone())
.map_with(|(key, patt), e| (StrPairPattern(key, Box::new(patt)), e.span()));
let dict_pattern = key_pair_pattern
.or(shorthand_pattern)
.or(str_pair_pattern)
.or(splattern.clone())
.labelled("pair pattern")
.separated_by(separators.clone())
.allow_leading()
.allow_trailing()
@ -738,11 +242,14 @@ where
)
.map_with(|dict, e| (DictPattern(dict), e.span()));
let keyword = select! {Token::Keyword(k) => Keyword(k),}.map_with(|k, e| (k, e.span()));
let keyword = select! {Token::Keyword(k) => Keyword(k)}
.map_with(|k, e| (k, e.span()))
.labelled("keyword");
let as_pattern = select! {Token::Word(w) => w}
.then_ignore(just(Token::Reserved("as")))
.then(select! {Token::Keyword(k) => k})
.labelled("keyword")
.map_with(|(w, t), e| (AsPattern(w, t), e.span()));
pattern.define(
@ -789,7 +296,8 @@ where
.allow_trailing()
.collect()
.delimited_by(just(Token::Punctuation("(")), just(Token::Punctuation(")")))
.map_with(|tuple, e| (Tuple(tuple), e.span()));
.map_with(|tuple, e| (Tuple(tuple), e.span()))
.labelled("tuple");
let args = simple
.clone()
@ -799,15 +307,21 @@ where
.allow_trailing()
.collect()
.delimited_by(just(Token::Punctuation("(")), just(Token::Punctuation(")")))
.map_with(|args, e| (Arguments(args), e.span()));
.map_with(|args, e| (Arguments(args), e.span()))
.labelled("args");
let or = just(Token::Reserved("or")).map_with(|_, e| (Or, e.span()));
let and = just(Token::Reserved("and")).map_with(|_, e| (And, e.span()));
let method = select!(Token::Method(m) => m)
.then(tuple.clone())
.map_with(|(m, t), e| (Ast::Method(m, Box::new(t)), e.span()))
.labelled("method");
let synth_root = or.or(and).or(word).or(keyword);
let synth_term = keyword.or(args);
let synth_term = keyword.or(args).or(method);
let synthetic = synth_root
.then(synth_term.clone())
@ -823,7 +337,8 @@ where
Splat(if let Word(w) = w { w } else { unreachable!() }),
e.span(),
)
});
})
.labelled("...");
let list = simple
.clone()
@ -835,15 +350,20 @@ where
.delimited_by(just(Token::Punctuation("[")), just(Token::Punctuation("]")))
.map_with(|list, e| (List(list), e.span()));
let pair = select! {Token::Keyword(k) => k}
let key_pair = select! {Token::Keyword(k) => k}
.then(simple.clone())
.map_with(|(key, value), e| (Pair(key, Box::new(value)), e.span()));
.map_with(|(key, value), e| (KeywordPair(key, Box::new(value)), e.span()));
let shorthand = select! {Token::Word(w) => w}
.map_with(|w, e| (Pair(w, Box::new((Word(w), e.span()))), e.span()));
.map_with(|w, e| (KeywordPair(w, Box::new((Word(w), e.span()))), e.span()));
let dict = pair
let str_pair = select! {Token::String(s) => s}
.then(simple.clone())
.map_with(|(key, value), e| (StringPair(key, Box::new(value)), e.span()));
let dict = key_pair
.or(shorthand)
.or(str_pair)
.or(splat.clone())
.separated_by(separators.clone())
.allow_leading()
@ -882,7 +402,7 @@ where
|span| (Error, span),
)));
let if_ = just(Token::Reserved("if"))
let r#if = just(Token::Reserved("if"))
.ignore_then(simple.clone())
.then_ignore(terminators.clone().or_not())
.then_ignore(just(Token::Reserved("then")))
@ -948,7 +468,7 @@ where
.then(
match_clause
.clone()
.or(guarded_clause)
.or(guarded_clause.clone())
.separated_by(terminators.clone())
.allow_leading()
.allow_trailing()
@ -957,15 +477,28 @@ where
)
.map_with(|(expr, clauses), e| (Match(Box::new(expr), clauses), e.span()));
let conditional = when.or(if_).or(r#match);
let receive = just(Token::Reserved("receive"))
.ignore_then(
match_clause
.clone()
.or(guarded_clause)
.separated_by(terminators.clone())
.allow_leading()
.allow_trailing()
.collect()
.delimited_by(just(Token::Punctuation("{")), just(Token::Punctuation("}"))),
)
.map_with(|clauses, e| (Receive(clauses), e.span()));
let conditional = when.or(r#if).or(r#match).or(receive);
let panic = just(Token::Reserved("panic!"))
.ignore_then(nonbinding.clone())
.map_with(|expr, e| (Panic(Box::new(expr)), e.span()));
let do_ = just(Token::Reserved("do"))
let r#do = just(Token::Reserved("do"))
.ignore_then(
nonbinding
simple
.clone()
.separated_by(
just(Token::Punctuation(">")).then(just(Token::Punctuation("\n")).repeated()),
@ -1044,7 +577,9 @@ where
.or(tuple.clone())
.or(list)
.or(dict)
.or(panic)
.or(string)
.or(r#do)
.or(lambda.clone())
.labelled("simple expression"),
);
@ -1054,8 +589,6 @@ where
.clone()
.or(conditional)
.or(block)
.or(panic)
.or(do_)
.or(repeat)
.or(r#loop)
.labelled("nonbinding expression"),

View File

@ -4,9 +4,9 @@
// * [ ] ensure loops have fixed arity (no splats)
// * [ ] ensure fn pattern splats are always highest (and same) arity
use crate::parser::*;
use crate::ast::{Ast, StringPart};
use crate::spans::{Span, Spanned};
use crate::value::Value;
use crate::value::{Key, Value};
use std::collections::{HashMap, HashSet};
#[derive(Clone, Debug, PartialEq)]
@ -61,7 +61,7 @@ fn match_arities(arities: &HashSet<Arity>, num_args: u8) -> bool {
#[derive(Debug, PartialEq)]
pub struct Validator<'a> {
pub locals: Vec<(String, &'a Span, FnInfo)>,
pub prelude: imbl::HashMap<&'static str, Value>,
pub prelude: imbl::HashMap<Key, Value>,
pub input: &'static str,
pub src: &'static str,
pub ast: &'a Ast,
@ -77,7 +77,7 @@ impl<'a> Validator<'a> {
span: &'a Span,
input: &'static str,
src: &'static str,
prelude: imbl::HashMap<&'static str, Value>,
prelude: imbl::HashMap<Key, Value>,
) -> Validator<'a> {
Validator {
input,
@ -113,9 +113,12 @@ impl<'a> Validator<'a> {
self.locals[i] = new_binding;
}
fn resolved(&self, name: &str) -> bool {
fn resolved(&self, name: &'static str) -> bool {
self.locals.iter().any(|(bound, ..)| name == bound.as_str())
|| self.prelude.iter().any(|(bound, _)| name == *bound)
|| self
.prelude
.iter()
.any(|(bound, _)| Key::Keyword(name) == *bound)
}
fn bound(&self, name: &str) -> Option<&(String, &Span, FnInfo)> {
@ -172,7 +175,7 @@ impl<'a> Validator<'a> {
for part in parts {
if let (StringPart::Word(name), span) = part {
self.span = span;
if !self.resolved(name.as_str()) {
if !self.resolved(name) {
self.err(format!("unbound name `{name}`"));
} else {
self.use_name(name.to_string());
@ -267,7 +270,7 @@ impl<'a> Validator<'a> {
self.status.tail_position = tailpos;
}
Pair(_, value) => self.visit(value.as_ref()),
KeywordPair(_, value) | StringPair(_, value) => self.visit(value.as_ref()),
Dict(dict) => {
if dict.is_empty() {
return;
@ -284,6 +287,13 @@ impl<'a> Validator<'a> {
// check arity against fn info if first term is word and second term is args
Synthetic(first, second, rest) => {
match (&first.0, &second.0) {
(Ast::Word(_), Ast::Method(_, args)) => {
self.visit(first.as_ref());
self.visit(args);
}
(Ast::Keyword(_), Ast::Method(_, args)) => {
self.visit(args);
}
(Ast::And, Ast::Arguments(_)) | (Ast::Or, Ast::Arguments(_)) => {
self.visit(second.as_ref())
}
@ -364,6 +374,11 @@ impl<'a> Validator<'a> {
self.visit(clause);
}
}
Receive(clauses) => {
for clause in clauses {
self.visit(clause);
}
}
FnDeclaration(name) => {
let tailpos = self.status.tail_position;
self.status.tail_position = false;
@ -520,7 +535,7 @@ impl<'a> Validator<'a> {
self.bind(name.to_string());
}
},
InterpolatedPattern(parts, _) => {
InterpolatedPattern(parts) => {
for (part, span) in parts {
if let StringPart::Word(name) = part {
self.span = span;
@ -580,9 +595,9 @@ impl<'a> Validator<'a> {
self.visit(last);
self.status.last_term = false;
}
PairPattern(_, patt) => self.visit(patt.as_ref()),
KeyPairPattern(_, patt) | StrPairPattern(_, patt) => self.visit(patt.as_ref()),
// terminals can never be invalid
Nil | Boolean(_) | Number(_) | Keyword(_) | String(_) | And | Or => (),
Nil | Boolean(_) | Number(_) | Keyword(_) | String(_) | And | Or | Method(..) => (),
// terminal patterns can never be invalid
NilPattern | BooleanPattern(..) | NumberPattern(..) | StringPattern(..)
| KeywordPattern(..) | PlaceholderPattern => (),

View File

@ -1,8 +1,7 @@
use crate::base::BaseFn;
use crate::chunk::Chunk;
// use crate::parser::Ast;
// use crate::spans::Spanned;
use imbl::{HashMap, Vector};
use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer};
use std::cell::RefCell;
use std::rc::Rc;
@ -114,6 +113,55 @@ pub struct Partial {
pub function: Value,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Key {
Keyword(&'static str),
Interned(&'static str),
String(Rc<String>),
}
impl std::fmt::Display for Key {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Key::Keyword(s) => write!(f, ":{s}"),
Key::Interned(s) => write!(f, "\"{s}\""),
Key::String(s) => write!(f, "\"{s}\""),
}
}
}
impl Serialize for Key {
fn serialize<S>(&self, srlzr: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Key::Keyword(s) => srlzr.serialize_str(s),
Key::Interned(s) => srlzr.serialize_str(s),
Key::String(s) => srlzr.serialize_str(s.as_str()),
}
}
}
impl Key {
pub fn to_value(&self) -> Value {
match self {
Key::Keyword(s) => Value::Keyword(s),
Key::Interned(s) => Value::Interned(s),
Key::String(s) => Value::String(s.clone()),
}
}
pub fn from_value(value: Value) -> Key {
match value {
Value::Keyword(s) => Key::Keyword(s),
Value::Interned(s) => Key::Interned(s),
Value::String(s) => Key::String(s.clone()),
_ => unreachable!("dict keys must be keywords or strings"),
}
}
}
#[derive(Clone, Debug)]
pub enum Value {
Nothing,
@ -126,11 +174,12 @@ pub enum Value {
Number(f64),
Tuple(Rc<Vec<Value>>),
List(Box<Vector<Value>>),
Dict(Box<HashMap<&'static str, Value>>),
Dict(Box<HashMap<Key, Value>>),
Box(Rc<RefCell<Value>>),
Fn(Rc<LFn>),
BaseFn(BaseFn),
BaseFn(Box<BaseFn>),
Partial(Rc<Partial>),
Process,
}
impl PartialEq for Value {
@ -167,6 +216,7 @@ impl std::fmt::Display for Value {
Interned(str) => write!(f, "\"{str}\""),
String(str) => write!(f, "\"{str}\""),
Number(n) => write!(f, "{n}"),
Process => write!(f, "Process"),
Tuple(members) => write!(
f,
"({})",
@ -197,7 +247,7 @@ impl std::fmt::Display for Value {
Box(value) => write!(f, "box {{ {} }}", value.as_ref().borrow()),
Fn(lfn) => write!(f, "fn {}", lfn.name()),
BaseFn(inner) => {
let name = match inner {
let name = match **inner {
crate::base::BaseFn::Nullary(name, _)
| crate::base::BaseFn::Unary(name, _)
| crate::base::BaseFn::Binary(name, _)
@ -210,10 +260,56 @@ impl std::fmt::Display for Value {
}
}
impl Serialize for Value {
fn serialize<S>(&self, srlzr: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use Value::*;
match self {
Nil => srlzr.serialize_none(),
True => srlzr.serialize_bool(true),
False => srlzr.serialize_bool(false),
Number(n) => srlzr.serialize_f64(*n),
Interned(s) => srlzr.serialize_str(s),
Keyword(k) => srlzr.serialize_str(k),
String(s) => srlzr.serialize_str(s.as_str()),
Tuple(t) => {
let mut seq = srlzr.serialize_seq(Some(t.len()))?;
for e in t.iter() {
seq.serialize_element(e)?;
}
seq.end()
}
List(l) => {
let mut seq = srlzr.serialize_seq(Some(l.len()))?;
for e in l.iter() {
seq.serialize_element(e)?;
}
seq.end()
}
Dict(d) => {
let mut map = srlzr.serialize_map(Some(d.len()))?;
for (k, v) in d.iter() {
map.serialize_entry(k, v)?;
}
map.end()
}
Box(b) => {
let boxed = b.borrow();
(*boxed).serialize(srlzr)
}
Fn(..) | BaseFn(..) | Partial(..) => unreachable!(),
Process | Nothing => unreachable!(),
}
}
}
impl Value {
pub fn show(&self) -> String {
use Value::*;
let mut out = match &self {
Process => "Process".to_string(),
Nil => "nil".to_string(),
True => "true".to_string(),
False => "false".to_string(),
@ -233,9 +329,8 @@ impl Value {
let members = d
.iter()
.map(|(k, v)| {
let key_show = Value::Keyword(k).show();
let value_show = v.show();
format!("{key_show} {value_show}")
format!("{k} {value_show}")
})
.collect::<Vec<_>>()
.join(", ");
@ -247,67 +342,84 @@ 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
}
}
pub fn to_json(&self) -> Option<String> {
use Value::*;
match self {
True | False | String(..) | Interned(..) | Number(..) => Some(self.show()),
Keyword(str) => Some(format!("\"{str}\"")),
List(members) => {
let mut joined = "".to_string();
let mut members = members.iter();
if let Some(member) = members.next() {
joined = member.to_json()?;
}
for member in members {
let json = member.to_json()?;
joined = format!("{joined},{json}");
}
Some(format!("[{joined}]"))
}
Tuple(members) => {
let mut joined = "".to_string();
let mut members = members.iter();
if let Some(member) = members.next() {
joined = member.to_json()?;
}
for member in members {
let json = member.to_json()?;
joined = format!("{joined},{json}");
}
Some(format!("[{joined}]"))
}
Dict(members) => {
let mut joined = "".to_string();
let mut members = members.iter();
if let Some((key, value)) = members.next() {
let json = value.to_json()?;
joined = format!("\"{key}\":{json}")
}
for (key, value) in members {
let json = value.to_json()?;
joined = format!("{joined},\"{key}\": {json}");
}
Some(format!("{{{joined}}}"))
}
not_serializable => {
println!("Cannot convert to json:");
dbg!(not_serializable);
None
}
}
}
// pub fn to_js(&self) -> JsValue {
// use Value::*;
// match self {
// Nil => JsValue::NULL,
// True => JsValue::TRUE,
// False => JsValue::FALSE,
// Number(n) => JsValue::from_f64(*n),
// Interned(s) => JsValue::from_str(s),
// String(s) => JsValue::from_str(s.as_str()),
// Keyword(k) => JsValue::from_str(k),
// _ => todo!(),
// }
// }
// pub fn to_json(&self) -> Option<String> {
// use Value::*;
// match self {
// True | False | Number(..) => Some(self.show()),
// String(string) => Some(string.escape_default().to_string()),
// Interned(str) => Some(str.escape_default().to_string()),
// Keyword(str) => Some(format!("\"{str}\"")),
// List(members) => {
// let mut joined = "".to_string();
// let mut members = members.iter();
// if let Some(member) = members.next() {
// joined = member.to_json()?;
// }
// for member in members {
// let json = member.to_json()?;
// joined = format!("{joined},{json}");
// }
// Some(format!("[{joined}]"))
// }
// Tuple(members) => {
// let mut joined = "".to_string();
// let mut members = members.iter();
// if let Some(member) = members.next() {
// joined = member.to_json()?;
// }
// for member in members {
// let json = member.to_json()?;
// joined = format!("{joined},{json}");
// }
// Some(format!("[{joined}]"))
// }
// Dict(members) => {
// let mut joined = "".to_string();
// let mut members = members.iter();
// if let Some((key, value)) = members.next() {
// let json = value.to_json()?;
// joined = format!("\"{key}\":{json}")
// }
// for (key, value) in members {
// let json = value.to_json()?;
// joined = format!("{joined},\"{key}\": {json}");
// }
// Some(format!("{{{joined}}}"))
// }
// not_serializable => {
// println!("Cannot convert to json:");
// dbg!(not_serializable);
// None
// }
// }
// }
pub fn stringify(&self) -> String {
use Value::*;
match &self {
Process => "process".to_string(),
Nil => "nil".to_string(),
True => "true".to_string(),
False => "false".to_string(),
@ -334,9 +446,8 @@ impl Value {
let members = d
.iter()
.map(|(k, v)| {
let key_show = Value::Keyword(k).stringify();
let value_show = v.stringify();
format!("{key_show} {value_show}")
format!("{k} {value_show}")
})
.collect::<Vec<_>>()
.join(", ");
@ -369,13 +480,73 @@ impl Value {
Fn(..) => "fn",
BaseFn(..) => "fn",
Partial(..) => "fn",
Process => "process",
}
}
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 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 {
Value::String(Rc::new(str))
}
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"),
// }
// }
}

548
src/vm.rs
View File

@ -1,49 +1,21 @@
use crate::base::BaseFn;
use crate::chunk::Chunk;
use crate::js::*;
use crate::op::Op;
use crate::parser::Ast;
use crate::spans::Spanned;
use crate::value::{LFn, Value};
use crate::panic::{Panic, PanicMsg};
use crate::value::{Key, LFn, Value};
use crate::world::Zoo;
use imbl::{HashMap, Vector};
use num_traits::FromPrimitive;
use std::cell::RefCell;
use std::collections::VecDeque;
use std::fmt;
use std::mem::swap;
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq)]
// pub struct Panic {
// pub input: &'static str,
// pub src: &'static str,
// pub msg: String,
// pub span: SimpleSpan,
// pub trace: Vec<Trace>,
// pub extra: String,
// }
pub enum Panic {
Str(&'static str),
String(String),
}
impl fmt::Display for Panic {
fn fmt(self: &Panic, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Panic::Str(msg) => write!(f, "{msg}"),
Panic::String(msg) => write!(f, "{msg}"),
}
}
}
const MAX_REDUCTIONS: usize = 1000;
#[derive(Debug, Clone, PartialEq)]
pub struct Trace {
pub callee: Spanned<Ast>,
pub caller: Spanned<Ast>,
pub function: Value,
pub arguments: Value,
pub input: &'static str,
pub src: &'static str,
}
pub struct CallFrame {
pub function: Value,
pub arity: u8,
@ -80,23 +52,39 @@ fn combine_bytes(high: u8, low: u8) -> usize {
out as usize
}
pub struct Vm {
pub stack: Vec<Value>,
pub call_stack: Vec<CallFrame>,
pub frame: CallFrame,
pub ip: usize,
pub return_register: [Value; 8],
pub matches: bool,
pub match_depth: u8,
const REGISTER_SIZE: usize = 8;
#[derive(Debug, Clone, PartialEq)]
pub struct Creature {
stack: Vec<Value>,
call_stack: Vec<CallFrame>,
frame: CallFrame,
ip: usize,
register: [Value; REGISTER_SIZE],
matches: bool,
match_depth: u8,
pub result: Option<Result<Value, Panic>>,
debug: bool,
last_code: usize,
pub pid: &'static str,
pub mbx: VecDeque<Value>,
msg_idx: usize,
reductions: usize,
zoo: Rc<RefCell<Zoo>>,
r#yield: bool,
scrutinee: Option<Value>,
}
impl Vm {
pub fn new(chunk: Chunk, debug: bool) -> Vm {
impl std::fmt::Display for Creature {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Creature. {} @{}", self.pid, self.ip)
}
}
impl Creature {
pub fn new(chunk: Chunk, zoo: Rc<RefCell<Zoo>>, debug: bool) -> Creature {
let lfn = LFn::Defined {
name: "user script",
name: "toplevel",
doc: None,
chunks: vec![chunk],
arities: vec![0],
@ -104,26 +92,50 @@ impl Vm {
closed: RefCell::new(vec![]),
};
let base_fn = Value::Fn(Rc::new(lfn));
Creature::spawn(base_fn, zoo, debug)
}
pub fn spawn(function: Value, zoo: Rc<RefCell<Zoo>>, debug: bool) -> Creature {
let base_frame = CallFrame {
function: base_fn.clone(),
function,
stack_base: 0,
ip: 0,
arity: 0,
};
Vm {
Creature {
stack: vec![],
call_stack: Vec::with_capacity(64),
frame: base_frame,
ip: 0,
return_register: [const { Value::Nothing }; 8],
register: [const { Value::Nothing }; REGISTER_SIZE],
matches: false,
match_depth: 0,
result: None,
debug,
last_code: 0,
pid: "",
zoo,
mbx: VecDeque::new(),
reductions: 0,
r#yield: false,
msg_idx: 0,
scrutinee: None,
}
}
pub fn reduce(&mut self) {
self.reductions += 1;
}
pub fn reset_reductions(&mut self) {
self.reductions = 0;
self.r#yield = false;
}
pub fn receive(&mut self, value: Value) {
self.mbx.push_back(value);
}
pub fn chunk(&self) -> &Chunk {
self.frame.chunk()
}
@ -151,12 +163,21 @@ impl Vm {
}
let inner = inner.join("|");
let register = self
.return_register
.register
.iter()
.map(|val| val.to_string())
.collect::<Vec<_>>()
.join(",");
println!("{:04}: [{inner}] ({register})", self.last_code);
let mbx = self
.mbx
.iter()
.map(|val| val.show())
.collect::<Vec<_>>()
.join("/");
println!(
"{:04}: [{inner}] ({register}) {} {{{mbx}}}",
self.last_code, self.pid
);
}
fn print_debug(&self) {
@ -165,35 +186,34 @@ impl Vm {
self.chunk().dissasemble_instr(&mut ip);
}
pub fn run(&mut self) -> &Result<Value, Panic> {
while self.result.is_none() {
self.interpret();
}
self.result.as_ref().unwrap()
fn panic(&mut self, msg: PanicMsg) {
// first prep the current frame for parsing
let mut frame = self.frame.clone();
frame.ip = self.last_code;
// add it to our cloned stack
let mut call_stack = self.call_stack.clone();
call_stack.push(frame);
// console_log!(
// "{}",
// call_stack
// .iter()
// .map(|s| s.to_string())
// .collect::<Vec<_>>()
// .join("\n")
// );
//make a panic
let panic = Panic {
msg,
scrutinee: self.scrutinee.clone(),
call_stack,
};
// and gtfo
self.result = Some(Err(panic));
self.r#yield = true;
}
pub fn call_stack(&mut self) -> String {
let mut stack = format!(" calling {}", self.frame.function.show());
for frame in self.call_stack.iter().rev() {
let mut name = frame.function.show();
name = if name == "fn user script" {
"user script".to_string()
} else {
name
};
stack = format!("{stack}\n from {name}");
}
stack
}
pub fn panic(&mut self, msg: &'static str) {
let msg = format!("{msg}\nPanic traceback:\n{}", self.call_stack());
self.result = Some(Err(Panic::String(msg)));
}
pub fn panic_with(&mut self, msg: String) {
let msg = format!("{msg}\nPanic traceback:\n{}", self.call_stack());
self.result = Some(Err(Panic::String(msg)));
fn panic_with(&mut self, msg: String) {
self.panic(PanicMsg::Generic(msg));
}
fn get_value_at(&mut self, idx: u8) -> Value {
@ -223,10 +243,103 @@ impl Vm {
self.ip >= self.chunk().bytecode.len()
}
fn send_msg(&mut self, pid: Value, msg: Value) {
let Value::Keyword(pid) = pid else {
return self.panic_with(format!("Ludus expected pid keyword, and instead got {pid}"));
};
if self.pid == pid {
self.mbx.push_back(msg.clone());
} else {
self.zoo.as_ref().borrow_mut().send_msg(pid, msg);
}
self.push(Value::Keyword("ok"));
}
fn handle_msg(&mut self, args: Vec<Value>) {
println!("message received by {}: {}", self.pid, args[0]);
let Value::Keyword(msg) = args.first().unwrap() else {
return self.panic_with("malformed message to Process".to_string());
};
match *msg {
"self" => self.push(Value::Keyword(self.pid)),
"send" => self.send_msg(args[1].clone(), args[2].clone()),
"spawn" => {
let f = args[1].clone();
let proc = Creature::spawn(f, self.zoo.clone(), self.debug);
let id = self.zoo.as_ref().borrow_mut().put(proc);
println!("spawning new process {id}!");
self.push(Value::Keyword(id));
}
"yield" => {
self.r#yield = true;
println!("yielding from {}", self.pid);
self.push(Value::Keyword("ok"));
}
"alive" => {
let Value::Keyword(pid) = args[1].clone() else {
unreachable!();
};
let is_alive = self.zoo.as_ref().borrow().is_alive(pid);
if is_alive {
self.push(Value::True)
} else {
self.push(Value::False)
}
}
"link" => todo!(),
"flush" => {
let msgs = self.mbx.iter().cloned().collect::<Vec<_>>();
let msgs = Vector::from(msgs);
println!(
"delivering messages: {}",
msgs.iter()
.map(|x| x.show())
.collect::<Vec<_>>()
.join(" | ")
);
self.mbx = VecDeque::new();
println!("flushing messages in {}", self.pid);
self.push(Value::List(Box::new(msgs)));
}
"sleep" => {
println!("sleeping {} for {}", self.pid, args[1]);
let Value::Number(ms) = args[1] else {
unreachable!()
};
self.zoo.as_ref().borrow_mut().sleep(self.pid, ms);
self.r#yield = true;
self.push(Value::Keyword("ok"));
}
msg => panic!("Process does not understand message: {msg}"),
}
}
pub fn interpret(&mut self) {
println!("starting process {}", self.pid);
println!(
"mbx: {}",
self.mbx
.iter()
.map(|x| x.show())
.collect::<Vec<_>>()
.join(" | ")
);
loop {
if self.at_end() {
self.result = Some(Ok(self.stack.pop().unwrap()));
let result = self.stack.pop().unwrap();
// println!("process {} has returned {result}", self.pid);
self.result = Some(Ok(result));
return;
}
if self.r#yield {
// println!("process {} has explicitly yielded", self.pid);
return;
}
if self.reductions >= MAX_REDUCTIONS {
// println!(
// "process {} is yielding after {MAX_REDUCTIONS} reductions",
// self.pid
// );
return;
}
let code = self.read();
@ -280,7 +393,10 @@ impl Vm {
match cond {
Value::Number(x) if x <= 0.0 => self.ip += jump_len,
Value::Number(..) => (),
_ => return self.panic("repeat requires a number"),
_ => {
return self
.panic_with(format!("repeat requires a number, but got {cond}"))
}
}
}
Pop => {
@ -300,31 +416,31 @@ impl Vm {
let Value::Keyword(name) = key else {
unreachable!("internal Ludus error: expected key for global resolution")
};
let value = self.chunk().env.get(name).unwrap();
let value = self.chunk().env.get(&Key::Keyword(name)).unwrap();
self.push(value.clone());
}
Store => {
self.return_register[0] = self.pop();
self.register[0] = self.pop();
}
StoreN => {
let n = self.read() as usize;
for i in (0..n).rev() {
self.return_register[i] = self.pop();
self.register[i] = self.pop();
}
}
Stash => {
self.return_register[0] = self.peek().clone();
self.register[0] = self.peek().clone();
}
Load => {
let mut value = Value::Nothing;
swap(&mut self.return_register[0], &mut value);
swap(&mut self.register[0], &mut value);
self.push(value);
}
LoadN => {
let n = self.read() as usize;
for i in 0..n {
let mut value = Value::Nothing;
swap(&mut self.return_register[i], &mut value);
swap(&mut self.register[i], &mut value);
self.push(value);
}
}
@ -356,9 +472,19 @@ impl Vm {
let value = self.get_scrutinee();
self.matches = value == Value::False;
}
PanicIfNoMatch => {
PanicNoMatch => {
if !self.matches {
return self.panic("no match");
return self.panic(PanicMsg::NoMatch);
}
}
PanicNoLetMatch => {
if !self.matches {
return self.panic(PanicMsg::NoLetMatch);
}
}
PanicNoFnMatch => {
if !self.matches {
return self.panic(PanicMsg::NoFnMatch);
}
}
MatchConstant => {
@ -434,14 +560,18 @@ impl Vm {
self.push(member.clone());
}
}
_ => return self.panic("internal error: expected tuple"),
_ => {
return self
.panic_with(format!("internal error: expected tuple, got {tuple}"))
}
};
}
LoadSplattedTuple => {
let load_len = self.read() as usize;
let tuple = self.get_scrutinee();
let Value::Tuple(members) = tuple else {
return self.panic("internal error: expected tuple");
return self
.panic_with(format!("internal error: expected tuple, got {tuple}"));
};
for i in 0..load_len - 1 {
self.push(members[i].clone());
@ -458,20 +588,24 @@ impl Vm {
AppendList => {
let value = self.pop();
let list = self.pop();
let Value::List(mut list) = list else {
return self.panic("only lists may be splatted into lists");
let Value::List(mut members) = list else {
return self.panic_with(format!(
"only lists may be splatted into lists, but got {list}"
));
};
list.push_back(value);
self.push(Value::List(list));
members.push_back(value);
self.push(Value::List(members));
}
ConcatList => {
let splatted = self.pop();
let list = self.pop();
let target = self.pop();
let Value::List(mut target) = target else {
unreachable!()
};
let Value::List(splatted) = splatted else {
return self.panic("only lists may be splatted into lists");
let Value::List(splatted) = list else {
return self.panic_with(format!(
"only lists may be splatted into lists, but got {list}"
));
};
target.append(*splatted);
self.push(Value::List(target));
@ -503,14 +637,18 @@ impl Vm {
self.push(member.clone());
}
}
_ => return self.panic("internal error: expected list"),
_ => {
return self
.panic_with(format!("internal error: expected list, got {list}"))
}
};
}
LoadSplattedList => {
let loaded_len = self.read() as usize;
let list = self.get_scrutinee();
let Value::List(members) = list else {
return self.panic("internal error: expected list");
return self
.panic_with(format!("internal error: expected list, got {list}"));
};
for i in 0..loaded_len - 1 {
self.push(members[i].clone());
@ -523,9 +661,7 @@ impl Vm {
}
AppendDict => {
let value = self.pop();
let Value::Keyword(key) = self.pop() else {
unreachable!()
};
let key = Key::from_value(self.pop());
let Value::Dict(mut dict) = self.pop() else {
unreachable!()
};
@ -533,8 +669,11 @@ impl Vm {
self.push(Value::Dict(dict));
}
ConcatDict => {
let Value::Dict(splatted) = self.pop() else {
return self.panic("only dicts may be splatted into dicts");
let prolly_dict = self.pop();
let Value::Dict(splatted) = prolly_dict else {
return self.panic_with(format!(
"only dicts may be splatted into dicts, got {prolly_dict}"
));
};
let Value::Dict(target) = self.pop() else {
unreachable!()
@ -554,9 +693,7 @@ impl Vm {
unreachable!("expected dict, got {value}")
}
};
let Value::Keyword(key) = self.pop() else {
unreachable!("expected keyword, got something else")
};
let key = Key::from_value(self.pop());
let value = dict.get(&key).unwrap_or(&Value::Nil);
self.push(value.clone());
}
@ -579,13 +716,11 @@ impl Vm {
}
}
DropDictEntry => {
let Value::Keyword(key_to_drop) = self.pop() else {
unreachable!()
};
let key_to_drop = Key::from_value(self.pop());
let Value::Dict(mut dict) = self.pop() else {
unreachable!()
};
dict.remove(key_to_drop);
dict.remove(&key_to_drop);
self.push(Value::Dict(dict));
}
PushBox => {
@ -593,13 +728,10 @@ impl Vm {
self.push(Value::Box(Rc::new(RefCell::new(val))));
}
GetKey => {
let key = self.pop();
let Value::Keyword(idx) = key else {
unreachable!()
};
let key = Key::from_value(self.pop());
let dict = self.pop();
let value = match dict {
Value::Dict(d) => d.as_ref().get(&idx).unwrap_or(&Value::Nil).clone(),
Value::Dict(d) => d.get(&key).unwrap_or(&Value::Nil).clone(),
_ => Value::Nil,
};
self.push(value);
@ -627,7 +759,7 @@ impl Vm {
if let Value::Number(x) = val {
self.push(Value::Number(x as usize as f64));
} else {
return self.panic("repeat requires a number");
return self.panic_with(format!("repeat requires a number, but got {val}"));
}
}
Decrement => {
@ -635,7 +767,8 @@ impl Vm {
if let Value::Number(x) = val {
self.push(Value::Number(x - 1.0));
} else {
return self.panic("you may only decrement a number");
return self
.panic_with(format!("you may only decrement a number, but got {val}"));
}
}
Duplicate => {
@ -644,8 +777,10 @@ impl Vm {
MatchDepth => {
self.match_depth = self.read();
}
PanicNoWhen | PanicNoMatch => {
return self.panic("no match");
PanicWhenFallthrough => {
return self.panic_with(
"when form fallthrough: expected one clause to be truthy".to_string(),
);
}
Eq => {
let first = self.pop();
@ -659,40 +794,48 @@ impl Vm {
Add => {
let first = self.pop();
let second = self.pop();
if let (Value::Number(x), Value::Number(y)) = (first, second) {
if let (Value::Number(x), Value::Number(y)) = (first.clone(), second.clone()) {
self.push(Value::Number(x + y))
} else {
return self.panic("`add` requires two numbers");
return self.panic_with(format!(
"`add` requires two numbers, but got {second}, {first}"
));
}
}
Sub => {
let first = self.pop();
let second = self.pop();
if let (Value::Number(x), Value::Number(y)) = (first, second) {
if let (Value::Number(x), Value::Number(y)) = (first.clone(), second.clone()) {
self.push(Value::Number(y - x))
} else {
return self.panic("`sub` requires two numbers");
return self.panic_with(format!(
"`sub` requires two numbers, but got {second}, {first}"
));
}
}
Mult => {
let first = self.pop();
let second = self.pop();
if let (Value::Number(x), Value::Number(y)) = (first, second) {
if let (Value::Number(x), Value::Number(y)) = (first.clone(), second.clone()) {
self.push(Value::Number(x * y))
} else {
return self.panic("`mult` requires two numbers");
return self.panic_with(format!(
"`mult` requires two numbers, but got {second}, {first}"
));
}
}
Div => {
let first = self.pop();
let second = self.pop();
if let (Value::Number(x), Value::Number(y)) = (first, second) {
if let (Value::Number(x), Value::Number(y)) = (first.clone(), second.clone()) {
if x == 0.0 {
return self.panic("division by 0");
return self.panic_with("division by 0".to_string());
}
self.push(Value::Number(y / x))
} else {
return self.panic("`div` requires two numbers");
return self.panic_with(format!(
"`div` requires two numbers, but got {second}, {first}"
));
}
}
Unbox => {
@ -700,7 +843,8 @@ impl Vm {
let inner = if let Value::Box(b) = the_box {
b.borrow().clone()
} else {
return self.panic("`unbox` requires a box");
return self
.panic_with(format!("`unbox` requires a box, but got {the_box}"));
};
self.push(inner);
}
@ -710,32 +854,39 @@ impl Vm {
if let Value::Box(b) = the_box {
b.replace(new_value.clone());
} else {
return self.panic("`store` requires a box");
return self
.panic_with(format!("`store` requires a box, but got {the_box}"));
}
self.push(new_value);
}
Assert => {
let value = self.stack.last().unwrap();
if let Value::Nil | Value::False = value {
return self.panic("asserted falsy value");
return self.panic_with("asserted falsy value".to_string());
}
}
Get => {
let key = self.pop();
if !matches!(
key,
Value::Keyword(_) | Value::String(_) | Value::Interned(_)
) {
return self.panic_with(format!(
"dict keys must be keywords or strings, but got {key}"
));
}
let key = Key::from_value(key);
let dict = self.pop();
let value = match (key, dict) {
(Value::Keyword(k), Value::Dict(d)) => {
d.as_ref().get(&k).unwrap_or(&Value::Nil).clone()
}
(Value::Keyword(_), _) => Value::Nil,
_ => return self.panic("keys must be keywords"),
let value = match dict {
Value::Dict(d) => d.get(&key).unwrap_or(&Value::Nil).clone(),
_ => Value::Nil.clone(),
};
self.push(value);
}
At => {
let idx = self.pop();
let ordered = self.pop();
let value = match (ordered, idx) {
let value = match (ordered, idx.clone()) {
(Value::List(l), Value::Number(i)) => {
l.get(i as usize).unwrap_or(&Value::Nil).clone()
}
@ -743,7 +894,10 @@ impl Vm {
t.get(i as usize).unwrap_or(&Value::Nil).clone()
}
(_, Value::Number(_)) => Value::Nil,
_ => return self.panic("indexes must be numbers"),
_ => {
return self
.panic_with(format!("indexes must be numbers, but got {idx}"))
}
};
self.push(value);
}
@ -786,7 +940,9 @@ impl Vm {
let arity = self.read();
let the_fn = self.pop();
let Value::Fn(ref inner) = the_fn else {
return self.panic("only functions may be partially applied");
return self.panic_with(format!(
"only functions may be partially applied, but got {the_fn}"
));
};
let args = self.stack.split_off(self.stack.len() - arity as usize);
let partial = crate::value::Partial {
@ -797,6 +953,7 @@ impl Vm {
self.push(Value::Partial(Rc::new(partial)));
}
TailCall => {
self.reduce();
let arity = self.read();
let called = self.pop();
@ -809,6 +966,10 @@ impl Vm {
}
match called {
Value::Process => {
let args = self.stack.split_off(self.stack.len() - arity as usize);
self.handle_msg(args);
}
Value::Fn(_) => {
if !called.as_fn().accepts(arity) {
return self.panic_with(format!(
@ -818,17 +979,23 @@ impl Vm {
}
// first put the arguments in the register
for i in 0..arity as usize {
self.return_register[arity as usize - i - 1] = self.pop();
self.register[arity as usize - i - 1] = self.pop();
}
// self.print_stack();
// save the arguments as our scrutinee
let mut scrutinee = vec![];
for i in 0..arity as usize {
scrutinee.push(self.register[i].clone())
}
self.scrutinee = Some(Value::tuple(scrutinee));
// then pop everything back to the current stack frame
self.stack.truncate(self.frame.stack_base);
// then push the arguments back on the stack
let mut i = 0;
while i < 8 && self.return_register[i] != Value::Nothing {
while i < 8 && self.register[i] != Value::Nothing {
let mut value = Value::Nothing;
swap(&mut self.return_register[i], &mut value);
swap(&mut self.register[i], &mut value);
self.push(value);
i += 1;
}
@ -860,7 +1027,7 @@ impl Vm {
self.ip = 0;
}
Value::BaseFn(base_fn) => {
let value = match (arity, base_fn) {
let value = match (arity, *base_fn) {
(0, BaseFn::Nullary(_, f)) => f(),
(1, BaseFn::Unary(_, f)) => f(&self.pop()),
(2, BaseFn::Binary(_, f)) => {
@ -874,20 +1041,13 @@ impl Vm {
let x = &self.pop();
f(x, y, z)
}
_ => return self.panic("internal ludus error"),
_ => {
return self.panic_with(
"internal ludus error: bad base fn call".to_string(),
)
}
};
// // algo:
// // clear the stack
// self.stack.truncate(self.frame.stack_base);
// // then pop back out to the enclosing stack frame
// self.frame = self.call_stack.pop().unwrap();
// self.ip = self.frame.ip;
// // finally, throw the value on the stack
self.push(value);
// println!(
// "=== returning to {} ===",
// self.frame.function.as_fn().name()
// );
}
Value::Partial(partial) => {
let last_arg = self.pop();
@ -919,6 +1079,7 @@ impl Vm {
}
}
Call => {
self.reduce();
let arity = self.read();
let called = self.pop();
@ -928,6 +1089,10 @@ impl Vm {
}
match called {
Value::Process => {
let args = self.stack.split_off(self.stack.len() - arity as usize);
self.handle_msg(args);
}
Value::Fn(_) => {
if !called.as_fn().accepts(arity) {
return self.panic_with(format!(
@ -935,6 +1100,7 @@ impl Vm {
called.show()
));
}
let splat_arity = called.as_fn().splat_arity();
if splat_arity > 0 && arity >= splat_arity {
let splatted_args = self.stack.split_off(
@ -943,11 +1109,22 @@ impl Vm {
let gathered_args = Vector::from(splatted_args);
self.push(Value::List(Box::new(gathered_args)));
}
let mut scrutinee = vec![];
for i in 0..arity {
scrutinee.push(
self.stack[self.stack.len() - arity as usize + i as usize]
.clone(),
)
}
self.scrutinee = Some(Value::tuple(scrutinee));
let arity = if splat_arity > 0 {
splat_arity.min(arity)
} else {
arity
};
let mut frame = CallFrame {
function: called,
arity,
@ -962,7 +1139,7 @@ impl Vm {
self.ip = 0;
}
Value::BaseFn(base_fn) => {
let value = match (arity, base_fn) {
let value = match (arity, *base_fn) {
(0, BaseFn::Nullary(_, f)) => f(),
(1, BaseFn::Unary(_, f)) => f(&self.pop()),
(2, BaseFn::Binary(_, f)) => {
@ -976,7 +1153,11 @@ impl Vm {
let x = &self.pop();
f(x, y, z)
}
_ => return self.panic("internal ludus error"),
_ => {
return self.panic_with(
"internal ludus error: bad base fn call".to_string(),
)
}
};
self.push(value);
}
@ -1013,11 +1194,20 @@ impl Vm {
if self.debug {
println!("== returning from {} ==", self.frame.function.show())
}
self.frame = self.call_stack.pop().unwrap();
self.ip = self.frame.ip;
let mut value = Value::Nothing;
swap(&mut self.return_register[0], &mut value);
self.push(value);
swap(&mut self.register[0], &mut value);
match self.call_stack.pop() {
Some(frame) => {
self.ip = frame.ip;
self.frame = frame;
self.push(value);
}
None => {
println!("process {} has returned with {}", self.pid, value);
self.result = Some(Ok(value));
return;
}
}
}
Print => {
println!("{}", self.pop().show());
@ -1038,6 +1228,38 @@ impl Vm {
unreachable!();
}
}
NextMessage => {
self.msg_idx += 1;
}
LoadMessage => match self.mbx.get(self.msg_idx) {
Some(msg) => self.push(msg.clone()),
None => {
self.msg_idx = 0;
self.r#yield = true;
self.ip -= 2;
}
},
MatchMessage => {
self.mbx.remove(self.msg_idx).unwrap();
}
ClearMessage => {
self.msg_idx = 0;
}
SendMethod => {
let Value::Tuple(args) = self.pop() else {
unreachable!("method args should be a tuple");
};
let method = self.pop();
let target = self.pop();
let mut msg = vec![method];
for arg in args.as_ref() {
msg.push(arg.clone());
}
self.send_msg(target, Value::tuple(msg));
}
LoadScrutinee => {
self.scrutinee = Some(self.peek().clone());
}
}
}
}

485
src/world.rs Normal file
View File

@ -0,0 +1,485 @@
use crate::chunk::Chunk;
use crate::value::{Value, Key};
use crate::vm::Creature;
use crate::panic::Panic;
use crate::errors::panic;
use crate::js::{random, now};
use crate::io::{MsgOut, MsgIn, do_io};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::mem::swap;
use std::rc::Rc;
const ANIMALS: [&str; 32] = [
"tortoise",
"hare",
"squirrel",
"hawk",
"woodpecker",
"cardinal",
"coyote",
"raccoon",
"rat",
"axolotl",
"cormorant",
"duck",
"orca",
"humbpack",
"tern",
"quokka",
"koala",
"kangaroo",
"zebra",
"hyena",
"giraffe",
"hippopotamus",
"capybara",
"python",
"gopher",
"crab",
"trout",
"osprey",
"lemur",
"wobbegong",
"walrus",
"opossum",
];
#[derive(Debug, Clone, PartialEq)]
enum Status {
Empty,
Borrowed,
Nested(Creature),
}
impl std::fmt::Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Status::Empty => write!(f, "empty"),
Status::Borrowed => write!(f, "borrowed"),
Status::Nested(creature) => write!(f, "nested {creature}"),
}
}
}
impl Status {
pub fn receive(&mut self, msg: Value) {
match self {
Status::Nested(creature) => creature.receive(msg),
Status::Borrowed => println!("sending a message to a borrowed process"),
Status::Empty => println!("sending a message to a dead process"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Zoo {
procs: Vec<Status>,
empty: Vec<usize>,
ids: HashMap<&'static str, usize>,
dead: HashSet<&'static str>,
kill_list: Vec<&'static str>,
sleeping: HashMap<&'static str, f64>,
active_idx: usize,
active_id: &'static str,
}
impl Zoo {
pub fn new() -> Zoo {
Zoo {
procs: vec![],
empty: vec![],
ids: HashMap::new(),
kill_list: vec![],
dead: HashSet::new(),
sleeping: HashMap::new(),
active_idx: 0,
active_id: "",
}
}
fn random_id(&self) -> String {
let rand_idx = (random() * 32.0) as usize;
let idx = self.procs.len();
format!("{}_{idx}", ANIMALS[rand_idx])
}
fn new_id(&self) -> &'static str {
let mut new = self.random_id();
while self.dead.iter().any(|old| *old == new) {
new = self.random_id();
}
new.leak()
}
pub fn put(&mut self, mut proc: Creature) -> &'static str {
if self.empty.is_empty() {
let id = self.new_id();
let idx = self.procs.len();
proc.pid = id;
self.procs.push(Status::Nested(proc));
self.ids.insert(id, idx);
id
} else {
let idx = self.empty.pop().unwrap();
let rand = (random() * 32.0) as usize;
let id = format!("{}_{idx}", ANIMALS[rand]).leak();
proc.pid = id;
self.ids.insert(id, idx);
self.procs[idx] = Status::Nested(proc);
id
}
}
pub fn kill(&mut self, id: &'static str) {
self.kill_list.push(id);
}
pub fn sleep(&mut self, id: &'static str, ms: f64) {
self.sleeping
.insert(id, now() + ms);
}
pub fn is_alive(&self, id: &'static str) -> bool {
if self.kill_list.contains(&id) {
return false;
}
let idx = self.ids.get(id);
match idx {
Some(idx) => match self.procs.get(*idx) {
Some(proc) => match proc {
Status::Empty => false,
Status::Borrowed => true,
Status::Nested(_) => true,
},
None => false,
},
None => false,
}
}
pub fn clean_up(&mut self) {
while let Some(id) = self.kill_list.pop() {
if let Some(idx) = self.ids.get(id) {
println!("buried process {id}");
self.procs[*idx] = Status::Empty;
self.empty.push(*idx);
self.ids.remove(id);
self.dead.insert(id);
}
}
self.sleeping
.retain(|_, wakeup_time| now() < *wakeup_time);
println!(
"currently sleeping processes: {}",
self.sleeping
.keys()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(" | ")
);
}
pub fn catch(&mut self, id: &'static str) -> Creature {
if let Some(idx) = self.ids.get(id) {
let mut proc = Status::Borrowed;
swap(&mut proc, &mut self.procs[*idx]);
let Status::Nested(proc) = proc else {
unreachable!("tried to borrow an empty or already-borrowed process {id}");
};
proc
} else {
unreachable!("tried to borrow a non-existent process {id}");
}
}
pub fn release(&mut self, proc: Creature) {
let id = proc.pid;
if let Some(idx) = self.ids.get(id) {
let mut proc = Status::Nested(proc);
swap(&mut proc, &mut self.procs[*idx]);
}
}
pub fn is_available(&self) -> bool {
match &self.procs[self.active_idx] {
Status::Empty => false,
Status::Borrowed => false,
Status::Nested(proc) => !self.sleeping.contains_key(proc.pid),
}
}
pub fn next(&mut self) -> &'static str {
self.clean_up();
let starting_idx = self.active_idx;
self.active_idx = (self.active_idx + 1) % self.procs.len();
while !self.is_available() {
// we've gone round the process queue already
// that means no process is active
// but we may have processes that are alive and asleep
// if nothing is active, yield back to the world's event loop
if self.active_idx == starting_idx {
return ""
}
self.active_idx = (self.active_idx + 1) % self.procs.len();
}
match &self.procs[self.active_idx] {
Status::Empty | Status::Borrowed => unreachable!(),
Status::Nested(proc) => proc.pid,
}
}
pub fn send_msg(&mut self, id: &'static str, msg: Value) {
let Some(idx) = self.ids.get(id) else {
return;
};
self.procs[*idx].receive(msg);
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Buffers {
console: Value,
commands: Value,
fetch_out: Value,
fetch_in: Value,
input: Value,
}
impl Buffers {
pub fn new (prelude: imbl::HashMap<Key, Value>) -> Buffers {
Buffers {
console: prelude.get(&Key::Keyword("console")).unwrap().clone(),
commands: prelude.get(&Key::Keyword("turtle_commands")).unwrap().clone(),
fetch_out: prelude.get(&Key::Keyword("fetch_outbox")).unwrap().clone(),
fetch_in: prelude.get(&Key::Keyword("fetch_inbox")).unwrap().clone(),
input: prelude.get(&Key::Keyword("input")).unwrap().clone(),
}
}
pub fn console (&self) -> Rc<RefCell<Value>> {
self.console.as_box()
}
pub fn input (&self) -> Rc<RefCell<Value>> {
self.input.as_box()
}
pub fn commands (&self) -> Rc<RefCell<Value>> {
self.commands.as_box()
}
pub fn fetch_out (&self) -> Rc<RefCell<Value>> {
self.fetch_out.as_box()
}
pub fn fetch_in (&self) -> Rc<RefCell<Value>> {
self.fetch_in.as_box()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct World {
zoo: Rc<RefCell<Zoo>>,
active: Option<Creature>,
main: &'static str,
pub result: Option<Result<Value, Panic>>,
buffers: Buffers,
last_io: f64,
kill_signal: bool,
}
impl World {
pub fn new(chunk: Chunk, prelude: imbl::HashMap<Key, Value>, debug: bool) -> World {
let zoo = Rc::new(RefCell::new(Zoo::new()));
let main = Creature::new(chunk, zoo.clone(), debug);
let id = zoo.borrow_mut().put(main);
let buffers = Buffers::new(prelude);
World {
zoo,
active: None,
main: id,
result: None,
buffers,
last_io: 0.0,
kill_signal: false,
}
}
fn next(&mut self) {
let mut active = None;
swap(&mut active, &mut self.active);
let mut zoo = self.zoo.borrow_mut();
if let Some(active) = active {
zoo.release(active);
}
let new_active_id = zoo.next();
if new_active_id.is_empty() {
self.active = None;
return;
}
let mut new_active_proc = zoo.catch(new_active_id);
new_active_proc.reset_reductions();
let mut new_active_opt = Some(new_active_proc);
swap(&mut new_active_opt, &mut self.active);
}
fn activate_main(&mut self) {
let main = self.zoo.borrow_mut().catch(self.main);
self.active = Some(main);
}
fn active_id(&mut self) -> Option<&'static str> {
match &self.active {
Some(creature) => Some(creature.pid),
None => None,
}
}
fn kill_active(&mut self) {
if let Some(pid) = self.active_id() {
self.zoo.borrow_mut().kill(pid);
}
}
fn active_result(&mut self) -> &Option<Result<Value, Panic>> {
if self.active.is_none() { return &None; }
&self.active.as_ref().unwrap().result
}
fn flush_buffers(&mut self) -> Vec<MsgOut> {
let mut outbox = vec![];
if let Some(console) = self.flush_console() {
outbox.push(console);
}
if let Some(commands) = self.flush_commands() {
outbox.push(commands);
}
if let Some(fetch) = self.make_fetch_happen() {
outbox.push(fetch);
}
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> {
let console = self.buffers.console();
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() {
None
} else {
Some(MsgOut::Console(working_value.clone()))
}
}
fn flush_commands(&self) -> Option<MsgOut> {
let commands = self.buffers.commands();
let working_copy = RefCell::new(Value::new_list());
commands.swap(&working_copy);
let commands = working_copy.borrow();
if commands.as_list().is_empty() {
None
} else {
Some(MsgOut::Commands(commands.clone()))
}
}
fn complete_main(&mut self) -> Vec<MsgOut> {
let mut outbox = self.flush_buffers();
// TODO: if we have a panic, actually add the panic message to the console
let result = self.active_result().clone().unwrap();
self.result = Some(result.clone());
let result_msg = match result {
Ok(value) => MsgOut::Complete(Value::string(value.show())),
Err(p) => MsgOut::Error(panic(p))
};
outbox.push(result_msg);
outbox
}
fn interpret_active(&mut self) {
self.active.as_mut().unwrap().interpret();
}
async fn maybe_do_io(&mut self) {
if self.last_io + 10.0 < now() {
let outbox = self.flush_buffers();
let inbox = do_io(outbox).await;
self.fill_buffers(inbox);
self.last_io = now();
}
}
fn fill_input(&mut self, str: String) {
let value = Value::string(str);
let working = RefCell::new(value);
let input = self.buffers.input();
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>) {
for msg in inbox {
match msg {
MsgIn::Input(str) => self.fill_input(str),
MsgIn::Kill => self.kill_signal = true,
MsgIn::Fetch(..) => self.fetch_reply(msg.into_value()),
_ => todo!()
}
}
}
async fn ready_io(&mut self) {
let inbox = do_io(vec![MsgOut::Ready]).await;
self.fill_buffers(inbox);
self.last_io = now();
}
pub async fn run(&mut self) {
self.activate_main();
self.ready_io().await;
loop {
self.maybe_do_io().await;
if self.kill_signal {
let mut outbox = self.flush_buffers();
outbox.push(MsgOut::Error("Ludus killed by user".to_string()));
do_io(outbox).await;
return;
}
if self.active.is_some() {
self.interpret_active();
}
if self.active_result().is_some() {
if self.active_id().unwrap() == self.main {
let outbox = self.complete_main();
do_io(outbox).await;
return;
}
self.kill_active();
}
self.next();
}
}
}