Joseph Ferano 6cc94e00d6 defunion is C's union, and reading the member you did not write is defined
The name freed up by the rename now means what C means by it: the members
overlay one storage, the size is the largest of them, the alignment the
strictest, and nothing anywhere records which one was written. It serves
two things that wanted it. Binding a C header means holding the union the
library holds and reading whichever member the library's own tag says is
live -- a tag Flan cannot see, because the rule relating them is prose in
a manual. Overlaying an f32 on a u32 to look at its bits is the other,
and it is the same read.

So that read is defined rather than refused. This is the one place in the
checker where bytes win over safety on purpose, and the alternative was
not a safer language, it was no feature: type punning *is* reading the
member that was not written. The promise is the one C's implementations
make and C's standard does not -- the layout is the target's, the bytes
are the bytes, a read is a reinterpretation of them -- and what is not
promised is anything about bytes nobody wrote, where a member wider than
the one last stored reads a tail that is indeterminate exactly as a
struct's padding is. ZII narrows that to almost nothing: a union starts
all-bytes-zero unless uninit says otherwise.

uninit on one is allowed, unlike on a defdata. The refusal there was
never about garbage; it is that a tag steers, and a tag no case names
falls past every comparison in a match into a block LLVM may treat as
unreachable. An untagged union steers nothing.

Which is also why three things are refused, each for a reason that does
not expire with a milestone. No move-only member: nothing knows which
member is live, so nothing can tear one down, and unlike the struct and
defdata refusals this is not waiting on recursive teardown -- there is no
fact for teardown to read. No bool at any depth: an i1 loaded from a byte
that is neither 0 nor 1 is a value the optimiser may assume cannot exist,
and a union is the only type that can produce one. No defdata at any
depth, for the reason uninit gives, arriving the other way round. An
Option member is fine and the walk says why: its match is a tag test and
a branch, not a chain with an unreachable tail.

Two members in one literal, a match on a union, a union map key and a
member written into a global initialiser are each refused by name.

A union is a field list whose every offset is zero, so it travels as a
Tast.structure and the checker, the emitter and the x86 backend each grow
one table rather than one shape. A value is a zeroed temporary and a
store -- Set over Pfield, which every backend already has -- so there is
no new IR node and no layout rule spelled out a second time per backend.
The LLVM type is the blob clang gives a union, the DWARF is
DW_TAG_union_type with every member at zero, and the printer names the
type and does not walk it: it cannot know which member is live, and one
of them may be a pointer.

cimport can now check what it could not. A C record holding a union
member was not recorded at all, so the defstruct beside it went unchecked
rather than checked wrongly; a named union member resolves to a defunion
now and the whole record is compared field by field. The defunion itself
is compared against the header's union as a set and not in order --
every member is at offset zero, so a permuted one is the same type and
reporting it would be a finding that is not one -- while a member the
header has and Flan lacks is reported, because that is what changes the
size. A defunion against a C struct, or a defstruct against a C union,
is reported in both directions. An anonymous union member is still
skipped, and the comment now says that the gap is on the Flan side:
there is nothing to declare.
2026-09-17 19:54:32 +07:00
2024-07-09 21:01:55 +10:00
2026-09-10 14:56:35 +07:00
2026-09-10 14:40:34 +07:00

Flan

Flan

A statically typed Lisp for native games and interactive development.

Flan is an experimental, ahead-of-time compiled Lisp for programs that need predictable memory use and a fast editrun loop. It combines S-expressions, static types, explicit ownership, and a development session that can replace a function in a running program without resetting its state.

It is being built around games, but the interesting part is broader: a compiled language where the running program remains available for inspection, experimentation, and small changes.

In practical terms: you get parentheses, a debugger that would like to have a conversation, and no garbage collector quietly choosing the dramatic moment to join your frame loop.

