Repository: ykjit/yk
Pull request #1642: Implement stuff required to elide instruction loading in yklua.
Author: login: vext01
Created: 2025-03-06T16:32:10Z | Merged: 2025-03-11T16:22:10Z

This includes:

 - Promote support for pointers.
 - Implementing various todo! relating to constant pointers.
 - Optimisations for ptr_add upon const pointers.
 - SpillState support for const pointers.


Diff:
diff --git a/ykcapi/yk.h b/ykcapi/yk.h
index 74dbd7209..675630bdb 100644
--- a/ykcapi/yk.h
+++ b/ykcapi/yk.h
@@ -88,13 +88,15 @@ void yk_location_drop(YkLocation);
                                int: __yk_promote_c_int, \
                                unsigned int: __yk_promote_c_unsigned_int, \
                                long long: __yk_promote_c_long_long, \
-                               uintptr_t: __yk_promote_usize \
+                               uintptr_t: __yk_promote_usize, \
+                               void *: __yk_promote_ptr \
                               )(X)
 int __yk_promote_c_int(int);
 unsigned int __yk_promote_c_unsigned_int(unsigned int);
 long long __yk_promote_c_long_long(long long);
 // Rust defines `usize` to be layout compatible with `uintptr_t`.
 uintptr_t __yk_promote_usize(uintptr_t);
+void *__yk_promote_ptr(void *);
 
 /// Associate a UTF-8 compatible string to the next instruction to be traced.
 /// The string will be copied by this function, so callers can safely reuse the
diff --git a/ykrt/src/compile/jitc_yk/codegen/x64/lsregalloc.rs b/ykrt/src/compile/jitc_yk/codegen/x64/lsregalloc.rs
index cb89b91e7..ae094690d 100644
--- a/ykrt/src/compile/jitc_yk/codegen/x64/lsregalloc.rs
+++ b/ykrt/src/compile/jitc_yk/codegen/x64/lsregalloc.rs
@@ -370,12 +370,18 @@ impl LSRegAlloc<'_> {
         self.spills[usize::from(iidx)] = SpillState::Stack(frame_off);
     }
 
-    /// Forcibly assign a constant to an instruction. This typically only happens when traces pass
-    /// live variables that have been optimised to constants into side-traces.
-    pub(crate) fn assign_const(&mut self, iidx: InstIdx, bits: u32, v: u64) {
+    /// Forcibly assign a constant integer to an instruction. This typically only happens when
+    /// traces pass live variables that have been optimised to constants into side-traces.
+    pub(crate) fn assign_const_int(&mut self, iidx: InstIdx, bits: u32, v: u64) {
         self.spills[usize::from(iidx)] = SpillState::ConstInt { bits, v };
     }
 
+    /// Forcibly assign a constant pointer to an instruction. This typically only happens when
+    /// traces pass live variables that have been optimised to constants into side-traces.
+    pub(crate) fn assign_const_ptr(&mut self, iidx: InstIdx, v: usize) {
+        self.spills[usize::from(iidx)] = SpillState::ConstPtr(v);
+    }
+
     /// Return a currently unused general purpose register, if one exists.
     ///
     /// Note: you must be *very* careful in when you use the register that's returned.
@@ -710,7 +716,7 @@ impl LSRegAlloc<'_> {
                                             }
                                         }
                                     },
-                                    SpillState::ConstInt { .. } => {
+                                    SpillState::ConstInt { .. } | SpillState::ConstPtr(_) => {
                                         // Should we encounter multiple constants in registers
                                         // (which isn't very likely...), we want to spill the one
                                         // in the lowest register, since that's more likely to be
@@ -1143,6 +1149,12 @@ impl LSRegAlloc<'_> {
                 }
                 _ => todo!("{bits}"),
             },
+            SpillState::ConstPtr(v) => {
+                // unwrap cannot fail since pointers are sized.
+                let bitw = self.m.type_(self.m.ptr_tyidx()).bitw().unwrap();
+                assert_eq!(bitw, 64);
+                dynasm!(asm; mov Rq(reg.code()), QWORD v as i64);
+            }
         }
         self.gp_regset.set(reg);
         self.gp_reg_states[usize::from(reg.code())] =
@@ -1220,6 +1232,7 @@ impl LSRegAlloc<'_> {
                         size,
                     },
                     SpillState::ConstInt { bits, v } => VarLocation::ConstInt { bits, v },
