255 lines
12 KiB
Plaintext
255 lines
12 KiB
Plaintext
=== Goal
|
|
|
|
A memory-safe language with a simple translator to x86 that can be feasibly
|
|
written without itself needing a translator.
|
|
|
|
Memory-safe: it should be impossible to:
|
|
a) create a pointer out of arbitrary data, or
|
|
b) to access heap memory after it's been freed.
|
|
|
|
Simple: do all the work in a 2-pass translator:
|
|
Pass 1: check each instruction's types in isolation.
|
|
Pass 2: emit code for each instruction in isolation.
|
|
|
|
=== Overview of the language
|
|
|
|
A program consists of a series of type, function and global variable declarations.
|
|
(Also constants and tests, but let's focus on these.)
|
|
|
|
Type declarations basically follow Hindley-Milner with product and (tagged) sum
|
|
types. Types are written in s-expression form. There's a `ref` type that's a
|
|
type-safe fat pointer, with an alloc id that gets incremented after each
|
|
allocation. Memory allocation and reclamation is manual. Dereferencing a ref
|
|
after its underlying memory is reclaimed (pointer alloc id no longer matches
|
|
payload alloc id) is guaranteed to immediately kill the program (like a
|
|
segfault).
|
|
|
|
# product type
|
|
type foo [
|
|
x : int
|
|
y : (ref int)
|
|
z : bar
|
|
]
|
|
|
|
# sum type
|
|
choice bar [
|
|
x : int
|
|
y : point
|
|
]
|
|
|
|
Functions have a header and a series of instructions in the body:
|
|
|
|
fn f a : int -> b : int [
|
|
...
|
|
]
|
|
|
|
Instructions have the following format:
|
|
|
|
io1, io2, ... <- operation i1, i2, ...
|
|
|
|
i1, i2 operands on the right hand side are immutable. io1, io2 are in-out
|
|
operands. They're written to, and may also be read.
|
|
|
|
User-defined functions will be called with the same syntax. They'll translate
|
|
to a sequence of push instructions (one per operand, both in and in-out), a
|
|
call instruction, and a sequence of pop instructions, either to a black hole
|
|
(in operands) or a location (in-out operands). This follows the standard Unix
|
|
calling convention. Each operand needs to be something push/pop can accept.
|
|
|
|
Primitive operations depend on the underlying processor. We'd like each primitive
|
|
operation supported by the language to map to a single instruction in the ISA.
|
|
Sometimes we have to violate that (see below), but we definitely won't be
|
|
writing to any temporary locations behind the scenes. The language affords
|
|
control over registers, and tracking unused registers gets complex, and
|
|
besides we may have no unused registers at a specific point. Instructions only
|
|
modify their operands.
|
|
|
|
In most ISAs, instructions operate on at most a word of data at a time. They
|
|
also tend to not have more than 2-3 operands, and not modify more than 2
|
|
locations in memory.
|
|
|
|
Since the number of reads from memory is limited, we break up complex high-level
|
|
operations using a special type called `address`. Addresses are strictly
|
|
short-term entities. They can't be stored in a compound type, and they can't
|
|
be passed into or returned from a user-defined function. They also can't be
|
|
used after a function call (because it could free the underlying memory) or
|
|
label (because it gets complex to check control flow, and we want to translate
|
|
each instruction simply and in isolation).
|
|
|
|
=== Compilation to 32-bit x86
|
|
|
|
Values can be stored:
|
|
in code (literals)
|
|
in registers
|
|
on the stack
|
|
on the global segment
|
|
|
|
Variables on the stack are stored at *(ESP+n)
|
|
Global variables are stored at *disp32, where disp32 is statically known
|
|
|
|
Address variables have to be in a register.
|
|
- You need them in a register to do a lookup, and
|
|
- Saving them to even the stack increases the complexity of checks needed on
|
|
function calls or labels.
|
|
|
|
Compilation proceeds by pattern matching over an instruction along with
|
|
knowledge about the types of its operands, as well as where they're stored
|
|
(register/stack/global). We now enumerate mappings for various categories of
|
|
instructions, based on the type and location of their operands.
|
|
|
|
Where types of operands aren't mentioned below, all operands of an instruction
|
|
should have the same (word-length) type.
|
|
|
|
Lots of special cases because of limitations of the x86 ISA. Beware.
|
|
|
|
A. x : int <- add y
|
|
|
|
Requires y to be scalar. Result will always be an int. No pointer arithmetic.
|
|
|
|
reg <- add literal => 81 0/subop 3/mod ...(0)
|
|
reg <- add reg => 01 3/mod ...(1)
|
|
reg <- add stack => 03 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 reg/r32 ...(2)
|
|
reg <- add global => 03 0/mod 5/rm32/include-disp32 global/disp32 reg/r32 ...(3)
|
|
stack <- add literal => 81 0/subop 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 literal/imm32 ...(4)
|
|
stack <- add reg => 01 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 reg/r32 ...(5)
|
|
stack <- add stack => disallowed
|
|
stack <- add global => disallowed
|
|
global <- add literal => 81 0/subop 0/mod 5/rm32/include-disp32 global/disp32 literal/imm32 ...(6)
|
|
global <- add reg => 01 0/mod 5/rm32/include-disp32 global/disp32 reg/r32 ...(7)
|
|
global <- add stack => disallowed
|
|
global <- add global => disallowed
|
|
|
|
Similarly for sub, and, or, xor and even copy. Replace the opcodes above with corresponding ones from this table:
|
|
|
|
add sub and or xor copy/mov
|
|
reg <- op literal 81 0/subop 81 5/subop 81 4/subop 81 1/subop 81 6/subop c7
|
|
reg <- op reg 01 or 03 29 or 2b 21 or 23 09 or 0b 31 or 33 89 or 8b
|
|
reg <- op stack 03 2b 23 0b 33 8b
|
|
reg <- op global 03 2b 23 0b 33 8b
|
|
stack <- op literal 81 0/subop 81 5/subop 81 4/subop 81 1/subop 81 6/subop c7
|
|
stack <- op reg 01 29 21 09 31 89
|
|
global <- op literal 81 0/subop 81 5/subop 81 4/subop 81 1/subop 81 6/subop c7
|
|
global <- op reg 01 29 21 09 31 89
|
|
|
|
B. x/reg : int <- mul y
|
|
|
|
Requires both y to be scalar.
|
|
x must be in a register. Multiplies can't write to memory.
|
|
|
|
reg <- mul literal => 69 ...(8)
|
|
reg <- mul reg => 0f af 3/mod ...(9)
|
|
reg <- mul stack => 0f af 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 reg/r32 ...(10)
|
|
reg <- mul global => 0f af 0/mod 5/rm32/include-disp32 global/disp32 reg/r32 ...(11)
|
|
|
|
C. x/EAX/quotient : int, y/EDX/remainder : int <- idiv z # divide EAX by z; store the result in EAX and EDX
|
|
|
|
Requires source x and z to both be scalar.
|
|
x must be in EAX and y must be in EDX. Divides can't write anywhere else.
|
|
|
|
First clear EDX (we don't support ints larger than 32 bits):
|
|
31/xor 3/mod 2/rm32/EDX 2/r32/EDX
|
|
|
|
then:
|
|
EAX, EDX <- idiv literal => disallowed
|
|
EAX, EDX <- idiv reg => f7 7/subop 3/mod ...(12)
|
|
EAX, EDX <- idiv stack => f7 7/subop 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 ...(13)
|
|
EAX, EDX <- idiv global => f7 7/subop 0/mod 5/rm32/include-disp32 global/disp32 reg/r32 ...(14)
|
|
|
|
D. x : int <- not
|
|
|
|
Requires x to be an int.
|
|
|
|
reg <- not => f7 3/mod ...(15)
|
|
stack <- not => f7 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 ...(16)
|
|
global <- not => f7 0/mod 5/rm32/include-disp32 global/disp32 reg/r32 ...(17)
|
|
|
|
E. x : (address t) <- get o : T, %f
|
|
|
|
(Assumes T.f has type t.)
|
|
|
|
o can't be on a register since it's a non-primitive (likely larger than a word)
|
|
f is a literal
|
|
x must be in a register (by definition for an address)
|
|
|
|
below '*' works on either address or ref types
|
|
|
|
For raw stack values we want to read *(ESP+n)
|
|
For raw global values we want to read *disp32
|
|
For address stack values we want to read *(ESP+n)+
|
|
*(ESP+n) contains an address
|
|
so we want to compute *(ESP+n) + literal
|
|
|
|
reg1 <- get reg2, literal => 8d/lea 1/mod reg2/rm32 literal/disp8 reg1/r32 ...(18)
|
|
reg <- get stack, literal => 8d/lea 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n+literal/disp8 reg/r32 ...(19)
|
|
(simplifying assumption: stack frames can't be larger than 256 bytes)
|
|
reg <- get global, literal => 8d/lea 0/mod 5/rm32/include-disp32 global+literal/disp32, reg/r32 ...(20)
|
|
|
|
F. x : (offset T) <- index i : int, %size(T)
|
|
|
|
reg1 <- index reg2, literal => 69/mul 3/mod reg2/rm32 literal/imm32 -> reg1/r32
|
|
or 68/mul 3/mod reg2/rm32 literal/imm8 -> reg1/r32 ...(21)
|
|
reg1 <- index stack, literal => 69/mul 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n/disp8 literal/imm32 -> reg1/r32 ...(22)
|
|
reg1 <- index global, literal => 69/mul 0/mod 5/rm32/include-disp32 global/disp32 literal/imm32 -> reg1/r32 ...(23)
|
|
|
|
optimization: avoid multiply if literal is a power of 2
|
|
use SIB byte if literal is 2, 4 or 8
|
|
or left shift
|
|
|
|
G. x : (address T) <- advance o : (array T), idx : (offset T)
|
|
|
|
reg <- advance a/reg, idx/reg => 8d/lea 0/mod 4/rm32/SIB a/base idx/index 0/scale reg/r32 ...(24)
|
|
reg <- advance stack, literal => 8d/lea 1/mod 4/rm32/SIB 4/base/ESP 4/index/none 0/scale n+literal/disp8 reg/r32 ...(25)
|
|
reg <- advance stack, reg2 => 8d/lea 1/mod 4/rm32/SIB 4/base/ESP reg2/index 0/scale n/disp8 reg/r32 ...(26)
|
|
reg <- advance global, literal => 8d/lea 0/mod 5/rm32/include-disp32 global+literal/disp32, reg/r32 ...(27)
|
|
|
|
also instructions for runtime bounds checking
|
|
|
|
=== Example
|
|
|
|
Putting it all together: code generation for `a[i].y = 4` where a is an array
|
|
of 2-d points with x, y coordinates.
|
|
|
|
If a is allocated on the stack, say of type (array point 6) at (ESP+4):
|
|
|
|
offset/EAX : (offset point) <- index i, 8 # (22)
|
|
tmp/EBX : (address point) <- advance a : (array point 6), offset/EAX # (26)
|
|
tmp2/ECX : (address number) <- get tmp/EBX : (address point), 4/y # (18)
|
|
*tmp2/ECX <- copy 4 # (5 for copy/mov with 0 disp8)
|
|
|
|
Many instructions, particularly variants of 'get' and 'advance' -- end up encoding the exact same instructions.
|
|
But the types differ, and the type-checker checks them differently.
|
|
|
|
=== Advanced checks
|
|
|
|
Couple of items require inserting mapping to multiple instructions:
|
|
bounds checking against array length in 'advance'
|
|
dereferencing 'ref' types (see type list up top)
|
|
|
|
A. Dereferencing a ref
|
|
|
|
tmp/EDX <- advance *s, tmp0/EDI
|
|
=> compare (ESP+4), *(ESP+8) ; '*' from compiler2
|
|
jump-unless-equal panic
|
|
EDX <- add ESP, 8
|
|
EDX <- copy *EDX
|
|
EDX <- add EDX, 4
|
|
EDX <- 8d/lea EDX + result
|
|
|
|
=== More speculative ideas
|
|
|
|
Initialize data segment with special extensible syntax for literals. All
|
|
literals except numbers and strings start with %.
|
|
|
|
%size(type) => compiler replaces with size of type
|
|
%point(3, 4) => two words
|
|
|
|
and so on.
|
|
|
|
=== Credits
|
|
|
|
Forth
|
|
C
|
|
Rust
|
|
Lisp
|
|
qhasm
|