flan/test/programs/format.flan
Joseph Ferano 26c53e0a19 Every defn in the tree states its return type, and Unit is written ()
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.
2026-09-12 23:06:40 +07:00

87 lines
3.8 KiB
Plaintext

;;;; format-f64: a number rendered to a fixed number of decimal places.
;;;;
;;;; The runtime's f64->bytes is snprintf "%g" and there is no precision to
;;;; pass it, so this is the first number formatter in the language that a
;;;; caller can steer. Every case below is one a plausible wrong version gets
;;;; wrong, and three of them are the ones that actually ship broken: the
;;;; carry, where the rounded fraction equals the scale and is not a fraction
;;;; at all; the zero padding, without which 1.005 prints as "1.5"; and the
;;;; sign, which belongs to the number and not to its integer part, because
;;;; -0.5 has an integer part of 0 and 0 carries no sign.
(defn show [x f64 p i32] ()
(let [v (format-f64 x p)]
(println (string (as-slice v)))
(free v)))
(defn main [] i32
;; The ordinary cases, and the one %g cannot do at all: 1/60 wanted to two
;; places is a frame time, and "%g" answers 0.0166667.
(show 3.14159 2) ; 3.14
(show 0.0166667 2) ; 0.02
(show 1234.5 1) ; 1234.5
(show 2.0 0) ; 2
(show 2.0 3) ; 2.000
;; Rounding is half away from zero at the last digit kept, on both signs.
(show 0.125 2) ; 0.13
(show -0.125 2) ; -0.13
(show 2.5 0) ; 3
(show -2.5 0) ; -3
;; The carry. 0.999995 scaled by 10^5 rounds to exactly 100000, which is the
;; next integer; without the carry this prints "0.100000".
(show 0.999995 5) ; 1.00000
(show 9.99 1) ; 10.0
(show -9.99 1) ; -10.0
(show 0.99 0) ; 1
;; Zero padding. The fraction of 1.005 at three places is 5, and five digits
;; is not the same number as 005.
(show 1.005 3) ; 1.005
(show 1.0001 4) ; 1.0001
(show 7.0 6) ; 7.000000
;; The sign lives on the number, not on the integer part: both of these have
;; an integer part of 0, which i64->bytes renders without a sign.
(show -0.5 2) ; -0.50
(show -0.004 2) ; -0.00
;; -0.0 prints as a plain zero. The sign test is (< x 0.0), which -0.0 fails,
;; and text is not where the sign of a zero should be read from.
(show 0.0 2) ; 0.00
(show -0.0 2) ; 0.00
;; Precision is clamped rather than refused, at both ends.
(show 1.5 -3) ; 2
(show 1.5 40) ; 1.500000000
;; The three inputs with no decimal expansion.
(show (/ 0.0 0.0) 2) ; nan
(show (/ 1.0 0.0) 2) ; inf
(show (/ -1.0 0.0) 2) ; -inf
;; Past 9e18 an f64 has no fractional bits and the integer part does not fit
;; in an i64, so this falls back to %g rather than approximating.
(show 1e20 2) ; 1e+20
;; A large magnitude that does fit, where the fraction is genuinely gone: an
;; f64 has no bits below 1 up there, so the padding produces the zeros.
(show 1234567890123.0 2) ; 1234567890123.00
;; And the thing it is for: a formatted number inside a built string, which
;; needs the integer part copied out before the fraction is rendered, because
;; both come through the runtime's one shared scratch buffer.
(let [b (vec-new u8)]
(append! (addr b) (bytes "fps "))
(let [f (format-f64 59.94 1)]
(append! (addr b) (as-slice f))
(free f))
(append! (addr b) (bytes " / frame "))
(let [f (format-f64 0.0166667 4)]
(append! (addr b) (as-slice f))
(free f))
(println (string (as-slice b))) ; fps 59.9 / frame 0.0167
(free b))
0)