The script is in tools/ rather than thrown away, because two lanes are
writing Flan in the old spelling right now and their files need the same
pass at merge.
It works on forms, not on text: a keyword becomes a dot only where it sits
in a field-label position inside a brace, so an enum member in value
position, a map key inside an EDN string and a type-position {K V} are all
left alone. :keys keeps its colon -- it names no field.
126 lines
5.0 KiB
Plaintext
126 lines
5.0 KiB
Plaintext
;;;; calc-me — THE FIRST ACCEPTANCE PROGRAM (build sequence milestone 2).
|
|
;;;;
|
|
;;;; $ calc-me "1 + 2 * (3 - 0.5) / 2"
|
|
;;;; 3.5
|
|
;;;;
|
|
;;;; Chosen to be the smallest program that is still a real one. What it needs:
|
|
;;;; functions, recursion, structs, (Ptr T) and `addr`, byte slices, `at`/`len`,
|
|
;;;; `while`, `set` on locals and on fields, `cond`, `match`, Option, i32/u8/f64,
|
|
;;;; and argv. What it deliberately does NOT need: an allocator, Vec, Map, any
|
|
;;;; generic function, any macro the user wrote, FFI beyond argv and stdout,
|
|
;;;; a window, or a frame loop. It runs headless, so it is the same test on
|
|
;;;; native and on wasm32 — which is how the second target gets proven early.
|
|
;;;;
|
|
;;;; No `ns` form and no package declaration: the package name is inferred from
|
|
;;;; the directory. `(package calc)` is written only when the name must differ
|
|
;;;; from the directory name. See plan.org "Modules".
|
|
|
|
;; ── Cursor over the input. A plain value struct; recursive descent shares
|
|
;; ── one by pointer. `addr` takes the address of a local; the pointer never
|
|
;; ── outlives the frame, so no allocator is involved.
|
|
(defstruct Cursor
|
|
[src [u8] ; non-owning slice into argv — calc-me never owns a byte
|
|
pos i32]) ; no initialiser means zeroed
|
|
|
|
(defn peek [c (Ptr Cursor)] u8
|
|
(if (< (.pos c) (len (.src c)))
|
|
(at (.src c) (.pos c))
|
|
0)) ; 0 doubles as end-of-input
|
|
|
|
(defn advance [c (Ptr Cursor)]
|
|
(set (.pos c) (+ (.pos c) 1))) ; field access auto-derefs one level
|
|
|
|
(defn skip-spaces [c (Ptr Cursor)]
|
|
(while (= (peek c) \space)
|
|
(advance c)))
|
|
|
|
;; digit? was written here until the prelude grew one. There is a single
|
|
;; top-level namespace, so a second definition is now an error rather than a
|
|
;; shadow — which is the rule working: two byte-identical digit? functions
|
|
;; that later drift apart is exactly what it exists to prevent.
|
|
|
|
;; ── number := digit+ ("." digit+)? ────────────────────────────────────
|
|
(defn parse-number [c (Ptr Cursor)] (Option f64)
|
|
(skip-spaces c)
|
|
(let [start (.pos c)]
|
|
(while (digit? (peek c))
|
|
(advance c))
|
|
(when (= (peek c) \.)
|
|
(advance c)
|
|
(while (digit? (peek c))
|
|
(advance c)))
|
|
(if (= start (.pos c))
|
|
None
|
|
(Some (bytes->f64 (slice (.src c) start (.pos c)))))))
|
|
|
|
;; ── primary := number | "(" expr ")" | "-" primary ────────────────────
|
|
;; `some` unwraps Some and early-returns None from THIS function. It and
|
|
;; Option are the only error handling here; Result, try and errdefer wait.
|
|
(defn parse-primary [c (Ptr Cursor)] (Option f64)
|
|
(skip-spaces c)
|
|
(cond
|
|
(= (peek c) \-)
|
|
(do (advance c)
|
|
(Some (- 0.0 (some (parse-primary c)))))
|
|
|
|
(= (peek c) \()
|
|
(do (advance c)
|
|
(let [v (some (parse-expr c 1))]
|
|
(skip-spaces c)
|
|
(if (= (peek c) \))
|
|
(do (advance c) (Some v))
|
|
None))) ; unbalanced paren
|
|
|
|
:else
|
|
(parse-number c)))
|
|
|
|
(defn precedence [op u8] i32
|
|
(cond
|
|
(or (= op \+) (= op \-)) 1
|
|
(or (= op \*) (= op \/)) 2
|
|
:else 0)) ; 0 means "not an operator"
|
|
|
|
(defn apply-op [op u8 l f64 r f64] f64
|
|
(cond
|
|
(= op \+) (+ l r)
|
|
(= op \-) (- l r)
|
|
(= op \*) (* l r)
|
|
:else (/ l r)))
|
|
|
|
;; ── expr := primary (op primary)*, precedence climbing ────────────────
|
|
;; Left-associative: the right operand is parsed at prec+1, so 1-2-3 is
|
|
;; (1-2)-3 and not 1-(2-3). Mutually recursive with parse-primary; top-level
|
|
;; names in a package are order-independent, so no forward declaration.
|
|
(defn parse-expr [c (Ptr Cursor) min-prec i32] (Option f64)
|
|
(let [lhs (some (parse-primary c))]
|
|
(skip-spaces c)
|
|
(let [prec (precedence (peek c))]
|
|
(while (and (> prec 0) (>= prec min-prec))
|
|
(let [op (peek c)]
|
|
(advance c)
|
|
(set lhs (apply-op op lhs (some (parse-expr c (+ prec 1))))))
|
|
(skip-spaces c)
|
|
(set prec (precedence (peek c)))))
|
|
(Some lhs)))
|
|
|
|
;; ── Whole input, or nothing. Trailing junk is an error, not ignored. ──
|
|
(defn evaluate [src [u8]] (Option f64)
|
|
(let [c (Cursor {.src src})] ; pos omitted: zeroed
|
|
(let [v (some (parse-expr (addr c) 1))]
|
|
(skip-spaces (addr c))
|
|
(if (= (peek (addr c)) 0)
|
|
(Some v)
|
|
None))))
|
|
|
|
;; Entry point: (defn main [args [string]] i32). Both the parameter and the
|
|
;; return type are optional — sand.flan uses the bare (defn main []) form.
|
|
;; print and println are compiler-provided and structural: the walk over the
|
|
;; argument's concrete type happens at compile time, so there is nothing to
|
|
;; dispatch on at run time and no type to name at the call site.
|
|
(defn main [args [string]] i32
|
|
(if (< (len args) 2)
|
|
(do (println "usage: calc-me \"1 + 2 * 3\"") 1)
|
|
(match (evaluate (bytes (at args 1)))
|
|
(Some v) (do (print v) (println "") 0)
|
|
None (do (println "calc-me: cannot parse") 1))))
|