The article explores unexpected behavior in Common Lisp's SBCL where array sub-typing works for integer types but fails for constrained types like (unsigned-byte 16), attributed to compiler optimizations and upgraded array element types.
# Unexpected (to me) behaviour in Lisp sub-typing
Source: [https://simondobson.org/2026/08/07/strange-behaviour-in-lisp-sub-typing/](https://simondobson.org/2026/08/07/strange-behaviour-in-lisp-sub-typing/)
I just encountered some unexpected behaviour in how Common Lisp \(or at least[SBCL](https://sbcl.org/)\) handles array sub\-typing\.
Suppose you have a structure declaration like this:
```
(defstruct A
(a #1A() :type (simple-array integer)))
```
We can create instances of this struct by providing an initial value, and it will be type\-checked\.
```
#S(A :A #(1 2 3))
```
We can also omit the argument and get the default specified in the declaration:
```
#S(A :A #())
```
So far so good\. But suppose we now specialise the`simple\-array`type against a type such as`\(unsigned\-byte 16\)`\(the type of unsigned 16\-bit integers\)\.
```
(defstruct B
(b #1A() :type (simple-array (unsigned-byte 16))))
(make-b :b #1A(1 2 3))
```
This generates an error, as does using the default value\.
Why is this? I eventually tracked the problem down to the fact that the expression`\#1A\(\)`is given type`\(simple\-vector 0\)`while the expression`\#1A\(1 2 3\)`is given type`\(simple\-vector 3\)`, and neither of these are sub\-types of`\(simple\-array \(unsigned\-byte 16\)\)`:
```
(SIMPLE-VECTOR 0)
```
```
(SIMPLE-VECTOR 3)
```
```
(typep #1A(1 2 3) '(simple-vector 3))
```
```
T
```
```
(typep #1A(1 2 3) '(simple-array (unsigned-byte 16)))
```
```
NIL
```
So the typing works for`integer`\(and indeed for`number`and`float`\) but not for the constrained types like`\(unsigned\-byte 16\)`– although it*does*work for`unsigned\-byte`\(with no width\)\.
Interestingly the tests fail for empty arrays as well:
```
(typep #1A() '(simple-array (unsigned-byte 16)))
```
```
NIL
```
I assume that the behaviour is because the compiler is at liberty to optimise the storage of arrays with specific types, and doing so makes it awkward to compare against types like`simple\-vector`that don’t contain their element type\. This would also explain why the check succeeds for types like`integer`, whose values are stored boxed and so can represented uniformly\.
There’s a simple way around this, of course, once you know what the problem is: make the type of the value explicit rather than relying on the reader macro\. For example:
```
(defstruct C
(c (make-array '(0) :element-type '(unsigned-byte 16)) :type (simple-array (unsigned-byte 16))))
(make-c)
```
```
#S(C :C #())
```
and also:
```
(make-c :c (make-array '(3) :element-type '(unsigned-byte 16) :initial-contents (alexandria:iota 3)))
```
```
#S(C :C #(0 1 2))
```
I can’t find this behaviour detailed in the manuals \(or anywhere else\) though\. However, it seems to be related to the*upgraded array element type*\. When an array is created, the implementation is allowed to use a different type to the one specified in its representation, which must be a supertype of the type requested\. For types like`integer`and`unsigned\-byte`this is`t`, the top type in the lattice:
```
(upgraded-array-element-type 'unsigned-byte)
```
```
T
```
But for the “constrained” types the upgraded type is the type itself:
```
(upgraded-array-element-type '(unsigned-byte 16))
```
```
(UNSIGNED-BYTE 16)
```
This suggests to me that the compiler \(SBCL\) is indeed optimising the representation and then also performing more detailed type\-checking for elements\.
The article explains the confusing behavior of C array types, including their decay to pointers, exceptions like sizeof and function parameters, and compares it to function types, suggesting a mental model where arrays and pointers are strictly separated.
A developer discovers that swapping two equivalent macros caused unexpected integer changes in unrelated functions, and the blog post investigates the mystery, questioning an LLM's explanation about control flow guard.
The article explores the Liskov Substitution Principle beyond its common interpretation, emphasizing its formal basis in subtyping with preconditions, postconditions, invariants, and history properties, and citing original research papers.
A technical blog post exploring how to use SBCL as a breadboard for assembly code, focusing on stack-based virtual machine techniques such as rotating stacks and efficient primop dispatch, with references to the F18 processor and x87 stack.
This article discusses a fundamental memory safety challenge involving tagged unions where a pointer to one variant is used after the union is overwritten with a different variant, leading to type confusion. The author also argues that buffer overflows are the most exploitable memory error and could have been mitigated with better array syntax.