A narrow shift masks its own count, since the 64-bit instruction masks to 63

This commit is contained in:
Joseph Ferano 2026-09-18 07:22:57 +07:00
parent 47cb46aa00
commit 8c9e8a314c
2 changed files with 113 additions and 6 deletions

View File

@ -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

View File

@ -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 width1, 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)