diff --git a/TODO.org b/TODO.org index 08a02ae2..3803e3b7 100644 --- a/TODO.org +++ b/TODO.org @@ -128,6 +128,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=, diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el index 96e0c01c..2217f941 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 4e0d3b30..5c49844c 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 1a69fbd1..5c8185ac 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 aa7423a1..4b3590f3 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -677,12 +677,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 @@ -10086,7 +10087,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" @@ -10206,14 +10209,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 @@ -10224,12 +10229,79 @@ 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 pair with a bool in it is usually refused before [bits_operand] sees it, + as a mismatch between the bool and the other operand. When the pair is + refused, each operand is checked on its own terms, and a bool among them is + the refusal given. The compile is already failing, so the second check + costs nothing that matters. *) +and bool_first : 'a. ctx -> string -> Ast.expr list -> (unit -> 'a) -> 'a = + fun ctx name args k -> + try k () with + | Loc.Error _ as e -> + List.iter + (fun a -> + match check ctx a with + | v when v.Tast.ty = Types.Bool -> bool_bits v.Tast.loc name + | _ -> () + | exception Loc.Error _ -> ()) + (List.filteri (fun i _ -> i < 2) args); + raise e + +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 @@ -10989,8 +11061,32 @@ and named_call ?(qualified = false) ctx ~want loc name args = | _ -> Tast.BitXor in fold_arity loc name args; - fold_left_prim ctx ~want loc name p ~needs:"integer?" Types.is_integer - "integers" args + bool_first ctx name args (fun () -> + 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; + 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. @@ -11002,22 +11098,33 @@ 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)); + let a, b = + bool_first ctx name args (fun () -> + 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 @@ -11032,6 +11139,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. @@ -14457,19 +14565,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 97240c6e..5efc9a84 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) @@ -5034,6 +5082,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 a86235e1..e3fd42cb 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) @@ -451,7 +462,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)]. *) @@ -463,7 +474,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 -> @@ -480,7 +491,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"; _ }; @@ -499,7 +510,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 @@ -739,7 +750,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 @@ -771,7 +784,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 -> @@ -972,9 +985,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 @@ -1068,7 +1081,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"; _ } :: _ :: _ :: _) -> @@ -1274,7 +1287,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 @@ -1339,7 +1352,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 @@ -1354,7 +1367,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 b703d7f4..5c004229 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,16 +665,16 @@ 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. *) +(* 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 @@ -727,7 +738,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 = @@ -740,18 +755,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) @@ -768,7 +783,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 @@ -780,18 +795,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 @@ -804,21 +819,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) @@ -919,7 +934,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. *) @@ -1063,7 +1078,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; @@ -1072,7 +1087,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 @@ -1103,7 +1118,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 8c2a24e6..4f4f17cf 100644 --- a/runtime/flan_dyn.c +++ b/runtime/flan_dyn.c @@ -2785,6 +2785,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 5e6a118b..73e1b16d 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 bda24f0a..754753b9 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 ff0185a4..c86ce46e 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 6b853e5f..a4990919 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -1375,6 +1375,36 @@ 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"; + rejects_check "bit-and over bools points at and" + "(defn f [a bool b bool] bool (= (bit-and a b) 0))" + ~needle:"bit-and works on the bits of an integer, and this is a bool. \ + For true and false, write (and a b)"; + rejects_check "bit-not over a bool points at not" + "(defn f [a bool] i32 (bit-not a) 0)" ~needle:"write (not a)"; + rejects_check "bit-xor over bools points at !=" + "(defn f [a bool b bool] i32 (bit-xor a b) 0)" ~needle:"write (!= a b)"; + rejects_check "a typed bool beside a dyn is refused before it runs" + "(defn f [a bool d dyn] dyn (bit-or a d))" ~needle:"write (or a b)"; + rejects_check "a shift of a bool" + "(defn f [a bool] i32 (<< a 1) 0)" + ~needle:"combined with and, or and not"; + rejects_check "a bool beside an integer" + "(defn f [x i32 flag bool] i32 (bit-and x flag))" ~needle:"write (and a b)"; + rejects_check "a bool beside a literal" + "(defn f [flag bool] i32 (bit-or flag 1))" ~needle:"write (or 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 +2779,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 a623b0a4..a054e6e5 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. *) @@ -524,6 +528,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"; @@ -909,7 +928,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)";