From b706c9462a10e64afaf5197ff26b8a434cc33158 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:56:26 +0000 Subject: [PATCH 01/20] syntax: add the `Ptr` type Introducing a dedicated pointer type for use in the earlier passes was planned for some time now (it removes the need to pass the pointer size to some of the passes) -- the LLVM pass more or less requires it. --- languages/lang3.md | 2 ++ passes/syntax.nim | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/languages/lang3.md b/languages/lang3.md index 35871677..3d4d98bc 100644 --- a/languages/lang3.md +++ b/languages/lang3.md @@ -17,7 +17,9 @@ typedesc -= (Blob ) typedesc += (Record size: align: (Field )+) | (Union size: align: +) | (Array size: align: count: ) + | (Ptr) # XXX: temporary solution +type += (Ptr) # XXX: temporary solution type -= (Blob ) ``` diff --git a/passes/syntax.nim b/passes/syntax.nim index 201d0b02..5565f3bb 100644 --- a/passes/syntax.nim +++ b/passes/syntax.nim @@ -11,7 +11,7 @@ import type NodeKind* = enum Immediate, IntVal, FloatVal, StringVal, ProcVal, Proc, Type, Local, Global - Int, UInt, Float + Int, UInt, Float, Ptr List @@ -48,7 +48,7 @@ using lit: var Literals template isAtom*(x: NodeKind): bool = - ord(x) <= ord(Float) + ord(x) <= ord(Ptr) proc toSexp*(tree: PackedTree[NodeKind], idx: NodeIndex, n: TreeNode[NodeKind]): SexpNode = @@ -65,10 +65,13 @@ proc toSexp*(tree: PackedTree[NodeKind], idx: NodeIndex, of Int: sexp([newSSymbol("Int"), sexp n.val.int]) of UInt: sexp([newSSymbol("UInt"), sexp n.val.int]) of Float: sexp([newSSymbol("Float"), sexp n.val.int]) + of Ptr: sexp([newSSymbol("Ptr")]) else: unreachable() proc fromSexp*(kind: NodeKind): Node = - raise ValueError.newException($kind & " node is missing operand") + case kind + of Ptr: Node(kind: Ptr) + else:raise ValueError.newException($kind & " node is missing operand") proc fromSexp*(kind: NodeKind, val: BiggestInt, lit): Node = case kind From 1431322c8342a4742cc4ef22a11d87f0c06bda20 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:56:26 +0000 Subject: [PATCH 02/20] lower aggregate parameters to pointers Now that there's a dedicated pointer type, use it. --- passes/pass_aggregateParams.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/passes/pass_aggregateParams.nim b/passes/pass_aggregateParams.nim index e5e0c077..89737aa3 100644 --- a/passes/pass_aggregateParams.nim +++ b/passes/pass_aggregateParams.nim @@ -100,7 +100,7 @@ proc typeof(c; tree; n): Node = unreachable() proc newLocal(c; typ: Node): Node = - assert typ.kind in {Type, UInt, Int, Float} + assert typ.kind in {Type, UInt, Int, Float, Ptr} result = Node(kind: Local, val: uint32(c.firstTemp + c.temps.len)) c.temps.add typ @@ -409,7 +409,7 @@ proc lowerProc(c; tree; n; sig: NodeIndex, bu) = proc lower*(tree; ptrSize: int): ChangeSet[NodeKind] = ## Computes the changeset representing the lowering for a whole module ## (`tree`). `ptrSize` is the size-in-bytes of a pointer value. - var c = Context(addrType: Node(kind: UInt, val: ptrSize.uint32)) + var c = Context(addrType: Node(kind: Ptr)) for it in tree.items(tree.child(0)): if tree[it].kind == ProcTy: From 8fc97029e2c361c787228584e0916e12e56b033b Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:56:27 +0000 Subject: [PATCH 03/20] skully: use the new `Ptr` type * translate NimSkull pointer types to the IL `Ptr` type * emit conversions when treating globals or integer constants as pointers --- skully/backend.nim | 66 ++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/skully/backend.nim b/skully/backend.nim index cb988a7b..973eb4b0 100644 --- a/skully/backend.nim +++ b/skully/backend.nim @@ -385,7 +385,7 @@ proc translate(c; env: TypeEnv, id: TypeId, bu) = of tkUInt: bu.add node(UInt, desc.size(env).uint32) of tkPointer: - bu.add node(UInt, desc.size(env).uint32) + bu.add node(Ptr, 0) of tkBool, tkChar: bu.add node(UInt, 1) of tkArray: @@ -560,6 +560,12 @@ proc genConst(c; env; tree; n; dest: Expr, stmts) = template putIntoDest(n: Node) = stmts.putInto dest, n + template addPtrLit(bu: var Builder[NodeKind], i: int64) = + bu.subTree Reinterp: + bu.add typeRef(c, env, PointerType) + bu.add typeRef(c, env, UInt64Type) + bu.add node(IntVal, c.lit.pack(i)) + iterator args(tree; n): (int, NodePosition) = var i = 0 for it in tree.items(n, 0, ^1): @@ -598,19 +604,19 @@ proc genConst(c; env; tree; n; dest: Expr, stmts) = of mnkStrLit: let str {.cursor.} = env[tree[n].strVal] - proc emitString(c; str: string, data: uint32, stmts) {.nimcall.} = + proc emitString(c; env; str: string, data: uint32, stmts) {.nimcall.} = # there's no built-in support for strings in the ILs/VM, requiring # the manual initialization of each character for i, it in pairs(str): stmts.addStmt NodeKind.Store: bu.add node(UInt, 1) - bu.add node(IntVal, c.lit.pack(int64(data) + i)) + bu.addPtrLit int64(data) + i bu.add node(IntVal, c.lit.pack(ord it)) # emit the NUL terminator: stmts.addStmt NodeKind.Store: bu.add node(UInt, 1) - bu.add node(IntVal, c.lit.pack(int64(data) + str.len)) + bu.addPtrLit int64(data) + str.len bu.add node(IntVal, c.lit.pack(0)) case env.types.headerFor(typ, Canonical).kind @@ -619,9 +625,12 @@ proc genConst(c; env; tree; n; dest: Expr, stmts) = # terminator let data = c.globalsAddress + AddressBias c.globalsAddress += str.len.uint32 + 1 - c.emitString(str, data, stmts) + c.emitString(env, str, data, stmts) - putIntoDest node(IntVal, c.lit.pack(data.int64)) + stmts.putInto dest, Reinterp: + bu.add typeRef(c, env, PointerType) + bu.add typeRef(c, env, UInt64Type) + bu.add node(IntVal, c.lit.pack(data.int64)) of tkString: # it's a NimSkull string (a length + payload pointer) let @@ -633,10 +642,10 @@ proc genConst(c; env; tree; n; dest: Expr, stmts) = # emit the capacity initialization: stmts.addStmt NodeKind.Store: bu.add typeRef(c, env, env.types.sizeType) - bu.add node(IntVal, c.lit.pack(data.int64)) + bu.addPtrLit data.int64 bu.add node(IntVal, c.lit.pack(str.len or StrLitFlag)) - c.emitString(str, data + 8, stmts) + c.emitString(env, str, data + 8, stmts) stmts.addStmt Asgn: bu.subTree Field: @@ -647,7 +656,7 @@ proc genConst(c; env; tree; n; dest: Expr, stmts) = bu.subTree Field: bu.useLvalue dest bu.add node(Immediate, 1) - bu.add node(IntVal, c.lit.pack(data.int64)) + bu.addPtrLit data.int64 else: unreachable() of mnkSeqConstr: @@ -678,12 +687,12 @@ proc genConst(c; env; tree; n; dest: Expr, stmts) = bu.subTree Field: bu.useLvalue dest bu.add node(Immediate, 1) - bu.add node(IntVal, c.lit.pack(payloadAddr.int64)) + bu.addPtrLit payloadAddr.int64 # emit the capacity initialization: stmts.addStmt NodeKind.Store: bu.add typeRef(c, env, env.types.sizeType) - bu.add node(IntVal, c.lit.pack(payloadAddr.int64)) + bu.addPtrLit payloadAddr.int64 bu.add node(IntVal, c.lit.pack(tree.len(n) or StrLitFlag)) # emit the payload data initialization: @@ -691,7 +700,7 @@ proc genConst(c; env; tree; n; dest: Expr, stmts) = let nDest = makeExpr elem: bu.subTree Deref: bu.add typeRef(c, env, elem) - bu.add node(IntVal, c.lit.pack(dataAddr.int64 + size * i)) + bu.addPtrLit (dataAddr.int64 + size * i) c.genConst(env, tree, it, nDest, stmts) of mnkSetConstr: @@ -766,6 +775,14 @@ proc genExit(c; tree; n; bu) = else: unreachable() +proc loadGlobalAddr(c; env: MirEnv, id: uint32, bu) = + ## Emits a load of the address stored in the global with the given id. + bu.subTree Reinterp: + bu.add typeRef(c, env, PointerType) + bu.add typeRef(c, env, UInt64Type) + bu.subTree Copy: + bu.add Node(kind: Global, val: id) + proc translateValue(c; env: MirEnv, tree: MirTree, n: NodePosition, wantValue: bool, bu) = ## `wantValue` tells translation an rvalue expression is expected. @@ -785,12 +802,11 @@ proc translateValue(c; env: MirEnv, tree: MirTree, n: NodePosition, case tree[n].kind of mnkGlobal: - # all globals are pointers (because the target IL doesn't support mutable - # global variables) + # all globals are offsets into static memory (because the target IL + # doesn't support mutable global variables) bu.subTree (if wantValue: Load else: Deref): bu.add typeRef(c, env, tree[n].typ) - bu.subTree Copy: - bu.add node(Global, c.globalsMap[tree[n].global]) + c.loadGlobalAddr(env, c.globalsMap[tree[n].global], bu) of mnkNilLit: bu.add node(IntVal, 0) of mnkIntLit, mnkUIntLit: @@ -826,13 +842,11 @@ proc translateValue(c; env: MirEnv, tree: MirTree, n: NodePosition, bu.subTree (if wantValue: Load else: Deref): bu.add typeRef(c, env, tree[n].typ) - bu.subTree Copy: - bu.add node(Global, c.dataMap[extract(id)]) + c.loadGlobalAddr(env, c.dataMap[extract(id)], bu) else: bu.subTree (if wantValue: Load else: Deref): bu.add typeRef(c, env, tree[n].typ) - bu.subTree Copy: - bu.add node(Global, c.constMap[tree[n].cnst]) + c.loadGlobalAddr(env, c.constMap[tree[n].cnst], bu) of mnkProcVal: bu.add node(ProcVal, c.registerProc(tree[n].prc)) of mnkDeref, mnkDerefView: @@ -964,8 +978,7 @@ proc getTypeInfoV2(c; env: var MirEnv, typ: TypeId): Expr = c.rttiCache[typ] = global result = makeExpr PointerType: - bu.subTree Copy: - bu.add node(Global, global) + c.loadGlobalAddr(env, global, bu) proc genDefault(c; env: var MirEnv; dest: Expr, typ: TypeId, bu) = let typ = env.types.canonical(typ) @@ -2764,8 +2777,7 @@ proc generateCodeForMain(c; env: var MirEnv; m: Module, let dest = makeExpr typ: bu.subTree Deref: bu.add typeRef(c, env, typ) - bu.subTree Copy: - bu.add node(Global, id) + c.loadGlobalAddr(env, id, bu) c.genConst(env, env[cnst], NodePosition(0), dest, bu) @@ -2774,12 +2786,10 @@ proc generateCodeForMain(c; env: var MirEnv; m: Module, for cnst, id in c.constMap.pairs: bu.subTree NodeKind.Store: bu.add typeRef(c, env, env.types.add(env[cnst].typ)) - bu.subTree Copy: - bu.add node(Global, id) + c.loadGlobalAddr(env, id, bu) bu.subTree Load: bu.add typeRef(c, env, env.types.add(env[cnst].typ)) - bu.subTree Copy: - bu.add node(Global, c.dataMap[env.dataFor(cnst)]) + c.loadGlobalAddr(env, c.dataMap[env.dataFor(cnst)], bu) c.prc.active = true genAll(env, body.code, bu, c) From be2a718481a616d314048c025cd65ac7ac0234df Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:56:27 +0000 Subject: [PATCH 04/20] skully: consider pointer types in `cast` Pointer types are no longer identical to unsigned integers, thus requiring the same to-uint conversion signed integers do. --- skully/backend.nim | 59 +++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/skully/backend.nim b/skully/backend.nim index 973eb4b0..733c9a58 100644 --- a/skully/backend.nim +++ b/skully/backend.nim @@ -2283,40 +2283,39 @@ proc translateExpr(c; env: var MirEnv, tree; n; dest: Expr, stmts) = bu.add typeRef(c, env, dst) bu.add typeRef(c, env, src) value tree.child(n, 0) - elif isUnsigned(dst): - if isUnsigned(src): - # a simple conversion (i.e., a zero extension) is enough - wrapAsgn Conv: - bu.add typeRef(c, env, dst) - bu.add typeRef(c, env, src) - value tree.child(n, 0) - else: - wrapAsgn Conv: - bu.add typeRef(c, env, dst) - bu.add node(UInt, b.uint32) + else: + var tmp = makeExpr src: + value tree.child(n, 0) + + const UInts = [1: UInt8Type, 2: UInt16Type, UInt32Type, 4: UInt32Type, + UInt64Type, UInt64Type, UInt64Type, 8: UInt64Type] + ## byte-width to type map + + if not isUnsigned(src): + # cast to uint first, as only uint values can be zero extended + # or truncated + tmp = makeExpr UInts[b]: bu.subTree Reinterp: - bu.add node(UInt, b.uint32) + bu.add typeRef(c, env, UInts[b]) bu.add typeRef(c, env, src) - value tree.child(n, 0) - elif isUnsigned(src): - wrapAsgn Reinterp: - bu.add typeRef(c, env, dst) - bu.add node(UInt, a.uint32) - bu.subTree Conv: - bu.add node(UInt, a.uint32) - bu.add typeRef(c, env, src) - value tree.child(n, 0) - else: - wrapAsgn Reinterp: - bu.add typeRef(c, env, dst) - bu.add node(UInt, a.uint32) + bu.use tmp + + # truncate or zero extend (both done via Conv at the moment) + tmp = makeExpr UInts[b]: bu.subTree Conv: - bu.add node(UInt, a.uint32) - bu.add node(UInt, b.uint32) + bu.add typeRef(c, env, UInts[a]) + bu.add typeRef(c, env, UInts[b]) + bu.use tmp + + if isPointer(dst) or not isUnsigned(dst): + # cast to the destination type + tmp = makeExpr dst: bu.subTree Reinterp: - bu.add node(UInt, b.uint32) - bu.add typeRef(c, env, src) - value tree.child(n, 0) + bu.add typeRef(c, env, dst) + bu.add typeRef(c, env, UInts[a]) + bu.use tmp + + genAsgn(dest, tmp, stmts) else: unreachable() From 7e379318aba517b3801e5eb070f30b5239f7b9d5 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:56:28 +0000 Subject: [PATCH 05/20] skully: handle pointer conversion Converting between different pointers types is a no-op at the IL level. --- skully/backend.nim | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/skully/backend.nim b/skully/backend.nim index 733c9a58..d5ec2a1a 100644 --- a/skully/backend.nim +++ b/skully/backend.nim @@ -1946,10 +1946,18 @@ proc translateExpr(c; env: var MirEnv, tree; n; dest: Expr, stmts) = wrapAsgn: value(n) of mnkConv, mnkStdConv: - wrapAsgn Conv: - bu.add typeRef(c, env, tree[n].typ) - bu.add typeRef(c, env, tree[n, 0].typ) - value(tree.child(n, 0)) + let dstTyp = typeRef(c, env, tree[n].typ) + let srcTyp = typeRef(c, env, tree[n, 0].typ) + if dstTyp == srcTyp: + # happens for pointer <-> cstring and ptr <-> pointer conversions. + # These types are identical at the IL level, so no conversion is needed + wrapAsgn: + value tree.child(n, 0) + else: + wrapAsgn Conv: + bu.add dstTyp + bu.add srcTyp + value(tree.child(n, 0)) of mnkCopy, mnkMove, mnkSink: wrapAsgn: value(tree.child(n, 0)) @@ -2267,8 +2275,7 @@ proc translateExpr(c; env: var MirEnv, tree; n; dest: Expr, stmts) = bu.add node(IntVal, c.lit.pack(size)) else: template isUnsigned(id: TypeId): bool = - env.types.headerFor(id, Lowered).kind in - {tkPtr, tkPointer, tkRef, tkUInt, tkChar, tkBool} + env.types.headerFor(id, Lowered).kind in {tkUInt, tkChar, tkBool} let a = env.types.headerFor(dst, Lowered).size(env.types) let b = env.types.headerFor(src, Lowered).size(env.types) @@ -2307,7 +2314,7 @@ proc translateExpr(c; env: var MirEnv, tree; n; dest: Expr, stmts) = bu.add typeRef(c, env, UInts[b]) bu.use tmp - if isPointer(dst) or not isUnsigned(dst): + if not isUnsigned(dst): # cast to the destination type tmp = makeExpr dst: bu.subTree Reinterp: From 3f83251bce88ed51dad3ff9976e64e1be427a019 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:56:28 +0000 Subject: [PATCH 06/20] implement an L3 -> LLVM IR pass Exceptions currently use the Itanium "zero cost" exception handling, but this might be subject to change, should implementing the personality function be too much of a hassle. --- passes/passllvm.nim | 898 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 898 insertions(+) create mode 100644 passes/passllvm.nim diff --git a/passes/passllvm.nim b/passes/passllvm.nim new file mode 100644 index 00000000..b3173a54 --- /dev/null +++ b/passes/passllvm.nim @@ -0,0 +1,898 @@ +## Implements the translation of L3 code into LLVM IR (the textual form). + +import + std/[ + strutils, + strformat, + sets, + tables + ], + passes/[ + trees, + syntax + ], + vm/utils + +type + + NumericType = object + kind: range[Int..Float] + size: uint32 + + LLVMTypeKind = enum + ## Corresponds roughly to the LLVM types. + ltVoid + ltInt + ltFloat + ltDouble + ltPtr + ltAggregate + + LLVMType = object + case kind: LLVMTypeKind + of ltInt: + width: int + of ltAggregate: + str: string ## the raw inline type definition + of ltFloat, ltDouble, ltPtr, ltVoid: + discard + + ValKind = enum + vkConst ## some integer/float constant + vkGlobal## some global value + vkTemp ## LLVM local + vkFromGlobal ## LLVM local that holds a value loaded from a global + vkExpr ## raw expression + + Value = object + typ: LLVMType + case kind: ValKind + of vkConst, vkGlobal, vkExpr: + val: string + of vkTemp, vkFromGlobal: + id: int + + PassCtx = object + ## Context object for the pass. Available in most procedures. + code: string + ## full code for the module + procs: seq[tuple[name: string, typ: NodeIndex]] + ## for each procedure the name plus the cached type index + + # per procedure state: + nextName: int + ## ID to use for the next local LLVM name + locals: seq[tuple[typ: NodeIndex, name: int]] + ## all locals of the currently processed procedure + deferred: Table[uint32, (uint32, Value)] + ## maps block IDs to an assignment (represented by a local and value), + ## which needs to be inserted at the start of the respective block + +# give a name to some commonly LLVM types: +const + PtrType = LLVMType(kind: ltPtr) + LBoolType = LLVMType(kind: ltInt, width: 1) # LLVM boolean + BoolType = LLVMType(kind: ltInt, width: 8) + I32Type = LLVMType(kind: ltInt, width: 32) + +# shorten some common parameter definitions: +using + c: var PassCtx + tree: PackedTree[NodeKind] + +func imm(n: TreeNode[NodeKind]): uint32 {.inline.} = + assert n.kind == Immediate + n.val + +func id(n: TreeNode[NodeKind]): uint32 {.inline.} = + assert n.kind in {Local, Global, Proc, ProcVal} + n.val + +func typ(n: TreeNode[NodeKind]): uint32 {.inline.} = + assert n.kind == Type + n.val + +proc follow(tree; n: NodeIndex): NodeIndex = + tree.child(tree.child(0), tree[n].typ) + +proc sizeof(tree; n: NodeIndex): uint32 = + ## Computes the size for the type at `n`. + case tree[n].kind + of Type: + sizeof(tree, follow(tree, n)) + of Int, Float, UInt: + tree[n].val + of Ptr: + 8 + of Array, Record, Union: + tree[n, 0].val + else: + unreachable() + +proc alignof(tree; n: NodeIndex): uint32 = + ## Computes the alignment for the type at `n`. + case tree[n].kind + of Type: + alignof(tree, follow(tree, n)) + of Int, Float, UInt: + tree[n].val + of Ptr: + 8 + of Array, Record, Union: + tree[n, 1].val + else: + unreachable() + +proc formatValue(result: var string, t: LLVMType, spec: string) = + ## Renders `t` and appends it to `result` (picked up by ``fmt``). + case t.kind + of ltVoid: result.add "void" + of ltFloat: result.add "float" + of ltDouble: result.add "double" + of ltPtr: result.add "ptr" + of ltInt: + result.add "i" + result.addInt t.width + of ltAggregate: + result.add t.str + +proc formatValue(result: var string, r: Value, spec: string) = + ## Renders `r` and appends it to `result` (picked up by ``fmt``). + if spec.len == 0: + formatValue(result, r.typ, "") + result.add " " + + case r.kind + of vkConst, vkGlobal, vkExpr: + result.add r.val + of vkTemp, vkFromGlobal: + result.add "%" + result.addInt r.id + +proc intConst(i: SomeInteger, typ: LLVMType): Value = + if typ.kind == ltPtr and i == 0: + Value(kind: vkConst, val: "null", typ: typ) + else: + Value(kind: vkConst, val: $i, typ: typ) + +proc floatConst(f: float, typ: LLVMType): Value = + Value(kind: vkConst, val: "0x" & toHex(cast[uint64](f)), typ: typ) + +proc global(name: sink string, typ: LLVMType): Value = + Value(kind: vkGlobal, val: name, typ: typ) + +proc fullTy(tree; n: NodeIndex): LLVMType = + ## Parses the type at `n` into an ``LLVMType``. + case tree[n].kind + of Type: + fullTy(tree, follow(tree, n)) + of Record: + var r = "{" + var first = true + for it in tree.items(n, 2): + # TODO: padding is missing + if not first: + r.add ", " + first = false + r.formatValue(fullTy(tree, tree.child(it, 1)), "") + r.add "}" + LLVMType(kind: ltAggregate, str: r) + of Array: + LLVMType(kind: ltAggregate, str: fmt"[{tree[n, 2].imm} x {fullTy(tree, tree.child(n, 3))}]") + of Union: + # there are no unions in LLVM. A struct with the same size and alignment + # as the union is emitted + # find the most aligned member: + var a = 0'u32 + var item: NodeIndex + for it in tree.items(n, 2): + if alignof(tree, it) >= a: + a = alignof(tree, it) + item = it + + if sizeof(tree, item) < sizeof(tree, n): + # emit a struct containing the most aligned member + padding + LLVMType(kind: ltAggregate, str: + &"{{{fullTy(tree, item)}, [{sizeof(tree, n) - sizeof(tree, item)} x i8]}}") + else: + # no padding is needed + fullTy(tree, item) + of Int, UInt: + LLVMType(kind: ltInt, width: int(tree[n].val * 8)) + of Ptr: + LLVMType(kind: ltPtr) + of Float: + if tree[n].val == 4: + LLVMType(kind: ltFloat) + else: + LLVMType(kind: ltDouble) + of Void: + LLVMType(kind: ltVoid) + else: + unreachable() + +proc shortTy(tree; n: NodeIndex): LLVMType = + ## Similar to `fullTy`, but uses identified types for aggregates. + case tree[n].kind + of Type: + let nn = follow(tree, n) + if tree[nn].kind in {Array, Record, Union}: + LLVMType(kind: ltAggregate, str: "%Type_" & $tree[n].typ) + else: + shortTy(tree, nn) + of Void, Int, UInt, Float, Ptr: + fullTy(tree, n) + else: + unreachable() + +proc toNumeric(tree; n: NodeIndex): NumericType = + case tree[n].kind + of Type: + toNumeric(tree, follow(tree, n)) + of Int, Float, UInt: + NumericType(kind: tree[n].kind, size: tree[n].val) + else: + unreachable() + +proc isAggregateTy(tree; n: NodeIndex): bool = + tree[n].kind in {Union, Array, Record} or + (tree[n].kind == Type and isAggregateTy(tree, follow(tree, n))) + +proc isFloatTy(tree; n: NodeIndex): bool = + tree[n].kind == Float or + (tree[n].kind == Type and isFloatTy(tree, follow(tree, n))) + +proc isUnsignedTy(tree; n: NodeIndex): bool = + tree[n].kind == UInt or + (tree[n].kind == Type and isUnsignedTy(tree, follow(tree, n))) + + +proc emitOp(c; op: string) = + c.code.add " " + c.code.add op + c.code.add "\n" + +proc emitOp(c; typ: LLVMType, op: string): Value = + result = Value(kind: vkTemp, id: c.nextName, typ: typ) + c.code.add " %" + c.code.addInt c.nextName + c.code.add " = " + c.code.add op + c.code.add "\n" + inc c.nextName + +proc emitOp(c; tree; typ: NodeIndex, op: string): Value = + result = Value(kind: vkTemp, id: c.nextName, typ: shortTy(tree, typ)) + c.code.add " %" + c.code.addInt c.nextName + c.code.add " = " + c.code.add op + c.code.add "\n" + inc c.nextName + +proc emitLabel(c; label: string) = + c.code.add label + c.code.add ":\n" + +proc use(c; id: uint32): Value = + # locals are always allocated on the stack, meaning that the values have + # type 'ptr' + Value(kind: vkTemp, id: c.locals[id].name, typ: PtrType) + +proc emitMemcpy(c; a, b, size: Value) = + c.emitOp(fmt"call void @llvm.memcpy.i32({a}, {b}, {size}, i1 0)") + +proc emitMemcpy(c; a, b: Value, size: uint32) = + emitMemcpy(c, a, b, intConst(BiggestInt(size), I32Type)) + +proc emitMemclear(c; a, size: Value) = + c.emitOp(fmt"call void @llvm.memset.i32({a}, i8 0, {size}, i1 0)") + +proc emitLoad(c; tree; typ: NodeIndex, a: Value): Value = + let t = shortTy(tree, typ) + c.emitOp(t, fmt"load {t}, ptr {a:raw}") + +proc emitStore(c; tree; typ: NodeIndex, dst, val: Value) = + c.emitOp(fmt"store {val}, ptr {dst:raw}") + +proc genExpr(c; tree; val: NodeIndex, typ = LLVMType(kind: ltVoid)): Value + +proc receive(c; tree; n: NodeIndex, typ: LLVMType): Value {.inline.} = + ## Same as ``genExpr``, but with an additional type check. + result = genExpr(c, tree, n, typ) + # sanity check + assert result.typ.kind == typ.kind + +proc genCall(c; tree; call: NodeIndex, + start: int, fin: BackwardsIndex): (LLVMType, string) = + ## Generates (but doesn't emit) the code for a call. `start` and `fin` are + ## where the relevant parts of the list in `call` start and end, + ## respectively. + var args = "" + var callee: Value + var typ: NodeIndex + if tree[call, start].kind == Proc: + # it's a static call + typ = c.procs[tree[call, start].id].typ + var i = 1 + for it in tree.items(call, start + 1, fin): + let v = c.receive(tree, it, shortTy(tree, tree.child(typ, i))) + if args.len > 0: + args.add ", " + args.formatValue(v, "") + inc i + + callee = global(c.procs[tree[call, start].id].name, PtrType) + else: + # it's an indirect call + typ = follow(tree, tree.child(call, start)) + var i = 1 + for it in tree.items(call, start + 2, fin): + let v = c.receive(tree, it, shortTy(tree, tree.child(typ, i))) + if args.len > 0: + args.add ", " + args.formatValue(v, "") + inc i + + callee = c.receive(tree, tree.child(call, start + 1), PtrType) + + # add a null guard + # TODO: mark the null case as unlikely + let cond = c.emitOp(LBoolType, fmt"icmp eq {callee}, null") + c.emitOp(fmt"br i1 {cond:raw}, label %{c.nextName}, label %{c.nextName+1}") + c.emitLabel($c.nextName) + c.emitOp("call void @llvm.trap()") + c.emitOp("unreachable") + c.emitLabel($(c.nextName + 1)) + inc c.nextName, 2 + + result[0] = shortTy(tree, tree.child(typ, 0)) + result[1] = fmt"{result[0]} {callee:raw}({args})" + +proc operandTriple(tree; n: NodeIndex): (LLVMType, NodeIndex, NodeIndex, NodeIndex) = + let (a, b, c) = triplet(tree, n) + (shortTy(tree, a), a, b, c) + +proc genBinaryOp(c; tree; n: NodeIndex, i, u, f: string): Value = + ## Generates the code for a two-operand operation, with the opcode picked + ## based on the type. + let + (typ, ot, a, b) = operandTriple(tree, n) + av = c.receive(tree, a, typ) + bv = c.receive(tree, b, typ) + if isFloatTy(tree, ot): + c.emitOp(typ, fmt"{f} {av}, {bv:raw}") + elif isUnsignedTy(tree, ot): + c.emitOp(typ, fmt"{u} {av}, {bv:raw}") + else: + c.emitOp(typ, fmt"{i} {av}, {bv:raw}") + +proc genBinaryOp(c; tree; n: NodeIndex, op: string): Value = + ## Generates the code for a two-operand operation. + let + (typ, _, a, b) = operandTriple(tree, n) + av = c.receive(tree, a, typ) + bv = c.receive(tree, b, typ) + c.emitOp(typ, fmt"{op} {av}, {bv:raw}") + +proc toBool(c; r: sink Value): Value = + ## Converts an LLVM boolean (i1) value to an L4 boolean (i8). + c.emitOp(BoolType, fmt"zext {r} to i8") + +proc genCmpOp(c; tree; op: NodeIndex, i, u, f: string): Value = + let (typ, ot, a, b) = operandTriple(tree, op) + let av = c.receive(tree, a, typ) + let bv = c.receive(tree, b, typ) + if isFloatTy(tree, ot): + c.emitOp(LBoolType, fmt"{f} {av}, {bv:raw}") + elif isUnsignedTy(tree, ot): + c.emitOp(LBoolType, fmt"{u} {av}, {bv:raw}") + else: + c.emitOp(LBoolType, fmt"{i} {av}, {bv:raw}") + +proc genAsgn(c; tree; id: uint32, src: sink Value) = + ## Emits an assignment of `src` to the local with the given id. + let typ = c.locals[id].typ + if isAggregateTy(tree, typ): + c.emitMemcpy(c.use(id), src, sizeof(tree, typ)) + else: + c.emitStore(tree, typ, c.use(id), src) + +proc genPath(c; tree; n: NodeIndex): Value = + ## Emits the 'getelementptr' operation(s) for the ``Path`` at `n`. + var (val, typ) = + if tree[n, 1].kind == Local: + (c.use(tree[n, 1].id), c.locals[tree[n, 1].id].typ) + else: + (c.receive(tree, tree.child(n, 1), PtrType), + tree.child(tree.child(n, 1), 0)) + + var ltype = shortTy(tree, typ) + var r = ", i32 0" + + if tree[typ].kind == Type: + typ = follow(tree, typ) + + for it in tree.items(n, 2): + case tree[typ].kind + of Record: + typ = tree.child(tree.child(typ, 2 + tree[it].val), 1) + r.add ", i32 " + r.addInt tree[it].val + of Union: + typ = tree.child(typ, 2 + tree[it].val) + # LLVM doesn't have union types, so we cast the pointer-to-union + # to the active type + val = c.emitOp(PtrType, fmt"getelementptr {ltype}, {val}{r}") + r = ", i32 0" + ltype = shortTy(tree, typ) + of Array: + typ = tree.child(typ, 3) + r.add ", " + # literal values use i32 in this context + r.formatValue(c.genExpr(tree, it, I32Type), "") + else: + unreachable() + + if tree[typ].kind == Type: + typ = follow(tree, typ) + + c.emitOp(PtrType, fmt"getelementptr {ltype}, {val}{r}") + +proc genCheckedOp(c; tree; n: NodeIndex, name: string): Value = + ## Generates and emits the code for a checked arithmetic operation. + let (typ, _, a, b) = operandTriple(tree, n) + let av = c.receive(tree, a, typ) + let bv = c.receive(tree, b, typ) + let tv = LLVMType(kind: ltAggregate, str: fmt"{{{typ}, i1}}") + let tmp = c.emitOp(tv, fmt"call {tv} @{name}.{typ}({av}, {bv})") + c.genAsgn(tree, tree[n, 3].id, Value(kind: vkExpr, val: fmt"extractvalue {tmp}, 0")) + c.toBool c.emitOp(LBoolType, fmt"extractvalue {tmp}, 1") + +proc genShift(c; tree; n: NodeIndex, u, s: string): Value = + ## Generates and emits the code for a left- or right-shift operation. + let (typ, ot, a, b) = operandTriple(tree, n) + let av = c.receive(tree, a, typ) + # a different type is allowed for the RHS. It's converted to the correct + # size first + var bv = c.genExpr(tree, b, typ) + if av.typ.width > bv.typ.width: + bv = c.emitOp(av.typ, fmt"zext {bv} to {av.typ}") + elif av.typ.width < bv.typ.width: + bv = c.emitOp(av.typ, fmt"trunc {bv} to {av.typ}") + + # XXX: the L3 doesn't specify what the result of shl should be when the + # shift value is larger than the destination's width. We use the + # modulus of the distance to make the result well-defined + let tmp = c.emitOp(bv.typ, fmt"and {bv}, {typ.width - 1}") + if isUnsignedTy(tree, ot): + c.emitOp(typ, fmt"{u} {av}, {tmp:raw}") + else: + c.emitOp(typ, fmt"{s} {av}, {tmp:raw}") + +proc genExpr(c; tree; val: NodeIndex, typ: LLVMType): Value = + ## Generates and emits the code for an expression (`val`). + case tree[val].kind + of IntVal: + intConst(tree.getInt(val), typ) + of Immediate: + intConst(tree[val].imm, typ) + of FloatVal: + floatConst(tree.getFloat(val), typ) + of ProcVal: + global(c.procs[tree[val].id].name, PtrType) + of Global: + let typ = tree.child(tree.child(tree.child(1), tree[val].id), 0) + # globals in LLVM are always pointers to a location, so a load is required + # to get the actual value + let tmp = c.emitLoad(tree, typ, global("@g" & $tree[val].id, PtrType)) + # remember that the value came from a global: + Value(kind: vkFromGlobal, id: tmp.id, typ: tmp.typ) + of Copy: + case tree[val, 0].kind + of Path: + if isAggregateTy(tree, tree.child(tree.child(val, 0), 0)): + # the actual copy (or not) is handled by the receiver + c.genPath(tree, tree.child(val, 0)) + else: + let v = c.genPath(tree, tree.child(val, 0)) + c.emitLoad(tree, tree.child(tree.child(val, 0), 0), v) + of Local: + let src = c.use(tree[val, 0].id) + if isAggregateTy(tree, c.locals[tree[val, 0].id].typ): + src + else: + c.emitLoad(tree, c.locals[tree[val, 0].id].typ, src) + of Global: + c.genExpr(tree, tree.child(val, 0)) + else: + unreachable() + of Addr: + if tree[val, 0].kind == Path: + c.genPath(tree, tree.child(val, 0)) + else: + # all locals are represented as a pointer-to-stack-loc + c.use(tree[tree.child(val, 0)].id) + of Deref: + # the receiver does the actual dereferencing (or not) + c.receive(tree, tree.child(val, 1), PtrType) + of Load: + let (typ, a) = pair(tree, val) + let x = c.receive(tree, a, PtrType) + if isAggregateTy(tree, typ): + x + else: + c.emitLoad(tree, typ, x) + of Neg: + let (typ, operand) = pair(tree, val) + let inp = c.receive(tree, operand, shortTy(tree, typ)) + if isFloatTy(tree, typ): + c.emitOp(tree, typ, fmt"fneg {inp}") + else: + # compute the two's complement + let x = c.emitOp(tree, typ, fmt"xor {inp}, -1") + c.emitOp(tree, typ, fmt"add {x}, 1") + of Add: + c.genBinaryOp(tree, val, "add", "add", "fadd") + of Sub: + c.genBinaryOp(tree, val, "sub", "sub", "fsub") + of Mul: + c.genBinaryOp(tree, val, "mul", "mul", "fmul") + of Div: + c.genBinaryOp(tree, val, "sdiv", "udiv", "fdiv") + of Mod: + c.genBinaryOp(tree, val, "srem", "urem", "frem") + of AddChck: + genCheckedOp(c, tree, val, "llvm.sadd.overflow") + of SubChck: + genCheckedOp(c, tree, val, "llvm.ssub.overflow") + of MulChck: + genCheckedOp(c, tree, val, "llvm.smul.overflow") + of BitNot: + let (typ, operand) = pair(tree, val) + let x = c.receive(tree, operand, shortTy(tree, typ)) + c.emitOp(x.typ, fmt"xor {x}, -1") + of BitAnd: + c.genBinaryOp(tree, val, "and") + of BitOr: + c.genBinaryOp(tree, val, "or") + of BitXor: + c.genBinaryOp(tree, val, "xor") + of Shl: + c.genShift(tree, val, "shl", "shl") + of Shr: + c.genShift(tree, val, "lshr", "ashr") + of Not: + let v = c.genExpr(tree, tree.child(val, 0), BoolType) + c.toBool c.emitOp(LBoolType, fmt"icmp eq {v}, 0") + of Eq: + c.toBool c.genCmpOp(tree, val, "icmp eq", "icmp eq", "fcmp ueq") + of Lt: + c.toBool c.genCmpOp(tree, val, "icmp slt", "icmp ult", "fcmp ult") + of Le: + c.toBool c.genCmpOp(tree, val, "icmp sle", "icmp ule", "fcmp ule") + of Reinterp: + # reinterpret the bit pattern as another type + let (dtyp, styp, a) = triplet(tree, val) + let v = c.receive(tree, a, shortTy(tree, styp)) + let ltyp = shortTy(tree, dtyp) + # pointers require special conversions + if ltyp.kind == ltPtr: + if v.kind in {vkConst, vkFromGlobal}: + # treat the constant as an offset into the static memory region + # XXX: this is a temporary workaround to make the code generated by + # skully work + c.emitOp(PtrType, fmt"getelementptr i8, ptr @phy_mem, {v}") + else: + c.emitOp(ltyp, fmt"inttoptr {v} to {ltyp}") + elif v.typ.kind == ltPtr: + c.emitOp(ltyp, fmt"ptrtoint {v} to {ltyp}") + else: + c.emitOp(ltyp, fmt"bitcast {v} to {ltyp}") + of Conv: + # numeric conversion + let (dtyp, styp, a) = triplet(tree, val) + let ltyp = shortTy(tree, dtyp) + let x = c.genExpr(tree, a, shortTy(tree, styp)) + let dst = toNumeric(tree, dtyp) + let src = toNumeric(tree, styp) + # NOTE: the conversion from signed-to-unsigned (and vice versa), as well + # as float-to-int are not well-defined at the moment, invoking undefined + # behaviour at the LLVM side + case dst.kind + of Int: + case src.kind + of Int, UInt: + if src.size < dst.size: + c.emitOp(ltyp, fmt"sext {x} to {ltyp}") + elif src.size > dst.size: + c.emitOp(ltyp, fmt"trunc {x} to {ltyp}") + else: + x + of Float: + c.emitOp(ltyp, fmt"fptosi {x} to {ltyp}") + of UInt: + case src.kind + of Int, UInt: + if src.size < dst.size: + c.emitOp(ltyp, fmt"zext {x} to {ltyp}") + elif src.size > dst.size: + c.emitOp(ltyp, fmt"trunc {x} to {ltyp}") + else: + x + of Float: + c.emitOp(ltyp, fmt"fptoui {x} to {ltyp}") + of Float: + case src.kind + of Int: + c.emitOp(ltyp, fmt"sitofp {x} to {ltyp}") + of UInt: + c.emitOp(ltyp, fmt"uitofp {x} to {ltyp}") + of Float: + if src.size == 8: + c.emitOp(ltyp, fmt"fptrunc {x} to float") + else: + c.emitOp(ltyp, fmt"fpext {x} to double") + of Call: + let (typ, call) = c.genCall(tree, val, 0, ^1) + c.emitOp(typ, "call " & call) + else: + unreachable() + +proc genExit(c; tree; exit: NodeIndex) = + ## Generates and emits the code for a basic-block exit. + case tree[exit].kind + of Goto, Loop: + c.emitOp(fmt"br label %L{tree[exit, 0].imm}") + of Return: + if tree.len(exit) == 1: + let v = c.genExpr(tree, tree.child(exit, 0)) + c.emitOp(fmt"ret {v}") + else: + c.emitOp("ret void") + of Raise: + let v = c.receive(tree, tree.child(exit, 0), PtrType) + if tree[exit, 1].kind == Unwind: + c.emitOp(fmt"call void @phy_raise({v})") + c.emitOp("unreachable") + else: + c.emitOp(fmt"invoke void @phy_raise({v}) to label %{c.nextName} unwind label %L{tree[tree.child(exit, 1), 0].imm}") + c.emitLabel($c.nextName) + c.emitOp("unreachable") + inc c.nextName + of Branch: + let (sel, a, b) = triplet(tree, exit) + let cond = c.receive(tree, sel, BoolType) + let tmp = c.emitOp(LBoolType, fmt"trunc {cond} to i1") + c.emitOp(fmt"br i1 {tmp:raw}, label %L{tree[b, 0].imm}, label %L{$tree[a, 0].imm}") + of Select: + # TODO: implement + unreachable("missing") + of CheckedCall: + let (_, call) = c.genCall(tree, exit, 0, ^3) + if tree[tree.child(exit, ^1)].kind == Unwind: + # no local exception handler + c.emitOp("call " & call) + c.emitOp("br label %L" & $tree[tree.child(exit, ^2), 0].imm) + else: + c.emitOp(fmt"invoke {call} to label %L{$tree[tree.child(exit, ^2), 0].imm} unwind label %L{$tree[tree.child(exit, ^1), 0].imm}") + of CheckedCallAsgn: + let (typ, call) = c.genCall(tree, exit, 1, ^3) + if tree[tree.child(exit, ^1)].kind == Unwind: + # no local exception handler + let tmp = c.emitOp(typ, "call " & call) + c.genAsgn(tree, tree[exit, 0].id, tmp) + c.emitOp("br label %L" & $tree[tree.child(exit, ^2), 0].imm) + else: + let tmp = c.emitOp(typ, fmt"invoke {call} to label %L{$tree[tree.child(exit, ^2), 0].imm} unwind label %L{$tree[tree.child(exit, ^1), 0].imm}") + # a direct assignment is not possible. Instead, the assignment must be + # part of the target basic block + c.deferred[tree[tree.child(exit, ^2), 0].imm] = (tree[exit, 0].id, tmp) + of Unreachable: + # important: the L3 unreachable does not directly map to the LLVM + # unreachable + c.emitOp("call void @llvm.trap()") + c.emitOp("unreachable") + else: + unreachable() + +proc genStmt(c; tree; n: NodeIndex) = + ## Generates the bytecode for a statement (`n`). + case tree[n].kind + of Asgn: + let (a, b) = pair(tree, n) + let (typ, dst) = + case tree[a].kind + of Path: (tree.child(a, 0), c.genPath(tree, a)) + of Local: (c.locals[tree[a].id].typ, c.use(tree[a].id)) + else: unreachable() + + if isAggregateTy(tree, typ): + let src = c.receive(tree, b, PtrType) + c.emitMemcpy(dst, src, sizeof(tree, typ)) + else: + let src = c.receive(tree, b, shortTy(tree, typ)) + c.emitStore(tree, typ, dst, src) + of Store: + let (typ, ot, a, b) = operandTriple(tree, n) + let dst = c.receive(tree, a, PtrType) + if isAggregateTy(tree, ot): + let src = c.receive(tree, b, PtrType) + c.emitMemcpy(dst, src, sizeof(tree, ot)) + else: + let src = c.receive(tree, b, typ) + c.emitStore(tree, ot, dst, src) + of Clear: + let x = c.receive(tree, tree.child(n, 0), PtrType) + let y = c.genExpr(tree, tree.child(n, 1), I32Type) + c.emitMemclear(x, y) + of Blit: + let x = c.receive(tree, tree.child(n, 0), PtrType) + let y = c.receive(tree, tree.child(n, 1), PtrType) + let z = c.genExpr(tree, tree.child(n, 2), I32Type) + # XXX: are the blit arguments guaranteed to not overlap? + c.emitMemcpy(x, y, z) + of Call: + let (_, call) = c.genCall(tree, n, 0, ^1) + c.emitOp("call " & call) + of Drop: + # genExpr always emits either a constant or a temp, so dropping the + # value just means doing nothing with it + discard c.genExpr(tree, tree.child(n, 0)) + else: + unreachable() + +proc gen(c; tree; n: NodeIndex) = + ## Generates the code for a basic block. + case tree[n].kind + of Except: + # XXX: exception handling is unfinished + let tmp = c.emitOp(PtrType, "landingpad ptr catch ptr null") + # assign the exception pointer to the local specified as the + # block parameter: + c.genAsgn(tree, tree[tree.child(tree.child(n, 0), 0)].id, tmp) + c.emitOp("call void @phy_catch()") + of Block: + discard "nothing to do" + else: + unreachable() + + for it in tree.items(n, 1, ^2): + genStmt(c, tree, it) + + c.genExit(tree, tree.last(n)) + +proc translate(c; tree; def: NodeIndex) = + ## Generates the code for the single procedure `def`. + let (_, locals, blocks) = tree.triplet(def) + + # reserve the slots for the locals: + c.locals.setLen(tree.len(locals)) + # reserve the parameter names: + c.nextName = tree.len(tree.child(tree.child(blocks, 0), 0)) + + c.emitOp("entry:") + + # set up all locals. We don't bother with SSA or allocating vars in the + # first-used basic-block. Instead, all locals are stack-allocated in the + # entry block + for i, it in tree.pairs(locals): + let tmp = c.emitOp(PtrType, fmt"alloca {shortTy(tree, it)}") + c.locals[i] = (it, tmp.id) + + # set up the parameters. In the L3, parameters are mutable callee-side + # variables, but in LLVM functions they're not. The actual parameter + # values need to be copied into the locations first + for i, it in tree.pairs(tree.child(tree.child(blocks, 0), 0)): + let + id = tree[it].id + typ = Value(kind: vkTemp, id: i, typ: shortTy(tree, c.locals[id].typ)) + c.genAsgn(tree, id, typ) + + c.emitOp("br label %L0") + + for i, it in tree.pairs(blocks): + c.emitLabel("L" & $i) + # emit the deferred assignment, if one exists: + var p: (uint32, Value) + if c.deferred.pop(i.uint32, p): + c.genAsgn(tree, p[0], p[1]) + + c.gen(tree, it) + +proc sig(tree; n: NodeIndex): (LLVMType, seq[LLVMType]) = + ## Returns the LLVM types for the ``ProcTy`` at `n`. + result[0] = shortTy(tree, tree.child(n, 0)) + for it in tree.items(n, 1): + result[1].add shortTy(tree, it) + +proc mangle(name: string): string = + name.replace('.', '_') + +proc translate*(module: PackedTree[NodeKind]): string = + ## Translates a complete module to the VM bytecode and the associated + ## environmental data. + let (types, globals, procs) = module.triplet(NodeIndex(0)) + + var c = PassCtx() + c.procs.newSeq(module.len(procs)) + + # emit the EH helper routines declarations. Their actual definition needs to + # be provided separately: + c.code.add "declare void @phy_raise(ptr)\n" + c.code.add "declare void @phy_catch()\n" + c.code.add "declare i32 @phy_eh_personality(...)\n" + + # create identified types for all aggregates: + for i, it in module.pairs(types): + if module.isAggregateTy(it): + c.code.add &"%Type_{i} = type {fullTy(module, it)}\n" + + # handle the exports, if any: + if module.len(NodeIndex 0) == 4: + let exports = module.next(procs) + var memSize = 1024'i64 + for it in module.items(exports): + case module.getString(module.child(it, 0)) + of "total_memory": + memSize = module.getInt(module.child(module.child(globals, module[it, 1].id), 1)) + else: + # use the export name as the procedure name + if module[it, 1].kind == Proc: + c.procs[module[it, 1].id].name = "@" & mangle(module.getString(module.child(it, 0))) + + # heap memory is emulated via a fixed-size static memory region, with the + # size given by a global + c.code.add &"@phy_mem = private global [{memSize} x i8] zeroinitializer, align 8\n" + + # add the defined globals to the environment: + for i, def in module.pairs(globals): + let (typ, val) = module.pair(def) + # globals are currently immutable + c.code.add &"@g{i} = private constant {c.genExpr(module, val, shortTy(module, typ))}\n" + + var imports: HashSet[string] + + # generate the procedure names and emit the imports: + for i, def in module.pairs(procs): + let typ = follow(module, module.child(def, 0)) + if module[def].kind == ProcDef: + if c.procs[i].name.len == 0: + # only generate a name if the procedure doesn't have one yet + c.procs[i].name = "@p" & $i + c.procs[i].typ = typ + else: + let name = mangle(module.getString(module.child(def, 1))) + # the same function may be imported multiple times; only emit a + # declaration once + if not imports.containsOrIncl(name): + let (ret, params) = sig(module, typ) + c.code.add fmt"declare {ret} @{name}(" + for j, it in params.pairs: + if j > 0: + c.code.add ", " + c.code.formatValue(it, "") + c.code.add ")\n" + + c.procs[i] = ("@" & name, typ) + + # generate the code for the procedures and add them to the environment: + for i, def in module.pairs(procs): + if module[def].kind == ProcDef: + let (ret, params) = sig(module, c.procs[i].typ) + c.code.add fmt"define hidden {ret} {c.procs[i].name}(" + + for j, it in params.pairs: + if j > 0: + c.code.add ", " + c.code.formatValue(it, "") + + # to aid with debugging, request stack-smashing protection (=ssp) for all + # procedures + c.code.add ") ssp personality ptr @phy_eh_personality {\n" + c.translate(module, def) + c.code.add "}\n" + + # export the last L3 function as the Main procedure + c.code.add &"@Main = external alias ptr, ptr {c.procs[module.len(procs)-1].name}\n" + result = c.code From be024c13b8f081e25d1f4bf78d034c8139c72fec Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:56:28 +0000 Subject: [PATCH 07/20] phy: integrate the LLVM pass --- phy/phy.nim | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/phy/phy.nim b/phy/phy.nim index a5a493d3..4ab66906 100644 --- a/phy/phy.nim +++ b/phy/phy.nim @@ -38,6 +38,7 @@ import pass_stackAlloc, pass25, pass30, + passllvm, trees ], phy/[ @@ -64,6 +65,7 @@ import passes/syntax_source except NodeKind type Language = enum langBytecode = "vm" + langLLVM = "llvm" lang0 = "L0" lang1 = "L1" lang2 = "L2" @@ -210,7 +212,7 @@ proc compile(tree: var PackedTree[syntax.NodeKind], source, target: Language) = var current = source while current != target: case current - of lang0, langBytecode, langSource: + of lang0, langBytecode, langSource, langLLVM: assert false, "cannot be handled here: " & $current of lang1: syntaxCheck(tree, lang1_checks, module) @@ -354,6 +356,10 @@ proc main(args: openArray[string]) = syntaxCheck(code, lang0) module = pass0.translate(code) # the bytecode is verified later + elif target == langLLVM: + compile(code, newSource, lang3) + syntaxCheck(code, lang3) + writeFile("out.ll", passllvm.translate(code)) else: compile(code, newSource, target) # make sure the output code is correct: From 666f7a95c03070900891444ab4165f4b3e8fb5f4 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:56:29 +0000 Subject: [PATCH 08/20] implement the runtime for LLVM programs --- runtime.c | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 runtime.c diff --git a/runtime.c b/runtime.c new file mode 100644 index 00000000..162ee27c --- /dev/null +++ b/runtime.c @@ -0,0 +1,193 @@ +/// Implements the runtime for Phy-generated LLVM modules. This includes the +/// executable's entry point, as well as an implementation of the skully and +/// Phy runtime procedures. + +// TODO: split this into three files: +// * one for the LLVM runtime +// * one for the skully runtime +// * and one for the phy runtime + +#include +#include +#include +#include +#include + +struct PhyException { + struct _Unwind_Exception header; + void* data; +}; + +struct PhyException g_exc; +// only a single exception can be in-flight at any time + +void cleanupException() { + // the exception is part of static memory, so there's nothing to deallocate +} + +void phy_raise(void* ex) { + _Unwind_RaiseException(&g_exc.header); + // raising the exception failed for whatever reason -> abort + printf("internal error: raising exception failed\n"); + abort(); +} + +void phy_catch() { + g_exc.data = NULL; +} + +_Unwind_Reason_Code phy_eh_personality( + int version, + _Unwind_Action action, + _Unwind_Exception_Class class, + struct _Unwind_Exception * ex, + struct _Unwind_Context * ctx) { + // TODO: implement, or not. "Zero cost" exception handling might have zero + // run-time overhead (when no exception is thrown), but it sure doesn't + // have zero implementation cost + return _URC_NO_REASON; +} + +// ---- implementation of the skully interface + +// in case of doubt, refer to the `host_impl.nim` module for how the runtime +// functions should behave + +int g_argc = 0; +char** g_args = 0; + +FILE* cio_getNativeStream(int64_t id) { + switch (id) { + case 1: return stdin; + case 2: return stdout; + case 3: return stderr; + } +} + +FILE* cio_fopen(const char* name, const char* mode) { + return fopen(name, mode); +} + +int cio_fclose(FILE* file) { + return fclose(file); +} +int cio_setvbuf(FILE* f, void* buf, int mode, size_t size) { + return setvbuf(f, buf, mode, size); +} +int cio_fflush(FILE* file) { + return fflush(file); +} +size_t cio_fread(void* buf, size_t size, size_t n, FILE* f) { + return fread(buf, size, n, f); +} +size_t cio_fwrite(const void* buf, size_t size, size_t n, FILE* f) { + return fwrite(buf, size, n, f); +} +char* cio_fgets(char* c, int n, FILE* f) { + return fgets(c, n, f); +} +int cio_fgetc(FILE* f) { + return fgetc(f); +} +int cio_ungetc(int c, FILE* f) { + return ungetc(c, f); +} +int cio_fseek(FILE* f, int64_t offset, int whence) { + return fseek(f, offset, whence); +} +int cio_ftello(FILE* f) { + return ftello(f); +} +void cio_clearerr(FILE* f) { + clearerr(f); +} +int cio_ferror(FILE* f) { + return ferror(f); +} +char* cstr_strerror(int errnum) { + return strerror(errnum); +} +double cstr_strtod(const char* buf, char** endptr) { + return strtod(buf, endptr); +} + +size_t pe_paramCount() { + return g_argc - 1; +} +size_t pe_paramStr(size_t i, void* p, size_t len) { + // the first value in the argument array is the path/name of the program + // itself; ignore + i += 1; + if (i >= 1 && i < g_argc) { + if (p) { + memcpy(p, g_args[i], len); + return len; + } else { + // only return the required amount of space + return strlen(g_args[i]); + } + } else { + return -1; + } +} + +// ---- implementation of the core API + +// the core API isn't very well-defined w.r.t. error handling. In most cases, +// we perform no error handling here + +void* toCstring(const char* str, size_t len) { + void* d = malloc(len); + memcpy(d, str, len); + return d; +} + +size_t core_test(size_t in) { + return in; +} +void core_write(const char* data, size_t len) { + fwrite(data, len, 1, stdout); +} +void core_writeErr(const char* data, size_t len) { + fwrite(data, len, 1, stderr); +} +size_t core_fileSize(const char* name, size_t len) { + char* tmp = toCstring(name, len); + FILE* f = fopen(tmp, "rb"); + free(tmp); + if (f) { + fseeko(f, 0, SEEK_END); + off_t s = ftello(f); + fclose(f); + return s; + } else { + return 0; + } +} +size_t core_readFile(void* dst, size_t dstLen, const char* name, size_t nameLen) { + char* sname = toCstring(name, nameLen); + FILE* f = fopen(sname, "rb"); + free(sname); + if (f) { + size_t n = fread(dst, dstLen, 1, f); + if (ferror(f) != 0) { + fclose(f); + return 0; + } + fclose(f); + return n; + } else { + return 0; + } +} + +// ---- main + +extern int Main(); // the actual main function + +int main(int argc, char** args) { + g_argc = argc; + g_args = args; + g_exc.header.exception_cleanup = cleanupException; + return Main(); +} From a1147f9da91e347090cbd57b75f1ceb2cd76b96a Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:56:29 +0000 Subject: [PATCH 09/20] overrides: fix `strcmp` hook return type mismatch --- skully/patch/overrides.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skully/patch/overrides.nim b/skully/patch/overrides.nim index 5a9d1802..5093b551 100644 --- a/skully/patch/overrides.nim +++ b/skully/patch/overrides.nim @@ -33,7 +33,7 @@ proc hook_strstr(haystack, needle: cstring): cstring {.compilerproc.} = result = nil # not found -proc hook_strcmp(a, b: cstring): int {.compilerproc.} = +proc hook_strcmp(a, b: cstring): cint {.compilerproc.} = # the most inefficient implementation imaginable let aLen = len(a) @@ -45,7 +45,7 @@ proc hook_strcmp(a, b: cstring): int {.compilerproc.} = result = 1 else: for i in 0.. Date: Sun, 13 Apr 2025 16:56:30 +0000 Subject: [PATCH 10/20] skully: fix `mOrd` translation A conversion was always emitted, even if the types were already equal. The target IL requires the Conv types to be different. --- skully/backend.nim | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/skully/backend.nim b/skully/backend.nim index d5ec2a1a..02f3ef1f 100644 --- a/skully/backend.nim +++ b/skully/backend.nim @@ -1389,14 +1389,19 @@ proc genMagic(c; env: var MirEnv, tree; n; dest: Expr, stmts) = bu.add typeRef(c, env, tree[n].typ) value(tree.argument(n, 0)) value(tree.argument(n, 1)) - of mOrd: - wrapAsgn: - value(tree.argument(n, 0)) - of mChr: - wrapAsgn Conv: - bu.add typeRef(c, env, tree[n].typ) - bu.add typeRef(c, env, tree[tree.argument(n, 0)].typ) - value(tree.argument(n, 0)) + of mOrd, mChr: + # converts the operand to an int or char + let dst = typeRef(c, env, tree[n].typ) + let src = typeRef(c, env, tree[tree.argument(n, 0)].typ) + if dst == src: + # copies, but otherwise a no-op + wrapAsgn: + value(tree.argument(n, 0)) + else: + wrapAsgn Conv: + bu.add dst + bu.add src + value(tree.argument(n, 0)) of mIncl, mExcl, mLtSet, mLeSet, mEqSet, mMinusSet, mPlusSet, mMulSet, mInSet: c.genSetOp(env, tree, n, dest, stmts) From c73e4fb6f8b0c065e8adeab7b6ed4b3a3cac8e38 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:56:30 +0000 Subject: [PATCH 11/20] skully: fix `mDestroy` translation The length instead of the payload field was tested for a nil value. At compile time, this shows up as a type error in passes that care about proper typing. --- skully/backend.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skully/backend.nim b/skully/backend.nim index 02f3ef1f..36cf20e7 100644 --- a/skully/backend.nim +++ b/skully/backend.nim @@ -1707,7 +1707,7 @@ proc genMagic(c; env: var MirEnv, tree; n; dest: Expr, stmts) = bu.subTree Copy: bu.subTree Field: lvalue(tree.argument(n, 0)) - bu.add node(Immediate, 0) + bu.add node(Immediate, 1) bu.add node(IntVal, 0) bu.goto then bu.goto els From 6a6378047015c8e87dadea5cd99c0362576b55e6 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:56:30 +0000 Subject: [PATCH 12/20] skully: correctly compute union sizes None of the previous passes cared about the size of unions, but the LLVM code generator does, so the size and alignment has to be set to the correct values now. --- skully/backend.nim | 95 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 71 insertions(+), 24 deletions(-) diff --git a/skully/backend.nim b/skully/backend.nim index 36cf20e7..c65c256b 100644 --- a/skully/backend.nim +++ b/skully/backend.nim @@ -308,6 +308,49 @@ proc translateProcType(c; env: TypeEnv, id: TypeId, ignoreClosure: bool, bu) = if desc.callConv(env) == ccClosure and not ignoreClosure: bu.add typeRef(c, env, PointerType) +proc computeSizeAlign(c; env: TypeEnv, typ: Typeid, + withTag = true): (int64, int16) = + ## Computes the size and alignment of an embedded record/union. + # TODO: the NimSkull compiler needs to set the size and alignment + # for embedded records + let desc = env.headerFor(typ, Lowered) + var size = 0'i64 + var align = 0'i16 + case desc.kind + of tkTaggedUnion: + if withTag: + let tag = env.headerFor(env[env.lookupField(typ, 0)].typ, Lowered) + align = tag.align + size = tag.size(env) + + for _, it in env.fields(desc, 1): + if env.isEmbedded(it.typ): + # an anonymous record; its fields need to be considered too + let (s, a) = computeSizeAlign(c, env, it.typ) + align = max(align, a) + size = max(size, s) + else: + let sub = env.headerFor(it.typ, Canonical) + align = max(align, sub.align) + size = max(size, sub.size(env)) + of tkRecord: + for _, it in env.fields(desc, 0): + # align the offset, then add the size + if env.isEmbedded(it.typ): + let (s, a) = computeSizeAlign(c, env, it.typ) + size = align(size, a) + size += s + align = max(align, a) + else: + let sub = env.headerFor(it.typ, Canonical) + size = align(size, sub.align) + size += sub.size(env) + align = max(align, sub.align) + else: + unreachable() + # don't align the size to the alignment (meaning no trailing padding) + result = (size, align) + proc embedTaggedUnion(c; env: TypeEnv, id: TypeId, bu) = privateAccess(RecField) # required for accessing the field offsets @@ -317,29 +360,16 @@ proc embedTaggedUnion(c; env: TypeEnv, id: TypeId, bu) = let desc = env.headerFor(id, Lowered) tagField = env.lookupField(id, 0) - - # compute the alignment for the union: - # TODO: this needs to be upstreamed into NimSkull - var alignment = 0'i16 - for _, it in env.fields(desc, 1): - let sub = env.headerFor(it.typ, Canonical) - if env.isEmbedded(it.typ): - # an anonymous record; its fields need to be considered too - for _, inner in env.fields(sub): - alignment = max(alignment, env.headerFor(inner.typ, Canonical).align) - else: - alignment = max(alignment, sub.align) - - let offset = align(env[tagField].offset.uint32 + desc.size(env).uint32, - uint32 alignment) - ## the offset of the union within the embedding record + tagDesc = env.headerFor(env[tagField].typ, Lowered) + (size, alignment) = computeSizeAlign(c, env, id, withTag=false) + # the size and alignment of the union (without the tag) + offset = align(env[tagField].offset.uint32 + tagDesc.size(env).uint32, + uint32 alignment) + ## the offset of the union within the embedding record let inner = block: - # XXX: size and alignment are currently ignored, as it's simpler this way - # and no pass inspects these values anyhow (at least at the time of - # writing) var bu = initBuilder(Union) - bu.add node(Immediate, 0) + bu.add node(Immediate, uint32 size) bu.add node(Immediate, uint32 alignment) for _, it in env.fields(desc, 1): if env.isEmbedded(it.typ): @@ -347,12 +377,13 @@ proc embedTaggedUnion(c; env: TypeEnv, id: TypeId, bu) = # IL record types must not be empty; add a dummy field bu.add node(UInt, 1) else: + let (s, a) = computeSizeAlign(c, env, it.typ) # emit the record type for the embedded type, but make sure to # correct the field offsets; they need to be relative to the start # of the union, not to the start of the embedding record var bu2 = initBuilder(Record) - bu2.add node(Immediate, 0) - bu2.add node(Immediate, 0) + bu2.add node(Immediate, s.uint32) + bu2.add node(Immediate, a.uint32) for _, recf in env.fields(env.headerFor(it.typ, Lowered)): bu2.subTree Field: bu2.add node(Immediate, recf.offset.uint32 - offset) @@ -2628,6 +2659,8 @@ proc translateProc(c; env: var MirEnv, procType: TypeId, let typ = c.genProcType(env, procType) c.complete(env, typ, c.prc, body, content) +import compiler/mir/utils + proc replaceProcAst(config: ConfigRef, prc: PSym, with: PNode) = ## Replaces the ``PSym.ast`` of `prc` with the routine AST `with`, ## reparenting all symbols found in the body. This is crude, brittle, and @@ -2748,8 +2781,13 @@ proc processEvent(env: var MirEnv, bodies: var ProcMap, var body = evt.body apply(body, env) # apply the additional MIR passes - let procType = env.types.add(evt.sym.typ) - bodies[c.registerProc(evt.id)] = c.translateProc(env, procType, body) + try: + let procType = env.types.add(evt.sym.typ) + bodies[c.registerProc(evt.id)] = c.translateProc(env, procType, body) + except: + echo evt.sym + echo render(body.code, addr env, addr body) + raise of bekImported: # dynlib procedures are not supported c.graph.config.localReport(evt.sym.info, @@ -2811,6 +2849,8 @@ proc generateCodeForMain(c; env: var MirEnv; m: Module, let typ = c.genProcType(env, env.types.add(prc.typ)) result = (prc, c.complete(env, typ, c.prc, body, finish(bu))) +import compiler/backend/ccgutils + proc generateCode*(graph: ModuleGraph): PackedTree[NodeKind] = ## Generates the IL code for the full program represented by `graph` and ## returns the result as a single self-contained IL module. @@ -2920,4 +2960,11 @@ proc generateCode*(graph: ModuleGraph): PackedTree[NodeKind] = bu.add Node(kind: StringVal, val: c.lit.pack("total_memory")) bu.add Node(kind: Global, val: c.globals.len.uint32 + 2) + for id, v in c.procMap.pairs: + let s = env.procedures[id] + if sfImportc notin s.flags: + bu.subTree Export: + bu.add Node(kind: StringVal, val: c.lit.pack(mangle(s.skipGenericOwner.name.s & "." & s.name.s))) + bu.add Node(kind: Proc, val: v.uint32) + result = initTree[NodeKind](finish(bu), c.lit) From 554b7a7f506eaee248fe3b4d683c08009fad292e Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Fri, 16 May 2025 00:22:56 +0000 Subject: [PATCH 13/20] skully: fix syntax error & cleanup --- skully/backend.nim | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/skully/backend.nim b/skully/backend.nim index 05dee02e..d382b6d1 100644 --- a/skully/backend.nim +++ b/skully/backend.nim @@ -654,12 +654,6 @@ proc genConst(c; env; tree; n; dest: Expr, stmts) = template putIntoDest(n: Node) = stmts.putInto dest, n - template addPtrLit(bu: var Builder[NodeKind], i: int64) = - bu.subTree Reinterp: - bu.add typeRef(c, env, PointerType) - bu.add typeRef(c, env, UInt64Type) - bu.add node(IntVal, c.lit.pack(i)) - iterator args(tree; n): (int, NodePosition) = var i = 0 for it in tree.items(n, 0, ^1): @@ -853,14 +847,6 @@ proc genExit(c; tree; n; bu) = else: unreachable() -proc loadGlobalAddr(c; env: MirEnv, id: uint32, bu) = - ## Emits a load of the address stored in the global with the given id. - bu.subTree Reinterp: - bu.add typeRef(c, env, PointerType) - bu.add typeRef(c, env, UInt64Type) - bu.subTree Copy: - bu.add Node(kind: Global, val: id) - proc translateValue(c; env: MirEnv, tree: MirTree, n: NodePosition, wantValue: bool, bu) = ## `wantValue` tells translation an rvalue expression is expected. @@ -1519,7 +1505,7 @@ proc genMagic(c; env: var MirEnv, tree; n; dest: Expr, stmts) = bu.add typeRef(c, env, tree[n].typ) value(tree.argument(n, 0)) value(tree.argument(n, 1)) - of mOrd, mChr + of mOrd, mChr: # it's a conversion, nothing more let input = c.gen(env, tree, NodePosition(tree.argument(n, 0)), true) wrapAsgn: From bd0112d2513e5b21853df276e9d8388557c1ab23 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Wed, 21 May 2025 16:31:03 +0000 Subject: [PATCH 14/20] lang3: remove the old `Ptr` workaround There's a proper `Ptr` type now. --- languages/lang3.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/languages/lang3.md b/languages/lang3.md index cfa94ceb..225d21d5 100644 --- a/languages/lang3.md +++ b/languages/lang3.md @@ -17,9 +17,7 @@ typedesc -= (Blob ) typedesc += (Record size: align: (Field )+) | (Union size: align: +) | (Array size: align: count: ) - | (Ptr) # XXX: temporary solution -type += (Ptr) # XXX: temporary solution type -= (Blob ) ``` From a0245c044c065ed1519b6ee9c98c470c8c73ba2c Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Wed, 21 May 2025 16:31:04 +0000 Subject: [PATCH 15/20] passllvm: adjust to the new L3 semantics The L3 semantics changed considerably. Conversions have become simpler, there are dedicated pointers types, and globals support storing complex data. --- passes/passllvm.nim | 124 ++++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 63 deletions(-) diff --git a/passes/passllvm.nim b/passes/passllvm.nim index b3173a54..61645877 100644 --- a/passes/passllvm.nim +++ b/passes/passllvm.nim @@ -10,8 +10,7 @@ import passes/[ trees, syntax - ], - vm/utils + ] type @@ -41,7 +40,6 @@ type vkConst ## some integer/float constant vkGlobal## some global value vkTemp ## LLVM local - vkFromGlobal ## LLVM local that holds a value loaded from a global vkExpr ## raw expression Value = object @@ -49,7 +47,7 @@ type case kind: ValKind of vkConst, vkGlobal, vkExpr: val: string - of vkTemp, vkFromGlobal: + of vkTemp: id: int PassCtx = object @@ -145,15 +143,12 @@ proc formatValue(result: var string, r: Value, spec: string) = case r.kind of vkConst, vkGlobal, vkExpr: result.add r.val - of vkTemp, vkFromGlobal: + of vkTemp: result.add "%" result.addInt r.id proc intConst(i: SomeInteger, typ: LLVMType): Value = - if typ.kind == ltPtr and i == 0: - Value(kind: vkConst, val: "null", typ: typ) - else: - Value(kind: vkConst, val: $i, typ: typ) + Value(kind: vkConst, val: $i, typ: typ) proc floatConst(f: float, typ: LLVMType): Value = Value(kind: vkConst, val: "0x" & toHex(cast[uint64](f)), typ: typ) @@ -470,6 +465,14 @@ proc genShift(c; tree; n: NodeIndex, u, s: string): Value = else: c.emitOp(typ, fmt"{s} {av}, {tmp:raw}") +proc genConvOp(c; tree; n: NodeIndex, op: string): Value = + ## Emits the LLVM conversion operation `op` for the IL operation at `n`. + let + (dtyp, styp, v) = triplet(tree, n) + ltyp = shortTy(tree, dtyp) + val = c.genExpr(tree, v, shortTy(tree, styp)) + c.emitOp(ltyp, fmt"{op} {val} to {ltyp}") + proc genExpr(c; tree; val: NodeIndex, typ: LLVMType): Value = ## Generates and emits the code for an expression (`val`). case tree[val].kind @@ -481,13 +484,10 @@ proc genExpr(c; tree; val: NodeIndex, typ: LLVMType): Value = floatConst(tree.getFloat(val), typ) of ProcVal: global(c.procs[tree[val].id].name, PtrType) + of Nil: + Value(kind: vkConst, val: "null", typ: PtrType) of Global: - let typ = tree.child(tree.child(tree.child(1), tree[val].id), 0) - # globals in LLVM are always pointers to a location, so a load is required - # to get the actual value - let tmp = c.emitLoad(tree, typ, global("@g" & $tree[val].id, PtrType)) - # remember that the value came from a global: - Value(kind: vkFromGlobal, id: tmp.id, typ: tmp.typ) + global("@g" & $tree[val].id, PtrType) of Copy: case tree[val, 0].kind of Path: @@ -504,6 +504,7 @@ proc genExpr(c; tree; val: NodeIndex, typ: LLVMType): Value = else: c.emitLoad(tree, c.locals[tree[val, 0].id].typ, src) of Global: + # FIXME: this is wrong for constants, which require a load c.genExpr(tree, tree.child(val, 0)) else: unreachable() @@ -578,13 +579,7 @@ proc genExpr(c; tree; val: NodeIndex, typ: LLVMType): Value = let ltyp = shortTy(tree, dtyp) # pointers require special conversions if ltyp.kind == ltPtr: - if v.kind in {vkConst, vkFromGlobal}: - # treat the constant as an offset into the static memory region - # XXX: this is a temporary workaround to make the code generated by - # skully work - c.emitOp(PtrType, fmt"getelementptr i8, ptr @phy_mem, {v}") - else: - c.emitOp(ltyp, fmt"inttoptr {v} to {ltyp}") + c.emitOp(ltyp, fmt"inttoptr {v} to {ltyp}") elif v.typ.kind == ltPtr: c.emitOp(ltyp, fmt"ptrtoint {v} to {ltyp}") else: @@ -596,32 +591,13 @@ proc genExpr(c; tree; val: NodeIndex, typ: LLVMType): Value = let x = c.genExpr(tree, a, shortTy(tree, styp)) let dst = toNumeric(tree, dtyp) let src = toNumeric(tree, styp) - # NOTE: the conversion from signed-to-unsigned (and vice versa), as well - # as float-to-int are not well-defined at the moment, invoking undefined - # behaviour at the LLVM side + # NOTE: the conversion from float-to-int (and vice versa) is not well- + # defined at the moment, invoking undefined behaviour at the LLVM side case dst.kind of Int: - case src.kind - of Int, UInt: - if src.size < dst.size: - c.emitOp(ltyp, fmt"sext {x} to {ltyp}") - elif src.size > dst.size: - c.emitOp(ltyp, fmt"trunc {x} to {ltyp}") - else: - x - of Float: - c.emitOp(ltyp, fmt"fptosi {x} to {ltyp}") + c.emitOp(ltyp, fmt"fptosi {x} to {ltyp}") of UInt: - case src.kind - of Int, UInt: - if src.size < dst.size: - c.emitOp(ltyp, fmt"zext {x} to {ltyp}") - elif src.size > dst.size: - c.emitOp(ltyp, fmt"trunc {x} to {ltyp}") - else: - x - of Float: - c.emitOp(ltyp, fmt"fptoui {x} to {ltyp}") + c.emitOp(ltyp, fmt"fptoui {x} to {ltyp}") of Float: case src.kind of Int: @@ -629,10 +605,17 @@ proc genExpr(c; tree; val: NodeIndex, typ: LLVMType): Value = of UInt: c.emitOp(ltyp, fmt"uitofp {x} to {ltyp}") of Float: - if src.size == 8: - c.emitOp(ltyp, fmt"fptrunc {x} to float") - else: - c.emitOp(ltyp, fmt"fpext {x} to double") + unreachable() + of Promote: + c.genConvOp(tree, val, "fpext") + of Demote: + c.genConvOp(tree, val, "fptrunc") + of Zext: + c.genConvOp(tree, val, "zext") + of Sext: + c.genConvOp(tree, val, "sext") + of Trunc: + c.genConvOp(tree, val, "trunc") of Call: let (typ, call) = c.genCall(tree, val, 0, ^1) c.emitOp(typ, "call " & call) @@ -809,6 +792,19 @@ proc sig(tree; n: NodeIndex): (LLVMType, seq[LLVMType]) = proc mangle(name: string): string = name.replace('.', '_') +proc llvmStr(x: string): string = + ## Creates an LLVM string literal from `x`. + result.add '\"' + for c in x.items: + case c + of '\0'..'\31', '\127'..'\255': + result.add '\\' + result.add toHex(ord(c), 2) + of '\\': result.add "\\\\" + of '\"': result.add "\\22" + else: result.add c + result.add '\"' + proc translate*(module: PackedTree[NodeKind]): string = ## Translates a complete module to the VM bytecode and the associated ## environmental data. @@ -831,25 +827,27 @@ proc translate*(module: PackedTree[NodeKind]): string = # handle the exports, if any: if module.len(NodeIndex 0) == 4: let exports = module.next(procs) - var memSize = 1024'i64 for it in module.items(exports): - case module.getString(module.child(it, 0)) - of "total_memory": - memSize = module.getInt(module.child(module.child(globals, module[it, 1].id), 1)) - else: - # use the export name as the procedure name - if module[it, 1].kind == Proc: - c.procs[module[it, 1].id].name = "@" & mangle(module.getString(module.child(it, 0))) - - # heap memory is emulated via a fixed-size static memory region, with the - # size given by a global - c.code.add &"@phy_mem = private global [{memSize} x i8] zeroinitializer, align 8\n" + # use the export name as the procedure name + if module[it, 1].kind == Proc: + c.procs[module[it, 1].id].name = "@" & mangle(module.getString(module.child(it, 0))) # add the defined globals to the environment: for i, def in module.pairs(globals): let (typ, val) = module.pair(def) - # globals are currently immutable - c.code.add &"@g{i} = private constant {c.genExpr(module, val, shortTy(module, typ))}\n" + case module[val].kind + of Data: + let align = module.getUInt(module.child(val, 0)) + if module[val, 1].kind == StringVal: + let init = module.getString(module.child(val, 1)) + c.code.add &"@g{i} = private global [{init.len} x i8] c{llvmStr(init)}, align {align}\n" + else: + let size = module.getUInt(module.child(val, 1)) + c.code.add &"@g{i} = private global [{size} x i8] zeroinitializer, align {align}\n" + else: + # a non-data initializer can currently only mean that it's a constant + let init = c.genExpr(module, val, shortTy(module, typ)) + c.code.add &"@g{i} = private constant {init}\n" var imports: HashSet[string] From fd0f40dc40dceac6d48e7c63eb5bfef97ce71838 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Wed, 21 May 2025 16:31:04 +0000 Subject: [PATCH 16/20] phy: support `--measure` for LLVM code generator --- phy/phy.nim | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/phy/phy.nim b/phy/phy.nim index 2cad1826..c78c9629 100644 --- a/phy/phy.nim +++ b/phy/phy.nim @@ -429,7 +429,10 @@ proc main(args: openArray[string]) = elif target == langLLVM: compile(code, newSource, lang3) syntaxCheck(code, lang3) - writeFile("out.ll", passllvm.translate(code)) + var output: string + measure "llvm-gen": + output = passllvm.translate(code) + writeFile("out.ll", output) else: compile(code, newSource, target) # make sure the output code is correct: From 98e9a0a7d117c196446c78d614994a25342c4772 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Wed, 21 May 2025 16:31:05 +0000 Subject: [PATCH 17/20] ci: test the LLVM code generator This includes building and running an executable out of the code- generator produced assembler code. --- .github/workflows/build_and_test.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index d110fca8..3b54b31b 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -168,3 +168,17 @@ jobs: env: TEST_FILE: tests/expr/t06_call_proc_with_tuple_return_type.test TEST_OUTPUT: "(TupleCons 100 200) : (TupleTy (IntTy) (IntTy))(Done 0)" + + # test the LLVM code generator + - name: Compile phy to LLVM + run: bin/phy --source:L25 --target:llvm --measure c build/phy.txt + + - name: Assemble LLVM bitcode + run: clang -fstack-protector -O3 -o build/phy.bc out.ll + + - name: Compile final program + run: clang -fstack-protector -O3 -o build/phy-llvm build/phy.bc runtime.c + + # use the LLVM-compiled phy to compile phy to LLVM + - name: Test LLVM-built phy + run: build/phy-llvm --source:L25 --target:llvm c phy.txt From e68504f1efbb3fd8323261d3a9228c62f0c636ce Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Wed, 21 May 2025 16:39:39 +0000 Subject: [PATCH 18/20] ci: fix LLVM test --- .github/workflows/build_and_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 3b54b31b..fcbe4b50 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -174,7 +174,7 @@ jobs: run: bin/phy --source:L25 --target:llvm --measure c build/phy.txt - name: Assemble LLVM bitcode - run: clang -fstack-protector -O3 -o build/phy.bc out.ll + run: clang -fstack-protector -O3 --emit-llvm -o build/phy.bc out.ll - name: Compile final program run: clang -fstack-protector -O3 -o build/phy-llvm build/phy.bc runtime.c From 0cfbfc9fd159204189a3ef4b3f21a215ca41ec5e Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Wed, 21 May 2025 16:39:39 +0000 Subject: [PATCH 19/20] skully: remove leftover debug code This also makes the LLVM produced by the code generator compile, as there are no longer any duplicated interface names. --- skully/backend.nim | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/skully/backend.nim b/skully/backend.nim index a6227425..e2be3ce9 100644 --- a/skully/backend.nim +++ b/skully/backend.nim @@ -2730,8 +2730,6 @@ proc translateProc(c; env: var MirEnv, procType: TypeId, let typ = c.genProcType(env, procType) c.complete(env, typ, c.prc, body, content) -import compiler/mir/utils - proc replaceProcAst(config: ConfigRef, prc: PSym, with: PNode) = ## Replaces the ``PSym.ast`` of `prc` with the routine AST `with`, ## reparenting all symbols found in the body. This is crude, brittle, and @@ -2852,13 +2850,8 @@ proc processEvent(env: var MirEnv, bodies: var ProcMap, var body = evt.body apply(body, env) # apply the additional MIR passes - try: - let procType = env.types.add(evt.sym.typ) - bodies[c.registerProc(evt.id)] = c.translateProc(env, procType, body) - except: - echo evt.sym - echo render(body.code, addr env, addr body) - raise + let procType = env.types.add(evt.sym.typ) + bodies[c.registerProc(evt.id)] = c.translateProc(env, procType, body) of bekImported: # dynlib procedures are not supported c.graph.config.localReport(evt.sym.info, @@ -2916,8 +2909,6 @@ proc generateCodeForMain(c; env: var MirEnv; m: Module, let typ = c.genProcType(env, env.types.add(prc.typ)) result = (prc, c.complete(env, typ, c.prc, body, finish(bu))) -import compiler/backend/ccgutils - proc generateCode*(graph: ModuleGraph): PackedTree[NodeKind] = ## Generates the IL code for the full program represented by `graph` and ## returns the result as a single self-contained IL module. @@ -3015,11 +3006,4 @@ proc generateCode*(graph: ModuleGraph): PackedTree[NodeKind] = bu.add Node(kind: StringVal, val: c.lit.pack("stack_size")) bu.add Node(kind: Global, val: c.globals.len.uint32) - for id, v in c.procMap.pairs: - let s = env.procedures[id] - if sfImportc notin s.flags: - bu.subTree Export: - bu.add Node(kind: StringVal, val: c.lit.pack(mangle(s.skipGenericOwner.name.s & "." & s.name.s))) - bu.add Node(kind: Proc, val: v.uint32) - result = initTree[NodeKind](finish(bu), c.lit) From cac5fdfabfacf612ed7e156aa3ac3fd986f25a88 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Wed, 21 May 2025 16:46:18 +0000 Subject: [PATCH 20/20] ci: really fix the test this time --- .github/workflows/build_and_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index fcbe4b50..a5809aa7 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -174,7 +174,7 @@ jobs: run: bin/phy --source:L25 --target:llvm --measure c build/phy.txt - name: Assemble LLVM bitcode - run: clang -fstack-protector -O3 --emit-llvm -o build/phy.bc out.ll + run: clang -fstack-protector -O3 -emit-llvm -o build/phy.bc -c out.ll - name: Compile final program run: clang -fstack-protector -O3 -o build/phy-llvm build/phy.bc runtime.c