Add better error handling, improve prelude, postlude.
This commit is contained in:
parent
480e7abcf0
commit
8ce97081d0
33
src/ludus/error.cljc
Normal file
33
src/ludus/error.cljc
Normal file
|
@ -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)
|
||||
)
|
||||
)
|
|
@ -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 {}))
|
||||
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
(do
|
||||
|
||||
(-> "foo" run :errors)
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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!
|
||||
|
|
Loading…
Reference in New Issue
Block a user