Skip to content

Commit

Permalink
source2il: implement alignment support (#112)
Browse files Browse the repository at this point in the history
## Summary

Implement internal support for locations with an alignment requirement
greater than 1 byte. This is a preparation for the source language
gaining type alignment requirements.

## Details

The alignment for numeric types (int and float) is equal to their size,
while for aggregate types (other than `seq`) it's that of their most
aligned constituent type.

The size of aggregate types is padded to be a multiple of their
alignment. This is so that their size matches the space they would take
up within an array, while still allowing copying `size` bytes from a
pointer to a location of said type to be valid, regardless of where the
location is located. Record fields are placed such that their relative
offset is a multiple of their alignment requirement.

The allocator procedures `alloc` and `realloc` take an additional 
parameter specifying the allocation's required alignment. `grow` and
`preparedAdd` also take an additional parameter, which specifies the
alignment of the dynamic array's element type. The callsites of all
four procedures are updated accordingly.

Finally, the value rendering in `vmexec` is updated to take the new
object layout into account.
  • Loading branch information
zerbina authored Jan 29, 2025
1 parent 822b02f commit 0944c9f
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 47 deletions.
132 changes: 87 additions & 45 deletions passes/source2il.nim
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ using
expr: var IrNode
stmts: var seq[IrNode]

func align(val: SizeUnit, to: SizeUnit): SizeUnit =
## Rounds `val` to the multiple of `to`, the latter must be a power of two.
let mask = val - 1
(val + mask) and not mask

template `+`(t: SemType, a: set[ExprFlag]): ExprType =
## Convenience shortcut for creating an ``ExprType``.
(typ: t, attribs: a)
Expand Down Expand Up @@ -311,29 +316,27 @@ proc typeToIL(c; typ: SemType): uint32 =
of tkTuple:
let args = mapIt(typ.elems, c.typeToIL(it))
c.addType Record:
c.types.add Node(kind: Immediate, val: size(typ).uint32)
# XXX: for the sake of ease of implementation, records use the maximum
# possible alignment
c.types.add Node(kind: Immediate, val: 8)
c.types.add Node(kind: Immediate, val: paddedSize(typ).uint32)
c.types.add Node(kind: Immediate, val: alignment(typ).uint32)
var off = 0 ## the current field offset
for i, it in args.pairs:
off = align(off, alignment(typ.elems[i]))
c.types.subTree Field:
c.types.add Node(kind: Immediate, val: off.uint32)
c.types.add Node(kind: Type, val: it)
off += size(typ.elems[i])
off += paddedSize(typ.elems[i])
of tkUnion:
let args = mapIt(typ.elems, c.typeToIL(it))
let inner = c.addType Union:
c.types.add Node(kind: Immediate, val: size(typ).uint32 - 8)
c.types.add Node(kind: Immediate, val: 8)
c.types.add Node(kind: Immediate, val: innerSize(typ).uint32)
c.types.add Node(kind: Immediate, val: innerAlignment(typ).uint32)
for it in args.items:
c.types.add Node(kind: Type, val: it)

let tag = c.typeToIL(prim(tkInt))
c.addType Record:
c.types.add Node(kind: Immediate, val: size(typ).uint32)
# XXX: alignment is ignored at the moment and just set to 1
c.types.add Node(kind: Immediate, val: 8)
c.types.add Node(kind: Immediate, val: paddedSize(typ).uint32)
c.types.add Node(kind: Immediate, val: alignment(typ).uint32)
# the tag field:
c.types.subTree Field:
c.types.add Node(kind: Immediate, val: 0)
Expand All @@ -351,8 +354,8 @@ proc typeToIL(c; typ: SemType): uint32 =
lengthType = c.typeToIL(prim(tkInt))
pointerType = c.typeToIL(pointerType)
c.addType Record:
c.types.add Node(kind: Immediate, val: size(typ).uint32)
c.types.add Node(kind: Immediate, val: 8)
c.types.add Node(kind: Immediate, val: paddedSize(typ).uint32)
c.types.add Node(kind: Immediate, val: alignment(typ).uint32)
# the length field:
c.types.subTree Field:
c.types.add Node(kind: Immediate, val: 0)
Expand Down Expand Up @@ -399,6 +402,10 @@ proc genProcType(c; typ: SemType): uint32 =
result = rawGenProcType(c, typ)
c.procTypeCache[typ] = result

proc payloadAlignment(typ: SemType): SizeUnit =
## Computes the alignment for a seq payload for type `typ`.
max(alignment(prim(tkInt)), alignment(typ))

proc genPayloadType(c; typ: SemType): uint32 =
## Generates and emits the payload type for a sequence with the given
## element type (`typ`). Returns the ID of the payload type.
Expand All @@ -410,13 +417,13 @@ proc genPayloadType(c; typ: SemType): uint32 =

let arrayType = c.addType Array:
c.types.add Node(kind: Immediate, val: 1) # size
c.types.add Node(kind: Immediate, val: 8) # alignment
c.types.add Node(kind: Immediate, val: alignment(typ).uint32) # alignment
c.types.add Node(kind: Immediate, val: 0)
c.types.add Node(kind: Type, val: elem)

c.addType Record:
c.types.add Node(kind: Immediate, val: 9)
c.types.add Node(kind: Immediate, val: 8)
c.types.add Node(kind: Immediate, val: payloadAlignment(typ).uint32)
# the capacity field:
c.types.subTree Field:
c.types.add Node(kind: Immediate, val: 0)
Expand Down Expand Up @@ -448,6 +455,9 @@ proc genCapAccess(c; e: sink IrNode, typ: SemType): IrNode =
proc newIntOp(c; op: NodeKind, a, b: sink IrNode): IrNode =
newBinaryOp(op, c.typeToIL(prim(tkInt)), a, b)

proc newAllocCall(size, align: sink IrNode): IrNode =
newCall(AllocProc, @[size, align])

proc genTypeBoundOp(c; op: TypeAttachedOp, typ: SemType): uint32 =
## Synthesizes and emits the `op` type-bound operator for `typ`. Returns the
## ID of the synthesized procedure; no caching is performed.
Expand Down Expand Up @@ -481,13 +491,15 @@ proc genTypeBoundOp(c; op: TypeAttachedOp, typ: SemType): uint32 =

var els = IrNode(kind: Stmts)
els.add newAsgn(dstLen, srcLen)
# the size of the payload is sizeof(capacity) + sizeof(element) * length
els.add newAsgn(newFieldExpr(dst, 1), newCall(AllocProc,
newBinaryOp(Add, c.typeToIL(prim(tkInt)),
newBinaryOp(Mul, c.typeToIL(prim(tkInt)),
# the size of the payload is:
# align(sizeof(capacity), alignment(element)) + sizeof(element) * length
els.add newAsgn(newFieldExpr(dst, 1), newAllocCall(
c.newIntOp(Add,
c.newIntOp(Mul,
srcLen,
newIntVal(size(typ.elems[0]))),
newIntVal(size(prim(tkInt))))))
newIntVal(paddedSize(typ.elems[0]))),
newIntVal(align(size(prim(tkInt)), alignment(typ.elems[0])))),
newIntVal(alignment(typ.elems[0]))))

els.add newAsgn(c.genCapAccess(dst, typ), srcLen)

Expand Down Expand Up @@ -954,7 +966,8 @@ proc callToIL(c; t; n: NodeIndex, expr; stmts): SemType =
newDeref(c.typeToIL(elem.typ),
newCall(PrepareAddProc, @[newAddr(newFieldExpr(tmp, 0)),
newAddr(newFieldExpr(tmp, 1)),
newIntVal(size(elem.typ))])),
newIntVal(paddedSize(elem.typ)),
newIntVal(alignment(elem.typ))])),
use(c, elem, stmts))

