diff --git a/assets/prelude.ld b/assets/prelude.ld new file mode 100644 index 0000000..8139895 --- /dev/null +++ b/assets/prelude.ld @@ -0,0 +1,1466 @@ +& this file, uniquely, gets `base` loaded as context. See src/base.janet for exports + +& some forward declarations +& TODO: fix this so that we don't need (as many of) them +fn and +fn append +fn apply_command +fn assoc +fn atan/2 +fn deg/rad +fn dict +fn first +fn floor +fn get +fn join +fn mod +fn neg? +fn print! +fn some? +fn store! +fn string +fn turn/rad +fn unbox +fn update! + +& the very base: know something's type +fn type { + "Returns a keyword representing the type of the value passed in." + (x) -> base :type (x) +} + +& some helper type functions +fn coll? { + "Returns true if a value is a collection: dict, list, tuple, or set." + (coll as :dict) -> true + (coll as :list) -> true + (coll as :tuple) -> true + & (coll as :set) -> true + (_) -> false +} + +fn ordered? { + "Returns true if a value is an indexed collection: list or tuple." + (coll as :list) -> true + (coll as :tuple) -> true + (coll as :string) -> true + (_) -> false +} + +fn assoc? { + "Returns true if a value is an associative collection: a dict or a pkg." + (d as :dict) -> true + (p as :pkg) -> true + (_) -> false +} + +&&& nil: working with nothing + +fn nil? { + "Returns true if a value is nil." + (nil) -> true + (_) -> false +} + +fn some? { + "Returns true if a value is not nil." + (nil) -> false + (_) -> true +} + +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 +} + +& ...and if two things are the same +fn eq? { + "Returns true if all arguments have the same value." + (x) -> true + (x, y) -> base :eq? (x, y) + (x, y, ...zs) -> if eq? (x, y) + then loop (y, zs) with { + (a, []) -> eq? (a, x) + (a, [b, ...cs]) -> if eq? (a, x) + then recur (b, cs) + else false + } + else false +} + +&&& true & false: boolean logic (part the first) +fn bool? { + "Returns true if a value is of type :boolean." + (false) -> true + (true) -> true + (_) -> false +} + +fn true? { + "Returns true if a value is boolean `true`. Useful to distinguish between `true` and anything else." + (true) -> true + (_) -> false +} + +fn false? { + "Returns `true` if a value is `false`, otherwise returns `false`. Useful to distinguish between `false` and `nil`." + (false) -> true + (_) -> false +} + +fn bool { + "Returns false if a value is nil or false, otherwise returns true." + (nil) -> false + (false) -> false + (_) -> true +} + +fn not { + "Returns false if a value is truthy, true if a value is falsy." + (nil) -> true + (false) -> true + (_) -> 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? { + "Returns true if a value is a tuple." + (tuple as :tuple) -> true + (_) -> false +} + +&&& functions: getting things done +fn fn? { + "Returns true if an argument is a function." + (f as :fn) -> true + (_) -> false +} + +& what we need for some very basic list manipulation +fn rest { + "Returns all but the first element of a list or tuple, as a list." + ([]) -> [] + (()) -> () + (xs as :list) -> base :rest (xs) + (xs as :tuple) -> base :rest (xs) + (xs as :string) -> base :str_slice (xs, 1) +} + +fn inc { + "Increments a number." + (x as :number) -> base :inc (x) +} + +fn dec { + "Decrements a number." + (x as :number) -> base :dec (x) +} + +fn count { + "Returns the number of elements in a collection (including string)." + (xs as :list) -> base :count (xs) + (xs as :tuple) -> base :count (xs) + (xs as :dict) -> base :count (xs) + (xs as :string) -> base :count (xs) + & (xs as :set) -> base :count (xs) +} + +fn empty? { + "Returns true if something is empty. Otherwise returns false (including for things that can't logically be empty, like numbers)." + ([]) -> true + (#{}) -> true + & (s as :set) -> eq? (s, ${}) + (()) -> true + ("") -> true + (_) -> false +} + +fn any? { + "Returns true if something is not empty, otherwise returns false (including for things that can't be logically full, like numbers)." + ([...]) -> true + (#{...}) -> true + & (s as :set) -> not (empty? (s)) + ((...)) -> true + (s as :string) -> not (empty? (s)) + (_) -> false +} + +fn list? { + "Returns true if the value is a list." + (l as :list) -> true + (_) -> false +} + +fn list { + "Takes a value and returns it as a list. For values, it simply wraps them in a list. For collections, conversions are as follows. A tuple->list conversion preservers order and length. Unordered collections do not preserve order: sets and dicts don't have predictable or stable ordering in output. Dicts return lists of (key, value) tuples." + (x) -> base :to_list (x) +} + +fn fold { + "Folds a list." + (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 (prev, curr) + (prev, curr, remaining) -> recur ( + f (prev, curr) + 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, xs) -> { + fn mapper (prev, curr) -> append (prev, f (curr)) + fold (mapper, xs, []) + } + (kw as :keyword, xs) -> { + fn mapper (prev, curr) -> append (prev, kw (curr)) + fold (mapper, xs, []) + } +} + +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) -> { + fn filterer (filtered, x) -> if p? (x) + then append (filtered, x) + else filtered + fold (filterer, xs, []) + } +} + +fn keep { + "Takes a list and returns a new list with any `nil` values omitted." + (xs) -> filter (some?, xs) +} + +fn append { + "Adds an element to a list or set." + () -> [] + (xs as :list) -> xs + (xs as :list, x) -> base :conj (xs, x) + & (xs as :set) -> xs + & (xs as :set, x) -> base :conj (xs, x) +} + +fn append! { + "Adds an element to a list, modifying it." + () -> [] + (xs as :list) -> xs + (xs as :list, x) -> base :conj! (xs, x) +} + +fn concat { + "Combines two lists, strings, or sets." + (x as :string, y as :string) -> base :concat (x, y) + (xs as :list, ys as :list) -> base :concat (xs, ys) + & (xs as :set, ys as :set) -> base :concat (xs, ys) + (xs, ys, ...zs) -> fold (concat, zs, concat (xs, ys)) +} + +& fn 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? { +& "Returns true if a value is a set." +& (xs as :set) -> true +& (_) -> false +& } + +fn contains? { + "Returns true if a set or list contains a value." + & (value, s as :set) -> bool (base :get (s, value)) + (value, l as :list) -> contains? (value, set (list)) +} + +& fn omit { +& "Returns a new set with the value omitted." +& (value, s as :set) -> base :disj (s, value) +& } + +fn print! { + "Sends a text representation of Ludus values to the console." + (...args) -> { + base :print! (args) + :ok + } +} + +fn show { + "Returns a text representation of a Ludus value as a string." + (x) -> base :show (x) +} + +fn report! { + "Prints a value, then returns it." + (x) -> { + print! (x) + x + } + (msg as :string, x) -> { + print! (concat ("{msg} ", show (x))) + x + } +} + +fn doc! { + "Prints the documentation of a function to the console." + (f as :fn) -> do f > base :doc > print! + (_) -> :none +} + +&&& strings: harder than they look! +fn string? { + "Returns true if a value is a string." + (x as :string) -> true + (_) -> false +} + +fn string { + "Converts a value to a string by using `show`. If it is a string, returns it unharmed. Use this to build up strings of different kinds of values." + (x as :string) -> x + (x) -> show (x) + (x, ...xs) -> loop (x, xs) with { + (out, [y]) -> concat (out, show (y)) + (out, [y, ...ys]) -> recur (concat (out, show (y)), ys) + } +} + +fn join { + "Takes a list of strings, and joins them into a single string, interposing an optional separator." + ([]) -> "" + ([str as :string]) -> str + (strs as :list) -> join (strs, "") + ([], separator as :string) -> "" + ([str as :string], separator as :string) -> str + ([str, ...strs], separator as :string) -> fold ( + fn (joined, to_join) -> concat (joined, separator, to_join) + strs + str + ) +} + +fn split { + "Takes a string, and turns it into a list of strings, breaking on the separator." + (str as :string, break as :string) -> base :split (break, str) +} + +fn trim { + "Trims whitespace from a string. Takes an optional argument, `:left` or `:right`, to trim only on the left or right." + (str as :string) -> base :trim (str) + (str as :string, :left) -> base :triml (str) + (str as :string, :right) -> base :trimr (str) +} + +fn upcase { + "Takes a string and returns it in all uppercase. Works only for ascii characters." + (str as :string) -> base :upcase (str) +} + +fn downcase { + "Takes a string and returns it in all lowercase. Works only for ascii characters." + (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 + ("\n") -> true + ("\t") -> true + (_) -> false +} + +fn strip { + "Removes punctuation from a string, removing all instances of ,.;:?!" + ("{x},{y}") -> strip ("{x}{y}") + ("{x}.{y}") -> strip ("{x}{y}") + ("{x};{y}") -> strip ("{x}{y}") + ("{x}:{y}") -> strip ("{x}{y}") + ("{x}?{y}") -> strip ("{x}{y}") + ("{x}!{y}") -> strip ("{x}{y}") + (x) -> x +} + +fn words { + "Takes a string and returns a list of the words in the string. Strips all whitespace." + (str as :string) -> { + let no_punct = strip (str) + let strs = split (no_punct, " ") + fn worder (l, s) -> if empty? (s) + then l + else append (l, s) + fold (worder, strs, []) + } +} + +fn sentence { + "Takes a list of words and turns it into a 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? { + "Returns true if a value is a box." + (b as :box) -> true + (_) -> false +} + +fn unbox { + "Returns the value that is stored in a box." + (b as :box) -> base :unbox (b) +} + +fn store! { + "Stores a value in a box, replacing the value that was previously there. Returns the value." + (b as :box, value) -> { + base :store! (b, value) + value + } +} + +fn update! { + "Updates a box by applying a function to its value. Returns the new value." + (b as :box, f as :fn) -> { + let current = unbox (b) + let new = f (current) + store! (b, new) + } +} + +&&& numbers, basically: arithmetic and not much else, yet +& TODO: add nan?, +fn number? { + "Returns true if a value is a number." + (x as :number) -> true + (_) -> false +} + +fn add { + "Adds numbers or vectors." + () -> 0 + (x as :number) -> x + (x as :number, y as :number) -> base :add (x, y) + (x, y, ...zs) -> fold (add, zs, base :add (x, y)) + & add vectors + ((x1, y1), (x2, y2)) -> (add (x1, x2), add (y1, y2)) +} + +fn sub { + "Subtracts numbers or vectors." + () -> 0 + (x as :number) -> x + (x as :number, y as :number) -> base :sub (x, y) + (x, y, ...zs) -> fold (sub, zs, base :sub (x, y)) + ((x1, y1), (x2, y2)) -> (base :sub (x1, x2), base :sub (y1, y2)) +} + +fn mult { + "Multiplies numbers or vectors." + () -> 1 + (x as :number) -> x + (x as :number, y as :number) -> base :mult (x, y) + (x, y, ...zs) -> fold (mult, zs, mult (x, y)) + (scalar as :number, (x, y)) -> (mult (x, scalar), mult (y, scalar)) + ((x, y), scalar as :number) -> mult (scalar, (x, y)) +} + +fn div { + "Divides numbers. Panics on division by zero." + (x as :number) -> x + (_, 0) -> panic! "Division by zero." + (x as :number, y as :number) -> base :div (x, y) + (x, y, ...zs) -> { + let divisor = fold (mult, zs, y) + div (x, divisor) + } +} + +fn div/0 { + "Divides numbers. Returns 0 on division by zero." + (x as :number) -> x + (_, 0) -> 0 + (x as :number, y as :number) -> base :div (x, y) + (x, y, ...zs) -> { + let divisor = fold (mult, zs, y) + div/0 (x, divisor) + } +} + +fn div/safe { + "Divides a number. Returns a result tuple." + (x as :number) -> (:ok, x) + (_, 0) -> (:err, "Division by zero") + (x, y) -> (:ok, div (x, y)) + (x, y, ...zs) -> { + let divisor = fold (mult, zs, y) + div/safe (x, divisor) + } +} + +fn inv { + "Returns the inverse of a number: 1/n or `div (1, n)`. Panics on division by zero." + (x as :number) -> div (1, x) +} + +fn inv/0 { + "Returns the inverse of a number: 1/n or `div/0 (1, n)`. Returns 0 on division by zero." + (x as :number) -> div/0 (1, x) +} + +fn inv/safe { + "Returns the inverse of a number: 1/n or `div/safe (1, n)`. Returns a result tuple." + (x as :number) -> div/safe (1, x) +} + +fn abs { + "Returns the absolute value of a number." + (0) -> 0 + (n as :number) -> if neg? (n) then mult (-1, n) else n +} + +fn neg { + "Multiplies a number by -1, negating it." + (n as :number) -> mult (n, -1) +} + +fn angle { + "Calculates the angle between two vectors." + (v1, v2) -> sub (atan/2 (v2), atan/2 (v1)) +} + +fn zero? { + "Returns true if a number is 0." + (0) -> true + (_) -> false +} + +fn gt? { + "Returns true if numbers are in decreasing order." + (x as :number) -> true + (x as :number, y as :number) -> base :gt (x, y) + (x, y, ...zs) -> loop (y, zs) with { + (a, [b]) -> base :gt (a, b) + (a, [b, ...cs]) -> if base :gt (a, b) + then recur (b, cs) + else false + } +} + +fn gte? { + "Returns true if numbers are in decreasing or flat order." + (x as :number) -> true + (x as :number, y as :number) -> base :gte (x, y) + (x, y, ...zs) -> loop (y, zs) with { + (a, [b]) -> base :gte (a, b) + (a, [b, ...cs]) -> if base :gte (a, b) + then recur (b, cs) + else false + } +} + +fn lt? { + "Returns true if numbers are in increasing order." + (x as :number) -> true + (x as :number, y as :number) -> base :lt (x, y) + (x, y, ...zs) -> loop (y, zs) with { + (a, [b]) -> base :lt (a, b) + (a, [b, ...cs]) -> if base :lt (a, b) + then recur (b, cs) + else false + } +} + +fn lte? { + "Returns true if numbers are in increasing or flat order." + (x as :number) -> true + (x as :number, y as :number) -> base :lte (x, y) + (x, y, ...zs) -> loop (y, zs) with { + (a, [b]) -> base :lte (a, b) + (a, [b, ...cs]) -> if base :lte (a, b) + then recur (b, cs) + else false + } +} + +fn between? { + "Returns true if a number is in the range [lower, higher): greater than or equal to the lower number, less than the higher." + (lower as :number, higher as :number, x as :number) -> and ( + gte? (x, lower) + lt? (x, higher) + ) +} + +fn neg? { + "Returns true if a value is a negative number, otherwise returns false." + (x as :number) if lt? (x, 0) -> true + (_) -> false +} + +fn pos? { + "Returns true if a value is a positive number, otherwise returns false." + (x as :number) if gt? (x, 0) -> true + (_) -> false +} + +fn even? { + "Returns true if a value is an even number, otherwise returns false." + (x as :number) if eq? (0, mod (x, 2)) -> true + (_) -> false +} + +fn odd? { + "Returns true if a value is an odd number, otherwise returns false." + (x as :number) if eq? (1, mod (x, 2)) -> true + (_) -> false +} + +fn min { + "Returns the number in its arguments that is closest to negative infinity." + (x as :number) -> x + (x as :number, y as :number) -> if lt? (x, y) then x else y + (x, y, ...zs) -> fold (min, zs, min (x, y)) +} + +fn max { + "Returns the number in its arguments that is closest to positive infinity." + (x as :number) -> x + (x as :number, y as :number) -> if gt? (x, y) then x else y + (x, y, ...zs) -> fold (max, zs, max (x, y)) +} + +& 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)) + } + } + (_) -> nil +} + +fn first { + "Returns the first element of a list or tuple." + (xs) if ordered? (xs) -> at (xs, 0) +} + +fn second { + "Returns the second element of a list or tuple." + (xs) if ordered? (xs) -> at (xs, 1) +} + +fn last { + "Returns the last element of a list or tuple." + (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) + (xs as :list, start as :number, end as :number) -> when { + gte? (start, end) -> [] + gt? (end, count (xs)) -> slice (xs, start, count (xs)) + neg? (start) -> slice (xs, 0, end) + true -> base :slice (xs, start, end) + } + (str as :string, end as :number) -> base :str_slice (str, 0, end) + (str as :string, start as :number, end as :number) -> base :str_slice (str, start, end) +} + +&&& keywords: funny names +fn keyword? { + "Returns true if a value is a keyword, otherwise returns false." + (kw as :keyword) -> true + (_) -> false +} + +& TODO: determine if Ludus should have a `keyword` function that takes a string and returns a keyword. Too many panics, it has weird memory consequences, etc. + +& TODO: make `and` and `or` special forms which lazily evaluate arguments +fn and { + "Returns true if all values passed in are truthy. Note that this does not short-circuit: all arguments are evaulated before they are passed in." + () -> true + (x) -> bool (x) + (x, y) -> base :and (x, y) + (x, y, ...zs) -> fold (and, zs, base :and (x, y)) +} + +fn or { + "Returns true if any value passed in is truthy. Note that this does not short-circuit: all arguments are evaluated before they are passed in." + () -> true + (x) -> bool (x) + (x, y) -> base :or (x, y) + (x, y, ...zs) -> fold (or, zs, base :or (x, y)) +} + +fn assoc { + "Takes a dict, key, and value, and returns a new dict with the key set to 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." + (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." + (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." + (d as :dict) -> do d > list > map (first, _) +} + +fn values { + "Takes a dict and returns a list of values in that dict." + (d as :dict) -> do d > list > map (second, _) +} + +& fn diff { +& "Takes two dicts and returns a dict describing their differences. Does this shallowly, offering diffs only for keys in the original dict." +& (d1 as :dict, d2 as :dict) -> { +& let key1 = keys (d1) +& let key2 = keys (d2) +& let all = do concat (d1, d2) > set > list +& let diffs = loop (all, []) with { +& & TODO: reduce this redundancy? +& ([k, ...ks], diffs) -> { +& let v1 = get (k, d1) +& let v2 = get (k, d2) +& if eq? (v1, v2) +& then recur (ks, diffs) +& else recur (ks, append (diffs, (k, (v1, v2)))) +& } +& ([k], diffs) -> { +& let v1 = get (k, d1) +& let v2 = get (k, d2) +& if eq? (v1, v2) +& then diffs +& else append (diffs, (k, (v1, v2))) +& } +& } +& dict (diffs) +& } +& } + +& 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 > nil? +} + +fn dict { + "Takes a list or tuple of (key, value) tuples and returns it as a dict. Returns dicts unharmed." + (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." + (d as :dict) -> true + (_) -> false +} + +fn each! { + "Takes a list and applies a function, presumably with side effects, to each element in the list. Returns nil." + (f! as :fn, []) -> nil + (f! as :fn, [x]) -> { f! (x); nil } + (f! as :fn, [x, ...xs]) -> { f! (x); each! (f!, xs) } +} + +&&& Trigonometry functions + +& Ludus uses turns as its default unit to measure angles +& However, anything that takes an angle can also take a +& units argument, that's a keyword of :turns, :degrees, or :radians + +let pi = base :pi + +let tau = mult (2, pi) + +fn sin { + "Returns the sine of an angle. Default angle measure is turns. An optional keyword argument specifies the units of the angle passed in." + (a as :number) -> do a > turn/rad > base :sin + (a as :number, :turns) -> do a > turn/rad > base :sin + (a as :number, :degrees) -> do a > deg/rad > base :sin + (a as :number, :radians) -> base :sin (a) +} + +fn cos { + "Returns the cosine of an angle. Default angle measure is turns. An optional keyword argument specifies the units of the angle passed in." + (a as :number) -> do a > turn/rad > base :cos + (a as :number, :turns) -> do a > turn/rad > base :cos + (a as :number, :degrees) -> do a > deg/rad > base :cos + (a as :number, :radians) -> base :cos (a) +} + +fn tan { + "Returns the sine of an angle. Default angle measure is turns. An optional keyword argument specifies the units of the angle passed in." + (a as :number) -> do a > turn/rad > base :tan + (a as :number, :turns) -> do a > turn/rad > base :tan + (a as :number, :degrees) -> do a > deg/rad > base :tan + (a as :number, :radians) -> base :tan (a) +} + +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), 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))) + ) +} + +fn turn/deg { + "Converts an angle in turns to an angle in degrees." + (a as :number) -> mult (a, 360) +} + +fn deg/turn { + "Converts an angle in degrees to an angle in turns." + (a as :number) -> div (a, 360) +} + +fn turn/rad { + "Converts an angle in turns to an angle in radians." + (a as :number) -> mult (a, tau) +} + +fn rad/turn { + "Converts an angle in radians to an angle in turns." + (a as :number) -> div (a, tau) +} + +fn deg/rad { + "Converts an angle in degrees to an angle in radians." + (a as :number) -> mult (tau, div (a, 360)) +} + +fn rad/deg { + "Converts an angle in radians to an angle in degrees." + (a as :number) -> mult (360, div (a, tau)) +} + +fn atan/2 { + "Returns an angle from a slope. Takes an optional keyword argument to specify units. Takes either two numbers or a vector tuple." + (x as :number, y as :number) -> do base :atan_2 (x, y) > rad/turn + (x, y, :turns) -> atan/2 (x, y) + (x, y, :radians) -> base :atan_2 (x, y) + (x, y, :degrees) -> do base :atan_2 (x, y) > rad/deg + ((x, y)) -> atan/2 (x, y) + ((x, y), units as :keyword) -> atan/2 (x, y, units) +} + +fn mod { + "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 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 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 { + "Squares a number." + (x as :number) -> mult (x, x) +} + +fn sqrt { + "Returns the square root of a number. Panics if the number is negative." + (x as :number) if not (neg? (x)) -> base :sqrt (x) +} + +fn sqrt/safe { + "Returns a result containing the square root of a number, or an error if the number is negative." + (x as :number) -> if not (neg? (x)) + then (:ok, base :sqrt (x)) + else (:err, "sqrt of negative number") +} + +fn sum_of_squares { + "Returns the sum of squares of numbers." + () -> 0 + (x as :number) -> square (x) + (x as :number, y as :number) -> add (square (x), square (y)) + (x, y, ...zs) -> fold ( + fn (sum, z) -> add (sum, square (z)) + zs + sum_of_squares (x, y)) +} + +fn dist { + "Returns the distance from the origin to a point described by x and y, or by the vector (x, y)." + (x as :number, y as :number) -> sqrt (sum_of_squares (x, y)) + ((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 (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))) + (l as :list) -> { + 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) + } + & (s as :set) -> do s > list > random +} + +fn random_int { + "Returns a random integer. With one argument, returns a random integer between 0 and that number. With two arguments, returns a random integer between them." + (n as :number) -> do n > random > floor + (m as :number, n as :number) -> floor (random (m, n)) +} + +fn floor { + "Truncates a number towards negative infinity. With positive numbers, it returns the integer part. With negative numbers, returns the next more-negative integer." + (n as :number) -> base :floor (n) +} + +fn ceil { + "Truncates a number towards positive infinity. With negative numbers, it returns the integer part. With positive numbers, returns the next more-positive integer." + (n as :number) -> base :ceil (n) +} + +fn round { + "Rounds a number to the nearest integer." + (n as :number) -> base :round (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) + (start as :number, end as :number) -> base :range (start, end) +} + +&&& 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 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 + +#{ + abs & math + add & math + and & bool + angle & math + any? & dicts lists strings sets tuples + append & lists sets + assert! & errors + assoc & dicts + assoc? & dicts + at & lists strings + atan/2 & math + back! & turtles + background! & turtles + between? & math + bg! & turtles + bk! & turtles + bool & bool + bool? & bool + box? & boxes + butlast & lists strings tuples + ceil & math + chars & strings + clear! & turtles + coll? & dicts lists sets tuples + colors & turtles + concat & string list set + contains? & list set + cos & math + count & string list set tuple dict + dec & math + deg/rad & math + deg/turn & math + dict & dict + dict? & dict + & diff & dict + dissoc & dict + dist & math + div & math + div/0 & math + div/safe & math + doc! & env + downcase & string + each! & list + empty? & list dict set string tuple + eq? & values + err & result + err? & result + even? & math + false? & bool + fd! & turtles + filter & list + first & list tuple + floor & math + fn? & functions + fold & lists + forward! & turtles + get & dicts + goto! & turtles + gt? & math + gte? & math + heading & turtles + heading/vector & math + hideturtle! & turtles + home! & turtles + inc & math + inv & math + inv/0 & math + inv/safe & math + join & lists strings + keep & lists + keys & dicts + keyword? & keywords + last & lists tuples + left! & turtles + list & lists + list? & lists + loadstate! & turtles + lt! & turtles + lt? & math + lte? & math + map & lists + max & math + min & math + mod & math + mod/0 + mod/safe + mult & math + neg & math + neg? & math + neq? & values + nil? & nil + not & bool + odd? & math + ok & results + ok? & results + & omit & set + or & bool + ordered? & lists tuples strings + pc! & turtles + pd! & turtles + pencolor & turtles + pencolor! & turtles + pendown! & turtles + pendown? & turtles + penup! & turtles + penwidth & turtles + penwidth! & turtles + pi & math + pos? & math + position & turtles + print! & environment + pu! & turtles + pw! & turtles + rad/deg & math + rad/turn & math + random & math dicts lists tuples sets + random_int & math + range & math lists + report! & environment + rest & lists tuples + right! & turtles + round & math + rt! & turtles + second & lists tuples + sentence & lists strings + & set & sets + & set? & sets + setheading! & turtles + show & strings + showturtle! & turtles + sin & math + slice & lists tuples strings + some & values + some? & values + split & strings + sqrt & math + sqrt/safe & math + square & math + state & environment + store! & boxes + string & strings + string? & strings + strip & strings + sub & math + 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 + type & values + unbox & boxes + unwrap! & results + unwrap_or & results + upcase & strings + update & dicts + update! & boxes + values & dicts + words & strings lists + ws? & strings + zero? & math +}