From 8ce97081d0978bc95846161d9334d918a3189594 Mon Sep 17 00:00:00 2001 From: Scott Richmond Date: Wed, 6 Dec 2023 20:02:14 -0500 Subject: [PATCH] Add better error handling, improve prelude, postlude. --- src/ludus/error.cljc | 33 ++++++ src/ludus/interpreter.cljc | 13 ++- src/ludus/node.cljc | 35 ++++-- src/ludus/postlude.ld | 5 +- src/ludus/prelude.ld | 224 ++++++++++++++++++++++++++++--------- 5 files changed, 241 insertions(+), 69 deletions(-) create mode 100644 src/ludus/error.cljc diff --git a/src/ludus/error.cljc b/src/ludus/error.cljc new file mode 100644 index 0000000..4fddd3b --- /dev/null +++ b/src/ludus/error.cljc @@ -0,0 +1,33 @@ +(ns ludus.error + (:require [clojure.string :as string])) + +(defn get-line [source {:keys [line]}] + (let [lines (string/split-lines source) + the_line (nth lines (dec line))] + the_line)) + +(defn get-underline [source {:keys [line start lexeme]} prefix] + (let [lines (string/split-lines source) + lines-before (subvec lines 0 (dec line)) + line-start (reduce (fn [len line] (+ len (count line))) (count lines-before) lines-before) + from-start (- start line-start) + underline-length (count lexeme) + padding (string/join (take (+ prefix from-start) (repeat " "))) + underline (string/join (take underline-length (repeat "^")))] + (apply str padding underline) + )) + +(defn scan-error [] :TODO) + +(defn parse-error [source {:keys [trace token]}] + (let [line (get-line source token) + line-num (:line token) + prefix (str line-num ": ") + underline (get-underline source token (count prefix)) + expected (first trace) + got (:type token) + message (str "Ludus found a parsing error on line " line-num ".\nExpected: " expected "\nGot: " got "\n") + ] + (str message "\n" prefix line "\n" underline) + ) + ) diff --git a/src/ludus/interpreter.cljc b/src/ludus/interpreter.cljc index 3d037fb..ebf239a 100644 --- a/src/ludus/interpreter.cljc +++ b/src/ludus/interpreter.cljc @@ -946,7 +946,7 @@ (defn interpret-safe [source parsed ctx] (let [base-ctx (volatile! {::parent (volatile! (merge ludus-prelude ctx))})] (try - (println "Running source: " source) + ;(println "Running source: " source) (interpret-ast parsed base-ctx) (catch #?(:clj Throwable :cljs js/Object) e (println "Ludus panicked!") @@ -954,17 +954,20 @@ (println ">>> " (get-line source (get-in (ex-data e) [:ast :token :line]))) (println (ex-message e)) ;(pp/pprint (ex-data e)) - (throw e) + ;(throw e) + {::data/error true + :line (get-in (ex-data e) [:ast :token :line]) + :message (ex-message e)} )))) ;; repl -(comment +(do - (def source "fn foo () -> :foo") + (def source "1 2") (def tokens (-> source scanner/scan :tokens)) - (def ast (p/apply-parser g/fn-named tokens)) + (def ast (p/apply-parser g/script tokens)) ;(def result (interpret-safe source ast {})) diff --git a/src/ludus/node.cljc b/src/ludus/node.cljc index b8c3289..e86f9c7 100644 --- a/src/ludus/node.cljc +++ b/src/ludus/node.cljc @@ -7,6 +7,7 @@ [ludus.show :as show] [ludus.base :as base] [ludus.data :as data] + [ludus.error :as error] ) ) @@ -26,28 +27,38 @@ :fn (throw (ex-info (str "Cannot export functions from Ludus to Clojure. You tried exporting " (show/show value)) {})))) +(defn clean-out [value] + #?(:clj value :cljs (clj->js value))) + (defn run [source] (let [user_scanned (s/scan source) user_tokens (:tokens user_scanned) user_parsed (p/apply-parser g/script user_tokens) user_result (i/interpret-safe source user_parsed {}) + result_str (show/show user_result) post_scanned (s/scan pre/postlude) post_tokens (:tokens post_scanned) post_parsed (p/apply-parser g/script post_tokens) post_result (i/interpret-safe source post_parsed {}) - ludus_result (assoc post_result :result user_result) + ludus_result (assoc post_result :result result_str) clj_result (ld->clj ludus_result) ] - #?(:clj clj_result :cljs (clj->js clj_result)) + (cond + (not-empty (:errors user_tokens)) + (clean-out {:errors (:errors user_tokens)}) + + (= :err (:status user_parsed)) + (clean-out {:errors [(error/parse-error source user_parsed)]}) + + (::data/error user_result) + (clean-out {:errors [user_result]}) + + :else + (clean-out clj_result) + ) )) -(comment - (def res (run " -fd! (100) -rt! (0.25) -fd! (100) -pencolor! (200) -fd! (50) - ")) - (:draw res) - ) \ No newline at end of file +(do + + (-> "foo" run :errors) + ) diff --git a/src/ludus/postlude.ld b/src/ludus/postlude.ld index 024d8a0..d82f206 100644 --- a/src/ludus/postlude.ld +++ b/src/ludus/postlude.ld @@ -1,6 +1,7 @@ & this file runs after any given interpretation -& the goal is to output any global state -& this does not have base loaded into it: must be pure ludus +& even if the original interpretation panics +& the goal is to output any global state held in Ludus +& this does not have base loaded into it, only prelude: must be pure Ludus if turtle_state() :visible? then render_turtle! () else nil diff --git a/src/ludus/prelude.ld b/src/ludus/prelude.ld index 2a32d02..47001ba 100644 --- a/src/ludus/prelude.ld +++ b/src/ludus/prelude.ld @@ -1,5 +1,25 @@ & this file, uniquely, gets `base` loaded as context. See src/ludus/base.cljc for exports +& the very base: know something's type +fn type { + "Returns a keyword representing the type of the value passed in." + (x) -> base :type (x) +} + +& ...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) -> loop (y, zs) with { + (a, [b]) -> base :eq (a, b) + (a, [b, ...cs]) -> if base :eq (a, b) + then recur (b, cs) + else 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) @@ -16,7 +36,7 @@ fn dec { (x as :number) -> base :dec (x) } -fn nth { +fn at { "Returns the element at index n of a list or tuple. Zero-indexed: the first element is at index 0." (xs as :list, n as :number) -> when { neg? (n) -> nil @@ -32,17 +52,17 @@ fn nth { fn first { "Returns the first element of a list or tuple." - (xs) -> nth (xs, 0) + (xs) -> at (xs, 0) } fn second { "Returns the second element of a list or tuple." - (xs) -> nth (xs, 1) + (xs) -> at (xs, 1) } fn last { "Returns the last element of a list or tuple." - (xs) -> nth (xs, sub (count (xs), 1)) + (xs) -> at (xs, sub (count (xs), 1)) } fn butlast { @@ -125,6 +145,17 @@ fn append { (xs as :set, x) -> base :conj (xs, x) } +fn concat { + "Combines two lists, strings, or sets." + (x as :string, y as :string) -> base :str (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)) +} + +& the console: sending messages to the outside world +& the console is *both* something we send to the host language's console +& ...and also a list of messages. ref console = [] fn flush! { @@ -138,11 +169,10 @@ fn flush! { fn add_msg! { "Adds a message to the console." - (msgs) -> { - let strs = map (show, msgs) - let msg = fold (concat, strs, "") + (msg as :string) -> update! (console, append, (_, msg)) + (msgs as :list) -> { + let msg = do msgs > map (string, _) > join update! (console, append (_, msg)) - :ok } } @@ -151,6 +181,7 @@ fn print! { (...args) -> { base :print (args) add_msg! (args) + :ok } } @@ -159,24 +190,11 @@ fn show { (x) -> base :show (x) } -fn type { - "Returns a keyword representing the type of the value passed in." - (x) -> base :type (x) -} - fn prn! { "Prints the underlying Clojure data structure of a Ludus value." (x) -> base :prn (x) } -fn concat { - "Combines two lists, strings, or sets." - (x as :string, y as :string) -> base :str (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 report! { "Prints a value, then returns it." (x) -> { @@ -189,6 +207,56 @@ fn report! { } } +fn panic! { + "Causes Ludus to panic, outputting any arguments as messages." + () -> { + add_msg! ("Ludus panicked!") + base :panic! () + } + (...args) -> { + add_msg! ("Ludus panicked!") + add_msg! (args) + base :panic! (args) + } +} + +fn doc! { + "Prints the documentation of a function to the console." + (f as :fn) -> base :doc (f) + (_) -> :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 differen kinds of values." + (x as :string) -> x + (x) -> show (x) + (x, ...xs) -> fold (string, xs, x) +} + +fn join { + "Takes a list of strings, and joins them into a single string, interposing an optional separator." + ([]) -> "" + ([str as :string]) -> s + (strs as :list) -> join (strs, "") + (strs, separator as :string) -> fold ( + fn (joined, to_join) -> concat (joined, separator, to_join) + ) +} + +& in another prelude, with a better actual base language than Java (thanks, Rich), counting strings would be reasonable but complex: count/bytes, count/points, count/glyphs. Java's UTF16 strings make this unweildy. + +& TODO: add trim, trim/left, trim/right; pad/left, pad/right +& ...also a version of at, + +&&& references: mutable state and state changes + fn ref? { "Returns true if a value is a ref." (r as :ref) -> true @@ -214,6 +282,8 @@ fn update! { } } +&&& numbers, basically: arithmetic and not much else, yet + fn number? { "Returns true if a value is a number." (x as :number) -> true @@ -261,7 +331,7 @@ fn div { } fn div/0 { - "Divides number. Returns 0 on division by zero." + "Divides numbers. Returns 0 on division by zero." (x as :number) -> x (_, 0) -> 0 (x as :number, y as :number) -> base :div (x, y) @@ -304,18 +374,6 @@ fn zero? { (_) -> false } -fn eq? { - "Returns true if all arguments have the same value." - (x) -> true - (x, y) -> base :eq (x, y) - (x, y, ...zs) -> loop (y, zs) with { - (a, [b]) -> base :eq (a, b) - (a, [b, ...cs]) -> if base :eq (a, b) - then recur (b, cs) - else false - } -} - fn gt? { "Returns true if numbers are in decreasing order." (x as :number) -> true @@ -376,12 +434,22 @@ fn pos? { (_) -> 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 +} + +&&& true & false: boolean logic + fn bool? { "Returns true if a value is of type :boolean." (false) -> true @@ -420,6 +488,8 @@ fn or { (x, y, ...zs) -> fold (base :or, zs, base :or (x, y)) } +&&& associative collections: dicts, structs, namespaces +& TODO?: get_in, update_in fn assoc { "Takes a dict, key, and value, and returns a new dict with the key set to value." () -> #{} @@ -471,6 +541,7 @@ fn coll? { (coll as :list) -> true (coll as :tuple) -> true (coll as :set) -> true + (coll as :ns) -> true (_) -> false } @@ -516,20 +587,12 @@ fn each! { } } -fn panic! { - "Causes Ludus to panic, outputting any arguments as messages." - () -> base :panic! () - (...args) -> base :panic! (args) -} - -fn doc! { - "Prints the documentation of a function to the console." - (f as :fn) -> base :doc (f) - (_) -> :none -} - &&& 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) @@ -665,7 +728,55 @@ fn range { (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! ("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) + (value, message) -> if value + then value + else panic! ("Assert failed:", message, value) +} + &&& Turtle & other graphics + +& some basic colors +&&& TODO: add colors let colors = @{ :white (255, 255, 255, 255) :light_gray (150, 150, 150, 255) @@ -686,15 +797,19 @@ let turtle_init = #{ :visible? true } +& turtle states: refs that get modified by calls & turtle_commands is a list of commands, expressed as tuples -& the first member of each tuple is the command ref turtle_commands = [] & and a list of turtle states ref turtle_states = [turtle_init] +& and a list of calls to p5--at least for now ref p5_calls = [] +& ...and finally, a background color +& we need to store this separately because, while it can be updated later, +& it must be the first call to p5. ref bgcolor = colors :black fn add_call! (call) -> update! (p5_calls, append (_, call)) @@ -747,7 +862,7 @@ fn state/call () -> { let cmd = do turtle_commands > deref > last > first let states = deref (turtle_states) let curr = last (states) - let prev = nth (states, sub (count (states), 2)) + let prev = at (states, sub (count (states), 2)) match cmd with { :forward -> if curr :pendown? then make_line (prev :position, curr :position) @@ -785,6 +900,8 @@ fn back! { let bk! = back! +& turtles, like eveyrthing else in Ludus, use turns by default, +& not degrees fn left! { "Rotates the turtle left, measured in turns. Alias: lt!" (turns as :number) -> add_command! ((:left, turns)) @@ -857,7 +974,7 @@ fn goto! { 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.75 is 0º + & 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)) } @@ -925,7 +1042,7 @@ ns prelude { first second rest - nth + at last butlast slice @@ -966,6 +1083,7 @@ ns prelude { lt? lte? nil? + some? bool? bool not @@ -1006,6 +1124,12 @@ ns prelude { ceil round range + ok + ok? + err + err? + unwrap! + unwrap_or colors forward!, fd! back!, bk!