Skip to content

Commit 07fcdab

Browse files
committed
v0.1.606: f_pow, exp and log on RISC-V answer libm's bits; an extern's unit parameter is dropped by its type
On RV32IM/RV64IM these are prelude Mere over softfloat. Against libm on 6000-call sweeps, f_pow disagreed on 3131 points by up to 20 ulp, exp on 303, log on 598 by up to 2; now 47, 2 and 4, each 1 ulp. The log pair carries the rounding of s = (m-1)/(m+1), exp takes its argument as a pair and rounds once, and small integer powers walk as pairs. test/float/rv_libm_points.mere holds 66 points to libm's bits (rv_float_check.sh, with MEMU). C and LLVM dropped only a literal () for an extern's unit parameter, so a unit-typed variable was passed to a function declared to take nothing: clang refused it, LLVM linked by luck.
1 parent 30bf6e8 commit 07fcdab

10 files changed

Lines changed: 336 additions & 53 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ source tree is needed to build or run an app.
103103

104104
It **self-hosts** — the Mere-in-Mere compiler, compiled by itself and run as Wasm, emits byte-identical output to the reference. The RV32IM backend emits a flat binary with no external assembler or linker, and `--bare` hands the program the machine instead of a host; on that there is a kernel, a scheduler and a shell written in Mere, and the self-hosted compiler runs there as a user process — on a RISC-V CPU also written in Mere — emitting WAT byte-identical to the native one. See [docs/bare-metal.md](docs/bare-metal.md).
105105

