Compare commits

...

97 Commits
to_zig ... main

Author SHA1 Message Date
Scott Richmond
fd6279cda0 add llist fns 2025-06-12 17:53:28 -04:00
Scott Richmond
3cd4ec456f build 2025-06-12 17:52:52 -04:00
Scott Richmond
71b139c048 fix typo in filter 2025-06-12 17:52:44 -04:00
Scott Richmond
1c650cf1bb add llist fns 2025-06-12 17:52:00 -04:00
Scott Richmond
846a9839d6 add llist fns; fix missing colors bug; filter works on sets; other improvements 2025-06-12 17:50:22 -04:00
Scott Richmond
6ca5c9ae48 add int? 2025-06-12 16:57:23 -04:00
Scott Richmond
fe029573cf add foldr 2025-06-12 16:55:17 -04:00
Scott Richmond
763219d111 update docs 2025-06-12 16:46:24 -04:00
Scott Richmond
5c96cd8538 0.1.41 2025-06-12 16:45:07 -04:00
Scott Richmond
a5b0d796d6 build 2025-06-12 16:44:56 -04:00
Scott Richmond
6d1d8e11b5 0.1.40 2025-06-12 16:40:54 -04:00
Scott Richmond
508e4b4be9 build 2025-06-12 16:38:54 -04:00
Scott Richmond
ec7ffbdd9a eq? and neq? bugfix; add pow 2025-06-12 16:30:01 -04:00
Scott Richmond
360be76de9 0.1.38 2025-06-10 17:22:16 -04:00
Scott Richmond
6a71e3c244 fix svg typo bug 2025-06-10 17:22:06 -04:00
Scott Richmond
a86041ed66 0.1.38 2025-06-10 17:00:07 -04:00
Scott Richmond
d61c9a6880 0.1.37 2025-06-10 16:59:47 -04:00
Scott Richmond
02ccd5327c add document size in inches 2025-06-10 16:59:37 -04:00
Scott Richmond
d76a08cfe3 start improving documentation 2025-01-11 15:31:37 -05:00
Scott Richmond
7477f3f59a fix svg rendering 2025-01-11 15:31:18 -05:00
Scott Richmond
d833db3b2d fix aqua rgb 2025-01-11 14:48:31 -05:00
Scott Richmond
c5d7bc6838 fix bug in aqua color tuple 2025-01-03 13:52:15 -05:00
Scott Richmond
63b0b11510 change turtle color based on background color so it's always visible 2025-01-02 16:28:12 -05:00
Scott Richmond
5de97b8a9c create doc dir 2024-12-30 20:08:55 -05:00
Scott Richmond
386108794f repl stuff 2024-12-30 20:08:30 -05:00
Scott Richmond
b08fac2daf to_number in doc topics 2024-11-06 17:45:56 -05:00
Scott Richmond
1f7a9ebd4c fix doc comment for to_number 2024-11-06 17:44:18 -05:00
Scott Richmond
951edccebf 0.1.36 2024-11-03 17:20:54 -05:00
Scott Richmond
49c0ee20b3 remove test svgs 2024-11-03 17:20:47 -05:00
Scott Richmond
1ead4a8c5c build 2024-11-03 17:19:11 -05:00
Scott Richmond
2f5557a552 build 2024-11-03 17:04:04 -05:00
Scott Richmond
066edb00b5 build 2024-11-03 16:45:20 -05:00
Scott Richmond
5ed314de15 update prelude docs 2024-11-01 16:23:32 -04:00
Scott Richmond
ee09b9f208 random works on tuples 2024-11-01 16:23:15 -04:00
Scott Richmond
a7fcad18a6 0.1.35 2024-11-01 14:59:51 -04:00
Scott Richmond
58dc94ce7e 0.1.34 2024-11-01 14:59:36 -04:00
Scott Richmond
7aedbc18d6 build 2024-11-01 14:59:17 -04:00
Scott Richmond
339c2095aa include to_number 2024-10-23 17:30:14 -04:00
Scott Richmond
e278d91bba 0.1.33 2024-10-23 17:29:28 -04:00
Scott Richmond
bb0aaef060 build 2024-10-23 17:27:32 -04:00
Scott Richmond
54e7597b33 0.1.32 2024-10-23 16:44:58 -04:00
Scott Richmond
13856cb8b2 fix penwidth bug 2024-10-23 16:44:51 -04:00
Scott Richmond
f0d19da532 update prelude docs 2024-10-20 15:12:29 -04:00
Scott Richmond
ab3cf98bb5 update doc topics; escape slashes in links 2024-10-20 15:12:17 -04:00
Scott Richmond
440b97d47f update prelude documentation 2024-10-20 14:50:22 -04:00
Scott Richmond
b392e3282c 0.1.31 2024-10-20 14:39:42 -04:00
Scott Richmond
12f1a85e27 fix p5 call push/pop errors 2024-10-20 14:39:36 -04:00
Scott Richmond
fd55dd9a50 0.1.30 2024-10-20 14:22:21 -04:00
Scott Richmond
8d98f3e2b4 fix p5 call snafu 2024-10-20 14:22:14 -04:00
Scott Richmond
ca1a026980 0.1.29 2024-10-20 14:17:47 -04:00
Scott Richmond
d0a5128e0e fix turtle/path rotation, add p5 canvas transformations to adapter from web frontend 2024-10-20 14:17:39 -04:00
Scott Richmond
7dfaed11e3 0.1.28 2024-10-18 15:22:46 -04:00
Scott Richmond
deaeb7b4db build 2024-10-18 15:22:40 -04:00
Scott Richmond
2686d70a49 change test case 2024-10-18 15:18:56 -04:00
Scott Richmond
e6b2ffe4c9 add ludus-extraction function from svg 2024-09-09 18:23:13 -04:00
Scott Richmond
7285e599c5 0.1.27 2024-08-02 14:56:05 -04:00
Scott Richmond
39984ac537 build 2024-08-02 14:55:46 -04:00
Scott Richmond
73b2343963 add p5 test file 2024-08-02 13:56:37 -04:00
Scott Richmond
1733de7dff build 2024-08-01 18:54:18 -04:00
Scott Richmond
be9a86a973 build 2024-08-01 18:50:09 -04:00
Scott Richmond
479e304357 fix viewBox math so we see just what is there 2024-08-01 18:35:28 -04:00
Scott Richmond
db5622bccd new random seed for every run 2024-08-01 18:00:11 -04:00
Scott Richmond
7f64164078 build 2024-08-01 17:45:09 -04:00
Scott Richmond
d9e0fd23ec build 2024-08-01 17:43:29 -04:00
Scott Richmond
ffed651b6e build 2024-08-01 17:40:54 -04:00
Scott Richmond
ea80f81c33 build 2024-08-01 17:31:36 -04:00
Scott Richmond
d477782ff6 build 2024-08-01 17:05:41 -04:00
Scott Richmond
4baabc0a20 build 2024-08-01 16:43:01 -04:00
Scott Richmond
1085c7ae44 build out svg adapter, viewBox isn't working yet 2024-07-31 19:25:22 -04:00
Scott Richmond
e72b9f91ca build 2024-07-31 12:06:42 -04:00
Scott Richmond
d8f152998e build out p5 adapter 2024-07-30 18:36:13 -04:00
Scott Richmond
af125ffbbb fix background! bugs; delete a bunch of p5 adapter code, now moved to JS 2024-07-30 18:35:55 -04:00
Scott Richmond
657ff3dedb remove print! 2024-07-30 18:32:54 -04:00
Scott Richmond
2c9f6f8279 start building out p5 adapter 2024-07-22 11:39:57 -04:00
Scott Richmond
4ca1da1240 build 2024-07-21 20:00:34 -04:00
Scott Richmond
121446c5c4 first step: Ludus speaks turtle graphics, not p5 calls 2024-07-21 19:22:42 -04:00
Scott Richmond
df85be3c1e ref->box everywhere 2024-07-21 16:31:20 -04:00
Scott Richmond
60106d10f0 consider turtle-reported vs. expected-calculated state 2024-07-21 16:26:56 -04:00
Scott Richmond
e068059362 update turtle graphics protocol doc 2024-07-20 16:54:37 -04:00
Scott Richmond
dd3867968e add description of turtle graphics protocol 2024-07-20 16:34:12 -04:00
Scott Richmond
98421a9215 allow shadowing of prelude 2024-07-20 13:51:11 -04:00
Scott Richmond
7467bc8867 disallow shadowing, remove all shadowing from Prelude. 2024-07-19 16:48:11 -04:00
Scott Richmond
2ec95c8f33 add loop & recur back into the language: we do actually need it!--just not *pedagogically* 2024-07-19 16:25:18 -04:00
Scott Richmond
7afc32d9d1 remove loop & recur from the language 2024-07-19 16:11:30 -04:00
Scott Richmond
d4adc1d912 clean up 2024-07-19 16:09:31 -04:00
Scott Richmond
4a069278b8 finally fix script, block, tuple, list, dict, etc. off by one errors for last term 2024-07-19 16:00:17 -04:00
Scott Richmond
e9fee4c0e1 make some additional comments re: off-by-one error errors 2024-07-16 20:16:28 -04:00
Scott Richmond
2027490614 improve some things 2024-07-16 20:12:21 -04:00
Scott Richmond
cb7098ac4e start fixing off-by-one errors: script, block, tuple, args, tup-patt 2024-07-16 19:40:40 -04:00
Scott Richmond
d416511b48 remove repl cruft 2024-07-14 14:42:31 -04:00
Scott Richmond
a6c899a85f Pretty-patterns shows words at ends of splatterns 2024-07-14 14:41:53 -04:00
Scott Richmond
9ddb43a30f called keywords work in pipelines 2024-07-14 14:28:09 -04:00
Scott Richmond
9e50f0cbdf Fix typo 2024-07-14 14:18:10 -04:00
Scott Richmond
2f03bbb12f mismatched arity -> wrong number of arguments 2024-07-14 13:53:32 -04:00
Scott Richmond
8cf84e63d3 add char functions to doc 2024-07-14 13:49:49 -04:00
Scott Richmond
5c32d32f24 add chars to prelude 2024-07-14 13:48:47 -04:00
Scott Richmond
32b42e0242 ignore .zig-cache 2024-07-13 18:30:45 -04:00
27 changed files with 3753 additions and 403 deletions

1
.gitignore vendored
View File

@ -33,3 +33,4 @@ target/repl-port
.repl-buffer.janet
.env
src/jpm_tree
.zig-cache

Binary file not shown.

View File

@ -2,8 +2,377 @@ import init from "./out.mjs"
const mod = await init()
let res = null
let code = null
export function run (source) {
const result = mod.ludus(source).value
console.log(result)
return JSON.parse(result)
code = source
const output = mod.ludus(source).value
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
}

View File

