add turtle graphics, fix jump len error in tuple pattern

This commit is contained in:
Scott Richmond 2025-06-22 23:57:11 -04:00
parent 480e05f561
commit bf97a34771
6 changed files with 355 additions and 33 deletions

View File

@ -791,16 +791,16 @@ fn range {
& additional list operations now that we have comparitors
fn at {
"Returns the element at index n of a list or tuple, or the byte at index n of a string. Zero-indexed: the first element is at index 0. Returns nil if nothing is found in a list or tuple; returns an empty string if nothing is found in a string."
(xs as :list, n as :number) -> base :nth (n, xs)
(xs as :tuple, n as :number) -> base :nth (n, xs)
(str as :string, n as :number) -> {
let raw = base :nth (n, str)
when {
nil? (raw) -> nil
gte? (raw, 128) -> panic! "not an ASCII char"
true -> base :str_slice (str, n, inc (n))
}
}
(xs as :list, n as :number) -> base :at (xs, n)
(xs as :tuple, n as :number) -> base :at (xs, n)
& (str as :string, n as :number) -> {
& let raw = base :at (str, n)
& when {
& nil? (raw) -> nil
& gte? (raw, 128) -> panic! "not an ASCII char"
& true -> base :str_slice (str, n, inc (n))
& }
& }
(_) -> nil
}
@ -819,11 +819,6 @@ fn last {
(xs) if ordered? (xs) -> at (xs, dec (count (xs)))
}
fn butlast {
"Returns a list, omitting the last element."
(xs as :list) -> base :slice (xs, 0, dec (count (xs)))
}
fn slice {
"Returns a slice of a list or a string, representing a sub-list or sub-string."
(xs as :list, end as :number) -> slice (xs, 0, end)
@ -837,6 +832,11 @@ fn slice {
(str as :string, start as :number, end as :number) -> base :str_slice (str, start, end)
}
fn butlast {
"Returns a list, omitting the last element."
(xs as :list) -> slice (xs, 0, dec (count (xs)))
}
&&& keywords: funny names
fn keyword? {
"Returns true if a value is a keyword, otherwise returns false."
@ -860,10 +860,21 @@ fn dissoc {
(d as :dict, k as :keyword) -> base :dissoc (d, k)
}
& 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."
(k as :keyword) -> get (k, _)
(k as :keyword, d as :dict) -> base :get (d, k)
(k as :keyword, d as :dict, default) -> match base :get (d, k) with {
nil -> default
val -> val
}
}
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."
(d as :dict) -> d
(d as :dict, k as :keyword, updater as :fn) -> base :assoc (d, k, updater (k (d)))
(d as :dict, k as :keyword, updater as :fn) -> assoc (d, k, updater (get (k, d)))
}
fn keys {
@ -876,19 +887,11 @@ fn values {
(d as :dict) -> do d > list > map (second, _)
}
& 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."
(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."
(k as :keyword) -> has? (k, _)
(k as :keyword, d as :dict) -> do d > k > some?
(k as :keyword, d as :dict) -> do d > get (k) > some?
}
fn dict {
@ -938,9 +941,273 @@ fn random_int {
}
&&& Results, errors and other unhappy values
fn ok {
"Takes a value and wraps it in an :ok result tuple."
(value) -> (:ok, value)
}
fn ok? {
"Takes a value and returns true if it is an :ok result tuple."
((:ok, _)) -> true
(_) -> false
}
fn err {
"Takes a value and wraps it in an :err result tuple, presumably as an error message."
(msg) -> (:err, msg)
}
fn err? {
"Takes a value and returns true if it is an :err result tuple."
((:err, _)) -> true
(_) -> false
}
fn unwrap! {
"Takes a result tuple. If it's :ok, then returns the value. If it's not :ok, then it panics. If it's not a result tuple, it also panics."
((:ok, value)) -> value
((:err, msg)) -> panic! string ("Unwrapped :err! ", msg)
(_) -> panic! "Cannot unwrap something that's not an error tuple."
}
fn unwrap_or {
"Takes a value that is a result tuple and a default value. If it's :ok, then it returns the value. If it's :err, returns the default value."
((:ok, value), _) -> value
((:err, _), default) -> default
}
fn assert! {
"Asserts a condition: returns the value if the value is truthy, panics if the value is falsy. Takes an optional message."
(value) -> if value
then value
else panic! "Assert failed: {value}"
(msg, value) -> if value
then value
else panic! "Assert failed: {msg} with {value}"
}
&&& Turtle & other graphics
& some basic colors
& these are the "basic" css colors
& https://developer.mozilla.org/en-US/docs/Web/CSS/named-color
let 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, 25, 255)
}
& 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 :white
:penwidth 1
:visible? true
}
& turtle states: refs that get modified by calls
& turtle_commands is a list of commands, expressed as tuples
box turtle_commands = []
box turtle_state = turtle_init
fn apply_command
fn add_command! (command) -> {
update! (turtle_commands, append (_, command))
let prev = unbox (turtle_state)
let curr = apply_command (prev, command)
store! (turtle_state, curr)
:ok
}
fn forward! {
"Moves the turtle forward by a number of steps. Alias: fd!"
(steps as :number) -> add_command! ((:forward, steps))
}
let fd! = forward!
fn back! {
"Moves the turtle backward by a number of steps. Alias: bk!"
(steps as :number) -> add_command! ((:back, steps))
}
let bk! = back!
fn left! {
"Rotates the turtle left, measured in turns. Alias: lt!"
(turns as :number) -> add_command! ((:left, turns))
}
let lt! = left!
fn right! {
"Rotates the turtle right, measured in turns. Alias: rt!"
(turns as :number) -> add_command! ((:right, turns))
}
let rt! = right!
fn penup! {
"Lifts the turtle's pen, stopping it from drawing. Alias: pu!"
() -> add_command! ((:penup))
}
let pu! = penup!
fn pendown! {
"Lowers the turtle's pen, causing it to draw. Alias: pd!"
() -> add_command! ((:pendown))
}
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) -> add_command! ((:pencolor, 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 pc! = pencolor!
fn penwidth! {
"Sets the width of the turtle's pen, measured in pixels. Alias: pw!"
(width as :number) -> add_command! ((:penwidth, width))
}
let pw! = penwidth!
fn background! {
"Sets the background color behind the turtle and path. Alias: bg!"
(color as :keyword) -> add_command! ((:background, 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!
fn home! {
"Sends the turtle home: to the centre of the screen, pointing up. If the pen is down, the turtle will draw a path to home."
() -> add_command! ((:home))
}
fn clear! {
"Clears the canvas and sends the turtle home."
() -> add_command! ((:clear))
}
fn goto! {
"Sends the turtle to (x, y) coordinates. If the pen is down, the turtle will draw a path to its new location."
(x as :number, y as :number) -> add_command! ((:goto, (x, y)))
((x, y)) -> goto! (x, y)
}
fn setheading! {
"Sets the turtle's heading. The angle is specified in turns, with 0 pointing up. Increasing values rotate the turtle counter-clockwise."
(heading as :number) -> add_command! ((:setheading, heading))
}
fn showturtle! {
"If the turtle is hidden, shows the turtle. If the turtle is already visible, does nothing."
() -> add_command! ((:show))
}
fn hideturtle! {
"If the turtle is visible, hides it. If the turtle is already hidden, does nothing."
() -> add_command! ((:hide))
}
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))
}
}
fn apply_command {
"Takes a turtle state and a command and calculates a new state."
(state, command) -> match command with {
(:goto, (x, y)) -> assoc (state, :position, (x, y))
(:home) -> do state >
assoc (_, :position, (0, 0)) >
assoc (_, :heading, 0)
(:clear) -> do state >
assoc (state, :position, (0, 0)) >
assoc (_, :heading, 0)
(:right, turns) -> update (state, :heading, add (_, turns))
(:left, turns) -> update (state, :heading, sub (_, turns))
(:forward, steps) -> {
let #{heading, position, ...} = state
let unit = heading/vector (heading)
let vect = mult (steps, unit)
update (state, :position, add (vect, _))
}
(:back, steps) -> {
let #{heading, position, ...} = state
let unit = heading/vector (heading)
let vect = mult (steps, unit)
update (state, :position, sub (_, vect))
}
(:penup) -> assoc (state, :pendown?, false)
(:pendown) -> assoc (state, :pendown?, true)
(: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)
(:background, _) -> state
}
}
& position () -> (x, y)
fn position {
"Returns the turtle's current position."
() -> do turtle_state > unbox > :position
}
fn heading {
"Returns the turtle's current heading."
() -> do turtle_state > unbox > :heading
}
fn pendown? {
"Returns the turtle's pen state: true if the pen is down."
() -> do turtle_state > unbox > :pendown?
}
fn 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."
() -> do turtle_state > unbox > :penwidth
}
box state = nil
#{
type
@ -1065,4 +1332,46 @@ fn random_int {
each!
random
random_int
ok
ok?
err
err?
unwrap!
unwrap_or
assert!
colors
turtle_init
turtle_commands
turtle_state
forward!
fd!
back!
bk!
left!
lt!
right!
rt!
penup!
pu!
pendown!
pd!
pencolor!
pc!
penwidth!
pw!
background!
bg!
home!
clear!
goto!
setheading!
showturtle!
hideturtle!
loadstate!
position
heading
pendown?
pencolor
penwidth
state
}

