Skip to content
Will Clinger edited this page Jun 24, 2017 · 5 revisions

The Great Primop Cleanup

In Larceny, a primop is one of the operations that can be performed by an op1, op2, op2imm, or op3 instruction. Most primops correspond to basic Scheme procedures that operate in constant time.

In the original version of Larceny, the machine code generated for a primop was responsible for performing all runtime safety checks that are expected of the corresponding Scheme procedure. Later, to expose these runtime checks to compiler optimization, we added trusted and semi-trusted versions for the most common primops, while retaining the fully checked versions of the primops.

Twobit relies on a compiler table (Compiler/common.imp.sch) that guards the most common primops with explicit runtime checks. Only the trusted versions of those primops are ever called, so the other versions can be dropped from our code generators along with any supporting code. For some targets (Larceny/x86, Petit Larceny, and Common Larceny) the trusted version is often the same as the untrusted version, which means the runtime checks are being performed twice when Twobit cannot remove them through optimization, and are being performed once when Twobit has proved they don't need to be performed at all.

Cleaning this up will simplify our code generators, make them more reliable (since there will be less code to test), and make programs run faster. It will also simplify implementation of many procedures introduced by the R6RS and R7RS standards.

Data Structures

For heap-allocated data structures, the only primops we really need are:


cons
pair?
car:pair
cdr:pair

make-cell
cell-ref:trusted                  ; aka cell-ref
cell-set!:trusted                 ; aka cell-set!
cell-set!:nwb

make-procedure:trusted            ; make-procedure (IAssassin) includes run-time checking
procedure?
procedure-length:fun              ; not implemented on IAssassin
procedure-ref:trusted             ; procedure-ref (IAssassin) includes run-time checking
procedure-set!:trusted            ; procedure-set! (IAssassin) includes run-time checking

make-vector-like:trusted          ; make-vector (IAssassin) includes run-time checking
vector-like?
vector-like-length:vec            ; vector-like-length (IAssassin) includes run-time checking
vector-like-ref:trusted           ; vector-like-ref (IAssassin) includes run-time checking
vector-like-set!:trusted          ; vector-like-set! (IAssassin) includes run-time checking
vector-like-set!:nwb
vector-like-cas!:trusted          ; not implemented on IAssassin

vector?
rectnum?                          ; not implemented on IAssassin
ratnum?                           ; not implemented on IAssassin
symbol?
port?
structure?

make-bytevector-like:trusted      ; make-bytevector (IAssassin) includes run-time checking
bytevector-like?
bytevector-like-length:bvl
bytevector-like-ref:trusted
bytevector-like-ref16:trusted     ; not implemented on IAssassin
bytevector-like-ref32:trusted     ; not implemented on IAssassin
bytevector-like-ref64:trusted     ; not implemented on IAssassin
bytevector-like-set!:trusted
bytevector-like-set16!:trusted    ; not implemented on IAssassin
bytevector-like-set32!:trusted    ; not implemented on IAssassin
bytevector-like-set64!:trusted    ; not implemented on IAssassin
bytevector-like-cas64!:trusted    ; not implemented on IAssassin

bytevector?
flonum?
compnum?
bignum?                           ; not implemented on IAssassin

make-string:trusted               ; make-string (IAssassin, aka make-ustring) includes run-time checking
string?                           ; aka ustring?
string-length:str                 ; aka ustring-length:str
string-ref:trusted                ; aka ustring-ref:trusted
string-set!:trusted               ; aka ustring-set!:trusted

Note: the names listed above are internal names, which may be different for different target architectures. The external names, which are used by Twobit (and not seen by the back ends) often begin with a dot.

Strings are a special case because they may not be bytevector-like on all target architectures. Even if they are bytevector-like, they are different from other bytevector-like objects because they may contain fully boxed immediates.

Arithmetic and Immediates

We also need:


char?
fixnum?

integer->char:trusted             ; (must be protected by range/exclusion check)
char->integer:chr

<:fix:fix                         ; >:fix:fix implemented by symmetry
=:fix:fix
<=:fix:fix                        ; >=:fix:fix implemented by symmetry

+:idx:idx
-:idx:idx
*:idx:idx                         ; not implemented on IAssassin

+:fix:fix
-:fix:fix
*:fix:fix                         ; not implemented on IAssassin
fxdiv:fix:fix                     ; not implemented on IAssassin
fxmod:fix:fix                     ; not implemented on IAssassin
fxnot:fix:fix                     ; fxlognot (IAssassin) includes run-time checking
fxand:fix:fix                     ; fxlogand (IAssassin) includes run-time checking
fxior:fix:fix                     ; fxlogior (IAssassin) includes run-time checking
fxxor:fix:fix                     ; fxlogxor (IAssassin) includes run-time checking
fxlsh:fix:fix                     ; fxlsh (IAssassin) includes run-time checking
fxrsha:fix:fix                    ; fxrsha (IAssassin) includes run-time checking
fxrshl:fix:fix                    ; fxrshl (IAssassin) includes run-time checking

<:flo:flo                         ; >:flo:flo implemented by symmetry
=:flo:flo
<=:flo:flo                        ; >=:flo:flo implemented by symmetry
<>:flo:flo                        ; not implemented on IAssassin

+:flo:flo
-:flo:flo
*:flo:flo
/:flo:flo

The above list is subject to revision, of course, but it would be good to support all of the above primops on all target architectures. Target-specific local optimization would then make it possible to avoid boxing some intermediate results of arithmetic calculations.

Bignums

The following primops have a different character, and were added to IAssassin only recently. For now, they are used only within src/Lib/Arch/IAssassin/primops.sch, and only within a few procedures. Other architectures can implement those procedures portably without using these primops, so we don't have to implement these primops on all architectures.


bignum-add-step!
bignum-subtract-step!
bignum-multiply-step!

These primops take more than three arguments, so those arguments are passed in general registers and the primop is invoked by an op1 instruction. That means these primops can be used only at the head of a procedure that won't be inlined, lambda-lifted, or closure-converted, or within hand-written .mal code.

Clone this wiki locally