+                    SpillState::ConstPtr(v) => VarLocation::ConstPtr(v),
                 },
             }
         }
@@ -1636,8 +1649,8 @@ impl LSRegAlloc<'_> {
                 self.fp_regset.set(reg);
             }
             SpillState::Direct(_off) => todo!(),
-            SpillState::ConstInt { bits: _bits, v: _v } => {
-                todo!()
+            SpillState::ConstInt { .. } | SpillState::ConstPtr(_) => {
+                panic!(); // would indicate some kind of type confusion.
             }
         }
     }
@@ -2057,7 +2070,12 @@ enum SpillState {
     /// Note: two SSA variables can alias to the same `Direct` location.
     Direct(i32),
     /// This variable is a constant.
-    ConstInt { bits: u32, v: u64 },
+    ConstInt {
+        bits: u32,
+        v: u64,
+    },
+    // This variable is a constant pointer.
+    ConstPtr(usize),
 }
 
 #[cfg(test)]
diff --git a/ykrt/src/compile/jitc_yk/codegen/x64/mod.rs b/ykrt/src/compile/jitc_yk/codegen/x64/mod.rs
index bf03c5976..d518b208d 100644
--- a/ykrt/src/compile/jitc_yk/codegen/x64/mod.rs
+++ b/ykrt/src/compile/jitc_yk/codegen/x64/mod.rs
@@ -276,6 +276,7 @@ impl From<&VarLocation> for yksmp::Location {
                     todo!(">32 bit constant")
                 }
             }
+            VarLocation::ConstPtr(v) => yksmp::Location::LargeConstant(u64::try_from(*v).unwrap()),
             e => todo!("{:?}", e),
         }
     }
@@ -1271,7 +1272,7 @@ impl<'a> Assemble<'a> {
                     .force_assign_inst_indirect(iidx, i32::try_from(frame_off).unwrap());
             }
             VarLocation::ConstInt { bits, v } => {
-                self.ra.assign_const(iidx, bits, v);
+                self.ra.assign_const_int(iidx, bits, v);
             }
             e => panic!("{:?}", e),
         }
@@ -2465,7 +2466,7 @@ impl<'a> Assemble<'a> {
                     // match. But since the value can't change we can safely ignore this.
                 }
                 VarLocation::Register(_reg) => (), // Handled in the earlier loop
-                _ => todo!(),
+                _ => todo!("{dst:?}"),
             }
         }
 
@@ -2609,7 +2610,10 @@ impl<'a> Assemble<'a> {
                             );
                         }
                         VarLocation::ConstInt { bits, v } => {
-                            self.ra.assign_const(iidx, bits, v);
+                            self.ra.assign_const_int(iidx, bits, v);
+                        }
+                        VarLocation::ConstPtr(v) => {
+                            self.ra.assign_const_ptr(iidx, v);
                         }
                         e => panic!("{:?}", e),
                     }
diff --git a/ykrt/src/compile/jitc_yk/opt/heapvalues.rs b/ykrt/src/compile/jitc_yk/opt/heapvalues.rs
index 5f1b7720c..7aba05a15 100644
--- a/ykrt/src/compile/jitc_yk/opt/heapvalues.rs
+++ b/ykrt/src/compile/jitc_yk/opt/heapvalues.rs
@@ -4,7 +4,7 @@
 //!
 //! Broadly speaking, loads add new information; stores tend to remove most old information and add
 //! new information; and barriers remove all information.
-use super::super::jit_ir::{Inst, InstIdx, Module, Operand};
+use super::super::jit_ir::{Const, Inst, InstIdx, Module, Operand};
 use std::collections::HashMap;
 
 /// An abstract "address" representing a location in RAM.
@@ -40,7 +40,12 @@ impl Address {
                 }
                 Address::PtrPlusOff(iidx, off)
             }
-            Operand::Const(_) => todo!(),
+            Operand::Const(cidx) => {
+                let Const::Ptr(v) = m.const_(cidx) else {
+                    panic!();
+                };
+                Address::Const(*v)
+            }
         }
     }
 }