expr = tmp
Expand Down Expand Up @@ -1151,8 +1164,9 @@ proc exprToIL(c; t: InTree, n: NodeIndex, expr, stmts): ExprType =

# emit the payload field assignment:
if str.len > 0:
let size = size(prim(tkInt)) + size(elem) * str.len
stmts.add newAsgn(payloadField, newCall(AllocProc, newIntVal(size)))
let size = size(prim(tkInt)) + paddedSize(elem) * str.len
stmts.add newAsgn(payloadField,
newAllocCall(newIntVal(size), newIntVal(alignment(elem))))

let payloadExpr = newDeref(c.genPayloadType(elem), payloadField)
# emit the capacity assignment:
Expand Down Expand Up @@ -1194,9 +1208,11 @@ proc exprToIL(c; t: InTree, n: NodeIndex, expr, stmts): ExprType =

# emit the payload field assignment:
if length > 0:
let size = size(prim(tkInt)) + size(typ) * length
let size = align(size(prim(tkInt)), alignment(typ)) +
(paddedSize(typ) * length)
# the size of the payload is sizeof(capacity) + sizeof(element) * length
stmts.add newAsgn(payloadField, newCall(AllocProc, newIntVal(size)))
stmts.add newAsgn(payloadField,
newAllocCall(newIntVal(size), newIntVal(alignment(typ))))

