Remix.run Logo
skulk 3 days ago

FWIW, SBCL is pretty good at optimizing away dynamic type checks if you help it out.

Here are some examples under:

    (declaim (optimize (speed 2)))
First example is a generic multiplication. x and y could be _any_ type at all.

    (defun fn (x y) (* x y))
If we disassemble this function, we get the following:

    ; disassembly for FN
    ; Size: 34 bytes. Origin: #x1001868692                        ; FN
    ; 92:       488975F8         MOV [RBP-8], RSI
    ; 96:       4C8945F0         MOV [RBP-16], R8
    ; 9A:       498BD0           MOV RDX, R8
    ; 9D:       488BFE           MOV RDI, RSI
    ; A0:       FF142540061050   CALL [#x50100640]                ; SB-VM::GENERIC-*
    ; A7:       4C8B45F0         MOV R8, [RBP-16]
    ; AB:       488B75F8         MOV RSI, [RBP-8]
    ; AF:       C9               LEAVE
    ; B0:       F8               CLC
    ; B1:       C3               RET
    ; B2:       CC0F             INT3 15                          ; Invalid argument count trap
Note that it calls `GENERIC-*` which probably checks a lot of things and has a decent overhead.

Now, if we tell it that x and y are bytes, it's going to give us much simpler code.

    (declaim (ftype (function ((unsigned-byte 8) (unsigned-byte 8)) (unsigned-byte 16)) fn-t))
    (defun fn-t (x y) (* x y))
The resulting code uses the imul instruction.

    ; disassembly for FN-T
    ; Size: 15 bytes. Origin: #x1001868726                        ; FN-T
    ; 26:       498BD0           MOV RDX, R8
    ; 29:       48D1FA           SAR RDX, 1
    ; 2C:       480FAFD7         IMUL RDX, RDI
    ; 30:       C9               LEAVE
    ; 31:       F8               CLC
    ; 32:       C3               RET
    ; 33:       CC0F             INT3 15                          ; Invalid argument count trap*
matheusmoreira 2 days ago | parent [-]

Can SBCL actually check at compile time that the arguments to fn-t are bytes? I wonder how that works with Lisp's extreme dynamism. Also wondering about the calling convention it uses.

reikonomusha 2 days ago | parent [-]

It can detect simple-ish instances, like calling

    (fn-t "hello" "world")
But a good rule-of-thumb is that these compile-time type errors are more of a courtesy, rather than a guarantee. As soon as you abstract over fn-t with another function, like so:

    (defun g (x y)
      (fn-t x y))
and proceed to use g in your code, all the static checking won't happen anymore, because as far as g is concerned, it can take any input argument types.

    CL-USER> (defun will-it-type-error? ()
               (g "x" "y"))
    ;; compilation WILL-IT-TYPE-ERROR? successful
No compile-time warning is issued. Contrast with Coalton:

    COALTON-USER> (coalton-toplevel
                    (declare fn-t (U8 -> U8 -> U8))
                    (define (fn-t x y)
                      (* x y))
                    
                    (define (g x y)
                      (fn-t x y))
                    
                    (define (will-it-type-error?)
                      (g "hello" "world")))

    error: Type mismatch
      --> <macroexpansion>:8:7
       |
     8 |      (G "hello" "world")))
       |         ^^^^^^^ Expected type 'U8' but got 'STRING'
       [Condition of type COALTON-IMPL/TYPECHECKER/BASE:TC-ERROR]