diff --git a/ykrt/src/compile/jitc_yk/opt/mod.rs b/ykrt/src/compile/jitc_yk/opt/mod.rs
index 6e0f2e0fa..033adc766 100644
--- a/ykrt/src/compile/jitc_yk/opt/mod.rs
+++ b/ykrt/src/compile/jitc_yk/opt/mod.rs
@@ -789,7 +789,16 @@ impl Opt {
             // `checked_add` with `wrapping_add`.
             off = off.checked_add(i64::from(pa_inst.off())).unwrap();
             match self.an.op_map(&self.m, pa_inst.ptr(&self.m)) {
-                Operand::Const(_) => todo!(),
+                Operand::Const(cidx) => {
+                    let Const::Ptr(cptr) = self.m.const_(cidx) else {
+                        panic!();
+                    };
+                    // FIXME: JIT IR assumes a usize is pointer-sized.
+                    let off = usize::try_from(off.cast_unsigned()).unwrap();
+                    let cidx = self.m.insert_const(Const::Ptr(cptr.wrapping_add(off)))?;
+                    self.m.replace(iidx, Inst::Const(cidx));
+                    break;
+                }
                 Operand::Var(op_iidx) => {
                     if let Inst::PtrAdd(x) = self.m.inst(op_iidx) {
                         pa_inst = x;
@@ -870,6 +879,7 @@ impl Opt {
                             debug_assert_eq!(lhs.bitw(), rhs.bitw());
                             lhs == rhs
                         }
+                        (Const::Ptr(lhs), Const::Ptr(rhs)) => lhs == rhs,
                         x => todo!("{x:?}"),
                     },
                 },
@@ -1805,6 +1815,60 @@ mod test {
         ",
         );
 
+        // constant pointer optimisations.
+        Module::assert_ir_transform_eq(
+            "
+          entry:
+            %0: ptr = ptr_add 0x0, 0
+            %1: ptr = ptr_add 0x6, 10
+            black_box %0
+            black_box %1
+            ",
+            |m| opt(m).unwrap(),
+            "
+            ...
+          entry:
+            black_box 0x0
+            black_box 0x10
+         ",
+        );
+
+        #[cfg(target_pointer_width = "64")]
+        Module::assert_ir_transform_eq(
+            "
+            entry:
+              %0: ptr = ptr_add 0x8, -16
+              %1: ptr = ptr_add 0xffffffffffffffff, 16
+              black_box %0
+              black_box %1
+            ",
+            |m| opt(m).unwrap(),
+            "
+            ...
+            entry:
+              black_box 0xfffffffffffffff8
+              black_box 0xf
+            ",
+        );
+
+        #[cfg(target_pointer_width = "32")]
+        Module::assert_ir_transform_eq(
+            "
+            entry:
+              %0: ptr = ptr_add 0x8, -16
+              %1: ptr = ptr_add 0xffffffff, 16
+              black_box %0
+              black_box %1
+            ",
+            |m| opt(m).unwrap(),
+            "
+            ...
+            entry:
+              black_box 0xfffffff8
+              black_box 0xf
+            ",
+        );
+
         Module::assert_ir_transform_eq(
             "
           entry:
diff --git a/ykrt/src/compile/jitc_yk/trace_builder.rs b/ykrt/src/compile/jitc_yk/trace_builder.rs
index 22fb41028..a67df3149 100644
--- a/ykrt/src/compile/jitc_yk/trace_builder.rs
+++ b/ykrt/src/compile/jitc_yk/trace_builder.rs
@@ -1144,7 +1144,17 @@ impl<Register: Send + Sync + 'static> TraceBuilder<Register> {
                 self.promote_idx += bytew;
                 Const::Int(tyidx, ArbBitInt::from_u64(*bitw, v))
             }
-            jit_ir::Ty::Ptr => todo!(),
+            jit_ir::Ty::Ptr => {
+                let bytew = ty.byte_size().unwrap();
+                assert_eq!(bytew, std::mem::size_of::<usize>());
+                let v = usize::from_ne_bytes(
+                    self.promotions[self.promote_idx..self.promote_idx + bytew]
+                        .try_into()
+                        .unwrap(),
+                );
+                self.promote_idx += bytew;
+                Const::Ptr(v)
+            }
             jit_ir::Ty::Func(_) => todo!(),
             jit_ir::Ty::Float(_) => todo!(),
             jit_ir::Ty::Unimplemented(_) => todo!(),
diff --git a/ykrt/src/promote.rs b/ykrt/src/promote.rs
index 0bd7371fe..9bc09ec57 100644
--- a/ykrt/src/promote.rs
+++ b/ykrt/src/promote.rs
@@ -4,7 +4,7 @@
 //! right method in this module to call.
 
 use crate::mt::MTThread;
-use std::ffi::{c_int, c_longlong, c_uint};
+use std::ffi::{c_int, c_longlong, c_uint, c_void};
 
 /// Promote a `c_int` during trace recording.
 #[no_mangle]
@@ -49,6 +49,16 @@ pub extern "C" fn __yk_promote_usize(val: usize) -> usize {
     val
 }
 
+/// Promote a pointer during trace recording.
+#[no_mangle]
+pub extern "C" fn __yk_promote_ptr(val: *const c_void) -> *const c_void {
+    MTThread::with_borrow_mut(|mtt| {
+        // We ignore the return value as we can't really cancel tracing from this function.
+        mtt.promote_usize(val as usize);
+    });
+    val
+}
+
 /// Records the return value of an idempotent function during trace recording.
 #[no_mangle]
 pub extern "C" fn __yk_idempotent_promote_usize(val: usize) -> usize {


Reviews:
user: login: ltratt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:33:42Z
user: login: ltratt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:34:25Z
user: login: vext01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:36:35Z
user: login: ltratt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:37:24Z
user: login: ltratt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:38:10Z
user: login: ltratt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:38:29Z
user: login: vext01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:45:59Z
user: login: ltratt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:47:37Z
user: login: vext01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:49:42Z
user: login: ltratt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:50:05Z
user: login: ltratt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T22:32:45Z
user: login: vext01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:43:52Z
user: login: vext01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:44:07Z
user: login: vext01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:46:27Z
user: login: vext01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:47:14Z
user: login: vext01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:47:47Z
user: login: ltratt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T14:09:31Z
user: login: vext01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T14:30:07Z

Review comments:
diff_hunk: @@ -1143,6 +1149,15 @@ impl LSRegAlloc<'_> {
                 }
                 _ => todo!("{bits}"),
             },
+            SpillState::ConstPtr(v) => {
+                // unwrap cannot fail since pointers are sized.
+                let bitw = self.m.type_(self.m.ptr_tyidx()).bitw().unwrap();
+
+                match bitw {
path: ykrt/src/compile/jitc_yk/codegen/x64/lsregalloc.rs
user: login: ltratt
body: This is an `assert` masquerading as a `todo` I think?
created_at: 2025-03-06T16:33:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2609,7 +2610,11 @@ impl<'a> Assemble<'a> {
                             );
                         }
                         VarLocation::ConstInt { bits, v } => {
-                            self.ra.assign_const(iidx, bits, v);
+                            self.ra.assign_const_int(iidx, bits, v);
+                        }
+                        VarLocation::ConstPtr(v) => {
+                            // unwrap can't fail because pointers are sized.
path: ykrt/src/compile/jitc_yk/codegen/x64/mod.rs
user: login: ltratt
body: There is no `unwrap` here.
created_at: 2025-03-06T16:34:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1143,6 +1149,15 @@ impl LSRegAlloc<'_> {
                 }
                 _ => todo!("{bits}"),
             },
+            SpillState::ConstPtr(v) => {
+                // unwrap cannot fail since pointers are sized.
+                let bitw = self.m.type_(self.m.ptr_tyidx()).bitw().unwrap();
+
+                match bitw {
path: ykrt/src/compile/jitc_yk/codegen/x64/lsregalloc.rs
user: login: vext01
body: Yeah, just the result of copying code from elsewhere :) Will fix.
created_at: 2025-03-06T16:36:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -782,7 +782,20 @@ impl Opt {
         loop {
             off += pa_inst.off();
             match self.an.op_map(&self.m, pa_inst.ptr(&self.m)) {
-                Operand::Const(_) => todo!(),
+                Operand::Const(cidx) => {
+                    let Const::Ptr(cptr) = self.m.const_(cidx) else {
+                        panic!();
+                    };
+                    // Important details:
+                    //  - offsets can be negative
+                    //  - LLVM semantics require the offset to be sign extended or truncated to
+                    //    the "pointer index type" before computing the offset.
+                    let cidx = self
+                        .m
+                        .insert_const(Const::Ptr((*cptr as isize + off as isize) as usize))?;
path: ykrt/src/compile/jitc_yk/opt/mod.rs
user: login: ltratt
body: We avoid `as` if at all possible. These should probably be `isize::try_from(...).unwrap()` (etc.) and you will need to decide if you want `saturating_add` or `wrapped_add` etc. to deal with poison cases. `+` will bork in debug mode when it detects overflow, which is not what we want, particularly as these situations do happen because `select` later gets rid of the poisoned value.
created_at: 2025-03-06T16:37:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1762,6 +1776,12 @@ mod test {
 
     #[test]
     fn opt_ptradd() {
+        // FIXME: The offset operand of ptr_add should allow specifying a type (e.g. `ptr_add %0,
path: ykrt/src/compile/jitc_yk/opt/mod.rs
user: login: ltratt
body: This will need to be fixed in this PR. We need to make the optimiser tests as complete as possible, because reasoning about it is already beyond my pea brain.
created_at: 2025-03-06T16:38:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1795,6 +1815,24 @@ mod test {
             black_box %0
         ",
         );
+
+        // constant pointer optimisations.
path: ykrt/src/compile/jitc_yk/opt/mod.rs
user: login: ltratt
body: This needs to test all the edge cases too e.g. adding to the max value etc.
created_at: 2025-03-06T16:38:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -782,7 +782,20 @@ impl Opt {
         loop {
             off += pa_inst.off();
             match self.an.op_map(&self.m, pa_inst.ptr(&self.m)) {
-                Operand::Const(_) => todo!(),
+                Operand::Const(cidx) => {
+                    let Const::Ptr(cptr) = self.m.const_(cidx) else {
+                        panic!();
+                    };
+                    // Important details:
+                    //  - offsets can be negative
+                    //  - LLVM semantics require the offset to be sign extended or truncated to
+                    //    the "pointer index type" before computing the offset.
+                    let cidx = self
+                        .m
+                        .insert_const(Const::Ptr((*cptr as isize + off as isize) as usize))?;
path: ykrt/src/compile/jitc_yk/opt/mod.rs
user: login: vext01
body: By `as` casting the offset to isize we truncate or sign extend, as is (I believe) required by LLVM.

This section of the [llvm manual](https://llvm.org/docs/LangRef.html#id238):

> The indices are first converted to offsets in the pointer’s index type. If the currently indexed type is a struct type, the struct offset corresponding to the index is sign-extended or truncated to the pointer index type. Otherwise, the index itself is sign-extended or truncated, and then multiplied by the type allocation size (that is, the size rounded up to the ABI alignment) of the currently indexed type.

I must admit, I find this part of the documentation confusing. I hope I've interpreted it correctly.
created_at: 2025-03-06T16:45:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -782,7 +782,20 @@ impl Opt {
         loop {
             off += pa_inst.off();
             match self.an.op_map(&self.m, pa_inst.ptr(&self.m)) {
-                Operand::Const(_) => todo!(),
+                Operand::Const(cidx) => {
+                    let Const::Ptr(cptr) = self.m.const_(cidx) else {
+                        panic!();
+                    };
+                    // Important details:
+                    //  - offsets can be negative
+                    //  - LLVM semantics require the offset to be sign extended or truncated to
+                    //    the "pointer index type" before computing the offset.
+                    let cidx = self
+                        .m
+                        .insert_const(Const::Ptr((*cptr as isize + off as isize) as usize))?;
path: ykrt/src/compile/jitc_yk/opt/mod.rs
user: login: ltratt
body: The problem with `as` is that if you change the types later, it still "works" but does weird things. It's better to use the full type names, as we do elsewhere. Only if you *really* have to should you use `as` (regrettably sometimes you do have to; I don't *think* this is one of those cases, but I might be wrong).
created_at: 2025-03-06T16:47:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -782,7 +782,20 @@ impl Opt {
         loop {
             off += pa_inst.off();
             match self.an.op_map(&self.m, pa_inst.ptr(&self.m)) {
-                Operand::Const(_) => todo!(),
+                Operand::Const(cidx) => {
+                    let Const::Ptr(cptr) = self.m.const_(cidx) else {
+                        panic!();
+                    };
+                    // Important details:
+                    //  - offsets can be negative
+                    //  - LLVM semantics require the offset to be sign extended or truncated to
+                    //    the "pointer index type" before computing the offset.
+                    let cidx = self
+                        .m
+                        .insert_const(Const::Ptr((*cptr as isize + off as isize) as usize))?;
path: ykrt/src/compile/jitc_yk/opt/mod.rs
user: login: vext01
body: I think we can split this into two cases: truncate and sign-extend.

`todo!` the former, since I don't anticipate seeing that any time soon. `isize::try_from()` the latter.
created_at: 2025-03-06T16:49:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -782,7 +782,20 @@ impl Opt {
         loop {
             off += pa_inst.off();
             match self.an.op_map(&self.m, pa_inst.ptr(&self.m)) {
-                Operand::Const(_) => todo!(),
+                Operand::Const(cidx) => {
+                    let Const::Ptr(cptr) = self.m.const_(cidx) else {
+                        panic!();
+                    };
+                    // Important details:
+                    //  - offsets can be negative
+                    //  - LLVM semantics require the offset to be sign extended or truncated to
+                    //    the "pointer index type" before computing the offset.
+                    let cidx = self
+                        .m
+                        .insert_const(Const::Ptr((*cptr as isize + off as isize) as usize))?;
path: ykrt/src/compile/jitc_yk/opt/mod.rs
user: login: ltratt
body: Works for me!
created_at: 2025-03-06T16:50:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -782,7 +782,20 @@ impl Opt {
         loop {
             off += pa_inst.off();
             match self.an.op_map(&self.m, pa_inst.ptr(&self.m)) {
-                Operand::Const(_) => todo!(),
+                Operand::Const(cidx) => {
+                    let Const::Ptr(cptr) = self.m.const_(cidx) else {
+                        panic!();
+                    };
+                    // Important details:
+                    //  - offsets can be negative
+                    //  - LLVM semantics require the offset to be sign extended or truncated to
+                    //    the "pointer index type" before computing the offset.
+                    let cidx = self
+                        .m
+                        .insert_const(Const::Ptr((*cptr as isize + off as isize) as usize))?;
path: ykrt/src/compile/jitc_yk/opt/mod.rs
user: login: ltratt
body: BTW, in terms of the `+` stuff, the `BinOp::Add` optimisation probably serves as a useful template for this.
created_at: 2025-03-06T22:32:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1143,6 +1149,15 @@ impl LSRegAlloc<'_> {
                 }
                 _ => todo!("{bits}"),
             },
+            SpillState::ConstPtr(v) => {
+                // unwrap cannot fail since pointers are sized.
+                let bitw = self.m.type_(self.m.ptr_tyidx()).bitw().unwrap();
+
+                match bitw {
path: ykrt/src/compile/jitc_yk/codegen/x64/lsregalloc.rs
user: login: vext01
body: fixed in f9da2df36725f8e28714bb64acd9e52604b9c73f
created_at: 2025-03-11T13:43:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2609,7 +2610,11 @@ impl<'a> Assemble<'a> {
                             );
                         }
                         VarLocation::ConstInt { bits, v } => {
-                            self.ra.assign_const(iidx, bits, v);
+                            self.ra.assign_const_int(iidx, bits, v);
+                        }
+                        VarLocation::ConstPtr(v) => {
+                            // unwrap can't fail because pointers are sized.
path: ykrt/src/compile/jitc_yk/codegen/x64/mod.rs
user: login: vext01
body: Fixed in f9da2df36725f8e28714bb64acd9e52604b9c73f
created_at: 2025-03-11T13:44:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -782,7 +782,20 @@ impl Opt {
         loop {
             off += pa_inst.off();
             match self.an.op_map(&self.m, pa_inst.ptr(&self.m)) {
-                Operand::Const(_) => todo!(),
+                Operand::Const(cidx) => {
+                    let Const::Ptr(cptr) = self.m.const_(cidx) else {
+                        panic!();
+                    };
+                    // Important details:
+                    //  - offsets can be negative
+                    //  - LLVM semantics require the offset to be sign extended or truncated to
+                    //    the "pointer index type" before computing the offset.
+                    let cidx = self
+                        .m
+                        .insert_const(Const::Ptr((*cptr as isize + off as isize) as usize))?;
path: ykrt/src/compile/jitc_yk/opt/mod.rs
user: login: vext01
body: Fixed in 9055875dbb7aca0c38f37f40201e87ec2f050ebd with the caveat that in the JIT IR a constant pointer is assumed to be the same size as `usize`, so we have a similar problem as in the last PR.

https://github.com/ykjit/yk/blob/13c330cccbdc750e05d4a032c84a91ed17ee172d/ykrt/src/compile/jitc_yk/jit_ir/mod.rs#L1298

I'm not sure if I should try to tackle this in this PR or not though.
created_at: 2025-03-11T13:46:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1762,6 +1776,12 @@ mod test {
 
     #[test]
     fn opt_ptradd() {
+        // FIXME: The offset operand of ptr_add should allow specifying a type (e.g. `ptr_add %0,
path: ykrt/src/compile/jitc_yk/opt/mod.rs
user: login: vext01
body: Decided that I was talking nonsense in be9345628c86eadce254d0b0d2cf85e96a68a4e1
created_at: 2025-03-11T13:47:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1795,6 +1815,24 @@ mod test {
             black_box %0
         ",
         );
+
+        // constant pointer optimisations.
path: ykrt/src/compile/jitc_yk/opt/mod.rs
user: login: vext01
body: Test added in c9cd29bf8e000fbe6122dabcd5677d8e2dd81bcd
created_at: 2025-03-11T13:47:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -782,7 +782,20 @@ impl Opt {
         loop {
             off += pa_inst.off();
             match self.an.op_map(&self.m, pa_inst.ptr(&self.m)) {
-                Operand::Const(_) => todo!(),
+                Operand::Const(cidx) => {
+                    let Const::Ptr(cptr) = self.m.const_(cidx) else {
+                        panic!();
+                    };
+                    // Important details:
+                    //  - offsets can be negative
+                    //  - LLVM semantics require the offset to be sign extended or truncated to
+                    //    the "pointer index type" before computing the offset.
+                    let cidx = self
+                        .m
+                        .insert_const(Const::Ptr((*cptr as isize + off as isize) as usize))?;
path: ykrt/src/compile/jitc_yk/opt/mod.rs
user: login: ltratt
body: Don't tackle `Ptr(usize)` in this PR: there's an element of original sin in the JIT IR. Fixing it won't be a small job.

In terms of the 9055875 (which -- thanks GitHub! -- I can't add comments to), `unsafe transmute` is worse than `as` :p

Looking at the code after the change, the good news is that we *do* have to use `as` but it's for the safe (in our case...) idiom of "bitcast from X to Y where X and Y are the same bitwidth". So we probably just need:

```
.insert_const(Const::Ptr((*ptr as isize).wrapping_add(off)))?
```
created_at: 2025-03-11T14:09:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -782,7 +782,20 @@ impl Opt {
         loop {
             off += pa_inst.off();
             match self.an.op_map(&self.m, pa_inst.ptr(&self.m)) {
-                Operand::Const(_) => todo!(),
+                Operand::Const(cidx) => {
+                    let Const::Ptr(cptr) = self.m.const_(cidx) else {
+                        panic!();
+                    };
+                    // Important details:
+                    //  - offsets can be negative
+                    //  - LLVM semantics require the offset to be sign extended or truncated to
+                    //    the "pointer index type" before computing the offset.
+                    let cidx = self
+                        .m
+                        .insert_const(Const::Ptr((*cptr as isize + off as isize) as usize))?;
path: ykrt/src/compile/jitc_yk/opt/mod.rs
user: login: vext01
body: Replaced with my new favourite method `cast_unsigned()` in [e1b0376](https://github.com/ykjit/yk/pull/1642/commits/e1b03766fb22ecf45d0b1f914e46ca5bf46813d6)
created_at: 2025-03-11T14:30:07Z
author_association: CONTRIBUTOR

PR comments:
user: login: vext01
created_at: 2025-03-06T16:33:13Z
body: @ltratt you could help me with a test for the `heapvalues.rs` change?

I had a brief look, but I don't know this part of the system at all.
author_association: CONTRIBUTOR
user: login: ltratt
created_at: 2025-03-06T16:38:59Z
body: This looks good! I do have several comments, mostly because the optimiser has to be kept in tip-top condition. But this is already a solid start!
author_association: CONTRIBUTOR
user: login: ltratt
created_at: 2025-03-11T14:10:12Z
body: Only one comment left!
author_association: CONTRIBUTOR
user: login: vext01
created_at: 2025-03-11T14:48:55Z
body: OK to squash?
author_association: CONTRIBUTOR
user: login: ltratt
created_at: 2025-03-11T14:49:06Z
body: Please squash.
author_association: CONTRIBUTOR
user: login: vext01
created_at: 2025-03-11T15:12:58Z
body: squashed.
author_association: CONTRIBUTOR
user: login: vext01
created_at: 2025-03-11T16:07:09Z
body: force pushed formatting fix.
author_association: CONTRIBUTOR