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
|
||||||
156
README.md
156
README.md
@ -118,17 +118,139 @@ be threaded through every caller. See
|
|||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
Eleven of them, and the four anyone starts with:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
flan check <file.flan> type-check a program
|
flan check <file.flan> type-check a program
|
||||||
flan run <file.flan> [args...] build and run it
|
flan run <file.flan> [flags] [-- args...] build and run it
|
||||||
flan build <file.flan> [-o out] [options] build a native executable
|
flan build <file.flan> [-o out] [flags] build a native executable
|
||||||
flan dev <file.flan> [-s socket] start a live development session
|
flan dev <file.flan> [-s socket] start a live development session
|
||||||
```
|
```
|
||||||
|
|
||||||
Useful build options include `--debug`, `--sanitize`, `--no-bounds-checks`,
|
Useful build options include `-O0`…`-O3`, `--debug`, `--sanitize`,
|
||||||
`--x86`, and `--target=wasm32-wasi|web`. `run` is native-only; cross-built
|
`--no-bounds-checks`, `--x86`, and `--target=wasm32-wasi|web`. `run` takes the
|
||||||
output should be run with an appropriate WASI runtime or browser. A `.wasm`
|
same build flags and keeps them: everything after `--` goes to the program, and
|
||||||
file is not a tiny native executable in a trench coat.
|
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
|
## Checking it
|
||||||
|
|
||||||
@ -146,13 +268,19 @@ hand-written x86 backend still agrees with LLVM, and a `--dev` build still calls
|
|||||||
through its indirection cells.
|
through its indirection cells.
|
||||||
|
|
||||||
The two are separate on purpose. A suite that goes red because prose drifted
|
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
|
teaches you to skim past red. Both of this repository's silent failures — two
|
||||||
runs it for you — there is no CI here. The convention that has to carry it is
|
backend refusals that sat for a month, a page of examples that stopped
|
||||||
that a lane's handoff quotes `@checks`, the way the x86 handoffs already quote
|
compiling for two days — were found by accident, and neither would have
|
||||||
the survey's counts. Both of this repository's silent failures — two backend
|
survived one person typing one command.
|
||||||
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
|
`.github/workflows/checks.yml` is now that person: it runs `dune build`,
|
||||||
typing one command.
|
`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
|
## Project map
|
||||||
|
|
||||||
|
|||||||
138
bin/main.ml
138
bin/main.ml
@ -39,6 +39,31 @@ let with_errors path f =
|
|||||||
prerr_endline ("flan: " ^ m);
|
prerr_endline ("flan: " ^ m);
|
||||||
ignore path;
|
ignore path;
|
||||||
exit 1
|
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 summarise (d : Flan.Ast.decl) =
|
||||||
let open Flan.Ast in
|
let open Flan.Ast in
|
||||||
@ -151,8 +176,43 @@ let flags =
|
|||||||
usage error. *)
|
usage error. *)
|
||||||
let target_prefix = "--target="
|
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 =
|
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 =
|
let target_of args =
|
||||||
List.find_map
|
List.find_map
|
||||||
@ -515,6 +575,8 @@ let () =
|
|||||||
let debug = List.mem debug_flag rest in
|
let debug = List.mem debug_flag rest in
|
||||||
let sanitize = List.mem sanitize_flag rest in
|
let sanitize = List.mem sanitize_flag rest in
|
||||||
let x86 = List.mem x86_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 target = target_of rest in
|
||||||
let out =
|
let out =
|
||||||
match List.filter (fun a -> not (is_flag a)) rest with
|
match List.filter (fun a -> not (is_flag a)) rest with
|
||||||
@ -535,7 +597,8 @@ let () =
|
|||||||
| _ -> base)
|
| _ -> base)
|
||||||
| _ ->
|
| _ ->
|
||||||
prerr_endline
|
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]";
|
[--dev] [--debug] [--sanitize] [--target=wasm32-wasi|web|js]";
|
||||||
exit 2
|
exit 2
|
||||||
in
|
in
|
||||||
@ -549,7 +612,9 @@ let () =
|
|||||||
let p, csrcs, lflags = Flan.Reach.link ~dev l p in
|
let p, csrcs, lflags = Flan.Reach.link ~dev l p in
|
||||||
ignore (Flan.Build.executable
|
ignore (Flan.Build.executable
|
||||||
~opts:{ Flan.Build.default with checks; dev; debug; sanitize;
|
~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 [])
|
~csrcs ~lflags ~pnames:(if debug then param_names l else [])
|
||||||
p ~out))
|
p ~out))
|
||||||
(* The daemon an editor talks to: one session, the program it belongs to
|
(* 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.";
|
this host can exec. Use flan build --target=... and a wasm runtime.";
|
||||||
exit 2
|
exit 2
|
||||||
| _ :: "run" :: path :: args ->
|
| _ :: "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 () ->
|
with_errors path (fun () ->
|
||||||
let exe =
|
let exe =
|
||||||
Filename.concat (Filename.get_temp_dir_name ())
|
Filename.concat (Filename.get_temp_dir_name ())
|
||||||
@ -645,9 +761,16 @@ let () =
|
|||||||
let l = load path in
|
let l = load path in
|
||||||
let p = Flan.Check.program_all l.decls in
|
let p = Flan.Check.program_all l.decls in
|
||||||
let p, csrcs, lflags = Flan.Reach.link l p 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 =
|
let code =
|
||||||
Sys.command (String.concat " " (List.map Filename.quote (exe :: args)))
|
Sys.command
|
||||||
|
(String.concat " " (List.map Filename.quote (exe :: prog_args)))
|
||||||
in
|
in
|
||||||
(try Sys.remove exe with Sys_error _ -> ());
|
(try Sys.remove exe with Sys_error _ -> ());
|
||||||
exit code)
|
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\
|
"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 import-c <header.h> [package.flan...] [clang flags...]\n\
|
||||||
\ flan generate-c <package-dir>\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\
|
[--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 reload <program.flan> <forms.flan> [-o out.so] [--x86]\n\
|
||||||
\ flan dev <program.flan> [-s socket] [--x86]";
|
\ flan dev <program.flan> [-s socket] [--x86]";
|
||||||
exit 2
|
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 |
|
| **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 |
|
| **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` |
|
| **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
|
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
|
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
|
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.
|
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
|
### 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
|
**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
|
(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
|
You also need the `flan` binary on your `PATH`. If it is somewhere else, set
|
||||||
`flan-dev-command`.
|
`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
|
## Starting a program
|
||||||
|
|||||||
@ -1,5 +1,24 @@
|
|||||||
;;; flan-cnr.el --- What a stopped program is offering -*- lexical-binding: t; -*-
|
;;; 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
|
;; `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
|
;; 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
|
;; 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; -*-
|
;;; 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
|
;; 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.
|
;; 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; -*-
|
;;; 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
|
;; 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
|
;; 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
|
;; 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."
|
"Buffer the running program's own output is appended to."
|
||||||
:type 'string)
|
: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
|
(defvar flan-dev--connection nil
|
||||||
"The open connection, or 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)
|
(defun flan-dev--read-reply (proc)
|
||||||
"Block until PROC sends one complete framed message, and read it."
|
"Block until PROC sends one complete framed message, and read it."
|
||||||
(with-current-buffer (process-buffer proc)
|
(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.
|
;; The header first: digits up to a newline.
|
||||||
(while (and (not (save-excursion (goto-char (point-min))
|
(while (and (not (save-excursion (goto-char (point-min))
|
||||||
(re-search-forward "\\`\\([0-9]+\\)\n" nil t)))
|
(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
|
;; before replying, the evaluation may well have happened — sending it
|
||||||
;; again would install it twice, or run a side-effecting expression
|
;; again would install it twice, or run a side-effecting expression
|
||||||
;; twice. Reconnecting happens before a send, never after one.
|
;; 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)
|
(if (process-live-p proc)
|
||||||
(error "flan dev: no reply in 30s from %s"
|
(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"
|
||||||
(abbreviate-file-name (or flan-dev--socket "the daemon")))
|
flan-dev-reply-timeout
|
||||||
|
(abbreviate-file-name (or flan-dev--socket "the daemon"))
|
||||||
|
flan-dev-daemon-buffer)
|
||||||
(error
|
(error
|
||||||
"flan dev: the daemon on %s closed the connection; not resent, because it may already have run"
|
"flan dev: the daemon on %s closed the connection; not resent, because it may already have run"
|
||||||
(abbreviate-file-name (or flan-dev--socket "?")))))
|
(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."
|
A name is looked up on `exec-path'; a path is used as given."
|
||||||
:type 'string)
|
:type 'string)
|
||||||
|
|
||||||
(defcustom flan-dev-daemon-buffer "*flan-dev*"
|
;; `flan-dev-daemon-buffer' belongs to this section and is declared with the
|
||||||
"Buffer the daemon's own output goes to.
|
;; other buffer names at the top of the file instead, because the reply reader
|
||||||
This is where a build that failed says so: the daemon compiles the program
|
;; -- which runs long before any of this -- names it in the message it gives
|
||||||
before it binds its socket, so a program that does not compile produces no
|
;; when a request times out, and the byte-compiler reads a file in order.
|
||||||
socket at all and this buffer is the only account of why."
|
|
||||||
:type 'string)
|
|
||||||
|
|
||||||
(defcustom flan-dev-start-timeout 60
|
(defcustom flan-dev-start-timeout 60
|
||||||
"Seconds to wait for a daemon started from Emacs to accept a connection.
|
"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; -*-
|
;;; 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
|
;; `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
|
;; 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,
|
;; 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; -*-
|
;;; 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 --
|
;; `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,
|
;; 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
|
;; 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; -*-
|
;;; 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
|
;; Derived from `prog-mode', borrowing `lisp-mode''s machinery for the parts
|
||||||
;; that are simply s-expressions: sexp motion, paren matching and
|
;; that are simply s-expressions: sexp motion, paren matching and
|
||||||
;; `beginning-of-defun' already do the right thing.
|
;; `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; -*-
|
;;; 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
|
;; 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
|
;; 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
|
;; 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; -*-
|
;;; 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
|
;; 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
|
;; frame's values and nothing else. Ported from the author's Clojure
|
||||||
;; `clj-watch', with one thing kept and one thing inverted.
|
;; `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
|
(* The entry point, plan.org: (defn main [args [string]] i32), with both the
|
||||||
parameter and the return type optional. *)
|
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
|
match Hashtbl.find_opt env.fns "main" with
|
||||||
| None -> () (* a library, or a file being checked on its own *)
|
| None -> () (* a library, or a file being checked on its own *)
|
||||||
| Some (params, ret) ->
|
| Some (params, ret) ->
|
||||||
@ -6347,12 +6367,12 @@ let check_main env =
|
|||||||
| _ -> false
|
| _ -> false
|
||||||
in
|
in
|
||||||
if not ok_params then
|
if not ok_params then
|
||||||
fail Loc.unknown
|
fail at
|
||||||
"main takes no parameters or one [string], not (%s)"
|
"main takes no parameters or one [string], not (%s)"
|
||||||
(String.concat " " (List.map Types.to_string params));
|
(String.concat " " (List.map Types.to_string params));
|
||||||
if not (Types.equal ret Types.Unit || Types.equal ret (Types.Int Types.I32))
|
if not (Types.equal ret Types.Unit || Types.equal ret (Types.Int Types.I32))
|
||||||
then
|
then
|
||||||
fail Loc.unknown "main returns i32 or nothing, not %s"
|
fail at "main returns i32 or nothing, not %s"
|
||||||
(Types.to_string ret)
|
(Types.to_string ret)
|
||||||
|
|
||||||
(* The environment as well as the program. A session needs it to check an
|
(* 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_finite env;
|
||||||
check_union_members env;
|
check_union_members env;
|
||||||
let s = Loc.sink ~on:keep_going in
|
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
|
(* 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
|
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
|
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. *)
|
it found. *)
|
||||||
let report_all (ds : diag list) =
|
let report_all (ds : diag list) =
|
||||||
(* Source order, with the placeless ones last. A diagnostic the checker
|
(* Source order, with the placeless ones last. A diagnostic the checker
|
||||||
raised against [unknown] — a wrong [main] signature is the one that
|
raised against [unknown] — a rule about a declaration that is not in this
|
||||||
happens — has line 0, and sorting on the number alone would put it at the
|
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
|
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. *)
|
real error and it is not anywhere, so it goes after the ones that are. *)
|
||||||
let placed (d : diag) = d.dloc.line > 0 in
|
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
|
let n = List.length ds in
|
||||||
String.concat "\n" (List.map report ds)
|
String.concat "\n" (List.map report ds)
|
||||||
^ Printf.sprintf "\n%d error%s" n (if n = 1 then "" else "s")
|
^ 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
|
above it is Flan. That is what keeps a second backend cheap — a primitive
|
||||||
is the only thing implemented twice.
|
is the only thing implemented twice.
|
||||||
|
|
||||||
It lives here as a string rather than as a file because there is no package
|
It lives here as a string rather than as a file, and every program gets it
|
||||||
loader yet; at milestone 3 it becomes an ordinary [core:] package and this
|
prepended by [Check.build_program] whether it asked for one or not. That is
|
||||||
module goes away. The acceptance programs may call anything defined here.
|
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
|
No printing function is here at all any more. [print] and [println] are
|
||||||
the whole printing surface, and neither is a function: both 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
|
; Valgrind present, and folding them in would make @checks the thing you do not
|
||||||
; have time for -- which is the disease, not the cure.
|
; 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
|
; What it buys is that deciding to check and checking everything are now the
|
||||||
; here runs itself, because there is no CI, and a commit hook that costs two
|
; same act, so the gap between "somebody wondered" and "everything was
|
||||||
; minutes gets switched off in a week. What it buys is that deciding to check
|
; verified" is one command instead of five.
|
||||||
; 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
|
; It used to say here that nothing runs this, because there was no CI. There is
|
||||||
; habit that closes the rest of the gap is written down in README.md: a lane's
|
; now: .github/workflows/checks.yml runs `dune build`, `dune test --force` and
|
||||||
; handoff quotes this alias, the way the x86 handoffs already quote survey.sh.
|
; 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
|
(alias
|
||||||
(name checks)
|
(name checks)
|
||||||
(deps
|
(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"
|
outputs ~opt:"-O0" "utf-8, splitting and ascii case, -O0"
|
||||||
"programs/utf8.flan" utf8_out;
|
"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"
|
if !failures = 0 then print_endline "acceptance: all tests passed"
|
||||||
else begin
|
else begin
|
||||||
Printf.printf "\n%d failure(s)\n" !failures;
|
Printf.printf "\n%d failure(s)\n" !failures;
|
||||||
|
|||||||
@ -945,6 +945,27 @@ let () =
|
|||||||
~needle:"main takes no parameters";
|
~needle:"main takes no parameters";
|
||||||
rejects_check "main returning the wrong type" "(defn main [] bool true)"
|
rejects_check "main returning the wrong type" "(defn main [] bool true)"
|
||||||
~needle:"main returns i32";
|
~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 ──── *)
|
(* ── Unconstrained operators, and everything past milestone 2 ──── *)
|
||||||
rejects_check "no built-in = on strings"
|
rejects_check "no built-in = on strings"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user