maybe get git right? ugh
This commit is contained in:
commit
0c17b64fd7
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
/target
|
/target
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
|
/node_modules
|
||||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -5,16 +5,20 @@ edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ariadne = { git = "https://github.com/zesterer/ariadne" }
|
ariadne = { git = "https://github.com/zesterer/ariadne" }
|
||||||
chumsky = { git = "https://github.com/zesterer/chumsky", features = ["label"] }
|
chumsky = { git = "https://github.com/zesterer/chumsky", features = ["label"] }
|
||||||
imbl = "3.0.0"
|
imbl = "3.0.0"
|
||||||
struct_scalpel = "0.1.1"
|
|
||||||
ran = "2.0.1"
|
ran = "2.0.1"
|
||||||
rust-embed = "8.5.0"
|
|
||||||
boxing = "0.1.2"
|
|
||||||
ordered-float = "4.5.0"
|
|
||||||
index_vec = "0.1.4"
|
|
||||||
num-derive = "0.4.2"
|
num-derive = "0.4.2"
|
||||||
num-traits = "0.2.19"
|
num-traits = "0.2.19"
|
||||||
regex = "1.11.1"
|
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"
|
||||||
|
|
|
@ -267,35 +267,33 @@ fn contains? {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print! {
|
&&& boxes: mutable state and state changes
|
||||||
"Sends a text representation of Ludus values to the console."
|
fn box? {
|
||||||
(...args) -> {
|
"Returns true if a value is a box."
|
||||||
base :print! (args)
|
(b as :box) -> true
|
||||||
:ok
|
(_) -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unbox {
|
||||||
|
"Returns the value that is stored in a box."
|
||||||
|
(b as :box) -> base :unbox (b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store! {
|
||||||
|
"Stores a value in a box, replacing the value that was previously there. Returns the value."
|
||||||
|
(b as :box, value) -> {
|
||||||
|
base :store! (b, value)
|
||||||
|
value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show {
|
fn update! {
|
||||||
"Returns a text representation of a Ludus value as a string."
|
"Updates a box by applying a function to its value. Returns the new value."
|
||||||
(x) -> base :show (x)
|
(b as :box, f as :fn) -> {
|
||||||
}
|
let current = unbox (b)
|
||||||
|
let new = f (current)
|
||||||
fn report! {
|
store! (b, new)
|
||||||
"Prints a value, then returns it."
|
|
||||||
(x) -> {
|
|
||||||
print! (x)
|
|
||||||
x
|
|
||||||
}
|
}
|
||||||
(msg as :string, x) -> {
|
|
||||||
print! (concat ("{msg} ", show (x)))
|
|
||||||
x
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn doc! {
|
|
||||||
"Prints the documentation of a function to the console."
|
|
||||||
(f as :fn) -> do f > base :doc! > print!
|
|
||||||
(_) -> :none
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&&& strings: harder than they look!
|
&&& strings: harder than they look!
|
||||||
|
@ -305,6 +303,11 @@ fn string? {
|
||||||
(_) -> false
|
(_) -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn show {
|
||||||
|
"Returns a text representation of a Ludus value as a string."
|
||||||
|
(x) -> base :show (x)
|
||||||
|
}
|
||||||
|
|
||||||
fn string {
|
fn string {
|
||||||
"Converts a value to a string by using `show`. If it is a string, returns it unharmed. Use this to build up strings of different kinds of values."
|
"Converts a value to a string by using `show`. If it is a string, returns it unharmed. Use this to build up strings of different kinds of values."
|
||||||
(x as :string) -> x
|
(x as :string) -> x
|
||||||
|
@ -405,34 +408,33 @@ fn to_number {
|
||||||
(num as :string) -> base :number (num)
|
(num as :string) -> base :number (num)
|
||||||
}
|
}
|
||||||
|
|
||||||
&&& boxes: mutable state and state changes
|
box console = []
|
||||||
|
|
||||||
fn box? {
|
fn print! {
|
||||||
"Returns true if a value is a box."
|
"Sends a text representation of Ludus values to the console."
|
||||||
(b as :box) -> true
|
(...args) -> {
|
||||||
(_) -> false
|
let line = do args > map (string, _) > join (_, " ")
|
||||||
}
|
update! (console, append (_, line))
|
||||||
|
:ok
|
||||||
fn unbox {
|
|
||||||
"Returns the value that is stored in a box."
|
|
||||||
(b as :box) -> base :unbox (b)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn store! {
|
|
||||||
"Stores a value in a box, replacing the value that was previously there. Returns the value."
|
|
||||||
(b as :box, value) -> {
|
|
||||||
base :store! (b, value)
|
|
||||||
value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update! {
|
fn report! {
|
||||||
"Updates a box by applying a function to its value. Returns the new value."
|
"Prints a value, then returns it."
|
||||||
(b as :box, f as :fn) -> {
|
(x) -> {
|
||||||
let current = unbox (b)
|
print! (x)
|
||||||
let new = f (current)
|
x
|
||||||
store! (b, new)
|
|
||||||
}
|
}
|
||||||
|
(msg as :string, x) -> {
|
||||||
|
print! (concat ("{msg} ", show (x)))
|
||||||
|
x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn doc! {
|
||||||
|
"Prints the documentation of a function to the console."
|
||||||
|
(f as :fn) -> do f > base :doc! > print!
|
||||||
|
(_) -> :none
|
||||||
}
|
}
|
||||||
|
|
||||||
&&& numbers, basically: arithmetic and not much else, yet
|
&&& numbers, basically: arithmetic and not much else, yet
|
||||||
|
@ -1210,9 +1212,6 @@ fn penwidth {
|
||||||
box state = nil
|
box state = nil
|
||||||
|
|
||||||
#{
|
#{
|
||||||
apply_command
|
|
||||||
add_command!
|
|
||||||
|
|
||||||
abs
|
abs
|
||||||
abs
|
abs
|
||||||
add
|
add
|
||||||
|
@ -1240,6 +1239,7 @@ box state = nil
|
||||||
coll?
|
coll?
|
||||||
colors
|
colors
|
||||||
concat
|
concat
|
||||||
|
console
|
||||||
contains?
|
contains?
|
||||||
cos
|
cos
|
||||||
count
|
count
|
||||||
|
|
|
@ -522,6 +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 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.
|
NEW SOLUTION: create a function that does the offset properly, and replace everywhere we directly access the stack.
|
||||||
|
<<<<<<< HEAD
|
||||||
<<<<<<< Updated upstream
|
<<<<<<< Updated upstream
|
||||||
This is the thing I am about to do
|
This is the thing I am about to do
|
||||||
||||||| Stash base
|
||||||| Stash base
|
||||||
|
@ -631,10 +632,6 @@ for i in 0..idx {
|
||||||
println!("line {line_no}: {}", lines[line_no - 1]);
|
println!("line {line_no}: {}", lines[line_no - 1]);
|
||||||
```
|
```
|
||||||
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
|
|
||||||
=======
|
|
||||||
This is the thing I am about to do.
|
This is the thing I am about to do.
|
||||||
|
|
||||||
### I think the interpreter, uh, works?
|
### I think the interpreter, uh, works?
|
||||||
|
@ -758,42 +755,23 @@ println!("line {line_no}: {}", lines[line_no - 1]);
|
||||||
* Users can create their own (public) repos and put stuff in there.
|
* Users can create their own (public) repos and put stuff in there.
|
||||||
* We still want saving text output from web Ludus
|
* We still want saving text output from web Ludus
|
||||||
* Later, with perceptrons & the book, we'll need additional solutions.
|
* Later, with perceptrons & the book, we'll need additional solutions.
|
||||||
>>>>>>> Stashed changes
|
|
||||||
||||||| Stash base
|
|
||||||
|
|
||||||
=======
|
#### Integration hell
|
||||||
### Integration meeting with mnl
|
As predicted, Javascript is the devil.
|
||||||
#### 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)?
|
|
||||||
* 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)
|
|
||||||
- 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.
|
|
||||||
* Users can create their own (public) repos and put stuff in there.
|
|
||||||
* We still want saving text output from web Ludus
|
|
||||||
* Later, with perceptrons & the book, we'll need additional solutions.
|
|
||||||
|
|
||||||
### Integration meeting with mnl
|
wasm-pack has several targets:
|
||||||
#### 2025-06-25
|
* nodejs -> this should be what we want
|
||||||
* Web workers
|
* web -> this could be what we want
|
||||||
* My javascript wrapper needs to execute WASM in its own thread (ugh)
|
* bundler -> webpack confuses me
|
||||||
- [ ] is this a thing that can be done easily in a platform-independent way (node vs. bun vs. browser)?
|
|
||||||
* Top priorities:
|
The simplest, shortest route should be to create a viable nodejs library.
|
||||||
- [ ] Get a node package out
|
It works.
|
||||||
- [ ] Stand up actors + threads, etc.
|
I can wire up the wasm-pack output with a package.json, pull it down from npm, and it work.
|
||||||
- [ ] How to model keyboard input from p5?
|
However, because of course, vite, which svelte uses, doesn't like this.
|
||||||
* [ ] Model after the p5 keyboard input API
|
We get an error that `TextEncoder is not a constructor`.
|
||||||
* [ ] ludus keyboard API: `key_is_down(), key_pressed(), key_released()`, key code values (use a dict)
|
This, apparently, has something to do with the way that vite packages up node libraries?
|
||||||
- Assets:
|
See https://github.com/vitejs/vite/discussions/12826.
|
||||||
* 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.
|
Web, in some ways, is even more straightforward.
|
||||||
* Users can create their own (public) repos and put stuff in there.
|
It produces an ESM that just works in the browser.
|
||||||
* We still want saving text output from web Ludus
|
And
|
||||||
* Later, with perceptrons & the book, we'll need additional solutions.
|
|
||||||
|
|
1594
package-lock.json
generated
Normal file
1594
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "@ludus/rudus",
|
||||||
|
"version": "0.1.3",
|
||||||
|
"description": "A Rust-based Ludus bytecode interpreter.",
|
||||||
|
"type": "module",
|
||||||
|
"main": "pkg/ludus.js",
|
||||||
|
"directories": {},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Scott Richmond",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"files": [
|
||||||
|
"pkg/rudus.js",
|
||||||
|
"pkg/ludus.js",
|
||||||
|
"pkg/rudus_bg.wasm",
|
||||||
|
"pkg/rudus_bg.wasm.d.ts",
|
||||||
|
"pkg/rudus.d.ts"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
}
|
||||||
|
}
|
3
pkg/README.md
Normal file
3
pkg/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# rudus
|
||||||
|
|
||||||
|
A Rust implementation of Ludus.
|
21
pkg/index.html
Normal file
21
pkg/index.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
|
||||||
|
<title>Testing Ludus/WASM integration</title>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module">
|
||||||
|
import {run} from "./ludus.js";
|
||||||
|
|
||||||
|
window.ludus = run;
|
||||||
|
|
||||||
|
console.log(run(":foobar"));
|
||||||
|
</script>
|
||||||
|
<p>
|
||||||
|
Open the console. All the action's in there.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
378
pkg/ludus.js
Normal file
378
pkg/ludus.js
Normal file
|
@ -0,0 +1,378 @@
|
||||||
|
import init, {ludus} from "./rudus.js";
|
||||||
|
|
||||||
|
await init();
|
||||||
|
|
||||||
|
let res = null
|
||||||
|
|
||||||
|
let code = null
|
||||||
|
|
||||||
|
export function run (source) {
|
||||||
|
code = source
|
||||||
|
const output = ludus(source)
|
||||||
|
res = JSON.parse(output)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stdout () {
|
||||||
|
if (!res) return ""
|
||||||
|
return res.io.stdout.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export function turtle_commands () {
|
||||||
|
if (!res) return []
|
||||||
|
return res.io.turtle.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export function result () {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
const turtle_init = {
|
||||||
|
position: [0, 0],
|
||||||
|
heading: 0,
|
||||||
|
pendown: true,
|
||||||
|
pencolor: "white",
|
||||||
|
penwidth: 1,
|
||||||
|
visible: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
black: [0, 0, 0, 255],
|
||||||
|
silver: [192, 192, 192, 255],
|
||||||
|
gray: [128, 128, 128, 255],
|
||||||
|
white: [255, 255, 255, 255],
|
||||||
|
maroon: [128, 0, 0, 255],
|
||||||
|
red: [255, 0, 0, 255],
|
||||||
|
purple: [128, 0, 128, 255],
|
||||||
|
fuchsia: [255, 0, 255, 255],
|
||||||
|
green: [0, 128, 0, 255],
|
||||||
|
lime: [0, 255, 0, 255],
|
||||||
|
olive: [128, 128, 0, 255],
|
||||||
|
yellow: [255, 255, 0, 255],
|
||||||
|
navy: [0, 0, 128, 255],
|
||||||
|
blue: [0, 0, 255, 255],
|
||||||
|
teal: [0, 128, 128, 255],
|
||||||
|
aqua: [0, 255, 255, 255],
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolve_color (color) {
|
||||||
|
if (typeof color === 'string') return colors[color]
|
||||||
|
if (typeof color === 'number') return [color, color, color, 255]
|
||||||
|
if (Array.isArray(color)) return color
|
||||||
|
return [0, 0, 0, 255] // default to black?
|
||||||
|
}
|
||||||
|
|
||||||
|
let background_color = "black"
|
||||||
|
|
||||||
|
function add (v1, v2) {
|
||||||
|
const [x1, y1] = v1
|
||||||
|
const [x2, y2] = v2
|
||||||
|
return [x1 + x2, y1 + y2]
|
||||||
|
}
|
||||||
|
|
||||||
|
function mult (vector, scalar) {
|
||||||
|
const [x, y] = vector
|
||||||
|
return [x * scalar, y * scalar]
|
||||||
|
}
|
||||||
|
|
||||||
|
function unit_of (heading) {
|
||||||
|
const turns = -heading + 0.25
|
||||||
|
const radians = turn_to_rad(turns)
|
||||||
|
return [Math.cos(radians), Math.sin(radians)]
|
||||||
|
}
|
||||||
|
|
||||||
|
function command_to_state (prev_state, curr_command) {
|
||||||
|
const verb = curr_command[0]
|
||||||
|
switch (verb) {
|
||||||
|
case "goto": {
|
||||||
|
const [_, x, y] = curr_command
|
||||||
|
return {...prev_state, position: [x, y]}
|
||||||
|
}
|
||||||
|
case "home": {
|
||||||
|
return {...prev_state, position: [0, 0], heading: 0}
|
||||||
|
}
|
||||||
|
case "right": {
|
||||||
|
const [_, angle] = curr_command
|
||||||
|
const {heading} = prev_state
|
||||||
|
return {...prev_state, heading: heading + angle}
|
||||||
|
}
|
||||||
|
case "left": {
|
||||||
|
const [_, angle] = curr_command
|
||||||
|
const {heading} = prev_state
|
||||||
|
return {...prev_state, heading: heading - angle}
|
||||||
|
}
|
||||||
|
case "forward": {
|
||||||
|
const [_, steps] = curr_command
|
||||||
|
const {heading, position} = prev_state
|
||||||
|
const unit = unit_of(heading)
|
||||||
|
const move = mult(unit, steps)
|
||||||
|
return {...prev_state, position: add(position, move)}
|
||||||
|
}
|
||||||
|
case "back": {
|
||||||
|
const [_, steps] = curr_command
|
||||||
|
const {heading, position} = prev_state
|
||||||
|
const unit = unit_of(heading)
|
||||||
|
const move = mult(unit, -steps)
|
||||||
|
return {...prev_state, position: add(position, move)}
|
||||||
|
}
|
||||||
|
case "penup": {
|
||||||
|
return {...prev_state, pendown: false}
|
||||||
|
}
|
||||||
|
case "pendown": {
|
||||||
|
return {...prev_state, pendown: true}
|
||||||
|
}
|
||||||
|
case "penwidth": {
|
||||||
|
const [_, width] = curr_command
|
||||||
|
return {...prev_state, penwidth: width}
|
||||||
|
}
|
||||||
|
case "pencolor": {
|
||||||
|
const [_, color] = curr_command
|
||||||
|
return {...prev_state, pencolor: color}
|
||||||
|
}
|
||||||
|
case "setheading": {
|
||||||
|
const [_, heading] = curr_command
|
||||||
|
return {...prev_state, heading: heading}
|
||||||
|
}
|
||||||
|
case "loadstate": {
|
||||||
|
// console.log("LOADSTATE: ", curr_command)
|
||||||
|
const [_, [x, y], heading, visible, pendown, penwidth, pencolor] = curr_command
|
||||||
|
return {position: [x, y], heading, visible, pendown, penwidth, pencolor}
|
||||||
|
}
|
||||||
|
case "show": {
|
||||||
|
return {...prev_state, visible: true}
|
||||||
|
}
|
||||||
|
case "hide": {
|
||||||
|
return {...prev_state, visible: false}
|
||||||
|
}
|
||||||
|
case "background": {
|
||||||
|
background_color = curr_command[1]
|
||||||
|
return prev_state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function eq_vect (v1, v2) {
|
||||||
|
const [x1, y1] = v1
|
||||||
|
const [x2, y2] = v2
|
||||||
|
return (x1 === x2) && (y1 === y2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function eq_color (c1, c2) {
|
||||||
|
if (c1 === c2) return true
|
||||||
|
const res1 = resolve_color(c1)
|
||||||
|
const res2 = resolve_color(c2)
|
||||||
|
for (let i = 0; i < res1.length; ++i) {
|
||||||
|
if (res1[i] !== res2[i]) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function states_to_call (prev, curr) {
|
||||||
|
const calls = []
|
||||||
|
// whose state should we use?
|
||||||
|
// pen states will only differ on more than one property
|
||||||
|
// if we use `loadstate`
|
||||||
|
// my sense is `prev`, but that may change
|
||||||
|
if (prev.pendown && !eq_vect(prev.position, curr.position)) {
|
||||||
|
calls.push(["line", prev.position[0], prev.position[1], curr.position[0], curr.position[1]])
|
||||||
|
}
|
||||||
|
if (!eq_color(curr.pencolor, prev.pencolor)) {
|
||||||
|
calls.push(["stroke", ...resolve_color(curr.pencolor)])
|
||||||
|
}
|
||||||
|
if (curr.penwidth !== prev.penwidth) {
|
||||||
|
calls.push(["strokeWeight", curr.penwidth])
|
||||||
|
}
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
const turtle_radius = 20
|
||||||
|
|
||||||
|
const turtle_angle = 0.385
|
||||||
|
|
||||||
|
let turtle_color = [255, 255, 255, 150]
|
||||||
|
|
||||||
|
function p5_call_root () {
|
||||||
|
return [
|
||||||
|
["background", ...resolve_color(background_color)],
|
||||||
|
["push"],
|
||||||
|
["rotate", Math.PI],
|
||||||
|
["scale", -1, 1],
|
||||||
|
["stroke", ...resolve_color(turtle_init.pencolor)],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotate (vector, heading) {
|
||||||
|
const radians = turn_to_rad(heading)
|
||||||
|
const [x, y] = vector
|
||||||
|
return [
|
||||||
|
(x * Math.cos (radians)) - (y * Math.sin (radians)),
|
||||||
|
(x * Math.sin (radians)) + (y * Math.cos (radians))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function turn_to_rad (heading) {
|
||||||
|
return (heading % 1) * 2 * Math.PI
|
||||||
|
}
|
||||||
|
|
||||||
|
function turn_to_deg (heading) {
|
||||||
|
return (heading % 1) * 360
|
||||||
|
}
|
||||||
|
|
||||||
|
function hex (n) {
|
||||||
|
return n.toString(16).padStart(2, "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
function svg_render_line (prev, curr) {
|
||||||
|
if (!prev.pendown) return ""
|
||||||
|
if (eq_vect(prev.position, curr.position)) return ""
|
||||||
|
const {position: [x1, y1], pencolor, penwidth} = prev
|
||||||
|
const {position: [x2, y2]} = curr
|
||||||
|
const [r, g, b, a] = resolve_color(pencolor)
|
||||||
|
return `
|
||||||
|
<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#${hex(r)}${hex(g)}${hex(b)}" stroke-linecap="square" stroke-opacity="${a/255}" stroke-width="${penwidth}"/>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function escape_svg (svg) {
|
||||||
|
return svg
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extract_ludus (svg) {
|
||||||
|
const code = svg.split("<ludus>")[1]?.split("</ludus>")[0] ?? ""
|
||||||
|
return code
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, `"`)
|
||||||
|
.replace(/'/g, `'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function svg_render_path (states) {
|
||||||
|
const path = []
|
||||||
|
for (let i = 1; i < states.length; ++i) {
|
||||||
|
const prev = states[i - 1]
|
||||||
|
const curr = states[i]
|
||||||
|
path.push(svg_render_line(prev, curr))
|
||||||
|
}
|
||||||
|
return path.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
function svg_render_turtle (state) {
|
||||||
|
if (!state.visible) return ""
|
||||||
|
const [fr, fg, fb, fa] = turtle_color
|
||||||
|
const fill_alpha = fa/255
|
||||||
|
const {heading, pencolor, position: [x, y], pendown, penwidth} = state
|
||||||
|
const origin = [0, turtle_radius]
|
||||||
|
const [x1, y1] = origin
|
||||||
|
const [x2, y2] = rotate(origin, turtle_angle)
|
||||||
|
const [x3, y3] = rotate(origin, -turtle_angle)
|
||||||
|
const [pr, pg, pb, pa] = resolve_color(pencolor)
|
||||||
|
const pen_alpha = pa/255
|
||||||
|
const ink = pendown ? `<line x1="${x1}" y1="${y1}" x2="0" y2="0" stroke="#${hex(pr)}${hex(pg)}${hex(pb)}" stroke-linecap="round" stroke-opacity="${pen_alpha}" stroke-width="${penwidth}" />` : ""
|
||||||
|
return `
|
||||||
|
<g transform="translate(${x}, ${y})rotate(${-turn_to_deg(heading)})">
|
||||||
|
<polygon points="${x1} ${y1} ${x2} ${y2} ${x3} ${y3}" stroke="none" fill="#${hex(fr)}${hex(fg)}${hex(fb)})" fill-opacity="${fill_alpha}"/>
|
||||||
|
${ink}
|
||||||
|
</g>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function svg (commands) {
|
||||||
|
// console.log(commands)
|
||||||
|
const states = [turtle_init]
|
||||||
|
commands.reduce((prev_state, command) => {
|
||||||
|
const new_state = command_to_state(prev_state, command)
|
||||||
|
states.push(new_state)
|
||||||
|
return new_state
|
||||||
|
}, turtle_init)
|
||||||
|
// console.log(states)
|
||||||
|
const {maxX, maxY, minX, minY} = states.reduce((accum, {position: [x, y]}) => {
|
||||||
|
accum.maxX = Math.max(accum.maxX, x)
|
||||||
|
accum.maxY = Math.max(accum.maxY, y)
|
||||||
|
accum.minX = Math.min(accum.minX, x)
|
||||||
|
accum.minY = Math.min(accum.minY, y)
|
||||||
|
return accum
|
||||||
|
|
||||||
|
}, {maxX: 0, maxY: 0, minX: 0, minY: 0})
|
||||||
|
const [r, g, b, a] = 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
|
||||||
|
const margin = Math.max(view_width, view_height) * 0.1
|
||||||
|
const x_origin = minX - margin
|
||||||
|
const y_origin = -maxY - margin
|
||||||
|
const path = svg_render_path(states)
|
||||||
|
const turtle = svg_render_turtle(states[states.length - 1])
|
||||||
|
return `<?xml version="1.0" standalone="no"?>
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="${x_origin} ${y_origin} ${view_width} ${view_height}" width="10in" height="8in">
|
||||||
|
|
||||||
|
<rect x="${x_origin - 5}" y="${y_origin - 5}" width="${view_width + 10}" height="${view_height + 10}" fill="#${hex(r)}${hex(g)}${hex(b)}" stroke-width="0" paint-order="fill" />
|
||||||
|
|
||||||
|
<g transform="scale(-1, 1) rotate(180)">
|
||||||
|
${path}
|
||||||
|
${turtle}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<ludus>
|
||||||
|
${escape_svg(code)}
|
||||||
|
</ludus>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function p5_render_turtle (state, calls) {
|
||||||
|
if (!state.visible) return
|
||||||
|
calls.push(["push"])
|
||||||
|
const [r, g, b, a] = turtle_color
|
||||||
|
calls.push(["fill", r, g, b, a])
|
||||||
|
const {heading, pencolor, position: [x, y], pendown, penwidth} = state
|
||||||
|
const origin = [0, turtle_radius]
|
||||||
|
const [x1, y1] = origin
|
||||||
|
const [x2, y2] = rotate(origin, turtle_angle)
|
||||||
|
const [x3, y3] = rotate(origin, -turtle_angle)
|
||||||
|
calls.push(["translate", x, y])
|
||||||
|
// need negative turtle rotation with the other p5 translations
|
||||||
|
calls.push(["rotate", -turn_to_rad(heading)])
|
||||||
|
calls.push(["noStroke"])
|
||||||
|
calls.push(["beginShape"])
|
||||||
|
calls.push(["vertex", x1, y1])
|
||||||
|
calls.push(["vertex", x2, y2])
|
||||||
|
calls.push(["vertex", x3, y3])
|
||||||
|
calls.push(["endShape"])
|
||||||
|
calls.push(["strokeWeight", penwidth])
|
||||||
|
calls.push(["stroke", ...resolve_color(pencolor)])
|
||||||
|
if (pendown) calls.push(["line", 0, 0, x1, y1])
|
||||||
|
calls.push(["pop"])
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
export function p5 (commands) {
|
||||||
|
const states = [turtle_init]
|
||||||
|
commands.reduce((prev_state, command) => {
|
||||||
|
const new_state = command_to_state(prev_state, command)
|
||||||
|
states.push(new_state)
|
||||||
|
return new_state
|
||||||
|
}, turtle_init)
|
||||||
|
// console.log(states)
|
||||||
|
const [r, g, b, _] = resolve_color(background_color)
|
||||||
|
if ((r + g + b)/3 > 128) turtle_color = [0, 0, 0, 150]
|
||||||
|
const p5_calls = [...p5_call_root()]
|
||||||
|
for (let i = 1; i < states.length; ++i) {
|
||||||
|
const prev = states[i - 1]
|
||||||
|
const curr = states[i]
|
||||||
|
const calls = states_to_call(prev, curr)
|
||||||
|
for (const call of calls) {
|
||||||
|
p5_calls.push(call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p5_calls[0] = ["background", ...resolve_color(background_color)]
|
||||||
|
p5_render_turtle(states[states.length - 1], p5_calls)
|
||||||
|
p5_calls.push(["pop"])
|
||||||
|
return p5_calls
|
||||||
|
}
|
||||||
|
|
15
pkg/package.json
Normal file
15
pkg/package.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"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/*"
|
||||||
|
]
|
||||||
|
}
|
36
pkg/rudus.d.ts
vendored
Normal file
36
pkg/rudus.d.ts
vendored
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export function ludus(src: string): string;
|
||||||
|
|
||||||
|
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 __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_start: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||||
|
/**
|
||||||
|
* Instantiates the given `module`, which can either be bytes or
|
||||||
|
* a precompiled `WebAssembly.Module`.
|
||||||
|
*
|
||||||
|
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||||
|
*
|
||||||
|
* @returns {InitOutput}
|
||||||
|
*/
|
||||||
|
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||||
|
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||||
|
*
|
||||||
|
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||||
|
*
|
||||||
|
* @returns {Promise<InitOutput>}
|
||||||
|
*/
|
||||||
|
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
211
pkg/rudus.js
Normal file
211
pkg/rudus.js
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
let wasm;
|
||||||
|
|
||||||
|
let WASM_VECTOR_LEN = 0;
|
||||||
|
|
||||||
|
let cachedUint8ArrayMemory0 = null;
|
||||||
|
|
||||||
|
function getUint8ArrayMemory0() {
|
||||||
|
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||||
|
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachedUint8ArrayMemory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
|
||||||
|
|
||||||
|
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
|
||||||
|
? function (arg, view) {
|
||||||
|
return cachedTextEncoder.encodeInto(arg, view);
|
||||||
|
}
|
||||||
|
: function (arg, view) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
view.set(buf);
|
||||||
|
return {
|
||||||
|
read: arg.length,
|
||||||
|
written: buf.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function passStringToWasm0(arg, malloc, realloc) {
|
||||||
|
|
||||||
|
if (realloc === undefined) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
const ptr = malloc(buf.length, 1) >>> 0;
|
||||||
|
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||||
|
WASM_VECTOR_LEN = buf.length;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = arg.length;
|
||||||
|
let ptr = malloc(len, 1) >>> 0;
|
||||||
|
|
||||||
|
const mem = getUint8ArrayMemory0();
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (; offset < len; offset++) {
|
||||||
|
const code = arg.charCodeAt(offset);
|
||||||
|
if (code > 0x7F) break;
|
||||||
|
mem[ptr + offset] = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset !== len) {
|
||||||
|
if (offset !== 0) {
|
||||||
|
arg = arg.slice(offset);
|
||||||
|
}
|
||||||
|
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||||
|
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||||
|
const ret = encodeString(arg, view);
|
||||||
|
|
||||||
|
offset += ret.written;
|
||||||
|
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
WASM_VECTOR_LEN = offset;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(); };
|
||||||
|
|
||||||
|
function getStringFromWasm0(ptr, len) {
|
||||||
|
ptr = ptr >>> 0;
|
||||||
|
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} src
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function __wbg_load(module, imports) {
|
||||||
|
if (typeof Response === 'function' && module instanceof Response) {
|
||||||
|
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||||
|
try {
|
||||||
|
return await WebAssembly.instantiateStreaming(module, imports);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
if (module.headers.get('Content-Type') != 'application/wasm') {
|
||||||
|
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = await module.arrayBuffer();
|
||||||
|
return await WebAssembly.instantiate(bytes, imports);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const instance = await WebAssembly.instantiate(module, imports);
|
||||||
|
|
||||||
|
if (instance instanceof WebAssembly.Instance) {
|
||||||
|
return { instance, module };
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_get_imports() {
|
||||||
|
const imports = {};
|
||||||
|
imports.wbg = {};
|
||||||
|
imports.wbg.__wbindgen_init_externref_table = function() {
|
||||||
|
const table = wasm.__wbindgen_export_0;
|
||||||
|
const offset = table.grow(4);
|
||||||
|
table.set(0, undefined);
|
||||||
|
table.set(offset + 0, undefined);
|
||||||
|
table.set(offset + 1, null);
|
||||||
|
table.set(offset + 2, true);
|
||||||
|
table.set(offset + 3, false);
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
|
return imports;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_init_memory(imports, memory) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_finalize_init(instance, module) {
|
||||||
|
wasm = instance.exports;
|
||||||
|
__wbg_init.__wbindgen_wasm_module = module;
|
||||||
|
cachedUint8ArrayMemory0 = null;
|
||||||
|
|
||||||
|
|
||||||
|
wasm.__wbindgen_start();
|
||||||
|
return wasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSync(module) {
|
||||||
|
if (wasm !== undefined) return wasm;
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined') {
|
||||||
|
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||||
|
({module} = module)
|
||||||
|
} else {
|
||||||
|
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imports = __wbg_get_imports();
|
||||||
|
|
||||||
|
__wbg_init_memory(imports);
|
||||||
|
|
||||||
|
if (!(module instanceof WebAssembly.Module)) {
|
||||||
|
module = new WebAssembly.Module(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new WebAssembly.Instance(module, imports);
|
||||||
|
|
||||||
|
return __wbg_finalize_init(instance, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function __wbg_init(module_or_path) {
|
||||||
|
if (wasm !== undefined) return wasm;
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof module_or_path !== 'undefined') {
|
||||||
|
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
||||||
|
({module_or_path} = module_or_path)
|
||||||
|
} else {
|
||||||
|
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module_or_path === 'undefined') {
|
||||||
|
module_or_path = new URL('rudus_bg.wasm', import.meta.url);
|
||||||
|
}
|
||||||
|
const imports = __wbg_get_imports();
|
||||||
|
|
||||||
|
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
||||||
|
module_or_path = fetch(module_or_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
__wbg_init_memory(imports);
|
||||||
|
|
||||||
|
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
||||||
|
|
||||||
|
return __wbg_finalize_init(instance, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initSync };
|
||||||
|
export default __wbg_init;
|
BIN
pkg/rudus_bg.wasm
Normal file
BIN
pkg/rudus_bg.wasm
Normal file
Binary file not shown.
9
pkg/rudus_bg.wasm.d.ts
vendored
Normal file
9
pkg/rudus_bg.wasm.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/* 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 __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_start: () => void;
|
5
pkg/test.js
Normal file
5
pkg/test.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import * as mod from "./ludus.js";
|
||||||
|
|
||||||
|
console.log(mod.run(`
|
||||||
|
:foobar
|
||||||
|
`));
|
21
sandbox.ld
21
sandbox.ld
|
@ -1,19 +1,4 @@
|
||||||
fn circle! () -> repeat 20 {
|
repeat 1 {
|
||||||
fd! (2)
|
fd! (100)
|
||||||
rt! (inv (20))
|
rt! (0.25)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flower! () -> repeat 10 {
|
|
||||||
circle! ()
|
|
||||||
rt! (inv (10))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn garland! () -> repeat 10 {
|
|
||||||
flower! ()
|
|
||||||
fd! (30)
|
|
||||||
}
|
|
||||||
|
|
||||||
garland! ()
|
|
||||||
|
|
||||||
do turtle_commands > unbox > print!
|
|
||||||
do turtle_state > unbox > print!
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
// use crate::process::{LErr, Trace};
|
// use crate::process::{LErr, Trace};
|
||||||
use crate::validator::VErr;
|
use crate::validator::VErr;
|
||||||
use crate::value::Value;
|
|
||||||
use ariadne::{sources, Color, Label, Report, ReportKind};
|
use ariadne::{sources, Color, Label, Report, ReportKind};
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
// pub fn report_panic(err: LErr) {
|
// pub fn report_panic(err: LErr) {
|
||||||
// let mut srcs = HashSet::new();
|
// let mut srcs = HashSet::new();
|
||||||
|
@ -32,7 +30,6 @@ use std::collections::HashSet;
|
||||||
// stack.push(label);
|
// stack.push(label);
|
||||||
// srcs.insert((*input, *src));
|
// srcs.insert((*input, *src));
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Report::build(ReportKind::Error, (err.input, err.span.into_range()))
|
// Report::build(ReportKind::Error, (err.input, err.span.into_range()))
|
||||||
// .with_message(format!("Ludus panicked! {}", err.msg))
|
// .with_message(format!("Ludus panicked! {}", err.msg))
|
||||||
// .with_label(Label::new((err.input, err.span.into_range())).with_color(Color::Red))
|
// .with_label(Label::new((err.input, err.span.into_range())).with_color(Color::Red))
|
||||||
|
|
209
src/lib.rs
Normal file
209
src/lib.rs
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
use chumsky::{input::Stream, prelude::*};
|
||||||
|
use imbl::HashMap;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
const DEBUG_SCRIPT_COMPILE: bool = false;
|
||||||
|
const DEBUG_SCRIPT_RUN: bool = false;
|
||||||
|
const DEBUG_PRELUDE_COMPILE: bool = false;
|
||||||
|
const DEBUG_PRELUDE_RUN: bool = false;
|
||||||
|
|
||||||
|
mod base;
|
||||||
|
|
||||||
|
mod spans;
|
||||||
|
use crate::spans::Spanned;
|
||||||
|
|
||||||
|
mod lexer;
|
||||||
|
use crate::lexer::lexer;
|
||||||
|
|
||||||
|
mod parser;
|
||||||
|
use crate::parser::{parser, Ast};
|
||||||
|
|
||||||
|
mod validator;
|
||||||
|
use crate::validator::Validator;
|
||||||
|
|
||||||
|
mod errors;
|
||||||
|
use crate::errors::report_invalidation;
|
||||||
|
|
||||||
|
mod chunk;
|
||||||
|
mod op;
|
||||||
|
|
||||||
|
mod compiler;
|
||||||
|
use crate::compiler::Compiler;
|
||||||
|
|
||||||
|
mod value;
|
||||||
|
use value::Value;
|
||||||
|
|
||||||
|
mod vm;
|
||||||
|
use vm::Vm;
|
||||||
|
|
||||||
|
const PRELUDE: &str = include_str!("../assets/test_prelude.ld");
|
||||||
|
|
||||||
|
fn prelude() -> HashMap<&'static str, 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!();
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed = parsed.unwrap();
|
||||||
|
let (ast, span) = &parsed;
|
||||||
|
|
||||||
|
let base = base::make_base();
|
||||||
|
let mut base_env = imbl::HashMap::new();
|
||||||
|
base_env.insert("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!();
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: &'static Spanned<Ast> = Box::leak(Box::new(parsed));
|
||||||
|
let mut compiler = Compiler::new(
|
||||||
|
parsed,
|
||||||
|
"prelude",
|
||||||
|
PRELUDE,
|
||||||
|
0,
|
||||||
|
HashMap::new(),
|
||||||
|
DEBUG_PRELUDE_COMPILE,
|
||||||
|
);
|
||||||
|
compiler.emit_constant(base);
|
||||||
|
compiler.bind("base");
|
||||||
|
compiler.compile();
|
||||||
|
|
||||||
|
let chunk = compiler.chunk;
|
||||||
|
let mut vm = Vm::new(chunk, DEBUG_PRELUDE_RUN);
|
||||||
|
let prelude = vm.run().clone().unwrap();
|
||||||
|
match prelude {
|
||||||
|
Value::Dict(hashmap) => *hashmap,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn ludus(src: String) -> String {
|
||||||
|
let src = src.to_string().leak();
|
||||||
|
let (tokens, lex_errs) = lexer().parse(src).into_output_errors();
|
||||||
|
if !lex_errs.is_empty() {
|
||||||
|
return 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 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()));
|
||||||
|
|
||||||
|
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());
|
||||||
|
validator.validate();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
compiler.compile();
|
||||||
|
if DEBUG_SCRIPT_COMPILE {
|
||||||
|
println!("=== source code ===");
|
||||||
|
println!("{src}");
|
||||||
|
compiler.disassemble();
|
||||||
|
println!("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if DEBUG_SCRIPT_RUN {
|
||||||
|
println!("=== vm run ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
let vm_chunk = compiler.chunk;
|
||||||
|
|
||||||
|
let mut vm = Vm::new(vm_chunk, DEBUG_SCRIPT_RUN);
|
||||||
|
let result = vm.run();
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
188
src/main.rs
188
src/main.rs
|
@ -1,190 +1,10 @@
|
||||||
use chumsky::{input::Stream, prelude::*};
|
use rudus::ludus;
|
||||||
use imbl::HashMap;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
const DEBUG_SCRIPT_COMPILE: bool = false;
|
|
||||||
const DEBUG_SCRIPT_RUN: bool = false;
|
|
||||||
const DEBUG_PRELUDE_COMPILE: bool = false;
|
|
||||||
const DEBUG_PRELUDE_RUN: bool = false;
|
|
||||||
|
|
||||||
mod base;
|
|
||||||
|
|
||||||
mod spans;
|
|
||||||
use crate::spans::Spanned;
|
|
||||||
|
|
||||||
mod lexer;
|
|
||||||
use crate::lexer::lexer;
|
|
||||||
|
|
||||||
mod parser;
|
|
||||||
use crate::parser::{parser, Ast};
|
|
||||||
|
|
||||||
mod validator;
|
|
||||||
use crate::validator::Validator;
|
|
||||||
|
|
||||||
mod errors;
|
|
||||||
use crate::errors::report_invalidation;
|
|
||||||
|
|
||||||
mod chunk;
|
|
||||||
mod op;
|
|
||||||
|
|
||||||
mod compiler;
|
|
||||||
use crate::compiler::Compiler;
|
|
||||||
|
|
||||||
mod value;
|
|
||||||
use value::Value;
|
|
||||||
|
|
||||||
mod vm;
|
|
||||||
use vm::Vm;
|
|
||||||
|
|
||||||
const PRELUDE: &str = include_str!("../assets/test_prelude.ld");
|
|
||||||
|
|
||||||
pub fn prelude() -> HashMap<&'static str, 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!();
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed = parsed.unwrap();
|
|
||||||
let (ast, span) = &parsed;
|
|
||||||
|
|
||||||
let base = base::make_base();
|
|
||||||
let mut base_env = imbl::HashMap::new();
|
|
||||||
base_env.insert("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!();
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed: &'static Spanned<Ast> = Box::leak(Box::new(parsed));
|
|
||||||
let mut compiler = Compiler::new(
|
|
||||||
parsed,
|
|
||||||
"prelude",
|
|
||||||
PRELUDE,
|
|
||||||
0,
|
|
||||||
HashMap::new(),
|
|
||||||
DEBUG_PRELUDE_COMPILE,
|
|
||||||
);
|
|
||||||
compiler.emit_constant(base);
|
|
||||||
compiler.bind("base");
|
|
||||||
compiler.compile();
|
|
||||||
|
|
||||||
let chunk = compiler.chunk;
|
|
||||||
let mut vm = Vm::new(chunk, DEBUG_PRELUDE_RUN);
|
|
||||||
let prelude = vm.run().clone().unwrap();
|
|
||||||
match prelude {
|
|
||||||
Value::Dict(hashmap) => *hashmap,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(src: &'static str) {
|
|
||||||
let (tokens, lex_errs) = lexer().parse(src).into_output_errors();
|
|
||||||
if !lex_errs.is_empty() {
|
|
||||||
println!("{:?}", lex_errs);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
println!("{:?}", parse_errors);
|
|
||||||
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 prelude = imbl::HashMap::new();
|
|
||||||
|
|
||||||
let mut validator = Validator::new(&parsed.0, &parsed.1, "user input", src, prelude.clone());
|
|
||||||
validator.validate();
|
|
||||||
|
|
||||||
if !validator.errors.is_empty() {
|
|
||||||
println!("Ludus found some validation errors:");
|
|
||||||
report_invalidation(validator.errors);
|
|
||||||
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");
|
|
||||||
|
|
||||||
compiler.compile();
|
|
||||||
if DEBUG_SCRIPT_COMPILE {
|
|
||||||
println!("=== source code ===");
|
|
||||||
println!("{src}");
|
|
||||||
compiler.disassemble();
|
|
||||||
println!("\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if DEBUG_SCRIPT_RUN {
|
|
||||||
println!("=== vm run ===");
|
|
||||||
}
|
|
||||||
|
|
||||||
let vm_chunk = compiler.chunk;
|
|
||||||
|
|
||||||
let mut vm = Vm::new(vm_chunk, DEBUG_SCRIPT_RUN);
|
|
||||||
let result = vm.run();
|
|
||||||
let output = match result {
|
|
||||||
Ok(val) => val.to_string(),
|
|
||||||
Err(panic) => format!("Ludus panicked! {panic}"),
|
|
||||||
};
|
|
||||||
if DEBUG_SCRIPT_RUN {
|
|
||||||
vm.print_stack();
|
|
||||||
}
|
|
||||||
println!("{output}");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ld_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())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
env::set_var("RUST_BACKTRACE", "1");
|
env::set_var("RUST_BACKTRACE", "1");
|
||||||
let src: &'static str = fs::read_to_string("sandbox.ld").unwrap().leak();
|
let src = fs::read_to_string("sandbox.ld").unwrap();
|
||||||
match ld_fmt(src) {
|
let json = ludus(src);
|
||||||
Ok(src) => println!("{}", src),
|
println!("{json}");
|
||||||
Err(msg) => println!("Could not format source with errors:\n{}", msg),
|
|
||||||
}
|
|
||||||
run(src);
|
|
||||||
}
|
}
|
||||||
|
|
126
src/parser.rs
126
src/parser.rs
|
@ -1,37 +1,11 @@
|
||||||
|
// TODO: move AST to its own module
|
||||||
|
// TODO: remove StringMatcher cruft
|
||||||
|
// TODO: good error messages?
|
||||||
|
|
||||||
use crate::lexer::*;
|
use crate::lexer::*;
|
||||||
use crate::spans::*;
|
use crate::spans::*;
|
||||||
use chumsky::{input::ValueInput, prelude::*, recursive::Recursive};
|
use chumsky::{input::ValueInput, prelude::*, recursive::Recursive};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use struct_scalpel::Dissectible;
|
|
||||||
|
|
||||||
// #[derive(Clone, Debug, PartialEq)]
|
|
||||||
// pub struct WhenClause {
|
|
||||||
// pub cond: Spanned<Ast>,
|
|
||||||
// pub body: Spanned<Ast>,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl fmt::Display for WhenClause {
|
|
||||||
// fn fmt(self: &WhenClause, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
// write!(f, "cond: {}, body: {}", self.cond.0, self.body.0)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[derive(Clone, Debug, PartialEq)]
|
|
||||||
// pub struct MatchClause {
|
|
||||||
// pub patt: Spanned<Pattern>,
|
|
||||||
// pub guard: Option<Spanned<Ast>>,
|
|
||||||
// pub body: Spanned<Ast>,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl fmt::Display for MatchClause {
|
|
||||||
// fn fmt(self: &MatchClause, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
// write!(
|
|
||||||
// f,
|
|
||||||
// "pattern: {}, guard: {:?} body: {}",
|
|
||||||
// self.patt.0, self.guard, self.body.0
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum StringPart {
|
pub enum StringPart {
|
||||||
|
@ -51,13 +25,7 @@ impl fmt::Display for StringPart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LFn {
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
name: &'static str,
|
|
||||||
clauses: Vec<Spanned<Ast>>,
|
|
||||||
doc: Option<&'static str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Dissectible)]
|
|
||||||
pub enum Ast {
|
pub enum Ast {
|
||||||
// a special Error node
|
// a special Error node
|
||||||
// may come in handy?
|
// may come in handy?
|
||||||
|
@ -219,14 +187,11 @@ impl Ast {
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n ")
|
.join("\n ")
|
||||||
),
|
),
|
||||||
FnBody(clauses) => format!(
|
FnBody(clauses) => clauses
|
||||||
"{}",
|
.iter()
|
||||||
clauses
|
.map(|(clause, _)| clause.show())
|
||||||
.iter()
|
.collect::<Vec<_>>()
|
||||||
.map(|(clause, _)| clause.show())
|
.join("\n "),
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n ")
|
|
||||||
),
|
|
||||||
Fn(name, body, doc) => {
|
Fn(name, body, doc) => {
|
||||||
let mut out = format!("fn {name} {{\n");
|
let mut out = format!("fn {name} {{\n");
|
||||||
if let Some(doc) = doc {
|
if let Some(doc) = doc {
|
||||||
|
@ -267,7 +232,7 @@ impl Ast {
|
||||||
.join(", ")
|
.join(", ")
|
||||||
),
|
),
|
||||||
MatchClause(pattern, guard, body) => {
|
MatchClause(pattern, guard, body) => {
|
||||||
let mut out = format!("{}", pattern.0.show());
|
let mut out = pattern.0.show();
|
||||||
if let Some(guard) = guard.as_ref() {
|
if let Some(guard) = guard.as_ref() {
|
||||||
out = format!("{out} if {}", guard.0.show());
|
out = format!("{out} if {}", guard.0.show());
|
||||||
}
|
}
|
||||||
|
@ -523,75 +488,6 @@ impl fmt::Debug for StringMatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[derive(Clone, Debug, PartialEq)]
|
|
||||||
// pub enum Pattern {
|
|
||||||
// Nil,
|
|
||||||
// Boolean(bool),
|
|
||||||
// Number(f64),
|
|
||||||
// String(&'static str),
|
|
||||||
// Interpolated(Vec<Spanned<StringPart>>, StringMatcher),
|
|
||||||
// Keyword(&'static str),
|
|
||||||
// Word(&'static str),
|
|
||||||
// As(&'static str, &'static str),
|
|
||||||
// Splattern(Box<Spanned<Self>>),
|
|
||||||
// Placeholder,
|
|
||||||
// Tuple(Vec<Spanned<Self>>),
|
|
||||||
// List(Vec<Spanned<Self>>),
|
|
||||||
// Pair(&'static str, Box<Spanned<Self>>),
|
|
||||||
// Dict(Vec<Spanned<Self>>),
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl fmt::Display for Pattern {
|
|
||||||
// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
// match self {
|
|
||||||
// Pattern::Nil => write!(f, "nil"),
|
|
||||||
// Pattern::Boolean(b) => write!(f, "{}", b),
|
|
||||||
// Pattern::Number(n) => write!(f, "{}", n),
|
|
||||||
// Pattern::String(s) => write!(f, "{}", s),
|
|
||||||
// Pattern::Keyword(k) => write!(f, ":{}", k),
|
|
||||||
// Pattern::Word(w) => write!(f, "{}", w),
|
|
||||||
// Pattern::As(w, t) => write!(f, "{} as {}", w, t),
|
|
||||||
// Pattern::Splattern(p) => write!(f, "...{}", p.0),
|
|
||||||
// Pattern::Placeholder => write!(f, "_"),
|
|
||||||
// Pattern::Tuple(t) => write!(
|
|
||||||
// f,
|
|
||||||
// "({})",
|
|
||||||
// t.iter()
|
|
||||||
// .map(|x| x.0.to_string())
|
|
||||||
// .collect::<Vec<_>>()
|
|
||||||
// .join(", ")
|
|
||||||
// ),
|
|
||||||
// Pattern::List(l) => write!(
|
|
||||||
// f,
|
|
||||||
// "({})",
|
|
||||||
// l.iter()
|
|
||||||
// .map(|x| x.0.to_string())
|
|
||||||
// .collect::<Vec<_>>()
|
|
||||||
// .join(", ")
|
|
||||||
// ),
|
|
||||||
// Pattern::Dict(entries) => write!(
|
|
||||||
// f,
|
|
||||||
// "#{{{}}}",
|
|
||||||
// entries
|
|
||||||
// .iter()
|
|
||||||
// .map(|(pair, _)| pair.to_string())
|
|
||||||
// .collect::<Vec<_>>()
|
|
||||||
// .join(", ")
|
|
||||||
// ),
|
|
||||||
// Pattern::Pair(key, value) => write!(f, ":{} {}", key, value.0),
|
|
||||||
// Pattern::Interpolated(strprts, _) => write!(
|
|
||||||
// f,
|
|
||||||
// "interpolated: \"{}\"",
|
|
||||||
// strprts
|
|
||||||
// .iter()
|
|
||||||
// .map(|part| part.0.to_string())
|
|
||||||
// .collect::<Vec<_>>()
|
|
||||||
// .join("")
|
|
||||||
// ),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
fn is_word_char(c: char) -> bool {
|
fn is_word_char(c: char) -> bool {
|
||||||
if c.is_ascii_alphanumeric() {
|
if c.is_ascii_alphanumeric() {
|
||||||
return true;
|
return true;
|
||||||
|
|
632
src/process.rs
632
src/process.rs
|
@ -1,632 +0,0 @@
|
||||||
use crate::base::*;
|
|
||||||
use crate::parser::*;
|
|
||||||
use crate::spans::*;
|
|
||||||
use crate::validator::FnInfo;
|
|
||||||
use crate::value::Value;
|
|
||||||
use chumsky::prelude::SimpleSpan;
|
|
||||||
use imbl::HashMap;
|
|
||||||
use imbl::Vector;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct LErr<'src> {
|
|
||||||
pub input: &'static str,
|
|
||||||
pub src: &'static str,
|
|
||||||
pub msg: String,
|
|
||||||
pub span: SimpleSpan,
|
|
||||||
pub trace: Vec<Trace<'src>>,
|
|
||||||
pub extra: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Trace<'src> {
|
|
||||||
pub callee: Spanned<Ast>,
|
|
||||||
pub caller: Spanned<Ast>,
|
|
||||||
pub function: Value<'src>,
|
|
||||||
pub arguments: Value<'src>,
|
|
||||||
pub input: &'static str,
|
|
||||||
pub src: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'src> LErr<'src> {
|
|
||||||
pub fn new(
|
|
||||||
msg: String,
|
|
||||||
span: SimpleSpan,
|
|
||||||
input: &'static str,
|
|
||||||
src: &'static str,
|
|
||||||
) -> LErr<'src> {
|
|
||||||
LErr {
|
|
||||||
msg,
|
|
||||||
span,
|
|
||||||
input,
|
|
||||||
src,
|
|
||||||
trace: vec![],
|
|
||||||
extra: "".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type LResult<'src> = Result<Value<'src>, LErr<'src>>;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Process<'src> {
|
|
||||||
pub input: &'static str,
|
|
||||||
pub src: &'static str,
|
|
||||||
pub locals: Vec<(String, Value<'src>)>,
|
|
||||||
pub prelude: Vec<(String, Value<'src>)>,
|
|
||||||
pub ast: &'src Ast,
|
|
||||||
pub span: SimpleSpan,
|
|
||||||
pub fn_info: std::collections::HashMap<*const Ast, FnInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'src> Process<'src> {
|
|
||||||
pub fn resolve(&self, word: &String) -> LResult<'src> {
|
|
||||||
let resolved_local = self.locals.iter().rev().find(|(name, _)| word == name);
|
|
||||||
|
|
||||||
match resolved_local {
|
|
||||||
Some((_, value)) => Ok(value.clone()),
|
|
||||||
None => {
|
|
||||||
let resolved_prelude = self.prelude.iter().rev().find(|(name, _)| word == name);
|
|
||||||
match resolved_prelude {
|
|
||||||
Some((_, value)) => Ok(value.clone()),
|
|
||||||
None => Err(LErr::new(
|
|
||||||
format!("unbound name `{word}`"),
|
|
||||||
self.span,
|
|
||||||
self.input,
|
|
||||||
self.src,
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn panic(&self, msg: String) -> LResult<'src> {
|
|
||||||
Err(LErr::new(msg, self.span, self.input, self.src))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bind(&mut self, word: String, value: &Value<'src>) {
|
|
||||||
self.locals.push((word, value.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match_eq<T>(&self, x: T, y: T) -> Option<&Process<'src>>
|
|
||||||
where
|
|
||||||
T: PartialEq,
|
|
||||||
{
|
|
||||||
if x == y {
|
|
||||||
Some(self)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match_pattern(&mut self, patt: &Ast, val: &Value<'src>) -> Option<&Process<'src>> {
|
|
||||||
use Ast::*;
|
|
||||||
match (patt, val) {
|
|
||||||
(NilPattern, Value::Nil) => Some(self),
|
|
||||||
(PlaceholderPattern, _) => Some(self),
|
|
||||||
(NumberPattern(x), Value::Number(y)) => self.match_eq(x, y),
|
|
||||||
(BooleanPattern(x), Value::Boolean(y)) => self.match_eq(x, y),
|
|
||||||
(KeywordPattern(x), Value::Keyword(y)) => self.match_eq(x, y),
|
|
||||||
(StringPattern(x), Value::InternedString(y)) => self.match_eq(x, y),
|
|
||||||
(StringPattern(x), Value::AllocatedString(y)) => self.match_eq(&x.to_string(), y),
|
|
||||||
(InterpolatedPattern(_, StringMatcher(matcher)), Value::InternedString(y)) => {
|
|
||||||
match matcher(y.to_string()) {
|
|
||||||
Some(matches) => {
|
|
||||||
let mut matches = matches
|
|
||||||
.iter()
|
|
||||||
.map(|(word, string)| {
|
|
||||||
(
|
|
||||||
word.clone(),
|
|
||||||
Value::AllocatedString(Rc::new(string.clone())),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
self.locals.append(&mut matches);
|
|
||||||
Some(self)
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(WordPattern(w), val) => {
|
|
||||||
self.bind(w.to_string(), val);
|
|
||||||
Some(self)
|
|
||||||
}
|
|
||||||
(AsPattern(word, type_str), value) => {
|
|
||||||
let ludus_type = r#type(value);
|
|
||||||
let type_kw = Value::Keyword(type_str);
|
|
||||||
if type_kw == ludus_type {
|
|
||||||
self.bind(word.to_string(), value);
|
|
||||||
Some(self)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(TuplePattern(x), Value::Tuple(y)) => {
|
|
||||||
let has_splat = x.iter().any(|patt| matches!(patt, (Splattern(_), _)));
|
|
||||||
if x.len() > y.len() || (!has_splat && x.len() != y.len()) {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let to = self.locals.len();
|
|
||||||
for i in 0..x.len() {
|
|
||||||
if let Splattern(patt) = &x[i].0 {
|
|
||||||
let mut list = Vector::new();
|
|
||||||
for i in i..y.len() {
|
|
||||||
list.push_back(y[i].clone())
|
|
||||||
}
|
|
||||||
let list = Value::List(list);
|
|
||||||
self.match_pattern(&patt.0, &list);
|
|
||||||
} else if self.match_pattern(&x[i].0, &y[i]).is_none() {
|
|
||||||
self.locals.truncate(to);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(self)
|
|
||||||
}
|
|
||||||
(ListPattern(x), Value::List(y)) => {
|
|
||||||
let has_splat = x.iter().any(|patt| matches!(patt, (Splattern(_), _)));
|
|
||||||
if x.len() > y.len() || (!has_splat && x.len() != y.len()) {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let to = self.locals.len();
|
|
||||||
for (i, (patt, _)) in x.iter().enumerate() {
|
|
||||||
if let Splattern(patt) = &patt {
|
|
||||||
let list = Value::List(y.skip(i));
|
|
||||||
self.match_pattern(&patt.0, &list);
|
|
||||||
} else if self.match_pattern(patt, y.get(i).unwrap()).is_none() {
|
|
||||||
self.locals.truncate(to);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(self)
|
|
||||||
}
|
|
||||||
// TODO: optimize this on several levels
|
|
||||||
// - [ ] opportunistic mutation
|
|
||||||
// - [ ] get rid of all the pointer indirection in word splats
|
|
||||||
(DictPattern(x), Value::Dict(y)) => {
|
|
||||||
let has_splat = x.iter().any(|patt| matches!(patt, (Splattern(_), _)));
|
|
||||||
if x.len() > y.len() || (!has_splat && x.len() != y.len()) {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let to = self.locals.len();
|
|
||||||
let mut matched = vec![];
|
|
||||||
for (pattern, _) in x {
|
|
||||||
match pattern {
|
|
||||||
PairPattern(key, patt) => {
|
|
||||||
if let Some(val) = y.get(key) {
|
|
||||||
if self.match_pattern(&patt.0, val).is_none() {
|
|
||||||
self.locals.truncate(to);
|
|
||||||
return None;
|
|
||||||
} else {
|
|
||||||
matched.push(key);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Splattern(pattern) => match pattern.0 {
|
|
||||||
WordPattern(w) => {
|
|
||||||
// TODO: find a way to take ownership
|
|
||||||
// this will ALWAYS make structural changes, because of this clone
|
|
||||||
// we want opportunistic mutation if possible
|
|
||||||
let mut unmatched = y.clone();
|
|
||||||
for key in matched.iter() {
|
|
||||||
unmatched.remove(*key);
|
|
||||||
}
|
|
||||||
self.bind(w.to_string(), &Value::Dict(unmatched));
|
|
||||||
}
|
|
||||||
PlaceholderPattern => (),
|
|
||||||
_ => unreachable!(),
|
|
||||||
},
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(self)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match_clauses(
|
|
||||||
&mut self,
|
|
||||||
value: &Value<'src>,
|
|
||||||
clauses: &'src [Spanned<Ast>],
|
|
||||||
) -> LResult<'src> {
|
|
||||||
{
|
|
||||||
let root = self.ast;
|
|
||||||
let to = self.locals.len();
|
|
||||||
let mut clauses_iter = clauses.iter();
|
|
||||||
while let Some((Ast::MatchClause(patt, guard, body), _)) = clauses_iter.next() {
|
|
||||||
if self.match_pattern(&patt.0, value).is_some() {
|
|
||||||
let pass_guard = match guard.as_ref() {
|
|
||||||
None => true,
|
|
||||||
Some(guard_expr) => self.visit(guard_expr)?.bool(),
|
|
||||||
};
|
|
||||||
if !pass_guard {
|
|
||||||
self.locals.truncate(to);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let result = self.visit(body);
|
|
||||||
self.locals.truncate(to);
|
|
||||||
self.ast = root;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let patterns = clauses
|
|
||||||
.iter()
|
|
||||||
.map(|clause| {
|
|
||||||
let (Ast::MatchClause(patt, ..), _) = clause else {
|
|
||||||
unreachable!("internal Ludus error")
|
|
||||||
};
|
|
||||||
let patt = &patt.as_ref().0;
|
|
||||||
patt.to_string()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n");
|
|
||||||
dbg!(&patterns);
|
|
||||||
Err(LErr {
|
|
||||||
input: self.input,
|
|
||||||
src: self.src,
|
|
||||||
msg: "no match".to_string(),
|
|
||||||
span: self.span,
|
|
||||||
trace: vec![],
|
|
||||||
extra: format!("expected {value} to match one of\n{}", patterns),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply(&mut self, callee: Value<'src>, caller: Value<'src>) -> LResult<'src> {
|
|
||||||
use Value::*;
|
|
||||||
match (callee, caller) {
|
|
||||||
(Keyword(kw), Dict(dict)) => {
|
|
||||||
if let Some(val) = dict.get(kw) {
|
|
||||||
Ok(val.clone())
|
|
||||||
} else {
|
|
||||||
Ok(Nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Dict(dict), Keyword(kw)) => {
|
|
||||||
if let Some(val) = dict.get(kw) {
|
|
||||||
Ok(val.clone())
|
|
||||||
} else {
|
|
||||||
Ok(Nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Fn(f), Tuple(args)) => {
|
|
||||||
// can't just use the `caller` value b/c borrow checker nonsense
|
|
||||||
let args = Tuple(args);
|
|
||||||
let to = self.locals.len();
|
|
||||||
let mut f = f.borrow_mut();
|
|
||||||
for i in 0..f.enclosing.len() {
|
|
||||||
let (name, value) = f.enclosing[i].clone();
|
|
||||||
if !f.has_run && matches!(value, Value::FnDecl(_)) {
|
|
||||||
let defined = self.resolve(&name);
|
|
||||||
match defined {
|
|
||||||
Ok(Value::Fn(defined)) => f.enclosing[i] = (name.clone(), Fn(defined)),
|
|
||||||
Ok(Value::FnDecl(_)) => {
|
|
||||||
return self.panic(format!(
|
|
||||||
"function `{name}` called before it was defined"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => unreachable!("internal Ludus error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.locals.push(f.enclosing[i].clone());
|
|
||||||
}
|
|
||||||
f.has_run = true;
|
|
||||||
let input = self.input;
|
|
||||||
let src = self.src;
|
|
||||||
self.input = f.input;
|
|
||||||
self.src = f.src;
|
|
||||||
let result = self.match_clauses(&args, f.body);
|
|
||||||
self.locals.truncate(to);
|
|
||||||
self.input = input;
|
|
||||||
self.src = src;
|
|
||||||
result
|
|
||||||
}
|
|
||||||
// TODO: partially applied functions shnould work! In #15
|
|
||||||
(Fn(_f), Args(_partial_args)) => todo!(),
|
|
||||||
(_, Keyword(_)) => Ok(Nil),
|
|
||||||
(_, Args(_)) => self.panic("only functions and keywords may be called".to_string()),
|
|
||||||
(Base(f), Tuple(args)) => match f {
|
|
||||||
BaseFn::Nullary(f) => {
|
|
||||||
let num_args = args.len();
|
|
||||||
if num_args != 0 {
|
|
||||||
self.panic(format!("wrong arity: expected 0 arguments, got {num_args}"))
|
|
||||||
} else {
|
|
||||||
Ok(f())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BaseFn::Unary(f) => {
|
|
||||||
let num_args = args.len();
|
|
||||||
if num_args != 1 {
|
|
||||||
self.panic(format!("wrong arity: expected 1 argument, got {num_args}"))
|
|
||||||
} else {
|
|
||||||
Ok(f(&args[0]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BaseFn::Binary(r#fn) => {
|
|
||||||
let num_args = args.len();
|
|
||||||
if num_args != 2 {
|
|
||||||
self.panic(format!("wrong arity: expected 2 arguments, got {num_args}"))
|
|
||||||
} else {
|
|
||||||
Ok(r#fn(&args[0], &args[1]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BaseFn::Ternary(f) => {
|
|
||||||
let num_args = args.len();
|
|
||||||
if num_args != 3 {
|
|
||||||
self.panic(format!("wrong arity: expected 3 arguments, got {num_args}"))
|
|
||||||
} else {
|
|
||||||
Ok(f(&args[0], &args[1], &args[2]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn visit(&mut self, node: &'src Spanned<Ast>) -> LResult<'src> {
|
|
||||||
let (expr, span) = node;
|
|
||||||
self.ast = expr;
|
|
||||||
self.span = *span;
|
|
||||||
self.eval()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval(&mut self) -> LResult<'src> {
|
|
||||||
use Ast::*;
|
|
||||||
let (root_node, root_span) = (self.ast, self.span);
|
|
||||||
let result = match root_node {
|
|
||||||
Nil => Ok(Value::Nil),
|
|
||||||
Boolean(b) => Ok(Value::Boolean(*b)),
|
|
||||||
Number(n) => Ok(Value::Number(*n)),
|
|
||||||
Keyword(k) => Ok(Value::Keyword(k)),
|
|
||||||
String(s) => Ok(Value::InternedString(s)),
|
|
||||||
Interpolated(parts) => {
|
|
||||||
let mut interpolated = std::string::String::new();
|
|
||||||
for part in parts {
|
|
||||||
match &part.0 {
|
|
||||||
StringPart::Data(s) => interpolated.push_str(s.as_str()),
|
|
||||||
StringPart::Word(w) => {
|
|
||||||
let val = self.resolve(w)?;
|
|
||||||
interpolated.push_str(val.interpolate().as_str())
|
|
||||||
}
|
|
||||||
StringPart::Inline(_) => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Value::AllocatedString(Rc::new(interpolated)))
|
|
||||||
}
|
|
||||||
Block(exprs) => {
|
|
||||||
let to = self.locals.len();
|
|
||||||
let mut result = Value::Nil;
|
|
||||||
for expr in exprs {
|
|
||||||
result = self.visit(expr)?;
|
|
||||||
}
|
|
||||||
self.locals.truncate(to);
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
If(cond, if_true, if_false) => {
|
|
||||||
let truthy = self.visit(cond)?;
|
|
||||||
let to_visit = if truthy.bool() { if_true } else { if_false };
|
|
||||||
self.visit(to_visit)
|
|
||||||
}
|
|
||||||
List(members) => {
|
|
||||||
let mut vect = Vector::new();
|
|
||||||
for member in members {
|
|
||||||
let member_value = self.visit(member)?;
|
|
||||||
match member.0 {
|
|
||||||
Ast::Splat(_) => match member_value {
|
|
||||||
Value::List(list) => vect.append(list),
|
|
||||||
_ => {
|
|
||||||
return self
|
|
||||||
.panic("only lists may be splatted into lists".to_string())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => vect.push_back(member_value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Value::List(vect))
|
|
||||||
}
|
|
||||||
Tuple(members) => {
|
|
||||||
let mut vect = Vec::new();
|
|
||||||
for member in members {
|
|
||||||
vect.push(self.visit(member)?);
|
|
||||||
}
|
|
||||||
Ok(Value::Tuple(Rc::new(vect)))
|
|
||||||
}
|
|
||||||
Word(w) | Ast::Splat(w) => {
|
|
||||||
let val = self.resolve(&w.to_string())?;
|
|
||||||
Ok(val)
|
|
||||||
}
|
|
||||||
Let(patt, expr) => {
|
|
||||||
let val = self.visit(expr)?;
|
|
||||||
let result = match self.match_pattern(&patt.0, &val) {
|
|
||||||
Some(_) => Ok(val),
|
|
||||||
None => self.panic("no match".to_string()),
|
|
||||||
};
|
|
||||||
result
|
|
||||||
}
|
|
||||||
Placeholder => Ok(Value::Placeholder),
|
|
||||||
Arguments(a) => {
|
|
||||||
let mut args = vec![];
|
|
||||||
for arg in a.iter() {
|
|
||||||
args.push(self.visit(arg)?)
|
|
||||||
}
|
|
||||||
let result = if args.iter().any(|arg| matches!(arg, Value::Placeholder)) {
|
|
||||||
Ok(Value::Args(Rc::new(args)))
|
|
||||||
} else {
|
|
||||||
Ok(Value::Tuple(Rc::new(args)))
|
|
||||||
};
|
|
||||||
result
|
|
||||||
}
|
|
||||||
Dict(terms) => {
|
|
||||||
let mut dict = HashMap::new();
|
|
||||||
for term in terms {
|
|
||||||
match term {
|
|
||||||
(Ast::Pair(key, value), _) => {
|
|
||||||
dict.insert(*key, self.visit(value)?);
|
|
||||||
}
|
|
||||||
(Ast::Splat(_), _) => {
|
|
||||||
let resolved = self.visit(term)?;
|
|
||||||
let Value::Dict(to_splat) = resolved else {
|
|
||||||
return self.panic("cannot splat non-dict into dict".to_string());
|
|
||||||
};
|
|
||||||
dict = to_splat.union(dict);
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Value::Dict(dict))
|
|
||||||
}
|
|
||||||
LBox(name, expr) => {
|
|
||||||
let val = self.visit(expr)?;
|
|
||||||
let boxed = Value::Box(name, Rc::new(RefCell::new(val)));
|
|
||||||
self.bind(name.to_string(), &boxed);
|
|
||||||
Ok(boxed)
|
|
||||||
}
|
|
||||||
Synthetic(root, first, rest) => {
|
|
||||||
let root_val = self.visit(root)?;
|
|
||||||
let first_val = self.visit(first)?;
|
|
||||||
let mut result = self.apply(root_val.clone(), first_val.clone());
|
|
||||||
if let Err(mut err) = result {
|
|
||||||
err.trace.push(Trace {
|
|
||||||
callee: *root.clone(),
|
|
||||||
caller: *first.clone(),
|
|
||||||
function: root_val,
|
|
||||||
arguments: first_val,
|
|
||||||
input: self.input,
|
|
||||||
src: self.src,
|
|
||||||
});
|
|
||||||
return Err(err);
|
|
||||||
};
|
|
||||||
let mut prev_node;
|
|
||||||
let mut this_node = first.as_ref();
|
|
||||||
for term in rest.iter() {
|
|
||||||
prev_node = this_node;
|
|
||||||
this_node = term;
|
|
||||||
let caller = self.visit(term)?;
|
|
||||||
let callee = result.unwrap();
|
|
||||||
result = self.apply(callee.clone(), caller.clone());
|
|
||||||
|
|
||||||
if let Err(mut err) = result {
|
|
||||||
err.trace.push(Trace {
|
|
||||||
callee: prev_node.clone(),
|
|
||||||
caller: this_node.clone(),
|
|
||||||
function: caller,
|
|
||||||
arguments: callee,
|
|
||||||
input: self.input,
|
|
||||||
src: self.src,
|
|
||||||
});
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
When(clauses) => {
|
|
||||||
for clause in clauses.iter() {
|
|
||||||
let WhenClause(cond, body) = &clause.0 else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
if self.visit(cond)?.bool() {
|
|
||||||
return self.visit(body);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
self.panic("no match".to_string())
|
|
||||||
}
|
|
||||||
Match(scrutinee, clauses) => {
|
|
||||||
let value = self.visit(scrutinee)?;
|
|
||||||
self.match_clauses(&value, clauses)
|
|
||||||
}
|
|
||||||
Fn(name, clauses, doc) => {
|
|
||||||
let doc = doc.map(|s| s.to_string());
|
|
||||||
let ptr: *const Ast = root_node;
|
|
||||||
let info = self.fn_info.get(&ptr).unwrap();
|
|
||||||
let FnInfo::Defined(_, _, enclosing) = info else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
let enclosing = enclosing
|
|
||||||
.iter()
|
|
||||||
.filter(|binding| binding != name)
|
|
||||||
.map(|binding| (binding.clone(), self.resolve(binding).unwrap().clone()))
|
|
||||||
.collect();
|
|
||||||
let the_fn = Value::Fn::<'src>(Rc::new(RefCell::new(crate::value::Fn::<'src> {
|
|
||||||
name: name.to_string(),
|
|
||||||
body: clauses,
|
|
||||||
doc,
|
|
||||||
enclosing,
|
|
||||||
has_run: false,
|
|
||||||
input: self.input,
|
|
||||||
src: self.src,
|
|
||||||
})));
|
|
||||||
|
|
||||||
let maybe_decl_i = self.locals.iter().position(|(binding, _)| binding == name);
|
|
||||||
|
|
||||||
match maybe_decl_i {
|
|
||||||
None => self.bind(name.to_string(), &the_fn),
|
|
||||||
Some(i) => {
|
|
||||||
let declared = &self.locals[i].1;
|
|
||||||
match declared {
|
|
||||||
Value::FnDecl(_) => {
|
|
||||||
self.locals[i] = (name.to_string(), the_fn.clone());
|
|
||||||
}
|
|
||||||
_ => unreachable!("internal Ludus error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(the_fn)
|
|
||||||
}
|
|
||||||
FnDeclaration(name) => {
|
|
||||||
let decl = Value::FnDecl(name);
|
|
||||||
self.bind(name.to_string(), &decl);
|
|
||||||
Ok(decl)
|
|
||||||
}
|
|
||||||
Panic(msg) => {
|
|
||||||
let msg = self.visit(msg)?;
|
|
||||||
self.panic(format!("{msg}"))
|
|
||||||
}
|
|
||||||
Repeat(times, body) => {
|
|
||||||
let times_num = match self.visit(times) {
|
|
||||||
Ok(Value::Number(n)) => n as usize,
|
|
||||||
_ => return self.panic("`repeat` may only take numbers".to_string()),
|
|
||||||
};
|
|
||||||
for _ in 0..times_num {
|
|
||||||
self.visit(body)?;
|
|
||||||
}
|
|
||||||
Ok(Value::Nil)
|
|
||||||
}
|
|
||||||
Do(terms) => {
|
|
||||||
let mut result = self.visit(&terms[0])?;
|
|
||||||
for term in terms.iter().skip(1) {
|
|
||||||
let next = self.visit(term)?;
|
|
||||||
let arg = Value::Tuple(Rc::new(vec![result]));
|
|
||||||
result = self.apply(next, arg)?;
|
|
||||||
}
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
Loop(init, clauses) => {
|
|
||||||
let mut args = self.visit(init)?;
|
|
||||||
loop {
|
|
||||||
let result = self.match_clauses(&args, clauses)?;
|
|
||||||
if let Value::Recur(recur_args) = result {
|
|
||||||
args = Value::Tuple(Rc::new(recur_args));
|
|
||||||
} else {
|
|
||||||
return Ok(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Recur(args) => {
|
|
||||||
let mut vect = Vec::new();
|
|
||||||
for arg in args {
|
|
||||||
vect.push(self.visit(arg)?);
|
|
||||||
}
|
|
||||||
Ok(Value::Recur(vect))
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
self.ast = root_node;
|
|
||||||
self.span = root_span;
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
67
src/value.rs
67
src/value.rs
|
@ -219,10 +219,7 @@ impl Value {
|
||||||
False => "false".to_string(),
|
False => "false".to_string(),
|
||||||
Number(n) => format!("{n}"),
|
Number(n) => format!("{n}"),
|
||||||
Interned(str) => format!("\"{str}\""),
|
Interned(str) => format!("\"{str}\""),
|
||||||
String(str) => {
|
String(str) => format!("\"{str}\""),
|
||||||
let str_str = str.to_string();
|
|
||||||
format!("\"{str_str}\"")
|
|
||||||
}
|
|
||||||
Keyword(str) => format!(":{str}"),
|
Keyword(str) => format!(":{str}"),
|
||||||
Tuple(t) => {
|
Tuple(t) => {
|
||||||
let members = t.iter().map(|e| e.show()).collect::<Vec<_>>().join(", ");
|
let members = t.iter().map(|e| e.show()).collect::<Vec<_>>().join(", ");
|
||||||
|
@ -258,6 +255,56 @@ impl Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 stringify(&self) -> String {
|
pub fn stringify(&self) -> String {
|
||||||
use Value::*;
|
use Value::*;
|
||||||
match &self {
|
match &self {
|
||||||
|
@ -266,14 +313,14 @@ impl Value {
|
||||||
False => "false".to_string(),
|
False => "false".to_string(),
|
||||||
Number(n) => format!("{n}"),
|
Number(n) => format!("{n}"),
|
||||||
Interned(str) => str.to_string(),
|
Interned(str) => str.to_string(),
|
||||||
Keyword(str) => str.to_string(),
|
Keyword(str) => format!(":{str}"),
|
||||||
Tuple(t) => {
|
Tuple(t) => {
|
||||||
let members = t
|
let members = t
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| e.stringify())
|
.map(|e| e.stringify())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
members.to_string()
|
format!("({members})")
|
||||||
}
|
}
|
||||||
List(l) => {
|
List(l) => {
|
||||||
let members = l
|
let members = l
|
||||||
|
@ -281,7 +328,7 @@ impl Value {
|
||||||
.map(|e| e.stringify())
|
.map(|e| e.stringify())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
members.to_string()
|
format!("[{members}]")
|
||||||
}
|
}
|
||||||
Dict(d) => {
|
Dict(d) => {
|
||||||
let members = d
|
let members = d
|
||||||
|
@ -293,12 +340,14 @@ impl Value {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
members.to_string()
|
format!("#{{{members}}}")
|
||||||
}
|
}
|
||||||
String(s) => s.as_ref().clone(),
|
String(s) => s.as_ref().clone(),
|
||||||
Box(x) => x.as_ref().borrow().stringify(),
|
Box(x) => x.as_ref().borrow().stringify(),
|
||||||
Fn(lfn) => format!("fn {}", lfn.name()),
|
Fn(lfn) => format!("fn {}", lfn.name()),
|
||||||
_ => todo!(),
|
Partial(partial) => format!("fn {}/partial", partial.name),
|
||||||
|
BaseFn(_) => format!("{self}"),
|
||||||
|
Nothing => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user