Compare commits

...

13 Commits

Author SHA1 Message Date
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
7 changed files with 250 additions and 103 deletions

View File

@ -50,8 +50,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
}
@ -293,8 +293,8 @@ 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))
}
fn omit {
@ -345,8 +345,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)
}
}
@ -423,9 +423,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, [])
}
}
@ -744,31 +744,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 {
@ -801,28 +801,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
}
@ -869,10 +869,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)))
)
}
@ -917,21 +917,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 {
@ -1143,10 +1143,10 @@ fn render_turtle! () -> {
: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))
let origin = mult ((0, 1), turtle_radius)
let (x1, y1) = origin
let (x2, y2) = rotate (origin, turtle_angle)
let (x3, y3) = rotate (origin, neg (turtle_angle))
add_call! ((:push))
add_call! ((:translate, x, y))
add_call! ((:rotate, turn/rad (heading)))
@ -1295,8 +1295,8 @@ 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))
let a = add (heading, 0.25)
(cos (a), sin (a))
}
}

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

@ -55,7 +55,9 @@
# (do
# (def start (os/clock))
(def source `
doc! (add)
box foo = :bar
store! (foo, :baz)
unbox (foo)
`)
(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 `
[...x]
{
foo bar
quux frobulate
baz
12 23 42
}
`)
(def scanned (s/scan source))
# (print "\n***NEW PARSE***\n")
(def a-parser (new-parser scanned))
(try (def parsed (pattern a-parser)) ([e] (pp e)))
(def parsed (parse scanned))
(pp (map (fn [err] (err :msg)) (parsed :errors)))
(print (pp-ast (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
@ -16,15 +17,15 @@
"ns" :ns ## impl
"panic!" :panic ## impl (should _not_ be a function)
"pkg" :pkg
"recur" :recur ## impl
"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

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))
@ -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*)
@ -764,6 +771,7 @@ Deferred until a later iteration of Ludus:
(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)

81
turtle-graphics.md Normal file
View File

@ -0,0 +1,81 @@
# 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, 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.