// Type checking and LLVM IR generation (as text), written in Finch. // // The output is a .ll file that clang turns into a program, linked with Finch's C runtime // (runtime/finch_rt.c). Values are copied when assigned and freed when their block ends. import ast import docs import cimport import platform import "stdio.h" struct Var { str name ast.Type ty str slot bool readonly int order // how many variables were declared before this one (for defer) bool owned // it holds memory that is dropped when its scope ends bool moved // `return x` moved it out: its cleanup is skipped str dpath // where it was declared, for the language server int dline int dcol } // A scope's variables, and the defers that run when it ends, newest first. // A defer sees only the variables declared before it: `orders` says how many. // What the driver asks for besides the program itself. struct Options { bool shared // finch build --shared bool index // finch index: say what every name in the program is, for the language server bool debug // -g str cwd // the folder the compiler was started in (for the debug info) platform.Target target bool trace // runtime errors show the chain of calls (off for --shared and --no-trace) bool test // finch test: main runs the test "..." blocks instead []str testModules // the modules whose tests run ("" is the main file); not the standard library's []str features // what the processor it is built for has (from --cpu, asked of clang): avx2, i8mm ... } // A generic struct or function, kept as written until something asks for a version of it. struct Template { str module str name str recv // a method: the generic struct it belongs to []str tparams int prog int index bool isStruct bool mutself } struct Binds { []ast.Type types []bool bound } struct EnumDef { str module str name []str variants []int values } // What happens when a scope ends, in the order things happened (run newest first): a variable is dropped, // a defer is run. `idx` is the variable's number in the scope, or the defer's. struct Cleanup { bool isDefer int idx } // An enum whose variants hold values (see sumStruct in parser.fch): it is a struct `tag, V_0, ...`. // firsts[k] is the number of the struct field holding variant k's first value. struct SumDef { str module str name []str variants []int counts []int firsts []ast.Node nodes } struct SumPat { int k []ast.Node binds } struct Scope { []Var vars []ast.Node defers []int orders []Cleanup cleanups } // A value some expression is holding while it computes the rest: if `try` or `or` leaves the function now, // it has to be dropped. isAddr: v is the address of the value, not the value. struct Pending { str v ast.Type ty bool isAddr } struct Loop { str cont str brk int depth // the scope depth at the loop: break and continue unwind down to it int pend // and so are the temporaries held by the expressions around it } struct FnSig { str module str name str recv // a method of this struct ("" for a function) str recvmod // and the module of that struct: "C" for a struct from a header bool recvEnum // the receiver is an enum bool exported // export fn: also a C function of a library []str tnames // an instance of a generic function: its type parameters and what they stand for []ast.Type targs str shown // max[int], for messages bool fallible // -> T!: returns { i1 ok, T value, str error } bool mutself // the method changes self str ll ast.Type ret []ast.Type params []str pnames []bool borrow int prog int index } struct StructDef { str module str name bool isC // a struct from a C header: module "C", laid out as C laid it out []int lidx // the LLVM element number of each field (C structs have padding between) []int offs // C: the byte offset of each field str base // an instance of a generic struct: Stack for Stack[int] []str tnames []ast.Type targs str unsupported // C: why it can't be passed by value int csize int calign []str fnames []ast.Type ftypes int prog int index int state // 0 new, 1 resolving, 2 done bool owning bool isUnion // C: a union, every field at byte 0 } struct Val { str v ast.Type ty bool fresh // a new value nobody holds: it can be stored without copying bool lit // a number written in the code } struct Place { str addr ast.Type ty } struct Ref { bool isPlace Place pl Val val } struct Gen { []ast.Program progs []str warned // file:line:col of the warnings given bool trace // each function keeps a frame { prev, name, file, line } in @finch_trace str frame // the current function's frame ("" in helpers) str frameLine // where its line goes []str constBusy // the constants being worked out: a loop between them is an error []str files []str head []str out []str body []str allocas int tmp int lbl int nstr bool term []Scope scopes []Pending pending []Loop loops FnSig cur str module []FnSig sigs []StructDef structs []Template templates []str tnames // the type parameters in force, and what they stand for []ast.Type targs str instNote // " (while building max[Pixel])", added to errors found inside a generic []EnumDef enums []SumDef sums []str done []str mm // names of methods that change self, for the mutation analysis int nvars // variables declared so far in this function cimport.CImports ci bool shared // building a library (finch build --shared): no main, its export fns are the way in bool index // the language server's index is being made: see note... []str idx // its lines bool debug // -g: debug info (DWARF, as LLVM metadata) platform.Target tg // the system the program is for str cwd []str md // the metadata, numbered by position: !0 = md[0] map[str]int mdcache // metadata made already: types by key, locations by line:column map[str]int wide // LLVM types that want more than the 16 bytes malloc and the stack promise: loaded with align 16 []int mdfiles // the DIFile of each source file int dbgScope // the DISubprogram of the function being built, or -1 (helpers have none) int dbgLine int dbgCol int dbgArg // the parameter number of the next variable, or 0 []str cdone // C functions and variables already declared str ftarget // the call an enclosing or / try is waiting on (see nodeKey) str ffailed // set by that call: an i1, true if it failed str fmsg // and the error text bool deferOn // running a defer: variables declared after it are out of sight int deferLimit int inDefer []str features // the processor features of the whole program (--cpu) []str curFeatures // and of the function being built: those and its @cpu(...) } // ---------- basics ---------- // ---------- the language server's index ---------- // // `finch index file.fch` runs the compiler up to the end of type checking and prints what it learned instead of // the program, one record per line, fields separated by tabs: // @R file line col len defFile defLine defCol hover a name in the source, what it is, where it was made // @V name type file line col fnFile fnLine fnEnd a variable (for completion), in the function that spans those lines // @F module name recv file line col endLine signature a function or method // @S module name file line col and @SF module struct field type // @E module name file line col variants an enum // @I module a module the file imports // Text in a record has its newlines written \n. fn esc(str s) -> str { return s.replace("\\", "\\\\").replace("\n", "\\n").replace("\t", " ") } fn noteRef(ptr[Gen] g, ast.Node at, int col, int len, str hover, str defPath, int defLine, int defCol) { if !g.index { return } g.idx.push("@R\t" + g.files[at.file] + "\t" + str(at.line) + "\t" + str(col) + "\t" + str(len) + "\t" + defPath + "\t" + str(defLine) + "\t" + str(defCol) + "\t" + esc(hover)) } // fn add(int a, int b) -> int fn Counter.add(int n) fn parse(str s) -> int! fn sigText(FnSig sig) -> str { []str ps for k in 0..sig.params.len { ps.push(ast.show(sig.params[k]) + " " + sig.pnames[k]) } name := sig.name if sig.recv.len > 0 { name = sig.recv + "." + sig.name } text := "fn " + name + "(" + ps.join(", ") + ")" if sig.fallible { if sig.ret.kind == "void" { text = text + " -> !" } else { text = text + " -> " + ast.show(sig.ret) + "!" } } else if sig.ret.kind != "void" { text = text + " -> " + ast.show(sig.ret) } return text } // a call of a Finch function or method: the name in the source, and the function it is fn noteCall(ptr[Gen] g, ast.Node c, FnSig sig) { if !g.index { return } col := c.col if c.kind == "method" { col = c.col + 1 } f := g.progs[sig.prog].fns[sig.index] shownName := sig.name if sig.targs.len > 0 && sig.recv.len == 0 { shownName = f.name } noteRef(g, c, col, shownName.len, "```finch\n" + sigText(sig) + "\n```", g.files[f.file], f.line, f.col + 3) } // what is known about every function, struct and enum, for completion and the outline fn indexAll(ptr[Gen] g, []str mainImports) { for i in 0..mainImports.len { g.idx.push("@I\t" + mainImports[i] + "\t" + g.progs[0].modules[i]) // the name used here, the module } for sg in g.sigs { f := g.progs[sg.prog].fns[sg.index] if sg.targs.len > 0 { continue } []str ps for k in 0..sg.params.len { ps.push(ast.show(sg.params[k]) + " " + sg.pnames[k]) } g.idx.push("@F\t" + sg.module + "\t" + sg.name + "\t" + sg.recv + "\t" + g.files[f.file] + "\t" + str(f.line) + "\t" + str(f.col) + "\t" + str(f.endLine) + "\t" + esc(sigText(sg)) + "\t" + ps.join("|")) } for tp in g.templates { if tp.isStruct { continue } f := g.progs[tp.prog].fns[tp.index] g.idx.push("@F\t" + tp.module + "\t" + tp.name + "\t" + tp.recv + "\t" + g.files[f.file] + "\t" + str(f.line) + "\t" + str(f.col) + "\t" + str(f.endLine) + "\t" + esc("fn " + tp.name + "[" + tp.tparams.join(", ") + "](...)") + "\t") } for sd in g.structs { if sd.isC || sd.base.len > 0 { continue } node := g.progs[sd.prog].structs[sd.index] g.idx.push("@S\t" + sd.module + "\t" + sd.name + "\t" + g.files[node.file] + "\t" + str(node.line) + "\t" + str(node.col)) for k in 0..sd.fnames.len { g.idx.push("@SF\t" + sd.module + "\t" + sd.name + "\t" + sd.fnames[k] + "\t" + ast.show(sd.ftypes[k])) } } for pi in 0..g.progs.len { for en in g.progs[pi].enums { vs := "" []str names for v in en.kids { names.push(v.name) } vs = names.join(",") g.idx.push("@E\t" + g.progs[pi].module + "\t" + en.name + "\t" + g.files[en.file] + "\t" + str(en.line) + "\t" + str(en.col) + "\t" + vs) } } } fn fail(ptr[Gen] g, ast.Node n, str msg) { if g.index && g.idx.len > 0 { print(g.idx.join("\n")) // what was learned before the error } print(g.files[n.file] + ":" + str(n.line) + ":" + str(n.col) + ": error: " + msg + g.instNote) exit(1) } // A warning: the program is still built. On stderr (finch ir's output stays the IR); an @W record for editors. fn warn(ptr[Gen] g, ast.Node n, str msg) { where := g.files[n.file] + ":" + str(n.line) + ":" + str(n.col) if g.warned.contains(where) { return // once per place (a generic function is built more than once) } g.warned.push(where) if g.index { g.idx.push("@W\t" + g.files[n.file] + "\t" + str(n.line) + "\t" + str(n.col) + "\t" + msg) return } fputs(where + ": warning: " + msg + "\n", stderr) } // About how many bytes of plain data (numbers, structs of them, C's [N]T) a value has, for the copy warnings fn byteSize(ptr[Gen] g, ast.Type t) -> int { k := t.kind if k == "struct" { sd := g.structs[findStruct(g, t.module, t.name)] if sd.isC && sd.csize > 0 { return sd.csize } n := 0 for ft in sd.ftypes { n += byteSize(g, ft) } return n } if (k == "fixed" || k == "vec") && t.elem.len > 0 { return t.count * byteSize(g, t.elem[0]) } if k == "str" || k == "array" || k == "map" { return 0 // only its { ptr, len, cap } is copied when it is passed: not counted } if k == "ptr" || k == "enum" || k == "int" || k == "float" { return 8 } return bitsOf(t) / 8 + int(bitsOf(t) == 1) } // a struct this big passed or looped over by value is copied byte by byte each time fn bigCopy(ptr[Gen] g, ast.Type t) -> int { if t.kind != "struct" { return 0 } n := byteSize(g, t) if n >= 256 { return n } return 0 } fn emit(ptr[Gen] g, str line) { // with -g every call in a function that has debug info has to say where it is if g.dbgScope >= 0 && line.contains("call ") { g.body.push(" " + line + ", !dbg !" + str(dbgLocation(g))) return } g.body.push(" " + line) } fn tmpName(ptr[Gen] g) -> str { g.tmp += 1 return "%t" + str(g.tmp) } fn newLabel(ptr[Gen] g, str hint) -> str { g.lbl += 1 return hint + str(g.lbl) } fn label(ptr[Gen] g, str l) { g.body.push(l + ":") g.term = false } fn br(ptr[Gen] g, str l) { if !g.term { emit(g, "br label %" + l) g.term = true } } fn condBr(ptr[Gen] g, str c, str yes, str no) { emit(g, "br i1 " + c + ", label %" + yes + ", label %" + no) g.term = true } fn alloca(ptr[Gen] g, str ty) -> str { g.tmp += 1 name := "%a" + str(g.tmp) g.allocas.push(" " + name + " = alloca " + ty + ", align 16") return name } fn load(ptr[Gen] g, str ty, str at) -> str { t := tmpName(g) emit(g, t + " = load " + ty + ", ptr " + at + alignFor(g, ty)) return t } fn store(ptr[Gen] g, str ty, str v, str at) { emit(g, "store " + ty + " " + v + ", ptr " + at + alignFor(g, ty)) } // A vector of 32 or 64 bytes (f32x8, i8x64) wants to sit at an address that many bytes apart, and so does a struct // that holds one; memory from malloc and the stack is 16 apart. Telling LLVM so keeps it from using the // instructions that crash on a smaller alignment. fn alignFor(ptr[Gen] g, str ty) -> str { if g.wide.len > 0 && g.wide.has(ty) { return ", align 16" } return "" } fn spill(ptr[Gen] g, Val v) -> str { a := alloca(g, llty(g, v.ty)) store(g, llty(g, v.ty), v.v, a) return a } fn op(ptr[Gen] g, str text) -> str { t := tmpName(g) emit(g, t + " = " + text) return t } fn has([]str list, str x) -> bool { return list.contains(x) } // ---------- types ---------- fn T(str kind) -> ast.Type { return ast.simple(kind) } // `int` is i64 and `float` is f64: the everyday names // for the two default sizes, and the only way they are ever written back. fn isSigned(ast.Type t) -> bool { k := t.kind return k == "int" || k == "i8" || k == "i16" || k == "i32" } fn isUnsigned(ast.Type t) -> bool { k := t.kind return k == "u8" || k == "u16" || k == "u32" || k == "u64" } fn isInt(ast.Type t) -> bool { return isSigned(t) || isUnsigned(t) } fn isFloat(ast.Type t) -> bool { return t.kind == "float" || t.kind == "f32" } fn isNumber(ast.Type t) -> bool { return isInt(t) || isFloat(t) } fn bitsOf(ast.Type t) -> int { k := t.kind if k == "bool" { return 1 } if k == "char" || k == "i8" || k == "u8" { return 8 } if k == "i16" || k == "u16" { return 16 } if k == "i32" || k == "u32" || k == "f32" { return 32 } return 64 } // Can every value of `from` be stored in `to`? (i8 -> i32 yes, i32 -> u32 no) fn widens(ast.Type from, ast.Type to) -> bool { if !isInt(from) || !isInt(to) { return false } if isSigned(from) && isUnsigned(to) { return false } return bitsOf(to) > bitsOf(from) } // Does the number `text` (written in the code, of type `from`) fit into `to`? fn fits(str text, ast.Type from, ast.Type to) -> bool { if !isInt(to) { return false } n := int(text) b := bitsOf(to) if n < 0 { if isUnsigned(to) { return false } if b == 64 { return true } return n >= 0 - (1 << (b - 1)) } if b == 64 { return true } max := (1 << b) - 1 if isSigned(to) { max = (1 << (b - 1)) - 1 } return n <= max } fn findStruct(ptr[Gen] g, str module, str name) -> int { for i in 0..g.structs.len { if g.structs[i].module == module && g.structs[i].name == name { return i } } return -1 } fn findEnum(ptr[Gen] g, str module, str name) -> int { for i in 0..g.enums.len { if g.enums[i].module == module && g.enums[i].name == name { return i } } return -1 } fn findSum(ptr[Gen] g, str module, str name) -> int { for i in 0..g.sums.len { if g.sums[i].module == module && g.sums[i].name == name { return i } } return -1 } fn progOf(ptr[Gen] g, str module) -> int { for i in 0..g.progs.len { if g.progs[i].module == module { return i } } return -1 } fn imports(ptr[Gen] g, str module) -> bool { return modFor(g, module).len > 0 } // the module a name stands for in this file: button -> "ui/button", p -> "path"; "" if none fn modFor(ptr[Gen] g, str name) -> str { pr := g.progs[progOf(g, g.module)] k := pr.imports.find(name) if k < 0 { return "" } return pr.modules[k] } fn resolve(ptr[Gen] g, ast.Type t, ast.Node at) -> ast.Type { if t.kind == "array" { return ast.arrayOf(resolve(g, t.elem[0], at)) } if t.kind == "map" { key := resolve(g, t.elem[0], at) if !isInt(key) && key.kind != "char" && key.kind != "bool" && key.kind != "str" && key.kind != "enum" { fail(g, at, "a map key must be a whole number, a char, a bool or a str, not " + ast.show(key)) } value := resolve(g, t.elem[1], at) if value.kind == "void" { fail(g, at, "a map value can't have the type 'nothing'") } return ast.mapOf(key, value) } if t.kind == "fixed" { out := ast.Type(kind: "fixed", count: t.count) out.elem.push(resolve(g, t.elem[0], at)) return out } if t.kind == "ptr" { if t.elem.len == 0 { return t } return ast.ptrTo(resolve(g, t.elem[0], at)) } if t.kind != "named" { return t } // T inside a generic if t.module.len == 0 && t.elem.len == 0 && g.tnames.contains(t.name) { return g.targs[g.tnames.find(t.name)] } // Stack[int] if t.elem.len > 0 { []ast.Type args for a in t.elem { args.push(resolve(g, a, at)) } gm := g.module if t.module.len > 0 { if !imports(g, t.module) { fail(g, at, "'" + t.module + "' is not imported here (add: import " + t.module + ")") } gm = modFor(g, t.module) } return instStruct(g, gm, t.name, args, at) } if t.module == "C" { if findStruct(g, "C", t.name) < 0 { fail(g, at, "there is no C struct named '" + t.name + "'") } return ast.Type(kind: "struct", name: t.name, module: "C") } module := g.module if t.module.len > 0 { if !imports(g, t.module) { fail(g, at, "'" + t.module + "' is not imported here (add: import " + t.module + ")") } module = modFor(g, t.module) } if findEnum(g, module, t.name) >= 0 { return ast.Type(kind: "enum", name: t.name, module: module) } if findStruct(g, module, t.name) < 0 { // a bare name may be a struct from a C header if t.module.len == 0 && findStruct(g, "C", t.name) >= 0 { return ast.Type(kind: "struct", name: t.name, module: "C") } fail(g, at, "there is no type named '" + ast.show(t) + "'") } return ast.Type(kind: "struct", name: t.name, module: module) } // Stack[int] and Pair[str, int] as part of an LLVM name fn mangle(str name) -> str { return name.replace("[", ".").replace("]", "").replace(",", ".").replace(" ", "") } fn modName(str m) -> str { if m.len == 0 { return "main" } return m.replace("/", "$") // ui/button -> ui$button in symbol names } fn llty(ptr[Gen] g, ast.Type t) -> str { k := t.kind if k == "bool" { return "i1" } if isInt(t) || k == "char" { return "i" + str(bitsOf(t)) } if k == "float" { return "double" } if k == "f32" { return "float" } if k == "str" || k == "array" { return "{ ptr, i64, i64 }" } if k == "map" { return "{ ptr, i64, i64, i64, ptr, i64, i64 }" // entries, len, used, cap, index, icap, adds } if k == "fixed" { return "[" + str(t.count) + " x " + llty(g, t.elem[0]) + "]" } if k == "vec" { out := "<" + str(t.count) + " x " + llty(g, t.elem[0]) + ">" if t.count * bitsOf(t.elem[0]) > 128 && !g.wide.has(out) { g.wide[out] = 1 } return out } if k == "enum" { return "i32" } if k == "ptr" || k == "null" { return "ptr" } if k == "void" { return "void" } return "%S." + modName(t.module) + "." + mangle(t.name) } // What a function returns as far as LLVM is concerned: a function that can fail // returns { i1 ok, T value, str error } (an i8 stands in for no value). fn retTy(ptr[Gen] g, FnSig sig) -> str { if !sig.fallible { return llty(g, sig.ret) } v := "i8" if sig.ret.kind != "void" { v = llty(g, sig.ret) } return "{ i1, " + v + ", { ptr, i64, i64 } }" } // Identifies a call node, so that the call an `or` / `try` is waiting for can be told from the ones inside it. fn nodeKey(ast.Node n) -> str { return n.kind + ":" + str(n.line) + ":" + str(n.col) + ":" + str(n.file) } fn zeroOf(ast.Type t) -> str { k := t.kind if isInt(t) || k == "char" || k == "enum" { return "0" } if k == "bool" { return "false" } if isFloat(t) { return "0.0" } if k == "ptr" || k == "null" { return "null" } return "zeroinitializer" } fn sizeOf(ptr[Gen] g, ast.Type t) -> str { return "ptrtoint (ptr getelementptr (" + llty(g, t) + ", ptr null, i32 1) to i64)" } fn owning(ptr[Gen] g, ast.Type t) -> bool { if t.kind == "str" || t.kind == "array" || t.kind == "map" { return true } if t.kind == "struct" { return g.structs[findStruct(g, t.module, t.name)].owning } return false } fn typeKey(ast.Type t) -> str { if t.kind == "array" { return "arr_" + typeKey(t.elem[0]) } if t.kind == "map" { return "map_" + typeKey(t.elem[0]) + "_" + typeKey(t.elem[1]) } if t.kind == "enum" { return "E_" + modName(t.module) + "_" + t.name } if t.kind == "vec" { return ast.show(t) } if t.kind == "struct" { return "S_" + modName(t.module) + "_" + mangle(t.name) } return t.kind } // ---------- string constants ---------- fn hex(int n) -> str { digits := "0123456789ABCDEF" return str(digits[n / 16]) + str(digits[n % 16]) } fn strGlobal(ptr[Gen] g, str s) -> str { g.nstr += 1 name := "@.str." + str(g.nstr) []str enc for c in s { n := int(c) if n < 0 { n += 256 } if n >= 32 && n < 127 && c != '"' && c != '\\' { enc.push(str(c)) } else { enc.push("\\" + hex(n)) } } g.head.push(name + " = private unnamed_addr constant [" + str(s.len + 1) + " x i8] c\"" + enc.join("") + "\\00\"") return name } fn strConst(ptr[Gen] g, str s) -> Val { gl := strGlobal(g, s) return Val(v: "{ ptr " + gl + ", i64 " + str(s.len) + ", i64 0 }", ty: T("str"), fresh: true) } // ---------- runtime errors ---------- fn panicIf(ptr[Gen] g, str cond, str msg, ast.Node at) { bad := newLabel(g, "panic") ok := newLabel(g, "ok") condBr(g, cond, bad, ok) label(g, bad) file := strGlobal(g, g.files[at.file]) m := strGlobal(g, msg) emit(g, "call void @finch_panic(ptr " + file + ", i64 " + str(at.line) + ", ptr " + m + ")") emit(g, "unreachable") g.term = true label(g, ok) } fn boundsCheck(ptr[Gen] g, str i, str len, ast.Node at) { bad := newLabel(g, "range") ok := newLabel(g, "inrange") c := op(g, "icmp uge i64 " + i + ", " + len) condBr(g, c, bad, ok) label(g, bad) file := strGlobal(g, g.files[at.file]) emit(g, "call void @finch_panic_index(ptr " + file + ", i64 " + str(at.line) + ", i64 " + i + ", i64 " + len + ")") emit(g, "unreachable") g.term = true label(g, ok) } // ---------- helper functions (generated once per type) ---------- struct Saved { str frame str frameLine []str body []str allocas int tmp bool term int dbgScope } fn beginHelper(ptr[Gen] g) -> Saved { s := Saved(body: g.body, allocas: g.allocas, tmp: g.tmp, term: g.term, dbgScope: g.dbgScope, frame: g.frame, frameLine: g.frameLine) g.dbgScope = -1 // a helper made on the side has no source lines g.frame = "" g.frameLine = "" []str empty g.body = empty g.allocas = empty g.tmp = 0 g.term = false return s } fn endHelper(ptr[Gen] g, Saved s, str header) { g.out.push(header + " {\nentry:\n" + g.allocas.join("\n") + "\n" + g.body.join("\n") + "\n}\n") g.body = s.body g.allocas = s.allocas g.tmp = s.tmp g.term = s.term g.dbgScope = s.dbgScope g.frame = s.frame g.frameLine = s.frameLine } // for i in 0..n, inside the current function fn loopStart(ptr[Gen] g, str n) -> []str { iv := alloca(g, "i64") store(g, "i64", "0", iv) cond := newLabel(g, "n.cond") body := newLabel(g, "n.body") end := newLabel(g, "n.end") br(g, cond) label(g, cond) i := load(g, "i64", iv) c := op(g, "icmp slt i64 " + i + ", " + n) condBr(g, c, body, end) label(g, body) return [iv, cond, end, i] } fn loopEnd(ptr[Gen] g, []str l) { i := load(g, "i64", l[0]) store(g, "i64", op(g, "add i64 " + i + ", 1"), l[0]) br(g, l[1]) label(g, l[2]) } fn copyAt(ptr[Gen] g, str dst, str src, ast.Type t) { if t.kind == "str" { emit(g, "call void @finch_str_copy(ptr " + dst + ", ptr " + src + ")") } else if t.kind == "array" || t.kind == "map" || (t.kind == "struct" && owning(g, t)) { emit(g, "call void " + copyFn(g, t) + "(ptr " + dst + ", ptr " + src + ")") } else { store(g, llty(g, t), load(g, llty(g, t), src), dst) } } fn copyFn(ptr[Gen] g, ast.Type t) -> str { name := "@finch.copy." + typeKey(t) if has(g.done, name) { return name } g.done.push(name) s := beginHelper(g) if t.kind == "array" { et := t.elem[0] emit(g, "call void @finch_arr_clone_raw(ptr %dst, ptr %src, i64 " + sizeOf(g, et) + ")") if owning(g, et) { d := load(g, "ptr", "%dst") sp := load(g, "ptr", "%src") len := load(g, "i64", op(g, "getelementptr inbounds { ptr, i64, i64 }, ptr %src, i32 0, i32 1")) l := loopStart(g, len) dp := op(g, "getelementptr inbounds " + llty(g, et) + ", ptr " + d + ", i64 " + l[3]) spp := op(g, "getelementptr inbounds " + llty(g, et) + ", ptr " + sp + ", i64 " + l[3]) copyAt(g, dp, spp, et) loopEnd(g, l) } } else if t.kind == "map" { emit(g, "call void @finch_map_clone_raw(ptr %dst, ptr %src, i64 " + entrySize(g, t) + ")") if owning(g, t.elem[0]) || owning(g, t.elem[1]) { // the slots were copied byte for byte: now give the copy its own keys and values used := load(g, "i64", op(g, "getelementptr inbounds " + llty(g, t) + ", ptr %src, i32 0, i32 2")) l := loopStart(g, used) from := entryAt(g, "%src", t, l[3]) to := entryAt(g, "%dst", t, l[3]) live := newLabel(g, "entry.live") next := newLabel(g, "entry.next") condBr(g, op(g, "icmp ne i64 " + load(g, "i64", op(g, "getelementptr inbounds " + entryTy(g, t) + ", ptr " + from + ", i32 0, i32 0")) + ", 0"), live, next) label(g, live) copyAt(g, entryField(g, to, t, 1), entryField(g, from, t, 1), t.elem[0]) copyAt(g, entryField(g, to, t, 2), entryField(g, from, t, 2), t.elem[1]) br(g, next) label(g, next) loopEnd(g, l) } } else { sd := g.structs[findStruct(g, t.module, t.name)] st := llty(g, t) store(g, st, load(g, st, "%src"), "%dst") for k in 0..sd.ftypes.len { if owning(g, sd.ftypes[k]) { dp := op(g, "getelementptr inbounds " + st + ", ptr %dst, i32 0, i32 " + str(sd.lidx[k])) spp := op(g, "getelementptr inbounds " + st + ", ptr %src, i32 0, i32 " + str(sd.lidx[k])) copyAt(g, dp, spp, sd.ftypes[k]) } } } emit(g, "ret void") endHelper(g, s, "define internal void " + name + "(ptr %dst, ptr %src)") return name } fn copyValue(ptr[Gen] g, str v, ast.Type t) -> str { if !owning(g, t) { return v } src := spill(g, Val(v: v, ty: t)) dst := alloca(g, llty(g, t)) copyAt(g, dst, src, t) return load(g, llty(g, t), dst) } // ---------- dropping ---------- fn dropAt(ptr[Gen] g, str slot, ast.Type t) { if !owning(g, t) { return } if t.kind == "str" { emit(g, "call void @finch_str_drop(ptr " + slot + ")") } else { emit(g, "call void " + dropFn(g, t) + "(ptr " + slot + ")") } } fn dropValue(ptr[Gen] g, str v, ast.Type t) { if owning(g, t) { dropAt(g, spill(g, Val(v: v, ty: t)), t) } } // a temporary nobody keeps: dropped right after use. Constants (text literals, empty arrays) own no memory. fn release(ptr[Gen] g, Val v) { if v.fresh && v.v.len > 0 && !isConstant(v.v) && owning(g, v.ty) { dropValue(g, v.v, v.ty) } } fn isConstant(str v) -> bool { return !v.starts_with("%") } fn dropFn(ptr[Gen] g, ast.Type t) -> str { name := "@finch.drop." + typeKey(t) if has(g.done, name) { return name } g.done.push(name) s := beginHelper(g) if t.kind == "array" { et := t.elem[0] if owning(g, et) { d := load(g, "ptr", "%a") len := load(g, "i64", op(g, "getelementptr inbounds { ptr, i64, i64 }, ptr %a, i32 0, i32 1")) l := loopStart(g, len) ep := op(g, "getelementptr inbounds " + llty(g, et) + ", ptr " + d + ", i64 " + l[3]) dropAt(g, ep, et) loopEnd(g, l) } emit(g, "call void @finch_arr_free(ptr %a)") } else if t.kind == "map" { kt := t.elem[0] vt := t.elem[1] if owning(g, kt) || owning(g, vt) { used := load(g, "i64", op(g, "getelementptr inbounds " + llty(g, t) + ", ptr %a, i32 0, i32 2")) l := loopStart(g, used) e := entryAt(g, "%a", t, l[3]) live := newLabel(g, "entry.live") next := newLabel(g, "entry.next") condBr(g, op(g, "icmp ne i64 " + load(g, "i64", entryField(g, e, t, 0)) + ", 0"), live, next) label(g, live) dropAt(g, entryField(g, e, t, 1), kt) dropAt(g, entryField(g, e, t, 2), vt) br(g, next) label(g, next) loopEnd(g, l) } emit(g, "call void @finch_map_free(ptr %a)") } else { sd := g.structs[findStruct(g, t.module, t.name)] st := llty(g, t) for k in 0..sd.ftypes.len { if owning(g, sd.ftypes[k]) { dropAt(g, op(g, "getelementptr inbounds " + st + ", ptr %a, i32 0, i32 " + str(sd.lidx[k])), sd.ftypes[k]) } } } emit(g, "ret void") endHelper(g, s, "define internal void " + name + "(ptr %a)") return name } // ---------- values held while an expression goes on ---------- fn hold(ptr[Gen] g, Val v) { if v.fresh && v.v.len > 0 && !isConstant(v.v) && owning(g, v.ty) { g.pending.push(Pending(v: v.v, ty: v.ty, isAddr: false)) } } fn holdAddr(ptr[Gen] g, str slot, ast.Type t) { if owning(g, t) { g.pending.push(Pending(v: slot, ty: t, isAddr: true)) } } // emit the drops of pending[downTo..], newest first (for a path that leaves the function or a loop) fn dropPending(ptr[Gen] g, int downTo) { i := g.pending.len while i > downTo { i -= 1 p := g.pending[i] if p.isAddr { dropAt(g, p.v, p.ty) } else { dropValue(g, p.v, p.ty) } } } // the expression is done: what it held is not pending any more fn heldEnd(ptr[Gen] g, int mark) { while g.pending.len > mark { g.pending.pop() } } // something that can be stored: a fresh value moves, anything else is copied fn own(ptr[Gen] g, Val v) -> str { if !owning(g, v.ty) || v.fresh { return v.v } return copyValue(g, v.v, v.ty) } // ---------- printing ---------- // f32 prints the shortest text that is the same f32 (0.1, not 0.100000001) fn floatName(ast.Type t) -> str { if t.kind == "f32" { return "f32" } return "float" } fn single(ast.Type t) -> str { if t.kind == "f32" { return "1" } return "0" } fn printf(ptr[Gen] g, str fmt, str args) { f := strGlobal(g, fmt) t := tmpName(g) extra := "" if args.len > 0 { extra = ", " + args } emit(g, t + " = call i32 (ptr, ...) @printf(ptr " + f + extra + ")") } fn cstr(ptr[Gen] g, str v) -> str { p := op(g, "extractvalue { ptr, i64, i64 } " + v + ", 0") isNull := op(g, "icmp eq ptr " + p + ", null") empty := strGlobal(g, "") return op(g, "select i1 " + isNull + ", ptr " + empty + ", ptr " + p) } fn emitPrint(ptr[Gen] g, str v, ast.Type t, bool quoted) { k := t.kind if isSigned(t) { printf(g, "%lld", "i64 " + intCast(g, v, t, T("int"))) } else if isUnsigned(t) { printf(g, "%llu", "i64 " + intCast(g, v, t, T("u64"))) } else if isFloat(t) { emit(g, "call void @finch_print_float(double " + floatCast(g, v, t, T("float")) + ", i32 " + single(t) + ")") } else if k == "char" { z := op(g, "sext i8 " + v + " to i32") if quoted { printf(g, "'%c'", "i32 " + z) } else { printf(g, "%c", "i32 " + z) } } else if k == "bool" { yes := strGlobal(g, "true") no := strGlobal(g, "false") s := op(g, "select i1 " + v + ", ptr " + yes + ", ptr " + no) printf(g, "%s", "ptr " + s) } else if k == "str" { len := op(g, "extractvalue { ptr, i64, i64 } " + v + ", 1") n := op(g, "trunc i64 " + len + " to i32") p := cstr(g, v) if quoted { printf(g, "\"%.*s\"", "i32 " + n + ", ptr " + p) } else { printf(g, "%.*s", "i32 " + n + ", ptr " + p) } } else if k == "ptr" { isNull := op(g, "icmp eq ptr " + v + ", null") nul := newLabel(g, "ptr.null") at := newLabel(g, "ptr.addr") done := newLabel(g, "ptr.done") condBr(g, isNull, nul, at) label(g, nul) printf(g, "null", "") br(g, done) label(g, at) printf(g, "0x%llx", "i64 " + op(g, "ptrtoint ptr " + v + " to i64")) br(g, done) label(g, done) } else if k == "null" { printf(g, "null", "") } else { // arrays and structs: built as text first, the same text str(x) gives buf := alloca(g, "{ ptr, i64, i64 }") store(g, "{ ptr, i64, i64 }", "zeroinitializer", buf) emitFormat(g, buf, v, t, quoted) emit(g, "call void @finch_buf_print(ptr " + buf + ")") } } fn bufText(ptr[Gen] g, str buf, str text) { emit(g, "call void @finch_buf_add(ptr " + buf + ", ptr " + strGlobal(g, text) + ", i64 " + str(text.len) + ")") } // Append a value's text to the str at `buf`. Quoted: how it looks inside an array or struct. fn emitFormat(ptr[Gen] g, str buf, str v, ast.Type t, bool quoted) { k := t.kind q := "0" if quoted { q = "1" } if isSigned(t) { emit(g, "call void @finch_buf_int(ptr " + buf + ", i64 " + intCast(g, v, t, T("int")) + ")") } else if isUnsigned(t) { emit(g, "call void @finch_buf_uint(ptr " + buf + ", i64 " + intCast(g, v, t, T("u64")) + ")") } else if isFloat(t) { emit(g, "call void @finch_buf_" + floatName(t) + "(ptr " + buf + ", double " + floatCast(g, v, t, T("float")) + ")") } else if k == "char" { emit(g, "call void @finch_buf_char(ptr " + buf + ", i8 " + v + ", i32 " + q + ")") } else if k == "bool" { emit(g, "call void @finch_buf_bool(ptr " + buf + ", i32 " + op(g, "zext i1 " + v + " to i32") + ")") } else if k == "str" { emit(g, "call void @finch_buf_str(ptr " + buf + ", ptr " + spill(g, Val(v: v, ty: t)) + ", i32 " + q + ")") } else if k == "ptr" || k == "null" { emit(g, "call void @finch_buf_ptr(ptr " + buf + ", ptr " + v + ")") } else { emit(g, "call void " + formatFn(g, t) + "(ptr " + buf + ", ptr " + spill(g, Val(v: v, ty: t)) + ")") } } // One function per array / struct type: (str *buf, T *value) fn formatFn(ptr[Gen] g, ast.Type t) -> str { name := "@finch.format." + typeKey(t) if has(g.done, name) { return name } g.done.push(name) s := beginHelper(g) if t.kind == "vec" { // f32x4(1, 2.5, 3, 4) v := load(g, llty(g, t), "%a") bufText(g, "%buf", ast.show(t) + "(") for k in 0..t.count { if k > 0 { bufText(g, "%buf", ", ") } lane := op(g, "extractelement " + llty(g, t) + " " + v + ", i64 " + str(k)) emitFormat(g, "%buf", lane, t.elem[0], false) } bufText(g, "%buf", ")") } else if t.kind == "array" { et := t.elem[0] d := load(g, "ptr", "%a") len := load(g, "i64", op(g, "getelementptr inbounds { ptr, i64, i64 }, ptr %a, i32 0, i32 1")) bufText(g, "%buf", "[") l := loopStart(g, len) sep := newLabel(g, "sep") item := newLabel(g, "item") c := op(g, "icmp sgt i64 " + l[3] + ", 0") condBr(g, c, sep, item) label(g, sep) bufText(g, "%buf", ", ") br(g, item) label(g, item) ep := op(g, "getelementptr inbounds " + llty(g, et) + ", ptr " + d + ", i64 " + l[3]) emitFormat(g, "%buf", load(g, llty(g, et), ep), et, true) loopEnd(g, l) bufText(g, "%buf", "]") } else if t.kind == "enum" { // the variant's name; a number that is no variant shows as Color(7) ed := g.enums[findEnum(g, t.module, t.name)] v := load(g, "i32", "%a") def := newLabel(g, "enum.other") done := newLabel(g, "enum.done") []str cases []str labels for k in 0..ed.variants.len { labels.push(newLabel(g, "enum.v")) cases.push("i32 " + str(ed.values[k]) + ", label %" + labels[k]) } emit(g, "switch i32 " + v + ", label %" + def + " [ " + cases.join(" ") + " ]") g.term = true for k in 0..ed.variants.len { label(g, labels[k]) bufText(g, "%buf", ed.variants[k]) br(g, done) } label(g, def) bufText(g, "%buf", ed.name + "(") emit(g, "call void @finch_buf_int(ptr %buf, i64 " + op(g, "sext i32 " + v + " to i64") + ")") bufText(g, "%buf", ")") br(g, done) label(g, done) } else if t.kind == "map" { // {"a": 1, "b": 2} first := alloca(g, "i1") store(g, "i1", "true", first) bufText(g, "%buf", "{") used := load(g, "i64", op(g, "getelementptr inbounds " + llty(g, t) + ", ptr %a, i32 0, i32 2")) l := loopStart(g, used) e := entryAt(g, "%a", t, l[3]) live := newLabel(g, "entry.live") next := newLabel(g, "entry.next") condBr(g, op(g, "icmp ne i64 " + load(g, "i64", entryField(g, e, t, 0)) + ", 0"), live, next) label(g, live) sep := newLabel(g, "sep") item := newLabel(g, "item") condBr(g, load(g, "i1", first), item, sep) label(g, sep) bufText(g, "%buf", ", ") br(g, item) label(g, item) store(g, "i1", "false", first) emitFormat(g, "%buf", load(g, llty(g, t.elem[0]), entryField(g, e, t, 1)), t.elem[0], true) bufText(g, "%buf", ": ") emitFormat(g, "%buf", load(g, llty(g, t.elem[1]), entryField(g, e, t, 2)), t.elem[1], true) br(g, next) label(g, next) loopEnd(g, l) bufText(g, "%buf", "}") } else if t.kind == "struct" && findSum(g, t.module, t.name) >= 0 { // Circle(2.5), Point sm := g.sums[findSum(g, t.module, t.name)] sd := g.structs[findStruct(g, t.module, t.name)] st := llty(g, t) tag := load(g, "i64", op(g, "getelementptr inbounds " + st + ", ptr %a, i32 0, i32 " + str(sd.lidx[0]))) def := newLabel(g, "sum.other") done := newLabel(g, "sum.done") []str cases []str labels for k in 0..sm.variants.len { labels.push(newLabel(g, "sum.v")) cases.push("i64 " + str(k) + ", label %" + labels[k]) } emit(g, "switch i64 " + tag + ", label %" + def + " [ " + cases.join(" ") + " ]") g.term = true for k in 0..sm.variants.len { label(g, labels[k]) bufText(g, "%buf", sm.variants[k]) if sm.counts[k] > 0 { bufText(g, "%buf", "(") for j in 0..sm.counts[k] { if j > 0 { bufText(g, "%buf", ", ") } fi := sm.firsts[k] + j fp := op(g, "getelementptr inbounds " + st + ", ptr %a, i32 0, i32 " + str(sd.lidx[fi])) emitFormat(g, "%buf", load(g, llty(g, sd.ftypes[fi]), fp), sd.ftypes[fi], true) } bufText(g, "%buf", ")") } br(g, done) } label(g, def) bufText(g, "%buf", sm.name + "(?)") br(g, done) label(g, done) } else { sd := g.structs[findStruct(g, t.module, t.name)] st := llty(g, t) bufText(g, "%buf", sd.name + "(") for k in 0..sd.ftypes.len { sep := ", " if k == 0 { sep = "" } bufText(g, "%buf", sep + sd.fnames[k] + ": ") fp := op(g, "getelementptr inbounds " + st + ", ptr %a, i32 0, i32 " + str(sd.lidx[k])) emitFormat(g, "%buf", load(g, llty(g, sd.ftypes[k]), fp), sd.ftypes[k], true) } bufText(g, "%buf", ")") } emit(g, "ret void") endHelper(g, s, "define internal void " + name + "(ptr %buf, ptr %a)") return name } // ---------- conversions ---------- // An integer of one size as an integer of another: grow by the sign of the // source (sext for signed, zext for unsigned), shrink by cutting. fn intCast(ptr[Gen] g, str v, ast.Type from, ast.Type to) -> str { fb := bitsOf(from) tb := bitsOf(to) if fb == tb { return v } if tb < fb { return op(g, "trunc " + llty(g, from) + " " + v + " to " + llty(g, to)) } if isSigned(from) { return op(g, "sext " + llty(g, from) + " " + v + " to " + llty(g, to)) } return op(g, "zext " + llty(g, from) + " " + v + " to " + llty(g, to)) } fn floatCast(ptr[Gen] g, str v, ast.Type from, ast.Type to) -> str { if from.kind == to.kind { return v } if bitsOf(to) > bitsOf(from) { return op(g, "fpext " + llty(g, from) + " " + v + " to " + llty(g, to)) } return op(g, "fptrunc " + llty(g, from) + " " + v + " to " + llty(g, to)) } fn intToFloat(ptr[Gen] g, str v, ast.Type from, ast.Type to) -> str { if isSigned(from) { return op(g, "sitofp " + llty(g, from) + " " + v + " to " + llty(g, to)) } return op(g, "uitofp " + llty(g, from) + " " + v + " to " + llty(g, to)) } fn coerce(ptr[Gen] g, Val v, ast.Type want, ast.Node at, str what) -> Val { if ast.same(v.ty, want) { return v } if v.ty.kind == "null" && want.kind == "ptr" { return Val(v: "null", ty: want) } if v.ty.kind == "ptr" && want.kind == "ptr" && (v.ty.elem.len == 0 || want.elem.len == 0) { return Val(v: v.v, ty: want) } if isInt(v.ty) && isInt(want) { if v.lit && fits(v.v, v.ty, want) { return Val(v: v.v, ty: want, lit: true) } if widens(v.ty, want) { return Val(v: intCast(g, v.v, v.ty, want), ty: want) } if v.lit { fail(g, at, "the number " + v.v + " doesn't fit in " + ast.show(want)) } } if isInt(v.ty) && isFloat(want) { if v.lit && want.kind == "float" { return Val(v: v.v + ".0", ty: want, lit: true) } return Val(v: intToFloat(g, v.v, v.ty, want), ty: want) } if v.ty.kind == "f32" && want.kind == "float" { return Val(v: floatCast(g, v.v, v.ty, want), ty: want) } if v.ty.kind == "float" && want.kind == "f32" && v.lit { return Val(v: floatCast(g, v.v, v.ty, want), ty: want) } msg := what + " must be " + ast.show(want) + ", but this is " + ast.show(v.ty) if isNumber(v.ty) && isNumber(want) { msg = msg + " (use " + ast.show(want) + "(...) to convert" if isFloat(v.ty) && isInt(want) { msg = msg + ", it cuts off the fraction" } msg = msg + ")" } if want.kind == "str" && (isNumber(v.ty) || v.ty.kind == "bool" || v.ty.kind == "char") { msg = msg + " (turn it into text with str(...))" } if want.kind == "bool" && v.ty.kind == "vec" && v.ty.elem[0].kind == "bool" { msg = msg + " (a comparison of vectors has an answer for every lane: use .all() or .any())" } if want.kind == "vec" && isNumber(v.ty) { msg = msg + " (" + ast.show(want) + "(x) puts x in every lane)" } if want.kind == "vec" && v.ty.kind == "vec" { msg = msg + " (convert it: " + ast.show(want) + "(...))" } fail(g, at, msg) return v } // ---------- scopes ---------- fn lookup(ptr[Gen] g, str name) -> int { // returns scope * 100000 + index, or -1 for k in 0..g.scopes.len { s := g.scopes.len - 1 - k for i in 0..g.scopes[s].vars.len { if g.scopes[s].vars[i].name == name { if g.deferOn && g.scopes[s].vars[i].order >= g.deferLimit { continue } return s * 100000 + i } } } return -1 } fn varAt(ptr[Gen] g, int id) -> Var { return g.scopes[id / 100000].vars[id % 100000] } fn findFn(ptr[Gen] g, str module, str name) -> int { for i in 0..g.sigs.len { if g.sigs[i].module == module && g.sigs[i].name == name && g.sigs[i].recv.len == 0 { return i } } return -1 } fn findMethod(ptr[Gen] g, str module, str recv, str name) -> int { for i in 0..g.sigs.len { if g.sigs[i].recvmod == module && g.sigs[i].recv == recv && g.sigs[i].name == name { return i } } return -1 } fn isBuiltin(str n) -> bool { names := ["print", "addr", "str", "int", "char", "float", "bool", "read_file", "write_file", "file_exists", "shell", "exit", "input", "new", "free", "ptr", "delete_file", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64", "error"] return names.contains(n) } fn addVar(ptr[Gen] g, ast.Node at, str name, ast.Type t, str init, bool readonly, bool owned) -> str { if lookup(g, name) >= 0 && name != "err" { fail(g, at, "a variable named '" + name + "' already exists here") } if findFn(g, g.module, name) >= 0 { fail(g, at, "'" + name + "' is already the name of a function") } if isBuiltin(name) { fail(g, at, "'" + name + "' is a built-in name, pick another one") } slot := alloca(g, llty(g, t)) store(g, llty(g, t), init, slot) sc := g.scopes.len - 1 mine := owned && owning(g, t) if mine { g.scopes[sc].cleanups.push(Cleanup(isDefer: false, idx: g.scopes[sc].vars.len)) } g.scopes[sc].vars.push(Var(name: name, ty: t, slot: slot, readonly: readonly, owned: mine, order: g.nvars, dpath: g.files[at.file], dline: at.line, dcol: at.col)) g.nvars += 1 dbgVariable(g, at, name, t, slot) if g.index && name != "err" && !name.starts_with("$") { fnode := g.progs[g.cur.prog].fns[g.cur.index] g.idx.push("@V\t" + name + "\t" + ast.show(t) + "\t" + g.files[at.file] + "\t" + str(at.line) + "\t" + str(at.col) + "\t" + g.files[fnode.file] + "\t" + str(fnode.line) + "\t" + str(fnode.endLine)) noteRef(g, at, at.col, name.len, "```finch\n" + ast.show(t) + " " + name + "\n```", "", 0, 0) } return slot } fn pushScope(ptr[Gen] g) { g.scopes.push(Scope()) } // The end of a scope: its variables are dropped and its defers run, unless it has already been left by return, // break or continue. fn popScope(ptr[Gen] g) { if !g.term { runCleanups(g, g.scopes.len - 1) } g.scopes.pop() } // Drop the variables of scope `idx` and run its defers, newest first, as if written where the scope ends. fn runCleanups(ptr[Gen] g, int idx) { k := g.scopes[idx].cleanups.len while k > 0 { k -= 1 c := g.scopes[idx].cleanups[k] if !c.isDefer { v := g.scopes[idx].vars[c.idx] if !v.moved { dropAt(g, v.slot, v.ty) } continue } savedOn := g.deferOn savedLimit := g.deferLimit g.deferOn = true g.deferLimit = g.scopes[idx].orders[c.idx] g.inDefer += 1 pushScope(g) stmt(g, g.scopes[idx].defers[c.idx]) popScope(g) g.inDefer -= 1 g.deferOn = savedOn g.deferLimit = savedLimit } } // The defers of every scope from the innermost down to `depth`, for return, break and continue. fn unwind(ptr[Gen] g, int depth) { i := g.scopes.len while i > depth { i -= 1 runCleanups(g, i) } } fn endScope(ptr[Gen] g) { popScope(g) } // ---------- mutation analysis: does a function change a parameter? ---------- fn rootVar(ast.Node e) -> str { if e.kind == "var" { return e.name } if e.kind == "member" || e.kind == "index" { return rootVar(e.kids[0]) } return "" } fn mutates(ptr[Gen] g, str name, ast.Node n) -> bool { changing := ["push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"] if n.kind == "assign" && rootVar(n.kids[0]) == name { return true } // a method that changes its receiver changes whatever it is called on; the // list holds names only, so a plain function by the same name is treated // the same way -- never wrong, sometimes one copy too many if n.kind == "method" && (changing.contains(n.name) || g.mm.contains(n.name)) && rootVar(n.kids[0]) == name { return true } if n.kind == "call" && n.name == "addr" && n.kids.len == 1 && rootVar(n.kids[0]) == name { return true } for k in n.kids { if mutates(g, name, k) { return true } } return false } // ---------- program ---------- fn declare(ptr[Gen] g) { for pi in 0..g.progs.len { g.module = g.progs[pi].module for si in 0..g.progs[pi].structs.len { s := g.progs[pi].structs[si] if findStruct(g, g.module, s.name) >= 0 || findTemplate(g, g.module, s.name, "", true) >= 0 { fail(g, s, "the struct '" + s.name + "' is defined twice") } if s.tparams.len > 0 { // a generic struct: it only becomes a real struct for the types it is used with g.templates.push(Template(module: g.module, name: s.name, tparams: s.tparams, prog: pi, index: si, isStruct: true)) continue } g.structs.push(StructDef(module: g.module, name: s.name, prog: pi, index: si)) } } for pi in 0..g.progs.len { g.module = g.progs[pi].module for ei in 0..g.progs[pi].enums.len { e := g.progs[pi].enums[ei] if e.flag { if findEnum(g, g.module, e.name) >= 0 || findSum(g, g.module, e.name) >= 0 { fail(g, e, "'" + e.name + "' is defined twice") } sd := SumDef(module: g.module, name: e.name) first := 1 for v in e.kids { if sd.variants.contains(v.name) { fail(g, v, "the enum " + e.name + " has the variant " + v.name + " twice") } sd.variants.push(v.name) sd.counts.push(v.kids.len) sd.firsts.push(first) first += v.kids.len } g.sums.push(sd) continue } if findEnum(g, g.module, e.name) >= 0 || findStruct(g, g.module, e.name) >= 0 { fail(g, e, "'" + e.name + "' is defined twice") } ed := EnumDef(module: g.module, name: e.name) next := 0 for v in e.kids { if v.text.len > 0 { next = int(v.text) } if ed.variants.contains(v.name) { fail(g, v, "the enum " + e.name + " has the variant " + v.name + " twice") } if ed.values.contains(next) { fail(g, v, "two variants of " + e.name + " have the number " + str(next) + " (give each its own)") } ed.variants.push(v.name) ed.values.push(next) next += 1 } g.enums.push(ed) } } for ni in 0..g.ci.structOrder.len { g.structs.push(StructDef(module: "C", name: g.ci.structOrder[ni], isC: true, prog: -1, index: ni)) } for i in 0..g.structs.len { resolveStruct(g, i) } for pi in 0..g.progs.len { g.module = g.progs[pi].module for fi in 0..g.progs[pi].fns.len { f := g.progs[pi].fns[fi] if f.tparams.len > 0 { // generic: kept as written, and made for the types it is called with if findTemplate(g, g.module, f.name, f.recv, false) >= 0 { fail(g, f, "function '" + f.name + "' is defined twice") } if f.recv.len > 0 && findTemplate(g, g.module, f.recv, "", true) < 0 { fail(g, f, "there is no generic struct named '" + f.recv + "' here (a method goes in the same file as its struct)") } g.templates.push(Template(module: g.module, name: f.name, recv: f.recv, tparams: f.tparams, prog: pi, index: fi)) continue } recvmod := g.module recvEnum := false if f.recv.len > 0 && findEnum(g, g.module, f.recv) >= 0 { recvEnum = true if findMethod(g, recvmod, f.recv, f.name) >= 0 { fail(g, f, "the method " + f.recv + "." + f.name + " is defined twice") } } else if f.recv.len > 0 { si := findStruct(g, g.module, f.recv) if si < 0 && findStruct(g, "C", f.recv) >= 0 { si = findStruct(g, "C", f.recv) // a method on a struct from C recvmod = "C" } if si < 0 { fail(g, f, "there is no struct named '" + f.recv + "' here (a method goes in the same file as its struct, or on a struct from C)") } if findMethod(g, recvmod, f.recv, f.name) >= 0 { fail(g, f, "the method " + f.recv + "." + f.name + " is defined twice") } if g.structs[si].fnames.contains(f.name) { fail(g, f, "the struct " + f.recv + " already has a field named '" + f.name + "'") } } else { if findFn(g, g.module, f.name) >= 0 { fail(g, f, "function '" + f.name + "' is defined twice") } if isBuiltin(f.name) { fail(g, f, "'" + f.name + "' is a built-in function, pick another name") } } sig := FnSig(module: g.module, name: f.name, recv: f.recv, recvmod: recvmod, recvEnum: recvEnum, fallible: f.fallible, exported: f.exported, prog: pi, index: fi) sig.ll = "@finch." + modName(g.module) + "." + f.name if f.recv.len > 0 { sig.ll = "@finch." + modName(g.module) + "." + f.recv + "." + f.name } sig.ret = resolve(g, f.ty, f) for k in 0..f.kids.len - 1 { if f.kids[k].name == "self" && f.recv.len > 0 { fail(g, f.kids[k], "'self' is already the " + f.recv + " the method is called on") } sig.params.push(resolve(g, f.kids[k].ty, f.kids[k])) sig.pnames.push(f.kids[k].name) } if f.exported { checkExport(g, f, sig) } g.sigs.push(sig) } } // which methods change self? calling one of them on something changes it // too, so repeat until nothing new turns up changed := true while changed { changed = false for i in 0..g.sigs.len { if g.sigs[i].recv.len > 0 && !g.sigs[i].mutself { f := g.progs[g.sigs[i].prog].fns[g.sigs[i].index] if mutates(g, "self", f.kids[f.kids.len - 1]) { g.sigs[i].mutself = true g.mm.push(g.sigs[i].name) changed = true } } } for ti in 0..g.templates.len { tpl := g.templates[ti] if tpl.recv.len > 0 && !tpl.mutself { tf := g.progs[tpl.prog].fns[tpl.index] if mutates(g, "self", tf.kids[tf.kids.len - 1]) { g.templates[ti].mutself = true g.mm.push(tpl.name) changed = true } } } } // a parameter nobody changes is borrowed rather than copied for i in 0..g.sigs.len { f := g.progs[g.sigs[i].prog].fns[g.sigs[i].index] body := f.kids[f.kids.len - 1] for k in 0..g.sigs[i].params.len { pt := g.sigs[i].params[k] g.sigs[i].borrow.push(owning(g, pt) && !mutates(g, g.sigs[i].pnames[k], body)) } } } fn findTemplate(ptr[Gen] g, str module, str name, str recv, bool isStruct) -> int { for i in 0..g.templates.len { t := g.templates[i] if t.module == module && t.name == name && t.recv == recv && t.isStruct == isStruct { return i } } return -1 } fn shownTypes([]ast.Type args) -> str { []str parts for a in args { parts.push(ast.show(a)) } return parts.join(", ") } // Stack[int]: the struct with T = int, made the first time it is asked for. fn instStruct(ptr[Gen] g, str module, str base, []ast.Type args, ast.Node at) -> ast.Type { ti := findTemplate(g, module, base, "", true) if ti < 0 { fail(g, at, "there is no generic struct named '" + base + "' (a struct written struct " + base + "[T] { ... } takes type arguments)") } tpl := g.templates[ti] if args.len != tpl.tparams.len { fail(g, at, base + " takes " + str(tpl.tparams.len) + " type argument(s), like " + base + "[" + tpl.tparams.join(", ") + "], but got " + str(args.len)) } name := base + "[" + shownTypes(args) + "]" if findStruct(g, module, name) < 0 { g.structs.push(StructDef(module: module, name: name, prog: tpl.prog, index: tpl.index, base: base, tnames: tpl.tparams, targs: args)) resolveStruct(g, g.structs.len - 1) } return ast.Type(kind: "struct", name: name, module: module) } // max[int]: the function with T = int; for a method, `recvType` is the struct instance it belongs to. fn instFn(ptr[Gen] g, int ti, []ast.Type args, ast.Type recvType, ast.Node at) -> int { tpl := g.templates[ti] shown := tpl.name + "[" + shownTypes(args) + "]" if tpl.recv.len > 0 { existing := findMethod(g, tpl.module, recvType.name, tpl.name) if existing >= 0 { return existing } } else { existing := findFn(g, tpl.module, shown) if existing >= 0 { return existing } } f := g.progs[tpl.prog].fns[tpl.index] sig := FnSig(module: tpl.module, name: shown, recvmod: tpl.module, fallible: f.fallible, prog: tpl.prog, index: tpl.index, tnames: tpl.tparams, targs: args, mutself: tpl.mutself, shown: shown) sig.ll = "@finch." + modName(tpl.module) + "." + mangle(shown) if tpl.recv.len > 0 { sig.name = tpl.name sig.recv = recvType.name sig.shown = recvType.name + "." + tpl.name sig.ll = "@finch." + modName(tpl.module) + "." + mangle(recvType.name) + "." + tpl.name } savedN := g.tnames savedA := g.targs savedM := g.module savedNote := g.instNote g.tnames = tpl.tparams g.targs = args g.module = tpl.module g.instNote = " (while building " + sig.shown + ")" sig.ret = resolve(g, f.ty, f) body := f.kids[f.kids.len - 1] for k in 0..f.kids.len - 1 { pt := resolve(g, f.kids[k].ty, f.kids[k]) sig.params.push(pt) sig.pnames.push(f.kids[k].name) sig.borrow.push(owning(g, pt) && !mutates(g, f.kids[k].name, body)) } g.tnames = savedN g.targs = savedA g.module = savedM g.instNote = savedNote g.sigs.push(sig) return g.sigs.len - 1 } // What each type parameter stands for, from the types the arguments turned out to have. fn bindType(ptr[Gen] g, ast.Type pat, ast.Type actual, []str tparams, ptr[Binds] b) { if pat.kind == "named" && pat.module.len == 0 && pat.elem.len == 0 && tparams.contains(pat.name) { i := tparams.find(pat.name) if !b.bound[i] { b.types[i] = actual b.bound[i] = true } return } if (pat.kind == "array" || pat.kind == "ptr" || pat.kind == "fixed") && pat.elem.len > 0 && actual.kind == pat.kind && actual.elem.len > 0 { bindType(g, pat.elem[0], actual.elem[0], tparams, b) return } if pat.kind == "map" && actual.kind == "map" { bindType(g, pat.elem[0], actual.elem[0], tparams, b) bindType(g, pat.elem[1], actual.elem[1], tparams, b) return } if pat.kind == "named" && pat.elem.len > 0 && actual.kind == "struct" { sd := g.structs[findStruct(g, actual.module, actual.name)] if sd.base == pat.name && sd.targs.len == pat.elem.len { for i in 0..pat.elem.len { bindType(g, pat.elem[i], sd.targs[i], tparams, b) } } } } // max(3, 4), or max[float](3, 4): make the version the arguments ask for, and call it. fn genericCall(ptr[Gen] g, ast.Node c, int ti, int first, str shown) -> Val { tpl := g.templates[ti] f := g.progs[tpl.prog].fns[tpl.index] nparams := f.kids.len - 1 checkArgs(g, c, first, nparams, shown) []ast.Type args if c.ty.elem.len > 0 { if c.ty.elem.len != tpl.tparams.len { fail(g, c, shown + " takes " + str(tpl.tparams.len) + " type argument(s), but got " + str(c.ty.elem.len)) } for a in c.ty.elem { args.push(resolve(g, a, c)) } return callFn(g, instFn(g, ti, args, T("void"), c), c, first, shown, "", "") } b := Binds() for i in 0..tpl.tparams.len { b.types.push(T("void")) b.bound.push(false) } []Val vals mark := g.pending.len for k in 0..nparams { v := expr(g, c.kids[first + k]) if v.ty.kind == "void" || v.ty.kind == "null" { fail(g, c.kids[first + k], "this has no type to work out " + tpl.tparams.join(", ") + " from") } vals.push(v) hold(g, v) bindType(g, f.kids[k].ty, v.ty, tpl.tparams, addr(b)) } for i in 0..tpl.tparams.len { if !b.bound[i] { fail(g, c, "can't tell what " + tpl.tparams[i] + " is here; write it out: " + tpl.name + "[int](...)") } } si := instFn(g, ti, b.types, T("void"), c) sig := g.sigs[si] noteCall(g, c, sig) []str out for k in 0..nparams { v := coerce(g, vals[k], sig.params[k], c.kids[first + k], "argument '" + sig.pnames[k] + "'") out.push(llty(g, sig.params[k]) + " " + v.v) } r := emitCall(g, sig, c, out, shown) for v in vals { release(g, v) } heldEnd(g, mark) return r } // A struct from a C header: the fields clang says it has, at the offsets clang says, // with byte padding wherever C left a gap (or a field Finch can't see). fn resolveCStruct(ptr[Gen] g, int i) { cs := g.ci.structs[g.structs[i].name] g.structs[i].state = 1 if cs.isUnion { resolveCUnion(g, i, cs) return } cur := 0 []str elems for f in cs.fields { if f.hidden { continue } t := resolve(g, f.ty, ast.Node()) if t.kind == "struct" { resolveStruct(g, findStruct(g, t.module, t.name)) } if f.offset > cur { elems.push("[" + str(f.offset - cur) + " x i8]") } g.structs[i].fnames.push(f.name) g.structs[i].ftypes.push(t) g.structs[i].lidx.push(elems.len) g.structs[i].offs.push(f.offset) elems.push(llty(g, t)) cur = f.offset + f.size } if cs.size > cur { elems.push("[" + str(cs.size - cur) + " x i8]") } g.structs[i].unsupported = cs.unsupported if cs.hasHidden && cs.unsupported.len == 0 { g.structs[i].unsupported = "has fields Finch can't see (unions or bit fields)" } g.structs[i].csize = cs.size g.structs[i].calign = cs.align g.structs[i].state = 2 g.head.push("%S.C." + cs.name + " = type { " + elems.join(", ") + " }") } // A C union: room for its biggest field, aligned like C aligns it; every field is at byte 0. fn resolveCUnion(ptr[Gen] g, int i, cimport.CStruct cs) { for f in cs.fields { if f.hidden { continue } t := resolve(g, f.ty, ast.Node()) if t.kind == "struct" { resolveStruct(g, findStruct(g, t.module, t.name)) } g.structs[i].fnames.push(f.name) g.structs[i].ftypes.push(t) g.structs[i].lidx.push(0) g.structs[i].offs.push(0) } a := cs.align if a > 8 { a = 8 } if a < 1 { a = 1 } n := (cs.size + a - 1) / a g.structs[i].unsupported = cs.unsupported g.structs[i].isUnion = true g.structs[i].csize = cs.size g.structs[i].calign = cs.align g.structs[i].state = 2 g.head.push("%S.C." + cs.name + " = type { [" + str(n) + " x i" + str(a * 8) + "] }") } fn resolveStruct(ptr[Gen] g, int i) { if g.structs[i].state == 2 { return } if g.structs[i].isC { resolveCStruct(g, i) return } if g.structs[i].base.len == 0 { resolveStructBody(g, i) return } // an instance of a generic struct: its fields are written in terms of T savedN := g.tnames savedA := g.targs savedNote := g.instNote g.tnames = g.structs[i].tnames g.targs = g.structs[i].targs g.instNote = " (while building " + g.structs[i].name + ")" resolveStructBody(g, i) g.tnames = savedN g.targs = savedA g.instNote = savedNote } fn resolveStructBody(ptr[Gen] g, int i) { decl := g.progs[g.structs[i].prog].structs[g.structs[i].index] if g.structs[i].state == 1 { fail(g, decl, "the struct " + decl.name + " contains itself; use ptr[" + decl.name + "] or []" + decl.name + " for that field") } g.structs[i].state = 1 saved := g.module g.module = g.structs[i].module owns := false []str fields for f in decl.kids { t := resolve(g, f.ty, f) if t.kind == "struct" { resolveStruct(g, findStruct(g, t.module, t.name)) } g.structs[i].lidx.push(g.structs[i].fnames.len) g.structs[i].fnames.push(f.name) g.structs[i].ftypes.push(t) if owning(g, t) { owns = true } fields.push(llty(g, t)) } g.structs[i].owning = owns g.module = saved g.structs[i].state = 2 name := "%S." + modName(g.structs[i].module) + "." + mangle(g.structs[i].name) for f in fields { if g.wide.has(f) { g.wide[name] = 1 // it holds a wide vector (or a struct that does) } } g.head.push(name + " = type { " + fields.join(", ") + " }") } // ---------- debug info (-g) ---------- // // DWARF, written as LLVM metadata: a compile unit, a DISubprogram per function, a DILocation on every call, // and a DILocalVariable for every variable, with types a debugger can open up (structs, arrays, maps, enums). fn md(ptr[Gen] g, str text) -> int { g.md.push(text) return g.md.len - 1 } fn mdRef(int id) -> str { if id < 0 { return "null" } return "!" + str(id) } struct SizeAlign { int size int align } fn alignUp(int n, int a) -> int { if a <= 1 { return n } return (n + a - 1) / a * a } // the size and alignment LLVM gives a type (what a debugger needs to find the fields) fn sizeAlign(ptr[Gen] g, ast.Type t) -> SizeAlign { k := t.kind if k == "bool" || k == "char" || k == "i8" || k == "u8" { return SizeAlign(size: 1, align: 1) } if k == "i16" || k == "u16" { return SizeAlign(size: 2, align: 2) } if k == "i32" || k == "u32" || k == "f32" || k == "enum" { return SizeAlign(size: 4, align: 4) } if k == "str" || k == "array" { return SizeAlign(size: 24, align: 8) } if k == "map" { return SizeAlign(size: 56, align: 8) } if k == "fixed" { e := sizeAlign(g, t.elem[0]) return SizeAlign(size: e.size * t.count, align: e.align) } if k == "vec" { n := t.count * sizeAlign(g, t.elem[0]).size return SizeAlign(size: n, align: n) // LLVM lays a vector out at a multiple of its size } if k == "struct" { sd := g.structs[findStruct(g, t.module, t.name)] if sd.isC { return SizeAlign(size: sd.csize, align: sd.calign) } cur := 0 most := 1 for ft in sd.ftypes { f := sizeAlign(g, ft) cur = alignUp(cur, f.align) + f.size if f.align > most { most = f.align } } return SizeAlign(size: alignUp(cur, most), align: most) } return SizeAlign(size: 8, align: 8) } // where each field of a struct is, in bytes fn fieldOffsets(ptr[Gen] g, StructDef sd) -> []int { if sd.isC { return sd.offs } []int out cur := 0 for ft in sd.ftypes { f := sizeAlign(g, ft) cur = alignUp(cur, f.align) out.push(cur) cur += f.size } return out } fn dbgMember(ptr[Gen] g, str name, int scope, int file, int line, int base, int bits, int offset) -> int { return md(g, "!DIDerivedType(tag: DW_TAG_member, name: \"" + name + "\", scope: !" + str(scope) + ", file: !" + str(file) + ", line: " + str(line) + ", baseType: " + mdRef(base) + ", size: " + str(bits) + ", offset: " + str(offset) + ")") } fn basicType(ptr[Gen] g, str name, int bits, str enc) -> int { return md(g, "!DIBasicType(name: \"" + name + "\", size: " + str(bits) + ", encoding: " + enc + ")") } // the debug type of a Finch type; -1 where there is none (nothing, null) fn diType(ptr[Gen] g, ast.Type t) -> int { k := t.kind if k == "void" || k == "null" { return -1 } key := "T:" + ast.show(t) if g.mdcache.has(key) { return g.mdcache[key] } id := -1 file := g.mdfiles[0] if k == "bool" { id = basicType(g, "bool", 8, "DW_ATE_boolean") } else if k == "char" { id = basicType(g, "char", 8, "DW_ATE_signed_char") } else if k == "int" { id = basicType(g, "int", 64, "DW_ATE_signed") } else if k == "float" { id = basicType(g, "float", 64, "DW_ATE_float") } else if k == "f32" { id = basicType(g, "f32", 32, "DW_ATE_float") } else if isInt(t) { enc := "DW_ATE_unsigned" if isSigned(t) { enc = "DW_ATE_signed" } id = basicType(g, k, bitsOf(t), enc) } else if k == "enum" { ed := g.enums[findEnum(g, t.module, t.name)] []str items for i in 0..ed.variants.len { items.push(mdRef(md(g, "!DIEnumerator(name: \"" + ed.variants[i] + "\", value: " + str(ed.values[i]) + ")"))) } id = md(g, "!DICompositeType(tag: DW_TAG_enumeration_type, name: \"" + ed.name + "\", file: !" + str(file) + ", size: 32, elements: !{" + items.join(", ") + "})") } else { // the rest can point at themselves (a list node holds a pointer to the next), so the number comes first id = md(g, "") g.mdcache[key] = id if k == "ptr" { base := -1 if t.elem.len > 0 { base = diType(g, t.elem[0]) } g.md[id] = "!DIDerivedType(tag: DW_TAG_pointer_type, baseType: " + mdRef(base) + ", size: 64)" } else if k == "vec" { e := sizeAlign(g, t.elem[0]) sub := md(g, "!DISubrange(count: " + str(t.count) + ")") g.md[id] = "!DICompositeType(tag: DW_TAG_array_type, baseType: " + mdRef(diType(g, t.elem[0])) + ", size: " + str(e.size * t.count * 8) + ", flags: DIFlagVector, elements: !{!" + str(sub) + "})" } else if k == "fixed" { e := sizeAlign(g, t.elem[0]) sub := md(g, "!DISubrange(count: " + str(t.count) + ")") g.md[id] = "!DICompositeType(tag: DW_TAG_array_type, baseType: " + mdRef(diType(g, t.elem[0])) + ", size: " + str(e.size * t.count * 8) + ", elements: !{!" + str(sub) + "})" } else if k == "str" || k == "array" { elem := ast.simple("char") if k == "array" { elem = t.elem[0] } ptrT := md(g, "!DIDerivedType(tag: DW_TAG_pointer_type, baseType: " + mdRef(diType(g, elem)) + ", size: 64)") intT := diType(g, ast.simple("int")) a := dbgMember(g, "ptr", id, file, 0, ptrT, 64, 0) b := dbgMember(g, "len", id, file, 0, intT, 64, 64) c := dbgMember(g, "cap", id, file, 0, intT, 64, 128) g.md[id] = "!DICompositeType(tag: DW_TAG_structure_type, name: \"" + ast.show(t) + "\", file: !" + str(file) + ", size: 192, align: 64, elements: !{!" + str(a) + ", !" + str(b) + ", !" + str(c) + "})" } else if k == "map" { intT := diType(g, ast.simple("int")) raw := md(g, "!DIDerivedType(tag: DW_TAG_pointer_type, baseType: null, size: 64)") names := ["entries", "len", "used", "cap", "index", "icap", "adds"] []str members for i in 0..7 { base := intT if i == 0 || i == 4 { base = raw } members.push("!" + str(dbgMember(g, names[i], id, file, 0, base, 64, 64 * i))) } g.md[id] = "!DICompositeType(tag: DW_TAG_structure_type, name: \"" + ast.show(t) + "\", file: !" + str(file) + ", size: 448, align: 64, elements: !{" + members.join(", ") + "})" } else if k == "struct" { sd := g.structs[findStruct(g, t.module, t.name)] offs := fieldOffsets(g, sd) sa := sizeAlign(g, t) []str members for i in 0..sd.ftypes.len { fs := sizeAlign(g, sd.ftypes[i]) members.push("!" + str(dbgMember(g, sd.fnames[i], id, file, 0, diType(g, sd.ftypes[i]), fs.size * 8, offs[i] * 8))) } g.md[id] = "!DICompositeType(tag: DW_TAG_structure_type, name: \"" + sd.name + "\", file: !" + str(file) + ", size: " + str(sa.size * 8) + ", align: 8, elements: !{" + members.join(", ") + "})" } else { g.md[id] = "!DIBasicType(name: \"" + k + "\", size: 64, encoding: DW_ATE_unsigned)" } return id } g.mdcache[key] = id return id } fn dbgLocation(ptr[Gen] g) -> int { key := "L:" + str(g.dbgLine) + ":" + str(g.dbgCol) + ":" + str(g.dbgScope) if g.mdcache.has(key) { return g.mdcache[key] } id := md(g, "!DILocation(line: " + str(g.dbgLine) + ", column: " + str(g.dbgCol) + ", scope: !" + str(g.dbgScope) + ")") g.mdcache[key] = id return id } fn dbgSubprogram(ptr[Gen] g, ast.Node f) -> int { sig := g.cur []str types types.push(mdRef(diType(g, sig.ret))) if sig.recv.len > 0 { types.push("null") } for p in sig.params { types.push(mdRef(diType(g, p))) } file := g.mdfiles[f.file] sub := md(g, "!DISubroutineType(types: !{" + types.join(", ") + "})") return md(g, "distinct !DISubprogram(name: \"" + sig.name + "\", linkageName: \"" + sig.ll.sub(1, sig.ll.len) + "\", scope: !" + str(file) + ", file: !" + str(file) + ", line: " + str(f.line) + ", type: !" + str(sub) + ", scopeLine: " + str(f.line) + ", flags: DIFlagPrototyped, spFlags: DISPFlagDefinition | DISPFlagLocalToUnit, unit: !0)") } fn dbgVariable(ptr[Gen] g, ast.Node at, str name, ast.Type t, str slot) { if !g.debug || g.dbgScope < 0 { return } ty := diType(g, t) if ty < 0 { return } arg := "" if g.dbgArg > 0 { arg = ", arg: " + str(g.dbgArg) } v := md(g, "!DILocalVariable(name: \"" + name + "\", scope: !" + str(g.dbgScope) + ", file: !" + str(g.mdfiles[at.file]) + ", line: " + str(at.line) + ", type: !" + str(ty) + arg + ")") g.dbgLine = at.line g.dbgCol = at.col emit(g, "call void @llvm.dbg.declare(metadata ptr " + slot + ", metadata !" + str(v) + ", metadata !DIExpression())") } // the metadata, at the end of the module fn dbgModule(ptr[Gen] g) -> str { []str lines = ["!llvm.dbg.cu = !{!0}", "!llvm.module.flags = !{!1, !2}"] for i in 0..g.md.len { lines.push("!" + str(i) + " = " + g.md[i]) } return lines.join("\n") + "\n" } fn define(ptr[Gen] g, int si) { g.cur = g.sigs[si] g.module = g.cur.module g.tnames = g.cur.tnames g.targs = g.cur.targs g.instNote = "" if g.cur.targs.len > 0 { g.instNote = " (while building " + g.cur.shown + ")" } f := g.progs[g.cur.prog].fns[g.cur.index] ownCpu := cpuOf(g, f) g.curFeatures = g.features for x in ownCpu { if !g.curFeatures.contains(x) { g.curFeatures.push(x) } } []str empty g.body = empty g.allocas = empty g.tmp = 0 g.term = false g.nvars = 0 g.dbgScope = -1 dbgsp := -1 if g.debug { dbgsp = dbgSubprogram(g, f) g.dbgScope = dbgsp g.dbgLine = f.line g.dbgCol = f.col } g.deferOn = false g.inDefer = 0 []Scope none g.scopes = none pushScope(g) g.frame = "" g.frameLine = "" if g.trace { // this call on the chain a runtime error shows: in move (game.fch:12) shownName := g.cur.name if g.cur.recv.len > 0 { shownName = g.cur.recv + "." + g.cur.name } if f.text.len > 0 && g.cur.name.starts_with("__test_") { shownName = "test \"" + f.text + "\"" } g.frame = alloca(g, "{ ptr, ptr, ptr, i64 }") emit(g, "store ptr " + load(g, "ptr", "@finch_trace") + ", ptr " + g.frame) emit(g, "store ptr " + strGlobal(g, shownName) + ", ptr " + op(g, "getelementptr inbounds { ptr, ptr, ptr, i64 }, ptr " + g.frame + ", i32 0, i32 1")) emit(g, "store ptr " + strGlobal(g, g.files[f.file]) + ", ptr " + op(g, "getelementptr inbounds { ptr, ptr, ptr, i64 }, ptr " + g.frame + ", i32 0, i32 2")) g.frameLine = op(g, "getelementptr inbounds { ptr, ptr, ptr, i64 }, ptr " + g.frame + ", i32 0, i32 3") emit(g, "store i64 " + str(f.line) + ", ptr " + g.frameLine) emit(g, "store ptr " + g.frame + ", ptr @finch_trace") } []str params if g.cur.recv.len > 0 { // self is the caller's struct itself, reached through its address params.push("ptr %self") selfKind := "struct" if g.cur.recvEnum { selfKind = "enum" } selfTy := ast.Type(kind: selfKind, name: g.cur.recv, module: g.cur.recvmod) g.scopes[0].vars.push(Var(name: "self", ty: selfTy, slot: "%self", readonly: false)) if g.debug { // the struct is at the address %self holds: shown as the struct, whose slot that address is dbgVariable(g, f, "self", selfTy, "%self") } } for k in 0..g.cur.params.len { pt := g.cur.params[k] big := bigCopy(g, pt) if big > 0 { warn(g, f.kids[k], "'" + g.cur.pnames[k] + "' is a " + ast.show(pt) + " (" + str(big) + " bytes), copied on every call to " + f.name + "; take ptr[" + ast.show(pt) + "] to share it instead (callers can still pass the variable as it is)") } params.push(llty(g, pt) + " %p" + str(k)) v := "%p" + str(k) if owning(g, pt) && !g.cur.borrow[k] { v = copyValue(g, v, pt) } g.dbgArg = k + 1 if g.cur.recv.len > 0 { g.dbgArg = k + 2 } addVar(g, f.kids[k], g.cur.pnames[k], pt, v, false, owning(g, pt) && !g.cur.borrow[k]) g.dbgArg = 0 } block(g, f.kids[f.kids.len - 1], false) if !g.term { if g.cur.ret.kind != "void" { fail(g, f, "function '" + f.name + "' can reach its end without returning " + ast.show(g.cur.ret)) } unwind(g, 0) if g.cur.fallible { emit(g, "ret " + retTy(g, g.cur) + " { i1 true, i8 0, { ptr, i64, i64 } zeroinitializer }") } else { emit(g, "ret void") } } attach := "" if ownCpu.len > 0 { []str plus wide := false for x in ownCpu { plus.push("+" + llvmFeature(x)) wide = wide || x.starts_with("avx512") } if wide { plus.push("+evex512") // LLVM 18 and later: the 512-bit registers themselves (clang adds it for C too) } attach = " \"target-features\"=\"" + plus.join(",") + "\"" } if dbgsp >= 0 { attach = attach + " !dbg !" + str(dbgsp) } g.dbgScope = -1 g.out.push("define internal " + retTy(g, g.cur) + " " + g.cur.ll + "(" + params.join(", ") + ")" + attach + " {\nentry:\n" + g.allocas.join("\n") + "\n" + g.body.join("\n") + "\n}\n") } // ---------- statements ---------- fn block(ptr[Gen] g, ast.Node b, bool scope) { if scope { pushScope(g) } for s in b.kids { if g.term { fail(g, s, "this code can never run (it comes after return, break, continue or exit)") } stmt(g, s) } if scope { endScope(g) } } fn stmt(ptr[Gen] g, ast.Node s) { g.dbgLine = s.line g.dbgCol = s.col if g.frameLine.len > 0 && !g.term { emit(g, "store i64 " + str(s.line) + ", ptr " + g.frameLine) } k := s.kind if k == "block" { block(g, s, true) } else if k == "decl" { declStmt(g, s) } else if k == "assign" { assignStmt(g, s) } else if k == "expr" { if s.kids[0].kind == "or" { release(g, orElse(g, s.kids[0], true)) } else { release(g, expr(g, s.kids[0])) } } else if k == "match" { matchGen(g, s, false) } else if k == "if" { ifStmt(g, s) } else if k == "while" { whileStmt(g, s) } else if k == "for" { forStmt(g, s) } else if k == "each" { eachStmt(g, s) } else if k == "return" { returnStmt(g, s) } else if k == "defer" { if g.inDefer > 0 { fail(g, s, "defer inside defer is not allowed") } sc := g.scopes.len - 1 g.scopes[sc].cleanups.push(Cleanup(isDefer: true, idx: g.scopes[sc].defers.len)) g.scopes[sc].defers.push(s.kids[0]) g.scopes[sc].orders.push(g.nvars) } else if k == "break" || k == "continue" { if g.inDefer > 0 { fail(g, s, k + " can't be used inside defer") } if g.loops.len == 0 { fail(g, s, k + " can only be used inside a loop") } l := g.loops[g.loops.len - 1] dropPending(g, l.pend) unwind(g, l.depth) if k == "break" { br(g, l.brk) } else { br(g, l.cont) } } else { fail(g, s, "unknown statement " + k) } } fn declStmt(ptr[Gen] g, ast.Node s) { if !s.flag { v := expr(g, s.kids[0]) if v.ty.kind == "void" { fail(g, s.kids[0], "this gives back nothing, so it can't be stored") } if v.ty.kind == "null" { fail(g, s.kids[0], "null needs a type, like: ptr[int] " + s.name + " = null") } addVar(g, s, s.name, v.ty, own(g, v), false, true) return } t := resolve(g, s.ty, s) if s.kids.len == 0 { if t.kind == "struct" && hasDefaults(g, t) { []ast.Node noArgs []str noNames v := construct(g, findStruct(g, t.module, t.name), noArgs, noNames, s) addVar(g, s, s.name, t, v.v, false, true) return } addVar(g, s, s.name, t, zeroOf(t), false, true) return } v := coerce(g, exprWant(g, s.kids[0], t), t, s.kids[0], "'" + s.name + "'") addVar(g, s, s.name, t, own(g, v), false, true) } fn hasDefaults(ptr[Gen] g, ast.Type t) -> bool { sd := g.structs[findStruct(g, t.module, t.name)] if sd.isC { return false } for f in g.progs[sd.prog].structs[sd.index].kids { if f.kids.len > 0 { return true } } // a field that is itself a struct with defaults gets them too for ft in sd.ftypes { if ft.kind == "struct" && hasDefaults(g, ft) { return true } } return false } fn assignStmt(ptr[Gen] g, ast.Node s) { target := s.kids[0] root := rootVar(target) if root.len > 0 { id := lookup(g, root) if id < 0 && target.kind == "var" { if findConst(g, g.module, root) >= 0 { fail(g, s, "'" + root + "' is a constant; it can't be changed") } fail(g, s, "there is no variable named '" + root + "' (create it with " + root + " := ...)") } if id >= 0 && varAt(g, id).readonly { fail(g, s, "the loop variable '" + root + "' can't be changed") } } what := "the value" if target.kind == "var" { what = "'" + root + "'" } if s.name == "=" { // the right side first: it may move memory the target lives in (a.push(...) inside it) want := T("void") known := false if target.kind == "var" { id := lookup(g, root) if id >= 0 { want = varAt(g, id).ty known = true } } v := Val() if known { v = exprWant(g, s.kids[1], want) } else { v = expr(g, s.kids[1]) } mark := g.pending.len hold(g, v) // while the target is computed (a[try f()] = ...) dst := place(g, target, true) v = coerce(g, v, dst.ty, s.kids[1], what) nv := own(g, v) if owning(g, dst.ty) { old := load(g, llty(g, dst.ty), dst.addr) store(g, llty(g, dst.ty), nv, dst.addr) dropValue(g, old, dst.ty) } else { store(g, llty(g, dst.ty), nv, dst.addr) } heldEnd(g, mark) return } rhs := expr(g, s.kids[1]) mark := g.pending.len hold(g, rhs) dst := place(g, target, true) cur := Val(v: load(g, llty(g, dst.ty), dst.addr), ty: dst.ty) opName := s.name.sub(0, 1) v := coerce(g, arith(g, opName, cur, rhs, s), dst.ty, s, what) nv := own(g, v) store(g, llty(g, dst.ty), nv, dst.addr) if owning(g, dst.ty) { dropValue(g, cur.v, dst.ty) } heldEnd(g, mark) } fn condition(ptr[Gen] g, ast.Node e) -> str { return coerce(g, expr(g, e), T("bool"), e, "a condition").v } fn ifStmt(ptr[Gen] g, ast.Node s) { c := condition(g, s.kids[0]) then := newLabel(g, "then") els := newLabel(g, "else") end := newLabel(g, "endif") if s.kids.len > 2 { condBr(g, c, then, els) } else { condBr(g, c, then, end) } label(g, then) block(g, s.kids[1], true) reached := !g.term br(g, end) if s.kids.len > 2 { label(g, els) stmt(g, s.kids[2]) reached = reached || !g.term br(g, end) } else { reached = true } if reached { label(g, end) } } fn whileStmt(ptr[Gen] g, ast.Node s) { cond := newLabel(g, "while.cond") body := newLabel(g, "while.body") end := newLabel(g, "while.end") br(g, cond) label(g, cond) infinite := s.kids[0].kind == "bool" && s.kids[0].text == "true" c := condition(g, s.kids[0]) if infinite { br(g, body) } else { condBr(g, c, body, end) } label(g, body) g.loops.push(Loop(cont: cond, brk: end, depth: g.scopes.len, pend: g.pending.len)) breaks := countBreaks(s.kids[1]) block(g, s.kids[1], true) g.loops.pop() br(g, cond) if !infinite || breaks { label(g, end) } } // does a loop body contain a break for this loop? fn countBreaks(ast.Node n) -> bool { if n.kind == "break" { return true } if n.kind == "while" || n.kind == "for" || n.kind == "each" { return false } for k in n.kids { if countBreaks(k) { return true } } return false } fn forStmt(ptr[Gen] g, ast.Node s) { from := coerce(g, expr(g, s.kids[0]), T("int"), s.kids[0], "the start of a range").v to := coerce(g, expr(g, s.kids[1]), T("int"), s.kids[1], "the end of a range").v pushScope(g) i := addVar(g, s, s.name, T("int"), from, true, false) cond := newLabel(g, "for.cond") body := newLabel(g, "for.body") step := newLabel(g, "for.step") end := newLabel(g, "for.end") br(g, cond) label(g, cond) iv := load(g, "i64", i) c := op(g, "icmp slt i64 " + iv + ", " + to) condBr(g, c, body, end) label(g, body) g.loops.push(Loop(cont: step, brk: end, depth: g.scopes.len, pend: g.pending.len)) block(g, s.kids[2], true) g.loops.pop() br(g, step) label(g, step) iv2 := load(g, "i64", i) store(g, "i64", op(g, "add i64 " + iv2 + ", 1"), i) br(g, cond) label(g, end) popScope(g) } // for key in map / for key, value in map: in the order the keys were added fn eachMap(ptr[Gen] g, ast.Node s, str mp, ast.Type t, bool live) { kt := t.elem[0] vt := t.elem[1] changes := live && mutates(g, rootVar(s.kids[0]), s.kids[1]) iv := alloca(g, "i64") store(g, "i64", "0", iv) adds := "" if live { adds = load(g, "i64", op(g, "getelementptr inbounds " + llty(g, t) + ", ptr " + mp + ", i32 0, i32 6")) } cond := newLabel(g, "map.cond") check := newLabel(g, "map.check") body := newLabel(g, "map.body") step := newLabel(g, "map.step") end := newLabel(g, "map.end") br(g, cond) label(g, cond) // adding keys inside the loop could reorder the entries: that stops the program (removing is fine) if live && changes { changed := newLabel(g, "map.changed") same := newLabel(g, "map.same") now := load(g, "i64", op(g, "getelementptr inbounds " + llty(g, t) + ", ptr " + mp + ", i32 0, i32 6")) condBr(g, op(g, "icmp ne i64 " + now + ", " + adds), changed, same) label(g, changed) emit(g, "call void @finch_map_changed(ptr " + strGlobal(g, g.files[s.file]) + ", i64 " + str(s.line) + ")") emit(g, "unreachable") g.term = true label(g, same) } i := load(g, "i64", iv) used := load(g, "i64", op(g, "getelementptr inbounds " + llty(g, t) + ", ptr " + mp + ", i32 0, i32 2")) condBr(g, op(g, "icmp slt i64 " + i + ", " + used), check, end) label(g, check) entry := entryAt(g, mp, t, i) hash := load(g, "i64", entryField(g, entry, t, 0)) condBr(g, op(g, "icmp eq i64 " + hash + ", 0"), step, body) // a removed entry label(g, body) g.loops.push(Loop(cont: step, brk: end, depth: g.scopes.len, pend: g.pending.len)) pushScope(g) kv := load(g, llty(g, kt), entryField(g, entry, t, 1)) if changes { kv = copyValue(g, kv, kt) } addVar(g, s, s.name, kt, kv, true, changes) if s.text.len > 0 { vv := load(g, llty(g, vt), entryField(g, entry, t, 2)) if changes { vv = copyValue(g, vv, vt) } addVar(g, s, s.text, vt, vv, true, changes) } block(g, s.kids[1], true) popScope(g) g.loops.pop() br(g, step) label(g, step) i2 := load(g, "i64", iv) store(g, "i64", op(g, "add i64 " + i2 + ", 1"), iv) br(g, cond) label(g, end) } fn eachStmt(ptr[Gen] g, ast.Node s) { pushScope(g) r := ref(g, s.kids[0], false) listAddr := "" lt := T("void") if r.isPlace { listAddr = r.pl.addr lt = r.pl.ty } else { lt = r.val.ty listAddr = addVar(g, s, "$list" + str(g.nvars), lt, own(g, r.val), false, true) } if lt.kind == "map" { eachMap(g, s, listAddr, lt, r.isPlace) popScope(g) return } if lt.kind != "array" && lt.kind != "str" { fail(g, s.kids[0], "for ... in needs an array, a str or a map, but this is " + ast.show(lt) + " (for a range of numbers write 0..n)") } et := T("char") if lt.kind == "array" { et = lt.elem[0] } copyElems := r.isPlace && owning(g, et) && mutates(g, rootVar(s.kids[0]), s.kids[1]) big := bigCopy(g, et) if big > 0 && s.text.len == 0 { list := "list" if s.kids[0].kind == "var" { list = s.kids[0].name } warn(g, s, "each " + ast.show(et) + " (" + str(big) + " bytes) is copied into '" + s.name + "'; for i in 0.." + list + ".len with " + list + "[i] works on it in place") } iv := alloca(g, "i64") store(g, "i64", "0", iv) cond := newLabel(g, "each.cond") body := newLabel(g, "each.body") step := newLabel(g, "each.step") end := newLabel(g, "each.end") br(g, cond) label(g, cond) len := load(g, "i64", op(g, "getelementptr inbounds { ptr, i64, i64 }, ptr " + listAddr + ", i32 0, i32 1")) i := load(g, "i64", iv) c := op(g, "icmp slt i64 " + i + ", " + len) condBr(g, c, body, end) label(g, body) data := load(g, "ptr", listAddr) ep := op(g, "getelementptr inbounds " + llty(g, et) + ", ptr " + data + ", i64 " + i) elem := load(g, llty(g, et), ep) if copyElems { elem = copyValue(g, elem, et) } g.loops.push(Loop(cont: step, brk: end, depth: g.scopes.len, pend: g.pending.len)) pushScope(g) elemVar := s.name if s.text.len > 0 { // for i, x in list addVar(g, s, s.name, T("int"), i, true, false) elemVar = s.text } addVar(g, s, elemVar, et, elem, true, copyElems) block(g, s.kids[1], true) popScope(g) g.loops.pop() br(g, step) label(g, step) i2 := load(g, "i64", iv) store(g, "i64", op(g, "add i64 " + i2 + ", 1"), iv) br(g, cond) label(g, end) popScope(g) } fn returnStmt(ptr[Gen] g, ast.Node s) { if g.inDefer > 0 { fail(g, s, "return can't be used inside defer") } // return error("...") if s.kids.len > 0 && s.kids[0].kind == "call" && s.kids[0].name == "error" { c := s.kids[0] if !g.cur.fallible { want := "!" if g.cur.ret.kind != "void" { want = ast.show(g.cur.ret) + "!" } fail(g, c, "'" + g.cur.name + "' can't fail, so it can't return an error (make it fallible: -> " + want + ")") } checkArgs(g, c, 0, 1, "error") msg := coerce(g, expr(g, c.kids[0]), T("str"), c.kids[0], "the error message") out := own(g, msg) dropPending(g, 0) unwind(g, 0) emit(g, "ret " + retTy(g, g.cur) + " " + op(g, "insertvalue " + retTy(g, g.cur) + " zeroinitializer, { ptr, i64, i64 } " + out + ", 2")) g.term = true return } if g.cur.ret.kind == "void" { if s.kids.len > 0 { fail(g, s.kids[0], "'" + g.cur.name + "' doesn't return a value (it has no '-> type')") } dropPending(g, 0) unwind(g, 0) if g.cur.fallible { emit(g, "ret " + retTy(g, g.cur) + " { i1 true, i8 0, { ptr, i64, i64 } zeroinitializer }") } else { emit(g, "ret void") } g.term = true return } if s.kids.len == 0 { fail(g, s, "'" + g.cur.name + "' must return " + ast.show(g.cur.ret)) } // `return local` moves the variable out instead of copying it if s.kids[0].kind == "var" { name := s.kids[0].name idx := g.scopes.len while idx > 0 { idx -= 1 found := -1 for vi in 0..g.scopes[idx].vars.len { if g.scopes[idx].vars[vi].name == name && !(g.deferOn && g.scopes[idx].vars[vi].order >= g.deferLimit) { found = vi } } if found < 0 { continue } lv := g.scopes[idx].vars[found] if lv.owned && ast.same(lv.ty, g.cur.ret) { out := load(g, llty(g, lv.ty), lv.slot) g.scopes[idx].vars[found].moved = true dropPending(g, 0) unwind(g, 0) g.scopes[idx].vars[found].moved = false finishReturn(g, out) return } break } } v := coerce(g, exprWant(g, s.kids[0], g.cur.ret), g.cur.ret, s.kids[0], "the returned value") out := own(g, v) dropPending(g, 0) unwind(g, 0) finishReturn(g, out) } fn finishReturn(ptr[Gen] g, str out) { if g.cur.fallible { a := op(g, "insertvalue " + retTy(g, g.cur) + " zeroinitializer, i1 true, 0") b := op(g, "insertvalue " + retTy(g, g.cur) + " " + a + ", " + llty(g, g.cur.ret) + " " + out + ", 1") emit(g, "ret " + retTy(g, g.cur) + " " + b) } else { emit(g, "ret " + llty(g, g.cur.ret) + " " + out) } g.term = true } // ---------- expressions ---------- // ---------- maps ---------- // // map[K]V is { entries, len, used, cap, index, icap, adds } (see runtime/finch_rt.c). The runtime finds and // places entries; the compiler stores, copies and drops the keys and values in them. An entry is // { i64 hash, K key, V value }; hash 0 marks a removed entry. fn entryTy(ptr[Gen] g, ast.Type t) -> str { return "{ i64, " + llty(g, t.elem[0]) + ", " + llty(g, t.elem[1]) + " }" } fn entrySize(ptr[Gen] g, ast.Type t) -> str { return "ptrtoint (ptr getelementptr (" + entryTy(g, t) + ", ptr null, i32 1) to i64)" } // the address of entry number `i` of the map at `mp` fn entryAt(ptr[Gen] g, str mp, ast.Type t, str i) -> str { entries := load(g, "ptr", op(g, "getelementptr inbounds " + llty(g, t) + ", ptr " + mp + ", i32 0, i32 0")) return op(g, "getelementptr inbounds " + entryTy(g, t) + ", ptr " + entries + ", i64 " + i) } // field 0 hash, 1 key, 2 value fn entryField(ptr[Gen] g, str entry, ast.Type t, int f) -> str { return op(g, "getelementptr inbounds " + entryTy(g, t) + ", ptr " + entry + ", i32 0, i32 " + str(f)) } fn mapArgs(ptr[Gen] g, str mp, str keyAddr, ast.Type t) -> str { kstr := "0" if t.elem[0].kind == "str" { kstr = "1" } return "ptr " + mp + ", ptr " + keyAddr + ", i64 " + entrySize(g, t) + ", i64 " + sizeOf(g, t.elem[0]) + ", i32 " + kstr } fn mapFind(ptr[Gen] g, str mp, str keyAddr, ast.Type t) -> str { return op(g, "call i64 @finch_map_find(" + mapArgs(g, mp, keyAddr, t) + ")") } fn defaultVal(ptr[Gen] g, ast.Type t, ast.Node at) -> str { if t.kind == "struct" && hasDefaults(g, t) { []ast.Node noArgs []str noNames return construct(g, findStruct(g, t.module, t.name), noArgs, noNames, at).v } return zeroOf(t) } // The entry for `key`, added if it is missing: the key is copied in, and the value is its // type's default when `fill` (m[k] += 1 starts from 0). fn mapSlot(ptr[Gen] g, str mp, ast.Type t, Val key, bool fill, ast.Node at) -> str { keyAddr := spill(g, key) added := alloca(g, "i32") store(g, "i32", "0", added) i := op(g, "call i64 @finch_map_slot(" + mapArgs(g, mp, keyAddr, t) + ", ptr " + added + ")") entry := entryAt(g, mp, t, i) fresh := newLabel(g, "map.new") have := newLabel(g, "map.have") condBr(g, op(g, "icmp ne i32 " + load(g, "i32", added) + ", 0"), fresh, have) label(g, fresh) copyAt(g, entryField(g, entry, t, 1), keyAddr, t.elem[0]) if fill { store(g, llty(g, t.elem[1]), own(g, Val(v: defaultVal(g, t.elem[1], at), ty: t.elem[1], fresh: true)), entryField(g, entry, t, 2)) } br(g, have) label(g, have) return entry } // m[key]: reading a missing key stops the program; writing (m[k] = v, m[k] += 1, m[k].push(x)) adds it fn mapIndex(ptr[Gen] g, ast.Node e, Ref obj, ast.Type t, bool write) -> Ref { kt := t.elem[0] vt := t.elem[1] key := coerce(g, exprWant(g, e.kids[1], kt), kt, e.kids[1], "the key") if write { if !obj.isPlace { fail(g, e, "this is a temporary map, it can't be changed") } entry := mapSlot(g, obj.pl.addr, t, key, true, e) release(g, key) return Ref(isPlace: true, pl: Place(addr: entryField(g, entry, t, 2), ty: vt)) } mp := "" if obj.isPlace { mp = obj.pl.addr } else { mp = spill(g, obj.val) } keyAddr := spill(g, key) i := mapFind(g, mp, keyAddr, t) missing := newLabel(g, "map.missing") found := newLabel(g, "map.found") condBr(g, op(g, "icmp slt i64 " + i + ", 0"), missing, found) label(g, missing) text := alloca(g, "{ ptr, i64, i64 }") store(g, "{ ptr, i64, i64 }", "zeroinitializer", text) emitFormat(g, text, key.v, kt, true) emit(g, "call void @finch_map_missing(ptr " + strGlobal(g, g.files[e.file]) + ", i64 " + str(e.line) + ", ptr " + text + ")") emit(g, "unreachable") g.term = true label(g, found) release(g, key) va := entryField(g, entryAt(g, mp, t, i), t, 2) if obj.isPlace { return Ref(isPlace: true, pl: Place(addr: va, ty: vt)) } v := load(g, llty(g, vt), va) out := Val(v: v, ty: vt) if owning(g, vt) && obj.val.fresh { out = Val(v: copyValue(g, v, vt), ty: vt, fresh: true) } release(g, obj.val) // from a temporary map: take the value, drop the rest return Ref(isPlace: false, val: out) } // ["a": 1, "b": 2], or [:] where the type is known fn mapLit(ptr[Gen] g, ast.Node e, ast.Type want) -> Val { t := want first := false k0 := Val() v0 := Val() if want.kind != "map" { if e.kids.len == 0 { fail(g, e, "an empty [:] needs a type, like: map[str]int m = [:]") } // number values written in the code are all floats if any of them has a dot allNum := true anyFloat := false for i in 0..e.kids.len / 2 { y := e.kids[2 * i + 1] if y.kind == "unary" && y.name == "-" { y = y.kids[0] } if y.kind == "float" { anyFloat = true } else if y.kind != "int" { allNum = false } } k0 = expr(g, e.kids[0]) v0 = expr(g, e.kids[1]) if k0.ty.kind == "void" || k0.ty.kind == "null" { fail(g, e.kids[0], "a map key needs a value with a type") } if v0.ty.kind == "void" || v0.ty.kind == "null" { fail(g, e.kids[1], "a map value needs a value with a type") } vt := v0.ty if allNum && anyFloat { vt = T("float") } t = ast.mapOf(k0.ty, vt) first = true } mp := alloca(g, llty(g, t)) store(g, llty(g, t), "zeroinitializer", mp) outer := g.pending.len holdAddr(g, mp, t) // the entries so far, if a later one leaves early for i in 0..e.kids.len / 2 { mark := g.pending.len k := k0 if !(i == 0 && first) { k = exprWant(g, e.kids[2 * i], t.elem[0]) } k = coerce(g, k, t.elem[0], e.kids[2 * i], "every key of this map") hold(g, k) v := v0 if !(i == 0 && first) { v = exprWant(g, e.kids[2 * i + 1], t.elem[1]) } v = coerce(g, v, t.elem[1], e.kids[2 * i + 1], "every value of this map") vv := own(g, v) entry := mapSlot(g, mp, t, k, false, e) va := entryField(g, entry, t, 2) dropAt(g, va, t.elem[1]) // a key given twice: the last value wins store(g, llty(g, t.elem[1]), vv, va) release(g, k) heldEnd(g, mark) } heldEnd(g, outer) return Val(v: load(g, llty(g, t), mp), ty: t, fresh: true) } fn mapMethod(ptr[Gen] g, ast.Node m, str mp, ast.Type t) -> Val { n := m.name kt := t.elem[0] vt := t.elem[1] if n == "has" || n == "get" || n == "remove" { if n == "get" { checkArgs(g, m, 1, 2, n) } else { checkArgs(g, m, 1, 1, n) } key := coerce(g, exprWant(g, m.kids[1], kt), kt, m.kids[1], "the key") i := mapFind(g, mp, spill(g, key), t) release(g, key) was := op(g, "icmp sge i64 " + i + ", 0") if n == "has" { return Val(v: was, ty: T("bool")) } if n == "remove" { hit := newLabel(g, "remove.hit") end := newLabel(g, "remove.end") condBr(g, was, hit, end) label(g, hit) ent := entryAt(g, mp, t, i) dropAt(g, entryField(g, ent, t, 1), kt) dropAt(g, entryField(g, ent, t, 2), vt) emit(g, "call void @finch_map_remove_at(ptr " + mp + ", i64 " + i + ", i64 " + entrySize(g, t) + ")") br(g, end) label(g, end) return Val(v: was, ty: T("bool")) } // get(key, default): the default is only computed when the key is missing found := newLabel(g, "get.found") missing := newLabel(g, "get.missing") fend := newLabel(g, "get.fend") mend := newLabel(g, "get.mend") end := newLabel(g, "get.end") condBr(g, was, found, missing) label(g, found) v := copyValue(g, load(g, llty(g, vt), entryField(g, entryAt(g, mp, t, i), t, 2)), vt) br(g, fend) label(g, fend) br(g, end) label(g, missing) d := own(g, coerce(g, exprWant(g, m.kids[2], vt), vt, m.kids[2], "the default")) br(g, mend) label(g, mend) br(g, end) label(g, end) return Val(v: op(g, "phi " + llty(g, vt) + " [ " + v + ", %" + fend + " ], [ " + d + ", %" + mend + " ]"), ty: vt, fresh: owning(g, vt)) } if n == "clear" { checkArgs(g, m, 1, 0, n) if owning(g, kt) || owning(g, vt) { used := load(g, "i64", op(g, "getelementptr inbounds " + llty(g, t) + ", ptr " + mp + ", i32 0, i32 2")) l := loopStart(g, used) e := entryAt(g, mp, t, l[3]) live := newLabel(g, "entry.live") next := newLabel(g, "entry.next") condBr(g, op(g, "icmp ne i64 " + load(g, "i64", entryField(g, e, t, 0)) + ", 0"), live, next) label(g, live) dropAt(g, entryField(g, e, t, 1), kt) dropAt(g, entryField(g, e, t, 2), vt) br(g, next) label(g, next) loopEnd(g, l) } emit(g, "call void @finch_map_clear(ptr " + mp + ")") return Val(ty: T("void")) } if n == "keys" || n == "values" { checkArgs(g, m, 1, 0, n) field := 1 et := kt if n == "values" { field = 2 et = vt } at := ast.arrayOf(et) out := alloca(g, "{ ptr, i64, i64 }") store(g, "{ ptr, i64, i64 }", "zeroinitializer", out) len := load(g, "i64", op(g, "getelementptr inbounds " + llty(g, t) + ", ptr " + mp + ", i32 0, i32 1")) emit(g, "call void @finch_arr_make(ptr " + out + ", i64 " + len + ", i64 " + sizeOf(g, et) + ")") data := load(g, "ptr", out) j := alloca(g, "i64") store(g, "i64", "0", j) used := load(g, "i64", op(g, "getelementptr inbounds " + llty(g, t) + ", ptr " + mp + ", i32 0, i32 2")) l := loopStart(g, used) entry := entryAt(g, mp, t, l[3]) live := newLabel(g, "entry.live") next := newLabel(g, "entry.next") condBr(g, op(g, "icmp ne i64 " + load(g, "i64", entryField(g, entry, t, 0)) + ", 0"), live, next) label(g, live) jv := load(g, "i64", j) copyAt(g, op(g, "getelementptr inbounds " + llty(g, et) + ", ptr " + data + ", i64 " + jv), entryField(g, entry, t, field), et) store(g, "i64", op(g, "add i64 " + jv + ", 1"), j) br(g, next) label(g, next) loopEnd(g, l) return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: at, fresh: true) } fail(g, m, "a map has no method '" + n + "' (it has: has, get, remove, clear, keys, values)") return Val() } // ---------- match ---------- // A pattern as a value of the subject's type. For an enum, a bare variant name (Red) works too. fn patternValue(ptr[Gen] g, ast.Node pat, ast.Type st) -> Val { if st.kind == "enum" && pat.kind == "var" && lookup(g, pat.name) < 0 { ed := g.enums[findEnum(g, st.module, st.name)] k := ed.variants.find(pat.name) if k < 0 { fail(g, pat, "'" + pat.name + "' is not a variant of " + ed.name + " (it has: " + ed.variants.join(", ") + ")") } return Val(v: str(ed.values[k]), ty: st) } return coerce(g, exprWant(g, pat, st), st, pat, "this pattern") } // match value { A, B -> ... C -> ... else -> ... }, as a statement or (asValue) as an expression. // The patterns are tried in order; for an enum the arms have to cover every variant, or end with else. fn matchGen(ptr[Gen] g, ast.Node e, bool asValue) -> Val { subj := expr(g, e.kids[0]) st := subj.ty if st.kind == "void" || st.kind == "null" { fail(g, e.kids[0], "there is nothing to match here") } mark := g.pending.len hold(g, subj) // a temporary subject lives through the arms si := -1 tagv := "" if st.kind == "struct" { si = findSum(g, st.module, st.name) if si >= 0 { tagv = op(g, "extractvalue " + llty(g, st) + " " + subj.v + ", " + str(g.structs[findStruct(g, st.module, st.name)].lidx[0])) } } end := newLabel(g, "match.end") hasElse := false reached := false // does any arm go on to the end? []str covered []str inVals []str inLabels resultTy := T("void") arms := e.kids.len - 1 for ai in 0..arms { arm := e.kids[1 + ai] if hasElse { fail(g, arm, "this arm can never be used: 'else' already takes everything that is left") } body := newLabel(g, "match.arm") next := newLabel(g, "match.next") []ast.Node binds bindK := -1 if arm.flag { hasElse = true br(g, body) } else { pats := arm.kids[0].kids for pi in 0..pats.len { hitv := "" if si >= 0 { sp := sumPattern(g, pats[pi], si) if covered.contains(str(sp.k)) { fail(g, pats[pi], "this variant is already handled by an earlier arm") } covered.push(str(sp.k)) if sp.binds.len > 0 { if pats.len > 1 { fail(g, pats[pi], "an arm with several variants can't name their values") } binds = sp.binds bindK = sp.k } hitv = op(g, "icmp eq i64 " + tagv + ", " + str(sp.k)) } else { pv := patternValue(g, pats[pi], st) if covered.contains(pv.v) && st.kind == "enum" { fail(g, pats[pi], "this variant is already handled by an earlier arm") } covered.push(pv.v) hitv = compare(g, "==", subj, pv, pats[pi]).v } if pi == pats.len - 1 { condBr(g, hitv, body, next) } else { more := newLabel(g, "match.or") condBr(g, hitv, body, more) label(g, more) } } } label(g, body) scoped := binds.len > 0 if scoped { pushScope(g) sx := g.structs[findStruct(g, st.module, st.name)] for j in 0..binds.len { if binds[j].name == "_" { continue } fi := g.sums[si].firsts[bindK] + j ft := sx.ftypes[fi] fv := op(g, "extractvalue " + llty(g, st) + " " + subj.v + ", " + str(sx.lidx[fi])) addVar(g, binds[j], binds[j].name, ft, copyValue(g, fv, ft), true, true) } } node := arm.kids[1] if asValue { v := expr(g, node) if ai == 0 || resultTy.kind == "void" { if v.ty.kind == "void" || v.ty.kind == "null" { fail(g, node, "an arm of a match that gives a value has to give one") } resultTy = v.ty } v = coerce(g, v, resultTy, node, "this arm (the arms of a match have to give the same type)") out := own(g, v) if scoped { popScope(g) } from := newLabel(g, "match.from") br(g, from) label(g, from) inVals.push(out) inLabels.push(from) br(g, end) reached = true } else { if node.kind == "block" { block(g, node, true) } else { stmt(g, node) } if !g.term { reached = true } if scoped { popScope(g) } br(g, end) } if !arm.flag { label(g, next) } } if !hasElse { if si >= 0 { []str missing for k in 0..g.sums[si].variants.len { if !covered.contains(str(k)) { missing.push(g.sums[si].variants[k]) } } if missing.len > 0 { fail(g, e, "this match doesn't handle " + missing.join(", ") + " (add them, or an 'else' arm)") } } else if st.kind == "enum" { ed := g.enums[findEnum(g, st.module, st.name)] []str missing for k in 0..ed.variants.len { if !covered.contains(str(ed.values[k])) { missing.push(ed.variants[k]) } } if missing.len > 0 { fail(g, e, "this match doesn't handle " + missing.join(", ") + " (add them, or an 'else' arm)") } } else { fail(g, e, "a match on " + ast.show(st) + " needs an 'else' arm for everything that is not listed") } // every variant is handled; only a number that was never a variant gets here panicIf(g, "true", "no arm of this match fits the value", e) emit(g, "unreachable") g.term = true } heldEnd(g, mark) label(g, end) if !reached { emit(g, "unreachable") g.term = true return Val(ty: T("void")) } if asValue { out := Val(v: op(g, "phi " + llty(g, resultTy) + " " + phiList(inVals, inLabels)), ty: resultTy, fresh: owning(g, resultTy)) release(g, subj) return out } release(g, subj) return Val(ty: T("void")) } // x := if c { a } else { b }: both branches give the same type fn ifValue(ptr[Gen] g, ast.Node e, ast.Type want) -> Val { c := coerce(g, expr(g, e.kids[0]), T("bool"), e.kids[0], "the condition") yes := newLabel(g, "ifv.yes") no := newLabel(g, "ifv.no") end := newLabel(g, "ifv.end") condBr(g, c.v, yes, no) label(g, yes) a := exprWant(g, e.kids[1], want) if a.ty.kind == "void" || a.ty.kind == "null" { fail(g, e.kids[1], "this branch has to give a value") } if want.kind != "void" { a = coerce(g, a, want, e.kids[1], "this branch") } t := a.ty av := own(g, a) fromYes := newLabel(g, "ifv.from") br(g, fromYes) label(g, fromYes) br(g, end) label(g, no) b := coerce(g, exprWant(g, e.kids[2], t), t, e.kids[2], "this branch (both branches of the if have to give the same type)") bv := own(g, b) fromNo := newLabel(g, "ifv.from") br(g, fromNo) label(g, fromNo) br(g, end) label(g, end) return Val(v: op(g, "phi " + llty(g, t) + " " + phiList([av, bv], [fromYes, fromNo])), ty: t, fresh: owning(g, t)) } fn phiList([]str vals, []str labels) -> str { []str parts for i in 0..vals.len { parts.push("[ " + vals[i] + ", %" + labels[i] + " ]") } return parts.join(", ") } // ---------- errors as values ---------- struct Fallible { Val v str failed // an i1 str msg // a str } // Emit a call that may fail; `failed` and `msg` tell how it went. fn fallibleCall(ptr[Gen] g, ast.Node site) -> Fallible { if site.kind != "call" && site.kind != "method" { fail(g, site, "only a function call can fail; 'or' and 'try' go after one, like int(text) or 0") } savedT := g.ftarget savedF := g.ffailed savedM := g.fmsg g.ftarget = nodeKey(site) g.ffailed = "" g.fmsg = "" v := expr(g, site) out := Fallible(v: v, failed: g.ffailed, msg: g.fmsg) g.ftarget = savedT g.ffailed = savedF g.fmsg = savedM if out.failed.len == 0 { fail(g, site, "this call can't fail, so it needs no 'or' or 'try'") } return out } // try f(x): on failure, the current function fails with the same error fn tryExpr(ptr[Gen] g, ast.Node e) -> Val { if g.inDefer > 0 { fail(g, e, "try can't be used inside defer") } if !g.cur.fallible { want := "!" if g.cur.ret.kind != "void" { want = ast.show(g.cur.ret) + "!" } fail(g, e, "try passes the error on, so '" + g.cur.name + "' must be able to fail too (-> " + want + "); or handle it here: ... or / ... or { ... }") } f := fallibleCall(g, e.kids[0]) bad := newLabel(g, "try.failed") ok := newLabel(g, "try.ok") condBr(g, f.failed, bad, ok) label(g, bad) dropPending(g, 0) // what the expressions around this `try` were holding unwind(g, 0) emit(g, "ret " + retTy(g, g.cur) + " " + op(g, "insertvalue " + retTy(g, g.cur) + " zeroinitializer, { ptr, i64, i64 } " + f.msg + ", 2")) g.term = true label(g, ok) return f.v } // f(x) or fallback / f(x) or { ... err ... } fn orElse(ptr[Gen] g, ast.Node e, bool discard) -> Val { f := fallibleCall(g, e.kids[0]) v := f.v bad := newLabel(g, "or.failed") ok := newLabel(g, "or.ok") condBr(g, f.failed, bad, ok) label(g, bad) if e.flag { pushScope(g) addVar(g, e, "err", T("str"), f.msg, true, true) block(g, e.kids[1], true) left := g.term popScope(g) if !left && v.ty.kind != "void" && !discard { fail(g, e.kids[1], "this block must leave with return, break, continue or exit(...): after a failure there is no value to go on with (or give one: ... or )") } if discard && v.ty.kind != "void" { join := newLabel(g, "or.end") br(g, join) label(g, ok) release(g, v) // read_file(p) or { ... } as a statement: the unused value goes br(g, join) label(g, join) return Val(ty: T("void")) } br(g, ok) label(g, ok) return v } if v.ty.kind == "void" { fail(g, e.kids[1], "this call gives back no value, so there is nothing to replace; handle the failure with or { ... }") } pushScope(g) addVar(g, e, "err", T("str"), f.msg, true, true) fb := coerce(g, exprWant(g, e.kids[1], v.ty), v.ty, e.kids[1], "the value after 'or'") fv := own(g, fb) popScope(g) badEnd := newLabel(g, "or.bend") join := newLabel(g, "or.end") br(g, badEnd) label(g, badEnd) br(g, join) label(g, ok) br(g, join) label(g, join) return Val(v: op(g, "phi " + llty(g, v.ty) + " [ " + fv + ", %" + badEnd + " ], [ " + v.v + ", %" + ok + " ]"), ty: v.ty, fresh: owning(g, v.ty)) } // An argument: a struct variable given where a ptr to that struct goes is passed by its address, // as if it were addr(x) (worldDef for b2CreateWorld(const b2WorldDef*), body for move(ptr[Body] b)) fn argWant(ptr[Gen] g, ast.Node a, ast.Type want) -> Val { if a.kind == "var" && want.kind == "ptr" && want.elem.len == 1 && want.elem[0].kind == "struct" { id := lookup(g, a.name) if id >= 0 && ast.same(varAt(g, id).ty, want.elem[0]) { pl := place(g, a, true) return Val(v: pl.addr, ty: ast.ptrTo(pl.ty)) } } return exprWant(g, a, want) } fn exprWant(ptr[Gen] g, ast.Node e, ast.Type want) -> Val { if e.kind == "array" { return arrayLit(g, e, want) } if e.kind == "map" { return mapLit(g, e, want) } if e.kind == "ifvalue" { return ifValue(g, e, want) // f32 x = if c { 1.0 } else { 0.5 } } return expr(g, e) } fn expr(ptr[Gen] g, ast.Node e) -> Val { k := e.kind if k == "int" { if e.text.starts_with("0x") || e.text.starts_with("0X") { n := 0 for c in e.text.sub(2, e.text.len) { d := int(c) - int('0') if c >= 'a' && c <= 'f' { d = int(c) - int('a') + 10 } if c >= 'A' && c <= 'F' { d = int(c) - int('A') + 10 } n = n * 16 + d } return Val(v: str(n), ty: T("int"), lit: true) } return Val(v: str(int(e.text)), ty: T("int"), lit: true) } if k == "float" { return Val(v: e.text, ty: T("float"), lit: true) } if k == "bool" { return Val(v: e.text, ty: T("bool")) } if k == "char" { n := int(e.text[0]) return Val(v: str(n), ty: T("char")) } if k == "str" { return strConst(g, e.text) } if k == "null" { return Val(v: "null", ty: T("null")) } if k == "var" || k == "member" || k == "index" { r := ref(g, e, false) if !r.isPlace { return r.val } return Val(v: load(g, llty(g, r.pl.ty), r.pl.addr), ty: r.pl.ty) } if k == "match" { return matchGen(g, e, true) } if k == "ifvalue" { return ifValue(g, e, T("void")) } if k == "try" { return tryExpr(g, e) } if k == "or" { return orElse(g, e, false) } if k == "unary" { return unary(g, e) } if k == "binary" { return binary(g, e) } if k == "call" { return call(g, e) } if k == "method" { return method(g, e) } if k == "array" { return arrayLit(g, e, T("void")) } if k == "map" { return mapLit(g, e, T("void")) } fail(g, e, "unknown expression " + k) return Val() } fn unary(ptr[Gen] g, ast.Node e) -> Val { v := expr(g, e.kids[0]) if v.ty.kind == "vec" && (e.name != "!" || v.ty.elem[0].kind == "bool") { return vecUnary(g, e, v) } if e.name == "!" { c := coerce(g, v, T("bool"), e.kids[0], "the value after '!'") return Val(v: op(g, "xor i1 " + c.v + ", true"), ty: T("bool")) } if e.name == "~" { if !isInt(v.ty) { fail(g, e, "'~' flips the bits of a whole number, not of " + ast.show(v.ty)) } return Val(v: op(g, "xor " + llty(g, v.ty) + " " + v.v + ", -1"), ty: v.ty) } if isUnsigned(v.ty) { fail(g, e, ast.show(v.ty) + " can't be negative (it's unsigned)") } if isSigned(v.ty) { if v.lit { if v.v.starts_with("-") { return Val(v: v.v.sub(1, v.v.len), ty: v.ty, lit: true) } return Val(v: "-" + v.v, ty: v.ty, lit: true) } return Val(v: op(g, "sub " + llty(g, v.ty) + " 0, " + v.v), ty: v.ty) } if isFloat(v.ty) { if v.lit { // -1.5 is still a number written in the code: f32 x = -1.5 works like f32 x = 1.5 if v.v.starts_with("-") { return Val(v: v.v.sub(1, v.v.len), ty: v.ty, lit: true) } return Val(v: "-" + v.v, ty: v.ty, lit: true) } return Val(v: op(g, "fneg " + llty(g, v.ty) + " " + v.v), ty: v.ty) } fail(g, e, "can't put '-' in front of " + ast.show(v.ty)) return v } fn badOperands(ptr[Gen] g, str o, Val l, Val r, ast.Node at) { msg := "can't use '" + o + "' on " + ast.show(l.ty) if !ast.same(l.ty, r.ty) { msg = msg + " and " + ast.show(r.ty) } if o == "+" && (l.ty.kind == "str" || r.ty.kind == "str") { msg = msg + " (turn the other side into text first: str(...))" } if (o == "&" || o == "|") && l.ty.kind == "bool" { if o == "&" { msg = msg + " (for true/false use '&&')" } else { msg = msg + " (for true/false use '||')" } } else if o == "&" || o == "|" || o == "^" || o == "<<" || o == ">>" { msg = msg + " (bit operators work on whole numbers only)" } if (l.ty.kind == "struct" || l.ty.kind == "array") && (o == "==" || o == "!=") { msg = msg + " (compare the parts one by one)" } fail(g, at, msg) } fn unify(ptr[Gen] g, ptr[Val] l, ptr[Val] r) -> bool { if ast.same(l.ty, r.ty) { return true } if isInt(l.ty) && isInt(r.ty) { // a number written in the code adapts to the other side; otherwise // the smaller type grows into the bigger one if l.lit && !r.lit && fits(l.v, l.ty, r.ty) { l.value = Val(v: l.v, ty: r.ty, lit: true) return true } if r.lit && !l.lit && fits(r.v, r.ty, l.ty) { r.value = Val(v: r.v, ty: l.ty, lit: true) return true } if widens(l.ty, r.ty) { l.value = Val(v: intCast(g, l.v, l.ty, r.ty), ty: r.ty) return true } if widens(r.ty, l.ty) { r.value = Val(v: intCast(g, r.v, r.ty, l.ty), ty: l.ty) return true } return false } if isInt(l.ty) && isFloat(r.ty) { l.value = coerce(g, l.value, r.ty, Node0(), "") return true } if isFloat(l.ty) && isInt(r.ty) { r.value = coerce(g, r.value, l.ty, Node0(), "") return true } if isFloat(l.ty) && isFloat(r.ty) { // a number written in the code adapts to an f32 on the other side: x * 2.0 stays f32 if l.ty.kind == "f32" && r.lit { r.value = Val(v: floatCast(g, r.v, r.ty, l.ty), ty: l.ty) return true } if r.ty.kind == "f32" && l.lit { l.value = Val(v: floatCast(g, l.v, l.ty, r.ty), ty: r.ty) return true } if l.ty.kind == "f32" { l.value = Val(v: floatCast(g, l.v, l.ty, T("float")), ty: T("float")) } if r.ty.kind == "f32" { r.value = Val(v: floatCast(g, r.v, r.ty, T("float")), ty: T("float")) } return true } return false } fn Node0() -> ast.Node { return ast.Node() } fn arith(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { if o == "+" && l.ty.kind == "str" && r.ty.kind == "str" { out := alloca(g, "{ ptr, i64, i64 }") emit(g, "call void @finch_str_concat(ptr " + out + ", ptr " + spill(g, l) + ", ptr " + spill(g, r) + ")") release(g, l) release(g, r) return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: T("str"), fresh: true) } if l.ty.kind == "vec" || r.ty.kind == "vec" { return vecArith(g, o, l, r, at) } if !unify(g, addr(l), addr(r)) { badOperands(g, o, l, r, at) } if isInt(l.ty) { names := ["+", "-", "*", "/", "%", "&", "|", "^", "<<", ">>"] insts := ["add", "sub", "mul", "sdiv", "srem", "and", "or", "xor", "shl", "ashr"] if isUnsigned(l.ty) { insts = ["add", "sub", "mul", "udiv", "urem", "and", "or", "xor", "shl", "lshr"] } k := names.find(o) if k < 0 { badOperands(g, o, l, r, at) } lt := llty(g, l.ty) if o == "/" || o == "%" { if r.lit && r.v == "0" { fail(g, at, "division by zero") } if !r.lit { panicIf(g, op(g, "icmp eq " + lt + " " + r.v + ", 0"), "division by zero", at) } } if l.lit && r.lit && l.ty.kind == "int" { // numbers written in the code stay such a number: FLAG_A | FLAG_B fits a u64 or a u32 a := int(l.v) or 0 b := int(r.v) or 0 if (o != "<<" && o != ">>") || (b >= 0 && b < 63) { return Val(v: str(foldInt(o, a, b)), ty: l.ty, lit: true) } } return Val(v: op(g, insts[k] + " " + lt + " " + l.v + ", " + r.v), ty: l.ty) } if isFloat(l.ty) { names := ["+", "-", "*", "/", "%"] insts := ["fadd", "fsub", "fmul", "fdiv", "frem"] k := names.find(o) if k < 0 { badOperands(g, o, l, r, at) } return Val(v: op(g, insts[k] + " " + llty(g, l.ty) + " " + l.v + ", " + r.v), ty: l.ty) } badOperands(g, o, l, r, at) return l } fn foldInt(str o, int a, int b) -> int { if o == "+" { return a + b } else if o == "-" { return a - b } else if o == "*" { return a * b } else if o == "/" { return a / b } else if o == "%" { return a % b } else if o == "&" { return a & b } else if o == "|" { return a | b } else if o == "^" { return a ^ b } else if o == "<<" { return a << b } return a >> b } fn compare(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { eq := o == "==" || o == "!=" if l.ty.kind == "ptr" || r.ty.kind == "ptr" || l.ty.kind == "null" || r.ty.kind == "null" { if l.ty.kind == "str" || r.ty.kind == "str" { fail(g, at, "a str is never null; to check for empty text use s.len == 0") } if !eq { badOperands(g, o, l, r, at) } pred := "eq" if o == "!=" { pred = "ne" } return Val(v: op(g, "icmp " + pred + " ptr " + l.v + ", " + r.v), ty: T("bool")) } if l.ty.kind == "str" && r.ty.kind == "str" { if eq { c := op(g, "call i32 @finch_str_eq(ptr " + spill(g, l) + ", ptr " + spill(g, r) + ")") pred := "ne" if o == "!=" { pred = "eq" } res := op(g, "icmp " + pred + " i32 " + c + ", 0") release(g, l) release(g, r) return Val(v: res, ty: T("bool")) } c := op(g, "call i64 @finch_str_cmp(ptr " + spill(g, l) + ", ptr " + spill(g, r) + ")") res := op(g, "icmp " + intPred(o, true) + " i64 " + c + ", 0") release(g, l) release(g, r) return Val(v: res, ty: T("bool")) } if l.ty.kind == "vec" || r.ty.kind == "vec" { return vecCompare(g, o, l, r, at) } if !unify(g, addr(l), addr(r)) { badOperands(g, o, l, r, at) } k := l.ty.kind if isFloat(l.ty) { ops := ["==", "!=", "<", "<=", ">", ">="] preds := ["oeq", "une", "olt", "ole", "ogt", "oge"] return Val(v: op(g, "fcmp " + preds[ops.find(o)] + " " + llty(g, l.ty) + " " + l.v + ", " + r.v), ty: T("bool")) } if k == "enum" { if !eq { fail(g, at, "enums can only be compared with == and != (to go through them in order, give the variants numbers and use int(...))") } pred := "eq" if o == "!=" { pred = "ne" } return Val(v: op(g, "icmp " + pred + " i32 " + l.v + ", " + r.v), ty: T("bool")) } if k == "bool" && !eq { badOperands(g, o, l, r, at) } if !isInt(l.ty) && k != "char" && k != "bool" { badOperands(g, o, l, r, at) } return Val(v: op(g, "icmp " + intPred(o, isSigned(l.ty)) + " " + llty(g, l.ty) + " " + l.v + ", " + r.v), ty: T("bool")) } fn intPred(str o, bool signed) -> str { ops := ["==", "!=", "<", "<=", ">", ">="] preds := ["eq", "ne", "ult", "ule", "ugt", "uge"] if signed { preds = ["eq", "ne", "slt", "sle", "sgt", "sge"] } return preds[ops.find(o)] } fn binary(ptr[Gen] g, ast.Node e) -> Val { o := e.name if o == "&&" || o == "||" { l := condition(g, e.kids[0]) from := newLabel(g, "logic.from") rhs := newLabel(g, "logic.rhs") end := newLabel(g, "logic.end") br(g, from) label(g, from) if o == "&&" { condBr(g, l, rhs, end) } else { condBr(g, l, end, rhs) } label(g, rhs) r := condition(g, e.kids[1]) rhsEnd := newLabel(g, "logic.rend") br(g, rhsEnd) label(g, rhsEnd) br(g, end) label(g, end) short := "true" if o == "&&" { short = "false" } return Val(v: op(g, "phi i1 [ " + short + ", %" + from + " ], [ " + r + ", %" + rhsEnd + " ]"), ty: T("bool")) } l := expr(g, e.kids[0]) mark := g.pending.len hold(g, l) // while the right side is computed (it may `try`) r := expr(g, e.kids[1]) heldEnd(g, mark) if o == "==" || o == "!=" || o == "<" || o == "<=" || o == ">" || o == ">=" { return compare(g, o, l, r, e) } return arith(g, o, l, r, e) } fn arrayLit(ptr[Gen] g, ast.Node a, ast.Type want) -> Val { et := T("void") if want.kind == "array" { et = want.elem[0] } else if a.kids.len == 0 { fail(g, a, "an empty [] needs a type, like: []int nums = []") } []str vals mark := g.pending.len for k in a.kids { v := Val() if et.kind == "void" { v = expr(g, k) et = v.ty } else { v = exprWant(g, k, et) } v = coerce(g, v, et, k, "every element of this array") vals.push(own(g, v)) hold(g, Val(v: vals[vals.len - 1], ty: et, fresh: true)) // while the next elements are computed } heldEnd(g, mark) at := ast.arrayOf(et) n := str(vals.len) data := "null" if vals.len > 0 { size := op(g, "mul i64 " + n + ", " + sizeOf(g, et)) data = op(g, "call ptr @finch_alloc(i64 " + size + ")") } for i in 0..vals.len { p := op(g, "getelementptr inbounds " + llty(g, et) + ", ptr " + data + ", i64 " + str(i)) store(g, llty(g, et), vals[i], p) } v1 := op(g, "insertvalue { ptr, i64, i64 } undef, ptr " + data + ", 0") v2 := op(g, "insertvalue { ptr, i64, i64 } " + v1 + ", i64 " + n + ", 1") v3 := op(g, "insertvalue { ptr, i64, i64 } " + v2 + ", i64 " + n + ", 2") return Val(v: v3, ty: at, fresh: true) } // ---------- places: variables, fields, elements ---------- fn place(ptr[Gen] g, ast.Node e, bool write) -> Place { if e.kind == "var" && lookup(g, e.name) < 0 { fail(g, e, "there is no variable named '" + e.name + "'") } r := ref(g, e, write) if !r.isPlace { fail(g, e, "this is a temporary value, it can't be changed or pointed to") } return r.pl } fn ref(ptr[Gen] g, ast.Node e, bool write) -> Ref { if e.kind == "var" { id := lookup(g, e.name) if id < 0 && g.ci.consts.has(e.name) { c := g.ci.consts[e.name] if c.structName.len > 0 { // RAYWHITE: Color(245, 245, 245, 255) si := findStruct(g, "C", c.structName) []ast.Node args []str names for i in 0..c.fields.len { kind := "int" if c.fieldFloat[i] { kind = "float" } args.push(ast.Node(kind: kind, text: c.fields[i], line: e.line, col: e.col, file: e.file)) names.push(g.structs[si].fnames[i]) // by name: C leaves the fields not given at 0 } return Ref(isPlace: false, val: construct(g, si, args, names, e)) } if c.isStr { return Ref(isPlace: false, val: strConst(g, c.s)) } if c.isFloat { return Ref(isPlace: false, val: Val(v: llFloat(c.text), ty: T("float"), lit: true)) } return Ref(isPlace: false, val: Val(v: str(c.i), ty: T("int"), lit: true)) } // C's standard streams are macros on some systems; the runtime knows them everywhere if id < 0 && !g.ci.globals.has(e.name) && g.ci.fns.has("fprintf") && (e.name == "stdin" || e.name == "stdout" || e.name == "stderr") { which := "2" if e.name == "stdin" { which = "0" } else if e.name == "stdout" { which = "1" } return Ref(isPlace: false, val: Val(v: op(g, "call ptr @finch_std_stream(i32 " + which + ")"), ty: T("ptr"))) } if id < 0 && g.ci.globals.has(e.name) { gt := resolve(g, g.ci.globals[e.name], e) if !g.cdone.contains("g:" + e.name) { g.cdone.push("g:" + e.name) g.head.push("@" + e.name + " = external global " + llty(g, gt)) } return Ref(isPlace: true, pl: Place(addr: "@" + e.name, ty: gt)) } if id < 0 && findConst(g, g.module, e.name) >= 0 { if write { fail(g, e, "'" + e.name + "' is a constant; it can't be changed") } return Ref(isPlace: false, val: constValue(g, g.module, e.name, e)) } if id < 0 { if imports(g, e.name) { fail(g, e, "'" + e.name + "' is a module; use something from it, like " + e.name + ".name(...)") } fail(g, e, "there is no variable named '" + e.name + "'") } v := varAt(g, id) noteRef(g, e, e.col, e.name.len, "```finch\n" + ast.show(v.ty) + " " + e.name + "\n```", v.dpath, v.dline, v.dcol) return Ref(isPlace: true, pl: Place(addr: v.slot, ty: v.ty)) } if e.kind == "member" { return member(g, e, write) } if e.kind == "index" { return index(g, e, write) } return Ref(isPlace: false, val: expr(g, e)) } // A float as C wrote it, in the form LLVM's text format wants: 1e10 -> 1.0e10, .5 -> 0.5 fn llFloat(str text) -> str { s := text if s.starts_with(".") { s = "0" + s } if !s.contains(".") { e := s.find("e") if e < 0 { e = s.find("E") } if e < 0 { return s + ".0" } return s.sub(0, e) + ".0" + s.sub(e, s.len) } return s } // ---------- constants ---------- fn findConst(ptr[Gen] g, str module, str name) -> int { pi := progOf(g, module) if pi < 0 { return -1 } for i in 0..g.progs[pi].consts.len { if g.progs[pi].consts[i].name == name { return i } } return -1 } // A constant is worked out where it is used, from the code it was written with: numbers, // text, true/false, chars, other constants, enum variants and operators between them. fn constValue(ptr[Gen] g, str module, str name, ast.Node at) -> Val { pi := progOf(g, module) c := g.progs[pi].consts[findConst(g, module, name)] key := module + "." + name if g.constBusy.contains(key) { fail(g, at, "the constant '" + name + "' is worked out from itself") } checkConst(g, c.kids[0]) g.constBusy.push(key) savedScopes := g.scopes savedModule := g.module []Scope none g.scopes = none pushScope(g) g.module = module v := Val() if c.flag { t := resolve(g, c.ty, c) v = coerce(g, exprWant(g, c.kids[0], t), t, c.kids[0], "the constant '" + name + "'") } else { v = expr(g, c.kids[0]) } g.module = savedModule g.scopes = savedScopes g.constBusy.pop() noteRef(g, at, at.col, name.len, "```finch\nconst " + ast.show(v.ty) + " " + name + "\n```", g.files[c.file], c.line, c.col) return v } fn checkConst(ptr[Gen] g, ast.Node e) { k := e.kind if ["int", "float", "char", "str", "bool", "var", "unary", "binary", "member"].contains(k) { for kid in e.kids { checkConst(g, kid) } return } if k == "call" && numType(e.name).len > 0 { for kid in e.kids { checkConst(g, kid) } return // f32(0.5), u8(255) } fail(g, e, "a constant is made of numbers, text, true/false, chars and other constants (not calls or variables)") } // Which enum does `m.kids[0]` name, if it names one? Color, or shapes.Color. -1 if neither. fn enumNamed(ptr[Gen] g, ast.Node n) -> int { if n.kind == "var" && lookup(g, n.name) < 0 { return findEnum(g, g.module, n.name) } if n.kind == "member" && n.kids[0].kind == "var" && lookup(g, n.kids[0].name) < 0 && imports(g, n.kids[0].name) { return findEnum(g, modFor(g, n.kids[0].name), n.name) } return -1 } // Which payload enum does `n` name? Shape, or shapes.Shape. -1 if neither. fn sumNamed(ptr[Gen] g, ast.Node n) -> int { if n.kind == "var" && lookup(g, n.name) < 0 { return findSum(g, g.module, n.name) } if n.kind == "member" && n.kids[0].kind == "var" && lookup(g, n.kids[0].name) < 0 && imports(g, n.kids[0].name) { return findSum(g, modFor(g, n.kids[0].name), n.name) } return -1 } fn sumVariant(ptr[Gen] g, int si, str name, ast.Node at) -> int { sd := g.sums[si] k := sd.variants.find(name) if k < 0 { fail(g, at, "the enum " + sd.name + " has no variant '" + name + "' (it has: " + sd.variants.join(", ") + ")") } return k } // Shape.Circle(2.0), or Shape.Point fn sumMake(ptr[Gen] g, int si, str vname, []ast.Node args, ast.Node at) -> Val { sd := g.sums[si] k := sumVariant(g, si, vname, at) if args.len != sd.counts[k] { if sd.counts[k] == 0 { fail(g, at, vname + " holds nothing: write " + sd.name + "." + vname) } fail(g, at, vname + " holds " + str(sd.counts[k]) + " value(s), but got " + str(args.len) + ": " + sd.name + "." + vname + "(...)") } []ast.Node all []str names all.push(ast.Node(kind: "int", text: str(k), line: at.line, col: at.col, file: at.file)) names.push("tag") for j in 0..args.len { all.push(args[j]) names.push(vname + "_" + str(j)) } noteRef(g, at, at.col, vname.len, "```finch\n" + sd.name + "." + vname + "\n```", "", 0, 0) return construct(g, findStruct(g, sd.module, sd.name), all, names, at) } // the variant a match pattern names, and the names it binds: Circle(r) Shape.Rect(w, h) Point fn sumPattern(ptr[Gen] g, ast.Node pat, int si) -> SumPat { sd := g.sums[si] name := "" []ast.Node binds if pat.kind == "var" && lookup(g, pat.name) < 0 { name = pat.name } else if pat.kind == "call" { name = pat.name binds = pat.kids } else if pat.kind == "member" && sumNamed(g, pat.kids[0]) == si { name = pat.name } else if pat.kind == "method" && sumNamed(g, pat.kids[0]) == si { name = pat.name for q in 1..pat.kids.len { binds.push(pat.kids[q]) } } else { fail(g, pat, "this pattern is not a variant of " + sd.name + " (it has: " + sd.variants.join(", ") + ")") } k := sumVariant(g, si, name, pat) if binds.len != sd.counts[k] { if sd.counts[k] == 0 { fail(g, pat, name + " holds nothing: write just " + name) } fail(g, pat, name + " holds " + str(sd.counts[k]) + " value(s): name them, like " + name + "(x), or use _ to ignore one") } for b in binds { if b.kind != "var" { fail(g, b, "write a name to hold the value (or _ to ignore it)") } } return SumPat(k: k, binds: binds) } // Color.Red fn enumValue(ptr[Gen] g, ast.Node m, int ei) -> Val { ed := g.enums[ei] k := ed.variants.find(m.name) if k < 0 { fail(g, m, "the enum " + ed.name + " has no variant '" + m.name + "' (it has: " + ed.variants.join(", ") + ")") } noteRef(g, m, m.col + 1, m.name.len, "```finch\n" + ed.name + "." + m.name + " = " + str(ed.values[k]) + "\n```", "", 0, 0) return Val(v: str(ed.values[k]), ty: ast.Type(kind: "enum", name: ed.name, module: ed.module)) } fn member(ptr[Gen] g, ast.Node m, bool write) -> Ref { sm := sumNamed(g, m.kids[0]) if sm >= 0 { if write { fail(g, m, "a variant of an enum can't be changed") } k := sumVariant(g, sm, m.name, m) if g.sums[sm].counts[k] > 0 { fail(g, m, m.name + " holds " + str(g.sums[sm].counts[k]) + " value(s): " + g.sums[sm].name + "." + m.name + "(...)") } []ast.Node none return Ref(isPlace: false, val: sumMake(g, sm, m.name, none, m)) } ei := enumNamed(g, m.kids[0]) if ei >= 0 { if write { fail(g, m, "a variant of an enum can't be changed") } return Ref(isPlace: false, val: enumValue(g, m, ei)) } mod := m.kids[0] if mod.kind == "var" && lookup(g, mod.name) < 0 && imports(g, mod.name) && findConst(g, modFor(g, mod.name), m.name) >= 0 { if write { fail(g, m, "'" + mod.name + "." + m.name + "' is a constant; it can't be changed") } return Ref(isPlace: false, val: constValue(g, modFor(g, mod.name), m.name, m)) // shapes.SIDES } obj := ref(g, m.kids[0], write) t := obj.val.ty if obj.isPlace { t = obj.pl.ty } if t.kind == "ptr" { pv := obj.val.v if obj.isPlace { pv = load(g, "ptr", obj.pl.addr) } if t.elem.len == 0 { fail(g, m, "this ptr has no type, so Finch doesn't know what's inside") } panicIf(g, op(g, "icmp eq ptr " + pv + ", null"), "used ." + m.name + " on a null pointer", m) target := t.elem[0] structHasValue := false if target.kind == "struct" { structHasValue = g.structs[findStruct(g, target.module, target.name)].fnames.contains("value") } if m.name == "value" && !structHasValue { return Ref(isPlace: true, pl: Place(addr: pv, ty: target)) } if target.kind != "struct" { fail(g, m, "a " + ast.show(t) + " only has '.value' (the thing it points to)") } obj = Ref(isPlace: true, pl: Place(addr: pv, ty: target)) t = target } if t.kind == "vec" { if m.name != "len" { fail(g, m, "a " + ast.show(t) + " has .len and [i] (and methods like .sum()), not '." + m.name + "'") } if write { fail(g, m, ".len can't be changed") } return Ref(isPlace: false, val: Val(v: str(t.count), ty: T("int"), lit: true)) } if t.kind == "fixed" { if write { fail(g, m, "." + m.name + " can't be changed directly") } if m.name == "len" { return Ref(isPlace: false, val: Val(v: str(t.count), ty: T("int"), lit: true)) } if m.name != "ptr" { fail(g, m, "an array has .len and .ptr, not '." + m.name + "'") } if !obj.isPlace { fail(g, m, ".ptr of a temporary value would point to nothing") } return Ref(isPlace: false, val: Val(v: obj.pl.addr, ty: ast.ptrTo(t.elem[0]))) } if (t.kind == "array" || t.kind == "str") && m.name == "ptr" { // the address of the first element, for C; a literal lives as long as the program, other temporaries do not if write { fail(g, m, ".ptr can't be changed directly") } // a constant (a text literal) lives as long as the program; a computed value is freed right after if !obj.isPlace && obj.val.fresh && obj.val.v.starts_with("%") { fail(g, m, ".ptr of a temporary value would point to freed memory") } v := obj.val.v if obj.isPlace { v = load(g, llty(g, t), obj.pl.addr) } if t.kind == "str" { return Ref(isPlace: false, val: Val(v: cstr(g, v), ty: ast.ptrTo(T("char")))) } return Ref(isPlace: false, val: Val(v: op(g, "extractvalue { ptr, i64, i64 } " + v + ", 0"), ty: ast.ptrTo(t.elem[0]))) } if t.kind == "array" || t.kind == "str" || t.kind == "map" { if m.name != "len" { fail(g, m, "an array, map or str has .len, not '." + m.name + "'") } if write { fail(g, m, ".len can't be changed directly (use push or pop)") } if obj.isPlace { lp := op(g, "getelementptr inbounds " + llty(g, t) + ", ptr " + obj.pl.addr + ", i32 0, i32 1") return Ref(isPlace: false, val: Val(v: load(g, "i64", lp), ty: T("int"))) } n := op(g, "extractvalue " + llty(g, t) + " " + obj.val.v + ", 1") release(g, obj.val) return Ref(isPlace: false, val: Val(v: n, ty: T("int"))) } if t.kind != "struct" { fail(g, m, ast.show(t) + " has no fields (no '." + m.name + "')") } sd := g.structs[findStruct(g, t.module, t.name)] k := sd.fnames.find(m.name) if k < 0 { fail(g, m, "the struct " + ast.show(t) + " has no field '" + m.name + "' (it has: " + sd.fnames.join(", ") + ")") } ft := sd.ftypes[k] noteRef(g, m, m.col + 1, m.name.len, "```finch\n" + ast.show(ft) + " " + m.name + "\n```", "", 0, 0) if obj.isPlace { fp := op(g, "getelementptr inbounds " + llty(g, t) + ", ptr " + obj.pl.addr + ", i32 0, i32 " + str(sd.lidx[k])) return Ref(isPlace: true, pl: Place(addr: fp, ty: ft)) } if sd.isUnion { u := spill(g, obj.val) return Ref(isPlace: false, val: Val(v: load(g, llty(g, ft), u), ty: ft)) } fv := op(g, "extractvalue " + llty(g, t) + " " + obj.val.v + ", " + str(sd.lidx[k])) out := Val(v: fv, ty: ft) if owning(g, ft) && obj.val.fresh { out = Val(v: copyValue(g, fv, ft), ty: ft, fresh: true) } release(g, obj.val) // a field of a temporary struct: take it, then drop the rest return Ref(isPlace: false, val: out) } fn index(ptr[Gen] g, ast.Node e, bool write) -> Ref { obj := ref(g, e.kids[0], write) mark := g.pending.len if !obj.isPlace { hold(g, obj.val) // while the index is computed } r := indexOf(g, e, obj, write) heldEnd(g, mark) return r } fn indexOf(ptr[Gen] g, ast.Node e, Ref obj, bool write) -> Ref { t := obj.val.ty if obj.isPlace { t = obj.pl.ty } if t.kind == "map" { return mapIndex(g, e, obj, t, write) } i := coerce(g, expr(g, e.kids[1]), T("int"), e.kids[1], "an index").v if t.kind == "vec" && t.elem[0].kind == "bool" { // a mask keeps a lane in a bit, so a lane is read from the whole value if write { fail(g, e, "a lane of a mask can't be set by itself; make a new mask (with a comparison or select)") } boundsCheck(g, i, str(t.count), e) mv := obj.val.v if obj.isPlace { mv = load(g, llty(g, t), obj.pl.addr) } return Ref(isPlace: false, val: Val(v: op(g, "extractelement " + llty(g, t) + " " + mv + ", i64 " + i), ty: T("bool"))) } if t.kind == "vec" { // a lane: v[2], v[i] = x boundsCheck(g, i, str(t.count), e) if obj.isPlace { ep := op(g, "getelementptr inbounds " + llty(g, t.elem[0]) + ", ptr " + obj.pl.addr + ", i64 " + i) return Ref(isPlace: true, pl: Place(addr: ep, ty: t.elem[0])) } return Ref(isPlace: false, val: Val(v: op(g, "extractelement " + llty(g, t) + " " + obj.val.v + ", i64 " + i), ty: t.elem[0])) } if t.kind == "fixed" { base := "" if obj.isPlace { base = obj.pl.addr } else { base = spill(g, obj.val) } boundsCheck(g, i, str(t.count), e) ep := op(g, "getelementptr inbounds " + llty(g, t) + ", ptr " + base + ", i64 0, i64 " + i) return Ref(isPlace: true, pl: Place(addr: ep, ty: t.elem[0])) } if t.kind == "ptr" { if t.elem.len == 0 { fail(g, e, "this ptr has no type, so Finch doesn't know what's inside") } pv := obj.val.v if obj.isPlace { pv = load(g, "ptr", obj.pl.addr) } et := t.elem[0] return Ref(isPlace: true, pl: Place(addr: op(g, "getelementptr inbounds " + llty(g, et) + ", ptr " + pv + ", i64 " + i), ty: et)) } if t.kind != "array" && t.kind != "str" { fail(g, e, "[ ] works on arrays, str and pointers, not on " + ast.show(t)) } et := T("char") if t.kind == "array" { et = t.elem[0] } if t.kind == "str" && write { if !obj.isPlace { fail(g, e, "this is a temporary str, it can't be changed") } emit(g, "call void @finch_str_own(ptr " + obj.pl.addr + ")") } v := obj.val.v if obj.isPlace { v = load(g, "{ ptr, i64, i64 }", obj.pl.addr) } len := op(g, "extractvalue { ptr, i64, i64 } " + v + ", 1") boundsCheck(g, i, len, e) data := op(g, "extractvalue { ptr, i64, i64 } " + v + ", 0") ep := op(g, "getelementptr inbounds " + llty(g, et) + ", ptr " + data + ", i64 " + i) if obj.isPlace { return Ref(isPlace: true, pl: Place(addr: ep, ty: et)) } ev := load(g, llty(g, et), ep) out := Val(v: ev, ty: et) if owning(g, et) && obj.val.fresh { out = Val(v: copyValue(g, ev, et), ty: et, fresh: true) } release(g, obj.val) return Ref(isPlace: false, val: out) } // ---------- calls ---------- fn checkArgs(ptr[Gen] g, ast.Node c, int first, int want, str name) { have := c.kids.len - first for n in c.names { if n.len > 0 { fail(g, c, "named arguments (" + n + ": ...) are only for creating structs") } } if have != want { fail(g, c, "'" + name + "' takes " + str(want) + " argument(s), but got " + str(have)) } } fn strArg(ptr[Gen] g, ast.Node e, str what) -> Val { return coerce(g, expr(g, e), T("str"), e, what) } fn strOut(ptr[Gen] g) -> str { return alloca(g, "{ ptr, i64, i64 }") } // The type a conversion call names: int(x), u8(x), f32(x), bool(x). i64 and // f64 are the same types as int and float. fn numType(str n) -> str { if n == "i64" { return "int" } if n == "f64" { return "float" } names := ["int", "float", "char", "bool", "i8", "i16", "i32", "u8", "u16", "u32", "u64", "f32", "ptr"] if names.contains(n) { return n } return "" } // Explicit conversions between the number-like types. Cutting a bigger // integer down wraps, as on hardware; text is parsed, with a runtime error. fn convert(ptr[Gen] g, ast.Node c, ast.Type to) -> Val { checkArgs(g, c, 0, 1, c.name) v := expr(g, c.kids[0]) from := v.ty if ast.same(from, to) { return v } if from.kind == "str" && isNumber(to) && g.ftarget == nodeKey(c) { // int(text) or 0: a bad number is reported to the `or`, not a runtime error err := alloca(g, "{ ptr, i64, i64 }") store(g, "{ ptr, i64, i64 }", "zeroinitializer", err) if isInt(to) { out := alloca(g, "i64") store(g, "i64", "0", out) ok := op(g, "call i32 @finch_str_try_int(ptr " + spill(g, v) + ", ptr " + out + ", ptr " + err + ")") g.ffailed = op(g, "icmp eq i32 " + ok + ", 0") g.fmsg = load(g, "{ ptr, i64, i64 }", err) release(g, v) return Val(v: intCast(g, load(g, "i64", out), T("int"), to), ty: to) } out := alloca(g, "double") store(g, "double", "0.0", out) ok := op(g, "call i32 @finch_str_try_float(ptr " + spill(g, v) + ", ptr " + out + ", ptr " + err + ")") g.ffailed = op(g, "icmp eq i32 " + ok + ", 0") g.fmsg = load(g, "{ ptr, i64, i64 }", err) release(g, v) return Val(v: floatCast(g, load(g, "double", out), T("float"), to), ty: to) } if from.kind == "str" && isNumber(to) { file := strGlobal(g, g.files[c.file]) if isInt(to) { r := op(g, "call i64 @finch_str_to_int(ptr " + spill(g, v) + ", ptr " + file + ", i64 " + str(c.line) + ")") release(g, v) return Val(v: intCast(g, r, T("int"), to), ty: to) } r := op(g, "call double @finch_str_to_float(ptr " + spill(g, v) + ", ptr " + file + ", i64 " + str(c.line) + ")") release(g, v) return Val(v: floatCast(g, r, T("float"), to), ty: to) } if isInt(to) && from.kind == "enum" { return Val(v: intCast(g, v.v, T("i32"), to), ty: to) } if isInt(to) || to.kind == "char" { if isInt(from) || from.kind == "char" { return Val(v: intCast(g, v.v, from, to), ty: to) } if from.kind == "bool" { return Val(v: op(g, "zext i1 " + v.v + " to " + llty(g, to)), ty: to) } if isFloat(from) && isInt(to) { conv := "fptoui" if isSigned(to) { conv = "fptosi" } return Val(v: op(g, conv + " " + llty(g, from) + " " + v.v + " to " + llty(g, to)), ty: to) } } if isFloat(to) { if isInt(from) { return Val(v: intToFloat(g, v.v, from, to), ty: to) } if isFloat(from) { return Val(v: floatCast(g, v.v, from, to), ty: to) } } if to.kind == "bool" && isInt(from) { return Val(v: op(g, "icmp ne " + llty(g, from) + " " + v.v + ", 0"), ty: to) } if to.kind == "ptr" && (from.kind == "ptr" || from.kind == "null") { return Val(v: v.v, ty: to) } if to.kind == "ptr" && isInt(from) { return Val(v: op(g, "inttoptr i64 " + intCast(g, v.v, from, T("u64")) + " to ptr"), ty: to) // an address or offset for C } if isInt(to) && from.kind == "ptr" { return Val(v: op(g, "ptrtoint ptr " + v.v + " to " + llty(g, to)), ty: to) } if to.kind == "ptr" && from.kind == "str" { fail(g, c.kids[0], "use .ptr to get a str's address (it is only valid while the str lives)") } fail(g, c.kids[0], "can't turn " + ast.show(from) + " into " + c.name) return v } // ---------- the C calling convention for structs passed by value ---------- // // System V x86-64: a struct of up to 16 bytes travels in registers, one INTEGER or SSE class per eightbyte; // bigger ones are copied to the stack (byval) and returned through a hidden pointer (sret). // ARM64 (AAPCS64, and Apple's variant) and Windows x64 have their own rules: classifyArm64, and the // windows branch of classify. struct Abi { str kind // "direct", "expand" (in registers: `parts`) or "memory" []str parts str coerced int align } struct Leaf { int offset int size bool isFloat bool isDouble } struct Plan { Abi ret []Abi params str retLL bool sret } fn sizeBytes(ptr[Gen] g, ast.Type t) -> int { if t.kind == "struct" { return g.structs[findStruct(g, t.module, t.name)].csize } if t.kind == "fixed" { return t.count * sizeBytes(g, t.elem[0]) } if t.kind == "ptr" || t.kind == "str" { return 8 } if t.kind == "float" { return 8 } if t.kind == "f32" { return 4 } if t.kind == "bool" { return 1 } return bitsOf(t) / 8 } // A value flattened into its scalar pieces, with byte offsets. fn leavesOf(ptr[Gen] g, ast.Type t, int base) -> []Leaf { []Leaf out if t.kind == "struct" { sd := g.structs[findStruct(g, t.module, t.name)] for k in 0..sd.ftypes.len { for l in leavesOf(g, sd.ftypes[k], base + sd.offs[k]) { out.push(l) } } } else if t.kind == "fixed" { es := sizeBytes(g, t.elem[0]) for i in 0..t.count { for l in leavesOf(g, t.elem[0], base + i * es) { out.push(l) } } } else { out.push(Leaf(offset: base, size: sizeBytes(g, t), isFloat: isFloat(t), isDouble: t.kind == "float")) } return out } // AAPCS64 (ARM64 Linux, Apple): a group of one to four floats or doubles travels in vector registers; // anything else up to 16 bytes in general registers; bigger is copied by the caller and passed by address fn classifyArm64(ptr[Gen] g, ast.Type t, Abi first, int size, int align) -> Abi { a := first ls := leavesOf(g, t, 0) hfa := ls.len > 0 && ls.len <= 4 for l in ls { if !l.isFloat || l.isDouble != ls[0].isDouble { hfa = false } } if hfa { ft := "float" if ls[0].isDouble { ft = "double" } a.kind = "expand" a.parts.push("[" + str(ls.len) + " x " + ft + "]") []str elems for l in ls { elems.push(ft) } a.coerced = "{ " + elems.join(", ") + " }" return a } if size > 16 { a.kind = "indirect" return a } a.kind = "expand" if align >= 16 { a.parts.push("i128") a.coerced = "i128" } else if size <= 8 { a.parts.push("i64") a.coerced = "i" + str(size * 8) } else { a.parts.push("[2 x i64]") a.coerced = "[2 x i64]" } return a } fn classify(ptr[Gen] g, ast.Type t) -> Abi { a := Abi(kind: "direct") if t.kind != "struct" { return a } sd := g.structs[findStruct(g, t.module, t.name)] size := sd.csize a.align = 8 if sd.calign > 8 { a.align = sd.calign } if g.tg.arm64 { return classifyArm64(g, t, a, size, sd.calign) } if g.tg.windows { // Microsoft x64: 1, 2, 4 or 8 bytes travel as one integer (even if they hold floats); // anything else is copied by the caller and passed by address if size == 1 || size == 2 || size == 4 || size == 8 { a.kind = "expand" a.parts.push("i" + str(size * 8)) a.coerced = a.parts[0] } else { a.kind = "indirect" } return a } if size > 16 || size == 0 { a.kind = "memory" return a } ls := leavesOf(g, t, 0) n := (size + 7) / 8 []int cls = [0, 0] // 0 nothing, 1 INTEGER, 2 SSE []bool dbl = [false, false] for l in ls { m := l.size if m > 8 { m = 8 } if l.offset % m != 0 { // a misaligned field (a packed struct): memory a.kind = "memory" return a } k := l.offset / 8 if cls[k] == 1 || !l.isFloat { cls[k] = 1 } else { cls[k] = 2 } if l.isDouble { dbl[k] = true } } a.kind = "expand" for k in 0..n { bytes := size - 8 * k if bytes > 8 { bytes = 8 } if cls[k] == 2 { if dbl[k] { a.parts.push("double") } else if bytes > 4 { a.parts.push("<2 x float>") } else { a.parts.push("float") } } else { a.parts.push("i" + str(bytes * 8)) } } if a.parts.len == 1 { a.coerced = a.parts[0] } else { a.coerced = "{ " + a.parts.join(", ") + " }" } return a } // Decide how every parameter travels, counting the 6 integer and 8 SSE argument registers. fn makePlan(ptr[Gen] g, ast.Type ret, []ast.Type params) -> Plan { pl := Plan() intRegs := 6 sseRegs := 8 pl.ret = classify(g, ret) pl.retLL = "void" if ret.kind != "void" { pl.retLL = cLL(g, ret) } if pl.ret.kind == "indirect" { pl.ret.kind = "memory" // returned through a hidden pointer } if pl.ret.kind == "memory" { pl.sret = true pl.retLL = "void" intRegs -= 1 } else if pl.ret.kind == "expand" { pl.retLL = pl.ret.coerced } for t in params { a := classify(g, t) if a.kind == "expand" { ni := 0 ns := 0 for p in a.parts { if p.starts_with("i") { ni += 1 } else { ns += 1 } } // only System V counts registers here; elsewhere the backend places each part if g.tg.windows || g.tg.arm64 || (ni <= intRegs && ns <= sseRegs) { intRegs -= ni sseRegs -= ns } else { a.kind = "memory" // out of registers: the whole struct goes on the stack } } else if a.kind == "direct" { if isFloat(t) { sseRegs -= 1 } else { intRegs -= 1 } } pl.params.push(a) } return pl } fn checkByValue(ptr[Gen] g, ast.Node at, str fname, ast.Type t, str what) { if t.kind != "struct" { return } sd := g.structs[findStruct(g, t.module, t.name)] if !sd.isC { fail(g, at, "the C function '" + fname + "' " + what + " the Finch struct " + ast.show(t) + ", which C can't take") } if sd.unsupported.len > 0 { fail(g, at, "the C function '" + fname + "' " + what + " the struct " + ast.show(t) + " by value, which Finch can't do yet: it " + sd.unsupported) } } // export fn: what a C program can be given, and call fn cPlain(ast.Type t) -> bool { return isNumber(t) || t.kind == "bool" || t.kind == "char" || t.kind == "ptr" } fn checkExport(ptr[Gen] g, ast.Node f, FnSig sig) { if f.recv.len > 0 { fail(g, f, "methods can't be exported (export a plain fn that calls it)") } if f.fallible { fail(g, f, "an export fn can't fail (C has no errors as values): handle them inside it") } if f.tparams.len > 0 { fail(g, f, "an export fn can't be generic") } for k in 0..sig.params.len { if !cPlain(sig.params[k]) { fail(g, f.kids[k], "an export fn takes numbers, bool, char and pointers only (this is " + ast.show(sig.params[k]) + "; pass text as ptr[char] and make it a str inside)") } } if sig.ret.kind != "void" && !cPlain(sig.ret) { fail(g, f, "an export fn returns nothing, a number, bool, char or a pointer (not " + ast.show(sig.ret) + ")") } for other in g.sigs { if other.exported && other.name == sig.name { fail(g, f, "'" + sig.name + "' is exported twice (from two modules)") } } } // export fn name(...): a C function called `name` that calls the Finch one (a bool is a C byte) fn exportWrapper(ptr[Gen] g, FnSig sig) -> str { []str params []str args s := beginHelper(g) for k in 0..sig.params.len { t := sig.params[k] if t.kind == "bool" { params.push("i8 zeroext %e" + str(k)) args.push("i1 " + op(g, "icmp ne i8 %e" + str(k) + ", 0")) } else { params.push(cLL(g, t) + cExt(g, t) + " %e" + str(k)) args.push(llty(g, t) + " %e" + str(k)) } } text := "call " + llty(g, sig.ret) + " " + sig.ll + "(" + args.join(", ") + ")" savedTrace := traceSave(g) retLL := "void" retExt := "" if sig.ret.kind == "void" { tracedEmit(g, text, savedTrace) emit(g, "ret void") } else if sig.ret.kind == "bool" { retLL = "i8" retExt = " zeroext" emit(g, "ret i8 " + op(g, "zext i1 " + tracedCall(g, text, savedTrace) + " to i8")) } else { retLL = llty(g, sig.ret) retExt = cExt(g, sig.ret) emit(g, "ret " + retLL + " " + tracedCall(g, text, savedTrace)) } dll := "" if g.tg.windows { dll = " dllexport" } endHelper(g, s, "define" + dll + retExt + " " + retLL + " @" + sig.name + "(" + params.join(", ") + ")") return sig.name } // A call from C into Finch (a callback, an export fn) leaves the chain of calls as it found it fn traceSave(ptr[Gen] g) -> str { if !g.trace { return "" } return load(g, "ptr", "@finch_trace") } fn tracedCall(ptr[Gen] g, str text, str saved) -> str { r := op(g, text) if saved.len > 0 { emit(g, "store ptr " + saved + ", ptr @finch_trace") } return r } fn tracedEmit(ptr[Gen] g, str text, str saved) { emit(g, text) if saved.len > 0 { emit(g, "store ptr " + saved + ", ptr @finch_trace") } } // A C-callable door into a Finch function, for addr(fn): callbacks like qsort's or libclang's. fn cThunk(ptr[Gen] g, int fi, ast.Node at) -> str { sig := g.sigs[fi] name := "@finch.c." + modName(sig.module) + "." + sig.name if g.cdone.contains("t:" + name) { return name } g.cdone.push("t:" + name) if sig.recv.len > 0 || sig.fallible { fail(g, at, "addr(" + sig.name + ") gives C a function pointer, so it can't be a method or a function that can fail") } for t in sig.params { if !(isNumber(t) || t.kind == "bool" || t.kind == "char" || t.kind == "ptr" || (t.kind == "struct" && g.structs[findStruct(g, t.module, t.name)].isC)) { fail(g, at, "addr(" + sig.name + ") gives C a function pointer, so its parameters must be C types (numbers, ptr, C structs), not " + ast.show(t)) } } if !(isNumber(sig.ret) || sig.ret.kind == "bool" || sig.ret.kind == "char" || sig.ret.kind == "ptr" || sig.ret.kind == "void" || (sig.ret.kind == "struct" && g.structs[findStruct(g, sig.ret.module, sig.ret.name)].isC)) { fail(g, at, "addr(" + sig.name + "): a function given to C can't return " + ast.show(sig.ret)) } pl := makePlan(g, sig.ret, sig.params) s := beginHelper(g) []str hdr ai := 0 sretName := "" if pl.sret { hdr.push("ptr sret(" + llty(g, sig.ret) + ") align 16 %c0") sretName = "%c0" ai = 1 } []str args for i in 0..sig.params.len { t := sig.params[i] a := pl.params[i] if a.kind == "memory" { hdr.push("ptr byval(" + llty(g, t) + ") align " + str(a.align) + " %c" + str(ai)) args.push(llty(g, t) + " " + load(g, llty(g, t), "%c" + str(ai))) ai += 1 } else if a.kind == "indirect" { hdr.push("ptr %c" + str(ai)) args.push(llty(g, t) + " " + load(g, llty(g, t), "%c" + str(ai))) ai += 1 } else if a.kind == "expand" { slot := alloca(g, "[32 x i8]") for k in 0..a.parts.len { hdr.push(a.parts[k] + " %c" + str(ai)) store(g, a.parts[k], "%c" + str(ai), op(g, "getelementptr inbounds i8, ptr " + slot + ", i64 " + str(8 * k))) ai += 1 } args.push(llty(g, t) + " " + load(g, llty(g, t), slot)) } else { hdr.push(cLL(g, t) + cExt(g, t) + " %c" + str(ai)) args.push(llty(g, t) + " %c" + str(ai)) ai += 1 } } text := "call " + llty(g, sig.ret) + " " + sig.ll + "(" + args.join(", ") + ")" savedTrace := traceSave(g) retExt := "" if sig.ret.kind == "void" { tracedEmit(g, text, savedTrace) emit(g, "ret void") } else if pl.sret { store(g, llty(g, sig.ret), tracedCall(g, text, savedTrace), sretName) emit(g, "ret void") } else if pl.ret.kind == "expand" { slot := alloca(g, "[32 x i8]") store(g, llty(g, sig.ret), tracedCall(g, text, savedTrace), slot) emit(g, "ret " + pl.ret.coerced + " " + load(g, pl.ret.coerced, slot)) } else { retExt = cExt(g, sig.ret) emit(g, "ret " + cLL(g, sig.ret) + " " + tracedCall(g, text, savedTrace)) } endHelper(g, s, "define internal" + retExt + " " + pl.retLL + " " + name + "(" + hdr.join(", ") + ")") return name } // Color(n): a number as an enum. A literal is checked while compiling, anything else when it runs. fn enumFrom(ptr[Gen] g, ast.Node c, int ei) -> Val { ed := g.enums[ei] checkArgs(g, c, 0, 1, ed.name) v := expr(g, c.kids[0]) if !isInt(v.ty) { fail(g, c.kids[0], ed.name + "(...) turns a whole number into the enum, but this is " + ast.show(v.ty)) } t := ast.Type(kind: "enum", name: ed.name, module: ed.module) if v.lit { if !ed.values.contains(int(v.v)) { fail(g, c.kids[0], v.v + " is not the number of any variant of " + ed.name) } return Val(v: v.v, ty: t) } wide := intCast(g, v.v, v.ty, T("int")) known := "" for k in 0..ed.values.len { hit := op(g, "icmp eq i64 " + wide + ", " + str(ed.values[k])) if k == 0 { known = hit } else { known = op(g, "or i1 " + known + ", " + hit) } } panicIf(g, op(g, "xor i1 " + known + ", true"), "this number is not a variant of " + ed.name, c) return Val(v: op(g, "trunc i64 " + wide + " to i32"), ty: t) } // ---------- calling C ---------- // How C extends a number narrower than 32 bits: plain `char` is signed on x86. fn cExt(ptr[Gen] g, ast.Type t) -> str { if t.kind == "bool" || isUnsigned(t) { if bitsOf(t) < 32 { return " zeroext" } } if (isSigned(t) || (t.kind == "char" && platform.charIsSigned(g.tg))) && bitsOf(t) < 32 { return " signext" } if t.kind == "char" && bitsOf(t) < 32 { return " zeroext" } return "" } fn cLL(ptr[Gen] g, ast.Type t) -> str { if t.kind == "str" { return "ptr" } return llty(g, t) } fn callC(ptr[Gen] g, ast.Node c, cimport.CFunc f) -> Val { if g.index { []str ps for p in f.params { ps.push(ast.show(p)) } if f.variadic { ps.push("...") } noteRef(g, c, c.col, f.name.len, "C function from `" + f.header + "`\n```c\n" + ast.show(f.ret) + " " + f.name + "(" + ps.join(", ") + ")\n```", "", 0, 0) } if f.unsupported.len > 0 { fail(g, c, "the C function '" + f.name + "' can't be used from Finch yet: it " + f.unsupported) } have := c.kids.len if f.variadic { if have < f.params.len { fail(g, c, "'" + f.name + "' takes at least " + str(f.params.len) + " argument(s), but got " + str(have)) } } else if have != f.params.len { fail(g, c, "'" + f.name + "' takes " + str(f.params.len) + " argument(s), but got " + str(have)) } ret := resolve(g, f.ret, c) checkByValue(g, c, f.name, ret, "returns") []ast.Type ptypes for p in f.params { pt := resolve(g, p, c) checkByValue(g, c, f.name, pt, "takes") ptypes.push(pt) } pl := makePlan(g, ret, ptypes) []str ptys []str args sretSlot := "" if pl.sret { sretSlot = alloca(g, llty(g, ret)) ptys.push("ptr sret(" + llty(g, ret) + ") align 16") args.push("ptr sret(" + llty(g, ret) + ") align 16 " + sretSlot) } []Val temps mark := g.pending.len for i in 0..c.kids.len { if i < f.params.len { pt := ptypes[i] a := pl.params[i] v := coerce(g, argWant(g, c.kids[i], pt), pt, c.kids[i], "argument " + str(i + 1) + " of " + f.name) temps.push(v) hold(g, v) if a.kind == "memory" { slot := alloca(g, llty(g, pt)) store(g, llty(g, pt), v.v, slot) spec := "ptr byval(" + llty(g, pt) + ") align " + str(a.align) ptys.push(spec) args.push(spec + " " + slot) } else if a.kind == "indirect" { slot := alloca(g, llty(g, pt)) store(g, llty(g, pt), v.v, slot) ptys.push("ptr") args.push("ptr " + slot) } else if a.kind == "expand" { slot := alloca(g, "[32 x i8]") store(g, llty(g, pt), v.v, slot) for k in 0..a.parts.len { x := load(g, a.parts[k], op(g, "getelementptr inbounds i8, ptr " + slot + ", i64 " + str(8 * k))) ptys.push(a.parts[k]) args.push(a.parts[k] + " " + x) } } else { ptys.push(cLL(g, pt) + cExt(g, pt)) if pt.kind == "str" { args.push("ptr " + cstr(g, v.v)) } else { args.push(llty(g, pt) + cExt(g, pt) + " " + v.v) } } continue } // the '...' part: C's default promotions v := expr(g, c.kids[i]) t := v.ty if t.kind == "void" { fail(g, c.kids[i], "this gives back nothing") } temps.push(v) hold(g, v) if t.kind == "struct" || t.kind == "array" { fail(g, c.kids[i], "structs and arrays can't be passed to the '...' part of a C function") } x := v.v if t.kind == "f32" { x = floatCast(g, x, t, T("float")) t = T("float") } else if t.kind == "str" { x = cstr(g, x) t = T("ptr") } else if t.kind == "bool" { x = op(g, "zext i1 " + v.v + " to i32") t = T("i32") } else if (isInt(t) || t.kind == "char") && bitsOf(t) < 32 { x = intCast(g, x, t, T("i32")) t = T("i32") } args.push(llty(g, t) + " " + x) } // declared once (the runtime already declares a few of the libc ones) retLL := pl.retLL retExt := "" if pl.ret.kind == "direct" { retExt = cExt(g, ret) } tail := "" if f.variadic { tail = ", ..." if ptys.len == 0 { tail = "..." } } callee := "@" + f.name sig := "" if f.variadic { sig = " (" + ptys.join(", ") + tail + ")" } if f.inline { // a static inline function has no symbol: the linker builds a C file with void *finch_inl_name = &name; if !g.cdone.contains("i:" + f.name) { g.cdone.push("i:" + f.name) g.head.push("@finch_inl_" + f.name + " = external global ptr") } callee = load(g, "ptr", "@finch_inl_" + f.name) sig = " (" + ptys.join(", ") + tail + ")" } else if !g.cdone.contains("f:" + f.name) { g.cdone.push("f:" + f.name) if !runtimeDeclares(f.name) { g.head.push("declare" + retExt + " " + retLL + " @" + f.name + "(" + ptys.join(", ") + tail + ")") } } text := "call" + retExt + " " + retLL + sig + " " + callee + "(" + args.join(", ") + ")" if ret.kind == "void" { emit(g, text) releaseAll(g, temps, mark) return Val(ty: ret) } if pl.sret { emit(g, text) releaseAll(g, temps, mark) return Val(v: load(g, llty(g, ret), sretSlot), ty: ret) } r := op(g, text) releaseAll(g, temps, mark) if pl.ret.kind == "expand" { slot := alloca(g, "[32 x i8]") store(g, pl.ret.coerced, r, slot) return Val(v: load(g, llty(g, ret), slot), ty: ret) } if ret.kind == "str" { out := strOut(g) store(g, "{ ptr, i64, i64 }", "zeroinitializer", out) emit(g, "call void @finch_str_from_c(ptr " + out + ", ptr " + r + ")") return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: ret) // borrowed: copied if it is kept } return Val(v: r, ty: ret) } // the call is made: what its arguments held is dropped fn releaseAll(ptr[Gen] g, []Val vals, int mark) { for v in vals { release(g, v) } heldEnd(g, mark) } // libc functions the runtime declares itself, with the signatures C has fn runtimeDeclares(str name) -> bool { return ["printf", "fflush", "exit", "free", "qsort"].contains(name) } // Help people who forgot an import. fn headerHint(str name) -> str { names := ["printf", "puts", "putchar", "scanf", "fopen", "getchar", "sqrt", "pow", "sin", "cos", "floor", "fabs", "malloc", "rand", "srand", "abs", "atoi", "strlen", "strcpy", "memcpy", "time", "sleep", "usleep"] headers := ["stdio.h", "stdio.h", "stdio.h", "stdio.h", "stdio.h", "stdio.h", "math.h", "math.h", "math.h", "math.h", "math.h", "math.h", "stdlib.h", "stdlib.h", "stdlib.h", "stdlib.h", "stdlib.h", "string.h", "string.h", "string.h", "time.h", "unistd.h", "unistd.h"] k := names.find(name) if k < 0 { return "" } return " (it's from C: add import \"" + headers[k] + "\" at the top)" } fn call(ptr[Gen] g, ast.Node c) -> Val { n := c.name if g.index && isBuiltin(n) { d := docs.find(docs.builtins(), n) if d.name.len > 0 { noteRef(g, c, c.col, n.len, docs.hover(d), "", 0, 0) } } if n == "error" { fail(g, c, "error(...) makes an error to return from a function that can fail: return error(\"...\")") } if n == "print" { for i in 0..c.kids.len { v := expr(g, c.kids[i]) if v.ty.kind == "void" { fail(g, c.kids[i], "this gives back nothing, so there is nothing to print") } if i > 0 { printf(g, " ", "") } emitPrint(g, v.v, v.ty, false) release(g, v) } printf(g, "\n", "") return Val(ty: T("void")) } if n == "addr" { checkArgs(g, c, 0, 1, n) a := c.kids[0] if a.kind == "var" && lookup(g, a.name) < 0 { fi := findFn(g, g.module, a.name) if fi >= 0 { return Val(v: cThunk(g, fi, a), ty: T("ptr")) // a C function pointer to a Finch function } } if a.kind != "var" && a.kind != "member" && a.kind != "index" { fail(g, a, "addr(...) needs a variable, a field or an element, like addr(x)") } pl := place(g, a, true) return Val(v: pl.addr, ty: ast.ptrTo(pl.ty)) } if n == "str" { if c.kids.len == 2 { // str(x, 2): a float with that many digits after the point checkArgs(g, c, 0, 2, n) x := expr(g, c.kids[0]) if !isNumber(x.ty) { fail(g, c.kids[0], "str(x, digits) is for numbers, but this is " + ast.show(x.ty)) } x = coerce(g, x, T("float"), c.kids[0], "the number") d := coerce(g, expr(g, c.kids[1]), T("int"), c.kids[1], "the number of digits") fixed := strOut(g) emit(g, "call void @finch_str_fixed(ptr " + fixed + ", double " + x.v + ", i64 " + d.v + ")") return Val(v: load(g, "{ ptr, i64, i64 }", fixed), ty: T("str"), fresh: true) } checkArgs(g, c, 0, 1, n) v := expr(g, c.kids[0]) out := strOut(g) k := v.ty.kind borrowed := false // a C string is not copied: whoever keeps it copies it if k == "str" { return Val(v: own(g, v), ty: v.ty, fresh: true) } else if isSigned(v.ty) { emit(g, "call void @finch_str_from_int(ptr " + out + ", i64 " + intCast(g, v.v, v.ty, T("int")) + ")") } else if isUnsigned(v.ty) { emit(g, "call void @finch_str_from_uint(ptr " + out + ", i64 " + intCast(g, v.v, v.ty, T("u64")) + ")") } else if isFloat(v.ty) { emit(g, "call void @finch_str_from_" + floatName(v.ty) + "(ptr " + out + ", double " + floatCast(g, v.v, v.ty, T("float")) + ")") } else if k == "char" { emit(g, "call void @finch_str_from_char(ptr " + out + ", i8 signext " + v.v + ")") } else if k == "bool" { z := op(g, "zext i1 " + v.v + " to i32") emit(g, "call void @finch_str_from_bool(ptr " + out + ", i32 " + z + ")") } else if k == "ptr" { // a C string, copied store(g, "{ ptr, i64, i64 }", "zeroinitializer", out) emit(g, "call void @finch_str_from_c(ptr " + out + ", ptr " + v.v + ")") borrowed = true } else if k == "array" && (v.ty.elem[0].kind == "u8" || v.ty.elem[0].kind == "char") { // bytes become text store(g, "{ ptr, i64, i64 }", "zeroinitializer", out) emit(g, "call void @finch_str_from_bytes(ptr " + out + ", ptr " + spill(g, v) + ")") release(g, v) } else if k == "bool" || k == "char" || k == "array" || k == "map" || k == "struct" || k == "enum" || k == "vec" { store(g, "{ ptr, i64, i64 }", "zeroinitializer", out) emitFormat(g, out, v.v, v.ty, false) release(g, v) } else { fail(g, c.kids[0], "can't turn " + ast.show(v.ty) + " into str") } return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: T("str"), fresh: !borrowed) } to := numType(n) if to.len > 0 { return convert(g, c, T(to)) } vt := ast.vecType(n) if vt.kind.len > 0 { return vecMake(g, c, vt) } if n == "read_file" || n == "file_exists" { checkArgs(g, c, 0, 1, n) pv := strArg(g, c.kids[0], "the file name") path := spill(g, pv) if n == "file_exists" { r := op(g, "call i32 @finch_file_exists(ptr " + path + ")") release(g, pv) return Val(v: op(g, "icmp ne i32 " + r + ", 0"), ty: T("bool")) } out := strOut(g) if g.ftarget == nodeKey(c) { err := alloca(g, "{ ptr, i64, i64 }") store(g, "{ ptr, i64, i64 }", "zeroinitializer", out) store(g, "{ ptr, i64, i64 }", "zeroinitializer", err) ok := op(g, "call i32 @finch_try_read_file(ptr " + out + ", ptr " + path + ", ptr " + err + ")") g.ffailed = op(g, "icmp eq i32 " + ok + ", 0") g.fmsg = load(g, "{ ptr, i64, i64 }", err) release(g, pv) return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: T("str"), fresh: true) } file := strGlobal(g, g.files[c.file]) emit(g, "call void @finch_read_file(ptr " + out + ", ptr " + path + ", ptr " + file + ", i64 " + str(c.line) + ")") release(g, pv) return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: T("str"), fresh: true) } if n == "write_file" { checkArgs(g, c, 0, 2, n) pv := strArg(g, c.kids[0], "the file name") mark := g.pending.len hold(g, pv) tv := strArg(g, c.kids[1], "the text") heldEnd(g, mark) path := spill(g, pv) text := spill(g, tv) if g.ftarget == nodeKey(c) { err := alloca(g, "{ ptr, i64, i64 }") store(g, "{ ptr, i64, i64 }", "zeroinitializer", err) ok := op(g, "call i32 @finch_try_write_file(ptr " + path + ", ptr " + text + ", ptr " + err + ")") g.ffailed = op(g, "icmp eq i32 " + ok + ", 0") g.fmsg = load(g, "{ ptr, i64, i64 }", err) release(g, pv) release(g, tv) return Val(ty: T("void")) } r := op(g, "call i32 @finch_write_file(ptr " + path + ", ptr " + text + ")") release(g, pv) release(g, tv) return Val(v: op(g, "icmp ne i32 " + r + ", 0"), ty: T("bool")) } if n == "delete_file" { checkArgs(g, c, 0, 1, n) p := strArg(g, c.kids[0], "the file name") heap := strOut(g) emit(g, "call void @finch_str_copy(ptr " + heap + ", ptr " + spill(g, p) + ")") if g.ftarget == nodeKey(c) { err := alloca(g, "{ ptr, i64, i64 }") store(g, "{ ptr, i64, i64 }", "zeroinitializer", err) ok := op(g, "call i32 @finch_try_delete_file(ptr " + heap + ", ptr " + err + ")") g.ffailed = op(g, "icmp eq i32 " + ok + ", 0") g.fmsg = load(g, "{ ptr, i64, i64 }", err) dropAt(g, heap, T("str")) release(g, p) return Val(ty: T("void")) } r := op(g, "call i32 @finch_delete_file(ptr " + heap + ")") dropAt(g, heap, T("str")) release(g, p) return Val(v: op(g, "icmp ne i32 " + r + ", 0"), ty: T("bool")) } if n == "shell" { checkArgs(g, c, 0, 1, n) cmd := strArg(g, c.kids[0], "the command") heap := strOut(g) emit(g, "call void @finch_str_copy(ptr " + heap + ", ptr " + spill(g, cmd) + ")") r := op(g, "call i64 @finch_shell(ptr " + heap + ")") dropAt(g, heap, T("str")) release(g, cmd) return Val(v: r, ty: T("int")) } if n == "exit" { checkArgs(g, c, 0, 1, n) code := coerce(g, expr(g, c.kids[0]), T("int"), c.kids[0], "the exit code") t := op(g, "trunc i64 " + code.v + " to i32") f := op(g, "call i32 @fflush(ptr null)") emit(g, "call void @exit(i32 " + t + ")") emit(g, "unreachable") g.term = true // like return: an or-block may end with it return Val(ty: T("void")) } if n == "input" { prompt := strConst(g, "") if c.kids.len > 0 { checkArgs(g, c, 0, 1, n) prompt = strArg(g, c.kids[0], "the question") } out := strOut(g) emit(g, "call void @finch_input(ptr " + out + ", ptr " + spill(g, prompt) + ")") release(g, prompt) return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: T("str"), fresh: true) } if n == "new" { checkArgs(g, c, 0, 1, n) v := expr(g, c.kids[0]) mem := op(g, "call ptr @finch_alloc(i64 " + sizeOf(g, v.ty) + ")") store(g, llty(g, v.ty), own(g, v), mem) return Val(v: mem, ty: ast.ptrTo(v.ty)) } if n == "free" { checkArgs(g, c, 0, 1, n) v := expr(g, c.kids[0]) if v.ty.kind != "ptr" { fail(g, c.kids[0], "free(...) is only for pointers from new(...) or C; arrays, str and structs are freed by Finch at the end of their block") } if v.ty.elem.len > 0 && owning(g, v.ty.elem[0]) { drop := newLabel(g, "free.drop") done := newLabel(g, "free.done") condBr(g, op(g, "icmp eq ptr " + v.v + ", null"), done, drop) label(g, drop) dropAt(g, v.v, v.ty.elem[0]) br(g, done) label(g, done) } emit(g, "call void @free(ptr " + v.v + ")") return Val(ty: T("void")) } fi := findFn(g, g.module, n) if fi >= 0 { if c.ty.elem.len > 0 { fail(g, c, "'" + n + "' is not generic, so it takes no type arguments") } return callFn(g, fi, c, 0, n, "", "") } gti := findTemplate(g, g.module, n, "", false) if gti >= 0 { return genericCall(g, c, gti, 0, n) } if findTemplate(g, g.module, n, "", true) >= 0 { // Stack[int](...): a generic struct made with its type if c.ty.elem.len == 0 { fail(g, c, "'" + n + "' is generic, so say which type it holds: " + n + "[int](...)") } []ast.Type targs for a in c.ty.elem { targs.push(resolve(g, a, c)) } gt := instStruct(g, g.module, n, targs, c) return construct(g, findStruct(g, gt.module, gt.name), c.kids, c.names, c) } si := findStruct(g, g.module, n) if si >= 0 { sd := g.structs[si] if g.index && !sd.isC { node := g.progs[sd.prog].structs[sd.index] noteRef(g, c, c.col, n.len, "```finch\nstruct " + sd.name + " { " + sd.fnames.join(", ") + " }\n```", g.files[node.file], node.line, node.col) } return construct(g, si, c.kids, c.names, c) } ei := findEnum(g, g.module, n) if ei >= 0 { return enumFrom(g, c, ei) } if n == "select" && lookup(g, n) < 0 && findFn(g, g.module, n) < 0 && !g.ci.fns.has(n) { return selectCall(g, c) } if n == "cpu_has" && lookup(g, n) < 0 && findFn(g, g.module, n) < 0 { return cpuHasCall(g, c) } if n == "concat" && lookup(g, n) < 0 && findFn(g, g.module, n) < 0 && !g.ci.fns.has(n) { return concatCall(g, c) } if ["fma", "muladd"].contains(n) { return fmaCall(g, c) // Finch's, also for f32 and vectors (C's fma is the same for one double) } if ["f32_bits", "f32_from_bits", "f64_bits", "f64_from_bits", "f16_to_f32", "f32_to_f16", "bf16_to_f32", "f32_to_bf16"].contains(n) && !g.ci.fns.has(n) { return bitsCall(g, c) } if g.ci.fns.has(n) && !["min", "max", "abs", "clamp"].contains(n) { return callC(g, c, g.ci.fns[n]) // (C's abs only takes an int; Finch's takes any number) } si = findStruct(g, "C", n) if si >= 0 { return construct(g, si, c.kids, c.names, c) } if ["min", "max", "abs", "clamp"].contains(n) { return mathCall(g, c) // only when nothing of yours (or C's abs) has the name } if n == "assert" { return assertCall(g, c) } if ["atomic_add", "atomic_sub", "atomic_load", "atomic_store", "atomic_swap", "atomic_cas"].contains(n) { return atomicCall(g, c) } if lookup(g, n) >= 0 { fail(g, c, "'" + n + "' is a variable, not a function") } fail(g, c, "there is no function named '" + n + "'" + headerHint(n)) return Val() } // min(a, b, ...), max(a, b, ...), abs(x), clamp(x, lo, hi) for any numbers // assert(cond) and assert(cond, "why"): stops the program (in finch test: fails the test) when cond is false fn assertCall(ptr[Gen] g, ast.Node c) -> Val { if c.kids.len != 1 && c.kids.len != 2 { fail(g, c, "assert takes a condition, and maybe a message: assert(x > 0, \"x must be positive\")") } cond := coerce(g, expr(g, c.kids[0]), T("bool"), c.kids[0], "the condition of assert") bad := newLabel(g, "assert.failed") ok := newLabel(g, "assert.ok") condBr(g, cond.v, ok, bad) label(g, bad) file := strGlobal(g, g.files[c.file]) msg := strConst(g, "") if c.kids.len == 2 { msg = strArg(g, c.kids[1], "the message") } emit(g, "call void @finch_assert_failed(ptr " + file + ", i64 " + str(c.line) + ", ptr " + spill(g, msg) + ")") emit(g, "unreachable") g.term = true label(g, ok) return Val(ty: T("void")) } fn traceInit() -> str { return " call void @finch_trace_init(ptr @finch_trace_top, ptr @finch_trace_set)\n" } // finch test: a main that runs every test "..." block of these modules, each on its own fn testMain(ptr[Gen] g, []str modules) { body := "" n := 0 for pr in g.progs { if !modules.contains(pr.module) { continue } for i in 0..pr.tests.len { fi := findFn(g, pr.module, pr.tests[i]) name := g.progs[g.sigs[fi].prog].fns[g.sigs[fi].index].text shown := name if pr.module.len > 0 { shown = pr.module + ": " + name } body = body + " call void @finch_test_run(ptr " + strGlobal(g, shown) + ", ptr " + g.sigs[fi].ll + ")\n" n += 1 } } if n == 0 { print(g.files[0] + ":1:1: error: there are no tests here (write them as test \"what it checks\" { ... })") exit(1) } g.out.push("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n" + traceInit() + body + " %r = call i32 @finch_test_summary()\n ret i32 %r\n}\n") } fn mathCall(ptr[Gen] g, ast.Node c) -> Val { n := c.name if g.index { noteRef(g, c, c.col, n.len, docs.hover(docs.find(docs.builtins(), n)), "", 0, 0) } for nm in c.names { if nm.len > 0 { fail(g, c, "named arguments (" + nm + ": ...) are only for creating structs") } } []Val vals for a in c.kids { v := expr(g, a) if !isNumber(v.ty) && v.ty.kind != "vec" { fail(g, a, n + "(...) is for numbers, but this is " + ast.show(v.ty)) } vals.push(v) } if n == "abs" { checkArgs(g, c, 0, 1, n) x := vals[0] if x.ty.kind == "vec" { return vecAbs(g, x) } if isUnsigned(x.ty) { return x } zero := "0" if isFloat(x.ty) { zero = "0.0" } neg := arith(g, "-", Val(v: zero, ty: x.ty, lit: true), x, c) below := compare(g, "<", x, Val(v: zero, ty: x.ty, lit: true), c) return Val(v: op(g, "select i1 " + below.v + ", " + llty(g, x.ty) + " " + neg.v + ", " + llty(g, x.ty) + " " + x.v), ty: x.ty) } if n == "clamp" { checkArgs(g, c, 0, 3, n) low := pick(g, "<", vals[0], vals[2], c) // min(x, hi) return pick(g, ">", low, vals[1], c) // max(that, lo) } if vals.len < 2 { fail(g, c, "'" + n + "' takes two or more numbers") } o := "<" if n == "max" { o = ">" } out := vals[0] for i in 1..vals.len { out = pick(g, o, out, vals[i], c) } return out } fn atomicAlign(ptr[Gen] g, ast.Type t) -> str { return str(bitsOf(t) / 8) } // atomic_add(p, n) and the rest: one step on a whole number that other threads see all at once. // p is a ptr[int] (or any whole number type) to it; add, sub and swap give back what was there before, // cas(p, old, new) puts new there only if old is there, and says whether it did. fn atomicCall(ptr[Gen] g, ast.Node c) -> Val { n := c.name if g.index { noteRef(g, c, c.col, n.len, docs.hover(docs.find(docs.builtins(), n)), "", 0, 0) } want := 2 if n == "atomic_load" { want = 1 } else if n == "atomic_cas" { want = 3 } checkArgs(g, c, 0, want, n) p := expr(g, c.kids[0]) if p.ty.kind != "ptr" || p.ty.elem.len == 0 || !isInt(p.ty.elem[0]) { fail(g, c.kids[0], n + "(...) works on a whole number through its address, like " + n + "(addr(count), ...) -- this is " + ast.show(p.ty)) } t := p.ty.elem[0] lt := llty(g, t) []str vs for i in 1..c.kids.len { vs.push(coerce(g, exprWant(g, c.kids[i], t), t, c.kids[i], "the value").v) } if n == "atomic_load" { return Val(v: op(g, "load atomic " + lt + ", ptr " + p.v + " seq_cst, align " + atomicAlign(g, t)), ty: t) } if n == "atomic_store" { emit(g, "store atomic " + lt + " " + vs[0] + ", ptr " + p.v + " seq_cst, align " + atomicAlign(g, t)) return Val(ty: T("void")) } if n == "atomic_cas" { r := op(g, "cmpxchg ptr " + p.v + ", " + lt + " " + vs[0] + ", " + lt + " " + vs[1] + " seq_cst seq_cst") return Val(v: op(g, "extractvalue { " + lt + ", i1 } " + r + ", 1"), ty: T("bool")) } how := "add" if n == "atomic_sub" { how = "sub" } else if n == "atomic_swap" { how = "xchg" } return Val(v: op(g, "atomicrmw " + how + " ptr " + p.v + ", " + lt + " " + vs[0] + " seq_cst"), ty: t) } // the smaller (o "<") or bigger (o ">") of two numbers, in the type both fit fn pick(ptr[Gen] g, str o, Val a, Val b, ast.Node at) -> Val { if a.ty.kind == "vec" || b.ty.kind == "vec" { return vecPick(g, o, a, b, at) } l := a r := b if !unify(g, addr(l), addr(r)) { badOperands(g, o, a, b, at) } if l.lit && r.lit && l.ty.kind == "int" { x := int(l.v) or 0 y := int(r.v) or 0 if (o == "<") == (x < y) { return l } return r } first := compare(g, o, l, r, at) t := llty(g, l.ty) return Val(v: op(g, "select i1 " + first.v + ", " + t + " " + l.v + ", " + t + " " + r.v), ty: l.ty) } fn callFn(ptr[Gen] g, int fi, ast.Node c, int first, str shown, str selfAddr, str selfRoot) -> Val { sig := g.sigs[fi] if sig.module.len == 0 && sig.name == "main" { fail(g, c, "main can't be called, it is where the program starts") } noteCall(g, c, sig) checkArgs(g, c, first, sig.params.len, shown) []str args if sig.recv.len > 0 { args.push("ptr " + selfAddr) } []Val fresh mark := g.pending.len for k in 0..sig.params.len { a := c.kids[first + k] v := coerce(g, argWant(g, a, sig.params[k]), sig.params[k], a, "argument '" + sig.pnames[k] + "'") // the argument lives inside the receiver (c.merge(c)): the method gets its own copy if owning(g, sig.params[k]) && !v.fresh && selfRoot.len > 0 && rootVar(a) == selfRoot { v = Val(v: copyValue(g, v.v, v.ty), ty: v.ty, fresh: true) } args.push(llty(g, sig.params[k]) + " " + v.v) // owning values are lent: the function never drops them if v.fresh { fresh.push(v) } hold(g, v) } r := emitCall(g, sig, c, args, shown) for v in fresh { release(g, v) } heldEnd(g, mark) return r } // The call itself, once the arguments are values ("i64 5"); the receiver's address, if any, is already first. fn emitCall(ptr[Gen] g, FnSig sig, ast.Node c, []str args, str shown) -> Val { checkCpuCall(g, sig, c, shown) if sig.fallible && g.ftarget != nodeKey(c) { fail(g, c, "'" + shown + "' can fail, so say what happens then: " + shown + "(...) or , " + shown + "(...) or { ... }, or try " + shown + "(...)") } text := "call " + retTy(g, sig) + " " + sig.ll + "(" + args.join(", ") + ")" if g.frame.len > 0 { // the callee put its frame on the chain; it is gone once the call is back out := Val(ty: sig.ret) r := "" if sig.ret.kind == "void" && !sig.fallible { emit(g, text) } else { r = op(g, text) } emit(g, "store ptr " + g.frame + ", ptr @finch_trace") if sig.fallible { ok := op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 0") g.ffailed = op(g, "xor i1 " + ok + ", true") g.fmsg = op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 2") if sig.ret.kind == "void" { return out } return Val(v: op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 1"), ty: sig.ret, fresh: owning(g, sig.ret)) } if sig.ret.kind == "void" { return out } return Val(v: r, ty: sig.ret, fresh: owning(g, sig.ret)) } if sig.fallible { r := op(g, text) ok := op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 0") g.ffailed = op(g, "xor i1 " + ok + ", true") g.fmsg = op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 2") if sig.ret.kind == "void" { return Val(ty: sig.ret) } return Val(v: op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 1"), ty: sig.ret, fresh: owning(g, sig.ret)) } if sig.ret.kind == "void" { emit(g, text) return Val(ty: sig.ret) } return Val(v: op(g, text), ty: sig.ret, fresh: owning(g, sig.ret)) } fn construct(ptr[Gen] g, int si, []ast.Node args, []str names, ast.Node at) -> Val { sd := g.structs[si] decl := ast.Node() if !sd.isC { decl = g.progs[sd.prog].structs[sd.index] } t := ast.Type(kind: "struct", name: sd.name, module: sd.module) st := llty(g, t) named := names.len > 0 && names[0].len > 0 if sd.isUnion && args.len > 0 { // a union holds one field at a time: Ev(key: k) if args.len != 1 || !named { fail(g, at, sd.name + " is a C union: give one field by name, like " + sd.name + "(" + sd.fnames[0] + ": ...), or none") } k := sd.fnames.find(names[0]) if k < 0 { fail(g, args[0], sd.name + " has no field '" + names[0] + "'") } u := alloca(g, st) store(g, st, "zeroinitializer", u) fv := coerce(g, exprWant(g, args[0], sd.ftypes[k]), sd.ftypes[k], args[0], "the field '" + names[0] + "'") store(g, llty(g, sd.ftypes[k]), fv.v, u) return Val(v: load(g, st, u), ty: t) } []int given for k in 0..sd.fnames.len { given.push(-1) } for i in 0..args.len { if (names[i].len > 0) != named { fail(g, args[i], "give either all fields by name (x: 1, y: 2) or none (1, 2), not a mix") } if !named { if args.len != sd.fnames.len { fail(g, at, sd.name + " has " + str(sd.fnames.len) + " fields, but got " + str(args.len) + " values (or name them: " + sd.name + "(field: value))") } given[i] = i continue } k := sd.fnames.find(names[i]) if k < 0 { fail(g, args[i], sd.name + " has no field '" + names[i] + "'") } if given[k] >= 0 { fail(g, args[i], "the field '" + names[i] + "' is given twice") } given[k] = i } v := "zeroinitializer" mark := g.pending.len for k in 0..sd.fnames.len { ft := sd.ftypes[k] fv := Val() if given[k] >= 0 { fv = coerce(g, exprWant(g, args[given[k]], ft), ft, args[given[k]], "the field '" + sd.fnames[k] + "'") } else if !sd.isC && decl.kids[k].kids.len > 0 { // a default value is evaluated where the struct is defined, without the caller's variables savedScopes := g.scopes savedModule := g.module savedTN := g.tnames savedTA := g.targs g.tnames = sd.tnames g.targs = sd.targs []Scope none g.scopes = none pushScope(g) g.module = sd.module fv = coerce(g, exprWant(g, decl.kids[k].kids[0], ft), ft, decl.kids[k].kids[0], "the default of '" + sd.fnames[k] + "'") g.module = savedModule g.tnames = savedTN g.targs = savedTA g.scopes = savedScopes } else if ft.kind == "struct" && hasDefaults(g, ft) { fv = Val(v: defaultVal(g, ft, at), ty: ft, fresh: owning(g, ft)) } else { continue } fo := own(g, fv) hold(g, Val(v: fo, ty: ft, fresh: true)) v = op(g, "insertvalue " + st + " " + v + ", " + llty(g, ft) + " " + fo + ", " + str(sd.lidx[k])) } heldEnd(g, mark) return Val(v: v, ty: t, fresh: owning(g, t)) } // ---------- methods ---------- // a method of an array, text or map: hover from the built-in docs fn noteMethod(ptr[Gen] g, ast.Node m, []docs.Doc list) { if !g.index { return } d := docs.find(list, m.name) if d.name.len > 0 { noteRef(g, m, m.col + 1, m.name.len, docs.hover(d), "", 0, 0) } } fn method(ptr[Gen] g, ast.Node m) -> Val { objNode := m.kids[0] sm := sumNamed(g, objNode) if sm >= 0 { []ast.Node vals for q in 1..m.kids.len { vals.push(m.kids[q]) } return sumMake(g, sm, m.name, vals, m) } if objNode.kind == "var" && lookup(g, objNode.name) < 0 { mn := objNode.name if !imports(g, mn) { fail(g, objNode, "there is no variable or module named '" + mn + "' (to use a module, add import " + mn + " at the top)") } shown := mn mn = modFor(g, mn) fi := findFn(g, mn, m.name) if fi >= 0 { return callFn(g, fi, m, 1, shown + "." + m.name, "", "") } mti := findTemplate(g, mn, m.name, "", false) if mti >= 0 { return genericCall(g, m, mti, 1, shown + "." + m.name) } mei := findEnum(g, mn, m.name) if mei >= 0 { return enumFrom(g, m, mei) } si := findStruct(g, mn, m.name) if si >= 0 { []ast.Node args for k in 1..m.kids.len { args.push(m.kids[k]) } return construct(g, si, args, m.names, m) } fail(g, m, "the module '" + shown + "' has no function or struct named '" + m.name + "'") } changing := ["push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"] r := ref(g, objNode, changing.contains(m.name)) mark := g.pending.len t := r.val.ty if r.isPlace { t = r.pl.ty } if t.kind == "vec" { v := r.val if r.isPlace { v = Val(v: load(g, llty(g, t), r.pl.addr), ty: t) } noteMethod(g, m, docs.vecMethods()) heldEnd(g, mark) return vecMethod(g, m, v) } if t.kind == "array" { a := "" if r.isPlace { a = r.pl.addr } else { if changing.contains(m.name) { fail(g, m, m.name + "() would change a temporary array; store it in a variable first") } a = spill(g, r.val) if r.val.fresh { holdAddr(g, a, t) } } noteMethod(g, m, docs.arrayMethods()) out := arrayMethod(g, m, a, t) if !r.isPlace && r.val.fresh { dropAt(g, a, t) } heldEnd(g, mark) return out } if t.kind == "str" { sv := r.val if r.isPlace { sv = Val(v: load(g, "{ ptr, i64, i64 }", r.pl.addr), ty: t) } noteMethod(g, m, docs.strMethods()) if !r.isPlace { hold(g, sv) } out := strMethod(g, m, spill(g, sv)) if !r.isPlace { release(g, sv) } heldEnd(g, mark) return out } if t.kind == "map" { mp := "" if r.isPlace { mp = r.pl.addr } else { if changing.contains(m.name) { fail(g, m, m.name + "() would change a temporary map; store it in a variable first") } mp = spill(g, r.val) if r.val.fresh { holdAddr(g, mp, t) } } noteMethod(g, m, docs.mapMethods()) out := mapMethod(g, m, mp, t) if !r.isPlace && r.val.fresh { dropAt(g, mp, t) } heldEnd(g, mark) return out } heldEnd(g, mark) st := t selfAddr := "" temp := false if t.kind == "ptr" && t.elem.len > 0 && t.elem[0].kind == "struct" { pv := r.val.v if r.isPlace { pv = load(g, "ptr", r.pl.addr) } panicIf(g, op(g, "icmp eq ptr " + pv + ", null"), "called ." + m.name + "() on a null pointer", m) st = t.elem[0] selfAddr = pv } else if t.kind == "struct" || t.kind == "enum" { if r.isPlace { selfAddr = r.pl.addr } else { selfAddr = spill(g, r.val) // a temporary struct: Point(1, 2).length() temp = true } } if st.kind == "struct" || st.kind == "enum" { fi := findMethod(g, st.module, st.name, m.name) if fi < 0 && st.kind == "struct" { sdi := findStruct(g, st.module, st.name) if g.structs[sdi].base.len > 0 { mti := findTemplate(g, st.module, m.name, g.structs[sdi].base, false) if mti >= 0 { fi = instFn(g, mti, g.structs[sdi].targs, st, m) } } } if fi < 0 { []str have for sg in g.sigs { if sg.recv == st.name && sg.recvmod == st.module { have.push(sg.name) } } if have.len == 0 { fail(g, m, "the " + st.kind + " " + ast.show(st) + " has no methods; add one with fn " + st.name + "." + m.name + "(...) { ... }") } fail(g, m, "the " + st.kind + " " + ast.show(st) + " has no method '" + m.name + "' (it has: " + have.join(", ") + ")") } // a method that changes self can't be called on a temporary or a read-only value if g.sigs[fi].mutself && (t.kind == "struct" || t.kind == "enum") { if !r.isPlace { fail(g, m, m.name + "() would change a temporary " + ast.show(st) + "; store it in a variable first") } root := rootVar(objNode) if root.len > 0 { id := lookup(g, root) if id >= 0 && varAt(g, id).readonly { fail(g, m, "the loop variable '" + root + "' can't be changed") } } } if temp && r.val.fresh { holdAddr(g, selfAddr, st) } out := callFn(g, fi, m, 1, ast.show(st) + "." + m.name, selfAddr, rootVar(objNode)) if temp && r.val.fresh && owning(g, st) { dropAt(g, selfAddr, st) } heldEnd(g, mark) return out } fail(g, m, ast.show(t) + " has no methods") return Val() } fn need(ptr[Gen] g, ast.Node m, int n) { checkArgs(g, m, 1, n, m.name) } fn arrayMethod(ptr[Gen] g, ast.Node m, str a, ast.Type at) -> Val { et := at.elem[0] ET := llty(g, et) n := m.name lenAddr := op(g, "getelementptr inbounds { ptr, i64, i64 }, ptr " + a + ", i32 0, i32 1") if n == "push" { need(g, m, 1) v := own(g, coerce(g, exprWant(g, m.kids[1], et), et, m.kids[1], "the element")) l := load(g, "i64", lenAddr) l1 := op(g, "add i64 " + l + ", 1") emit(g, "call void @finch_arr_reserve(ptr " + a + ", i64 " + l1 + ", i64 " + sizeOf(g, et) + ")") data := load(g, "ptr", a) store(g, ET, v, op(g, "getelementptr inbounds " + ET + ", ptr " + data + ", i64 " + l)) store(g, "i64", l1, lenAddr) return Val(ty: T("void")) } if n == "pop" { need(g, m, 0) l := load(g, "i64", lenAddr) panicIf(g, op(g, "icmp eq i64 " + l + ", 0"), "pop() on an empty array", m) last := op(g, "sub i64 " + l + ", 1") data := load(g, "ptr", a) v := load(g, ET, op(g, "getelementptr inbounds " + ET + ", ptr " + data + ", i64 " + last)) store(g, "i64", last, lenAddr) return Val(v: v, ty: et, fresh: owning(g, et)) } if n == "clear" { need(g, m, 0) dropFrom(g, a, at, "0") store(g, "i64", "0", lenAddr) return Val(ty: T("void")) } if n == "find" || n == "contains" { need(g, m, 1) x := coerce(g, exprWant(g, m.kids[1], et), et, m.kids[1], "the value to look for") res := alloca(g, "i64") store(g, "i64", "-1", res) l := load(g, "i64", lenAddr) data := load(g, "ptr", a) iv := alloca(g, "i64") store(g, "i64", "0", iv) cond := newLabel(g, "find.cond") body := newLabel(g, "find.body") hit := newLabel(g, "find.hit") nxt := newLabel(g, "find.next") end := newLabel(g, "find.end") br(g, cond) label(g, cond) i := load(g, "i64", iv) c := op(g, "icmp slt i64 " + i + ", " + l) condBr(g, c, body, end) label(g, body) ev := Val(v: load(g, ET, op(g, "getelementptr inbounds " + ET + ", ptr " + data + ", i64 " + i)), ty: et) eq := compare(g, "==", ev, Val(v: x.v, ty: x.ty), m).v condBr(g, eq, hit, nxt) label(g, hit) store(g, "i64", i, res) br(g, end) label(g, nxt) store(g, "i64", op(g, "add i64 " + i + ", 1"), iv) br(g, cond) label(g, end) release(g, x) r := load(g, "i64", res) if n == "find" { return Val(v: r, ty: T("int")) } return Val(v: op(g, "icmp sge i64 " + r + ", 0"), ty: T("bool")) } if n == "join" { need(g, m, 1) if et.kind != "str" { fail(g, m, "join() works on an array of str") } sepv := strArg(g, m.kids[1], "the separator") sep := spill(g, sepv) out := strOut(g) emit(g, "call void @finch_str_join(ptr " + out + ", ptr " + a + ", ptr " + sep + ")") release(g, sepv) return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: T("str"), fresh: true) } if n == "insert" { need(g, m, 2) i := coerce(g, expr(g, m.kids[1]), T("int"), m.kids[1], "the position").v v := own(g, coerce(g, exprWant(g, m.kids[2], et), et, m.kids[2], "the element")) file := strGlobal(g, g.files[m.file]) emit(g, "call void @finch_arr_insert_gap(ptr " + a + ", i64 " + i + ", i64 " + sizeOf(g, et) + ", ptr " + file + ", i64 " + str(m.line) + ")") data := load(g, "ptr", a) store(g, ET, v, op(g, "getelementptr inbounds " + ET + ", ptr " + data + ", i64 " + i)) return Val(ty: T("void")) } if n == "remove" { need(g, m, 1) i := coerce(g, expr(g, m.kids[1]), T("int"), m.kids[1], "the position").v boundsCheck(g, i, load(g, "i64", lenAddr), m) data := load(g, "ptr", a) v := load(g, ET, op(g, "getelementptr inbounds " + ET + ", ptr " + data + ", i64 " + i)) emit(g, "call void @finch_arr_remove_gap(ptr " + a + ", i64 " + i + ", i64 " + sizeOf(g, et) + ")") return Val(v: v, ty: et, fresh: owning(g, et)) } if n == "resize" { need(g, m, 1) k := coerce(g, expr(g, m.kids[1]), T("int"), m.kids[1], "the new length").v dropFrom(g, a, at, k) emit(g, "call void @finch_arr_resize(ptr " + a + ", i64 " + k + ", i64 " + sizeOf(g, et) + ")") return Val(ty: T("void")) } if n == "reverse" { need(g, m, 0) l := load(g, "i64", lenAddr) data := load(g, "ptr", a) half := op(g, "sdiv i64 " + l + ", 2") lp := loopStart(g, half) x := op(g, "getelementptr inbounds " + ET + ", ptr " + data + ", i64 " + lp[3]) j := op(g, "sub i64 " + op(g, "sub i64 " + l + ", 1") + ", " + lp[3]) y := op(g, "getelementptr inbounds " + ET + ", ptr " + data + ", i64 " + j) xv := load(g, ET, x) yv := load(g, ET, y) store(g, ET, yv, x) store(g, ET, xv, y) loopEnd(g, lp) return Val(ty: T("void")) } if n == "slice" { need(g, m, 2) st := coerce(g, expr(g, m.kids[1]), T("int"), m.kids[1], "the start").v en := coerce(g, expr(g, m.kids[2]), T("int"), m.kids[2], "the end").v l := load(g, "i64", lenAddr) bad1 := op(g, "icmp slt i64 " + st + ", 0") bad2 := op(g, "icmp sgt i64 " + en + ", " + l) bad3 := op(g, "icmp sgt i64 " + st + ", " + en) panicIf(g, op(g, "or i1 " + op(g, "or i1 " + bad1 + ", " + bad2) + ", " + bad3), "slice(start, end) is out of range", m) cnt := op(g, "sub i64 " + en + ", " + st) out := alloca(g, "{ ptr, i64, i64 }") emit(g, "call void @finch_arr_make(ptr " + out + ", i64 " + cnt + ", i64 " + sizeOf(g, et) + ")") src := load(g, "ptr", a) dst := load(g, "ptr", out) lp := loopStart(g, cnt) dp := op(g, "getelementptr inbounds " + ET + ", ptr " + dst + ", i64 " + lp[3]) sp := op(g, "getelementptr inbounds " + ET + ", ptr " + src + ", i64 " + op(g, "add i64 " + st + ", " + lp[3])) copyAt(g, dp, sp, et) loopEnd(g, lp) return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: at, fresh: true) } if n == "sort" { need(g, m, 0) k := et.kind if !isNumber(et) && k != "char" && k != "str" { fail(g, m, "sort() works on arrays of numbers, chars or str") } cmp := cmpFn(g, et) l := load(g, "i64", lenAddr) data := load(g, "ptr", a) emit(g, "call void @qsort(ptr " + data + ", i64 " + l + ", i64 " + sizeOf(g, et) + ", ptr " + cmp + ")") return Val(ty: T("void")) } fail(g, m, "an array has no method '" + n + "' (it has: push, pop, insert, remove, clear, resize, find, contains, join, slice, reverse, sort)") return Val() } // resize/clear: the elements from `from` on go away, so they are dropped fn dropFrom(ptr[Gen] g, str a, ast.Type at, str from) { et := at.elem[0] if !owning(g, et) { return } ET := llty(g, et) l := load(g, "i64", op(g, "getelementptr inbounds { ptr, i64, i64 }, ptr " + a + ", i32 0, i32 1")) d := load(g, "ptr", a) under := op(g, "icmp slt i64 " + from + ", " + l) neg := op(g, "icmp slt i64 " + from + ", 0") start := op(g, "select i1 " + under + ", i64 " + op(g, "select i1 " + neg + ", i64 0, i64 " + from) + ", i64 " + l) lp := loopStart(g, op(g, "sub i64 " + l + ", " + start)) ep := op(g, "getelementptr inbounds " + ET + ", ptr " + d + ", i64 " + op(g, "add i64 " + start + ", " + lp[3])) dropAt(g, ep, et) loopEnd(g, lp) } // -1, 0 or 1, for qsort fn cmpFn(ptr[Gen] g, ast.Type t) -> str { name := "@finch.cmp." + typeKey(t) if has(g.done, name) { return name } g.done.push(name) s := beginHelper(g) r := "" if t.kind == "str" { c := op(g, "call i64 @finch_str_cmp(ptr %x, ptr %y)") r = op(g, "trunc i64 " + c + " to i32") } else { ty := llty(g, t) x := load(g, ty, "%x") y := load(g, ty, "%y") lt := "" gt := "" if isFloat(t) { lt = op(g, "fcmp olt " + ty + " " + x + ", " + y) gt = op(g, "fcmp ogt " + ty + " " + x + ", " + y) } else { lt = op(g, "icmp " + intPred("<", isSigned(t)) + " " + ty + " " + x + ", " + y) gt = op(g, "icmp " + intPred(">", isSigned(t)) + " " + ty + " " + x + ", " + y) } r = op(g, "sub i32 " + op(g, "zext i1 " + gt + " to i32") + ", " + op(g, "zext i1 " + lt + " to i32")) } emit(g, "ret i32 " + r) endHelper(g, s, "define internal i32 " + name + "(ptr %x, ptr %y)") return name } fn strMethod(ptr[Gen] g, ast.Node m, str s) -> Val { n := m.name out := strOut(g) file := strGlobal(g, g.files[m.file]) if n == "sub" { need(g, m, 2) a := coerce(g, expr(g, m.kids[1]), T("int"), m.kids[1], "the start").v b := coerce(g, expr(g, m.kids[2]), T("int"), m.kids[2], "the end").v emit(g, "call void @finch_str_sub(ptr " + out + ", ptr " + s + ", i64 " + a + ", i64 " + b + ", ptr " + file + ", i64 " + str(m.line) + ")") return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: T("str"), fresh: true) } if n == "find" || n == "contains" || n == "starts_with" || n == "ends_with" { need(g, m, 1) tv := strArg(g, m.kids[1], "the text to look for") t := spill(g, tv) if n == "find" { res := op(g, "call i64 @finch_str_find(ptr " + s + ", ptr " + t + ")") release(g, tv) return Val(v: res, ty: T("int")) } if n == "contains" { r := op(g, "call i64 @finch_str_find(ptr " + s + ", ptr " + t + ")") res := op(g, "icmp sge i64 " + r + ", 0") release(g, tv) return Val(v: res, ty: T("bool")) } fname := "@finch_str_starts" if n == "ends_with" { fname = "@finch_str_ends" } r := op(g, "call i32 " + fname + "(ptr " + s + ", ptr " + t + ")") res := op(g, "icmp ne i32 " + r + ", 0") release(g, tv) return Val(v: res, ty: T("bool")) } if n == "trim" || n == "upper" || n == "lower" { need(g, m, 0) emit(g, "call void @finch_str_" + n + "(ptr " + out + ", ptr " + s + ")") return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: T("str"), fresh: true) } if n == "replace" { need(g, m, 2) av := strArg(g, m.kids[1], "the text to replace") mark := g.pending.len hold(g, av) bv := strArg(g, m.kids[2], "the new text") heldEnd(g, mark) a := spill(g, av) b := spill(g, bv) emit(g, "call void @finch_str_replace(ptr " + out + ", ptr " + s + ", ptr " + a + ", ptr " + b + ")") release(g, av) release(g, bv) return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: T("str"), fresh: true) } if n == "split" { need(g, m, 1) sepv := strArg(g, m.kids[1], "the separator") sep := spill(g, sepv) ao := strOut(g) emit(g, "call void @finch_str_split(ptr " + ao + ", ptr " + s + ", ptr " + sep + ")") release(g, sepv) return Val(v: load(g, "{ ptr, i64, i64 }", ao), ty: ast.arrayOf(T("str")), fresh: true) } if n == "repeat" { need(g, m, 1) k := coerce(g, expr(g, m.kids[1]), T("int"), m.kids[1], "the count").v emit(g, "call void @finch_str_repeat(ptr " + out + ", ptr " + s + ", i64 " + k + ")") return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: T("str"), fresh: true) } if n == "bytes" { need(g, m, 0) ao := strOut(g) emit(g, "call void @finch_bytes_from_str(ptr " + ao + ", ptr " + s + ")") return Val(v: load(g, "{ ptr, i64, i64 }", ao), ty: ast.arrayOf(T("u8")), fresh: true) } fail(g, m, "a str has no method '" + n + "' (it has: sub, find, contains, starts_with, ends_with, split, trim, upper, lower, replace, repeat, bytes)") return Val() } // ---------- the whole program ---------- fn runtimeDecls() -> str { decls := [ "declare void @finch_panic(ptr, i64, ptr) noreturn cold", "declare void @finch_panic_index(ptr, i64, i64, i64) noreturn cold", "declare ptr @finch_alloc(i64)", "declare void @finch_str_copy(ptr, ptr)", "declare void @finch_str_drop(ptr)", "declare void @finch_arr_free(ptr)", "declare void @finch_map_free(ptr)", "declare void @finch_str_own(ptr)", "declare void @finch_str_concat(ptr, ptr, ptr)", "declare i32 @finch_str_eq(ptr, ptr)", "declare i64 @finch_str_cmp(ptr, ptr)", "declare void @finch_buf_add(ptr, ptr, i64)", "declare void @finch_buf_int(ptr, i64)", "declare void @finch_buf_uint(ptr, i64)", "declare void @finch_buf_float(ptr, double)", "declare void @finch_buf_bool(ptr, i32)", "declare void @finch_buf_ptr(ptr, ptr)", "declare void @finch_buf_char(ptr, i8, i32)", "declare void @finch_buf_str(ptr, ptr, i32)", "declare void @finch_buf_print(ptr)", "declare void @finch_str_from_int(ptr, i64)", "declare void @finch_str_from_uint(ptr, i64)", "declare void @finch_str_from_float(ptr, double)", "declare void @finch_str_from_f32(ptr, double)", "declare void @finch_str_fixed(ptr, double, i64)", "declare void @finch_print_float(double, i32)", "declare void @finch_buf_f32(ptr, double)", "declare void @finch_str_from_char(ptr, i8 signext)", "declare void @finch_str_from_bool(ptr, i32)", "declare i64 @finch_str_to_int(ptr, ptr, i64)", "declare double @finch_str_to_float(ptr, ptr, i64)", "declare void @finch_str_sub(ptr, ptr, i64, i64, ptr, i64)", "declare i64 @finch_str_find(ptr, ptr)", "declare i32 @finch_str_starts(ptr, ptr)", "declare i32 @finch_str_ends(ptr, ptr)", "declare void @finch_str_trim(ptr, ptr)", "declare void @finch_str_upper(ptr, ptr)", "declare void @finch_str_lower(ptr, ptr)", "declare void @finch_str_replace(ptr, ptr, ptr, ptr)", "declare void @finch_str_split(ptr, ptr, ptr)", "declare void @finch_str_join(ptr, ptr, ptr)", "declare void @finch_str_repeat(ptr, ptr, i64)", "declare void @finch_bytes_from_str(ptr, ptr)", "declare i64 @finch_map_find(ptr, ptr, i64, i64, i32)", "declare i64 @finch_map_slot(ptr, ptr, i64, i64, i32, ptr)", "declare void @finch_map_remove_at(ptr, i64, i64)", "declare void @finch_map_clear(ptr)", "declare void @finch_map_clone_raw(ptr, ptr, i64)", "declare void @finch_map_missing(ptr, i64, ptr) noreturn cold", "declare void @finch_map_changed(ptr, i64) noreturn cold", "declare void @finch_arr_reserve(ptr, i64, i64)", "declare void @finch_arr_clone_raw(ptr, ptr, i64)", "declare void @finch_arr_make(ptr, i64, i64)", "declare void @finch_arr_resize(ptr, i64, i64)", "declare void @finch_arr_insert_gap(ptr, i64, i64, ptr, i64)", "declare void @finch_arr_remove_gap(ptr, i64, i64)", "declare void @qsort(ptr, i64, i64, ptr)", "declare void @finch_args(ptr, i32, ptr)", "declare void @finch_args_free(ptr)", "declare void @finch_assert_failed(ptr, i64, ptr)", "declare void @finch_test_run(ptr, ptr)", "declare void @finch_trace_init(ptr, ptr)", "declare i32 @finch_test_summary()", "declare void @finch_input(ptr, ptr)", "declare i32 @finch_file_exists(ptr)", "declare i32 @finch_cpu_has(ptr)", "declare i32 @finch_str_try_int(ptr, ptr, ptr)", "declare i32 @finch_str_try_float(ptr, ptr, ptr)", "declare i32 @finch_try_read_file(ptr, ptr, ptr)", "declare i32 @finch_try_write_file(ptr, ptr, ptr)", "declare i32 @finch_try_delete_file(ptr, ptr)", "declare void @finch_report_error(ptr)", "declare void @finch_read_file(ptr, ptr, ptr, i64)", "declare i32 @finch_write_file(ptr, ptr)", "declare i64 @finch_shell(ptr)", "declare i32 @finch_delete_file(ptr)", "declare i32 @printf(ptr, ...)", "declare i32 @fflush(ptr)", "declare void @finch_str_from_c(ptr, ptr)", "declare void @finch_on_fatal(ptr)", "declare ptr @finch_std_stream(i32)", "declare void @finch_str_from_bytes(ptr, ptr)", "declare void @exit(i32) noreturn", "declare void @free(ptr)", ] return decls.join("\n") + "\n" } fn assemble(ptr[Gen] g) -> str { text := "; generated by the Finch compiler written in Finch\n\n" if g.tg.cross || g.tg.windows { text = text + "target triple = \"" + g.tg.triple + "\"\n\n" } text = text + g.head.join("\n") + "\n\n" + runtimeDecls() + "\n" // the chain of calls a runtime error shows; the runtime reads it through these two tls := "thread_local(initialexec)" if g.shared || g.tg.windows { tls = "thread_local" } text = text + "@finch_trace = internal " + tls + " global ptr null\n" text = text + "define hidden ptr @finch_trace_top() {\nentry:\n %t = load ptr, ptr @finch_trace\n ret ptr %t\n}\n" text = text + "define hidden void @finch_trace_set(ptr %t) {\nentry:\n store ptr %t, ptr @finch_trace\n ret void\n}\n\n" if g.debug { text = text + "declare void @llvm.dbg.declare(metadata, metadata, metadata)\n\n" } text = text + g.out.join("\n") if g.debug { text = text + "\n" + dbgModule(g) } return text } fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt) -> str { shared := opt.shared gen := Gen(progs: progs, files: files, ci: ci, shared: opt.shared, debug: opt.debug, index: opt.index, cwd: opt.cwd, tg: opt.target, dbgScope: -1, features: opt.features) g := addr(gen) g.trace = opt.trace && !opt.shared if g.debug { // !0 the compile unit, !1 and !2 the module flags, then the source files g.md.push("") g.md.push("!{i32 7, !\"Dwarf Version\", i32 4}") g.md.push("!{i32 2, !\"Debug Info Version\", i32 3}") for fname in files { g.mdfiles.push(md(g, "!DIFile(filename: \"" + fname + "\", directory: \"" + g.cwd + "\")")) } g.md[0] = "distinct !DICompileUnit(language: DW_LANG_C, file: !" + str(g.mdfiles[0]) + ", producer: \"finch\", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)" } declare(g) main := findFn(g, "", "main") if opt.test { di := 0 while di < g.sigs.len { define(g, di) di += 1 } testMain(g, opt.testModules) return assemble(g) } if shared { if main >= 0 { fail(g, g.progs[g.sigs[main].prog].fns[g.sigs[main].index], "a library (--shared) has no main: the program that loads it calls its 'export fn's") } } else if main < 0 { print(files[0] + ":1:1: error: there is no main function (add 'fn main() { ... }')") exit(1) } // instantiating a generic adds functions while this goes on, so the end is not fixed di := 0 while di < g.sigs.len { define(g, di) di += 1 } if g.index { mainImports := g.progs[0].imports indexAll(g, mainImports) return g.idx.join("\n") } for sg in g.sigs { if sg.exported { exportWrapper(g, sg) } } if shared { // finch_module_on_fatal(handler): the host's say on runtime errors (see runtime/finch_rt.c) dll := "" if g.tg.windows { dll = " dllexport" } g.out.push("define" + dll + " void @finch_module_on_fatal(ptr %handler) {\nentry:\n call void @finch_on_fatal(ptr %handler)\n ret void\n}\n") return assemble(g) } // the C entry point calls the Finch main m := g.sigs[main] callText := "call " + retTy(g, m) + " " + m.ll + "(" pre := "" if m.params.len == 1 { pre = " %args = alloca { ptr, i64, i64 }\n call void @finch_args(ptr %args, i32 %argc, ptr %argv)\n %a = load { ptr, i64, i64 }, ptr %args\n" callText = callText + "{ ptr, i64, i64 } %a" } callText = callText + ")" ret := " ret i32 0\n" after := "" if m.params.len == 1 { after = " call void @finch_args_free(ptr %args)\n" } if m.fallible { // fn main() -> !: a failure is printed, and the exit code is 1 callText = "%r = " + callText pre = pre + " " + callText + "\n" callText = "" rt := retTy(g, m) pre = pre + " %ok = extractvalue " + rt + " %r, 0\n br i1 %ok, label %main.ok, label %main.bad\nmain.bad:\n" pre = pre + " %m = extractvalue " + rt + " %r, 2\n %mp = alloca { ptr, i64, i64 }\n store { ptr, i64, i64 } %m, ptr %mp\n" pre = pre + " call void @finch_report_error(ptr %mp)\n" + after + " ret i32 1\nmain.ok:\n" if m.ret.kind == "int" { ret = " %v = extractvalue " + rt + " %r, 1\n %code = trunc i64 %v to i32\n ret i32 %code\n" } } else if m.ret.kind == "int" { callText = "%r = " + callText ret = " %code = trunc i64 %r to i32\n ret i32 %code\n" } g.out.push("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n" + traceInit() + pre + " " + callText + "\n" + after + ret + "}\n") return assemble(g) } // ---------- vectors: f32x8, i8x32, f64x4, ... (SIMD) ---------- // // A vector is N numbers of one type side by side: <8 x float> in LLVM. + - * / work on every lane at once, and // LLVM picks the instructions: SSE, AVX2 or AVX-512 on x86-64, NEON on ARM64 (with --cpu native, the ones of the // machine that builds it). A plain number next to a vector counts for every lane: v * 2.0. fn laneBytes(ast.Type vt) -> str { return str(bitsOf(vt.elem[0]) / 8) } // v8f32, v32i8 (or f32, i64 for a plain number): how LLVM's intrinsics are named for this type fn vecSuffix(ast.Type vt) -> str { if vt.kind != "vec" { if isFloat(vt) { return "f" + str(bitsOf(vt)) } return "i" + str(bitsOf(vt)) } if isFloat(vt.elem[0]) { return "v" + str(vt.count) + "f" + str(bitsOf(vt.elem[0])) } return "v" + str(vt.count) + "i" + str(bitsOf(vt.elem[0])) } fn declareOnce(ptr[Gen] g, str decl) { if !has(g.done, decl) { g.done.push(decl) g.head.push(decl) } } // call @llvm..(args...), declared the first time fn vecIntrinsic(ptr[Gen] g, str ret, str name, ast.Type vt, []str argTypes, []str args, str flags) -> str { fname := "@llvm." + name + "." + vecSuffix(vt) declareOnce(g, "declare " + ret + " " + fname + "(" + argTypes.join(", ") + ")") []str both for i in 0..args.len { both.push(argTypes[i] + " " + args[i]) } return op(g, "call " + flags + ret + " " + fname + "(" + both.join(", ") + ")") } // the same number in every lane fn splat(ptr[Gen] g, str v, ast.Type vt) -> str { lt := llty(g, vt) one := op(g, "insertelement " + lt + " poison, " + llty(g, vt.elem[0]) + " " + v + ", i64 0") return op(g, "shufflevector " + lt + " " + one + ", " + lt + " poison, <" + str(vt.count) + " x i32> zeroinitializer") } // a value next to a vector: the vector itself, or a number put in every lane fn asVec(ptr[Gen] g, Val v, ast.Type vt, str o, ast.Node at) -> Val { if v.ty.kind == "vec" { if !ast.same(v.ty, vt) { fail(g, at, "can't use '" + o + "' on " + ast.show(vt) + " and " + ast.show(v.ty) + " (convert one: " + ast.show(vt) + "(...))") } return v } s := coerce(g, v, vt.elem[0], at, "a number used with " + ast.show(vt)) return Val(v: splat(g, s.v, vt), ty: vt) } fn vecOf(Val l, Val r) -> ast.Type { if l.ty.kind == "vec" { return l.ty } return r.ty } // i, i+1, ..., i+n-1 are all in a list of `len` fn vecBounds(ptr[Gen] g, str i, int n, str len, ast.Node at) { end := op(g, "add i64 " + i + ", " + str(n)) before := op(g, "icmp slt i64 " + i + ", 0") after := op(g, "icmp sgt i64 " + end + ", " + len) panicIf(g, op(g, "or i1 " + before + ", " + after), "the " + str(n) + " numbers of the vector don't all fit in the list there", at) } // f32x8(x), f32x8(a, b, ..., h), f32x8(list, i), f32x8(i32x8 v) fn vecMake(ptr[Gen] g, ast.Node c, ast.Type vt) -> Val { if g.index { noteRef(g, c, c.col, c.name.len, docs.hover(docs.find(docs.builtins(), "f32x8")), "", 0, 0) } for nm in c.names { if nm.len > 0 { fail(g, c, "named arguments (" + nm + ": ...) are only for creating structs") } } shown := ast.show(vt) lane := vt.elem[0] lt := llty(g, vt) n := vt.count usage := shown + "(x) puts x in every lane, " + shown + "(a, b, ...) takes " + str(n) + " numbers, " + shown + "(list, i) loads them from a list" if c.kids.len == 0 { fail(g, c, usage) } first := ref(g, c.kids[0], false) ft := first.val.ty if first.isPlace { ft = first.pl.ty } if ft.kind == "array" || ft.kind == "ptr" { if lane.kind == "bool" { fail(g, c, "a mask isn't loaded from a list; compare instead, like f32x8(list, i) > 0.0") } if c.kids.len != 2 && c.kids.len != 3 { fail(g, c, shown + "(list, i): which index to start at? (" + shown + "(list, 0) for the first " + str(n) + ")") } if c.kids.len == 3 && ft.kind == "ptr" { fail(g, c, shown + "(list, i, fill) needs a list: a ptr doesn't know where its numbers end") } if ft.elem.len == 0 || !ast.same(ft.elem[0], lane) { fail(g, c.kids[0], shown + "(list, i) loads " + ast.show(lane) + " numbers, but this is " + ast.show(ft)) } mark := g.pending.len if !first.isPlace { hold(g, first.val) } i := coerce(g, expr(g, c.kids[1]), T("int"), c.kids[1], "an index").v fill := "" if c.kids.len == 3 { fill = coerce(g, expr(g, c.kids[2]), lane, c.kids[2], "the number for the lanes past the end").v } heldEnd(g, mark) data := "" if ft.kind == "ptr" { data = first.val.v if first.isPlace { data = load(g, "ptr", first.pl.addr) } } else { av := first.val.v if first.isPlace { av = load(g, "{ ptr, i64, i64 }", first.pl.addr) } len := op(g, "extractvalue { ptr, i64, i64 } " + av + ", 1") data = op(g, "extractvalue { ptr, i64, i64 } " + av + ", 0") if fill.len > 0 { // the end of a list: the lanes that are in it are loaded, the others get `fill` m := tailMask(g, i, n, len, c) ep := op(g, "getelementptr " + llty(g, lane) + ", ptr " + data + ", i64 " + i) ml := llty(g, shaped(vt, "bool")) fname := "@llvm.masked.load." + vecSuffix(vt) + ".p0" declareOnce(g, "declare " + lt + " " + fname + "(ptr, i32, " + ml + ", " + lt + ")") out := op(g, "call " + lt + " " + fname + "(ptr " + ep + ", i32 " + laneBytes(vt) + ", " + ml + " " + m + ", " + lt + " " + splat(g, fill, vt) + ")") if !first.isPlace { release(g, first.val) } return Val(v: out, ty: vt) } vecBounds(g, i, n, len, c) } ep := op(g, "getelementptr inbounds " + llty(g, lane) + ", ptr " + data + ", i64 " + i) out := op(g, "load " + lt + ", ptr " + ep + ", align " + laneBytes(vt)) if !first.isPlace { release(g, first.val) } return Val(v: out, ty: vt) } v := first.val if first.isPlace { v = Val(v: load(g, llty(g, ft), first.pl.addr), ty: ft) } if c.kids.len == 1 { if ft.kind == "vec" { return vecConvert(g, v, vt, c.kids[0]) } if !isNumber(ft) { fail(g, c.kids[0], usage + "; this is " + ast.show(ft)) } s := coerce(g, v, lane, c.kids[0], "the number for every lane") return Val(v: splat(g, s.v, vt), ty: vt) } if c.kids.len != n { fail(g, c, usage + "; this has " + str(c.kids.len) + " values") } cur := "poison" for k in 0..n { x := v if k > 0 { x = expr(g, c.kids[k]) } s := coerce(g, x, lane, c.kids[k], "lane " + str(k)) cur = op(g, "insertelement " + lt + " " + cur + ", " + llty(g, lane) + " " + s.v + ", i64 " + str(k)) } return Val(v: cur, ty: vt) } // every lane converted, as the same conversion of one number would: f32x8(ints), i32x8(floats) cuts fractions fn vecConvert(ptr[Gen] g, Val v, ast.Type to, ast.Node at) -> Val { from := v.ty if ast.same(from, to) { return v } if from.count != to.count { fail(g, at, "can't turn " + ast.show(from) + " into " + ast.show(to) + ": they have a different number of lanes") } fl := from.elem[0] tl := to.elem[0] if tl.kind == "bool" { // a lane that isn't 0 is true if isFloat(fl) { return Val(v: op(g, "fcmp une " + llty(g, from) + " " + v.v + ", zeroinitializer"), ty: to) } return Val(v: op(g, "icmp ne " + llty(g, from) + " " + v.v + ", zeroinitializer"), ty: to) } if fl.kind == "bool" { // true is 1 if isFloat(tl) { return Val(v: op(g, "uitofp " + llty(g, from) + " " + v.v + " to " + llty(g, to)), ty: to) } return Val(v: op(g, "zext " + llty(g, from) + " " + v.v + " to " + llty(g, to)), ty: to) } inst := "" if isInt(fl) && isInt(tl) { if bitsOf(fl) == bitsOf(tl) { return Val(v: v.v, ty: to) // i32x8 <-> u32x8: the same bits } if bitsOf(tl) < bitsOf(fl) { inst = "trunc" } else if isSigned(fl) { inst = "sext" } else { inst = "zext" } } else if isInt(fl) { inst = "uitofp" if isSigned(fl) { inst = "sitofp" } } else if isInt(tl) { inst = "fptoui" if isSigned(tl) { inst = "fptosi" } } else if bitsOf(tl) > bitsOf(fl) { inst = "fpext" } else { inst = "fptrunc" } return Val(v: op(g, inst + " " + llty(g, from) + " " + v.v + " to " + llty(g, to)), ty: to) } fn vecUnary(ptr[Gen] g, ast.Node e, Val v) -> Val { lane := v.ty.elem[0] lt := llty(g, v.ty) if lane.kind == "bool" { if e.name != "!" && e.name != "~" { fail(g, e, "can't put '" + e.name + "' in front of a mask (" + ast.show(v.ty) + "); '!' flips it") } return Val(v: op(g, "xor " + lt + " " + v.v + ", " + splat(g, "true", v.ty)), ty: v.ty) } if e.name == "~" { if !isInt(lane) { fail(g, e, "'~' flips the bits of whole numbers, not of " + ast.show(v.ty)) } return Val(v: op(g, "xor " + lt + " " + v.v + ", " + splat(g, "-1", v.ty)), ty: v.ty) } if isUnsigned(lane) { fail(g, e, ast.show(v.ty) + " can't be negative (its lanes are unsigned)") } if isFloat(lane) { return Val(v: op(g, "fneg " + lt + " " + v.v), ty: v.ty) } return Val(v: op(g, "sub " + lt + " zeroinitializer, " + v.v), ty: v.ty) } fn vecArith(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { vt := vecOf(l, r) a := asVec(g, l, vt, o, at) b := asVec(g, r, vt, o, at) lane := vt.elem[0] if lane.kind == "bool" && o != "&" && o != "|" && o != "^" { fail(g, at, "can't use '" + o + "' on a mask (" + ast.show(vt) + "); masks take & | ^ and !") } names := ["+", "-", "*", "/", "&", "|", "^", "<<", ">>"] insts := ["add", "sub", "mul", "", "and", "or", "xor", "shl", "ashr"] if isUnsigned(lane) { insts[8] = "lshr" } if isFloat(lane) { names = ["+", "-", "*", "/"] insts = ["fadd", "fsub", "fmul", "fdiv"] } k := names.find(o) if k < 0 || insts[k].len == 0 { msg := "can't use '" + o + "' on " + ast.show(vt) if o == "/" || o == "%" { msg = msg + " (x86 and ARM have no instruction that divides whole numbers lane by lane; convert to floats: f32x" + str(vt.count) + "(v))" } else if isFloat(lane) { msg = msg + " (bit operators work on whole numbers only)" } fail(g, at, msg) } return Val(v: op(g, insts[k] + " " + llty(g, vt) + " " + a.v + ", " + b.v), ty: vt) } // v < w, v == w ...: a mask (boolxN), the answer for every lane. m.all(), m.any(), select(m, a, b) use it. fn vecCompare(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { vt := vecOf(l, r) a := asVec(g, l, vt, o, at) b := asVec(g, r, vt, o, at) lane := vt.elem[0] mask := shaped(vt, "bool") if lane.kind == "bool" && o != "==" && o != "!=" { fail(g, at, "masks can only be compared with == and !=") } if isFloat(lane) { ops := ["==", "!=", "<", "<=", ">", ">="] preds := ["oeq", "une", "olt", "ole", "ogt", "oge"] return Val(v: op(g, "fcmp " + preds[ops.find(o)] + " " + llty(g, vt) + " " + a.v + ", " + b.v), ty: mask) } return Val(v: op(g, "icmp " + intPred(o, isSigned(lane)) + " " + llty(g, vt) + " " + a.v + ", " + b.v), ty: mask) } // min(v, w) and max(v, w), lane by lane (clamp is the two) fn vecPick(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { vt := vecOf(l, r) a := asVec(g, l, vt, o, at) b := asVec(g, r, vt, o, at) lane := vt.elem[0] name := "" if isFloat(lane) { name = "minnum" if o == ">" { name = "maxnum" } } else { sign := "u" if isSigned(lane) { sign = "s" } name = sign + "min" if o == ">" { name = sign + "max" } } lt := llty(g, vt) return Val(v: vecIntrinsic(g, lt, name, vt, [lt, lt], [a.v, b.v], ""), ty: vt) } fn vecAbs(ptr[Gen] g, Val v) -> Val { lane := v.ty.elem[0] lt := llty(g, v.ty) if isUnsigned(lane) { return v } if isFloat(lane) { return Val(v: vecIntrinsic(g, lt, "fabs", v.ty, [lt], [v.v], ""), ty: v.ty) } return Val(v: vecIntrinsic(g, lt, "abs", v.ty, [lt, "i1"], [v.v, "false"], ""), ty: v.ty) } // all the lanes made into one number: "add", "min" or "max" fn vecReduce(ptr[Gen] g, Val v, str how) -> Val { lane := v.ty.elem[0] lt := llty(g, v.ty) et := llty(g, lane) if isFloat(lane) { if how == "add" { // reassoc: the lanes may be added in any order, which is what makes it fast return Val(v: vecIntrinsic(g, et, "vector.reduce.fadd", v.ty, [et, lt], ["-0.0", v.v], "reassoc "), ty: lane) } return Val(v: vecIntrinsic(g, et, "vector.reduce.f" + how, v.ty, [lt], [v.v], ""), ty: lane) } if how == "add" { return Val(v: vecIntrinsic(g, et, "vector.reduce.add", v.ty, [lt], [v.v], ""), ty: lane) } sign := "u" if isSigned(lane) { sign = "s" } return Val(v: vecIntrinsic(g, et, "vector.reduce." + sign + how, v.ty, [lt], [v.v], ""), ty: lane) } fn vecMethod(ptr[Gen] g, ast.Node m, Val v) -> Val { vt := v.ty lane := vt.elem[0] lt := llty(g, vt) n := m.name if lane.kind == "bool" && n != "shuffle" && n != "lo" && n != "hi" { return maskMethod(g, m, v) } if n == "sum" || n == "min" || n == "max" { need(g, m, 0) how := n if n == "sum" { how = "add" } return vecReduce(g, v, how) } if n == "dot" { need(g, m, 1) w := asVec(g, expr(g, m.kids[1]), vt, "dot", m.kids[1]) return vecReduce(g, vecArith(g, "*", v, w, m), "add") } if n == "abs" { need(g, m, 0) return vecAbs(g, v) } if n == "sqrt" || n == "floor" || n == "ceil" || n == "round" || n == "fma" || n == "muladd" { if !isFloat(lane) { fail(g, m, n + "() is for f32 and f64 lanes, but this is " + ast.show(vt)) } if n == "fma" || n == "muladd" { need(g, m, 2) b := asVec(g, expr(g, m.kids[1]), vt, n, m.kids[1]) c := asVec(g, expr(g, m.kids[2]), vt, n, m.kids[2]) return Val(v: fusedOp(g, n, vt, v.v, b.v, c.v), ty: vt) } need(g, m, 0) return Val(v: vecIntrinsic(g, lt, n, vt, [lt], [v.v], ""), ty: vt) } if n == "store" || n == "store_part" { need(g, m, 2) dst := ref(g, m.kids[1], true) dt := dst.val.ty if dst.isPlace { dt = dst.pl.ty } if (dt.kind != "array" && dt.kind != "ptr") || dt.elem.len == 0 || !ast.same(dt.elem[0], lane) { fail(g, m.kids[1], "store(list, i) writes " + ast.show(lane) + " numbers into a []" + ast.show(lane) + " or a ptr[" + ast.show(lane) + "], but this is " + ast.show(dt)) } if dt.kind == "array" && !dst.isPlace { fail(g, m.kids[1], n + "(list, i) needs a list in a variable (this one would be thrown away)") } if n == "store_part" && dt.kind != "array" { fail(g, m.kids[1], "store_part(list, i) needs a list: a ptr doesn't know where its numbers end") } i := coerce(g, expr(g, m.kids[2]), T("int"), m.kids[2], "an index").v data := "" if dt.kind == "ptr" { data = dst.val.v if dst.isPlace { data = load(g, "ptr", dst.pl.addr) } } else { av := load(g, "{ ptr, i64, i64 }", dst.pl.addr) len := op(g, "extractvalue { ptr, i64, i64 } " + av + ", 1") data = op(g, "extractvalue { ptr, i64, i64 } " + av + ", 0") if n == "store_part" { // only the lanes that are in the list are written mk := tailMask(g, i, vt.count, len, m) ep := op(g, "getelementptr " + llty(g, lane) + ", ptr " + data + ", i64 " + i) ml := llty(g, shaped(vt, "bool")) fname := "@llvm.masked.store." + vecSuffix(vt) + ".p0" declareOnce(g, "declare void " + fname + "(" + lt + ", ptr, i32, " + ml + ")") emit(g, "call void " + fname + "(" + lt + " " + v.v + ", ptr " + ep + ", i32 " + laneBytes(vt) + ", " + ml + " " + mk + ")") return Val(ty: T("void")) } vecBounds(g, i, vt.count, len, m) } ep := op(g, "getelementptr inbounds " + llty(g, lane) + ", ptr " + data + ", i64 " + i) emit(g, "store " + lt + " " + v.v + ", ptr " + ep + ", align " + laneBytes(vt)) return Val(ty: T("void")) } if n == "shuffle" || n == "lo" || n == "hi" || n == "lookup" { return shuffleMethod(g, m, v) } if n == "dot_add" { return dotAdd(g, m, v) } fail(g, m, "a " + ast.show(vt) + " has no method '" + n + "' (it has: sum, min, max, dot, abs, sqrt, floor, ceil, round, fma, muladd, store, store_part, shuffle, lo, hi, lookup, dot_add)") return v } // ---------- fused multiply-add, and numbers as bits ---------- // // Finch never fuses on its own: a * b + c is a multiply and an add, two roundings, on every processor and with every // --cpu (the IR has no fast-math flags, and clang compiles it with -ffp-contract=on, which fuses only what asks to be). // Fusing is asked for by name: // fma(a, b, c) one rounding, always (one instruction with FMA hardware, a library call without it) // muladd(a, b, c) whichever is faster here: fused where the processor can, a multiply and an add where it can't // the llvm.fma / llvm.fmuladd call for a number or a vector type fn fusedOp(ptr[Gen] g, str name, ast.Type t, str a, str b, str c) -> str { lt := llty(g, t) which := "fma" if name == "muladd" { which = "fmuladd" } return vecIntrinsic(g, lt, which, t, [lt, lt, lt], [a, b, c], "") } fn fmaCall(ptr[Gen] g, ast.Node c) -> Val { n := c.name if g.index { noteRef(g, c, c.col, n.len, docs.hover(docs.find(docs.builtins(), n)), "", 0, 0) } checkArgs(g, c, 0, 3, n) []Val vals for a in c.kids { v := expr(g, a) if !isNumber(v.ty) && v.ty.kind != "vec" { fail(g, a, n + "(...) is for numbers, but this is " + ast.show(v.ty)) } vals.push(v) } // the type all three take: a vector if one is, else the float type they fit t := T("void") for v in vals { if v.ty.kind == "vec" { t = v.ty } } if t.kind == "vec" { if !isFloat(t.elem[0]) { fail(g, c, n + "(...) is for f32 and f64 lanes, but this is " + ast.show(t)) } []str vs for i in 0..3 { vs.push(asVec(g, vals[i], t, n, c.kids[i]).v) } return Val(v: fusedOp(g, n, t, vs[0], vs[1], vs[2]), ty: t) } // f32 when an f32 takes part and nothing wider does (numbers written in the code adapt), else float narrow := false wide := false for v in vals { if v.ty.kind == "f32" { narrow = true } else if !v.lit { wide = true } } t = T("float") if narrow && !wide { t = T("f32") } []str vs for i in 0..3 { vs.push(coerce(g, vals[i], t, c.kids[i], "each number of " + n + "(...)").v) } return Val(v: fusedOp(g, n, t, vs[0], vs[1], vs[2]), ty: t) } // the same shape as t (a number, or a vector of as many lanes) with lanes of another kind fn shaped(ast.Type t, str lane) -> ast.Type { if t.kind != "vec" { return T(lane) } out := ast.Type(kind: "vec", count: t.count) out.elem.push(T(lane)) return out } // a constant in every lane of t (or just the number) fn konst(ptr[Gen] g, ast.Type t, str n) -> str { if t.kind != "vec" { return n } return splat(g, n, t) } // f32_bits(x) -> u32 and back, f64 the same; f16 and bf16 (as the u16 that holds them) to f32 and back. // Each works on a number or on a vector, lane by lane: f16_to_f32(u16x8) is an f32x8. fn bitsCall(ptr[Gen] g, ast.Node c) -> Val { n := c.name if g.index { noteRef(g, c, c.col, n.len, docs.hover(docs.find(docs.builtins(), n)), "", 0, 0) } checkArgs(g, c, 0, 1, n) v := expr(g, c.kids[0]) from := "" // the lane type it takes to := "" if n == "f32_bits" || n == "f32_to_f16" || n == "f32_to_bf16" { from = "f32" to = "u32" if n != "f32_bits" { to = "u16" } } else if n == "f64_bits" { from = "float" to = "u64" } else if n == "f32_from_bits" { from = "u32" to = "f32" } else if n == "f64_from_bits" { from = "u64" to = "float" } else { from = "u16" to = "f32" } lane := v.ty if v.ty.kind == "vec" { lane = v.ty.elem[0] } // the bits may come as a signed number of the same size (i32 for u32), and a number written in the code fits if isInt(T(from)) && isInt(lane) && bitsOf(lane) == bitsOf(T(from)) { v = Val(v: v.v, ty: shaped(v.ty, from)) } else if v.ty.kind != "vec" && v.lit { v = coerce(g, v, T(from), c.kids[0], "the value of " + n + "(...)") } else if lane.kind != from { fail(g, c.kids[0], n + "(...) takes " + ast.show(T(from)) + " (or a vector of them), but this is " + ast.show(v.ty)) } src := shaped(v.ty, from) out := shaped(v.ty, to) il := llty(g, src) ol := llty(g, out) if n == "f32_bits" || n == "f64_bits" || n == "f32_from_bits" || n == "f64_from_bits" { return Val(v: op(g, "bitcast " + il + " " + v.v + " to " + ol), ty: out) } if n == "bf16_to_f32" { // bf16 is the top half of an f32 wide := shaped(v.ty, "u32") wl := llty(g, wide) z := op(g, "zext " + il + " " + v.v + " to " + wl) sh := op(g, "shl " + wl + " " + z + ", " + konst(g, wide, "16")) return Val(v: op(g, "bitcast " + wl + " " + sh + " to " + ol), ty: out) } if n == "f32_to_bf16" { // the top half, rounded to the nearest (ties to even); a NaN stays a NaN wide := shaped(v.ty, "u32") wl := llty(g, wide) u := op(g, "bitcast " + il + " " + v.v + " to " + wl) hi := op(g, "lshr " + wl + " " + u + ", " + konst(g, wide, "16")) odd := op(g, "and " + wl + " " + hi + ", " + konst(g, wide, "1")) bias := op(g, "add " + wl + " " + odd + ", " + konst(g, wide, "32767")) rounded := op(g, "lshr " + wl + " " + op(g, "add " + wl + " " + u + ", " + bias) + ", " + konst(g, wide, "16")) quiet := op(g, "or " + wl + " " + hi + ", " + konst(g, wide, "64")) isNan := op(g, "fcmp uno " + il + " " + v.v + ", " + v.v) pickd := op(g, "select " + llty(g, shaped(v.ty, "bool")) + " " + isNan + ", " + wl + " " + quiet + ", " + wl + " " + rounded) return Val(v: op(g, "trunc " + wl + " " + pickd + " to " + ol), ty: out) } // f16: LLVM's half type (one instruction with F16C or on ARM64, a small library function elsewhere) hl := "half" if v.ty.kind == "vec" { hl = "<" + str(v.ty.count) + " x half>" } if n == "f16_to_f32" { h := op(g, "bitcast " + il + " " + v.v + " to " + hl) return Val(v: op(g, "fpext " + hl + " " + h + " to " + ol), ty: out) } h := op(g, "fptrunc " + il + " " + v.v + " to " + hl) return Val(v: op(g, "bitcast " + hl + " " + h + " to " + ol), ty: out) } // ---------- masks: boolxN ---------- // // A comparison of vectors gives a mask, one true/false a lane (). It picks lanes with select, says // whether any or all lanes are true, and loads and stores the end of a list (f32x8(list, i, fill), store_part). // lanes 0, 1, ... n-1 that are inside a list of `len` from index i (i may be past the end: then none) fn tailMask(ptr[Gen] g, str i, int n, str len, ast.Node at) -> str { panicIf(g, op(g, "icmp slt i64 " + i + ", 0"), "a vector's index is below 0", at) rest := op(g, "sub i64 " + len + ", " + i) []str lanes for k in 0..n { lanes.push("i64 " + str(k)) } wide := ast.Type(kind: "vec", count: n) wide.elem.push(T("int")) return op(g, "icmp slt <" + str(n) + " x i64> <" + lanes.join(", ") + ">, " + splat(g, rest, wide)) } // select(mask, a, b): a's lane where the mask is true, b's where it is false fn selectCall(ptr[Gen] g, ast.Node c) -> Val { if g.index { noteRef(g, c, c.col, c.name.len, docs.hover(docs.find(docs.builtins(), "select")), "", 0, 0) } checkArgs(g, c, 0, 3, "select") m := expr(g, c.kids[0]) if m.ty.kind != "vec" || m.ty.elem[0].kind != "bool" { fail(g, c.kids[0], "select(mask, a, b) takes a mask first, from a comparison of vectors like v > 0.0; this is " + ast.show(m.ty)) } a := expr(g, c.kids[1]) b := expr(g, c.kids[2]) vt := a.ty if vt.kind != "vec" { vt = b.ty } if vt.kind != "vec" { fail(g, c, "select(mask, a, b): a or b has to be a vector (the other may be a number for every lane)") } if vt.count != m.ty.count { fail(g, c, "the mask has " + str(m.ty.count) + " lanes, but " + ast.show(vt) + " has " + str(vt.count)) } av := asVec(g, a, vt, "select", c.kids[1]) bv := asVec(g, b, vt, "select", c.kids[2]) lt := llty(g, vt) return Val(v: op(g, "select " + llty(g, m.ty) + " " + m.v + ", " + lt + " " + av.v + ", " + lt + " " + bv.v), ty: vt) } fn maskMethod(ptr[Gen] g, ast.Node m, Val v) -> Val { n := m.name ml := llty(g, v.ty) if n == "any" || n == "all" { need(g, m, 0) how := "or" if n == "all" { how = "and" } return Val(v: vecIntrinsic(g, "i1", "vector.reduce." + how, v.ty, [ml], [v.v], ""), ty: T("bool")) } if n == "count" { need(g, m, 0) wide := shaped(v.ty, "i32") wl := llty(g, wide) z := op(g, "zext " + ml + " " + v.v + " to " + wl) sum := vecIntrinsic(g, "i32", "vector.reduce.add", wide, [wl], [z], "") return Val(v: op(g, "zext i32 " + sum + " to i64"), ty: T("int")) } fail(g, m, "a mask (" + ast.show(v.ty) + ") has .len, [i] and the methods any, all, count; pick lanes with select(mask, a, b)") return v } // ---------- moving lanes around ---------- fn indexList([]int idx) -> str { []str parts for k in idx { parts.push("i32 " + str(k)) } return "<" + parts.join(", ") + ">" } fn shuffleOf(ptr[Gen] g, Val v, []int idx, str v2) -> Val { vt := v.ty out := ast.Type(kind: "vec", count: idx.len) out.elem.push(vt.elem[0]) second := v2 if second.len == 0 { second = "poison" } return Val(v: op(g, "shufflevector " + llty(g, vt) + " " + v.v + ", " + llty(g, vt) + " " + second + ", <" + str(idx.len) + " x i32> " + indexList(idx)), ty: out) } fn fitsVector(ptr[Gen] g, ast.Node at, ast.Type lane, int n) { if n != 2 && n != 4 && n != 8 && n != 16 && n != 32 && n != 64 { fail(g, at, "a vector has 2, 4, 8, 16, 32 or 64 lanes, not " + str(n)) } bits := bitsOf(lane) if n * bits > 512 { fail(g, at, "that would be " + str(n) + " lanes of " + ast.show(lane) + ", more than 512 bits") } } // v.shuffle(3, 2, 1, 0): lanes picked by number (numbers written in the code); v.lo(), v.hi(): the halves; // table.lookup(idx): table[idx % table.len] for every lane of idx (pshufb on x86, tbl on ARM) fn shuffleMethod(ptr[Gen] g, ast.Node m, Val v) -> Val { vt := v.ty n := m.name if n == "lo" || n == "hi" { need(g, m, 0) if vt.count < 4 { fail(g, m, "a " + ast.show(vt) + " has only " + str(vt.count) + " lanes; take them with v[0] and v[1]") } []int idx start := 0 if n == "hi" { start = vt.count / 2 } for k in 0..vt.count / 2 { idx.push(start + k) } return shuffleOf(g, v, idx, "") } if n == "shuffle" { []int idx for q in 1..m.kids.len { x := expr(g, m.kids[q]) if !isInt(x.ty) || !x.lit { fail(g, m.kids[q], "shuffle(...) takes lane numbers written in the code (or constants), like v.shuffle(3, 2, 1, 0)") } k := int(x.v) or -1 if k < 0 || k >= vt.count { fail(g, m.kids[q], "lane " + x.v + " isn't in a " + ast.show(vt) + " (its lanes are 0 to " + str(vt.count - 1) + ")") } idx.push(k) } fitsVector(g, m, vt.elem[0], idx.len) return shuffleOf(g, v, idx, "") } // lookup need(g, m, 1) ix := expr(g, m.kids[1]) if ix.ty.kind != "vec" || !isInt(ix.ty.elem[0]) { fail(g, m.kids[1], "lookup(indices) takes a vector of whole numbers (like u8x16), but this is " + ast.show(ix.ty)) } if vt.elem[0].kind == "bool" { fail(g, m, "a mask can't be a lookup table") } fitsVector(g, m.kids[1], vt.elem[0], ix.ty.count) il := llty(g, ix.ty) masked := op(g, "and " + il + " " + ix.v + ", " + splat(g, str(vt.count - 1), ix.ty)) out := shaped(ix.ty, vt.elem[0].kind) ol := llty(g, out) el := llty(g, vt.elem[0]) xl := llty(g, ix.ty.elem[0]) cur := "poison" for k in 0..ix.ty.count { at := op(g, "extractelement " + il + " " + masked + ", i64 " + str(k)) lane := op(g, "extractelement " + llty(g, vt) + " " + v.v + ", " + xl + " " + at) cur = op(g, "insertelement " + ol + " " + cur + ", " + el + " " + lane + ", i64 " + str(k)) } return Val(v: cur, ty: out) } // concat(a, b): a's lanes, then b's fn concatCall(ptr[Gen] g, ast.Node c) -> Val { if g.index { noteRef(g, c, c.col, c.name.len, docs.hover(docs.find(docs.builtins(), "concat")), "", 0, 0) } checkArgs(g, c, 0, 2, "concat") a := expr(g, c.kids[0]) b := expr(g, c.kids[1]) if a.ty.kind != "vec" || !ast.same(a.ty, b.ty) { fail(g, c, "concat(a, b) joins two vectors of the same type, but these are " + ast.show(a.ty) + " and " + ast.show(b.ty)) } fitsVector(g, c, a.ty.elem[0], a.ty.count * 2) []int idx for k in 0..a.ty.count * 2 { idx.push(k) } return shuffleOf(g, a, idx, b.v) } // ---------- processor features: @cpu(...), cpu_has(...) ---------- // // --cpu gives the whole program a processor (main.fch asks clang which features that means); @cpu("avx2,fma") gives one // function more, as LLVM's "target-features". Such a function is only called after cpu_has("avx2") said yes. Features of // the other architecture are dropped, so one source holds the x86 and the ARM versions side by side. fn x86Features() -> []str { return ["sse3", "ssse3", "sse4.1", "sse4.2", "popcnt", "avx", "avx2", "fma", "f16c", "bmi1", "bmi2", "avx512f", "avx512bw", "avx512dq", "avx512vl", "avx512vnni", "avx512bf16", "avx512fp16", "avxvnni"] } fn armFeatures() -> []str { return ["neon", "fp16", "dotprod", "i8mm", "bf16", "sve", "sve2"] } // the name LLVM uses for a feature fn llvmFeature(str x) -> str { if x == "bmi1" { return "bmi" } if x == "fp16" { return "fullfp16" } return x } // the features of a level: x86-64-v3 is the AVX2 generation, v4 the AVX-512 one fn levelFeatures(str name) -> []str { []str out if name == "x86-64-v2" || name == "x86-64-v3" || name == "x86-64-v4" { for x in ["sse3", "ssse3", "sse4.1", "sse4.2", "popcnt"] { out.push(x) } } if name == "x86-64-v3" || name == "x86-64-v4" { for x in ["avx", "avx2", "bmi1", "bmi2", "f16c", "fma"] { out.push(x) } } if name == "x86-64-v4" { for x in ["avx512f", "avx512bw", "avx512dq", "avx512vl"] { out.push(x) } } return out } // a function's own @cpu features, for the architecture it is built for fn cpuOf(ptr[Gen] g, ast.Node f) -> []str { []str out if f.cpu.len == 0 { return out } for part in f.cpu.split(",") { x := part.trim() names := levelFeatures(x) if names.len == 0 { if !x86Features().contains(x) && !armFeatures().contains(x) { fail(g, f, "@cpu: '" + x + "' isn't a feature Finch knows (x86: " + x86Features().join(", ") + ", x86-64-v2/v3/v4; ARM: " + armFeatures().join(", ") + ")") } names.push(x) } for y in names { if armFeatures().contains(y) == g.tg.arm64 && !out.contains(y) { out.push(y) } } } return out } // a vector wider than 16 bytes travels in different registers depending on the features (two xmm, or one ymm), so it // can't cross from a function with one set of features to a function with another fn checkCpuCall(ptr[Gen] g, FnSig sig, ast.Node c, str shown) { if sig.prog < 0 || sig.prog >= g.progs.len || sig.index >= g.progs[sig.prog].fns.len { return } callee := g.progs[sig.prog].fns[sig.index].cpu mine := g.progs[g.cur.prog].fns[g.cur.index].cpu if callee == mine { return } []ast.Type ts = sig.params ts.push(sig.ret) for t in ts { if t.kind == "vec" && t.count * bitsOf(t.elem[0]) > 128 { fail(g, c, "'" + shown + "' has other @cpu features than this function, so a " + ast.show(t) + " can't be passed between them (its registers differ); pass a list or a ptr, or give both the same @cpu") } } } // cpu_has("avx2"): does the processor running the program have it? (false for the other architecture's names) fn cpuHasCall(ptr[Gen] g, ast.Node c) -> Val { if g.index { noteRef(g, c, c.col, c.name.len, docs.hover(docs.find(docs.builtins(), "cpu_has")), "", 0, 0) } checkArgs(g, c, 0, 1, "cpu_has") if c.kids[0].kind == "str" { x := c.kids[0].text if !x86Features().contains(x) && !armFeatures().contains(x) { fail(g, c.kids[0], "cpu_has: '" + x + "' isn't a feature Finch knows (x86: " + x86Features().join(", ") + "; ARM: " + armFeatures().join(", ") + ")") } } s := strArg(g, c.kids[0], "the feature's name") r := op(g, "call i32 @finch_cpu_has(ptr " + spill(g, s) + ")") release(g, s) return Val(v: op(g, "icmp ne i32 " + r + ", 0"), ty: T("bool")) } // ---------- acc.dot_add(a, b): the step of an int8 dot product ---------- // // acc is i32xN; a and b have 4N bytes (a u8 or i8, b i8 or u8). Lane k of acc gains a[4k]*b[4k] + ... + a[4k+3]*b[4k+3], // exactly (no saturation). That is vpdpbusd (AVX-VNNI, AVX-512 VNNI) and usdot / sdot / udot (ARM), used when the // function's features have them; elsewhere the same sum is written out, which LLVM makes into pmaddwd and adds. fn dotAdd(ptr[Gen] g, ast.Node m, Val acc) -> Val { need(g, m, 2) at := acc.ty if at.elem[0].kind != "i32" && at.elem[0].kind != "u32" { fail(g, m, "dot_add is called on an i32 vector (the sums), like acc.dot_add(a, b) with acc an i32x8; this is " + ast.show(at)) } a := expr(g, m.kids[1]) b := expr(g, m.kids[2]) for x in [a, b] { if x.ty.kind != "vec" || (x.ty.elem[0].kind != "u8" && x.ty.elem[0].kind != "i8") || x.ty.count != at.count * 4 { fail(g, m, "dot_add(a, b) on an " + ast.show(at) + " takes two vectors of " + str(at.count * 4) + " bytes (u8 or i8), but these are " + ast.show(a.ty) + " and " + ast.show(b.ty)) } } au := a.ty.elem[0].kind == "u8" bu := b.ty.elem[0].kind == "u8" if !au && bu { // u8 x i8 is the same sum as i8 x u8: put the unsigned one first t := a a = b b = t au = true bu = false } al := llty(g, at) bits := at.count * 32 f := g.curFeatures if !g.tg.arm64 && au && !bu { vnni := (bits == 512 && f.contains("avx512vnni")) || (bits <= 256 && (f.contains("avxvnni") || (f.contains("avx512vnni") && f.contains("avx512vl")))) if vnni { a32 := op(g, "bitcast " + llty(g, a.ty) + " " + a.v + " to " + al) b32 := op(g, "bitcast " + llty(g, b.ty) + " " + b.v + " to " + al) name := "@llvm.x86.avx512.vpdpbusd." + str(bits) declareOnce(g, "declare " + al + " " + name + "(" + al + ", " + al + ", " + al + ")") return Val(v: op(g, "call " + al + " " + name + "(" + al + " " + acc.v + ", " + al + " " + a32 + ", " + al + " " + b32 + ")"), ty: at) } } if g.tg.arm64 && bits % 128 == 0 { which := "" if au && !bu && f.contains("i8mm") { which = "usdot" } else if au && bu && f.contains("dotprod") { which = "udot" } else if !au && !bu && f.contains("dotprod") { which = "sdot" } if which.len > 0 { // one instruction per 16 bytes name := "@llvm.aarch64.neon." + which + ".v4i32.v16i8" declareOnce(g, "declare <4 x i32> " + name + "(<4 x i32>, <16 x i8>, <16 x i8>)") parts := at.count / 4 []str outs for q in 0..parts { []int ai for k in 0..4 { ai.push(q * 4 + k) } []int bi for k in 0..16 { bi.push(q * 16 + k) } ac := shuffleOf(g, acc, ai, "").v ap := shuffleOf(g, a, bi, "").v bp := shuffleOf(g, b, bi, "").v outs.push(op(g, "call <4 x i32> " + name + "(<4 x i32> " + ac + ", <16 x i8> " + ap + ", <16 x i8> " + bp + ")")) } return Val(v: joinParts(g, outs, 4), ty: at) } } // written out: widen, multiply, add each group of four w := shaped(a.ty, "i32") wl := llty(g, w) ext := "sext" if au { ext = "zext" } aw := op(g, ext + " " + llty(g, a.ty) + " " + a.v + " to " + wl) ext = "sext" if bu { ext = "zext" } bw := op(g, ext + " " + llty(g, b.ty) + " " + b.v + " to " + wl) prod := Val(v: op(g, "mul " + wl + " " + aw + ", " + bw), ty: w) sum := acc.v for k in 0..4 { []int idx for j in 0..at.count { idx.push(4 * j + k) } sum = op(g, "add " + al + " " + sum + ", " + shuffleOf(g, prod, idx, "").v) } return Val(v: sum, ty: at) } // <4 x i32> pieces back into one vector of n * 4 lanes fn joinParts(ptr[Gen] g, []str parts, int each) -> str { if parts.len == 1 { return parts[0] } []str cur = parts size := each while cur.len > 1 { []str next t := ast.Type(kind: "vec", count: size) t.elem.push(T("i32")) k := 0 while k < cur.len { []int idx for j in 0..size * 2 { idx.push(j) } next.push(shuffleOf(g, Val(v: cur[k], ty: t), idx, cur[k + 1]).v) k += 2 } cur = next size = size * 2 } return cur[0] }