@ -6489,7 +6489,7 @@ var __emscripten_stack_alloc = (a0) => (__emscripten_stack_alloc = wasmExports['
var _emscripten_stack_get_current = () => (_emscripten_stack_get_current = wasmExports['emscripten_stack_get_current'])();
var ___cxa_is_pointer_type = createExportWrapper('__cxa_is_pointer_type', 1);
var dynCall_jiji = Module['dynCall_jiji'] = createExportWrapper('dynCall_jiji', 5);
var ___emscripten_embedded_file_data = Module['___emscripten_embedded_file_data'] = 1840592;
var ___emscripten_embedded_file_data = Module['___emscripten_embedded_file_data'] = 1850840;
function invoke_i(index) {
var sp = stackSave();
try {

Binary file not shown.

13
build/p5_test.mjs Normal file
View File

@ -0,0 +1,13 @@
import {run, p5} from "./ludus.mjs"
const code = `
print! ("Hello, world!")
pencolor! (colors :white)
fd! (50)
pw! (3)
`
const result = run(code)
console.log(result.io.stdout.data)
console.log(p5(result.io.turtle.data))

37
build/svg_test.mjs Normal file
View File

@ -0,0 +1,37 @@
import {run, svg, stdout} from "./ludus.mjs"
const code = `
penwidth!(7)
pencolor!(:red)
background!(:maroon)
fn ngon! (num_sides, side_len) -> {
repeat num_sides {
forward! (side_len)
right! (inv (num_sides))
}
}
fn show_gons! (n, side_len) -> {
if lte? (n, 0)
then :ok
else {
ngon! (n, side_len)
penup!()
rt!(0.01)
fd!(add (side_len, 10))
pendown!()
show_gons! (sub (n, 1), side_len)
}
}
show_gons! (100, 100)
hideturtle!()
`
const result = run(code)
// console.log(stdout(result))
console.log(svg(result.io.turtle.data))

View File

@ -1,3 +1,9 @@
import {run} from "./ludus.mjs"
console.log(run(`let foo = 42; "{foo} bar"`))
console.log(run(`
forward! (100)
right! (0.25)
print! ("foobar")
`))

458
doc/introduction.md Normal file
View File

@ -0,0 +1,458 @@
# Ludus for programmers
## A brief introduction
Ludus is mostly understood by its research and design team as a language for _learners_.
It is a _pedagogical_ language, whose primary purpose is to lead students to critical encounters with the history and present of computing.
The design principles, then, lead with learnability as well as making certain key examples in the history of computing easy.
Because of that, Ludus has some weird features.
It will likely not feel especially familiar, particularly if you have not written funtional code before.
We encourage you to feel disoriented by it, and to lean into that disorientation.
Instead of trying to write code like you have in the past, write code like Ludus wants you to.
There are two big influences on Ludus.
In terms of historical languages, Ludus draws a lot from Logo and Scheme.
In terms of contemporary languages, Ludus has deep affinities with Elixir and Clojure.
To put it in an abstraction cluster, Ludus is a dynamically typed, extremely strict functional programming language with persistent data structures, deep immutability, and pattern matching.
None of these are especially mainstream.
It is not "batteries included," but rather offers a quite minimalistic set of capabilities.
These are devised, as I noted above, to make encountering key concepts from the history of comptuing easy.
But beyond that, Ludus is extremely minimal, in the tradition of Scheme and Logo.
The profound pedagogical perspective behind Scheme and Logo is that building the things you want is an important motivator for learning how to make computers do things.
Ludus follows in this path.
If you've mostly written object-oriented code, Ludus will, frankly, feel weird.
And that's awesome.
### Ludus is expression based
Ludus has no statements, only expressions.
Every expression returns a value, including conditional forms like `if`, `when`, and `match`.
In Ludus, different types of expressions are called _forms_, riffing on the grand Lisp tradition.
### Ludus is dynamically typed
Like its inspirations, Elixir and Clojure and the whole family of Lisps, Ludus is dynamically typed.
It is _strictly_ typed, however.
Unlike Javascript, Ludus will never convert between values of one type or another.
Ludus has the following types:
* `:nil`: The type of `nil`, Ludus's name for nothing.
* `:bool`: Boolean--`true` or `false`.
* `:number`: IEEE-754 64-bit floating point numbers. Ludus does not have an integer type. That said, Ludus avoids `NaN` as much as possible.
* `:string`: UTF-8 strings.
* `:keyword`: Keywords are self-identical atoms, evaluating only to themselves. The equivalent of a `Symbol` in Javascript (or a keyword in Clojure or Elixir). (The types in this list--and in Ludus--are represented as keywords.)
* `:tuple`: Fixed-length, fully immutable collections of zero or more values. Tuples are comma-or-newline separated values, surrounded by parentheses: `(1, 2, 3)`.
* `:list`: Persistent, immutable ordered list of any number of Ludus values. Lists are comma-or-newline separated values, surrounded by square brackets: `[:foo, :bar, :baz]`.
* `:dict`: Persistent, immutable associative collection of keyword keys and any Ludus values. Dicts are comma-or-newline separated keyword-and-value pairs, introduced by `#{` and closed with a curly brace: `#{:a 1, :b 2}`.
* `:fn`: Functions!
* `:box`: A holder for any value, which can change over time. A cognate of Clojure's atom. This is the only place in Ludus you will find mutable state.
At current, three other types are planned but not implemented: `:set`, `:pkg`, `:process`.
Ludus does not allow creating new types.
### Ludus has a weird comment character
It uses the ampersand--`&`--to introduce comments.
It does not have mulitline comments.
### Ludus does not have variables, it has bindings
The basic form of assignment in Ludus looks very familiar:
```
let foo = 42
let bar = :quux
let baz = "hello, world"
```
These are _let bindings_.
#### Let bindings are extremely immutable
They may not change.
In addition, you may not shadow let bindings.
You may not shadow a binding, like in Rust, where you can re-use the name and discard the old binding.
You may also not bind the same name in a nested scope (e.g., inside a function).
Once you bind a name, it is forever bound to that value.
The value in this is that language learners need (almost) never wonder what value a name is bound to, since it can never change.
Except, of course, with function calls.
#### The left-hand side of a let binding is a _pattern_
Ludus makes extensive use of pattern matching.
The left-hand side of a let binding need not be a simple name.
A simple name is only one kind of pattern.
For example, this is valid Ludus:
```
let foo = 42
let 42 = foo
let nil = nil
```
The second line does nothing _except_ match the value on the left hand side to the value on the right hand side.
If a let binding does not match, e.g., `let 1 = 2`, then Ludus will panic.
Patterns can also be used to destructure all Ludus collections:
```
let (:ok, x) = (:ok, 42) & tuple pattern: x is now 42
let [l, m, ...] = [1, 2, 3] & list pattern: l = 1, m = 2
let #{a, b} = #{:a 1, :b 2} & dict pattern: a = 1, b = 2
```
#### Collection patterns are exact & complete, unless otherwise specified
In the second line in the example above, the pattern `[l, m, ...]` includes a splat pattern (or splattern).
If we had written `let [l, m] = [1, 2, 3]`, Ludus would have panicked with `no match`.
There are three list members on the right, only two on the left.
The splat, `...` (or ellipsis) matches "anything else in the list."
You may also include a name after the splat, which will be bound to "anything else in the list," e.g.
```
let [head, ...tail] = [1, 2, 3, 4, 5]
head &=> 1
tail &=> [2, 3, 4, 5]
```
#### The placeholder is a special pattern
A placholder pattern, `_`, matches against anything but does not bind a name.
Also, you may name your placholders, e.g., `_ignored`, but that is for the programmer only.
Named or unnamed placeholder patterns are strictly equivalent.
### Ludus panics
Ludus has exactly one type of runtime error: a panic.
Panics will always crash the program.
You cannot `catch` a panic.
You can raise a panic thusly:
```
panic! "oh shit"
```
`panic!` may only take a single value, but that value can be a collection.
**Eventually** (not long from now!), Ludus will have actor-style concurrency, and a panic will only bring down a process.
But this is not yet implemented.
### Almost everything is a function
Ludus does not have operators.
In the grand Lisp tradition, all operations look like (and, for the most part, substantively are) function calls.
* To add two numbers in Ludus: `add (1, 2) &=> 3`.
* To subtract one number from another: `sub (2, 1) &=> 1`
* To determine, are two things equal?: `eq? (3, "three") &=> false`
### The Prelude is loaded before every evaluation
The Ludus Prelude is its standard library, and all functions in the Prelude are available in every Ludus script.
Consult the [Prelude documentation](./prelude.md) for information for all functions in Prelude.
Everything you'll want to do with Ludus involves the Prelude in some way.
Note that most Prelude function names can, in fact, be shadowed by local bindings in a script.
That said, there are several functions that, for optimization reasons, are "builtin," whose names may never be used, e.g., `add`, `sub`, `eq?`, `inc`, `dec`, and so on.
#### Boolean functions are "special forms"
`and` and `or` are special, in that they are compiled differently than other functions.
Their arguments are evaluated lazily, rather than eagerly, so they can short-circuit (and prevent panics).
### Ludus lists and dicts are persistent
Dicts and lists are persistent.
This means you cannot mutate them.
However, you can still add things to a list--you just get back a _new_ list with the value added:
```
let foo = [1, 2, 3]
let bar = append (foo, 4) &=> [1, 2, 3, 4]
let baz = #{:a 1, :b 2}
let quux = assoc (baz, :c, 3) &=> #{:a 1, :b 2, :c 3}
foo &=> [1, 2, 3]
baz &=> #{:a 1, :b 2}
```
Persistent data structures are wonderful, and use a lot of computer science magic to make them competitive in terms of performance: they use "structural sharing" and attempt "opportunistic mutation."
### Ludus has three conditional forms
The three conditional forms in Ludus are `if`, `when`, and `match`.
#### The `if` form
Ludus's base conditional form is `if`:
```
if foo then bar else baz
```
Does what you'd expect!
But with two caveats.
(Before the caveats: you can put newlines before `then` and `else`.)
#### Falsy falues: `nil` and `false`
The condition (`foo` in the example above) is evaluated not strictly as `true` or `false`.
Ludus "falsy" values are `nil` and `false`.
Everything else is truthy, including `0` and `()` (the empty tuple), and `""` (the empty string).
This holds across anywhere in the language you are dealing with notions of truth and falsity: `if` and `when` forms, `guard` expressions in `match` forms, `and` and `or`, etc.
#### Both `then` and `else` are obligatory
`if` forms in Ludus _must_ have both `then` and `else` branches.
This is because every expression in Ludus must return a value.
If you want to throw away a value, you can do that, but you'll need something like `else nil` or `else :nothing`.
#### The `when` form
If you have multiple conditions you'd like to chain together, `when` forms are what you want.
(Ludus does not have an `else if` form.)
`when` puts multiple clauses together, each of which has a left-hand condition expression and a right-hand body expression: `<condition expr> -> <body expr>`
Ludus will evaluate the left-hand expression, and, if it's truthy, evaluate and return the corresponding right-hand expression:
```
when {
eq? (1, 2) -> :nope
eq? (3, 4) -> :not_this_either
eq? (0, 0) -> :this!
} &=> :this!
```
If no clause in a when form has a truthy left-hand side, Ludus panics.
Any truthy value will do if you want the equivalent of an `else` branch.
By convention, `:else` is used as the catch-all at the end of a match form.
#### The `match` form
A `match` form is the equivalent of a `switch` statement in C-family languages.
It is much more powerful, however.
`match` is much beloved by functional programmers.
`match` forms are similar to `when` forms, but they require a value--a "scrutinee."
And, in place of expressions on the left-hand side of their clauses, they have patterns: `<pattern> -> <expr>`.
They attempt to match the value against each pattern until there is a match.
If no clause matches, then Ludus panics.
This is an extremely common pattern in Ludus:
```
let might_fail = (:ok, 42)
match might_fail with {
(:ok, value) -> print! ("got {value}!")
(:err, _) -> print! ("the thing failed")
} &=> :ok, prints "got 42!"
```
##### Match clauses may have a guard expression
A match clause may also have a guard expression.
Afer the pattern and before the arrow, you may put `if <expr>`.
(Here you may not use `then` or `else`.)
Bindings made in the pattern are valid in that expression.
If the expression is truthy, then that's a match.
If it's falsy, no match:
```
let foo = 42
let bar = 23
match bar with {
x if even? (x) -> :even
x if eq? (foo, x) -> :foo
_ -> :odd_not_foo
} &=> :odd_not_foo
```
### Ludus groups expressions together with blocks
A block groups expressions together.
Ludus blocks must have at least one expression (because everything in Ludus must return a value).
A block evaluates to its last expression.
Expressions are separated by one or more terminators--newlines or semicolons.
Use curly braces to form a block:
```
if true
then {
:first; :second & these are two different expressions
:third
}
else {
:nothing
} &=> :third
```
Blocks can go most anywhere expressions can go.
### Ludus has synthetic expressions
We have already seen function calls, e.g., `add (1, 2)`.
This is a _synthetic_ expression, which is a chained combination of bound names, tuples, and keywords.
The root of a synthetic expression may be either a name or a keyword.
Subsequent terms must either be tuples or keywords.
They are evaluated by applying the second term to the first, then applying the third term to the result of that first application, and applying the fourth to the second result, and so on.
Applying a tuple will call something as a function: `add (1, 2)`.
Applying a keyword will access the value stored at that key in a dict: `foo :bar`.
These may be chained arbitrarily.
Take, for example, `foo :bar (1, 2) :baz`.
This accesses `:bar` on `foo`, applies the arguments `(1, 2)` to that value (presumably a function), and then access `:baz` on value returned by that function.
#### Keywords may be called as functions
Following Clojure's example, you may call a keyword as a function: `foo :bar` and `:bar (foo)` are strictly equivalent.
### Ludus has function pipelines
In addition to normal function application, Ludus also has function pipelines, equivalent to Elixir's pipelines or Clojure's thread macros.
In these, the first term is applied, as a single argument, to the second. The result of that is then applied, as a single argument, to the third, and so on.
Function pipelines are introduced by the reserved word, `do`.
These two expressions are exactly equivalent:
```
do foo > bar >
baz > quux
quux (baz (bar (foo)))
```
Newlines may be inserted _after_ the `>` pipeline symbol, not before.
Note that a line ending with the pipeline symbol will "eat" the line after it, even if separated by many terminators, so be careful.
Because keywords can be called like functions, bare keywords may be used in function pipelines.
### Ludus has partial function application
Any function in Ludus may be partially applied by using the placholder, `_`, in place of an argument.
Doing so returns a function that takes a single argument.
When that function is called, it calls the original function with that argument put in the placeholder's position.
Here's a simple example:
```
let double = mult (2, _)
double (3) &=> 6
double (12) &=> 24
```
Partially applied functions play very nicely with pipelines:
```
let double = mult (2, _)
let mynums = [1, 2, 3, 4, 5, 6]
do mynums >
filter (even?, _) > &-> [2, 4, 6]
map (double, _) &=> [4, 8, 12]
```
### Ludus function definitions
Functions come in three flavours, all of which have a concept of a function clause.
A function clause is a special case of a match clause: it has a _tuple_ pattern on its left hand side (since we call functions with tuples).
Otherwise,
#### Anonymous lambdas
An anonymous lambda is the `fn` reserved word, followed by a function clause:
```
let double = fn (x) -> mult (x, 2)
double &=> fn anon.
double (13) &=> 26
```
#### Named functions
Named functions are exactly the same as anonyomous lambdas, but they have a name between `fn` and the clause:
```
fn double (x) -> mult (x, 2)
double &=> fn double
double (-4) &=> -8
```
#### Compound functions
Compound functions have multiple clauses, separated off by curly braces:
```
fn foo? {
("foo") -> true
(:foo) -> true
(_) -> false
}
foo? (:bar) &=> false
```
There's a very close relationship between match forms and function definitions.
##### docstrings
A compound function may, optionally, take a string before any of its clauses, that serves as documentation for the function:
```
fn foo? {
"Tells if its argument is a `foo`."
("foo") -> true
(:foo) -> true
(_) -> false
}
```
Ludus will print the documentation for a function by means of the `doc!` function.
### Ludus has a convention of "commands": they end with a bang
By convention, Ludus functions that end in an exclamation point have side effects.
These are called _commands_.
`doc!` is a command; so is `print!`.
Ludus commands typically return the keyword `:ok` rather than `nil`.
Much of Ludus involves manipulating turtle graphics commands, `forward!`, and so on.
### Ludus has loops, but you should probably use recursion
Ludus, in the grand Lisp (and Logo) tradition, eschews looping constructs in favour of functional recursion.
Ludus is tail-call optimized, which means that recursion, even mutual recursion, is as fast as looping.
The `loop` form, anyway, isn't anything like you're expecting; it's basically function calls.
Two examples of factorial, looping and recurisve:
```
loop (6, 1) with {
(0, acc) -> acc
(n, acc) -> recur (dec (n), mult (n, acc))
} &=> 720
fn fact {
(n) -> fact (n, 1)
(0, acc) -> acc
(n, acc) -> fact (dec (n), mult (n, acc))
}
fact (6) &=> 720
```
The difference between these is that Ludus will throw a compile error if `recur` isn't in tail position.
In addition, all clauses in a loop form, and all invocations of `recur` must have the same arity, whereas functions may have clauses of arbitrary arity.
### Ludus has multiple "levels" of expressions
Not all Ludus expressions can appear anywhere you need an expression.
Ludus has four levels of expressions that restrict where they may go: simple, nonbinding, expressions, and toplevel.
* _Simple_ expressions include all literals as well as bare names and synthetic expressions. They may go anywhere you expect an expression, e.g. in the condition position in if or when forms. But in these positions, you may not use, say, another conditional form, nor bind a name.
* _Nonbinding_ forms include all expressions _except_ those that bind a name. These include all simple expressions, as well as conditional expressions (`if`, `when`, `match`), anonymous lambdas, and `do` pipelines.
* _Expressions_ (tout court) include all Ludus expressions, including those that bind names: `let`, named `fn`s, and `box`.
* _Toplevel_ expressions may only go at the root scope of a script. At current, the are not yet implemented (`pkg`, `use`, `test`). These are statically checked.
### Ludus has carefully managed state
At some point, you need state.
(You need far less than you think!)
For that, you need a `box`.
A box holds a value that can change over time.
It can hold any other Ludus value, including a box.
Getting a value out of a box isn't as simple, however, as using its name.
The name is bound to the _box_, not its value.
To get the value out of a box, you use the `unbox` function:
```
box foo = 42
foo &=> box [42]
unbox (foo) &=> 42
```
To change the value in a box, you use either the `store!` command, or the `update!` command.
`store!` takes a box and a value, and simply puts the new value in the box.
`update!` (not to be confused with the function `update`) takes a box and a function, and updates the value in the box by applying the function to the value in the box:
```
box foo = 42 &=> box [42]
store! (foo, 23) &=> box [23]
update! (foo, add(13, _)) &=> box [36]
unbox (foo) &=> 36
```
#### Boxes are not variables
We have put the section on boxes last in this introduction because boxes are _not_ variables.
Most state can actually be, and within Ludus, absolutely ought to be, modeled not with boxes but with recursive functions.
Consider the factorial example from earlier.
A straightforward Javascript implementation might look like this:
```javascript
function fact (n) {
let acc = 1;
while n > 1 {
acc = n * acc;
n--;
}
return acc;
}
```
You'll note that the `while` statement doesn't have an easy equivalent in Ludus.
But if you were really stubborn about wanting to twised boxes into variables, you could do something like this:
```
fn fact (n) -> {
box acc = 1
loop (n) with (m) -> if lt? (m, 1)
then unbox (acc)
else {
store! (acc, mult (m, unbox (acc)))
recur (dec (m))
}
}
```
Let me tell you, this is _wild_ Ludus.
The `loop` there is very weird indeed.
The short version is, if you can possibly avoid it--and you probably can--don't use boxes.
The more complex version is this:
The functional and immutable nature of Ludus will change your ideas about programming.
This is part of the point.
So

513
doc/language.md Normal file
View File

@ -0,0 +1,513 @@
# Ludus language reference
This is not intended for beginners, but to be a language overview for experienced programmers. That said, it may help beginners orient themselves in the language.
## Comments
Ludus's comment character is `&`. Anything after an ampersand on a line is ignored. There are no multiline comments.
## Atomic values
Ludus has four types of atomic values.
### `nil`
`nil` is Ludus's representation of nothing. In the grand Lisp tradition, Ludus can, and occasionally does, use `nil`-punning. Its type is `:nil`.
### Booleans
`true` and `false`. That said, in all conditional constructs, `nil` and `false` are "falsy," and everything else is "truthy." Their type is `:boolean`.
### Numbers
Ludus has numbers, which are IEEE-754 64-bit floats. Numbers are more complicated than you think, probably.
Number literals in Ludus are either integers or decimal floating point numbers, e.g. `32.34`, `42`, `-0.23`. Underscores in numbers are ignored, and can be used to separate long numbers, e.g. `1_234_567_890`.
Numbers' type is `:number`.
### Keywords
Ludus keywords begin with a colon and a letter, e.g. `:keyword`. Types are represented as keywords. Some functions take an optional units argument as a keyword, e.g. `:radians`. Keywords are also used as keys for associative collections. Keywords' type is `:keyword`.
Keywords must begin with an ASCII upper- or lower-case letter, and can then include any letter character, as well as `_`, `/`, `!`, `?`, and `*`.
## Strings
Ludus strings are UTF-8 strings, and only use double quotes. Strings may be multiline. For example, this is a string: `"foo"`. So is this:
```
"foo
bar baz"
```
Strings use backslashes for escapes, including `\n` for newline, `\t` for tab, `\"` for a double quote, and `\{` for an open curly brace (see below on interpolation).
Strings' type is `:string`.
### String interpolation
Strings may also insert a string representation of any Ludus value that is bound to a name, by inserting that name in curly braces. To wit,
```
let foo = :foo
let bar = 42
let baz = [1, 2, 3]
"{foo} {bar} {baz}" &=> ":foo 42 1, 2, 3"
```
Interpolations may _not_ be arbitrary expressions: only bound names may be used in interpolations.
## Collections
Ludus has a few different types of collections, in increasing order of complexity: tuples, lists, sets, dicts, and packages. All collections are immutable.
#### Separators
In all collection literals, members are written with a separator between them. On the same line, use a comma; or a newline will also separate elements. You may use as many separators as you wish at any point inside a collection or pattern. `(,,,,,,,3,,4,,,,,,)` and `(3, 4)` are the same value.
#### Efficiency
At the current moment, Ludus collections are all copy-on-write; this means that Ludus is _not_ performant with large collections. Eventually, Ludus will have Clojure-style persistent, immutable collections.
### Tuples
Tuples are fully-immutable, ordered collections of any kinds of values, delimited by parentheses, e.g. `(1, :a, "foo")`. At current, they have no length limit (although they eventually will). Unlike in some languages, tuples can be empty or contain a single element: `()` and `(:foo)` are both just fine. Tuples largely cannot be manipulated functionally; they must be written as literals and unpacked using pattern matching. They can, however, be converted to lists, either through pattern matching or the `list` function. Their type is `:tuple`.
### Lists
Lists are persistent and immutable ordered collections of any kinds of values, delimited by square braces, e.g. `[1, :a, "foo"]`. Their type is `:list`.
Lists may be combined using splats, written with ellipses, e.g., `[...foo, ...bar]`.
### Sets
Sets are persistent and immutable unordered collections of any kinds of values, which can only contain one instance of any given value. They are written similarly to ordered collections: `${1, :a, "foo"}`. Their type is `:set`.
### Dictionaries, or dicts
Dicts are persistent and immutable associative collections of any kinds of values. Dicts use keywords as keys (and cannot use any other kind of Ludus value as a key, not even strings), but can store any values. Dict literals are written as keyword-value pairs: `#{:a 1, :b false}`. Single words may be used as a shorthand for a key-value pair. Accessing a key that holds no value returns `nil`. Their type is `:dict`.
### Packages
Packages are immutable collections of bindings. They may only be described at the top level of a script, and their names must begin with a capital letter. Accessing a key that has no value on a package results in a validation error. They may not be accessed using functions, but only direct keyword access. Their type is `:pkg`.
They are written with the form `pkg`, then a package name, beginning with a capital letter, that will be bound as their name, and then an associative structure (pairs or word shorthands), delimited by `{}`, e.g.:
```
pkg Foo {
:bar "bar"
:baz 42
quux
}
```
### Working with collections
Ludus names are bound permanently and immutably. Collections are immutable. How do you add something to a list or a dict? How do you get things out of them?
Ludus provides functions that allow working with persistent collections. They're detailed in [the Prelude](./prelude.md). That said, all functions that modify collections take a collection and produce the modified collection _as a return value_, without changing the original collection. E.g., `append ([1, 2, 3], 4)` will produce `[1, 2, 3, 4]`, but the original list is unchanged. (For dicts, the equivalent is `assoc`.)
## Expressions
Ludus is an expression-based language: all forms in the language are expressions and return values, except `panic!`. That said, not all expressions may be used everywhere.
### Terminating expressions
Expressions in scripts and blocks are terminated by a newline or a semicolon. In compound forms, like, `if`, the terminator comes after the `else` expression.
In forms with multiple clauses surrounded by curly braces (i.e., function bodies, `match`, `when`, etc.), you may separate clauses with semicolons as well as newlines.
### Toplevel expressions
Some expressions may only be used in the "top level" of a script. Because they are the toplevel, they are assured to be statically knowable. These include: `pkg`, `ns`, `use`, `import`, and `test`. (NB: not all of these are yet implemented.)
### Non-binding expressions
Some forms may take any expression that does _not_ [bind a name](#words-and-bindings), for example, any entry in a collection, or the right-hand side of a `let` binding. This is because binding a name in some positions is ambiguous, or nonsensical, or leads to unwarranted complications.
### Simple expressions
Many compound forms will only accept "simple" expressions. Formally, simple expressions are either literal (atomic, string, or collection literals) or synthetic expressions. They are expressions which do not take sub-expressions: no `if`, `when`, `match`, etc. (`do` expressions are currently not simple, but that may be revised.)
## Words and bindings
Ludus uses _words_ to bind values to names. Words must start with a lower case ASCII letter, and can subsequently include any letter character (modulo backing character encoding), as well as , `_`, `/`, `?`, `!`, and `*`.
Ludus binds values to names immutably and permanently: no name in the same scope may ever be re-bound to a different value. (Although see [boxes](#boxes-and-state), below.
Attempting to use an unbound name (a word that has not had a value bound to it) will result in a validation error, and the script will not run.
### `let` bindings: a very short introduction
Ludus's basic binding form is `let`:
```
let foo = :bar & `foo` is now bound to `bar` for the rest of the scope.
let foo = :baz & Validation error: name foo was bound in line 1
```
`let` bindings are more complex; we will return to these below.
## Patterns
Ludus makes extensive use of pattern-matching. Patterns do two jobs at once: they match values (or don't); and they bind names. The left-hand side of the examples just above in the `let` binding is not just a word: it is a pattern. Patterns also arise in conditional forms and function declarations.
### The placeholder: `_`
The simplest pattern is the placeholder: it matches against anything, and does not bind a name. It is written as a single underscore: `_`, e.g., `let _ = :foo`.
#### Ignored names
If you wish to be a bit more explict than using a placeholder, you can use an ignored name, which is a name that starts with an underscore: `_foo`. This is not bound, is not a valid name, and can be used however much you wish, even multiple times in the same pattern. It is, in fact, a placeholder, plus a reader-facing description.
### Literal patterns
Patterns can be literal atomic values or strings: `0`, `false`, `nil`, `:foo`, etc. That means you can write `let 0 = 0` or `let :foo = :foo`, and, while nothing will happen, everything will be just fine.
Literals match against, well, literal values: if the pattern and the value are the same, they match! If not, they don't match.
Literal values do not bind anything.
### Word patterns
Word patterns match against any value, and bind that value to the word as a name. The scope of that binding depends on the form the pattern is in. `let foo = :bar` binds `:bar` to `foo` for the rest of the scope.
#### Typed patterns
Word patterns can, optionally, take a type, using the `as` reserved word, and the keyword representing the desired type: `let foo as :number = 42`.
### String patterns
Ludus has a simple but powerful form of string pattern matching that mirrors string interpolation. Any word inside curly braces in a string will match against a substring of a string passed into a pattern.
```
let i_am = "I am the walrus"
let "I {verb} the {noun}" = i_am
(verb, noun) &=> ("am", "walrus")
```
Note that such names may well be bound to empty strings: a match does not guarantee that there will be anything in the string. This is particularly relevant at the beginning and end of string patterns:
```
let we_are = "We are the eggmen"
let "{first}We {what}" = we_are
(first, what) &=> ("", "are the eggmen")
```
### Collection patterns
Tuples, lists, and dicts can be destructured using patterns. They are written nearly identically to their literal counterparts. Collection patterns are composed of any number of simpler patterns or other collection patterns. They bind any names nested in them, match literals in them, etc.
#### Tuple patterns
Tuple patterns are delimited using parens, using commas or newlines to separate any number of other patterns. Consider `let (x, y, z) = (1, 2, 3)`. `x`, `y`, and `z` are now bound to 1, 2, and 3, respectively.
The last item in a tuple pattern can be a splat--`...`--which either discards any remaining unenumerated values in a tuple, or binds them to a list. Without a splat, tuples patterns only match against tuples of the same length.
```
let mytup = (1, 2, 3)
let (x, _, y) = mytup & x is now 1, y is now 3
let (a, ...) = mytup & a is now 1; a bare splat (without a name) is just fine
let (_, ...cs) = mytup & cs is now [2, 3]
let (p, q) = mytup & panic! no match
let () = () & empty tuples are also patterns
```
#### List patterns
List patterns are identical to tuple patterns, but they are written using square braces. They also match against a specific number of members, and may take a splat in the last position, e.g. `let [first, ...rest] = [1, 2, 3]`.
Note that list patterns, like tuple patterns, match on explicit length. That means that if you are matching only the first items of a list, you must explicitly include a splat pattern, e.g. `let [first, second, ...] = [1, 2, 3, 4]`.
#### Dict patterns
Dict patterns are written either with shorthand words, or keyword-pattern pairs. Consider: `let #{:a foo, :b 12, c} = #{:a 1, :b 12, :c 4}`. `foo` is now 1; `b` is now 12, `c` is now 4. If a dict does not hold a value at a particular key, there is no match.
Dict patterns may also use a splat as their last member: `let #{:a 1, ...b} = #{:a 1, :b 2, :c 3}` will bind `b` to `#{:b 2, :c 3}`.
Like tuple and list patterns, dict patterns without a splat at the end match only on exact equivalence on all keys.
## `let` bindings
`let` bindings are the basic form of matching and binding in Ludus. It is written `let {pattern} = {non-binding expression}`. The pattern can be arbitrarily complex. If the left-hand side of a `let` binding does not match, Ludus will raise a panic, halting evaluation of the script.
## Scope and blocks
Ludus is lexically scoped. Bindings are valid for the remainder of the scope they act on. To introduce a new scope, Ludus uses a block, a collection of expressions delimited by curly braces and separated by semicolons or newlines. The value of a block, as well as the value of a script, is the last expression in it. In `let foo = {:this; :set; :of; :expressions; "is actually"; :ignored }`, `foo` will be bound to `:ignored`.
That said, you can use bindings in blocks, which will not be available outside that block--but blocks can use bidnings from their parent scope:
```
let outer = 42
let first = {
let inner = 23
add (outer, inner)
} & first is now bound to 65
inner & Validation error: unbound name inner
```
### Shadowing
Even though names are bound permanently in Ludus, it is perfectly possible (and occasionally quite useful) to "shadow" names from an enclosing scope.
```
let x = 42
let y = {
let first = x
let x = 23 & this is fine
let second = x
add (first, second)
} & y is now 65
```
## Conditional forms
Ludus has a robust set of conditional forms, all of which are expressions and resolve to a single value.
### `if`
`if` evaluates a condition; if the result of the condition is truthy, it evaluates is `then` branch; if the condition is falsy, it evaluates its `else` branch. Both branches must always be present. Newlines may come before `then` and `else`.
`if {simple expression} then {non-binding expression} else {non-binding expression}`
### `when`
`when` is like Lisp's `cond`: it takes a series of clauses, separated by semicolons or newlines, delimited by curly braces. Clauses are written `lhs -> rhs`. `when` expressions are extremely useful for avoiding nested `if`s.
The left hand of a clause is a simple expression; the right hand of a clause is any expression. When the left hand is truthy, the right hand is evaluated, and the result of that evaluation is returned; no further clauses are evaluated. If no clause has a truthy left-hand value, then a panic is raised. In the example below, not the use of literal `true` as an always-matching clause.
```
when {
maybe () -> :something
mabye_not () -> :something_else
true -> :always
}
```
### `match`
A `match` form is the most powerful conditional form in Ludus. It consists of a test expression, and a series of clauses. The test expression must be a simple expression, followed by `with`, and then a series of clauses separated by a semicolon or newline, delimited by curly braces.
```
match may_fail () with {
(:ok, value) -> calculate_result (value)
(:err, msg) -> { log! (msg); recover_somehow () }
}
```
The left hand of a match clause is a pattern; the right hand is an expression: `pattern -> expression`. If the pattern matches the test expression of a clause, the expression is evaluated with any bindings from the pattern, and `match` form evaluates to the result of that expression.
If a test expression does not match against any clause's pattern, a panic is raised.
Ludus does not attempt to do any exhaustiveness checking on match forms; match errors are always runtime errors.
#### Guards
`match` clauses may have a _guard expression_, which allows a clause only to match if the expression's result is truthy. In the previous example, consider that we might want different behaviour depending on the value of the number:
```
match may_fail () with {
(:ok, value) if pos? (value) -> calculate_positive_result (value)
(:ok, value) if neg? (value) -> calculate_negative_result (value)
(:ok, 0) -> do_something_with_zero ()
(:err, msg) -> { log! (msg); recover_somehow () }
}
```
## Functions
Ludus is an emphatically functional language. Almost everything in Ludus is accomplished by applying functions to values, or calling functions with arguments. (These are precise synonyms.)
Functions have the type `:fn`.
### Calling functions
Functions are called by placing a tuple with arguments immediately after a function name, e.g. `add (1, 2)` adds `1` and `2`. Because they are represented as tuples, arguments must be explicitly written; splats cannot be used to pass an arbitrary number of arguments to a function.
### Defining functions
Functions have three increasingly complex forms to define them. All of them include the concept of a function clause, which is just a match clause whose left hand side must be a _tuple_ pattern.
#### Anonymous lambda
An anonymous lambda is written `fn {tuple pattern} -> {expression}`, `fn (x, y) -> if gt? (x, y) then x else add (x, y)`. Lambdas may only have one clause.
#### Named functions
A named function is identical to a lambda, with the one change that a word follows the `fn` reserved word: `fn {name} {tuple pattern} -> {expression}`. E.g., `fn add_1 (x) -> add (x, 1)`. The name of the function is bound for the remainder of the scope.
#### Compound functions
Compound functions are functions that have multiple clauses. They must be named, and in place of a single clause after a name, they consist in one or more clauses, separated by semicolons or newlines, delimited by curly braces. Optionally, compound functions may have a docstring as their first element after the opening curly brace. The docstring explains the function's purpose and use, before any of the function clauses.
An example from Ludus's Prelude:
```
fn some {
"Takes a possibly `nil` value and a default value. Returns the value if it's not `nil`, returns the default if it's `nil`."
(nil, default) -> default
(value, _) -> value
}
```
### Closures
Functions in Ludus are closures: function bodies have access not only to their specific scope, but any enclosing scope. That said, functions only have access to names bound _before_ they are defined; nothing is hoisted in Ludus.
### Mutual recursion and forward declaration
If you try the following, you'll get a validation error:
```
fn stupid_odd? {
(0) -> false
(x) -> supid_even? (dec (x)) & Validation error: unbound name stupid_even?
}
fn stupid_even? {
(0) -> true
(x) -> stupid_odd? (dec (x))
}
```
To allow for mutual recursion, Ludus allows forward declarations, which are written `fn name` without any clauses. In the example above, we would simply put `fn stupid_even?` before we define `stupid_odd?`.
If you declare a function without defining it, however, Ludus will raise a validation error.
### The Prelude
The Prelude is a substantial set of functions that is available in any given Ludus script. (It is, itself, just a Ludus file that has special access to host functions.) Because of that, a large number of functions are always available. The prelude documentation is [here](./prelude.md).
### Partial application
Functions in Ludus can be partially applied by using a placeholder in the arguments. Partial application may only use a single placeholder (partially applied functions are always unary), but it can be anywhere in the arguments: `let add_1 = add(1, _)` or `let double = mult(_, 2)`.
Unary functions and called keywords may _not_ be partially applied: it is redundant.
Because of "partial application," Ludus has a concept of an "argument tuple" (which may include a single placeholder) in addition to a tuple literal (which may not include a placeholder).
### Function pipelines, or `do` forms
In place of nesting function calls inside other function calls, Ludus allows for a more streamlined version of function application: the `do` form or function pipeline. `do` is followed by an initial expression. `do` expressions use `>` as an operator: whatever is on the left hand side of the `>` is passed in as a single argument to whatever is on its right hand side. For example:
```
let silly_result = do 23 >
mult (_, 2) > add (1, _) >
sub (_, 2) > div (_, 9) & silly_result is 5
```
Newlines may appear after any instance of `>` in a `do` expression. That does, however, mean that you must be careful not to accidentally include any trailing `>`s.
### Called keywords
Keywords may be called as functions, in which case they extract the value stored at that key in the value passed in:
```
let foo = #{:a 1, :b 2}
let bar = :a (foo) & `bar` is now 1
```
Called keywords can be used in pipelines.
In addition, keywords may be called when they are bound to names:
```
let foo = #{:a 1, :b 2}
let bar = :a
bar (foo) & => 1
```
## Synthetic expressions
Synthetic expressions are valid combinations of words, keywords, package names, and argument tuples which allow for calling functions and extracting values from associative collections. The root--first term--of a synthetic expression must be a word or a keyword; subsequent terms must be either argument tuples or keywords.
```
let foo = #{:a 1, :b #{:c "bar" :d "baz"}}
let bar = foo :b :c & `bar` is now "bar"
let baz = :b (foo) :d & `baz` is now "baz"
```
## Looping forms
Ludus has optimized tail calls--the most straightforward way to accomplish repeating behaviour is function recursion. There are two additional looping forms, `repeat` and `loop`.
### `repeat`
`repeat` is a help to learners, and is useful for executing side effects multiple times. It is written `repeat {number|word} { {exprs} }`. From turtle graphics:
```
repeat 4 {
forward! (100)
right! (0.25)
}
```
Note that `repeat` does two interesting things:
1. It never returns a value other than `nil`. If it's in the block, it disappears. This is a unique (and frankly, undesirable) property in Ludus.
2. Unlike everything else in Ludus, repeate _requires_ a block, and not simply an expression. You cannot write `repeat 4 forward! (100)`.
### `loop`/`recur`
`loop` and `recur` are largely identical to recursive functions for repetition, but use a special form to allow an anonymous construction and a few guard rails.
The syntax here is `loop <tuple> with { <function clauses> }`. (Or, you can have a single function clause instead of a set of clauses.) The tuple is passed in as the first set of arguments.
```
let xs = [1, 2, 3, 4]
loop (xs, 0) with {
([x], sum) -> add (x, sum) & matches against the last element of the list
([x, ...xs], sum) -> recur (xs, add (x, sum)) & recurs with the tail
} &=> 10
```
`recur` is the recursive call. It must be in tail position--`recur` must be the root of a synthetic expression, in return position. If `recur` is not in tail position, a validation error will be raised.
In addition, `recur` calls must have the same number of arguments as the original tuple passed to `loop`. While Ludus will allow you to write clauses in `loop` forms with a different arity than the original tuple, those will never match.
`recur` calls return to the nearest `loop`. Nested `loop`s are probably a bad idea and should be avoided when possible.
## Environment and context: the toplevel
The "toplevel" of a script are the expressions that are not embedded in other expressions or forms: not inside a block, not a member of a collection, not on the right hand side of a binding, not inside a function body. The toplevel-only forms:
### `import`
`import` allows for the evaluation of other Ludus scripts: `import "path/to/file" as name`. `import` just evaluates that file, and then binds a name to the result of evaluating that script. This, right now, is quite basic: circular imports are currently allowed but will lead to endless recursion; results are not cached, so each `import` in a chain re-evaluates the file; and so on.
Status: not yet implemented.
### `use`
`use` loads the contents of a namespace into a script's context. To ensure that this is statically checkable, this must be at the toplevel.
Status: not yet implemented.
### `pkg`
Packages, `pkg`es, may only be described at the toplevel of a script. This is to ensure they can be statically evaluatable.
### `test`
A `test` expression is a way of ensuring things behave the way you want them to. Run the script in test mode, and these are evaluated. If the expression under `test` returns a truthy value, you're all good! If the expression under `test` returns a falsy value or raises a panic, then Ludus will report which test(s) failed.
```
test "something goes right" eq? (:foo, :foo)
test "something goes wrong" {
let foo = :foo
let bar = :bar
eq? (foo, bar)
} &=> test failed: "something goes wrong" on line 3
```
`test`s must be at the toplevel--or embedded within other tests in _their_ highest level.
Formally: `test <string> <expression>`.
Status: not yet implemented.
## Changing things: `box`es
Ludus does not let you re-bind names. It does, however, have a type that allows for changing values over time: `box`. A box is a place to put things, it has its own identity, it can store whatever you put in it, but to get what's in it, you have to `unbox` it.
Syntactically and semantically, `box`es are straightforward, but do require a bit more overhead than `let` bindings. The idea is that Ludus makes it obvious where mutable state is in a program, as well as where that mutable state may change. It does so elegantly, but with some guardrails that may take a little getting used to.
The type of a `box` is, predictably, `:box`.
```
box foo = 42 & foo is now bound to a _box that contains 42_
add (1, foo) & panic! no match: foo is _not_ a number
store! (foo, 23) & foo is now a box containing 23
update! (foo, inc) & foo is now a ref containing 24
unbox (foo) &=> 23; use unbox to get the value contained in a box
```
### Ending with a bang!
Ludus has a strong naming convention that functions that change state or could cause an explicit panic end in an exclamation point (or, in computer nerd parlance, a "bang"). So anything function that mutates the value held in a reference ends with a bang: `store!` and `update!` take bangs; `unbox` does not.
This convention also includes anything that prints to the console: `print!`, `report!`, `doc!`, `update!`, `store!`, etc.
(Note that there are a few counter-examples to this: math functions that could cause a panic [in place of returning NaN] do not end with bangs: `div`, `inv`, and `mod`; each of these has variants that allow for more graceful error handling).
### Ending with a whimper?
Relatedly, just about any function that returns a boolean value is a predicate function--and has a name that ends with a question mark: `eq?` tests for equality; `box?` tells you if something is a ref or not; `lte?` is less-than-or-equal.
## Errors: panic! in the Ludus script
A special form, `panic!`, halts script execution with the expression that follows as an error message.
```
panic! :oops
if true then :ok else panic! "It's false!"
```
Panics also happen in the following cases:
* a `let` binding pattern has no match against the value of its expression
* a `match` or `when` form has no matching clause
* a function is called with arguments that do not match any of its clauses
* something that is not a function or keyword is called as a function
* a called keyword is partially applied
* `div`, `inv`, or `mod` divides by zero
* `sqrt` takes the square root of a negative number
* certain error handling functions, like `unwrap!` or `assert!`, are invoked on values that cause them to panic
In fact, the only functions in the Prelude which explicitly cause panics are, at current, `div`, `inv`, `mod`, `sqrt`, `unwrap!`, and `assert!`.
### `nil`s, not errors
Ludus, however, tries to return `nil` instead of panicking where it seems prudent. So, for example, attempting to get access a value at a keyword off a number or `nil`, while nonsensical, will return `nil` rather than panicking:
```
let a = true
a :b :c :d :e &=> nil
let b = [1, 2, 3]
at (b, 12) &=> nil
```
### Result tuples
Operations that could fail--especially when you want some information about why--don't always return `nil` on failures. Instead of exceptions or special error values, recoverable errors in Ludus are handled instead by result tuples: `(:ok, value)` and `(:err, msg)`. So, for example, `unwrap!` takes a result tuple and either returns the value in the `:ok` case, or panics in the `:err` case.
Variants of some functions that may have undesirably inexplicit behaviour are written as `{name}/safe`. So, for example, you can get a variant of `div` that returns a result tuple in `div/safe`, which returns `(:ok, result)` when everything's good; and `(:err, "division by zero")` when the divisor is 0.

1574
doc/prelude.md Normal file

File diff suppressed because one or more lines are too long

92
doc/turtle-graphics.md Normal file
View File

@ -0,0 +1,92 @@
# Turtle Graphics protocol
name: "turtle-graphics"
version: 0.1.0
### Description
Turtle graphics describe the movements and drawing behaviours of screen, robot, and print "turtles."
* `proto`: `["turtle-graphics", "{version number}"]`
* `data`: an array of arrays; each array represents a turtle command; the first element of a command array is the verb; any subsequent items are the arguments to the verbs.
* Valid arguments are numbers, strings, and booleans.
* Depending on what we end up doing, we may add arrays of these, representing tuples or lists, and/or objects with string keys whose text are well-formed keywords in Ludus. For now, however, arguments must be atomic values.
* E.g., `["forward", 100]`
* Each turtle has its own stream.
* At current, this protocol describes the behaviour of turtle-like objects, all of which "live" in the same "world"; there is not yet a provision for multiple canvases/worlds. That said, an additional field for "world" in at the top level may well be added in the future to allow for multiple worlds to unfold at the same time.
### Verbs and arguments
* `forward`, steps: number
- Moves the turtle forward by the number of steps/pixels.
* `back`, steps: number
- Moves the turtle backwards by the number of steps/pixels.
* `right`, turns: number
- Turns the turtle right by the number of turns. (1 turn = 360 degrees.)
* `left`, turns: number
- Turns the turtle to the left by the number of turns. (1 turn = 360 degrees.)
* `penup`, no arguments
- "Lifts" the turtle's pen, keeping it from drawing.
* `pendown`, no arguments
- "Lowers" the turtle's pen, starting it drawing a path.
* `pencolor`, red: number, green: number, blue: number, alpha: number, OR: color: string
- Sets the turtle's pen's color to the specified RGBA color.
* `penwidth`, width: number
- Sets the width of the turtle's pen, in pixels (or some other metric).
* `home`, no arguments
- Sends the turtle back to its starting point, with a heading of 0.
* `goto`, x: number, y: number
- Sends the turtle to the specified Cartesian coordinates, where the origin is the turtle's starting position.
* `setheading`, heading: number
- Sets the turtle's heading. 0 is the turtle's starting heading, with increasing numbers turning to the right.
* `show`, no arguments
- Shows the turtle.
* `hide`, no arguments
- Hides the turtle.
* `loadstate`, x: number, y: number, heading: number, visible: boolean, pendown: boolean, width: number, color: string OR r: number, g: number, b: number, a: number
- Loads a turtle state.
* `clear`, no arguments
- Erases any paths drawn and sets the background color to the default.
* `background`, red: number, green: number, blue: number, alpha: number
- Sets the background color to the specified RGBA color, OR: color: string
These last two feel a little weird to me, since the background color is more the property of the **world** the turtle is in, not the turtle itself. Worlds with multiple turtles will be set up so that _any_ turtle will be able to change the background, and erase all paths.
That said, since we don't yet have a world abstraction/entity, then there's no other place to put them. This will likely be shifted around in later versions of the protocol.
### Other considerations
**Not all turtles will know how to do all these things.**
The idea is that this single abstraction will talk to all the turtle-like things we eventually use.
That means that some turtles won't be able to do all the things; that's fine!
They just won't do things they can't do; but warnings should go to `stderr`.
**Errors are not passed back to Ludus.**
These are fire-off commands.
Errors should be _reported_ to `stderr` or equivalent.
But Ludus sending things to its output streams should only cause Ludus panics when there's an issue in Ludus.
**Colors aren't always RGBA.**
For pen-and-paper turtles, we don't have RGBA colors.
Colors should also be specifiable with strings corresponding to CSS basic colors: black, silver, gray, white, maroon, red, purple, fuchsia, green, lime, olive, yellow, navy, blue, teal, and aqua.
**Turtles should communicate states.**
Ludus should have access to turtle states.
This is important for push/pop situations that we use for L-systems.
There are two ways to do this: Ludus does its own bookkeeping for turtle states, or it has a way to get the state from a turtle.
The latter has the value of being instantaneous, and gives us an _expected_ state of the turtle after the commands are all processed.
In particular, this will be necessary for the recursive L-systems that require pushing and popping turtle state.
The latter has the drawback of potentially allowing the turtle state and expected turtle state to fall out of synch.
The former has the value of always giving us the correct, actual state of the turtle.
It has the drawback of requiring such state reporting to be asynchronous, and perhaps wildly asynchronous, as things like moving robots and plotters will take quite some time to actually draw what Ludus tells it to.
(Being able to wait until `eq? (expected, actual)` to do anything else may well be extremely useful.)
That suggests, then, that both forms of turtle state are desirable and necessary.
Thus: turtles should communicate states (and thus there ought to be a protocol for communicating state back to Ludus) and Ludus should always do the bookkeeping of calculating the expected state.
**Turtles use Cartesian, rather than screen, coordinates.**
The starting position of the turtle is `(0, 0)`, which is the origin, and _centred_ in the field of view.
Increasing the x-coordinate moves the turtle to the right; increasing the y-coordinate moves the turtle _up_.
**Turtles use compass headings, not mathematical angles.**
Turtles start pointing vertially, at heading `0`.
Turning right _increases_ the heading; pointing due "east" is `0.25`; south `0.5`, and west, `0.75`.

View File

@ -23,9 +23,9 @@ publish:
# build the ludus jimage
build:
rm build/out.mjs
rm build/out.wasm
rm build/ludus.jimage
rm -f build/out.mjs
rm -f build/out.wasm
rm -f build/ludus.jimage
janet -c src/ludus.janet build/ludus.jimage
cd build && just build
git commit -am "build"

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@ludus/ludus-js-pure",
"version": "0.1.26",
"version": "0.1.41",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ludus/ludus-js-pure",
"version": "0.1.26",
"version": "0.1.41",
"license": "GPL-3.0",
"devDependencies": {
"shadow-cljs": "^2.26.0",

View File

@ -1,6 +1,6 @@
{
"name": "@ludus/ludus-js-pure",
"version": "0.1.26",
"version": "0.1.41",
"description": "A Ludus interpreter in a pure JS function.",
"type": "module",
"main": "build/ludus.mjs",

View File

@ -3,23 +3,5 @@
& the goal is to output any global state held in Ludus
& this does not have base loaded into it, only prelude: must be pure Ludus
if turtle_state () :visible? then render_turtle! () else nil
reset_turtle! ()
& let console_msgs = flush! ()
let (r, g, b, a) = unbox (bgcolor)
store! (bgcolor, colors :black)
let draw_calls = unbox (p5_calls)
store! (p5_calls, [])
#{
& :result result is provided elsewhere
& :errors [] & if we get here there are no errors
& :console console_msgs
:draw concat (
[(:background, r, g, b, a), (:stroke, 255, 255, 255, 255)]
draw_calls)
}
store! (turtle_state, turtle_init)
store! (turtle_commands, [])

View File

@ -2,11 +2,13 @@
& some forward declarations
& TODO: fix this so that we don't need (as many of) them
fn add
fn and
fn append
fn apply_command
fn assoc
fn atan/2
fn count
fn deg/rad
fn dict
fn first
@ -15,9 +17,10 @@ fn get
fn join
fn mod
fn neg?
fn not
fn print!
fn set
fn some?
fn state/call
fn store!
fn string
fn turn/rad
@ -50,8 +53,8 @@ fn ordered? {
fn assoc? {
"Returns true if a value is an associative collection: a dict or a pkg."
(assoc as :dict) -> true
(assoc as :pkg) -> true
(d as :dict) -> true
(p as :pkg) -> true
(_) -> false
}
@ -82,7 +85,7 @@ fn eq? {
(x, y) -> base :eq? (x, y)
(x, y, ...zs) -> if eq? (x, y)
then loop (y, zs) with {
(a, []) -> eq? (a, x)
(a, [b]) -> if eq? (a, x) then eq? (a, b) else false
(a, [b, ...cs]) -> if eq? (a, x)
then recur (b, cs)
else false
@ -90,6 +93,17 @@ fn eq? {
else false
}
fn neq? {
"Returns true if none of the arguments have the same value."
(x) -> false
(x, y) -> not (eq? (x, y))
(x, y, ...zs) -> {
let z_set = set(zs)
let xyz_set = ${x, y, ...z_set}
eq? (count (xyz_set), add(2, count (zs)))
}
}
&&& true & false: boolean logic (part the first)
fn bool? {
"Returns true if a value is of type :boolean."
@ -124,19 +138,6 @@ fn not {
(_) -> false
}
fn neq? {
"Returns true if none of the arguments have the same value."
(x) -> false
(x, y) -> not (eq? (x, y))
(x, y, ...zs) -> if eq? (x, y)
then false
else loop (y, zs) with {
(a, []) -> neq? (a, x)
(a, [b, ...cs]) -> if neq? (a, x)
then recur (b, cs)
else false
}
}
& tuples: not a lot you can do with them functionally
fn tuple? {
@ -227,9 +228,26 @@ fn fold {
}
}
fn foldr {
"Folds a list, right-associative."
(f as :fn, []) -> []
(f as :fn, xs as :list) -> fold (f, xs, f ())
(f as :fn, [], root) -> []
(f as :fn, xs as :list, root) -> loop (root, first (xs), rest (xs)) with {
(prev, curr, []) -> f (curr, prev)
(prev, curr, remaining) -> recur (
f (curr, prev)
first (remaining)
rest (remaining)
)
}
}
& TODO: optimize these with base :conj!
fn map {
"Maps a function over a list: returns a new list with elements that are the result of applying the function to each element in the original list. E.g., `map ([1, 2, 3], inc) &=> [2, 3, 4]`."
(f as :fn) -> map (f, _)
(f as :fn, xs) -> {
fn mapper (prev, curr) -> append (prev, f (curr))
fold (mapper, xs, [])
@ -241,18 +259,21 @@ fn map {
}
fn filter {
"Takes a list and a predicate function, and returns a new list with only the items that produce truthy values when the function is called on them. E.g., `filter ([1, 2, 3, 4], odd?) &=> [1, 3]`."
(p? as :fn, xs) -> {
"Takes a list and a predicate function, and returns a new list with only the items that produce truthy values when the function is called on them. E.g., `filter ([1, 2, 3, 4], odd?) &=> [1, 3]`. Also works on sets."
(p? as :fn) -> filter(p?, _)
(p? as :fn, xs as :list) -> {
fn filterer (filtered, x) -> if p? (x)
then append (filtered, x)
else filtered
fold (filterer, xs, [])
}
(p? as :fn, xs as :set) -> do xs > list > filter (p?, _) > set
}
fn keep {
"Takes a list and returns a new list with any `nil` values omitted."
(xs) -> filter (some?, xs)
(xs as :list) -> filter (some?, xs)
(xs as :set) -> filter (some?, xs)
}
fn append {
@ -280,9 +301,10 @@ fn concat {
}
fn set {
"Takes an ordered collection--list or tuple--and turns it into a set."
"Takes an ordered collection--list or tuple--and turns it into a set. Returns sets unharmed."
(xs as :list) -> fold (append, xs, ${})
(xs as :tuple) -> do xs > list > set
(xs as :set) -> xs
}
fn set? {
@ -293,8 +315,9 @@ fn set? {
fn contains? {
"Returns true if a set or list contains a value."
(value, set as :set) -> bool (base :get (set, value))
(value, list as :list) -> contains? (value, set (list))
(value, s as :set) -> bool (base :get (s, value))
(value, l as :list) -> contains? (value, set (list))
(key, d as :dict) -> do d > key > some?
}
fn omit {
@ -345,8 +368,8 @@ fn string {
(x as :string) -> x
(x) -> show (x)
(x, ...xs) -> loop (x, xs) with {
(out, [x]) -> concat (out, show (x))
(out, [x, ...xs]) -> recur (concat (out, show (x)), xs)
(out, [y]) -> concat (out, show (y))
(out, [y, ...ys]) -> recur (concat (out, show (y)), ys)
}
}
@ -386,6 +409,19 @@ fn downcase {
(str as :string) -> base :downcase (str)
}
fn chars {
"Takes a string and returns its characters as a list. Works only for strings with only ascii characters. Panics on any non-ascii characters."
(str as :string) -> match base :chars (str) with {
(:ok, chrs) -> chrs
(:err, msg) -> panic! msg
}
}
fn chars/safe {
"Takes a string and returns its characters as a list, wrapped in a result tuple. Works only for strings with only ascii characters. Returns an error tuple on any non-ascii characters."
(str as :string) -> base :chars (str)
}
fn ws? {
"Tells if a string is a whitespace character."
(" ") -> true
@ -410,9 +446,9 @@ fn words {
(str as :string) -> {
let no_punct = strip (str)
let strs = split (no_punct, " ")
fn worder (list, str) -> if empty? (str)
then list
else append (list, str)
fn worder (l, s) -> if empty? (s)
then l
else append (l, s)
fold (worder, strs, [])
}
}
@ -422,6 +458,11 @@ fn sentence {
(strs as :list) -> join (strs, " ")
}
fn to_number {
"Takes a string that presumably contains a representation of a number, and tries to give you back the number represented. Returns a result tuple."
(num as :string) -> base :to_number (num)
}
&&& boxes: mutable state and state changes
fn box? {
@ -489,6 +530,11 @@ fn mult {
((x, y), scalar as :number) -> mult (scalar, (x, y))
}
fn pow {
"Raises a number to the power of another number."
(x as :number, y as :number) -> base :pow (x, y)
}
fn div {
"Divides numbers. Panics on division by zero."
(x as :number) -> x
@ -731,31 +777,31 @@ fn or {
fn assoc {
"Takes a dict, key, and value, and returns a new dict with the key set to value."
() -> #{}
(dict as :dict) -> dict
(dict as :dict, key as :keyword, value) -> base :assoc (dict, key, value)
(dict as :dict, (key as :keyword, value)) -> base :assoc (dict, key, value)
(d as :dict) -> d
(d as :dict, k as :keyword, val) -> base :assoc (d, k, val)
(d as :dict, (k as :keyword, val)) -> base :assoc (d, k, val)
}
fn dissoc {
"Takes a dict and a key, and returns a new dict with the key and associated value omitted."
(dict as :dict) -> dict
(dict as :dict, key as :keyword) -> base :dissoc (dict, key)
(d as :dict) -> d
(d as :dict, k as :keyword) -> base :dissoc (d, k)
}
fn update {
"Takes a dict, key, and function, and returns a new dict with the key set to the result of applying the function to original value held at the key."
(dict as :dict) -> dict
(dict as :dict, key as :keyword, updater as :fn) -> base :assoc (dict, key, updater (get (key, dict)))
(d as :dict) -> d
(d as :dict, k as :keyword, updater as :fn) -> base :assoc (d, k, updater (get (k, d)))
}
fn keys {
"Takes a dict and returns a list of keys in that dict."
(dict as :dict) -> do dict > list > map (first, _)
(d as :dict) -> do d > list > map (first, _)
}
fn values {
"Takes a dict and returns a list of values in that dict."
(dict) -> do dict > list > map (second, _)
(d as :dict) -> do d > list > map (second, _)
}
fn diff {
@ -788,28 +834,28 @@ fn diff {
& TODO: consider merging `get` and `at`
fn get {
"Takes a key, dict, and optional default value; returns the value at key. If the value is not found, returns nil or the default value."
(key as :keyword) -> get (key, _)
(key as :keyword, dict as :dict) -> get (key, dict, nil)
(key as :keyword, dict as :dict, default) -> base :get (key, dict, default)
(k as :keyword) -> get (k, _)
(k as :keyword, d as :dict) -> get (k, d, nil)
(k as :keyword, d as :dict, default) -> base :get (k, d, default)
}
& TODO: add sets to this?
fn has? {
"Takes a key and a dict, and returns true if there is a non-`nil` value stored at the key."
(key as :keyword) -> has? (key, _)
(key as :keyword, dict as :dict) -> do dict > key > nil?
(k as :keyword) -> has? (k, _)
(k as :keyword, d as :dict) -> do d> k > nil?
}
fn dict {
"Takes a list or tuple of (key, value) tuples and returns it as a dict. Returns dicts unharmed."
(dict as :dict) -> dict
(list as :list) -> fold (assoc, list)
(tup as :tuple) -> do tup > list > dict
(d as :dict) -> d
(l as :list) -> fold (assoc, l)
(t as :tuple) -> do t > list > dict
}
fn dict? {
"Returns true if a value is a dict."
(dict as :dict) -> true
(d as :dict) -> true
(_) -> false
}
@ -856,10 +902,10 @@ fn tan {
fn rotate {
"Rotates a vector by an angle. Default angle measure is turns. An optional keyword argument specifies the units of the angle passed in."
((x, y), angle) -> rotate ((x, y), angle, :turns)
((x, y), angle, units as :keyword) -> (
sub (mult (x, cos (angle, units)), mult (y, sin (angle, units)))
add (mult (x, sin (angle, units)), mult (y, cos (angle, units)))
((x, y), a) -> rotate ((x, y), a, :turns)
((x, y), a, units as :keyword) -> (
sub (mult (x, cos (a, units)), mult (y, sin (a, units)))
add (mult (x, sin (a, units)), mult (y, cos (a, units)))
)
}
@ -904,21 +950,21 @@ fn atan/2 {
}
fn mod {
"Returns the modulus of num and div. Truncates towards negative infinity. Panics if div is 0."
(num as :number, 0) -> panic! "Division by zero."
(num as :number, div as :number) -> base :mod (num, div)
"Returns the modulus of x and y. Truncates towards negative infinity. Panics if y is 0."
(x as :number, 0) -> panic! "Division by zero."
(x as :number, y as :number) -> base :mod (x, y)
}
fn mod/0 {
"Returns the modulus of num and div. Truncates towards negative infinity. Returns 0 if div is 0."
(num as :number, 0) -> 0
(num as :number, div as :number) -> base :mod (num, div)
"Returns the modulus of x and y. Truncates towards negative infinity. Returns 0 if y is 0."
(x as :number, 0) -> 0
(x as :number, y as :number) -> base :mod (x, y)
}
fn mod/safe {
"Returns the modulus of num and div in a result tuple, or an error if div is 0. Truncates towards negative infinity."
(num as :number, 0) -> (:err, "Division by zero.")
(num as :number, div as :number) -> (:ok, base :mod (num, div))
"Returns the modulus of x and y in a result tuple, or an error if y is 0. Truncates towards negative infinity."
(x as :number, 0) -> (:err, "Division by zero.")
(x as :number, y as :number) -> (:ok, base :mod (x, y))
}
fn square {
@ -955,9 +1001,18 @@ fn dist {
((x, y)) -> dist (x, y)
}
fn heading/vector {
"Takes a turtle heading, and returns a unit vector of that heading."
(heading) -> {
& 0 is 90º/0.25T, 0.25 is 180º/0.5T, 0.5 is 270º/0.75T, 0.75 is 0º/0T
let a = add (neg (heading), 0.25)
(cos (a), sin (a))
}
}
&&& more number functions
fn random {
"Returns a random something. With zero arguments, returns a random number between 0 (inclusive) and 1 (exclusive). With one argument, returns a random number between 0 and n. With two arguments, returns a random number between m and n. Alternately, given a collection (list, dict, set), it returns a random member of that collection."
"Returns a random something. With zero arguments, returns a random number between 0 (inclusive) and 1 (exclusive). With one argument, returns a random number between 0 and n. With two arguments, returns a random number between m and n. Alternately, given a collection (tuple, list, dict, set), it returns a random member of that collection."
() -> base :random ()
(n as :number) -> mult (n, random ())
(m as :number, n as :number) -> add (m, random (sub (n, m)))
@ -965,6 +1020,10 @@ fn random {
let i = do l > count > random > floor
at (l, i)
}
(t as :tuple) -> {
let i = do t > count > random > floor
at (t, i)
}
(d as :dict) -> {
let key = do d > keys > random
get (key, d)
@ -993,6 +1052,11 @@ fn round {
(n as :number) -> base :round (n)
}
fn int? {
"Tells if a number is an integer."
(n as :number) -> eq? (n, floor (n))
}
fn range {
"Returns the set of integers between start (inclusive) and end (exclusive) as a list: [start, end). With one argument, starts at 0. If end is less than start, returns an empty list."
(end as :number) -> base :range (0, end)
@ -1055,6 +1119,7 @@ let colors = #{
:black (0, 0, 0, 255)
:silver (192, 192, 192, 255)
:gray (128, 128, 128, 255)
:grey (128, 128, 128, 255)
:white (255, 255, 255, 255)
:maroon (128, 0, 0, 255)
:red (255, 0, 0, 255)
@ -1067,15 +1132,17 @@ let colors = #{
:navy (0, 0, 128, 255)
:blue (0, 0, 255, 255)
:teal (0, 128, 128, 255)
:aqua (0, 255, 25, 255)
:aqua (0, 255, 255, 255)
}
let colours = colors
& the initial turtle state
let turtle_init = #{
:position (0, 0) & let's call this the origin for now
:heading 0 & this is straight up
:pendown? true
:pencolor colors :white
:pencolor :white
:penwidth 1
:visible? true
}
@ -1083,101 +1150,14 @@ let turtle_init = #{
& turtle states: refs that get modified by calls
& turtle_commands is a list of commands, expressed as tuples
box turtle_commands = []
& and a list of turtle states
box turtle_states = [turtle_init]
fn reset_turtle! {
"Resets the turtle to its original state."
() -> store! (turtle_states, [turtle_init])
}
& and a list of calls to p5--at least for now
box p5_calls = []
& ...and finally, a background color
& we need to store this separately because, while it can be updated later,
& it must be the first call to p5.
box bgcolor = colors :black
fn add_call! (call) -> update! (p5_calls, append! (_, call))
box turtle_state = turtle_init
fn add_command! (command) -> {
update! (turtle_commands, append! (_, command))
let prev = do turtle_states > unbox > last
let prev = unbox (turtle_state)
let curr = apply_command (prev, command)
update! (turtle_states, append! (_, curr))
let call = state/call ()
if call then { add_call! (call); :ok } else :ok
}
fn make_line ((x1, y1), (x2, y2)) -> (:line, x1, y1, x2, y2)
let turtle_radius = 20
let turtle_angle = 0.385
let turtle_color = (255, 255, 255, 150)
fn render_turtle! () -> {
let state = do turtle_states > unbox > last
if state :visible?
then {
let (r, g, b, a) = turtle_color
add_call! ((:fill, r, g, b, a))
let #{heading
:pencolor (pen_r, pen_g, pen_b, pen_a)
:position (x, y)
pendown?
...} = state
let first = mult ((0, 1), turtle_radius)
let (x1, y1) = first
let (x2, y2) = rotate (first, turtle_angle)
let (x3, y3) = rotate (first, neg (turtle_angle))
add_call! ((:push))
add_call! ((:translate, x, y))
add_call! ((:rotate, turn/rad (heading)))
add_call! ((:noStroke))
add_call! ((:beginShape))
add_call! ((:vertex, x1, y1))
add_call! ((:vertex, x2, y2))
add_call! ((:vertex, x3, y3))
add_call! ((:endShape))
& there's a happy bug here: the stroke will be the same width as the pen width. Keep this for now. Consider also showing the pen colour here?
add_call! ((:stroke, pen_r, pen_g, pen_b, pen_a))
if pendown? then add_call! ((:line, 0, 0, x1, y1)) else nil
add_call! ((:pop))
:ok
}
else :ok
}
fn state/call () -> {
let cmd = do turtle_commands > unbox > last > first
let states = unbox (turtle_states)
let curr = last (states)
let prev = at (states, sub (count (states), 2))
match cmd with {
:forward -> if curr :pendown?
then make_line (prev :position, curr :position)
else nil
:back -> if curr :pendown?
then make_line (prev :position, curr :position)
else nil
:home -> if curr :pendown?
then make_line (prev :position, curr :position)
else nil
:goto -> if curr :pendown?
then make_line (prev :position, curr :position)
else nil
:penwidth -> (:strokeWeight, curr :penwidth)
:pencolor -> {
let (r, g, b, a) = curr :pencolor
(:stroke, r, g, b, a)
}
:clear -> (:background, 0, 0, 0, 255)
_ -> nil
}
store! (turtle_state, curr)
:ok
}
fn forward! {
@ -1224,11 +1204,13 @@ let pd! = pendown!
fn pencolor! {
"Changes the turtle's pen color. Takes a single grayscale value, an rgb tuple, or an rgba tuple. Alias: pc!"
(color as :keyword) -> if contains? (colors, color) then add_command! ((:pencolor, color)) else panic! "Ludus doesn't know about the color {color}"
(gray as :number) -> add_command! ((:pencolor, (gray, gray, gray, 255)))
((r as :number, g as :number, b as :number)) -> add_command! ((:pencolor, (r, g, b, 255)))
((r as :number, g as :number, b as :number, a as :number)) -> add_command! ((:pencolor, (r, g, b, a)))
}
let pencolour! = pencolor!
let pc! = pencolor!
fn penwidth! {
@ -1240,9 +1222,10 @@ let pw! = penwidth!
fn background! {
"Sets the background color behind the turtle and path. Alias: bg!"
(gray as :number) -> store! (bgcolor, (gray, gray, gray, 255))
((r as :number, g as :number, b as :number)) -> store! (bgcolor, (r, g, b, 255))
((r as :number, g as :number, b as :number, a as :number)) -> store! (bgcolor, (r, g, b, a))
(color as :keyword) -> if contains? (colors, color) then add_command! ((:background, color)) else panic! "Ludus doesn't know about the color {color}"
(gray as :number) -> add_command! ((:background, (gray, gray, gray, 255)))
((r as :number, g as :number, b as :number)) -> add_command! ((:background, (r, g, b, 255)))
((r as :number, g as :number, b as :number, a as :number)) -> add_command! ((:background, (r, g, b, a)))
}
let bg! = background!
@ -1278,12 +1261,11 @@ fn hideturtle! {
() -> add_command! ((:hide))
}
fn heading/vector {
"Takes a turtle heading, and returns a unit vector of that heading."
(heading) -> {
& 0 is 90º/0.25T, 0.25 is 180º/0.5T, 0.5 is 270º/0.75T, 0.75 is 0º/0T
let angle = add (heading, 0.25)
(cos (angle), sin (angle))
fn loadstate! {
"Sets the turtle state to a previously saved state."
(state) -> {
let #{position, heading, pendown?, pencolor, penwidth, visible?} = state
add_command! ((:loadstate, position, heading, visible?, pendown?, penwidth, pencolor))
}
}
@ -1316,49 +1298,57 @@ fn apply_command {
(:penwidth, pixels) -> assoc (state, :penwidth, pixels)
(:pencolor, color) -> assoc (state, :pencolor, color)
(:setheading, heading) -> assoc (state, :heading, heading)
(:loadstate, position, heading, visible?, pendown?, penwidth, pencolor) -> #{position, heading, visible?, pendown?, penwidth, pencolor}
(:show) -> assoc (state, :visible?, true)
(:hide) -> assoc (state, :visible?, false)
}
}
fn turtle_state {
"Returns the turtle's current state."
() -> do turtle_states > unbox > last
}
fn load_turtle_state! {
"Sets the turtle state to a previously saved state. Returns the state."
(state) -> {
update! (turtle_states, append! (_, state))
let call = state/call ()
if call then { add_call! (call); :ok } else :ok
(:background, _) -> state
}
}
& position () -> (x, y)
fn position {
"Returns the turtle's current position."
() -> turtle_state () :position
() -> do turtle_state > unbox > :position
}
fn heading {
"Returns the turtle's current heading."
() -> turtle_state () :heading
() -> do turtle_state > unbox > :heading
}
fn pendown? {
"Returns the turtle's pen state: true if the pen is down."
() -> turtle_state () :pendown?
() -> do turtle_state > unbox > :pendown?
}
fn pencolor {
"Returns the turtle's pen color as an (r, g, b, a) tuple."
() -> turtle_state () :pencolor
"Returns the turtle's pen color as an (r, g, b, a) tuple or keyword."
() -> do turtle_state > unbox > :pencolor
}
fn penwidth {
"Returns the turtle's pen width in pixels."
() -> turtle_state () :penwidth
() -> do turtle_state > unbox > :penwidth
}
fn cons {
"Old-timey lisp `cons`. `Cons`tructs a tuple out of two arguments."
(x, y) -> (x, y)
}
fn car {
"Old-timey lisp `car`. Stands for 'contents of the address register.' Returns the first element in a `cons`ed pair (or any two-tuple)."
((x, _)) -> x
}
fn cdr {
"Old-timey list `cdr`. Stands for 'contents of the decrement register.' Returns the second element in a `cons`ed pair, usually representing the rest of the list."
((_, x)) -> x
}
fn llist {
"Makes an old-timey linked list of its arguments, of LISt Processor fame."
(...xs) -> foldr (cons, xs, nil)
}
box state = nil
@ -1379,17 +1369,20 @@ pkg Prelude {
background! & turtles
between? & math
bg! & turtles
bgcolor & turtles
bk! & turtles
bool & bool
bool? & bool
box? & boxes
butlast & lists strings tuples
car & llist
cdr & llist
ceil & math
chars & strings
clear! & turtles
coll? & dicts lists sets tuples
colors & turtles
concat & string list set
cons & llist
contains? & list set
cos & math
count & string list set tuple dict
@ -1419,6 +1412,7 @@ pkg Prelude {
floor & math
fn? & functions
fold & lists
foldr & lists
forward! & turtles
get & dicts
goto! & turtles
@ -1429,6 +1423,7 @@ pkg Prelude {
hideturtle! & turtles
home! & turtles
inc & math
int? & math
inv & math
inv/0 & math
inv/safe & math
@ -1440,7 +1435,7 @@ pkg Prelude {
left! & turtles
list & lists
list? & lists
load_turtle_state! & turtles
loadstate! & turtles
lt! & turtles
lt? & math
lte? & math
@ -1462,7 +1457,6 @@ pkg Prelude {
omit & set
or & bool
ordered? & lists tuples strings
p5_calls & turtles
pc! & turtles
pd! & turtles
pencolor & turtles
@ -1475,6 +1469,7 @@ pkg Prelude {
pi & math
pos? & math
position & turtles
pow & math
print! & environment
pu! & turtles
pw! & turtles
@ -1483,9 +1478,7 @@ pkg Prelude {
random & math dicts lists tuples sets
random_int & math
range & math lists
render_turtle! & turtles
report! & environment
reset_turtle! & turtles
rest & lists tuples
right! & turtles
round & math
@ -1514,13 +1507,14 @@ pkg Prelude {
sum_of_squares & math
tan & math
tau & math
to_number & strings numbers
trim & strings
tuple? & tuples
turn/deg & math
turn/rad & math
turtle_commands & turtles
turtle_init & turtles
turtle_state & turtles
turtle_states & turtles
type & values
unbox & boxes
unwrap! & results

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,8 @@
# A base library for Ludus
# Only loaded in the prelude
(import /src/scanner :as s)
(defn bool [x] (if (= :^nil x) nil x))
(defn ludus/and [& args] (every? (map bool args)))
@ -129,7 +131,7 @@
:typed (string (show-patt (get-in x [:data 1])) " as " (show-patt (get-in x [:data 0])))
:interpolated (get-in x [:token :lexeme])
:string (get-in x [:token :lexeme])
:splat (string "..." (when (x :splatted) (show-patt (x :splatted))))
:splat (string "..." (when (x :data) (show-patt (x :data))))
(error (string "cannot show pattern of unknown type " (x :type)))))
(defn pretty-patterns [fnn]
@ -235,6 +237,33 @@
(defn mod [x y]
(% x y))
(defn- byte->ascii [c i]
(if (< c 128)
(string/from-bytes c)
(error (string "non-ASCII character at index" i))))
(defn chars [str]
(def out @[])
(try
(for i 0 (length str)
(array/push out (byte->ascii (str i) i)))
([e] (break [:err e])))
[:ok out])
(defn to_number [str]
(when (string/find "&" str)
(break [:err (string "Could not parse `" str "` as a number")]))
(def scanned (s/scan (string/trim str)))
(when (< 0 (length (scanned :errors)))
(break [:err (string "Could not parse `" str "` as a number")]))
(def tokens (scanned :tokens))
(when (< 3 (length tokens))
(break [:err (string "Could not parse `" str "` as a number")]))
(def fst (first tokens))
(when (not= :number (fst :type))
(break [:err (string "Could not parse `" str "` as a number")]))
[:ok (fst :literal)])
(def ctx {
"add" +
"and" ludus/and
@ -243,6 +272,7 @@
"atan_2" math/atan2
"bool" bool
"ceil" math/ceil
"chars" chars
"concat" concat
"conj!" conj!
"conj" conj
@ -256,6 +286,7 @@
"div" /
"doc" doc
"downcase" string/ascii-lower
"e" math/e
"eq?" deep=
"first" first
"floor" math/floor
@ -272,6 +303,7 @@
"nth" ludus/get
"or" ludus/or
"pi" math/pi
"pow" math/pow
"print!" print!
"prn" prn
"push" array/push
@ -290,6 +322,7 @@
"sub" -
"tan" math/tan
"to_list" to_list
"to_number" to_number
"trim" string/trim
"triml" string/triml
"trimr" string/trimr
@ -298,10 +331,8 @@
"upcase" string/ascii-upper
})
(def base (let [b @{}]
(def base (let [b @{:^type :dict}]
(each [k v] (pairs ctx)
(set (b (keyword k)) v))
b))
(set (base :^type) :dict)

View File

@ -12,7 +12,8 @@
(defn escape-punctuation [str] (->> str
(string/replace "?" "")
(string/replace "!" "")))
(string/replace "!" "")
(string/replace "/" "")))
(defn toc-entry [name]
(def escaped (escape-underscores name))
@ -22,18 +23,19 @@
(string/join (map toc-entry sorted-names) "&nbsp;&nbsp;&nbsp; "))
(def topics {
"math" ["abs" "add" "angle" "atan/2" "between?" "ceil" "cos" "dec" "deg/rad" "deg/turn" "dist" "div" "div/0" "div/safe" "even?" "floor" "gt?" "gte?" "heading/vector" "inc" "inv" "inv/0" "inv/safe" "lt?" "lte?" "max" "min" "mod" "mod/0" "mod/safe" "mult" "neg" "neg?" "odd?" "pi" "pos?" "rad/deg" "rad/turn" "random" "random_int" "range" "round" "sin" "sqrt" "sqrt/safe" "square" "sub" "sum_of_squares" "tan" "tau" "turn/deg" "turn/rad" "zero?"]
"math" ["abs" "add" "angle" "atan/2" "between?" "ceil" "cos" "dec" "deg/rad" "deg/turn" "dist" "div" "div/0" "div/safe" "even?" "floor" "gt?" "gte?" "heading/vector" "inc" "inv" "inv/0" "inv/safe" "lt?" "lte?" "max" "min" "mod" "mod/0" "mod/safe" "mult" "neg" "neg?" "odd?" "pi" "pos?" "rad/deg" "rad/turn" "random" "random_int" "range" "round" "sin" "sqrt" "sqrt/safe" "square" "sub" "sum_of_squares" "tan" "tau" "to_number" "turn/deg" "turn/rad" "zero?"]
"boolean" ["and" "bool" "bool?" "false?" "not" "or" "true?"]
"dicts" ["any?" "assoc" "assoc?" "coll?" "count" "dict" "dict?" "diff" "dissoc" "empty?" "get" "keys" "random" "update" "values"]
"lists" ["any?" "append" "at" "butlast" "coll?" "concat" "count" "each!" "empty?" "filter" "first" "fold" "join" "keep" "last" "list" "list?" "map" "ordered?" "random" "range" "rest" "second" "sentence" "slice"]
"llists" ["car" "cdr" "cons" "llist"]
"sets" ["any?" "append" "coll?" "concat" "contains?" "count" "empty?" "omit" "random" "set" "set?"]
"tuples" ["any?" "at" "coll?" "count" "empty?" "first" "last" "ordered?" "rest" "second" "tuple?"]
"strings" ["any?" "concat" "count" "downcase" "empty?" "join" "sentence" "show" "slice" "split" "string" "string?" "strip" "trim" "upcase" "words"]
"strings" ["any?" "chars" "chars/safe" "concat" "count" "downcase" "empty?" "join" "sentence" "show" "slice" "split" "string" "string?" "strip" "to_number" "trim" "upcase" "words"]
"types and values" ["assoc?" "bool?" "box?" "coll?" "dict?" "eq?" "fn?" "keyword?" "list?" "neq?" "nil?" "number?" "ordered?" "set?" "show" "some" "some?" "string?" "tuple?" "type"]
"boxes and state" ["box?" "unbox" "store!" "update!"]
"results" ["err" "err?" "ok" "ok?" "unwrap!" "unwrap_or"]
"errors" ["assert!"]
"turtle graphics" ["back!" "background!" "bk!" "clear!" "colors" "fd!" "forward!" "goto!" "heading" "heading/vector" "home!" "left!" "lt!" "pc!" "pd!" "pencolor" "pencolor!" "pendown!" "pendown?" "penup!" "penwidth" "penwidth!" "position" "pu!" "pw!" "render_turtle!" "reset_turtle!" "right!" "rt!" "turtle_state"]
"turtle graphics" ["back!" "background!" "bk!" "clear!" "colors" "fd!" "forward!" "goto!" "heading" "heading/vector" "hideturtle!" "home!" "left!" "loadstate!" "lt!" "pc!" "pd!" "pencolor" "pencolor!" "pendown!" "pendown?" "penup!" "penwidth" "penwidth!" "position" "pu!" "pw!" "render_turtle!" "reset_turtle!" "right!" "rt!" "setheading!" "showturtle!" "turtle_state"]
"environment and i/o" ["doc!" "print!" "report!" "state"]
})

View File

@ -345,7 +345,7 @@
(set (the-dict key) value))))
the-dict)
(defn- ref [ast ctx]
(defn- box [ast ctx]
(def {:data value-ast :name name} ast)
(def value (interpret value-ast ctx))
(def box @{:^type :box :^value value :name name})
@ -460,6 +460,7 @@
[:function :tuple] (call-fn root-ast prev curr)
# [:applied :tuple] (call-partial root-ast prev curr)
[:keyword :args] (get (first curr) prev :^nil)
[:keyword :tuple] (get (first curr) prev :^nil)
[:dict :keyword] (get prev curr :^nil)
[:nil :keyword] :^nil
[:pkg :keyword] (get prev curr :^nil)
@ -473,7 +474,7 @@
# (pp terms)
(def first-term (first terms))
(def last-term (last terms))
(var prev (interpret first-term ctx))
(var prev (interpret first-term ctx))
# (print "root term: ")
# (pp prev)
(for i 1 (-> terms length dec)
@ -489,9 +490,9 @@
(def last-term (last terms))
(for i 1 (-> terms length dec)
(def curr (interpret (terms i) ctx))
(set prev (call-fn (first terms) curr [prev])))
(set prev (apply-synth-term (first terms) curr [prev])))
(def last-fn (interpret last-term ctx))
(call-fn (first terms) last-fn [prev]))
(apply-synth-term (first terms) last-fn [prev]))
(defn- pkg [ast ctx]
(def members (ast :data))
@ -594,7 +595,7 @@
# named/naming forms
:word (word ast ctx)
:interpolated (interpolated ast ctx)
:ref (ref ast ctx)
:box (box ast ctx)
:pkg (pkg ast ctx)
:pkg-name (word ast ctx)

View File

@ -12,50 +12,87 @@
(import /src/json :as j)
(defn ludus [source]
# if we can't load prelude, bail
(when (= :error prelude/pkg) (error "could not load prelude"))
# get us a clean working slate
(def ctx @{:^parent prelude/ctx})
(def errors @[])
(def draw @[])
(var result @"")
(def console @"")
# capture all `print`s
(setdyn :out console)
(def out @{:errors errors :draw draw :result result :console console})
# an output table
# this will change: the shape of our output
# at the moment, there's only one stack of turtle graphics
# we will be getting more
(def out @{:errors errors :result result
:io @{
:stdout @{:proto [:text-stream "0.1.0"] :data console}
:turtle @{:proto [:turtle-graphics "0.1.0"] :data @[]}}})
### start the program
# first, scanning
(def scanned (s/scan source))
(when (any? (scanned :errors))
(each err (scanned :errors)
(e/scan-error err))
(break (-> out j/encode string)))
# then, parsing
(def parsed (p/parse scanned))
(when (any? (parsed :errors))
(each err (parsed :errors)
(e/parse-error err))
(break (-> out j/encode string)))
# then, validation
(def validated (v/valid parsed ctx))
(when (any? (validated :errors))
(each err (validated :errors)
(e/validation-error err))
(break (-> out j/encode string)))
(try
(set result (i/interpret (parsed :ast) ctx))
# and, finally, try interpreting the program
(try (do
# we need to do this every run or we get the very same sequence of "random" numbers every time we run a program
(math/seedrandom (os/cryptorand 8))
(set result (i/interpret (parsed :ast) ctx)))
([err]
(e/runtime-error err)
(break (-> out j/encode string))))
(setdyn :out stdout)
(set (out :result) (b/show result))
(var post @{})
(try
(set post (i/interpret prelude/post/ast ctx))
([err] (e/runtime-error err)))
(set (out :draw) (post :draw))
# out
(-> out j/encode string)
)
# stop capturing output
(setdyn :out stdout)
# update our output table with our output
(set (out :result) (b/show result))
(set (((out :io) :turtle) :data) (get-in prelude/pkg [:turtle_commands :^value]))
# run the "postlude": any Ludus code that needs to run after each program
# right now this is just resetting the boxes that hold turtle commands and state
(try
(i/interpret prelude/post/ast ctx)
([err] (e/runtime-error err)))
# json-encode our output table, and convert it from a buffer to a string (which we require for playing nice with WASM/C)
(-> out j/encode string))
#### REPL
(comment
# (do
(def start (os/clock))
(def source `
at ("aéc", 3)
fn fib {
(1) -> 1
(2) -> 1
(n) -> add (
fib (sub (n, 1))
fib (sub (n, 2))
)
}
fib (30)
`)
(def out (-> source
ludus

View File

@ -3,6 +3,9 @@
### We still need to scan some things
(import /src/scanner :as s)
# stash janet type
(def janet-type type)
(defmacro declare
"Forward-declares a function name, so that it can be called in a mutually recursive manner."
[& names]
@ -18,6 +21,26 @@
(if-not (dyn name) (error "recursive functions must be declared before they are defined"))
~(set ,name (defn- ,name ,;forms)))
### Some more human-readable formatting
(defn- pp-tok [token]
(if (not token) (break "nil"))
(def {:line line :lexeme lex :type type :start start} token)
(string "<" line "[" start "]" ": " type ": " lex ">"))
(defn- pp-ast [ast &opt indent]
(default indent 0)
(def {:token token :data data :type type} ast)
(def pretty-tok (pp-tok token))
(def data-rep (if (= :array (janet-type data))
(string "[\n"
(string/join (map (fn [x] (pp-ast x (inc indent))) data)
(string (string/repeat " " indent) "\n"))
"\n" (string/repeat " " indent) "]")
data
))
(string (string/repeat " " indent) type ": " pretty-tok " " data-rep)
)
### Next: a data structure for a parser
(defn- new-parser
"Creates a new parser data structure to pass around"
@ -75,7 +98,9 @@
(has-value? terminators ttype))
# breakers are what terminate panics
(def breaking [:break :newline :semicolon :comma :eof :then :else])
(def breaking [:break :newline :semicolon :comma :eof
# :then :else :arrow
])
(defn- breaks?
"Returns true if the current token in the parser should break a panic"
@ -89,12 +114,12 @@
[parser message]
# (print "Panic in the parser: " message)
(def origin (current parser))
(advance parser)
(def skipped @[origin])
(def skipped @[])
(while (not (breaks? parser))
(array/push skipped (current parser))
(advance parser))
(array/push skipped (current parser))
# (advance parser)
(def err {:type :error :data skipped :token origin :msg message})
(update parser :errors array/push err)
(error err))
@ -279,8 +304,10 @@
(def origin (current parser))
(advance parser) # consume the :lparen
(def ast @{:type :args :data @[] :token origin :partial false})
(while (separates? parser) (advance parser)) # consume any separators
(while (not (check parser :rparen))
(accept-many parser :newline :comma)
(when (= :break ((current parser) :type))
(break (advance parser)))
(when (check parser :eof)
(def err {:type :error :token origin :msg "unclosed paren"})
(array/push (parser :errors) err)
@ -299,8 +326,7 @@
{:type :placeholder :token origin}))
(capture nonbinding parser)))
(array/push (ast :data) term)
(try (separators parser)
([e] (array/push (ast :data) e))))
(capture separators parser))
(advance parser)
ast)
@ -333,20 +359,26 @@
{:type :synthetic :data [;terms] :token origin})
# collections
### XXX: the current panic/capture structure in this, script, etc. is blowing up when the LAST element (line, tuple member, etc.) has an error
# it does, however, work perfectly well when there isn't one
# there's something about advancing past the breaking token, or not
# aslo, I removed the captures here around nonbinding and separators, and we got into a loop with a panic
# oy
(defn- tup [parser]
(def origin (current parser))
(advance parser) # consume the :lparen
(def ast {:type :tuple :data @[] :token origin})
(while (separates? parser) (advance parser)) # consume any separators
(while (not (check parser :rparen))
(accept-many parser :newline :comma)
(when (= :break ((current parser) :type))
(break (advance parser)))
(when (check parser :eof)
(def err {:type :error :token origin :msg "unclosed paren"})
(array/push (parser :errors) err)
(error err))
(def term (capture nonbinding parser))
(array/push (ast :data) term)
(try (separators parser)
([e] (array/push (ast :data) e))))
(capture separators parser))
(advance parser)
ast)
@ -354,8 +386,10 @@
(def origin (current parser))
(advance parser)
(def ast {:type :list :data @[] :token origin})
(while (separates? parser) (advance parser))
(while (not (check parser :rbracket))
(accept-many parser :newline :comma)
(when (= :break ((current parser) :type))
(break (advance parser)))
(when (check parser :eof)
(def err {:type :error :token origin :msg "unclosed bracket"})
(array/push (parser :errors) err)
@ -369,8 +403,7 @@
)
(capture nonbinding parser)))
(array/push (ast :data) term)
(try (separators parser)
([e] (array/push (ast :data) e))))
(capture separators parser))
(advance parser)
ast)
@ -378,8 +411,10 @@
(def origin (current parser))
(advance parser)
(def ast {:type :set :data @[] :token origin})
(while (separates? parser) (advance parser))
(while (not (check parser :rbrace))
(accept-many parser :newline :comma)
(when (= :break ((current parser) :type))
(break (advance parser)))
(when (check parser :eof)
(def err {:type :error :token origin :msg "unclosed brace"})
(array/push (parser :errors) err)
@ -393,8 +428,7 @@
)
(capture nonbinding parser)))
(array/push (ast :data) term)
(try (separators parser)
([e] (array/push (ast :data) e))))
(capture separators parser))
(advance parser)
ast)
@ -402,8 +436,10 @@
(def origin (current parser))
(advance parser)
(def ast {:type :dict :data @[] :token origin})
(while (separates? parser) (advance parser))
(while (not (check parser :rbrace))
(accept-many parser :newline :comma)
(when (= :break ((current parser) :type))
(break (advance parser)))
(when (check parser :eof)
(def err {:type :error :token origin :msg "unclosed brace"})
(array/push (parser :errors) err)
@ -423,7 +459,7 @@
(try (panic parser (string "expected dict term, got " (type origin))) ([e] e))
))
(array/push (ast :data) term)
(try (separators parser) ([e] (array/push (ast :data) e))))
(capture separators parser))
(advance parser)
ast)
@ -452,8 +488,10 @@
(def origin (current parser))
(advance parser) # consume the :lparen
(def ast {:type :tuple :data @[] :token origin})
(while (separates? parser) (advance parser)) # consume any separators
(while (not (check parser :rparen))
(accept-many parser :newline :comma)
(when (= :break ((current parser) :type))
(break (advance parser)))
(when (check parser :eof)
(def err {:type :error :token origin :msg "unclosed paren"})
(array/push (parser :errors) err)
@ -466,8 +504,7 @@
{:type :splat :data splatted :token origin})
(capture pattern parser)))
(array/push (ast :data) term)
(try (separators parser)
([e] (array/push (ast :data) e))))
(capture separators parser))
(advance parser)
ast)
@ -475,8 +512,10 @@
(def origin (current parser))
(advance parser)
(def ast {:type :list :data @[] :token origin})
(while (separates? parser) (advance parser))
(while (not (check parser :rbracket))
(accept-many parser :newline :comma)
(when (= :break ((current parser) :type))
(break (advance parser)))
(when (check parser :eof)
(def err {:type :error :token origin :msg "unclosed bracket"})
(array/push (parser :errors) err)
@ -489,8 +528,7 @@
{:type :splat :data splatted :token origin})
(capture pattern parser)))
(array/push (ast :data) term)
(try (separators parser)
([e] (array/push (ast :data) e))))
(capture separators parser))
(advance parser)
ast)
@ -498,8 +536,10 @@
(def origin (current parser))
(advance parser)
(def ast {:type :dict :data @[] :token origin})
(while (separates? parser) (advance parser))
(while (not (check parser :rbrace))
(accept-many parser :newline :comma)
(when (= :break ((current parser) :type))
(break (advance parser)))
(when (check parser :eof)
(def err {:type :error :token origin :msg "unclosed brace"})
(array/push (parser :errors) err)
@ -519,7 +559,7 @@
(try (panic parser (string "expected dict term, got " (type origin))) ([e] e))
))
(array/push (ast :data) term)
(try (separators parser) ([e] (array/push (ast :data) e))))
(capture separators parser))
(advance parser)
ast)
@ -560,22 +600,25 @@
(defn- iff [parser]
(def ast {:type :if :data @[] :token (current parser)})
(advance parser) #consume the if
(array/push (ast :data) (capture simple parser))
(array/push (ast :data) (simple parser))
(accept-many parser :newline)
(if-let [err (expect-ret parser :then)]
(array/push (ast :data) err)
(advance parser))
(array/push (ast :data) (capture nonbinding parser))
(array/push (ast :data) (nonbinding parser))
(accept-many parser :newline)
(if-let [err (expect-ret parser :else)]
(array/push (ast :data) err)
(advance parser))
(array/push (ast :data) (capture nonbinding parser))
(array/push (ast :data) (nonbinding parser))
ast)
(defn- literal-terminator? [token]
(def tok-type (token :type))
(or (= :newline tok-type) (= :semicolon tok-type)))
(defn- terminator [parser]
(if-not (terminates? parser)
# this line panics, captures the panic, advances the parser, and re-throws the error; solves an off-by-one error
(panic parser "expected terminator"))
(advance parser)
(while (terminates? parser) (advance parser)))
@ -798,13 +841,15 @@
(defn- block [parser]
(def origin (current parser))
(expect parser :lbrace) (advance parser)
(accept-many parser ;terminators)
(def data @[])
(while (not (check parser :rbrace))
(accept-many parser :newline :semicolon)
(when (= :break ((current parser) :type))
(break (advance parser)))
(if (check parser :eof)
(error {:type :error :token origin :data data :msg "unclosed brace"}))
(array/push data (capture expr parser))
(terminator parser))
(capture terminator parser))
(advance parser)
{:type :block :data data :token origin})
@ -826,16 +871,16 @@
(array/push data (capture simple parser)))
{:type :do :data data :token origin})
### refs, pkgs, nses, etc.
(defn- ref [parser]
### boxs, pkgs, nses, etc.
(defn- box [parser]
(def origin (current parser))
(expect parser :ref) (advance parser)
(expect parser :box) (advance parser)
(try
(do
(def name (-> parser word-only (get :data)))
(expect parser :equals) (advance parser)
(def value (nonbinding parser))
{:type :ref :data value :name name :token origin})
{:type :box :data value :name name :token origin})
([err] err)))
(defn- pkg-name [parser]
@ -966,7 +1011,7 @@
### expressions
# four levels of expression complexity:
# simple (atoms, collections, synthetic expressions; no conditionals or binding or blocks)
# nonbinding (excludes let, ref, named fn: what is allowed inside collections)
# nonbinding (excludes let, box, named fn: what is allowed inside collections)
# plain old exprs (anything but toplevel)
# toplevel (exprs + ns, pkg, test, import, use)
@ -1054,7 +1099,7 @@
# binding forms
:let (lett parser)
:fn (fnn parser)
:ref (ref parser)
:box (box parser)
# nonbinding forms
:nil (nill parser)
@ -1103,8 +1148,12 @@
(def origin (current parser))
(def lines @[])
(while (not (check parser :eof))
(accept-many parser :newline)
(array/push lines (capture toplevel parser))
# (print "starting script loop with " (pp-tok origin))
(accept-many parser :newline :semicolon)
(when (= :break ((current parser) :type))
(break (advance parser)))
(def term (capture toplevel parser))
(array/push lines term)
(capture terminator parser))
{:type :script :data lines :token origin})
@ -1117,10 +1166,16 @@
# (do
(comment
(def source `
let foo = :bar
{
foo bar
quux frobulate
baz
12 23 42
}
`)
(def scanned (s/scan source))
# (print "\n***NEW PARSE***\n")
(def a-parser (new-parser scanned))
(def parsed (lett a-parser))
(def parsed (parse scanned))
(pp (map (fn [err] (err :msg)) (parsed :errors)))
(print (pp-ast (parsed :ast)))
)

View File

@ -39,3 +39,4 @@
(def validation-errors (post-validated :errors))
(when (any? validation-errors) (each err validation-errors (e/validation-error err)) (break :error))
(post-parsed :ast)))

View File

@ -1,8 +1,9 @@
(def reserved-words
"List of Ludus reserved words."
## see ludus-spec repo for more info
{"as" :as ## impl
"box" :ref
{
"as" :as ## impl
"box" :box
"do" :do ## impl
"else" :else ## impl
"false" :false ## impl -> literal word
@ -17,14 +18,14 @@
"panic!" :panic ## impl (should _not_ be a function)
"pkg" :pkg
"recur" :recur ## impl
"repeat" :repeat ## impl
"test" :test
"then" :then ## impl
"true" :true ## impl -> literal word
"use" :use ## wip
"with" :with ## impl
"when" :when ## impl, replaces cond
"repeat" :repeat ## syntax sugar over "loop": still unclear what this syntax could be
"test" :test
})
"with" :with ## impl
})
(def literal-words {"true" true
"false" false
@ -348,8 +349,7 @@
(recur (-> scanner (scan-token) (next-token)))))
(recur (new-scanner source input)))
(comment
# (do
(def source "add 1 2 () four")
(scan source)
)
# (comment
(do
(def source " -123 ")
(length ((scan source) :tokens)))

View File

@ -102,6 +102,11 @@ Deferred until a later iteration of Ludus:
(def node (get ctx name))
(if node node (resolve-name (get ctx :^parent) name)))
(defn- resolve-name-in-script [ctx name]
(when (ctx :^toplevel) (break nil))
(def node (ctx name))
(if node node (resolve-name-in-script (ctx :^parent) name)))
(defn- word [validator]
(def ast (validator :ast))
(def name (ast :data))
@ -157,10 +162,12 @@ Deferred until a later iteration of Ludus:
(def ast (validator :ast))
(def name (ast :data))
(def ctx (validator :ctx))
(when (has-key? ctx name)
(def {:line line :input input} (get-in ctx [name :token]))
### XXX TODO: this resolution should ONLY be for userspace, NOT prelude
(def resolved (resolve-name-in-script ctx name))
(when resolved
(def {:line line :input input} resolved)
(array/push (validator :errors)
{:node ast :msg (string "name is already bound on line "
{:node ast :msg (string "name " name " is already bound on line "
line " of " input)}))
(set (ctx name) ast)
# (pp ctx)
@ -336,7 +343,7 @@ Deferred until a later iteration of Ludus:
(set (ast :arities) arities)
validator)
(defn- ref [validator]
(defn- box [validator]
(def ast (validator :ast))
(def ctx (validator :ctx))
(def expr (ast :data))
@ -435,12 +442,12 @@ Deferred until a later iteration of Ludus:
(def rest-arities (keys (arities :rest)))
(when (empty? rest-arities)
(array/push (validator :errors)
{:node ast :msg "mismatched arity"})
{:node ast :msg "wrong number of arguments"})
(break validator))
(def rest-min (min ;rest-arities))
(when (< num-args rest-min)
(array/push (validator :errors)
{:node ast :msg "mismatched arity"}))
{:node ast :msg "wrong number of arguments"}))
validator)
(defn- kw-root [validator]
@ -750,7 +757,7 @@ Deferred until a later iteration of Ludus:
:use (usee validator)
:loop (loopp validator)
:recur (recur validator)
:ref (ref validator)
:box (box validator)
(error (string "unknown node type " type)))))
(set validate validate*)
@ -758,12 +765,13 @@ Deferred until a later iteration of Ludus:
(defn- cleanup [validator]
(def declared (get-in validator [:status :declared] {}))
(when (any? declared)
(each declaration declared
(each declaration (keys declared)
(array/push (validator :errors) {:node declaration :msg "declared fn, but not defined"})))
validator)
(defn valid [ast &opt ctx]
(default ctx @{})
(set (ctx :^toplevel) true)
(def validator (new-validator ast))
(def base-ctx @{:^parent ctx})
(set (validator :ctx) base-ctx)

92
turtle-graphics.md Normal file
View File

@ -0,0 +1,92 @@
# Turtle Graphics protocol
name: "turtle-graphics"
version: 0.1.0
### Description
Turtle graphics describe the movements and drawing behaviours of screen, robot, and print "turtles."
* `proto`: `["turtle-graphics", "{version number}"]`
* `data`: an array of arrays; each array represents a turtle command; the first element of a command array is the verb; any subsequent items are the arguments to the verbs.
* Valid arguments are numbers, strings, and booleans.
* Depending on what we end up doing, we may add arrays of these, representing tuples or lists, and/or objects with string keys whose text are well-formed keywords in Ludus. For now, however, arguments must be atomic values.
* E.g., `["forward", 100]`
* Each turtle has its own stream.
* At current, this protocol describes the behaviour of turtle-like objects, all of which "live" in the same "world"; there is not yet a provision for multiple canvases/worlds. That said, an additional field for "world" in at the top level may well be added in the future to allow for multiple worlds to unfold at the same time.
### Verbs and arguments
* `forward`, steps: number
- Moves the turtle forward by the number of steps/pixels.
* `back`, steps: number
- Moves the turtle backwards by the number of steps/pixels.
* `right`, turns: number
- Turns the turtle right by the number of turns. (1 turn = 360 degrees.)
* `left`, turns: number
- Turns the turtle to the left by the number of turns. (1 turn = 360 degrees.)
* `penup`, no arguments
- "Lifts" the turtle's pen, keeping it from drawing.
* `pendown`, no arguments
- "Lowers" the turtle's pen, starting it drawing a path.
* `pencolor`, red: number, green: number, blue: number, alpha: number, OR: color: string
- Sets the turtle's pen's color to the specified RGBA color.
* `penwidth`, width: number
- Sets the width of the turtle's pen, in pixels (or some other metric).
* `home`, no arguments
- Sends the turtle back to its starting point, with a heading of 0.
* `goto`, x: number, y: number
- Sends the turtle to the specified Cartesian coordinates, where the origin is the turtle's starting position.
* `setheading`, heading: number
- Sets the turtle's heading. 0 is the turtle's starting heading, with increasing numbers turning to the right.
* `show`, no arguments
- Shows the turtle.
* `hide`, no arguments
- Hides the turtle.
* `loadstate`, x: number, y: number, heading: number, visible: boolean, pendown: boolean, width: number, color: string OR r: number, g: number, b: number, a: number
- Loads a turtle state.
* `clear`, no arguments
- Erases any paths drawn and sets the background color to the default.
* `background`, red: number, green: number, blue: number, alpha: number
- Sets the background color to the specified RGBA color, OR: color: string
These last two feel a little weird to me, since the background color is more the property of the **world** the turtle is in, not the turtle itself. Worlds with multiple turtles will be set up so that _any_ turtle will be able to change the background, and erase all paths.
That said, since we don't yet have a world abstraction/entity, then there's no other place to put them. This will likely be shifted around in later versions of the protocol.
### Other considerations
**Not all turtles will know how to do all these things.**
The idea is that this single abstraction will talk to all the turtle-like things we eventually use.
That means that some turtles won't be able to do all the things; that's fine!
They just won't do things they can't do; but warnings should go to `stderr`.
**Errors are not passed back to Ludus.**
These are fire-off commands.
Errors should be _reported_ to `stderr` or equivalent.
But Ludus sending things to its output streams should only cause Ludus panics when there's an issue in Ludus.
**Colors aren't always RGBA.**
For pen-and-paper turtles, we don't have RGBA colors.
Colors should also be specifiable with strings corresponding to CSS basic colors: black, silver, gray, white, maroon, red, purple, fuchsia, green, lime, olive, yellow, navy, blue, teal, and aqua.
**Turtles should communicate states.**
Ludus should have access to turtle states.
This is important for push/pop situations that we use for L-systems.
There are two ways to do this: Ludus does its own bookkeeping for turtle states, or it has a way to get the state from a turtle.
The latter has the value of being instantaneous, and gives us an _expected_ state of the turtle after the commands are all processed.
In particular, this will be necessary for the recursive L-systems that require pushing and popping turtle state.
The latter has the drawback of potentially allowing the turtle state and expected turtle state to fall out of synch.
The former has the value of always giving us the correct, actual state of the turtle.
It has the drawback of requiring such state reporting to be asynchronous, and perhaps wildly asynchronous, as things like moving robots and plotters will take quite some time to actually draw what Ludus tells it to.
(Being able to wait until `eq? (expected, actual)` to do anything else may well be extremely useful.)
That suggests, then, that both forms of turtle state are desirable and necessary.
Thus: turtles should communicate states (and thus there ought to be a protocol for communicating state back to Ludus) and Ludus should always do the bookkeeping of calculating the expected state.
**Turtles use Cartesian, rather than screen, coordinates.**
The starting position of the turtle is `(0, 0)`, which is the origin, and _centred_ in the field of view.
Increasing the x-coordinate moves the turtle to the right; increasing the y-coordinate moves the turtle _up_.
**Turtles use compass headings, not mathematical angles.**
Turtles start pointing vertially, at heading `0`.
Turning right _increases_ the heading; pointing due "east" is `0.25`; south `0.5`, and west, `0.75`.