From a5e0c012246d08567c85e23c02a3ef1f0860008c Mon Sep 17 00:00:00 2001
From: Joseph Ferano Call that binary Places — the forms flan. Its subcommands:
+flan (read|parse|check|emit|shim) <file.flan>...
-flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] [--target=wasm32-wasi]
-flan run <file.flan> [args...]
-flan reload <program.flan> <forms.flan> [-o out.so]
-flan dev <program.flan> [-s socket]$ flan
+usage: flan (read|parse|check|emit|shim) <file.flan>...
+ flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] [--target=wasm32-wasi]
+ flan run <file.flan> [args...]
+ flan reload <program.flan> <forms.flan> [-o out.so]
+ flan dev <program.flan> [-s socket]read, parse, check, emit and
shim each stop the pipeline one stage further along and print what it
@@ -260,14 +261,36 @@ struct is shared mutably by passing its address down the call chain.set accepts — are a fixed list, not an extensible
setf:
+(set x v) ; a local or a defvar
-(set (.field x) v) ; x may be a struct or a (Ptr S)
-(set (at a i ...) v) ; a fixed array or a slice element
-(set (deref p) v) ; a whole-object store through a pointer
+
+(defstruct Enemy [hp i32 name string])
+
+(defvar spawned i32)
+(defconst room-size 4)
+(defvar room [room-size i32])
+
+;; `set` takes a fixed list of forms, not an extensible setf.
+(defn main []
+ (let [e (Enemy {:hp 10 :name "slime"})
+ p (addr e)]
+ (set spawned (+ spawned 1)) ; a local or a defvar
+ (set (.hp e) 7) ; a struct field
+ (set (.hp p) 8) ; through a (Ptr Enemy) — derefs one level
+ (set (at room 2) 5) ; a fixed array or slice element
+ (set (deref p) (Enemy {:hp 3 :name "wisp"})) ; a whole-object store
+
+ (print-i64 (i64 (.hp e))) (newline)
+ (print-line (.name e))
+ (print-i64 (i64 (at room 2))) (newline)
+ (print-i64 (i64 spawned)) (newline)))3
+wisp
+5
+1.field and at dereference exactly one pointer level, which
-is why (set (.pos c) …) is legal when c is a
-(Ptr Cursor).(set (.hp p) 8) above is legal when p is a
+(Ptr Enemy). Note the last two stores: the whole-object store through
+p overwrote e itself, so hp reads 3 and not 8.
(defconst cell-size 5) ; a compile-time constant
(defconst gravity f32 0.05) ; with its type named
(defvar current-color i32) ; zeroed storage
-(defvar grid [rows [cols u32]]) ; BSS, rows*cols*4 bytes
+(defconst rows 3)
+(defconst cols 4)
+(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))
+
+5
+0.05
+0
+0
A defconst the checker consumed — an array length, for instance — is
part of the shape of the program. One it did not is only ever bytes in memory, which
@@ -450,7 +488,7 @@ whose type matters is named at the top level rather than written inline.
negative
-4 3 2 1
+4 3 2 1
unless runs when the test is false
8
@@ -513,8 +551,7 @@ second
let, a loop or a branch, rather than accepted with surprising scope. Block
scoping it is real work and is not done:
-defer must be a top-level form in a function body — block-scoped defer
-is not implemented yet (milestone 4)
+defer must be a top-level form in a function body — block-scoped defer is not implemented yet (milestone 4)
;; pkg.flan
+;; pkg.flan — the directory is the package, and everything it declares
+;; arrives qualified by the alias this import chose.
(import g "geom")
(defn main []
@@ -668,12 +706,12 @@ signalling end says here is something notable, here is the data, and an
caller decides what to do about it — or decides nothing, in which case the signaller
carries on.
-(signal c) ; Unit. Handler returns → carry on. No handler → no-op.
+(signal c) ; Unit. Handler returns -> carry on. No handler -> no-op.
(error c) ; Never. Only a transfer gets past; else the program stops.
-(handler-bind [(Type [c] body ...) ...] body ...)
+(handler-bind [(Type [c] body ...) ...] body ...) ; match by type, no hierarchy
-(restart-case BODY ; BODY and every clause have the same type
+(restart-case BODY ; BODY and every clause have the same type = the form's
(name [] CLAUSE) ...)
(invoke-restart 'name) ; Never. Innermost frame offering the name wins.
@@ -841,7 +879,11 @@ is generated; a Flan string crosses as ptr+len, exactly as it is stored.
signature, and the compiler writes the wrapper. This is what raylib's package is made
of — one line per binding:
-(declare-c draw-texture [t Texture2D x i32 y i32 tint Color] "DrawTexture")
+(declare-c unload-texture [texture Texture2D] "UnloadTexture")
+
+(declare-c draw-texture
+ [texture Texture2D x i32 y i32 tint Color]
+ "DrawTexture")
The reason for the wrapper is that an aggregate's calling convention is not
part of its layout. On x86-64, clang gives raylib's own prototypes
@@ -932,9 +974,20 @@ 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.
-@"flan.cell.bump" = global ptr @"flan.bump" ; the host defines it
-%p = load ptr, ptr @"flan.cell.bump" ; every call site
-%r = call i64 %p()
+Here is the whole of hello.flan through
+flan emit --dev, which is the shortest thing that shows it:
+
+@"flan.cell.print-line" = global ptr @"flan.print-line"
+
+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)
+
+Two things are visible there at once. The call site loads the cell rather than naming
+@"flan.print-line" directly, and the signature carries ptr %xfer
+— the transfer channel from conditions, which every Flan
+function has, release builds included.
Redefinition is then one store, below a microsecond, which is what makes a
frame-boundary swap a non-event. Three rules fall out of it. A redefinition module
@@ -946,12 +999,24 @@ address inside a module's text, so unloading it would leave call sites pointing
unmapped memory. Old code is never unloaded, which is also why a thread mid-execution
finishes safely in the old version.
-The agent. vendor/agent is a package like any other:
-three calls, a listener thread, and a single-producer ring.
+The agent. vendor/agent is a package like any other: a
+listener thread, a single-producer ring, and three calls. This is the whole of
+agent.flan — no aggregate crosses the boundary, so a plain
+declare does it and there is no shim.
-(agent/start path) ; listen on a unix socket; once, at startup
-(agent/poll) ; install whatever has arrived; returns how many
-(agent/wait ms) ; the same, but waits for something first
+(declare start-raw [path string] i32 "flan_agent_start")
+(declare poll-raw [] i32 "flan_agent_poll")
+(declare wait-raw [ms i32] i32 "flan_agent_wait")
+
+(defn start [path string] i32 (start-raw path))
+(defn poll [] i32 (poll-raw))
+(defn wait [ms i32] i32 (wait-raw ms))
+
+(agent/start path) listens on a unix socket, once, at startup.
+(agent/poll) installs whatever has arrived and returns how many.
+(agent/wait ms) is the same but waits for something first, which is what a
+headless test uses so that a reload is deterministic rather than a race against the
+frame rate.
The split between loading and installing is the design. dlopen relocates
a module and takes the loader lock — milliseconds, unbounded — so it happens on the
@@ -1014,9 +1079,10 @@ there, in the thunk. What comes back looks like this:
big 18446744073709551615
col :blue
(.pos b) (V {:x 1.5 :y 0})
-b (Blob {:id 7 :name "sandy \"quoted\"" :pos (V {:x 1.5 :y 0})})
+b (Blob {:id 7 :name "sandy \"quoted\"" :pos (V {:x 1.5 :y 0}) :tags [ 0 42 0]})
(slice (.tags b) 0 3) [ 0 42 0]
-(rl/get-color 0x11223344) (rl/Color {:r 17 :g 34 :b 51 :a 68})
+(rl/get-color 0x11223344) (rl/Color {:r 17 :g 34 :b 51 :a 68})
+sim/grid [ [ 0 0 0 0 0 0 0 0 ...] [ 0 ... ] ...]
A pointer is never followed — it renders as <ptr> — because it is
the only thing that could make the walk cycle, and dereferencing one a REPL was handed is
@@ -1102,16 +1168,16 @@ to, and the tests assert on the reason.
You write The compiler says
-(Vec T)(Vec T) is not implemented yet — milestone 6
-(Map K V)(Map K V) is not implemented yet — milestone 6
-(Result T E)(Result T E) is not implemented yet — milestone 6
-(Handle T)(Handle T) is not implemented yet — milestone 6
-(try …)try (Result) is not implemented yet — milestone 6
-a union type the union type Shape is not implemented yet — milestone 6
-(Fn [T] R)a function type is not implemented yet — milestone 5
-(fn [x i32] …)calling something other than a named function is not implemented yet — milestone 5
-a type variable generic code over the type variable a is not implemented yet — milestone 5
-'syma quoted symbol (restart names) is not implemented yet — milestone 6
+(Vec T)(Vec T) is not implemented yet — milestone 6 (see plan.org)
+(Map K V)(Map K V) is not implemented yet — milestone 6 (see plan.org)
+(Result T E)(Result T E) is not implemented yet — milestone 6 (see plan.org)
+(Handle T)(Handle T) is not implemented yet — milestone 6 (see plan.org)
+(try …)try (Result) is not implemented yet — milestone 6 (see plan.org)
+a union type the union type Shape is not implemented yet — milestone 6 (see plan.org)
+(Fn [T] R)a function type is not implemented yet — milestone 5 (see plan.org)
+(fn [x i32] …)calling something other than a named function is not implemented yet — milestone 5 (see plan.org)
+a type variable generic code over the type variable a is not implemented yet — milestone 5 (see plan.org)
+'syma quoted symbol (restart names) is not implemented yet — milestone 6 (see plan.org)
(defmacro …)parses, but is not expanded: running a macro means compiling it and loading it into the compiler, which is not wired up yet
`(a ~b)is read, but not expanded: macro expansion is not wired up yet
handler-casehandler-case is not implemented yet