diff --git a/README.md b/README.md index cf9d01c..46ffb41 100644 --- a/README.md +++ b/README.md @@ -118,17 +118,139 @@ be threaded through every caller. See ## Commands +Eleven of them, and the four anyone starts with: + ```text flan check type-check a program -flan run [args...] build and run it -flan build [-o out] [options] build a native executable -flan dev [-s socket] start a live development session +flan run [flags] [-- args...] build and run it +flan build [-o out] [flags] build a native executable +flan dev [-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. +Useful build options include `-O0`…`-O3`, `--debug`, `--sanitize`, +`--no-bounds-checks`, `--x86`, and `--target=wasm32-wasi|web`. `run` takes the +same build flags and keeps them: everything after `--` goes to the program, and +a dash-argument before it that `run` does not offer is refused rather than +guessed at. `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. + +The other seven. Four print a stage of the pipeline, which is how you find out +what the compiler thinks it was given: + +```text +flan read ... the forms the reader produced +flan parse ... one line per declaration +flan shim ... the C a (declare-c ...) generated +flan emit [--x86] [--dev] [--debug] [--no-bounds-checks] + LLVM IR, or x86-64 assembly under --x86 +``` + +One builds a redefinition module the way `flan dev` does, for scripting the +loop without an editor: + +```text +flan reload [-o out.so] [--debug] [--x86] +``` + +And two are the C binding tools, which are the most useful thing here that +nothing else documents. + +### `flan import-c` — read a header, print the bindings it implies + +It builds nothing and writes nothing. It reads a C header (through clang's own +parser), prints the `declare-c` forms it would generate, and then prints every +function it *refused* and the reason — which is the half that earns its keep, +because "this binding is missing" and "this binding cannot exist" are different +problems: + +```text +$ flan import-c test/headers/sample.h +(declare-c set-seed [seed u32] "set_seed") +(declare-c add-ints [a i32 b i32] i32 "add_ints") +(declare-c name-length [text string] i32 "name_length") +... +;; 8 imported, 12 refused, of 21 functions in test/headers/sample.h +;; refused name-of: name_of returns char *, and a string only crosses as a +;; parameter — a C function that returns one returns something Flan has no +;; owner for +;; refused printf-like: printf_like is variadic, and a wrapper cannot forward +;; an argument list it does not know the shape of +;; refused file-time: file_time long has a width that differs between this +;; project's own targets (64 bits on x86-64, 32 on wasm32), so no single +;; Flan type is right for it +``` + +Hand the package's `.flan` files along with the header and it diffs against +them too: a `defstruct` whose layout no longer matches the header's record is +named, which is the failure that otherwise shows up as a wrong pixel. + +```text +$ flan import-c vendor/raylib/raylib-5.5.h vendor/raylib/*.flan +``` + +Anything after the header that is not a `.flan` file is passed to clang, so +`-I` and `-D` work as they do anywhere else. + +### `flan generate-c` — write those bindings into the package + +The same machinery, committing its answer. It takes a package *directory*, not +a header: the header comes from the package's own `headers` file, at the +version its `link` file names, because which version may be read is a property +of the package and not of the command line. It reads every `.flan` in the +directory except the one it writes, so hand-written declarations keep winning, +and it writes `generated.flan`. + +```text +$ flan generate-c vendor/raylib +;; refused get-clipboard-text: GetClipboardText returns char *, and a string +;; only crosses as a parameter — ... +;; refused load-shader: LoadShader Shader is a struct the package does not +;; describe — add a defstruct for it, or keep a hand-written declare-c +``` + +It exits non-zero and writes nothing if the package and the header disagree. +That is the point of it: the committed file is the one thing in the package +with no second opinion, so the moment of writing it is the last moment at which +the installed library can contradict it. + +## Environment + +Thirteen variables the toolchain reads, none of which has to be set on a +machine with clang, LLVM and binutils on `PATH`. Each exists for a machine +where the thing is somewhere else, or is the wrong one. + +| Variable | What it replaces | Read at | +|---|---|---| +| `FLAN_CLANG` | `clang`, which compiles the IR and the runtime's C | `lib/build.ml:14` | +| `FLAN_LLC` | `llc`, used only by the live loop | `lib/build.ml:886` | +| `FLAN_LD` | `ld`, used only by the live loop | `lib/build.ml:887` | +| `FLAN_AS` | `as`, used only by `--x86` | `lib/build.ml:978` | +| `FLAN_OBJDUMP` | `objdump`, used only by the disassembly verb | `lib/dev.ml:2018` | +| `FLAN_OCAMLFIND` | `ocamlfind`, which a merged `flan dev` needs **at run time** | `lib/dev.ml:2898` | +| `FLAN_EMCC` | `emcc`, for `--target=web` | `lib/build.ml:23` | +| `FLAN_LIBDIR` | where `flan.cmxa` and `flan.a` are, if not beside the binary | `lib/dev.ml:3187` | +| `FLAN_CACHE_DIR` | the object cache, default `$XDG_CACHE_HOME/flan/objcache` | `lib/build.ml:84` | +| `FLAN_WASM_SYSROOT` | the wasi-libc sysroot, default `/usr/wasm32-wasi` | `lib/build.ml:239` | +| `FLAN_WASM_BUILTINS` | `libclang_rt.builtins-wasm32.a`, which is not in clang's resource directory on Fedora | `lib/build.ml:272` | +| `FLAN_WEB_SHELL` | the HTML shell a `--target=web` build wraps the module in | `lib/build.ml:458` | +| `FLAN_DEV_LEAKS` | set (to anything) to have a `--dev` build print what it still held at exit | `runtime/flan_dev.c:1084` | + +`${FLAN_RAYLIB_WEB}` is not read by the compiler: it is expanded inside +`vendor/raylib/link`, which is where a package writes a linker argument that +has to differ per target. Any `${NAME}` in a `link` file expands from the +environment and an unset one is refused by name. + +**`llc` and `ld` have to match `clang`.** The live loop does not call the clang +driver at all — it goes `llc` + `ld -shared` + `dlopen`, which is what makes +`C-c C-c` cost milliseconds. So an `llc` from a different LLVM release than +`clang` breaks the dev loop while `flan build` keeps working perfectly, which +is a confusing shape of failure to meet without warning. + +Variables beginning `FLAN_DEV_` other than `FLAN_DEV_LEAKS`, plus +`FLAN_AGENT_SOCKET` and `FLAN_COMPILER_STAMP`, are internal: `flan dev` sets +them across its own `exec` to hand the merged binary what it needs. Setting +them by hand is not supported. ## Checking it diff --git a/docs/DISCUSS.md b/docs/DISCUSS.md index b326f8f..1f05819 100644 --- a/docs/DISCUSS.md +++ b/docs/DISCUSS.md @@ -759,13 +759,22 @@ Against `tast.ml`'s `expr_kind`, in four buckets: | **Mechanical** | `While` `Break` `Continue` (the jump patching exists), `Global` `Str` `Zero` `Uninit`, the rest of `place`, `Field` `Deref` `Addr`, `Arr`, `Some_` `None_` `UnwrapSome`, `Match` on a tag | | **Bulky, not hard** | floats — a second register file, SSE encodings, `Cast`'s eight conversions, and the SSE half of the calling convention. Perhaps a third of the total instruction work for a small fraction of the programs | | **Fiddly** | aggregate copy on assignment (a struct `store` *is* the copy `spec-memory.md` requires), `Make` `MakeCase` `CaseField` over the payload blob, `CallPtr`, `FnAddr`'s three cases and the cell load behind `Fnval` | -| **No plan** | `Handled` `Signal` `RestartCase` `InvokeRestart` `WithAlloc`, the transfer-channel guard after every call, the landing pad, and `fdefers` on the transfer exit path | +| **~~No plan~~ Done** | `Handled` `Signal` `RestartCase` `InvokeRestart` `WithAlloc`, the transfer-channel guard after every call, the landing pad, and `fdefers` on the transfer exit path | The last row is the one to take seriously. The spike never emitted a guard or a pad, and the guard is on *every call site* in the real thing — `emit.ml`'s `guard`, `current_pad`, `emit_restart_case` and `emit_with_alloc` are several hundred lines of control flow that a second backend reimplements from the spec rather than copies. Conditions are not an advanced feature to defer: `spec-conditions.md` is load-bearing in the prelude already. +**Correction, and it matters because this row argues the opposite of the truth.** The last row was written before the +backend existed and it is no longer the state of anything. `x86.ml:1587-1615` lowers the whole condition family — +`Handled`, `Signal`, `RestartCase`, `InvokeRestart`, `WithAlloc`, the per-call-site transfer guard, the landing pad and +the defers on the transfer exit — and the parity survey is **104 / 104 MATCH, 0 DIFFER**. Item 16 below is where that +was built and item 17 is where conditions and bounds checks were taken through the whole corpus rather than a sample; +this row is kept struck through rather than deleted because other files cite this table by position. Anyone reading it +for a production decision should read items 16, 17 and 18 instead: the second backend refuses nothing this one does, +and "no plan" has not been true since. + ### Question 3 — the SysV boundary, and the obstacle nobody named **The C boundary is the easy half, and `BUILT.md` is why.** `check.ml` rejects an aggregate in a `declare` signature diff --git a/lib/prelude.ml b/lib/prelude.ml index 3ed65e6..157081e 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -5,9 +5,17 @@ above it is Flan. That is what keeps a second backend cheap — a primitive is the only thing implemented twice. - It lives here as a string rather than as a file because there is no package - loader yet; at milestone 3 it becomes an ordinary [core:] package and this - module goes away. The acceptance programs may call anything defined here. + It lives here as a string rather than as a file, and every program gets it + prepended by [Check.build_program] whether it asked for one or not. That is + a real limit and it is stated as one: the prelude cannot be read as Flan, + extended, or replaced without rebuilding the compiler. There *is* a package + loader now — [lib/load.ml], and [vendor/raylib] is a package that uses it — + so the obstacle is no longer the mechanism. What is missing is the decision + about what a [core:] package would mean for a program that imports nothing, + and until that is made this docstring does not promise one. (It used to say + the prelude became a [core:] package "at milestone 3". Milestone 3 came and + went; the package did not.) The acceptance programs may call anything + defined here. No printing function is here at all any more. [print] and [println] are the whole printing surface, and neither is a function: both are