From b8019a96d580923033fe5147bed7bbeb8b78e348 Mon Sep 17 00:00:00 2001
From: Joseph Ferano run builds to a temporary file and execs it.
The smallest program:
(defn main []
- (print-line "hello from flan"))
+ (println "hello from flan"))
The entry point is (defn main [args [string]] i32). Both the parameter
and the return type are optional: omitting args means the program ignores
@@ -337,10 +338,10 @@ heap is involved.
3
wisp
@@ -361,13 +362,13 @@ wisp
;; catches it — the same message, and the program stops where it happened.
(defn main []
(let [i 7]
- (print-line "before")
- (print-i64 (i64 (at xs i)))
- (print-line "unreachable")))
+ (println "before")
+ (print (at xs i))
+ (println "unreachable")))
$ flan run bounds.flan
before
-bounds.flan:9:28: index 7 is out of bounds for length 3
+bounds.flan:9:19: index 7 is out of bounds for length 3
$ echo $?
134
@@ -408,9 +409,9 @@ have one type, and every conversion is written as a cast:
(let [n 40 ; i32, inferred
big (i64 n) ; every widening is written
x 1.5] ; f64
- (print-i64 (+ big 2)) (newline)
- (print-f64 (* x 2.5)) (newline)
- (print-i64 (i64 (bit-xor (<< 1 8) 255))) (newline)
+ (print (+ big 2)) (println "")
+ (print (* x 2.5)) (println "")
+ (print (bit-xor (<< 1 8) 255)) (println "")
0))
42
@@ -455,9 +456,9 @@ its fields, and omitted fields are zeroed.
(defn main []
(let [c (Cursor {:src (bytes "hi")})] ; pos omitted, so pos is 0
- (print-i64 (i64 (peek (addr c)))) (newline)
+ (print (peek (addr c))) (println "")
(advance (addr c))
- (print-i64 (i64 (peek (addr c)))) (newline)))
+ (print (peek (addr c))) (println "")))
104
105
@@ -466,7 +467,8 @@ its fields, and omitted fields are zeroed.
byte is a u8 — but there is a byte literal, so \h is 104 and
\space is 32, and the prelude's digit? reads as
(and (>= b \0) (<= b \9)). To see a byte as a letter rather than as a
-number, print a slice of them with print-bytes.
+number, print a slice of them: print writes a [u8] as
+its bytes.
An enum is an i32 at run time and its own type in the checker. A
keyword at a call site resolves against the parameter's enum type at compile time, so a
@@ -484,8 +486,8 @@ typo is an error there rather than a wrong number later.
space
an arrow
@@ -515,10 +517,10 @@ functions need no forward declaration. Globals come in two kinds:
(defvar grid [rows [cols u32]]) ; BSS, rows*cols*4 bytes
(defn main []
- (print-i64 (i64 cell-size)) (newline)
- (print-f64 (f64 gravity)) (newline)
- (print-i64 (i64 current-color)) (newline)
- (print-i64 (i64 (at grid 2 3))) (newline))
+ (print cell-size) (println "")
+ (print gravity) (println "")
+ (print current-color) (println "")
+ (print (at grid 2 3)) (println ""))
5
0.05
@@ -551,10 +553,10 @@ whose type matters is named at the top level rather than written inline.
(defn countdown [n i32]
(let [i n]
(while (> i 0)
- (print-i64 (i64 i))
- (print-str " ")
+ (print i)
+ (print " ")
(set i (- i 1)))
- (newline)))
+ (println "")))
(defn first-even [s [i32]] (Option i32)
(dotimes [i (len s)]
@@ -563,13 +565,13 @@ whose type matters is named at the top level rather than written inline.
None)
(defn main []
- (print-line (classify -3))
+ (println (classify -3))
(countdown 4)
(unless false
- (print-line "unless runs when the test is false"))
+ (println "unless runs when the test is false"))
(match (first-even (slice nums 0 (len nums)))
- (Some n) (do (print-i64 (i64 n)) (newline))
- None (print-line "none")))
+ (Some n) (do (print n) (println ""))
+ None (println "none")))
negative
4 3 2 1
@@ -604,11 +606,11 @@ on nothing else today. some unwraps Some and early-ret
(defn main []
(match (doubled-first (slice nums 0 4))
- (Some i) (do (print-i64 (i64 i)) (newline)) ; 4
- None (print-line "not found"))
+ (Some i) (do (print i) (println "")) ; 4
+ None (println "not found"))
(match (index-of-i32 (slice nums 0 4) 99)
- (Some i) (do (print-i64 (i64 i)) (newline))
- None (print-line "not found")))
+ (Some i) (do (print i) (println ""))
+ None (println "not found")))
4
not found
@@ -620,16 +622,16 @@ not found
has not executed yet and must not fire.
(defn work [n i32] i32
- (defer (print-line "second"))
- (defer (print-line "first")) ; innermost-first at exit
+ (defer (println "second"))
+ (defer (println "first")) ; innermost-first at exit
(when (< n 0)
(return 0)) ; runs both defers above it
- (print-line "body")
+ (println "body")
n)
(defn main []
- (print-i64 (i64 (work 3)))
- (newline))
+ (print (work 3))
+ (println ""))
body
first
@@ -660,18 +662,18 @@ It is a place: (set (at grid r c) v) and (addr (at grid r c))
(defn main []
(set (at grid 1 2) 7)
- (print-i64 (i64 (at grid 1 2))) (newline) ; 7
- (print-i64 (i64 (len palette))) (newline) ; 4
+ (print (at grid 1 2)) (println "") ; 7
+ (print (len palette)) (println "") ; 4
;; A slice is ptr+len and non-owning: it views the array, it does not copy it.
(let [row (slice (at grid 1) 0 cols)]
(set (at row 0) 5)
- (print-i64 (i64 (at grid 1 0))) (newline) ; 5 — the same storage
- (print-i64 (sum-i32 row)) (newline)) ; 12
+ (print (at grid 1 0)) (println "") ; 5 — the same storage
+ (print (sum-i32 row)) (println "")) ; 12
;; (zeroed) is a memset, not an allocation.
(set grid (zeroed))
- (print-i64 (i64 (at grid 1 2))) (newline)) ; 0
+ (print (at grid 1 2)) (println "")) ; 0
7
4
@@ -682,17 +684,72 @@ It is a place: (set (at grid r c) v) and (addr (at grid r c))
A reversed range — lo greater than hi — traps, rather than
yielding a huge unsigned length.
+Printing
+
+println prints a value and a newline; print is the same
+walk without the newline. There is one of each and they take any type, but neither is a
+function and neither is overloading: the compiler walks the argument's type where the
+call is written and emits the printing for it. Nothing is decided at run time — a Flan
+value carries no header, so nothing at run time could say what it is — and there is no
+user-supplied printer to choose between.
+
+(defenum Key [space 32 left 263])
+(defstruct Enemy [hp i32 name string key Key])
+
+(defn look-up [k Key] (Option i32)
+ (if (= k :space) (Some 32) None))
+
+(defn main []
+ (println 42) ; an i32, uncast
+ (println 1.5)
+ (println (Enemy {:hp 3 :name "wisp" :key :left}))
+ (println (look-up :space))
+ (println (look-up :left))
+ (print "no newline: ") (println true))
+
+42
+1.5
+(Enemy {:hp 3 :name "wisp" :key :left})
+(some 32)
+none
+no newline: true
+
+The walk covers every integer and float type, bool, Unit,
+string, [u8], enums, Ptr, Option,
+structs, fixed arrays and slices. An enum member comes back as its name: the value is
+an i32 by the time the backend sees it, so the name is recovered here from
+the checker's table, and a value outside the declared members falls through to the
+number, which is what you would want to see. A Ptr prints as
+<ptr> and is never followed — it is the one thing that could make
+the walk cycle, and dereferencing a pointer on someone else's behalf is not safe.
+
+A string prints raw at the top level and quoted-and-escaped inside a structure.
+Those are not in conflict: (println "hello") has to print
+hello or it is useless, and the name field above has to be
+quoted or it could not be told from the punctuation around it.
+
+The walk is bounded in depth and in span, so a deeply nested value or a
+[100 [100 u32]] grid prints ... rather than a screenful — and,
+since the walk is unrolled at compile time, rather than putting ten thousand printing
+sites in the module.
+
+That the walk takes the argument's own type matters more here than it would in a
+language that widens implicitly. Nothing widens implicitly in this one, so a printer
+that named a type would need a cast written at every call — and a u64
+above 263 put through a signed one comes out negative. print
+takes the value as it is and prints the number it holds.
+
The prelude
The prelude is written in Flan, all but one line of it, and prepended to every
-program, so nothing in it needs importing. Printing is deliberately not a primitive:
-write-stdout is the one output primitive and everything above it is
-ordinary Flan.
+program, so nothing in it needs importing. It holds no printing of its own:
+print and println are the compiler's, and
+write-stdout — the one output primitive — is what they are written
+over.
Group Names
-output print-str, print-bytes, print-i64, print-f64, print-line, newline
slices of i32 swap-i32!, reverse-i32!, sort-i32!, index-of-i32, min-i32, max-i32, sum-i32
bytes bytes=?, starts-with?, ends-with?, index-of-byte, index-of-bytes, trim, digit?, space?
parsing parse-i64, parse-f64
@@ -754,8 +811,8 @@ and no ceremony.
(defn main []
(let [v (g/add (g/V2 {:x 3.0 :y 0.0})
(g/V2 {:x 0.0 :y 4.0}))]
- (print-f64 (f64 (g/length v)))
- (newline)))
+ (print (g/length v))
+ (println "")))
5
@@ -827,12 +884,12 @@ normally leaves the signaller to carry on — the accumulation case:
(defn main []
(load-all) ; no handler: a no-op
- (print-i64 seen) (newline) ; 0
+ (print seen) (println "") ; 0
;; A handler that returns normally accumulates and lets the signaller run on.
(handler-bind [(AssetMissing [c] (set seen (+ seen (i64 (.id c)))))]
(load-all))
- (print-i64 seen) (newline)) ; 3
+ (print seen) (println "")) ; 3
0
3
@@ -860,12 +917,12 @@ first, before the clause body starts.
(retry [] 7)))
(defn main []
- (print-i64 (i64 (fetch 1))) (newline) ; 101 — nothing handled it
+ (print (fetch 1)) (println "") ; 101 — nothing handled it
(handler-bind [(AssetMissing [c] (invoke-restart 'use-placeholder))]
- (print-i64 (i64 (fetch 2))) (newline)) ; -1
+ (print (fetch 2)) (println "")) ; -1
- (print-i64 cleanups) (newline)) ; 2 — the defer ran both times
+ (print cleanups) (println "")) ; 2 — the defer ran both times
101
-1
@@ -947,8 +1004,8 @@ and an exit status of 134:
(retry [] 7)))
(defn main []
- (print-i64 (i64 (load 1)))
- (newline))
+ (print (load 1))
+ (println ""))
$ flan run boom.flan
unhandled Missing
@@ -966,7 +1023,7 @@ is generated; a Flan string crosses as ptr+len, exactly as it is stored.
(declare cos-f64 [x f64] f64 "cos")
(defn main []
- (print-f64 (cos-f64 0.0)) (newline))
+ (print (cos-f64 0.0)) (println ""))
1
@@ -1072,20 +1129,22 @@ bound at link time cannot be made to notice one, so a --dev build r
every Flan-to-Flan call through a cell — a mutable global holding the address of the
function that is current.
-Here is the whole of hello.flan through
-flan emit --dev:
+Here is the defer example from above — its
+main calls work — through flan emit --dev:
-@"flan.cell.print-line" = global ptr @"flan.print-line"
+@"flan.cell.work" = global ptr @"flan.work"
define {} @"flan.main"(ptr %xfer) {
entry:
- %t1 = load ptr, ptr @"flan.cell.print-line"
- %t2 = call {} %t1(%slice { ptr @".str.36", i64 15 }, ptr %xfer)
+ %t7 = alloca %slice
+ %t1 = load ptr, ptr @"flan.cell.work"
+ %t2 = call i32 %t1(i32 3, ptr %xfer)
-The call site loads the cell rather than naming @"flan.print-line"
+
The call site loads the cell rather than naming @"flan.work"
directly. The signature carries ptr %xfer — the transfer channel from
conditions, on every Flan function, release builds
-included.
+included. println is not in there: it is not a Flan function, so there is
+no call to route and no cell for it.
Redefinition is then one store, below a microsecond. Three rules follow. A
redefinition module declares every global external, so globals live in the host
@@ -1242,10 +1301,10 @@ module, and the headless sand acceptance program prints the same 64-bit hash und
it does natively:
$ flan run test/programs/sand-headless.flan
--2851001042534928384
+15595743031174623232
$ flan build test/programs/sand-headless.flan --target=wasm32-wasi -o sand.wasm
$ node --no-warnings test/wasm-run.mjs sand.wasm
--2851001042534928384
+15595743031174623232
The RNG is written in Flan rather than called from libc for that number: a grid hash
is only a regression test if the sequence is byte-identical on both targets. It holds at