Finch/src/gen.fch
Claude 04f1f94a21
Vectors for kernels: masks and select, shuffles and byte lookup, dot_add, @cpu and cpu_has; --cpu native on Nix
- comparisons of vectors give masks (boolxN): select(m, a, b), m.any/all/count, & | ^ !;
  f32x8(list, i, fill) and v.store_part(list, i) for the end of a list (masked load/store)
- v.shuffle(constant lanes), v.lo(), v.hi(), concat(a, b), table.lookup(indices):
  pshufb / vpshufb on x86 (SSSE3 and up), tbl on ARM64 (checked in the assembly, LLVM 18 and 21)
- acc.dot_add(a, b): i32 lanes gain 4 byte products each, exactly; vpdpbusd with AVX-VNNI or AVX-512 VNNI,
  usdot/sdot/udot on ARM64, the sum written out elsewhere (pmaddwd); run here with all three paths agreeing
- @cpu("avx2,fma") fn: LLVM target-features for one function (+evex512 for AVX-512, which LLVM 18+ needs);
  cpu_has("avx2"): CPUID + XGETBV on x86, hwcaps on Linux ARM64, sysctl on macOS; a wide vector can't be passed
  between functions with different @cpu (its registers differ), and the compiler says so
- the compiler asks clang which features --cpu means (its predefined macros, cached), so dot_add picks the instruction
- Nix's clang wrapper dropped -march=native in nix-shell (reproduced): finch tells it NIX_ENFORCE_NO_NATIVE=0, shell.nix too,
  and warns if --cpu native still made no difference

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Psc42moPLwP43Cmb9oZqSR
2026-11-01 15:41:47 +00:00

7741 lines
272 KiB
Text

// 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 <value> / ... 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 <value>)")
}
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 <value>, " + 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.<name>.<suffix>(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 (<N x i1>). 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]
}