The driver refuses in sentences, and CI exists
This commit is contained in:
commit
0fd83dc95e
148
.github/workflows/checks.yml
vendored
Normal file
148
.github/workflows/checks.yml
vendored
Normal file
@ -0,0 +1,148 @@
|
||||
# The thing README.md and test/dune both said did not exist.
|
||||
#
|
||||
# `dune build @checks` on every push, which is the whole job. Both of this
|
||||
# repository's silent failures — two x86 refusals that sat for a month, a page
|
||||
# of examples that stopped compiling for two days — would have been caught by
|
||||
# one person typing one command, and the argument for CI here is not process:
|
||||
# it is that nobody typed it. @x86 parity in particular is *not* under
|
||||
# `dune test`, so the routine suite does not protect it.
|
||||
#
|
||||
# `dune test` stays what it is: seconds, run constantly, unchanged by this
|
||||
# file. The workflow adds a caller, not an alias.
|
||||
#
|
||||
# ── What this does not cover, and why ────────────────────────────────────
|
||||
#
|
||||
# Stated plainly, because a green tick that quietly checks less than it looks
|
||||
# like it does is worse than no tick.
|
||||
#
|
||||
# raylib. vendor/raylib/link names `-l:libraylib.so.550` by exact soname,
|
||||
# which is a Fedora spelling; Ubuntu ships neither that soname nor 5.5.
|
||||
# So every raylib program is skipped here, on three separate paths that
|
||||
# all already existed: the acceptance cases probe with `ldconfig -p |
|
||||
# grep libraylib` and print a skip line; survey.sh records a program that
|
||||
# will not link as `does-not-compile`, which SURVEY_STRICT deliberately
|
||||
# does not fail on; and web/examples/check.sh skips shimdemo.flan by name.
|
||||
# Nothing had to be weakened to make this pass — but the graphics half of
|
||||
# the corpus is unchecked here and is checked only on a machine with
|
||||
# raylib installed.
|
||||
#
|
||||
# emscripten and the web target. test_web probes for emcc and a raylib
|
||||
# archive built by vendor/raylib/build-web.sh and skips with the reason.
|
||||
# Installing an emsdk per run is minutes for a target NEXT.md has
|
||||
# deprioritised, so it is left out on purpose.
|
||||
#
|
||||
# wasm32-wasi. Needs a wasi-libc sysroot and a builtins archive that no
|
||||
# Ubuntu package supplies. The acceptance case probes by building and
|
||||
# running the smallest program and skips with what failed, so this is a
|
||||
# gap in coverage and not a failure.
|
||||
#
|
||||
# lldb. The debug-information cases want lldb on PATH and say so when it is
|
||||
# absent. The DWARF is still emitted and still checked for
|
||||
# self-consistency; what is skipped is the half that says a person can
|
||||
# actually sit in a debugger.
|
||||
#
|
||||
# @sanitize and @valgrind. Out of @checks by design — tens of minutes each
|
||||
# — and out of here for the same reason. They are the thing to run before
|
||||
# a release, by hand, not the thing to run on every push.
|
||||
#
|
||||
# One platform. ubuntu-latest only. macOS and Windows portability is
|
||||
# recorded as real and not current (aligned_alloc, MSG_NOSIGNAL,
|
||||
# stdatomic), and a matrix that goes red for known reasons teaches people
|
||||
# to ignore red.
|
||||
|
||||
name: checks
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
# A superseded run is wasted minutes and a confusing status on the commit it
|
||||
# no longer describes.
|
||||
concurrency:
|
||||
group: checks-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# clang compiles the IR and the runtime's C; llc and ld are the live
|
||||
# loop, which never calls the clang driver; as is the --x86 path; emacs
|
||||
# is what test_emacs and test_cider drive the client with, and they skip
|
||||
# silently without it, which would be four suites' worth of coverage
|
||||
# disappearing without a word.
|
||||
- name: Install the toolchain
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
clang llvm lld binutils emacs-nox
|
||||
|
||||
# llc has to come from the same LLVM release as clang. The live loop
|
||||
# goes llc + ld -shared + dlopen rather than through the clang driver,
|
||||
# so a mismatch breaks `C-c C-c` and every reload test while `flan
|
||||
# build` keeps working perfectly — which is a confusing shape of failure
|
||||
# to debug from a CI log. Ubuntu installs llc under a version suffix and
|
||||
# may or may not provide the bare name, so the version is read off clang
|
||||
# and FLAN_LLC is pinned to match it rather than left to PATH order.
|
||||
- name: Point FLAN_LLC at the llc that matches clang
|
||||
run: |
|
||||
set -eu
|
||||
clang --version
|
||||
major=$(clang --version | sed -n 's/.*version \([0-9][0-9]*\).*/\1/p' | head -1)
|
||||
if command -v "llc-$major" > /dev/null; then
|
||||
llc="llc-$major"
|
||||
elif command -v llc > /dev/null \
|
||||
&& llc --version | grep -q "LLVM version $major"; then
|
||||
llc=llc
|
||||
else
|
||||
echo "no llc at LLVM $major; clang is $(clang --version | head -1)" >&2
|
||||
echo "what is on PATH: $(ls /usr/bin/llc* 2>/dev/null || echo none)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "FLAN_LLC=$llc" >> "$GITHUB_ENV"
|
||||
"$llc" --version | head -3
|
||||
|
||||
- uses: ocaml/setup-ocaml@v3
|
||||
with:
|
||||
ocaml-compiler: "5.2"
|
||||
# There is no (package ...) stanza yet — the install story is Tier 2
|
||||
# of docs/REVIEW-production-readiness.md and undecided — so there is
|
||||
# nothing to pin and nothing to resolve dependencies from. The
|
||||
# library depends on unix and on nothing else.
|
||||
opam-pin: false
|
||||
dune-cache: true
|
||||
|
||||
- run: opam install -y dune
|
||||
|
||||
- name: Build
|
||||
run: opam exec -- dune build
|
||||
|
||||
# --force because the point of a CI run is to run the tests, not to
|
||||
# discover that dune's cache already knew the answer.
|
||||
#
|
||||
# The log is kept and then read, because a suite that passes while
|
||||
# leaving an unhandled exception on stderr is a suite telling you
|
||||
# something and being ignored. That happened here — three suites left a
|
||||
# message-less `Fatal error: exception Flan.Loc.Error(_)` while
|
||||
# reporting zero failures — and it was found by a person reading a log,
|
||||
# which is exactly the kind of finding that should not need a person.
|
||||
- name: The suite
|
||||
run: |
|
||||
set -eu
|
||||
set -o pipefail
|
||||
opam exec -- dune test --force 2>&1 | tee suite.log
|
||||
if grep -n 'Fatal error' suite.log; then
|
||||
echo "the suite passed but left an unhandled exception above" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The part `dune test` does not cover: the reference page's examples
|
||||
# still print what the page says, the hand-written x86 backend still
|
||||
# agrees with LLVM over the whole corpus, and a --dev build still calls
|
||||
# through its indirection cells.
|
||||
- name: Everything else that can fail
|
||||
run: opam exec -- dune build @checks
|
||||
154
README.md
154
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 <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 run <file.flan> [flags] [-- args...] build and run it
|
||||
flan build <file.flan> [-o out] [flags] 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.
|
||||
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 <file.flan>... the forms the reader produced
|
||||
flan parse <file.flan>... one line per declaration
|
||||
flan shim <file.flan>... the C a (declare-c ...) generated
|
||||
flan emit <file.flan> [--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 <program.flan> <forms.flan> [-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
|
||||
|
||||
@ -146,13 +268,19 @@ 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.
|
||||
teaches you to skim past red. 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.
|
||||
|
||||
`.github/workflows/checks.yml` is now that person: it runs `dune build`,
|
||||
`dune test --force` and `dune build @checks` on every push. What it cannot run
|
||||
is written down in the workflow itself rather than left to be discovered —
|
||||
raylib, emscripten, wasm32-wasi and lldb are all absent from an Ubuntu runner,
|
||||
and every one of them was already a self-skip, so the tick is green over less
|
||||
than it is on a machine with those installed. The habit that covers the rest is
|
||||
still worth keeping: a lane's handoff quotes `@checks`, the way the x86
|
||||
handoffs already quote the survey's counts.
|
||||
|
||||
## Project map
|
||||
|
||||
|
||||
138
bin/main.ml
138
bin/main.ml
@ -39,6 +39,31 @@ let with_errors path f =
|
||||
prerr_endline ("flan: " ^ m);
|
||||
ignore path;
|
||||
exit 1
|
||||
(* A file that is not there, or that cannot be read. The exception already
|
||||
carries the path and the reason the operating system gave, which is the
|
||||
whole of what anybody can act on, so it is passed through as it is —
|
||||
[lib/dev.ml]'s [message_of_exn] does the same for the same reason, and
|
||||
the daemon has had this arm since before the CLI did. Without it
|
||||
[flan check nosuch.flan] ends in [Fatal error: exception Sys_error(...)],
|
||||
which is the compiler telling the user it did not expect to be asked. *)
|
||||
| Sys_error m ->
|
||||
prerr_endline ("flan: " ^ m);
|
||||
ignore path;
|
||||
exit 1
|
||||
(* The backstop. Nothing in this binary reaches it today: every [Hashtbl.find]
|
||||
on a path a command can take is guarded by a [find_opt]. It is here so
|
||||
that the day one is not, the failure is a sentence naming the file being
|
||||
worked on rather than a bare [Fatal error: exception Not_found] — which
|
||||
says nothing at all, not even which file. Its own exit status, because a
|
||||
compiler bug is not a refusal of the program and a script should be able
|
||||
to tell the two apart. *)
|
||||
| Not_found ->
|
||||
prerr_endline
|
||||
("flan: internal error — a lookup failed with no name to report, while \
|
||||
working on " ^ path
|
||||
^ ". This is a bug in the compiler and not a fault in the program; \
|
||||
please report the file that provoked it.");
|
||||
exit 4
|
||||
|
||||
let summarise (d : Flan.Ast.decl) =
|
||||
let open Flan.Ast in
|
||||
@ -151,8 +176,43 @@ let flags =
|
||||
usage error. *)
|
||||
let target_prefix = "--target="
|
||||
|
||||
(* The optimisation level, which until now had no spelling at all: [Build.default]
|
||||
pinned -O2 and [--debug] was the only route to anything else. Four levels and
|
||||
not five — -Os is clang's and llc rejects it outright ("invalid optimization
|
||||
level"), and the same string reaches both ([Build] at the clang compile and
|
||||
again at the llc step of the live loop), so offering a level one of the two
|
||||
tools does not know would be a flag that works for [flan build] and breaks
|
||||
[C-c C-c].
|
||||
|
||||
Last one wins, which is what every compiler does with a repeated -O and the
|
||||
only rule that does not need explaining. *)
|
||||
let opt_levels = [ "-O0"; "-O1"; "-O2"; "-O3" ]
|
||||
|
||||
let opt_of args =
|
||||
List.fold_left
|
||||
(fun acc a -> if List.mem a opt_levels then Some a else acc)
|
||||
None args
|
||||
|
||||
let is_flag a =
|
||||
List.mem a flags || String.starts_with ~prefix:target_prefix a
|
||||
List.mem a flags || List.mem a opt_levels
|
||||
|| String.starts_with ~prefix:target_prefix a
|
||||
|
||||
(* [--debug] forces -O0 in [Build] and says why there: [llvm.dbg.declare]
|
||||
describes an alloca and mem2reg deletes the alloca, so a debug build at -O2
|
||||
has a line table over code whose locals are gone. That is a good rule, and
|
||||
it makes [--debug -O2] a request that cannot be honoured — so it is refused
|
||||
here by name instead of being quietly overruled two modules away. *)
|
||||
let check_opt_against_debug ~debug ~opt =
|
||||
match (debug, opt) with
|
||||
| true, Some o when o <> "-O0" ->
|
||||
prerr_endline
|
||||
("flan: --debug and " ^ o
|
||||
^ " ask for opposite things — a debug build is -O0 because \
|
||||
llvm.dbg.declare describes an alloca and mem2reg at any higher level \
|
||||
deletes it, leaving a line table over locals that are not there. \
|
||||
Drop one of the two.");
|
||||
exit 2
|
||||
| _ -> ()
|
||||
|
||||
let target_of args =
|
||||
List.find_map
|
||||
@ -515,6 +575,8 @@ let () =
|
||||
let debug = List.mem debug_flag rest in
|
||||
let sanitize = List.mem sanitize_flag rest in
|
||||
let x86 = List.mem x86_flag rest in
|
||||
let opt = opt_of rest in
|
||||
check_opt_against_debug ~debug ~opt;
|
||||
let target = target_of rest in
|
||||
let out =
|
||||
match List.filter (fun a -> not (is_flag a)) rest with
|
||||
@ -535,7 +597,8 @@ let () =
|
||||
| _ -> base)
|
||||
| _ ->
|
||||
prerr_endline
|
||||
"usage: flan build <file.flan> [-o out] [--no-bounds-checks] \
|
||||
"usage: flan build <file.flan> [-o out] [-O0|-O1|-O2|-O3] \
|
||||
[--no-bounds-checks] \
|
||||
[--dev] [--debug] [--sanitize] [--target=wasm32-wasi|web|js]";
|
||||
exit 2
|
||||
in
|
||||
@ -549,7 +612,9 @@ let () =
|
||||
let p, csrcs, lflags = Flan.Reach.link ~dev l p in
|
||||
ignore (Flan.Build.executable
|
||||
~opts:{ Flan.Build.default with checks; dev; debug; sanitize;
|
||||
target; x86 }
|
||||
target; x86;
|
||||
opt = Option.value opt
|
||||
~default:Flan.Build.default.Flan.Build.opt }
|
||||
~csrcs ~lflags ~pnames:(if debug then param_names l else [])
|
||||
p ~out))
|
||||
(* The daemon an editor talks to: one session, the program it belongs to
|
||||
@ -637,6 +702,57 @@ let () =
|
||||
this host can exec. Use flan build --target=... and a wasm runtime.";
|
||||
exit 2
|
||||
| _ :: "run" :: path :: args ->
|
||||
(* One command, two argument lists, and until now no rule saying which was
|
||||
which: [flan run game.flan --debug] built at -O2 and handed the game a
|
||||
[--debug] it had never heard of. Nothing reported that, because neither
|
||||
side thought it had been given anything wrong.
|
||||
|
||||
The rule, in one line: a build flag is the build's, [--] ends the build
|
||||
flags, and everything after [--] is the program's whatever it looks
|
||||
like. Before [--], an argument that starts with a dash and is not a
|
||||
build flag this command offers is refused by name — not guessed at,
|
||||
because guessing is the failure this exists to stop, and a program of
|
||||
one's own that wants [-v] has [--] to ask for it. Plain arguments need
|
||||
no ceremony: they were never ambiguous and they still go straight
|
||||
through, so [flan run calc-me.flan "1+2"] is unchanged. *)
|
||||
(* --dev is deliberately not on this list, and its absence is the point:
|
||||
a dev build's indirection cells exist so that something can attach and
|
||||
redefine through them, and nothing can attach to a process this command
|
||||
builds, execs, waits for and deletes. Leaving it off means it lands in
|
||||
the refusal below with a sentence, rather than quietly producing a
|
||||
spelling [Build] says does not exist — [flan build]'s --x86 arm records
|
||||
that `flan dev` never reaches its fork because --x86 is read only by
|
||||
[flan build], and an --x86 --dev route through here would have made that
|
||||
sentence false. *)
|
||||
let run_flags =
|
||||
[ no_checks_flag; debug_flag; sanitize_flag; x86_flag ] @ opt_levels
|
||||
in
|
||||
let build_args, prog_args =
|
||||
let rec split acc = function
|
||||
| "--" :: rest -> (List.rev acc, rest)
|
||||
| a :: rest when String.length a > 1 && a.[0] = '-' ->
|
||||
if List.mem a run_flags then split (a :: acc) rest
|
||||
else begin
|
||||
prerr_endline
|
||||
("flan run: " ^ a
|
||||
^ " is not a flag this command offers, and it will not be \
|
||||
guessed at — a build flag belongs to the build and \
|
||||
anything else belongs to the program. Write it after -- to \
|
||||
send it to the program: flan run "
|
||||
^ Filename.basename path ^ " -- " ^ a);
|
||||
exit 2
|
||||
end
|
||||
| a :: rest -> let l, r = split acc rest in (l, a :: r)
|
||||
| [] -> (List.rev acc, [])
|
||||
in
|
||||
split [] args
|
||||
in
|
||||
let checks = not (List.mem no_checks_flag build_args) in
|
||||
let debug = List.mem debug_flag build_args in
|
||||
let sanitize = List.mem sanitize_flag build_args in
|
||||
let x86 = List.mem x86_flag build_args in
|
||||
let opt = opt_of build_args in
|
||||
check_opt_against_debug ~debug ~opt;
|
||||
with_errors path (fun () ->
|
||||
let exe =
|
||||
Filename.concat (Filename.get_temp_dir_name ())
|
||||
@ -645,9 +761,16 @@ let () =
|
||||
let l = load path in
|
||||
let p = Flan.Check.program_all l.decls in
|
||||
let p, csrcs, lflags = Flan.Reach.link l p in
|
||||
ignore (Flan.Build.executable ~csrcs ~lflags p ~out:exe);
|
||||
ignore (Flan.Build.executable
|
||||
~opts:{ Flan.Build.default with checks; debug; sanitize;
|
||||
x86;
|
||||
opt = Option.value opt
|
||||
~default:Flan.Build.default.Flan.Build.opt }
|
||||
~csrcs ~lflags ~pnames:(if debug then param_names l else [])
|
||||
p ~out:exe);
|
||||
let code =
|
||||
Sys.command (String.concat " " (List.map Filename.quote (exe :: args)))
|
||||
Sys.command
|
||||
(String.concat " " (List.map Filename.quote (exe :: prog_args)))
|
||||
in
|
||||
(try Sys.remove exe with Sys_error _ -> ());
|
||||
exit code)
|
||||
@ -656,9 +779,10 @@ let () =
|
||||
"usage: flan (read|parse|check|emit|shim) <file.flan>...\n flan emit <file.flan> [--x86] [--dev] [--debug] [--no-bounds-checks]\n\
|
||||
\ flan import-c <header.h> [package.flan...] [clang flags...]\n\
|
||||
\ flan generate-c <package-dir>\n\
|
||||
\ flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] \
|
||||
\ flan build <file.flan> [-o out] [-O0|-O1|-O2|-O3] \
|
||||
[--no-bounds-checks] [--dev] \
|
||||
[--debug] [--sanitize] [--x86] [--target=wasm32-wasi|web|js]\n\
|
||||
\ flan run <file.flan> [args...]\n\
|
||||
\ flan run <file.flan> [build flags...] [--] [program args...]\n\
|
||||
\ flan reload <program.flan> <forms.flan> [-o out.so] [--x86]\n\
|
||||
\ flan dev <program.flan> [-s socket] [--x86]";
|
||||
exit 2
|
||||
|
||||
@ -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
|
||||
|
||||
@ -30,9 +30,34 @@ works without dape installed, and `C-c C-g` only exists once you load it.
|
||||
(require 'flan-dape) ; only if you have dape
|
||||
```
|
||||
|
||||
Or install it as a package, which gets you autoloads and byte-compiled files
|
||||
without a `load-path` line. `package-install-file` names the package after the
|
||||
directory, so the directory has to be called `flan-mode` — symlink it once and
|
||||
point Emacs at the link:
|
||||
|
||||
```sh
|
||||
ln -s ~/Development/flan/emacs ~/.emacs.d/flan-mode
|
||||
```
|
||||
|
||||
```
|
||||
M-x package-install-file RET ~/.emacs.d/flan-mode/ RET
|
||||
```
|
||||
|
||||
Every file carries `Version:` and `Package-Requires: ((emacs "29.1"))`, which is
|
||||
what package.el reads. dape is deliberately not in that list: `flan-dape.el`
|
||||
reaches it through `declare-function`, so everything else installs and works
|
||||
without it.
|
||||
|
||||
You also need the `flan` binary on your `PATH`. If it is somewhere else, set
|
||||
`flan-dev-command`.
|
||||
|
||||
Two settings worth knowing about before you need them. `flan-dev-start-timeout`
|
||||
(60s) bounds the wait for a daemon to come up, and `flan-dev-reply-timeout`
|
||||
(30s) bounds one request once it has. The second is the one a long first
|
||||
compile can exhaust: the message says so and names `*flan-dev*`, where the
|
||||
daemon's own build log is, so you can see whether it is still working before
|
||||
you raise it.
|
||||
|
||||
---
|
||||
|
||||
## Starting a program
|
||||
|
||||
@ -1,5 +1,24 @@
|
||||
;;; flan-cnr.el --- What a stopped program is offering -*- lexical-binding: t; -*-
|
||||
|
||||
;; Author: Joseph Ferano <joseph@ferano.io>
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "29.1"))
|
||||
;; Keywords: languages, lisp, tools
|
||||
|
||||
;; The headers above are what make this directory installable. M-x
|
||||
;; package-install-file on it reads them, and a file with no Version: is not a
|
||||
;; package as far as package.el is concerned -- until now the client was
|
||||
;; reachable only by adding it to load-path by hand, which is a thing to
|
||||
;; explain to every person who wants to try it.
|
||||
;;
|
||||
;; 29.1 is the floor because it is the oldest Emacs any of this has been run
|
||||
;; against, not because some function here is known to need it. dape, which
|
||||
;; flan-dape drives, asks for 29.1 as well and is a soft dependency: it is
|
||||
;; reached through declare-function, so the rest of the client loads and works
|
||||
;; without it and it is deliberately not listed above. The compiler this talks
|
||||
;; to is not an Emacs package and cannot be listed here either -- emacs/MANUAL.md
|
||||
;; says what has to be on PATH.
|
||||
|
||||
;; `C-c C-b' is a `completing-read' over restart names. That is the whole UI
|
||||
;; for the one moment the dev loop exists to make survivable, and it is thin in
|
||||
;; a way that is worth being specific about: it shows the names and nothing
|
||||
|
||||
@ -1,5 +1,24 @@
|
||||
;;; flan-dape.el --- Debug a Flan program with dape and lldb-dap -*- lexical-binding: t; -*-
|
||||
|
||||
;; Author: Joseph Ferano <joseph@ferano.io>
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "29.1"))
|
||||
;; Keywords: languages, lisp, tools
|
||||
|
||||
;; The headers above are what make this directory installable. M-x
|
||||
;; package-install-file on it reads them, and a file with no Version: is not a
|
||||
;; package as far as package.el is concerned -- until now the client was
|
||||
;; reachable only by adding it to load-path by hand, which is a thing to
|
||||
;; explain to every person who wants to try it.
|
||||
;;
|
||||
;; 29.1 is the floor because it is the oldest Emacs any of this has been run
|
||||
;; against, not because some function here is known to need it. dape, which
|
||||
;; flan-dape drives, asks for 29.1 as well and is a soft dependency: it is
|
||||
;; reached through declare-function, so the rest of the client loads and works
|
||||
;; without it and it is deliberately not listed above. The compiler this talks
|
||||
;; to is not an Emacs package and cannot be listed here either -- emacs/MANUAL.md
|
||||
;; says what has to be on PATH.
|
||||
|
||||
;; The other half of the dev loop. flan-dev.el is about a program that keeps
|
||||
;; running while you change it; this is about stopping one and reading it.
|
||||
;;
|
||||
|
||||
@ -1,5 +1,24 @@
|
||||
;;; flan-dev.el --- Talk to a running Flan program -*- lexical-binding: t; -*-
|
||||
|
||||
;; Author: Joseph Ferano <joseph@ferano.io>
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "29.1"))
|
||||
;; Keywords: languages, lisp, tools
|
||||
|
||||
;; The headers above are what make this directory installable. M-x
|
||||
;; package-install-file on it reads them, and a file with no Version: is not a
|
||||
;; package as far as package.el is concerned -- until now the client was
|
||||
;; reachable only by adding it to load-path by hand, which is a thing to
|
||||
;; explain to every person who wants to try it.
|
||||
;;
|
||||
;; 29.1 is the floor because it is the oldest Emacs any of this has been run
|
||||
;; against, not because some function here is known to need it. dape, which
|
||||
;; flan-dape drives, asks for 29.1 as well and is a soft dependency: it is
|
||||
;; reached through declare-function, so the rest of the client loads and works
|
||||
;; without it and it is deliberately not listed above. The compiler this talks
|
||||
;; to is not an Emacs package and cannot be listed here either -- emacs/MANUAL.md
|
||||
;; says what has to be on PATH.
|
||||
|
||||
;; The editor half of Flan's dev loop. `flan dev program.flan' compiles the
|
||||
;; program, launches it, and listens on .flan-dev.sock beside the source; this
|
||||
;; connects to that socket and sends it forms. M-x flan-dev starts that
|
||||
@ -77,6 +96,29 @@ that quietly did nothing, which is why it is on."
|
||||
"Buffer the running program's own output is appended to."
|
||||
:type 'string)
|
||||
|
||||
(defcustom flan-dev-daemon-buffer "*flan-dev*"
|
||||
"Buffer the daemon's own output goes to.
|
||||
This is where a build that failed says so: the daemon compiles the program
|
||||
before it binds its socket, so a program that does not compile produces no
|
||||
socket at all and this buffer is the only account of why. It is also where a
|
||||
build that is merely slow can be watched, which is what the reply timeout
|
||||
below points at."
|
||||
:type 'string)
|
||||
|
||||
(defcustom flan-dev-reply-timeout 30
|
||||
"Seconds to wait for one reply from the daemon before giving up.
|
||||
|
||||
This bounds a single request, not a session. Thirty seconds is generous
|
||||
for an evaluation and tight for a first compile of a large program on a
|
||||
cold object cache, which is the one case that legitimately runs long — so
|
||||
it is a setting rather than a constant.
|
||||
|
||||
Giving up here never resends. If the daemon took the request and died
|
||||
before replying the evaluation may well already have happened, and sending
|
||||
it again would install a body twice or run a side-effecting expression
|
||||
twice; reconnecting happens before a send and never after one."
|
||||
:type 'number)
|
||||
|
||||
(defvar flan-dev--connection nil
|
||||
"The open connection, or nil.")
|
||||
|
||||
@ -149,7 +191,7 @@ Point is in the process buffer, and the frame is known to be complete."
|
||||
(defun flan-dev--read-reply (proc)
|
||||
"Block until PROC sends one complete framed message, and read it."
|
||||
(with-current-buffer (process-buffer proc)
|
||||
(let ((deadline (+ (float-time) 30)))
|
||||
(let ((deadline (+ (float-time) flan-dev-reply-timeout)))
|
||||
;; The header first: digits up to a newline.
|
||||
(while (and (not (save-excursion (goto-char (point-min))
|
||||
(re-search-forward "\\`\\([0-9]+\\)\n" nil t)))
|
||||
@ -161,9 +203,17 @@ Point is in the process buffer, and the frame is known to be complete."
|
||||
;; before replying, the evaluation may well have happened — sending it
|
||||
;; again would install it twice, or run a side-effecting expression
|
||||
;; twice. Reconnecting happens before a send, never after one.
|
||||
;; What a person does next, not just what failed. A live daemon that
|
||||
;; has not answered is nearly always still working — a first compile
|
||||
;; of a large program on a cold object cache is the case that runs
|
||||
;; long — and the daemon's own log says which step it is on, so the
|
||||
;; message names the buffer to look in and the setting to raise
|
||||
;; rather than leaving both to be discovered.
|
||||
(if (process-live-p proc)
|
||||
(error "flan dev: no reply in 30s from %s"
|
||||
(abbreviate-file-name (or flan-dev--socket "the daemon")))
|
||||
(error "flan dev: no reply in %ss from %s; the daemon may still be building — see %s for its log, and raise `flan-dev-reply-timeout' if this build is simply long"
|
||||
flan-dev-reply-timeout
|
||||
(abbreviate-file-name (or flan-dev--socket "the daemon"))
|
||||
flan-dev-daemon-buffer)
|
||||
(error
|
||||
"flan dev: the daemon on %s closed the connection; not resent, because it may already have run"
|
||||
(abbreviate-file-name (or flan-dev--socket "?")))))
|
||||
@ -511,12 +561,10 @@ With no argument, look for `flan-dev-socket-name' up from this buffer."
|
||||
A name is looked up on `exec-path'; a path is used as given."
|
||||
:type 'string)
|
||||
|
||||
(defcustom flan-dev-daemon-buffer "*flan-dev*"
|
||||
"Buffer the daemon's own output goes to.
|
||||
This is where a build that failed says so: the daemon compiles the program
|
||||
before it binds its socket, so a program that does not compile produces no
|
||||
socket at all and this buffer is the only account of why."
|
||||
:type 'string)
|
||||
;; `flan-dev-daemon-buffer' belongs to this section and is declared with the
|
||||
;; other buffer names at the top of the file instead, because the reply reader
|
||||
;; -- which runs long before any of this -- names it in the message it gives
|
||||
;; when a request times out, and the byte-compiler reads a file in order.
|
||||
|
||||
(defcustom flan-dev-start-timeout 60
|
||||
"Seconds to wait for a daemon started from Emacs to accept a connection.
|
||||
|
||||
@ -1,5 +1,24 @@
|
||||
;;; flan-inspect.el --- Navigate a running program's values -*- lexical-binding: t; -*-
|
||||
|
||||
;; Author: Joseph Ferano <joseph@ferano.io>
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "29.1"))
|
||||
;; Keywords: languages, lisp, tools
|
||||
|
||||
;; The headers above are what make this directory installable. M-x
|
||||
;; package-install-file on it reads them, and a file with no Version: is not a
|
||||
;; package as far as package.el is concerned -- until now the client was
|
||||
;; reachable only by adding it to load-path by hand, which is a thing to
|
||||
;; explain to every person who wants to try it.
|
||||
;;
|
||||
;; 29.1 is the floor because it is the oldest Emacs any of this has been run
|
||||
;; against, not because some function here is known to need it. dape, which
|
||||
;; flan-dape drives, asks for 29.1 as well and is a soft dependency: it is
|
||||
;; reached through declare-function, so the rest of the client loads and works
|
||||
;; without it and it is deliberately not listed above. The compiler this talks
|
||||
;; to is not an Emacs package and cannot be listed here either -- emacs/MANUAL.md
|
||||
;; says what has to be on PATH.
|
||||
|
||||
;; `C-x C-e' renders a value once and puts it in the echo area. This is the
|
||||
;; interactive version of the same walk: the fields laid out one per line, RET
|
||||
;; to go into one, `l' to come back, `g' to read it again. CIDER's inspector,
|
||||
|
||||
@ -1,5 +1,24 @@
|
||||
;;; flan-lower.el --- Every lowering of one function, in one buffer -*- lexical-binding: t; -*-
|
||||
|
||||
;; Author: Joseph Ferano <joseph@ferano.io>
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "29.1"))
|
||||
;; Keywords: languages, lisp, tools
|
||||
|
||||
;; The headers above are what make this directory installable. M-x
|
||||
;; package-install-file on it reads them, and a file with no Version: is not a
|
||||
;; package as far as package.el is concerned -- until now the client was
|
||||
;; reachable only by adding it to load-path by hand, which is a thing to
|
||||
;; explain to every person who wants to try it.
|
||||
;;
|
||||
;; 29.1 is the floor because it is the oldest Emacs any of this has been run
|
||||
;; against, not because some function here is known to need it. dape, which
|
||||
;; flan-dape drives, asks for 29.1 as well and is a soft dependency: it is
|
||||
;; reached through declare-function, so the rest of the client loads and works
|
||||
;; without it and it is deliberately not listed above. The compiler this talks
|
||||
;; to is not an Emacs package and cannot be listed here either -- emacs/MANUAL.md
|
||||
;; says what has to be on PATH.
|
||||
|
||||
;; `spike/x86/dump.sh' prints four lowerings of one function side by side --
|
||||
;; the LLVM IR the frontend emits, what `llc' makes of it at -O0 and at -O2,
|
||||
;; and what the hand-written x86 backend emits. Reading one against another is
|
||||
|
||||
@ -1,5 +1,24 @@
|
||||
;;; flan-mode.el --- Major mode for Flan -*- lexical-binding: t; -*-
|
||||
|
||||
;; Author: Joseph Ferano <joseph@ferano.io>
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "29.1"))
|
||||
;; Keywords: languages, lisp, tools
|
||||
|
||||
;; The headers above are what make this directory installable. M-x
|
||||
;; package-install-file on it reads them, and a file with no Version: is not a
|
||||
;; package as far as package.el is concerned -- until now the client was
|
||||
;; reachable only by adding it to load-path by hand, which is a thing to
|
||||
;; explain to every person who wants to try it.
|
||||
;;
|
||||
;; 29.1 is the floor because it is the oldest Emacs any of this has been run
|
||||
;; against, not because some function here is known to need it. dape, which
|
||||
;; flan-dape drives, asks for 29.1 as well and is a soft dependency: it is
|
||||
;; reached through declare-function, so the rest of the client loads and works
|
||||
;; without it and it is deliberately not listed above. The compiler this talks
|
||||
;; to is not an Emacs package and cannot be listed here either -- emacs/MANUAL.md
|
||||
;; says what has to be on PATH.
|
||||
|
||||
;; Derived from `prog-mode', borrowing `lisp-mode''s machinery for the parts
|
||||
;; that are simply s-expressions: sexp motion, paren matching and
|
||||
;; `beginning-of-defun' already do the right thing.
|
||||
|
||||
@ -1,5 +1,24 @@
|
||||
;;; flan-repl.el --- A prompt for a running Flan program -*- lexical-binding: t; -*-
|
||||
|
||||
;; Author: Joseph Ferano <joseph@ferano.io>
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "29.1"))
|
||||
;; Keywords: languages, lisp, tools
|
||||
|
||||
;; The headers above are what make this directory installable. M-x
|
||||
;; package-install-file on it reads them, and a file with no Version: is not a
|
||||
;; package as far as package.el is concerned -- until now the client was
|
||||
;; reachable only by adding it to load-path by hand, which is a thing to
|
||||
;; explain to every person who wants to try it.
|
||||
;;
|
||||
;; 29.1 is the floor because it is the oldest Emacs any of this has been run
|
||||
;; against, not because some function here is known to need it. dape, which
|
||||
;; flan-dape drives, asks for 29.1 as well and is a soft dependency: it is
|
||||
;; reached through declare-function, so the rest of the client loads and works
|
||||
;; without it and it is deliberately not listed above. The compiler this talks
|
||||
;; to is not an Emacs package and cannot be listed here either -- emacs/MANUAL.md
|
||||
;; says what has to be on PATH.
|
||||
|
||||
;; A buffer to type expressions at, sent to the program `flan dev' is running
|
||||
;; and answered with the value they had *there*. It adds no protocol and no
|
||||
;; compiler support: every line goes through the same `eval-expr' request that
|
||||
|
||||
@ -1,5 +1,24 @@
|
||||
;;; flan-watch.el --- Watched values, in a pinned buffer or inline -*- lexical-binding: t; -*-
|
||||
|
||||
;; Author: Joseph Ferano <joseph@ferano.io>
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "29.1"))
|
||||
;; Keywords: languages, lisp, tools
|
||||
|
||||
;; The headers above are what make this directory installable. M-x
|
||||
;; package-install-file on it reads them, and a file with no Version: is not a
|
||||
;; package as far as package.el is concerned -- until now the client was
|
||||
;; reachable only by adding it to load-path by hand, which is a thing to
|
||||
;; explain to every person who wants to try it.
|
||||
;;
|
||||
;; 29.1 is the floor because it is the oldest Emacs any of this has been run
|
||||
;; against, not because some function here is known to need it. dape, which
|
||||
;; flan-dape drives, asks for 29.1 as well and is a soft dependency: it is
|
||||
;; reached through declare-function, so the rest of the client loads and works
|
||||
;; without it and it is deliberately not listed above. The compiler this talks
|
||||
;; to is not an Emacs package and cannot be listed here either -- emacs/MANUAL.md
|
||||
;; says what has to be on PATH.
|
||||
|
||||
;; A HUD for a running Flan program: a buffer that always shows the current
|
||||
;; frame's values and nothing else. Ported from the author's Clojure
|
||||
;; `clj-watch', with one thing kept and one thing inverted.
|
||||
|
||||
28
lib/check.ml
28
lib/check.ml
@ -6336,7 +6336,27 @@ let check_global env (d : Ast.decl) : Tast.global option =
|
||||
|
||||
(* The entry point, plan.org: (defn main [args [string]] i32), with both the
|
||||
parameter and the return type optional. *)
|
||||
let check_main env =
|
||||
(* Where [main] was written. [env.locs] is the table of where each *type* was
|
||||
declared — [collect] fills it for structs, data types, unions and enums and
|
||||
for nothing else — so a function's own location is not in it and the
|
||||
[find_opt] idiom the rest of this file uses does not apply here. The
|
||||
declaration list does have it, and the caller is holding the list anyway.
|
||||
|
||||
Without this both refusals below opened with <unknown>:0:0, which tells a
|
||||
reader that a rule exists and not where they broke it, and gives
|
||||
[next-error] nothing to jump to. A [main] that arrived some other way — a
|
||||
[declare], say — still has no [defn] to point at, so that case keeps the
|
||||
unknown span rather than inventing one. *)
|
||||
let main_loc (decls : Ast.decl list) =
|
||||
let is_main (d : Ast.decl) =
|
||||
match d.Ast.d with Ast.Defn fn -> fn.Ast.name = "main" | _ -> false
|
||||
in
|
||||
match List.find_opt is_main decls with
|
||||
| Some { Ast.d = Ast.Defn fn; _ } -> fn.Ast.nloc
|
||||
| _ -> Loc.unknown
|
||||
|
||||
let check_main env decls =
|
||||
let at = main_loc decls in
|
||||
match Hashtbl.find_opt env.fns "main" with
|
||||
| None -> () (* a library, or a file being checked on its own *)
|
||||
| Some (params, ret) ->
|
||||
@ -6347,12 +6367,12 @@ let check_main env =
|
||||
| _ -> false
|
||||
in
|
||||
if not ok_params then
|
||||
fail Loc.unknown
|
||||
fail at
|
||||
"main takes no parameters or one [string], not (%s)"
|
||||
(String.concat " " (List.map Types.to_string params));
|
||||
if not (Types.equal ret Types.Unit || Types.equal ret (Types.Int Types.I32))
|
||||
then
|
||||
fail Loc.unknown "main returns i32 or nothing, not %s"
|
||||
fail at "main returns i32 or nothing, not %s"
|
||||
(Types.to_string ret)
|
||||
|
||||
(* The environment as well as the program. A session needs it to check an
|
||||
@ -6385,7 +6405,7 @@ let build_program ~keep_going (decls : Ast.decl list) : Tast.program * env =
|
||||
check_finite env;
|
||||
check_union_members env;
|
||||
let s = Loc.sink ~on:keep_going in
|
||||
ignore (Loc.caught s (fun () -> check_main env));
|
||||
ignore (Loc.caught s (fun () -> check_main env decls));
|
||||
(* Every generic body, checked once with its variables left abstract, and
|
||||
the result thrown away. This is the pass plan.org's rule needs and Odin
|
||||
has no equivalent of: Odin checks a polymorphic body only per
|
||||
|
||||
25
lib/loc.ml
25
lib/loc.ml
@ -359,8 +359,8 @@ let report (d : diag) =
|
||||
it found. *)
|
||||
let report_all (ds : diag list) =
|
||||
(* Source order, with the placeless ones last. A diagnostic the checker
|
||||
raised against [unknown] — a wrong [main] signature is the one that
|
||||
happens — has line 0, and sorting on the number alone would put it at the
|
||||
raised against [unknown] — a rule about a declaration that is not in this
|
||||
file to point at — has line 0, and sorting on the number alone would put it at the
|
||||
top of the list, above every error that can actually be clicked. It is a
|
||||
real error and it is not anywhere, so it goes after the ones that are. *)
|
||||
let placed (d : diag) = d.dloc.line > 0 in
|
||||
@ -376,3 +376,24 @@ let report_all (ds : diag list) =
|
||||
let n = List.length ds in
|
||||
String.concat "\n" (List.map report ds)
|
||||
^ Printf.sprintf "\n%d error%s" n (if n = 1 then "" else "s")
|
||||
|
||||
(* The last line of defence, and the one nobody plans to reach. Every driver in
|
||||
this tree catches [Error] and [Errors] and prints [report]; a process that
|
||||
does not — a test binary walking the corpus, a tool written in an afternoon —
|
||||
dies through OCaml's default handler, and the default handler knows nothing
|
||||
about this record. What it prints is [Fatal error: exception
|
||||
Flan.Loc.Error(_)]: the name of a constructor, an underscore, and not one
|
||||
word of the diagnostic that was carefully built to say what was wrong and
|
||||
where. That is how three suite runs came to leave a message-less fatal on
|
||||
stderr while reporting that they had passed.
|
||||
|
||||
Registering a printer costs nothing and cannot change control flow — the
|
||||
exception still propagates and still kills whatever did not catch it. All it
|
||||
changes is that the corpse says which file and which line, which is the
|
||||
whole of what the diagnostic was for. Drivers that do catch are unaffected:
|
||||
they never ask [Printexc] anything. *)
|
||||
let () =
|
||||
Printexc.register_printer (function
|
||||
| Error d -> Some (report d)
|
||||
| Errors ds -> Some (report_all ds)
|
||||
| _ -> None)
|
||||
|
||||
@ -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
|
||||
|
||||
20
test/dune
20
test/dune
@ -319,13 +319,19 @@
|
||||
; Valgrind present, and folding them in would make @checks the thing you do not
|
||||
; have time for -- which is the disease, not the cure.
|
||||
;
|
||||
; It is honest to say what this does not do. It does not run itself. Nothing
|
||||
; here runs itself, because there is no CI, and a commit hook that costs two
|
||||
; minutes gets switched off in a week. What it buys is that deciding to check
|
||||
; and checking everything are now the same act, so the gap between "somebody
|
||||
; wondered" and "everything was verified" is one command instead of five. The
|
||||
; habit that closes the rest of the gap is written down in README.md: a lane's
|
||||
; handoff quotes this alias, the way the x86 handoffs already quote survey.sh.
|
||||
; What it buys is that deciding to check and checking everything are now the
|
||||
; same act, so the gap between "somebody wondered" and "everything was
|
||||
; verified" is one command instead of five.
|
||||
;
|
||||
; It used to say here that nothing runs this, because there was no CI. There is
|
||||
; now: .github/workflows/checks.yml runs `dune build`, `dune test --force` and
|
||||
; this alias on every push. That does not make the local run redundant, and the
|
||||
; workflow says why in as many words — an Ubuntu runner has no raylib, no
|
||||
; emscripten, no wasi sysroot and no lldb, so every one of those cases takes
|
||||
; the skip path it already had and the tick is green over less than this alias
|
||||
; covers here. The habit written down in README.md still carries the rest: a
|
||||
; lane's handoff quotes this alias, the way the x86 handoffs already quote
|
||||
; survey.sh.
|
||||
(alias
|
||||
(name checks)
|
||||
(deps
|
||||
|
||||
@ -3727,6 +3727,71 @@ ERR@7 unexpected token: not the kind the caller was reading
|
||||
outputs ~opt:"-O0" "utf-8, splitting and ascii case, -O0"
|
||||
"programs/utf8.flan" utf8_out;
|
||||
|
||||
(* ── The driver's own refusals ─────────────────────────────────────
|
||||
|
||||
Not about compiled code at all: about what the CLI does when it is
|
||||
handed something it cannot do. Each of these was an escape before —
|
||||
an OCaml exception printed by the default handler, or a build flag
|
||||
silently forwarded to the program — and each is pinned here because
|
||||
"it prints a sentence" is exactly the kind of claim that rots without
|
||||
a test to hold it. *)
|
||||
let cli args =
|
||||
let out = Filename.concat scratch "flan-cli.out" in
|
||||
let code =
|
||||
Sys.command
|
||||
(Printf.sprintf "../bin/main.exe %s > %s 2>&1" args (Filename.quote out))
|
||||
in
|
||||
let text = In_channel.with_open_bin out In_channel.input_all in
|
||||
(try Sys.remove out with Sys_error _ -> ());
|
||||
(code, text)
|
||||
in
|
||||
let cli_case name args ~code:want_code ~says =
|
||||
let code, text = cli args in
|
||||
let bad =
|
||||
code <> want_code
|
||||
|| List.exists (fun n -> not (contains text n)) says
|
||||
(* The point of half of these: no arm here may end in OCaml's default
|
||||
handler, whatever else it does. *)
|
||||
|| contains text "Fatal error"
|
||||
in
|
||||
if bad then begin
|
||||
incr failures;
|
||||
Printf.printf "FAIL %s\n got: %S (exit %d)\n wanted exit %d with %s\n"
|
||||
name text code want_code (String.concat ", " says)
|
||||
end
|
||||
in
|
||||
(* A path that is not there. This used to be
|
||||
[Fatal error: exception Sys_error("...")] — the compiler reporting that
|
||||
it had not expected to be asked. *)
|
||||
cli_case "check on a file that is not there"
|
||||
"check no-such-file.flan" ~code:1
|
||||
~says:[ "no-such-file.flan"; "No such file or directory" ];
|
||||
(* And every other front end takes the same route, since the arm is on the
|
||||
one wrapper they all go through. *)
|
||||
cli_case "build on a file that is not there"
|
||||
"build no-such-file.flan -o /dev/null" ~code:1
|
||||
~says:[ "no-such-file.flan" ];
|
||||
(* [flan run] and its two argument lists. -O0 is the build's, so calc-me
|
||||
never sees it and answers the expression that follows; before the split
|
||||
it was handed "-O0" as the expression and said it could not parse it. *)
|
||||
cli_case "run keeps a build flag out of the program's argv"
|
||||
"run ../calc-me.flan -O0 '1+2'" ~code:0 ~says:[ "3" ];
|
||||
(* -- hands the rest over whatever it looks like, which is what makes the
|
||||
refusal below affordable. *)
|
||||
cli_case "run passes everything after -- to the program"
|
||||
"run ../calc-me.flan -O0 -- '3*4'" ~code:0 ~says:[ "12" ];
|
||||
(* And an unknown dash argument is refused by name rather than guessed at
|
||||
in either direction. *)
|
||||
cli_case "run refuses a flag it does not offer"
|
||||
"run ../calc-me.flan --lint" ~code:2
|
||||
~says:[ "--lint"; "will not be guessed at"; "--" ];
|
||||
(* The one pair of flags that cannot both be honoured: --debug is -O0 in
|
||||
[Build] and says why, so asking for it alongside a higher level is a
|
||||
request with two answers. *)
|
||||
cli_case "--debug and an explicit -O are refused together"
|
||||
"build ../calc-me.flan --debug -O2 -o /dev/null" ~code:2
|
||||
~says:[ "--debug"; "-O2"; "Drop one of the two" ];
|
||||
|
||||
if !failures = 0 then print_endline "acceptance: all tests passed"
|
||||
else begin
|
||||
Printf.printf "\n%d failure(s)\n" !failures;
|
||||
|
||||
@ -945,6 +945,27 @@ let () =
|
||||
~needle:"main takes no parameters";
|
||||
rejects_check "main returning the wrong type" "(defn main [] bool true)"
|
||||
~needle:"main returns i32";
|
||||
(* And both of them point at the [main] that is wrong. They used to open with
|
||||
<unknown>:0:0 — the checker's rule about the entry point is the one place
|
||||
that had a name and no span, because [env.locs] records where a *type* was
|
||||
declared and a function is not in it. The span is the whole difference
|
||||
between a message you can act on and a message you have to go looking for,
|
||||
so it is pinned here rather than left to the reader of a report. *)
|
||||
let main_at name src =
|
||||
match checked src with
|
||||
| _ ->
|
||||
incr failures;
|
||||
Printf.printf "FAIL %s: expected a type error\n" name
|
||||
| exception Loc.Error { Loc.dloc; _ } ->
|
||||
let got = Loc.to_string dloc in
|
||||
if got <> "<test>:1:7" then begin
|
||||
incr failures;
|
||||
Printf.printf "FAIL %s\n wanted: %s\n got: %s\n"
|
||||
name "<test>:1:7" got
|
||||
end
|
||||
in
|
||||
main_at "a wrong main parameter points at main" "(defn main [n i32] ())";
|
||||
main_at "a wrong main return type points at main" "(defn main [] bool true)";
|
||||
|
||||
(* ── Unconstrained operators, and everything past milestone 2 ──── *)
|
||||
rejects_check "no built-in = on strings"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user