What it has

  • Native compilation through LLVM, plus an in-progress direct x86-64 backend.
  • C-like data layout: structs, fixed arrays, pointers, slices, and explicit allocation. There is no garbage collector.
  • Owned Vec and Map containers, plus checked moves and borrowing-oriented slice operations.
  • Generics, algebraic unions, enums, macros, packages, defer, and a C FFI.
  • Conditions and restarts for recoverable failures and interactive debugging.
  • A raylib package and a collection of ported raylib examples.
  • Native, WASI, and web build targets. The cross targets are useful but less complete than the native development workflow.

The project is exploratory software, not a stable language release. Some features are deliberately refused while their semantics are still undecided; the compiler aims to say why rather than quietly accepting a partial version. It has opinions, but at least they arrive as error messages.

Quick start

Building requires a current OCaml/Dune toolchain, LLVM/Clang, and the native C toolchain. Raylib is only needed for programs that use the bundled graphics package.

dune build
dune exec ./bin/main.exe -- run web/examples/hello.flan

To build a standalone native executable:

dune exec ./bin/main.exe -- build web/examples/hello.flan -o hello
./hello

The falling-sand demo uses raylib:

dune exec ./bin/main.exe -- run sand.flan

Once you are iterating regularly, put the built executable on your PATH if you want to use the shorter flan commands shown below.

The live development loop

Start a long-lived development session:

flan dev sand.flan

The program runs normally and publishes a local socket beside the source file. The bundled Emacs mode can attach to it, evaluate expressions in the live process, inspect a stopped program, and recompile a top-level function from the buffer. A body change takes effect on the next call; changing a function's signature is intentionally rejected. The program keeps its state, which is especially nice when you have finally arranged the sand into something almost worth saving.

To set up the mode:

(add-to-list 'load-path "~/path/to/flan/emacs")
(require 'flan-mode)

Then use M-x flan-dev to start and attach, or C-c C-z to attach to a session started in a terminal. The editor workflow is documented in emacs/MANUAL.md.

A small example

(defstruct AssetMissing [id i32])

(defn load-asset [id i32] i32
  (signal (AssetMissing {.id id}))
  100)

(defn asset-or-placeholder [id i32] i32
  (restart-case (load-asset id)
    (use-placeholder [] -1)))

(defn main [] ()
  (handler-bind [(AssetMissing [_] (invoke-restart 'use-placeholder))]
    (println (asset-or-placeholder 7))))

Here a missing asset signals a typed condition. The handler chooses a restart, so execution continues with a placeholder instead of requiring error values to be threaded through every caller. See web/examples/restart.flan for a runnable version.

Commands

flan check <file.flan>                         type-check a program
flan run <file.flan> [args...]                 build and run it
flan build <file.flan> [-o out] [options]      build a native executable
flan dev <file.flan> [-s socket]               start a live development session

Useful build options include --debug, --sanitize, --no-bounds-checks, --x86, and --target=wasm32-wasi|web. run is native-only; cross-built output should be run with an appropriate WASI runtime or browser. A .wasm file is not a tiny native executable in a trench coat.

Checking it

dune test                    the suite. Seconds. Run it constantly.
dune build @checks           everything else that can fail. A couple of minutes.
dune build @sanitize         the corpus under ASan and UBSan.
dune build @valgrind         the corpus under memcheck. Tens of minutes.

dune test means "the language still works". @checks — which is @page, @x86 and @cells — means "and everything written down about it is still true": the reference page's examples still print what the page says, the hand-written x86 backend still agrees with LLVM, and a --dev build still calls through its indirection cells.

The two are separate on purpose. A suite that goes red because prose drifted teaches you to skim past red. But @checks only helps if it is run, and nothing runs it for you — there is no CI here. The convention that has to carry it is that a lane's handoff quotes @checks, the way the x86 handoffs already quote the survey's counts. Both of this repository's silent failures — two backend refusals that sat for a month, a page of examples that stopped compiling for two days — were found by accident, and neither would have survived one person typing one command.

Project map

License

Flan is released under the MIT License. Third-party material under vendor/ is distributed under its own licenses.

Description
No description provided
Readme MIT 7.5 MiB
Languages
OCaml 67.2%
Emacs Lisp 15.2%
C 10.4%
HTML 2.9%
Standard ML 2.8%
Other 1.5%