From d2bd02209472c490544b80694207ce2c4dcfb85b Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 09:09:09 +0700 Subject: [PATCH] An enum and an integer convert, both ways, when you say so --- NEXT.md | 31 ++++++++-- examples/core-input-gamepad.flan | 40 ++++++------- examples/core-input-gestures-testbed.flan | 55 +++++++++++++----- lib/check.ml | 55 ++++++++++++++++-- lib/parse.ml | 11 +++- test/programs/enum-convert.flan | 71 +++++++++++++++++++++++ test/programs/raylib-ffi.flan | 18 ++++++ test/test_acceptance.ml | 25 +++++++- test/test_flan.ml | 33 +++++++++++ 9 files changed, 291 insertions(+), 48 deletions(-) create mode 100644 test/programs/enum-convert.flan diff --git a/NEXT.md b/NEXT.md index eea97ea..ff4b79b 100644 --- a/NEXT.md +++ b/NEXT.md @@ -216,12 +216,31 @@ vague intention — if it is listed, someone has already established it is real. Ranked by how often they were hit, top two first because they are walls rather than conveniences: 1. ~~No number reaches `draw-text`.~~ **Fixed** by `(string b)`. -2. **An enum parameter cannot be driven by a loop variable.** `(rl/get-gamepad-axis-movement pad i)` with an `i32` - index: *expected rl/GamepadAxis, found i32*, and the other direction refuses too — *i32 converts a number, found - rl/Gesture*. The obvious escape is closed as well: a second `declare-c` of the same symbol with an `i32` face gives - *one declare-c per C function, and another Flan name for it is a defn* — and a `defn` renames without retyping. - Two individually-correct rules composing into a wall; the caller writes a `cond` over the members instead. Cost: - every gamepad-axis loop, and four comparisons on the raw gesture bitfield. +2. ~~An enum parameter cannot be driven by a loop variable.~~ **Fixed** by explicit conversions in both directions: + `(i32 k)` takes an enum to its integer, `(GamepadAxis n)` takes an integer to an enum. Neither is an instruction — + an enum is an i32 at run time and `emit.ml`'s `cast` already reduced one to that before choosing an opcode — so the + change is a guard in `check.ml`'s cast arm and nothing in the backend. The rule the refusals came from is + deliberately *not* relaxed: a bare integer still does not fit an enum parameter, so `:spcae` is still an error at + the call site. The rule was "an integer must not arrive silently", and a written `(GamepadAxis i)` is not silent. + The other escape stays closed too — one `declare-c` per C function — and no longer needs to be open. + - **A value that is no declared member is allowed**, deliberately. raylib's gesture is a bitfield and an OR of + flags is a legal `Gesture` that is no single member; and `session.ml`'s printer already falls through to the + number for an out-of-range enum, on purpose, so refusing to construct one while agreeing to print it would be + incoherent. An `Option` would make every site unwrap for no safety bought, and a literal-only refusal would + catch nothing, because the bitfield case is a run-time value. + - **Only an integer converts *to* an enum.** Not a float, and not another enum — a cross-enum hop goes through + `(i32 x)` so both ends are written down. Enum → any numeric is always allowed: lossless to i32 by construction, + and a narrower target truncates by the rule every int→int cast already follows. + - **The comparisons needed nothing else.** `(> (i32 g) 255)` checks because `binary` takes the non-literal side + first; `binary` was deliberately left ignorant of enums, since teaching it would be the implicit conversion this + avoids. + - **A bit-set type later builds on this rather than replacing it.** It would be its own type with its own + operations and would still want a named escape to the underlying integer for the FFI, spelled the same way. If + `Gesture` becomes one, the `(i32 g)` calls stay valid and only the range tests migrate to a membership test. + - One parse fix came with it: `defenum` names were not in `parse.ml`'s type set, so a local enum could not be a + function's return type. They are in it now under a key of their own, admitted as a bare symbol and never as a + list head — because `(Key n)` is a *value* now, and putting `Key` in `types` would make a body starting with one + be eaten as a return type. 3. **`break` is not implemented.** Declined deliberately rather than built — see below. 4. **A `let` binding takes no type annotation**, so a fixed array is either a top-level `defvar` or a literal with every element spelled out. `(let [pts [4 rl/Vector2]] …)` parses as a two-element array literal and fails with diff --git a/examples/core-input-gamepad.flan b/examples/core-input-gamepad.flan index 6b76b25..167362c 100644 --- a/examples/core-input-gamepad.flan +++ b/examples/core-input-gamepad.flan @@ -63,32 +63,32 @@ ;; The C's read-out loop is `for (i = 0; i < GetGamepadAxisCount(gamepad); i++) ;; DrawText(TextFormat("AXIS %i: %.02f", i, GetGamepadAxisMovement(gamepad, i)))` -;; — an axis selected by a loop variable. That cannot go through the binding, -;; twice over: +;; — an axis selected by a loop variable, which is what this is: an integer +;; converts to an enum when the conversion is written by name. +;; +;; It used to be a six-armed cond over the members, because an enum is its own +;; type in the checker and a bare i32 does not fit one: ;; ;; expected rl/GamepadAxis, found i32 ;; -;; because an integer does not convert to an enum and a keyword can only name -;; one member; and a second declare-c of the same C function with an i32 -;; parameter is refused as well — +;; That rule is unchanged and should be. It is what makes a misspelled +;; `:lft-x` an error *here*, at the call site, rather than a wrong axis read +;; later — which is the whole reason an enum is a type at all rather than a +;; pile of i32 constants. What changed is that the conversion can be said out +;; loud: the rule was never "an integer is dangerous", it was "an integer must +;; not arrive silently", and (rl/GamepadAxis index) is not silent. ;; -;; rl/get-gamepad-axis-movement and rl/get-gamepad-axis-movement-by-index -;; both bind the C function GetGamepadAxisMovement — one declare-c per C -;; function, and another Flan name for it is a defn +;; (The other escape stays closed, and should: a second declare-c of +;; GetGamepadAxisMovement with an i32 face is still refused — one declare-c +;; per C function — and a defn wrapper still cannot retype a parameter. It is +;; no longer needed.) ;; -;; — and a defn wrapper cannot change a parameter's type. So the index is -;; turned back into a member here, by hand, which is the only shape left. It -;; covers the six axes raylib names; a pad reporting more reads 0.0 for the -;; extras, and the count is clamped below so they are not drawn at all. +;; An index raylib does not name converts too, rather than being refused, and +;; raylib bounds-checks its own axis and answers 0.0. So the (min 6 ...) clamp +;; at the call site is what keeps the read-out to the six axes raylib names — +;; load-bearing now rather than cosmetic, since it replaced the :else arm. (defn axis-at [pad i32 index i32] f32 - (cond - (= index 0) (rl/get-gamepad-axis-movement pad :left-x) - (= index 1) (rl/get-gamepad-axis-movement pad :left-y) - (= index 2) (rl/get-gamepad-axis-movement pad :right-x) - (= index 3) (rl/get-gamepad-axis-movement pad :right-y) - (= index 4) (rl/get-gamepad-axis-movement pad :left-trigger) - (= index 5) (rl/get-gamepad-axis-movement pad :right-trigger) - :else 0.0)) + (rl/get-gamepad-axis-movement pad (rl/GamepadAxis index))) (defn draw-pad-background [] (rl/draw-rectangle-rounded diff --git a/examples/core-input-gestures-testbed.flan b/examples/core-input-gestures-testbed.flan index 577184b..e50514a 100644 --- a/examples/core-input-gestures-testbed.flan +++ b/examples/core-input-gestures-testbed.flan @@ -5,20 +5,32 @@ ;;;; the language. Four things it needed, in descending order of how much they ;;;; cost. ;;;; -;;;; **An enum does not convert to an integer.** The C treats the gesture as -;;;; the raw bitfield it is and compares it with `<` and `>`: +;;;; **An enum converts to an integer, when you say so.** The C treats the +;;;; gesture as the raw bitfield it is and compares it with `<` and `>`: ;;;; `currentGesture > 255` picks out the two pinches, `> 15` the four swipes, ;;;; `!= 4` excludes hold, `< 3` admits tap and double-tap. `get-gesture- -;;;; detected` answers an `rl/Gesture`, and +;;;; detected` answers an `rl/Gesture`, and that used to end the discussion — ;;;; ;;;; i32 converts a number, found rl/Gesture ;;;; -;;;; so none of those comparisons can be written. They are spelled out below as -;;;; named predicates over keyword equalities — `pinch?`, `swipe?`, `tapish?`. -;;;; That is arguably better source than the magic numbers were, and it is -;;;; strictly more checkable, but it is not a choice: it is the only shape -;;;; available, and a gesture raylib adds later would silently fall out of -;;;; `swipe?` where the C's `> 15` would have caught it. +;;;; — so the three range tests were spelled out as keyword equalities, one +;;;; arm per member. They are `(> (i32 g) 255)`, `(> (i32 g) 15)` and +;;;; `(< (i32 g) 3)` now, still behind the named predicates below, and the fix +;;;; is not brevity: the enumerated version was *wrong about the future*. A +;;;; gesture raylib adds later falls silently out of a list of four members, +;;;; where the C's `> 15` catches it. The range test is the honest reading of +;;;; a bitfield and now it is the one written. +;;;; +;;;; The rule the refusal came from is unchanged and worth keeping: an enum is +;;;; its own type in the checker, so `:tpa` is an error at the call site +;;;; instead of a number that is wrong later, and a bare integer still does not +;;;; fit an `rl/Gesture` parameter. `(i32 g)` does not weaken that — it is +;;;; named, and it is at the site. The rule was "an integer must not arrive +;;;; silently", not "an integer is dangerous". +;;;; +;;;; The `!= 4` stays a keyword comparison, `(not (= g :hold))`. That one was +;;;; never a range test; it is a single member, and the C's 4 is a magic +;;;; number the keyword reads better than. ;;;; ;;;; **No sin or cos.** The prelude has sqrt-f32 — one `declare` over libm, ;;;; with a comment explaining why it is not a builtin — and nothing else @@ -76,18 +88,26 @@ ;; ── The comparisons the C makes on the raw bitfield ───────────────── ;; -;; See the header: an rl/Gesture will not convert to an i32, so `> 255` and -;; friends are these instead. +;; A gesture is a flag: 1, 2, 4, 8 … up to 512, so the ranges are the C's way +;; of asking which family a gesture belongs to. (i32 g) is what lets that be +;; written; they are named here rather than inline because a bare 255 at a +;; call site says nothing, and because a gesture raylib adds later lands in +;; the right family without this file being edited. (defn pinch? [g rl/Gesture] bool ; the C's `> 255` - (or (= g :pinch-in) (= g :pinch-out))) + (> (i32 g) 255)) (defn swipe? [g rl/Gesture] bool ; the C's `> 15` - (or (or (= g :swipe-right) (= g :swipe-left)) - (or (= g :swipe-up) (= g :swipe-down)))) + (> (i32 g) 15)) (defn tapish? [g rl/Gesture] bool ; the C's `< 3` - (or (= g :tap) (= g :double-tap))) + (< (i32 g) 3)) + +;; Two orderings these impose, both of them the C's as well. A pinch is above +;; 255 and therefore above 15, so swipe? has to be asked after the pinches +;; rather than before; and tapish? admits :none, which is 0, so it belongs +;; under a :none guard. The C's switch over single members hides both; a range +;; test cannot. (defn gesture-name [g rl/Gesture] string (cond @@ -109,9 +129,12 @@ (= g :tap) rl/blue (= g :double-tap) rl/skyblue (= g :drag) rl/lime - (swipe? g) rl/red + ;; The two pinches are above 255 and so are above 15 as well: swipe? is a + ;; range test and has to be asked after them, not before. The C gets this + ;; for free by being a switch over single members. (= g :pinch-in) rl/violet (= g :pinch-out) rl/orange + (swipe? g) rl/red :else rl/black)) ;; ── The log ───────────────────────────────────────────────────────── diff --git a/lib/check.ml b/lib/check.ml index e2f7faa..5b65f17 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -1359,13 +1359,60 @@ and named_call ctx ~want loc name args = arity loc name 0 args; prim Tast.Argv (Types.Slice Types.String) [] - (* ── casts: (i32 x), (f64 x) ───────────────────────────────────── *) + (* ── casts: (i32 x), (f64 x), and an enum both ways ──────────────── + + (i32 e) and (GamepadAxis n) are written here rather than in an arm of + their own because they are the same operation: an enum is an i32 at run + time — Types.Enum says so — and emit.ml's [cast] already reduces one to + its i32 before choosing an instruction. So both directions cost nothing: + src and target are equal after that reduction and [cast] answers the + value unchanged. + + Why this does not give the typo back. The property worth keeping is that + :spcae at a call site is an error at that site, and it still is: a + keyword resolves against the parameter's enum and a bare integer does not + fit one. What changes is only that a program can *say* it means the + conversion, by name, at the site. The rule was never "an integer is + dangerous", it was "an integer must not arrive silently", and a written + (GamepadAxis i) is not silent. + + Three sub-decisions: + + 1. enum → any numeric is always allowed and never checked. It is lossless + to i32 by construction, and a narrower target truncates by the same + rule every int→int cast already follows — no special case, and (f32 e) + means (f32 (i32 e)) rather than an arbitrary refusal. + + 2. integer → enum accepts a value that is not a declared member. raylib's + gesture is a bitfield and an OR of flags is a legal Gesture that is no + single member, so refusing it would refuse correct programs; and + session.ml's printer already falls through to the number for an + out-of-range enum, on purpose, so refusing to *construct* one while + blessing its display would be incoherent. An Option would make every + site unwrap for no safety bought, and a literal-only refusal would + catch nothing — the bitfield case is a run-time value. + + 3. Only an integer converts *to* an enum. Not a float, which has no + meaning here, and not another enum: an enum-to-enum hop goes through + (i32 x) so that both ends are written down. *) + | _ when Hashtbl.mem ctx.env.enums name -> + arity loc name 1 args; + let target = resolve_name ctx.env ~seen:[] loc name in + let a = check ctx (List.hd args) in + (match a.Tast.ty with + | Types.Int _ -> () + | other -> + fail loc "%s converts an integer to an enum, found %s — an enum or a \ + float goes through (i32 x) first" name + (Types.to_string other)); + prim (Tast.Cast target) target [ a ] | _ when is_cast name && List.length args = 1 -> let target = resolve_name ctx.env ~seen:[] loc name in let a = check ctx (List.hd args) in - if not (Types.is_numeric a.Tast.ty) then - fail loc "%s converts a number, found %s" name - (Types.to_string a.Tast.ty); + (match a.Tast.ty with + | Types.Enum _ -> () + | t when Types.is_numeric t -> () + | t -> fail loc "%s converts a number, found %s" name (Types.to_string t)); prim (Tast.Cast target) target [ a ] (* ── ordinary calls ────────────────────────────────────────────── *) diff --git a/lib/parse.ml b/lib/parse.ml index d513fe9..ef25878 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -802,7 +802,8 @@ and qualified_type types s = and is_type_form types (f : Form.t) = match f.v with - | Sym s -> Names.mem s types || qualified_type types s + | Sym s -> + Names.mem s types || Names.mem ("enum " ^ s) types || qualified_type types s | Vec _ -> true (* [T] and [n T] are only types *) | Map _ -> true (* {K V} in this position *) | List ({ v = Sym n; _ } :: _) -> @@ -831,6 +832,14 @@ let declared_types (forms : Form.t list) : Names.t = it that nothing imported. *) | List [ { v = Sym "import"; _ }; { v = Sym a; _ }; { v = Str _; _ } ] -> Names.add ("import " ^ a) acc + (* An enum is a type too, but under its own key rather than beside the + structs, because [(Key n)] is now a *value* — the integer-to-enum + conversion — and putting Key in [types] would make [is_type_form] + read that as a type application and eat it as a return type. So an + enum name counts only as a bare symbol, which is the one position it + can appear in as a type, and never as a list head. *) + | List [ { v = Sym "defenum"; _ }; { v = Sym n; _ }; _ ] -> + Names.add ("enum " ^ n) acc | _ -> acc) builtin_types forms diff --git a/test/programs/enum-convert.flan b/test/programs/enum-convert.flan new file mode 100644 index 0000000..a3617a4 --- /dev/null +++ b/test/programs/enum-convert.flan @@ -0,0 +1,71 @@ +;;;; Converting between an enum and an integer, both ways, explicitly. +;;;; +;;;; An enum is an i32 at run time and its own type in the checker. That is +;;;; what makes :spcae an error at the call site rather than a wrong number +;;;; later, and it is deliberately not weakened here: a bare integer still +;;;; does not fit an enum parameter, and a keyword still resolves against the +;;;; enum the site expects. What is added is a way to *say* the conversion, +;;;; by name, where it is meant — (i32 k) and (K n). +;;;; +;;;; Both directions are free. emit.ml's cast reduces an enum to its i32 +;;;; before choosing an instruction, so src and target are the same type and +;;;; the value is answered unchanged; there is no instruction to see at -O0 +;;;; either, which is why this runs at both optimisation levels. + +(defenum K [lo -1 mid 0 hi 1]) + +(defn num [k K] i32 (i32 k)) + +;; An enum parameter driven by a loop variable, which is the shape this exists +;; for: the caller has an index, not a member. +(defn name-at [i i32] string + (let [k (K i)] + (cond (= k :lo) "lo" + (= k :mid) "mid" + (= k :hi) "hi" + :else "other"))) + +(defn main [] i32 + ;; enum → i32. Lossless by construction: i32 is the representation. + (println (num :lo)) + (println (num :mid)) + (println (num :hi)) + + ;; i32 → enum, then back. A round trip is the identity both ways because + ;; neither direction is a conversion at run time. + (println (num (K 1))) + (println (i32 (K (num :lo)))) + + ;; A value that is no declared member is allowed. raylib's gesture bitfield + ;; is an OR of flags and is exactly this, and the printer already falls + ;; through to the number for an out-of-range enum, so refusing to construct + ;; one while agreeing to print it would be incoherent. + (let [odd (K 7)] + (println (num odd)) + ;; And printed as itself, which is the half that makes this coherent: the + ;; structural printer is a comparison chain over the declared members and + ;; falls through to the number when none match. Agreeing to show a value + ;; outside the members while refusing to build one would be the + ;; incoherence. + (println odd) + ;; The contrast: a value that IS a member prints as the member. + (println (K 1)) + (println (if (= odd :hi) "member" "not a member")) + ;; The comparisons the raw bitfield wants, which need nothing beyond the + ;; conversion: the enum goes to i32 and the literal follows it. + (println (if (> (i32 odd) 3) "above 3" "not above 3"))) + + ;; A narrower or wider target truncates and extends by the same rule every + ;; int→int cast follows — lo is -1, so i64 sign-extends and u8 wraps. + (println (i64 (K -1))) + (println (u8 (K -1))) + + ;; A float target means (f32 (i32 k)); nothing special. Written as (K 1) + ;; rather than :hi because a bare keyword outside an enum-typed position has + ;; no enum to resolve against, and that refusal is unchanged. + (println (f32 (K 1))) + + ;; And the index-driven loop the whole thing is for. + (dotimes [i 4] + (println (name-at (- i 1)))) + 0) diff --git a/test/programs/raylib-ffi.flan b/test/programs/raylib-ffi.flan index 9a788e5..c802b27 100644 --- a/test/programs/raylib-ffi.flan +++ b/test/programs/raylib-ffi.flan @@ -270,4 +270,22 @@ (Some p) (show-v p) None (println "no crossing")) + ;; An enum reaching a declare-c parameter through (Enum n): the axis is a + ;; loop variable, which is what nothing could express before. No pad is + ;; attached, so raylib answers its own resting value per axis — 0.0 for the + ;; four stick axes and -1.0 for the two triggers, which rest at the negative + ;; end. That split is what makes this pin worth having: it shows six + ;; distinct i32s arriving as six distinct GamepadAxis members rather than + ;; one constant answered six times. + (print "axes") + (dotimes [i 6] + (print " ") + (print (rl/get-gamepad-axis-movement 0 (rl/GamepadAxis i)))) + ;; And an axis that is no declared member. raylib bounds-checks it itself and + ;; answers 0.0; the conversion does not refuse it, for the same reason the + ;; printer shows an out-of-range enum as its number. + (print " past-end ") + (print (rl/get-gamepad-axis-movement 0 (rl/GamepadAxis 9))) + (println "") + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 1d6d98f..05b46a2 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -324,7 +324,8 @@ let () = point on line yes\npoint off line no\n\ point in poly yes\npoint outside poly no\n\ in square, four corners yes\nout of triangle, three no\n\ - no crossing\n" + no crossing\n\ + axes 0 0 0 0 -1 -1 past-end 0\n" in if Sys.command "ldconfig -p 2>/dev/null | grep -q libraylib" = 0 then begin outputs "raylib ffi, headless" "programs/raylib-ffi.flan" raylib_out; @@ -916,6 +917,28 @@ ERR@7 unexpected token: not the kind the caller was reading outputs ~opt:"-O0" "enum comparison, -O0" "programs/enum-compare.flan" enum_out; + (* Converting an enum, explicitly, in both directions: (i32 k) and (K n). + An enum is an i32 at run time, so neither direction is an instruction + and the interesting thing is what the checker will let through — which + is why this is here and at -O0 rather than only in test_flan. The rows + are the three members out, a round trip back, a value that is no + declared member and the comparisons it exists for, a narrowing and a + widening of a negative member, and an enum parameter driven by a loop + variable, which is the whole point. + + The two bare prints in the middle are the ones that hold the design up + rather than merely exercising it: 7 prints as 7 and 1 prints as :hi. + Allowing a non-member to be *built* is only coherent because the + printer already shows one as its number, and this is where that is + asserted rather than asserted about. *) + let enum_conv_out = + "-1\n0\n1\n1\n-1\n7\n7\n:hi\nnot a member\nabove 3\n-1\n255\n1\n\ + lo\nmid\nhi\nother\n" + in + outputs "enum conversion" "programs/enum-convert.flan" enum_conv_out; + outputs ~opt:"-O0" "enum conversion, -O0" "programs/enum-convert.flan" + enum_conv_out; + (* ── declare-c: the generated FFI shim (lib/shim.ml) ──────────────── The raylib package is the proof that the generator is real — 84 diff --git a/test/test_flan.ml b/test/test_flan.ml index 7c03f1a..ff84835 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -633,6 +633,39 @@ let () = ~needle:"has no member :spcae"; accepts "a keyword that is a member" "(defenum Key [space 32 r 82]) (defn g [k Key]) (defn f [] (g :r))"; + (* Converting an enum, explicitly, in both directions. The point of the + conversion is that it is written at the site: a bare integer still does + not fit an enum parameter, so the checked property — a typo is an error + here rather than a wrong number later — is untouched. *) + accepts "an enum converts to an integer" + "(defenum Key [space 32]) (defn f [k Key] i32 (i32 k))"; + accepts "an enum converts to a float, through its i32" + "(defenum Key [space 32]) (defn f [k Key] f32 (f32 k))"; + accepts "an integer converts to an enum" + "(defenum Key [space 32]) (defn g [k Key]) (defn f [i i32] (g (Key i)))"; + accepts "a value that is no declared member converts" + "(defenum Key [space 32]) (defn g [k Key]) (defn f [] (g (Key 999)))"; + rejects_check "an integer still does not fit an enum on its own" + "(defenum Key [space 32]) (defn g [k Key]) (defn f [i i32] (g i))" + ~needle:"expected Key"; + rejects_check "an enum does not convert to another enum" + "(defenum A [x 1]) (defenum B [y 1]) (defn f [a A] B (B a))" + ~needle:"converts an integer to an enum"; + rejects_check "a float does not convert to an enum" + "(defenum Key [space 32]) (defn f [x f32] Key (Key x))" + ~needle:"converts an integer to an enum"; + (* An enum is a return type, which needed parse.ml to know enum names. It + knows them under a key of their own: (Key n) is a value now, so putting + Key in [types] would make a body starting with one be eaten as a return + type — the exact trap [is_type_form]'s comment is about. *) + accepts "an enum is a return type" + "(defenum Key [space 32]) (defn f [i i32] Key (Key i))"; + accepts "an enum conversion at the head of a body is not a return type" + "(defenum Key [space 32]) (defn g [k Key]) \ + (defn f [] (Key 1) (g :space))"; + rejects_check "an enum conversion takes one argument" + "(defenum Key [space 32]) (defn f [] Key (Key 1 2))" + ~needle:"1 argument"; (* A folded constant skips [check], so its range check has to be its own. *) rejects_check "a folded constant is still range-checked" "(defconst c u8 300) (defn f [] u8 c)" ~needle:"does not fit in u8";