diff --git a/assets/test_prelude.ld b/assets/test_prelude.ld index 8325121..963e67f 100644 --- a/assets/test_prelude.ld +++ b/assets/test_prelude.ld @@ -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 } diff --git a/may_2025_thoughts.md b/may_2025_thoughts.md index 8e12ac9..938700e 100644 --- a/may_2025_thoughts.md +++ b/may_2025_thoughts.md @@ -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. diff --git a/sandbox.ld b/sandbox.ld index 0a54d57..cc29d5b 100644 --- a/sandbox.ld +++ b/sandbox.ld @@ -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}" } - diff --git a/src/compiler.rs b/src/compiler.rs index 1f9eb15..687722b 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -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 diff --git a/src/main.rs b/src/main.rs index 52d8542..70eb87e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -112,8 +112,8 @@ pub fn run(src: &'static str) { // in any event, the AST should live forever let parsed: &'static Spanned = 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(); diff --git a/src/vm.rs b/src/vm.rs index 77d1ac4..3c725f7 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -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();