The mechanical half, ahead of the parser change that needs it. tools/unit-return.py
fills the empty slot with () and rewrites Unit as () wherever a type is spelled --
(Fn [i32] Unit), (Map i32 Unit), a return type written out.
Deciding whether a defn already had a return type is the whole difficulty, and
the script does it the way parse.ml did: is_type_form is transcribed rather than
improved, because being identical to the parser it replaces is what makes the
sweep meaning-preserving. It is re-runnable, so the lanes that branched before
this can have the same pass at merge:
python3 tools/unit-return.py .
python3 tools/unit-return.py --in-strings test/test_flan.ml test/test_acceptance.ml \
test/test_session.ml emacs/test-flan-dev.el emacs/test-flan-mode.el
python3 tools/unit-return.py --raw-ml lib/prelude.ml
python3 tools/unit-return.py --in-html web/index.html
-v logs every defn it saw and what it decided, which is how a sweep of 440 sites
gets reviewed at all. Embedded modes pool a file's type declarations across all
its fragments, because a snippet split across concatenation -- decls ^ "(defn f
[s [u8]] Cursor ...)" -- cannot see the names the other half declared; pooled
names count only in bare-symbol position, for the same reason the prelude's do.
A fragment that cuts off mid-form is skipped rather than guessed at. Five sites
in test_flan.ml still needed a hand, and they are in this commit.
Two things ride along because the sweep needs them: parse.ml reads a lone () as
the return type of a function with no body, which was not a shape the old
optional slot could produce; and the map refusals name () rather than Unit, since
that is now the spelling a caller wrote.
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))))
|