View File

@ -488,4 +488,8 @@ Currently fixing little bugs in prelude.
Here's a list of things that need doing:
* [ ] Escape characters in strings: \n, \t, and \{, \}.
* [ ] `doc!` needs to print the patterns of a function.
* [ ] I need to return to the question of whether/how strings are ordered; do we use `at`, or do we want `char_at`? etc.
* [ ] Original implementation of `butlast` is breaking stack discipline; I don't know why. It ends up returning from evaluating one of the arguments straight into a `load` instruction. Something about tail calls and ternary synthetic expressions and base functions. (For now, I can call `slice` instead of `base :slice` and it works.)
- Original version of `update` also had this same problem with `assoc`; fixed it by calling the Ludus, rather than Rust, function.
- I need this fixed for optimization reasons.

View File

@ -1,7 +1,12 @@
when {
false -> :no
nil -> :nope
:else -> :yup
let command = (:forward, 100)
match command with {
(:goto, (x, y)) -> "goto: {x} and {y}"
(:home) -> "go home!"
& (:clear) -> "clear the screen"
& (:right, turns) -> "make {turns} to the right"
& (:left, turns) -> "turn left! by {turns}"
(:forward, steps) -> "forward {steps} turtle paces"
(:back, steps) -> "flip it and reverse it {steps}"
}

View File

@ -607,7 +607,7 @@ impl<'a> Compiler<'a> {
// patch up the previous no match jumps to jump to clean-up code
for idx in jump_idxes {
self.patch_jump(idx, self.len() - idx - 2)
self.patch_jump(idx, self.len() - idx - 3)
}
// pop everything that was pushed
// don't change the compiler stack representation, tho

View File

@ -112,8 +112,8 @@ pub fn run(src: &'static str) {
// in any event, the AST should live forever
let parsed: &'static Spanned<Ast> = Box::leak(Box::new(parse_result.unwrap()));
// let prelude = prelude();
let prelude = imbl::HashMap::new();
let prelude = prelude();
// let prelude = imbl::HashMap::new();
let mut validator = Validator::new(&parsed.0, &parsed.1, "user input", src, prelude.clone());
validator.validate();

View File

@ -976,6 +976,10 @@ impl Vm {
self.ip = self.frame.ip;
// finally, throw the value on the stack
self.push(value);
println!(
"=== returning to {} ===",
self.frame.function.as_fn().name()
);
}
Value::Partial(partial) => {
let last_arg = self.pop();