let payloadExpr = newDeref(c.genPayloadType(typ), payloadField)
# emit the capacity assignment:
Expand Down Expand Up @@ -1413,10 +1429,11 @@ proc importIoProcedures(c) =
newAddr(newFieldExpr(c.genPayloadAccess(newLocal(0), stringType), 1)),
newFieldExpr(newLocal(0), 0)]))
readFileBody.add newAsgn(newFieldExpr(newLocal(1), 1),
newCall(AllocProc,
newAllocCall(
c.newIntOp(Add,
newIntVal(size(prim(tkInt))),
newFieldExpr(newLocal(1), 0))))
newFieldExpr(newLocal(1), 0)),
newIntVal(payloadAlignment(stringType.elems[0]))))
readFileBody.add newAsgn(newFieldExpr(newLocal(1), 0),
newCall(readFileHostPrc.uint32, @[
newAddr(newFieldExpr(c.genPayloadAccess(newLocal(0), stringType), 1)),
Expand Down Expand Up @@ -1501,12 +1518,23 @@ proc open*(reporter: sink(ref ReportContext[string])): ModuleCtx =
# exception/panic is raised
const AllocBody = """
(ProcDef (Type $1)
(Params (Local 0))
(Locals (Type $2) (Type $2))
(Params (Local 0) (Local 2))
(Locals (Type $2) (Type $2) (Type $2))
(Stmts
(If (Eq (Type $2) (Load (Type $2) (Copy (Global 3))) (IntVal 0))
(Asgn (Local 1) (Copy (Global 1)))
(Asgn (Local 1) (Load (Type $2) (Copy (Global 3)))))
(Asgn (Local 1)
(BitAnd (Type $2)
(Add (Type $2)
(Copy (Local 1))
(Sub (Type $2)
(Copy (Local 2))
(IntVal 1)))
(BitNot (Type $2)
(Sub (Type $2)
(Copy (Local 2))
(IntVal 1)))))
(If
(Le (Type $2)
(Add (Type $2)
Expand All @@ -1522,7 +1550,8 @@ proc open*(reporter: sink(ref ReportContext[string])): ModuleCtx =
(Return (Copy (Local 1))))
(Raise (IntVal 0)))))
"""
var procTy = SemType(kind: tkProc, elems: @[pointerType, prim(tkInt)])
var procTy = SemType(kind: tkProc,
elems: @[pointerType, prim(tkInt), prim(tkInt)])
addProc procTy, AllocBody,
[$c.genProcType(procTy), $c.typeToIL(prim(tkInt))]

Expand All @@ -1536,18 +1565,23 @@ proc open*(reporter: sink(ref ReportContext[string])): ModuleCtx =
addProc procTy, DeallocBody,
[$c.genProcType(procTy), $c.typeToIL(prim(tkInt))]

# realloc(ptr: pointer, oldsize: int, newsize: int) -> pointer
# realloc(ptr: pointer, oldsize: int, newsize: int, align: int) -> pointer
const ReallocBody = """
(ProcDef (Type $1)
(Params (Local 0) (Local 1) (Local 2))
(Locals (Type $2) (Type $2) (Type $2) (Type $2))
(Params (Local 0) (Local 1) (Local 2) (Local 4))
(Locals (Type $2) (Type $2) (Type $2) (Type $2) (Type $2))
(If
(Eq (Type $2)
(Copy (Local 0))
(IntVal 0))
(Return (Call (Proc 0) (Copy (Local 2))))
(Return (Call (Proc 0)
(Copy (Local 2))
(Copy (Local 4))))
(Stmts
(Asgn (Local 3) (Call (Proc 0) (Copy (Local 2))))
(Asgn (Local 3)
(Call (Proc 0)
(Copy (Local 2))
(Copy (Local 4))))
(Blit (Copy (Local 3)) (Copy (Local 0)) (Copy (Local 1)))
(Drop (Call (Proc 1) (Copy (Local 0))))
(Return (Copy (Local 3))))
Expand All @@ -1556,18 +1590,23 @@ proc open*(reporter: sink(ref ReportContext[string])): ModuleCtx =
"""
procTy = SemType(kind: tkProc,
elems: @[pointerType, pointerType, prim(tkInt),
prim(tkInt)])
prim(tkInt), prim(tkInt)])
addProc procTy, ReallocBody,
[$c.genProcType(procTy), $c.typeToIL(prim(tkInt))]

# grow(payload: pointer, capacity: int, stride: int) -> pointer
# grow(payload: pointer, capacity: int, stride: int, align: int) -> pointer
# reallocates the given payload if its capacity is less than the requested
# one. The new payload is returned
const GrowBody = """
(ProcDef (Type $1)
(Params (Local 0) (Local 1) (Local 2))
(Locals (Type $2) (Type $2) (Type $2))
(Params (Local 0) (Local 1) (Local 2) (Local 3))
(Locals (Type $2) (Type $2) (Type $2) (Type $2))
(Stmts
(If
(Lt (Type $2)
(Copy (Local 3))
(IntVal 8))
(Asgn (Local 3) (IntVal 8)))
(If
(Eq (Type $2)
(Copy (Local 0))
Expand All @@ -1578,7 +1617,8 @@ proc open*(reporter: sink(ref ReportContext[string])): ModuleCtx =
(IntVal $3)
(Mul (Type $2)
(Copy (Local 1))
(Copy (Local 2))))))
(Copy (Local 2))))
(Copy (Local 3))))
(If
(Not
(Lt (Type $2)
Expand All @@ -1597,7 +1637,8 @@ proc open*(reporter: sink(ref ReportContext[string])): ModuleCtx =
(IntVal $3)
(Mul (Type $2)
(Copy (Local 1))
(Copy (Local 2))))))))
(Copy (Local 2))))
(Copy (Local 3))))))
(Store (Type $2)
(Copy (Local 0))
(Copy (Local 1)))
Expand All @@ -1607,14 +1648,14 @@ proc open*(reporter: sink(ref ReportContext[string])): ModuleCtx =
[$c.genProcType(procTy), $c.typeToIL(prim(tkInt)),
$size(prim(tkInt)) #[<- offset of payload's data field]#]

# prepareAdd(lenAddr: pointer, payloadAddr: pointer, stride: int) -> pointer
# prepareAdd(lenAddr: pointer, payloadAddr: pointer, stride: int, align: int) -> pointer
# increments the length and resizes the payload (via the grow procedure)
# when needed. The implementation works with all seq types in order to not
# having to synthesize a version for each used seq type
const PrepareAddBody = """
(ProcDef (Type $1)
(Params (Local 0) (Local 1) (Local 2))
(Locals (Type $2) (Type $2) (Type $2) (Type $2))
(Params (Local 0) (Local 1) (Local 2) (Local 4))
(Locals (Type $2) (Type $2) (Type $2) (Type $2) (Type $2))
(Stmts
(Asgn (Local 3)
(Load (Type $2)
Expand All @@ -1631,7 +1672,8 @@ proc open*(reporter: sink(ref ReportContext[string])): ModuleCtx =
(Copy (Local 1)))
(Load (Type $2)
(Copy (Local 0)))
(Copy (Local 2))))
(Copy (Local 2))
(Copy (Local 4))))
(Return
(Add (Type $2)
(Add (Type $2)
Expand Down
50 changes: 48 additions & 2 deletions phy/types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import
]

type
SizeUnit* = int
## Used for numbers that represent size and alignment values.
## 1 size-unit = 1 byte.
# TODO: make the type a distinct unsigned type

TypeKind* = enum
tkError
tkVoid
Expand Down Expand Up @@ -101,8 +106,8 @@ proc isSubtypeOf*(a, b: SemType): bool =
else:
false

proc size*(t: SemType): int =
## Computes the size-in-bytes that an instance of `t` occupies in memory.
proc size*(t: SemType): SizeUnit =
## Computes the size without padding of a location of type `t`.
case t.kind
of tkVoid: unreachable()
of tkError: 8 # TODO: return a value indicating "unknown"
Expand All @@ -122,6 +127,47 @@ proc size*(t: SemType): int =
of tkSeq:
size(prim(tkInt)) * 2 # length + pointer

proc alignment*(t: SemType): SizeUnit =
## Computes the alignment requirement for a location of type `t`.
case t.kind
of tkVoid: unreachable()
of tkError: 8
of tkUnit, tkBool, tkChar: 1
of tkInt, tkFloat: 8
of tkProc: 8
of tkTuple:
var a = 0
for it in t.elems.items:
a = max(a, alignment(it))
a
of tkUnion:
var a = 0
for it in t.elems.items:
a = max(a, alignment(it))
# the tag also contributes to the alignment
max(a, alignment(prim(tkInt)))
of tkSeq:
alignment(prim(tkInt))

proc innerSize*(t: SemType): SizeUnit =
## Computes the size without padding of a union (`t`) without the tag.
assert t.kind == tkUnion
result = 0
for it in t.elems.items:
result = max(size(it), result)

proc innerAlignment*(t: SemType): SizeUnit =
## Computes the size without padding of a union (`t`) without the tag.
assert t.kind == tkUnion
result = 0
for it in t.elems.items:
result = max(alignment(it), result)

proc paddedSize*(t: SemType): SizeUnit =
## Computes the size of an array element of type `t`.
let mask = alignment(t) - 1
(size(t) + mask) and not mask

proc commonType*(a, b: SemType): SemType =
## Finds the common type between `a` and `b`, or produces an error.
if a == b or b.isSubtypeOf(a):
Expand Down

0 comments on commit 0944c9f

Please sign in to comment.