maybe get git right? ugh

This commit is contained in:
Scott Richmond 2025-06-25 23:21:22 -04:00
commit 0c17b64fd7
22 changed files with 2657 additions and 1058 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target /target
/Cargo.lock /Cargo.lock
/node_modules

View File

@ -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"

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View 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
View File

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

21
pkg/index.html Normal file
View 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
View 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;")
}
export function extract_ludus (svg) {
const code = svg.split("<ludus>")[1]?.split("</ludus>")[0] ?? ""
return code
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, `"`)
.replace(/&apos;/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
View 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
View 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
View 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

Binary file not shown.

9
pkg/rudus_bg.wasm.d.ts vendored Normal file
View 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
View File

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

View File

@ -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!

View File

@ -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
View 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())
}

View File

@ -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);
} }

View File

@ -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;

View File

@ -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
}
}

View File

@ -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!(),
} }
} }