master: x86-64, struct-by-value: correct ABI classification for nested structs

stassats via Sbcl-commits <[email protected]>
Newsgroups gmane.lisp.steel-bank.cvs
Message-ID <[email protected]>
The branch "master" has been updated in SBCL:
       via  314b572626e177fc9c7625e26ef629cfe2b190c8 (commit)
      from  f484412bc4e3ebbbd2eedf83608a29aed6dc4e9c (commit)

- Log -----------------------------------------------------------------
commit 314b572626e177fc9c7625e26ef629cfe2b190c8
Author: Jesse Bouwman <[email protected]>
Date:   Sun Apr 26 17:25:24 2026 -0700

    x86-64, struct-by-value: correct ABI classification for nested structs
---
 src/compiler/x86-64/c-call.lisp         | 86 +++++++++++++--------------------
 tests/alien-struct-by-value.c           | 13 +++++
 tests/alien-struct-by-value.impure.lisp | 26 ++++++++++
 3 files changed, 73 insertions(+), 52 deletions(-)

diff --git a/src/compiler/x86-64/c-call.lisp b/src/compiler/x86-64/c-call.lisp
index bd4053c11..7d20f433d 100644
--- a/src/compiler/x86-64/c-call.lisp
+++ b/src/compiler/x86-64/c-call.lisp
@@ -127,29 +127,14 @@
 
 #-win32
 (defun classify-field-sysv-amd64 (type)
-  "Classify a single field type for SysV AMD64 ABI.
-   Returns :INTEGER, :DOUBLE, or :MEMORY."
+  "Classify a leaf scalar field for SysV AMD64 ABI.
+   Returns :INTEGER, :DOUBLE, or :MEMORY.  Aggregates (records and
+   arrays) are flattened by CLASSIFY-STRUCT and never reach here."
   (cond
-    ;; Check specific types first, before general type checks
     ((sb-alien::alien-integer-type-p type) :integer)
     ((sb-alien::alien-pointer-type-p type) :integer)
     ((sb-alien::alien-single-float-type-p type) :double)
     ((sb-alien::alien-double-float-type-p type) :double)
-    ;; Arrays are classified by their element type
-    ((sb-alien::alien-array-type-p type)
-     (let ((element-type (sb-alien::alien-array-type-element-type type)))
-       (classify-field-sysv-amd64 element-type)))
-    ;; Nested struct - recursively classify and inherit eightbyte classes
-    ((sb-alien::alien-record-type-p type)
-     (let ((nested (classify-struct type)))
-       (if (sb-alien::struct-classification-memory-p nested)
-           :memory
-           ;; Merge all slots from nested struct to get dominant class
-           ;; e.g., struct { double d; } should contribute :double, not :integer
-           (reduce #'merge-classes
-                   (sb-alien::struct-classification-register-slots nested)
-                   :initial-value :no-class))))
-    ;; System-area-pointer (must come after array/record checks)
     ((typep type 'sb-alien::alien-system-area-pointer-type) :integer)
     (t :memory)))
 
@@ -170,7 +155,10 @@
 #-win32
 (defun classify-struct (record-type)
   "Classify struct for x86-64 System V ABI return.
-   Returns STRUCT-CLASSIFICATION."
+   Returns STRUCT-CLASSIFICATION.
+
+   Walks fields recursively, descending into nested records and arrays
+   so each leaf scalar contributes to the eightbyte it lands in."
   (let* ((bits (sb-alien::alien-type-bits record-type))
          (byte-size (ceiling bits 8))
          (alignment (sb-alien::alien-type-alignment record-type)))
@@ -183,24 +171,35 @@
          :alignment alignment
          :memory-p t)))
 
-    ;; Classify each eightbyte
     (let* ((num-eightbytes (max 1 (ceiling byte-size 8)))
            (eightbytes (make-list num-eightbytes :initial-element :no-class)))
-      ;; Iterate through fields and classify
-      (dolist (field (sb-alien::alien-record-type-fields record-type))
-        (let* ((field-offset-bits (sb-alien::alien-record-field-offset field))
-               (field-type (sb-alien::alien-record-field-type field))
-               (field-bits (sb-alien::alien-type-bits field-type))
-               (field-offset-bytes (floor field-offset-bits 8))
-               (field-size-bytes (ceiling field-bits 8))
-               (field-class (classify-field-sysv-amd64 field-type)))
-          ;; Apply class to all eightbytes this field spans
-          (loop for byte-offset from field-offset-bytes below (+ field-offset-bytes field-size-bytes) by 8
-                for eightbyte-index = (floor byte-offset 8)
-                when (< eightbyte-index num-eightbytes)
-                do (setf (nth eightbyte-index eightbytes)
-                         (merge-classes (nth eightbyte-index eightbytes)
-                                        field-class)))))
+      (labels ((merge-leaf (offset-bytes size-bytes class)
+                 (loop for byte-offset from offset-bytes
+                       below (+ offset-bytes size-bytes)
+                       by 8
+                       for eb = (floor byte-offset 8)
+                       when (< eb num-eightbytes)
+                       do (setf (nth eb eightbytes)
+                                (merge-classes (nth eb eightbytes) class))))
+               (walk (type offset-bytes)
+                 (cond
+                   ((sb-alien::alien-record-type-p type)
+                    (dolist (field (sb-alien::alien-record-type-fields type))
+                      (walk (sb-alien::alien-record-field-type field)
+                            (+ offset-bytes
+                               (floor (sb-alien::alien-record-field-offset field) 8)))))
+                   ((sb-alien::alien-array-type-p type)
+                    (let* ((elt (sb-alien::alien-array-type-element-type type))
+                           (elt-bytes (ceiling (sb-alien::alien-type-bits elt) 8))
+                           (n (or (first (sb-alien::alien-array-type-dimensions type)) 0)))
+                      (dotimes (i n)
+                        (walk elt (+ offset-bytes (* i elt-bytes))))))
+                   ;; Leaf scalar
+                   (t
+                    (merge-leaf offset-bytes
+                                (ceiling (sb-alien::alien-type-bits type) 8)
+                                (classify-field-sysv-amd64 type))))))
+        (walk record-type 0))
 
       ;; Post-merge cleanup per ABI: if second eightbyte is MEMORY, first must be too
       (when (and (> num-eightbytes 1)
@@ -333,23 +332,6 @@ Floats are passed in integer registers."
       (4 (inst movss target (ea offset sap)))
       (8 (inst movsd target (ea offset sap))))))
 
-;;; VOPs for storing struct result registers to memory
-;;; These VOPs store result register values back to memory for struct-by-value returns
-
-(define-vop (store-struct-int-result)
-  (:args (value :scs (unsigned-reg signed-reg))
-         (sap :scs (sap-reg)))
-  (:info offset)
-  (:generator 5
-    (inst mov :qword (ea offset sap) value)))
-
-(define-vop (store-struct-sse-result)
-  (:args (value :scs (double-reg single-reg))
-         (sap :scs (sap-reg)))
-  (:info offset)
-  (:generator 5
-    (inst movsd (ea offset sap) value)))
-
 ;;; VOP to copy a full or partial qword from struct SAP to the C
 ;;; argument stack Used for passing large structs (>16 bytes) by value
 
diff --git a/tests/alien-struct-by-value.c b/tests/alien-struct-by-value.c
index 9eb00d782..a77cd7136 100644
--- a/tests/alien-struct-by-value.c
+++ b/tests/alien-struct-by-value.c
@@ -414,3 +414,16 @@ long long call_with_small_union(small_union_callback cb, long long val) {
 union small_union call_returning_small_union(small_union_return_callback cb, long long val) {
   return cb(val);
 }
+
+/*
+ * Nested struct classification fixture for call-by-value: The inner
+ * struct of nest_di spans both eightbytes of the outer: the SysV
+ * classifier must walk it field-by-field rather than collapsing it to
+ * a single class.
+ */
+struct inner_di { double d; int i; };
+struct nest_di { struct inner_di inner; };
+struct nest_di nest_di_make(double d, int i) {
+  struct nest_di s; s.inner.d = d; s.inner.i = i; return s;
+}
+double nest_di_sum(struct nest_di s) { return s.inner.d + (double)s.inner.i; }
diff --git a/tests/alien-struct-by-value.impure.lisp b/tests/alien-struct-by-value.impure.lisp
index 8b9bd805b..c845d43e2 100644
--- a/tests/alien-struct-by-value.impure.lisp
+++ b/tests/alien-struct-by-value.impure.lisp
@@ -760,3 +760,29 @@
                  1.5d0 2.5d0)))
     (assert (= (slot result 'd0) 1.5d0))
     (assert (= (slot result 'd1) 2.5d0))))
+
+;;; Test that an outer struct wrapping a nested {double, int} is
+;;; classified correctly and matches SysV ABI expectations.
+;;;
+;;; Without recursive flattening the nested struct collapses to
+;;; :INTEGER, both slots come out :INTEGER, and a double is misrouted
+;;; through a GPR.
+(define-alien-type nil (struct inner-di (d double) (i int)))
+(define-alien-type nil (struct nest-di (inner (struct inner-di))))
+(define-alien-routine nest-di-make (struct nest-di) (d double) (i int))
+(define-alien-routine nest-di-sum double (s (struct nest-di)))
+
+#+(and x86-64 (not win32))
+(with-test (:name :struct-by-value-nested-classifier)
+  (let* ((type (sb-alien::parse-alien-type '(struct nest-di) nil))
+         (cls  (sb-vm::classify-struct type)))
+    (assert (equal (sb-alien::struct-classification-register-slots cls)
+                   '(:double :integer)))
+    (assert (= (sb-alien::struct-classification-size cls) 16))
+    (assert (not (sb-alien::struct-classification-memory-p cls)))))
+
+(with-test (:name :struct-by-value-nested-double-int)
+  (let ((s (nest-di-make 3.5d0 7)))
+    (assert (= (slot (slot s 'inner) 'd) 3.5d0))
+    (assert (= (slot (slot s 'inner) 'i) 7))
+    (assert (= (nest-di-sum s) 10.5d0))))

-----------------------------------------------------------------------


hooks/post-receive
-- 
SBCL
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.