diff --git a/lib/x86.ml b/lib/x86.ml index 0c7249a..cbd5c20 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -351,6 +351,7 @@ let grp1_imm b ~ext ~dst n = rex b ~w:true ~r:0 ~x:0 ~m:dst; u8 b 0x81; modrm_r b ~r:ext ~m:dst; i32 b n let add_imm b ~dst n = grp1_imm b ~ext:0 ~dst n +let and_imm b ~dst n = grp1_imm b ~ext:4 ~dst n let sub_imm b ~dst n = grp1_imm b ~ext:5 ~dst n let cmp_imm b ~dst n = grp1_imm b ~ext:7 ~dst n @@ -364,8 +365,14 @@ let cqo b = u8 b 0x48; u8 b 0x99 let idiv_r b ~src = rex b ~w:true ~r:0 ~x:0 ~m:src; u8 b 0xf7; modrm_r b ~r:7 ~m:src let div_r b ~src = rex b ~w:true ~r:0 ~x:0 ~m:src; u8 b 0xf7; modrm_r b ~r:6 ~m:src -(* Shifts by cl. The count is masked to the operand width by the hardware, - which is the rule the language already defines (item 15's audit). *) +(* Shifts by cl, always 64 bits wide, because every integer in this backend is + computed in a full register and narrowed only on the way to memory. The + hardware masks the count to 63 for a 64-bit shift and to 31 for a 32-bit + one, and to nothing narrower than that — there is no 8- or 16-bit masking + rule to inherit. So the width the count is masked to is a property of the + *instruction* emitted, not of the language's type, and since the instruction + is always the 64-bit one the caller has to mask the count itself for every + width below 64. See the shift arm in [prim_to]. *) let shift_cl b ~ext ~dst = rex b ~w:true ~r:0 ~x:0 ~m:dst; u8 b 0xd3; modrm_r b ~r:ext ~m:dst let shl_cl b ~dst = shift_cl b ~ext:4 ~dst let shr_cl b ~dst = shift_cl b ~ext:5 ~dst @@ -2530,10 +2537,25 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) dst = | Tast.BitAnd -> and_rr f.b ~dst:rax ~src:rcx | Tast.BitOr -> or_rr f.b ~dst:rax ~src:rcx | Tast.BitXor -> xor_rr f.b ~dst:rax ~src:rcx - (* The count is masked to the operand width by the hardware, which is - the rule the language already defines. *) - | Tast.Shl -> shl_cl f.b ~dst:rax - | Tast.Shr -> if signed then sar_cl f.b ~dst:rax else shr_cl f.b ~dst:rax + (* The language masks a computed count to the operand's width minus one + and says so out loud (NEXT.md's sharp edges); [emit] writes that mask + into the IR at 2041. The hardware would do it too if the shift were + the operand's width, but [shift_cl] is 64 bits wide whatever the type + says, so what the silicon masks to is 63 and not 31, 15 or 7. Without + the [and] a count of 32 on an i32 shifts the sign-extended value out + of the low half and stores zero, where the rule — and LLVM — say the + count is 0 and the value comes back unchanged. One extra [and] on the + count register buys the agreement; 64-bit shifts skip it because + there the hardware's mask is already the rule's. A literal out of + range never reaches here: [check] rejects it as a typo. *) + | Tast.Shl | Tast.Shr -> + (match t with + | Types.Int k when Types.bits k < 64 -> + and_imm f.b ~dst:rcx (Types.bits k - 1) + | _ -> ()); + (match p with + | Tast.Shl -> shl_cl f.b ~dst:rax + | _ -> if signed then sar_cl f.b ~dst:rax else shr_cl f.b ~dst:rax) | Tast.Div | Tast.Rem -> (* The guard goes *before* the instruction, which is the whole of why it has to be emitted at all: `idiv` raises SIGFPE on both the zero diff --git a/spike/x86/p11-shift-edges.flan b/spike/x86/p11-shift-edges.flan new file mode 100644 index 0000000..41dae5e --- /dev/null +++ b/spike/x86/p11-shift-edges.flan @@ -0,0 +1,85 @@ +;;;; A shift whose count reaches the operand's own width, at every width. +;;;; +;;;; This backend computes every integer in a full 64-bit register and narrows +;;;; only on the way to memory, so the shift instruction it emits is the 64-bit +;;;; one whatever the type says. The hardware masks a 64-bit shift's count to +;;;; 63 — not to 31, 15 or 7 — so for every width below 64 the mask the +;;;; language promises (count masked to width−1, which is what emit writes into +;;;; the IR) has to be emitted by hand. Without it a count of 32 on an i32 +;;;; shifts the value clean out of the low half and stores zero, where the rule +;;;; says the count is 0 and the value comes back unchanged. +;;;; +;;;; Every count is a global and never a literal: the checker rejects a literal +;;;; count that is out of range — that is a typo, not a program — so the only +;;;; shift that can reach either backend out of range is a computed one, and a +;;;; literal would be folded away before the code under test ran anyway. + +(defvar i8min i8 -128) +(defvar i16min i16 -32768) +(defvar i32min i32 -2147483648) +(defvar i64min i64 -9223372036854775808) +(defvar u8hi u8 255) +(defvar u16hi u16 65535) +(defvar u32hi u32 4294967295) +(defvar one8 i8 1) +(defvar one16 i16 1) +(defvar one32 i32 1) +(defvar oneu8 u8 1) +(defvar oneu16 u16 1) +(defvar oneu32 u32 1) +(defvar one64 i64 1) + +(defvar c8 i8 8) +(defvar c9 i8 9) +(defvar cu8 u8 8) +(defvar c16 i16 16) +(defvar cu16 u16 17) +(defvar c32 i32 32) +(defvar c33 i32 33) +(defvar cu32 u32 32) +(defvar cu33 u32 33) +(defvar c64 i64 64) +(defvar c65 i64 65) + +(defn main [] i32 + ;; ── The count exactly at the width: masks to 0, so nothing moves ── + (println (<< one8 c8)) + (println (<< one16 c16)) + (println (<< one32 c32)) + (println (<< oneu8 cu8)) + (println (<< oneu32 cu32)) + (println (<< one64 c64)) + + ;; ── The sign bit, shifted by the width ──────────────────────────── + ;; An arithmetic shift of a negative number by a count the hardware would + ;; read as the full width answers all-ones; masked, it answers the value. + (println (<< i8min c8)) + (println (>> i8min c8)) + (println (<< i16min c16)) + (println (>> i16min c16)) + (println (<< i32min c32)) + (println (>> i32min c32)) + (println (<< i64min c64)) + (println (>> i64min c64)) + + ;; ── The same edge unsigned, where the shift is logical ──────────── + (println (>> u8hi cu8)) + (println (>> u16hi cu16)) ; 17, one past the width: masks to 1 + (println (>> u32hi cu32)) + (println (<< u32hi cu33)) ; 33 masks to 1: the top of u32 doubled, wrapped + + ;; ── One past the width, where the masked count is 1 ─────────────── + (println (<< one8 c9)) + (println (<< one32 c33)) + (println (<< one64 c65)) + + ;; ── And the ordinary counts, which never needed a mask ──────────── + (println (<< one32 one32)) + (println (>> i32min one32)) + (println (>> u32hi oneu32)) + (println (>> i8min one8)) + (println (>> u8hi oneu8)) + (println (<< one16 one16)) + (println (>> i16min one16)) + (println (>> u16hi oneu16)) + 0)