106-
- **2904 tests passing** (`dune test`), plus cross-backend parity (216 programs) and ~30 differential gates against external oracles in CI. Both numbers are re-derived from the repo by `scripts/first_run_check.sh`, because a number in a README rots silently: the parity count had been checked since v0.1.504 and the test count had not, which is how it sat at 2715 while the suite passed 2724 (v0.1.470) — and then at 2796 while the suite passed 2856 (found 2026-09-24, by running every gate rather than the ones that looked affected)
106+
- **2905 tests passing** (`dune test`), plus cross-backend parity (216 programs) and ~30 differential gates against external oracles in CI. Both numbers are re-derived from the repo by `scripts/first_run_check.sh`, because a number in a README rots silently: the parity count had been checked since v0.1.504 and the test count had not, which is how it sat at 2715 while the suite passed 2724 (v0.1.470) — and then at 2796 while the suite passed 2856 (found 2026-09-24, by running every gate rather than the ones that looked affected)
107107
- **🏆 Bootstrap fixpoint — truly self-hosting** (v0.1.10, 2026-07-12): the Mere-in-Mere compiler, **compiled by itself and run as wasm, produces byte-identical output to the reference** — and that output runs correctly. Compiling a program with (A) the compiler under the interpreter and (B) the compiler compiled by itself yields the same WAT, byte for byte (CI-verified by a permanent fixpoint regression test). Getting there required guaranteed tail calls in the self-host codegen (`return_call_indirect`, Stage 55f) and fixing three latent self-compilation bugs (an eager pattern-check payload dereference, pointer-equality string compares in the untyped self-host codegen, and a missing `\r` lexer escape — Stage 55g). See [docs/changelog.md](docs/changelog.md).
108108
- **🏗️ A fifth backend, and an operating system on top of it** (v0.1.134–198): `mere -rv` emits a flat RV32IM binary with no external assembler or linker, and `--bare` hands the program the machine instead of a host — raw memory arrives as an unforgeable window capability, traps as ordinary Mere closures. On that: a preemptive scheduler, a shell, a syscall boundary, and finally **Mere's own self-hosted compiler running as a user process on a Mere kernel, on a CPU written in Mere**, emitting WAT byte-identical to the native interpreter. `mere -rvg` then emits a debug map beside the binary — from the same item list the assembler consumes, so it describes the bytes that actually ran — which is what lets [riscv-dbg](https://github.com/284km/memu/tree/main/riscv-dbg) break on Mere **source lines**, print a backtrace, and step **backwards**. See [docs/bare-metal.md](docs/bare-metal.md).
109109
- **🛰️ gRPC and GraphQL, against real clients** (v0.1.280–283): `contrib/proto` (protobuf wire both ways, `FileDescriptorSet`, and a code generator from `.proto`), `contrib/http2` (framing + HPACK + an h2c server with flow control in both directions and a worker pool), and `contrib/graphql` (lexer / parser / printer / SDL / executor with null propagation, introspection, and 21 of the specification's 32 validation rules). `grpcurl` and `graphql-js` are the oracles, and interfaces and unions resolve from `__typename` the way graphql-js's default `resolveType` does. See [contrib/proto/README.md](contrib/proto/README.md), [contrib/http2/README.md](contrib/http2/README.md), [contrib/graphql/README.md](contrib/graphql/README.md).

‎docs/changelog.md‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,45 @@ Major implementation milestones recorded per-slice (newest first). See `git log`
44

55
---
66

7+
## v0.1.606 — 2026-10-04
8+
9+
_On RISC-V, `f_pow`, `exp` and `log` answer what libm answers: within 1 ulp everywhere, and the same bits on all but a few points in a thousand. `2.3 ** 3` is 12.166999999999998 there now, as everywhere else. An `extern` whose parameter is `unit` can be called with a unit-typed variable on C and LLVM._
10+
11+
**`f_pow` / `exp` / `log` on RV32IM and RV64IM** (found by mere-ruby on RV64,
12+
corpus 105 and 173). These are prelude Mere over softfloat there, and
13+
libm on every other backend. Measured against libm on 6000-call sweeps:
14+
`f_pow` disagreed on 3131 points by up to 20 ulp, `exp` on 303 by 1, `log` on
15+
598 by up to 2; mere-ruby printed `2.3 ** 3` as 12.166999999999996 and
16+
`8 ** (1.0 / 3)` as 1.9999999999999998. Now 47, 2 and 4 points, each 1 ulp
17+
(`f_pow`'s worst is 0.71 ulp from the true value; on most of the 47 libm is
18+
the closer one). Three changes, all built from exact `two_sum` / `two_prod`
19+
(Dekker) pieces and rounded once at the end:
20+
- the log's HEAD+TAIL pair carries the rounding of `s = (m-1)/(m+1)`, which
21+
was up to 4e-17 -- a whole ulp of log near 1, and y times that in `f_pow`;
22+
- `__fp_exp2` takes its argument as a pair, sums 1 + r + r^2/2 exactly, and
23+
scales the argument's tail by all of e^r: the first version scaled it by
24+
1 + r and was six ulps off at |y ln x| = 190, where the tail is 1e-14;
25+
- integer exponents |y| <= 32 walk the binary powering as pairs. The plain
26+
walk stays as the first step: it is exact where the answer is exact, and
27+
outside [2^-960, 2^960] (where a pair cannot be split) its answer stands.
28+
The cost is time and memory: a `f_pow` is about 2.3x the work, and on these
29+
targets every float operation allocates (about 2 KB through softfloat's
30+
limbs, and a region gives nothing back), so a `f_pow` now takes ~600 KB of
31+
the heap instead of ~300 KB. `test/float/rv_libm_points.mere` holds 66 points
32+
of the three to libm's bits -- points that 605 got wrong -- and
33+
`scripts/rv_float_check.sh` runs it on RV32I when `MEMU` is set; RV64 gives
34+
the same bits.
35+
36+
**An extern's `unit` parameter is dropped by its type** (found while wrapping
37+
mere-ruby's externs). The C prototype drops a unit parameter by its declared
38+
type (`extern int getpid(void);`), but the call dropped only a literal `()`,
39+
so `fn (u: unit) -> getpid u` emitted `getpid(mu_u)` and clang refused the
40+
program; LLVM declared `@getpid()` and called it with an `i64`, which linked
41+
by luck. Both calls now drop the argument by the parameter's declared type,
42+
and a non-literal one is still evaluated, ahead of the call.
43+
`test/ctests/extern_unit_param_variable.mere` (clang) and a test_basic case
44+
(the LLVM call) hold it.
45+
746
## v0.1.605 — 2026-10-03
847

948
_A chain of literal `++` is one literal by the time it is compiled, and a list is copied along its spine by a loop: ten million elements no longer run the copy out of C stack, and a 6703-line text written one literal per line no longer costs the square of its length at startup. On Wasm, a region block's result no longer overwrites itself on the way out._

‎lib/codegen_c.ml‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2763,14 +2763,32 @@ let rec emit_expr (e : Ast.expr) : string =
27632763
in
27642764
(match collect_extern (Ast.{ node = Ast.App (f, arg); ty = e.ty; loc = e.loc }) [] with
27652765
| Some (name, args) ->
2766+
(* A `unit` parameter has no C counterpart: c_extern_prototype drops it
2767+
by its DECLARED type. The call has to drop the same ones, and by the
2768+
same test -- dropping only a literal `()` passed a unit-typed variable
2769+
(`fn (u: unit) -> getpid u`) as an argument the prototype does not
2770+
have, and clang refused the program. A non-literal one is still
2771+
evaluated, ahead of the call. *)
2772+
let rec params t =
2773+
match Ast.walk t with
2774+
| Ast.TyArrow (p, r) -> Ast.walk p :: params r
2775+
| _ -> []
2776+
in
2777+
let ptys = params (Hashtbl.find extern_fn_decls name) in
2778+
let is_unit_param i a =
2779+
(match List.nth_opt ptys i with Some Ast.TyUnit -> true | _ -> false)
2780+
|| (match a.Ast.node with Ast.Unit_lit -> true | _ -> false)
2781+
in
27662782
let arg_strs =
2767-
List.filter_map (fun a ->
2768-
match a.Ast.node with
2769-
| Ast.Unit_lit -> None
2770-
| _ -> Some (emit_expr a))
2771-
args
2783+
List.concat (List.mapi (fun i a -> if is_unit_param i a then [] else [emit_expr a]) args)
2784+
in
2785+
let effects =
2786+
List.concat (List.mapi (fun i a ->
2787+
if is_unit_param i a && (match a.Ast.node with Ast.Unit_lit -> false | _ -> true)
2788+
then [Printf.sprintf "(void)(%s), " (emit_expr a)] else []) args)
27722789
in
2773-
let call_str = Printf.sprintf "%s(%s)" name (String.concat ", " arg_strs) in
2790+
let call_str = Printf.sprintf "%s%s(%s)" (String.concat "" effects) name (String.concat ", " arg_strs) in
2791+
let call_str = if effects = [] then call_str else "(" ^ call_str ^ ")" in
27742792
let rec result_ty t =
27752793
match Ast.walk t with
27762794
| Ast.TyArrow (_, r) -> result_ty r

‎lib/codegen_llvm.ml‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5151,18 +5151,28 @@ let rec emit_expr (env : env) (e : Ast.expr) : string =
51515151
| t -> t
51525152
in
51535153
let ret_ty = result_ty (Hashtbl.find extern_fn_decls_llvm name) in
5154+
(* A `unit` parameter is not in the declaration (it is dropped by type),
5155+
so the call drops it by the same test; a non-literal one -- a variable
5156+
of type unit -- is still evaluated. Dropping only the literal passed
5157+
`fn (u: unit) -> getpid u` an argument `@getpid()` does not take. *)
5158+
let rec params t =
5159+
match Ast.walk t with
5160+
| Ast.TyArrow (p, r) -> Ast.walk p :: params r
5161+
| _ -> []
5162+
in
5163+
let ptys = params (Hashtbl.find extern_fn_decls_llvm name) in
51545164
let arg_ll_list =
5155-
List.filter_map (fun a ->
5156-
match a.Ast.node with
5157-
| Ast.Unit_lit -> None
5165+
List.concat (List.mapi (fun i a ->
5166+
match a.Ast.node, List.nth_opt ptys i with
5167+
| Ast.Unit_lit, _ -> []
5168+
| _, Some Ast.TyUnit -> let _ = emit_expr env a in []
51585169
| _ ->
51595170
let v = emit_expr env a in
51605171
let t =
51615172
match a.Ast.ty with
51625173
| Some t -> Ast.walk t | None -> Ast.TyInt
51635174
in
5164-
Some (Printf.sprintf "%s %s" (llvm_ty_of t) v))
5165-
args
5175+
[Printf.sprintf "%s %s" (llvm_ty_of t) v]) args)
51665176
in
51675177
let arg_ll = String.concat ", " arg_ll_list in
51685178
(match ret_ty with

0 commit comments

Comments
 (0)