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.
155 lines
6.5 KiB
Plaintext
155 lines
6.5 KiB
Plaintext
;;;; The prelude's second tier: the functions that return new storage.
|
|
;;;;
|
|
;;;; Every one of these was refused by name in prelude.ml until there was an
|
|
;;;; allocator to return a Vec from, and this file is the corpus that says the
|
|
;;;; refusals are lifted. The cases are chosen the way the slice-algorithm
|
|
;;;; tests were: each is an input a plausible wrong version gets wrong.
|
|
;;;;
|
|
;;;; Everything allocated here is freed, even though leaking is defined
|
|
;;;; behaviour (spec-memory.md), because this file is the example people copy.
|
|
|
|
;;; A (Vec u8) printed as text, without the caller writing the two-step every
|
|
;;; time. as-slice borrows -- it copies ptr+len and never the elements -- so v
|
|
;;; is still the owner afterwards and is still free-able.
|
|
(defn show [v (Ptr (Vec u8))] ()
|
|
(println (string (as-slice (deref v)))))
|
|
|
|
(defn main [] i32
|
|
;; The builder. Three appends and two numbers into one Vec, which is the
|
|
;; case the shared static scratch buffer in the runtime makes impossible for
|
|
;; i64->bytes on its own: two of its results cannot be held at once, and
|
|
;; these two numbers are both in the answer.
|
|
(let [b (vec-new u8)]
|
|
(append! (addr b) (bytes "x="))
|
|
(append-i64! (addr b) 42)
|
|
(append! (addr b) (bytes " y="))
|
|
(append-i64! (addr b) -7)
|
|
(append! (addr b) (bytes " r="))
|
|
(append-f64! (addr b) 1.5)
|
|
(show (addr b)) ; x=42 y=-7 r=1.5
|
|
(free b))
|
|
|
|
;; concat over three parts, and over none -- the empty result rather than a
|
|
;; trap.
|
|
(let [parts [(bytes "one") (bytes "") (bytes "two")]]
|
|
(let [c (concat (slice parts 0 3))]
|
|
(show (addr c)) ; onetwo
|
|
(free c)))
|
|
(let [parts [(bytes "unused")]]
|
|
(let [c (concat (slice parts 0 0))]
|
|
(println (len c)) ; 0
|
|
(free c)))
|
|
|
|
;; join: n parts, n-1 separators. The one-part case is the one that must not
|
|
;; emit a separator at all, and the zero-part case is the one a "append then
|
|
;; chop the tail" join gets wrong because there is no tail.
|
|
(let [parts [(bytes "a") (bytes "b") (bytes "c")]]
|
|
(let [j (join (slice parts 0 3) (bytes ", "))]
|
|
(show (addr j)) ; a, b, c
|
|
(free j))
|
|
(let [j (join (slice parts 0 1) (bytes ", "))]
|
|
(show (addr j)) ; a
|
|
(free j))
|
|
(let [j (join (slice parts 0 0) (bytes ", "))]
|
|
(println (len j)) ; 0
|
|
(free j))
|
|
;; An empty separator is concat.
|
|
(let [j (join (slice parts 0 3) (bytes ""))]
|
|
(show (addr j)) ; abc
|
|
(free j)))
|
|
|
|
;; repeat, including zero times.
|
|
(let [r (repeat-bytes (bytes "ab") 3)]
|
|
(show (addr r)) ; ababab
|
|
(free r))
|
|
(let [r (repeat-bytes (bytes "ab") 0)]
|
|
(println (len r)) ; 0
|
|
(free r))
|
|
|
|
;; The allocating case pair. The input is a string literal, which lives in
|
|
;; .rodata -- an in-place lower would either segfault at -O0 or be deleted at
|
|
;; -O2, and that is exactly why these exist. Digits and punctuation pass
|
|
;; through untouched, which is the range check a table-free version gets
|
|
;; wrong by shifting every byte.
|
|
(let [l (to-lower (bytes "Hello, World 42!"))]
|
|
(show (addr l)) ; hello, world 42!
|
|
(free l))
|
|
(let [u (to-upper (bytes "Hello, World 42!"))]
|
|
(show (addr u)) ; HELLO, WORLD 42!
|
|
(free u))
|
|
|
|
;; replace. "aaa" with "aa" -> "b" is the non-overlapping rule: the answer is
|
|
;; "ba", because the match consumes both a's and the scan resumes after them.
|
|
(let [r (replace-bytes (bytes "aaa") (bytes "aa") (bytes "b"))]
|
|
(show (addr r)) ; ba
|
|
(free r))
|
|
;; A replacement longer than what it replaces, and one that is empty.
|
|
(let [r (replace-bytes (bytes "a,b,c") (bytes ",") (bytes " -- "))]
|
|
(show (addr r)) ; a -- b -- c
|
|
(free r))
|
|
(let [r (replace-bytes (bytes "a,b,c") (bytes ",") (bytes ""))]
|
|
(show (addr r)) ; abc
|
|
(free r))
|
|
;; No occurrence is a copy, and an empty `from` is a copy -- the reading
|
|
;; where it matches everywhere is an infinite loop.
|
|
(let [r (replace-bytes (bytes "abc") (bytes "z") (bytes "!"))]
|
|
(show (addr r)) ; abc
|
|
(free r))
|
|
(let [r (replace-bytes (bytes "abc") (bytes "") (bytes "!"))]
|
|
(show (addr r)) ; abc
|
|
(free r))
|
|
|
|
;; split. n separators, n+1 fields, always -- so the trailing empty field is
|
|
;; present, which is where Odin's own iterator and its allocating split
|
|
;; disagree with each other.
|
|
(let [f (split (bytes "a,b,c") \,)]
|
|
(println (len f)) ; 3
|
|
(println (string (at f 0))) ; a
|
|
(println (string (at f 2))) ; c
|
|
(free f))
|
|
(let [f (split (bytes "a,b,") \,)]
|
|
(println (len f)) ; 3
|
|
(println (len (at f 2))) ; 0
|
|
(free f))
|
|
(let [f (split (bytes ",a") \,)]
|
|
(println (len f)) ; 2
|
|
(println (len (at f 0))) ; 0
|
|
(free f))
|
|
;; No separator at all is one field, and the empty input is one empty field.
|
|
(let [f (split (bytes "abc") \,)]
|
|
(println (len f)) ; 1
|
|
(println (string (at f 0))) ; abc
|
|
(free f))
|
|
(let [f (split (bytes "") \,)]
|
|
(println (len f)) ; 1
|
|
(println (len (at f 0))) ; 0
|
|
(free f))
|
|
|
|
;; The fields are slices of the input and nothing was copied: this one
|
|
;; round-trips through join, and the separator it rebuilds with is a
|
|
;; different one, so an implementation that handed back the original slice
|
|
;; would print the original string.
|
|
(let [f (split (bytes "a,b,c") \,)]
|
|
(let [j (join (as-slice f) (bytes "/"))]
|
|
(show (addr j)) ; a/b/c
|
|
(free j))
|
|
(free f))
|
|
|
|
;; The allocator is the context's, so with-allocator moves the whole tier
|
|
;; into an arena -- which is the answer to the fixed arity of a defn, and the
|
|
;; reason none of these takes an allocator argument. free-all is what
|
|
;; releases the region, and arena-destroy hands it back.
|
|
(let [a (arena-new 4096)]
|
|
(with-allocator a
|
|
(let [parts [(bytes "in") (bytes "arena")]]
|
|
(let [j (join (slice parts 0 2) (bytes "-"))]
|
|
(show (addr j)) ; in-arena
|
|
;; The free is written because the binding is dead after it either
|
|
;; way, and it keeps the block: an arena cannot release one, which
|
|
;; is the difference the capability set exists to state. free-all
|
|
;; below is what actually releases this.
|
|
(free j))))
|
|
(free-all a)
|
|
(arena-destroy a))
|
|
0)
|