diff --git a/TODO.org b/TODO.org index b13b15df..a032425c 100644 --- a/TODO.org +++ b/TODO.org @@ -137,6 +137,14 @@ expansion that defines a macro re-runs the expander, in a build and in a session no =,',x=, since =quote= takes a symbol, and a macro defined by an expansion is not exported from a package. docs/BUILT.md, "Quasiquote runs before the walk". +** DONE Bit operators are && || ^^ ~~ in .fln (decision 123) +CLOSED: [2026-09-26] +Tighter than a comparison, looser than a shift, =&&= then =^^= then =||= +(Python and Rust), so =x && mask == 0= tests the masked bits. Integers only, a +bool refused toward =and=/=or=/=not=; a dyn shift count outside 0..63 traps. +=~~= is one token, so a nested .fln unquote is =~(~x)=; the paren reader keeps +=~~x= as unquote twice and reads =^^= as a name. Rules out C's precedence. + ** DONE A form the prelude relies on is built in; a form only programs use is a macro CLOSED: [2026-09-25] =cond=, =when= and =dotimes= are special forms in parse.ml; =inc=, =++=, =into=, @@ -730,6 +738,9 @@ One spelling for one operation; != stays, and not= is refused with a suggestion of !=. * Checker +** TODO Checking a wide fold of let operands is slow +A 2000-operand (bit-and (let …) …) takes 32 s to check (37 s before the bit operators); +2000 plain names take 0.03 s. Something per operand is quadratic or worse. ** DONE A dyn value takes .field and [:key] CLOSED: [2026-09-26] diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el index 882c0801..833b97c9 100644 --- a/emacs/flan-fln-mode.el +++ b/emacs/flan-fln-mode.el @@ -77,7 +77,8 @@ fine here. Brackets and strings are still paired." ;; that starts with one, or follows a line that ends with one, continues the ;; line above. (defconst flan-fln--binops - '("or" "and" "==" "!=" "<" "<=" ">" ">=" "<<" ">>" "+" "-" "*" "/" "%")) + '("or" "and" "==" "!=" "<" "<=" ">" ">=" "||" "^^" "&&" "<<" ">>" "+" "-" "*" + "/" "%")) (defconst flan-fln--binop-re (regexp-opt flan-fln--binops)) diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index b7bfafbc..38426a06 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -154,7 +154,9 @@ face says.") (defconst flan--builtins '(;; arithmetic, comparison, bits "+" "-" "*" "/" "%" "=" "!=" "<" "<=" ">" ">=" "not" - "bit-and" "bit-or" "bit-xor" "<<" ">>" "min" "max" + "bit-and" "bit-or" "bit-xor" "bit-not" "&&" "||" "^^" "<<" ">>" + "rotate-left" "rotate-right" "popcount" "leading-zeros" "trailing-zeros" + "min" "max" ;; the fill patterns "zeroed" "filled" "dead-beef" ;; allocators diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el index 6af0b925..76d5fbae 100644 --- a/emacs/test-flan-fln.el +++ b/emacs/test-flan-fln.el @@ -188,6 +188,21 @@ fn step() -> () (test-flan-fln--is "and not the start of the body" (test-flan-fln--thing 'flan-fln-body) "grid[r, c] = 1")) +;; The bit operators continue a line as the other spaced operators do. +;; Not through `test-flan-fln--in', whose `|' marks point and would eat one +;; half of `||'. +(dolist (op '("&&" "||" "^^")) + (with-temp-buffer + (insert "x = a " op "\n b\ny = a\n " op " b\n") + (flan-fln-mode) + (goto-char (point-min)) + (forward-line 1) + (test-flan--check (concat "a line after a trailing " op " continues it") + (flan-fln--continuation-p (point))) + (forward-line 2) + (test-flan--check (concat "a line starting with " op " continues") + (flan-fln--continuation-p (point))))) + (test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "velocity[row, col] = 0.0") (test-flan-fln--is "a top-level form ends before trailing comment lines" (test-flan-fln--thing 'flan-fln-toplevel) diff --git a/lib/check.ml b/lib/check.ml index f9aecd2f..acbe507d 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -606,6 +606,9 @@ let builtin_names : string list ref = ref [] twenty thousand calls. Filled beside the list. *) let builtin_set : (string, unit) Hashtbl.t = Hashtbl.create 128 +(* Set while [bool_operands] asks an operand its type; see there. *) +let probing = ref false + (* ── builtin/, the reserved qualifier ────────────────────────────────── [builtin/length] is the builtin [length], whatever else the program has decided [length] means. It is the way out of the dead end shadowing used to leave: a @@ -677,12 +680,13 @@ let spell_arg stand_for (a : Ast.expr) = (* Operators other languages spell differently, each mapped to the Flan builtin that computes the same thing. Only exact equivalents: [mod] is left - out because Clojure's is floored and [%] is not. *) + out because Clojure's is floored and [%] is not. [&&] and [||] are not + here: they are the bit operators, and a bool reaching one is told which + logical operator it wanted there. *) let operator_aliases = [ ("not=", ("!=", "Not-equal")); ("=/=", ("!=", "Not-equal")); ("/=", ("!=", "Not-equal")); ("<>", ("!=", "Not-equal")); ("==", ("=", "Equality")); ("===", ("=", "Equality")); - ("&&", ("and", "Logical and")); ("||", ("or", "Logical or")); ("!", ("not", "Logical not")) ] (* The fix, as the sentence that ends the refusal. The reader's call is @@ -10200,7 +10204,9 @@ and not_numeric name what (a : Tast.expr) = | _ -> false in let where = a.Tast.loc in - if text then + if a.Tast.ty = Types.Bool && String.equal what "integers" then + bool_bits where name + else if text then fail where "%s takes %s, and this is %s — there is no %s on text. The prelude \ concatenates with concat and join" @@ -10320,14 +10326,16 @@ and dyn_fold ctx ~want loc name first rest = | "+" -> "flan_dyn_add" | "-" -> "flan_dyn_sub" | "*" -> "flan_dyn_mul" | "/" -> "flan_dyn_div" | "%" -> "flan_dyn_rem" - | _ -> - (* Bitwise and shift operators land here if they ever admit a dyn - operand. They do not: the runtime carries no bitwise entry points, - and an integer operation on a value that might be a float is not - something to guess at. *) - no_dyn_yet loc ~into:false Types.Dyn - (Printf.sprintf " — %s has no dyn form" name) + | _ -> dyn_bits_sym name in + (* A bitwise fold takes integers on both sides, and the typed side of a + mixed pair can be asked now rather than at run time. *) + let bitwise = not (List.mem name [ "+"; "-"; "*"; "/"; "%" ]) in + if bitwise then + List.iter + (fun (v : Tast.expr) -> + if v.Tast.ty <> Types.Dyn then bits_operand ctx v.Tast.loc name v) + first; (* The site travels with the operands. A dyn arithmetic trap is this language's type error, and until now it printed with no file, no line and no column — [here loc] is the same string literal [cast_dyn] hands the @@ -10338,12 +10346,100 @@ and dyn_fold ctx ~want loc name first rest = | [ a; b ] -> apply (box loc a) b | _ -> assert false in - let acc = - List.fold_left (fun acc arg -> apply acc (check ctx ~want:Types.Dyn arg)) - acc rest + let operand arg = + if bitwise then begin + let v = check ctx arg in + if v.Tast.ty <> Types.Dyn then bits_operand ctx v.Tast.loc name v; + v + end + else check ctx ~want:Types.Dyn arg in + let acc = List.fold_left (fun acc arg -> apply acc (operand arg)) acc rest in expect ctx loc ~want acc +(* The runtime's entry point for each bit operation on a dyn int. *) +and dyn_bits_sym name = + match name with + | "bit-and" -> "flan_dyn_bitand" | "bit-or" -> "flan_dyn_bitor" + | "bit-xor" -> "flan_dyn_bitxor" | "bit-not" -> "flan_dyn_bitnot" + | "<<" -> "flan_dyn_shl" | ">>" -> "flan_dyn_shr" + | "rotate-left" -> "flan_dyn_rotl" | "rotate-right" -> "flan_dyn_rotr" + | "popcount" -> "flan_dyn_popcount" | "leading-zeros" -> "flan_dyn_clz" + | "trailing-zeros" -> "flan_dyn_ctz" + | _ -> invalid_arg ("dyn_bits_sym " ^ name) + +(* An operand of a bit operation, once it is known not to be dyn: an integer, + or a type variable the where clause bounds by [integer?]. A bool is the + likeliest thing to arrive here — [a && b] is logical and in C — so it is + answered with the operator that does what was meant. *) +and bits_operand ctx loc name (v : Tast.expr) = + match v.Tast.ty with + | Types.Int _ -> () + | t when generic_ty t -> unconstrained ctx.env loc name ~needs:"integer?" t + | Types.Bool -> bool_bits v.Tast.loc name + | other -> fail loc "%s takes integers, found %s" name (tyname loc other) + +(* A bool operand is refused before the operands are joined, and not left to + [bits_operand]: the join sees a bool beside an integer as a plain mismatch, + "expected i32, found bool", which says nothing of [and]. Each operand's own + type is asked in a trial that is always abandoned, so the check leaves no + trace — no slot, no lifted lambda, no recorded refusal — and the real check + below is the only one that counts. A literal is never a bool, and a call to + an arithmetic or bit operator answers a number or a dyn, so neither is + asked. Nor is anything asked while a probe is running: the probe wants a + type, and asking again inside it would check a nest of these once per + level for every level above it, which doubles with each level. *) +and bool_operands ctx name (args : Ast.expr list) = + if not !probing then + let never_bool (a : Ast.expr) = + match a.Ast.e with + | Ast.Int _ | Ast.UInt _ | Ast.Float _ | Ast.Byte _ | Ast.Str _ | Ast.Kw _ -> + true + | Ast.Call ({ Ast.e = Ast.Var h; _ }, _) -> + List.mem h + [ "+"; "-"; "*"; "/"; "%"; "bit-and"; "bit-or"; "bit-xor"; "bit-not"; + "&&"; "||"; "^^"; "~~"; "<<"; ">>"; "rotate-left"; "rotate-right"; + "popcount"; "leading-zeros"; "trailing-zeros" ] + && not (shadows_builtin ctx a.Ast.loc h) + | _ -> false + in + let is_bool (a : Ast.expr) = + (not (never_bool a)) + && + let ty = ref None in + probing := true; + Fun.protect ~finally:(fun () -> probing := false) (fun () -> + ignore + (trial ctx (fun () -> + let v = check ctx a in + ty := Some v.Tast.ty; + Loc.failk "check/probe" a.Ast.loc "abandoned"))); + !ty = Some Types.Bool + in + List.iter (fun a -> if is_bool a then bool_bits a.Ast.loc name) args + +and bool_bits loc name = + let fln = fln_source loc in + let shown = + if not fln then name + else match name with + | "bit-and" -> "&&" | "bit-or" -> "||" | "bit-xor" -> "^^" + | "bit-not" -> "~~" | n -> n + in + let logic = + match name with + | "bit-and" -> Some (if fln then "a and b" else "(and a b)") + | "bit-or" -> Some (if fln then "a or b" else "(or a b)") + | "bit-xor" -> Some (if fln then "a != b" else "(!= a b)") + | "bit-not" -> Some (if fln then "not a" else "(not a)") + | _ -> None + in + Loc.failk "check/bits-of-bool" loc + "%s works on the bits of an integer, and this is a bool. %s" shown + (match logic with + | Some l -> Printf.sprintf "For true and false, write %s" l + | None -> "True and false are combined with and, or and not") + (* A comparison over three operands or more asks about more than one pair, and every operand is bound to a slot before any pair is looked at. That is what makes "left to right, exactly once" true of the lowering and not only of @@ -11238,8 +11334,33 @@ and named_call ?(qualified = false) ctx ~want loc name args = | _ -> Tast.BitXor in fold_arity loc name args; + bool_operands ctx name args; fold_left_prim ctx ~want loc name p ~needs:"integer?" Types.is_integer "integers" args + (* The .fln operators, which the indented reader already spells as the words + above; a form built some other way may still carry them. [~qualified] + skips the shadowing arm, because a program that means its own [&&] has + been answered by that arm already under this name. *) + | "&&" | "||" | "^^" | "~~" -> + let canon = match name with + | "&&" -> "bit-and" | "||" -> "bit-or" | "^^" -> "bit-xor" + | _ -> "bit-not" + in + named_call ~qualified:true ctx ~want loc canon args + | "bit-not" | "popcount" | "leading-zeros" | "trailing-zeros" -> + arity ctx loc name 1 args; + bool_operands ctx name args; + let v = check ctx ?want:(numeric_want want) (List.hd args) in + if v.Tast.ty = Types.Dyn then + expect ctx loc ~want (rt loc Types.Dyn (dyn_bits_sym name) [ v; here loc ]) + else begin + bits_operand ctx loc name v; + let p = match name with + | "bit-not" -> Tast.BitNot | "popcount" -> Tast.Popcount + | "leading-zeros" -> Tast.Clz | _ -> Tast.Ctz + in + prim p v.Tast.ty [ v ] + end (* The shifts stay at two, and not only because a shift chain reads badly: each count would be checked against the same width below, so (<< x 30 30) would pass two legal shifts and still shift the value away entirely. @@ -11251,22 +11372,32 @@ and named_call ?(qualified = false) ctx ~want loc name args = type and the width the shift wraps at would be taken from a number that is only saying how far, and the range check just below, along with [emit]'s mask, is keyed to the *value's* width. A count wider than the value is - refused and is told to write the cast. *) - | "<<" | ">>" -> - let p = if String.equal name "<<" then Tast.Shl else Tast.Shr in + refused and is told to write the cast. + + The rotations share the rule and not the range check: a rotation by the + width is the value unchanged, so every count means something and is taken + modulo the width. *) + | "<<" | ">>" | "rotate-left" | "rotate-right" -> + let p = match name with + | "<<" -> Tast.Shl | ">>" -> Tast.Shr | "rotate-left" -> Tast.Rotl + | _ -> Tast.Rotr + in arity ctx loc name 2 args; - let a, b = binary ctx ~join:false name loc ~want:(numeric_want want) args in - (match a.Tast.ty with - | Types.Int _ -> () - (* A type variable under {:where (integer? $t)}: every type the bound - admits has a width to shift within, so the abstract pass lets the - body through and each instantiation meets the concrete checks below - at its own width. Anything weaker — [numeric?] included — is refused - here, at the definition, because a shift at f32 means nothing. *) - | t when generic_ty t -> - unconstrained ctx.env loc name ~needs:"integer?" t - | other -> fail loc "%s takes integers, found %s" name - (tyname loc other)); + bool_operands ctx name args; + let a, b = + binary ctx ~dyn_ok:true ~join:false name loc ~want:(numeric_want want) args + in + if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then begin + (* The typed side of a mixed pair still has to be an integer: the dyn + half is asked at run time, and this half can be asked now. *) + List.iter + (fun (v : Tast.expr) -> + if v.Tast.ty <> Types.Dyn then bits_operand ctx v.Tast.loc name v) + [ a; b ]; + expect ctx loc ~want + (rt loc Types.Dyn (dyn_bits_sym name) [ box loc a; box loc b; here loc ]) + end else begin + bits_operand ctx loc name a; (* A shift by the operand's own width or more is poison in LLVM, which at -O2 turns the whole function into an undefined value rather than into a wrong number. A literal count is rejected here — that is the typo — and @@ -11281,6 +11412,7 @@ and named_call ?(qualified = false) ctx ~want loc name args = (tyname loc a.Tast.ty) (Types.bits k) | _ -> ()); prim p a.Tast.ty [ a; b ] + end (* (min a b) and (max a b) evaluate each operand once — hence the slots — because a min over two calls must not call either of them twice. @@ -14715,19 +14847,41 @@ let builtins : (string * string * string) list = ("not", "not [bool] bool", "Negates a bool. Nothing else in this language is a truth value."); ("bit-and", "bit-and [int ...] int", - "Bitwise and, folded left. Integers only; operands of different widths \ - meet at the wider one, the way + does."); - ("bit-or", "bit-or [int ...] int", "Bitwise or, folded left over integers."); + "Bitwise and, folded left; a && b in a .fln file. Integers only; operands \ + of different widths meet at the wider one, the way + does."); + ("bit-or", "bit-or [int ...] int", + "Bitwise or, folded left over integers; a || b in a .fln file."); ("bit-xor", "bit-xor [int ...] int", - "Bitwise exclusive or, folded left over integers."); + "Bitwise exclusive or, folded left over integers; a ^^ b in a .fln file."); + ("bit-not", "bit-not [int] int", + "Every bit of an integer flipped; ~~a in a .fln file."); + ("&&", "&& [int ...] int", "bit-and, by its .fln spelling."); + ("||", "|| [int ...] int", "bit-or, by its .fln spelling."); + ("^^", "^^ [int ...] int", "bit-xor, by its .fln spelling."); + ("~~", "~~ [int] int", "bit-not, by its .fln spelling."); ("<<", "<< [int int] int", "Left shift. The value's type decides — a narrower count widens to it, a \ wider one is refused — and a literal count at or past the value's width \ - is refused too, because LLVM calls that poison."); + is refused too. On a dyn int, a count outside 0 to 63 traps."); (">>", ">> [int int] int", - "Right shift. The value's type decides and the count widens to it, never \ - the reverse; a literal count at or past the width is refused, as it is \ - for <<."); + "Right shift, arithmetic on a signed type and logical on an unsigned one. \ + The value's type decides and the count widens to it; a literal count at \ + or past the width is refused, as it is for <<."); + ("rotate-left", "rotate-left [int int] int", + "The bits of the value moved left by the count, the ones that fall off \ + the top coming back in at the bottom. The count is taken modulo the \ + width."); + ("rotate-right", "rotate-right [int int] int", + "The bits of the value moved right by the count, wrapping round to the \ + top. The count is taken modulo the width."); + ("popcount", "popcount [int] int", + "How many bits of the integer are set. The answer has the operand's \ + type."); + ("leading-zeros", "leading-zeros [int] int", + "How many zero bits come before the highest set bit, counted within the \ + operand's width: the width itself for 0."); + ("trailing-zeros", "trailing-zeros [int] int", + "How many zero bits come after the lowest set bit: the width for 0."); ("min", "min [ordered? ...] ordered?", "The smallest of two or more operands, each of them evaluated exactly \ once however many there are. Two widths meet at the wider: (min i8-x \ diff --git a/lib/emit.ml b/lib/emit.ml index e003b8f4..d186a686 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -1478,6 +1478,7 @@ let settled_prim (p : Tast.prim) = | Tast.Add | Tast.Sub | Tast.Mul | Tast.Eq | Tast.Ne | Tast.Lt | Tast.Le | Tast.Gt | Tast.Ge | Tast.Not | Tast.BitAnd | Tast.BitOr | Tast.BitXor | Tast.Shl | Tast.Shr + | Tast.BitNot | Tast.Popcount | Tast.Clz | Tast.Ctz | Tast.Rotl | Tast.Rotr (* Questions about a value's shape, answered from the layout tables. *) | Tast.Len | Tast.SizeOf _ | Tast.AlignOf _ | Tast.AddrOf -> true (* Everything else reaches C, signals, or both: an index and a slice are @@ -3873,6 +3874,33 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) = let t = fresh f in ins f "%s = xor i1 %s, true" t a; t + | Tast.BitNot, [ x ] -> + let a = value f x in + let t = fresh f in + ins f "%s = xor %s %s, -1" t (ll x.Tast.ty) a; + t + (* [i1 false] says a zero operand is defined — the width — rather than + poison, which is the language's answer for 0. *) + | (Tast.Popcount | Tast.Clz | Tast.Ctz), [ x ] -> + let a = value f x in + let ty = ll x.Tast.ty in + let t = fresh f in + (match p with + | Tast.Popcount -> ins f "%s = call %s @llvm.ctpop.%s(%s %s)" t ty ty ty a + | Tast.Clz -> + ins f "%s = call %s @llvm.ctlz.%s(%s %s, i1 false)" t ty ty ty a + | _ -> ins f "%s = call %s @llvm.cttz.%s(%s %s, i1 false)" t ty ty ty a); + t + (* A funnel shift of a value with itself is a rotation, and the funnel + shifts take their count modulo the width, which is the rotation's rule. *) + | (Tast.Rotl | Tast.Rotr), [ x; y ] -> + let a = value f x in + let b = value f y in + let ty = ll x.Tast.ty in + let t = fresh f in + ins f "%s = call %s @llvm.%s.%s(%s %s, %s %s, %s %s)" t ty + (if p = Tast.Rotl then "fshl" else "fshr") ty ty a ty a ty b; + t | Tast.Len, [ x ] -> (match x.Tast.ty with | Types.Array (n, _) -> Int64.to_string n @@ -4931,6 +4959,26 @@ let header = {|; Generated by flan. The layout is C's: no object headers anywher declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1 immarg) declare i32 @llvm.bswap.i32(i32) +declare i8 @llvm.ctpop.i8(i8) +declare i8 @llvm.ctlz.i8(i8, i1 immarg) +declare i8 @llvm.cttz.i8(i8, i1 immarg) +declare i8 @llvm.fshl.i8(i8, i8, i8) +declare i8 @llvm.fshr.i8(i8, i8, i8) +declare i16 @llvm.ctpop.i16(i16) +declare i16 @llvm.ctlz.i16(i16, i1 immarg) +declare i16 @llvm.cttz.i16(i16, i1 immarg) +declare i16 @llvm.fshl.i16(i16, i16, i16) +declare i16 @llvm.fshr.i16(i16, i16, i16) +declare i32 @llvm.ctpop.i32(i32) +declare i32 @llvm.ctlz.i32(i32, i1 immarg) +declare i32 @llvm.cttz.i32(i32, i1 immarg) +declare i32 @llvm.fshl.i32(i32, i32, i32) +declare i32 @llvm.fshr.i32(i32, i32, i32) +declare i64 @llvm.ctpop.i64(i64) +declare i64 @llvm.ctlz.i64(i64, i1 immarg) +declare i64 @llvm.cttz.i64(i64, i1 immarg) +declare i64 @llvm.fshl.i64(i64, i64, i64) +declare i64 @llvm.fshr.i64(i64, i64, i64) declare ptr @llvm.frameaddress.p0(i32 immarg) declare void @flan_rt_init(i32, ptr) declare void @flan_argv(ptr) @@ -5035,6 +5083,17 @@ declare i64 @flan_dyn_mul(i64, i64, ptr, i64) declare i64 @flan_dyn_div(i64, i64, ptr, i64) declare i64 @flan_dyn_rem(i64, i64, ptr, i64) declare i64 @flan_dyn_neg(i64, ptr, i64) +declare i64 @flan_dyn_bitand(i64, i64, ptr, i64) +declare i64 @flan_dyn_bitor(i64, i64, ptr, i64) +declare i64 @flan_dyn_bitxor(i64, i64, ptr, i64) +declare i64 @flan_dyn_bitnot(i64, ptr, i64) +declare i64 @flan_dyn_shl(i64, i64, ptr, i64) +declare i64 @flan_dyn_shr(i64, i64, ptr, i64) +declare i64 @flan_dyn_rotl(i64, i64, ptr, i64) +declare i64 @flan_dyn_rotr(i64, i64, ptr, i64) +declare i64 @flan_dyn_popcount(i64, ptr, i64) +declare i64 @flan_dyn_clz(i64, ptr, i64) +declare i64 @flan_dyn_ctz(i64, ptr, i64) declare i64 @flan_dyn_lt(i64, i64, ptr, i64) declare i64 @flan_dyn_le(i64, i64, ptr, i64) declare i64 @flan_dyn_gt(i64, i64, ptr, i64) diff --git a/lib/indent_printer.ml b/lib/indent_printer.ml index 8c991e88..26e1c403 100644 --- a/lib/indent_printer.ml +++ b/lib/indent_printer.ml @@ -296,36 +296,42 @@ let flatten (f : Form.t) (rest : Form.t list) = (* ── Expressions ───────────────────────────────────────────────────── *) -(* Text and syntactic level, the same scale [Indent_reader] reads: 10 an atom - or bracket, 9 a postfix chain, 8 a unary minus, 1-7 binary, 3 [not], 0 a - one-line [if] or a lambda. *) +(* Text and syntactic level, the same scale [Indent_reader] reads: 13 an atom + or bracket, 12 a postfix chain, 11 a prefix [-] or [~~], 1-10 binary, 3 + [not], 0 a one-line [if] or a lambda. *) +(* The operator a head prints as: [=] is [==], and the bit words are the + operators the reader turns into them. *) +let infix_op = function + | "=" -> "==" | "bit-and" -> "&&" | "bit-or" -> "||" | "bit-xor" -> "^^" + | s -> s + let rec expr (f : Form.t) : string * int = match f.v with | Form.Sym s when !hole && s = hole_sym -> (s, 0) | Form.Sym s -> sym f s | Form.Kw k -> - if kw_ok k then (":" ^ k, 10) else unprintable f "a keyword with no spelling" + if kw_ok k then (":" ^ k, 13) else unprintable f "a keyword with no spelling" | Form.Int i -> let t = Option.value (!spelling f) ~default:(Int64.to_string i) in - (t, if t.[0] = '-' then 8 else 10) - | Form.UInt (_, s) -> (s, 10) + (t, if t.[0] = '-' then 11 else 13) + | Form.UInt (_, s) -> (s, 13) | Form.Float x -> let s = Option.value (!spelling f) ~default:(Form.float_repr x) in if not (Reader.is_digit s.[0] || (s.[0] = '-' && String.length s > 1 && Reader.is_digit s.[1])) then unprintable f "a float with no literal"; - (s, if s.[0] = '-' then 8 else 10) - | Form.Str s -> ("\"" ^ Form.escape s ^ "\"", 10) - | Form.Byte b -> (Form.byte_repr b, 10) - | Form.Vec xs -> ("[" ^ vec_text xs ^ "]", 10) - | Form.Map xs -> ("{" ^ map_text xs ^ "}", 10) - | Form.List [] -> ("()", 10) + (s, if s.[0] = '-' then 11 else 13) + | Form.Str s -> ("\"" ^ Form.escape s ^ "\"", 13) + | Form.Byte b -> (Form.byte_repr b, 13) + | Form.Vec xs -> ("[" ^ vec_text xs ^ "]", 13) + | Form.Map xs -> ("{" ^ map_text xs ^ "}", 13) + | Form.List [] -> ("()", 13) | Form.List (h :: args) -> in_quasi f (fun () -> list f h args) and sym f s = if s = "==" then unprintable f "the name == (it reads as =)" - else if R.is_op_word s || s = "if" then (paren s, 10) - else if name_ok s then (s, 10) + else if R.is_op_word s || s = "if" then (paren s, 13) + else if name_ok s then (s, 13) else unprintable f (Printf.sprintf "the name %s" s) and at lvl f = @@ -347,16 +353,16 @@ and commas xs = String.concat ", " (comma_items xs) as soon as one element has an operator in it. *) and vec_text xs = let ts = List.map expr xs in - if List.for_all (fun (_, l) -> l >= 8) ts then String.concat " " (List.map fst ts) + if List.for_all (fun (_, l) -> l >= 11) ts then String.concat " " (List.map fst ts) else String.concat ", " (List.map (fun (t, _) -> t) ts) and map_text xs = let ts = List.map expr xs in - if List.for_all (fun (_, l) -> l >= 8) ts then String.concat " " (List.map fst ts) + if List.for_all (fun (_, l) -> l >= 11) ts then String.concat " " (List.map fst ts) else let rec pairs = function | (k, kl) :: (v, _) :: rest -> - ((if kl < 8 then paren k else k) ^ " " ^ v) :: pairs rest + ((if kl < 11 then paren k else k) ^ " " ^ v) :: pairs rest | [ (k, _) ] -> [ k ] | [] -> [] in @@ -367,22 +373,26 @@ and head_text (h : Form.t) = | Form.Sym "==" -> unprintable h "the name ==" | Form.Sym s when R.is_op_word s -> s | Form.Sym s -> fst (sym h s) - | _ -> at 9 h + | _ -> at 12 h and list f h args = - let call () = (head_text h ^ "(" ^ commas args ^ ")", 9) in + let call () = (head_text h ^ "(" ^ commas args ^ ")", 12) in match h.v, args with - | Form.Sym "quote", [ x ] -> ("'" ^ Form.to_source x, 10) - | Form.Sym "unquote", [ x ] -> ("~" ^ at 10 x, 10) - | Form.Sym "unquote-splicing", [ x ] -> ("~@" ^ at 10 x, 10) + | Form.Sym "quote", [ x ] -> ("'" ^ Form.to_source x, 13) + (* [~~] is bit-not, so an unquote of anything that starts with [~] is + parenthesised: [~(~x)]. *) + | Form.Sym "unquote", [ x ] -> + let t = at 13 x in + ((if t <> "" && t.[0] = '~' then "~(" ^ t ^ ")" else "~" ^ t), 13) + | Form.Sym "unquote-splicing", [ x ] -> ("~@" ^ at 13 x, 13) | Form.Sym s, _ :: _ :: _ - when (R.is_binop s || s = "=") && s <> "==" && not (s = "!=" && List.length args > 2) -> - let op = if s = "=" then "==" else s in + when R.is_binop (infix_op s) && s <> "==" && not (s = "!=" && List.length args > 2) -> + let op = infix_op s in let lvl = Option.get (R.binop_level op) in let first = List.hd args and rest = List.tl args in let ft, fl = expr first in let same = match first.v with - | Form.List (h' :: _ :: _ :: _) -> is_sym s h' || lvl = 4 + | Form.List (h' :: _ :: _ :: _) -> (match h'.v with Form.Sym s' -> infix_op s' = op | _ -> false) || lvl = 4 | _ -> false in let ft = if fl < lvl || (fl = lvl && same) then paren ft else ft in @@ -398,18 +408,19 @@ and list f h args = (ft :: List.map (fun x -> and_in_or x (at (lvl + 1) x)) rest), lvl) | Form.Sym "-", [ x ] -> let t, l = expr x in - if l >= 9 && t <> "" && R.is_neg_char t.[0] then ("-" ^ t, 8) - else ("-(" ^ at 0 x ^ ")", 9) + if l >= 12 && t <> "" && R.is_neg_char t.[0] then ("-" ^ t, 11) + else ("-(" ^ at 0 x ^ ")", 12) | Form.Sym "not", [ x ] -> ("not " ^ at 3 x, 3) + | Form.Sym ("bit-not" | "~~"), [ x ] -> ("~~" ^ at 11 x, 11) (* [and] or [or] of one value is that value. *) | Form.Sym ("and" | "or"), [ x ] when !quasi = 0 -> expr x - | Form.Sym "at", t :: (_ :: _ as idx) -> (at 9 t ^ "[" ^ commas idx ^ "]", 9) + | Form.Sym "at", t :: (_ :: _ as idx) -> (at 12 t ^ "[" ^ commas idx ^ "]", 12) | Form.Sym s, [ t ] when String.length s > 1 && s.[0] = '.' && name_ok s && not (String.contains (String.sub s 1 (String.length s - 1)) '.') -> let tt, tl = expr t in let glued = - tl >= 9 + tl >= 12 && (match t.v with | Form.Byte _ -> false | Form.Sym x -> name_ok x && not (String.contains x '.') && not (R.capitalised x) @@ -421,9 +432,9 @@ and list f h args = let c = tt.[String.length tt - 1] in c = ')' || c = ']' || c = '}' || c = '"') in - if glued then (tt ^ s, 9) else call () + if glued then (tt ^ s, 12) else call () | Form.Sym s, [ ({ v = Form.Map _; _ } as m) ] when name_ok s && R.capitalised s -> - (s ^ fst (expr m), 9) + (s ^ fst (expr m), 12) | Form.Sym "the", _ when (match typed_lambda f with Some (_, [ _ ]) -> true | _ -> false) -> (match typed_lambda f with | Some (head, [ body ]) -> (head ^ " => " ^ unit_text body, 0) @@ -442,7 +453,7 @@ and list f h args = (* [if let P = v], the head (if-let [P v] ...) is written with. *) and if_let_head (hd : Form.t) = match hd.v with - | Form.Vec [ pat; v ] -> "if let " ^ at 8 pat ^ " = " ^ at 1 v + | Form.Vec [ pat; v ] -> "if let " ^ at 11 pat ^ " = " ^ at 1 v | _ -> assert false (* A one-line slot's text — an arm's value, a then or an else, what follows @@ -462,7 +473,7 @@ and inline_text ?(lvl = 0) (f : Form.t) = | Form.List [ { v = Form.Sym "set"; _ }; t; v ] -> assign_text ~lvl t v | Form.List [ { v = Form.Sym "update"; _ }; t; { v = Form.Sym (("+" | "-" | "*" | "/") as op); _ }; w ] when not (R.simple_place t) -> - at 9 t ^ " " ^ op ^ "= " ^ at (max lvl 1) w + at 12 t ^ " " ^ op ^ "= " ^ at (max lvl 1) w | _ -> at lvl f (* A body after [=]: [()] there reads as [(do)]. *) @@ -474,7 +485,7 @@ and unit_text (f : Form.t) = (* [t = v], or [t += w] when [v] is [(+ t w)]. *) and assign_text ?(lvl = 0) t v = - let tt = at 9 t in + let tt = at 12 t in match v.v with | Form.List [ { v = Form.Sym (("+" | "-" | "*" | "/") as op); _ }; a; w ] when same a t && R.simple_place t -> @@ -491,7 +502,7 @@ and typed_lambda (f : Form.t) = match t.v with | Form.List [ { v = Form.Sym (("Fn" | "CFn") as h); _ }; { v = Form.Vec ps; _ }; r ] -> h ^ "(" ^ String.concat ", " (List.map tyt ps) ^ ") -> " ^ tyt r - | _ -> at 9 t + | _ -> at 12 t in match f.v with | Form.List [ { v = Form.Sym "the"; _ }; @@ -510,7 +521,7 @@ let rec ty (f : Form.t) = match f.v with | Form.List [ { v = Form.Sym (("Fn" | "CFn") as h); _ }; { v = Form.Vec ps; _ }; r ] -> h ^ "(" ^ commas ps ^ ") -> " ^ ty r - | _ -> at 9 f + | _ -> at 12 f (* A [defn]'s parameter type the reader could not mistake for a name: a primitive, a capitalised or [$] name, or a bracket. [[x y]] with a @@ -750,7 +761,9 @@ and wrapped n prefix (f : Form.t) = match f.v with | Form.List (h :: (_ :: _ as args)) when (match h.v with | Form.Sym ("at" | "quote" | "unquote" | "unquote-splicing") -> false - | Form.Sym s -> not (R.is_op_word s) && not (String.length s > 1 && s.[0] = '.') + | Form.Sym s -> + not (R.is_op_word (infix_op s)) && s <> "bit-not" + && not (String.length s > 1 && s.[0] = '.') | _ -> false) -> let open_ = prefix ^ head_text h ^ "(" in let col = n + String.length open_ in @@ -782,7 +795,7 @@ and wrapped n prefix (f : Form.t) = let open_ = prefix ^ "[" in let col = n + String.length open_ in let ts = List.map expr xs in - let sep = if List.for_all (fun (_, l) -> l >= 8) ts then "" else "," in + let sep = if List.for_all (fun (_, l) -> l >= 11) ts then "" else "," in let rec go line acc = function | [] -> List.rev ((line ^ "]") :: acc) | (t, _) :: rest -> @@ -963,9 +976,9 @@ and sugar n (f : Form.t) : string list option = Some [ i ^ guard (inline_text f) ] | Form.List [ { v = Form.Sym "set"; _ }; t; v ] -> let line = i ^ guard (assign_text t v) in - if String.length line <= width && lambda_value n (guard (at 9 t)) v = None + if String.length line <= width && lambda_value n (guard (at 12 t)) v = None then Some [ line ] - else Some (value_lines n (guard (at 9 t)) v) + else Some (value_lines n (guard (at 12 t)) v) | Form.List [ { v = Form.Sym "if"; _ }; c; a; b ] -> let simple (x : Form.t) = match x.v with @@ -1102,7 +1115,7 @@ and sugar n (f : Form.t) : string list option = :: List.concat_map (fun ((pat : Form.t), body) -> List.mapi (fun k l -> if k = 0 then Source_text.tag pat.loc.Loc.line l else l) @@ - let pt = at 8 pat in + let pt = at 11 pat in let line = ind (n + 2) ^ pt ^ " -> " ^ inline_text body in match body.v with | Form.List ({ v = Form.Sym "do"; _ } :: _ :: _ :: _) -> @@ -1305,7 +1318,7 @@ and sugar n (f : Form.t) : string list option = Some ("(" ^ fst (expr p0) ^ ": " ^ ty key ^ String.concat "" (List.map (fun p -> ", " ^ fst (expr p)) rest) ^ ")") | (Form.Kw _ | Form.Str _ | Form.Int _ | Form.Sym _), _ -> - Some ("(" ^ commas ps ^ ") when " ^ at 9 key) + Some ("(" ^ commas ps ^ ") when " ^ at 12 key) | _ -> None in Option.map (fun h -> fn_like n f (i ^ "method " ^ name ^ h) body) head @@ -1370,7 +1383,7 @@ and handler_clauses n cls = match c.v with | Form.List (t :: { v = Form.Vec [ { v = Form.Sym v; _ } ]; _ } :: (_ :: _ as b)) when def_name v -> - Some ((ind n ^ "on " ^ at 9 t ^ "(" ^ v ^ ")") :: block (n + 2) b) + Some ((ind n ^ "on " ^ at 12 t ^ "(" ^ v ^ ")") :: block (n + 2) b) | _ -> None in let cs = List.map clause cls in @@ -1385,7 +1398,7 @@ and let_lines n prs body = | Form.Sym x, Form.List [ { v = Form.Sym "the"; _ }; ty_; w ] when def_name x && typed_lambda v = None -> ("let " ^ x ^ ": " ^ ty ty_, w) - | _ -> ("let " ^ guard (at 8 t), v) + | _ -> ("let " ^ guard (at 11 t), v) in (* Each binding line carries its own source line, so a comment written after a binding stays on it. *) diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index d2b68438..bc578472 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -23,6 +23,7 @@ type tok = | COMMA | COLON (* x: T, and the trailing : of a call's block *) | UNQ | SPLICE (* ~ and ~@ *) + | BNOT (* ~~, bit-not; a nested unquote is ~(~x) *) | NEG (* the - glued to the front of a name *) | NEWLINE | INDENT | DEDENT | EOF @@ -36,7 +37,7 @@ let show = function | ATOM v -> Form.to_source (Form.make v Loc.unknown) | DATUM f -> Form.to_source f | LP -> "(" | RP -> ")" | LB -> "[" | RB -> "]" | LC -> "{" | RC -> "}" - | COMMA -> "," | COLON -> ":" | UNQ -> "~" | SPLICE -> "~@" | NEG -> "-" + | COMMA -> "," | COLON -> ":" | UNQ -> "~" | SPLICE -> "~@" | BNOT -> "~~" | NEG -> "-" | NEWLINE -> "the end of the line" | INDENT -> "an indented line" | DEDENT -> "the end of the block" @@ -45,17 +46,23 @@ let show = function (* ── Names ─────────────────────────────────────────────────────────── *) (* Binary operators and their levels, low to high (spec §2 "Precedence"). - [not] sits at 3 and unary minus at 8; neither is binary. *) + [not] sits at 3 and the prefix [-] and [~~] at 11; neither is binary. The + bit operators sit between the comparisons and the shifts, Python's and + Rust's order, so [x && mask == 0] is [(x && mask) == 0]. *) let binops = [ ("or", 1); ("and", 2); ("==", 4); ("!=", 4); ("<", 4); ("<=", 4); (">", 4); (">=", 4); - ("<<", 5); (">>", 5); ("+", 6); ("-", 6); ("*", 7); ("/", 7); ("%", 7) ] + ("||", 5); ("^^", 6); ("&&", 7); + ("<<", 8); (">>", 8); ("+", 9); ("-", 9); ("*", 10); ("/", 10); ("%", 10) ] let binop_level s = List.assoc_opt s binops let is_binop s = binop_level s <> None -(* [==] is Flan's [=]; every other operator is its own name. *) -let op_sym = function "==" -> "=" | s -> s +(* [==] is Flan's [=], and the bit operators are the words the Lisp side + writes; every other operator is its own name. *) +let op_sym = function + | "==" -> "=" | "&&" -> "bit-and" | "||" -> "bit-or" | "^^" -> "bit-xor" + | s -> s (* Words that are operators rather than names wherever a value is read. Alone before a comma or a closer they are the symbol itself, [reduce(+, 0, xs)]; @@ -185,7 +192,11 @@ let lex ?(line = 1) ?(col = 1) ~file src : token list = indented block, or quasiquote(x) on one line" | '~' -> Reader.advance st; - if Reader.peek st = '@' then begin + if Reader.peek st = '~' then begin + Reader.advance st; + emit BNOT (Loc.upto l0 (Reader.here st)) + end + else if Reader.peek st = '@' then begin Reader.advance st; emit SPLICE (Loc.upto l0 (Reader.here st)) end @@ -521,7 +532,7 @@ let where_ p = | _ -> t.loc let starts_value = function - | NAME _ | KW _ | ATOM _ | DATUM _ | LP | LB | LC | UNQ | SPLICE | NEG -> true + | NAME _ | KW _ | ATOM _ | DATUM _ | LP | LB | LC | UNQ | SPLICE | BNOT | NEG -> true | _ -> false let ends_value = function @@ -654,11 +665,6 @@ let refuse_ws ?(brace = false) loc e = (if brace then "entries" else "elements") (if brace then "{.x a + 1, .y 2}" else "[a - 1, b]") -(* Expressions come back with their syntactic level: 10 an atom or a bracket, - 9 a postfix chain, 8 a unary minus, 1-7 a binary operator's level, 3 a - [not], 0 a one-line [if] or a lambda. Anything under 8 is "compound": it - has an operator at its top, so it cannot sit in a list separated only by - whitespace. *) (* [loop] and [recur] are Lisp-syntax forms. A .fln loop is a [while], [until], [dotimes] or [for]; [read_all] refuses any that gets past the parser, in a [quote] or a quoted datum too. *) @@ -677,11 +683,16 @@ let when_else p = and None when it does not. For two branches write if c then a else b, \ or an if with an else block" +(* Expressions come back with their syntactic level: 13 an atom or a bracket, + 12 a postfix chain, 11 a prefix [-] or [~~], 1-10 a binary operator's + level, 3 a [not], 0 a one-line [if] or a lambda. Anything under 11 is + "compound": it has an operator at its top, so it cannot sit in a list + separated only by whitespace. *) let rec expr p : Form.t * int = binary p 1 and binary p lvl : Form.t * int = if lvl = 3 then not_ p - else if lvl > 7 then unary p + else if lvl > 10 then unary p else let l0 = (peek p).loc in let ((first, _) as fst_) = binary p (lvl + 1) in @@ -745,7 +756,11 @@ and unary p = | NEG -> ignore (advance p); let x, _ = postfix p in - (mk p t.loc (Form.List [ sym t.loc "-"; x ]), 8) + (mk p t.loc (Form.List [ sym t.loc "-"; x ]), 11) + | BNOT -> + ignore (advance p); + let x, _ = unary p in + (mk p t.loc (Form.List [ sym t.loc "bit-not"; x ]), 11) | _ -> postfix p and postfix p = @@ -758,18 +773,18 @@ and postfix p = | LP -> ignore (advance p); let args = items p RP t.loc ~what:"arguments" in - loop (mk p l0 (Form.List (f :: args)), 9) + loop (mk p l0 (Form.List (f :: args)), 12) | LB -> ignore (advance p); let idx = items p RB t.loc ~what:"indices" ~head:(text_of f) in - loop (mk p l0 (Form.List (sym t.loc "at" :: f :: idx)), 9) + loop (mk p l0 (Form.List (sym t.loc "at" :: f :: idx)), 12) | NAME s when String.length s > 1 && s.[0] = '.' -> ignore (advance p); - loop (mk p l0 (Form.List [ sym t.loc s; f ]), 9) + loop (mk p l0 (Form.List [ sym t.loc s; f ]), 12) | LC -> ignore (advance p); let m = map_items p t.loc in - loop (mk p l0 (Form.List [ f; Form.make (Form.Map m) (span p t.loc) ]), 9) + loop (mk p l0 (Form.List [ f; Form.make (Form.Map m) (span p t.loc) ]), 12) | _ -> fp in loop (primary p) @@ -801,7 +816,7 @@ and primary p : Form.t * int = else if is_op_word s then begin if glued_lp || ends_value nxt.tok then begin ignore (advance p); - (sym l0 (op_sym s), 10) + (sym l0 (op_sym s), 13) end else failk "operator-operand" l0 @@ -813,18 +828,18 @@ and primary p : Form.t * int = else begin ignore (advance p); check_name t s; - (sym l0 s, 10) + (sym l0 s, 13) end - | KW k -> ignore (advance p); (Form.make (Form.Kw k) l0, 10) + | KW k -> ignore (advance p); (Form.make (Form.Kw k) l0, 13) | ATOM v -> ignore (advance p); - (Form.make v l0, if negative_literal t.tok then 8 else 10) - | DATUM f -> ignore (advance p); (f, 10) + (Form.make v l0, if negative_literal t.tok then 11 else 13) + | DATUM f -> ignore (advance p); (f, 13) | LP -> ignore (advance p); if (peek p).tok = RP then begin ignore (advance p); - (mk p l0 (Form.List []), 10) + (mk p l0 (Form.List []), 13) end else let e, _ = expr p in @@ -837,21 +852,21 @@ and primary p : Form.t * int = Several values in a list are written in brackets, [a, b]; \ arguments go glued to a name, f(a, b)" | _ -> stray p ~after:(text_of e)); - (e, 10) + (e, 13) | LB -> ignore (advance p); let xs = vec_items p l0 in - (mk p l0 (Form.Vec xs), 10) + (mk p l0 (Form.Vec xs), 13) | LC -> ignore (advance p); let xs = map_items p l0 in - (mk p l0 (Form.Map xs), 10) + (mk p l0 (Form.Map xs), 13) | UNQ | SPLICE -> ignore (advance p); let x, _ = primary p in let name = if t.tok = UNQ then "unquote" else "unquote-splicing" in - (mk p l0 (Form.List [ sym l0 name; x ]), 10) - | NEG -> unary p + (mk p l0 (Form.List [ sym l0 name; x ]), 13) + | NEG | BNOT -> unary p | tk -> failk "expected-value" (where_ p) "expected a value here, and found %s" (show tk) @@ -998,7 +1013,7 @@ and fn_expr p = is what follows. *) | tk when names && n.loc.Loc.line > rp.loc.Loc.eline && starts_value tk -> lambda_arrow n.loc (header ()) - | _ -> (mk p t.loc (Form.List (sym t.loc "fn" :: args)), 9) + | _ -> (mk p t.loc (Form.List (sym t.loc "fn" :: args)), 12) (* What follows a lambda's [=>]: a value on the line, or the indented block under it. [header] is the lambda's header as written, for a message. *) @@ -1142,7 +1157,7 @@ and vec_items p open_loc = | EOF -> unclosed p '[' open_loc | _ -> let e, lvl = expr p in - if lvl < 8 && prev_ws then refuse_ws t.loc e; + if lvl < 11 && prev_ws then refuse_ws t.loc e; (match (peek p).tok with | COMMA -> if !spaces then mixed (peek p).loc; @@ -1151,7 +1166,7 @@ and vec_items p open_loc = | RB -> ignore (advance p); List.rev (e :: acc) | EOF -> unclosed p '[' open_loc | tk when starts_value tk && (peek p).sp -> - if lvl < 8 then refuse_ws t.loc e; + if lvl < 11 then refuse_ws t.loc e; if !commas then mixed (peek p).loc; spaces := true; go (e :: acc) true @@ -1182,7 +1197,7 @@ and map_items p open_loc = and no %s between it and the value" n (if tk = COLON then "colon" else "= sign") | tk when starts_value tk && (peek p).sp -> - if lvl < 8 then refuse_ws ~brace:true t.loc e; + if lvl < 11 then refuse_ws ~brace:true t.loc e; go (e :: acc) | _ -> stray p ~after:(text_of e)) in diff --git a/lib/js.ml b/lib/js.ml index 43d3af0c..7ab9f151 100644 --- a/lib/js.ml +++ b/lib/js.ml @@ -889,6 +889,12 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) = | Tast.Not, [ x ] -> Printf.sprintf "(!%s)" (value f x) | (Tast.BitAnd | Tast.BitOr | Tast.BitXor | Tast.Shl | Tast.Shr), [ x; y ] -> bitwise f e p x y + | Tast.BitNot, [ x ] -> ( + match x.Tast.ty with + | Types.Int k -> norm k (Printf.sprintf "~(%s)" (value f x)) + | t -> at loc "a bitwise operation on %s" (Types.to_string t)) + | (Tast.Popcount | Tast.Clz | Tast.Ctz | Tast.Rotl | Tast.Rotr), _ -> + at loc "the bit counts and rotations are not in the JS dialect" | Tast.Len, [ x ] -> ( match x.Tast.ty with | Types.Array (n, _) -> Printf.sprintf "%Ld" n diff --git a/lib/reader.ml b/lib/reader.ml index 40890424..6e50b465 100644 --- a/lib/reader.ml +++ b/lib/reader.ml @@ -209,6 +209,9 @@ let rec read_form st = advance st; if peek st = '@' then (advance st; read_wrapped st loc "unquote-splicing") else read_wrapped st loc "unquote" + (* [^^] is bit-xor's other name, and metadata on a form that starts with + [^] would mean nothing, so the two cannot collide. *) + | '^' when peek2 st = '^' -> read_symbol_or_keyword st | '^' -> Loc.failk "reader/metadata" loc "metadata (^) is not supported yet" diff --git a/lib/tast.ml b/lib/tast.ml index be09a3a5..e297c882 100644 --- a/lib/tast.ml +++ b/lib/tast.ml @@ -23,6 +23,10 @@ type prim = (* bitwise, integers only. [Shr] is arithmetic on a signed type and logical on an unsigned one, which is what the operand's own kind already says. *) | BitAnd | BitOr | BitXor | Shl | Shr + (* One operand each, and the answer has the operand's type. [Clz] and [Ctz] + answer the width for zero. [Rotl] and [Rotr] take the count modulo the + width, so no count is out of range. *) + | BitNot | Popcount | Clz | Ctz | Rotl | Rotr (* containers: fixed arrays and slices only at milestone 2 *) | Len | At | Slice (* (slice-from p n): a [T] made out of a (Ptr T) and a length the caller diff --git a/lib/x86.ml b/lib/x86.ml index 8a512401..78119d26 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -386,6 +386,32 @@ let shift_cl b ~ext ~dst = rex b ~w:true ~r:0 ~x:0 ~m:dst; u8 b 0xd3; modrm_r b let shl_cl b ~dst = shift_cl b ~ext:4 ~dst let shr_cl b ~dst = shift_cl b ~ext:5 ~dst let sar_cl b ~dst = shift_cl b ~ext:7 ~dst +let shift_imm b ~ext ~dst n = + rex b ~w:true ~r:0 ~x:0 ~m:dst; u8 b 0xc1; modrm_r b ~r:ext ~m:dst; u8 b n + +(* bsf (0xbc) and bsr (0xbd): the index of the lowest or highest set bit, with + ZF set and the destination undefined when the source is zero. Both are in + every x86-64 CPU, which tzcnt, lzcnt and popcnt are not. *) +let bitscan b ~op ~dst ~src = + rex b ~w:true ~r:dst ~x:0 ~m:src; u8 b 0x0f; u8 b op; modrm_r b ~r:dst ~m:src + +let cmovz_rr b ~dst ~src = + rex b ~w:true ~r:dst ~x:0 ~m:src; u8 b 0x0f; u8 b 0x44; modrm_r b ~r:dst ~m:src + +(* rol (ext 0) and ror (ext 1) by cl at the operand's own width, unlike the + shifts above: a rotation at 64 bits of a value that is 8 wide would bring + the wrong bits round. The hardware masks cl to 5 bits (6 at 64) and then + rotates modulo the width, which is the language's rule for every width. *) +let rot_cl b ~ext ~bits ~dst = + match bits with + | 8 -> + rex ~force:(dst >= 4) b ~w:false ~r:0 ~x:0 ~m:dst; u8 b 0xd2; + modrm_r b ~r:ext ~m:dst + | 16 -> + u8 b 0x66; rex b ~w:false ~r:0 ~x:0 ~m:dst; u8 b 0xd3; + modrm_r b ~r:ext ~m:dst + | 32 -> rex b ~w:false ~r:0 ~x:0 ~m:dst; u8 b 0xd3; modrm_r b ~r:ext ~m:dst + | _ -> rex b ~w:true ~r:0 ~x:0 ~m:dst; u8 b 0xd3; modrm_r b ~r:ext ~m:dst let setcc b ~cc ~dst = rex ~force:(dst >= 4) b ~w:false ~r:0 ~x:0 ~m:dst; @@ -3402,7 +3428,66 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) dst = end; movzx8 f.b ~dst:rax ~src:rax; store_loc f ~reg:rax dst Types.Bool - | Tast.Not, [ a ] -> + (* Zero-extended to 64 bits first, so a negative i8 counts eight bits and + not sixty-four. Every sequence below is baseline x86-64, the target LLVM + is given too: popcount is the SWAR sum LLVM writes for [ctpop] without + popcnt, and the two scans answer the width for zero through a cmov on the + flag bsr and bsf set. *) + | (Tast.Popcount | Tast.Clz | Tast.Ctz), [ a ] -> + let la = eval f a in + let w = + match a.Tast.ty with + | Types.Int k -> Types.bits k + | t -> unsupported "a bit count of %s" (Types.to_string t) + in + load_int f.b ~dst:rax ~mm:(lmem f la ~scratch:r11) ~size:(w / 8) + ~signed:false; + (match p with + | Tast.Popcount -> + mov_rr f.b ~dst:rcx ~src:rax; + shift_imm f.b ~ext:5 ~dst:rcx 1; + imm_into f ~reg:rdx 0x5555555555555555L; + and_rr f.b ~dst:rcx ~src:rdx; + sub_rr f.b ~dst:rax ~src:rcx; + imm_into f ~reg:rdx 0x3333333333333333L; + mov_rr f.b ~dst:rcx ~src:rax; + and_rr f.b ~dst:rcx ~src:rdx; + shift_imm f.b ~ext:5 ~dst:rax 2; + and_rr f.b ~dst:rax ~src:rdx; + add_rr f.b ~dst:rax ~src:rcx; + mov_rr f.b ~dst:rcx ~src:rax; + shift_imm f.b ~ext:5 ~dst:rcx 4; + add_rr f.b ~dst:rax ~src:rcx; + imm_into f ~reg:rdx 0x0f0f0f0f0f0f0f0fL; + and_rr f.b ~dst:rax ~src:rdx; + imm_into f ~reg:rdx 0x0101010101010101L; + imul_rr f.b ~dst:rax ~src:rdx; + shift_imm f.b ~ext:5 ~dst:rax 56 + | Tast.Clz -> + bitscan f.b ~op:0xbd ~dst:rcx ~src:rax; + imm_into f ~reg:rdx (-1L); + cmovz_rr f.b ~dst:rcx ~src:rdx; + imm_into f ~reg:rax (Int64.of_int (w - 1)); + sub_rr f.b ~dst:rax ~src:rcx + | _ -> + bitscan f.b ~op:0xbc ~dst:rcx ~src:rax; + imm_into f ~reg:rdx (Int64.of_int w); + cmovz_rr f.b ~dst:rcx ~src:rdx; + mov_rr f.b ~dst:rax ~src:rcx); + store_loc f ~reg:rax dst t + | (Tast.Rotl | Tast.Rotr), [ a; b ] -> + let la = eval f a in + let lb = eval f b in + let w = + match a.Tast.ty with + | Types.Int k -> Types.bits k + | t -> unsupported "a rotation of %s" (Types.to_string t) + in + load_loc f ~reg:rax la a.Tast.ty; + load_loc f ~reg:rcx lb b.Tast.ty; + rot_cl f.b ~ext:(if p = Tast.Rotl then 0 else 1) ~bits:w ~dst:rax; + store_loc f ~reg:rax dst t + | (Tast.Not | Tast.BitNot), [ a ] -> let la = eval f a in if Types.equal a.Tast.ty Types.Bool then begin load_loc f ~reg:rax la Types.Bool; diff --git a/runtime/flan_dyn.c b/runtime/flan_dyn.c index 81b91479..4ee91915 100644 --- a/runtime/flan_dyn.c +++ b/runtime/flan_dyn.c @@ -2802,6 +2802,118 @@ flan_dyn flan_dyn_rem(flan_dyn a, flan_dyn b, const uint8_t *loc, return arith(loc, loclen, "%", a, b); } +/* ── Bits ────────────────────────────────────────────────────────────── + * + * Ints only: a float has no bits a program means, and a bool is most likely + * a reach for logical and from C, so its trap says which operator that is. + * Every answer is the one typed i64 code gives for the same operands. A shift + * count outside 0..63 traps rather than being masked as typed code masks it: + * there is no width here to have been chosen, and a count out of range is a + * mistake the value cannot show. */ + +#define BITS_INT "it takes integers" +#define BITS_BOOL "it takes integers; true and false are combined with and, or and not" + +static int is_int(flan_dyn v) { return flan_dyn_tag(v) == FLAN_DYN_TAG_INT; } +static int is_bool(flan_dyn v) { return flan_dyn_tag(v) == FLAN_DYN_TAG_BOOL; } + +static void want_ints(const uint8_t *loc, int64_t loclen, const char *op, + flan_dyn a, flan_dyn b) { + if (!is_int(a) || !is_int(b)) + trap2(loc, loclen, TYPE_TRAP, op, + is_bool(a) || is_bool(b) ? BITS_BOOL : BITS_INT, a, b); +} + +static void want_int(const uint8_t *loc, int64_t loclen, const char *op, + flan_dyn a) { + if (!is_int(a)) + trap1(loc, loclen, TYPE_TRAP, op, is_bool(a) ? BITS_BOOL : BITS_INT, a); +} + +flan_dyn flan_dyn_bitand(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + want_ints(loc, loclen, "bit-and", a, b); + return flan_dyn_from_i64(dyn_int_value(a) & dyn_int_value(b)); +} +flan_dyn flan_dyn_bitor(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + want_ints(loc, loclen, "bit-or", a, b); + return flan_dyn_from_i64(dyn_int_value(a) | dyn_int_value(b)); +} +flan_dyn flan_dyn_bitxor(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + want_ints(loc, loclen, "bit-xor", a, b); + return flan_dyn_from_i64(dyn_int_value(a) ^ dyn_int_value(b)); +} +flan_dyn flan_dyn_bitnot(flan_dyn a, const uint8_t *loc, int64_t loclen) { + want_int(loc, loclen, "bit-not", a); + return flan_dyn_from_i64(~dyn_int_value(a)); +} + +static uint64_t shift_count(const uint8_t *loc, int64_t loclen, const char *op, + flan_dyn a, flan_dyn b) { + int64_t n; + want_ints(loc, loclen, op, a, b); + n = dyn_int_value(b); + if (n < 0 || n > 63) + trap2(loc, loclen, ARITH_TRAP, op, + "the count is outside 0 to 63, the bits an int has", a, b); + return (uint64_t)n; +} + +flan_dyn flan_dyn_shl(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + uint64_t n = shift_count(loc, loclen, "<<", a, b); + return flan_dyn_from_i64((int64_t)((uint64_t)dyn_int_value(a) << n)); +} +/* Arithmetic, as >> on a typed i64 is. */ +flan_dyn flan_dyn_shr(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + uint64_t n = shift_count(loc, loclen, ">>", a, b); + int64_t x = dyn_int_value(a); + /* >> on a negative int64_t is implementation-defined in C before C23; + * the complement trick is arithmetic on every compiler. */ + if (x < 0) return flan_dyn_from_i64(~(int64_t)(~(uint64_t)x >> n)); + return flan_dyn_from_i64((int64_t)((uint64_t)x >> n)); +} + +static uint64_t rot(uint64_t x, uint64_t n, int left) { + n &= 63; + if (n == 0) return x; + return left ? (x << n) | (x >> (64 - n)) : (x >> n) | (x << (64 - n)); +} + +flan_dyn flan_dyn_rotl(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + want_ints(loc, loclen, "rotate-left", a, b); + return flan_dyn_from_i64((int64_t)rot((uint64_t)dyn_int_value(a), + (uint64_t)dyn_int_value(b), 1)); +} +flan_dyn flan_dyn_rotr(flan_dyn a, flan_dyn b, const uint8_t *loc, + int64_t loclen) { + want_ints(loc, loclen, "rotate-right", a, b); + return flan_dyn_from_i64((int64_t)rot((uint64_t)dyn_int_value(a), + (uint64_t)dyn_int_value(b), 0)); +} + +flan_dyn flan_dyn_popcount(flan_dyn a, const uint8_t *loc, int64_t loclen) { + want_int(loc, loclen, "popcount", a); + return flan_dyn_from_i64(__builtin_popcountll((uint64_t)dyn_int_value(a))); +} +/* 64 for zero, which the builtins leave undefined. */ +flan_dyn flan_dyn_clz(flan_dyn a, const uint8_t *loc, int64_t loclen) { + uint64_t x; + want_int(loc, loclen, "leading-zeros", a); + x = (uint64_t)dyn_int_value(a); + return flan_dyn_from_i64(x == 0 ? 64 : __builtin_clzll(x)); +} +flan_dyn flan_dyn_ctz(flan_dyn a, const uint8_t *loc, int64_t loclen) { + uint64_t x; + want_int(loc, loclen, "trailing-zeros", a); + x = (uint64_t)dyn_int_value(a); + return flan_dyn_from_i64(x == 0 ? 64 : __builtin_ctzll(x)); +} + /* ── Ordering ────────────────────────────────────────────────────────── * * Numbers against numbers, text against text, and nothing else. Text orders diff --git a/runtime/flan_dyn.h b/runtime/flan_dyn.h index 5bffe658..d55bbc6f 100644 --- a/runtime/flan_dyn.h +++ b/runtime/flan_dyn.h @@ -205,6 +205,20 @@ flan_dyn flan_dyn_div(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen flan_dyn flan_dyn_rem(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); flan_dyn flan_dyn_neg(flan_dyn a, const uint8_t *loc, int64_t loclen); +/* The bit operations, on ints only. A shift count outside 0..63 traps, where + * typed code masks it; a rotation takes its count modulo 64. */ +flan_dyn flan_dyn_bitand(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_bitor(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_bitxor(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_bitnot(flan_dyn a, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_shl(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_shr(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_rotl(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_rotr(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_popcount(flan_dyn a, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_clz(flan_dyn a, const uint8_t *loc, int64_t loclen); +flan_dyn flan_dyn_ctz(flan_dyn a, const uint8_t *loc, int64_t loclen); + /* Answer a bool dyn. Numbers compare as numbers and text compares bytewise; * a mixture of the two, or anything else, traps. */ flan_dyn flan_dyn_lt(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); diff --git a/spec-syntax.md b/spec-syntax.md index df8a70ca..7ea1abbc 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -155,9 +155,15 @@ Each item: the proposal, then the reason in one line. ### Expressions - **Precedence**, low to high: `or` < `and` < `not` < comparisons - (`== != < <= > >=`) < `<< >>` < `+ -` < `* / %` < unary `-` < postfix (call, - index, field). **Built.** Mixing comparison operators in one chain, - `a < b <= c`, is refused. An operator glued to `(` is always a call. + (`== != < <= > >=`) < `||` < `^^` < `&&` < `<< >>` < `+ -` < `* / %` < + prefix `-` and `~~` < postfix (call, index, field). **Built.** Mixing + comparison operators in one chain, `a < b <= c`, is refused. An operator + glued to `(` is always a call. The bit operators sit where Python and Rust + put them, so `x && mask == 0` is `(x && mask) == 0`. +- **The bit operators** are `a && b`, `a || b`, `a ^^ b` and `~~a`, reading + `(bit-and a b)`, `(bit-or a b)`, `(bit-xor a b)` and `(bit-not a)`. They take + integers; `and`, `or` and `not` are the logical ones. `~~` is one token, so a + nested unquote is written `~(~x)`. **Built.** - **`==` is `=`; `=` is assignment.** `x = v` reads `(set x v)`, `a[i] = v` reads `(set (at a i) v)`, `p.x = v` reads `(set (.x p) v)`. `x += v` reads `(set x (+ x v))` where every part of the place is a name or a literal, and diff --git a/test/programs/bits-dyn.flan b/test/programs/bits-dyn.flan new file mode 100644 index 00000000..9dddbaa2 --- /dev/null +++ b/test/programs/bits-dyn.flan @@ -0,0 +1,51 @@ +;;;; The bit operators on dyn ints, beside the same operations on typed i64: +;;;; each line prints the dyn answer, the typed one, and whether they agree. +;;;; A dyn int is an i64, so the two must be the same number for every count +;;;; in 0..63. With an argument, the program traps instead: 1 a shift count +;;;; out of range, 2 a bool operand, 3 a float operand, 4 a negative count to >>. + +(defn dyn-ops [a b n] dyn + [(bit-and a b) (bit-or a b) (bit-xor a b) (bit-not a) (<< a n) (>> a n) + (rotate-left a n) (rotate-right a n) (popcount a) (leading-zeros a) + (trailing-zeros a) (bit-xor a b n) (&& a b) (|| a b)]) + +(defn typed-ops [a i64 b i64 n i64] [14 i64] + [(bit-and a b) (bit-or a b) (bit-xor a b) (bit-not a) (<< a n) (>> a n) + (rotate-left a n) (rotate-right a n) (popcount a) (leading-zeros a) + (trailing-zeros a) (bit-xor a b n) (&& a b) (|| a b)]) + +(defn compare [a i64 b i64 n i64] () + (let [d (dyn-ops a b n) + t (typed-ops a b n)] + (dotimes [i 14] + (print (at d i) " ") + (when (!= (i64 (at d i)) (at t i)) + (print "DIFFER at " i " typed " (at t i) " "))) + (println))) + +;; A typed operand beside a dyn one makes the whole operation dyn. +(defonce mask i32 255) +(defn mixed [x] dyn (bit-and x mask)) + +(defn shift [a n] dyn (<< a n)) +(defn sar [a n] dyn (>> a n)) +(defn band [a b] dyn (bit-and a b)) + +(defn main [args [str]] i32 + (let [k (if (> (length args) 1) (bytes->i64 (bytes-view (at args 1))) 0)] + (cond + (= k 0) + (do + (compare 0 0 0) + (compare -1 12345 63) + (compare -9000000000000000000 1234567890123 13) + (compare 9223372036854775807 -9223372036854775807 1) + (compare 281474976710656 -281474976710657 47) + (compare 1 3 62) + (println (mixed 4660) (mixed -1)) + (println (leading-zeros 0) (trailing-zeros 0) (popcount -1)) + 0) + (= k 1) (do (println "before") (println (shift 1 64)) 0) + (= k 2) (do (println "before") (println (band true 1)) 0) + (= k 3) (do (println "before") (println (band 1.5 1)) 0) + :else (do (println "before") (println (sar 1 -1)) 0)))) diff --git a/test/programs/bits.flan b/test/programs/bits.flan new file mode 100644 index 00000000..5d5ab743 --- /dev/null +++ b/test/programs/bits.flan @@ -0,0 +1,46 @@ +;;;; The bit operators at every integer width, typed. Every operand is a +;;;; global or a parameter, so -O2 folds nothing and the x86 backend lowers +;;;; each one; the three builds must print the same lines. One generic body +;;;; covers the widths, which also walks the integer? bound. + +(defn bits [a $t b $t n $t] () + {:where (integer? $t)} + (println (bit-and a b) (bit-or a b) (bit-xor a b) (bit-not a) + (bit-and a b n) (&& a b) (|| a b) (^^ a b)) + (println (<< a n) (>> a n) (rotate-left a n) (rotate-right a n)) + (println (popcount a) (leading-zeros a) (trailing-zeros a) + (popcount b) (leading-zeros b) (trailing-zeros b))) + +(defonce i8a i8 -100) (defonce i8b i8 45) (defonce i8n i8 3) +(defonce i16a i16 -30000) (defonce i16b i16 12345) (defonce i16n i16 5) +(defonce i32a i32 -2000000000) (defonce i32b i32 123456789) (defonce i32n i32 7) +(defonce i64a i64 -9000000000000000000) (defonce i64b i64 1234567890123) (defonce i64n i64 13) +(defonce u8a u8 200) (defonce u8b u8 45) (defonce u8n u8 3) +(defonce u16a u16 60000) (defonce u16b u16 12345) (defonce u16n u16 5) +(defonce u32a u32 4000000000) (defonce u32b u32 123456789) (defonce u32n u32 7) +(defonce u64a u64 18000000000000000000) (defonce u64b u64 1234567890123) (defonce u64n u64 13) + +;; Zero, for the counts: leading-zeros and trailing-zeros answer the width. +(defonce z8 i8 0) (defonce z16 u16 0) (defonce z32 i32 0) (defonce z64 u64 0) +;; A rotation count past the width, and a negative one: both modulo the width. +(defonce big32 i32 35) (defonce neg32 i32 -1) (defonce big8 u8 11) + +(defn main [] i32 + (bits i8a i8b i8n) + (bits i16a i16b i16n) + (bits i32a i32b i32n) + (bits i64a i64b i64n) + (bits u8a u8b u8n) + (bits u16a u16b u16n) + (bits u32a u32b u32n) + (bits u64a u64b u64n) + (println (leading-zeros z8) (trailing-zeros z8) + (leading-zeros z16) (trailing-zeros z16) + (leading-zeros z32) (trailing-zeros z32) + (leading-zeros z64) (trailing-zeros z64) (popcount z64)) + (println (rotate-left i32b big32) (rotate-right i32b big32) + (rotate-left i32b neg32) (rotate-left u8a big8) + (rotate-right u8a big8)) + ;; A narrower count widens to the value's type. + (println (<< i64b u8n) (rotate-left u64b u8n)) + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 1789987c..dfddda3d 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -669,6 +669,81 @@ let () = outputs "unary minus" "programs/negate.flan" neg_out; outputs ~opt:"-O0" "unary minus, -O0" "programs/negate.flan" neg_out; outputs ~x86:true "unary minus, x86" "programs/negate.flan" neg_out; + (* The bit operators at every width, and on dyn ints beside typed i64: + both backends and both optimisation levels print the same numbers. *) + let bits_out = + "12 -67 -79 99 0 12 -67 -79\n\ + -32 -13 -28 -109\n\ + 4 0 2 4 2 0\n\ + 16 -17671 -17687 29999 0 16 -17671 -17687\n\ + 23040 -938 23057 -31658\n\ + 6 0 4 6 2 0\n\ + 4869120 -1881412331 -1886281451 1999999999 0 4869120 -1881412331 -1886281451\n\ + 1698037760 -15625000 1698037828 17929432\n\ + 10 0 10 16 5 0\n\ + 1164229214208 -8999999929661324085 -9000001093890538293 8999999999999999999 0 1164229214208 -8999999929661324085 -9000001093890538293\n\ + 3636062617077809152 -1098632812500000 3636062617077813347 1153167001185248\n\ + 25 0 18 23 23 0\n\ + 8 237 229 55 0 8 237 229\n\ + 64 25 70 25\n\ + 3 0 3 4 2 0\n\ + 8224 64121 55897 5535 0 8224 64121 55897\n\ + 19456 1875 19485 1875\n\ + 7 0 5 6 2 0\n\ + 105580544 4017876245 3912295701 294967295 0 105580544 4017876245 3912295701\n\ + 898891776 31250000 898891895 31250000\n\ + 13 0 11 16 5 0\n\ + 5386010624 18000001229181879499 18000001223795868875 446744073709551615 0 5386010624 18000001229181879499 18000001223795868875\n\ + 11174618839553933312 2197265625000000 11174618839553941305 2197265625000000\n\ + 22 0 19 23 23 0\n\ + 8 8 16 16 32 32 64 64 0\n\ + 987654312 -1595180638 -2085755254 70 25\n\ + 9876543120984 9876543120984\n\ + " in + outputs "bit operators" "programs/bits.flan" bits_out; + outputs ~opt:"-O0" "bit operators, -O0" "programs/bits.flan" bits_out; + outputs ~x86:true "bit operators, x86" "programs/bits.flan" bits_out; + let bits_dyn_out = + "0 0 0 -1 0 0 0 0 0 64 64 0 0 0 \n\ + 12345 -1 -12346 0 -9223372036854775808 -1 -1 -1 64 0 0 -12295 12345 -1 \n\ + 1164229214208 -8999999929661324085 -9000001093890538293 8999999999999999999 3636062617077809152 -1098632812500000 3636062617077813347 1153167001185248 25 0 18 -9000001093890538298 1164229214208 -8999999929661324085 \n\ + 1 -1 -2 -9223372036854775808 -2 4611686018427387903 -2 -4611686018427387905 63 1 0 -1 1 -1 \n\ + 0 -1 -1 -281474976710657 0 2 2147483648 2 1 15 48 -48 0 -1 \n\ + 1 3 2 -2 4611686018427387904 0 4611686018427387904 4 1 63 0 60 1 3 \n\ + 52 255\n\ + 32 32 32\n\ + " in + outputs "dyn bit operators" "programs/bits-dyn.flan" bits_dyn_out; + outputs ~opt:"-O0" "dyn bit operators, -O0" "programs/bits-dyn.flan" + bits_dyn_out; + outputs ~x86:true "dyn bit operators, x86" "programs/bits-dyn.flan" + bits_dyn_out; + (* A dyn bit operation traps at its own site: a shift count out of range + either way, a bool, a float. *) + let bits_traps ?opt ?x86 () = + let exe = compile ?opt ?x86 "programs/bits-dyn.flan" in + let traps arg reason = + let code, text = run exe (Some arg) in + if code <> 134 || not (contains text "before\n") + || not (contains text "programs/bits-dyn.flan:") + || not (contains text reason) + then begin + incr failures; + Printf.printf + "FAIL dyn bit trap %s\n got: %S (exit %d)\n wanted: %S (exit 134)\n" + arg text code reason + end + in + traps "1" "dyn <<: int and int, and the count is outside 0 to 63"; + traps "2" "dyn bit-and: bool and int, and it takes integers; true and \ + false are combined with and, or and not"; + traps "3" "dyn bit-and: float and int, and it takes integers —"; + traps "4" "dyn >>: int and int, and the count is outside 0 to 63"; + (try Sys.remove exe with Sys_error _ -> ()) + in + bits_traps (); + bits_traps ~opt:"-O0" (); + bits_traps ~x86:true (); (* A literal arm takes the other arm's type. *) let arm_out = "4000000\n9000000000\n5000000000\n7\n9000000000\n3\n9000000000\n2.5\n" in diff --git a/test/test_flan.ml b/test/test_flan.ml index d0e4ede2..5923a78d 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -1375,6 +1375,91 @@ let () = ~needle:"bit-or takes two arguments or more, given 0"; rejects_check "one operand is not a min" "(defn f [] i32 (min 7))" ~needle:"min takes two arguments or more, given 1"; + (* The bit operators take integers, and a bool is answered with the logical + operator a C programmer meant. *) + infers "bit-not keeps its operand's type" "(bit-not (u16 5))" "u16"; + infers "popcount keeps its operand's type" "(popcount (i64 5))" "i64"; + infers "a rotation takes the value's type" "(rotate-left (u8 5) (u8 1))" "u8"; + infers "&& is bit-and" "(&& 6 3)" "i32"; + infers "a bit operation with a dyn operand is dyn" + "(bit-and (the dyn 6) (i32 3))" "dyn"; + infers "a shift with a dyn operand is dyn" "(<< (the dyn 1) 3)" "dyn"; + (* Through the whole-file check [flan check] and the editor run, which + records a refusal and goes on rather than raising at the first: a hint + that only a raised refusal could give would be lost there. [.fln] text is + checked from a file, since the operator's spelling follows the syntax. *) + let refuses_all name ?(fln = false) src needle = + let diags = + match + if fln then begin + let path = Test_support.tmp "bits-" (string_of_int (Hashtbl.hash name) ^ ".fln") in + Out_channel.with_open_bin path (fun oc -> output_string oc src); + let r = snd (Front.checked ~all:true path) in + Sys.remove path; r + end + else Check.program_all (Parse.program_all (read src)) + with + | _ -> [] + | exception Loc.Errors ds -> List.map (fun (d : Loc.diag) -> d.Loc.dmsg) ds + | exception Loc.Error d -> [ d.Loc.dmsg ] + in + if not (List.exists (fun m -> contains m needle) diags) then begin + incr failures; + Printf.printf "FAIL %s\n wanted: %s\n got: %s\n" name needle + (String.concat " | " diags) + end + in + refuses_all "bit-and over bools points at and" + "(defn f [a bool b bool] bool (= (bit-and a b) 0))" + "bit-and works on the bits of an integer, and this is a bool. \ + For true and false, write (and a b)"; + refuses_all "bit-not over a bool points at not" + "(defn f [a bool] i32 (bit-not a) 0)" "write (not a)"; + refuses_all "bit-xor over bools points at !=" + "(defn f [a bool b bool] i32 (bit-xor a b) 0)" "write (!= a b)"; + refuses_all "a typed bool beside a dyn is refused before it runs" + "(defn f [a bool d dyn] dyn (bit-or a d))" "write (or a b)"; + refuses_all "a shift of a bool" + "(defn f [a bool] i32 (<< a 1) 0)" "combined with and, or and not"; + refuses_all "a bool shift count" "(defn f [flag bool] i32 (<< 1 flag))" + "combined with and, or and not"; + refuses_all "a bool beside an integer, an integer wanted" + "(defn f [flag bool x i32] i32 (bit-and flag x))" "write (and a b)"; + refuses_all "a bool beside a literal, an integer wanted" + "(defn f [flag bool] i32 (bit-or flag 1))" "write (or a b)"; + refuses_all "a literal beside a bool" "(defn f [flag bool] i32 (bit-or 1 flag))" + "write (or a b)"; + refuses_all "a bool third" "(defn f [x i32 flag bool] i32 (bit-and x x flag))" + "write (and a b)"; + refuses_all "a comparison as an operand" + "(defn f [x i32 y i32] i32 (bit-and x (= x y)))" "write (and a b)"; + refuses_all "a bool field" "(defstruct S [on bool]) (defn f [s S] i32 (bit-or 1 (.on s)))" + "write (or a b)"; + refuses_all "a call that answers a bool" + "(defn p? [x i32] bool (> x 0)) (defn f [x i32] i32 (bit-and x (p? x)))" + "write (and a b)"; + refuses_all "an if whose type is bool" + "(defn f [x i32 c bool] i32 (bit-or 1 (if c true false)))" "write (or a b)"; + refuses_all "a dyn function's typed bool result" + "(defn p? [x] bool (> x 0)) (defn f [d dyn] i32 (<< 1 (p? d)))" + "combined with and, or and not"; + refuses_all "a bool inside a nest of bit operations" + "(defn p? [x i32] bool (> x 0)) \ + (defn f [x i32] i32 (bit-and x (bit-or x (bit-xor x (p? x)))))" + "write (!= a b)"; + refuses_all ~fln:true "&& in .fln, the bool first" + "fn f(flag: bool, x: i32) -> i32\n flag && x\n" "For true and false, write a and b"; + refuses_all ~fln:true "&& in .fln, a literal first" + "fn f(flag: bool) -> i32\n 1 && flag\n" "write a and b"; + refuses_all ~fln:true "&& in .fln, the bool last of three" + "fn f(flag: bool, x: i32) -> i32\n x && x && flag\n" "write a and b"; + refuses_all ~fln:true "~~ in .fln" "fn f(a: bool) -> i32\n ~~a\n" "~~ works on the bits"; + refuses_all ~fln:true "^^ in .fln" "fn f(a: bool, b: bool) -> bool\n a ^^ b == 0\n" + "write a != b"; + rejects_check "popcount of a float" + "(defn f [a f64] f64 (popcount a))" ~needle:"popcount takes integers, found f64"; + rejects_check "a rotation's count does not widen the value" + "(defn f [a u8 n i32] u8 (rotate-left a n))" ~needle:"i32"; (* ── Chained comparisons ───────────────────────────────────────── *) (* (< a b c) is a < b and b < c. The left fold — ((a < b) < c) — would be @@ -2749,18 +2834,16 @@ let () = "(defn f [a i32 b i32] bool (=/= a b))" ~needle:"Write (!= a b)"; rejects_check "== names =" "(defn f [a i32 b i32] bool (== a b))" ~needle:"Write (= a b)"; - rejects_check "&& names and" - "(defn f [a bool b bool] bool (&& a b))" ~needle:"Write (and a b)"; + rejects_check "&& over bools names and" + "(defn f [a bool b bool] bool (&& a b))" ~needle:"write (and a b)"; accepts "and that call compiles" "(defn f [a bool b bool] bool (and a b))"; - rejects_check "|| names or" - "(defn f [a bool b bool] bool (|| a b))" ~needle:"Write (or a b)"; + rejects_check "|| over bools names or" + "(defn f [a bool b bool] bool (|| a b))" ~needle:"write (or a b)"; rejects_check "! names not" "(defn f [a bool] bool (! a))" ~needle:"Write (not a)"; rejects_check "a ! at an arity not does not take gets not's shape" "(defn f [a bool b bool] bool (! a b))" ~needle:"called as (not x)"; - rejects_check "a bare && is written back as the and that compiles" - "(defn f [] bool (&&))" ~needle:"Write (and)"; - accepts "and it does" "(defn f [] bool (and))"; + accepts "a bare and compiles" "(defn f [] bool (and))"; accepts "a program's own not= is its own" "(defn not= [a i32 b i32] bool (!= a b)) \ (defn f [a i32 b i32] bool (not= a b))"; diff --git a/test/test_syntax.ml b/test/test_syntax.ml index 37a99d9f..594c853b 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -111,6 +111,10 @@ let canon (f : Form.t) : Form.t = let rec go env (f : Form.t) = let v = match f.v with + (* The Lisp side may write the bit operators' .fln spellings, which read + back as their words: the same builtin by two names. *) + | Form.Sym ("&&" | "||" | "^^" as s) -> + Form.Sym (match s with "&&" -> "bit-and" | "||" -> "bit-or" | _ -> "bit-xor") | Form.Sym s -> Form.Sym (look env s) (* Quoted data keeps its names: renaming them would hide a printer that renamed them too. *) @@ -528,6 +532,21 @@ let () = reads "chain" "x = a < b < c" "(set x (< a b c))"; reads "left to right" "x = a - b + c" "(set x (+ (- a b) c))"; reads "precedence" "x = a or b and not c == d" "(set x (or a (and b (not (= c d)))))"; + (* The bit operators: tighter than a comparison, looser than a shift, and + among themselves && then ^^ then ||. *) + reads "bit and under a comparison" "x = a && mask == 0" + "(set x (= (bit-and a mask) 0))"; + reads "bit operator order" "x = a || b ^^ c && d << 2 + 1" + "(set x (bit-or a (bit-xor b (bit-and c (<< d (+ 2 1))))))"; + reads "bit operators left to right" "x = a && b && c || d" + "(set x (bit-or (bit-and a b c) d))"; + reads "bit-not" "x = ~~a && ~~f(b)" "(set x (bit-and (bit-not a) (bit-not (f b))))"; + reads "bit-not of a negation" "x = ~~-a" "(set x (bit-not (- a)))"; + reads "bit operator values" "x = reduce(^^, 0, xs)" "(set x (reduce bit-xor 0 xs))"; + reads "bit-not in a spaced vector" "x = [~~a b]" "(set x [(bit-not a) b])"; + reads "a nested unquote" "quote\n f(~(~x))" "(quasiquote (f (unquote (unquote x))))"; + reads "bit-not in a template" "quote\n f(~~x, ~(~~y))" + "(quasiquote (f (bit-not x) (unquote (bit-not y))))"; refuses "not-equal chain" "x = a != b != c" "indent/chained-not-equal" "!=(a, b, c)"; reads "not-equal call" "x = !=(a, b, c)" "(set x (!= a b c))"; refuses "mixed comparison" "x = a < b <= c" "indent/mixed-comparison" "and"; @@ -927,7 +946,17 @@ let () = fail "%s: read back %s from %S" name (describe_diff forms back) text | exception e -> fail "%s: its text is refused: %s\n%s" name (diag_text e) text in - round "a one-line lambda" "(defn f [] () (h (fn [a] (+ a 1)) 2))" "= h(fn(a) => a + 1, 2)"; + round "bit operators print infix" + "(defn f [a i32 m i32] bool (= (bit-and a (bit-not m)) (bit-or (bit-xor a 1) (<< m 2))))" + "a && ~~m == a ^^ 1 || m << 2"; + round "bit operators parenthesise against precedence" + "(defn f [a i32 b i32 c i32] i32 (bit-and (bit-or a b) (+ c 1) (bit-not (bit-xor a b))))" + "= (a || b) && c + 1 && ~~(a ^^ b)"; + round "a nested unquote prints with parentheses" + "(defmacro m [x] `(defmacro n [] `(g ~~x ~(bit-not x))))" "~(~x)"; + prints "the Lisp spellings print as the operators" + "(defn f [a i32 b i32] i32 (^^ (&& a b) (|| a b)))" "a && b ^^ (a || b)"; + round "a one-line lambda""(defn f [] () (h (fn [a] (+ a 1)) 2))" "= h(fn(a) => a + 1, 2)"; round "a block lambda as a call's last argument" "(defn f [] () (sort-by xs (fn [a b] (g a) (< a b))))" " sort-by(xs, fn(a, b) =>\n g(a)\n a < b)";