diff --git a/editors/kate/finch.xml b/editors/kate/finch.xml index 883a6e0..f9f2642 100644 --- a/editors/kate/finch.xml +++ b/editors/kate/finch.xml @@ -5,7 +5,7 @@ Install: copy to ~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml (or run editors/kate/install.sh), then restart Kate. --> - @@ -103,6 +103,8 @@ + + diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md index 195374b..a0a750c 100644 --- a/editors/vscode/CHANGELOG.md +++ b/editors/vscode/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.6.0 + +- Vector types (`f32x8`, `i8x32`, `f64x4`, …) highlighted as types; completion of their methods (`sum`, `dot`, `fma`, `store`, …). + ## 2.5.0 - Highlighting for code inside text (`"hp \(hp)"`), `test "…" { }` blocks, `const`, `"""` text, `1e9`. diff --git a/editors/vscode/package-lock.json b/editors/vscode/package-lock.json index f98d8ad..5a0404d 100644 --- a/editors/vscode/package-lock.json +++ b/editors/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "finch-lang", - "version": "2.4.0", + "version": "2.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "finch-lang", - "version": "2.4.0", + "version": "2.6.0", "license": "Apache-2.0", "dependencies": { "vscode-languageclient": "^9.0.1" diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 40c7715..9c17bd1 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -2,7 +2,7 @@ "name": "finch-lang", "displayName": "Finch", "description": "The Finch programming language: highlighting, errors as you type, completion, hover, go to definition, and a Run button.", - "version": "2.5.0", + "version": "2.6.0", "publisher": "nexoniarz", "license": "Apache-2.0", "icon": "images/icon.png", diff --git a/editors/vscode/syntaxes/finch.tmLanguage.json b/editors/vscode/syntaxes/finch.tmLanguage.json index 9154a4e..e3c2dea 100644 --- a/editors/vscode/syntaxes/finch.tmLanguage.json +++ b/editors/vscode/syntaxes/finch.tmLanguage.json @@ -148,7 +148,7 @@ }, { "name": "storage.type.primitive.finch", - "match": "\\b(int|float|bool|char|str|ptr|map|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64)\\b" + "match": "\\b(int|float|bool|char|str|ptr|map|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64|[iuf](?:8|16|32|64)x(?:2|4|8|16|32|64))\\b" }, { "match": "\\b(print|input|read_file|write_file|file_exists|delete_file|shell|exit|addr|new|free|error|min|max|abs|clamp|assert|atomic_add|atomic_sub|atomic_load|atomic_store|atomic_swap|atomic_cas)\\s*(?=\\()", diff --git a/editors/vscode/test/grammar.js b/editors/vscode/test/grammar.js index bf2aae7..0903ec5 100644 --- a/editors/vscode/test/grammar.js +++ b/editors/vscode/test/grammar.js @@ -29,6 +29,7 @@ const code = [ 'test "lives \\(hp + f(1)) left" {', ' assert(clamp(x, 0, 9) == 3)', '}', + 'fn dot(f32x8 a, i8x32 b) {', ]; const expect = [ // [line, token text, scope that must be present] [0, 'import', 'keyword.control.import'], [0, '"stdio.h"', 'string.quoted.double'], @@ -49,6 +50,7 @@ const expect = [ // [line, token text, scope that must be present] [17, 'test', 'keyword.other.test'], [17, '\\(', 'punctuation.section.embedded.begin'], [17, 'hp', 'meta.embedded.line'], [17, 'f', 'meta.embedded.line'], [17, 'left', 'string.quoted.double'], [18, 'assert', 'support.function.builtin'], [18, 'clamp', 'support.function.builtin'], + [20, 'f32x8', 'storage.type.primitive'], [20, 'i8x32', 'storage.type.primitive'], ]; registry.loadGrammar('source.finch').then((g) => { let state = vsctm.INITIAL, toks = []; diff --git a/src/ast.fch b/src/ast.fch index 0870037..4c60451 100644 --- a/src/ast.fch +++ b/src/ast.fch @@ -36,6 +36,41 @@ fn ptrTo(Type t) -> Type { return out } +// f32x8, i8x32, u16x16, f64x4: N numbers of one type side by side, worked on all at once (SIMD). +// Its kind is "vec", elem[0] the type of one lane, count the number of lanes. kind "" if `name` is no such type. +fn vecType(str name) -> Type { + x := name.find("x") + if x < 2 || x + 1 >= name.len { + return Type() + } + lane := name.sub(0, x) + if !["i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64"].contains(lane) { + return Type() + } + n := 0 + for c in name.sub(x + 1, name.len) { + if c < '0' || c > '9' { + return Type() + } + n = n * 10 + (int(c) - int('0')) + } + if n != 2 && n != 4 && n != 8 && n != 16 && n != 32 && n != 64 { + return Type() + } + bits := int(lane.sub(1, lane.len)) or 0 + if n * bits > 512 { + return Type() // the widest registers (AVX-512) hold 512 bits + } + if lane == "i64" { + lane = "int" + } else if lane == "f64" { + lane = "float" + } + out := Type(kind: "vec", count: n) + out.elem.push(simple(lane)) + return out +} + fn same(Type a, Type b) -> bool { if a.kind != b.kind { return false @@ -43,10 +78,10 @@ fn same(Type a, Type b) -> bool { if a.kind == "struct" || a.kind == "named" || a.kind == "enum" { return a.name == b.name && a.module == b.module } - if a.kind == "fixed" && a.count != b.count { + if (a.kind == "fixed" || a.kind == "vec") && a.count != b.count { return false } - if a.kind == "array" || a.kind == "ptr" || a.kind == "map" || a.kind == "fixed" { + if a.kind == "array" || a.kind == "ptr" || a.kind == "map" || a.kind == "fixed" || a.kind == "vec" { if a.elem.len != b.elem.len { return false } @@ -67,6 +102,15 @@ fn show(Type t) -> str { if t.kind == "fixed" { return "[" + str(t.count) + "]" + show(t.elem[0]) } + if t.kind == "vec" { + lane := t.elem[0].kind + if lane == "int" { + lane = "i64" + } else if lane == "float" { + lane = "f64" + } + return lane + "x" + str(t.count) + } if t.kind == "map" { return "map[" + show(t.elem[0]) + "]" + show(t.elem[1]) } diff --git a/src/docs.fch b/src/docs.fch index 977f0de..04dc13e 100644 --- a/src/docs.fch +++ b/src/docs.fch @@ -40,7 +40,8 @@ fn builtins() -> []Doc { doc("float", "float(x) -> float", "Converts to a decimal number; parses text (int(s) or 0.0 handles bad text)."), doc("char", "char(int n) -> char", "The character with this code."), doc("bool", "bool(int n) -> bool", "true for any number but 0."), - doc("ptr", "ptr(x) -> ptr", "An untyped pointer: from a typed pointer, or from a number (an address or offset for C).") + doc("ptr", "ptr(x) -> ptr", "An untyped pointer: from a typed pointer, or from a number (an address or offset for C)."), + doc("f32x8", "f32x8(x) / f32x8(a, b, ...) / f32x8(list, i) / f32x8(v)", "A vector: 8 f32 side by side, worked on all at once (SIMD). Every lane x; one value per lane; 8 numbers from list[i]; or another 8-lane vector converted. The same goes for i8x16, i32x8, f64x4 and the rest.") ] } @@ -94,13 +95,32 @@ fn mapMethods() -> []Doc { ] } +fn vecMethods() -> []Doc { + return [ + doc("len", ".len -> int", "The number of lanes."), + doc("sum", "sum() -> T", "All the lanes added up (floats: in whatever order is fastest)."), + doc("min", "min() -> T", "The smallest lane."), + doc("max", "max() -> T", "The biggest lane."), + doc("dot", "dot(V w) -> T", "Lane times lane, added up: (v * w).sum()."), + doc("sqrt", "sqrt() -> V", "The square root of every lane (f32/f64 lanes)."), + doc("floor", "floor() -> V", "Every lane rounded down (f32/f64 lanes)."), + doc("ceil", "ceil() -> V", "Every lane rounded up (f32/f64 lanes)."), + doc("round", "round() -> V", "Every lane rounded to the nearest whole number, halves away from 0 (f32/f64 lanes)."), + doc("abs", "abs() -> V", "Every lane without its minus sign."), + doc("fma", "fma(V b, V c) -> V", "v * b + c in one step, with one rounding (f32/f64 lanes)."), + doc("store", "store([]T list, int i)", "Writes the lanes into list[i], list[i+1], ... (or through a ptr[T]).") + ] +} + fn keywords() -> []str { return ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false", "null", "import", "link", "struct", "enum", "match", "export", "defer", "or", "try", "self", "const"] } fn typeNames() -> []str { - return ["int", "float", "bool", "char", "str", "ptr", "map", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64"] + return ["int", "float", "bool", "char", "str", "ptr", "map", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64", + "f32x4", "f32x8", "f32x16", "f64x2", "f64x4", "f64x8", "i8x16", "i8x32", "i8x64", "u8x16", "u8x32", "u8x64", + "i16x8", "i16x16", "i16x32", "i32x4", "i32x8", "i32x16", "u32x4", "u32x8", "u32x16", "i64x2", "i64x4", "i64x8"] } // the entry called `name`, or an empty Doc diff --git a/src/gen.fch b/src/gen.fch index 4768440..7aba27c 100644 --- a/src/gen.fch +++ b/src/gen.fch @@ -206,6 +206,7 @@ struct Gen { 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 @@ -367,7 +368,7 @@ fn byteSize(ptr[Gen] g, ast.Type t) -> int { } return n } - if k == "fixed" && t.elem.len > 0 { + if (k == "fixed" || k == "vec") && t.elem.len > 0 { return t.count * byteSize(g, t.elem[0]) } if k == "str" || k == "array" || k == "map" { @@ -436,12 +437,22 @@ fn alloca(ptr[Gen] g, str ty) -> str { fn load(ptr[Gen] g, str ty, str at) -> str { t := tmpName(g) - emit(g, t + " = load " + ty + ", ptr " + at) + 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) + 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 { @@ -703,6 +714,13 @@ fn llty(ptr[Gen] g, ast.Type t) -> str { 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" } @@ -774,6 +792,9 @@ fn typeKey(ast.Type t) -> str { 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) } @@ -1221,7 +1242,19 @@ fn formatFn(ptr[Gen] g, ast.Type t) -> str { } g.done.push(name) s := beginHelper(g) - if t.kind == "array" { + 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")) @@ -1428,6 +1461,12 @@ fn coerce(ptr[Gen] g, Val v, ast.Type want, ast.Node at, str what) -> Val { 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 == "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 } @@ -2054,7 +2093,13 @@ fn resolveStructBody(ptr[Gen] g, int i) { g.structs[i].owning = owns g.module = saved g.structs[i].state = 2 - g.head.push("%S." + modName(g.structs[i].module) + "." + mangle(g.structs[i].name) + " = type { " + fields.join(", ") + " }") + 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) ---------- @@ -2108,6 +2153,10 @@ fn sizeAlign(ptr[Gen] g, ast.Type t) -> SizeAlign { 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 { @@ -2196,6 +2245,10 @@ fn diType(ptr[Gen] g, ast.Type t) -> int { 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) + ")") @@ -3628,6 +3681,9 @@ fn expr(ptr[Gen] g, ast.Node e) -> Val { fn unary(ptr[Gen] g, ast.Node e) -> Val { v := expr(g, e.kids[0]) + if v.ty.kind == "vec" && e.name != "!" { + 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")) @@ -3651,6 +3707,13 @@ fn unary(ptr[Gen] g, ast.Node e) -> Val { 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)) @@ -3746,6 +3809,9 @@ fn arith(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { 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) } @@ -3847,6 +3913,9 @@ fn compare(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { 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) } @@ -4281,6 +4350,15 @@ fn member(ptr[Gen] g, ast.Node m, bool write) -> Ref { 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") @@ -4376,6 +4454,15 @@ fn indexOf(ptr[Gen] g, ast.Node e, Ref obj, bool write) -> Ref { 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" { + // 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 { @@ -5307,7 +5394,7 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { 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" { + } 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) @@ -5320,6 +5407,10 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { 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") @@ -5570,7 +5661,7 @@ fn mathCall(ptr[Gen] g, ast.Node c) -> Val { []Val vals for a in c.kids { v := expr(g, a) - if !isNumber(v.ty) { + 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) @@ -5578,6 +5669,9 @@ fn mathCall(ptr[Gen] g, ast.Node c) -> Val { 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 } @@ -5659,6 +5753,9 @@ fn atomicCall(ptr[Gen] g, ast.Node c) -> Val { // 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)) { @@ -5902,6 +5999,15 @@ fn method(ptr[Gen] g, ast.Node m) -> Val { 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 { @@ -6530,3 +6636,396 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt) 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: how LLVM's vector intrinsics are named for this type +fn vecSuffix(ast.Type vt) -> str { + if isFloat(vt.elem[0]) { + return "v" + str(vt.count) + "f" + str(bitsOf(vt.elem[0])) + } + return "v" + str(vt.count) + "i" + str(bitsOf(vt.elem[0])) +} + +fn declareOnce(ptr[Gen] g, str decl) { + if !has(g.done, decl) { + g.done.push(decl) + g.head.push(decl) + } +} + +// call @llvm..(args...), declared the first time +fn vecIntrinsic(ptr[Gen] g, str ret, str name, ast.Type vt, []str argTypes, []str args, str flags) -> str { + fname := "@llvm." + name + "." + vecSuffix(vt) + declareOnce(g, "declare " + ret + " " + fname + "(" + argTypes.join(", ") + ")") + []str both + for i in 0..args.len { + both.push(argTypes[i] + " " + args[i]) + } + return op(g, "call " + flags + ret + " " + fname + "(" + both.join(", ") + ")") +} + +// the same number in every lane +fn splat(ptr[Gen] g, str v, ast.Type vt) -> str { + lt := llty(g, vt) + one := op(g, "insertelement " + lt + " poison, " + llty(g, vt.elem[0]) + " " + v + ", i64 0") + return op(g, "shufflevector " + lt + " " + one + ", " + lt + " poison, <" + str(vt.count) + " x i32> zeroinitializer") +} + +// a value next to a vector: the vector itself, or a number put in every lane +fn asVec(ptr[Gen] g, Val v, ast.Type vt, str o, ast.Node at) -> Val { + if v.ty.kind == "vec" { + if !ast.same(v.ty, vt) { + fail(g, at, "can't use '" + o + "' on " + ast.show(vt) + " and " + ast.show(v.ty) + " (convert one: " + ast.show(vt) + "(...))") + } + return v + } + s := coerce(g, v, vt.elem[0], at, "a number used with " + ast.show(vt)) + return Val(v: splat(g, s.v, vt), ty: vt) +} + +fn vecOf(Val l, Val r) -> ast.Type { + if l.ty.kind == "vec" { + return l.ty + } + return r.ty +} + +// i, i+1, ..., i+n-1 are all in a list of `len` +fn vecBounds(ptr[Gen] g, str i, int n, str len, ast.Node at) { + end := op(g, "add i64 " + i + ", " + str(n)) + before := op(g, "icmp slt i64 " + i + ", 0") + after := op(g, "icmp sgt i64 " + end + ", " + len) + panicIf(g, op(g, "or i1 " + before + ", " + after), "the " + str(n) + " numbers of the vector don't all fit in the list there", at) +} + +// f32x8(x), f32x8(a, b, ..., h), f32x8(list, i), f32x8(i32x8 v) +fn vecMake(ptr[Gen] g, ast.Node c, ast.Type vt) -> Val { + if g.index { + noteRef(g, c, c.col, c.name.len, docs.hover(docs.find(docs.builtins(), "f32x8")), "", 0, 0) + } + for nm in c.names { + if nm.len > 0 { + fail(g, c, "named arguments (" + nm + ": ...) are only for creating structs") + } + } + shown := ast.show(vt) + lane := vt.elem[0] + lt := llty(g, vt) + n := vt.count + usage := shown + "(x) puts x in every lane, " + shown + "(a, b, ...) takes " + str(n) + " numbers, " + shown + "(list, i) loads them from a list" + if c.kids.len == 0 { + fail(g, c, usage) + } + first := ref(g, c.kids[0], false) + ft := first.val.ty + if first.isPlace { + ft = first.pl.ty + } + if ft.kind == "array" || ft.kind == "ptr" { + if c.kids.len != 2 { + fail(g, c, shown + "(list, i): which index to start at? (" + shown + "(list, 0) for the first " + str(n) + ")") + } + 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 + 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) + } + vecBounds(g, i, n, op(g, "extractvalue { ptr, i64, i64 } " + av + ", 1"), c) + data = op(g, "extractvalue { ptr, i64, i64 } " + av + ", 0") + } + 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] + 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 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] + 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: every lane is equal. (Lane by lane: min, max, or v[i] < w[i].) +fn vecCompare(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { + vt := vecOf(l, r) + if o != "==" && o != "!=" { + fail(g, at, "'" + o + "' on " + ast.show(vt) + " would have an answer for every lane; use min(v, w), max(v, w), or compare lanes: v[i] " + o + " w[i]") + } + a := asVec(g, l, vt, o, at) + b := asVec(g, r, vt, o, at) + cmp := "" + if isFloat(vt.elem[0]) { + cmp = op(g, "fcmp oeq " + llty(g, vt) + " " + a.v + ", " + b.v) + } else { + cmp = op(g, "icmp eq " + llty(g, vt) + " " + a.v + ", " + b.v) + } + bits := ast.Type(kind: "vec", count: vt.count) + bits.elem.push(T("bool")) + all := vecIntrinsic(g, "i1", "vector.reduce.and", bits, ["<" + str(vt.count) + " x i1>"], [cmp], "") + if o == "!=" { + return Val(v: op(g, "xor i1 " + all + ", true"), ty: T("bool")) + } + return Val(v: all, ty: T("bool")) +} + +// 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 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" { + if !isFloat(lane) { + fail(g, m, n + "() is for f32 and f64 lanes, but this is " + ast.show(vt)) + } + if n == "fma" { + need(g, m, 2) + b := asVec(g, expr(g, m.kids[1]), vt, "fma", m.kids[1]) + c := asVec(g, expr(g, m.kids[2]), vt, "fma", m.kids[2]) + return Val(v: vecIntrinsic(g, lt, "fma", vt, [lt, lt, lt], [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" { + 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], "store(list, i) needs a list in a variable (this one would be thrown away)") + } + 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) + vecBounds(g, i, vt.count, op(g, "extractvalue { ptr, i64, i64 } " + av + ", 1"), m) + data = op(g, "extractvalue { ptr, i64, i64 } " + av + ", 0") + } + 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")) + } + fail(g, m, "a " + ast.show(vt) + " has no method '" + n + "' (it has: sum, min, max, dot, abs, sqrt, floor, ceil, round, fma, store)") + return v +} diff --git a/src/lsp.fch b/src/lsp.fch index 0bc0fcc..fc8299d 100644 --- a/src/lsp.fch +++ b/src/lsp.fch @@ -7,6 +7,7 @@ import json import host import docs +import ast import fmt import "stdio.h" import "stdlib.h" @@ -621,6 +622,8 @@ fn completionAt(Analysis a, str text, str path, int line, int col) -> json.Json methodItems(addr(items), docs.strMethods()) } else if type.starts_with("map[") { methodItems(addr(items), docs.mapMethods()) + } else if ast.vecType(type).kind.len > 0 { + methodItems(addr(items), docs.vecMethods()) // f32x8, i8x32, ... } else { base := type for st in a.structs { diff --git a/src/main.fch b/src/main.fch index f4c4f00..7c59c14 100644 --- a/src/main.fch +++ b/src/main.fch @@ -174,6 +174,9 @@ fn usage() { print(" --trace runtime errors show the chain of calls (finch run, finch test, -g and -O0 do it anyway)") print(" --no-trace they don't (finch build without -g or -O0 doesn't, it costs speed)") print(" --target build for another system: windows, linux, arm64 (Linux), macos, or an LLVM triple") + print(" --cpu use all of this processor's instructions (AVX2, AVX-512, NEON...): native is the one") + print(" building it; or x86-64-v3 (AVX2), x86-64-v4 (AVX-512), apple-m1, ... The program then") + print(" needs such a processor. Default: any processor of the target system") exit(1) } @@ -243,7 +246,7 @@ fn format([]str args) -> int { fn main([]str args) -> int { if args.len == 2 && (args[1] == "version" || args[1] == "--version") { - print("Finch 2.5.0 (the compiler is written in Finch)") + print("Finch 2.6.0 (the compiler is written in Finch)") return 0 } if args.len >= 2 && args[1] == "fmt" { @@ -293,6 +296,7 @@ fn main([]str args) -> int { trace := cmd != "build" // finch build: only with --trace, -g or -O0 (the chain costs speed) traceGiven := false targetName := "" + cpu := "" opt := "-O2" []str extraLibs []str extraDirs @@ -335,6 +339,9 @@ fn main([]str args) -> int { } else if a == "--target" && ai + 1 < args.len { targetName = args[ai + 1] ai += 1 + } else if a == "--cpu" && ai + 1 < args.len { + cpu = args[ai + 1] + ai += 1 } else if cmd == "run" { progArgs.push(a) } else { @@ -418,6 +425,9 @@ fn main([]str args) -> int { for f in man.list("cflags", targets) { cflags = cflags + " " + linker.quote(f) } + if cpu.len == 0 { + cpu = man.value("cpu", "") + } } for d in getenv("FINCH_PATH").split(host.pathSeparator()) { if d.len > 0 { @@ -494,6 +504,14 @@ fn main([]str args) -> int { if debug { extra = extra + " -g" // or clang drops the debug info that is in the IR } + if cpu.len > 0 { + // x86-64 names a processor with -march, ARM with -mcpu + if tg.arm64 { + extra = extra + " -mcpu=" + linker.quote(cpu) + } else { + extra = extra + " -march=" + linker.quote(cpu) + } + } []str cflagList for d in dirs { cflagList.push(" -I" + linker.quote(d)) // also the program's own folders (its headers, for inline C functions) diff --git a/src/parser.fch b/src/parser.fch index ce3aff3..db97175 100644 --- a/src/parser.fch +++ b/src/parser.fch @@ -86,7 +86,7 @@ fn node(ptr[Parser] p, str kind) -> ast.Node { fn isTypeName(str n) -> bool { names := ["int", "float", "bool", "char", "str", "ptr", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64"] - return names.contains(n) + return names.contains(n) || ast.vecType(n).kind.len > 0 } // int, str, ptr[T], []T, Name, module.Name @@ -117,6 +117,10 @@ fn parseType(ptr[Parser] p) -> ast.Type { if name == "f64" { return ast.simple("float") } + vt := ast.vecType(name) + if vt.kind.len > 0 { + return vt + } return ast.simple(name) } t := ast.Type(kind: "named", name: name) diff --git a/tests/fail/vec_intdiv.fch b/tests/fail/vec_intdiv.fch new file mode 100644 index 0000000..146aa49 --- /dev/null +++ b/tests/fail/vec_intdiv.fch @@ -0,0 +1,5 @@ +// expect: no instruction that divides whole numbers lane by lane +fn main() { + a := i32x4(8) + print(a / 2) +} diff --git a/tests/fail/vec_lane_index.fch b/tests/fail/vec_lane_index.fch new file mode 100644 index 0000000..8d71340 --- /dev/null +++ b/tests/fail/vec_lane_index.fch @@ -0,0 +1,6 @@ +// expect: index 4 +fn main() { + v := i32x4(1) + i := 4 + print(v[i]) +} diff --git a/tests/fail/vec_lanes.fch b/tests/fail/vec_lanes.fch new file mode 100644 index 0000000..3e4d0f2 --- /dev/null +++ b/tests/fail/vec_lanes.fch @@ -0,0 +1,4 @@ +// expect: f32x4(a, b, ...) takes 4 numbers +fn main() { + print(f32x4(1.0, 2.0, 3.0)) +} diff --git a/tests/fail/vec_less.fch b/tests/fail/vec_less.fch new file mode 100644 index 0000000..e9dadb0 --- /dev/null +++ b/tests/fail/vec_less.fch @@ -0,0 +1,5 @@ +// expect: use min(v, w), max(v, w), or compare lanes: v[i] < w[i] +fn main() { + a := f32x4(1.0) + print(a < a) +} diff --git a/tests/fail/vec_load_end.fch b/tests/fail/vec_load_end.fch new file mode 100644 index 0000000..aa5cb71 --- /dev/null +++ b/tests/fail/vec_load_end.fch @@ -0,0 +1,5 @@ +// expect: the 8 numbers of the vector don't all fit in the list there +fn main() { + []f32 data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + print(f32x8(data, 2)) +} diff --git a/tests/fail/vec_mix.fch b/tests/fail/vec_mix.fch new file mode 100644 index 0000000..4b05a3c --- /dev/null +++ b/tests/fail/vec_mix.fch @@ -0,0 +1,6 @@ +// expect: can't use '+' on f32x8 and i32x8 (convert one: f32x8(...)) +fn main() { + a := f32x8(1.0) + b := i32x8(1) + print(a + b) +} diff --git a/tests/fail/vec_type.fch b/tests/fail/vec_type.fch new file mode 100644 index 0000000..0f44299 --- /dev/null +++ b/tests/fail/vec_type.fch @@ -0,0 +1,5 @@ +// expect: f32x8(x) puts x in every lane +fn main() { + f32x8 v = 1.0 + print(v) +} diff --git a/tests/run/vectors.fch b/tests/run/vectors.fch new file mode 100644 index 0000000..2d7d95f --- /dev/null +++ b/tests/run/vectors.fch @@ -0,0 +1,69 @@ +// Vectors: N numbers side by side, worked on all at once (SIMD) + +struct Particle { + f32x8 pos + int id = 7 +} + +fn scale(f32x8 v, f32 k) -> f32x8 { + return v * k +} + +// a dot product of two lists, 8 at a time, with the rest one by one +fn dot([]f32 a, []f32 b) -> f32 { + acc := f32x8(0.0) + i := 0 + while i + 8 <= a.len { + acc = f32x8(a, i).fma(f32x8(b, i), acc) + i += 8 + } + total := acc.sum() + while i < a.len { + total += a[i] * b[i] + i += 1 + } + return total +} + +fn main() { + a := f32x4(1.0, 2.0, 3.0, 4.0) + b := f32x4(0.5) + print(a + b, a * 2.0, 10.0 - a, -a) + print(a.sum(), a.min(), a.max(), a.dot(a), a.len) + print((a * a).sqrt(), f32x4(1.5, -1.5, 2.5, -0.2).round(), f32x4(1.7).floor(), f32x4(1.2).ceil()) + print(abs(f32x4(-1.0, 2.0, -3.0, 4.0)), min(a, 2.5), max(a, f32x4(2.0)), clamp(a, 1.5, 3.5)) + + // lanes + a[0] = 9.0 + print(a[0], a[3], a) + print(a == f32x4(9.0, 2.0, 3.0, 4.0), a != a) + + // whole numbers, bits, conversions + n := i32x8(1, 2, 3, 4, 5, 6, 7, 8) + print(n * 3 - 1, n << 2, n & 1, ~n, n.sum(), n.max()) + print(f32x8(n) / 2.0, i32x8(f32x8(2.9)), i16x8(n), u8x16(200).sum()) + print(abs(i32x4(-3, 3, -7, 0)), min(u8x16(5), u8x16(9)).max()) + + // from lists and back + []f32 data + for i in 0..20 { + data.push(f32(i)) + } + v := f32x8(data, 4) + print(v) + (v * 10.0).store(data, 12) + print(data) + print(dot(data, data)) + + // in structs, arrays, functions, and str() + p := Particle(pos: f32x8(1.0)) + p.pos = scale(p.pos, 3.0) + p.pos[7] = 0.5 + print(p) + []f64x4 list + list.push(f64x4(1.0, 2.0, 3.0, 4.0)) + list.push(list[0] * list[0]) + print(list, str(list[1]).len) + i64x2 big = i64x2(1 << 40, -5) + print(big + big) +} diff --git a/tests/run/vectors.out b/tests/run/vectors.out new file mode 100644 index 0000000..8e130e6 --- /dev/null +++ b/tests/run/vectors.out @@ -0,0 +1,15 @@ +f32x4(1.5, 2.5, 3.5, 4.5) f32x4(2, 4, 6, 8) f32x4(9, 8, 7, 6) f32x4(-1, -2, -3, -4) +10 1 4 30 4 +f32x4(1, 2, 3, 4) f32x4(2, -2, 3, -0) f32x4(1, 1, 1, 1) f32x4(2, 2, 2, 2) +f32x4(1, 2, 3, 4) f32x4(1, 2, 2.5, 2.5) f32x4(2, 2, 3, 4) f32x4(1.5, 2, 3, 3.5) +9 4 f32x4(9, 2, 3, 4) +true false +i32x8(2, 5, 8, 11, 14, 17, 20, 23) i32x8(4, 8, 12, 16, 20, 24, 28, 32) i32x8(1, 0, 1, 0, 1, 0, 1, 0) i32x8(-2, -3, -4, -5, -6, -7, -8, -9) 36 8 +f32x8(0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4) i32x8(2, 2, 2, 2, 2, 2, 2, 2) i16x8(1, 2, 3, 4, 5, 6, 7, 8) 128 +i32x4(3, 3, 7, 0) 5 +f32x8(4, 5, 6, 7, 8, 9, 10, 11) +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 40, 50, 60, 70, 80, 90, 100, 110] +49706 +Particle(pos: f32x8(3, 3, 3, 3, 3, 3, 3, 0.5), id: 7) +[f64x4(1, 2, 3, 4), f64x4(1, 4, 9, 16)] 18 +i64x2(2199023255552, -10)