diff --git a/web/examples/quotes.sh b/web/examples/quotes.sh new file mode 100644 index 0000000..a9eaaa8 --- /dev/null +++ b/web/examples/quotes.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# The blocks on index.html that are not programs in this directory are quoted +# from somewhere: a repository file, or the output of a command. Each one is +# re-derived here and looked for in the page, so a quote cannot go stale +# quietly. check.sh covers the runnable blocks; this covers the rest. +# +# $ dune build && sh web/examples/quotes.sh +here=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$here/../.." && pwd) +FLAN=${FLAN:-$root/_build/default/bin/main.exe} +page=$here/../index.html +fail=0 + +# Look for a literal string in the page, allowing for HTML escaping of < and >. +want() { + esc=$(printf '%s' "$2" | sed -e 's/&/\&/g' -e 's//\>/g') + if grep -qF -- "$esc" "$page"; then echo "ok $1"; else + echo "FAIL $1" + echo " not on the page: $2" + fail=1 + fi +} + +# The usage text, from the binary itself. +want "flan usage" "$("$FLAN" 2>&1 | sed -n 2p)" + +# calc-me, the first acceptance program, still answers what the page says. +want "calc-me" "$("$FLAN" run "$root/calc-me.flan" '1 + 2 * (3 - 0.5) / 2')" + +# The refusal messages quoted in "Not implemented yet" and under defer. +for pair in \ + 'vec:(defvar xs (Vec i32))' \ + 'map:(defvar m (Map string i32))' \ + 'result:(defn f [] (Result i32 i32) None)' \ + 'handle:(defvar h (Handle i32))' \ + 'fnty:(defn f [g (Fn [i32] i32)] i32 (g 1))' \ + 'quoted:(defn f [] i32 (quote a))' \ + 'deferblock:(defn f [] i32 (let [x 1] (defer (print-line "a")) x))' +do + name=${pair%%:*}; src=${pair#*:} + printf '%s\n' "$src" > "$here/.q.flan" + msg=$("$FLAN" check "$here/.q.flan" 2>&1 | sed 's/^[^ ]*: //') + want "refusal: $name" "$msg" +done +rm -f "$here/.q.flan" + +# The cell and the transfer channel, from a real --dev emit of hello.flan. +ir=$("$FLAN" emit --dev "$here/hello.flan" 2>/dev/null) +want "cell global" "$(printf '%s\n' "$ir" | grep '^@"flan.cell.print-line"')" +want "cell load" "$(printf '%s\n' "$ir" | grep 'load ptr, ptr @"flan.cell.print-line"')" +want "xfer channel" "$(printf '%s\n' "$ir" | grep 'define {} @"flan.main"')" + +# The generated C, from flan shim. +want "shim wrapper" "$("$FLAN" shim "$here/shimdemo.flan" | grep 'GetMousePosition();')" + +# Lines quoted verbatim from repository files. +want "raylib binding" "$(grep -F 'unload-texture' "$root/vendor/raylib/raylib.flan")" +want "agent declare" "$(grep -F 'flan_agent_poll' "$root/vendor/agent/agent.flan" | head -1)" +want "conditions.org" "$(grep -F 'Innermost frame offering the name wins' "$root/conditions.org")" +want "renderer" "$(grep -F ':r 17 :g 34 :b 51 :a 68' "$root/NEXT.md")" + +exit $fail diff --git a/web/index.html b/web/index.html index 62d0adf..2ecbe34 100644 --- a/web/index.html +++ b/web/index.html @@ -211,11 +211,12 @@ $ ./_build/default/bin/main.exe run calc-me.flan "1 + 2 * (3 - 0.5) / 2"

Call that binary 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.

Places — the forms 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).

+is why (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.

Bounds are checked

@@ -375,6 +398,8 @@ number later.

:else "an arrow")) (defn main [] + ;; :space resolves against the parameter's enum at compile time. + ;; A typo is an error here, not a wrong number later. (print-line (key-name :space)) (print-line (key-name :left))) @@ -401,7 +426,20 @@ functions need no forward declaration. Globals come in two kinds:

(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.

None (print-line "none")))
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)

Arrays and slices

@@ -612,7 +649,8 @@ and no ceremony.

(defn length [v V2] f32 (sqrt-f32 (+ (* (.x v) (.x v)) (* (.y v) (.y v))))) -
;; 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 writeThe 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 typethe 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 variablegeneric 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 typethe 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 variablegeneric 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