Compare commits
74 Commits
init_relea
...
release
Author | SHA1 | Date | |
---|---|---|---|
|
294d7d6be2 | ||
|
2808c0b709 | ||
|
55483d54a2 | ||
|
3b8d3ff5e3 | ||
|
9228e060bb | ||
|
050a0f987d | ||
|
0d8b42662b | ||
|
f97f6670bd | ||
|
d6a004d9ac | ||
|
c6709bb2e8 | ||
|
9f9f59b33b | ||
|
659fdd3506 | ||
|
d334e483a5 | ||
|
2ffff9edd9 | ||
|
28d6dc24f0 | ||
|
0cd682de21 | ||
|
12389ae371 | ||
|
bf204696a5 | ||
|
6bdb9779d8 | ||
|
2f4ab41a62 | ||
|
1316c8228f | ||
|
dcf550ba2f | ||
|
df5c745ce9 | ||
|
1435e753e8 | ||
|
f6ad3b6966 | ||
|
5a778d9a55 | ||
|
62ad321a88 | ||
|
14a41dc1bd | ||
|
d9b095c3f3 | ||
|
44739adfe5 | ||
|
624c0bd2f8 | ||
|
1158821aff | ||
|
cfe8009861 | ||
|
33b7f78038 | ||
|
116a5b2ed9 | ||
|
9414dc64d9 | ||
|
197cbfc795 | ||
|
f3801b3c37 | ||
|
f8983d24a4 | ||
|
e5467e9e7e | ||
|
bba3e1e800 | ||
|
b7ff0eda80 | ||
|
5b2fd5e2d7 | ||
|
b12d0e00aa | ||
|
808368d2b9 | ||
|
88ff5886bb | ||
|
1ec60b9362 | ||
|
400bd5864b | ||
|
991705e734 | ||
|
989e217917 | ||
|
4e7557cbcc | ||
|
2f3f362f49 | ||
|
4eceb62ce5 | ||
|
173fdb913c | ||
|
bc49ece0cf | ||
|
5478e5e40e | ||
|
f6cbe3f800 | ||
|
c62b5c903d | ||
|
de6cb5380d | ||
|
4dd47dd56c | ||
|
f710beff46 | ||
|
f873be7668 | ||
|
48342ba4ea | ||
|
db52bc2687 | ||
|
a175ee7a41 | ||
|
759fc63cae | ||
|
8923581eed | ||
|
90505f89fe | ||
|
00ebac17ce | ||
|
888f5b62da | ||
|
c144702b98 | ||
|
801e5bcc01 | ||
|
b35657e698 | ||
|
b5528ced8f |
17
Cargo.toml
17
Cargo.toml
|
@ -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
31
assets/agent.ld
Normal 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!
|
|
@ -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 (xs, 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
40
justfile
Normal 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
|
||||
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
# rudus
|
||||
|
||||
A Rust implementation of Ludus.
|
|
@ -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>
|
||||
|
|
163
pkg/ludus.js
163
pkg/ludus.js
|
@ -1,32 +1,161 @@
|
|||
import init, {ludus} from "./rudus.js";
|
||||
if (window) window.ludus = {run, kill, flush_stdout, stdout, p5, svg, flush_commands, commands, result, input, is_running}
|
||||
|
||||
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
|
||||
|
||||
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");
|
||||
console.log("Main: ludus says => ", new_lines)
|
||||
ludus_console = ludus_console + 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) {
|
||||
if (running || ready) {
|
||||
return "TODO: handle this? should not be running"
|
||||
}
|
||||
running = true
|
||||
ready = false
|
||||
result = null
|
||||
code = source
|
||||
const output = ludus(source)
|
||||
res = JSON.parse(output)
|
||||
return res
|
||||
worker.postMessage([{verb: "Run", data: source}])
|
||||
outbox = []
|
||||
start_io_polling()
|
||||
}
|
||||
|
||||
// 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 = ""
|
||||
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 = []
|
||||
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
|
||||
}
|
||||
|
||||
//////////// turtle plumbing below
|
||||
// TODO: refactor this out into modules
|
||||
const turtle_init = {
|
||||
position: [0, 0],
|
||||
heading: 0,
|
||||
|
@ -81,8 +210,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 +429,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 +506,4 @@ export function p5 (commands) {
|
|||
return p5_calls
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
13
pkg/rudus.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
244
pkg/rudus.js
244
pkg/rudus.js
|
@ -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.
11
pkg/rudus_bg.wasm.d.ts
vendored
11
pkg/rudus_bg.wasm.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import * as mod from "./ludus.js";
|
||||
|
||||
console.log(mod.run(`
|
||||
:foobar
|
||||
`));
|
55
pkg/worker.js
Normal file
55
pkg/worker.js
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
36
sandbox.ld
36
sandbox.ld
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
959
sandbox_run.txt
959
sandbox_run.txt
File diff suppressed because it is too large
Load Diff
470
src/ast.rs
Normal file
470
src/ast.rs
Normal 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("")
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
220
src/base.rs
220
src/base.rs
|
@ -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)))
|
||||
}
|
||||
|
|
50
src/chunk.rs
50
src/chunk.rs
|
@ -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)
|
||||
// }
|
||||
}
|
||||
|
|
148
src/compiler.rs
148
src/compiler.rs
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
187
src/errors.rs
187
src/errors.rs
|
@ -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
100
src/io.rs
Normal 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
19
src/js.rs
Normal 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;
|
84
src/lexer.rs
84
src/lexer.rs
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
160
src/lib.rs
160
src/lib.rs
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
|
|
68
src/op.rs
68
src/op.rs
|
@ -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
47
src/panic.rs
Normal 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)
|
||||
}
|
||||
}
|
605
src/parser.rs
605
src/parser.rs
|
@ -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"),
|
||||
|
|
|
@ -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 => (),
|
||||
|
|
295
src/value.rs
295
src/value.rs
|
@ -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
548
src/vm.rs
|
@ -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
485
src/world.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user