From 68a9493572c9f07404a39146d8f5949f3b0e9e79 Mon Sep 17 00:00:00 2001
From: Claude
Date: Sun, 1 Nov 2026 13:50:35 +0000
Subject: [PATCH 1/7] Vector types (SIMD): f32x8, i8x32, f64x4 and the rest;
--cpu native
- fNxM / iNxM / uNxM up to 512 bits, as LLVM vectors (<8 x float>): LLVM picks SSE, AVX2,
AVX-512 or NEON for them
- f32x8(x) in every lane, f32x8(a, ..., h), f32x8(list, i) loads, f32x8(i32x8 v) converts
- + - * / and the bit operators lane by lane, with a plain number on either side counting for every lane;
v[i], v[i] = x, v.len, == / != (every lane), print and str
- methods: sum min max dot abs sqrt floor ceil round fma store; min/max/abs/clamp lane by lane
- wide vectors (and structs holding them) are loaded and stored with align 16, what malloc and the stack give
- --cpu (and cpu = "..." in finch.toml): native, x86-64-v3, apple-m1, ...
- debug info shows vectors; the language server completes their methods; Kate and VS Code highlight them
- f32 x = -1.5 works: a minus in front of a float written in the code keeps it such a number
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01Psc42moPLwP43Cmb9oZqSR
---
editors/kate/finch.xml | 4 +-
editors/vscode/CHANGELOG.md | 4 +
editors/vscode/package-lock.json | 4 +-
editors/vscode/package.json | 2 +-
editors/vscode/syntaxes/finch.tmLanguage.json | 2 +-
editors/vscode/test/grammar.js | 2 +
src/ast.fch | 48 +-
src/docs.fch | 24 +-
src/gen.fch | 513 +++++++++++++++++-
src/lsp.fch | 3 +
src/main.fch | 20 +-
src/parser.fch | 6 +-
tests/fail/vec_intdiv.fch | 5 +
tests/fail/vec_lane_index.fch | 6 +
tests/fail/vec_lanes.fch | 4 +
tests/fail/vec_less.fch | 5 +
tests/fail/vec_load_end.fch | 5 +
tests/fail/vec_mix.fch | 6 +
tests/fail/vec_type.fch | 5 +
tests/run/vectors.fch | 69 +++
tests/run/vectors.out | 15 +
21 files changed, 734 insertions(+), 18 deletions(-)
create mode 100644 tests/fail/vec_intdiv.fch
create mode 100644 tests/fail/vec_lane_index.fch
create mode 100644 tests/fail/vec_lanes.fch
create mode 100644 tests/fail/vec_less.fch
create mode 100644 tests/fail/vec_load_end.fch
create mode 100644 tests/fail/vec_mix.fch
create mode 100644 tests/fail/vec_type.fch
create mode 100644 tests/run/vectors.fch
create mode 100644 tests/run/vectors.out
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)
From 0bf66740dfdd8bd9b8eaa47b73467ff1407edd2e Mon Sep 17 00:00:00 2001
From: Claude
Date: Sun, 1 Nov 2026 13:57:28 +0000
Subject: [PATCH 2/7] Compiler and docs on every system: macOS Intel in CI,
releases made by CI, docs for vectors and --cpu
- CI: macOS on Apple Silicon and Intel (macos-15, macos-15-intel); a tag v* attaches every package
(Linux x64/ARM64, macOS ARM64/x64, Windows x64, the VS Code extension) to its release
- vectors: fma is llvm.fmuladd (llvm.fma was a libm call per lane without FMA hardware: 3.5x slower)
- examples/simd.fch: a dot product eight at a time, timed against the plain loop
- README and the English guides: installing on Linux, macOS and Windows; vectors (SIMD); --cpu;
-g is DWARF on Windows too (lldb from llvm-mingw); no more mention of the C++ compiler
- comments left from the C++ compiler (src/target.cpp, src/codegen.cpp, "never frees") brought up to date
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01Psc42moPLwP43Cmb9oZqSR
---
.github/workflows/ci.yml | 43 +++++++--
README.md | 35 +++++---
README.pl.md | 175 ++++++++++++++++++++-----------------
bootstrap/README.md | 19 ++--
docs/en/cheatsheet.md | 26 +++++-
docs/en/for-beginners.md | 7 +-
docs/en/for-engineers.md | 79 ++++++++++++-----
docs/en/for-technicians.md | 146 +++++++++++++++++++++++++++----
examples/simd.fch | 69 +++++++++++++++
runtime/finch_rt.c | 2 +-
src/cimport.fch | 7 +-
src/docs.fch | 2 +-
src/gen.fch | 18 ++--
src/main.fch | 5 +-
src/platform.fch | 1 -
15 files changed, 463 insertions(+), 171 deletions(-)
create mode 100644 examples/simd.fch
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1c85d92..f173eb8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,8 +1,8 @@
name: CI
# The compiler is written in Finch. It is built from bootstrap/.ll, its own LLVM IR (see bootstrap/README.md),
-# then from itself. Seeds: Linux x86-64 and ARM64 (tested here), Windows x86-64 (tested under Wine, and here),
-# macOS ARM64 and x86-64 (made with Apple's headers; only checked as IR until the macos job below passes).
+# then from itself, and tested, on every system it runs on: Linux x86-64 and ARM64, macOS ARM64 and x86-64, Windows x86-64.
+# A tag v* also makes the release: every package below is attached to it.
on:
push:
@@ -75,7 +75,14 @@ jobs:
fi
macos:
- runs-on: macos-15
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - { os: macos-15, arch: arm64 }
+ - { os: macos-15-intel, arch: x64 }
+ name: macos-${{ matrix.arch }}
+ runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install LLVM 21 (libclang)
@@ -92,11 +99,11 @@ jobs:
- name: Package
run: |
mkdir -p dist/finch && cp build/finch README.md LICENSE dist/finch/ && cp -r runtime std examples dist/finch/
- tar -czf finch-macos-arm64.tar.gz -C dist finch
+ tar -czf finch-macos-${{ matrix.arch }}.tar.gz -C dist finch
- uses: actions/upload-artifact@v4
with:
- name: finch-macos-arm64
- path: finch-macos-arm64.tar.gz
+ name: finch-macos-${{ matrix.arch }}
+ path: finch-macos-${{ matrix.arch }}.tar.gz
windows:
runs-on: windows-2022
@@ -154,3 +161,27 @@ jobs:
with:
name: finch-vscode
path: editors/vscode/finch-lang.vsix
+
+ release:
+ if: startsWith(github.ref, 'refs/tags/v')
+ needs: [linux, cross, macos, windows, vscode]
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ path: dist
+ merge-multiple: true
+ - name: Attach the packages to the release (made if it isn't there yet)
+ uses: softprops/action-gh-release@v2
+ with:
+ name: Finch ${{ github.ref_name }}
+ generate_release_notes: true
+ files: |
+ dist/finch-linux-x64.tar.gz
+ dist/finch-linux-arm64.tar.gz
+ dist/finch-macos-arm64.tar.gz
+ dist/finch-macos-x64.tar.gz
+ dist/finch-windows-x64.zip
+ dist/finch-lang.vsix
diff --git a/README.md b/README.md
index 7f62239..938d072 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,8 @@ fn main() {
Operator precedence that does not bite (`x & 1 == 0` means what it says).
- **Practical.** Maps, methods, errors as values (`int(s) or 0`, `try read_file(p)`), modules, any C
library, and a language server for your editor. Runs on Linux, macOS and Windows, on x86-64 and ARM64.
+- **SIMD built in.** `f32x8`, `i8x32`, `f64x4`: numbers side by side, added and multiplied all at once. LLVM turns them
+ into SSE, AVX2, AVX-512 or NEON instructions, so one piece of code is fast on a desktop, a laptop and ARM.
- **Errors that talk like a person:**
```
game.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert)
@@ -48,13 +50,17 @@ fn main() {
```
- **Any C library, directly.** `import "raylib.h"` + `link "raylib"`, and you can call it, structs by value
and callbacks included. Headers are read by libclang, so there is nothing to declare by hand.
-- **Written in itself.** The compiler is Finch code, and nothing else: `src/*.fch`, about 8.6k lines. It is built from a
- seed (its own LLVM IR, `bootstrap/`) and then from itself, and the two come out identical. Its first version, in C++,
- is kept on the `before-bootstrap` branch.
+- **Written in itself.** The compiler is Finch code, and nothing else: `src/*.fch`, about 12k lines. It is built from a
+ seed (its own LLVM IR, `bootstrap/`) and then from itself, and the two come out identical.
## Quick start
-**Building it** (Linux x86-64 or ARM64; needs `clang` and libclang, LLVM 21):
+**Ready builds** for Linux (x64, ARM64), macOS (Apple Silicon, Intel) and Windows (x64) are in the
+[releases](https://github.com/Nexoniarz/Finch/releases). Unpack one and keep `runtime/` and `std/` next to `finch`. What
+each system needs besides is in the [technicians' guide](docs/en/for-technicians.md#2-installing): clang and libclang
+from LLVM 21 on Linux, `brew install llvm@21` on a Mac, llvm-mingw on Windows.
+
+**Building it yourself** (on Linux, macOS, or Windows with Git Bash; needs `clang` and libclang, LLVM 21):
```sh
git clone https://github.com/Nexoniarz/Finch.git
@@ -64,13 +70,12 @@ bootstrap/build.sh # the compiler is build/finch: built from th
./build/finch run examples/hello.fch
./build/finch build examples/tour.fch -o tour && ./tour
-tests/run.sh # every program, every refusal, -g, a shared library, the language server
+tests/run.sh # every program, every expected error, -g, a shared library, the language server
```
-Ready builds of the compiler are in the [releases](https://github.com/Nexoniarz/Finch/releases) (Linux x64 and ARM64:
-unpack, keep `runtime/` and `std/` next to `finch`). **Programs for Windows, ARM64 and macOS** are built from Linux with
-`--target windows|arm64|macos` (see the [guide](docs/en/for-technicians.md#3-the-finch-command)); the compiler itself does not run on
-macOS or Windows yet (it needs a seed made with that system's headers: see [bootstrap/README.md](bootstrap/README.md)).
+The same `bootstrap/build.sh` works on a Mac (with Homebrew's `llvm@21`) and on Windows (with llvm-mingw): see
+[bootstrap/README.md](bootstrap/README.md). Programs for another system are built with `--target windows|linux|arm64|macos`
+(see the [guide](docs/en/for-technicians.md#building-for-another-system)).
Editors: **Kate** (`editors/kate/install.sh`), **VS Code** (the `finch-lang` `.vsix` from the release), or any
editor with LSP support (`finch lsp`).
@@ -91,13 +96,14 @@ Pick the guide that fits you:
| | |
|---|---|
| Variables | `x := 5` (inferred) or `int x = 5`; `const W = 640` |
-| Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, arrays `[]T`, maps `map[K]V`, pointers `ptr[T]` |
+| Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, arrays `[]T`, maps `map[K]V`, pointers `ptr[T]`, vectors `f32x8` `i8x32`… |
| Functions | `fn add(int a, int b) -> int { return a + b }` |
| Structs | `struct Point { … }` with one field per line (`int x`, `int y = 0`); create with `Point(1, 2)` or `Point(x: 1)` |
| Methods | `fn Point.move(int dx) { self.x += dx }` → `p.move(3)` |
| Arrays | `nums := [1, 2, 3]`, `nums.push(4)`, `nums[0]`, `nums.len`, `for i, n in nums { }` |
| Maps | `ages := ["ann": 31]`, `ages["bob"] = 25`, `ages.has("cy")`, `counts[w] += 1`, `for k, v in ages { }` |
| Errors | `fn parse(str s) -> int! { … return error("bad") }` → `parse(s) or 0`, `parse(s) or { … }`, `try parse(s)` |
+| Vectors (SIMD) | `v := f32x8(list, i)`, `v * 2.0 + w`, `v.sum()`, `v.dot(w)`, `min(v, 1.0)`, `v.store(list, i)`, `v[3]` |
| Text | `"hp \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
| Control | `if` / `else if` / `else` (also as a value), `while`, `for i in 0..10`, `break`, `continue`, `defer` |
| Memory | automatic for arrays, text and structs; `new(...)` / `free(...)` for your own heap structures |
@@ -108,7 +114,7 @@ Pick the guide that fits you:
| Tests | `test "name" { assert(x == 3) }` next to the code, run with `finch test` |
| C interop | `import "stdio.h"`, `link "glfw"`, `link "mine.c"` / `"glue.cpp"`, unions, `#define`s (also `(Color){…}`), `addr(fn)` for callbacks |
| I/O | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
-| Tools | `finch run/build/test/fmt/ir/lsp`, `finch.toml` for a project, a build cache, `--target windows/arm64/macos/android`, `-g` for gdb/lldb, `-O0`, the chain of calls on runtime errors |
+| Tools | `finch run/build/test/fmt/ir/lsp`, `finch.toml` for a project, a build cache, `--target windows/arm64/macos/android`, `--cpu native`, `-g` for gdb/lldb, `-O0`, the chain of calls on runtime errors |
## Examples
@@ -122,6 +128,7 @@ Pick the guide that fits you:
| [`examples/structs.fch`](examples/structs.fch) | structs, arrays of structs, a linked list with `new`/`free` |
| [`examples/types.fch`](examples/types.fch) | sized numbers and pointers |
| [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` |
+| [`examples/simd.fch`](examples/simd.fch) | vectors: a dot product eight numbers at a time, and the time it saves |
| [`examples/window.fch`](examples/window.fch) | a GLFW + OpenGL window |
| [`examples/opengl.fch`](examples/opengl.fch) | modern OpenGL 3.3: shaders, buffers, a spinning triangle |
| [`examples/vulkan.fch`](examples/vulkan.fch) | Vulkan: list the GPUs, their memory and queues, create a device |
@@ -151,14 +158,16 @@ Pick the guide that fits you:
- [x] A standard library in Finch: `json`, `path`, `time`, `proc`, `fs`
- [x] `const`, `if` as a value, `"\(x)"` in text, `"""` text, C unions and `#define` expressions
- [x] `test` blocks and `finch test`, `finch fmt`, `finch.toml`, a build cache, the chain of calls on runtime errors
-- [x] Threads (`import thread`) and atomics; C++ files; `--target android` (compiles; NDK linking and devices untried)
+- [x] Threads (`import thread`) and atomics; C++ files; `--target android`
+- [x] The compiler on every system it builds for: Linux, macOS (Apple Silicon and Intel), Windows; releases made by CI
+- [x] Vector types (`f32x8`, `i8x32`, …) for SIMD, and `--cpu native`
## Layout
```
src/ the compiler, in Finch: lexer, parser, AST, code generation (to LLVM IR as text), C import (libclang) and
the C calling conventions, the language server, the driver
-std/ the standard library, in Finch: json, path, time, proc, fs
+std/ the standard library, in Finch: json, path, time, proc, fs, thread
runtime/ the small C runtime linked into every program
bootstrap/ build.sh and the seeds: the compiler's own LLVM IR, one per system
examples/ example programs
diff --git a/README.pl.md b/README.pl.md
index 52b438d..098f317 100644
--- a/README.pl.md
+++ b/README.pl.md
@@ -5,8 +5,8 @@
-**Mały, zwinny, skuteczny.** Język programowania na LLVM ze składnią podobną do C,
-na tyle prosty, że nauczysz się go w godzinę, i tak szybki jak C.
+**Mały, szybki, celny.** Język programowania oparty na LLVM, ze składnią w stylu C. Nauczysz się go w godzinę,
+a programy działają tak szybko jak w C.
> 🇬🇧 [English version](README.md)
@@ -19,7 +19,7 @@ fn przeciwprostokatna(float a, float b) -> float {
fn main() {
imie := "Finch"
- print("Cześć od", imie, "!")
+ print("Cześć, tu", imie, "!")
for i in 1..4 {
print(i, "→", przeciwprostokatna(float(i), 1.0))
@@ -29,51 +29,58 @@ fn main() {
## Dlaczego Finch
-- **Prosty.** Bez średników, bez plików nagłówkowych, bez preprocesora. Każdą rzecz robi się na jeden sposób.
-- **Szybki.** Kompiluje się przez potok `-O2` z LLVM do kodu natywnego, bez garbage collectora.
- Rekurencyjne `fib(40)` trwa ok. 0,19 s (`clang -O2` na tym samym kodzie C: ok. 0,23 s), a kod na tablicach
- z kontrolą zakresu dorównuje C.
-- **Pamięć bez bólu.** Listy, teksty i struktury są wartościami: przypisanie kopiuje, a koniec bloku je zwalnia.
- Nie ma `free`, o którym można zapomnieć, nie ma użycia po zwolnieniu, a każdy test jest czysty pod valgrindem.
-- **Bezpieczniejszy niż C tam, gdzie to tanie.** Nie ma cichego zwężania typów ani mieszania liczb ze znakiem
- i bez znaku. Indeks poza zakresem, dzielenie przez zero i null zatrzymują program czytelnym błędem, zamiast
- powodować niezdefiniowane zachowanie. Priorytety operatorów nie gryzą (`x & 1 == 0` znaczy to, co widać).
-- **Praktyczny.** Mapy, metody, błędy jako wartości (`int(s) or 0`, `try read_file(p)`), moduły, każda
- biblioteka C i serwer języka dla twojego edytora. Działa na Linuksie, macOS i Windowsie, na x86-64 i ARM64.
-- **Błędy mówią po ludzku:**
+- **Prosty.** Nie ma średników, plików nagłówkowych ani preprocesora. Na każdą rzecz jest jeden sposób.
+- **Szybki.** Kod przechodzi przez optymalizacje LLVM (`-O2`) i staje się zwykłym programem, bez garbage collectora.
+ Rekurencyjne `fib(40)` liczy się ok. 0,19 s (ten sam kod w C z `clang -O2`: ok. 0,23 s), a pętle po tablicach
+ ze sprawdzaniem zakresu są tak samo szybkie jak w C.
+- **Pamięć bez kłopotów.** Listy, teksty i struktury są wartościami: przypisanie robi kopię, a koniec bloku sprząta.
+ Nie ma `free`, o którym da się zapomnieć, ani odwołań do zwolnionej pamięci. Valgrind nie znajduje błędów w żadnym teście.
+- **Bezpieczniejszy niż C tam, gdzie nic to nie kosztuje.** Liczby nie zmieniają po cichu typu, a liczb ze znakiem
+ i bez znaku nie da się przypadkiem pomieszać. Indeks poza tablicą, dzielenie przez zero czy `null` kończą program
+ jasnym komunikatem, a nie niezdefiniowanym zachowaniem. Kolejność działań jest taka, jakiej się spodziewasz
+ (`x & 1 == 0` znaczy dokładnie to, co widać).
+- **Praktyczny.** Mapy, metody, błędy jako wartości (`int(s) or 0`, `try read_file(p)`), moduły, dowolna biblioteka C
+ i serwer języka dla edytora. Działa na Linuksie, macOS i Windowsie, na procesorach x86-64 i ARM64.
+- **SIMD w języku.** `f32x8`, `i8x32`, `f64x4`: kilka liczb obok siebie, dodawanych i mnożonych naraz. LLVM zamienia je
+ na instrukcje SSE, AVX2, AVX-512 albo NEON, więc ten sam kod jest szybki na komputerze stacjonarnym, laptopie i ARM-ie.
+- **Komunikaty, które da się zrozumieć:**
```
gra.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert)
3 | i32 y = x
| ^
```
-- **Każda biblioteka C, bezpośrednio.** Piszesz `import "raylib.h"` i `link "raylib"`, i możesz z niej korzystać,
- także ze struktur przekazywanych przez wartość i z callbacków. Nagłówki czyta libclang, więc nie trzeba niczego deklarować ręcznie.
-- **Napisany w sobie.** Kompilator to kod w Finchu i nic więcej: `src/*.fch`, ok. 8,6 tys. linii. Buduje się go z ziarna
- (własny kod LLVM IR, `bootstrap/`), potem z samego siebie, a oba wyniki są identyczne. Pierwsza wersja, w C++,
- jest w gałęzi `before-bootstrap`.
+- **Biblioteki C bez pośredników.** Wystarczy `import "raylib.h"` i `link "raylib"`, żeby z niej korzystać, łącznie
+ ze strukturami przekazywanymi przez wartość i funkcjami zwrotnymi. Nagłówki czyta libclang, więc niczego nie trzeba
+ deklarować ręcznie.
+- **Napisany w samym sobie.** Kompilator to wyłącznie kod w Finchu: `src/*.fch`, ok. 12 tys. linii. Buduje się go
+ z pliku startowego (jego własnego kodu LLVM IR, w `bootstrap/`), a potem jeszcze raz nim samym. Oba wyniki są identyczne.
-## Szybki start
+## Na start
-**Budowanie** (Linux x86-64 lub ARM64; potrzebny `clang` i libclang, LLVM 21):
+**Gotowe kompilatory** na Linuksa (x64, ARM64), macOS (Apple Silicon, Intel) i Windowsa (x64) znajdziesz w
+[wydaniach](https://github.com/Nexoniarz/Finch/releases). Rozpakuj archiwum i trzymaj katalogi `runtime/` i `std/` obok
+pliku `finch`. Co jeszcze trzeba doinstalować, opisuje [przewodnik dla techników](docs/pl/dla-technikow.md#2-instalacja):
+na Linuksie clang i libclang z LLVM 21, na Macu `brew install llvm@21`, na Windowsie llvm-mingw.
+
+**Samodzielne budowanie** (na Linuksie, macOS albo Windowsie w Git Bash; potrzebne są `clang` i libclang z LLVM 21):
```sh
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
-nix-shell # albo zainstaluj samodzielnie clang + libclang (LLVM 21) + pkg-config
-bootstrap/build.sh # kompilator to build/finch: z ziarna, potem z samego siebie
+nix-shell # albo zainstaluj clang, libclang (LLVM 21) i pkg-config po swojemu
+bootstrap/build.sh # gotowy kompilator to build/finch
./build/finch run examples/hello.fch
./build/finch build examples/tour.fch -o tour && ./tour
-tests/run.sh # każdy program, każda odmowa, -g, biblioteka współdzielona, serwer języka
+tests/run.sh # wszystkie programy testowe, oczekiwane błędy, -g, biblioteka współdzielona, serwer języka
```
-Gotowe kompilatory są w [wydaniach](https://github.com/Nexoniarz/Finch/releases) (Linux x64 i ARM64: rozpakuj, zostaw
-`runtime/` i `std/` obok `finch`). **Programy dla Windowsa, ARM64 i macOS** buduje się z Linuksa przez
-`--target windows|arm64|macos`; sam kompilator nie działa jeszcze na macOS ani Windowsie (potrzebuje ziarna
-zrobionego z nagłówkami tego systemu: patrz [bootstrap/README.md](bootstrap/README.md)).
+Ten sam `bootstrap/build.sh` działa na Macu (z `llvm@21` z Homebrew) i na Windowsie (z llvm-mingw); szczegóły są w
+[bootstrap/README.md](bootstrap/README.md). Program na inny system zbudujesz opcją `--target windows|linux|arm64|macos`
+(zob. [przewodnik](docs/pl/dla-technikow.md#budowanie-na-inny-system)).
-Edytory: **Kate** (`editors/kate/install.sh`), **VS Code** (plik `.vsix` `finch-lang` z wydania) albo dowolny
-edytor z obsługą LSP (`finch lsp`).
+Edytory: **Kate** (`editors/kate/install.sh`), **VS Code** (plik `.vsix` `finch-lang` z wydania) albo każdy edytor,
+który obsługuje LSP (`finch lsp`).
## Dokumentacja
@@ -81,90 +88,94 @@ Wybierz przewodnik dla siebie:
| Przewodnik | Dla kogo | Polski | English |
|---|---|---|---|
-| **Początkujący** | Nigdy nie programowałeś: uczniowie, seniorzy, ciekawscy | [dla-poczatkujacych.md](docs/pl/dla-poczatkujacych.md) | [for-beginners.md](docs/en/for-beginners.md) |
-| **Technicy** | Znasz komputery, programowanie jest nowe: instalacja, cały język, biblioteki C, problemy | [dla-technikow.md](docs/pl/dla-technikow.md) | [for-technicians.md](docs/en/for-technicians.md) |
-| **Inżynierowie** | Wszystko: lekser, gramatyka, AST, reguły typów, tłumaczenie na IR, import przez libclang, ABI, wnętrze | [dla-inzynierow.md](docs/pl/dla-inzynierow.md) | [for-engineers.md](docs/en/for-engineers.md) |
-| **Ściąga** | Każda funkcja, metoda, operator i polecenie na jednej stronie | [sciaga.md](docs/pl/sciaga.md) | [cheatsheet.md](docs/en/cheatsheet.md) |
+| **Dla początkujących** | Dla tych, którzy nigdy nie programowali: uczniów, seniorów, ciekawych świata | [dla-poczatkujacych.md](docs/pl/dla-poczatkujacych.md) | [for-beginners.md](docs/en/for-beginners.md) |
+| **Dla techników** | Znasz komputery, ale programowanie jest dla ciebie nowe: instalacja, cały język, biblioteki C, rozwiązywanie problemów | [dla-technikow.md](docs/pl/dla-technikow.md) | [for-technicians.md](docs/en/for-technicians.md) |
+| **Dla inżynierów** | Wszystko: lekser, gramatyka, AST, reguły typów, generowanie IR, import przez libclang, ABI, budowa kompilatora | [dla-inzynierow.md](docs/pl/dla-inzynierow.md) | [for-engineers.md](docs/en/for-engineers.md) |
+| **Ściąga** | Wszystkie funkcje, metody, operatory i polecenia na jednej stronie | [sciaga.md](docs/pl/sciaga.md) | [cheatsheet.md](docs/en/cheatsheet.md) |
## Język w pigułce
| | |
|---|---|
-| Zmienne | `x := 5` (typ zgadnięty) albo `int x = 5`; `const W = 640` |
-| Typy | `int` `float` `bool` `char` `str`, z rozmiarem `i8`…`i64` `u8`…`u64` `f32` `f64`, tablice `[]T`, mapy `map[K]V`, wskaźniki `ptr[T]` |
-| Funkcje | `fn add(int a, int b) -> int { return a + b }` |
-| Struktury | `struct Punkt { … }` z jednym polem na linijkę (`int x`, `int y = 0`); tworzenie przez `Punkt(1, 2)` albo `Punkt(x: 1)` |
+| Zmienne | `x := 5` (typ wynika z wartości) albo `int x = 5`; `const W = 640` |
+| Typy | `int` `float` `bool` `char` `str`, o podanym rozmiarze: `i8`…`i64` `u8`…`u64` `f32` `f64`, tablice `[]T`, mapy `map[K]V`, wskaźniki `ptr[T]`, wektory `f32x8` `i8x32`… |
+| Funkcje | `fn dodaj(int a, int b) -> int { return a + b }` |
+| Struktury | `struct Punkt { … }`, jedno pole w linii (`int x`, `int y = 0`); tworzy się je przez `Punkt(1, 2)` albo `Punkt(x: 1)` |
| Metody | `fn Punkt.przesun(int dx) { self.x += dx }` → `p.przesun(3)` |
| Tablice | `liczby := [1, 2, 3]`, `liczby.push(4)`, `liczby[0]`, `liczby.len`, `for i, n in liczby { }` |
-| Mapy | `wiek := ["ania": 31]`, `wiek["bob"] = 25`, `wiek.has("cy")`, `ile[s] += 1`, `for k, v in wiek { }` |
-| Błędy | `fn parsuj(str s) -> int! { … return error("zły") }` → `parsuj(s) or 0`, `parsuj(s) or { … }`, `try parsuj(s)` |
-| Tekst | `"hp \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
-| Sterowanie | `if` / `else if` / `else` (też jako wartość), `while`, `for i in 0..10`, `break`, `continue`, `defer` |
-| Pamięć | automatyczna dla tablic, tekstów i struktur; `new(...)` / `free(...)` dla własnych struktur na stercie |
-| Enumy, match | `enum Kolor { Czerwony, Zielony }`, `match k { Czerwony -> ... else -> ... }` (także jako wartość) |
-| Generyki | `fn max[T](T a, T b) -> T`, `struct Stos[T]`, `Stos[int]()` |
+| Mapy | `wiek := ["ania": 31]`, `wiek["bob"] = 25`, `wiek.has("cyryl")`, `ile[slowo] += 1`, `for k, v in wiek { }` |
+| Błędy | `fn parsuj(str s) -> int! { … return error("zła liczba") }` → `parsuj(s) or 0`, `parsuj(s) or { … }`, `try parsuj(s)` |
+| Wektory (SIMD) | `v := f32x8(lista, i)`, `v * 2.0 + w`, `v.sum()`, `v.dot(w)`, `min(v, 1.0)`, `v.store(lista, i)`, `v[3]` |
+| Tekst | `"hp: \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
+| Sterowanie | `if` / `else if` / `else` (również jako wartość), `while`, `for i in 0..10`, `break`, `continue`, `defer` |
+| Pamięć | tablice, teksty i struktury zwalniają się same; `new(...)` / `free(...)` dla własnych struktur na stercie |
+| Enumy i `match` | `enum Kolor { Czerwony, Zielony }`, `match k { Czerwony -> ... else -> ... }` (również jako wartość) |
+| Typy generyczne | `fn max[T](T a, T b) -> T`, `struct Stos[T]`, `Stos[int]()` |
| Moduły | `import ksztalty` → `ksztalty.pole(p)`, `import ui/button as btn`; biblioteka standardowa: `json`, `path`, `time`, `proc`, `fs`, `thread` |
| Wątki | `thread.spawn(addr(praca), addr(dane))`, `t.join()`, `thread.mutex()`, `atomic_add(addr(n), 1)` |
-| Testy | `test "nazwa" { assert(x == 3) }` obok kodu, uruchamiane przez `finch test` |
-| Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"` / `"glue.cpp"`, unie, `#define` (też `(Color){…}`), `addr(fn)` dla callbacków |
-| Wejście/wyjście | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
-| Narzędzia | `finch run/build/test/fmt/ir/lsp`, `finch.toml` dla projektu, cache budowania, `--target windows/arm64/macos/android`, `-g` dla gdb/lldb, `-O0`, łańcuch wywołań przy błędach w czasie działania |
+| Testy | `test "nazwa" { assert(x == 3) }` tuż obok kodu, uruchamiane przez `finch test` |
+| Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"` / `"glue.cpp"`, unie, `#define` (także `(Color){…}`), `addr(fn)` jako funkcja zwrotna |
+| Wejście i wyjście | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
+| Narzędzia | `finch run/build/test/fmt/ir/lsp`, `finch.toml` dla projektu, pamięć podręczna kompilacji, `--target windows/arm64/macos/android`, `--cpu native`, `-g` dla gdb i lldb, `-O0`, ścieżka wywołań przy błędzie w trakcie działania |
## Przykłady
-| Plik | Pokazuje |
+| Plik | Co pokazuje |
|---|---|
-| [`examples/hello.fch`](examples/hello.fch) | najmniejszy program |
+| [`examples/hello.fch`](examples/hello.fch) | najkrótszy program |
| [`examples/tour.fch`](examples/tour.fch) | zmienne, funkcje, warunki, pętle |
-| [`examples/guess.fch`](examples/guess.fch) | gra w zgadywanie: wejście, pętle, `rand` z C |
+| [`examples/guess.fch`](examples/guess.fch) | gra w zgadywanie liczby: wczytywanie, pętle, `rand` z C |
| [`examples/todo.fch`](examples/todo.fch) | struktury, tablice, teksty i pliki |
| [`examples/wordcount.fch`](examples/wordcount.fch) | mapy, metody i błędy jako wartości: najczęstsze słowa w pliku |
-| [`examples/structs.fch`](examples/structs.fch) | struktury, tablice struktur, lista wiązana z `new`/`free` |
-| [`examples/types.fch`](examples/types.fch) | liczby z rozmiarem i wskaźniki |
+| [`examples/structs.fch`](examples/structs.fch) | struktury, tablice struktur, lista jednokierunkowa z `new`/`free` |
+| [`examples/types.fch`](examples/types.fch) | liczby o podanym rozmiarze i wskaźniki |
| [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` |
-| [`examples/window.fch`](examples/window.fch) | okno GLFW + OpenGL |
+| [`examples/simd.fch`](examples/simd.fch) | wektory: iloczyn skalarny liczony po osiem liczb naraz i zysk na czasie |
+| [`examples/window.fch`](examples/window.fch) | okno GLFW z OpenGL |
| [`examples/opengl.fch`](examples/opengl.fch) | nowoczesny OpenGL 3.3: shadery, bufory, obracający się trójkąt |
-| [`examples/vulkan.fch`](examples/vulkan.fch) | Vulkan: lista kart graficznych, ich pamięć i kolejki, tworzenie urządzenia |
-| [`examples/raylib.fch`](examples/raylib.fch) | raylib ze strukturami C przez wartość |
-| [`examples/llvm.fch`](examples/llvm.fch) | Finch budujący LLVM IR przez API LLVM-C |
-| [`src/`](src/) | kompilator napisany w Finchu (ziarno do jego zbudowania jest w [`bootstrap/`](bootstrap/)) |
+| [`examples/vulkan.fch`](examples/vulkan.fch) | Vulkan: lista kart graficznych z ich pamięcią i kolejkami, utworzenie urządzenia |
+| [`examples/raylib.fch`](examples/raylib.fch) | raylib i struktury C przekazywane przez wartość |
+| [`examples/llvm.fch`](examples/llvm.fch) | Finch, który buduje LLVM IR przez API LLVM-C |
+| [`src/`](src/) | kompilator napisany w Finchu (plik startowy do jego zbudowania leży w [`bootstrap/`](bootstrap/)) |
## Plan rozwoju
- [x] Lekser, parser, generowanie kodu LLVM, optymalizacja O2
-- [x] Typy, w tym liczby z rozmiarem, wskaźniki, konwersje; funkcje; `if`/`while`/`for`
-- [x] Czytelne błędy kompilacji; kontrole w czasie działania (dzielenie, null, zakres)
-- [x] Operatory bitowe z rozsądnymi priorytetami
-- [x] Nagłówki C przez libclang; `link` z pkg-config; porady przy błędach linkowania
-- [x] `struct`, także struktury C przez wartość (ABI System V) i callbacki
-- [x] Tablice z `.len`, kontrolą zakresu i metodami; `for x in lista`
-- [x] Pamięć: zwalnianie na końcu bloku, `defer`, `new`/`free`
+- [x] Typy, w tym liczby o podanym rozmiarze, wskaźniki i konwersje; funkcje; `if`/`while`/`for`
+- [x] Czytelne błędy kompilacji; sprawdzanie w trakcie działania (dzielenie, `null`, zakres tablicy)
+- [x] Operatory bitowe z rozsądną kolejnością działań
+- [x] Nagłówki C przez libclang; `link` z pkg-config; podpowiedzi przy błędach linkera
+- [x] `struct`, w tym struktury C przekazywane przez wartość (ABI System V) i funkcje zwrotne
+- [x] Tablice z `.len`, sprawdzaniem zakresu i metodami; `for x in lista`
+- [x] Pamięć: wartości zwalniane na końcu bloku, `defer`, `new`/`free`
- [x] Teksty: `+`, `.len`, indeksowanie, metody, `str(x)`, `int(s)`; `input()`; pliki
- [x] Moduły Fincha (`import nazwa`)
- [x] Informacje dla debuggera (`-g`)
-- [x] Bootstrap: kompilator Fincha napisany w Finchu, który buduje sam siebie
+- [x] Bootstrap: kompilator Fincha napisany w Finchu, który kompiluje sam siebie
- [x] Windows: `finch.exe`, programy `.exe`, ABI Microsoft x64; budowanie na Windowsa z Linuksa (`--target windows`)
-- [x] Serwer języka (`finch lsp`) i rozszerzenie VS Code
+- [x] Serwer języka (`finch lsp`) i rozszerzenie do VS Code
- [x] macOS i ARM64 (Linux i Apple Silicon) z ich konwencjami wywołań C; obsługa Kate
-- [x] Metody w strukturach, mapy, błędy jako wartości (`!`, `or`, `try`)
+- [x] Metody struktur, mapy, błędy jako wartości (`!`, `or`, `try`)
- [x] Typy generyczne (`fn max[T]`, `struct Stos[T]`), `match`, enumy
- [x] Biblioteka standardowa w Finchu: `json`, `path`, `time`, `proc`, `fs`
-- [x] `const`, `if` jako wartość, `"\(x)"` w tekście, tekst `"""`, unie C i wyrażenia `#define`
-- [x] Bloki `test` i `finch test`, `finch fmt`, `finch.toml`, cache budowania, łańcuch wywołań przy błędach
-- [x] Wątki (`import thread`) i operacje atomowe; pliki C++; `--target android` (kompiluje się; linkowanie z NDK i urządzenia niesprawdzone)
+- [x] `const`, `if` jako wartość, `"\(x)"` w tekście, tekst `"""`, unie C i wyrażenia w `#define`
+- [x] Bloki `test` i `finch test`, `finch fmt`, `finch.toml`, pamięć podręczna kompilacji, ścieżka wywołań przy błędach
+- [x] Wątki (`import thread`) i operacje atomowe; pliki C++; `--target android`
+- [x] Kompilator na każdym systemie, na który buduje: Linux, macOS (Apple Silicon i Intel), Windows; wydania robi CI
+- [x] Typy wektorowe (`f32x8`, `i8x32`, …) dla SIMD i `--cpu native`
-## Struktura
+## Co gdzie leży
```
-src/ kompilator, w Finchu: lekser, parser, AST, generowanie kodu (do LLVM IR jako tekst), import C (libclang)
- i konwencje wywołań C, serwer języka, sterownik
-std/ biblioteka standardowa, w Finchu: json, path, time, proc, fs
-runtime/ mały runtime w C dołączany do każdego programu
-bootstrap/ build.sh i ziarna: własny LLVM IR kompilatora, po jednym na system
+src/ kompilator w Finchu: lekser, parser, AST, generowanie kodu (LLVM IR jako tekst), import C (libclang)
+ i konwencje wywołań C, serwer języka, polecenie finch
+std/ biblioteka standardowa w Finchu: json, path, time, proc, fs, thread
+runtime/ mały runtime w C, dołączany do każdego programu
+bootstrap/ build.sh i pliki startowe: własny LLVM IR kompilatora, osobno dla każdego systemu
examples/ przykładowe programy
-editors/ rozszerzenie VS Code, podświetlanie i konfiguracja LSP dla Kate
-tests/ run/ new/ (wzorcowe wyjście), fail/ new/fail/ (oczekiwane błędy), run.sh, cross.sh, shared.sh, lsp_test.py
-docs/ przewodniki en/ i pl/ dla trzech grup odbiorców
+editors/ rozszerzenie do VS Code, podświetlanie składni i konfiguracja LSP dla Kate
+tests/ run/ new/ (wzorcowe wyniki), fail/ new/fail/ (oczekiwane błędy), run.sh, cross.sh, shared.sh, lsp_test.py
+docs/ przewodniki po angielsku (en/) i po polsku (pl/) dla trzech grup czytelników
```
## Licencja
diff --git a/bootstrap/README.md b/bootstrap/README.md
index f4825d8..3ea5dbe 100644
--- a/bootstrap/README.md
+++ b/bootstrap/README.md
@@ -3,12 +3,16 @@
Finch's compiler is written in Finch. To build it you need something that already runs Finch: the **seed**, which is
the compiler's own LLVM IR, one file per system:
-| File | System | Checked |
+| File | System | Built and tested in CI on |
|---|---|---|
-| `linux-x86_64.ll` | Linux x86-64 | built and tested here and in CI |
-| `linux-aarch64.ll` | Linux ARM64 | made with the ARM64 cross compiler; CI builds and tests it on an ARM64 runner |
-| `windows-x86_64.ll` | Windows x86-64 (MinGW: llvm-mingw's clang + libclang.dll) | built and run under Wine: it compiles itself, and every test program runs |
-| `macos-arm64.ll`, `macos-x86_64.ll` | macOS | made on Linux with the macOS 11.3 SDK; checked as IR only (nobody has run them yet) |
+| `linux-x86_64.ll` | Linux x86-64 | ubuntu-24.04 |
+| `linux-aarch64.ll` | Linux ARM64 | ubuntu-24.04-arm |
+| `macos-arm64.ll` | macOS, Apple Silicon | macos-15 |
+| `macos-x86_64.ll` | macOS, Intel | macos-15-intel |
+| `windows-x86_64.ll` | Windows x86-64 (MinGW: llvm-mingw's clang + libclang.dll) | windows-2022 |
+
+Each CI job builds the compiler from its seed, then from itself, runs every test, and packs the result; a tag `v*` attaches
+the packages to the release.
`bootstrap/build.sh` compiles the seed with `clang` (and the runtime, and libclang) into `build/finch0`; `finch0` builds
`src/` into `finch1`; `finch1` builds `src/` again into `build/finch`; the two must make the same IR, or the script stops.
@@ -41,5 +45,6 @@ official LLVM for Windows); `bootstrap/winbuild.sh` turns it into `finch.exe` fo
`bootstrap/build.sh` does the same from Git Bash: put llvm-mingw's `bin` in PATH and set `FINCH_LIBCLANG` (the path of
`libclang.dll`), `FINCH_LLVM_INCLUDE` (its `include` folder) and `FINCH_LDFLAGS=-L`.
-macOS: the seeds were made with the macOS 11.3 SDK; `brew install llvm@21` gives clang and libclang; the CI job sets
-`FINCH_CLANG`, `FINCH_LIBCLANG` and `FINCH_LLVM_INCLUDE`. If the macOS build fails, that job is where it shows first.
+macOS: the seeds were made with the macOS 11.3 SDK. `brew install llvm@21`
+gives clang and libclang; set `FINCH_CLANG`, `FINCH_LIBCLANG` and `FINCH_LLVM_INCLUDE` as the CI job does (the
+[technicians' guide](../docs/en/for-technicians.md#option-b-build-it-yourself) has the lines).
diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md
index d27262c..615856d 100644
--- a/docs/en/cheatsheet.md
+++ b/docs/en/cheatsheet.md
@@ -21,8 +21,9 @@ Everything in Finch on one page. For explanations, see the [guides](../../README
| `finch version` | version |
| `-l lib` | link a C library (like `link "lib"`) |
| `--target windows` / `linux` / `arm64` / `macos` / `android` / triple | build for another system (`android`: the NDK's clang from `ANDROID_NDK_HOME`, usually with `--shared`) |
-| `-g` | debug info (gdb, lldb, Visual Studio) |
+| `-g` | debug info (gdb, lldb) |
| `-O0` | no optimizations |
+| `--cpu native` / `x86-64-v3` / `apple-m1` … | use every instruction of that processor (AVX2, AVX-512, NEON); the program then needs one like it |
| `--trace` / `--no-trace` | runtime errors show the chain of calls; on for `run`, `test`, `-g`, `-O0`, off for a plain `build` (it costs speed) |
Environment: `FINCH_PATH` (folders with modules), `FINCH_CC` (the C compiler that links), `FINCH_CXX` (for `.cpp` files).
@@ -39,6 +40,7 @@ links = ["SDL3", "native/glue.cpp"] # libraries and your own .c / .cpp / .o / .a
libdirs = ["vendor/SDL3/lib"]
cflags = ["-DFAST"] # for your .c / .cpp files
ldflags = ["-Wl,-rpath,."]
+cpu = "native" # the same as --cpu native
[target.windows] # only when building for that system (also linux, macos, android, linux-arm64)
links = ["ws2_32"]
@@ -96,6 +98,7 @@ One statement per line, no semicolons, `{` on the same line, no brackets around
| `[]T` | array: `[1, 2, 3]`, `[]str names` |
| `map[K]V` | map: `["a": 1]`, `map[str]int counts`, `[:]` |
| `ptr[T]`, `ptr` | pointer, untyped pointer |
+| `f32x8`, `i8x32`, `f64x4`, `u16x16` … | vector: N numbers worked on at once (SIMD), see below |
| `Name` / `module.Name` | a struct |
Escapes: `\n \t \r \0 \\ \" \'`.
@@ -240,6 +243,27 @@ Fallible built-ins: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_fil
---
+## Vectors `f32x8` (SIMD)
+
+Types: `f32x4/8/16`, `f64x2/4/8`, `i8x16/32/64`, `i16x8/16/32`, `i32x4/8/16`, `i64x2/4/8`, and `u…` the same; at most 512 bits.
+
+| Code | Gives |
+|---|---|
+| `f32x8(x)` | `x` in every lane |
+| `f32x8(a, b, …, h)` | one number per lane |
+| `f32x8(list, i)` | `list[i]` … `list[i+7]` (a `[]f32` or `ptr[f32]`) |
+| `f32x8(v)` | an 8-lane vector converted lane by lane (`f32x8(ints)`, `i32x8(floats)`) |
+| `v + w`, `v * 2.0`, `-v` | lane by lane; a plain number counts for every lane (`/` only on floats) |
+| `& \| ^ << >> ~` | lane by lane, whole-number lanes |
+| `v == w`, `v != w` | bool: every lane equal / not |
+| `v[i]`, `v[i] = x`, `v.len` | a lane; the number of lanes |
+| `min(v, w)`, `max`, `abs(v)`, `clamp(v, lo, hi)` | lane by lane |
+| `v.sum()`, `v.min()`, `v.max()`, `v.dot(w)` | one number from all the lanes |
+| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()`, `v.fma(b, c)` | every lane (float lanes; `fma` = `v * b + c`) |
+| `v.store(list, i)` | lanes into `list[i]` … |
+
+---
+
## Text `str`
| Code | Gives |
diff --git a/docs/en/for-beginners.md b/docs/en/for-beginners.md
index fd3bedc..935e0b6 100644
--- a/docs/en/for-beginners.md
+++ b/docs/en/for-beginners.md
@@ -53,8 +53,9 @@ The computer reads your program from top to bottom, line by line.
## 2. Getting Finch ready
-Finch runs on **Linux** computers. If a teacher, parent or friend has already set it up
-for you, skip to the next section.
+Finch runs on **Linux**, **macOS** and **Windows** computers. If a teacher, parent or friend has already set it up
+for you, skip to the next section. (On a Mac or on Windows, ask someone to follow the
+[guide for technicians](for-technicians.md#2-installing): it has a ready-made Finch for both.)
Otherwise, open the **Terminal** program (a window where you type commands) and type
these lines one by one, pressing **Enter** after each:
@@ -76,7 +77,7 @@ To check it works, type:
./build/finch version
```
-You should see something like `Finch 2.5.0`.
+You should see something like `Finch 2.6.0`.
---
diff --git a/docs/en/for-engineers.md b/docs/en/for-engineers.md
index 56ce0a9..e018e5a 100644
--- a/docs/en/for-engineers.md
+++ b/docs/en/for-engineers.md
@@ -1,13 +1,11 @@
# Finch for Engineers
-**The internals of Finch 2.5, whose compiler is written in Finch.** How source becomes tokens, an AST and LLVM IR *as text*,
+**The internals of Finch 2.6, whose compiler is written in Finch.** How source becomes tokens, an AST and LLVM IR *as text*,
which `clang` compiles and links with a small C runtime; how C headers are read with libclang from Finch itself and how
the C calling conventions are reproduced by hand; the type and ownership rules; how the compiler builds itself from a
seed; and the language server.
Assumed: you can read Finch (see the [technicians' guide](for-technicians.md)) and have at least seen LLVM IR.
-The compiler's first version, in C++ on LLVM's C++ API, is kept on the `before-bootstrap` branch; this guide describes
-the Finch one.
> 🇵🇱 Polska wersja: [dla-inzynierow.md](../pl/dla-inzynierow.md)
@@ -34,7 +32,7 @@ the Finch one.
## 1. Architecture
The compiler has no LLVM library inside it. It writes LLVM IR as **text** and lets `clang` do optimisation, code
-generation and linking. That is what makes it small (about 8k lines) and what lets it be written in Finch itself.
+generation and linking. That is what makes it small (about 12k lines) and what lets it be written in Finch itself.
```
main.fch ─► loader ── lex ── parse ──► Program (+ every imported module, recursively)
@@ -60,7 +58,7 @@ Design decisions that shape the rest:
Finch type (a string kind: `int`, `f32`, `str`, `array`, `map`, `struct`, `enum`, `named`…) and flags.
- **Let clang do the work.** Locals are `alloca`s promoted by `mem2reg`. Optimisation is clang's `-O2` (or `-O0` with `-g`).
- **Values, not references.** Arrays, strings and structs have value semantics; ownership is decided at compile time (§8).
-- **A tiny C runtime** (`runtime/finch_rt.c`, ~760 lines) does what is tedious in IR: strings, array growth, maps, input,
+- **A small C runtime** (`runtime/finch_rt.c`, ~1150 lines) does what is tedious in IR: strings, array growth, maps, input,
files, panics. It is compiled once and cached in `~/.cache/finch`.
---
@@ -70,17 +68,17 @@ Design decisions that shape the rest:
| File | Lines | Responsibility |
|---|---|---|
| `src/lexer.fch` | 470 | Tokens, positions, `"""` text, `"\(x)"` turned into `("…" + str(x) + "…")` tokens |
-| `src/ast.fch` | 120 | One `Node` struct with a `kind` string, children, type names, positions (`line`, `endLine`) |
+| `src/ast.fch` | 170 | One `Node` struct with a `kind` string, children, type names, positions (`line`, `endLine`) |
| `src/parser.fch` | 910 | Recursive descent: declarations, statements, expressions, generics (`[T]`), `enum`, `match`, `export`, `link`, `const`, `test "…" { }` |
-| `src/gen.fch` | 6450 | Type check + IR: structs, functions, methods, scopes and drops, statements, expressions, built-ins, maps, errors as values, generics (monomorphisation), enums and `match`, `-g` debug metadata, the `index` mode |
+| `src/gen.fch` | 7030 | Type check + IR: structs, functions, methods, scopes and drops, statements, expressions, built-ins, maps, errors as values, generics (monomorphisation), enums and `match`, vectors (SIMD), `-g` debug metadata, the `index` mode |
| `src/cimport.fch` | 790 | libclang: functions, structs and unions (fields, offsets, padding), enums, constants, globals, macros (also struct-valued) |
-| `src/platform.fch` | 90 | `Target`: triple, data layout, C compiler, signedness of `char` per system |
-| `src/linker.fch` | 430 | The cache (runtime, program object, C/C++ files), libraries (`link`, pkg-config), link-error advice |
-| `src/main.fch` | 515 | Driver: CLI, loader with module lookup and overlays, `build/run/test/fmt/ir/index/lsp` |
+| `src/platform.fch` | 120 | `Target`: triple, data layout, C compiler, signedness of `char` per system |
+| `src/linker.fch` | 480 | The cache (runtime, program object, C/C++ files), libraries (`link`, pkg-config), link-error advice |
+| `src/main.fch` | 530 | Driver: CLI, loader with module lookup and overlays, `build/run/test/fmt/ir/index/lsp` |
| `src/manifest.fch` | 240 | `finch.toml`: a small TOML reader, per-target lists |
| `src/fmt.fch` | 225 | `finch fmt` and LSP formatting |
-| `src/lsp.fch` | 910 | The language server (§11) |
-| `src/docs.fch` | 110 | One description of the built-ins, shared by hover and completion |
+| `src/lsp.fch` | 950 | The language server (§11) |
+| `src/docs.fch` | 140 | One description of the built-ins, shared by hover and completion |
| `std/*.fch` | 1040 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`), `fs`, `thread` |
| `runtime/finch_rt.c` | 1150 | The C runtime: text, arrays, maps, files, float printing, fs, threads, the test runner, the call chain |
| `bootstrap/` | | `build.sh`, `install.sh`, the seeds (§10) |
@@ -168,6 +166,9 @@ offset 0, and a field is read through memory. They can't be passed by value to C
`ir` prints the IR; `index` prints the records the language server uses (§11); `--shared` makes a shared library of the
`export fn`s.
+`--cpu ` (or `cpu` in `finch.toml`) adds `-march=` on x86-64 or `-mcpu=` on ARM64 to the clang that
+compiles the IR (not the runtime); it is part of the cache key.
+
`--target windows|linux|arm64|macos|android|` changes the triple and data layout, the C `char` signedness, the ABI rules
above, the OS variant of modules (`name_windows.fch`), and the C compiler: `FINCH_CC` or `clang --target=…`. Linking
asks `pkg-config` for flags for `link "lib"`; when the linker fails the output is analysed and the likely missing
@@ -225,6 +226,7 @@ statement, so their shape (and the `"""` dedent) is kept.
| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64 }` (entries, len, used, cap, index, icap; see src/gen.fch) |
| `ptr`, `ptr[T]`, `null` | `ptr` (opaque) |
| `[N]T` | `[N x T]` |
+| `f32x8`, `i8x32`, … | `<8 x float>`, `<32 x i8>` (§7, Vectors) |
| Finch struct | named struct type, natural layout |
| a method's `self` | `ptr` (the first parameter) |
| the result of `-> T!` | `{ i1 ok, T value, str error }` (`i8` in place of `T` for `-> !`) |
@@ -235,11 +237,16 @@ Signedness lives only in the Finch type and selects `sdiv/udiv`, `ashr/lshr`, `i
### Values
-```cpp
-struct Value_ { Value *v; FType type; bool literal; bool fresh; };
+```c
+struct Val {
+ str v // the IR operand: %t12, 42, @.str.3, zeroinitializer
+ ast.Type ty
+ bool fresh
+ bool lit
+}
```
-- **`literal`**: a number written in the code or an imported C constant. It may become any integer type
+- **`lit`**: a number written in the code or an imported C constant. It may become any integer type
it fits (`fits()`), or `f32`. It survives unary minus and literal-only arithmetic that folds to a
constant; explicit conversions clear it (`u8(250)` is a typed constant, so `u8(250) + 10` wraps to 4).
- **`fresh`**: an owning value (str/array/owning struct) that nobody holds yet: the result of a call,
@@ -281,6 +288,38 @@ Pointers/null: `icmp eq/ne ptr` only. `str`: `finch_str_eq` (length + `memcmp`)
`finch_str_cmp` for ordering. Floats: `oeq olt ole ogt oge` and `une` for `!=`. Integers/char by
signedness (char unsigned). Bool: equality only. Arrays/structs: error.
+### Vectors
+
+`ast.vecType(name)` recognises `x` (lanes `i8`…`u64`, `f32`, `f64`; N = 2…64; at most 512 bits) and gives
+`Type(kind: "vec", elem: [lane], count: N)`, which is an LLVM vector ``. The parser takes such a name wherever a
+type goes; a call `f32x8(…)` goes to `vecMake()`.
+
+| Finch | IR |
+|---|---|
+| `f32x8(x)` | `insertelement` into lane 0 + `shufflevector … zeroinitializer` (a splat) |
+| `f32x8(a, …, h)` | a chain of `insertelement` |
+| `f32x8(list, i)` | bounds check of `i … i+7`, then `load <8 x float>, ptr …, align 4` (the lane's alignment) |
+| `f32x8(i32x8 v)` | `sitofp`, `fptosi`, `sext`, `trunc`… on the whole vector |
+| `v + w`, `v * 2.0` | `fadd`/`add`… on vectors; a scalar operand is coerced to the lane type, then splatted |
+| `v[i]` | a place: `getelementptr , ptr %v, i64 i` after a bounds check; or `extractelement` on a temporary |
+| `v == w` | `fcmp oeq`/`icmp eq` → `` → `llvm.vector.reduce.and` |
+| `min`, `max`, `abs` | `llvm.minnum`/`maxnum`, `llvm.smin`/`umin`…, `llvm.fabs`/`llvm.abs` |
+| `v.sum()`, `.min()`, `.max()` | `llvm.vector.reduce.fadd` (with `reassoc`), `.add`, `.fmin`, `.smin`… |
+| `v.sqrt()`, `.floor()`, `.ceil()`, `.round()` | the `llvm.*` intrinsic of that name |
+| `v.fma(b, c)` | `llvm.fmuladd` (fused where the target has FMA; `llvm.fma` would be a libm call per lane without it) |
+| `v.store(list, i)` | bounds check, `store <8 x float> …, align 4` |
+
+Integer `/` and `%` are refused: neither x86 nor ARM has a lane-wise integer divide, and LLVM would scalarise it.
+Ordering comparisons are refused because their result would be a mask, which has no Finch type.
+
+**Alignment.** LLVM gives `<8 x float>` an ABI alignment of 32 (its size), but `malloc` and Finch's `alloca … align 16`
+promise only 16. `llty()` records every vector wider than 16 bytes in `g.wide`, and `resolveStructBody` records a struct
+that holds one; `load()` and `store()` add `, align 16` for those types. Without it LLVM may emit `vmovaps` on a 16-byte
+aligned address and crash.
+
+**Which instructions** is clang's choice: `--cpu ` adds `-march=` (x86-64) or `-mcpu=` (ARM64) to the
+IR's compile, so the same IR becomes SSE2, AVX2 + FMA, AVX-512 or NEON code. Lanes wider than the registers are split.
+
---
---
@@ -428,8 +467,9 @@ and stops unless `finch1` and `finch` produce byte-identical IR (a fixpoint: the
whatever built it). `bootstrap/build.sh seed` rewrites the seed from the current compiler; do it whenever the compiler's
own source starts to use a feature the committed seed lacks.
-A seed is per system because the C calling convention for the libclang calls is baked into it (§5).
-Linux x86-64 and Linux ARM64 have seeds. macOS and Windows would need seeds made with Apple's and Microsoft's headers.
+A seed is per system because the C calling convention for the libclang calls is baked into it (§5). There are seeds
+for Linux x86-64 and ARM64, macOS ARM64 and x86-64 (made with Apple's SDK headers), and Windows x86-64 (made with
+llvm-mingw's headers). CI builds the compiler from each one on that system and runs the tests there.
---
@@ -518,13 +558,10 @@ evaluated twice and temporaries are never pointed to.
## 13. Known limitations
-- The compiler runs on Linux x86-64 and ARM64 only (no seed for macOS or Windows yet). Programs can be built for all three.
-- macOS output is only checked as IR; Windows and ARM64 are run under Wine and qemu in CI.
- One error per run, no recovery.
- `-g` debug info is best with `-O0`.
-- The driver uses `sh` utilities (`rm`, `mkdir`), so it needs a POSIX shell.
- Generics are monomorphised (code size grows with instantiations); there are no trait bounds, a wrong use is an error
inside the instantiated body.
- A failed test's owned memory is not freed (the test runner `longjmp`s out of it).
-- `--target android` is checked to the object file only; linking needs the NDK and has not been run on a device.
+- Vectors have no lane masks (`v < w`), shuffles or integer division.
diff --git a/docs/en/for-technicians.md b/docs/en/for-technicians.md
index 9e41283..712dea1 100644
--- a/docs/en/for-technicians.md
+++ b/docs/en/for-technicians.md
@@ -41,6 +41,7 @@ compiler works inside. For that, see the [guide for engineers](for-engineers.md)
28. [A project: finch.toml](#28-a-project-finchtoml)
29. [Threads](#29-threads)
30. [C++, Rust and Android](#30-c-rust-and-android)
+31. [Vectors (SIMD)](#31-vectors-simd)
---
@@ -67,19 +68,24 @@ messages that tell you how to fix the problem.
## 2. Installing
-The Finch compiler is written in Finch (see the [engineers' guide](for-engineers.md)). It runs on **Linux**
-(x86-64 and ARM64) and builds programs for **Linux, Windows and macOS**. It needs **clang 21** and **libclang**
-(LLVM 21) to read C headers and to compile its output.
+The Finch compiler is written in Finch (see the [engineers' guide](for-engineers.md)). It runs on **Linux** (x86-64 and
+ARM64), **macOS** (Apple Silicon and Intel) and **Windows** (x86-64), and builds programs for all of them. On every
+system it needs **clang** (it compiles each program with it) and **libclang** from LLVM 21 (it reads C headers with it).
-The compiler does not run on macOS or Windows yet; it needs a seed made with that system's headers
-([bootstrap/README.md](../../bootstrap/README.md) says why). Until then, build your Windows and macOS programs from Linux
-with `--target` (below), or use the older C++ compiler from the `before-bootstrap` branch.
+### Option A: a ready build
-### Option A: a ready build (Linux)
+Download the archive for your system from the [releases](https://github.com/Nexoniarz/Finch/releases) and unpack it.
+Keep `runtime/` and `std/` next to the `finch` file (or install it, below).
-Download `finch-linux-x64.tar.gz` or `finch-linux-arm64.tar.gz` from the
-[releases](https://github.com/Nexoniarz/Finch/releases) and unpack it. Keep `runtime/` and `std/` next to the `finch` file
-(or install it, below). You also need `clang` 21 and its libclang library:
+| System | Archive | Also install |
+|---|---|---|
+| Linux x86-64 | `finch-linux-x64.tar.gz` | clang 21 and libclang (table below) |
+| Linux ARM64 (Raspberry Pi 4/5, Graviton…) | `finch-linux-arm64.tar.gz` | the same |
+| macOS, Apple Silicon | `finch-macos-arm64.tar.gz` | `xcode-select --install` (clang and the system headers), `brew install llvm@21` (libclang) |
+| macOS, Intel | `finch-macos-x64.tar.gz` | the same |
+| Windows x64 | `finch-windows-x64.zip` (`finch.exe` with `libclang.dll`) | [llvm-mingw](https://github.com/mstorsjo/llvm-mingw/releases) (the `ucrt-x86_64` zip): unpack it and put its `bin` folder on your PATH |
+
+On Linux:
| Distribution | Packages |
|---|---|
@@ -88,12 +94,15 @@ Download `finch-linux-x64.tar.gz` or `finch-linux-arm64.tar.gz` from the
| Arch | `clang pkgconf` |
| Nix | `nix-shell` in the repository gives all of it |
+On Windows, llvm-mingw gives Finch `clang`, the C headers and the linker in one folder; Windows programs built with it
+need nothing else to run. Keep `libclang.dll` next to `finch.exe`.
+
### Option B: build it yourself
```sh
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
-nix-shell # clang, libclang, pkg-config (or install them with your distribution)
+nix-shell # Linux: clang, libclang, pkg-config (or install them with your distribution)
bootstrap/build.sh # build/finch
./build/finch version
```
@@ -102,6 +111,26 @@ bootstrap/build.sh # build/finch
`src/` with it, builds `src/` again with the result and checks the two are identical. If libclang isn't where the linker
looks, tell it: `FINCH_LIBCLANG="-L/usr/lib/llvm-21/lib -lclang-21" bootstrap/build.sh`.
+On a Mac, with Homebrew's LLVM:
+
+```sh
+brew install llvm@21
+export FINCH_CLANG="$(brew --prefix llvm@21)/bin/clang"
+export FINCH_LIBCLANG="-L$(brew --prefix llvm@21)/lib -lclang"
+export FINCH_LLVM_INCLUDE="$(brew --prefix llvm@21)/include"
+bootstrap/build.sh
+```
+
+On Windows, in Git Bash, with llvm-mingw on the PATH and the official LLVM for Windows (for `libclang.dll` and its headers)
+unpacked to `C:/llvm`:
+
+```sh
+export FINCH_LIBCLANG="C:/llvm/bin/libclang.dll"
+export FINCH_LLVM_INCLUDE="C:/llvm/include"
+export FINCH_LDFLAGS="-LC:/llvm/bin"
+bootstrap/build.sh # build/finch.exe, with libclang.dll copied next to it
+```
+
### Building for another system
`--target` builds a program for another system. Finch needs that system's C compiler to link:
@@ -151,7 +180,9 @@ options:
-l link a C library, same as link "lib" in the file
-I look for C headers there too
--target build for another system: windows, linux, arm64, macos, android, or an LLVM triple
- -g add debug info (for gdb / lldb / Visual Studio)
+ --cpu use every instruction of a processor: native (this one), x86-64-v3 (AVX2),
+ x86-64-v4 (AVX-512), apple-m1, ... (section 31)
+ -g add debug info (for gdb and lldb)
-O0 skip optimizations
--trace runtime errors show the chain of calls (on anyway for run, test, -g and -O0)
--no-trace they don't (a plain finch build doesn't: keeping the chain makes calls slower)
@@ -946,8 +977,7 @@ gdb ./game
```
`-g` adds debug info (lines, functions, variables, struct fields). `-O0` keeps every variable
-visible; without it the optimizer may remove some. On Windows, `-g` writes CodeView, which the
-Visual Studio debugger and WinDbg read.
+visible; without it the optimizer may remove some. On Windows the debug info is DWARF as well: use the `lldb` that comes with llvm-mingw, or gdb.
---
@@ -959,10 +989,11 @@ Visual Studio debugger and WinDbg read.
| `… come from "x.h", but its library isn't linked` | Add `link "name"` at the top of the file. |
| `the library 'x' wasn't found` | Not installed, or a wrong name. On Nix, add it to `shell.nix` and run inside `nix-shell`. |
| `the C function 'f' can't be used from Finch yet` | It uses a union, bit fields or another unsupported type by value. Look for a variant that takes pointers. |
+| `--cpu native`: the program stops with `Illegal instruction` on another computer | It was built for the processor that built it. Build without `--cpu`, or with an older one (`x86-64-v2`). |
| `can't find the module 'x'` | Put `x.fch` next to the importing file, or set `FINCH_PATH`. |
| `couldn't build the Finch runtime` | No working C compiler. Install gcc or clang, or set `CC`. |
| A library is not found when the program starts | It was linked from a folder the system doesn't search. Run inside the same `nix-shell`, or install it system-wide. |
-| Windows: `finch` can't link (`LNK…` errors about `libcmt`, `kernel32`) | Install Visual Studio Build Tools with *Desktop development with C++*. |
+| Windows: `no C compiler found to link with ('clang')` | Put llvm-mingw's `bin` folder on your PATH (section 2). |
| Windows: `finch.exe` doesn't start (missing `libclang.dll`) | Keep `libclang.dll` next to `finch.exe`. |
| macOS: `finch` doesn't start (`Library not loaded: …libLLVM…`) | `brew install llvm@21`. |
| macOS: `can't find the C header 'stdio.h'` | Install the command line tools: `xcode-select --install`. |
@@ -1134,7 +1165,7 @@ Not there yet:
- Interfaces.
- Unicode-aware text: `.len`, `s[i]` and `upper()` work on bytes / ASCII.
- Checks that stop two threads from changing the same array at once (use a mutex).
-- Windows on ARM64, 32-bit systems, WebAssembly. `--target android` makes code for Android, but linking with the NDK and running on a device are untried.
+- Windows on ARM64, 32-bit systems, WebAssembly.
### Warnings about big copies
@@ -1193,6 +1224,7 @@ links = ["SDL3", "native/glue.cpp"] # libraries, and your own .c / .cpp / .o /
libdirs = ["vendor/SDL3/lib"] # folders with libraries
cflags = ["-DFAST"] # for your .c / .cpp files
ldflags = ["-Wl,-rpath,$ORIGIN"] # for the linker
+cpu = "native" # the same as --cpu native (section 31)
[target.windows] # added only when building for Windows
links = ["ws2_32"]
@@ -1292,6 +1324,82 @@ Write the header (`float path_len(float x, float y);`, or let `cbindgen` write i
**Android**: `finch build game.fch --target android --shared` makes `libgame.so` for 64-bit ARM Android (API 24 and
up) with the NDK's clang, found through `ANDROID_NDK_HOME` (or `FINCH_CC`). Load it from an app, for example through
-SDL3's Android project (the library's `SDL_main` is an `export fn`). This is new and only partly checked: Finch's
-output compiles for Android (thread-local data goes through the emulated TLS older Android needs), but linking with the
-NDK and running on a device have not been tried yet.
+SDL3's Android project (the library's `SDL_main` is an `export fn`). Thread-local data goes through the emulated TLS
+that older Android versions need.
+
+---
+
+## 31. Vectors (SIMD)
+
+A processor can add or multiply several numbers with one instruction: 4 floats at once with SSE, 8 with AVX2, 16 with
+AVX-512, 4 with ARM's NEON. That is SIMD (single instruction, multiple data), and it is how image filters, audio, physics
+and neural networks get their speed. In Finch it is a type: **`f32x8` is 8 `f32` side by side**.
+
+```c
+a := f32x4(1.0, 2.0, 3.0, 4.0)
+b := f32x4(0.5) // 0.5 in every lane
+print(a + b) // f32x4(1.5, 2.5, 3.5, 4.5)
+print(a * 2.0) // a plain number counts for every lane: f32x4(2, 4, 6, 8)
+print(a.sum(), a[2], a.len) // 10 3 4
+```
+
+**The types** are a number type, `x`, and how many lanes: `f32x4` `f32x8` `f32x16`, `f64x2` `f64x4` `f64x8`,
+`i8x16` `i8x32` `i8x64`, `i16x8` `i16x16`, `i32x4` `i32x8` `i32x16`, `i64x2` `i64x4`, and the same with `u` (unsigned).
+2 to 64 lanes, at most 512 bits in all. A vector is a plain value like an `int`: it goes into variables, struct fields,
+arrays, maps and function parameters, and `print` and `str` show it.
+
+**Making one:**
+
+| | |
+|---|---|
+| `f32x8(x)` | `x` in every lane |
+| `f32x8(a, b, c, d, e, f, g, h)` | one number per lane |
+| `f32x8(list, i)` | `list[i]` … `list[i+7]` from a `[]f32` (or through a `ptr[f32]`); stops the program if they aren't all in the list |
+| `f32x8(v)` | another 8-lane vector converted lane by lane: `f32x8(ints)`, `i32x8(floats)` (cuts the fractions), `i16x8(i32s)` |
+
+**Working with it:**
+
+| | |
+|---|---|
+| `+ - * /` | lane by lane (`/` on floats only: x86 and ARM can't divide whole numbers lane by lane) |
+| `& \| ^ << >> ~` | lane by lane, on whole-number lanes |
+| `-v` | every lane negated |
+| `v == w`, `v != w` | `true` if every lane is equal / if any differs |
+| `v[i]`, `v[i] = x`, `v.len` | one lane; the number of lanes |
+| `min(v, w)`, `max(v, w)`, `abs(v)`, `clamp(v, lo, hi)` | lane by lane; `lo`, `hi` and `w` can be plain numbers |
+| `v.sum()`, `v.min()`, `v.max()` | all the lanes made into one number |
+| `v.dot(w)` | `(v * w).sum()` |
+| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | every lane (float lanes) |
+| `v.fma(b, c)` | `v * b + c`, as one instruction where the processor has one |
+| `v.store(list, i)` | writes the lanes to `list[i]` … (or through a `ptr`) |
+
+`<` and the other comparisons would give an answer per lane, so they aren't there; use `min`, `max` or `v[i] < w[i]`.
+
+**A real loop.** The usual shape: as many whole vectors as fit, then the rest one by one.
+
+```c
+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
+}
+```
+
+`examples/simd.fch` times this against the plain loop.
+
+**Which instructions.** By default a program runs on every processor of its system (on x86-64 that means SSE2, so an
+`f32x8` is done as two halves). `--cpu native` (or `cpu = "native"` in `finch.toml`) lets LLVM use everything the processor
+that builds it has: AVX2 and FMA, AVX-512, or NEON's newer parts on ARM. Such a program needs a processor like that one to
+run. To pick a level instead: `--cpu x86-64-v3` (AVX2, most PCs since 2015), `--cpu x86-64-v4` (AVX-512), `--cpu apple-m1`.
+
+Lanes wider than the processor's registers are split by LLVM, and the same code builds for x86-64 and ARM64 alike.
+`v.sum()` on float lanes adds them in whatever order is fastest, so the last digits can differ from adding them one by one.
diff --git a/examples/simd.fch b/examples/simd.fch
new file mode 100644
index 0000000..691227f
--- /dev/null
+++ b/examples/simd.fch
@@ -0,0 +1,69 @@
+// Vectors (SIMD): eight f32 at once instead of one.
+//
+// finch run examples/simd.fch runs on any x86-64 or ARM64 processor
+// finch run examples/simd.fch --cpu native uses all of this one's instructions (AVX2, AVX-512, NEON)
+import time
+
+// one number at a time
+fn dot_plain([]f32 a, []f32 b) -> f32 {
+ f32 total = 0.0
+ for i in 0..a.len {
+ total += a[i] * b[i]
+ }
+ return total
+}
+
+// eight at a time: f32x8(list, i) takes list[i] ... list[i+7], fma multiplies and adds in one step
+fn dot_simd([]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 { // the few that are left
+ total += a[i] * b[i]
+ i += 1
+ }
+ return total
+}
+
+// brighten every color: the same work on every lane, with min keeping it below 1
+fn brighten([]f32 pixels, f32 amount) {
+ i := 0
+ while i + 8 <= pixels.len {
+ v := min(f32x8(pixels, i) * (1.0 + amount), 1.0)
+ v.store(pixels, i)
+ i += 8
+ }
+}
+
+fn main() {
+ []f32 a
+ []f32 b
+ for i in 0..4_000_000 {
+ a.push(f32(i % 100) / 100.0)
+ b.push(0.5)
+ }
+
+ start := time.millis()
+ x := 0.0
+ for round in 0..20 {
+ x = float(dot_plain(a, b))
+ }
+ plain := time.millis() - start
+
+ start = time.millis()
+ y := 0.0
+ for round in 0..20 {
+ y = float(dot_simd(a, b))
+ }
+ simd := time.millis() - start
+
+ print("one at a time: ", str(x, 1), "in", plain, "ms")
+ print("eight at a time:", str(y, 1), "in", simd, "ms")
+
+ brighten(a, 0.5)
+ print("brightened:", a[10], a[90], a.len)
+}
diff --git a/runtime/finch_rt.c b/runtime/finch_rt.c
index ab806a3..e9a63fb 100644
--- a/runtime/finch_rt.c
+++ b/runtime/finch_rt.c
@@ -2,7 +2,7 @@
// It is embedded in the compiler, compiled once into a cache folder, and linked
// statically into every Finch program. Everything here is plain C with no state.
//
-// Layout shared with the compiler (src/codegen.cpp):
+// Layout shared with the compiler (src/gen.fch):
// str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated
// cap > 0 : heap memory owned by the value (cap bytes allocated)
// cap == 0 : a string literal, valid forever, never written to
diff --git a/src/cimport.fch b/src/cimport.fch
index 8d1701f..5d8fa14 100644
--- a/src/cimport.fch
+++ b/src/cimport.fch
@@ -1,10 +1,9 @@
// C headers, read through libclang, written in Finch.
//
// What `import "stdio.h"` teaches the compiler: the C functions, the constants (enum values and
-// plain #define numbers), the extern variables (stdout) and the structs. The same job as
-// src/cimport.cpp, done by calling libclang's C API through Finch's own C import -- which is
-// what makes this the part that closes the bootstrap: to compile this file the self-hosted
-// compiler has to read libclang's headers with this very code.
+// plain #define numbers), the extern variables (stdout) and the structs. It calls libclang's C API
+// through Finch's own C import -- so to compile this file, the compiler reads libclang's headers
+// with this very code.
//
// C structs by value appear in types as the named type `module "C"`.
import ast
diff --git a/src/docs.fch b/src/docs.fch
index 04dc13e..271660a 100644
--- a/src/docs.fch
+++ b/src/docs.fch
@@ -107,7 +107,7 @@ fn vecMethods() -> []Doc {
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("fma", "fma(V b, V c) -> V", "v * b + c, as one instruction where the processor has one (f32/f64 lanes)."),
doc("store", "store([]T list, int i)", "Writes the lanes into list[i], list[i+1], ... (or through a ptr[T]).")
]
}
diff --git a/src/gen.fch b/src/gen.fch
index 7aba27c..d3b3eb3 100644
--- a/src/gen.fch
+++ b/src/gen.fch
@@ -1,8 +1,7 @@
// 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 the same
-// C runtime the main compiler uses. This compiler copies values like the main one
-// (so programs behave the same) but never frees them: it is the bootstrap compiler.
+// 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
@@ -477,7 +476,7 @@ fn T(str kind) -> ast.Type {
return ast.simple(kind)
}
-// `int` is i64 and `float` is f64, as in the C++ compiler: the everyday names
+// `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
@@ -4642,11 +4641,12 @@ fn convert(ptr[Gen] g, ast.Node c, ast.Type to) -> Val {
return v
}
-// ---------- the C calling convention for structs passed by value (System V x86-64) ----------
+// ---------- the C calling convention for structs passed by value ----------
//
-// A struct of up to 16 bytes travels in registers, one INTEGER or SSE class per eightbyte;
+// 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).
-// Other targets (Windows, ARM64) have their own rules and are not done here yet.
+// 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"
@@ -6992,7 +6992,9 @@ fn vecMethod(ptr[Gen] g, ast.Node m, Val v) -> Val {
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)
+ // fmuladd: one fused instruction where the processor has one (FMA, NEON), a multiply and an add where
+ // it doesn't (llvm.fma would call a slow library function there, to round only once)
+ return Val(v: vecIntrinsic(g, lt, "fmuladd", 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)
diff --git a/src/main.fch b/src/main.fch
index 7c59c14..8bf35bd 100644
--- a/src/main.fch
+++ b/src/main.fch
@@ -1,7 +1,4 @@
-// finch-boot: the Finch compiler written in Finch.
-//
-// finch-boot build [-o name] compile a program
-// finch-boot ir print the LLVM IR
+// finch: the Finch compiler, written in Finch (`finch` with no arguments lists the commands).
//
// It reads the program and the modules it imports, checks it, writes LLVM IR as text
// and lets clang turn that into a program, linked with Finch's C runtime.
diff --git a/src/platform.fch b/src/platform.fch
index 44a678f..2cbb0c2 100644
--- a/src/platform.fch
+++ b/src/platform.fch
@@ -1,6 +1,5 @@
// The system a program is built for, and what depends on it: the C compiler that links, the file
// name of the program, how it is run here, and (through the flags) the C calling convention.
-// The same as src/target.cpp.
import host
import "stdlib.h"
From 8495bc718dc882e892870fa54911c6be14cb523c Mon Sep 17 00:00:00 2001
From: Claude
Date: Sun, 1 Nov 2026 14:01:56 +0000
Subject: [PATCH 3/7] docs: the Polish cheat sheet and engineers' guide
rewritten in natural Polish; the engineers' guide matches the Finch compiler
- names from the C++ compiler (declareFns, HelperScope, Fn::borrowParam, mutatingMethods) replaced by the real ones
- section 8's subsections numbered 8.x; the map's LLVM type has 7 fields; @W in the index records
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01Psc42moPLwP43Cmb9oZqSR
---
docs/en/for-engineers.md | 69 ++---
docs/pl/dla-inzynierow.md | 623 ++++++++++++++++++++------------------
docs/pl/sciaga.md | 355 +++++++++++-----------
3 files changed, 548 insertions(+), 499 deletions(-)
diff --git a/docs/en/for-engineers.md b/docs/en/for-engineers.md
index e018e5a..68bc9e8 100644
--- a/docs/en/for-engineers.md
+++ b/docs/en/for-engineers.md
@@ -114,18 +114,19 @@ dedented by the closing line's indentation in `longString()`.
## 4. Code generation
-`gen.fch` is one big `Gen` struct plus functions over it. The passes are:
+`gen.fch` is one big `Gen` struct plus functions over it. `generate()` runs:
-1. `declareStructs`: every Finch and C struct gets an LLVM type (C structs with their real padding); recursive-by-value
- structs are refused.
-2. `declareFns`: signatures, with a *borrow analysis* for parameters (a parameter that is never stored is passed without
- a copy).
-3. `define`: each body, type checked and emitted. Scopes track what must be dropped when they end (`defer` runs first).
-4. `defineMainWrapper`: the C `main(argc, argv)`, and `finch_on_fatal` for embedders.
-5. With `-g`, DWARF metadata is attached as the IR is written (use `-O0`).
+1. `declare()`: every Finch and C struct gets an LLVM type (C structs with their real padding; recursive-by-value
+ structs are refused), then every function gets its signature, with a *borrow analysis* for parameters (§8.2).
+2. `define()` for each function: the body, type checked and emitted. Scopes track what must be dropped when they end.
+ Instantiating a generic adds functions while this goes on, so the loop runs until no new ones appear.
+3. The C entry point `main(argc, argv)` (or, for `--shared`, `finch_module_on_fatal` and the `export fn` wrappers).
+4. `assemble()`: the declarations, struct types, string constants, helpers and bodies, as one `.ll` text.
+
+With `-g`, DWARF metadata is attached as the IR is written (use `-O0`).
Generics are **monomorphised**: a call `max(1, 2)` infers `T`, then the template's AST is generated again with `T` bound,
-under a mangled name (`max$int`). `match` over an enum is exhaustive (a missing variant is an error, `else` covers the
+under its shown name (`max[int]`, a symbol `max.int`). `match` over an enum is exhaustive (a missing variant is an error, `else` covers the
rest); on a value it is a chain of comparisons. Enum values are `i32`; `Color(n)` is range checked; printing uses a
generated name table.
@@ -162,7 +163,7 @@ offset 0, and a field is read through memory. They can't be passed by value to C
## 6. Driver, linking and targets
-`finch build|run|ir|index file.fch [-o out] [-O0] [-g] [-l lib] [-I dir] [--target t] [--shared] [--overlay path=file]`.
+`finch build|run|ir|index file.fch [-o out] [-O0] [-g] [-l lib] [-I dir] [--target t] [--cpu c] [--shared] [--overlay path=file]`.
`ir` prints the IR; `index` prints the records the language server uses (§11); `--shared` makes a shared library of the
`export fn`s.
@@ -211,6 +212,7 @@ owned at that point is not freed.
A line that starts at bracket depth 0 and doesn't continue a line ending in an operator or `,` is re-indented to
4 × brace depth; other lines (continuations, `"""` bodies) are shifted by the same amount as the line that started the
statement, so their shape (and the `"""` dedent) is kept.
+
---
## 7. Type system
@@ -223,7 +225,7 @@ statement, so their shape (and the `"""` dedent) is kept.
| `char` `i8` `u8` / `i16` `u16` / `i32` `u32` / `int` `u64` | `i8` / `i16` / `i32` / `i64` |
| `f32` / `float` | `float` / `double` |
| `str`, `[]T` | `{ ptr, i64, i64 }` (pointer, length, capacity) |
-| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64 }` (entries, len, used, cap, index, icap; see src/gen.fch) |
+| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64, i64 }` (entries, len, used, cap, index, icap, adds; see src/gen.fch) |
| `ptr`, `ptr[T]`, `null` | `ptr` (opaque) |
| `[N]T` | `[N x T]` |
| `f32x8`, `i8x32`, … | `<8 x float>`, `<32 x i8>` (§7, Vectors) |
@@ -322,11 +324,9 @@ IR's compile, so the same IR becomes SSE2, AVX2 + FMA, AVX-512 or NEON code. Lan
---
----
-
## 8. Ownership and memory
-### 9.1 The model
+### 8.1 The model
Every value of an **owning type** (`str`, `[]T`, or a Finch struct with an owning field) has exactly one
owner: a variable, an array element, a struct field, or a heap cell from `new()`. The owner drops the
@@ -350,21 +350,21 @@ A string literal is a fresh value with `cap = 0`: moving it costs nothing, dropp
(`release()` ignores constants), and writing into it (`s[i] = c`) first makes a heap copy
(`finch_str_own`). Strings from C are borrowed with `cap = -1` and always copied before being kept.
-### 9.2 Parameter borrowing: `mutates()`
+### 8.2 Parameter borrowing: `mutates()`
When declaring a function, every owning parameter is checked with `mutates(name, body)`: an
assignment whose root variable (`rootVar()`: through `.field` and `[i]`) is the parameter, a changing
method on it (`push pop insert remove clear resize sort reverse`), or `addr(…)` of it. Unchanged
-parameters are borrowed (`Fn::borrowParam`); changed ones are copied once at entry. The analysis is
+parameters are borrowed (`FnSig.borrow`); changed ones are copied once at entry. The analysis is
syntactic and conservative: anything that *might* change the parameter makes a copy, which is always correct.
The same analysis decides whether `for x in list` must copy elements.
-Methods take part: `declareFns()` first finds which methods change `self` (`mutates("self", body)`),
+Methods take part: `declare()` first finds which methods change `self` (`mutates("self", body)`),
repeating until nothing changes, because calling a changing method on `self` changes `self` too. Their
-names go into `mutatingMethods`, which `mutatesExpr()` treats like `push`: `p.move(1)` changes `p`.
-Only then is `borrowParam` computed for every function and method.
+names go into `g.mm`, which `mutates()` treats like `push`: `p.move(1)` changes `p`.
+Only then is `borrow` computed for every function and method.
-### 9.3 Copy and drop
+### 8.3 Copy and drop
`dropAt(addr, T)` / `copyAt(dst, src, T)` work on addresses:
@@ -374,10 +374,10 @@ Only then is `borrowParam` computed for every function and method.
element and then `finch_arr_free`; copies call `finch_arr_clone_raw` and then deep-copy owning
elements over the raw bits. Structs drop/copy only their owning fields.
-Helpers are emitted in their own functions (`HelperScope` saves the builder position and switches off
-debug locations), may call each other recursively (`struct Node { []Node kids }`), and are inlined by O2.
+Helpers are emitted in their own functions (`beginHelper()` / `endHelper()` set the function being written aside
+and switch off debug locations), may call each other recursively (`struct Node { []Node kids }`), and are inlined by O2.
-### 9.4 What it looks like
+### 8.4 What it looks like
```c
fn greet(str name) -> str {
@@ -420,22 +420,22 @@ The `-O0` IR of `main` (abridged):
and `greet` returns `msg` without copying (`ret … %5` after loading the slot: the variable was moved out).
-### 9.5 defer
+### 8.5 defer
-`defer stmt` pushes a `Cleanup{isDefer, body, order = current varOrder}` into the current scope; it
-emits nothing there. Whenever the scope's cleanups are emitted (falling off the end of the block,
+`defer stmt` records the statement in the current scope (`Scope.defers`, with `orders`: how many variables existed
+then) and a `Cleanup{isDefer}`; it emits nothing there. Whenever the scope's cleanups are emitted (falling off the end of the block,
`return`, `break`/`continue` leaving it) the body is generated **at that exit**, with `deferLimit = order`
so that `lookup()` hides variables declared after the defer: those are already dropped by then
(cleanups run in reverse). `return`/`break`/`continue` inside a deferred statement are errors (`inDefer`).
Defer bodies are therefore duplicated per exit path, which is what makes them free on the normal path.
-### 9.6 Manual memory
+### 8.6 Manual memory
`new(v)` allocates `sizeof(T)` with `finch_alloc`, stores `own(v)` and returns `ptr[T]`. `free(p)` drops
the pointee if `T` is owning (skipping null) and calls libc `free`. Pointers never own anything:
`addr(x)` can dangle after `x`'s block ends; this is not checked.
-### 9.7 Verifying it
+### 8.7 Verifying it
`MEMCHECK=1 tests/run.sh` builds every test program and runs it under valgrind with
`--leak-check=full --errors-for-leak-kinds=all`: zero leaks and zero invalid accesses across all of them,
@@ -444,8 +444,6 @@ iteration, nested arrays of structs, and C struct passing.
---
----
-
## 9. Generics, enums and match
See §4 for how they are generated. For the rules a user sees, read sections 23 and 24 of the
@@ -485,16 +483,17 @@ mode the compiler prints, besides IR-free output, tab-separated records:
| `@V name type file line col fnFile fnLine fnEnd` | a variable and the function it lives in |
| `@F module name recv file line col endLine signature params` | a function or method |
| `@S` / `@SF`, `@E`, `@I` | structs (and fields), enums, imports |
+| `@W` | a warning |
The first error is printed as `path:line:col: error: message` and becomes a diagnostic. When code does not compile, the
server keeps answering completion and signature help from the last version that did. `tests/lsp_test.py` is the acceptance
-test (22 checks).
+test.
---
## 12. Semantics vs. C
-| Situation | C | Finch 2.0 |
+| Situation | C | Finch |
|---|---|---|
| Signed integer overflow | UB | defined wraparound (no `nsw`) |
| Integer division by zero, `INT_MIN / -1` | UB | compile error if constant, otherwise runtime error |
@@ -507,13 +506,9 @@ test (22 checks).
| Uninitialized variables | UB | impossible (zero or defaults) |
| Missing return | UB if used | compile error |
| Order of evaluation | mostly unspecified | left to right; in an assignment the right side first |
-| Shift ≥ width, out-of-range float→int | UB | still poison (not checked yet) |
+| Shift ≥ width, out-of-range float→int | UB | poison, as in LLVM (not checked) |
| Dangling pointer from `addr()` / `new` + `free` | UB | still UB |
----
-
----
-
### The chain of calls on runtime errors
With `Options.trace` (default for `run`, `test`, `-g`, `-O0`; `--trace`/`--no-trace`; never with `--shared`) every Finch
diff --git a/docs/pl/dla-inzynierow.md b/docs/pl/dla-inzynierow.md
index c948e86..e8ef597 100644
--- a/docs/pl/dla-inzynierow.md
+++ b/docs/pl/dla-inzynierow.md
@@ -1,12 +1,11 @@
# Finch dla inżynierów
-**Wnętrze Fincha 2.5, którego kompilator jest napisany w Finchu.** Jak kod źródłowy staje się tokenami, drzewem AST i
-LLVM IR *jako tekstem*, który `clang` kompiluje i linkuje z małym runtime'em w C; jak nagłówki C czyta libclang wywoływany
-z samego Fincha i jak ręcznie odtwarzane są konwencje wywołań C; reguły typów i własności; jak kompilator buduje sam siebie
-z ziarna; oraz serwer języka.
+**Jak od środka działa Finch 2.6, którego kompilator jest napisany w Finchu.** Jak kod źródłowy zamienia się w tokeny,
+drzewo AST i LLVM IR *w postaci tekstu*, który `clang` kompiluje i linkuje z małym runtime'em w C; jak nagłówki C są
+czytane przez libclang z poziomu samego Fincha i jak ręcznie odtworzono konwencje wywołań C; reguły typów i własności
+wartości; jak kompilator buduje sam siebie z pliku startowego; i jak działa serwer języka.
-Zakładam, że czytasz Fincha (zobacz [przewodnik dla techników](dla-technikow.md)) i widziałeś już LLVM IR.
-Pierwsza wersja kompilatora, w C++ na API C++ LLVM, jest w gałęzi `before-bootstrap`; ten przewodnik opisuje wersję w Finchu.
+Zakładam, że umiesz czytać kod w Finchu (zob. [przewodnik dla techników](dla-technikow.md)) i że widziałeś już LLVM IR.
> 🇬🇧 English version: [for-engineers.md](../en/for-engineers.md)
@@ -15,194 +14,210 @@ Pierwsza wersja kompilatora, w C++ na API C++ LLVM, jest w gałęzi `before-boot
## Spis treści
1. [Architektura](#1-architektura)
-2. [Drzewo źródeł](#2-drzewo-źródeł)
-3. [Front end: lekser, parser, moduły](#3-front-end-lekser-parser-moduły)
+2. [Pliki źródłowe](#2-pliki-źródłowe)
+3. [Część frontowa: lekser, parser, moduły](#3-część-frontowa-lekser-parser-moduły)
4. [Generowanie kodu](#4-generowanie-kodu)
-5. [Import C i ABI C](#5-import-c-i-abi-c)
-6. [Sterownik, linkowanie i cele](#6-sterownik-linkowanie-i-cele)
+5. [Import z C i ABI języka C](#5-import-z-c-i-abi-języka-c)
+6. [Sterownik, linkowanie i systemy docelowe](#6-sterownik-linkowanie-i-systemy-docelowe)
7. [System typów](#7-system-typów)
8. [Własność i pamięć](#8-własność-i-pamięć)
-9. [Generyki, enumy i match](#9-generyki-enumy-i-match)
+9. [Typy generyczne, enumy i match](#9-typy-generyczne-enumy-i-match)
10. [Bootstrap](#10-bootstrap)
11. [Serwer języka](#11-serwer-języka)
-12. [Semantyka a C](#12-semantyka-a-c)
+12. [Semantyka w porównaniu z C](#12-semantyka-w-porównaniu-z-c)
13. [Znane ograniczenia](#13-znane-ograniczenia)
---
## 1. Architektura
-W kompilatorze nie ma biblioteki LLVM. Pisze LLVM IR jako **tekst**, a optymalizację, generowanie kodu i linkowanie zostawia
-`clangowi`. Dzięki temu jest mały (ok. 8 tys. linii) i może być napisany w samym Finchu.
+W kompilatorze nie ma biblioteki LLVM. Zapisuje on LLVM IR jako **tekst**, a optymalizację, generowanie kodu maszynowego
+i linkowanie zostawia `clangowi`. Dzięki temu jest mały (ok. 12 tys. linii) i dało się go napisać w samym Finchu.
```
main.fch ─► loader ── lex ── parse ──► Program (+ każdy importowany moduł, rekurencyjnie)
│
- `import "x.h"` ─► cimport.fch (libclang wołany z Fincha) ─► funkcje, struktury, stałe C
+ `import "x.h"` ─► cimport.fch (libclang wołany z Fincha) ─► funkcje, struktury i stałe C
│
▼
- gen.fch ─► generate(): struktury, sygnatury, ciała funkcji
- kontrola typów i IR w jednym przejściu; klasyfikacja ABI wywołań C
+ gen.fch ─► generate(): deklaracje struktur i funkcji, potem ciała funkcji;
+ sprawdzanie typów i emisja IR w jednym przejściu; klasyfikacja ABI dla wywołań C
│ (tekst IR)
▼
linker.fch ─► clang -c prog.ll ─► clang prog.o rt-.o [twoje .c/.o/.a] -lm [pkg-config / -l]
│
▼
- plik wykonywalny (finch run: uruchamiany z pliku tymczasowego, potem usuwany)
+ plik wykonywalny (finch run: uruchamiany z pliku tymczasowego, potem usuwany)
```
-Decyzje projektowe:
+Decyzje, od których zależy cała reszta:
-- **Szybko się przerywa.** Pierwszy błąd wypisuje `plik:linia:kol: error: …` z linią źródła i strzałką, i kompilator kończy.
- Bez odzyskiwania po błędach, więc kolejne fazy nie widzą półpoprawnych danych.
-- **Jedno przejście.** `gen.fch` sprawdza typy i emituje IR jednym przejściem po AST. Wyrażenie daje operand IR, typ Fincha
- (napis: `int`, `f32`, `str`, `array`, `map`, `struct`, `enum`, `named`…) i flagi.
-- **Niech clang pracuje.** Zmienne lokalne to `alloca`, zamieniane przez `mem2reg`. Optymalizacja to `-O2` clanga (albo `-O0` z `-g`).
-- **Wartości, nie referencje.** Tablice, teksty i struktury mają semantykę wartości; własność ustala się w czasie kompilacji (§8).
-- **Malutki runtime w C** (`runtime/finch_rt.c`, ok. 760 linii) robi to, co w IR jest uciążliwe: teksty, rośnięcie tablic, mapy,
- wejście, pliki, paniki. Kompilowany raz i trzymany w `~/.cache/finch`.
+- **Pierwszy błąd kończy pracę.** Kompilator wypisuje `plik:linia:kolumna: error: …` z linią źródła i strzałką,
+ po czym się zatrzymuje. Nie próbuje kontynuować po błędzie, więc dalsze etapy nigdy nie widzą częściowo poprawnego wejścia.
+- **Jedno przejście.** `gen.fch` sprawdza typy i emituje IR w jednym przejściu po AST. Każde wyrażenie daje operand IR,
+ swój typ Fincha (rodzaj zapisany tekstem: `int`, `f32`, `str`, `array`, `map`, `struct`, `enum`, `vec`, `named`…) i flagi.
+- **Resztę robi clang.** Zmienne lokalne to `alloca`, które `mem2reg` zamienia na rejestry. Optymalizuje `-O2` clanga
+ (albo `-O0` przy `-g`).
+- **Wartości, nie referencje.** Tablice, teksty i struktury zachowują się jak wartości; o własności decyduje kompilator (§8).
+- **Mały runtime w C** (`runtime/finch_rt.c`, ok. 1150 linii) robi to, co w IR byłoby żmudne: teksty, powiększanie tablic,
+ mapy, wczytywanie, pliki, błędy w trakcie działania. Kompiluje się go raz i trzyma w `~/.cache/finch`.
---
-## 2. Drzewo źródeł
+## 2. Pliki źródłowe
-| Plik | Linie | Zadanie |
+| Plik | Linie | Za co odpowiada |
|---|---|---|
-| `src/lexer.fch` | 470 | Tokeny i pozycje, tekst `"""`, `"\(x)"` zamienione na tokeny `("…" + str(x) + "…")` |
-| `src/ast.fch` | 120 | Jedna struktura `Node` z napisem `kind`, dziećmi, nazwami typów, pozycjami (`line`, `endLine`) |
-| `src/parser.fch` | 910 | Zejście rekurencyjne: deklaracje, instrukcje, wyrażenia, generyki (`[T]`), `enum`, `match`, `export`, `link`, `const`, `test "…" { }` |
-| `src/gen.fch` | 6450 | Kontrola typów + IR: struktury, funkcje, metody, zakresy i zwolnienia, instrukcje, wyrażenia, wbudowane, mapy, błędy jako wartości, generyki (monomorfizacja), enumy i `match`, metadane `-g`, tryb `index` |
-| `src/cimport.fch` | 790 | libclang: funkcje, struktury i unie (pola, offsety, wypełnienie), enumy, stałe, globalne, makra (także będące strukturą) |
-| `src/platform.fch` | 90 | `Target`: triple, układ danych, kompilator C, znak `char` zależnie od systemu |
-| `src/linker.fch` | 430 | Pamięć podręczna (runtime, obiekt programu, pliki C/C++), biblioteki (`link`, pkg-config), podpowiedzi przy błędach linkera |
-| `src/main.fch` | 515 | Sterownik: CLI, loader z wyszukiwaniem modułów i nakładkami, `build/run/test/fmt/ir/index/lsp` |
-| `src/manifest.fch` | 240 | `finch.toml`: mały czytnik TOML, listy zależne od celu |
-| `src/fmt.fch` | 225 | `finch fmt` i formatowanie w LSP |
-| `src/lsp.fch` | 910 | Serwer języka (§11) |
-| `src/docs.fch` | 110 | Jeden opis wbudowanych funkcji dla podpowiedzi i uzupełniania |
+| `src/lexer.fch` | 470 | Tokeny, pozycje, tekst `"""`, zamiana `"\(x)"` na tokeny `("…" + str(x) + "…")` |
+| `src/ast.fch` | 170 | Jedna struktura `Node` z rodzajem (`kind`) jako tekstem, dziećmi, nazwami typów i pozycją (`line`, `endLine`); rozpoznawanie typów wektorowych |
+| `src/parser.fch` | 910 | Zejście rekurencyjne: deklaracje, instrukcje, wyrażenia, typy generyczne (`[T]`), `enum`, `match`, `export`, `link`, `const`, `test "…" { }` |
+| `src/gen.fch` | 7030 | Sprawdzanie typów i IR: struktury, funkcje, metody, zasięgi i zwalnianie, instrukcje, wyrażenia, funkcje wbudowane, mapy, błędy jako wartości, typy generyczne (monomorfizacja), enumy i `match`, wektory (SIMD), metadane debuggera (`-g`), tryb `index` |
+| `src/cimport.fch` | 790 | libclang: funkcje, struktury i unie (pola, przesunięcia, wyrównanie), enumy, stałe, zmienne globalne, makra (także o wartości struktury) |
+| `src/platform.fch` | 120 | `Target`: triple, układ danych, kompilator C, znak typu `char` na danym systemie |
+| `src/linker.fch` | 480 | Pamięć podręczna (runtime, obiekt programu, pliki C/C++), biblioteki (`link`, pkg-config), podpowiedzi przy błędach linkera |
+| `src/main.fch` | 530 | Sterownik: wiersz poleceń, wczytywanie modułów i nakładek, `build/run/test/fmt/ir/index/lsp` |
+| `src/manifest.fch` | 240 | `finch.toml`: mały czytnik TOML, listy dla poszczególnych systemów |
+| `src/fmt.fch` | 225 | `finch fmt` i formatowanie w serwerze języka |
+| `src/lsp.fch` | 950 | Serwer języka (§11) |
+| `src/docs.fch` | 140 | Jeden opis funkcji wbudowanych, wspólny dla podpowiedzi i dymków |
| `std/*.fch` | 1040 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`), `fs`, `thread` |
-| `runtime/finch_rt.c` | 1150 | Runtime w C: tekst, tablice, mapy, pliki, wypisywanie liczb, fs, wątki, uruchamianie testów, łańcuch wywołań |
-| `bootstrap/` | | `build.sh`, `install.sh`, ziarna (§10) |
+| `runtime/finch_rt.c` | 1150 | Runtime w C: teksty, tablice, mapy, pliki, wypisywanie liczb, fs, wątki, uruchamianie testów, ścieżka wywołań |
+| `bootstrap/` | | `build.sh`, `install.sh`, pliki startowe (§10) |
---
-## 3. Front end: lekser, parser, moduły
+## 3. Część frontowa: lekser, parser, moduły
-**Lekser.** Koniec linii kończy instrukcję; wcięcia nic nie znaczą. Tokeny niosą plik, linię i kolumnę, z których korzysta każdy błąd.
+**Lekser.** Koniec linii kończy instrukcję; wcięcia nie mają znaczenia. Każdy token zna plik, linię i kolumnę, z których
+korzystają wszystkie komunikaty o błędach.
-**Parser.** Zejście rekurencyjne, jedna funkcja na konstrukcję. Typy są parsowane do napisu (`[]str`, `map[str]int`,
-`ptr[char]`, `Stack[int]`); `fn Typ.metoda` deklaruje metodę; `fn nazwa[T]` i `struct Nazwa[T]` to szablony; `enum` i `match`
-mają własne węzły; `export fn` oznacza funkcję widoczną dla C; blok zapamiętuje `endLine` (dla serwera języka). Stara
-kolejność `fn nazwa(...) typ` to błąd składni z podpowiedzią.
+**Parser.** Zejście rekurencyjne, jedna funkcja na każdą konstrukcję. Warto wiedzieć, że: typy są parsowane do nazwy
+(`[]str`, `map[str]int`, `ptr[char]`, `Stack[int]`, `f32x8`); `fn Typ.metoda` deklaruje metodę; `fn nazwa[T]`
+i `struct Nazwa[T]` deklarują szablony; `enum` i `match` mają własne węzły; `export fn` oznacza funkcję widoczną z C;
+blok zapamiętuje swoją `endLine` (korzysta z niej serwer języka). Stara kolejność `fn nazwa(...) typ` daje błąd z podpowiedzią.
-**Moduły.** `import nazwa` szuka kolejno `nazwa_.fch` obok importującego pliku (`windows`, `linux`, `macos`), `nazwa.fch`,
-katalogów z `FINCH_PATH`, potem biblioteki standardowej `std/`. Każdy moduł jest parsowany raz; jego nazwy są używane jako
-`modul.nazwa`. `import "x.h"` trafia do importera C.
+**Moduły.** `import nazwa` szuka po kolei: `nazwa_.fch` obok importującego pliku (`windows`, `linux`, `macos`),
+`nazwa.fch` w tym samym miejscu, katalogów z `FINCH_PATH`, a na końcu biblioteki standardowej `std/`. Każdy moduł jest
+parsowany raz; jego nazwy są dostępne jako `modul.nazwa`. `import "x.h"` trafia zamiast tego do importu z C.
-Moduł rozpoznaje się po **pliku**: kluczem jest jego ścieżka od folderu głównego pliku (albo od folderu z `FINCH_PATH`
-lub `std/`) bez `.fch` i bez końcówki `_`, np. `ui/przycisk`. `Program.imports` trzyma nazwę używaną w danym
-pliku (`przycisk`, albo `p` przy `import ui/przycisk as p`), a `Program.modules` klucz; `modFor()` zamienia jedno na drugie.
-Symbole używają klucza z `/` zamienionym na `$` (`@finch.ui$przycisk.rysuj`).
+Moduł rozpoznaje się po **pliku**: kluczem jest jego ścieżka względem katalogu głównego pliku (albo katalogu z
+`FINCH_PATH`, albo `std/`), bez `.fch` i bez końcówki `_`, np. `ui/button`. `Program.imports` przechowuje nazwę,
+której używa dany plik (`button` albo `btn` przy `import ui/button as btn`), a `Program.modules` klucz; `modFor()`
+przechodzi od jednego do drugiego. Symbole używają klucza, w którym `/` zamieniono na `$` (`@finch.ui$button.make`).
-**Stałe.** `const NAZWA = wyrażenie` to węzeł najwyższego poziomu; `constValue()` liczy wyrażenie przy każdym użyciu, w
-module, w którym je zdefiniowano, bez zmiennych lokalnych, więc wychodzi literał (operatory na literałach całkowitych
-składa `arith`). `checkConst()` dopuszcza tylko literały, nazwy, operatory, pola i konwersje liczb; `constBusy` łapie pętle.
+**Stałe.** `const NAZWA = wyrazenie` to węzeł najwyższego poziomu; `constValue()` oblicza wyrażenie przy każdym użyciu,
+w module, w którym je zdefiniowano, bez zmiennych lokalnych, więc wynik zawsze składa się do literału (operatory na
+literałach całkowitych są składane w `arith`). `checkConst()` dopuszcza tylko literały, nazwy, operatory, pola
+i konwersje liczbowe; `constBusy` wykrywa cykle.
-**Dodatki leksera.** `1e9` / `2.5e-3` to tokeny float (tekst dostaje `.` dla LLVM); teksty `"""` tracą wcięcie
-linii zamykającej w `longString()`.
+**Drobiazgi leksera.** `1e9` i `2.5e-3` to tokeny zmiennoprzecinkowe (tekst jest uzupełniany o `.`, którego wymaga LLVM);
+z tekstu `"""` funkcja `longString()` usuwa wcięcie linii zamykającej.
---
## 4. Generowanie kodu
-`gen.fch` to jedna duża struktura `Gen` i funkcje na niej. Przejścia:
+`gen.fch` to jedna duża struktura `Gen` i funkcje, które na niej pracują. `generate()` wykonuje kolejno:
-1. `declareStructs`: każda struktura Fincha i C dostaje typ LLVM (struktury C z prawdziwym wypełnieniem); rekurencja przez wartość jest odrzucana.
-2. `declareFns`: sygnatury, z *analizą pożyczania* parametrów (parametr, który nigdzie nie jest zapisywany, jest przekazywany bez kopii).
-3. `define`: każde ciało, sprawdzone i wyemitowane. Zakresy pamiętają, co zwolnić na końcu (`defer` działa pierwszy).
-4. `defineMainWrapper`: C-owe `main(argc, argv)` i `finch_on_fatal` dla osadzających.
-5. Z `-g` metadane DWARF dokładane są w trakcie pisania IR (używaj `-O0`).
+1. `declare()`: każda struktura Fincha i C dostaje typ LLVM (struktury C z prawdziwym wyrównaniem; struktury, które
+ przez wartość zawierają same siebie, są odrzucane), a potem każda funkcja dostaje sygnaturę z *analizą pożyczania*
+ parametrów (§8.2).
+2. `define()` dla każdej funkcji: ciało, ze sprawdzeniem typów i emisją IR. Zasięgi pamiętają, co trzeba zwolnić przy
+ wyjściu. Tworzenie instancji typów generycznych dokłada w tym czasie nowe funkcje, więc pętla trwa, dopóki pojawiają się nowe.
+3. Punkt wejścia C, `main(argc, argv)` (a przy `--shared`: `finch_module_on_fatal` i opakowania funkcji `export fn`).
+4. `assemble()`: deklaracje, typy struktur, stałe tekstowe, funkcje pomocnicze i ciała funkcji jako jeden tekst `.ll`.
-Generyki są **monomorfizowane**: wywołanie `max(1, 2)` wnioskuje `T`, potem AST szablonu generowane jest ponownie z
-przypisanym `T`, pod zmangowaną nazwą (`max$int`). `match` na enumie jest wyczerpujący (brak wariantu to błąd, `else` łapie
-resztę); na wartości to ciąg porównań. Wartości enuma to `i32`; `Color(n)` jest sprawdzane; wypisywanie używa wygenerowanej tabeli nazw.
+Przy `-g` metadane DWARF są dopisywane na bieżąco, w trakcie zapisywania IR (najlepiej razem z `-O0`).
-Błędy jako wartości: `-> T!` zwraca parę (wartość, tekst błędu); `try` wraca wcześniej przy błędzie, `or` go obsługuje, `error("…")` go tworzy.
+Typy generyczne są **monomorfizowane**: wywołanie `max(1, 2)` wyznacza `T`, a potem AST szablonu jest generowane jeszcze
+raz z podstawionym `T`, pod nazwą `max[int]` (symbol `max.int`). `match` na enumie musi być wyczerpujący (brak wariantu
+to błąd, `else` obejmuje resztę); na zwykłej wartości to ciąg porównań. Wartości enumów to `i32`; `Kolor(n)` sprawdza
+zakres; do wypisywania służy wygenerowana tabela nazw.
+
+Błędy jako wartości: `-> T!` zwraca parę (wartość, tekst błędu); `try` przy błędzie wychodzi z funkcji, `or` go obsługuje,
+`error("…")` go tworzy.
---
-## 5. Import C i ABI C
+## 5. Import z C i ABI języka C
-`cimport.fch` woła **libclang** wprost z Fincha (sam jest napisany z `import "clang-c/Index.h"`), parsuje nagłówek z flagami
-celu i przechodzi po kursorach. Typy: parametr `const char *` → `str`, inny `char *` → `ptr[char]`, wynik `char *` → `str`;
-liczby całkowite według rozmiaru i znaku (`char` jest bez znaku na ARM64 Linux); struktury i enumy według układu.
+`cimport.fch` woła **libclang** bezpośrednio z Fincha (sam jest napisany z `import "clang-c/Index.h"`), parsuje nagłówek
+z flagami systemu docelowego i przechodzi po kursorach. Typy tłumaczy tak: parametr `const char *` → `str`, inne
+`char *` → `ptr[char]`, wynik `char *` → `str`; liczby całkowite według rozmiaru i znaku (`char` jest bez znaku na
+Linuksie ARM64); struktury i enumy według ich układu w pamięci.
-LLVM IR nie realizuje za ciebie konwencji wywołań C, więc `gen.fch` ją odtwarza:
+LLVM IR nie realizuje sam konwencji wywołań C, więc `gen.fch` odtwarza ją ręcznie:
-- `classify`: System V x86-64: struktura do 16 bajtów idzie w rejestrach według klas ośmiobajtów (INTEGER/SSE), większa w pamięci;
-- `classifyArm64`: AAPCS64, z jednorodnymi agregatami zmiennoprzecinkowymi (HFA);
-- reguły Microsoft x64 (struktury 1, 2, 4, 8 bajtów w rejestrze, reszta przez wskaźnik);
-- `makePlan` zamienia klasyfikację w plan wywołania; `cThunk` robi z `addr(fn)` coś, co C może wywołać (callbacki).
-
-**Makra.** `#define` będący jedną liczbą albo tekstem czyta się z tokenów. Taki, którego tokeny mogą być wyrażeniem
-stałym (bez klamer, średników i słów instrukcji), liczy clang: drugie parsowanie nagłówka dodaje
-`static const __auto_type __finch_m_NAZWA = (NAZWA);` dla każdego, a `clang_Cursor_Evaluate` daje liczbę całkowitą,
-double albo tekst. Te, które nie są stałymi, dają tylko błędy w tym drugim parsowaniu i są pomijane.
-
-**Unie** to `CStruct` z `isUnion`: typ LLVM to `{ [n x iA] }` (rozmiar i wyrównanie od clanga), każde pole ma
-przesunięcie 0, a pole czyta się przez pamięć. Nie da się ich przekazać do C przez wartość (`unsupported` mówi dlaczego).
+- `classify`: System V x86-64; struktura do 16 bajtów idzie w rejestrach według klasy każdych 8 bajtów (INTEGER/SSE), większa przez pamięć;
+- `classifyArm64`: AAPCS64, w tym jednorodne agregaty liczb zmiennoprzecinkowych (HFA) i wariant Apple;
+- reguły Microsoft x64 (struktury 1, 2, 4 i 8-bajtowe przez wartość w rejestrze, pozostałe przez wskaźnik);
+- `makePlan` zamienia klasyfikację w plan wywołania; `cThunk` sprawia, że `addr(fn)` da się wywołać z C (funkcje zwrotne).
Ten sam kod pozwala kompilatorowi wołać libclang, którego API przekazuje `CXCursor`, `CXType` i `CXString` przez wartość.
+**Makra.** `#define` z jedną liczbą albo jednym literałem tekstowym jest odczytywany z tokenów. Taki, którego tokeny
+mogą tworzyć wyrażenie stałe (bez klamer, średników i słów kluczowych instrukcji), oblicza clang: drugie parsowanie
+nagłówka dopisuje dla każdego `static const __auto_type __finch_m_NAZWA = (NAZWA);`, a `clang_Cursor_Evaluate` zwraca
+liczbę całkowitą, liczbę typu double albo tekst. Makra, które nie są stałymi, dają w tym drugim parsowaniu tylko błędy
+i są pomijane.
+
+**Unie** to `CStruct` z `isUnion`: typ LLVM to `{ [n x iA] }` (rozmiar i wyrównanie podaje clang), każde pole ma
+przesunięcie 0, a pole czyta się przez pamięć. Nie da się ich przekazać do C przez wartość (pole `unsupported` mówi dlaczego).
+
---
-## 6. Sterownik, linkowanie i cele
+## 6. Sterownik, linkowanie i systemy docelowe
-`finch build|run|ir|index plik.fch [-o wyjście] [-O0] [-g] [-l biblioteka] [-I katalog] [--target cel] [--shared] [--overlay ścieżka=plik]`.
-`ir` wypisuje IR; `index` wypisuje rekordy dla serwera języka (§11); `--shared` robi bibliotekę współdzieloną z `export fn`.
+`finch build|run|ir|index plik.fch [-o wynik] [-O0] [-g] [-l bib] [-I katalog] [--target t] [--cpu c] [--shared] [--overlay sciezka=plik]`.
+`ir` wypisuje IR; `index` wypisuje rekordy dla serwera języka (§11); `--shared` tworzy bibliotekę współdzieloną z funkcji
+`export fn`.
-`--target windows|linux|arm64|macos|android|` zmienia triple i układ danych, znak `char`, reguły ABI powyżej, wariant OS
-modułów (`nazwa_windows.fch`) i kompilator C: `FINCH_CC` albo `clang --target=…`. Przy linkowaniu `pkg-config` podaje flagi
-dla `link "biblioteka"`; gdy linker zawiedzie, wynik jest analizowany i podpowiadane jest brakujące `link "…"`. Obiekt
-runtime'u jest kluczowany skrótem jego źródła i celu.
+`--cpu ` (albo `cpu` w `finch.toml`) dodaje `-march=` na x86-64 albo `-mcpu=` na ARM64 do wywołania
+clanga, które kompiluje IR (nie runtime); wartość wchodzi do klucza pamięci podręcznej.
-Zmienne środowiskowe: `FINCH_PATH` (katalogi modułów), `FINCH_RT` (źródło runtime'u), `FINCH_CC` (kompilator/linker C; IR
-zawsze kompiluje `clang`, nigdy `CC`), `FINCH_LIBCLANG` (dla `bootstrap/build.sh`).
+`--target windows|linux|arm64|macos|android|` zmienia triple i układ danych, znak typu `char` w C, reguły ABI
+opisane wyżej, wariant modułów dla systemu (`nazwa_windows.fch`) i kompilator C: `FINCH_CC` albo `clang --target=…`.
+Przy `link "bib"` linkowanie pyta `pkg-config` o flagi; gdy linker zawiedzie, jego wyjście jest analizowane
+i kompilator podpowiada brakujące `link "…"`. Obiekt runtime'u ma klucz z hasza swojego źródła i systemu docelowego.
-Kompilator znajduje `runtime/` i `std/` obok siebie (`../`, `./`) albo w `../share/finch/` (`bootstrap/install.sh`).
+Zmienne środowiskowe: `FINCH_PATH` (katalogi modułów), `FINCH_RT` (źródło runtime'u), `FINCH_CC` (kompilator C i linker;
+IR zawsze kompiluje `clang`, nigdy `CC`), `FINCH_LIBCLANG` (dla `bootstrap/build.sh`).
-**Pamięć podręczna budowania** (`host.cacheDir()`: `$XDG_CACHE_HOME/finch`, `~/.cache/finch`, `%LOCALAPPDATA%\finch`):
+Kompilator szuka `runtime/` i `std/` obok siebie (`../`, `./`) albo w `../share/finch/` (`bootstrap/install.sh`).
-- `ir/.o` z plikiem `.key`, w którym jest skrót clanga, flag i całego IR. Jedno miejsce
- na program, więc cache nie rośnie z każdą zmianą; to samo IR daje ten sam obiekt bez uruchamiania clanga.
-- `c/.o` dla `link "x.c"` / `.cpp`, budowane z `-MMD`; `.key` to skrót każdego
- pliku z listy `.d` (źródła i jego nagłówków). Zmiana któregokolwiek przebudowuje obiekt.
-- Obiekty są zapisywane do pliku `.part` i przemianowywane, więc dwa budowania naraz nie widzą pół pliku.
- Skróty to dwa niezależne 32-bitowe (FNV-1a i djb2), razem 64 bity.
+**Pamięć podręczna kompilacji** (`host.cacheDir()`: `$XDG_CACHE_HOME/finch`, `~/.cache/finch`, `%LOCALAPPDATA%\finch`):
-**`finch.toml`** (`src/manifest.fch`) jest szukany od folderu pliku głównego (albo bieżącego) w górę. Trafia do
-`values["sekcja.klucz"]` i `lists["sekcja.klucz"]`; `list(klucz, cele)` skleja listę z najwyższego poziomu i te z
-`[target.]` oraz `[target.-]`. Ścieżki liczą się od folderu manifestu. Wpisy trafiają tam, gdzie opcje
-z linii poleceń: `paths` → loader, `include` → `-I` (libclang i pliki C), `links` → `link`, `libdirs` i `ldflags` →
-polecenie linkowania, `cflags` → pliki C/C++.
+- `ir/.o` z plikiem `.key`, w którym jest hash clanga, flag i całego IR. Jedno
+ miejsce na program, więc pamięć nie rośnie przy każdej zmianie kodu; to samo IR daje ten sam obiekt bez uruchamiania clanga.
+- `c/.o` dla `link "x.c"` / `.cpp`, budowany z `-MMD`; `.key` to hash
+ wszystkich plików wymienionych w `.d` (źródła i jego nagłówków). Zmiana któregokolwiek z nich wymusza ponowną kompilację.
+- Obiekty są zapisywane do pliku `.part` i dopiero potem przemianowywane, więc dwie równoległe kompilacje nie
+ zobaczą połowy pliku. Hasze to dwa niezależne 32-bitowe (FNV-1a i djb2), razem 64 bity.
-**Pliki C++**: `.cpp`/`.cc`/`.cxx` w `links` kompiluje `cxxFor(cc)` (`FINCH_CXX` albo `clang` → `clang++`,
-`…gcc` → `…g++`, `cc` → `c++`), a potem ten sam sterownik linkuje, co dokłada standardową bibliotekę C++.
+**`finch.toml`** (`src/manifest.fch`) jest szukany od katalogu głównego pliku (albo katalogu roboczego) w górę.
+Trafia do `values["sekcja.klucz"]` i `lists["sekcja.klucz"]`; `list(klucz, systemy)` łączy listę z najwyższego poziomu
+z listami z `[target.]` i `[target.-]`. Ścieżki są liczone względem katalogu manifestu.
+Wpisy trafiają tam, gdzie opcje z wiersza poleceń: `paths` → wczytywanie modułów, `include` → `-I` (libclang i pliki C),
+`links` → `link`, `libdirs` i `ldflags` → polecenie linkowania, `cflags` → pliki C/C++.
+
+**Pliki C++**: plik `.cpp`/`.cc`/`.cxx` z `links` kompiluje `cxxFor(cc)` (`FINCH_CXX` albo `clang` → `clang++`,
+`…gcc` → `…g++`, `cc` → `c++`), a potem ten sam sterownik linkuje, co dołącza bibliotekę standardową C++.
**Android**: `--target android` to `aarch64-linux-android24`; kompilator C to clang z NDK,
-`toolchains/llvm/prebuilt//bin/aarch64-linux-android24-clang` (z `ANDROID_NDK_HOME`/`ANDROID_NDK_ROOT`), linkowanie
-dodaje `-llog` i nie dodaje `-lpthread` (bionic ma go w libc). Wariant OS modułów to `linux`.
+`toolchains/llvm/prebuilt//bin/aarch64-linux-android24-clang` (z `ANDROID_NDK_HOME`/`ANDROID_NDK_ROOT`);
+linkowanie dodaje `-llog`, a nie dodaje `-lpthread` (bionic ma wątki w libc). Wariantem modułów jest `linux`.
-**`finch test`** kompiluje jak `run`, z `Options.test`: wszystkie funkcje są dalej definiowane, ale `@main` wywołuje
-`finch_test_run(nazwa, fn)` dla każdego bloku `test` z modułów użytkownika (`__test_`, funkcja `void` zrobiona przez
-parser), a na końcu `finch_test_summary()`. Runtime uruchamia każdy test pod `setjmp`; `fatal()` (każdy błąd w czasie
-działania i nieudany `assert`) wraca przez `longjmp` z `fatal_hook`, więc jeden nieudany test nie kończy całości.
-Pamięć, którą test miał w tym momencie, nie jest zwalniana.
+**`finch test`** kompiluje tak jak `run`, z `Options.test`: wszystkie funkcje są definiowane jak zwykle, ale `@main` woła
+`finch_test_run(nazwa, fn)` dla każdego bloku `test` z modułów użytkownika (`__test_`, funkcja `void` utworzona przez
+parser), a potem `finch_test_summary()`. Runtime uruchamia każdy test pod `setjmp`; `fatal()` (każdy błąd w trakcie
+działania i nieudany `assert`) wraca przez `longjmp` z pomocą `fatal_hook`, więc jeden nieudany test nie przerywa reszty.
+Pamięć, którą test miał w chwili błędu, nie jest zwalniana.
+
+**`finch fmt`** działa na liniach: śledzi `{ }`, `( )`/`[ ]`, teksty (razem z `\( )`), `'…'`, `"""` i `/* */`.
+Linia, która zaczyna się na głębokości nawiasów 0 i nie kontynuuje linii zakończonej operatorem albo `,`, dostaje
+wcięcie 4 × głębokość klamer; pozostałe (kontynuacje, wnętrza `"""`) przesuwają się o tyle samo co linia, od której
+zaczęła się instrukcja, więc zachowują kształt (i usuwanie wcięcia z `"""`).
-**`finch fmt`** działa na liniach: śledzi `{ }`, `( )`/`[ ]`, teksty (z `\( )`), `'…'`, `"""` i `/* */`. Linia, która
-zaczyna się na głębokości nawiasów 0 i nie ciągnie linii zakończonej operatorem albo `,`, dostaje wcięcie 4 × głębokość
-klamer; pozostałe (ciągi dalsze, wnętrza `"""`) przesuwa się o tyle samo co linię, która zaczęła instrukcję, więc ich
-kształt (i ucinanie wcięcia `"""`) zostaje.
---
## 7. System typów
@@ -215,55 +230,61 @@ kształt (i ucinanie wcięcia `"""`) zostaje.
| `char` `i8` `u8` / `i16` `u16` / `i32` `u32` / `int` `u64` | `i8` / `i16` / `i32` / `i64` |
| `f32` / `float` | `float` / `double` |
| `str`, `[]T` | `{ ptr, i64, i64 }` (wskaźnik, długość, pojemność) |
-| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64 }` (entries, len, used, cap, index, icap; zob. src/gen.fch) |
+| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64, i64 }` (wpisy, len, used, cap, indeks, icap, adds; zob. src/gen.fch) |
| `ptr`, `ptr[T]`, `null` | `ptr` (nieprzezroczysty) |
| `[N]T` | `[N x T]` |
-| struktura Fincha | nazwany typ struct, naturalny układ |
-| `self` metody | `ptr` (pierwszy parametr) |
-| wynik `-> T!` | `{ i1 ok, T value, str error }` (`i8` w miejscu `T` dla `-> !`) |
-| struktura C | nazwany **upakowany** (packed) struct z jawnymi bajtami wypełnienia (§5) |
+| `f32x8`, `i8x32`, … | `<8 x float>`, `<32 x i8>` (§7, Wektory) |
+| struktura Fincha | nazwany typ struktury, naturalny układ |
+| `self` w metodzie | `ptr` (pierwszy parametr) |
+| wynik `-> T!` | `{ i1 ok, T wartosc, str blad }` (`i8` zamiast `T` dla `-> !`) |
+| struktura C | nazwana struktura **packed** z jawnymi bajtami wyrównania (§5) |
-Znakowość istnieje tylko w typie Fincha i wybiera `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`,
-`sext/zext`, `sitofp/uitofp`, `fptosi/fptoui`.
+Znak liczby istnieje tylko w typie Fincha i wybiera `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`, `sext/zext`,
+`sitofp/uitofp`, `fptosi/fptoui`.
### Wartości
-```cpp
-struct Value_ { Value *v; FType type; bool literal; bool fresh; };
+```c
+struct Val {
+ str v // operand IR: %t12, 42, @.str.3, zeroinitializer
+ ast.Type ty
+ bool fresh
+ bool lit
+}
```
-- **`literal`**: liczba wpisana w kod albo zaimportowana stała C. Może stać się dowolnym typem całkowitym,
- w którym się mieści (`fits()`), albo `f32`. Przechodzi przez minus jednoargumentowy i arytmetykę samych
- literałów zwijaną do stałej; jawne konwersje ją kasują (`u8(250)` to stała z typem, więc `u8(250) + 10` zawija się do 4).
-- **`fresh`**: wartość posiadająca pamięć (str/tablica/struktura z takimi polami), której nikt jeszcze nie
- trzyma: wynik wywołania, literał, sklejenie, konstruktor. Trzeba ją zapisać (przenieść) albo zwolnić (§8).
+- **`lit`**: liczba zapisana w kodzie albo stała zaimportowana z C. Może stać się dowolnym typem całkowitym,
+ w którym się mieści (`fits()`), albo `f32`. Przetrwa jednoargumentowy minus i działania na samych literałach,
+ które składają się do stałej; jawne konwersje ją usuwają (`u8(250)` to stała z typem, więc `u8(250) + 10` daje 4).
+- **`fresh`**: wartość posiadająca pamięć (str, tablica, struktura z takimi polami), której jeszcze nikt nie trzyma:
+ wynik wywołania, literał, połączenie tekstów, konstruktor. Trzeba ją zapisać (przenieść) albo zwolnić (§8).
### Konwersje niejawne: `coerce(v, want)`
-Identyczne typy; `null` → dowolny wskaźnik; wskaźnik z typem ↔ `ptr` bez typu; całkowita → całkowita,
-jeśli wartość to mieszczący się literał albo typ się rozszerza (`widens()`: ta sama znakowość i więcej bitów
-albo bez znaku → ściśle większa ze znakiem); całkowita → zmiennoprzecinkowa; `f32` → `f64`; literał `f64` → `f32`.
-Nic więcej: błąd podpowiada jawną konwersję. Typy posiadające pamięć konwertują się tylko na siebie.
-Literały tablic biorą typ elementu z miejsca, do którego trafiają (`exprWant()`), więc `[]f32 xs = [1, 2.5]` działa.
+Identyczne typy; `null` → dowolny wskaźnik; wskaźnik z typem ↔ `ptr` bez typu; liczba całkowita → całkowita, jeśli
+to pasujący literał albo typ się „poszerza” (`widens()`: ten sam znak i więcej bitów, albo bez znaku → większy ze znakiem);
+liczba całkowita → zmiennoprzecinkowa; `f32` → `f64`; literał `f64` → `f32`. Nic poza tym: komunikat błędu podpowiada
+jawną konwersję. Typy posiadające pamięć przechodzą tylko w siebie. Literał tablicy bierze typ elementów od miejsca,
+do którego trafia (`exprWant()`), więc `[]f32 xs = [1, 2.5]` działa.
### Argumenty operatorów: `unify(l, r)`
-Strona-literał dopasowuje się do drugiej (także literał float obok `f32`: `x * 2.0` zostaje `f32`); w przeciwnym razie
-rośnie strona, która się rozszerza; `i32` + `u32` to błąd; całkowita + zmiennoprzecinkowa → zmiennoprzecinkowa;
-f32 + f64 → f64. Nie ma promocji całkowitych z C: `u8 + u8` zostaje `u8`. Dwa literały całkowite składają się w literał,
-więc `FLAG_A | FLAG_B` dalej mieści się w `u64` albo `u32`.
+Strona, która jest literałem, dopasowuje się do drugiej (także literał zmiennoprzecinkowy obok `f32`: `x * 2.0`
+zostaje `f32`); w przeciwnym razie rośnie ta strona, którą da się poszerzyć; `i32` + `u32` to błąd; całkowita +
+zmiennoprzecinkowa → zmiennoprzecinkowa; f32 + f64 → f64. Nie ma promocji liczb całkowitych jak w C: `u8 + u8`
+zostaje `u8`. Dwa literały całkowite składają się w literał, więc `FLAG_A | FLAG_B` nadal mieści się w `u64` albo `u32`.
### Konwersje jawne: `convert()`
| Z → Na | IR |
|---|---|
-| int/char ↔ int/char | `trunc`/`sext`/`zext` wg znakowości źródła |
+| int/char ↔ int/char | `trunc`/`sext`/`zext` według znaku źródła |
| bool → int | `zext` |
-| float ↔ int | `fptosi`/`fptoui`, `sitofp`/`uitofp` |
-| float ↔ float | `fpext`/`fptrunc` |
+| zmiennoprzecinkowa ↔ całkowita | `fptosi`/`fptoui`, `sitofp`/`uitofp` |
+| zmiennoprzecinkowa ↔ zmiennoprzecinkowa | `fpext`/`fptrunc` |
| int → bool | `icmp ne 0` |
| liczba/bool/char → str | `finch_str_from_int/uint/float/char/bool` (fresh) |
-| str → int/float | `finch_str_to_int/float` (panic przy złym tekście) |
+| str → int/float | `finch_str_to_int/float` (przy złym tekście błąd w trakcie działania) |
| `[]u8`/`[]char` → str | `finch_str_from_bytes` |
| ptr → str | `finch_str_from_c` (pożyczony, `cap = -1`) |
| ptr ↔ ptr | nic |
@@ -271,102 +292,133 @@ więc `FLAG_A | FLAG_B` dalej mieści się w `u64` albo `u32`.
### Porównania
-Wskaźniki/null: tylko `icmp eq/ne ptr`. `str`: `finch_str_eq` (długość + `memcmp`) dla `==`/`!=`,
-`finch_str_cmp` dla porządku. Liczby zmiennoprzecinkowe: `oeq olt ole ogt oge` i `une` dla `!=`.
-Całkowite i znaki wg znakowości (char bez znaku). Bool: tylko równość. Tablice/struktury: błąd.
+Wskaźniki i `null`: tylko `icmp eq/ne ptr`. `str`: `finch_str_eq` (długość + `memcmp`) dla `==`/`!=`,
+`finch_str_cmp` dla kolejności. Liczby zmiennoprzecinkowe: `oeq olt ole ogt oge` oraz `une` dla `!=`. Liczby całkowite
+i `char` według znaku (`char` bez znaku). Bool: tylko równość. Tablice i struktury: błąd.
----
+### Wektory
+
+`ast.vecType(nazwa)` rozpoznaje `x` (elementy `i8`…`u64`, `f32`, `f64`; N = 2…64; najwyżej 512 bitów)
+i zwraca `Type(kind: "vec", elem: [element], count: N)`, czyli wektor LLVM ``. Parser przyjmuje taką nazwę
+wszędzie tam, gdzie może stać typ; wywołanie `f32x8(…)` trafia do `vecMake()`.
+
+| Finch | IR |
+|---|---|
+| `f32x8(x)` | `insertelement` do elementu 0 + `shufflevector … zeroinitializer` (rozpowielenie) |
+| `f32x8(a, …, h)` | ciąg `insertelement` |
+| `f32x8(lista, i)` | sprawdzenie zakresu `i … i+7`, potem `load <8 x float>, ptr …, align 4` (wyrównanie elementu) |
+| `f32x8(i32x8 v)` | `sitofp`, `fptosi`, `sext`, `trunc`… na całym wektorze |
+| `v + w`, `v * 2.0` | `fadd`/`add`… na wektorach; argument skalarny jest dopasowywany do typu elementu i rozpowielany |
+| `v[i]` | miejsce w pamięci: `getelementptr , ptr %v, i64 i` po sprawdzeniu zakresu; albo `extractelement` na wartości tymczasowej |
+| `v == w` | `fcmp oeq`/`icmp eq` → `` → `llvm.vector.reduce.and` |
+| `min`, `max`, `abs` | `llvm.minnum`/`maxnum`, `llvm.smin`/`umin`…, `llvm.fabs`/`llvm.abs` |
+| `v.sum()`, `.min()`, `.max()` | `llvm.vector.reduce.fadd` (z `reassoc`), `.add`, `.fmin`, `.smin`… |
+| `v.sqrt()`, `.floor()`, `.ceil()`, `.round()` | funkcja wewnętrzna `llvm.*` o tej nazwie |
+| `v.fma(b, c)` | `llvm.fmuladd` (jedna instrukcja tam, gdzie procesor ma FMA; `llvm.fma` bez FMA wołałoby funkcję z libm dla każdego elementu) |
+| `v.store(lista, i)` | sprawdzenie zakresu, `store <8 x float> …, align 4` |
+
+Dzielenie całkowite `/` i `%` jest odrzucane: ani x86, ani ARM nie mają instrukcji dzielącej liczby całkowite element
+po elemencie, więc LLVM i tak rozbiłby je na pojedyncze dzielenia. Porównania kolejności są odrzucane, bo ich wynikiem
+byłaby maska, dla której Finch nie ma typu.
+
+**Wyrównanie.** LLVM daje `<8 x float>` wyrównanie ABI równe 32 (jego rozmiarowi), a `malloc` i finchowe `alloca … align 16`
+gwarantują tylko 16. `llty()` zapisuje każdy wektor szerszy niż 16 bajtów w `g.wide`, a `resolveStructBody` zapisuje tam
+strukturę, która taki wektor zawiera; `load()` i `store()` dodają dla tych typów `, align 16`. Bez tego LLVM mógłby
+użyć `vmovaps` na adresie wyrównanym do 16 i program by się wysypał.
+
+**Jakie instrukcje** wybiera clang: `--cpu ` dodaje `-march=` (x86-64) albo `-mcpu=` (ARM64)
+do kompilacji IR, więc to samo IR staje się kodem SSE2, AVX2 + FMA, AVX-512 albo NEON. Wektory szersze niż rejestry
+procesora są dzielone na części.
---
## 8. Własność i pamięć
-### 9.1 Model
+### 8.1 Model
-Każda wartość **typu posiadającego pamięć** (`str`, `[]T` albo struktura Fincha z takim polem) ma
-dokładnie jednego właściciela: zmienną, element tablicy, pole struktury albo komórkę na stercie z `new()`.
-Właściciel zwalnia wartość, gdy sam znika. Nie ma liczenia referencji ani garbage collectora;
-każda decyzja zapada statycznie.
+Każda wartość **typu posiadającego pamięć** (`str`, `[]T` albo struktura Fincha z takim polem) ma dokładnie jednego
+właściciela: zmienną, element tablicy, pole struktury albo komórkę na stercie z `new()`. Właściciel zwalnia wartość,
+gdy sam znika. Nie ma liczenia referencji ani garbage collectora; wszystkie decyzje zapadają przy kompilacji.
| Sytuacja | Co się dzieje |
|---|---|
-| `x := expr` / `T x = expr` / `x = expr` / pole, element, `push` | **`own(v)`**: wartość fresh jest przenoszona; pożyczona jest kopiowana w głąb |
-| `x = …` (typ posiadający) | najpierw liczona jest nowa wartość, stara zwalniana po zapisie |
-| koniec bloku, `return`, `break`, `continue` | posiadane zmienne każdego opuszczanego zasięgu są zwalniane (od najnowszej), uruchamiają się defery |
-| `return lokalna` | zmienna jest **przenoszona na zewnątrz** (oznaczana jako `moved` na czas sprzątania tego returna) |
-| przekazanie argumentu | **pożyczka**: funkcja dostaje płytką kopię i nigdy jej nie zwalnia; argumenty fresh zwalnia wywołujący po wywołaniu |
-| parametr, który funkcja zmienia | kopiowany na wejściu i posiadany przez funkcję (decyduje `mutates()`, niżej) |
-| wartość tymczasowa, której nikt nie trzyma (`print(a + b)`, `f(g())`, instrukcja-wyrażenie) | **`release(v)`**: zwalniana zaraz po użyciu |
-| `a.pop()`, `a.remove(i)` | element jest przenoszony na zewnątrz (fresh) |
-| `for x in lista` | `x` pożycza kolejne elementy; jeśli ciało może zmieniać `lista`, elementy są kopiowane |
-| wywołanie metody `v.m()` | `self` **to jest** `v` (przekazywany jest jego adres): bez kopii; zmiany widzi wywołujący |
-| mapy | są właścicielami kluczy i wartości tak jak tablice elementów; `m[k] = v` przenosi/kopiuje `v` i zwalnia starą wartość |
+| `x := wyr` / `T x = wyr` / `x = wyr` / pole, element, `push` | **`own(v)`**: wartość świeża zostaje przeniesiona, pożyczona jest głęboko kopiowana |
+| `x = …` (typ posiadający) | najpierw liczona jest nowa wartość, a stara jest zwalniana po zapisie |
+| koniec bloku, `return`, `break`, `continue` | zmienne we wszystkich opuszczanych zasięgach są zwalniane (od najnowszej), wykonują się `defer` |
+| `return zmienna_lokalna` | zmienna jest **przenoszona** (oznaczona jako `moved` na czas sprzątania przy tym `return`) |
+| przekazanie argumentu | **pożyczka**: funkcja dostaje płytką kopię i nigdy jej nie zwalnia; świeże argumenty zwalnia wywołujący po wywołaniu |
+| parametr, który funkcja zmienia | kopiowany na wejściu i należący do funkcji (decyduje `mutates()`, niżej) |
+| wartość tymczasowa, której nikt nie zatrzymuje (`print(a + b)`, `f(g())`, wyrażenie jako instrukcja) | **`release(v)`**: zwalniana zaraz po użyciu |
+| `a.pop()`, `a.remove(i)` | element jest przenoszony na zewnątrz (świeży) |
+| `for x in lista` | `x` pożycza kolejne elementy; jeśli ciało pętli może zmienić `lista`, elementy są kopiowane |
+| wywołanie metody `v.m()` | `self` **to jest** `v` (przekazywany jest adres): bez kopii, zmiany widzi wywołujący |
+| mapy | są właścicielami kluczy i wartości tak jak tablice elementów; `m[k] = v` przenosi albo kopiuje `v` i zwalnia starą wartość |
-Literał tekstowy to wartość fresh z `cap = 0`: przeniesienie nic nie kosztuje, zwalnianie jest pomijane
+Literał tekstowy to świeża wartość z `cap = 0`: przeniesienie go nic nie kosztuje, zwalnianie jest pomijane
(`release()` ignoruje stałe), a zapis do niego (`s[i] = c`) najpierw robi kopię na stercie (`finch_str_own`).
-Teksty z C są pożyczane z `cap = -1` i zawsze kopiowane, zanim zostaną zatrzymane.
+Teksty z C są pożyczone z `cap = -1` i zawsze kopiowane, zanim zostaną zatrzymane.
-### 9.2 Pożyczanie parametrów: `mutates()`
+### 8.2 Pożyczanie parametrów: `mutates()`
-Przy deklarowaniu funkcji każdy parametr posiadający pamięć jest sprawdzany przez `mutates(nazwa, ciało)`:
-przypisanie, którego zmienna-korzeń (`rootVar()`: przez `.pole` i `[i]`) jest tym parametrem, metoda
-zmieniająca (`push pop insert remove clear resize sort reverse`) albo `addr(…)` na nim. Niezmieniane
-parametry są pożyczane (`Fn::borrowParam`), zmieniane kopiowane raz na wejściu. Analiza jest składniowa
-i zachowawcza: wszystko, co *może* zmienić parametr, powoduje kopię, co zawsze jest poprawne.
-Ta sama analiza decyduje, czy `for x in lista` musi kopiować elementy.
+Przy deklarowaniu funkcji każdy parametr typu posiadającego jest sprawdzany przez `mutates(nazwa, cialo)`: przypisanie,
+którego zmienną bazową (`rootVar()`: przez `.pole` i `[i]`) jest ten parametr, zmieniająca metoda na nim
+(`push pop insert remove clear resize sort reverse`) albo `addr(…)` na nim. Niezmieniane parametry są pożyczane
+(`FnSig.borrow`); zmieniane są kopiowane raz, na wejściu. Analiza jest składniowa i ostrożna: wszystko, co *mogłoby*
+zmienić parametr, powoduje kopię, a to zawsze jest poprawne. Ta sama analiza decyduje, czy `for x in lista` musi kopiować elementy.
-Metody biorą w tym udział: `declareFns()` najpierw ustala, które metody zmieniają `self`
-(`mutates("self", ciało)`), powtarzając, aż nic się nie zmieni, bo wywołanie zmieniającej metody na `self`
-też zmienia `self`. Ich nazwy trafiają do `mutatingMethods`, które `mutatesExpr()` traktuje jak `push`:
-`p.przesun(1)` zmienia `p`. Dopiero potem liczone jest `borrowParam` dla każdej funkcji i metody.
+Metody też w tym uczestniczą: `declare()` najpierw ustala, które metody zmieniają `self` (`mutates("self", cialo)`),
+powtarzając to, dopóki coś się zmienia, bo wywołanie zmieniającej metody na `self` też zmienia `self`. Ich nazwy trafiają
+do `g.mm`, które `mutates()` traktuje jak `push`: `p.przesun(1)` zmienia `p`. Dopiero potem dla każdej funkcji i metody
+liczone jest `borrow`.
-### 9.3 Kopiowanie i zwalnianie
+### 8.3 Kopiowanie i zwalnianie
-`dropAt(addr, T)` / `copyAt(dst, src, T)` działają na adresach:
+`dropAt(adres, T)` / `copyAt(cel, zrodlo, T)` działają na adresach:
- `str`: `finch_str_drop` (zwalnia, gdy `cap > 0`) / `finch_str_copy` (literały współdzieli, resztę duplikuje).
-- `[]T` i struktury posiadające: generowane helpery `finch.drop.` i `finch.copy.`, tworzone
- raz na typ (`dropFn`, `copyFn`, klucz z `typeKey()`, np. `arr_S_main_Gracz`). Tablice zwalniają każdy posiadający
- element, a potem `finch_arr_free`; kopie wołają `finch_arr_clone_raw`, a potem kopiują w głąb posiadające
- elementy na surowe bity. Struktury zwalniają/kopiują tylko pola posiadające.
+- `[]T` i struktury posiadające pamięć: wygenerowane funkcje pomocnicze `finch.drop.` i `finch.copy.`,
+ tworzone raz na typ (`dropFn`, `copyFn`, klucz z `typeKey()`, np. `arr_S_main_Player`). Tablica zwalnia każdy
+ element posiadający pamięć, a potem woła `finch_arr_free`; kopia woła `finch_arr_clone_raw`, a potem głęboko kopiuje
+ takie elementy w miejsce surowych bitów. Struktury zwalniają i kopiują tylko pola posiadające pamięć.
-Helpery są emitowane we własnych funkcjach (`HelperScope` zapamiętuje pozycję buildera i wyłącza lokacje
-debugowe), mogą wołać się rekurencyjnie (`struct Wezel { []Wezel dzieci }`) i są wstawiane (inline) przez O2.
+Funkcje pomocnicze powstają jako osobne funkcje (`beginHelper()` / `endHelper()` odkładają na bok funkcję, która jest
+w trakcie pisania, i wyłączają lokalizacje debuggera), mogą wołać się nawzajem rekurencyjnie (`struct Node { []Node kids }`),
+a O2 je wkleja w miejsce wywołań.
-### 9.4 Jak to wygląda
+### 8.4 Jak to wygląda
```c
-fn greet(str name) -> str {
- msg := "Hi, " + name
- return msg
+fn przywitaj(str imie) -> str {
+ wiad := "Cześć, " + imie
+ return wiad
}
fn main() {
a := ["x"]
b := a
- b.push(greet("Ola"))
+ b.push(przywitaj("Ola"))
print(b[1])
}
```
-IR funkcji `main` z `-O0` (skrócony):
+IR funkcji `main` przy `-O0` (w skrócie):
```llvm
%3 = call ptr @finch_alloc(i64 24) ; ["x"]: jeden element na stercie
store { ptr, i64, i64 } { ptr @str.1, i64 1, i64 0 }, ptr %4 ; literał, cap 0
...
- store { ptr, i64, i64 } %7, ptr %a ; przeniesione do a (fresh)
+ store { ptr, i64, i64 } %7, ptr %a ; przeniesione do a (świeże)
%a1 = load { ptr, i64, i64 }, ptr %a
store { ptr, i64, i64 } %a1, ptr %1
call void @finch.copy.arr_str(ptr %2, ptr %1) ; b := a kopiuje
%8 = load { ptr, i64, i64 }, ptr %2
store { ptr, i64, i64 } %8, ptr %b
- %9 = call { ptr, i64, i64 } @finch.greet({ ptr, i64, i64 } { ptr @str.2, i64 3, i64 0 })
+ %9 = call { ptr, i64, i64 } @finch.przywitaj({ ptr, i64, i64 } { ptr @str.2, i64 3, i64 0 })
...
- call void @finch_arr_reserve(ptr %b, i64 %12, i64 24) ; push: powiększ, jeśli trzeba
- store { ptr, i64, i64 } %9, ptr %15 ; wynik greet przeniesiony, bez kopii
+ call void @finch_arr_reserve(ptr %b, i64 %12, i64 24) ; push: powiększenie, jeśli trzeba
+ store { ptr, i64, i64 } %9, ptr %15 ; wynik przywitaj przeniesiony, bez kopii
...
- %20 = icmp uge i64 1, %19 ; b[1]: kontrola zakresu
+ %20 = icmp uge i64 1, %19 ; b[1]: sprawdzenie zakresu
br i1 %20, label %out.of.range, label %in.range
...
call void @finch.drop.arr_str(ptr %b) ; koniec main: od najnowszej
@@ -374,103 +426,101 @@ IR funkcji `main` z `-O0` (skrócony):
ret void
```
-a `greet` zwraca `msg` bez kopiowania (`ret … %5` po wczytaniu slotu: zmienna została przeniesiona).
+a `przywitaj` zwraca `wiad` bez kopiowania (`ret … %5` po wczytaniu zmiennej: została przeniesiona).
-### 9.5 defer
+### 8.5 defer
-`defer instrukcja` dokłada do bieżącego zasięgu `Cleanup{isDefer, body, order = bieżące varOrder}`
-i niczego w tym miejscu nie emituje. Za każdym razem, gdy emitowane jest sprzątanie zasięgu (dojście do
-końca bloku, `return`, wyjście przez `break`/`continue`), ciało jest generowane **w tym wyjściu**, z
-`deferLimit = order`, żeby `lookup()` ukrył zmienne zadeklarowane po defer: te są wtedy już zwolnione
-(sprzątanie idzie w odwrotnej kolejności). `return`/`break`/`continue` w instrukcji defer to błędy (`inDefer`).
-Ciała defer są więc powielane na każdą ścieżkę wyjścia, przez co na zwykłej ścieżce nic nie kosztują.
+`defer instrukcja` zapisuje instrukcję w bieżącym zasięgu (`Scope.defers`, razem z `orders`: ile zmiennych wtedy
+istniało) oraz `Cleanup{isDefer}`; w tym miejscu nic nie emituje. Ilekroć emitowane jest sprzątanie zasięgu (koniec
+bloku, `return`, wyjście przez `break`/`continue`), ciało jest generowane **przy tym wyjściu**, z `deferLimit`
+ustawionym tak, by `lookup()` ukrywało zmienne zadeklarowane po `defer`: w tym momencie są już zwolnione (sprzątanie
+idzie od końca). `return`/`break`/`continue` wewnątrz odroczonej instrukcji to błąd (`inDefer`). Ciała `defer` są więc
+powielane na każdą drogę wyjścia i dlatego na zwykłej ścieżce nic nie kosztują.
-### 9.6 Pamięć ręczna
+### 8.6 Pamięć zarządzana ręcznie
-`new(v)` alokuje `sizeof(T)` przez `finch_alloc`, zapisuje `own(v)` i zwraca `ptr[T]`. `free(p)` zwalnia
-wskazywaną wartość, jeśli `T` posiada pamięć (pomijając null), i woła `free` z libc. Wskaźniki nigdy
-niczego nie posiadają: `addr(x)` może wisieć po końcu bloku `x`; nie jest to sprawdzane.
+`new(v)` przydziela `sizeof(T)` przez `finch_alloc`, zapisuje `own(v)` i zwraca `ptr[T]`. `free(p)` zwalnia to, na co
+wskazuje wskaźnik, jeśli `T` posiada pamięć (pomija `null`), i woła `free` z libc. Wskaźniki nigdy niczego nie posiadają:
+`addr(x)` może wskazywać w próżnię po końcu bloku `x`; tego się nie sprawdza.
-### 9.7 Weryfikacja
+### 8.7 Sprawdzanie
`MEMCHECK=1 tests/run.sh` buduje każdy program testowy i uruchamia go pod valgrindem z
-`--leak-check=full --errors-for-leak-kinds=all`: zero wycieków i zero nieprawidłowych dostępów we wszystkich,
-w tym przy returnach z wnętrza pętli, `continue`/`break` z posiadanymi zmiennymi, deferach, modyfikacji
-podczas iteracji, zagnieżdżonych tablicach struktur i przekazywaniu struktur C.
+`--leak-check=full --errors-for-leak-kinds=all`: zero wycieków i zero błędnych dostępów we wszystkich, w tym przy
+`return` z wnętrza pętli, `continue`/`break` ze zmiennymi posiadającymi pamięć, `defer`, zmianach w trakcie iteracji,
+zagnieżdżonych tablicach struktur i przekazywaniu struktur C.
---
----
+## 9. Typy generyczne, enumy i match
-## 9. Generyki, enumy i match
-
-Jak są generowane, opisuje §4. Reguły widoczne dla użytkownika: sekcje 23 i 24 [przewodnika dla techników](dla-technikow.md).
-Testy: `tests/new/generics.fch`, `tests/new/enums.fch` i odmowy w `tests/new/fail/`.
+Jak są generowane, opisuje §4. Reguły widoczne dla użytkownika są w rozdziałach 23 i 24
+[przewodnika dla techników](dla-technikow.md). Testy to `tests/new/generics.fch`, `tests/new/enums.fch` i oczekiwane
+błędy w `tests/new/fail/`.
---
## 10. Bootstrap
-Kompilator budowany jest z **ziarna**: własnego IR zapisanego jako `bootstrap/.ll` (zobacz
-[bootstrap/README.md](../../bootstrap/README.md)). `bootstrap/build.sh`:
+Kompilator powstaje z **pliku startowego** (ang. *seed*): jego własnego IR, zapisanego w repozytorium jako
+`bootstrap/.ll` (zob. [bootstrap/README.md](../../bootstrap/README.md)). `bootstrap/build.sh` robi:
```
-ziarno .ll ──clang──► finch0 ──buduje src/──► finch1 ──buduje src/──► build/finch
+plik .ll ──clang──► finch0 ──buduje src/──► finch1 ──buduje src/──► build/finch
```
-i przerywa, jeśli `finch1` i `finch` nie dają identycznego IR (punkt stały: kompilator kompiluje siebie tak samo, niezależnie
-od tego, kto go zbudował). `bootstrap/build.sh seed` przepisuje ziarno z bieżącego kompilatora; rób to, gdy źródło
-kompilatora zacznie używać czegoś, czego zatwierdzone ziarno nie ma.
+i przerywa, jeśli `finch1` i `finch` nie dają identycznego bajt w bajt IR (punkt stały: kompilator kompiluje sam siebie
+tak samo, niezależnie od tego, co go zbudowało). `bootstrap/build.sh seed` zapisuje plik startowy na nowo z bieżącego
+kompilatora; trzeba to zrobić, gdy kod kompilatora zaczyna używać czegoś, czego zapisany plik startowy nie zna.
-Ziarno jest na system, bo zapieczono w nim konwencję wywołań C dla wywołań libclang (§5). Linux x86-64 i Linux ARM64 mają
-ziarna. Dla macOS i Windowsa trzeba by ziaren zrobionych z nagłówkami Apple i Microsoftu.
+Plik startowy jest osobny dla każdego systemu, bo zawiera konwencję wywołań C dla funkcji libclang (§5). Są pliki
+dla Linuksa x86-64 i ARM64, macOS ARM64 i x86-64 (zrobione z nagłówkami SDK Apple) oraz Windowsa x86-64 (z nagłówkami
+llvm-mingw). CI na każdym z tych systemów buduje z nich kompilator i uruchamia tam testy.
---
## 11. Serwer języka
-`finch lsp` mówi JSON-RPC przez stdin/stdout (ramki `Content-Length`, `std/json`). Sam nie sprawdza typów: przy każdej zmianie
-uruchamia `finch index plik.fch --overlay ścieżka=kopia …`, gdzie nakładki to niezapisany tekst edytora. W trybie `index`
-kompilator wypisuje rekordy rozdzielone tabulatorami:
+`finch lsp` rozmawia przez JSON-RPC na stdin/stdout (ramki `Content-Length`, `std/json`). Sam nie sprawdza typów:
+przy każdej zmianie uruchamia `finch index plik.fch --overlay sciezka=kopia …`, gdzie nakładki to niezapisany tekst
+z edytora. W trybie `index` kompilator zamiast IR wypisuje rekordy rozdzielone tabulatorami:
| Rekord | Znaczenie |
|---|---|
-| `@R plik linia kol dł plikDef liniaDef kolDef podpowiedź` | odwołanie: co pokazuje hover i gdzie jest definicja |
+| `@R plik linia kol dlug plikDef liniaDef kolDef dymek` | odwołanie: co pokazać w dymku i gdzie jest definicja |
| `@V nazwa typ plik linia kol plikFn liniaFn koniecFn` | zmienna i funkcja, w której żyje |
-| `@F moduł nazwa odb plik linia kol koniec sygnatura parametry` | funkcja lub metoda |
+| `@F modul nazwa odbiorca plik linia kol koniec sygnatura parametry` | funkcja albo metoda |
| `@S` / `@SF`, `@E`, `@I` | struktury (i pola), enumy, importy |
+| `@W` | ostrzeżenie |
-Pierwszy błąd wypisywany jest jako `ścieżka:linia:kol: error: komunikat` i staje się diagnostyką. Gdy kod się nie kompiluje,
-serwer podpowiada dalej z ostatniej wersji, która się skompilowała. Test akceptacyjny: `tests/lsp_test.py` (22 sprawdzenia).
+Pierwszy błąd jest wypisywany jako `sciezka:linia:kol: error: komunikat` i staje się diagnostyką. Gdy kod się nie
+kompiluje, serwer nadal podpowiada uzupełnienia i parametry na podstawie ostatniej wersji, która się kompilowała.
+Testem akceptacyjnym jest `tests/lsp_test.py`.
---
-## 12. Semantyka a C
+## 12. Semantyka w porównaniu z C
-| Sytuacja | C | Finch 2.0 |
+| Sytuacja | C | Finch |
|---|---|---|
-| Przepełnienie liczby ze znakiem | UB | zdefiniowane zawinięcie (bez `nsw`) |
-| Dzielenie całkowite przez zero, `INT_MIN / -1` | UB | błąd kompilacji, jeśli stałe, inaczej błąd w czasie działania |
-| Indeks tablicy poza zakresem | UB | błąd w czasie działania (także dla pól `[N]T` struktur C) |
-| Dereferencja null | UB | błąd w czasie działania dla `.value`, `.pole`, `[i]` |
+| Przepełnienie liczby całkowitej ze znakiem | UB | zdefiniowane zawinięcie (bez `nsw`) |
+| Dzielenie całkowite przez zero, `INT_MIN / -1` | UB | błąd kompilacji, jeśli to stała; w przeciwnym razie błąd w trakcie działania |
+| Indeks tablicy poza zakresem | UB | błąd w trakcie działania (także dla pól `[N]T` struktur C) |
+| Dereferencja `null` | UB | błąd w trakcie działania dla `.value`, `.pole`, `[i]` |
| Użycie po zwolnieniu / podwójne zwolnienie tablic, tekstów, struktur | częste błędy | niemożliwe: tych nie zwalnia się ręcznie |
-| Wyciek tablic, tekstów, struktur | częsty błąd | niemożliwy bez `new` |
+| Wyciek pamięci tablic, tekstów, struktur | częsty błąd | niemożliwy bez `new` |
| `x & 1 == 0` | `x & (1 == 0)` | `(x & 1) == 0` |
-| Niejawne zwężanie, mieszanie ze znakiem / bez znaku | po cichu | błędy kompilacji |
-| Niezainicjalizowane zmienne | UB | niemożliwe (zero albo wartości domyślne) |
-| Brak return | UB, jeśli użyty | błąd kompilacji |
-| Kolejność obliczeń | w większości nieokreślona | od lewej do prawej; w przypisaniu najpierw prawa strona |
-| Przesunięcie ≥ szerokość, float→int poza zakresem | UB | nadal poison (jeszcze niesprawdzane) |
+| Niejawne zwężanie, mieszanie liczb ze znakiem i bez | po cichu | błędy kompilacji |
+| Niezainicjowane zmienne | UB | niemożliwe (zero albo wartości domyślne) |
+| Brak `return` | UB, jeśli wynik jest użyty | błąd kompilacji |
+| Kolejność obliczania | w dużej mierze nieokreślona | od lewej do prawej; w przypisaniu najpierw prawa strona |
+| Przesunięcie o ≥ szerokość, liczba zmiennoprzecinkowa poza zakresem przy zamianie na całkowitą | UB | poison, jak w LLVM (niesprawdzane) |
| Wiszący wskaźnik z `addr()` / `new` + `free` | UB | nadal UB |
----
-
----
-
-### Łańcuch wywołań przy błędach w czasie działania
+### Ścieżka wywołań przy błędach w trakcie działania
Z `Options.trace` (domyślnie dla `run`, `test`, `-g`, `-O0`; `--trace`/`--no-trace`; nigdy z `--shared`) każda funkcja
-Fincha ma ramkę na swoim stosie:
+Fincha ma na stosie ramkę:
```llvm
%frame = alloca { ptr, ptr, ptr, i64 } ; ramka wywołującego, nazwa, plik, linia
@@ -479,45 +529,44 @@ store ptr @"nazwa", ...pole 1 ; ptr @"plik", pole 2 ; i64 , pole 3
store ptr %frame, ptr @finch_trace
```
-`@finch_trace` to globalna `thread_local` w module programu (model initial-exec, chyba że biblioteka albo Windows);
-każda instrukcja zapisuje swoją linię do pola 3. Po każdym wywołaniu funkcji Fincha wywołujący wpisuje z powrotem swoją
-ramkę do `@finch_trace`, co sprząta też ramki funkcji, która wróciła wcześniej, więc wiele ścieżek `return` nie
-potrzebuje epilogu. Opakowania wołane z C (thunki `addr(fn)`, `export fn`) zapamiętują i przywracają `@finch_trace`.
-`main` woła `finch_trace_init(top, set)` z dwiema małymi funkcjami do czytania i zapisu, i tak runtime (`print_trace()`
-w `fatal()` oraz uruchamianie testów) dostaje się do zmiennej wątku, której nie zna z nazwy. Koszt: ok. 3 razy na
-`fib(40)`, który jest samymi wywołaniami; dlatego zwykły `finch build` go pomija.
+`@finch_trace` to globalna zmienna `thread_local` w module programu (model initial-exec, chyba że to biblioteka
+albo Windows); każda instrukcja zapisuje swoją linię do pola 3. Po każdym wywołaniu funkcji Fincha wywołujący zapisuje
+z powrotem swoją ramkę do `@finch_trace`, co przy okazji usuwa ramki pozostawione przez funkcję, która wyszła wcześniej,
+więc na licznych ścieżkach powrotu nie trzeba nic sprzątać. Opakowania wywoływane z C (`addr(fn)`, `export fn`)
+zapamiętują i odtwarzają `@finch_trace` wokół wywołania. `main` woła `finch_trace_init(top, set)` z dwiema małymi
+funkcjami, które ją czytają i zapisują; tak runtime (`print_trace()` w `fatal()` i uruchamianie testów) dostaje się do
+zmiennej `thread_local`, której nie potrafi nazwać. Koszt: ok. 3 razy wolniej na `fib(40)`, które składa się wyłącznie
+z wywołań; dlatego zwykłe `finch build` tego nie dodaje.
### Wątki i operacje atomowe
-`std/thread.fch` to cienka warstwa nad runtime'em (`finch_thread_spawn/join`, `finch_mutex_*`: pthreads albo
-`CreateThread` i `CRITICAL_SECTION` na Windowsie). Funkcja wątku to dowolna `fn f(ptr)` podana przez `addr(f)`, co robi
-thunk wołany z C. `atomic_add/sub/swap` to `atomicrmw add/sub/xchg … seq_cst`, `atomic_load/store` to
-`load/store atomic … seq_cst, align `, a `atomic_cas` to `cmpxchg … seq_cst seq_cst` zwracające `i1`. Biorą
-`ptr[T]` do dowolnej liczby całkowitej. Tablice, teksty i mapy Fincha nie są synchronizowane; dzielenie ich między
-wątkami wymaga mutexu.
+`std/thread.fch` to cienka warstwa nad runtime'em (`finch_thread_spawn/join`, `finch_mutex_*`: pthreads albo, na
+Windowsie, `CreateThread` i `CRITICAL_SECTION`). Funkcja wątku to dowolna `fn f(ptr)`, przekazywana przez `addr(f)`,
+co tworzy opakowanie wywoływalne z C. `atomic_add/sub/swap` stają się `atomicrmw add/sub/xchg … seq_cst`,
+`atomic_load/store` to `load/store atomic … seq_cst, align `, a `atomic_cas` to `cmpxchg … seq_cst seq_cst`
+zwracające `i1`. Przyjmują `ptr[T]` do dowolnego typu całkowitego. Tablice, teksty i mapy Fincha nie są synchronizowane;
+dzielenie ich między wątkami wymaga muteksu.
### Ostrzeżenia
-`warn()` wypisuje `plik:linia:kolumna: warning: …` na stderr (więc `finch ir` daje samo IR) albo, w trybie `index`,
-rekord `@W`, który serwer języka publikuje z ważnością 2. Jedno na miejsce w kodzie. Są dwa: parametr-struktura oraz
-element `for x in lista`, gdy ich zwykłe dane (`byteSize()`: liczby, zagnieżdżone struktury, `[N]T` z C; `str`, tablice
-i mapy liczą się jako 0, bo kopiowany jest tylko ich uchwyt) mają 256 bajtów lub więcej.
+`warn()` wypisuje `plik:linia:kol: warning: …` na stderr (żeby `finch ir` pozostało czystym IR) albo, w trybie `index`,
+rekord `@W`, który serwer języka publikuje z ważnością 2. Jedno ostrzeżenie na miejsce w kodzie. Są dwa rodzaje:
+parametr będący strukturą oraz element w `for x in lista`, jeśli ich zwykłe dane (`byteSize()`: liczby, zagnieżdżone
+struktury, `[N]T` z C; `str`, tablice i mapy liczą się jako 0, bo kopiuje się tylko ich uchwyt) mają co najmniej 256 bajtów.
-### Struktura jako argument przez adres
+### Struktury przekazywane przez adres
-`argWant()` (tylko argumenty wywołań, nie przypisania): gdy parametr to `ptr[S]`, a argument to zwykła zmienna typu `S`,
-argumentem jest adres zmiennej, jakby napisano `addr(x)`. Tylko zmienne, więc nic nie jest liczone dwa razy, a na
-wartości tymczasowe nic nie wskazuje.
+`argWant()` (tylko argumenty wywołań, nie przypisania): gdy parametr ma typ `ptr[S]`, a argument jest zwykłą zmienną
+typu `S`, argumentem staje się adres tej zmiennej, tak jakby napisano `addr(x)`. Dotyczy to tylko zmiennych, więc nic
+nie jest obliczane dwa razy, a wskaźnik nigdy nie wskazuje na wartość tymczasową.
+
+---
## 13. Znane ograniczenia
-- Kompilator działa tylko na Linuksie x86-64 i ARM64 (brak ziarna dla macOS i Windowsa). Programy można budować na wszystkie trzy.
-- Wynik dla macOS jest sprawdzany tylko jako IR; Windows i ARM64 uruchamiane są w CI pod Wine i qemu.
-- Jeden błąd na uruchomienie, bez odzyskiwania.
-- Informacje `-g` najlepiej z `-O0`.
-- Sterownik używa narzędzi `sh` (`rm`, `mkdir`), więc wymaga powłoki POSIX.
-- Generyki są monomorfizowane (kod rośnie z liczbą instancji); nie ma ograniczeń na typy, błędne użycie to błąd wewnątrz
- wygenerowanego ciała.
-- Pamięć nieudanego testu nie jest zwalniana (uruchamianie testów wychodzi z niego przez `longjmp`).
-- `--target android` jest sprawdzany do pliku obiektowego; linkowanie wymaga NDK i nie było uruchamiane na urządzeniu.
-
+- Jeden błąd na uruchomienie, bez kontynuowania po nim.
+- Informacje dla debuggera (`-g`) są najlepsze z `-O0`.
+- Typy generyczne są monomorfizowane (kod rośnie z każdą instancją); nie ma ograniczeń na parametry typu, więc złe
+ użycie daje błąd wewnątrz ciała instancji.
+- Pamięć, którą miał nieudany test, nie jest zwalniana (uruchamianie testów wychodzi z niego przez `longjmp`).
+- Wektory nie mają masek (`v < w`), przetasowań ani dzielenia całkowitego.
diff --git a/docs/pl/sciaga.md b/docs/pl/sciaga.md
index cb4e7e7..d0d6c49 100644
--- a/docs/pl/sciaga.md
+++ b/docs/pl/sciaga.md
@@ -1,6 +1,6 @@
-# Ściąga Fincha
+# Ściąga z Fincha
-Cały Finch na jednej stronie. Wyjaśnienia są w [przewodnikach](../../README.pl.md#dokumentacja).
+Cały Finch na jednej stronie. Wyjaśnienia znajdziesz w [przewodnikach](../../README.pl.md#dokumentacja).
> 🇬🇧 English version: [cheatsheet.md](../en/cheatsheet.md)
@@ -8,39 +8,41 @@ Cały Finch na jednej stronie. Wyjaśnienia są w [przewodnikach](../../README.p
## Polecenie `finch`
-| Polecenie | Robi |
+| Polecenie | Co robi |
|---|---|
-| `finch run plik.fch [argumenty…]` | skompiluj i uruchom (argumenty trafiają do programu) |
-| `finch build plik.fch [-o nazwa]` | zrób program (`nazwa.exe` na Windowsie) |
-| `finch build plik.fch --shared` | zrób bibliotekę (`libplik.so`, `plik.dll`) dla programu w C; jej `export fn` to jej funkcje C |
-| `finch test plik.fch` | uruchom jego bloki `test "…" { }`, każdy osobno (także z importowanych modułów) |
-| `finch fmt [--check] [pliki/foldery]` | wcięcia wg `{ }` po 4 spacje, bez spacji na końcach linii, najwyżej jedna pusta linia (`--check`: tylko wypisz, co by zmienił) |
-| `finch run` / `build` / `test` bez pliku | w folderze z `finch.toml`: jego `entry` |
-| `finch ir plik.fch` | pokaż LLVM IR |
+| `finch run plik.fch [argumenty…]` | kompiluje i uruchamia (argumenty trafiają do programu) |
+| `finch build plik.fch [-o nazwa]` | tworzy program (na Windowsie `nazwa.exe`) |
+| `finch build plik.fch --shared [-I katalog]` | tworzy bibliotekę (`libplik.so`, `plik.dll`), którą wczyta program w C; jej funkcjami C są funkcje `export fn` (liczby, bool, char, wskaźniki) |
+| `finch test plik.fch` | uruchamia każdy blok `test "…" { }` osobno (także w importowanych modułach) |
+| `finch fmt [--check] [pliki/katalogi]` | wcięcia po 4 spacje na każdy poziom `{ }`, bez spacji na końcach linii, najwyżej jedna pusta linia z rzędu (`--check`: tylko wypisuje, co by zmienił) |
+| `finch run` / `build` / `test` bez pliku | w katalogu z `finch.toml`: plik z pola `entry` |
+| `finch ir plik.fch` | wypisuje LLVM IR |
| `finch lsp` | serwer języka (korzystają z niego edytory) |
| `finch version` | wersja |
-| `-l lib` | dolinkuj bibliotekę C (jak `link "lib"`) |
-| `--target windows` / `linux` / `arm64` / `macos` / `android` / triple | zbuduj dla innego systemu (`android`: clang z NDK wskazanego przez `ANDROID_NDK_HOME`, zwykle z `--shared`) |
-| `-g` | informacje dla debuggera (gdb, lldb, Visual Studio) |
+| `-l bib` | dolinkowuje bibliotekę C (to samo co `link "bib"`) |
+| `--target windows` / `linux` / `arm64` / `macos` / `android` / triple | buduje na inny system (`android`: clang z NDK wskazanego przez `ANDROID_NDK_HOME`, zwykle z `--shared`) |
+| `-g` | informacje dla debuggera (gdb, lldb) |
| `-O0` | bez optymalizacji |
-| `--trace` / `--no-trace` | błędy w czasie działania pokazują łańcuch wywołań; włączone dla `run`, `test`, `-g`, `-O0`, wyłączone dla zwykłego `build` (kosztuje szybkość) |
+| `--cpu native` / `x86-64-v3` / `apple-m1` … | używa wszystkich instrukcji danego procesora (AVX2, AVX-512, NEON); program będzie wtedy wymagał podobnego procesora |
+| `--trace` / `--no-trace` | błąd w trakcie działania pokazuje ścieżkę wywołań; domyślnie włączone dla `run`, `test`, `-g` i `-O0`, wyłączone dla zwykłego `build` (spowalnia program) |
-Zmienne środowiskowe: `FINCH_PATH` (foldery z modułami), `FINCH_CC` (kompilator C, który linkuje), `FINCH_CXX` (dla plików `.cpp`).
-Skompilowany kod programu i jego pliki `link "x.c"` zostają w cache (`~/.cache/finch`), dopóki one albo ich nagłówki się nie zmienią.
+Zmienne środowiskowe: `FINCH_PATH` (katalogi z modułami), `FINCH_CC` (kompilator C, który linkuje), `FINCH_CXX` (dla plików `.cpp`).
+Skompilowany program i jego pliki z `link "x.c"` zostają w pamięci podręcznej (`~/.cache/finch`), dopóki nie zmienią się one same albo ich nagłówki.
-`finch.toml` (obok kodu; `finch run` w tym folderze nie potrzebuje pliku ani flag):
+`finch.toml` (obok kodu; wtedy `finch run` nie potrzebuje ani nazwy pliku, ani opcji):
```toml
-name = "gra" # co robi finch build
+name = "gra" # co zbuduje finch build
entry = "src/main.fch"
-paths = ["src"] # foldery z modułami
+paths = ["src"] # katalogi z modułami
include = ["vendor/SDL3/include"] # nagłówki C
-links = ["SDL3", "native/glue.cpp"] # biblioteki i własne .c / .cpp / .o / .a
+links = ["SDL3", "native/glue.cpp"] # biblioteki i twoje pliki .c / .cpp / .o / .a
libdirs = ["vendor/SDL3/lib"]
-cflags = ["-DFAST"] # dla własnych plików .c / .cpp
+cflags = ["-DFAST"] # dla twoich plików .c / .cpp
ldflags = ["-Wl,-rpath,."]
+cpu = "native" # to samo co --cpu native
-[target.windows] # tylko przy budowaniu dla tego systemu (też linux, macos, android, linux-arm64)
+[target.windows] # tylko przy budowaniu na ten system (są też linux, macos, android, linux-arm64)
links = ["ws2_32"]
```
@@ -63,8 +65,8 @@ fn main() {
}
```
-`main` to `fn main()`, `fn main() -> int` (kod wyjścia) albo `fn main([]str args)`.
-Jedna instrukcja na linijkę, bez średników, `{` w tej samej linijce, bez nawiasów wokół warunków.
+`main` może wyglądać tak: `fn main()`, `fn main() -> int` (zwraca kod wyjścia) albo `fn main([]str args)`.
+Jedna instrukcja w linii, bez średników, `{` w tej samej linii, warunki bez nawiasów.
---
@@ -72,12 +74,12 @@ Jedna instrukcja na linijkę, bez średników, `{` w tej samej linijce, bez nawi
| Kod | Znaczenie |
|---|---|
-| `x := 5` | nowa zmienna, typ zgadnięty |
-| `int x = 5` | nowa zmienna, typ podany |
-| `int x` | zero / `""` / `[]` / `null` / wartości domyślne struktury (też struktur w niej) |
+| `x := 5` | nowa zmienna, typ wynika z wartości |
+| `int x = 5` | nowa zmienna z podanym typem |
+| `int x` | zero / `""` / `[]` / `null` / wartości domyślne struktury (także struktur w jej środku) |
| `x = 7` | przypisanie |
| `x += 1` `-=` `*=` `/=` `%=` | zmiana |
-| `const W = 640`, `const f32 G = 9.8` | stała (na najwyższym poziomie pliku; `ksztalty.W` z modułu) |
+| `const W = 640`, `const f32 G = 9.8` | stała (na najwyższym poziomie pliku; z modułu: `ksztalty.W`) |
---
@@ -87,15 +89,16 @@ Jedna instrukcja na linijkę, bez średników, `{` w tej samej linijce, bez nawi
|---|---|
| `int` (= `i64`) | `42`, `-7`, `0xFF`, `1_000_000` |
| `float` (= `f64`) | `3.14`, `1e9`, `2.5e-3` |
-| `bool` | `true` (prawda), `false` (fałsz) |
+| `bool` | `true`, `false` |
| `char` | `'A'`, `'\n'` |
-| `str` | `"tekst\t\"w cudzysłowie\""`, `"hp \(hp), prędkość \(v, 2)"` (kod w tekście; `, 2` = dwie cyfry po przecinku), `"""` kilka linii `"""` (wcięcie zamykających cudzysłowów znika) |
-| `i8 i16 i32 i64` | ze znakiem, z rozmiarem |
-| `u8 u16 u32 u64` | bez znaku, z rozmiarem |
-| `f32 f64` | zmiennoprzecinkowe |
+| `str` | `"tekst\t\"w cudzysłowie\""`, `"hp \(hp), prędkość \(v, 2)"` (kod w tekście; `, 2` = dwie cyfry po przecinku), `"""` kilka linii `"""` (wcięcie zamykającego cudzysłowu znika) |
+| `i8 i16 i32 i64` | liczby całkowite ze znakiem, o podanym rozmiarze |
+| `u8 u16 u32 u64` | liczby całkowite bez znaku |
+| `f32 f64` | liczby zmiennoprzecinkowe |
| `[]T` | tablica: `[1, 2, 3]`, `[]str imiona` |
| `map[K]V` | mapa: `["a": 1]`, `map[str]int ile`, `[:]` |
| `ptr[T]`, `ptr` | wskaźnik, wskaźnik bez typu |
+| `f32x8`, `i8x32`, `f64x4`, `u16x16` … | wektor: N liczb liczonych naraz (SIMD), zob. niżej |
| `Nazwa` / `modul.Nazwa` | struktura |
Znaki specjalne: `\n \t \r \0 \\ \" \'`.
@@ -107,11 +110,11 @@ Znaki specjalne: `\n \t \r \0 \\ \" \'`.
| Priorytet | Operatory |
|---|---|
| 5 | `*` `/` `%` `<<` `>>` `&` |
-| 4 | `+` `-` `\|` `^` (`+` skleja też tekst) |
+| 4 | `+` `-` `\|` `^` (`+` łączy też teksty) |
| 3 | `==` `!=` `<` `<=` `>` `>=` |
-| 2 | `&&` (i) |
-| 1 | `\|\|` (lub) |
-| jednoargumentowe | `-x` `!x` (nie) `~x` |
+| 2 | `&&` |
+| 1 | `\|\|` |
+| jednoargumentowe | `-x` `!x` `~x` |
`x & 1 == 0` znaczy `(x & 1) == 0`.
@@ -121,14 +124,14 @@ Znaki specjalne: `\n \t \r \0 \\ \" \'`.
```c
if a > b { … } else if a == b { … } else { … }
-x := if a > b { a } else { b } // jako wartość: jedna wartość na gałąź, else wymagane
+x := if a > b { a } else { b } // jako wartość: po jednej wartości w każdej gałęzi, else obowiązkowe
while x > 0 { … }
for i in 0..10 { … } // 0 … 9
-for el in lista { … } // każdy element (albo znak str)
+for el in lista { … } // każdy element (albo znak tekstu)
for i, el in lista { … } // razem z indeksem
-for klucz, wart in m { … } // każdy wpis mapy, w kolejności dodawania
+for klucz, wartosc in m { … } // każdy wpis mapy, w kolejności dodania
break continue return wartosc
-defer sprzatanie() // uruchomi się na końcu bloku
+defer sprzatnij() // wykona się, gdy blok się skończy
```
---
@@ -144,48 +147,48 @@ fn przywitaj(str imie) { // bez wyniku
}
```
-Parametry zachowują się jak kopie. Kolejność w pliku nie ma znaczenia. Przecinek po ostatnim argumencie jest dozwolony.
+Parametry zachowują się jak kopie. Kolejność funkcji w pliku nie ma znaczenia. Po ostatnim argumencie może stać przecinek.
---
## Metody
```c
-fn Punkt.przesun(int dx, int dy) { // self to Punkt, na którym wywołano (nie kopia)
+fn Punkt.przesun(int dx, int dy) { // self to punkt, na którym ją wywołano (nie kopia)
self.x += dx
self.y += dy
}
fn Punkt.odleglosc() -> float { … }
-p.przesun(1, 2) lista[0].przesun(1, 1) wsk_na_punkt.przesun(0, 1)
+p.przesun(1, 2) lista[0].przesun(1, 1) wskaznik_na_punkt.przesun(0, 1)
```
-Także na strukturach z C: `fn Vector2.length() -> f32 { … }`.
+Działają też na strukturach z C: `fn Vector2.length() -> f32 { … }`.
---
## Błędy jako wartości
```c
-fn parsuj(str s) -> int! { // ! = może się nie udać
+fn parsuj(str s) -> int! { // ! = może się nie udać
if s.len == 0 {
- return error("pusty")
+ return error("pusty tekst")
}
return int(s) or { return error("to nie liczba: " + s) }
}
-fn zapisz(str p, str t) -> ! { // może się nie udać, bez wyniku
+fn zapisz(str p, str t) -> ! { // może się nie udać, bez wyniku
try write_file(p, t)
}
```
-| Kod | Przy porażce |
+| Kod | Gdy się nie uda |
|---|---|
-| `x := parsuj(s) or 0` | użyj 0 |
-| `x := parsuj(s) or { print(err); return }` | wykonaj blok (`err`: komunikat) |
-| `x := try parsuj(s)` | zakończ porażką też bieżącą funkcję |
-| `fn main() -> !` | porażka wypisuje `error: …`, kod wyjścia 1 |
+| `x := parsuj(s) or 0` | weź 0 |
+| `x := parsuj(s) or { print(err); return }` | wykonaj blok (`err` to komunikat) |
+| `x := try parsuj(s)` | przerwij też bieżącą funkcję z tym samym błędem |
+| `fn main() -> !` | błąd wypisuje `error: …`, kod wyjścia 1 |
-Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_file`.
+Wbudowane funkcje, które mogą się nie udać: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_file`.
---
@@ -193,9 +196,9 @@ Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file
| Kod | Znaczenie |
|---|---|
-| `struct P { … }` | definicja (jedno pole na linijkę, `= domyślna` opcjonalnie) |
+| `struct P { … }` | definicja (jedno pole w linii, `= wartość` opcjonalnie) |
| `P(1, 2)` | utworzenie, wszystkie pola po kolei |
-| `P(x: 1)` | utworzenie po nazwie, reszta domyślna/zero |
+| `P(x: 1)` | utworzenie przez nazwy pól, reszta domyślna albo zero |
| `p.x`, `p.x = 5` | odczyt, zmiana pola |
| `b := a` | pełna kopia |
@@ -203,62 +206,83 @@ Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file
## Tablice `[]T`
-| Kod | Daje |
+| Kod | Wynik |
|---|---|
| `a.len` | liczba elementów |
-| `a[i]`, `a[i] = x` | element (z kontrolą zakresu) |
-| `a.push(x)` | dodaj na końcu |
-| `a.pop()` | zabierz ostatni i go oddaj |
-| `a.insert(i, x)` | wstaw na pozycję i |
-| `a.remove(i)` | usuń pozycję i i ją oddaj |
-| `a.clear()` | wyczyść |
-| `a.resize(n)` | długość n (nowe elementy to zera) |
+| `a[i]`, `a[i] = x` | element (zakres jest sprawdzany) |
+| `a.push(x)` | dodaje na końcu |
+| `a.pop()` | zdejmuje ostatni i go zwraca |
+| `a.insert(i, x)` | wstawia na pozycję i |
+| `a.remove(i)` | usuwa pozycję i i ją zwraca |
+| `a.clear()` | opróżnia |
+| `a.resize(n)` | długość n (nowe elementy są zerami) |
| `a.find(x)` | indeks albo -1 |
| `a.contains(x)` | bool |
-| `a.sort()` | posortuj (liczby, znaki, str) |
-| `a.reverse()` | odwróć |
-| `a.slice(s, e)` | nowa tablica z s … e-1 |
-| `a.join(sep)` | `[]str` → jeden str |
+| `a.sort()` | sortuje (liczby, znaki, teksty) |
+| `a.reverse()` | odwraca kolejność |
+| `a.slice(p, k)` | nowa tablica z elementów p … k-1 |
+| `a.join(sep)` | `[]str` → jeden tekst |
| `a.ptr` | adres pierwszego elementu (dla C) |
---
## Mapy `map[K]V`
-| Kod | Daje |
+| Kod | Wynik |
|---|---|
| `m := ["a": 1, "b": 2]` | mapa (klucze: liczby, char, bool, str) |
-| `m[k]` | wartość (brak klucza: program się zatrzymuje) |
-| `m[k] = v` | dodaj / zastąp |
+| `m[k]` | wartość (brak klucza zatrzymuje program) |
+| `m[k] = v` | dodaje albo podmienia |
| `m[k] += 1`, `m[k].push(x)` | brakujący klucz zaczyna od wartości domyślnej (0, `[]`, …) |
| `m.len` | liczba kluczy |
-| `m.has(k)` | czy jest |
-| `m.get(k, domyslna)` | wartość albo domyślna |
-| `m.remove(k)` | bool: czy był |
-| `m.clear()` | wyczyść |
-| `m.keys()`, `m.values()` | tablice, w kolejności dodawania |
+| `m.has(k)` | bool |
+| `m.get(k, domyslna)` | wartość albo `domyslna` |
+| `m.remove(k)` | bool: czy klucz był |
+| `m.clear()` | opróżnia |
+| `m.keys()`, `m.values()` | tablice, w kolejności dodania |
+
+---
+
+## Wektory `f32x8` (SIMD)
+
+Typy: `f32x4/8/16`, `f64x2/4/8`, `i8x16/32/64`, `i16x8/16/32`, `i32x4/8/16`, `i64x2/4/8` i tak samo `u…`; najwyżej 512 bitów.
+
+| Kod | Wynik |
+|---|---|
+| `f32x8(x)` | `x` w każdym elemencie |
+| `f32x8(a, b, …, h)` | po jednej liczbie na element |
+| `f32x8(lista, i)` | `lista[i]` … `lista[i+7]` (z `[]f32` albo `ptr[f32]`) |
+| `f32x8(v)` | 8-elementowy wektor przeliczony element po elemencie (`f32x8(calkowite)`, `i32x8(ulamki)`) |
+| `v + w`, `v * 2.0`, `-v` | element po elemencie; zwykła liczba działa na każdy element (`/` tylko dla liczb zmiennoprzecinkowych) |
+| `& \| ^ << >> ~` | element po elemencie, dla liczb całkowitych |
+| `v == w`, `v != w` | bool: czy wszystkie elementy są równe / czy któryś się różni |
+| `v[i]`, `v[i] = x`, `v.len` | jeden element; liczba elementów |
+| `min(v, w)`, `max`, `abs(v)`, `clamp(v, lo, hi)` | element po elemencie |
+| `v.sum()`, `v.min()`, `v.max()`, `v.dot(w)` | jedna liczba ze wszystkich elementów |
+| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()`, `v.fma(b, c)` | każdy element (liczby zmiennoprzecinkowe; `fma` = `v * b + c`) |
+| `v.store(lista, i)` | zapisuje elementy do `lista[i]` … |
---
## Tekst `str`
-| Kod | Daje |
+| Kod | Wynik |
|---|---|
-| `s.len` | liczba bajtów (ą, ż… to 2) |
+| `s.len` | liczba bajtów |
| `s[i]`, `s[i] = 'x'` | znak |
-| `a + b` | sklejony tekst |
-| `"x = \(x)"` | wartość dowolnego kodu w tekście (`\(x, 2)`: liczba z 2 cyframi po przecinku) |
+| `a + b` | połączony tekst |
+| `"x = \(x)"` | wartość dowolnego kodu wstawiona w tekst (`\(x, 2)`: liczba z dwiema cyframi po przecinku) |
| `s.sub(a, b)` | fragment a … b-1 |
| `s.find(t)` | pozycja albo -1 |
-| `s.contains(t)` | czy zawiera |
-| `s.starts_with(t)`, `s.ends_with(t)` | czy zaczyna / kończy się |
+| `s.contains(t)` | bool |
+| `s.starts_with(t)`, `s.ends_with(t)` | bool |
| `s.split(sep)` | `[]str` |
| `s.trim()` | bez spacji na końcach |
| `s.upper()`, `s.lower()` | A–Z / a–z |
-| `s.replace(a, b)` | każde a → b |
-| `s.repeat(n)` | n razy |
+| `s.replace(a, b)` | każde a zamienione na b |
+| `s.repeat(n)` | tekst n razy |
| `s.bytes()` | `[]u8` |
-| `s.ptr` | `ptr[char]` (dla C; `str` trafia też do parametru `const char *` bez zmian) |
+| `s.ptr` | `ptr[char]` (dla C; parametr `const char *` przyjmie też samo `str`) |
| `==` `!=` `<` `>` | porównanie |
---
@@ -271,41 +295,41 @@ Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file
| `int("42")` | `42` (zatrzymuje program, jeśli to nie liczba) |
| `int('A')` | `65` |
| `float(7)`, `float("2.5")` | `7`, `2.5` |
-| `u8(300)` | `44` (zostawia najniższe bity) |
+| `u8(300)` | `44` (zostają najniższe bity) |
| `char(66)` | `'B'` |
| `bool(0)` | `false` |
-| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | tekst (float: najkrótszy tekst, który jest tą samą liczbą) |
-| `str(3.14159, 2)` | `"3.14"` (tyle cyfr po kropce) |
+| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | tekst (liczba zmiennoprzecinkowa: najkrótszy zapis tej samej liczby) |
+| `str(3.14159, 2)` | `"3.14"` (tyle cyfr po przecinku) |
| `str([1, 2])`, `str(m)`, `str(punkt)` | ten sam tekst, który pokazuje `print` |
| `int("x") or 0`, `float(s) or 0.0` | liczba albo wartość zapasowa |
| `str(bajty)` | `[]u8` → tekst |
| `str(p)` | `char*` z C → tekst |
-| `ptr(p)`, `ptr(16)` | wskaźnik bez typu, ze wskaźnika albo liczby |
+| `ptr(p)`, `ptr(16)` | wskaźnik bez typu, ze wskaźnika albo z liczby |
| `int(p)` | wskaźnik → adres |
-Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowolna całkowita → float, `f32` → `f64`.
+Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza liczba → większa, liczba całkowita → zmiennoprzecinkowa, `f32` → `f64`.
---
## Funkcje wbudowane
-| Funkcja | Robi |
+| Funkcja | Co robi |
|---|---|
-| `print(a, b, …)` | wypisz, spacje między, nowa linia na końcu |
-| `input()`, `input("Pytanie? ")` | wczytaj linijkę |
+| `print(a, b, …)` | wypisuje wartości oddzielone spacjami i kończy linię |
+| `input()`, `input("Pytanie? ")` | wczytuje linię |
| `read_file(sciezka)` | plik → str |
| `write_file(sciezka, tekst)` | str → plik, `true`, jeśli się udało |
-| `file_exists(sciezka)` | czy plik istnieje |
-| `delete_file(sciezka)` | usuń plik |
-| `shell(polecenie)` | uruchom polecenie, jego kod wyjścia |
-| `exit(kod)` | zakończ program od razu (kończy też blok `or { }`) |
-| `min(a, b, …)`, `max(a, b, …)`, `abs(x)`, `clamp(x, od, do)` | dla dowolnych liczb (twoja funkcja o tej nazwie wygrywa) |
+| `file_exists(sciezka)` | bool |
+| `delete_file(sciezka)` | bool |
+| `shell(polecenie)` | uruchamia polecenie, zwraca jego kod wyjścia |
+| `exit(kod)` | od razu kończy program (kończy też blok `or { }`) |
+| `min(a, b, …)`, `max(a, b, …)`, `abs(x)`, `clamp(x, lo, hi)` | dla dowolnych liczb (twoja funkcja o tej samej nazwie ma pierwszeństwo) |
| `error(komunikat)` | `return error("…")` w funkcji `-> T!` |
-| `addr(x)` | wskaźnik na x; `addr(fn)` = callback dla C |
-| `assert(warunek)`, `assert(warunek, "dlaczego")` | zatrzymaj się z błędem, gdy warunek jest fałszywy (w bloku `test`: ten test nie przechodzi) |
-| `atomic_add(p, n)`, `atomic_sub`, `atomic_load(p)`, `atomic_store(p, n)`, `atomic_swap(p, n)`, `atomic_cas(p, stara, nowa)` | liczba całkowita zmieniana też przez inne wątki, przez jej `ptr` (`addr(licznik)`) |
+| `addr(x)` | wskaźnik na x; `addr(fn)` = funkcja zwrotna dla C |
+| `assert(warunek)`, `assert(warunek, "dlaczego")` | zatrzymuje program, gdy warunek jest fałszywy (w bloku `test`: ten test nie przechodzi) |
+| `atomic_add(p, n)`, `atomic_sub`, `atomic_load(p)`, `atomic_store(p, n)`, `atomic_swap(p, n)`, `atomic_cas(p, stara, nowa)` | liczba całkowita, którą zmieniają też inne wątki, przez jej `ptr` (`addr(licznik)`) |
| `new(wartosc)` | wartość na stercie → `ptr[T]` |
-| `free(p)` | oddaj ją |
+| `free(p)` | oddaje tę pamięć |
---
@@ -314,7 +338,7 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowo
| Finch | C |
|---|---|
| `ptr[int] p` | `int *p` |
-| `addr(x)` | `&x` (zmienna-struktura podana tam, gdzie idzie `ptr[TaStruktura]`: `pchnij(cialo)` = `pchnij(addr(cialo))`) |
+| `addr(x)` | `&x` (zmienna-struktura podana tam, gdzie oczekiwany jest `ptr[TaStruktura]`: `push(cialo)` = `push(addr(cialo))`) |
| `p.value` | `*p` |
| `p.pole` | `p->pole` |
| `p[i]` | `p[i]` |
@@ -325,23 +349,23 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowo
## Pamięć
-- Tablice, teksty i struktury zwalniają się same na końcu swojego bloku. Bez `free`.
-- `b := a` kopiuje; wynik wywołania jest przenoszony; `return x` przenosi.
-- Przekazywanie struktury: `fn f(Cialo c)` pracuje na własnej kopii (ta u wywołującego się nie zmienia), `fn f(ptr[Cialo] c)` zmienia tę u wywołującego (`f(cialo)` albo `f(addr(cialo))`), metoda dostaje `self` przez adres, nigdy kopię. Struktura z 256+ bajtami liczb przekazana przez wartość albo w `for x in lista` dostaje ostrzeżenie.
-- `new(...)` + `free(...)` (często z `defer`) do list wiązanych, drzew, współdzielenia.
-- Pamięć z C: funkcjami z C (`malloc` / `free`, `fclose`, …).
+- Tablice, teksty i struktury zwalniają się same na końcu swojego bloku. `free` nie jest potrzebne.
+- `b := a` kopiuje; wynik funkcji jest przenoszony; `return x` przenosi.
+- Przekazywanie struktury: `fn f(Cialo c)` pracuje na własnej kopii (struktura wywołującego się nie zmienia), `fn f(ptr[Cialo] c)` zmienia strukturę wywołującego (`f(cialo)` albo `f(addr(cialo))`), metoda dostaje `self` przez adres, nigdy jako kopię. Struktura z co najmniej 256 bajtami liczb, przekazywana przez wartość albo w `for x in lista`, dostaje ostrzeżenie.
+- `new(...)` z `free(...)` (często razem z `defer`) przydaje się do list wiązanych, drzew i współdzielenia.
+- Pamięć z C zwalniasz funkcjami C (`malloc` / `free`, `fclose`, …).
---
## Moduły
```c
-import ksztalty // ksztalty.fch obok tego pliku (albo w FINCH_PATH)
-ksztalty.pole(b) // jego funkcje
-ksztalty.Pudlo(2, 3) // jego struktury
-ksztalty.Pudlo inne // w deklaracjach
-import ui/przycisk // ui/przycisk.fch: przycisk.rysuj()
-import ui/przycisk as p // inna nazwa: p.rysuj()
+import ksztalty // ksztalty.fch obok tego pliku (albo w FINCH_PATH)
+ksztalty.pole(b) // jego funkcje
+ksztalty.Pudelko(2, 3) // jego struktury
+ksztalty.Pudelko inne // w deklaracjach
+import ui/button // ui/button.fch: button.draw()
+import ui/button as btn // pod inną nazwą: btn.draw()
```
---
@@ -350,14 +374,14 @@ import ui/przycisk as p // inna nazwa: p.rysuj()
| Kod | Znaczenie |
|---|---|
-| `import "x.h"` | funkcje, struktury, enumy, liczbowe `#define`, zmienne globalne |
-| `link "nazwa"` | libnazwa.so / nazwa.lib (najpierw pkg-config) |
-| `link "plik.c"`, `link "glue.cpp"` | skompiluj własny plik C albo C++ (C++: funkcje w `extern "C"`) |
-| `addr(mojaFn)` | funkcja Fincha jako callback |
+| `import "x.h"` | funkcje, struktury, enumy, liczby z `#define`, zmienne globalne |
+| `link "nazwa"` | libnazwa.so / nazwa.lib (najpierw pyta pkg-config) |
+| `link "plik.c"`, `link "glue.cpp"` | kompiluje twój plik C albo C++ (w C++ funkcje w `extern "C"`) |
+| `addr(mojaFn)` | przekazuje funkcję Fincha jako funkcję zwrotną |
-| Typ C | Finch |
+| Typ w C | W Finchu |
|---|---|
-| `int` / `long long` / `unsigned` | `i32` / `i64` / `u32` (wg rozmiaru) |
+| `int` / `long long` / `unsigned` | `i32` / `i64` / `u32` (według rozmiaru) |
| `float` / `double` | `f32` / `f64` |
| `char *` (parametr, wynik) | `str` |
| `char *` (pole, za wskaźnikiem) | `ptr[char]` |
@@ -365,46 +389,46 @@ import ui/przycisk as p // inna nazwa: p.rysuj()
| `T *` | `ptr[T]` |
| `void *`, uchwyty | `ptr` |
| `T x[N]` (w strukturze) | `[N]T` |
-| `union U` | `U`: `U u`, `u.pole`, `U(pole: v)`, przez wskaźnik do C (nie przez wartość) |
-| `#define N 4`, `"tekst"`, `(1u << 3)`, `((Uint64)2)` | stałe (bez makr-funkcji) |
-| `#define RAYWHITE (Color){ 245, 245, 245, 255 }` | `Color` |
+| `union U` | `U`: `U u`, `u.pole`, `U(pole: v)`, do C przez wskaźnik (nie przez wartość) |
+| `#define N 4`, `"tekst"`, `(1u << 3)`, `((Uint64)2)` | stałe (bez makr z parametrami) |
+| `#define RAYWHITE (Color){ 245, 245, 245, 255 }` | wartość typu `Color` |
-**`char *` to `str`**: funkcja C, która bierze albo zwraca `char *`, bierze albo daje `str` Fincha (bez `.ptr`, bez zamiany); tylko pole `char *` i `char **` zostają `ptr[char]`.
-C++ i Rust: przez nagłówek C. C++ z `extern "C"` (i `link "x.cpp"`), Rust z `#[no_mangle] pub extern "C" fn` zbudowany jako `staticlib` (`link "target/release/libgra.a"`) i nagłówek (napisze go cbindgen).
+**`char *` to `str`**: funkcja C, która przyjmuje albo zwraca `char *`, przyjmuje albo zwraca `str` Fincha (bez `.ptr` i bez zamiany); tylko pole `char *` i `char **` zostają jako `ptr[char]`.
+C++ i Rust: przez nagłówek C. C++ z `extern "C"` (i `link "x.cpp"`), Rust z `#[no_mangle] pub extern "C" fn`, zbudowany jako `staticlib` (`link "target/release/libgra.a"`), z nagłówkiem (może go napisać cbindgen).
-Wynik `char *` z funkcji C to od razu `str`; struktury przez wartość działają w konwencjach System V, Windows x64 i ARM64 Linux (sprawdzone w emulatorach; macOS przez CI).
+Wynik `char *` funkcji C od razu jest `str`; struktury przez wartość działają w konwencjach wywołań System V, Windows x64 i ARM64 (Linux i macOS).
---
-## Błędy w czasie działania (program zatrzymuje się z plikiem i linijką)
+## Błędy w trakcie działania (program kończy się z plikiem i linią)
| Komunikat | Przyczyna |
|---|---|
| `division by zero` | `/` albo `%` przez 0 |
| `index 5 is out of range (the length is 3)` | `a[5]` |
-| `pop() on an empty array` | `pop()` na pustej tablicy |
+| `pop() on an empty array` | |
| `used .value on a null pointer` | `.value` / `.pole` / `[i]` przez `null` |
| `can't turn "x" into int` | `int("x")` |
| `can't read the file "…"` | `read_file` (bez `or`) |
| `the key "x" is not in the map` | `m["x"]` |
| `assert failed: dlaczego` | `assert(warunek, "dlaczego")` |
-Pod komunikatem wywołania, które do niego doprowadziły (`finch run`, `finch test`, `-g`, `-O0`, `--trace`):
+Pod komunikatem pojawiają się wywołania, które do niego doprowadziły (`finch run`, `finch test`, `-g`, `-O0`, `--trace`):
```
gra.fch:6: runtime error: index 3 is out of range (the length is 2)
- in Enemy.next (gra.fch:6)
- in step (gra.fch:10)
+ in Wrog.nastepny (gra.fch:6)
+ in krok (gra.fch:10)
in main (gra.fch:15)
```
## Testy
```c
-test "wysokość skoku" { // obok kodu; finch test plik.fch je uruchamia, finch run/build je pomijają
- g := Gracz()
- g.skocz()
- assert(g.y > 0.0, "y to \(g.y)")
+test "wysokość skoku" { // obok kodu; uruchamia je finch test plik.fch, finch run/build je pomijają
+ p := Gracz()
+ p.skocz()
+ assert(p.y > 0.0, "y wynosi \(p.y)")
}
```
@@ -413,74 +437,55 @@ test "wysokość skoku" { // obok kodu; finch test plik.fch je uruchamia
## Edytory
Kate: `editors/kate/install.sh`, potem włącz wtyczkę **LSP Client**; uruchamianie przez **Build & Run** → `finch run %f`.
-Każdy edytor z LSP: `finch lsp`.
+Dowolny edytor z LSP: `finch lsp`.
### VS Code (rozszerzenie „Finch”)
-| Klawisz / akcja | Robi |
+| Klawisz / akcja | Co robi |
|---|---|
-| ▶ / `Ctrl+F5` | uruchom plik |
-| `Finch: Build This File` | zbuduj; błędy trafiają do panelu Problems |
-| `F12` | idź do definicji |
-| najechanie myszą | typ / sygnatura / opis |
+| ▶ / `Ctrl+F5` | uruchamia plik |
+| `Finch: Build This File` | buduje; błędy trafiają do panelu Problems |
+| `F12` | przejście do definicji |
+| najechanie kursorem | typ / sygnatura / opis |
| `Ctrl+Spacja` | podpowiedzi (po `.`: pola, metody, zawartość modułu) |
| `Ctrl+Shift+O` | funkcje i struktury w pliku |
-| `main`, `fn`, `fnr`, `fnf`, `method`, `struct`, `for`, `foreach`, `forkv`, `map`, `orb`, `if`, `ife`, `while`, `input` + Tab | szablony |
-| ustawienie `finch.path` | gdzie jest finch / finch.exe |
+| `main`, `fn`, `fnr`, `fnf`, `method`, `struct`, `for`, `foreach`, `forkv`, `map`, `orb`, `if`, `ife`, `while`, `input` + Tab | gotowe fragmenty kodu |
+| ustawienie `finch.path` | gdzie leży finch / finch.exe |
---
## Enumy i `match`
```
-enum Kolor { Czerwony, Zielony } enum Status { Ok = 200, Brak = 404 }
-k := Kolor.Zielony int(k) Kolor(1) k == Kolor.Czerwony // tylko == i !=; Kolor(7) to błąd
+enum Kolor { Czerwony, Zielony, Niebieski } enum Status { Ok = 200, NotFound = 404 }
+k := Kolor.Zielony int(k) Kolor(2) k == Kolor.Czerwony // tylko == i !=; Kolor(7) to błąd
fn Kolor.nastepny() -> Kolor { ... } // metody działają
-match k { Czerwony -> ... Zielony -> { ... } else -> ... } // same nazwy wariantów; każdy wariant albo else
-x := match n { 1, 2 -> "mało" else -> "dużo" } // wyrażenie: gałęzie tego samego typu
+match k { Czerwony -> ... Zielony, Niebieski -> { ... } else -> ... } // same nazwy wariantów; każdy wariant albo else
+x := match n { 1, 2 -> "kilka" else -> "dużo" } // wyrażenie: każda gałąź tego samego typu
```
## Typy generyczne
```
-fn max[T](T a, T b) -> T { ... } max(3, 9) max[float](1, 2.5) // wynika z argumentów albo zapisane
-struct Stos[T] { []T elementy } fn Stos[T].dodaj(T x) { ... } // typ zawsze podaje się przy tworzeniu:
+fn max[T](T a, T b) -> T { ... } max(3, 9) max[float](1, 2.5) // typ wywnioskowany albo podany
+struct Stos[T] { []T el } fn Stos[T].push(T x) { ... } // typ zawsze idzie z nazwą:
s := Stos[int]() Stos[int] t Para[str, int]("wiek", 31) // nigdy samo Stos()
```
-## Biblioteka standardowa (`import nazwa`; własny plik o tej nazwie ma pierwszeństwo)
+## Biblioteka standardowa (`import nazwa`; twój plik o tej samej nazwie ma pierwszeństwo)
| | |
|---|---|
| `path` | `join(a, b)` `base(p)` `dir(p)` `ext(p)` `stem(p)` `parts(p)` `clean(p)` `is_abs(p)` |
-| `time` | `now()` `millis()` `sleep(ms)` `date(t)` `local(t)` `format(t, fmt)` `days_in_month(r, m)` |
-| `proc` | `run(cmd)` `output(cmd)` (błąd z jego wyjściem) `env(nazwa, domyślna)` `pid()` `cwd()` |
+| `time` | `now()` `millis()` `sleep(ms)` `date(t)` `local(t)` `format(t, wzor)` `days_in_month(r, m)` |
+| `proc` | `run(polecenie)` `output(polecenie)` (przy błędzie zwraca jego wyjście) `env(nazwa, domyslna)` `pid()` `cwd()` |
| `thread` | `spawn(addr(praca), addr(dane))` → `t.join()`; `mutex()` → `m.lock()` `m.unlock()` `m.close()`; `cpus()` (funkcja wątku: `fn praca(ptr arg)`) |
-| `fs` | `read_bytes(p)` `write_bytes(p, dane)` `exists(p)` `is_dir(p)` `mkdir(p)` `list(folder)` `remove_dir(p)` `data_dir(app)` |
+| `fs` | `read_bytes(p)` `write_bytes(p, dane)` `exists(p)` `is_dir(p)` `mkdir(p)` `list(katalog)` `remove_dir(p)` `data_dir(aplikacja)` |
| `json` | `parse(tekst)` `stringify(v)` `pretty(v)` `new_text/number/bool/null/list/object` `v.get(k)` `v.at(i)` `v.set(k, x)` `v.push(x)` `v.has(k)` `v.str_or(d)` `v.int_or(d)` `v.float_or(d)` `v.bool_or(d)` |
## Słowa kluczowe
-| Słowo | Po polsku |
-|---|---|
-| `fn` | funkcja |
-| `return` | zwróć |
-| `if` / `else` | jeżeli / w przeciwnym razie |
-| `while` | dopóki |
-| `for` … `in` | dla … w |
-| `break` / `continue` | przerwij / kontynuuj |
-| `true` / `false` / `null` | prawda / fałsz / nic |
-| `import` / `link` | zaimportuj / dolinkuj |
-| `struct` | struktura |
-| `enum` | wyliczenie |
-| `match` | dopasuj |
-| `defer` | odłóż (na koniec bloku) |
-| `or` | albo (co zrobić przy porażce) |
-| `try` | spróbuj (porażkę przekaż dalej) |
-| `const` | stała |
-| `export` | wystaw (funkcja dla programu w C) |
-| `self` | sam (w metodzie: struktura, na której ją wywołano) |
-| `test` | test (tylko jako `test "nazwa" { }`) |
-| `as` | jako (tylko w `import x as y`) |
-| `map` | mapa |
+`fn return if else while for in break continue true false null import link struct enum match defer or try const export`
+(`test` i `as` tylko tam, gdzie coś znaczą: `test "nazwa" { }`, `import x as y`)
+(oraz `self` wewnątrz metod)
From 06bfe940fa856a43daef1f550693ab52742a643c Mon Sep 17 00:00:00 2001
From: Claude
Date: Sun, 1 Nov 2026 14:09:58 +0000
Subject: [PATCH 4/7] docs: the Polish technicians' and beginners' guides
rewritten in natural Polish; vectors in every guide
- dla-technikow.md and dla-poczatkujacych.md written anew from the English, in plain Polish
(no calques, the same words for the same things in every guide, addressing every reader alike)
- the technicians' guides (en, pl): "Variants that hold values" moved to the enums section;
the examples list has wordcount and simd
- src/embedded.fch packs the runtime with its updated comment
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01Psc42moPLwP43Cmb9oZqSR
---
docs/en/for-technicians.md | 50 +-
docs/pl/dla-inzynierow.md | 2 +-
docs/pl/dla-poczatkujacych.md | 410 +++++++-------
docs/pl/dla-technikow.md | 971 +++++++++++++++++++---------------
src/embedded.fch | 2 +-
5 files changed, 758 insertions(+), 677 deletions(-)
diff --git a/docs/en/for-technicians.md b/docs/en/for-technicians.md
index 712dea1..34aa27c 100644
--- a/docs/en/for-technicians.md
+++ b/docs/en/for-technicians.md
@@ -1011,7 +1011,7 @@ Finch/
├── std/ the standard library, in Finch: json, path, time, proc, fs, thread
├── runtime/ finch_rt.c: the small runtime (text, arrays, maps, input, files)
├── bootstrap/ build.sh, install.sh and the seeds (the compiler's own IR, one per system)
-├── examples/ hello, tour, types, structs, todo, guess, c_import, window, opengl, vulkan, raylib, llvm
+├── examples/ hello, tour, types, structs, todo, guess, wordcount, c_import, simd, window, opengl, vulkan, raylib, llvm
├── tests/
│ ├── run/ new/ programs + the exact output they must print (.out), input (.in)
│ ├── fail/ new/fail/ programs that must fail, with the expected error in line 1
@@ -1025,35 +1025,12 @@ Finch/
```
```sh
-tests/run.sh # programs, refusals, -g builds, a shared library, the language server
+tests/run.sh # programs, expected errors, -g builds, a shared library, the language server
MEMCHECK=1 tests/run.sh # the same under valgrind: no leaks, no bad memory access
tests/cross.sh windows # every test built for Windows and run with Wine
tests/cross.sh arm64 # every test built for ARM64 Linux and run with QEMU
```
-### Variants that hold values
-
-A variant can carry values. Make one with `Shape.Circle(2.0)`, take them out with `match`, which names them:
-
-```c
-enum Shape {
- Circle(float)
- Rect(float, float)
- Point
-}
-
-fn Shape.area() -> float {
- return match self {
- Circle(r) -> 3.14 * r * r
- Rect(w, h) -> w * h
- Point -> 0.0
- }
-}
-```
-
-`_` ignores a value. `match` must handle every variant (or end with `else`). Printing shows `Rect(2, 3.5)`. Such an enum is
-copied and freed like a struct; it can't be compared with `==` (match on it). Variants of these enums can't have numbers.
-
---
## 23. Enums and match
@@ -1100,6 +1077,29 @@ match word {
handle **every variant** or end with `else` (the compiler says which are missing); any other match needs `else`.
In an expression, every arm gives the same type.
+### Variants that hold values
+
+A variant can carry values. Make one with `Shape.Circle(2.0)`, take them out with `match`, which names them:
+
+```c
+enum Shape {
+ Circle(float)
+ Rect(float, float)
+ Point
+}
+
+fn Shape.area() -> float {
+ return match self {
+ Circle(r) -> 3.14 * r * r
+ Rect(w, h) -> w * h
+ Point -> 0.0
+ }
+}
+```
+
+`_` ignores a value. `match` must handle every variant (or end with `else`). Printing shows `Rect(2, 3.5)`. Such an enum is
+copied and freed like a struct; it can't be compared with `==` (match on it). Variants of these enums can't have numbers.
+
## 24. Generics
Write a function or a struct once for any type, with the type as a name in square brackets:
diff --git a/docs/pl/dla-inzynierow.md b/docs/pl/dla-inzynierow.md
index e8ef597..425c6d0 100644
--- a/docs/pl/dla-inzynierow.md
+++ b/docs/pl/dla-inzynierow.md
@@ -5,7 +5,7 @@ drzewo AST i LLVM IR *w postaci tekstu*, który `clang` kompiluje i linkuje z ma
czytane przez libclang z poziomu samego Fincha i jak ręcznie odtworzono konwencje wywołań C; reguły typów i własności
wartości; jak kompilator buduje sam siebie z pliku startowego; i jak działa serwer języka.
-Zakładam, że umiesz czytać kod w Finchu (zob. [przewodnik dla techników](dla-technikow.md)) i że widziałeś już LLVM IR.
+Zakładam, że umiesz czytać kod w Finchu (zob. [przewodnik dla techników](dla-technikow.md)) i że LLVM IR nie jest ci obcy.
> 🇬🇧 English version: [for-engineers.md](../en/for-engineers.md)
diff --git a/docs/pl/dla-poczatkujacych.md b/docs/pl/dla-poczatkujacych.md
index 5b48cf2..4f79846 100644
--- a/docs/pl/dla-poczatkujacych.md
+++ b/docs/pl/dla-poczatkujacych.md
@@ -1,8 +1,8 @@
# Finch dla początkujących
-**Ten przewodnik jest dla ciebie, jeśli jeszcze nigdy nie pisałeś programu.**
-Nie potrzebujesz żadnego doświadczenia ani matematyki ponad to, czego uczy się w podstawówce.
-Czytaj powoli, po jednym rozdziale, i przepisuj każdy przykład samodzielnie. Tak zostaje w głowie najlepiej.
+**Programowanie to dla ciebie coś zupełnie nowego? Ten przewodnik jest dla ciebie.**
+Nie musisz nic umieć: wystarczy matematyka ze szkoły podstawowej.
+Czytaj spokojnie, rozdział po rozdziale, i każdy przykład przepisuj sam: tak najlepiej zostaje w głowie.
> 🇬🇧 English version: [for-beginners.md](../en/for-beginners.md)
@@ -13,7 +13,7 @@ Czytaj powoli, po jednym rozdziale, i przepisuj każdy przykład samodzielnie. T
1. [Co to jest program?](#1-co-to-jest-program)
2. [Przygotowanie Fincha](#2-przygotowanie-fincha)
3. [Twój pierwszy program](#3-twój-pierwszy-program)
-4. [Wypisywanie na ekran](#4-wypisywanie-na-ekran)
+4. [Wypisywanie na ekranie](#4-wypisywanie-na-ekranie)
5. [Pudełka z nazwami: zmienne](#5-pudełka-z-nazwami-zmienne)
6. [Rodzaje wartości](#6-rodzaje-wartości)
7. [Liczenie](#7-liczenie)
@@ -28,36 +28,35 @@ Czytaj powoli, po jednym rozdziale, i przepisuj każdy przykład samodzielnie. T
16. [Kiedy coś pójdzie nie tak](#16-kiedy-coś-pójdzie-nie-tak)
17. [Małe projekty do zrobienia](#17-małe-projekty-do-zrobienia)
18. [Ściąga](#18-ściąga)
-19. [Słowniczek](#19-słowniczek)
+19. [Słowa, które będziesz słyszeć](#19-słowa-które-będziesz-słyszeć)
---
## 1. Co to jest program?
-Komputer jest bardzo szybki, ale nie myśli. Robi dokładnie to, co mu się każe,
-krok po kroku.
+Komputer jest bardzo szybki, ale nie myśli. Robi dokładnie to, co mu każesz, krok po kroku.
-**Program** to spisana lista takich kroków. Trochę jak przepis kuchenny:
+**Program** to spisana lista takich kroków. Trochę jak przepis:
> 1. Weź dwa jajka.
> 2. Wbij je do miski.
> 3. Wymieszaj.
-**Język programowania** to sposób zapisania takiego przepisu, który komputer rozumie.
-**Finch** (po polsku: zięba) jest jednym z takich języków. Zrobiono go tak, żeby był
-mały i prosty, więc nauczysz się go szybko.
+**Język programowania** to sposób zapisania takiego przepisu tak, żeby komputer go zrozumiał.
+**Finch** to jeden z takich języków. Powstał po to, żeby był mały i prosty, więc szybko się go nauczysz.
-Komputer czyta program od góry do dołu, linijka po linijce.
+Komputer czyta program od góry do dołu, linia po linii.
---
## 2. Przygotowanie Fincha
-Finch działa na komputerach z **Linuksem**. Jeśli nauczyciel, rodzic albo znajomy już go
-dla ciebie zainstalował, przejdź od razu do następnego rozdziału.
+Finch działa na komputerach z **Linuksem**, **macOS** i **Windowsem**. Jeśli nauczyciel, rodzic albo znajomy
+już go dla ciebie przygotował, przejdź do następnego rozdziału. (Na Macu i na Windowsie poproś kogoś o skorzystanie
+z [przewodnika dla techników](dla-technikow.md#2-instalacja): są tam gotowe wersje Fincha na oba systemy.)
-W przeciwnym razie otwórz program **Terminal** (okienko, w którym wpisuje się polecenia)
-i wpisz te linijki po kolei, naciskając **Enter** po każdej:
+W przeciwnym razie otwórz program **Terminal** (okno, w którym wpisuje się polecenia) i wpisz po kolei te linie,
+naciskając po każdej **Enter**:
```sh
git clone https://github.com/Nexoniarz/Finch.git
@@ -66,24 +65,24 @@ nix-shell
bootstrap/build.sh
```
-Jeśli ostatnia linijka skończy się bez słowa `error`, Finch jest gotowy.
-(Jeśli komputer nie zna polecenia `nix-shell`, poproś kogoś o skorzystanie z
-[przewodnika dla techników](dla-technikow.md#2-instalacja), gdzie opisano inny sposób.)
+Jeśli ostatnie polecenie skończy się bez słowa `error`, Finch jest gotowy.
+(Jeśli na twoim komputerze nie ma `nix-shell`, poproś kogoś o skorzystanie z
+[przewodnika dla techników](dla-technikow.md#2-instalacja). Jest tam opisany inny sposób.)
-Żeby sprawdzić, czy działa, wpisz:
+Żeby sprawdzić, czy wszystko działa, wpisz:
```sh
./build/finch version
```
-Powinieneś zobaczyć coś w stylu `finch 2.5.0`.
+Powinien pojawić się napis w rodzaju `Finch 2.6.0`.
---
## 3. Twój pierwszy program
-1. Otwórz dowolny edytor tekstu (na przykład *Edytor tekstu*, *Kate*, *gedit* albo *VS Code*).
-2. Wpisz dokładnie to:
+1. Otwórz dowolny edytor tekstu (np. *Edytor tekstu*, *Kate*, *gedit* albo *VS Code*).
+2. Przepisz dokładnie to:
```c
fn main() {
@@ -91,11 +90,11 @@ fn main() {
}
```
-3. Zapisz plik w folderze `Finch` pod nazwą **`czesc.fch`**. Końcówka `.fch` mówi, że to program w Finchu.
+3. Zapisz plik w katalogu `Finch` pod nazwą **`hello.fch`**. Końcówka `.fch` mówi, że to program w Finchu.
4. W Terminalu wpisz:
```sh
-./build/finch run czesc.fch
+./build/finch run hello.fch
```
Zobaczysz:
@@ -104,7 +103,7 @@ Zobaczysz:
Cześć!
```
-🎉 **Właśnie napisałeś i uruchomiłeś program.**
+🎉 **Właśnie udało ci się napisać i uruchomić program.**
### Co właściwie napisaliśmy?
@@ -114,26 +113,26 @@ fn main() { ← „tu zaczyna się główna część mojego programu
} ← „tu główna część się kończy”
```
-- `fn main()` to miejsce, od którego **zaczyna się** każdy program w Finchu. Każdy program ma dokładnie jedno `main`.
+- `fn main()` to miejsce, w którym **zaczyna się** każdy program w Finchu. Każdy program ma dokładnie jedno `main`.
- Nawiasy klamrowe `{` i `}` obejmują wszystko, co należy do `main`, jak dwie okładki książki.
- `print(...)` pokazuje coś na ekranie.
-- Tekst zawsze piszemy w cudzysłowie: `"Cześć!"`.
+- Tekst zawsze stoi w cudzysłowie: `"Cześć!"`.
-Spacje przed `print` nie są obowiązkowe, ale dzięki nim od razu widać, co jest w środku
-klamer. Zawsze je dodawaj, to dobry nawyk.
+Spacje przed `print` nie są obowiązkowe, ale dzięki nim od razu widać, co jest w środku nawiasów.
+Zawsze je dodawaj. To dobry nawyk.
---
-## 4. Wypisywanie na ekran
+## 4. Wypisywanie na ekranie
-`print` umie pokazać tekst i liczby. Między kolejne rzeczy wstaw przecinek, a Finch sam doda spację:
+`print` potrafi pokazać tekst i liczby. Oddziel je przecinkami, a Finch sam wstawi między nimi spacje:
```c
fn main() {
print("Mam", 10, "lat")
print("2 + 2 =", 2 + 2)
print()
- print("Linijka wyżej jest pusta")
+ print("Linia wyżej jest pusta")
}
```
@@ -143,10 +142,10 @@ Pokaże:
Mam 10 lat
2 + 2 = 4
-Linijka wyżej jest pusta
+Linia wyżej jest pusta
```
-Każdy `print` zaczyna nową linijkę. `print()` bez niczego w środku robi pustą linijkę.
+Każde `print` zaczyna nową linię. `print()` bez niczego w środku robi pustą linię.
### Notatki dla siebie: komentarze
@@ -163,8 +162,8 @@ fn main() {
## 5. Pudełka z nazwami: zmienne
-Wyobraź sobie pudełko z naklejoną etykietą. Możesz coś do niego włożyć, a potem sprawdzić,
-co jest w środku, czytając etykietę. W programowaniu takie pudełko nazywa się **zmienną**.
+Wyobraź sobie pudełko z naklejoną etykietą. Możesz coś do niego włożyć, a później sprawdzić, co jest w środku,
+czytając etykietę. W programowaniu takie pudełko nazywa się **zmienną**.
```c
fn main() {
@@ -173,9 +172,9 @@ fn main() {
}
```
-`wiek := 10` znaczy: *„zrób nowe pudełko, nazwij je `wiek` i włóż do niego 10”*.
+`wiek := 10` znaczy: *„zrób nowe pudełko, nazwij je `wiek` i włóż do niego 10”.*
-Później możesz zmienić zawartość pudełka, używając samego `=`:
+Później możesz zmienić zawartość pudełka, pisząc samo `=`:
```c
fn main() {
@@ -196,42 +195,39 @@ Punkty: 5
Punkty: 6
```
-`punkty = punkty + 1` czytamy tak: *„weź to, co jest w `punkty`, dodaj 1 i włóż wynik z powrotem do `punkty`”*.
-To samo można zapisać krócej: `punkty += 1`.
+`punkty = punkty + 1` czyta się tak: *„weź to, co jest w `punkty`, dodaj 1 i włóż wynik z powrotem do `punkty`”.*
+To samo można napisać krócej: `punkty += 1`.
**Zapamiętaj:**
-- `:=` robi **nowe** pudełko. Używasz tego tylko raz dla danej nazwy.
+- `:=` robi **nowe** pudełko. Dla każdej nazwy używasz tego tylko raz.
- `=` wkłada nową wartość do pudełka, **które już istnieje**.
### Nazywanie pudełek
-Nazwy mogą zawierać angielskie litery `a`–`z` i `A`–`Z`, cyfry i znak `_`, ale nie mogą zaczynać się od cyfry:
-`wiek`, `suma_punktow`, `gracz2` są w porządku. `2gracz` już nie.
-
-⚠️ **Bez polskich liter w nazwach!** `ą`, `ę`, `ł`, `ż` i inne mogą być tylko w tekście w cudzysłowie.
-Pisz `zolw`, a nie `żółw`. Natomiast `print("Żółw")` działa bez problemu.
-
+W nazwach można używać liter od `a` do `z` i od `A` do `Z`, cyfr i znaku `_`, ale nazwa nie może zaczynać się od cyfry:
+`wiek`, `suma_punktow`, `gracz2` są w porządku, a `2gracz` nie.
+Polskich liter (`ą`, `ę`, `ż`…) nie wolno używać w nazwach, tylko w tekstach w cudzysłowie: `print("Łódź")` jest w porządku.
Wybieraj nazwy, które mówią, co jest w środku: `cena` jest lepsza niż `c`.
---
## 6. Rodzaje wartości
-Finch pilnuje, *jakiego rodzaju* rzecz jest w każdym pudełku. Ten rodzaj nazywa się **typem**.
+Finch pilnuje, jaki *rodzaj* rzeczy leży w każdym pudełku. Taki rodzaj to **typ**.
Najczęściej będziesz używać tych:
-| Typ | Co przechowuje | Przykłady |
-|---------|---------------------------|--------------------------|
-| `int` | liczby całkowite | `0`, `42`, `-7` |
-| `float` | liczby z przecinkiem | `3.14`, `0.5`, `-2.0` |
-| `str` | tekst | `"Cześć"`, `"Ola"` |
-| `bool` | tak albo nie | `true` (prawda), `false` (fałsz) |
-| `char` | jeden znak | `'A'`, `'z'`, `'?'` |
+| Typ | Co przechowuje | Przykłady |
+|---------|-----------------------------|--------------------------|
+| `int` | liczby całkowite | `0`, `42`, `-7` |
+| `float` | liczby z przecinkiem | `3.14`, `0.5`, `-2.0` |
+| `str` | tekst | `"Cześć"`, `"Ola"` |
+| `bool` | tak albo nie | `true`, `false` |
+| `char` | jedną literę albo znak | `'A'`, `'z'`, `'?'` |
-⚠️ W liczbach zamiast przecinka piszemy **kropkę**: `3.14`, a nie `3,14`. Tak jest w prawie wszystkich językach programowania.
+Uwaga: w programach część ułamkową oddziela się **kropką**, nie przecinkiem: `3.14`, a nie `3,14`.
-Kiedy używasz `:=`, Finch sam rozpoznaje typ:
+Przy `:=` Finch sam rozpoznaje typ:
```c
imie := "Ola" // str
@@ -240,7 +236,7 @@ wzrost := 1.42 // float
lubi_koty := true // bool
```
-Typ możesz też napisać sam, przed nazwą:
+Typ można też napisać samemu, przed nazwą:
```c
int wiek = 10
@@ -249,24 +245,23 @@ str imie = "Ola"
Oba sposoby robią to samo. Wybierz ten, który ci bardziej pasuje.
-**Pudełko nie zmienia typu.** Jeśli w `wiek` są liczby całkowite, nie włożysz tam potem tekstu.
-Finch zatrzyma się i powie o tym, co pomaga wcześnie wyłapywać pomyłki.
+**Pudełko trzyma się swojego typu.** Jeśli w `wiek` są liczby całkowite, nie włożysz tam potem tekstu.
+Finch się zatrzyma i ci o tym powie, a to pomaga wcześnie wyłapywać pomyłki.
---
## 7. Liczenie
-| Znak | Co robi | Przykład | Wynik |
-|------|-----------------------------|----------|-------|
-| `+` | dodawanie | `7 + 2` | `9` |
-| `-` | odejmowanie | `7 - 2` | `5` |
-| `*` | mnożenie | `7 * 2` | `14` |
-| `/` | dzielenie | `7 / 2` | `3` |
-| `%` | reszta z dzielenia | `7 % 2` | `1` |
+| Znak | Znaczenie | Przykład | Wynik |
+|------|--------------------------------|----------|--------|
+| `+` | dodawanie | `7 + 2` | `9` |
+| `-` | odejmowanie | `7 - 2` | `5` |
+| `*` | mnożenie | `7 * 2` | `14` |
+| `/` | dzielenie | `7 / 2` | `3` |
+| `%` | reszta z dzielenia | `7 % 2` | `1` |
-**Uwaga na dzielenie liczb całkowitych:** `7 / 2` daje `3`, a nie `3.5`. Obie liczby są
-całkowite, więc wynik też jest całkowity (część po kropce jest odrzucana).
-Jeśli chcesz dostać `3.5`, użyj liczby z kropką: `7.0 / 2`.
+**Uważaj przy dzieleniu liczb całkowitych:** `7 / 2` daje `3`, a nie `3.5`, bo obie liczby są całkowite, więc wynik
+też jest całkowity (część po kropce znika). Jeśli chcesz `3.5`, użyj liczby z kropką: `7.0 / 2`.
Mnożenie i dzielenie wykonują się przed dodawaniem i odejmowaniem, tak jak w szkole.
Nawiasy zmieniają kolejność:
@@ -276,28 +271,28 @@ print(2 + 3 * 4) // 14
print((2 + 3) * 4) // 20
```
-`%` przydaje się do sprawdzania, czy liczba jest parzysta. Liczba parzysta dzielona przez 2
-nie daje reszty, więc `n % 2` wynosi `0`.
+`%` przydaje się do sprawdzania, czy liczba jest parzysta: liczba parzysta dzielona przez 2 nie daje reszty,
+więc `n % 2` wynosi `0`.
-**Nie wolno dzielić przez zero.** Jeśli program spróbuje, zatrzyma się i powie, w której linijce to się stało.
+**Nie wolno dzielić przez zero.** Jeśli program spróbuje, zatrzyma się i powie, w której linii to się stało.
---
## 8. Podejmowanie decyzji: if
-Program może wybierać, co zrobić. `if` (czyt. „if”, po polsku „jeżeli”) znaczy *„zrób to tylko wtedy, gdy coś jest prawdą”*.
+Program może wybierać, co zrobić. `if` znaczy *„zrób to tylko wtedy, gdy coś jest prawdą”*.
```c
fn main() {
temperatura := 25
if temperatura > 20 {
- print("Jest ciepło, załóż koszulkę")
+ print("Jest ciepło, włóż koszulkę")
}
}
```
-Dodaj `else` („w przeciwnym razie”):
+Dopisz `else`, żeby powiedzieć *„a jeśli nie, zrób to”*:
```c
if temperatura > 20 {
@@ -307,12 +302,11 @@ if temperatura > 20 {
}
```
-A `else if` dodaje kolejne możliwości. Finch sprawdza je od góry i wykonuje tylko
-**pierwszą** prawdziwą:
+A `else if` daje więcej możliwości. Finch sprawdza je od góry do dołu i wykonuje tylko **pierwszą**, która jest prawdą:
```c
if temperatura > 25 {
- print("Gorąco!")
+ print("Upał!")
} else if temperatura > 15 {
print("Przyjemnie")
} else {
@@ -328,18 +322,18 @@ if temperatura > 25 {
| `!=` | nie jest równe |
| `<` | jest mniejsze niż |
| `>` | jest większe niż |
-| `<=` | jest mniejsze lub równe |
-| `>=` | jest większe lub równe |
+| `<=` | jest mniejsze albo równe |
+| `>=` | jest większe albo równe |
⚠️ **Jedno `=` wkłada wartość do pudełka. Dwa `==` pytają „czy to jest równe?”.**
-Tekst też można porównywać: `if imie == "Ola" { ... }`.
+Porównywać można też teksty: `if imie == "Ola" { ... }`.
### Łączenie pytań
- `&&` znaczy **i**: oba warunki muszą być prawdziwe. `if wiek > 6 && wiek < 12 { ... }`
-- `||` znaczy **lub**: wystarczy, że jeden jest prawdziwy. `if dzien == "sob" || dzien == "nd" { ... }`
-- `!` znaczy **nie**: odwraca prawdę i fałsz. `if !pada { ... }`
+- `||` znaczy **lub**: przynajmniej jeden musi być prawdziwy. `if dzien == "sob" || dzien == "niedz" { ... }`
+- `!` znaczy **nie**: zamienia prawdę w fałsz i odwrotnie. `if !pada { ... }`
---
@@ -365,7 +359,7 @@ Liczba 4
Liczba 5
```
-`for i in 1..6` znaczy: *„licz za pomocą `i` od 1 do 6, ale **bez** 6”*.
+`for i in 1..6` znaczy: *„licz w `i` od 1 do 6, ale **bez** 6”*.
Czyli `0..10` daje dziesięć liczb: 0, 1, 2 … 9.
### Powtarzanie, dopóki coś jest prawdą: `while`
@@ -388,8 +382,8 @@ fn main() {
Start!
```
-⚠️ Upewnij się, że pętla może się skończyć. Gdyby `licznik` nigdy nie malał, pętla kręciłaby się
-w nieskończoność. Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**, żeby ją zatrzymać.
+⚠️ Upewnij się, że pętla może się skończyć. Gdyby `licznik` nigdy nie malał, pętla kręciłaby się bez końca.
+Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**, żeby ją zatrzymać.
### Wcześniejsze wyjście i pomijanie
@@ -399,9 +393,9 @@ w nieskończoność. Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**,
```c
for i in 1..100 {
if i == 5 {
- break // stop, gdy dojdziemy do 5
+ break // zatrzymaj się, gdy dojdziemy do 5
}
- print(i) // pokaże 1 2 3 4
+ print(i) // wypisze 1 2 3 4
}
```
@@ -409,7 +403,7 @@ for i in 1..100 {
## 10. Własne polecenia: funkcje
-Kiedy robisz to samo w wielu miejscach, nadaj temu nazwę. Taki nazwany kawałek kodu to **funkcja**.
+Kiedy robisz to samo w wielu miejscach, nadaj temu nazwę. Taki nazwany blok to **funkcja**.
Jedną już znasz: `main`.
```c
@@ -428,12 +422,12 @@ Cześć, Ola
Cześć, Tomek
```
-`str imie` w nawiasie znaczy: *„ta funkcja potrzebuje jednego tekstu, a w środku nazywamy go `imie`”*.
-To, co wpiszesz w nawias przy wywołaniu, zostanie przekazane do środka.
+`str imie` w nawiasach znaczy: *„ta funkcja potrzebuje jednego tekstu i w środku będziemy go nazywać `imie`”.*
+To, co wpiszesz w nawiasach przy jej użyciu, trafia do środka.
-### Funkcje, które oddają odpowiedź
+### Funkcje, które dają odpowiedź
-Napisz `->` i typ odpowiedzi, a odpowiedź oddaj słowem `return`:
+Napisz `->` i typ odpowiedzi, a odpowiedź oddaj przez `return`:
```c
fn dodaj(int a, int b) -> int {
@@ -446,14 +440,14 @@ fn main() {
}
```
-Nie ma znaczenia, gdzie w pliku umieścisz swoje funkcje, nad czy pod `main`.
+Nie ma znaczenia, gdzie w pliku umieścisz swoje funkcje, nad `main` czy pod nim.
---
## 11. Listy: wiele wartości w jednym pudełku
-Czasem jedno pudełko nie wystarczy. Na liście zakupów jest wiele rzeczy.
-**Lista** (programiści mówią też **tablica**) trzyma wiele wartości po kolei:
+Czasem jedno pudełko nie wystarcza. Na liście zakupów jest wiele rzeczy.
+**Lista** (programiści mówią też **tablica**) przechowuje wiele wartości po kolei:
```c
fn main() {
@@ -470,34 +464,34 @@ Mam 3 owoce
Pierwszy to jabłko
```
-- Nawiasy kwadratowe `[ ]` tworzą listę. Wartości oddzielamy przecinkami.
+- Nawiasy kwadratowe `[ ]` tworzą listę. Wartości oddziela się przecinkami.
- `.len` mówi, ile rzeczy jest na liście.
-- `owoce[0]` to **pierwsza** rzecz. Liczymy od **0**, więc `owoce[1]` to druga, a `owoce[2]` trzecia.
+- `owoce[0]` to **pierwsza** rzecz. Liczy się od **0**, więc `owoce[1]` to druga, a `owoce[2]` trzecia.
Jeśli zapytasz o miejsce, którego nie ma, np. `owoce[10]`, program się zatrzyma i powie:
-`index 10 is out of range (the length is 3)`, czyli „indeks 10 jest poza zakresem (długość to 3)”.
+`index 10 is out of range (the length is 3)` (indeks 10 jest poza listą, która ma długość 3).
### Dodawanie i zabieranie
```c
fn main() {
[]int wyniki // pusta lista liczb całkowitych
- wyniki.push(10) // dołóż 10 na koniec
+ wyniki.push(10) // dodaj 10 na końcu
wyniki.push(25)
wyniki.push(7)
print(wyniki) // [10, 25, 7]
- ostatni := wyniki.pop() // zabierz ostatni
+ ostatni := wyniki.pop() // zdejmij ostatni
print(ostatni, wyniki) // 7 [10, 25]
- wyniki.sort() // posortuj
+ wyniki.sort()
print(wyniki)
}
```
### Przechodzenie po liście
-`for ... in` odwiedza każdą wartość po kolei:
+`for ... in` odwiedza po kolei każdą wartość:
```c
fn main() {
@@ -509,8 +503,8 @@ fn main() {
}
```
-Jeśli chcesz też wiedzieć, *w którym miejscu* listy jesteś, daj `for` dwie nazwy: pierwsza liczy
-0, 1, 2…, druga to wartość:
+Jeśli chcesz też wiedzieć, *w którym miejscu* listy jesteś, podaj w `for` dwie nazwy: pierwsza liczy 0, 1, 2…,
+a druga to wartość:
```c
fn main() {
@@ -526,21 +520,20 @@ fn main() {
3 wiśnia
```
-Listy umieją więcej: `contains` (czy zawiera), `find` (znajdź), `insert` (wstaw), `remove` (usuń),
-`sort` (sortuj), `reverse` (odwróć), `join` (połącz). Wszystkie opisuje [przewodnik dla techników](dla-technikow.md#10-tablice-i-mapy).
+Listy potrafią więcej: `contains`, `find`, `insert`, `remove`, `sort`, `reverse`, `join`.
+Wszystkie są opisane w [przewodniku dla techników](dla-technikow.md#10-tablice-i-mapy).
### Szukanie po nazwie: mapy
-Lista znajduje rzeczy po miejscu: `owoce[0]`. Czasem chcesz znaleźć coś po **nazwie**, tak jak
-słowo w słowniku albo numer w książce telefonicznej. Do tego służy **mapa** (`map`): każdy
-**klucz** (nazwa) ma swoją **wartość**:
+Lista znajduje rzeczy po miejscu: `owoce[0]`. Czasem jednak chcesz szukać po **nazwie**, tak jak szuka się słowa
+w słowniku albo numeru w książce telefonicznej. Do tego służy **mapa**: każdy **klucz** (nazwa) ma swoją **wartość**:
```c
fn main() {
telefon := ["Ola": "555-1234", "Jan": "555-9876"]
print(telefon["Ola"]) // 555-1234
- telefon["Ewa"] = "555-0000" // nowa osoba
+ telefon["Ewa"] = "555-0000" // dodaj nowe imię
print(telefon.len) // 3
if telefon.has("Tomek") {
@@ -549,26 +542,26 @@ fn main() {
print("nie ma Tomka")
}
- for imie, numer in telefon { // każde imię z jego numerem
+ for imie, numer in telefon { // każde imię razem z numerem
print(imie, numer)
}
}
```
-- `["klucz": wartość, ...]` tworzy mapę. Klucze to zwykle tekst, ale mogą być też liczby.
+- `["klucz": wartość, ...]` tworzy mapę. Kluczami są często teksty, ale mogą być też liczby.
- `telefon["Ola"]` daje wartość dla klucza. Pytanie o klucz, którego nie ma, zatrzymuje program,
- więc najpierw sprawdź `telefon.has("Tomek")` (czy ma) albo użyj `telefon.get("Tomek", "nie wiem")`.
-- `for imie, numer in telefon` odwiedza każdy klucz z wartością, w kolejności dodawania.
+ więc najpierw sprawdź `telefon.has("Tomek")` albo użyj `telefon.get("Tomek", "nieznany")`.
+- `for imie, numer in telefon` odwiedza każdy klucz z jego wartością, w kolejności, w jakiej je dodano.
Mapy świetnie nadają się do **liczenia**. Nowy klucz zaczyna od 0, więc `+= 1` po prostu działa:
```c
fn main() {
- map[str]int glosy // pusta mapa: klucze to tekst, wartości to liczby
+ map[str]int glosy // pusta mapa: klucze to teksty, wartości to liczby całkowite
for g in ["kot", "pies", "kot", "kot", "rybka"] {
glosy[g] += 1
}
- print(glosy) // {"kot": 3, "pies": 1, "rybka": 1}
+ print(glosy) // {"kot": 3, "pies": 1, "rybka": 1}
}
```
@@ -576,7 +569,7 @@ fn main() {
## 12. Zabawa tekstem
-Teksty sklejasz znakiem `+`:
+Teksty można sklejać przez `+`:
```c
fn main() {
@@ -600,16 +593,16 @@ fn main() {
slowo := "Finch"
print(slowo.len) // 5 liter
print(slowo[0]) // F (pierwsza litera)
- print(slowo.upper()) // FINCH (wielkimi literami)
- print(slowo.lower()) // finch (małymi literami)
- print(slowo.contains("in")) // true (czy zawiera "in")
+ print(slowo.upper()) // FINCH
+ print(slowo.lower()) // finch
+ print(slowo.contains("in")) // true
for litera in slowo {
- print(litera) // F, i, n, c, h, każda w osobnej linijce
+ print(litera) // F, i, n, c, h, każda w osobnej linii
}
}
```
-⚠️ `.len` liczy bajty, a polskie litery (ą, ę, ż…) zajmują po 2 bajty. `"żółw".len` to 7, a nie 4.
+(`.len` liczy bajty, a polskie litery zajmują po dwa, więc `"żółw".len` to 7, a nie 4.)
---
@@ -634,15 +627,15 @@ fn main() {
}
```
-Jeśli ktoś wpisze coś, co nie jest liczbą, program zatrzyma się z komunikatem
-w stylu `can't turn "abc" into int` („nie da się zamienić "abc" na int”).
+Jeśli ktoś wpisze coś, co nie jest liczbą, program się zatrzyma z komunikatem
+w rodzaju `can't turn "abc" into int` (nie da się zamienić "abc" na liczbę).
---
## 14. Własne rodzaje wartości: struktury
-Wyobraź sobie kartę w bibliotece: każda karta ma tytuł, autora i rok.
-**Struktura** (`struct`) pozwala zrobić własny rodzaj wartości z nazwanymi częściami:
+Pomyśl o karcie w bibliotece: każda ma tytuł, autora i rok wydania.
+**Struktura** pozwala stworzyć własny rodzaj wartości z nazwanymi częściami:
```c
struct Ksiazka {
@@ -664,11 +657,11 @@ Hobbit napisał Tolkien
Ksiazka(tytul: "Hobbit", autor: "Tolkien", rok: 1938)
```
-- `struct Ksiazka { ... }` opisuje, co ma każda książka. Pisz to **poza** `main`.
+- `struct Ksiazka { ... }` opisuje, co ma każda książka. Umieść to **poza** `main`.
- `Ksiazka(tytul: ..., autor: ..., rok: ...)` tworzy jedną książkę.
-- `k.tytul` odczytuje jedną część, a `k.rok = 1938` ją zmienia.
+- `k.tytul` odczytuje jedną część. `k.rok = 1938` ją zmienia.
-Częściom możesz nadać wartość początkową. Wtedy nie trzeba ich podawać:
+Częściom można nadać wartość początkową. Wtedy nie trzeba ich wypełniać:
```c
struct Gracz {
@@ -682,12 +675,12 @@ fn main() {
}
```
-Możesz też trzymać wiele struktur na liście: `[]Ksiazka polka`, a potem `polka.push(k)`.
+Wiele takich wartości można trzymać na liście: `[]Ksiazka polka`, a potem `polka.push(k)`.
-### Polecenia twojej struktury: metody
+### Polecenia dla struktury: metody
-Struktura może mieć własne polecenia (**metody**). Napisz nazwę struktury, kropkę i nazwę polecenia.
-W środku `self` (ang. „sam”) to struktura, na której użyto polecenia:
+Struktura może mieć własne polecenia. Napisz nazwę struktury, kropkę i nazwę polecenia.
+W środku `self` to struktura, na której polecenie użyto:
```c
struct Gracz {
@@ -697,7 +690,7 @@ struct Gracz {
fn Gracz.trafiony() {
self.zycia -= 1
- print(self.imie, "ma jeszcze", self.zycia, "życia")
+ print(self.imie, "- pozostałe życia:", self.zycia)
}
fn Gracz.zyje() -> bool {
@@ -706,8 +699,8 @@ fn Gracz.zyje() -> bool {
fn main() {
g := Gracz(imie: "Ola")
- g.trafiony() // Ola ma jeszcze 2 życia
- g.trafiony() // Ola ma jeszcze 1 życia
+ g.trafiony() // Ola - pozostałe życia: 2
+ g.trafiony() // Ola - pozostałe życia: 1
print(g.zyje()) // true
}
```
@@ -731,14 +724,14 @@ fn main() {
- `read_file(nazwa)` oddaje tekst z pliku.
- `file_exists(nazwa)` mówi, czy plik istnieje (`true` albo `false`).
-Plik pojawi się w folderze, w którym uruchomiłeś program.
+Plik pojawi się w katalogu, w którym uruchomiono program.
---
## 16. Kiedy coś pójdzie nie tak
-Każdy się myli przy programowaniu, nawet eksperci, codziennie.
-Finch stara się tłumaczyć, co poszło nie tak, prostymi słowami (na razie po angielsku). Przykład:
+Każdy, kto programuje, robi błędy, nawet eksperci, codziennie.
+Finch bardzo się stara wyjaśnić prostymi słowami, co poszło nie tak. Przykład:
```c
fn main() {
@@ -748,68 +741,67 @@ fn main() {
```
```
-czesc.fch:3:11: error: there is no variable named 'wiekk'
+hello.fch:3:11: error: there is no variable named 'wiekk'
3 | print(wiekk)
| ^
```
Jak to czytać:
-- `czesc.fch` to plik,
-- `3` to numer **linijki**,
-- `11` to który znak w linijce (**kolumna**),
-- potem wyjaśnienie, a pod nim ta linijka ze strzałką `^` wskazującą problem.
+- `hello.fch` to plik,
+- `3` to numer **linii**,
+- `11` mówi, jak daleko w linii (to **kolumna**),
+- dalej jest wyjaśnienie, a pod nim ta linia ze znakiem `^`, który wskazuje problem.
-Tutaj po prostu źle napisaliśmy `wiek`. Komunikat znaczy: „nie ma zmiennej o nazwie 'wiekk'”.
+Komunikaty są po angielsku; tutaj znaczy to „nie ma zmiennej o nazwie 'wiekk'”. Po prostu literówka w `wiek`.
-### Częste błędy
+### Częste pomyłki
| Finch mówi | Co to znaczy | Jak naprawić |
|---|---|---|
-| `there is no variable named 'x'` | Nie ma zmiennej `x`: literówka albo zapomniałeś ją zrobić | Sprawdź pisownię. Utwórz ją przez `x := ...` |
-| `a variable named 'x' already exists here` | Użyłeś `:=` dwa razy dla tej samej nazwy | Za drugim razem użyj `=` |
+| `there is no variable named 'x'` | Literówka w nazwie albo takie pudełko nigdy nie powstało | Sprawdź pisownię. Utwórz je przez `x := ...` |
+| `a variable named 'x' already exists here` | Użyto `:=` dwa razy dla tej samej nazwy | Za drugim razem napisz `=` |
| `'x' must be int, but this is str` | Zły rodzaj wartości dla tego pudełka | Włóż wartość właściwego rodzaju |
-| `put each statement on its own line` | Dwa polecenia w jednej linijce | Wciśnij Enter między nimi |
+| `put each statement on its own line` | Dwa polecenia w jednej linii | Rozdziel je Enterem |
| `this '{' is never closed` | Brakuje `}` | Dopisz `}` |
-| `a condition must be bool` | `if` potrzebuje pytania tak/nie | Porównaj coś: `if x > 0` |
-| `unexpected character 'ż'` | Polska litera w nazwie | Zmień na zwykłą literę: `zolw` |
-| `runtime error: division by zero` | Program podzielił przez 0 w trakcie działania | Sprawdź liczbę przed dzieleniem |
-| `index 5 is out of range (the length is 3)` | Zapytałeś listę o miejsce, którego nie ma | Pamiętaj, że liczymy od 0; sprawdź `.len` |
-| `can't turn "abc" into int` | `int(...)` dostał tekst, który nie jest liczbą | Sprawdź, co wpisał użytkownik |
-| `can't use '+' on str and int` | Tekst plus liczba | Zamień liczbę na tekst przez `str(...)` |
-| `'f' can fail, so say what happens then` | Użyłeś polecenia, które może się nie udać, i nie napisałeś, co wtedy | Dopisz `or ...` (niżej) |
+| `a condition must be bool` | `if` potrzebuje pytania typu tak/nie | Porównaj coś: `if x > 0` |
+| `runtime error: division by zero` | W trakcie działania programu nastąpiło dzielenie przez 0 | Sprawdź liczbę przed dzieleniem |
+| `index 5 is out of range (the length is 3)` | Pytanie listy o miejsce, którego nie ma | Pamiętaj, że liczy się od 0; sprawdź `.len` |
+| `can't turn "abc" into int` | `int(...)` dostało tekst, który nie jest liczbą | Sprawdź, co wpisał użytkownik |
+| `can't use '+' on str and int` | Tekst plus liczba | Użyj `str(...)` na liczbie |
+| `'f' can fail, so say what happens then` | Użyto polecenia, które może się nie udać, bez powiedzenia, co wtedy zrobić | Dopisz `or ...` (zob. niżej) |
### Rzeczy, które mogą się nie udać: `or` i `try`
-Niektóre rzeczy mogą pójść źle *w trakcie działania programu*: ktoś wpisze `abc` zamiast liczby
-albo pliku nie ma. Zamiast zatrzymywać program, możesz powiedzieć, co wtedy zrobić, słowem **`or`** („albo”):
+Niektóre rzeczy mogą się nie udać *w trakcie działania programu*: ktoś wpisze `abc` tam, gdzie miała być liczba,
+albo nie ma pliku. Zamiast zatrzymywać program, możesz przez **`or`** powiedzieć, co wtedy zrobić:
```c
fn main() {
- wiek := int(input("Ile masz lat? ")) or 0 // to nie liczba? weź 0
+ wiek := int(input("Ile masz lat? ")) or 0 // to nie liczba? weź 0
print("Za rok będziesz mieć", wiek + 1)
- notatka := read_file("notatka.txt") or "(jeszcze brak notatki)"
+ notatka := read_file("notatka.txt") or "(jeszcze nie ma notatki)"
print(notatka)
}
```
-Po `or` może też być blok. W środku `err` to komunikat, co poszło nie tak:
+Po `or` może też stać blok. W jego środku `err` to komunikat, który mówi, co poszło nie tak:
```c
fn main() {
while true {
- n := int(input("Podaj liczbę: ")) or {
+ n := int(input("Wybierz liczbę: ")) or {
print("To nie jest liczba, spróbuj jeszcze raz")
continue
}
- print("Wybrałeś", n)
+ print("Wybrana liczba:", n)
break
}
}
```
-Twoje własne funkcje też mogą się nie udać. Dopisz `!` po typie wyniku i użyj `return error("...")`:
+Twoje własne funkcje też mogą się nie udać. Postaw `!` po typie wyniku i użyj `return error("...")`:
```c
fn bezpieczne_dzielenie(int a, int b) -> int! {
@@ -825,15 +817,15 @@ fn main() {
}
```
-Finch nie pozwoli ci zapomnieć: wywołanie `bezpieczne_dzielenie(10, 0)` bez `or` to błąd, który
-przypomina, żeby to obsłużyć. (Można też przekazać problem dalej przez `try`; wyjaśnia to
+Finch nie pozwoli o tym zapomnieć: wywołanie `bezpieczne_dzielenie(10, 0)` bez `or` to błąd, który każe ci to obsłużyć.
+(Problem można też przekazać dalej przez `try`; wyjaśnia to
[przewodnik dla techników](dla-technikow.md#błędy-jako-wartości).)
---
## 17. Małe projekty do zrobienia
-Spróbuj najpierw napisać każdy sam. Do rozwiązania zajrzyj dopiero, gdy utkniesz.
+Spróbuj najpierw napisać każdy sam. Do rozwiązania zajrzyj dopiero wtedy, gdy utkniesz.
### Projekt 1: Tabliczka mnożenia
@@ -852,7 +844,7 @@ fn main() {
### Projekt 2: Parzysta czy nieparzysta
-Dla każdej liczby od 1 do 10 napisz, czy jest parzysta, czy nieparzysta.
+Dla każdej liczby od 1 do 10 pokaż, czy jest parzysta, czy nieparzysta.
Rozwiązanie
@@ -871,7 +863,7 @@ fn main() {
### Projekt 3: Stopnie Celsjusza na Fahrenheita
-Pokaż tabelkę od 0 °C do 100 °C co 10 stopni. Wzór: `F = C * 9 / 5 + 32`.
+Pokaż tabelę od 0 °C do 100 °C co 10 stopni. Wzór: `F = C * 9 / 5 + 32`.
Rozwiązanie
@@ -893,8 +885,8 @@ fn main() {
### Projekt 4: Fizz Buzz
-Policz od 1 do 20. Dla liczb podzielnych przez 3 napisz `Fizz`, przez 5 napisz `Buzz`,
-przez obie naraz napisz `FizzBuzz`, a w pozostałych przypadkach samą liczbę.
+Policz od 1 do 20. Dla liczb podzielnych przez 3 powiedz `Fizz`, przez 5 `Buzz`,
+przez obie `FizzBuzz`, a w pozostałych przypadkach samą liczbę.
Rozwiązanie
@@ -917,7 +909,7 @@ fn main() {
### Projekt 5: Największa z trzech
-Napisz funkcję `najwieksza(int a, int b, int c) -> int`, która oddaje największą z trzech liczb.
+Napisz funkcję `najwieksza(int a, int b, int c) -> int`, która zwraca największą liczbę.
Rozwiązanie
@@ -940,7 +932,8 @@ fn main() {
### Projekt 6: Zgadnij liczbę
-Komputer ma tajną liczbę. Gracz zgaduje, aż trafi, a komputer za każdym razem mówi „więcej” albo „mniej”.
+Komputer wybiera tajną liczbę. Gracz zgaduje, aż trafi, a komputer za każdym razem
+mówi „więcej” albo „mniej”.
Rozwiązanie
@@ -956,7 +949,7 @@ fn main() {
} else if strzal > tajna {
print("Mniej!")
} else {
- print("Tak! Potrzebowałeś", proby, "prób")
+ print("Tak! Liczba prób:", proby)
return
}
}
@@ -968,7 +961,7 @@ fn main() {
### Projekt 7: Średnia ocen
-Zapytaj o 3 oceny, zapisz je na liście, a potem pokaż listę i średnią.
+Zapytaj o 3 oceny, zapisz je na liście i pokaż listę oraz średnią.
Rozwiązanie
@@ -998,9 +991,9 @@ fn main() {
fn main() { // tu zaczyna się program
print("tekst", 42) // pokaż coś
- x := 5 // nowe pudełko (typ zgadnięty)
+ x := 5 // nowe pudełko (typ rozpoznany sam)
int y = 10 // nowe pudełko (typ podany)
- x = 7 // zmień zawartość
+ x = 7 // zmiana zawartości pudełka
x += 1 // dodaj 1 do x
if x > 5 { // decyzja
@@ -1020,72 +1013,49 @@ fn main() { // tu zaczyna się program
}
}
-fn dodaj(int a, int b) -> int { // własne polecenie
+fn dodaj(int a, int b) -> int { // twoje własne polecenie
return a + b
}
-struct Zwierzak { // własny rodzaj wartości
+struct Zwierzak { // twój własny rodzaj wartości
str imie
int wiek = 1
}
-// listy: liczby := [1, 2, 3] liczby.push(4) liczby[0] liczby.len for n in liczby { } for i, n in liczby { }
-// mapy: wiek := ["Ola": 9] wiek["Jan"] = 10 wiek["Ola"] wiek.has("Tomek") for imie, lata in wiek { }
-// tekst: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x")
+// listy: liczby := [1, 2, 3] liczby.push(4) liczby[0] liczby.len for n in liczby { } for i, n in liczby { }
+// mapy: wiek := ["Ola": 9] wiek["Jan"] = 10 wiek["Ola"] wiek.has("Tomek") for imie, w in wiek { }
+// tekst: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x")
// pytanie: imie := input("Imię? ")
-// pliki: write_file("p.txt", tekst) read_file("p.txt")
-// metody: fn Zwierzak.urosnij() { self.wiek += 1 } z.urosnij()
+// pliki: write_file("p.txt", tekst) read_file("p.txt")
+// metody: fn Zwierzak.urosnij() { self.wiek += 1 } z.urosnij()
// porażki: n := int(tekst) or 0 x := read_file("p.txt") or { print(err) ... }
```
---
-## 19. Słowniczek
+## 19. Słowa, które będziesz słyszeć
| Słowo | Znaczenie |
|---|---|
| **program** | Lista kroków dla komputera |
| **kod** | Tekst programu |
| **uruchomić** | Kazać komputerowi wykonać kroki |
-| **kompilować** | Przetłumaczyć kod na coś, co komputer umie uruchomić. `finch run` robi to za ciebie |
-| **zmienna** | Pudełko z nazwą, w którym jest wartość |
+| **skompilować** | Przetłumaczyć kod na coś, co komputer potrafi uruchomić. `finch run` robi to za ciebie |
+| **zmienna** | Pudełko z nazwą, w którym leży wartość |
| **typ** | Rodzaj wartości: liczba, tekst, tak/nie… |
| **funkcja** | Nazwana grupa kroków, której można używać wiele razy |
| **pętla** | Kroki, które się powtarzają |
-| **warunek** | Pytanie tak/nie, np. `x > 5` |
+| **warunek** | Pytanie typu tak/nie, np. `x > 5` |
| **lista / tablica** | Wiele wartości po kolei, np. `[1, 2, 3]` |
-| **indeks** | Pozycja wartości na liście, liczona od 0 |
-| **struktura** (struct) | Własny rodzaj wartości złożony z nazwanych części |
+| **indeks** | Miejsce wartości na liście, liczone od 0 |
+| **struktura** | Twój własny rodzaj wartości z nazwanych części |
| **metoda** | Polecenie należące do struktury, np. `g.trafiony()` |
-| **mapa** (map) | Wartości znajdowane po kluczu (nazwie), jak w słowniku |
-| **wejście** (input) | To, co użytkownik wpisuje do programu |
-| **błąd** (error) | Komunikat, że coś jest nie tak, i gdzie |
+| **mapa** | Wartości wyszukiwane po kluczu (nazwie), jak w słowniku |
+| **wejście** (ang. *input*) | To, co ktoś wpisuje do programu |
+| **błąd** (ang. *error*) | Komunikat, że coś jest nie tak, i gdzie |
| **bug** | Pomyłka w programie |
-### Słówka po angielsku, które spotkasz w Finchu
-
-| Słowo | Po polsku |
-|---|---|
-| `print` | wypisz |
-| `if` / `else` | jeżeli / w przeciwnym razie |
-| `for` … `in` | dla … w |
-| `while` | dopóki |
-| `break` / `continue` | przerwij / kontynuuj |
-| `return` | zwróć (oddaj) |
-| `true` / `false` | prawda / fałsz |
-| `fn` (od *function*) | funkcja |
-| `struct` | struktura |
-| `push` / `pop` | dołóż / zabierz |
-| `input` | wejście (pytanie) |
-| `len` (od *length*) | długość |
-| `error` | błąd |
-| `or` | albo (co zrobić, gdy się nie uda) |
-| `try` | spróbuj (a jak się nie uda, przekaż błąd dalej) |
-| `self` | sam (struktura, na której użyto metody) |
-| `map` | mapa (słownik) |
-| `has` / `get` | ma / weź |
-
---
-**Co dalej?** Kiedy to wszystko wyda ci się łatwe, przejdź do
+**Co dalej?** Kiedy to wszystko będzie już łatwe, przejdź do
[przewodnika dla techników](dla-technikow.md). Opisuje każdą część Fincha.
diff --git a/docs/pl/dla-technikow.md b/docs/pl/dla-technikow.md
index 4ebceba..a925f03 100644
--- a/docs/pl/dla-technikow.md
+++ b/docs/pl/dla-technikow.md
@@ -1,9 +1,9 @@
# Finch dla techników
-**Dla osób, które dobrze znają komputery, ale programować nie umieją albo umieją niewiele.**
-Ten przewodnik opisuje instalację Fincha, obsługę polecenia `finch`, cały język, pamięć,
-moduły, biblioteki C, debugowanie i rozwiązywanie typowych problemów. Nie opisuje, jak
-kompilator działa w środku. Do tego służy [przewodnik dla inżynierów](dla-inzynierow.md).
+**Dla osób, które dobrze znają komputer, ale niewiele albo wcale nie programowały.**
+Ten przewodnik opisuje instalację Fincha, polecenie `finch`, cały język, pamięć, moduły, biblioteki C, debugowanie
+i rozwiązywanie typowych problemów. Nie opisuje, jak kompilator działa od środka; o tym jest
+[przewodnik dla inżynierów](dla-inzynierow.md).
> 🇬🇧 English version: [for-technicians.md](../en/for-technicians.md)
@@ -18,7 +18,7 @@ kompilator działa w środku. Do tego służy [przewodnik dla inżynierów](dla-
5. [Zmienne](#5-zmienne)
6. [Typy](#6-typy)
7. [Operatory](#7-operatory)
-8. [Sterowanie przebiegiem](#8-sterowanie-przebiegiem)
+8. [Sterowanie](#8-sterowanie)
9. [Funkcje](#9-funkcje)
10. [Tablice i mapy](#10-tablice-i-mapy)
11. [Tekst (str)](#11-tekst-str)
@@ -27,12 +27,12 @@ kompilator działa w środku. Do tego służy [przewodnik dla inżynierów](dla-
14. [Wskaźniki](#14-wskaźniki)
15. [Funkcje wbudowane](#15-funkcje-wbudowane)
16. [Moduły](#16-moduły)
-17. [Biblioteki z C](#17-biblioteki-z-c)
+17. [Biblioteki C](#17-biblioteki-c)
18. [Błędy](#18-błędy)
19. [Edytory: VS Code, Kate i inne](#19-edytory-vs-code-kate-i-inne)
20. [Debugowanie](#20-debugowanie)
21. [Rozwiązywanie problemów](#21-rozwiązywanie-problemów)
-22. [Struktura projektu i testy](#22-struktura-projektu-i-testy)
+22. [Układ repozytorium i testy](#22-układ-repozytorium-i-testy)
23. [Enumy i match](#23-enumy-i-match)
24. [Typy generyczne](#24-typy-generyczne)
25. [Biblioteka standardowa](#25-biblioteka-standardowa)
@@ -41,82 +41,111 @@ kompilator działa w środku. Do tego służy [przewodnik dla inżynierów](dla-
28. [Projekt: finch.toml](#28-projekt-finchtoml)
29. [Wątki](#29-wątki)
30. [C++, Rust i Android](#30-c-rust-i-android)
+31. [Wektory (SIMD)](#31-wektory-simd)
---
## 1. Czym jest Finch
-Finch to język **kompilowany**. Narzędzie `finch` tłumaczy plik `.fch` na prawdziwy program
-(natywny plik wykonywalny, taki jak te z C). Korzysta przy tym z **LLVM**, tego samego zaplecza
-kompilatorów, na którym działają clang, Rust i Swift.
+Finch to **kompilowany** język programowania. Polecenie `finch` tłumaczy plik `.fch` na prawdziwy program (natywny plik
+wykonywalny, taki jak programy napisane w C) za pomocą **LLVM**, tego samego zaplecza, z którego korzystają clang,
+Rust i Swift.
-Co to daje:
+Co z tego wynika:
-- **Szybkość zbliżoną do C.** Nie ma interpretera, maszyny wirtualnej ani garbage collectora.
- Kontrola zakresu tablic i inne zabezpieczenia po optymalizacji prawie nic nie kosztują.
+- **Szybkość bliska C.** Nie ma interpretera, maszyny wirtualnej ani garbage collectora. Sprawdzanie zakresów tablic
+ i inne zabezpieczenia po optymalizacji prawie nic nie kosztują.
- **Małe, samodzielne programy**, które można skopiować i uruchomić.
-- **Pamięć obsługiwana za ciebie.** Listy i teksty są zwalniane automatycznie na końcu bloku,
- do którego należą, bez garbage collectora.
-- **Bezpośredni dostęp do bibliotek C.** Piszesz `import "stdio.h"` i wołasz, co chcesz,
- nawet funkcje przyjmujące struktury.
+- **Pamięć zarządza się sama.** Listy i teksty są zwalniane automatycznie na końcu bloku, do którego należą,
+ bez udziału garbage collectora.
+- **Bezpośredni dostęp do bibliotek C.** `import "stdio.h"` i możesz wołać wszystko, co tam jest, nawet funkcje
+ przyjmujące struktury.
-Zasady projektu: jeden sposób na każdą rzecz, składnia podobna do C bez jej pułapek
-i komunikaty błędów, które mówią, jak problem naprawić.
+Zasady projektu: na każdą rzecz jeden sposób, składnia jak w C, ale bez jego pułapek, i komunikaty błędów, które mówią,
+jak naprawić problem.
---
## 2. Instalacja
-Kompilator Fincha jest napisany w Finchu (zobacz [przewodnik dla inżynierów](dla-inzynierow.md)). Działa na **Linuksie**
-(x86-64 i ARM64) i buduje programy dla **Linuksa, Windowsa i macOS**. Potrzebuje **clanga 21** i **libclang** (LLVM 21),
-żeby czytać nagłówki C i kompilować to, co wygeneruje.
+Kompilator Fincha jest napisany w Finchu (zob. [przewodnik dla inżynierów](dla-inzynierow.md)). Działa na **Linuksie**
+(x86-64 i ARM64), **macOS** (Apple Silicon i Intel) i **Windowsie** (x86-64) i na każdy z tych systemów buduje programy.
+Wszędzie potrzebuje **clanga** (kompiluje nim każdy program) i **libclang** z LLVM 21 (czyta nim nagłówki C).
-Kompilator nie działa jeszcze na macOS ani Windowsie: potrzebuje ziarna zrobionego z nagłówkami tego systemu
-([bootstrap/README.md](../../bootstrap/README.md) tłumaczy dlaczego). Do tego czasu programy na Windows i macOS buduj z Linuksa
-przez `--target` (niżej) albo użyj starszego kompilatora w C++ z gałęzi `before-bootstrap`.
+### Sposób A: gotowa wersja
-### Opcja A: gotowa wersja (Linux)
+Pobierz archiwum dla swojego systemu z [wydań](https://github.com/Nexoniarz/Finch/releases) i rozpakuj je.
+Katalogi `runtime/` i `std/` muszą leżeć obok pliku `finch` (albo zainstaluj go, zob. niżej).
-Pobierz `finch-linux-x64.tar.gz` albo `finch-linux-arm64.tar.gz` z [wydań](https://github.com/Nexoniarz/Finch/releases)
-i rozpakuj. Zostaw `runtime/` i `std/` obok pliku `finch` (albo zainstaluj, niżej). Potrzebny jest też `clang` 21
-z biblioteką libclang:
+| System | Archiwum | Doinstaluj |
+|---|---|---|
+| Linux x86-64 | `finch-linux-x64.tar.gz` | clang 21 i libclang (tabela niżej) |
+| Linux ARM64 (Raspberry Pi 4/5, Graviton…) | `finch-linux-arm64.tar.gz` | to samo |
+| macOS, Apple Silicon | `finch-macos-arm64.tar.gz` | `xcode-select --install` (clang i nagłówki systemu), `brew install llvm@21` (libclang) |
+| macOS, Intel | `finch-macos-x64.tar.gz` | to samo |
+| Windows x64 | `finch-windows-x64.zip` (`finch.exe` z `libclang.dll`) | [llvm-mingw](https://github.com/mstorsjo/llvm-mingw/releases) (archiwum `ucrt-x86_64`): rozpakuj i dodaj jego katalog `bin` do PATH |
+
+Na Linuksie:
| Dystrybucja | Pakiety |
|---|---|
| Debian / Ubuntu | `clang-21 libclang-21-dev` z [apt.llvm.org](https://apt.llvm.org), `pkg-config` |
| Fedora | `clang clang-devel pkgconf` |
| Arch | `clang pkgconf` |
-| Nix | `nix-shell` w repozytorium daje wszystko |
+| Nix | `nix-shell` w repozytorium daje wszystko naraz |
-### Opcja B: zbuduj sam
+Na Windowsie llvm-mingw daje Finchowi w jednym katalogu `clang`, nagłówki C i linker; zbudowane nim programy nie
+potrzebują do działania niczego więcej. Plik `libclang.dll` trzymaj obok `finch.exe`.
+
+### Sposób B: zbuduj sam
```sh
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
-nix-shell # clang, libclang, pkg-config (albo zainstaluj z dystrybucji)
+nix-shell # Linux: clang, libclang, pkg-config (albo zainstaluj je z dystrybucji)
bootstrap/build.sh # build/finch
./build/finch version
```
-`bootstrap/build.sh` kompiluje ziarno (`bootstrap/.ll`, własny LLVM IR kompilatora) do pierwszego kompilatora,
-buduje nim `src/`, buduje `src/` jeszcze raz wynikiem i sprawdza, że oba są identyczne. Jeśli libclang nie leży tam, gdzie
-szuka linker: `FINCH_LIBCLANG="-L/usr/lib/llvm-21/lib -lclang-21" bootstrap/build.sh`.
+`bootstrap/build.sh` kompiluje plik startowy (`bootstrap/.ll`, własny LLVM IR kompilatora) w pierwszy kompilator,
+buduje nim `src/`, buduje `src/` jeszcze raz otrzymanym kompilatorem i sprawdza, czy oba wyniki są identyczne. Jeśli
+linker nie znajduje libclang, wskaż ją: `FINCH_LIBCLANG="-L/usr/lib/llvm-21/lib -lclang-21" bootstrap/build.sh`.
+
+Na Macu, z LLVM z Homebrew:
+
+```sh
+brew install llvm@21
+export FINCH_CLANG="$(brew --prefix llvm@21)/bin/clang"
+export FINCH_LIBCLANG="-L$(brew --prefix llvm@21)/lib -lclang"
+export FINCH_LLVM_INCLUDE="$(brew --prefix llvm@21)/include"
+bootstrap/build.sh
+```
+
+Na Windowsie, w Git Bash, z llvm-mingw w PATH i oficjalnym LLVM dla Windowsa (dla `libclang.dll` i jej nagłówków)
+rozpakowanym do `C:/llvm`:
+
+```sh
+export FINCH_LIBCLANG="C:/llvm/bin/libclang.dll"
+export FINCH_LLVM_INCLUDE="C:/llvm/include"
+export FINCH_LDFLAGS="-LC:/llvm/bin"
+bootstrap/build.sh # build/finch.exe, z libclang.dll skopiowaną obok
+```
### Budowanie na inny system
-`--target` buduje program na inny system. Do linkowania Finch potrzebuje kompilatora C dla tego systemu:
+`--target` buduje program na inny system. Do linkowania Finch potrzebuje kompilatora C tamtego systemu:
-| `--target` | Buduje | Kompilator C | `finch run` używa |
+| `--target` | Buduje | Używany kompilator C | `finch run` uruchamia przez |
|---|---|---|---|
-| `windows` | `.exe` dla Windowsa | `x86_64-w64-mingw32-gcc` (MinGW-w64) | Wine |
+| `windows` | plik `.exe` dla Windowsa | `x86_64-w64-mingw32-gcc` (MinGW-w64) | Wine |
| `arm64` | program dla Linuksa ARM64 | `aarch64-linux-gnu-gcc` | `qemu-aarch64` |
| `linux` | program dla Linuksa x86-64 | `clang --target=…` | |
-| `macos` | program dla macOS (na Macu: dla drugiego procesora) | `clang --target=…` | |
+| `macos` | program dla macOS (na Macu: dla drugiego rodzaju procesora) | `clang --target=…` | |
| dowolny triple LLVM | np. `aarch64-unknown-linux-gnu` | `clang --target=` | |
```sh
finch build gra.fch --target windows # gra.exe
-finch run gra.fch --target arm64 # buduje na ARM64 i uruchamia w QEMU
+finch run gra.fch --target arm64 # buduje dla ARM64 i uruchamia przez QEMU
FINCH_CC=aarch64-unknown-linux-gnu-gcc finch build narzedzie.fch --target arm64 # inna nazwa kompilatora
```
@@ -131,41 +160,44 @@ bootstrap/install.sh ~/.local # albo do własnego katalogu
finch version
```
-Podczas działania Finch potrzebuje `clanga` (kompiluje IR każdego programu i go linkuje). Mała biblioteka uruchomieniowa
-budowana jest raz i trzymana w `~/.cache/finch`. `FINCH_CC` wskazuje inny kompilator do linkowania; `FINCH_PATH` dodaje
-katalogi, w których szukać modułów.
+Przy pracy Finch potrzebuje `clanga` (kompiluje nim IR każdego programu i linkuje go). Mała biblioteka runtime
+jest budowana raz i trzymana w `~/.cache/finch`. `FINCH_CC` wskazuje inny kompilator do linkowania; `FINCH_PATH`
+dodaje katalogi, w których Finch szuka modułów.
---
## 3. Polecenie finch
```
-finch run [argumenty...] skompiluj i od razu uruchom
-finch build [-o nazwa] skompiluj do programu (domyślna nazwa: nazwa pliku)
-finch test uruchom jego bloki test "..." { } (rozdział 27)
-finch fmt [--check] [pliki/foldery] uporządkuj wcięcia plików .fch (domyślnie wszystkich stąd w dół)
-finch ir pokaż LLVM IR (dla ciekawskich)
-finch lsp serwer języka, dla edytorów
-finch version pokaż wersję
+finch run [argumenty...] kompiluje i od razu uruchamia
+finch build [-o nazwa] kompiluje do programu (domyślna nazwa: nazwa pliku)
+finch test uruchamia jego bloki test "..." { } (rozdział 27)
+finch fmt [--check] [pliki/katalogi] porządkuje wcięcia plików .fch (domyślnie wszystkich w bieżącym katalogu)
+finch ir wypisuje LLVM IR (dla ciekawskich)
+finch lsp serwer języka, dla edytorów
+finch version pokazuje wersję
opcje:
- -l dolinkuj bibliotekę C, to samo co link "lib" w pliku
- -I szukaj nagłówków C także tam
+ -l dolinkuj bibliotekę C, to samo co link "bib" w pliku
+ -I szukaj nagłówków C także tam
--target zbuduj na inny system: windows, linux, arm64, macos, android albo triple LLVM
- -g dodaj informacje dla debuggera (gdb / lldb / Visual Studio)
+ --cpu użyj wszystkich instrukcji procesora: native (tego), x86-64-v3 (AVX2),
+ x86-64-v4 (AVX-512), apple-m1, ... (rozdział 31)
+ -g dodaj informacje dla debuggera (gdb i lldb)
-O0 bez optymalizacji
- --trace błędy w czasie działania pokazują łańcuch wywołań (i tak włączone dla run, test, -g i -O0)
- --no-trace nie pokazują (zwykły finch build go nie ma: pilnowanie łańcucha spowalnia wywołania)
+ --trace błędy w trakcie działania pokazują ścieżkę wywołań (i tak włączone dla run, test, -g i -O0)
+ --no-trace nie pokazują (zwykłe finch build ich nie pokazuje: pamiętanie ścieżki spowalnia wywołania)
```
-W folderze z plikiem `finch.toml` (rozdział 28) `finch run`, `finch build` i `finch test` nie potrzebują nazwy pliku.
+W katalogu z plikiem `finch.toml` (rozdział 28) `finch run`, `finch build` i `finch test` nie potrzebują nazwy pliku.
-Skompilowany program i pliki C, które buduje (`link "x.c"`), zostają w cache (`~/.cache/finch`, na Windowsie
-`%LOCALAPPDATA%`). Drugie budowanie tego samego kodu albo kodu, którego pliki C się nie zmieniły, pomija tę pracę.
+Skompilowany program i zbudowane przez niego pliki C (`link "x.c"`) trafiają do pamięci podręcznej (`~/.cache/finch`,
+na Windowsie `%LOCALAPPDATA%`). Druga kompilacja tego samego kodu, albo kodu, którego pliki C się nie zmieniły, pomija tę pracę.
-`finch fmt` zmienia tylko układ: cztery spacje na poziom `{ }`, bez spacji na końcach linii, najwyżej jedna pusta linia
-z rzędu. Linie, które ciągną wyrażenie (w `( )` albo po `+`), i tekst `"""` zachowują swój kształt. `finch fmt --check`
-niczego nie zmienia i kończy się błędem, gdy jakiś plik by się zmienił (dla CI). Edytory z serwerem języka formatują na żądanie.
+`finch fmt` zmienia tylko układ: cztery spacje na każdy poziom `{ }`, żadnych spacji na końcach linii, najwyżej jedna
+pusta linia z rzędu. Linie kontynuujące wyrażenie (w nawiasach `( )` albo po `+`) i tekst `"""` zachowują swój kształt.
+`finch fmt --check` niczego nie zmienia i kończy się błędem, jeśli jakiś plik by się zmienił (przydatne w CI).
+Edytory z serwerem języka formatują na żądanie.
Przykłady:
@@ -173,10 +205,10 @@ Przykłady:
finch run hello.fch # wypisuje wynik
finch run narzedzie.fch dane.txt -v # argumenty po nazwie pliku trafiają do programu
finch build gra.fch -o mojagra # tworzy ./mojagra
-finch build gra.fch -g -O0 # wersja dla debuggera
+finch build gra.fch -g -O0 # wersja do debuggera
```
-`finch run` przekazuje dalej kod wyjścia programu. Jeśli program się wysypie, powie jak
+`finch run` przekazuje kod wyjścia twojego programu. Jeśli program się wysypie, mówi jak
(np. `the program crashed: Segmentation fault`).
---
@@ -186,13 +218,13 @@ finch build gra.fch -g -O0 # wersja dla debuggera
```c
// komentarz zaczyna się od dwóch ukośników
/* albo zajmuje
- kilka linijek */
+ kilka linii */
-import "math.h" // nagłówki C (opcjonalne), na górze
-import ksztalty // moduły Fincha (opcjonalne): ksztalty.fch obok tego pliku
-link "m" // biblioteki albo pliki C do dolinkowania (opcjonalne)
+import "math.h" // nagłówki C (opcjonalnie), na początku pliku
+import ksztalty // moduły Fincha (opcjonalnie): ksztalty.fch obok tego pliku
+link "m" // biblioteki albo pliki C do dolinkowania (opcjonalnie)
-struct Punkt { // własne typy
+struct Punkt { // twoje własne typy
int x
int y
}
@@ -204,32 +236,32 @@ fn main() { // tu zaczyna się program
Zasady, które warto znać:
-- **Jedna instrukcja na linijkę.** Średników nie ma. Długie wyrażenie możesz złamać **po** operatorze
- (`a +⏎ b`) albo w dowolnym miejscu wewnątrz nawiasów `( )` lub `[ ]`.
-- **Klamry są zawsze obowiązkowe** po `if`, `else`, `for`, `while`, a otwierająca `{` stoi w tej samej linijce.
-- **Bez nawiasów wokół warunków:** `if x > 5 {`, a nie `if (x > 5) {`.
-- Funkcje i struktury mogą być w pliku w dowolnej kolejności.
+- **Jedna instrukcja w linii.** Nie ma średników. Długie wyrażenie można złamać **po** operatorze (`a +⏎ b`)
+ albo w dowolnym miejscu w nawiasach `( )` lub `[ ]`.
+- **Klamry są zawsze obowiązkowe** po `if`, `else`, `for`, `while`, a otwierająca `{` stoi w tej samej linii.
+- **Warunki bez nawiasów:** `if x > 5 {`, a nie `if (x > 5) {`.
+- Funkcje i struktury mogą stać w pliku w dowolnej kolejności.
- `main` to `fn main()`, `fn main() -> int` (zwraca kod wyjścia) albo `fn main([]str args)`
- (dostaje argumenty z linii poleceń; `args[0]` to ścieżka samego programu).
+ (dostaje argumenty z wiersza poleceń; `args[0]` to ścieżka samego programu).
---
## 5. Zmienne
```c
-x := 5 // nowa zmienna, typ zgadnięty (int)
-int y = 10 // nowa zmienna, typ podany jawnie
-int z // bez wartości: startuje od zera ("" dla str, [] dla tablic, null dla wskaźników)
+x := 5 // nowa zmienna, typ wynika z wartości (int)
+int y = 10 // nowa zmienna z podanym typem
+int z // bez wartości: zero ("" dla str, [] dla tablic, null dla wskaźników)
Punkt p // struktura bez wartości: zera albo wartości domyślne struktury
x = 7 // przypisanie
x += 1 // także -= *= /= %=
```
-- Nazwę można zadeklarować **raz** w funkcji. Finch nie pozwala użyć jej ponownie w bloku
- wewnętrznym (nie ma „przesłaniania”), bo to częste źródło pomyłek.
-- Zmienna istnieje od deklaracji do końca bloku `{ }`, w którym powstała.
-- Nazwy: litery ASCII, cyfry i `_`, bez cyfry na początku. Polskie litery tylko w tekstach w cudzysłowie.
+- Nazwę można zadeklarować w funkcji **tylko raz**. Finch nie pozwala użyć tej samej nazwy w bloku wewnętrznym
+ (nie ma „przesłaniania”), bo to częste źródło pomyłek.
+- Zmienna istnieje od deklaracji do końca bloku `{ }`, w którym stoi.
+- Nazwy: litery ASCII, cyfry i `_`, nie mogą zaczynać się od cyfry. Polskie litery wolno pisać tylko w tekstach.
Nazwy typów (`int`, `u8`, …), funkcji wbudowanych i słowa kluczowe są zarezerwowane.
**Słowa kluczowe:** `fn return if else while for in break continue true false null import link struct enum match defer or try const export`
@@ -243,8 +275,8 @@ const f32 GRAWITACJA = 9.8 // z typem
const TYTUL = "Moja gra"
```
-Stałą piszesz na najwyższym poziomie pliku, obok funkcji, i nie da się jej zmienić. Składa się z liczb, tekstu,
-`true`/`false`, znaków, innych stałych i operatorów; stała z modułu to `ksztalty.BOKI`.
+Stałą zapisuje się na najwyższym poziomie pliku, obok funkcji, i nie da się jej zmienić. Składa się z liczb, tekstów,
+`true`/`false`, znaków, innych stałych i operatorów; stała z modułu to np. `ksztalty.BOKI`.
---
@@ -254,18 +286,18 @@ Stałą piszesz na najwyższym poziomie pliku, obok funkcji, i nie da się jej z
| Typ | Przechowuje | Przykład |
|---------|-------------|----------|
-| `int` | liczba całkowita, 64-bitowa (to samo co `i64`) | `42`, `-7`, `0xFF`, `1_000_000` |
-| `float` | liczba z przecinkiem, 64-bitowa (to samo co `f64`) | `3.14` |
+| `int` | liczbę całkowitą, 64 bity (to samo co `i64`) | `42`, `-7`, `0xFF`, `1_000_000` |
+| `float` | liczbę zmiennoprzecinkową, 64 bity (to samo co `f64`) | `3.14` |
| `bool` | `true` albo `false` | |
| `char` | jeden znak wielkości bajtu | `'A'`, `'\n'` |
| `str` | tekst | `"cześć\tświecie"` |
-| `[]T` | lista (tablica) elementów `T` | `[1, 2, 3]` |
-| `map[K]V` | wartości typu `V` znajdowane po kluczu typu `K` | `["a": 1, "b": 2]` |
+| `[]T` | listę (tablicę) wartości typu `T` | `[1, 2, 3]` |
+| `map[K]V` | wartości typu `V` wyszukiwane po kluczu typu `K` | `["a": 1, "b": 2]` |
| twoje struktury | nazwane grupy wartości | `Punkt(1, 2)` |
-Znaki specjalne w tekstach: `\n` nowa linia, `\t` tabulator, `\r`, `\0`, `\\`, `\"`, `\'`.
+Znaki specjalne w tekstach i znakach: `\n` nowa linia, `\t` tabulator, `\r`, `\0`, `\\`, `\"`, `\'`.
-### Typy z rozmiarem (do plików, sprzętu, bibliotek C)
+### Typy o podanym rozmiarze (do plików, sprzętu, bibliotek C)
| Ze znakiem | Zakres | Bez znaku | Zakres |
|---|---|---|---|
@@ -274,7 +306,9 @@ Znaki specjalne w tekstach: `\n` nowa linia, `\t` tabulator, `\r`, `\0`, `\\`, `
| `i32` | ok. ±2,1 mld | `u32` | 0 … ok. 4,29 mld |
| `i64` | ok. ±9,2 × 10¹⁸ | `u64` | 0 … ok. 1,8 × 10¹⁹ |
-`f32` to 32-bitowa liczba z przecinkiem (ok. 7 cyfr znaczących), `f64` to 64-bitowa (ok. 15–16).
+`f32` to 32-bitowa liczba zmiennoprzecinkowa (ok. 7 cyfr znaczących), `f64` to 64-bitowa (ok. 15–16).
+
+Są też **wektory**, czyli kilka liczb obok siebie liczonych naraz: `f32x8`, `i8x32` i podobne (rozdział 31).
### Zamiana typów
@@ -282,27 +316,27 @@ Finch zamienia typy sam **tylko wtedy, gdy nic nie może zginąć**:
| Z | Na | Automatycznie? |
|---|---|---|
-| mniejsza liczba całkowita | większa o tej samej znakowości (`i8`→`i32`, `u8`→`u64`) | ✅ |
-| bez znaku | ściśle większa ze znakiem (`u8`→`i16`, `u32`→`i64`) | ✅ |
+| mniejsza liczba całkowita | większa o tym samym znaku (`i8`→`i32`, `u8`→`u64`) | ✅ |
+| bez znaku | większa ze znakiem (`u8`→`i16`, `u32`→`i64`) | ✅ |
| dowolna liczba całkowita | `float` / `f32` | ✅ |
| `f32` | `f64` | ✅ |
| `null` | dowolny wskaźnik | ✅ |
-| dowolny wskaźnik | `ptr`, a `ptr` na dowolny wskaźnik | ✅ |
-| wszystko inne | | ❌ napisz to jawnie: `typ(wartość)` |
+| dowolny wskaźnik | `ptr` i z `ptr` na dowolny wskaźnik | ✅ |
+| wszystko inne | | ❌ trzeba napisać: `typ(wartosc)` |
-**Liczby wpisane w kod się dopasowują.** Liczba napisana wprost w kodzie pasuje do każdego typu,
-który ją pomieści: `u8 b = 200` działa, a `u8 b = 300` to błąd. To samo dotyczy stałych z nagłówków C.
+**Liczby wpisane w kod dopasowują się.** Liczba zapisana w kodzie pasuje do każdego typu, który ją pomieści:
+`u8 b = 200` działa, `u8 b = 300` to błąd. Tak samo jest ze stałymi z nagłówków C.
-**Jawna zamiana** wygląda jak wywołanie funkcji o nazwie typu:
+**Jawne zamiany** zapisuje się nazwą typu, jak wywołanie funkcji:
```c
-int(3.99) // 3 (obcina część ułamkową)
+int(3.99) // 3 (ucina część ułamkową)
float(7) / 2 // 3.5
-u8(300) // 44 (zostawia najniższe 8 bitów)
+u8(300) // 44 (zostaje 8 najniższych bitów)
int('A') // 65
char(66) // 'B'
bool(0) // false (każda liczba różna od zera to true)
-str(42) // "42" (też float, bool, char; str([]u8) zamienia bajty na tekst)
+str(42) // "42" (także liczby zmiennoprzecinkowe, bool i znaki; str([]u8) zamienia bajty na tekst)
int("42") // 42 (zatrzymuje program, jeśli tekst nie jest liczbą)
float("2.5") // 2.5
ptr(p) // wskaźnik z typem jako wskaźnik bez typu
@@ -311,8 +345,8 @@ int(p) // wskaźnik jako liczba (jego adres)
str(p) // wskaźnik na znaki z C jako tekst
```
-**Liczby o stałym rozmiarze się „zawijają”**, jak w prawdziwym sprzęcie: `u8` równe 255 plus 1 daje 0.
-Działania liczą się w typie użytych wartości, więc `u8(250) + 10` daje `4`, a nie `260`.
+**Liczby o stałym rozmiarze się zawijają**, jak w prawdziwym sprzęcie: `u8` równe 255 plus 1 daje 0.
+Obliczenia odbywają się w typie użytych wartości, więc `u8(250) + 10` to `4`, a nie `260`.
---
@@ -322,13 +356,13 @@ Działania liczą się w typie użytych wartości, więc `u8(250) + 10` daje `4`
| Priorytet | Operatory | Znaczenie |
|---|---|---|
-| 5 | `*` `/` `%` `<<` `>>` `&` | mnożenie, dzielenie, reszta, przesunięcia bitów, bitowe AND |
-| 4 | `+` `-` `\|` `^` | dodawanie (i sklejanie tekstu), odejmowanie, bitowe OR, bitowe XOR |
+| 5 | `*` `/` `%` `<<` `>>` `&` | mnożenie, dzielenie, reszta, przesunięcie w lewo/prawo, bitowe AND |
+| 4 | `+` `-` `\|` `^` | dodawanie (i łączenie tekstów), odejmowanie, bitowe OR, bitowe XOR |
| 3 | `==` `!=` `<` `<=` `>` `>=` | porównania |
-| 2 | `&&` | logiczne I |
-| 1 | `\|\|` | logiczne LUB |
+| 2 | `&&` | logiczne AND |
+| 1 | `\|\|` | logiczne OR |
-Jednoargumentowe (przed wartością): `-x` zmiana znaku, `!x` logiczne NIE, `~x` odwrócenie wszystkich bitów.
+Jednoargumentowe (przed wartością): `-x` zmiana znaku, `!x` logiczne NOT, `~x` odwrócenie wszystkich bitów.
> **Celowo inaczej niż w C:** operatory bitowe wiążą mocniej niż porównania, więc
> `flagi & 4 != 0` znaczy `(flagi & 4) != 0`.
@@ -336,20 +370,20 @@ Jednoargumentowe (przed wartością): `-x` zmiana znaku, `!x` logiczne NIE, `~x`
Uwagi:
- `/` na liczbach całkowitych zaokrągla w stronę zera: `7 / 2` to `3`. `%` ma znak lewej strony.
-- Dzielenie przez zero **zatrzymuje program** komunikatem `runtime error: division by zero` z numerem linijki.
-- `&&` i `||` liczą prawą stronę tylko wtedy, gdy to potrzebne.
-- Tekst: `+` skleja, `==` `!=` porównują treść, `<` `>` porównują alfabetycznie (bajt po bajcie).
-- Wskaźniki porównuje się przez `==` i `!=`, także z `null`. `str` nigdy nie jest `null`.
-- Tablic i struktur nie porównasz przez `==`; porównuj ich części.
+- Dzielenie przez zero **zatrzymuje program** komunikatem `runtime error: division by zero` z numerem linii.
+- `&&` i `||` obliczają prawą stronę tylko wtedy, gdy trzeba.
+- Teksty: `+` łączy, `==` `!=` porównują zawartość, `<` `>` porównują alfabetycznie (bajt po bajcie).
+- Wskaźniki można porównywać przez `==` i `!=`, także z `null`. `str` nigdy nie jest `null`.
+- Tablic i struktur nie da się porównać przez `==`; porównaj ich części.
---
-## 8. Sterowanie przebiegiem
+## 8. Sterowanie
-`if` może też dawać wartość; każda gałąź to jedna wartość, a `else` jest wymagane:
+`if` może też dawać wartość; każda gałąź zawiera jedną wartość, a `else` jest obowiązkowe:
```c
-opis := if wynik > 100 { "dużo" } else if wynik > 10 { "średnio" } else { "mało" }
+ocena := if wynik > 100 { "wysoki" } else if wynik > 10 { "średni" } else { "niski" }
f32 predkosc = if biegnie { 2.5 } else { 1.0 }
```
@@ -366,12 +400,12 @@ while licznik > 0 {
licznik -= 1
}
-for i in 0..10 { // i = 0, 1, …, 9 (koniec nie jest wliczany)
+for i in 0..10 { // i = 0, 1, …, 9 (koniec nie wchodzi)
if i == 3 { continue }
if i == 8 { break }
}
-for imie in imiona { // każdy element tablicy (albo każdy znak str)
+for imie in imiona { // każdy element tablicy (albo każdy znak tekstu)
print(imie)
}
@@ -379,31 +413,30 @@ for i, imie in imiona { // razem z indeksem: 0, 1, 2, ...
print(i, imie)
}
-for klucz, wartosc in wiek { // każdy wpis mapy, w kolejności dodawania kluczy
- print(klucz, wartosc) // (for klucz in wiek: same klucze)
+for klucz, wartosc in wiek { // każdy wpis mapy, w kolejności dodawania kluczy
+ print(klucz, wartosc) // (for klucz in wiek: same klucze)
}
```
- Warunek musi być typu `bool`. `if x {` z liczbą to błąd; napisz `if x != 0 {`.
-- W `for i in a..b` oba końce są liczone **raz**. `i` jest typu `int` i nie można go zmieniać w pętli.
-- W `for x in lista` zmiennej `x` też nie zmienisz. Żeby zmieniać elementy, przechodź po indeksach:
- `for i in 0..lista.len { lista[i] = ... }`. Dodawanie do listy w trakcie pętli jest dozwolone.
+- W `for i in a..b` oba końce są obliczane **raz**. `i` jest typu `int` i nie można go zmieniać w pętli.
+- W `for x in lista` też nie można zmieniać `x`. Żeby zmieniać elementy, idź po indeksach:
+ `for i in 0..lista.len { lista[i] = ... }`. Dodawanie do listy w trakcie pętli jest w porządku.
- `while true { ... }` kręci się do `break` albo `return`.
-- Kod po `return`, `break` lub `continue` w tym samym bloku to błąd, bo nigdy się nie wykona.
+- Kod po `return`, `break` czy `continue` w tym samym bloku to błąd, bo nigdy się nie wykona.
### defer
-`defer` uruchamia instrukcję, gdy bieżący blok się kończy, niezależnie od tego, jak się kończy:
-normalnie, przez `return`, `break` lub `continue`. Kilka `defer` wykonuje się w odwrotnej kolejności.
-Przydaje się do sprzątania:
+`defer` wykonuje instrukcję, gdy kończy się bieżący blok, niezależnie od tego, jak się kończy: normalnie, przez
+`return`, `break` albo `continue`. Kilka `defer` wykonuje się w odwrotnej kolejności. Przydaje się do sprzątania:
```c
f := fopen("dane.txt", "r")
defer fclose(f)
-// ... używaj f; zostanie zamknięty na końcu bloku, na każdej ścieżce
+// ... używaj f; zostanie zamknięty na końcu bloku, którąkolwiek drogą program z niego wyjdzie
```
-Instrukcja w `defer` nie może zawierać `return`, `break` ani `continue`.
+Odroczona instrukcja nie może zawierać `return`, `break` ani `continue`.
---
@@ -415,19 +448,19 @@ fn nazwa(typ1 param1, typ2 param2) -> typ_wyniku {
return wartosc
}
-fn bez_wyniku(str wiadomosc) { // bez "->": nic nie zwraca
- print(wiadomosc)
+fn bez_wyniku(str komunikat) { // bez "->": niczego nie zwraca
+ print(komunikat)
}
```
-- Argumenty zachowują się jak kopie: zmiana parametru nigdy nie zmienia zmiennej u wywołującego.
+- Argumenty zachowują się jak kopie: zmiana parametru nigdy nie zmienia zmiennej wywołującego.
(Finch kopiuje naprawdę tylko wtedy, gdy trzeba: funkcja, która nie zmienia tablicy, dostaje ją za darmo.)
- Żeby zmienić zmienną wywołującego, przekaż wskaźnik, zobacz [Wskaźniki](#14-wskaźniki).
-- Funkcja z `-> typ` musi zwrócić wartość na każdej ścieżce. Finch to sprawdza.
-- Funkcji nie można definiować wewnątrz innej funkcji.
-- Funkcję, która może się nie udać, zapisuje się `-> int!` (albo `-> !` bez wyniku); zobacz
+ Żeby zmienić zmienną wywołującego, przekaż wskaźnik: zob. [Wskaźniki](#14-wskaźniki).
+- Funkcja z `-> typ` musi zwracać wartość na każdej ścieżce. Finch to sprawdza.
+- Nie można zdefiniować funkcji wewnątrz innej funkcji.
+- Funkcję, która może się nie udać, zapisuje się `-> int!` (albo `-> !` bez wyniku); zob.
[Błędy jako wartości](#błędy-jako-wartości).
-- Długie wywołania możesz rozbić na kilka linijek wewnątrz nawiasów; przecinek po ostatnim argumencie jest dozwolony.
+- Długie wywołania można rozbić na kilka linii w nawiasach; przecinek po ostatnim argumencie jest dozwolony.
---
@@ -440,20 +473,20 @@ siatka := [[1, 2], [3, 4]] // tablice tablic
[]f32 xs = [1, 2.5] // typ decyduje, czym staną się liczby
```
-| Operacja | Znaczenie |
+| Działanie | Znaczenie |
|---|---|
| `a.len` | liczba elementów |
-| `a[i]` | element `i` (od 0); poza zakresem program zatrzymuje się z czytelnym komunikatem |
+| `a[i]` | element `i` (licząc od 0); indeks poza zakresem zatrzymuje program jasnym komunikatem |
| `a[i] = x` | zmiana elementu |
-| `a.push(x)` | dodaj na końcu |
-| `a.pop()` | zabierz ostatni i go oddaj |
-| `a.insert(i, x)` | wstaw `x` na pozycję `i`, przesuwając resztę |
-| `a.remove(i)` | usuń pozycję `i` i ją oddaj |
-| `a.clear()` | usuń wszystko |
-| `a.resize(n)` | zmień długość na `n` (nowe elementy to zera) |
-| `a.contains(x)`, `a.find(x)` | czy `x` jest w środku? jego indeks albo -1 (liczby, znaki, bool, str) |
+| `a.push(x)` | dodanie na końcu |
+| `a.pop()` | zdjęcie ostatniego elementu i zwrócenie go |
+| `a.insert(i, x)` | wstawienie `x` na pozycję `i` z przesunięciem reszty |
+| `a.remove(i)` | usunięcie pozycji `i` i zwrócenie jej |
+| `a.clear()` | usunięcie wszystkiego |
+| `a.resize(n)` | zmiana długości na `n` (nowe elementy to zera) |
+| `a.contains(x)`, `a.find(x)` | czy `x` tam jest? jego indeks albo -1 (liczby, znaki, bool, str) |
| `a.sort()`, `a.reverse()` | w miejscu (liczby, znaki, str) |
-| `a.slice(s, e)` | nowa tablica z elementami `s` … `e-1` |
+| `a.slice(p, k)` | nowa tablica z elementami `p` … `k-1` |
| `a.join(sep)` | dla `[]str`: jeden tekst z `sep` między częściami |
| `a.ptr` | adres pierwszego elementu (dla C) |
| `print(a)` | wypisuje `[1, 2, 3]` |
@@ -462,24 +495,23 @@ siatka := [[1, 2], [3, 4]] // tablice tablic
### Mapy
-`map[K]V` znajduje wartości po kluczu. Kluczem może być liczba całkowita, `char`, `bool` albo `str`;
-wartością cokolwiek (tablice, struktury, inne mapy). Przechodzenie pętlą idzie w kolejności, w jakiej
-klucze zostały dodane po raz pierwszy.
+`map[K]V` znajduje wartości po kluczu. Kluczami mogą być liczby całkowite, `char`, `bool` albo `str`; wartościami
+cokolwiek (tablice, struktury, inne mapy). Przechodzenie po mapie idzie w kolejności, w jakiej klucze dodano po raz pierwszy.
```c
-wiek := ["anna": 31, "bob": 25] // literał mapy: map[str]int
+wiek := ["anna": 31, "bob": 25] // mapa zapisana wprost: map[str]int
map[str][]str grupy // pusta mapa
map[int]str nazwy = [:] // [:] to pusta mapa tam, gdzie typ jest znany
```
-| Operacja | Znaczenie |
+| Działanie | Znaczenie |
|---|---|
| `m[k]` | wartość dla `k`; brak klucza zatrzymuje program komunikatem `the key "k" is not in the map` |
-| `m[k] = v` | dodaj albo zastąp |
-| `m[k] += 1`, `m[k].push(x)`, `m[k].pole = …` | zmiana brakującego klucza najpierw go dodaje, z wartością domyślną typu (0, `""`, `[]`, …) |
+| `m[k] = v` | dodanie albo podmiana |
+| `m[k] += 1`, `m[k].push(x)`, `m[k].pole = …` | zmiana nieistniejącego klucza najpierw go dodaje, z wartością domyślną typu (0, `""`, `[]`, …) |
| `m.len` | liczba kluczy |
-| `m.has(k)` | czy `k` jest w mapie? |
-| `m.get(k, domyslna)` | wartość albo `domyslna` (liczona tylko w razie potrzeby) |
+| `m.has(k)` | czy `k` tam jest? |
+| `m.get(k, domyslna)` | wartość albo `domyslna` (obliczana tylko wtedy, gdy trzeba) |
| `m.remove(k)` | usuwa `k`; `true`, jeśli był |
| `m.clear()` | usuwa wszystko |
| `m.keys()`, `m.values()` | nowe tablice, w kolejności dodawania |
@@ -495,9 +527,9 @@ for slowo, n in ile {
}
```
-Wyszukiwanie używa haszowania (średnio stały czas). Tak jak tablice, mapy są właścicielami kluczy
-i wartości, kopiują się przy przypisaniu i zwalniają na końcu bloku. Nie dodawaj ani nie usuwaj kluczy
-mapy wewnątrz `for` po tej samej mapie; zbierz je do tablicy i zmień mapę po pętli.
+Wyszukiwanie korzysta z haszowania (średnio stały czas). Tak jak tablice, mapy są właścicielami swoich kluczy
+i wartości, kopiują się przy przypisaniu i zwalniają na końcu bloku. Nie dodawaj ani nie usuwaj kluczy mapy wewnątrz
+`for` po tej samej mapie; zbierz je w tablicy i zmień mapę po pętli.
---
@@ -505,37 +537,37 @@ mapy wewnątrz `for` po tej samej mapie; zbierz je do tablicy i zmień mapę po
```c
s := "Cześć"
-s += ", świecie" // sklejanie
-print(s.len, s[0]) // .len liczy bajty; ą, ś, ż… zajmują po 2
-s[0] = 'D' // zmiana znaku
+s += ", świecie" // łączenie
+print(s.len, s[0]) // 17 C (.len liczy bajty; ą, ś, ż, … zajmują po 2)
+s[0] = 'K' // zmiana znaku
```
-| Metoda | Oddaje |
+| Metoda | Zwraca |
|---|---|
| `s.sub(a, b)` | fragment od `a` do `b-1` |
| `s.find(t)` | gdzie zaczyna się `t`, albo -1 |
| `s.contains(t)`, `s.starts_with(t)`, `s.ends_with(t)` | `bool` |
| `s.split(sep)` | `[]str`; `split("")` daje pojedyncze znaki |
-| `s.trim()` | bez spacji i nowych linii na obu końcach |
-| `s.upper()`, `s.lower()` | zmienione litery A–Z / a–z (tylko ASCII) |
+| `s.trim()` | bez spacji i znaków nowej linii na obu końcach |
+| `s.upper()`, `s.lower()` | zamienione A–Z / a–z (litery ASCII) |
| `s.replace(a, b)` | każde `a` zamienione na `b` |
-| `s.repeat(n)` | `s` n razy |
+| `s.repeat(n)` | `s` powtórzone n razy |
| `s.bytes()` | `[]u8` z bajtami |
-| `s.ptr` | `ptr[char]` dla C (ważny, dopóki żyje `s`) |
+| `s.ptr` | `ptr[char]` dla C (ważny, dopóki `s` istnieje) |
-Wartości wstawia się do tekstu przez `\(...)`: dowolny kod, a `, 2` po liczbie daje dwie cyfry po przecinku.
-`\` przed `(`, które ma zostać zwykłym znakiem: `"\\(to nie kod)"`.
+Wartości wstawia się w tekst przez `\(...)`: dowolny kod, a po liczbie `, 2` dla dwóch cyfr po przecinku.
+Jeśli `(` ma zostać zwykłym znakiem, poprzedź `\` drugim ukośnikiem: `"\\(to nie kod)"`.
```c
print("hp \(hp) z \(max_hp), prędkość \(v, 2), imię \(p.imie.upper())")
```
-`str` pamięta swoją długość, więc `.len` działa natychmiast. Zawsze kończy się zerem, więc
-przekazanie go do C jako `char*` działa. W drugą stronę: funkcja C, która zwraca `char *`, daje ci `str`
-(kopia powstaje, gdy go zachowujesz), a `str(p)` czyta `ptr[char]` zdobyty inaczej.
+`str` pamięta swoją długość, więc `.len` jest natychmiastowe, a na końcu zawsze ma bajt zerowy, więc można go przekazać
+do C jako `char*`. W drugą stronę: funkcja C, która zwraca `char *`, daje ci `str` (kopia powstaje, gdy go zatrzymujesz),
+a `str(p)` czyta `ptr[char]` zdobyty w inny sposób.
-Tekst w kilku liniach piszesz między `"""`. Znak nowej linii zaraz po otwarciu znika, a z każdej linii znika
-wcięcie zamykających cudzysłowów, więc tekst może być wcięty razem z kodem:
+Tekst na kilka linii zapisuje się między `"""`. Znak nowej linii po otwierającym cudzysłowie znika, a z każdej linii
+znika też wcięcie zamykającego cudzysłowu, więc tekst może mieć wcięcie razem z kodem:
```c
shader := """
@@ -544,8 +576,8 @@ shader := """
"""
```
-Liczby jako tekst: `str(x)` to najkrótszy tekst, który czyta się z powrotem jako ta sama liczba (`0.1`,
-`0.30000000000000004`, `1e+20`); `str(x, 2)` ma zawsze dwie cyfry po kropce (`3.14`).
+Liczby jako tekst: `str(x)` to najkrótszy zapis, który da z powrotem tę samą liczbę (`0.1`, `0.30000000000000004`,
+`1e+20`); `str(x, 2)` ma zawsze dwie cyfry po przecinku (`3.14`).
---
@@ -556,27 +588,27 @@ struct Gracz {
str imie
int zycia = 3 // wartość domyślna
[]int wyniki
- Punkt pozycja // struktury w strukturach
+ Punkt poz // struktury w strukturach
}
-g := Gracz(imie: "Ola") // po nazwie: brakujące pola dostają domyślne (albo zero)
+p := Gracz(imie: "Ola") // przez nazwy: brakujące pola dostają wartość domyślną (albo zero)
q := Punkt(1, 2) // po kolei: wtedy trzeba podać każde pole
-g.zycia -= 1
-g.wyniki.push(10)
-g.pozycja.x = 5
-print(g) // Gracz(imie: "Ola", zycia: 2, wyniki: [10], pozycja: Punkt(x: 5, y: 0))
+p.zycia -= 1
+p.wyniki.push(10)
+p.poz.x = 5
+print(p) // Gracz(imie: "Ola", zycia: 2, wyniki: [10], poz: Punkt(x: 5, y: 0))
```
-- Przypisanie albo przekazanie struktury ją kopiuje, razem z jej tablicami i tekstami.
-- Struktura nie może zawierać samej siebie wprost; dla takiego pola użyj `ptr[Wezel]` albo `[]Wezel` (drzewa, listy).
+- Przypisanie albo przekazanie struktury kopiuje ją, razem z jej tablicami i tekstami.
+- Struktura nie może bezpośrednio zawierać samej siebie; do tego służy pole `ptr[Wezel]` albo `[]Wezel` (drzewa, listy).
### Metody
-Metoda to funkcja należąca do struktury: `fn Struktura.nazwa(...)`. W środku `self` to struktura,
-na której ją wywołano, i to jest sama wartość wywołującego, nie kopia: zmiany w `self` zostają.
+Metoda to funkcja należąca do struktury: `fn Struktura.nazwa(...)`. W środku `self` to struktura, na której ją
+wywołano, i to jest sama wartość wywołującego, a nie kopia: zmiany w `self` zostają.
```c
-fn Gracz.wylecz(int ile) {
+fn Gracz.ulecz(int ile) {
self.zycia += ile
}
@@ -584,41 +616,41 @@ fn Gracz.zyje() -> bool {
return self.zycia > 0
}
-g := Gracz(imie: "Ola")
-g.wylecz(2) // g.zycia wynosi teraz 5
-if g.zyje() { ... }
+p := Gracz(imie: "Ola")
+p.ulecz(2) // p.zycia wynosi teraz 5
+if p.zyje() { ... }
-druzyna[0].wylecz(1) // na elemencie tablicy, polu, m[klucz], przez ptr[Gracz] ...
+druzyna[0].ulecz(1) // na elemencie tablicy, polu, m[klucz], przez ptr[Gracz] ...
```
-- Metoda jest w tym samym pliku (module) co jej struktura i jest dostępna wszędzie, gdzie ta struktura.
-- Metody można też dopisywać do **struktur z nagłówków C**: `fn Vector2.length() -> f32 { ... }`.
+- Metoda stoi w tym samym pliku (module) co jej struktura i jest dostępna wszędzie tam, gdzie ta struktura.
+- Metody można dodawać też do **struktur z nagłówków C**: `fn Vector2.length() -> f32 { ... }`.
- Metoda nie może nazywać się tak jak pole.
-- Wywołanie metody zmieniającej `self` na zmiennej pętli (`for g in druzyna { g.wylecz(1) }`) to błąd,
- bo zmienna pętli jest tylko do odczytu; użyj `for i in 0..druzyna.len { druzyna[i].wylecz(1) }`.
-- Tak jak funkcje, metody mogą się nie udać (`-> !`); zobacz [Błędy jako wartości](#błędy-jako-wartości).
+- Wywołanie metody zmieniającej `self` na zmiennej pętli (`for p in druzyna { p.ulecz(1) }`) to błąd, bo zmienna pętli
+ jest tylko do odczytu; użyj `for i in 0..druzyna.len { druzyna[i].ulecz(1) }`.
+- Metody, tak jak funkcje, mogą się nie udać (`-> !`); zob. [Błędy jako wartości](#błędy-jako-wartości).
---
## 13. Pamięć: kto co zwalnia
-Tablic, tekstów i struktur nie zwalniasz nigdy. Finch robi to za ciebie, w przewidywalnym momencie:
-**gdy kończy się blok, do którego wartość należy** (`}`), albo wcześniej przy `return`, `break`, `continue`.
+Tablic, tekstów i struktur nigdy nie zwalniasz. Finch robi to za ciebie, w przewidywalnym momencie:
+**gdy kończy się blok, do którego wartość należy** (`}`), albo wcześniej, przy `return`, `break`, `continue`.
-- Zmienna **jest właścicielem** swojej wartości. Przypisanie kopiuje, więc dwie zmienne nigdy nie dzielą jednej listy.
-- Wartość z wywołania funkcji albo wyrażenia jest **przenoszona** tam, gdzie ją zapisujesz, bez kopiowania.
-- `return lista` przenosi zmienną poza funkcję, bez kopiowania.
-- Parametry funkcji są **pożyczane**: funkcja dostaje wartość wywołującego bez kopii. Jeśli ją zmienia,
- Finch najpierw robi jej prywatną kopię. Zauważysz to najwyżej po szybkości.
+- Zmienna jest **właścicielem** swojej wartości. Przypisanie ją kopiuje, więc dwie zmienne nigdy nie dzielą jednej listy.
+- Wartość z wywołania funkcji albo z wyrażenia jest **przenoszona** tam, gdzie trafia: bez kopii.
+- `return lista` przenosi zmienną na zewnątrz funkcji: bez kopii.
+- Parametry funkcji są **pożyczane**: funkcja dostaje wartość wywołującego bez kopii. Jeśli funkcja zmienia parametr,
+ Finch najpierw robi jej prywatną kopię. Zauważysz to najwyżej w szybkości.
-Do pamięci zarządzanej samodzielnie (listy wiązane, drzewa, dzielenie między strukturami):
+Dla pamięci, którą zarządzasz sam (listy wiązane, drzewa, współdzielenie między strukturami):
```c
-wezel := new(Wezel(wartosc: 1)) // wstaw wartość na stertę, dostajesz ptr[Wezel]
-defer free(wezel) // free(...) ją oddaje (razem z jej tablicami i tekstami)
+wezel := new(Wezel(wartosc: 1)) // wartość na stercie, dostajesz ptr[Wezel]
+defer free(wezel) // free(...) oddaje pamięć (i zwalnia tablice i teksty w środku)
```
-Pamięć z C (`malloc`, obiekty bibliotek) zwalniasz funkcjami z C, jak w C.
+Pamięć z C (`malloc`, obiekty bibliotek) zwalnia się funkcjami C, tak jak w C.
---
@@ -632,8 +664,8 @@ Wskaźnik przechowuje **adres** wartości. Finch zamiast `*` i `&` z C używa s
| `&x` | `addr(x)` | adres x (także `addr(a[i])`, `addr(s.pole)`) |
| `*p` | `p.value` | wartość, na którą wskazuje p |
| `p->pole` | `p.pole` | pole struktury, na którą wskazuje p |
-| `p[i]` | `p[i]` | indeksowanie jak w C (bez kontroli zakresu) |
-| `NULL` | `null` | wskazuje donikąd |
+| `p[i]` | `p[i]` | indeksowanie jak w C (bez sprawdzania zakresu) |
+| `NULL` | `null` | nigdzie nie wskazuje |
| `void *` | `ptr` | wskazuje na coś nieznanego typu |
```c
@@ -642,14 +674,14 @@ fn podwoj(ptr[int] p) {
}
x := 21
-podwoj(addr(x)) // x to 42
+podwoj(addr(x)) // x wynosi 42
```
Jeśli struktura ma własne pole o nazwie `value`, to `p.value` na wskaźniku do niej oznacza to pole.
-Użycie `.value`, `.pole` albo `[i]` na wskaźniku `null` **zatrzymuje program** z czytelnym komunikatem.
+Użycie `.value`, `.pole` albo `[i]` na wskaźniku `null` **zatrzymuje program** jasnym komunikatem.
-⚠️ Wskaźnika na zmienną nie wolno używać po zakończeniu jej bloku. Finch tego nie sprawdza.
+⚠️ Wskaźnika na zmienną nie wolno używać po zakończeniu bloku tej zmiennej. Finch tego nie sprawdza.
---
@@ -657,34 +689,34 @@ Użycie `.value`, `.pole` albo `[i]` na wskaźniku `null` **zatrzymuje program**
| Funkcja | Co robi |
|---|---|
-| `print(a, b, ...)` | Wypisuje wszystkie wartości oddzielone spacjami i przechodzi do nowej linii. Działa z każdym typem, także z tablicami i strukturami. |
-| `input()`, `input("pytanie")` | Czyta jedną linijkę wpisaną przez użytkownika (bez znaku nowej linii). Na końcu wejścia daje pusty tekst. |
-| `read_file(sciezka)` | Cały plik jako `str`. Jeśli nie da się go przeczytać, zatrzymuje program z czytelnym komunikatem, chyba że obsłużysz to: `read_file(p) or ...`. |
-| `write_file(sciezka, tekst)` | Zapisuje (zastępuje) plik; oddaje `true`, jeśli się udało. Z `or` / `try` porażka niesie przyczynę. |
+| `print(a, b, ...)` | Wypisuje wszystkie wartości oddzielone spacjami, a na końcu przechodzi do nowej linii. Działa z każdym typem, także z tablicami i strukturami. |
+| `input()`, `input("pytanie")` | Wczytuje jedną linię wpisaną przez użytkownika (bez znaku nowej linii). Na końcu wejścia zwraca pusty tekst. |
+| `read_file(sciezka)` | Cały plik jako `str`. Jeśli pliku nie da się przeczytać, zatrzymuje program jasnym komunikatem, chyba że to obsłużysz: `read_file(p) or ...`. |
+| `write_file(sciezka, tekst)` | Zapisuje (zastępuje) plik; zwraca `true`, jeśli się udało. Z `or` / `try` niepowodzenie niesie przyczynę. |
| `file_exists(sciezka)` | `true` / `false` |
| `delete_file(sciezka)` | Usuwa plik; `true`, jeśli się udało (albo przyczyna, z `or` / `try`). |
-| `error(komunikat)` | Porażka, którą zwraca funkcja mogąca się nie udać: `return error("...")`. |
-| `shell(polecenie)` | Uruchamia polecenie powłoki i oddaje jego kod wyjścia. |
+| `error(komunikat)` | Niepowodzenie, które zwraca funkcja mogąca się nie udać: `return error("...")`. |
+| `shell(polecenie)` | Uruchamia polecenie powłoki i zwraca jego kod wyjścia. |
| `exit(kod)` | Natychmiast kończy program. |
-| `addr(x)` | Wskaźnik na `x`; `addr(funkcja)` daje C wskaźnik na funkcję (callback). |
-| `new(wartosc)`, `free(p)` | Ręczna pamięć na stercie (zobacz [Pamięć](#13-pamięć-kto-co-zwalnia)). |
-| `int(x)`, `str(x)`, `u8(x)`, … | Zamiany typów (zobacz [Zamiana typów](#zamiana-typów)). `str(x)` działa na każdym typie i daje ten sam tekst, co `print`. `int(tekst) or 0` obsługuje tekst, który nie jest liczbą. |
+| `addr(x)` | Wskaźnik na `x`; `addr(funkcja)` daje C wskaźnik na funkcję (funkcję zwrotną). |
+| `new(wartosc)`, `free(p)` | Ręczna pamięć na stercie (zob. [Pamięć](#13-pamięć-kto-co-zwalnia)). |
+| `int(x)`, `str(x)`, `u8(x)`, … | Zamiany typów (zob. [Zamiana typów](#zamiana-typów)). `str(x)` działa na każdym typie i daje ten sam tekst, który pokazuje `print`. `int(tekst) or 0` obsługuje tekst, który nie jest liczbą. |
---
## 16. Moduły
-Program możesz podzielić na pliki. `import ksztalty` wczytuje `ksztalty.fch` z tego samego folderu
-(albo z folderu wymienionego w zmiennej środowiskowej `FINCH_PATH`, rozdzielonych `:`).
+Program można podzielić na pliki. `import ksztalty` wczytuje `ksztalty.fch` z tego samego katalogu (albo z katalogu
+wymienionego w zmiennej środowiskowej `FINCH_PATH`; kilka katalogów oddziela się `:`).
```c
// ksztalty.fch
-struct Pudlo {
+struct Pudelko {
float w
float h
}
-fn pole(Pudlo p) -> float {
+fn pole(Pudelko p) -> float {
return p.w * p.h
}
```
@@ -694,45 +726,45 @@ fn pole(Pudlo p) -> float {
import ksztalty
fn main() {
- p := ksztalty.Pudlo(2, 3)
+ p := ksztalty.Pudelko(2, 3)
print(ksztalty.pole(p))
- ksztalty.Pudlo inne // typ z modułu w deklaracji
+ ksztalty.Pudelko inne // typ z modułu w deklaracji
}
```
-- Wszystkiego z modułu używasz z jego nazwą z przodu: `ksztalty.pole`, `ksztalty.Pudlo`.
+- Wszystkiego z modułu używa się z nazwą modułu na początku: `ksztalty.pole`, `ksztalty.Pudelko`.
- Moduł nie ma `main`. Moduły mogą importować inne moduły.
-- Moduły w folderach: `import ui/przycisk` wczytuje `ui/przycisk.fch`, a używasz go jako `przycisk.rysuj()`.
-- Inna nazwa: `import ui/przycisk as p` → `p.rysuj()`. Dwa importy nie mogą mieć tej samej nazwy.
+- Moduły w katalogach: `import ui/button` wczytuje `ui/button.fch`, a używa się go jako `button.draw()`.
+- Pod inną nazwą: `import ui/button as btn` → `btn.draw()`. Dwa importy nie mogą mieć tej samej nazwy.
---
-## 17. Biblioteki z C
+## 17. Biblioteki C
### Krok 1: zaimportuj nagłówek
```c
import "stdio.h"
import "GLFW/glfw3.h" // ścieżka w systemowych katalogach nagłówków
-import "mojalib.h" // nagłówek obok twojego pliku .fch
+import "mojabib.h" // nagłówek obok twojego pliku .fch
```
Finch czyta nagłówek przez **libclang** (parser C z clanga) i udostępnia:
- **funkcje**, także te ze zmienną liczbą argumentów, jak `printf`,
-- **struktury**, przez wartość i przez wskaźnik (`Vector2`, `SDL_Rect`, `CXCursor`, …), razem z polami,
-- **wartości `enum`** i **proste stałe `#define`** (`M_PI`, `GL_COLOR_BUFFER_BIT`),
-- **zmienne globalne**, np. `stdout` i `stderr`.
+- **struktury**, przez wartość i przez wskaźnik (`Vector2`, `SDL_Rect`, `CXCursor`, …), razem z ich polami,
+- **wartości `enum`** i **proste stałe z `#define`** (`M_PI`, `GL_COLOR_BUFFER_BIT`),
+- **zmienne globalne**, jak `stdout` i `stderr`.
### Krok 2: dolinkuj bibliotekę
```c
-link "glfw" // używa libglfw.so (najpierw pyta pkg-config, jeśli jest)
-link "pomocnicze.c" // skompiluj i dołącz własny plik C (obok pliku .fch)
+link "glfw" // używa libglfw.so (najpierw pyta pkg-config, jeśli jest zainstalowany)
+link "pomocnicze.c" // kompiluje i linkuje twój plik C (obok pliku .fch)
link "gotowe.a" // albo plik obiektowy / bibliotekę statyczną
```
-Jeśli zapomnisz linijki `link`, Finch powie dokładnie, czego brakuje:
+Jeśli zapomnisz o linii `link`, Finch powie dokładnie, czego brakuje:
```
error: glfwInit, glfwCreateWindow, glfwMakeContextCurrent and 5 more come from "GLFW/glfw3.h", but its library isn't linked
@@ -749,15 +781,15 @@ error: glfwInit, glfwCreateWindow, glfwMakeContextCurrent and 5 more come from "
| `unsigned …` | `u16`, `u32`, `u64` |
| `float`, `double` | `f32`, `f64` |
| `_Bool` / `bool` | `bool` |
-| `enum` | odpowiadająca liczba całkowita ze znakiem |
+| `enum` | odpowiednia liczba całkowita ze znakiem |
| `char *` jako **parametr albo wynik** | `str` |
-| `char *` w strukturze albo za wskaźnikiem | `ptr[char]` (czytasz przez `str(p)`) |
+| `char *` w strukturze albo za wskaźnikiem | `ptr[char]` (czyta się go przez `str(p)`) |
| `struct X` przez wartość | `X`, struktura z tymi samymi polami |
| `struct X *`, `int *`, … | `ptr[X]`, `ptr[i32]`, … |
-| `int arr[16]` w strukturze | `[16]i32`: indeksujesz i używasz `.len` |
+| `int tab[16]` w strukturze | `[16]i32`: można go indeksować i użyć `.len` |
| `void *`, wskaźniki na funkcje, niekompletne struktury | `ptr` |
-### Przykład: struktury i raylib
+### Przykład: struktury i funkcje zwrotne
```c
import "raylib.h"
@@ -775,15 +807,15 @@ fn main() {
}
```
-Żeby dać C funkcję do wywołania zwrotnego (np. porównywarkę dla `qsort`), użyj `addr(mojaFunkcja)`.
+Żeby dać C funkcję do wywołania (jak funkcję porównującą dla `qsort`), użyj `addr(mojaFunkcja)`.
Jej parametry muszą być typami C (liczby, wskaźniki, struktury C).
### Grafika: OpenGL i Vulkan
Oba działają bezpośrednio; `examples/opengl.fch` i `examples/vulkan.fch` to kompletne programy.
-- **OpenGL 3+** na Linuksie: nowoczesne funkcje (`glCreateShader`, `glGenVertexArrays`, …) są deklarowane
- dopiero po zdefiniowaniu `GL_GLEXT_PROTOTYPES`. Finch nie definiuje makr C, więc dołączenia umieść w małym
+- **OpenGL 3+** na Linuksie: nowoczesne funkcje (`glCreateShader`, `glGenVertexArrays`, …) są zadeklarowane tylko
+ wtedy, gdy zdefiniowane jest `GL_GLEXT_PROTOTYPES`. Finch nie definiuje makr C, więc umieść te `#include` w malutkim
nagłówku obok programu i zaimportuj go:
```c
@@ -794,21 +826,21 @@ Oba działają bezpośrednio; `examples/opengl.fch` i `examples/vulkan.fch` to k
#include
```
- Potem `import "opengl.h"`, `link "glfw"`, `link "GL"`. Parametry wyjściowe przekazujesz przez `addr(...)`
- (`glGenBuffers(1, addr(vbo))`), dane bufora przez `.ptr` (`vertices.ptr`), a przesunięcia w bajtach przez
- `ptr(...)` (`glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, ptr(8))`).
-- **Vulkan**: `import "vulkan/vulkan.h"` i `link "vulkan"`. Struktury „create info” budujesz z nazwanymi polami
- (brakujące są zerami), uchwyty to `ptr`, a listy pobierasz typowym wzorcem „dwóch wywołań”: raz po liczbę,
- raz z `lista.ptr` po `lista.resize(liczba)`. Makra-funkcje, jak `VK_MAKE_API_VERSION`, nie są dostępne;
- policz liczbę sam (`1 << 22` to Vulkan 1.0).
+ Potem `import "opengl.h"`, `link "glfw"`, `link "GL"`. Parametry wyjściowe przekazuje się przez `addr(...)`
+ (`glGenBuffers(1, addr(vbo))`), dane bufora przez `.ptr` (`wierzcholki.ptr`), a przesunięcia w bajtach przez
+ `ptr(...)` (`glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, krok, ptr(8))`).
+- **Vulkan**: `import "vulkan/vulkan.h"` i `link "vulkan"`. Struktury „create info” buduje się przez nazwy pól
+ (brakujące są zerami), uchwyty to `ptr`, a listy pobiera się zwykłym sposobem z dwoma wywołaniami: raz po liczbę,
+ drugi raz z `lista.ptr` po `lista.resize(liczba)`. Makra z parametrami, takie jak `VK_MAKE_API_VERSION`, nie są
+ dostępne; zamiast nich policz liczbę (`1 << 22` to Vulkan 1.0).
-### Co jeszcze nie działa
+### Czego jeszcze nie ma
-- **Unie** C i struktury z **polami bitowymi** przekazywane do funkcji C albo zwracane z niej **przez wartość**
- (wskaźniki do nich działają, unie jako zmienne też: `SDL_Event e`, `e.type`, `e.key.key`, `SDL_PollEvent(addr(e))`).
-- **Makra-funkcje** (`#define MAX(a, b) ...`). Makra będące liczbą, tekstem, wyrażeniem z nich
- (`(1u << 3)`, `((Uint64)1)`, `(A | B)`) albo strukturą z liczb (`RAYWHITE` z raylib, `(Color){ 245, 245, 245, 255 }`) działają.
-- `long double`, liczby 128-bitowe.
+- **Unii** C i struktur z **polami bitowymi** przekazywanych do funkcji C albo z niej zwracanych **przez wartość**
+ (wskaźniki na nie działają, unie jako zmienne też: `SDL_Event e`, `e.type`, `e.key.key`, `SDL_PollEvent(addr(e))`).
+- **Makr z parametrami** (`#define MAX(a, b) ...`). Działają makra będące liczbą, tekstem, wyrażeniem z nich
+ (`(1u << 3)`, `((Uint64)1)`, `(A | B)`) albo strukturą liczb (`RAYWHITE` z raylib, `(Color){ 245, 245, 245, 255 }`).
+- `long double` i liczb 128-bitowych.
---
@@ -816,12 +848,12 @@ Oba działają bezpośrednio; `examples/opengl.fch` i `examples/vulkan.fch` to k
### Błędy jako wartości
-Niektóre porażki są normalne: użytkownik wpisze `abc`, pliku nie ma. Finch obsługuje je bez wyjątków
-i bez ukrytych skoków. Funkcja, która może się nie udać, mówi to znakiem `!` po typie wyniku i kończy się
-porażką przez `return error("komunikat")`:
+Niektóre niepowodzenia są normalne: użytkownik wpisze `abc`, pliku brakuje. Finch obsługuje je bez wyjątków i bez
+ukrytych skoków. Funkcja, która może się nie udać, mówi o tym znakiem `!` po typie wyniku, a niepowodzenie zgłasza
+przez `return error("komunikat")`:
```c
-fn port_z_tekstu(str tekst) -> int! {
+fn parsuj_port(str tekst) -> int! {
n := int(tekst) or { return error("'" + tekst + "' to nie liczba") }
if n < 1 || n > 65535 {
return error("port " + str(n) + " jest poza zakresem")
@@ -829,23 +861,23 @@ fn port_z_tekstu(str tekst) -> int! {
return n
}
-fn zapisz(str sciezka, str tekst) -> ! { // może się nie udać, nic nie oddaje
+fn zapisz(str sciezka, str tekst) -> ! { // może się nie udać, niczego nie zwraca
try write_file(sciezka, tekst)
}
```
-Wywołujący **musi** obsłużyć porażkę, na jeden z trzech sposobów (zapomnienie to błąd kompilacji):
+Wywołujący **musi** obsłużyć niepowodzenie, na jeden z trzech sposobów (zapomnienie to błąd kompilacji):
-| Kod | W razie porażki |
+| Kod | Gdy się nie uda |
|---|---|
-| `port := port_z_tekstu(s) or 8080` | użyj tej wartości (liczonej tylko przy porażce; może to być kolejne wywołanie z `or`) |
-| `port := port_z_tekstu(s) or { print(err); return }` | wykonaj blok; `err` to komunikat (`str`). Blok musi wyjść (`return`, `break`, `continue`, `exit`), chyba że wynik nie jest używany. |
-| `port := try port_z_tekstu(s)` | zakończ bieżącą funkcję tym samym błędem (musi też móc się nie udać) |
+| `port := parsuj_port(s) or 8080` | użyj tej wartości (jest obliczana tylko przy niepowodzeniu; może to być kolejne wywołanie z `or`) |
+| `port := parsuj_port(s) or { print(err); return }` | wykonaj blok; `err` to komunikat (`str`). Blok musi wyjść (`return`, `break`, `continue`, `exit`), chyba że wynik nie jest używany. |
+| `port := try parsuj_port(s)` | przerwij bieżącą funkcję z tym samym błędem (ona też musi móc się nie udać) |
```c
fn wczytaj(str sciezka) -> Konfig! {
tekst := try read_file(sciezka)
- port := try port_z_tekstu(tekst.trim())
+ port := try parsuj_port(tekst.trim())
return Konfig(port: port)
}
@@ -855,12 +887,11 @@ fn main() -> ! { // main też może się nie udać: błąd zostan
}
```
-Funkcje wbudowane, które mogą się nie udać, działają tak samo: `int(tekst)`, `float(tekst)`, `read_file`,
-`write_file`, `delete_file`. Bez `or` / `try` zachowują się jak wcześniej (`int("x")` zatrzymuje program,
-`write_file` oddaje `bool`).
+Tak samo działają wbudowane funkcje, które mogą się nie udać: `int(tekst)`, `float(tekst)`, `read_file`, `write_file`,
+`delete_file`. Bez `or` / `try` zachowują się jak dawniej (`int("x")` zatrzymuje program, `write_file` zwraca `bool`).
-Pod spodem funkcja, która może się nie udać, zwraca wartość z flagą i komunikatem: bez alokacji przy
-sukcesie, bez zwijania stosu, a porażka kosztuje tyle, co zbudowanie komunikatu.
+Pod spodem taka funkcja zwraca wartość razem z flagą i komunikatem: przy powodzeniu nic nie jest przydzielane,
+nie ma odwijania stosu, a niepowodzenie kosztuje tyle, co zbudowanie komunikatu.
### Błędy kompilacji
@@ -870,12 +901,11 @@ gra.fch:12:9: error: 'wynik' must be int, but this is str
| ^
```
-Format: `plik:linijka:kolumna: error: wyjaśnienie`, a pod spodem linijka ze znakiem `^`.
-Komunikaty są po angielsku.
+Format: `plik:linia:kolumna: error: wyjaśnienie`, a pod spodem linia z `^` pod miejscem błędu.
### Błędy w trakcie działania
-Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje się z komunikatem i kodem wyjścia 1:
+Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje program z komunikatem i kodem wyjścia 1:
| Komunikat | Przyczyna |
|---|---|
@@ -887,29 +917,29 @@ Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje się z komunikat
| `runtime error: can't turn "x" into int` | `int(...)` / `float(...)` na tekście, który nie jest liczbą |
| `runtime error: can't read the file "…"` | `read_file` na brakującym albo nieczytelnym pliku (obsłuż to przez `or`) |
| `runtime error: the key "x" is not in the map …` | `m["x"]`, gdy mapa nie ma takiego klucza (użyj `.has` albo `.get`) |
-| `runtime error: called .f() on a null pointer` | metoda wywołana przez `null` w `ptr[T]` |
+| `runtime error: called .f() on a null pointer` | metoda wywołana przez `ptr[T]` równy `null` |
| `runtime error: assert failed: …` | `assert(warunek, "…")` z fałszywym warunkiem |
-Pod komunikatem jest łańcuch wywołań, które do niego doprowadziły, od najgłębszego:
+Pod komunikatem jest ścieżka wywołań, które do niego doprowadziły, od najgłębszego:
```
gra.fch:6: runtime error: index 3 is out of range (the length is 2)
- in Enemy.next (gra.fch:6)
- in step (gra.fch:10)
+ in Wrog.nastepny (gra.fch:6)
+ in krok (gra.fch:10)
in main (gra.fch:15)
```
-`finch run`, `finch test` oraz budowanie z `-g` i `-O0` go mają; zwykły `finch build` go pomija, bo pilnowanie go kosztuje
-czas w każdym wywołaniu (ok. 3 razy w programie złożonym z samych drobnych wywołań, dużo mniej w prawdziwym). `--trace` go dodaje.
+`finch run`, `finch test` oraz kompilacje z `-g` i `-O0` ją pokazują; zwykłe `finch build` jej nie dodaje, bo
+pamiętanie jej kosztuje czas przy każdym wywołaniu (ok. 3 razy wolniej w programie złożonym wyłącznie z drobnych
+wywołań, w prawdziwym programie dużo mniej). `--trace` ją włącza.
---
## 19. Edytory: VS Code, Kate i inne
-Wszystkie funkcje edytorów pochodzą z jednego serwera języka, `finch lsp`, który używa prawdziwego
-kompilatora: błędy na bieżąco, typy i opisy po najechaniu myszą, idź do definicji, podpowiedzi (pola,
-metody, metody map i tablic po `.`, zawartość modułów, nazwy z nagłówków C), konspekt pliku i podpowiedzi
-parametrów.
+Wszystkie funkcje edytorów pochodzą z jednego serwera języka, `finch lsp`, który korzysta z prawdziwego kompilatora:
+błędy w trakcie pisania, typy i opisy po najechaniu kursorem, przejście do definicji, podpowiedzi (pola, metody,
+metody map i tablic po `.`, zawartość modułów, nazwy z nagłówków C), spis zawartości pliku i podpowiedzi parametrów.
### Kate (oraz KWrite, KDevelop)
@@ -917,20 +947,20 @@ parametrów.
editors/kate/install.sh
```
-Instaluje podświetlanie (`~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml`) i dopisuje `finch`
-do ustawień klienta LSP w Kate, zachowując twoje pozostałe serwery. Uruchom Kate ponownie i włącz wtyczkę
+Skrypt instaluje podświetlanie składni (`~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml`) i dopisuje
+`finch` do ustawień klienta LSP w Kate, nie ruszając twoich innych serwerów. Uruchom Kate ponownie i włącz wtyczkę
**LSP Client**. Żeby uruchamiać programy, dodaj `finch run %f` jako cel we wtyczce **Build & Run**; komunikaty
-Fincha `plik:linia:kolumna: error:` są wtedy klikalne. Szczegóły: `editors/kate/README.md`.
+`plik:linia:kolumna: error:` będą wtedy klikalne. Szczegóły: `editors/kate/README.md`.
### VS Code
Rozszerzenie **Finch** (`editors/vscode`, dołączane też do każdego wydania jako plik `.vsix`) daje:
-podświetlanie składni, błędy na bieżąco, podpowiedzi (także pola i metody po `.`), typy po najechaniu
-myszą, idź do definicji (F12), konspekt pliku, podpowiedzi parametrów, szablony i przycisk ▶ **Run**
-(`Ctrl+F5`). Błędy z **Finch: Build This File** trafiają do panelu Problems.
+podświetlanie, błędy w trakcie pisania, podpowiedzi (także pól i metod po `.`), typy po najechaniu kursorem,
+przejście do definicji (F12), spis zawartości pliku, podpowiedzi parametrów, gotowe fragmenty kodu i przycisk
+▶ **Run** (`Ctrl+F5`). Błędy z **Finch: Build This File** trafiają do panelu Problems.
-Instalacja: Rozszerzenia → `…` → *Install from VSIX…* → wybierz `finch-lang-*.vsix`. Rozszerzenie uruchamia
-`finch` z PATH; jeśli jest gdzie indziej, ustaw **Finch: Path** w ustawieniach.
+Instalacja: Extensions → `…` → *Install from VSIX…* → wybierz `finch-lang-*.vsix`. Rozszerzenie uruchamia `finch`
+z PATH; jeśli leży gdzie indziej, ustaw **Finch: Path** w ustawieniach.
### Inne
@@ -944,14 +974,14 @@ finch build gra.fch -g -O0 -o gra
gdb ./gra
(gdb) break gra.fch:12
(gdb) run
-(gdb) bt # gdzie jesteśmy, przez które funkcje
+(gdb) bt # gdzie jesteśmy i przez które funkcje
(gdb) info locals # wszystkie zmienne
(gdb) print gracz.zycia
```
-`-g` dodaje informacje dla debuggera (linijki, funkcje, zmienne, pola struktur). `-O0` zostawia wszystkie
-zmienne widoczne; bez niego optymalizator może część z nich usunąć. Na Windowsie `-g` zapisuje format
-CodeView, który czyta debugger Visual Studio i WinDbg.
+`-g` dodaje informacje dla debuggera (linie, funkcje, zmienne, pola struktur, wektory). `-O0` zostawia widoczną
+każdą zmienną; bez tego optymalizator może niektóre usunąć. Na Windowsie to też jest DWARF: użyj `lldb` z llvm-mingw
+albo gdb.
---
@@ -960,73 +990,51 @@ CodeView, który czyta debugger Visual Studio i WinDbg.
| Problem | Rozwiązanie |
|---|---|
| `can't find the C header 'x.h'` | Brakuje plików deweloperskich biblioteki. Zainstaluj je (Debian: `libx-dev`; Nix: dopisz do `shell.nix`). |
-| `… come from "x.h", but its library isn't linked` | Dopisz `link "nazwa"` na górze pliku. |
-| `the library 'x' wasn't found` | Nie jest zainstalowana albo nazwa jest zła. Na Niksie dopisz ją do `shell.nix` i pracuj w `nix-shell`. |
-| `the C function 'f' can't be used from Finch yet` | Używa unii, pól bitowych albo innego nieobsługiwanego typu przez wartość. Poszukaj wariantu ze wskaźnikami. |
+| `… come from "x.h", but its library isn't linked` | Dodaj `link "nazwa"` na początku pliku. |
+| `the library 'x' wasn't found` | Biblioteka nie jest zainstalowana albo nazwa jest błędna. Na Niksie dopisz ją do `shell.nix` i pracuj w `nix-shell`. |
+| `the C function 'f' can't be used from Finch yet` | Używa przez wartość unii, pól bitowych albo innego nieobsługiwanego typu. Poszukaj wariantu, który przyjmuje wskaźniki. |
+| `--cpu native`: na innym komputerze program kończy się `Illegal instruction` | Został zbudowany pod procesor, na którym go budowano. Zbuduj bez `--cpu` albo ze starszym (`x86-64-v2`). |
| `can't find the module 'x'` | Połóż `x.fch` obok importującego pliku albo ustaw `FINCH_PATH`. |
-| `couldn't build the Finch runtime` | Brak działającego kompilatora C. Zainstaluj gcc lub clang albo ustaw `CC`. |
-| Biblioteka nie znajduje się przy starcie programu | Zlinkowano ją z folderu, którego system nie przeszukuje. Uruchamiaj w tym samym `nix-shell` albo zainstaluj ją systemowo. |
-| Windows: `finch` nie linkuje (błędy `LNK…` o `libcmt`, `kernel32`) | Zainstaluj Visual Studio Build Tools z *Desktop development with C++*. |
+| `couldn't build the Finch runtime` | Brak działającego kompilatora C. Zainstaluj gcc albo clang, albo ustaw `CC`. |
+| Program po uruchomieniu nie znajduje biblioteki | Została dolinkowana z katalogu, którego system nie przeszukuje. Uruchamiaj w tym samym `nix-shell` albo zainstaluj ją w systemie. |
+| Windows: `no C compiler found to link with ('clang')` | Dodaj katalog `bin` z llvm-mingw do PATH (rozdział 2). |
| Windows: `finch.exe` się nie uruchamia (brak `libclang.dll`) | Trzymaj `libclang.dll` obok `finch.exe`. |
| macOS: `finch` się nie uruchamia (`Library not loaded: …libLLVM…`) | `brew install llvm@21`. |
| macOS: `can't find the C header 'stdio.h'` | Zainstaluj narzędzia wiersza poleceń: `xcode-select --install`. |
| `--target arm64`: `no C compiler found to link with` | Zainstaluj `aarch64-linux-gnu-gcc` (Debian: `gcc-aarch64-linux-gnu`) albo ustaw `FINCH_CC`. |
-| `clang` w `nix-shell` nie widzi `stdio.h` | Masz stary `shell.nix`: `llvmPackages.clang` musi być przed `llvmPackages.libclang`. |
+| `clang` w `nix-shell` nie znajduje `stdio.h` | Masz stary `shell.nix`: `llvmPackages.clang` musi stać przed `llvmPackages.libclang`. |
| Program nigdy się nie kończy | Warunek pętli nigdy nie staje się fałszywy. Naciśnij Ctrl+C. |
---
-## 22. Struktura projektu i testy
+## 22. Układ repozytorium i testy
```
Finch/
-├── src/ kompilator, w Finchu (lekser, parser, gen: LLVM IR jako tekst, cimport: libclang, lsp, main ...)
-├── std/ biblioteka standardowa, w Finchu: json, path, time, proc, fs, thread
-├── runtime/ finch_rt.c: mała biblioteka uruchomieniowa (teksty, tablice, mapy, wejście, pliki)
-├── bootstrap/ build.sh, install.sh i ziarna (własny IR kompilatora, po jednym na system)
-├── examples/ hello, tour, types, structs, todo, guess, c_import, window, opengl, vulkan, raylib, llvm
+├── src/ kompilator w Finchu (lexer, parser, gen: LLVM IR jako tekst, cimport: libclang, lsp, main ...)
+├── std/ biblioteka standardowa w Finchu: json, path, time, proc, fs, thread
+├── runtime/ finch_rt.c: mały runtime (teksty, tablice, mapy, wczytywanie, pliki)
+├── bootstrap/ build.sh, install.sh i pliki startowe (własny IR kompilatora, osobno dla każdego systemu)
+├── examples/ hello, tour, types, structs, todo, guess, wordcount, c_import, simd, window, opengl, vulkan, raylib, llvm
├── tests/
-│ ├── run/ new/ programy + dokładny wynik (.out) i wejście (.in)
-│ ├── fail/ new/fail/ programy, które muszą się nie udać, z oczekiwanym błędem w 1. linijce
-│ ├── tool/ finch test, finch fmt, łańcuch wywołań, projekt z finch.toml
+│ ├── run/ new/ programy + dokładny wynik, który muszą wypisać (.out), wejście (.in)
+│ ├── fail/ new/fail/ programy, które muszą się nie skompilować albo zatrzymać, z oczekiwanym błędem w 1. linii
+│ ├── tool/ finch test, finch fmt, ścieżka wywołań, projekt z finch.toml
│ ├── run.sh uruchamia testy (MEMCHECK=1 sprawdza też pamięć valgrindem)
-│ ├── cross.sh każdy test zbudowany na Windows / ARM64 i uruchomiony w Wine / QEMU
-│ └── lsp_test.py serwer języka, obsługiwany jak przez edytor
+│ ├── cross.sh każdy test zbudowany na Windowsa / ARM64 i uruchomiony przez Wine / QEMU
+│ └── lsp_test.py serwer języka, z którym test rozmawia tak jak edytor
├── editors/ rozszerzenie VS Code (vscode/), podświetlanie i konfiguracja LSP dla Kate (kate/)
-├── docs/ ta dokumentacja (en, pl), łącznie ze ściągą
-└── shell.nix środowisko deweloperskie Nix
+├── docs/ ta dokumentacja (en, pl), razem ze ściągą
+└── shell.nix środowisko programistyczne dla Niksa
```
```sh
-tests/run.sh # programy, odmowy, buildy z -g, biblioteka współdzielona, serwer języka
-MEMCHECK=1 tests/run.sh # to samo pod valgrindem: bez wycieków i złych dostępów do pamięci
-tests/cross.sh windows # każdy test zbudowany na Windowsa i uruchomiony w Wine
-tests/cross.sh arm64 # każdy test zbudowany na Linuksa ARM64 i uruchomiony w QEMU
+tests/run.sh # programy, oczekiwane błędy, kompilacje z -g, biblioteka współdzielona, serwer języka
+MEMCHECK=1 tests/run.sh # to samo pod valgrindem: bez wycieków i błędnych dostępów do pamięci
+tests/cross.sh windows # każdy test zbudowany na Windowsa i uruchomiony przez Wine
+tests/cross.sh arm64 # każdy test zbudowany na Linuksa ARM64 i uruchomiony przez QEMU
```
-### Warianty z wartościami
-
-Wariant może nieść wartości. Tworzysz go przez `Shape.Circle(2.0)`, a wyciągasz `match`-em, który je nazywa:
-
-```c
-enum Shape {
- Circle(float)
- Rect(float, float)
- Point
-}
-
-fn Shape.area() -> float {
- return match self {
- Circle(r) -> 3.14 * r * r
- Rect(w, h) -> w * h
- Point -> 0.0
- }
-}
-```
-
-`_` pomija wartość. `match` musi obsłużyć każdy wariant (albo kończyć się `else`). Wypisanie pokazuje `Rect(2, 3.5)`. Taki enum
-jest kopiowany i zwalniany jak struktura; nie da się go porównać przez `==` (użyj `match`). Warianty takich enumów nie mają numerów.
-
---
## 23. Enumy i match
@@ -1035,22 +1043,22 @@ jest kopiowany i zwalniany jak struktura; nie da się go porównać przez `==` (
```
enum Kolor { Czerwony, Zielony, Niebieski }
-enum Status { Ok = 200, Brak = 404 } // wariantowi można nadać numer
+enum Status { Ok = 200, NotFound = 404 } // wariant może dostać liczbę, jeśli jej potrzebuje
k := Kolor.Zielony
print(k) // Zielony
-print(int(Status.Brak)) // 404
-print(Kolor(2)) // Niebieski: liczba zamieniona na enum (sprawdzana: Kolor(7) to błąd)
+print(int(Status.NotFound)) // 404
+print(Kolor(2)) // Niebieski: liczba zamieniona na enum (sprawdzane: Kolor(7) to błąd)
```
-Warianty pisze się `Kolor.Czerwony`. Można je porównywać przez `==` i `!=` (nie przez `<`), używać jako klucze map,
-pola struktur i elementy tablic, a enum może mieć metody jak struktura: `fn Kolor.nastepny() -> Kolor { ... }`.
+Warianty zapisuje się `Kolor.Czerwony`. Można je porównywać przez `==` i `!=` (nie przez `<`), używać jako kluczy map,
+pól struktur i elementów tablic, a enum może mieć metody tak jak struktura: `fn Kolor.nastepny() -> Kolor { ... }`.
-**match** wybiera, co zrobić, na podstawie wartości — zamiast drabinki `if`:
+**match** wybiera, co zrobić, na podstawie wartości, zamiast drabinki `if`:
```
match k {
- Czerwony -> print("ciepły") // w matchu na enumie wystarczy sama nazwa
+ Czerwony -> print("ciepły") // w match na enumie wystarczy sama nazwa
Zielony, Niebieski -> print("zimny") // kilka wartości w jednej gałęzi
}
@@ -1065,17 +1073,41 @@ match slowo {
print("pa")
return
}
- else -> print("co?")
+ else -> print("słucham?")
}
```
-`match` działa na enumach, liczbach całkowitych, znakach, tekście i bool. Gałęzie sprawdzane są po kolei. Match na
-enumie musi obsłużyć **każdy wariant** albo kończyć się `else` (kompilator wskaże brakujące); każdy inny match
-potrzebuje `else`. W wyrażeniu wszystkie gałęzie dają ten sam typ.
+`match` działa na enumach, liczbach całkowitych, znakach, tekstach i wartościach bool. Gałęzie są sprawdzane po kolei.
+`match` na enumie musi obsłużyć **każdy wariant** albo kończyć się `else` (kompilator mówi, których brakuje); każdy inny
+`match` potrzebuje `else`. W wyrażeniu każda gałąź daje wartość tego samego typu.
+
+### Warianty z wartościami
+
+Wariant może przenosić wartości. Tworzy się go przez `Ksztalt.Kolo(2.0)`, a wartości wyjmuje przez `match`, który je nazywa:
+
+```c
+enum Ksztalt {
+ Kolo(float)
+ Prostokat(float, float)
+ Punkt
+}
+
+fn Ksztalt.pole() -> float {
+ return match self {
+ Kolo(r) -> 3.14 * r * r
+ Prostokat(w, h) -> w * h
+ Punkt -> 0.0
+ }
+}
+```
+
+`_` pomija wartość. `match` musi obsłużyć każdy wariant (albo kończyć się `else`). Wypisanie pokazuje `Prostokat(2, 3.5)`.
+Taki enum kopiuje się i zwalnia jak struktura; nie da się go porównać przez `==` (użyj `match`). Warianty takich enumów
+nie mogą mieć liczb.
## 24. Typy generyczne
-Napisz funkcję lub strukturę raz dla dowolnego typu, z typem jako nazwą w nawiasach kwadratowych:
+Funkcję albo strukturę można napisać raz dla dowolnego typu, podając typ jako nazwę w nawiasach kwadratowych:
```
fn max[T](T a, T b) -> T {
@@ -1089,73 +1121,72 @@ struct Stos[T] {
[]T elementy
}
-fn Stos[T].dodaj(T x) {
+fn Stos[T].push(T x) {
self.elementy.push(x)
}
-print(max(3, 9), max("jabłko", "gruszka")) // T wynika z argumentów
-print(max[float](1, 2.5)) // albo zapisane wprost, gdzie nie da się go ustalić
-s := Stos[int]() // generyczną strukturę zawsze pisze się z typem
-s.dodaj(7)
+print(max(3, 9), max("jabłko", "gruszka")) // T wynika z argumentów
+print(max[float](1, 2.5)) // albo jest podany, gdy nie da się go ustalić
+s := Stos[int]() // struktura generyczna zawsze ma podany typ
+s.push(7)
```
-Parametrów może być kilka (`struct Para[A, B]`), typy można zagnieżdżać (`Stos[Para[int, str]]`). Każde użycie tworzy
-własną kopię kodu, więc generyk jest tak szybki jak kod napisany ręcznie. Nie ma ograniczeń: jeśli `T` nie potrafi
-tego, co robi funkcja (`a > b` na strukturze), błąd mówi o tym i podaje, którą wersję budowano, np.
-`(while building wiekszy[Punkt])`.
+Działa kilka parametrów (`struct Para[A, B]`) i typy zagnieżdżone (`Stos[Para[int, str]]`). Każde użycie tworzy własną
+kopię kodu, więc typ generyczny jest tak szybki jak kod napisany ręcznie. Nie ma ograniczeń na typ: jeśli `T` nie potrafi
+tego, czego wymaga funkcja (`a > b` na strukturze), błąd o tym mówi i podaje, którą wersję budował, np.
+`(while building bigger[Punkt])`.
## 25. Biblioteka standardowa
-Moduły dołączone do Fincha, napisane w Finchu. `import json` szuka najpierw `json.fch` obok Twojego pliku, potem w
-bibliotece — więc Twój własny plik o tej nazwie ma pierwszeństwo.
+Moduły dostarczane z Finchem, napisane w Finchu. `import json` najpierw szuka `json.fch` obok twojego pliku, a dopiero
+potem w bibliotece, więc twój plik o tej samej nazwie ma pierwszeństwo.
| Moduł | Co robi |
|---|---|
-| `path` | `join`, `base`, `dir`, `ext`, `stem`, `parts`, `clean`, `is_abs`: ścieżki jako tekst, rozdzielone `/` |
-| `time` | `now()`, `millis()` (zegar do mierzenia), `sleep(ms)`, `date(t)`, `local(t)`, `format(t, "%Y-%m-%d")`, `days_in_month` |
-| `proc` | `run(cmd)` (kod wyjścia), `output(cmd)` (co wypisało; błąd, jeśli się nie udało), `env(nazwa, domyślna)`, `pid()`, `cwd()` |
-| `json` | `parse(tekst)`, `stringify(v)`, `pretty(v)` oraz typ wartości `Json` z `get`, `at`, `set`, `push`, `has`, `str_or`, `int_or` ... |
-| `fs` | `read_bytes(p)` / `write_bytes(p, dane)` (`[]u8`, dowolne bajty), `exists`, `is_dir`, `mkdir` (z folderami wyżej), `list` (posortowane nazwy), `remove_dir`, `data_dir(app)` (folder na zapisy użytkownika) |
+| `path` | `join`, `base`, `dir`, `ext`, `stem`, `parts`, `clean`, `is_abs`: ścieżki jako tekst, rozdzielane `/` |
+| `time` | `now()`, `millis()` (zegar do mierzenia czasu), `sleep(ms)`, `date(t)`, `local(t)`, `format(t, "%Y-%m-%d")`, `days_in_month` |
+| `proc` | `run(polecenie)` (kod wyjścia), `output(polecenie)` (to, co wypisało; błąd, jeśli się nie udało), `env(nazwa, domyslna)`, `pid()`, `cwd()` |
+| `json` | `parse(tekst)`, `stringify(v)`, `pretty(v)` i typ wartości `Json` z `get`, `at`, `set`, `push`, `has`, `str_or`, `int_or` ... |
+| `fs` | `read_bytes(p)` / `write_bytes(p, dane)` (`[]u8`, dowolne bajty), `exists`, `is_dir`, `mkdir` (razem z katalogami nadrzędnymi), `list` (posortowane nazwy), `remove_dir`, `data_dir(aplikacja)` (katalog na zapisy użytkownika) |
| `thread` | `spawn(addr(praca), addr(dane))`, `t.join()`, `mutex()` z `lock`, `unlock`, `close`, `cpus()` (rozdział 29) |
-`min(a, b, ...)`, `max(a, b, ...)`, `abs(x)` i `clamp(x, od, do)` działają na dowolnych liczbach bez importu (twoja
-własna funkcja o tej samej nazwie wygrywa).
+`min(a, b, ...)`, `max(a, b, ...)`, `abs(x)` i `clamp(x, lo, hi)` działają na dowolnych liczbach (i wektorach)
+bez importu (twoja funkcja o tej samej nazwie ma pierwszeństwo).
```
import json
import path
dok := try json.parse(read_file("config.json"))
-nazwa := dok.get("name").str_or("bez nazwy") // brakujący klucz to null, więc łańcuch nigdy się nie psuje
+nazwa := dok.get("name").str_or("bez nazwy") // brakujący klucz to null, więc łańcuch wywołań się nie wysypie
dok.set("name", json.new_text(nazwa + "!"))
write_file(path.join("out", "config.json"), json.pretty(dok))
```
## 26. Obecne ograniczenia
-Jeszcze nie ma:
+Czego jeszcze nie ma:
- Interfejsów.
-- Tekstu świadomego Unicode: `.len`, `s[i]` i `upper()` działają na bajtach / ASCII.
-- Pilnowania, żeby dwa wątki nie zmieniały tej samej tablicy naraz (do tego mutex).
-- Windowsa na ARM64, systemów 32-bitowych, WebAssembly. `--target android` robi kod dla Androida, ale linkowanie
- z NDK i uruchomienie na urządzeniu nie były jeszcze próbowane.
+- Tekstów świadomych Unicode: `.len`, `s[i]` i `upper()` działają na bajtach / ASCII.
+- Zabezpieczenia przed zmienianiem tej samej tablicy przez dwa wątki naraz (użyj muteksu).
+- Windowsa na ARM64, systemów 32-bitowych, WebAssembly.
### Ostrzeżenia o dużych kopiach
-Struktura z 256 lub więcej bajtami zwykłych danych (liczby, struktury z liczb, `[N]T` z C) jest kopiowana bajt po bajcie,
-gdy jest parametrem (`fn f(Cialo c)`) i w `for x in ciala`. Finch o tym mówi, a program i tak buduje:
+Struktura z co najmniej 256 bajtami zwykłych danych (liczb, struktur z liczbami, `[N]T` z C) jest kopiowana bajt po
+bajcie, gdy jest parametrem (`fn f(Cialo c)`) i w `for x in ciala`. Finch o tym ostrzega, ale i tak buduje program:
```
-gra.fch:27:11: warning: 'c' is a Cialo (264 bytes), copied on every call to waga; take ptr[Cialo] to share it instead (callers can still pass the variable as it is)
-gra.fch:45:5: warning: each Cialo (264 bytes) is copied into 'x'; for i in 0..ciala.len with ciala[i] works on it in place
+gra.fch:27:11: warning: 'b' is a Body (264 bytes), copied on every call to weight; take ptr[Body] to share it instead (callers can still pass the variable as it is)
+gra.fch:45:5: warning: each Body (264 bytes) is copied into 'x'; for i in 0..bodies.len with bodies[i] works on it in place
```
-Teksty, tablice i mapy w strukturze się nie liczą: przekazanie struktury kopiuje tylko ich uchwyty, nie zawartość.
-`b := a` nie dostaje ostrzeżenia: w kodzie widać, że to kopia. Metoda nigdy nie kopiuje `self`. Edytory pokazują
+Teksty, tablice i mapy w strukturze się nie liczą: przekazanie struktury kopiuje tylko ich uchwyty, a nie zawartość.
+`b := a` nie daje ostrzeżenia, bo w kodzie widać, że to kopia. Metoda nigdy nie kopiuje `self`. Edytory pokazują
ostrzeżenia na żółto.
-Plan rozwoju jest w [README](../../README.pl.md#plan-rozwoju).
+Zob. [plan rozwoju w README](../../README.pl.md#plan-rozwoju).
## 27. Testy: bloki test i finch test
@@ -1172,33 +1203,35 @@ test "obrażenia nigdy nie schodzą poniżej zera" {
}
```
-`finch test gra.fch` uruchamia każdy blok `test` z pliku i z modułów, które importuje (bez biblioteki standardowej),
-każdy osobno: nieudany `assert` albo dowolny błąd w czasie działania kończy tylko ten test, a następny rusza dalej.
+`finch test gra.fch` uruchamia każdy blok `test` z tego pliku i z importowanych modułów (ale nie z biblioteki
+standardowej), każdy osobno: nieudany `assert` albo dowolny błąd w trakcie działania kończy tylko ten jeden test,
+a następny rusza dalej.
```
ok obrażenia nigdy nie schodzą poniżej zera
-FAIL wczytanie poziomu
- poziom.fch:31: runtime error: can't read the file "levels/1.txt"
+FAIL wczytywanie poziomu
+ poziom.fch:31: runtime error: can't read the file "poziomy/1.txt"
1 passed, 1 failed
```
-Kod wyjścia to 1, gdy jakiś test nie przeszedł, więc CI może z tego korzystać. `finch run` i `finch build` pomijają bloki test.
-`assert(warunek)` działa też poza testami: zatrzymuje program z `assert failed`, gdy warunek jest fałszywy.
+Gdy któryś test się nie uda, kod wyjścia to 1, więc CI może z tego korzystać. `finch run` i `finch build` pomijają
+bloki test. `assert(warunek)` działa też poza testami: zatrzymuje program komunikatem `assert failed`, gdy warunek jest fałszywy.
## 28. Projekt: finch.toml
-`finch.toml` obok kodu mówi raz to, co inaczej trzeba by podawać w każdym poleceniu:
+Plik `finch.toml` obok kodu zapisuje raz to, co inaczej trzeba by podawać przy każdym poleceniu:
```toml
-name = "gra" # program, który robi finch build (z entry)
-entry = "src/main.fch" # gdy finch run / build / test nie dostaną pliku
-paths = ["src", "lib"] # foldery z modułami Fincha
-include = ["vendor/SDL3/include"] # foldery z nagłówkami C (też dla własnych .c / .cpp)
-links = ["SDL3", "native/glue.cpp"] # biblioteki i własne pliki .c / .cpp / .o / .a
-libdirs = ["vendor/SDL3/lib"] # foldery z bibliotekami
-cflags = ["-DFAST"] # dla własnych plików .c / .cpp
+name = "gra" # program, który zbuduje finch build (z pliku entry)
+entry = "src/main.fch" # używany, gdy finch run / build / test nie dostaną pliku
+paths = ["src", "lib"] # katalogi z modułami Fincha
+include = ["vendor/SDL3/include"] # katalogi z nagłówkami C (także dla twoich plików .c / .cpp)
+links = ["SDL3", "native/glue.cpp"] # biblioteki i twoje pliki .c / .cpp / .o / .a
+libdirs = ["vendor/SDL3/lib"] # katalogi z bibliotekami
+cflags = ["-DFAST"] # dla twoich plików .c / .cpp
ldflags = ["-Wl,-rpath,$ORIGIN"] # dla linkera
+cpu = "native" # to samo co --cpu native (rozdział 31)
[target.windows] # dodawane tylko przy budowaniu na Windowsa
links = ["ws2_32"]
@@ -1208,8 +1241,8 @@ libdirs = ["vendor/SDL3/lib/x64"]
libdirs = ["vendor/SDL3/lib/arm64"]
```
-Foldery liczą się od `finch.toml`. Finch szuka go w folderze pliku i w folderach wyżej, więc
-`finch run src/narzedzia/edytor.fch` wewnątrz projektu też z niego korzysta. Czytana jest tylko taka część TOML:
+Ścieżki są liczone względem `finch.toml`. Finch szuka go w katalogu pliku i w katalogach nad nim, więc
+`finch run src/narzedzia/edytor.fch` w środku projektu też z niego skorzysta. Z formatu TOML czytane jest tylko tyle:
`[sekcje]`, `klucz = "tekst"`, `klucz = ["a", "b"]` (także w kilku liniach), liczby, `true`/`false` i komentarze `#`.
## 29. Wątki
@@ -1222,10 +1255,10 @@ struct Kawalek {
float szczyt
}
-fn analizuj(ptr arg) { // funkcja wątku bierze jeden ptr
+fn analizuj(ptr arg) { // funkcja wątku przyjmuje jeden ptr
ptr[Kawalek] k = arg
- for s in k.probki {
- k.szczyt = max(k.szczyt, abs(s))
+ for p in k.probki {
+ k.szczyt = max(k.szczyt, abs(p))
}
}
@@ -1239,65 +1272,143 @@ fn main() {
})
}
for i in 0..dzialajace.len {
- dzialajace[i].join() // poczekaj na wszystkie
+ dzialajace[i].join() // czekamy na wszystkie
}
}
```
-To, co dostaje wątek, jest współdzielone, nie kopiowane, więc:
+To, co dostaje wątek, jest współdzielone, a nie kopiowane, więc:
-- musi żyć do `join()` (tu `kawalki` żyje do końca `main`);
-- nie zmieniaj tablicy, w którą wskazują wątki (bez `push`), dopóki się nie skończą;
-- to, co zmienia więcej niż jeden wątek, idzie pod blokadę albo jest liczbą zmienianą przez `atomic_...`.
+- musi istnieć aż do `join()` (tutaj `kawalki` żyje do końca `main`);
+- nie zmieniaj tablicy, do której wskazują wątki (żadnego `push`), dopóki nie zostaną połączone;
+- to, co zmienia więcej niż jeden wątek, trzeba chronić blokadą albo musi to być liczba zmieniana przez `atomic_...`.
```c
m := thread.mutex()
defer m.close()
m.lock()
-dziennik.push("gotowe") // jeden wątek naraz między lock a unlock
+dziennik.push("gotowe") // między lock a unlock pracuje tylko jeden wątek naraz
m.unlock()
atomic_add(addr(gotowe), 1) // gotowe to int wspólny dla wątków
n := atomic_load(addr(gotowe))
```
-`atomic_add`, `atomic_sub`, `atomic_swap` zwracają starą wartość; `atomic_cas(p, stara, nowa)` wstawia `nowa` tylko
-wtedy, gdy jest tam `stara`, i mówi, czy to zrobił. `thread.cpus()` mówi, ile wątków komputer wykonuje naraz.
+`atomic_add`, `atomic_sub` i `atomic_swap` zwracają poprzednią wartość; `atomic_cas(p, stara, nowa)` wpisuje `nowa`
+tylko wtedy, gdy jest tam `stara`, i mówi, czy to zrobiło. `thread.cpus()` mówi, ile wątków komputer wykonuje naraz.
## 30. C++, Rust i Android
-Finch rozmawia z innymi językami przez C, jak każdy język.
+Finch rozmawia z innymi językami przez C, tak jak każdy język.
-**C++**: to, co woła Finch, wystaw przez nagłówek C, z `extern "C"` po stronie C++:
+**C++**: to, co ma wołać Finch, wystaw przez nagłówek C, z `extern "C"` po stronie C++:
```cpp
// glue.h
#ifdef __cplusplus
extern "C" {
#endif
-int physics_step(float dt);
+int krok_fizyki(float dt);
#ifdef __cplusplus
}
#endif
```
-i `import "glue.h"` z `link "glue.cpp"` (albo `links = ["glue.cpp"]` w `finch.toml`). Plik `.cpp` buduje kompilator C++
-(`clang++` albo `g++`, albo `FINCH_CXX`), który wtedy też linkuje, więc biblioteka C++ dochodzi sama.
-Bibliotekę C++ z API w C (Box2D 3, JoltC dla Jolt, wrapper C dla PhysX) importuje się jak każdą bibliotekę C.
+i `import "glue.h"` z `link "glue.cpp"` (albo `links = ["glue.cpp"]` w `finch.toml`). Plik `.cpp` kompiluje kompilator
+C++ (`clang++` albo `g++`, albo `FINCH_CXX`), który potem też linkuje, więc biblioteka C++ dołącza się sama.
+Bibliotekę C++ z API w C (Box2D 3, JoltC dla Jolta, nakładka C na PhysX) importuje się jak każdą bibliotekę C.
-**Rust**: zrób `staticlib` i wystaw funkcje C:
+**Rust**: zbuduj `staticlib` i wystaw funkcje C:
```rust
// Cargo.toml: [lib] crate-type = ["staticlib"]
#[no_mangle]
-pub extern "C" fn path_len(x: f32, y: f32) -> f32 { (x * x + y * y).sqrt() }
+pub extern "C" fn dlugosc(x: f32, y: f32) -> f32 { (x * x + y * y).sqrt() }
```
-Napisz nagłówek (`float path_len(float x, float y);` albo niech napisze go `cbindgen`), potem `import "pathfind.h"` i
-`link "target/release/libpathfind.a"`. Na Linuksie statyczna biblioteka Rusta potrzebuje też `links = ["dl", "pthread"]`.
+Napisz nagłówek (`float dlugosc(float x, float y);` albo wygeneruj go przez `cbindgen`), a potem `import "trasa.h"`
+i `link "target/release/libtrasa.a"`. Na Linuksie biblioteka statyczna z Rusta potrzebuje też `links = ["dl", "pthread"]`.
-**Android**: `finch build gra.fch --target android --shared` robi `libgra.so` dla 64-bitowego Androida na ARM (API 24
-i wyżej) clangiem z NDK, znalezionym przez `ANDROID_NDK_HOME` (albo `FINCH_CC`). Wczytaj go z aplikacji, na przykład
-przez projekt Androida z SDL3 (`SDL_main` biblioteki to `export fn`). To nowość, sprawdzona tylko częściowo: kod Fincha
-kompiluje się dla Androida (dane wątków idą przez emulowane TLS, którego wymaga starszy Android), ale linkowanie z NDK
-i uruchomienie na urządzeniu nie były jeszcze próbowane.
+**Android**: `finch build gra.fch --target android --shared` tworzy `libgra.so` dla 64-bitowego Androida na ARM
+(API 24 i nowsze) clangiem z NDK, którego Finch szuka przez `ANDROID_NDK_HOME` (albo `FINCH_CC`). Wczytaj ją z aplikacji,
+np. przez projekt Androida z SDL3 (`SDL_main` biblioteki to `export fn`). Dane lokalne dla wątków idą przez emulowane
+TLS, którego wymagają starsze wersje Androida.
+
+---
+
+## 31. Wektory (SIMD)
+
+Procesor potrafi dodać albo pomnożyć kilka liczb jedną instrukcją: 4 liczby `float` naraz przez SSE, 8 przez AVX2,
+16 przez AVX-512, 4 przez NEON na ARM. To SIMD (jedna instrukcja, wiele danych) i to dzięki temu filtry obrazu,
+dźwięk, fizyka i sieci neuronowe są szybkie. W Finchu to po prostu typ: **`f32x8` to 8 liczb `f32` obok siebie**.
+
+```c
+a := f32x4(1.0, 2.0, 3.0, 4.0)
+b := f32x4(0.5) // 0.5 w każdym elemencie
+print(a + b) // f32x4(1.5, 2.5, 3.5, 4.5)
+print(a * 2.0) // zwykła liczba działa na każdy element: f32x4(2, 4, 6, 8)
+print(a.sum(), a[2], a.len) // 10 3 4
+```
+
+**Typy** zapisuje się jako typ liczby, `x` i liczbę elementów: `f32x4` `f32x8` `f32x16`, `f64x2` `f64x4` `f64x8`,
+`i8x16` `i8x32` `i8x64`, `i16x8` `i16x16`, `i32x4` `i32x8` `i32x16`, `i64x2` `i64x4` i to samo z `u` (bez znaku).
+Od 2 do 64 elementów, razem najwyżej 512 bitów. Wektor to zwykła wartość, tak jak `int`: może być zmienną, polem
+struktury, elementem tablicy, wartością w mapie i parametrem funkcji, a `print` i `str` go pokazują.
+
+**Tworzenie:**
+
+| | |
+|---|---|
+| `f32x8(x)` | `x` w każdym elemencie |
+| `f32x8(a, b, c, d, e, f, g, h)` | po jednej liczbie na element |
+| `f32x8(lista, i)` | `lista[i]` … `lista[i+7]` z `[]f32` (albo przez `ptr[f32]`); zatrzymuje program, jeśli nie wszystkie są w liście |
+| `f32x8(v)` | inny 8-elementowy wektor przeliczony element po elemencie: `f32x8(calkowite)`, `i32x8(ulamki)` (ucina części ułamkowe), `i16x8(i32ki)` |
+
+**Działania:**
+
+| | |
+|---|---|
+| `+ - * /` | element po elemencie (`/` tylko na liczbach zmiennoprzecinkowych: x86 i ARM nie dzielą liczb całkowitych element po elemencie) |
+| `& \| ^ << >> ~` | element po elemencie, na liczbach całkowitych |
+| `-v` | każdy element ze zmienionym znakiem |
+| `v == w`, `v != w` | `true`, jeśli wszystkie elementy są równe / jeśli któryś się różni |
+| `v[i]`, `v[i] = x`, `v.len` | jeden element; liczba elementów |
+| `min(v, w)`, `max(v, w)`, `abs(v)`, `clamp(v, lo, hi)` | element po elemencie; `lo`, `hi` i `w` mogą być zwykłymi liczbami |
+| `v.sum()`, `v.min()`, `v.max()` | wszystkie elementy złożone w jedną liczbę |
+| `v.dot(w)` | `(v * w).sum()` |
+| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | każdy element (liczby zmiennoprzecinkowe) |
+| `v.fma(b, c)` | `v * b + c`, jedną instrukcją tam, gdzie procesor ją ma |
+| `v.store(lista, i)` | zapisuje elementy do `lista[i]` … (albo przez `ptr`) |
+
+`<` i inne porównania dałyby osobną odpowiedź dla każdego elementu, więc ich nie ma; użyj `min`, `max` albo `v[i] < w[i]`.
+
+**Prawdziwa pętla.** Zwykły schemat: tyle całych wektorów, ile się zmieści, a resztę po jednej liczbie.
+
+```c
+fn iloczyn([]f32 a, []f32 b) -> f32 {
+ suma := f32x8(0.0)
+ i := 0
+ while i + 8 <= a.len {
+ suma = f32x8(a, i).fma(f32x8(b, i), suma)
+ i += 8
+ }
+ wynik := suma.sum()
+ while i < a.len {
+ wynik += a[i] * b[i]
+ i += 1
+ }
+ return wynik
+}
+```
+
+`examples/simd.fch` mierzy czas tej pętli i porównuje go ze zwykłą.
+
+**Jakie instrukcje.** Domyślnie program działa na każdym procesorze swojego systemu (na x86-64 oznacza to SSE2, więc
+`f32x8` liczy się w dwóch połówkach). `--cpu native` (albo `cpu = "native"` w `finch.toml`) pozwala LLVM użyć wszystkiego,
+co ma procesor, na którym budujesz: AVX2 i FMA, AVX-512 albo nowszych części NEON na ARM. Taki program potrzebuje potem
+podobnego procesora. Można też wybrać poziom: `--cpu x86-64-v3` (AVX2, większość komputerów od 2015 roku),
+`--cpu x86-64-v4` (AVX-512), `--cpu apple-m1`.
+
+Wektory szersze niż rejestry procesora LLVM dzieli na części, a ten sam kod buduje się tak samo na x86-64 i na ARM64.
+`v.sum()` na liczbach zmiennoprzecinkowych dodaje elementy w kolejności, która jest najszybsza, więc ostatnie cyfry mogą
+się różnić od dodawania ich po kolei.
diff --git a/src/embedded.fch b/src/embedded.fch
index cfef97d..68230e4 100644
--- a/src/embedded.fch
+++ b/src/embedded.fch
@@ -20,7 +20,7 @@ fn names() -> []str {
fn texts() -> []str {
[]str list
- list.push("// The Finch runtime: string and array helpers, input, files and runtime errors.\n// It is embedded in the compiler, compiled once into a cache folder, and linked\n// statically into every Finch program. Everything here is plain C with no state.\n//\n// Layout shared with the compiler (src/codegen.cpp):\n// str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated\n// cap > 0 : heap memory owned by the value (cap bytes allocated)\n// cap == 0 : a string literal, valid forever, never written to\n// cap == -1: borrowed from C, must be copied before it is kept\n// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0\n// map[K]V: see \"maps\" below\n\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#ifdef _WIN32\n#define WIN32_LEAN_AND_MEAN\n#define NOMINMAX\n#include \n#include \n#include \n#else\n#include \n#include \n#include \n#include \n#include \n#endif\n\ntypedef struct {\n char *ptr;\n int64_t len;\n int64_t cap;\n} FStr;\n\ntypedef struct {\n void *ptr;\n int64_t len;\n int64_t cap;\n} FArr;\n\nstatic char empty[1] = \"\";\n\n// ---------- runtime errors ----------\n\n// A program that loads Finch libraries (finch build --shared) can take runtime errors over\n// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler\n// gets the message and must not return (longjmp out, or exit itself).\nstatic void (*fatal_hook)(const char *msg);\nvoid finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; }\n\n// The chain of Finch calls, kept by the program itself (see @finch_trace in the compiler's output):\n// each frame is { the caller's frame, the function's name, its file, the line it is at }.\ntypedef struct FFrame {\n struct FFrame *prev;\n const char *name;\n const char *file;\n int64_t line;\n} FFrame;\nstatic void *(*trace_top)(void);\nstatic void (*trace_set)(void *);\nvoid finch_trace_init(void *(*top)(void), void (*set)(void *)) {\n trace_top = top;\n trace_set = set;\n}\n\nstatic void print_trace(void) {\n if (!trace_top) return;\n FFrame *f = trace_top();\n for (int n = 0; f; n++, f = f->prev) {\n if (n == 30) {\n fprintf(stderr, \" ...\\n\");\n break;\n }\n fprintf(stderr, \" in %s (%s:%lld)\\n\", f->name, f->file, (long long)f->line);\n }\n}\n\nstatic __attribute__((noreturn)) void fatal(const char *msg) {\n if (fatal_hook) fatal_hook(msg);\n fflush(stdout);\n fprintf(stderr, \"%s\\n\", msg);\n print_trace();\n exit(1);\n}\n\n__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) {\n char full[1024];\n snprintf(full, sizeof full, \"%s:%lld: runtime error: %s\", file, (long long)line, msg);\n fflush(stdout);\n fatal(full);\n}\n\n__attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {\n char msg[128];\n snprintf(msg, sizeof msg, \"index %lld is out of range (the length is %lld)\", (long long)i, (long long)len);\n finch_panic(file, line, msg);\n}\n\nstatic void *must(void *p) {\n if (!p) {\n fflush(stdout);\n fatal(\"runtime error: out of memory\");\n }\n return p;\n}\n\nvoid *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); }\nvoid *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); }\n\n// ---------- assert and finch test ----------\n\n__attribute__((noreturn, cold)) void finch_assert_failed(const char *file, int64_t line, const FStr *why) {\n char msg[600];\n if (why->len) snprintf(msg, sizeof msg, \"assert failed: %.*s\", (int)(why->len > 500 ? 500 : why->len), why->ptr);\n else snprintf(msg, sizeof msg, \"assert failed\");\n finch_panic(file, line, msg);\n}\n\n// Each test runs on its own: a runtime error (a failed assert too) ends that test, and the next one runs.\nstatic jmp_buf test_jump;\nstatic char test_message[1100];\nstatic int64_t tests_passed, tests_failed;\n\nstatic void test_fatal(const char *msg) {\n snprintf(test_message, sizeof test_message, \"%s\", msg);\n longjmp(test_jump, 1);\n}\n\nvoid finch_test_run(const char *name, void (*fn)(void)) {\n void (*saved)(const char *) = fatal_hook;\n void *chain = trace_top ? trace_top() : NULL;\n test_message[0] = 0;\n fatal_hook = test_fatal;\n int failed = setjmp(test_jump);\n if (!failed) fn();\n fatal_hook = saved;\n if (trace_set) trace_set(chain); // a test that stopped early left its calls on the chain\n fflush(stdout);\n if (failed) {\n tests_failed++;\n printf(\"FAIL %s\\n %s\\n\", name, test_message);\n } else {\n tests_passed++;\n printf(\"ok %s\\n\", name);\n }\n fflush(stdout);\n}\n\nint finch_test_summary(void) {\n printf(\"\\n%lld passed, %lld failed\\n\", (long long)tests_passed, (long long)tests_failed);\n return tests_failed ? 1 : 0;\n}\n\n// ---------- strings ----------\n\nstatic void str_heap(FStr *out, const char *p, int64_t len) {\n char *m = finch_alloc(len + 1);\n if (len) memcpy(m, p, (size_t)len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void str_static(FStr *out, const char *lit) {\n out->ptr = (char *)lit;\n out->len = (int64_t)strlen(lit);\n out->cap = 0;\n}\n\n// A copy that can be kept: literals are shared, everything else is duplicated.\nvoid finch_str_copy(FStr *out, const FStr *s) {\n if (s->cap == 0) *out = *s;\n else str_heap(out, s->ptr, s->len);\n}\n\n// Make `s` writable (before s[i] = c).\nvoid finch_str_own(FStr *s) {\n if (s->cap > 0) return;\n FStr t;\n str_heap(&t, s->ptr, s->len);\n *s = t;\n}\n\nvoid finch_str_drop(FStr *s) {\n if (s->cap > 0) free(s->ptr);\n}\n\nvoid finch_str_from_c(FStr *out, const char *p) {\n if (!p) {\n str_static(out, empty);\n return;\n }\n out->ptr = (char *)p;\n out->len = (int64_t)strlen(p);\n out->cap = -1;\n}\n\nvoid finch_str_concat(FStr *out, const FStr *a, const FStr *b) {\n int64_t len = a->len + b->len;\n char *m = finch_alloc(len + 1);\n memcpy(m, a->ptr, (size_t)a->len);\n memcpy(m + a->len, b->ptr, (size_t)b->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nint finch_str_eq(const FStr *a, const FStr *b) {\n return a->len == b->len && memcmp(a->ptr, b->ptr, (size_t)a->len) == 0;\n}\n\nint64_t finch_str_cmp(const FStr *a, const FStr *b) {\n int64_t n = a->len < b->len ? a->len : b->len;\n int c = memcmp(a->ptr, b->ptr, (size_t)n);\n if (c) return c < 0 ? -1 : 1;\n return a->len < b->len ? -1 : a->len > b->len;\n}\n\nvoid finch_str_from_int(FStr *out, int64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%lld\", (long long)v);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_uint(FStr *out, uint64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%llu\", (unsigned long long)v);\n str_heap(out, buf, n);\n}\n\n// The shortest text that reads back as the same number (as an f32 when `single`):\n// 0.1 -> \"0.1\", 1.0/3 -> \"0.3333333333333333\", 1e8 -> \"100000000\", 1e20 -> \"1e+20\".\nstatic int fmt_float(char *buf, size_t size, double v, int single) {\n if (isnan(v)) return snprintf(buf, size, \"nan\");\n if (isinf(v)) return snprintf(buf, size, v < 0 ? \"-inf\" : \"inf\");\n int p = 1;\n for (; p < 17; p++) {\n snprintf(buf, size, \"%.*e\", p - 1, v);\n double back = strtod(buf, NULL);\n if (single ? (float)back == (float)v : back == v) break;\n }\n int exp = atoi(strchr(buf, 'e') + 1);\n if (exp < -5 || exp >= 17) return snprintf(buf, size, \"%.*g\", p, v);\n int decimals = p - 1 - exp;\n return snprintf(buf, size, \"%.*f\", decimals > 0 ? decimals : 0, v);\n}\n\nvoid finch_str_from_float(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 0);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_f32(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 1);\n str_heap(out, buf, n);\n}\n\n// str(x, 2): a fixed number of digits after the point\nvoid finch_str_fixed(FStr *out, double v, int64_t digits) {\n char buf[400];\n if (digits < 0) digits = 0;\n if (digits > 60) digits = 60;\n int n = snprintf(buf, sizeof buf, \"%.*f\", (int)digits, v);\n str_heap(out, buf, n);\n}\n\nvoid finch_print_float(double v, int32_t single) {\n char buf[64];\n fmt_float(buf, sizeof buf, v, single);\n fputs(buf, stdout);\n}\n\nvoid finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }\n\nvoid finch_str_from_bool(FStr *out, int b) { str_static(out, b ? \"true\" : \"false\"); }\n\nstatic void number_error(char *msg, size_t size, const FStr *s, const char *kind) {\n snprintf(msg, size, \"can't turn \\\"%.*s\\\" into %s\", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : \"\", kind);\n}\n\nstatic int parse_int(const FStr *s, int64_t *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n errno = 0;\n long long v = strtoll(p, &end, 10);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end || errno == ERANGE);\n}\n\nstatic int parse_float(const FStr *s, double *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n double v = strtod(p, &end);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end);\n}\n\nint64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {\n int64_t v;\n if (!parse_int(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\ndouble finch_str_to_float(const FStr *s, const char *file, int64_t line) {\n double v;\n if (!parse_float(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\n// ---------- errors as values: int(s) or 0, try read_file(p) ... ----------\n// Each gives back 1 on success; on failure 0, with the message in *err.\n\nstatic void set_error(FStr *err, const char *msg) { str_heap(err, msg, (int64_t)strlen(msg)); }\n\nint finch_str_try_int(const FStr *s, int64_t *out, FStr *err) {\n if (parse_int(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nint finch_str_try_float(const FStr *s, double *out, FStr *err) {\n if (parse_float(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nstatic void file_error(FStr *err, const char *what, const FStr *path) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't %s the file \\\"%.*s\\\": %s\", what, (int)(path->len > 200 ? 200 : path->len),\n path->ptr ? path->ptr : \"\", strerror(errno));\n set_error(err, msg);\n}\n\n// when `fn main() -> !` fails\nvoid finch_report_error(const FStr *msg) {\n fflush(stdout);\n fprintf(stderr, \"error: %.*s\\n\", (int)msg->len, msg->ptr ? msg->ptr : \"\");\n finch_str_drop((FStr *)msg);\n}\n\nvoid finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {\n if (start < 0 || end > s->len || start > end) {\n char msg[160];\n snprintf(msg, sizeof msg, \"sub(%lld, %lld) is out of range (the length is %lld)\", (long long)start,\n (long long)end, (long long)s->len);\n finch_panic(file, line, msg);\n }\n str_heap(out, s->ptr + start, end - start);\n}\n\nint64_t finch_str_find(const FStr *s, const FStr *t) {\n if (t->len == 0) return 0;\n for (int64_t i = 0; i + t->len <= s->len; i++)\n if (s->ptr[i] == t->ptr[0] && memcmp(s->ptr + i, t->ptr, (size_t)t->len) == 0) return i;\n return -1;\n}\n\nint finch_str_starts(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr, t->ptr, (size_t)t->len) == 0;\n}\n\nint finch_str_ends(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr + s->len - t->len, t->ptr, (size_t)t->len) == 0;\n}\n\nvoid finch_str_trim(FStr *out, const FStr *s) {\n int64_t a = 0, b = s->len;\n while (a < b && isspace((unsigned char)s->ptr[a])) a++;\n while (b > a && isspace((unsigned char)s->ptr[b - 1])) b--;\n str_heap(out, s->ptr + a, b - a);\n}\n\nvoid finch_str_upper(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)toupper((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_lower(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)tolower((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_replace(FStr *out, const FStr *s, const FStr *from, const FStr *to) {\n if (from->len == 0) {\n str_heap(out, s->ptr, s->len);\n return;\n }\n int64_t count = 0;\n for (int64_t i = 0; i + from->len <= s->len;) {\n if (memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n count++;\n i += from->len;\n } else {\n i++;\n }\n }\n int64_t len = s->len + count * (to->len - from->len);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < s->len;) {\n if (i + from->len <= s->len && memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n memcpy(w, to->ptr, (size_t)to->len);\n w += to->len;\n i += from->len;\n } else {\n *w++ = s->ptr[i++];\n }\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len);\n\n// \"a,b,,c\".split(\",\") -> [\"a\", \"b\", \"\", \"c\"]; split(\"\") gives single characters.\nvoid finch_str_split(FArr *out, const FStr *s, const FStr *sep) {\n out->ptr = NULL;\n out->len = out->cap = 0;\n if (sep->len == 0) {\n for (int64_t i = 0; i < s->len; i++) arr_push_str(out, s->ptr + i, 1);\n return;\n }\n int64_t start = 0;\n for (int64_t i = 0; i + sep->len <= s->len;) {\n if (memcmp(s->ptr + i, sep->ptr, (size_t)sep->len) == 0) {\n arr_push_str(out, s->ptr + start, i - start);\n i += sep->len;\n start = i;\n } else {\n i++;\n }\n }\n arr_push_str(out, s->ptr + start, s->len - start);\n}\n\nvoid finch_str_join(FStr *out, const FArr *parts, const FStr *sep) {\n const FStr *p = parts->ptr;\n int64_t len = 0;\n for (int64_t i = 0; i < parts->len; i++) len += p[i].len + (i ? sep->len : 0);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < parts->len; i++) {\n if (i) {\n memcpy(w, sep->ptr, (size_t)sep->len);\n w += sep->len;\n }\n memcpy(w, p[i].ptr, (size_t)p[i].len);\n w += p[i].len;\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_repeat(FStr *out, const FStr *s, int64_t n) {\n if (n < 0) n = 0;\n int64_t len = s->len * n;\n char *m = finch_alloc(len + 1);\n for (int64_t i = 0; i < n; i++) memcpy(m + i * s->len, s->ptr, (size_t)s->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }\n\n// ---------- building text: print(x) and str(x) of arrays, maps and structs ----------\n// A buffer starts as an empty str and grows on the heap.\n\nvoid finch_buf_add(FStr *b, const char *p, int64_t n) {\n if (b->len + n + 1 > b->cap) {\n int64_t cap = b->cap > 0 ? b->cap * 2 : 32;\n while (cap < b->len + n + 1) cap *= 2;\n b->ptr = must(realloc(b->cap > 0 ? b->ptr : NULL, (size_t)cap));\n b->cap = cap;\n }\n if (n) memcpy(b->ptr + b->len, p, (size_t)n);\n b->len += n;\n b->ptr[b->len] = 0;\n}\n\nstatic void buf_fmt(FStr *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));\nstatic void buf_fmt(FStr *b, const char *fmt, ...) {\n char t[64];\n va_list ap;\n va_start(ap, fmt);\n int n = vsnprintf(t, sizeof t, fmt, ap);\n va_end(ap);\n finch_buf_add(b, t, n);\n}\n\nvoid finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, \"%lld\", (long long)v); }\nvoid finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, \"%llu\", (unsigned long long)v); }\nvoid finch_buf_float(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 0));\n}\nvoid finch_buf_f32(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 1));\n}\nvoid finch_buf_bool(FStr *b, int v) { finch_buf_add(b, v ? \"true\" : \"false\", v ? 4 : 5); }\nvoid finch_buf_ptr(FStr *b, const void *p) {\n if (p) buf_fmt(b, \"0x%llx\", (unsigned long long)(uintptr_t)p);\n else finch_buf_add(b, \"null\", 4);\n}\n\nvoid finch_buf_char(FStr *b, char c, int quoted) {\n if (quoted) finch_buf_add(b, \"'\", 1);\n finch_buf_add(b, &c, 1);\n if (quoted) finch_buf_add(b, \"'\", 1);\n}\n\nvoid finch_buf_str(FStr *b, const FStr *s, int quoted) {\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n finch_buf_add(b, s->ptr, s->len);\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n}\n\n// print the buffer and free it\nvoid finch_buf_print(FStr *b) {\n if (b->len) fwrite(b->ptr, 1, (size_t)b->len, stdout);\n if (b->cap > 0) free(b->ptr);\n}\n\n// ---------- arrays ----------\n\nvoid finch_arr_reserve(FArr *a, int64_t need, int64_t es) {\n if (need <= a->cap) return;\n int64_t cap = a->cap * 2;\n if (cap < 4) cap = 4;\n if (cap < need) cap = need;\n a->ptr = must(realloc(a->cap > 0 ? a->ptr : NULL, (size_t)(cap * es)));\n a->cap = cap;\n}\n\nvoid finch_arr_make(FArr *out, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n out->ptr = len ? finch_zalloc(len * es) : NULL;\n out->len = len;\n out->cap = len;\n}\n\n// grow or shrink to `len`; new elements are zero\nvoid finch_arr_resize(FArr *a, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n finch_arr_reserve(a, len, es);\n if (len > a->len) memset((char *)a->ptr + a->len * es, 0, (size_t)((len - a->len) * es));\n a->len = len;\n}\n\n// a byte-for-byte copy of the elements (the compiler deep-copies owning elements afterwards)\nvoid finch_arr_clone_raw(FArr *out, const FArr *a, int64_t es) {\n out->len = a->len;\n out->cap = a->len;\n out->ptr = a->len ? finch_alloc(a->len * es) : NULL;\n if (a->len) memcpy(out->ptr, a->ptr, (size_t)(a->len * es));\n}\n\nvoid finch_arr_free(FArr *a) {\n if (a->cap > 0) free(a->ptr);\n}\n\n// open a hole at i (0 <= i <= len), len grows by one\nvoid finch_arr_insert_gap(FArr *a, int64_t i, int64_t es, const char *file, int64_t line) {\n if (i < 0 || i > a->len) finch_panic_index(file, line, i, a->len);\n finch_arr_reserve(a, a->len + 1, es);\n char *p = a->ptr;\n memmove(p + (i + 1) * es, p + i * es, (size_t)((a->len - i) * es));\n a->len++;\n}\n\n// close the hole at i (the element was already moved out), len shrinks by one\nvoid finch_arr_remove_gap(FArr *a, int64_t i, int64_t es) {\n char *p = a->ptr;\n memmove(p + i * es, p + (i + 1) * es, (size_t)((a->len - i - 1) * es));\n a->len--;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len) {\n finch_arr_reserve(a, a->len + 1, (int64_t)sizeof(FStr));\n str_heap((FStr *)a->ptr + a->len, p, len);\n a->len++;\n}\n\nvoid finch_bytes_from_str(FArr *out, const FStr *s) {\n out->len = out->cap = s->len;\n out->ptr = s->len ? finch_alloc(s->len) : NULL;\n if (s->len) memcpy(out->ptr, s->ptr, (size_t)s->len);\n}\n\n// ---------- program arguments, input and files ----------\n\nvoid finch_args(FArr *out, int argc, char **argv) {\n out->len = out->cap = argc;\n out->ptr = finch_alloc((int64_t)sizeof(FStr) * (argc ? argc : 1));\n for (int i = 0; i < argc; i++) str_heap((FStr *)out->ptr + i, argv[i], (int64_t)strlen(argv[i]));\n}\n\n// after main: the arguments go\nvoid finch_args_free(FArr *a) {\n for (int64_t i = 0; i < a->len; i++) finch_str_drop((FStr *)a->ptr + i);\n finch_arr_free(a);\n}\n\n// input(\"Name? \") prints the prompt and reads one line, without the newline.\nvoid finch_input(FStr *out, const FStr *prompt) {\n if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);\n fflush(stdout);\n // read one line of any length (getline isn't available everywhere)\n size_t size = 128, n = 0;\n char *line = finch_alloc((int64_t)size);\n int c, got = 0;\n while ((c = fgetc(stdin)) != EOF) {\n got = 1;\n if (c == '\\n') break;\n if (n + 1 >= size) line = must(realloc(line, size *= 2));\n line[n++] = (char)c;\n }\n if (!got) {\n free(line);\n str_static(out, empty);\n return;\n }\n while (n > 0 && line[n - 1] == '\\r') n--;\n line[n] = 0;\n out->ptr = line;\n out->len = (int64_t)n;\n out->cap = (int64_t)size;\n}\n\nint finch_file_exists(const FStr *path) {\n FILE *f = fopen(path->ptr, \"rb\");\n if (f) fclose(f);\n return f != NULL;\n}\n\nint finch_try_read_file(FStr *out, const FStr *path, FStr *err) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n file_error(err, \"read\", path);\n return 0;\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n return 1;\n}\n\nvoid finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't read the file \\\"%.*s\\\": %s\", (int)(path->len > 200 ? 200 : path->len),\n path->ptr, strerror(errno));\n finch_panic(file, line, msg);\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n}\n\n// shell(\"ls -l\"): run a command, give back its exit code\nint64_t finch_shell(const FStr *cmd) {\n fflush(stdout);\n int status = system(cmd->ptr ? cmd->ptr : \"\");\n#ifdef _WIN32\n return status; // Windows gives the exit code directly\n#else\n if (status == -1) return -1;\n if (WIFEXITED(status)) return WEXITSTATUS(status);\n return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);\n#endif\n}\n\nint finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : \"\") == 0; }\n\nint finch_try_delete_file(const FStr *path, FStr *err) {\n if (remove(path->ptr ? path->ptr : \"\") == 0) return 1;\n file_error(err, \"delete\", path);\n return 0;\n}\n\n// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here\nvoid *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; }\n\nint finch_write_file(const FStr *path, const FStr *text) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"wb\");\n if (!f) return 0;\n size_t put = text->len ? fwrite(text->ptr, 1, (size_t)text->len, f) : 0;\n return fclose(f) == 0 && put == (size_t)text->len;\n}\n\nint finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {\n errno = 0;\n if (finch_write_file(path, text)) return 1;\n if (!errno) errno = EIO;\n file_error(err, \"write\", path);\n return 0;\n}\n\n// ---------- folders (std/fs.fch, through std/finch_fs.h) ----------\n\nint finch_fs_is_dir(const char *path) {\n#ifdef _WIN32\n struct _finddata_t d;\n intptr_t h = _findfirst(path, &d);\n if (h == -1) return 0;\n _findclose(h);\n return (d.attrib & _A_SUBDIR) != 0;\n#else\n struct stat st;\n return stat(path, &st) == 0 && S_ISDIR(st.st_mode);\n#endif\n}\n\nint finch_fs_exists(const char *path) {\n#ifdef _WIN32\n return _access(path, 0) == 0;\n#else\n struct stat st;\n return stat(path, &st) == 0;\n#endif\n}\n\nstatic int make_one_dir(const char *p) {\n#ifdef _WIN32\n return _mkdir(p) == 0 || errno == EEXIST;\n#else\n return mkdir(p, 0777) == 0 || errno == EEXIST;\n#endif\n}\n\n// mkdir -p: the parents too; 1 if the folder is there afterwards\nint finch_fs_mkdir(const char *path) {\n size_t n = strlen(path);\n char *p = finch_alloc((int64_t)n + 1);\n memcpy(p, path, n + 1);\n for (size_t i = 1; i < n; i++) {\n if (p[i] == '/' || p[i] == '\\\\') {\n if (p[i - 1] == ':') continue; /* C: is a drive */\n char c = p[i];\n p[i] = 0;\n make_one_dir(p);\n p[i] = c;\n }\n }\n int ok = make_one_dir(p) && finch_fs_is_dir(p);\n free(p);\n return ok;\n}\n\nint finch_fs_remove_dir(const char *path) {\n#ifdef _WIN32\n return _rmdir(path) == 0;\n#else\n return rmdir(path) == 0;\n#endif\n}\n\n// the names in a folder, one per call (\"\" at the end), without . and ..\ntypedef struct {\n#ifdef _WIN32\n intptr_t h;\n struct _finddata_t d;\n int first;\n#else\n DIR *d;\n#endif\n} FsList;\n\nvoid *finch_fs_list_open(const char *path) {\n FsList *l = finch_zalloc((int64_t)sizeof(FsList));\n#ifdef _WIN32\n size_t n = strlen(path);\n char *pat = finch_alloc((int64_t)n + 3);\n memcpy(pat, path, n);\n memcpy(pat + n, \"\\\\*\", 3);\n l->h = _findfirst(pat, &l->d);\n free(pat);\n if (l->h == -1) {\n free(l);\n return NULL;\n }\n l->first = 1;\n#else\n l->d = opendir(path);\n if (!l->d) {\n free(l);\n return NULL;\n }\n#endif\n return l;\n}\n\nconst char *finch_fs_list_next(void *handle) {\n FsList *l = handle;\n for (;;) {\n#ifdef _WIN32\n if (!l->first && _findnext(l->h, &l->d) != 0) return \"\";\n l->first = 0;\n const char *name = l->d.name;\n#else\n struct dirent *e = readdir(l->d);\n if (!e) return \"\";\n const char *name = e->d_name;\n#endif\n if (strcmp(name, \".\") != 0 && strcmp(name, \"..\") != 0) return name;\n }\n}\n\nvoid finch_fs_list_close(void *handle) {\n FsList *l = handle;\n#ifdef _WIN32\n _findclose(l->h);\n#else\n closedir(l->d);\n#endif\n free(l);\n}\n\n// where a program keeps its own files for this user (saves, settings); \"\" if unknown\n// Linux: $XDG_DATA_HOME or ~/.local/share macOS: ~/Library/Application Support Windows: %APPDATA%\nconst char *finch_fs_data_home(void) {\n static char buf[4096];\n const char *v;\n#if defined(_WIN32)\n v = getenv(\"APPDATA\");\n snprintf(buf, sizeof buf, \"%s\", v ? v : \"\");\n#elif defined(__APPLE__)\n v = getenv(\"HOME\");\n if (v) snprintf(buf, sizeof buf, \"%s/Library/Application Support\", v);\n else buf[0] = 0;\n#else\n v = getenv(\"XDG_DATA_HOME\");\n if (v && *v) snprintf(buf, sizeof buf, \"%s\", v);\n else if ((v = getenv(\"HOME\"))) snprintf(buf, sizeof buf, \"%s/.local/share\", v);\n else buf[0] = 0;\n#endif\n return buf;\n}\n\n// ---------- threads (std/thread.fch, through std/finch_thread.h) ----------\n\ntypedef struct {\n void (*fn)(void *);\n void *arg;\n} ThreadStart;\n\n#ifdef _WIN32\nstatic DWORD WINAPI thread_main(LPVOID p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return 0;\n}\n#else\nstatic void *thread_main(void *p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return NULL;\n}\n#endif\n\n// run fn(arg) on a new thread; NULL if it couldn't start\nvoid *finch_thread_spawn(void (*fn)(void *), void *arg) {\n ThreadStart *st = finch_alloc((int64_t)sizeof(ThreadStart));\n st->fn = fn;\n st->arg = arg;\n#ifdef _WIN32\n HANDLE h = CreateThread(NULL, 0, thread_main, st, 0, NULL);\n if (!h) {\n free(st);\n return NULL;\n }\n return h;\n#else\n pthread_t *t = finch_alloc((int64_t)sizeof(pthread_t));\n if (pthread_create(t, NULL, thread_main, st) != 0) {\n free(st);\n free(t);\n return NULL;\n }\n return t;\n#endif\n}\n\n// wait for the thread to end; 1 if that worked\nint finch_thread_join(void *t) {\n if (!t) return 0;\n#ifdef _WIN32\n int ok = WaitForSingleObject((HANDLE)t, INFINITE) == WAIT_OBJECT_0;\n CloseHandle((HANDLE)t);\n return ok;\n#else\n int ok = pthread_join(*(pthread_t *)t, NULL) == 0;\n free(t);\n return ok;\n#endif\n}\n\nvoid *finch_mutex_new(void) {\n#ifdef _WIN32\n CRITICAL_SECTION *m = finch_alloc((int64_t)sizeof(CRITICAL_SECTION));\n InitializeCriticalSection(m);\n#else\n pthread_mutex_t *m = finch_alloc((int64_t)sizeof(pthread_mutex_t));\n pthread_mutex_init(m, NULL);\n#endif\n return m;\n}\n\nvoid finch_mutex_lock(void *m) {\n#ifdef _WIN32\n EnterCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_lock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_unlock(void *m) {\n#ifdef _WIN32\n LeaveCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_unlock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_free(void *m) {\n if (!m) return;\n#ifdef _WIN32\n DeleteCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_destroy((pthread_mutex_t *)m);\n#endif\n free(m);\n}\n\nint finch_cpu_count(void) {\n#ifdef _WIN32\n SYSTEM_INFO si;\n GetSystemInfo(&si);\n return (int)si.dwNumberOfProcessors;\n#else\n long n = sysconf(_SC_NPROCESSORS_ONLN);\n return n > 0 ? (int)n : 1;\n#endif\n}\n\n// ---------- maps ----------\n// map[K]V = { entries; len; used; cap; index; icap; adds }\n// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.\n// A removed entry keeps its slot (hash 0) until the next resize packs the slots.\n// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.\n// adds: how many keys were ever added; a `for` over the map stops with an error if it changes.\n// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.\n// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).\n\ntypedef struct {\n char *entries;\n int64_t len, used, cap;\n int64_t *index;\n int64_t icap;\n int64_t adds;\n} FMap;\n\nstatic uint64_t map_hash(const void *key, int64_t ks, int kstr) {\n const unsigned char *p;\n size_t n;\n if (kstr) {\n const FStr *s = key;\n p = (const unsigned char *)s->ptr;\n n = (size_t)s->len;\n } else {\n p = key;\n n = (size_t)ks;\n }\n uint64_t h = 1469598103934665603ULL; // FNV-1a, then mixed so the low bits spread well\n for (size_t i = 0; i < n; i++) {\n h ^= p[i];\n h *= 1099511628211ULL;\n }\n h ^= h >> 31;\n h *= 0xbf58476d1ce4e5b9ULL;\n h ^= h >> 29;\n return h | 1; // 0 marks a removed entry\n}\n\nstatic int map_key_eq(const char *a, const void *b, int64_t ks, int kstr) {\n if (!kstr) return memcmp(a, b, (size_t)ks) == 0;\n const FStr *x = (const FStr *)a, *y = b;\n return x->len == y->len && (x->len == 0 || memcmp(x->ptr, y->ptr, (size_t)x->len) == 0);\n}\n\nstatic void map_reindex(FMap *m, int64_t es) {\n if (m->len != m->used) { // pack the slots of removed entries away\n int64_t j = 0;\n for (int64_t i = 0; i < m->used; i++) {\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == 0) continue;\n if (i != j) memmove(m->entries + j * es, e, (size_t)es);\n j++;\n }\n m->used = j;\n }\n int64_t icap = 16;\n while (icap < m->cap * 2) icap *= 2;\n free(m->index);\n m->index = finch_alloc(icap * (int64_t)sizeof(int64_t));\n memset(m->index, 0xff, (size_t)icap * sizeof(int64_t));\n m->icap = icap;\n for (int64_t i = 0; i < m->used; i++) {\n uint64_t h = *(uint64_t *)(m->entries + i * es);\n int64_t s = (int64_t)(h & (uint64_t)(icap - 1));\n while (m->index[s] != -1) s = (s + 1) & (icap - 1);\n m->index[s] = i;\n }\n}\n\n// the entry number of `key`, or -1\nint64_t finch_map_find(const FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr) {\n if (m->len == 0) return -1;\n uint64_t h = map_hash(key, ks, kstr);\n for (int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));; s = (s + 1) & (m->icap - 1)) {\n int64_t i = m->index[s];\n if (i == -1) return -1;\n if (i < 0) continue;\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == h && map_key_eq(e + 8, key, ks, kstr)) return i;\n }\n}\n\n// The entry for `key`. A new one is zeroed except its hash (*added = 1); the compiler then stores the key.\nint64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr, int32_t *added) {\n int64_t i = finch_map_find(m, key, es, ks, kstr);\n *added = i < 0;\n if (i >= 0) return i;\n if (m->used == m->cap) {\n if (m->len * 4 < m->cap * 3) { // many removed slots: pack them instead of growing\n map_reindex(m, es);\n } else {\n m->cap = m->cap ? m->cap * 2 : 8;\n m->entries = must(realloc(m->entries, (size_t)(m->cap * es)));\n map_reindex(m, es);\n }\n }\n uint64_t h = map_hash(key, ks, kstr);\n i = m->used++;\n char *e = m->entries + i * es;\n memset(e, 0, (size_t)es);\n *(uint64_t *)e = h;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);\n m->index[s] = i;\n m->len++;\n m->adds++;\n return i;\n}\n\n// Forget entry i (its key and value were already dropped by the compiler).\nvoid finch_map_remove_at(FMap *m, int64_t i, int64_t es) {\n char *e = m->entries + i * es;\n uint64_t h = *(uint64_t *)e;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] != i) s = (s + 1) & (m->icap - 1);\n m->index[s] = -2;\n *(uint64_t *)e = 0;\n if (--m->len == 0) {\n m->used = 0;\n memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n }\n}\n\nvoid finch_map_clear(FMap *m) {\n m->len = m->used = 0;\n if (m->index) memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n}\n\nvoid finch_map_free(FMap *m) {\n free(m->entries);\n free(m->index);\n}\n\n// A copy of the slots and the index; the compiler then copies the keys and values that own memory.\nvoid finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {\n *out = *m;\n if (!m->cap) return;\n out->entries = finch_alloc(m->cap * es);\n memcpy(out->entries, m->entries, (size_t)(m->used * es));\n out->index = finch_alloc(m->icap * (int64_t)sizeof(int64_t));\n memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));\n}\n\n__attribute__((noreturn, cold)) void finch_map_changed(const char *file, int64_t line) {\n finch_panic(file, line, \"a key was added to the map while looping over it (collect the new keys, and add them after the loop)\");\n}\n\n__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {\n char msg[200];\n snprintf(msg, sizeof msg, \"the key %.*s is not in the map (check with .has(key), or use .get(key, default))\",\n (int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : \"\");\n finch_panic(file, line, msg);\n}\n")
+ list.push("// The Finch runtime: string and array helpers, input, files and runtime errors.\n// It is embedded in the compiler, compiled once into a cache folder, and linked\n// statically into every Finch program. Everything here is plain C with no state.\n//\n// Layout shared with the compiler (src/gen.fch):\n// str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated\n// cap > 0 : heap memory owned by the value (cap bytes allocated)\n// cap == 0 : a string literal, valid forever, never written to\n// cap == -1: borrowed from C, must be copied before it is kept\n// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0\n// map[K]V: see \"maps\" below\n\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#ifdef _WIN32\n#define WIN32_LEAN_AND_MEAN\n#define NOMINMAX\n#include \n#include \n#include \n#else\n#include \n#include \n#include \n#include \n#include \n#endif\n\ntypedef struct {\n char *ptr;\n int64_t len;\n int64_t cap;\n} FStr;\n\ntypedef struct {\n void *ptr;\n int64_t len;\n int64_t cap;\n} FArr;\n\nstatic char empty[1] = \"\";\n\n// ---------- runtime errors ----------\n\n// A program that loads Finch libraries (finch build --shared) can take runtime errors over\n// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler\n// gets the message and must not return (longjmp out, or exit itself).\nstatic void (*fatal_hook)(const char *msg);\nvoid finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; }\n\n// The chain of Finch calls, kept by the program itself (see @finch_trace in the compiler's output):\n// each frame is { the caller's frame, the function's name, its file, the line it is at }.\ntypedef struct FFrame {\n struct FFrame *prev;\n const char *name;\n const char *file;\n int64_t line;\n} FFrame;\nstatic void *(*trace_top)(void);\nstatic void (*trace_set)(void *);\nvoid finch_trace_init(void *(*top)(void), void (*set)(void *)) {\n trace_top = top;\n trace_set = set;\n}\n\nstatic void print_trace(void) {\n if (!trace_top) return;\n FFrame *f = trace_top();\n for (int n = 0; f; n++, f = f->prev) {\n if (n == 30) {\n fprintf(stderr, \" ...\\n\");\n break;\n }\n fprintf(stderr, \" in %s (%s:%lld)\\n\", f->name, f->file, (long long)f->line);\n }\n}\n\nstatic __attribute__((noreturn)) void fatal(const char *msg) {\n if (fatal_hook) fatal_hook(msg);\n fflush(stdout);\n fprintf(stderr, \"%s\\n\", msg);\n print_trace();\n exit(1);\n}\n\n__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) {\n char full[1024];\n snprintf(full, sizeof full, \"%s:%lld: runtime error: %s\", file, (long long)line, msg);\n fflush(stdout);\n fatal(full);\n}\n\n__attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {\n char msg[128];\n snprintf(msg, sizeof msg, \"index %lld is out of range (the length is %lld)\", (long long)i, (long long)len);\n finch_panic(file, line, msg);\n}\n\nstatic void *must(void *p) {\n if (!p) {\n fflush(stdout);\n fatal(\"runtime error: out of memory\");\n }\n return p;\n}\n\nvoid *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); }\nvoid *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); }\n\n// ---------- assert and finch test ----------\n\n__attribute__((noreturn, cold)) void finch_assert_failed(const char *file, int64_t line, const FStr *why) {\n char msg[600];\n if (why->len) snprintf(msg, sizeof msg, \"assert failed: %.*s\", (int)(why->len > 500 ? 500 : why->len), why->ptr);\n else snprintf(msg, sizeof msg, \"assert failed\");\n finch_panic(file, line, msg);\n}\n\n// Each test runs on its own: a runtime error (a failed assert too) ends that test, and the next one runs.\nstatic jmp_buf test_jump;\nstatic char test_message[1100];\nstatic int64_t tests_passed, tests_failed;\n\nstatic void test_fatal(const char *msg) {\n snprintf(test_message, sizeof test_message, \"%s\", msg);\n longjmp(test_jump, 1);\n}\n\nvoid finch_test_run(const char *name, void (*fn)(void)) {\n void (*saved)(const char *) = fatal_hook;\n void *chain = trace_top ? trace_top() : NULL;\n test_message[0] = 0;\n fatal_hook = test_fatal;\n int failed = setjmp(test_jump);\n if (!failed) fn();\n fatal_hook = saved;\n if (trace_set) trace_set(chain); // a test that stopped early left its calls on the chain\n fflush(stdout);\n if (failed) {\n tests_failed++;\n printf(\"FAIL %s\\n %s\\n\", name, test_message);\n } else {\n tests_passed++;\n printf(\"ok %s\\n\", name);\n }\n fflush(stdout);\n}\n\nint finch_test_summary(void) {\n printf(\"\\n%lld passed, %lld failed\\n\", (long long)tests_passed, (long long)tests_failed);\n return tests_failed ? 1 : 0;\n}\n\n// ---------- strings ----------\n\nstatic void str_heap(FStr *out, const char *p, int64_t len) {\n char *m = finch_alloc(len + 1);\n if (len) memcpy(m, p, (size_t)len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void str_static(FStr *out, const char *lit) {\n out->ptr = (char *)lit;\n out->len = (int64_t)strlen(lit);\n out->cap = 0;\n}\n\n// A copy that can be kept: literals are shared, everything else is duplicated.\nvoid finch_str_copy(FStr *out, const FStr *s) {\n if (s->cap == 0) *out = *s;\n else str_heap(out, s->ptr, s->len);\n}\n\n// Make `s` writable (before s[i] = c).\nvoid finch_str_own(FStr *s) {\n if (s->cap > 0) return;\n FStr t;\n str_heap(&t, s->ptr, s->len);\n *s = t;\n}\n\nvoid finch_str_drop(FStr *s) {\n if (s->cap > 0) free(s->ptr);\n}\n\nvoid finch_str_from_c(FStr *out, const char *p) {\n if (!p) {\n str_static(out, empty);\n return;\n }\n out->ptr = (char *)p;\n out->len = (int64_t)strlen(p);\n out->cap = -1;\n}\n\nvoid finch_str_concat(FStr *out, const FStr *a, const FStr *b) {\n int64_t len = a->len + b->len;\n char *m = finch_alloc(len + 1);\n memcpy(m, a->ptr, (size_t)a->len);\n memcpy(m + a->len, b->ptr, (size_t)b->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nint finch_str_eq(const FStr *a, const FStr *b) {\n return a->len == b->len && memcmp(a->ptr, b->ptr, (size_t)a->len) == 0;\n}\n\nint64_t finch_str_cmp(const FStr *a, const FStr *b) {\n int64_t n = a->len < b->len ? a->len : b->len;\n int c = memcmp(a->ptr, b->ptr, (size_t)n);\n if (c) return c < 0 ? -1 : 1;\n return a->len < b->len ? -1 : a->len > b->len;\n}\n\nvoid finch_str_from_int(FStr *out, int64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%lld\", (long long)v);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_uint(FStr *out, uint64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%llu\", (unsigned long long)v);\n str_heap(out, buf, n);\n}\n\n// The shortest text that reads back as the same number (as an f32 when `single`):\n// 0.1 -> \"0.1\", 1.0/3 -> \"0.3333333333333333\", 1e8 -> \"100000000\", 1e20 -> \"1e+20\".\nstatic int fmt_float(char *buf, size_t size, double v, int single) {\n if (isnan(v)) return snprintf(buf, size, \"nan\");\n if (isinf(v)) return snprintf(buf, size, v < 0 ? \"-inf\" : \"inf\");\n int p = 1;\n for (; p < 17; p++) {\n snprintf(buf, size, \"%.*e\", p - 1, v);\n double back = strtod(buf, NULL);\n if (single ? (float)back == (float)v : back == v) break;\n }\n int exp = atoi(strchr(buf, 'e') + 1);\n if (exp < -5 || exp >= 17) return snprintf(buf, size, \"%.*g\", p, v);\n int decimals = p - 1 - exp;\n return snprintf(buf, size, \"%.*f\", decimals > 0 ? decimals : 0, v);\n}\n\nvoid finch_str_from_float(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 0);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_f32(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 1);\n str_heap(out, buf, n);\n}\n\n// str(x, 2): a fixed number of digits after the point\nvoid finch_str_fixed(FStr *out, double v, int64_t digits) {\n char buf[400];\n if (digits < 0) digits = 0;\n if (digits > 60) digits = 60;\n int n = snprintf(buf, sizeof buf, \"%.*f\", (int)digits, v);\n str_heap(out, buf, n);\n}\n\nvoid finch_print_float(double v, int32_t single) {\n char buf[64];\n fmt_float(buf, sizeof buf, v, single);\n fputs(buf, stdout);\n}\n\nvoid finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }\n\nvoid finch_str_from_bool(FStr *out, int b) { str_static(out, b ? \"true\" : \"false\"); }\n\nstatic void number_error(char *msg, size_t size, const FStr *s, const char *kind) {\n snprintf(msg, size, \"can't turn \\\"%.*s\\\" into %s\", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : \"\", kind);\n}\n\nstatic int parse_int(const FStr *s, int64_t *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n errno = 0;\n long long v = strtoll(p, &end, 10);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end || errno == ERANGE);\n}\n\nstatic int parse_float(const FStr *s, double *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n double v = strtod(p, &end);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end);\n}\n\nint64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {\n int64_t v;\n if (!parse_int(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\ndouble finch_str_to_float(const FStr *s, const char *file, int64_t line) {\n double v;\n if (!parse_float(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\n// ---------- errors as values: int(s) or 0, try read_file(p) ... ----------\n// Each gives back 1 on success; on failure 0, with the message in *err.\n\nstatic void set_error(FStr *err, const char *msg) { str_heap(err, msg, (int64_t)strlen(msg)); }\n\nint finch_str_try_int(const FStr *s, int64_t *out, FStr *err) {\n if (parse_int(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nint finch_str_try_float(const FStr *s, double *out, FStr *err) {\n if (parse_float(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nstatic void file_error(FStr *err, const char *what, const FStr *path) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't %s the file \\\"%.*s\\\": %s\", what, (int)(path->len > 200 ? 200 : path->len),\n path->ptr ? path->ptr : \"\", strerror(errno));\n set_error(err, msg);\n}\n\n// when `fn main() -> !` fails\nvoid finch_report_error(const FStr *msg) {\n fflush(stdout);\n fprintf(stderr, \"error: %.*s\\n\", (int)msg->len, msg->ptr ? msg->ptr : \"\");\n finch_str_drop((FStr *)msg);\n}\n\nvoid finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {\n if (start < 0 || end > s->len || start > end) {\n char msg[160];\n snprintf(msg, sizeof msg, \"sub(%lld, %lld) is out of range (the length is %lld)\", (long long)start,\n (long long)end, (long long)s->len);\n finch_panic(file, line, msg);\n }\n str_heap(out, s->ptr + start, end - start);\n}\n\nint64_t finch_str_find(const FStr *s, const FStr *t) {\n if (t->len == 0) return 0;\n for (int64_t i = 0; i + t->len <= s->len; i++)\n if (s->ptr[i] == t->ptr[0] && memcmp(s->ptr + i, t->ptr, (size_t)t->len) == 0) return i;\n return -1;\n}\n\nint finch_str_starts(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr, t->ptr, (size_t)t->len) == 0;\n}\n\nint finch_str_ends(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr + s->len - t->len, t->ptr, (size_t)t->len) == 0;\n}\n\nvoid finch_str_trim(FStr *out, const FStr *s) {\n int64_t a = 0, b = s->len;\n while (a < b && isspace((unsigned char)s->ptr[a])) a++;\n while (b > a && isspace((unsigned char)s->ptr[b - 1])) b--;\n str_heap(out, s->ptr + a, b - a);\n}\n\nvoid finch_str_upper(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)toupper((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_lower(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)tolower((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_replace(FStr *out, const FStr *s, const FStr *from, const FStr *to) {\n if (from->len == 0) {\n str_heap(out, s->ptr, s->len);\n return;\n }\n int64_t count = 0;\n for (int64_t i = 0; i + from->len <= s->len;) {\n if (memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n count++;\n i += from->len;\n } else {\n i++;\n }\n }\n int64_t len = s->len + count * (to->len - from->len);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < s->len;) {\n if (i + from->len <= s->len && memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n memcpy(w, to->ptr, (size_t)to->len);\n w += to->len;\n i += from->len;\n } else {\n *w++ = s->ptr[i++];\n }\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len);\n\n// \"a,b,,c\".split(\",\") -> [\"a\", \"b\", \"\", \"c\"]; split(\"\") gives single characters.\nvoid finch_str_split(FArr *out, const FStr *s, const FStr *sep) {\n out->ptr = NULL;\n out->len = out->cap = 0;\n if (sep->len == 0) {\n for (int64_t i = 0; i < s->len; i++) arr_push_str(out, s->ptr + i, 1);\n return;\n }\n int64_t start = 0;\n for (int64_t i = 0; i + sep->len <= s->len;) {\n if (memcmp(s->ptr + i, sep->ptr, (size_t)sep->len) == 0) {\n arr_push_str(out, s->ptr + start, i - start);\n i += sep->len;\n start = i;\n } else {\n i++;\n }\n }\n arr_push_str(out, s->ptr + start, s->len - start);\n}\n\nvoid finch_str_join(FStr *out, const FArr *parts, const FStr *sep) {\n const FStr *p = parts->ptr;\n int64_t len = 0;\n for (int64_t i = 0; i < parts->len; i++) len += p[i].len + (i ? sep->len : 0);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < parts->len; i++) {\n if (i) {\n memcpy(w, sep->ptr, (size_t)sep->len);\n w += sep->len;\n }\n memcpy(w, p[i].ptr, (size_t)p[i].len);\n w += p[i].len;\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_repeat(FStr *out, const FStr *s, int64_t n) {\n if (n < 0) n = 0;\n int64_t len = s->len * n;\n char *m = finch_alloc(len + 1);\n for (int64_t i = 0; i < n; i++) memcpy(m + i * s->len, s->ptr, (size_t)s->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }\n\n// ---------- building text: print(x) and str(x) of arrays, maps and structs ----------\n// A buffer starts as an empty str and grows on the heap.\n\nvoid finch_buf_add(FStr *b, const char *p, int64_t n) {\n if (b->len + n + 1 > b->cap) {\n int64_t cap = b->cap > 0 ? b->cap * 2 : 32;\n while (cap < b->len + n + 1) cap *= 2;\n b->ptr = must(realloc(b->cap > 0 ? b->ptr : NULL, (size_t)cap));\n b->cap = cap;\n }\n if (n) memcpy(b->ptr + b->len, p, (size_t)n);\n b->len += n;\n b->ptr[b->len] = 0;\n}\n\nstatic void buf_fmt(FStr *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));\nstatic void buf_fmt(FStr *b, const char *fmt, ...) {\n char t[64];\n va_list ap;\n va_start(ap, fmt);\n int n = vsnprintf(t, sizeof t, fmt, ap);\n va_end(ap);\n finch_buf_add(b, t, n);\n}\n\nvoid finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, \"%lld\", (long long)v); }\nvoid finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, \"%llu\", (unsigned long long)v); }\nvoid finch_buf_float(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 0));\n}\nvoid finch_buf_f32(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 1));\n}\nvoid finch_buf_bool(FStr *b, int v) { finch_buf_add(b, v ? \"true\" : \"false\", v ? 4 : 5); }\nvoid finch_buf_ptr(FStr *b, const void *p) {\n if (p) buf_fmt(b, \"0x%llx\", (unsigned long long)(uintptr_t)p);\n else finch_buf_add(b, \"null\", 4);\n}\n\nvoid finch_buf_char(FStr *b, char c, int quoted) {\n if (quoted) finch_buf_add(b, \"'\", 1);\n finch_buf_add(b, &c, 1);\n if (quoted) finch_buf_add(b, \"'\", 1);\n}\n\nvoid finch_buf_str(FStr *b, const FStr *s, int quoted) {\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n finch_buf_add(b, s->ptr, s->len);\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n}\n\n// print the buffer and free it\nvoid finch_buf_print(FStr *b) {\n if (b->len) fwrite(b->ptr, 1, (size_t)b->len, stdout);\n if (b->cap > 0) free(b->ptr);\n}\n\n// ---------- arrays ----------\n\nvoid finch_arr_reserve(FArr *a, int64_t need, int64_t es) {\n if (need <= a->cap) return;\n int64_t cap = a->cap * 2;\n if (cap < 4) cap = 4;\n if (cap < need) cap = need;\n a->ptr = must(realloc(a->cap > 0 ? a->ptr : NULL, (size_t)(cap * es)));\n a->cap = cap;\n}\n\nvoid finch_arr_make(FArr *out, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n out->ptr = len ? finch_zalloc(len * es) : NULL;\n out->len = len;\n out->cap = len;\n}\n\n// grow or shrink to `len`; new elements are zero\nvoid finch_arr_resize(FArr *a, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n finch_arr_reserve(a, len, es);\n if (len > a->len) memset((char *)a->ptr + a->len * es, 0, (size_t)((len - a->len) * es));\n a->len = len;\n}\n\n// a byte-for-byte copy of the elements (the compiler deep-copies owning elements afterwards)\nvoid finch_arr_clone_raw(FArr *out, const FArr *a, int64_t es) {\n out->len = a->len;\n out->cap = a->len;\n out->ptr = a->len ? finch_alloc(a->len * es) : NULL;\n if (a->len) memcpy(out->ptr, a->ptr, (size_t)(a->len * es));\n}\n\nvoid finch_arr_free(FArr *a) {\n if (a->cap > 0) free(a->ptr);\n}\n\n// open a hole at i (0 <= i <= len), len grows by one\nvoid finch_arr_insert_gap(FArr *a, int64_t i, int64_t es, const char *file, int64_t line) {\n if (i < 0 || i > a->len) finch_panic_index(file, line, i, a->len);\n finch_arr_reserve(a, a->len + 1, es);\n char *p = a->ptr;\n memmove(p + (i + 1) * es, p + i * es, (size_t)((a->len - i) * es));\n a->len++;\n}\n\n// close the hole at i (the element was already moved out), len shrinks by one\nvoid finch_arr_remove_gap(FArr *a, int64_t i, int64_t es) {\n char *p = a->ptr;\n memmove(p + i * es, p + (i + 1) * es, (size_t)((a->len - i - 1) * es));\n a->len--;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len) {\n finch_arr_reserve(a, a->len + 1, (int64_t)sizeof(FStr));\n str_heap((FStr *)a->ptr + a->len, p, len);\n a->len++;\n}\n\nvoid finch_bytes_from_str(FArr *out, const FStr *s) {\n out->len = out->cap = s->len;\n out->ptr = s->len ? finch_alloc(s->len) : NULL;\n if (s->len) memcpy(out->ptr, s->ptr, (size_t)s->len);\n}\n\n// ---------- program arguments, input and files ----------\n\nvoid finch_args(FArr *out, int argc, char **argv) {\n out->len = out->cap = argc;\n out->ptr = finch_alloc((int64_t)sizeof(FStr) * (argc ? argc : 1));\n for (int i = 0; i < argc; i++) str_heap((FStr *)out->ptr + i, argv[i], (int64_t)strlen(argv[i]));\n}\n\n// after main: the arguments go\nvoid finch_args_free(FArr *a) {\n for (int64_t i = 0; i < a->len; i++) finch_str_drop((FStr *)a->ptr + i);\n finch_arr_free(a);\n}\n\n// input(\"Name? \") prints the prompt and reads one line, without the newline.\nvoid finch_input(FStr *out, const FStr *prompt) {\n if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);\n fflush(stdout);\n // read one line of any length (getline isn't available everywhere)\n size_t size = 128, n = 0;\n char *line = finch_alloc((int64_t)size);\n int c, got = 0;\n while ((c = fgetc(stdin)) != EOF) {\n got = 1;\n if (c == '\\n') break;\n if (n + 1 >= size) line = must(realloc(line, size *= 2));\n line[n++] = (char)c;\n }\n if (!got) {\n free(line);\n str_static(out, empty);\n return;\n }\n while (n > 0 && line[n - 1] == '\\r') n--;\n line[n] = 0;\n out->ptr = line;\n out->len = (int64_t)n;\n out->cap = (int64_t)size;\n}\n\nint finch_file_exists(const FStr *path) {\n FILE *f = fopen(path->ptr, \"rb\");\n if (f) fclose(f);\n return f != NULL;\n}\n\nint finch_try_read_file(FStr *out, const FStr *path, FStr *err) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n file_error(err, \"read\", path);\n return 0;\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n return 1;\n}\n\nvoid finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't read the file \\\"%.*s\\\": %s\", (int)(path->len > 200 ? 200 : path->len),\n path->ptr, strerror(errno));\n finch_panic(file, line, msg);\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n}\n\n// shell(\"ls -l\"): run a command, give back its exit code\nint64_t finch_shell(const FStr *cmd) {\n fflush(stdout);\n int status = system(cmd->ptr ? cmd->ptr : \"\");\n#ifdef _WIN32\n return status; // Windows gives the exit code directly\n#else\n if (status == -1) return -1;\n if (WIFEXITED(status)) return WEXITSTATUS(status);\n return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);\n#endif\n}\n\nint finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : \"\") == 0; }\n\nint finch_try_delete_file(const FStr *path, FStr *err) {\n if (remove(path->ptr ? path->ptr : \"\") == 0) return 1;\n file_error(err, \"delete\", path);\n return 0;\n}\n\n// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here\nvoid *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; }\n\nint finch_write_file(const FStr *path, const FStr *text) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"wb\");\n if (!f) return 0;\n size_t put = text->len ? fwrite(text->ptr, 1, (size_t)text->len, f) : 0;\n return fclose(f) == 0 && put == (size_t)text->len;\n}\n\nint finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {\n errno = 0;\n if (finch_write_file(path, text)) return 1;\n if (!errno) errno = EIO;\n file_error(err, \"write\", path);\n return 0;\n}\n\n// ---------- folders (std/fs.fch, through std/finch_fs.h) ----------\n\nint finch_fs_is_dir(const char *path) {\n#ifdef _WIN32\n struct _finddata_t d;\n intptr_t h = _findfirst(path, &d);\n if (h == -1) return 0;\n _findclose(h);\n return (d.attrib & _A_SUBDIR) != 0;\n#else\n struct stat st;\n return stat(path, &st) == 0 && S_ISDIR(st.st_mode);\n#endif\n}\n\nint finch_fs_exists(const char *path) {\n#ifdef _WIN32\n return _access(path, 0) == 0;\n#else\n struct stat st;\n return stat(path, &st) == 0;\n#endif\n}\n\nstatic int make_one_dir(const char *p) {\n#ifdef _WIN32\n return _mkdir(p) == 0 || errno == EEXIST;\n#else\n return mkdir(p, 0777) == 0 || errno == EEXIST;\n#endif\n}\n\n// mkdir -p: the parents too; 1 if the folder is there afterwards\nint finch_fs_mkdir(const char *path) {\n size_t n = strlen(path);\n char *p = finch_alloc((int64_t)n + 1);\n memcpy(p, path, n + 1);\n for (size_t i = 1; i < n; i++) {\n if (p[i] == '/' || p[i] == '\\\\') {\n if (p[i - 1] == ':') continue; /* C: is a drive */\n char c = p[i];\n p[i] = 0;\n make_one_dir(p);\n p[i] = c;\n }\n }\n int ok = make_one_dir(p) && finch_fs_is_dir(p);\n free(p);\n return ok;\n}\n\nint finch_fs_remove_dir(const char *path) {\n#ifdef _WIN32\n return _rmdir(path) == 0;\n#else\n return rmdir(path) == 0;\n#endif\n}\n\n// the names in a folder, one per call (\"\" at the end), without . and ..\ntypedef struct {\n#ifdef _WIN32\n intptr_t h;\n struct _finddata_t d;\n int first;\n#else\n DIR *d;\n#endif\n} FsList;\n\nvoid *finch_fs_list_open(const char *path) {\n FsList *l = finch_zalloc((int64_t)sizeof(FsList));\n#ifdef _WIN32\n size_t n = strlen(path);\n char *pat = finch_alloc((int64_t)n + 3);\n memcpy(pat, path, n);\n memcpy(pat + n, \"\\\\*\", 3);\n l->h = _findfirst(pat, &l->d);\n free(pat);\n if (l->h == -1) {\n free(l);\n return NULL;\n }\n l->first = 1;\n#else\n l->d = opendir(path);\n if (!l->d) {\n free(l);\n return NULL;\n }\n#endif\n return l;\n}\n\nconst char *finch_fs_list_next(void *handle) {\n FsList *l = handle;\n for (;;) {\n#ifdef _WIN32\n if (!l->first && _findnext(l->h, &l->d) != 0) return \"\";\n l->first = 0;\n const char *name = l->d.name;\n#else\n struct dirent *e = readdir(l->d);\n if (!e) return \"\";\n const char *name = e->d_name;\n#endif\n if (strcmp(name, \".\") != 0 && strcmp(name, \"..\") != 0) return name;\n }\n}\n\nvoid finch_fs_list_close(void *handle) {\n FsList *l = handle;\n#ifdef _WIN32\n _findclose(l->h);\n#else\n closedir(l->d);\n#endif\n free(l);\n}\n\n// where a program keeps its own files for this user (saves, settings); \"\" if unknown\n// Linux: $XDG_DATA_HOME or ~/.local/share macOS: ~/Library/Application Support Windows: %APPDATA%\nconst char *finch_fs_data_home(void) {\n static char buf[4096];\n const char *v;\n#if defined(_WIN32)\n v = getenv(\"APPDATA\");\n snprintf(buf, sizeof buf, \"%s\", v ? v : \"\");\n#elif defined(__APPLE__)\n v = getenv(\"HOME\");\n if (v) snprintf(buf, sizeof buf, \"%s/Library/Application Support\", v);\n else buf[0] = 0;\n#else\n v = getenv(\"XDG_DATA_HOME\");\n if (v && *v) snprintf(buf, sizeof buf, \"%s\", v);\n else if ((v = getenv(\"HOME\"))) snprintf(buf, sizeof buf, \"%s/.local/share\", v);\n else buf[0] = 0;\n#endif\n return buf;\n}\n\n// ---------- threads (std/thread.fch, through std/finch_thread.h) ----------\n\ntypedef struct {\n void (*fn)(void *);\n void *arg;\n} ThreadStart;\n\n#ifdef _WIN32\nstatic DWORD WINAPI thread_main(LPVOID p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return 0;\n}\n#else\nstatic void *thread_main(void *p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return NULL;\n}\n#endif\n\n// run fn(arg) on a new thread; NULL if it couldn't start\nvoid *finch_thread_spawn(void (*fn)(void *), void *arg) {\n ThreadStart *st = finch_alloc((int64_t)sizeof(ThreadStart));\n st->fn = fn;\n st->arg = arg;\n#ifdef _WIN32\n HANDLE h = CreateThread(NULL, 0, thread_main, st, 0, NULL);\n if (!h) {\n free(st);\n return NULL;\n }\n return h;\n#else\n pthread_t *t = finch_alloc((int64_t)sizeof(pthread_t));\n if (pthread_create(t, NULL, thread_main, st) != 0) {\n free(st);\n free(t);\n return NULL;\n }\n return t;\n#endif\n}\n\n// wait for the thread to end; 1 if that worked\nint finch_thread_join(void *t) {\n if (!t) return 0;\n#ifdef _WIN32\n int ok = WaitForSingleObject((HANDLE)t, INFINITE) == WAIT_OBJECT_0;\n CloseHandle((HANDLE)t);\n return ok;\n#else\n int ok = pthread_join(*(pthread_t *)t, NULL) == 0;\n free(t);\n return ok;\n#endif\n}\n\nvoid *finch_mutex_new(void) {\n#ifdef _WIN32\n CRITICAL_SECTION *m = finch_alloc((int64_t)sizeof(CRITICAL_SECTION));\n InitializeCriticalSection(m);\n#else\n pthread_mutex_t *m = finch_alloc((int64_t)sizeof(pthread_mutex_t));\n pthread_mutex_init(m, NULL);\n#endif\n return m;\n}\n\nvoid finch_mutex_lock(void *m) {\n#ifdef _WIN32\n EnterCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_lock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_unlock(void *m) {\n#ifdef _WIN32\n LeaveCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_unlock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_free(void *m) {\n if (!m) return;\n#ifdef _WIN32\n DeleteCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_destroy((pthread_mutex_t *)m);\n#endif\n free(m);\n}\n\nint finch_cpu_count(void) {\n#ifdef _WIN32\n SYSTEM_INFO si;\n GetSystemInfo(&si);\n return (int)si.dwNumberOfProcessors;\n#else\n long n = sysconf(_SC_NPROCESSORS_ONLN);\n return n > 0 ? (int)n : 1;\n#endif\n}\n\n// ---------- maps ----------\n// map[K]V = { entries; len; used; cap; index; icap; adds }\n// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.\n// A removed entry keeps its slot (hash 0) until the next resize packs the slots.\n// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.\n// adds: how many keys were ever added; a `for` over the map stops with an error if it changes.\n// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.\n// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).\n\ntypedef struct {\n char *entries;\n int64_t len, used, cap;\n int64_t *index;\n int64_t icap;\n int64_t adds;\n} FMap;\n\nstatic uint64_t map_hash(const void *key, int64_t ks, int kstr) {\n const unsigned char *p;\n size_t n;\n if (kstr) {\n const FStr *s = key;\n p = (const unsigned char *)s->ptr;\n n = (size_t)s->len;\n } else {\n p = key;\n n = (size_t)ks;\n }\n uint64_t h = 1469598103934665603ULL; // FNV-1a, then mixed so the low bits spread well\n for (size_t i = 0; i < n; i++) {\n h ^= p[i];\n h *= 1099511628211ULL;\n }\n h ^= h >> 31;\n h *= 0xbf58476d1ce4e5b9ULL;\n h ^= h >> 29;\n return h | 1; // 0 marks a removed entry\n}\n\nstatic int map_key_eq(const char *a, const void *b, int64_t ks, int kstr) {\n if (!kstr) return memcmp(a, b, (size_t)ks) == 0;\n const FStr *x = (const FStr *)a, *y = b;\n return x->len == y->len && (x->len == 0 || memcmp(x->ptr, y->ptr, (size_t)x->len) == 0);\n}\n\nstatic void map_reindex(FMap *m, int64_t es) {\n if (m->len != m->used) { // pack the slots of removed entries away\n int64_t j = 0;\n for (int64_t i = 0; i < m->used; i++) {\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == 0) continue;\n if (i != j) memmove(m->entries + j * es, e, (size_t)es);\n j++;\n }\n m->used = j;\n }\n int64_t icap = 16;\n while (icap < m->cap * 2) icap *= 2;\n free(m->index);\n m->index = finch_alloc(icap * (int64_t)sizeof(int64_t));\n memset(m->index, 0xff, (size_t)icap * sizeof(int64_t));\n m->icap = icap;\n for (int64_t i = 0; i < m->used; i++) {\n uint64_t h = *(uint64_t *)(m->entries + i * es);\n int64_t s = (int64_t)(h & (uint64_t)(icap - 1));\n while (m->index[s] != -1) s = (s + 1) & (icap - 1);\n m->index[s] = i;\n }\n}\n\n// the entry number of `key`, or -1\nint64_t finch_map_find(const FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr) {\n if (m->len == 0) return -1;\n uint64_t h = map_hash(key, ks, kstr);\n for (int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));; s = (s + 1) & (m->icap - 1)) {\n int64_t i = m->index[s];\n if (i == -1) return -1;\n if (i < 0) continue;\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == h && map_key_eq(e + 8, key, ks, kstr)) return i;\n }\n}\n\n// The entry for `key`. A new one is zeroed except its hash (*added = 1); the compiler then stores the key.\nint64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr, int32_t *added) {\n int64_t i = finch_map_find(m, key, es, ks, kstr);\n *added = i < 0;\n if (i >= 0) return i;\n if (m->used == m->cap) {\n if (m->len * 4 < m->cap * 3) { // many removed slots: pack them instead of growing\n map_reindex(m, es);\n } else {\n m->cap = m->cap ? m->cap * 2 : 8;\n m->entries = must(realloc(m->entries, (size_t)(m->cap * es)));\n map_reindex(m, es);\n }\n }\n uint64_t h = map_hash(key, ks, kstr);\n i = m->used++;\n char *e = m->entries + i * es;\n memset(e, 0, (size_t)es);\n *(uint64_t *)e = h;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);\n m->index[s] = i;\n m->len++;\n m->adds++;\n return i;\n}\n\n// Forget entry i (its key and value were already dropped by the compiler).\nvoid finch_map_remove_at(FMap *m, int64_t i, int64_t es) {\n char *e = m->entries + i * es;\n uint64_t h = *(uint64_t *)e;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] != i) s = (s + 1) & (m->icap - 1);\n m->index[s] = -2;\n *(uint64_t *)e = 0;\n if (--m->len == 0) {\n m->used = 0;\n memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n }\n}\n\nvoid finch_map_clear(FMap *m) {\n m->len = m->used = 0;\n if (m->index) memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n}\n\nvoid finch_map_free(FMap *m) {\n free(m->entries);\n free(m->index);\n}\n\n// A copy of the slots and the index; the compiler then copies the keys and values that own memory.\nvoid finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {\n *out = *m;\n if (!m->cap) return;\n out->entries = finch_alloc(m->cap * es);\n memcpy(out->entries, m->entries, (size_t)(m->used * es));\n out->index = finch_alloc(m->icap * (int64_t)sizeof(int64_t));\n memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));\n}\n\n__attribute__((noreturn, cold)) void finch_map_changed(const char *file, int64_t line) {\n finch_panic(file, line, \"a key was added to the map while looping over it (collect the new keys, and add them after the loop)\");\n}\n\n__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {\n char msg[200];\n snprintf(msg, sizeof msg, \"the key %.*s is not in the map (check with .has(key), or use .get(key, default))\",\n (int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : \"\");\n finch_panic(file, line, msg);\n}\n")
list.push("// Files and folders: bytes, folders, listing, and a place for a program's own files.\n//\n// import fs\n// data := try fs.read_bytes(\"atlas.png\") the whole file as []u8 (any bytes, NUL included)\n// try fs.write_bytes(\"out.bin\", data)\n// try fs.mkdir(\"saves/slot1\") with the folders above it\n// names := try fs.list(\"saves\") the names in a folder, sorted\n// fs.exists(p) fs.is_dir(p) try fs.remove_dir(p)\n// dir := try fs.data_dir(\"MyGame\") a folder for this user's saves, made if needed\nimport \"finch_fs.h\"\n\nfn read_bytes(str path) -> []u8! {\n text := try read_file(path)\n return text.bytes()\n}\n\nfn write_bytes(str path, []u8 data) -> ! {\n try write_file(path, str(data))\n}\n\nfn exists(str path) -> bool {\n return finch_fs_exists(path) != 0\n}\n\nfn is_dir(str path) -> bool {\n return finch_fs_is_dir(path) != 0\n}\n\nfn mkdir(str path) -> ! {\n if finch_fs_mkdir(path) == 0 {\n return error(\"can't make the folder \\\"\" + path + \"\\\"\")\n }\n}\n\n// only an empty folder\nfn remove_dir(str path) -> ! {\n if finch_fs_remove_dir(path) == 0 {\n return error(\"can't remove the folder \\\"\" + path + \"\\\" (is it empty?)\")\n }\n}\n\nfn list(str path) -> []str! {\n l := finch_fs_list_open(path)\n if l == null {\n return error(\"can't list the folder \\\"\" + path + \"\\\"\")\n }\n []str names\n while true {\n name := finch_fs_list_next(l)\n if name.len == 0 {\n break\n }\n names.push(name)\n }\n finch_fs_list_close(l)\n names.sort()\n return names\n}\n\n// The folder where a program keeps this user's files (saves, settings), made if it isn't there:\n// ~/.local/share/ on Linux, ~/Library/Application Support/ on macOS, %APPDATA%\\ on Windows.\nfn data_dir(str app) -> str! {\n home := finch_fs_data_home()\n if home.len == 0 {\n return error(\"this system doesn't say where programs keep their files\")\n }\n dir := home + \"/\" + app\n try mkdir(dir)\n return dir\n}\n")
list.push("// JSON: read it, build it, write it.\n//\n// import json\n//\n// doc := try json.parse(\"{\\\"name\\\": \\\"Finch\\\", \\\"tags\\\": [\\\"fast\\\", \\\"small\\\"], \\\"stars\\\": 5}\")\n// print(doc.get(\"name\").str_or(\"\")) Finch\n// print(doc.get(\"tags\").at(1).str_or(\"\")) small\n// print(doc.get(\"stars\").int_or(0)) 5\n// print(doc.get(\"missing\").is_null()) true (a missing key is null, so chains never fail)\n//\n// out := json.new_object()\n// out.set(\"ok\", json.new_bool(true))\n// list := json.new_list()\n// list.push(json.new_number(1.5))\n// out.set(\"values\", list)\n// print(json.stringify(out)) {\"ok\":true,\"values\":[1.5]}\n// print(json.pretty(out)) the same, over several lines\n\nenum Kind { Null, Bool, Number, Text, List, Object }\n\n// One JSON value. `kind` says which of the other fields it uses: flag for Bool, num for Number,\n// text for Text, items for List, and items with keys (the same length, in the same order) for Object.\nstruct Json {\n Kind kind\n bool flag\n float num\n str text\n []Json items\n []str keys\n}\n\nfn new_null() -> Json {\n return Json(kind: Kind.Null)\n}\n\nfn new_bool(bool b) -> Json {\n return Json(kind: Kind.Bool, flag: b)\n}\n\nfn new_number(float n) -> Json {\n return Json(kind: Kind.Number, num: n)\n}\n\nfn new_text(str s) -> Json {\n return Json(kind: Kind.Text, text: s)\n}\n\nfn new_list() -> Json {\n return Json(kind: Kind.List)\n}\n\nfn new_object() -> Json {\n return Json(kind: Kind.Object)\n}\n\n// ---------- looking inside ----------\n\nfn Json.is_null() -> bool {\n return self.kind == Kind.Null\n}\n\n// the number of elements of a list, of keys of an object, of bytes of a text; 0 for the rest\nfn Json.len() -> int {\n return match self.kind {\n List, Object -> self.items.len\n Text -> self.text.len\n else -> 0\n }\n}\n\nfn Json.has(str key) -> bool {\n return self.kind == Kind.Object && self.keys.contains(key)\n}\n\n// the value of a key of an object; null if there is none (or this is not an object)\nfn Json.get(str key) -> Json {\n if self.kind != Kind.Object {\n return new_null()\n }\n i := self.keys.find(key)\n if i < 0 {\n return new_null()\n }\n return self.items[i]\n}\n\n// the element of a list; null if there is none\nfn Json.at(int i) -> Json {\n if self.kind != Kind.List || i < 0 || i >= self.items.len {\n return new_null()\n }\n return self.items[i]\n}\n\n// the value if it is of that kind, else what was given\nfn Json.str_or(str fallback) -> str {\n if self.kind == Kind.Text {\n return self.text\n }\n return fallback\n}\n\nfn Json.float_or(float fallback) -> float {\n if self.kind == Kind.Number {\n return self.num\n }\n return fallback\n}\n\nfn Json.int_or(int fallback) -> int {\n if self.kind == Kind.Number {\n return int(self.num)\n }\n return fallback\n}\n\nfn Json.bool_or(bool fallback) -> bool {\n if self.kind == Kind.Bool {\n return self.flag\n }\n return fallback\n}\n\n// the keys of an object, in the order they were written\nfn Json.names() -> []str {\n return self.keys\n}\n\n// ---------- building ----------\n\n// add to the end of a list\nfn Json.push(Json v) {\n self.items.push(v)\n}\n\n// give a key a value in an object; a key that is already there is replaced\nfn Json.set(str key, Json v) {\n i := self.keys.find(key)\n if i >= 0 {\n self.items[i] = v\n return\n }\n self.keys.push(key)\n self.items.push(v)\n}\n\n// ---------- writing ----------\n\nfn quote(str s) -> str {\n []str parts = [\"\\\"\"]\n for c in s {\n n := int(c)\n if c == '\"' {\n parts.push(\"\\\\\\\"\")\n } else if c == '\\\\' {\n parts.push(\"\\\\\\\\\")\n } else if c == '\\n' {\n parts.push(\"\\\\n\")\n } else if c == '\\r' {\n parts.push(\"\\\\r\")\n } else if c == '\\t' {\n parts.push(\"\\\\t\")\n } else if n < 32 {\n parts.push(\"\\\\u00\" + hexdigit(n / 16) + hexdigit(n % 16))\n } else {\n parts.push(str(c))\n }\n }\n parts.push(\"\\\"\")\n return parts.join(\"\")\n}\n\nfn hexdigit(int n) -> str {\n digits := \"0123456789abcdef\"\n return str(digits[n])\n}\n\nfn number_text(float n) -> str {\n if n == float(int(n)) && n > -1000000000000000.0 && n < 1000000000000000.0 {\n return str(int(n))\n }\n return str(n)\n}\n\nstruct Writer {\n []str out\n}\n\nfn write(Json j, str indent, str pad, ptr[Writer] w) {\n nl := \"\"\n inner := \"\"\n sep := \",\"\n colon := \":\"\n if indent.len > 0 {\n nl = \"\\n\"\n inner = pad + indent\n colon = \": \"\n }\n match j.kind {\n Null -> w.out.push(\"null\")\n Bool -> {\n if j.flag {\n w.out.push(\"true\")\n } else {\n w.out.push(\"false\")\n }\n }\n Number -> w.out.push(number_text(j.num))\n Text -> w.out.push(quote(j.text))\n List -> {\n if j.items.len == 0 {\n w.out.push(\"[]\")\n return\n }\n w.out.push(\"[\" + nl)\n for i, item in j.items {\n if i > 0 {\n w.out.push(sep + nl)\n }\n w.out.push(inner)\n write(item, indent, inner, w)\n }\n w.out.push(nl + pad + \"]\")\n }\n Object -> {\n if j.items.len == 0 {\n w.out.push(\"{}\")\n return\n }\n w.out.push(\"{\" + nl)\n for i, item in j.items {\n if i > 0 {\n w.out.push(sep + nl)\n }\n w.out.push(inner + quote(j.keys[i]) + colon)\n write(item, indent, inner, w)\n }\n w.out.push(nl + pad + \"}\")\n }\n }\n}\n\n// the value as text on one line\nfn stringify(Json j) -> str {\n w := Writer()\n write(j, \"\", \"\", addr(w))\n return w.out.join(\"\")\n}\n\n// the value as text over several lines, indented by two spaces\nfn pretty(Json j) -> str {\n w := Writer()\n write(j, \" \", \"\", addr(w))\n return w.out.join(\"\")\n}\n\n// ---------- reading ----------\n\nstruct Reader {\n str src\n int i\n}\n\n// the line and column of a position, for a message\nfn where(ptr[Reader] r) -> str {\n line := 1\n col := 1\n for k in 0..r.i {\n if k < r.src.len && r.src[k] == '\\n' {\n line += 1\n col = 1\n } else {\n col += 1\n }\n }\n return \"line \" + str(line) + \", column \" + str(col)\n}\n\nfn peek(ptr[Reader] r) -> char {\n if r.i < r.src.len {\n return r.src[r.i]\n }\n return char(0)\n}\n\nfn skip_space(ptr[Reader] r) {\n while r.i < r.src.len && (r.src[r.i] == ' ' || r.src[r.i] == '\\n' || r.src[r.i] == '\\t' || r.src[r.i] == '\\r') {\n r.i += 1\n }\n}\n\nfn unexpected(ptr[Reader] r, str wanted) -> str {\n if r.i >= r.src.len {\n return \"json: the text ends where \" + wanted + \" should be (\" + where(r) + \")\"\n }\n return \"json: found '\" + str(r.src[r.i]) + \"' where \" + wanted + \" should be (\" + where(r) + \")\"\n}\n\n// does `word` come next?\nfn comes(ptr[Reader] r, str word) -> bool {\n return r.src.len - r.i >= word.len && r.src.sub(r.i, r.i + word.len) == word\n}\n\nfn expect_word(ptr[Reader] r, str word) -> ! {\n if !comes(r, word) {\n return error(unexpected(r, \"'\" + word + \"'\"))\n }\n r.i += word.len\n}\n\nfn hex_value(char c) -> int {\n if c >= '0' && c <= '9' {\n return int(c) - int('0')\n }\n if c >= 'a' && c <= 'f' {\n return int(c) - int('a') + 10\n }\n if c >= 'A' && c <= 'F' {\n return int(c) - int('A') + 10\n }\n return -1\n}\n\n// a code point as UTF-8\nfn utf8(int cp) -> str {\n []u8 b\n if cp < 128 {\n b.push(u8(cp))\n } else if cp < 2048 {\n b.push(u8(192 + cp / 64))\n b.push(u8(128 + cp % 64))\n } else if cp < 65536 {\n b.push(u8(224 + cp / 4096))\n b.push(u8(128 + (cp / 64) % 64))\n b.push(u8(128 + cp % 64))\n } else {\n b.push(u8(240 + cp / 262144))\n b.push(u8(128 + (cp / 4096) % 64))\n b.push(u8(128 + (cp / 64) % 64))\n b.push(u8(128 + cp % 64))\n }\n return str(b)\n}\n\nfn read_hex4(ptr[Reader] r) -> int! {\n n := 0\n for k in 0..4 {\n if r.i >= r.src.len || hex_value(r.src[r.i]) < 0 {\n return error(unexpected(r, \"a hexadecimal digit\"))\n }\n n = n * 16 + hex_value(r.src[r.i])\n r.i += 1\n }\n return n\n}\n\nfn read_string(ptr[Reader] r) -> str! {\n r.i += 1 // the opening quote\n []str parts\n start := r.i\n while true {\n if r.i >= r.src.len {\n return error(\"json: a text is never closed (\" + where(r) + \")\")\n }\n c := r.src[r.i]\n if c == '\"' {\n parts.push(r.src.sub(start, r.i))\n r.i += 1\n return parts.join(\"\")\n }\n if int(c) < 32 {\n return error(\"json: a line break or control character inside a text (\" + where(r) + \")\")\n }\n if c != '\\\\' {\n r.i += 1\n continue\n }\n parts.push(r.src.sub(start, r.i))\n r.i += 1\n e := peek(r)\n r.i += 1\n if e == 'n' {\n parts.push(\"\\n\")\n } else if e == 't' {\n parts.push(\"\\t\")\n } else if e == 'r' {\n parts.push(\"\\r\")\n } else if e == 'b' {\n parts.push(utf8(8))\n } else if e == 'f' {\n parts.push(utf8(12))\n } else if e == '\"' || e == '\\\\' || e == '/' {\n parts.push(str(e))\n } else if e == 'u' {\n cp := try read_hex4(r)\n // a pair of 😀 stands for one character above U+FFFF\n if cp >= 55296 && cp < 56320 && comes(r, \"\\\\u\") {\n r.i += 2\n low := try read_hex4(r)\n cp = 65536 + (cp - 55296) * 1024 + (low - 56320)\n }\n parts.push(utf8(cp))\n } else {\n return error(\"json: unknown escape \\\\\" + str(e) + \" in a text (\" + where(r) + \")\")\n }\n start = r.i\n }\n}\n\nfn read_number(ptr[Reader] r) -> Json! {\n start := r.i\n if peek(r) == '-' {\n r.i += 1\n }\n while r.i < r.src.len && ((r.src[r.i] >= '0' && r.src[r.i] <= '9') || r.src[r.i] == '.' || r.src[r.i] == 'e' || r.src[r.i] == 'E' || r.src[r.i] == '+' || r.src[r.i] == '-') {\n r.i += 1\n }\n n := float(r.src.sub(start, r.i)) or {\n r.i = start\n return error(unexpected(r, \"a number\"))\n }\n return new_number(n)\n}\n\nfn read_value(ptr[Reader] r) -> Json! {\n skip_space(r)\n c := peek(r)\n if r.i >= r.src.len {\n return error(unexpected(r, \"a value\"))\n }\n if c == '{' {\n obj := new_object()\n r.i += 1\n skip_space(r)\n if peek(r) == '}' {\n r.i += 1\n return obj\n }\n while true {\n skip_space(r)\n if peek(r) != '\"' {\n return error(unexpected(r, \"a key in quotes\"))\n }\n key := try read_string(r)\n skip_space(r)\n if peek(r) != ':' {\n return error(unexpected(r, \"':'\"))\n }\n r.i += 1\n v := try read_value(r)\n obj.set(key, v)\n skip_space(r)\n if peek(r) == ',' {\n r.i += 1\n continue\n }\n if peek(r) == '}' {\n r.i += 1\n return obj\n }\n return error(unexpected(r, \"',' or '}'\"))\n }\n }\n if c == '[' {\n list := new_list()\n r.i += 1\n skip_space(r)\n if peek(r) == ']' {\n r.i += 1\n return list\n }\n while true {\n v := try read_value(r)\n list.push(v)\n skip_space(r)\n if peek(r) == ',' {\n r.i += 1\n continue\n }\n if peek(r) == ']' {\n r.i += 1\n return list\n }\n return error(unexpected(r, \"',' or ']'\"))\n }\n }\n if c == '\"' {\n s := try read_string(r)\n return new_text(s)\n }\n if c == 't' {\n try expect_word(r, \"true\")\n return new_bool(true)\n }\n if c == 'f' {\n try expect_word(r, \"false\")\n return new_bool(false)\n }\n if c == 'n' {\n try expect_word(r, \"null\")\n return new_null()\n }\n if c == '-' || (c >= '0' && c <= '9') {\n return try read_number(r)\n }\n return error(unexpected(r, \"a value\"))\n}\n\n// the JSON in `text`; an error with the line and column where it went wrong\nfn parse(str text) -> Json! {\n r := Reader(src: text)\n v := try read_value(addr(r))\n skip_space(addr(r))\n if r.i < r.src.len {\n return error(unexpected(addr(r), \"the end of the text\"))\n }\n return v\n}\n")
list.push("// Paths, as text. Elements are separated by \"/\".\n//\n// import path\n// path.join(\"a\", \"b.txt\") a/b.txt\n// path.base(\"/tmp/x/a.tar.gz\") a.tar.gz path.stem(...) a.tar path.ext(...) .gz\n// path.dir(\"/tmp/x/a.txt\") /tmp/x\n// path.clean(\"a//b/../c/./d\") a/c/d\n\n// a and b with one separator between; an absolute b replaces a\nfn join(str a, str b) -> str {\n if a.len == 0 {\n return b\n }\n if b.len == 0 {\n return a\n }\n if b[0] == '/' {\n return b\n }\n if a[a.len - 1] == '/' {\n return a + b\n }\n return a + \"/\" + b\n}\n\nfn is_abs(str p) -> bool {\n return p.len > 0 && p[0] == '/'\n}\n\n// the last element: \"a/b.txt\" -> \"b.txt\", \"a/b/\" -> \"b\", \"\" -> \".\", \"/\" -> \"/\"\nfn base(str p) -> str {\n if p.len == 0 {\n return \".\"\n }\n end := p.len\n while end > 1 && p[end - 1] == '/' {\n end -= 1\n }\n if end == 1 && p[0] == '/' {\n return \"/\"\n }\n start := end\n while start > 0 && p[start - 1] != '/' {\n start -= 1\n }\n return p.sub(start, end)\n}\n\n// everything before the last element: \"a/b.txt\" -> \"a\", \"b.txt\" -> \".\", \"/b\" -> \"/\"\nfn dir(str p) -> str {\n end := p.len\n while end > 1 && p[end - 1] == '/' {\n end -= 1\n }\n i := end\n while i > 0 && p[i - 1] != '/' {\n i -= 1\n }\n if i == 0 {\n return \".\"\n }\n while i > 1 && p[i - 1] == '/' {\n i -= 1\n }\n return p.sub(0, i)\n}\n\n// the part of the last element from its last dot: \"a.tar.gz\" -> \".gz\"; \"\" when there is none\nfn ext(str p) -> str {\n b := base(p)\n i := b.len\n while i > 0 && b[i - 1] != '.' {\n i -= 1\n }\n if i <= 1 { // no dot, or a name that only starts with one (.bashrc)\n return \"\"\n }\n return b.sub(i - 1, b.len)\n}\n\n// the last element without its extension\nfn stem(str p) -> str {\n b := base(p)\n e := ext(p)\n return b.sub(0, b.len - e.len)\n}\n\n// the elements of a path: \"/a/b/c\" -> [\"a\", \"b\", \"c\"]\nfn parts(str p) -> []str {\n []str out\n for piece in p.split(\"/\") {\n if piece.len > 0 {\n out.push(piece)\n }\n }\n return out\n}\n\n// the shortest path with the same meaning: no \"//\", no \".\", and \"..\" resolved where it can be\nfn clean(str p) -> str {\n if p.len == 0 {\n return \".\"\n }\n abs := is_abs(p)\n []str out\n for piece in parts(p) {\n if piece == \".\" {\n continue\n }\n if piece == \"..\" {\n if out.len > 0 && out[out.len - 1] != \"..\" {\n out.pop()\n } else if !abs {\n out.push(\"..\")\n }\n continue\n }\n out.push(piece)\n }\n joined := out.join(\"/\")\n if abs {\n return \"/\" + joined\n }\n if joined.len == 0 {\n return \".\"\n }\n return joined\n}\n")
From 0ceaaa27c415cb704a34415db213aa3e445d81b8 Mon Sep 17 00:00:00 2001
From: Claude
Date: Sun, 1 Nov 2026 15:25:40 +0000
Subject: [PATCH 5/7] Floats as written, never fused; fma and muladd; f32/f64
bits, f16 and bf16
- a * b + c is two roundings on every processor and --cpu: the IR has no fast-math flags, and clang
compiles it with -ffp-contract=on (checked in the assembly: never fused, also with FMA hardware)
- fma(a, b, c) / v.fma(b, c): one rounding, always (llvm.fma)
- muladd(a, b, c) / v.muladd(b, c): the fastest way here (llvm.fmuladd), what v.fma was before
- f32_bits / f32_from_bits / f64_bits / f64_from_bits: bitcasts, also of vectors
- f16_to_f32 / f32_to_f16 (LLVM half), bf16_to_f32 / f32_to_bf16 (integer ops, ties to even, NaN kept)
- docs (en, pl): what is guaranteed about floats, the new built-ins
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01Psc42moPLwP43Cmb9oZqSR
---
docs/en/cheatsheet.md | 6 +-
docs/en/for-engineers.md | 16 ++-
docs/en/for-technicians.md | 41 +++++++-
docs/pl/dla-inzynierow.md | 18 +++-
docs/pl/dla-technikow.md | 42 +++++++-
docs/pl/sciaga.md | 6 +-
examples/simd.fch | 4 +-
src/docs.fch | 13 ++-
src/gen.fch | 200 +++++++++++++++++++++++++++++++++++--
src/linker.fch | 3 +-
tests/run/floatbits.fch | 31 ++++++
tests/run/floatbits.out | 13 +++
tests/run/vectors.fch | 2 +-
13 files changed, 373 insertions(+), 22 deletions(-)
create mode 100644 tests/run/floatbits.fch
create mode 100644 tests/run/floatbits.out
diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md
index 615856d..86fbf2e 100644
--- a/docs/en/cheatsheet.md
+++ b/docs/en/cheatsheet.md
@@ -259,7 +259,8 @@ Types: `f32x4/8/16`, `f64x2/4/8`, `i8x16/32/64`, `i16x8/16/32`, `i32x4/8/16`, `i
| `v[i]`, `v[i] = x`, `v.len` | a lane; the number of lanes |
| `min(v, w)`, `max`, `abs(v)`, `clamp(v, lo, hi)` | lane by lane |
| `v.sum()`, `v.min()`, `v.max()`, `v.dot(w)` | one number from all the lanes |
-| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()`, `v.fma(b, c)` | every lane (float lanes; `fma` = `v * b + c`) |
+| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | every lane (float lanes) |
+| `v.fma(b, c)`, `v.muladd(b, c)` | `v * b + c`: one rounding always / the fastest way here |
| `v.store(list, i)` | lanes into `list[i]` … |
---
@@ -324,6 +325,9 @@ Automatic only when nothing is lost: smaller → bigger number, any int → floa
| `shell(command)` | run a command, its exit code |
| `exit(code)` | stop the program now (also ends an `or { }` block) |
| `min(a, b, …)`, `max(a, b, …)`, `abs(x)`, `clamp(x, lo, hi)` | for any numbers (your own function of that name wins) |
+| `fma(a, b, c)`, `muladd(a, b, c)` | `a * b + c` with one rounding / fused where the processor can (`a * b + c` written out is never fused) |
+| `f32_bits(x)`, `f32_from_bits(u)`, `f64_bits`, `f64_from_bits` | a float's bits as `u32`/`u64` and back, unchanged |
+| `f16_to_f32(h)`, `f32_to_f16(x)`, `bf16_to_f32(b)`, `f32_to_bf16(x)` | half and bfloat16 numbers (in a `u16`) ↔ `f32`; also on vectors |
| `error(message)` | `return error("…")` in a `-> T!` function |
| `addr(x)` | pointer to x; `addr(fn)` = C callback |
| `assert(cond)`, `assert(cond, "why")` | stop with a runtime error when cond is false (in a `test` block: that test fails) |
diff --git a/docs/en/for-engineers.md b/docs/en/for-engineers.md
index 68bc9e8..66c90cf 100644
--- a/docs/en/for-engineers.md
+++ b/docs/en/for-engineers.md
@@ -308,7 +308,8 @@ type goes; a call `f32x8(…)` goes to `vecMake()`.
| `min`, `max`, `abs` | `llvm.minnum`/`maxnum`, `llvm.smin`/`umin`…, `llvm.fabs`/`llvm.abs` |
| `v.sum()`, `.min()`, `.max()` | `llvm.vector.reduce.fadd` (with `reassoc`), `.add`, `.fmin`, `.smin`… |
| `v.sqrt()`, `.floor()`, `.ceil()`, `.round()` | the `llvm.*` intrinsic of that name |
-| `v.fma(b, c)` | `llvm.fmuladd` (fused where the target has FMA; `llvm.fma` would be a libm call per lane without it) |
+| `v.fma(b, c)`, `fma(a, b, c)` | `llvm.fma` (one rounding; a libm call per lane without FMA hardware) |
+| `v.muladd(b, c)`, `muladd(a, b, c)` | `llvm.fmuladd` (fused where the target has FMA, `fmul` + `fadd` where not) |
| `v.store(list, i)` | bounds check, `store <8 x float> …, align 4` |
Integer `/` and `%` are refused: neither x86 nor ARM has a lane-wise integer divide, and LLVM would scalarise it.
@@ -507,6 +508,19 @@ test.
| Missing return | UB if used | compile error |
| Order of evaluation | mostly unspecified | left to right; in an assignment the right side first |
| Shift ≥ width, out-of-range float→int | UB | poison, as in LLVM (not checked) |
+| `a * b + c` on floats | may be fused into an FMA (`-ffp-contract`) | never fused: two roundings |
+
+### Floating point
+
+Float operations are emitted as plain `fadd`/`fsub`/`fmul`/`fdiv`/`fneg` with **no fast-math flags**, so LLVM may not
+reassociate, contract or assume no NaNs; `linker.fch` also compiles the IR with `-ffp-contract=on`, which fuses only the
+`llvm.fmuladd` calls Finch asks for. The only flagged instruction is `v.sum()`'s `llvm.vector.reduce.fadd` (`reassoc`).
+`fma` → `llvm.fma`, `muladd` → `llvm.fmuladd`, on scalars and vectors (`fmaCall`, `fusedOp`).
+
+`f32_bits`/`f32_from_bits`/`f64_…` are `bitcast` (also between vectors). `f16_to_f32` is `bitcast i16 → half` + `fpext`,
+`f32_to_f16` is `fptrunc` + `bitcast`: one instruction with F16C or on ARM64, `__extendhfsf2`/`__truncsfhf2` from the C
+compiler's runtime library elsewhere. `bf16_to_f32` is `zext` + `shl 16` + `bitcast`; `f32_to_bf16` rounds to nearest even
+in integer ops (`u + 0x7fff + ((u >> 16) & 1)`, then `>> 16`) and keeps NaNs quiet (`| 0x40`), so it needs no library.
| Dangling pointer from `addr()` / `new` + `free` | UB | still UB |
### The chain of calls on runtime errors
diff --git a/docs/en/for-technicians.md b/docs/en/for-technicians.md
index 34aa27c..02d55fe 100644
--- a/docs/en/for-technicians.md
+++ b/docs/en/for-technicians.md
@@ -345,6 +345,40 @@ str(p) // a C char pointer as text
**Fixed-size numbers wrap around**, like real hardware: a `u8` holding 255 plus 1 becomes 0.
Calculations happen in the type of the values involved, so `u8(250) + 10` is `4`, not `260`.
+### Decimal numbers: what is guaranteed
+
+`f32` and `f64` are IEEE 754 numbers, and Finch computes them **exactly as written**, the same on every processor and
+with every `--cpu`:
+
+- Each operation rounds once, in the order written. `a * b - c * d` is two multiplies and a subtract, never folded into a
+ fused multiply-add (C compilers often do that by default, which changes the last digits).
+- Nothing is reordered or simplified: `(a + b) + c` is not turned into `a + (b + c)`, and `x * 0.0` is not assumed to be 0
+ (it is NaN when `x` is). The one exception is `v.sum()` on a vector, which adds the lanes in the fastest order.
+
+When speed matters more than the last bit, ask for it by name:
+
+| | |
+|---|---|
+| `fma(a, b, c)` | `a * b + c` with a single rounding, always. One instruction where the processor has FMA (with `--cpu`, on ARM64); a slower library call elsewhere. |
+| `muladd(a, b, c)` | `a * b + c` the fastest way on this processor: fused where it can be, a multiply and an add where not. |
+
+Both work on numbers and on vectors (`v.fma(b, c)`, `v.muladd(b, c)`).
+
+**Numbers as bits.** File formats and model weights store numbers as raw bits. These look at the bits without
+converting the value (all of them also work lane by lane on vectors):
+
+| | |
+|---|---|
+| `f32_bits(x)` / `f32_from_bits(u)` | `f32` ↔ the `u32` with the same bits (`f32_bits(1.0)` is `0x3f800000`) |
+| `f64_bits(x)` / `f64_from_bits(u)` | `f64` ↔ `u64` |
+| `f16_to_f32(h)` / `f32_to_f16(x)` | half precision, kept in a `u16`, ↔ `f32` (rounded to the nearest) |
+| `bf16_to_f32(b)` / `f32_to_bf16(x)` | bfloat16, kept in a `u16`, ↔ `f32` (rounded to the nearest, ties to even) |
+
+```c
+[]u16 weights = read_weights() // f16 numbers from a file
+v := f16_to_f32(u16x8(weights, 0)) // 8 of them as f32, at once
+```
+
---
## 7. Operators
@@ -697,6 +731,8 @@ Using `.value`, `.field` or `[i]` on a `null` pointer **stops the program** with
| `exit(code)` | Ends the program right away. |
| `addr(x)` | A pointer to `x`; `addr(function)` gives C a function pointer (callback). |
| `new(value)`, `free(p)` | Manual heap memory (see [Memory](#13-memory-who-frees-what)). |
+| `fma(a, b, c)`, `muladd(a, b, c)` | `a * b + c` with one rounding / the fastest way (see [Decimal numbers](#decimal-numbers-what-is-guaranteed)). |
+| `f32_bits(x)`, `f32_from_bits(u)`, `f16_to_f32(h)`, … | A number's bits, and half-precision numbers (see [Decimal numbers](#decimal-numbers-what-is-guaranteed)). |
| `int(x)`, `str(x)`, `u8(x)`, … | Conversions (see [Conversions](#conversions)). `str(x)` works on every type and gives the same text `print` shows. `int(text) or 0` handles text that isn't a number. |
---
@@ -1370,7 +1406,8 @@ arrays, maps and function parameters, and `print` and `str` show it.
| `v.sum()`, `v.min()`, `v.max()` | all the lanes made into one number |
| `v.dot(w)` | `(v * w).sum()` |
| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | every lane (float lanes) |
-| `v.fma(b, c)` | `v * b + c`, as one instruction where the processor has one |
+| `v.fma(b, c)` | `v * b + c` with one rounding, always |
+| `v.muladd(b, c)` | `v * b + c` the fastest way here: one instruction where the processor can |
| `v.store(list, i)` | writes the lanes to `list[i]` … (or through a `ptr`) |
`<` and the other comparisons would give an answer per lane, so they aren't there; use `min`, `max` or `v[i] < w[i]`.
@@ -1382,7 +1419,7 @@ 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)
+ acc = f32x8(a, i).muladd(f32x8(b, i), acc)
i += 8
}
total := acc.sum()
diff --git a/docs/pl/dla-inzynierow.md b/docs/pl/dla-inzynierow.md
index 425c6d0..65d700f 100644
--- a/docs/pl/dla-inzynierow.md
+++ b/docs/pl/dla-inzynierow.md
@@ -314,7 +314,8 @@ wszędzie tam, gdzie może stać typ; wywołanie `f32x8(…)` trafia do `vecMake
| `min`, `max`, `abs` | `llvm.minnum`/`maxnum`, `llvm.smin`/`umin`…, `llvm.fabs`/`llvm.abs` |
| `v.sum()`, `.min()`, `.max()` | `llvm.vector.reduce.fadd` (z `reassoc`), `.add`, `.fmin`, `.smin`… |
| `v.sqrt()`, `.floor()`, `.ceil()`, `.round()` | funkcja wewnętrzna `llvm.*` o tej nazwie |
-| `v.fma(b, c)` | `llvm.fmuladd` (jedna instrukcja tam, gdzie procesor ma FMA; `llvm.fma` bez FMA wołałoby funkcję z libm dla każdego elementu) |
+| `v.fma(b, c)`, `fma(a, b, c)` | `llvm.fma` (jedno zaokrąglenie; bez sprzętowego FMA wywołanie libm dla każdego elementu) |
+| `v.muladd(b, c)`, `muladd(a, b, c)` | `llvm.fmuladd` (jedna instrukcja tam, gdzie procesor ma FMA, a gdzie nie, `fmul` + `fadd`) |
| `v.store(lista, i)` | sprawdzenie zakresu, `store <8 x float> …, align 4` |
Dzielenie całkowite `/` i `%` jest odrzucane: ani x86, ani ARM nie mają instrukcji dzielącej liczby całkowite element
@@ -515,6 +516,21 @@ Testem akceptacyjnym jest `tests/lsp_test.py`.
| Brak `return` | UB, jeśli wynik jest użyty | błąd kompilacji |
| Kolejność obliczania | w dużej mierze nieokreślona | od lewej do prawej; w przypisaniu najpierw prawa strona |
| Przesunięcie o ≥ szerokość, liczba zmiennoprzecinkowa poza zakresem przy zamianie na całkowitą | UB | poison, jak w LLVM (niesprawdzane) |
+| `a * b + c` na liczbach zmiennoprzecinkowych | może zostać złożone w FMA (`-ffp-contract`) | nigdy nie jest łączone: dwa zaokrąglenia |
+
+### Liczby zmiennoprzecinkowe
+
+Działania na liczbach zmiennoprzecinkowych to zwykłe `fadd`/`fsub`/`fmul`/`fdiv`/`fneg` **bez flag fast-math**, więc LLVM
+nie może zmieniać kolejności działań, łączyć ich ani zakładać braku NaN; `linker.fch` kompiluje też IR z
+`-ffp-contract=on`, które łączy tylko wywołania `llvm.fmuladd`, o które prosi Finch. Jedyna instrukcja z flagą to
+`llvm.vector.reduce.fadd` w `v.sum()` (`reassoc`). `fma` → `llvm.fma`, `muladd` → `llvm.fmuladd`, na liczbach i wektorach
+(`fmaCall`, `fusedOp`).
+
+`f32_bits`/`f32_from_bits`/`f64_…` to `bitcast` (także między wektorami). `f16_to_f32` to `bitcast i16 → half` + `fpext`,
+`f32_to_f16` to `fptrunc` + `bitcast`: jedna instrukcja z F16C albo na ARM64, a gdzie indziej `__extendhfsf2`/`__truncsfhf2`
+z biblioteki uruchomieniowej kompilatora C. `bf16_to_f32` to `zext` + `shl 16` + `bitcast`; `f32_to_bf16` zaokrągla do
+najbliższej parzystej na liczbach całkowitych (`u + 0x7fff + ((u >> 16) & 1)`, potem `>> 16`) i zostawia NaN cichym
+(`| 0x40`), więc nie potrzebuje żadnej biblioteki.
| Wiszący wskaźnik z `addr()` / `new` + `free` | UB | nadal UB |
### Ścieżka wywołań przy błędach w trakcie działania
diff --git a/docs/pl/dla-technikow.md b/docs/pl/dla-technikow.md
index a925f03..281a4a5 100644
--- a/docs/pl/dla-technikow.md
+++ b/docs/pl/dla-technikow.md
@@ -348,6 +348,41 @@ str(p) // wskaźnik na znaki z C jako tekst
**Liczby o stałym rozmiarze się zawijają**, jak w prawdziwym sprzęcie: `u8` równe 255 plus 1 daje 0.
Obliczenia odbywają się w typie użytych wartości, więc `u8(250) + 10` to `4`, a nie `260`.
+### Liczby zmiennoprzecinkowe: co jest gwarantowane
+
+`f32` i `f64` to liczby w standardzie IEEE 754, a Finch liczy je **dokładnie tak, jak są zapisane**, tak samo na każdym
+procesorze i z każdym `--cpu`:
+
+- Każde działanie zaokrągla raz, w zapisanej kolejności. `a * b - c * d` to dwa mnożenia i odejmowanie, nigdy nie
+ złożone w jedno „mnożenie z dodawaniem” (FMA); kompilatory C często robią to domyślnie, co zmienia ostatnie cyfry.
+- Nic nie jest przestawiane ani upraszczane: `(a + b) + c` nie zamienia się w `a + (b + c)`, a `x * 0.0` nie jest
+ z góry uznawane za 0 (gdy `x` to NaN, wynik też jest NaN). Jedyny wyjątek to `v.sum()` na wektorze, które dodaje
+ elementy w najszybszej kolejności.
+
+Gdy szybkość jest ważniejsza niż ostatni bit, poproś o nią wprost:
+
+| | |
+|---|---|
+| `fma(a, b, c)` | `a * b + c` z jednym zaokrągleniem, zawsze. Jedna instrukcja tam, gdzie procesor ma FMA (z `--cpu`, na ARM64); gdzie indziej wolniejsza funkcja biblioteczna. |
+| `muladd(a, b, c)` | `a * b + c` najszybciej, jak się da na tym procesorze: jedną instrukcją, gdzie można, a gdzie nie, mnożeniem i dodawaniem. |
+
+Obie działają na liczbach i na wektorach (`v.fma(b, c)`, `v.muladd(b, c)`).
+
+**Liczby jako bity.** Formaty plików i wagi modeli zapisują liczby jako surowe bity. Te funkcje patrzą na bity bez
+przeliczania wartości (wszystkie działają też na wektorach, element po elemencie):
+
+| | |
+|---|---|
+| `f32_bits(x)` / `f32_from_bits(u)` | `f32` ↔ `u32` z tymi samymi bitami (`f32_bits(1.0)` to `0x3f800000`) |
+| `f64_bits(x)` / `f64_from_bits(u)` | `f64` ↔ `u64` |
+| `f16_to_f32(h)` / `f32_to_f16(x)` | liczba połówkowej precyzji, trzymana w `u16`, ↔ `f32` (zaokrąglenie do najbliższej) |
+| `bf16_to_f32(b)` / `f32_to_bf16(x)` | bfloat16, trzymany w `u16`, ↔ `f32` (zaokrąglenie do najbliższej, remisy do parzystej) |
+
+```c
+[]u16 wagi = wczytaj_wagi() // liczby f16 z pliku
+v := f16_to_f32(u16x8(wagi, 0)) // osiem z nich jako f32, naraz
+```
+
---
## 7. Operatory
@@ -700,6 +735,8 @@ Użycie `.value`, `.pole` albo `[i]` na wskaźniku `null` **zatrzymuje program**
| `exit(kod)` | Natychmiast kończy program. |
| `addr(x)` | Wskaźnik na `x`; `addr(funkcja)` daje C wskaźnik na funkcję (funkcję zwrotną). |
| `new(wartosc)`, `free(p)` | Ręczna pamięć na stercie (zob. [Pamięć](#13-pamięć-kto-co-zwalnia)). |
+| `fma(a, b, c)`, `muladd(a, b, c)` | `a * b + c` z jednym zaokrągleniem / najszybciej (zob. [Liczby zmiennoprzecinkowe](#liczby-zmiennoprzecinkowe-co-jest-gwarantowane)). |
+| `f32_bits(x)`, `f32_from_bits(u)`, `f16_to_f32(h)`, … | Bity liczby i liczby połówkowej precyzji (zob. [Liczby zmiennoprzecinkowe](#liczby-zmiennoprzecinkowe-co-jest-gwarantowane)). |
| `int(x)`, `str(x)`, `u8(x)`, … | Zamiany typów (zob. [Zamiana typów](#zamiana-typów)). `str(x)` działa na każdym typie i daje ten sam tekst, który pokazuje `print`. `int(tekst) or 0` obsługuje tekst, który nie jest liczbą. |
---
@@ -1377,7 +1414,8 @@ struktury, elementem tablicy, wartością w mapie i parametrem funkcji, a `print
| `v.sum()`, `v.min()`, `v.max()` | wszystkie elementy złożone w jedną liczbę |
| `v.dot(w)` | `(v * w).sum()` |
| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | każdy element (liczby zmiennoprzecinkowe) |
-| `v.fma(b, c)` | `v * b + c`, jedną instrukcją tam, gdzie procesor ją ma |
+| `v.fma(b, c)` | `v * b + c` z jednym zaokrągleniem, zawsze |
+| `v.muladd(b, c)` | `v * b + c` najszybciej, jak się da: jedną instrukcją, gdzie procesor to potrafi |
| `v.store(lista, i)` | zapisuje elementy do `lista[i]` … (albo przez `ptr`) |
`<` i inne porównania dałyby osobną odpowiedź dla każdego elementu, więc ich nie ma; użyj `min`, `max` albo `v[i] < w[i]`.
@@ -1389,7 +1427,7 @@ fn iloczyn([]f32 a, []f32 b) -> f32 {
suma := f32x8(0.0)
i := 0
while i + 8 <= a.len {
- suma = f32x8(a, i).fma(f32x8(b, i), suma)
+ suma = f32x8(a, i).muladd(f32x8(b, i), suma)
i += 8
}
wynik := suma.sum()
diff --git a/docs/pl/sciaga.md b/docs/pl/sciaga.md
index d0d6c49..fe062ae 100644
--- a/docs/pl/sciaga.md
+++ b/docs/pl/sciaga.md
@@ -259,7 +259,8 @@ Typy: `f32x4/8/16`, `f64x2/4/8`, `i8x16/32/64`, `i16x8/16/32`, `i32x4/8/16`, `i6
| `v[i]`, `v[i] = x`, `v.len` | jeden element; liczba elementów |
| `min(v, w)`, `max`, `abs(v)`, `clamp(v, lo, hi)` | element po elemencie |
| `v.sum()`, `v.min()`, `v.max()`, `v.dot(w)` | jedna liczba ze wszystkich elementów |
-| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()`, `v.fma(b, c)` | każdy element (liczby zmiennoprzecinkowe; `fma` = `v * b + c`) |
+| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | każdy element (liczby zmiennoprzecinkowe) |
+| `v.fma(b, c)`, `v.muladd(b, c)` | `v * b + c`: zawsze z jednym zaokrągleniem / najszybciej na tym procesorze |
| `v.store(lista, i)` | zapisuje elementy do `lista[i]` … |
---
@@ -324,6 +325,9 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza liczba → większa, licz
| `shell(polecenie)` | uruchamia polecenie, zwraca jego kod wyjścia |
| `exit(kod)` | od razu kończy program (kończy też blok `or { }`) |
| `min(a, b, …)`, `max(a, b, …)`, `abs(x)`, `clamp(x, lo, hi)` | dla dowolnych liczb (twoja funkcja o tej samej nazwie ma pierwszeństwo) |
+| `fma(a, b, c)`, `muladd(a, b, c)` | `a * b + c` z jednym zaokrągleniem / jedną instrukcją, gdzie procesor potrafi (zapisane `a * b + c` nigdy nie jest łączone) |
+| `f32_bits(x)`, `f32_from_bits(u)`, `f64_bits`, `f64_from_bits` | bity liczby zmiennoprzecinkowej jako `u32`/`u64` i z powrotem, bez zmian |
+| `f16_to_f32(h)`, `f32_to_f16(x)`, `bf16_to_f32(b)`, `f32_to_bf16(x)` | liczby połówkowej precyzji i bfloat16 (w `u16`) ↔ `f32`; także na wektorach |
| `error(komunikat)` | `return error("…")` w funkcji `-> T!` |
| `addr(x)` | wskaźnik na x; `addr(fn)` = funkcja zwrotna dla C |
| `assert(warunek)`, `assert(warunek, "dlaczego")` | zatrzymuje program, gdy warunek jest fałszywy (w bloku `test`: ten test nie przechodzi) |
diff --git a/examples/simd.fch b/examples/simd.fch
index 691227f..a2b2af1 100644
--- a/examples/simd.fch
+++ b/examples/simd.fch
@@ -13,12 +13,12 @@ fn dot_plain([]f32 a, []f32 b) -> f32 {
return total
}
-// eight at a time: f32x8(list, i) takes list[i] ... list[i+7], fma multiplies and adds in one step
+// eight at a time: f32x8(list, i) takes list[i] ... list[i+7]; muladd multiplies and adds, in one step where it can
fn dot_simd([]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)
+ acc = f32x8(a, i).muladd(f32x8(b, i), acc)
i += 8
}
total := acc.sum()
diff --git a/src/docs.fch b/src/docs.fch
index 271660a..9a132b3 100644
--- a/src/docs.fch
+++ b/src/docs.fch
@@ -26,6 +26,16 @@ fn builtins() -> []Doc {
doc("max", "max(a, b, ...) -> number", "The biggest of the numbers, in the type they all fit."),
doc("abs", "abs(x) -> number", "x without its minus sign (any number type)."),
doc("clamp", "clamp(x, lo, hi) -> number", "x, but at least lo and at most hi."),
+ doc("fma", "fma(a, b, c) -> float", "a * b + c with one rounding, always (numbers or vectors). a * b + c written out is never fused: two roundings."),
+ doc("muladd", "muladd(a, b, c) -> float", "a * b + c the fastest way here: one fused instruction where the processor has one, else a multiply and an add."),
+ doc("f32_bits", "f32_bits(f32 x) -> u32", "The bits of x, unchanged (also f32xN -> u32xN). f64_bits gives a u64."),
+ doc("f32_from_bits", "f32_from_bits(u32 bits) -> f32", "The f32 with these bits (also u32xN -> f32xN). f64_from_bits takes a u64."),
+ doc("f64_bits", "f64_bits(float x) -> u64", "The bits of x, unchanged (also f64xN -> u64xN)."),
+ doc("f64_from_bits", "f64_from_bits(u64 bits) -> float", "The float with these bits (also u64xN -> f64xN)."),
+ doc("f16_to_f32", "f16_to_f32(u16 half) -> f32", "A half-precision number, kept in a u16, as an f32 (also u16xN -> f32xN)."),
+ doc("f32_to_f16", "f32_to_f16(f32 x) -> u16", "x as a half-precision number in a u16, rounded to the nearest (also f32xN -> u16xN)."),
+ doc("bf16_to_f32", "bf16_to_f32(u16 b) -> f32", "A bfloat16, kept in a u16, as an f32 (also u16xN -> f32xN)."),
+ doc("f32_to_bf16", "f32_to_bf16(f32 x) -> u16", "x as a bfloat16 in a u16, rounded to the nearest, ties to even (also f32xN -> u16xN)."),
doc("atomic_add", "atomic_add(ptr[int] p, n) -> int", "Adds n to the number at p in one step other threads can't split; gives back what was there before."),
doc("atomic_sub", "atomic_sub(ptr[int] p, n) -> int", "Takes n from the number at p in one step; gives back what was there before."),
doc("atomic_load", "atomic_load(ptr[int] p) -> int", "Reads the number at p, as another thread last wrote it."),
@@ -107,7 +117,8 @@ fn vecMethods() -> []Doc {
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, as one instruction where the processor has one (f32/f64 lanes)."),
+ doc("fma", "fma(V b, V c) -> V", "v * b + c with one rounding, always (f32/f64 lanes). One instruction with FMA hardware, slower without."),
+ doc("muladd", "muladd(V b, V c) -> V", "v * b + c the fastest way here: fused into one instruction where the processor can (f32/f64 lanes)."),
doc("store", "store([]T list, int i)", "Writes the lanes into list[i], list[i+1], ... (or through a ptr[T]).")
]
}
diff --git a/src/gen.fch b/src/gen.fch
index d3b3eb3..2d1441a 100644
--- a/src/gen.fch
+++ b/src/gen.fch
@@ -5572,6 +5572,12 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
if ei >= 0 {
return enumFrom(g, c, ei)
}
+ 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)
}
@@ -6647,8 +6653,14 @@ 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
+// 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]))
}
@@ -6984,17 +6996,15 @@ fn vecMethod(ptr[Gen] g, ast.Node m, Val v) -> Val {
need(g, m, 0)
return vecAbs(g, v)
}
- if n == "sqrt" || n == "floor" || n == "ceil" || n == "round" || n == "fma" {
+ 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" {
+ if n == "fma" || n == "muladd" {
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])
- // fmuladd: one fused instruction where the processor has one (FMA, NEON), a multiply and an add where
- // it doesn't (llvm.fma would call a slow library function there, to round only once)
- return Val(v: vecIntrinsic(g, lt, "fmuladd", vt, [lt, lt, lt], [v.v, b.v, c.v], ""), ty: vt)
+ 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)
@@ -7028,6 +7038,178 @@ fn vecMethod(ptr[Gen] g, ast.Node m, Val v) -> Val {
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)")
+ 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)")
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)
+}
diff --git a/src/linker.fch b/src/linker.fch
index e55addc..8b452db 100644
--- a/src/linker.fch
+++ b/src/linker.fch
@@ -391,7 +391,8 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
target = " --target=" + tg.triple
}
log := b.out + ".log"
- obj := irObject(clang, target + " " + b.opt + b.extra, b.ir, b.name + "\n" + tg.triple, log) or {
+ // -ffp-contract=on: only what asks to be fused is (muladd); a * b + c stays two roundings, whatever clang's default
+ obj := irObject(clang, target + " " + b.opt + " -ffp-contract=on" + b.extra, b.ir, b.name + "\n" + tg.triple, log) or {
print(err)
host.remove(log)
return 1
diff --git a/tests/run/floatbits.fch b/tests/run/floatbits.fch
new file mode 100644
index 0000000..fe63f3a
--- /dev/null
+++ b/tests/run/floatbits.fch
@@ -0,0 +1,31 @@
+// a * b + c is never fused; fma rounds once; numbers as their bits; f16 and bf16
+
+fn plain(float a, float b, float c) -> float {
+ return a * b + c
+}
+
+fn main() {
+ // 0.1 * 10 is 1.0000000000000000555..., which rounds to 1 before the - 1 (two roundings), but not inside fma
+ print(plain(0.1, 10.0, -1.0), fma(0.1, 10.0, -1.0))
+ print(fma(f32(0.1), 10.0, -1.0), fma(2, 3, 4))
+ m := muladd(1.5, 2.0, 0.25) // fused or not, exact here
+ print(m, f32x4(1.5).muladd(2.0, 0.25), fma(f32x4(0.1), 10.0, f32x4(-1.0))[0])
+ det := f32x4(0.1) * f32x4(10.0) - f32x4(1.0)
+ print(det[0])
+
+ // bits
+ print(f32_bits(1.0), f64_bits(1.0), f32_bits(-0.0))
+ print(f32_from_bits(0x40490fdb), f64_from_bits(f64_bits(2.5)), f32_from_bits(i32(-1082130432)))
+ u32 inf = 0x7f800000
+ print(f32_from_bits(inf), f32_bits(f32x4(1.0, 2.0, -2.0, 0.5)))
+
+ // f16
+ print(f16_to_f32(0x3c00), f16_to_f32(0xc000), f16_to_f32(0x7bff), f16_to_f32(0x0001))
+ print(f32_to_f16(0.1), f32_to_f16(65504.0), f32_to_f16(1.0e6), f16_to_f32(f32_to_f16(0.333)))
+ print(f16_to_f32(u16x8(0x3c00, 0x4000, 0x4200, 0x4400, 0, 0x8000, 0x3800, 0x3400)))
+
+ // bf16
+ print(bf16_to_f32(0x3f80), bf16_to_f32(0x4049), f32_to_bf16(3.14159))
+ print(f32_to_bf16(f32_from_bits(0x3f808000)), f32_to_bf16(f32_from_bits(0x3f818000)), f32_to_bf16(f32_from_bits(0x7fc00001)))
+ print(f32_to_bf16(f32x4(1.0, -2.0, 0.5, 3.14159)), bf16_to_f32(u16x4(0x3f80, 0xc000, 0x3f00, 0x4049)))
+}
diff --git a/tests/run/floatbits.out b/tests/run/floatbits.out
new file mode 100644
index 0000000..10ab32f
--- /dev/null
+++ b/tests/run/floatbits.out
@@ -0,0 +1,13 @@
+0 5.551115123125783e-17
+1.4901161e-08 10
+3.25 f32x4(3.25, 3.25, 3.25, 3.25) 1.4901161e-08
+0
+1065353216 4607182418800017408 2147483648
+3.1415927 2.5 -1
+inf u32x4(1065353216, 1073741824, 3221225472, 1056964608)
+1 -2 65504 5.9604645e-08
+11878 31743 31744 0.3330078
+f32x8(1, 2, 3, 4, 0, -0, 0.5, 0.25)
+1 3.140625 16457
+16256 16258 32704
+u16x4(16256, 49152, 16128, 16457) f32x4(1, -2, 0.5, 3.140625)
diff --git a/tests/run/vectors.fch b/tests/run/vectors.fch
index 2d7d95f..38eca29 100644
--- a/tests/run/vectors.fch
+++ b/tests/run/vectors.fch
@@ -14,7 +14,7 @@ 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)
+ acc = f32x8(a, i).muladd(f32x8(b, i), acc)
i += 8
}
total := acc.sum()
From 04f1f94a217f86ff6b629253fcded1dc1c85f5e1 Mon Sep 17 00:00:00 2001
From: Claude
Date: Sun, 1 Nov 2026 15:41:47 +0000
Subject: [PATCH 6/7] 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
Claude-Session: https://claude.ai/code/session_01Psc42moPLwP43Cmb9oZqSR
---
runtime/finch_rt.c | 93 +++++
shell.nix | 3 +
src/ast.fch | 5 +-
src/docs.fch | 14 +-
src/embedded.fch | 2 +-
src/gen.fch | 574 +++++++++++++++++++++++++++++--
src/lexer.fch | 2 +-
src/linker.fch | 76 +++-
src/main.fch | 19 +-
src/parser.fch | 16 +
tests/fail/cpu_attr_name.fch | 7 +
tests/fail/cpu_has_name.fch | 4 +
tests/fail/cpu_wide_vector.fch | 8 +
tests/fail/vec_less.fch | 6 +-
tests/fail/vec_shuffle_range.fch | 5 +
tests/fail/vec_shuffle_var.fch | 6 +
tests/run/dot_int8.fch | 69 ++++
tests/run/dot_int8.out | 4 +
tests/run/masks.fch | 50 +++
tests/run/masks.out | 9 +
tests/run/shuffles.fch | 27 ++
tests/run/shuffles.out | 8 +
tests/run/vectors.fch | 2 +-
23 files changed, 964 insertions(+), 45 deletions(-)
create mode 100644 tests/fail/cpu_attr_name.fch
create mode 100644 tests/fail/cpu_has_name.fch
create mode 100644 tests/fail/cpu_wide_vector.fch
create mode 100644 tests/fail/vec_shuffle_range.fch
create mode 100644 tests/fail/vec_shuffle_var.fch
create mode 100644 tests/run/dot_int8.fch
create mode 100644 tests/run/dot_int8.out
create mode 100644 tests/run/masks.fch
create mode 100644 tests/run/masks.out
create mode 100644 tests/run/shuffles.fch
create mode 100644 tests/run/shuffles.out
diff --git a/runtime/finch_rt.c b/runtime/finch_rt.c
index e9a63fb..dd9afee 100644
--- a/runtime/finch_rt.c
+++ b/runtime/finch_rt.c
@@ -1147,3 +1147,96 @@ __attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t
(int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : "");
finch_panic(file, line, msg);
}
+
+// ---------- cpu_has("avx2"): what the processor running the program can do ----------
+//
+// x86: CPUID, and for the AVX and AVX-512 families also XGETBV (the system must save those registers, or the
+// instructions fault). ARM64: the kernel's hwcaps on Linux and Android, sysctl on macOS. A name of the other
+// architecture is false.
+
+#if defined(__x86_64__) || defined(__i386__)
+#include
+static int x86_leaf(unsigned leaf, unsigned sub, unsigned r[4]) {
+ unsigned max = __get_cpuid_max(0, 0);
+ if (leaf > max) return 0;
+ __cpuid_count(leaf, sub, r[0], r[1], r[2], r[3]);
+ return 1;
+}
+static unsigned long long x86_xcr0(void) {
+ unsigned lo, hi;
+ __asm__ volatile("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0));
+ return ((unsigned long long)hi << 32) | lo;
+}
+static int finch_cpu_has_c(const char *n) {
+ unsigned l1[4] = {0}, l7[4] = {0}, l71[4] = {0};
+ x86_leaf(1, 0, l1);
+ x86_leaf(7, 0, l7);
+ x86_leaf(7, 1, l71);
+ int osx = (l1[2] >> 27) & 1; // OSXSAVE
+ unsigned long long xcr0 = osx ? x86_xcr0() : 0;
+ int ymm = osx && (xcr0 & 6) == 6; // the system saves xmm and ymm
+ int zmm = ymm && (xcr0 & 0xe0) == 0xe0; // and the AVX-512 registers
+ if (!strcmp(n, "sse3")) return l1[2] & 1;
+ if (!strcmp(n, "ssse3")) return (l1[2] >> 9) & 1;
+ if (!strcmp(n, "sse4.1")) return (l1[2] >> 19) & 1;
+ if (!strcmp(n, "sse4.2")) return (l1[2] >> 20) & 1;
+ if (!strcmp(n, "popcnt")) return (l1[2] >> 23) & 1;
+ if (!strcmp(n, "avx")) return ymm && ((l1[2] >> 28) & 1);
+ if (!strcmp(n, "fma")) return ymm && ((l1[2] >> 12) & 1);
+ if (!strcmp(n, "f16c")) return ymm && ((l1[2] >> 29) & 1);
+ if (!strcmp(n, "avx2")) return ymm && ((l7[1] >> 5) & 1);
+ if (!strcmp(n, "bmi1")) return (l7[1] >> 3) & 1;
+ if (!strcmp(n, "bmi2")) return (l7[1] >> 8) & 1;
+ if (!strcmp(n, "avxvnni")) return ymm && ((l71[0] >> 4) & 1);
+ if (!strcmp(n, "avx512f")) return zmm && ((l7[1] >> 16) & 1);
+ if (!strcmp(n, "avx512dq")) return zmm && ((l7[1] >> 17) & 1);
+ if (!strcmp(n, "avx512bw")) return zmm && ((l7[1] >> 30) & 1);
+ if (!strcmp(n, "avx512vl")) return zmm && ((l7[1] >> 31) & 1);
+ if (!strcmp(n, "avx512vnni")) return zmm && ((l7[2] >> 11) & 1);
+ if (!strcmp(n, "avx512fp16")) return zmm && ((l7[3] >> 23) & 1);
+ if (!strcmp(n, "avx512bf16")) return zmm && ((l71[0] >> 5) & 1);
+ return 0;
+}
+#elif defined(__aarch64__) && defined(__APPLE__)
+#include
+static int sysctl_flag(const char *name) {
+ int v = 0;
+ size_t len = sizeof v;
+ if (sysctlbyname(name, &v, &len, NULL, 0) != 0) return 0;
+ return v != 0;
+}
+static int finch_cpu_has_c(const char *n) {
+ if (!strcmp(n, "neon")) return 1;
+ if (!strcmp(n, "fp16")) return sysctl_flag("hw.optional.arm.FEAT_FP16");
+ if (!strcmp(n, "dotprod")) return sysctl_flag("hw.optional.arm.FEAT_DotProd");
+ if (!strcmp(n, "i8mm")) return sysctl_flag("hw.optional.arm.FEAT_I8MM");
+ if (!strcmp(n, "bf16")) return sysctl_flag("hw.optional.arm.FEAT_BF16");
+ return 0; // no SVE on Apple chips
+}
+#elif defined(__aarch64__) && defined(__linux__)
+#include
+static int finch_cpu_has_c(const char *n) {
+ unsigned long h = getauxval(AT_HWCAP), h2 = getauxval(AT_HWCAP2);
+ if (!strcmp(n, "neon")) return 1;
+ if (!strcmp(n, "fp16")) return (h >> 10) & 1; // HWCAP_ASIMDHP
+ if (!strcmp(n, "dotprod")) return (h >> 20) & 1; // HWCAP_ASIMDDP
+ if (!strcmp(n, "sve")) return (h >> 22) & 1; // HWCAP_SVE
+ if (!strcmp(n, "sve2")) return (h2 >> 1) & 1; // HWCAP2_SVE2
+ if (!strcmp(n, "i8mm")) return (h2 >> 13) & 1; // HWCAP2_I8MM
+ if (!strcmp(n, "bf16")) return (h2 >> 14) & 1; // HWCAP2_BF16
+ return 0;
+}
+#else
+static int finch_cpu_has_c(const char *n) {
+ (void)n;
+ return 0;
+}
+#endif
+
+int finch_cpu_has(const FStr *name) {
+ char n[32];
+ int64_t len = name->len < 31 ? name->len : 31;
+ memcpy(n, name->ptr ? name->ptr : "", (size_t)len);
+ n[len] = 0;
+ return finch_cpu_has_c(n) ? 1 : 0;
+}
diff --git a/shell.nix b/shell.nix
index 1e3655a..90201e2 100644
--- a/shell.nix
+++ b/shell.nix
@@ -15,4 +15,7 @@ pkgs.mkShell {
vulkan-headers
vulkan-loader
];
+
+ # Nix's clang wrapper drops -march=native unless this is 0 (finch --cpu native also tells it so itself)
+ NIX_ENFORCE_NO_NATIVE = 0;
}
diff --git a/src/ast.fch b/src/ast.fch
index 4c60451..abbc87b 100644
--- a/src/ast.fch
+++ b/src/ast.fch
@@ -44,7 +44,7 @@ fn vecType(str name) -> Type {
return Type()
}
lane := name.sub(0, x)
- if !["i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64"].contains(lane) {
+ if !["i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64", "bool"].contains(lane) {
return Type()
}
n := 0
@@ -57,7 +57,7 @@ fn vecType(str name) -> Type {
if n != 2 && n != 4 && n != 8 && n != 16 && n != 32 && n != 64 {
return Type()
}
- bits := int(lane.sub(1, lane.len)) or 0
+ bits := int(lane.sub(1, lane.len)) or 1 // bool: a mask, one bit a lane
if n * bits > 512 {
return Type() // the widest registers (AVX-512) hold 512 bits
}
@@ -147,6 +147,7 @@ struct Node {
bool flag // decl: has a type; or: the right side is a { block }
bool fallible // fn: -> T! or -> !
bool exported // export fn: a C function of a library (finch build --shared)
+ str cpu // @cpu("avx2,fma") fn: the processor features the function may use
[]Node kids
[]str names // named arguments: Point(x: 1)
int line
diff --git a/src/docs.fch b/src/docs.fch
index 9a132b3..44f4615 100644
--- a/src/docs.fch
+++ b/src/docs.fch
@@ -26,6 +26,9 @@ fn builtins() -> []Doc {
doc("max", "max(a, b, ...) -> number", "The biggest of the numbers, in the type they all fit."),
doc("abs", "abs(x) -> number", "x without its minus sign (any number type)."),
doc("clamp", "clamp(x, lo, hi) -> number", "x, but at least lo and at most hi."),
+ doc("cpu_has", "cpu_has(str feature) -> bool", "Does the processor running the program have this feature (\"avx2\", \"avx512f\", \"avxvnni\", \"dotprod\", \"i8mm\" ...)? Check before calling a @cpu(...) function."),
+ doc("concat", "concat(V a, V b) -> V2", "One vector with a's lanes, then b's (twice as many)."),
+ doc("select", "select(mask, a, b) -> V", "Lane by lane: a where the mask is true, b where it is false. The mask comes from comparing vectors (v > 0.0)."),
doc("fma", "fma(a, b, c) -> float", "a * b + c with one rounding, always (numbers or vectors). a * b + c written out is never fused: two roundings."),
doc("muladd", "muladd(a, b, c) -> float", "a * b + c the fastest way here: one fused instruction where the processor has one, else a multiply and an add."),
doc("f32_bits", "f32_bits(f32 x) -> u32", "The bits of x, unchanged (also f32xN -> u32xN). f64_bits gives a u64."),
@@ -119,7 +122,16 @@ fn vecMethods() -> []Doc {
doc("abs", "abs() -> V", "Every lane without its minus sign."),
doc("fma", "fma(V b, V c) -> V", "v * b + c with one rounding, always (f32/f64 lanes). One instruction with FMA hardware, slower without."),
doc("muladd", "muladd(V b, V c) -> V", "v * b + c the fastest way here: fused into one instruction where the processor can (f32/f64 lanes)."),
- doc("store", "store([]T list, int i)", "Writes the lanes into list[i], list[i+1], ... (or through a ptr[T]).")
+ doc("store", "store([]T list, int i)", "Writes the lanes into list[i], list[i+1], ... (or through a ptr[T])."),
+ doc("store_part", "store_part([]T list, int i)", "Writes the lanes that fit in the list (for its last, shorter piece)."),
+ doc("shuffle", "shuffle(int a, int b, ...) -> V", "A vector of the lanes with these numbers (written in the code): v.shuffle(3, 2, 1, 0) reverses an f32x4."),
+ doc("lo", "lo() -> V", "The lower half of the lanes."),
+ doc("hi", "hi() -> V", "The upper half of the lanes."),
+ doc("lookup", "lookup(I indices) -> V", "For every lane of indices: this vector's lane (index % len). A byte-table lookup (pshufb, tbl)."),
+ doc("dot_add", "dot_add(a, b) -> i32xN", "On an i32xN of sums: each lane gains the sum of 4 byte products of a and b (u8/i8, 4N lanes). One instruction with AVX-VNNI, AVX-512 VNNI or ARM's dot product."),
+ doc("any", "any() -> bool", "A mask: is any lane true?"),
+ doc("all", "all() -> bool", "A mask: are all lanes true?"),
+ doc("count", "count() -> int", "A mask: how many lanes are true.")
]
}
diff --git a/src/embedded.fch b/src/embedded.fch
index 68230e4..465820d 100644
--- a/src/embedded.fch
+++ b/src/embedded.fch
@@ -20,7 +20,7 @@ fn names() -> []str {
fn texts() -> []str {
[]str list
- list.push("// The Finch runtime: string and array helpers, input, files and runtime errors.\n// It is embedded in the compiler, compiled once into a cache folder, and linked\n// statically into every Finch program. Everything here is plain C with no state.\n//\n// Layout shared with the compiler (src/gen.fch):\n// str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated\n// cap > 0 : heap memory owned by the value (cap bytes allocated)\n// cap == 0 : a string literal, valid forever, never written to\n// cap == -1: borrowed from C, must be copied before it is kept\n// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0\n// map[K]V: see \"maps\" below\n\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#ifdef _WIN32\n#define WIN32_LEAN_AND_MEAN\n#define NOMINMAX\n#include \n#include \n#include \n#else\n#include \n#include \n#include \n#include \n#include \n#endif\n\ntypedef struct {\n char *ptr;\n int64_t len;\n int64_t cap;\n} FStr;\n\ntypedef struct {\n void *ptr;\n int64_t len;\n int64_t cap;\n} FArr;\n\nstatic char empty[1] = \"\";\n\n// ---------- runtime errors ----------\n\n// A program that loads Finch libraries (finch build --shared) can take runtime errors over\n// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler\n// gets the message and must not return (longjmp out, or exit itself).\nstatic void (*fatal_hook)(const char *msg);\nvoid finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; }\n\n// The chain of Finch calls, kept by the program itself (see @finch_trace in the compiler's output):\n// each frame is { the caller's frame, the function's name, its file, the line it is at }.\ntypedef struct FFrame {\n struct FFrame *prev;\n const char *name;\n const char *file;\n int64_t line;\n} FFrame;\nstatic void *(*trace_top)(void);\nstatic void (*trace_set)(void *);\nvoid finch_trace_init(void *(*top)(void), void (*set)(void *)) {\n trace_top = top;\n trace_set = set;\n}\n\nstatic void print_trace(void) {\n if (!trace_top) return;\n FFrame *f = trace_top();\n for (int n = 0; f; n++, f = f->prev) {\n if (n == 30) {\n fprintf(stderr, \" ...\\n\");\n break;\n }\n fprintf(stderr, \" in %s (%s:%lld)\\n\", f->name, f->file, (long long)f->line);\n }\n}\n\nstatic __attribute__((noreturn)) void fatal(const char *msg) {\n if (fatal_hook) fatal_hook(msg);\n fflush(stdout);\n fprintf(stderr, \"%s\\n\", msg);\n print_trace();\n exit(1);\n}\n\n__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) {\n char full[1024];\n snprintf(full, sizeof full, \"%s:%lld: runtime error: %s\", file, (long long)line, msg);\n fflush(stdout);\n fatal(full);\n}\n\n__attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {\n char msg[128];\n snprintf(msg, sizeof msg, \"index %lld is out of range (the length is %lld)\", (long long)i, (long long)len);\n finch_panic(file, line, msg);\n}\n\nstatic void *must(void *p) {\n if (!p) {\n fflush(stdout);\n fatal(\"runtime error: out of memory\");\n }\n return p;\n}\n\nvoid *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); }\nvoid *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); }\n\n// ---------- assert and finch test ----------\n\n__attribute__((noreturn, cold)) void finch_assert_failed(const char *file, int64_t line, const FStr *why) {\n char msg[600];\n if (why->len) snprintf(msg, sizeof msg, \"assert failed: %.*s\", (int)(why->len > 500 ? 500 : why->len), why->ptr);\n else snprintf(msg, sizeof msg, \"assert failed\");\n finch_panic(file, line, msg);\n}\n\n// Each test runs on its own: a runtime error (a failed assert too) ends that test, and the next one runs.\nstatic jmp_buf test_jump;\nstatic char test_message[1100];\nstatic int64_t tests_passed, tests_failed;\n\nstatic void test_fatal(const char *msg) {\n snprintf(test_message, sizeof test_message, \"%s\", msg);\n longjmp(test_jump, 1);\n}\n\nvoid finch_test_run(const char *name, void (*fn)(void)) {\n void (*saved)(const char *) = fatal_hook;\n void *chain = trace_top ? trace_top() : NULL;\n test_message[0] = 0;\n fatal_hook = test_fatal;\n int failed = setjmp(test_jump);\n if (!failed) fn();\n fatal_hook = saved;\n if (trace_set) trace_set(chain); // a test that stopped early left its calls on the chain\n fflush(stdout);\n if (failed) {\n tests_failed++;\n printf(\"FAIL %s\\n %s\\n\", name, test_message);\n } else {\n tests_passed++;\n printf(\"ok %s\\n\", name);\n }\n fflush(stdout);\n}\n\nint finch_test_summary(void) {\n printf(\"\\n%lld passed, %lld failed\\n\", (long long)tests_passed, (long long)tests_failed);\n return tests_failed ? 1 : 0;\n}\n\n// ---------- strings ----------\n\nstatic void str_heap(FStr *out, const char *p, int64_t len) {\n char *m = finch_alloc(len + 1);\n if (len) memcpy(m, p, (size_t)len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void str_static(FStr *out, const char *lit) {\n out->ptr = (char *)lit;\n out->len = (int64_t)strlen(lit);\n out->cap = 0;\n}\n\n// A copy that can be kept: literals are shared, everything else is duplicated.\nvoid finch_str_copy(FStr *out, const FStr *s) {\n if (s->cap == 0) *out = *s;\n else str_heap(out, s->ptr, s->len);\n}\n\n// Make `s` writable (before s[i] = c).\nvoid finch_str_own(FStr *s) {\n if (s->cap > 0) return;\n FStr t;\n str_heap(&t, s->ptr, s->len);\n *s = t;\n}\n\nvoid finch_str_drop(FStr *s) {\n if (s->cap > 0) free(s->ptr);\n}\n\nvoid finch_str_from_c(FStr *out, const char *p) {\n if (!p) {\n str_static(out, empty);\n return;\n }\n out->ptr = (char *)p;\n out->len = (int64_t)strlen(p);\n out->cap = -1;\n}\n\nvoid finch_str_concat(FStr *out, const FStr *a, const FStr *b) {\n int64_t len = a->len + b->len;\n char *m = finch_alloc(len + 1);\n memcpy(m, a->ptr, (size_t)a->len);\n memcpy(m + a->len, b->ptr, (size_t)b->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nint finch_str_eq(const FStr *a, const FStr *b) {\n return a->len == b->len && memcmp(a->ptr, b->ptr, (size_t)a->len) == 0;\n}\n\nint64_t finch_str_cmp(const FStr *a, const FStr *b) {\n int64_t n = a->len < b->len ? a->len : b->len;\n int c = memcmp(a->ptr, b->ptr, (size_t)n);\n if (c) return c < 0 ? -1 : 1;\n return a->len < b->len ? -1 : a->len > b->len;\n}\n\nvoid finch_str_from_int(FStr *out, int64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%lld\", (long long)v);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_uint(FStr *out, uint64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%llu\", (unsigned long long)v);\n str_heap(out, buf, n);\n}\n\n// The shortest text that reads back as the same number (as an f32 when `single`):\n// 0.1 -> \"0.1\", 1.0/3 -> \"0.3333333333333333\", 1e8 -> \"100000000\", 1e20 -> \"1e+20\".\nstatic int fmt_float(char *buf, size_t size, double v, int single) {\n if (isnan(v)) return snprintf(buf, size, \"nan\");\n if (isinf(v)) return snprintf(buf, size, v < 0 ? \"-inf\" : \"inf\");\n int p = 1;\n for (; p < 17; p++) {\n snprintf(buf, size, \"%.*e\", p - 1, v);\n double back = strtod(buf, NULL);\n if (single ? (float)back == (float)v : back == v) break;\n }\n int exp = atoi(strchr(buf, 'e') + 1);\n if (exp < -5 || exp >= 17) return snprintf(buf, size, \"%.*g\", p, v);\n int decimals = p - 1 - exp;\n return snprintf(buf, size, \"%.*f\", decimals > 0 ? decimals : 0, v);\n}\n\nvoid finch_str_from_float(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 0);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_f32(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 1);\n str_heap(out, buf, n);\n}\n\n// str(x, 2): a fixed number of digits after the point\nvoid finch_str_fixed(FStr *out, double v, int64_t digits) {\n char buf[400];\n if (digits < 0) digits = 0;\n if (digits > 60) digits = 60;\n int n = snprintf(buf, sizeof buf, \"%.*f\", (int)digits, v);\n str_heap(out, buf, n);\n}\n\nvoid finch_print_float(double v, int32_t single) {\n char buf[64];\n fmt_float(buf, sizeof buf, v, single);\n fputs(buf, stdout);\n}\n\nvoid finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }\n\nvoid finch_str_from_bool(FStr *out, int b) { str_static(out, b ? \"true\" : \"false\"); }\n\nstatic void number_error(char *msg, size_t size, const FStr *s, const char *kind) {\n snprintf(msg, size, \"can't turn \\\"%.*s\\\" into %s\", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : \"\", kind);\n}\n\nstatic int parse_int(const FStr *s, int64_t *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n errno = 0;\n long long v = strtoll(p, &end, 10);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end || errno == ERANGE);\n}\n\nstatic int parse_float(const FStr *s, double *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n double v = strtod(p, &end);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end);\n}\n\nint64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {\n int64_t v;\n if (!parse_int(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\ndouble finch_str_to_float(const FStr *s, const char *file, int64_t line) {\n double v;\n if (!parse_float(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\n// ---------- errors as values: int(s) or 0, try read_file(p) ... ----------\n// Each gives back 1 on success; on failure 0, with the message in *err.\n\nstatic void set_error(FStr *err, const char *msg) { str_heap(err, msg, (int64_t)strlen(msg)); }\n\nint finch_str_try_int(const FStr *s, int64_t *out, FStr *err) {\n if (parse_int(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nint finch_str_try_float(const FStr *s, double *out, FStr *err) {\n if (parse_float(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nstatic void file_error(FStr *err, const char *what, const FStr *path) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't %s the file \\\"%.*s\\\": %s\", what, (int)(path->len > 200 ? 200 : path->len),\n path->ptr ? path->ptr : \"\", strerror(errno));\n set_error(err, msg);\n}\n\n// when `fn main() -> !` fails\nvoid finch_report_error(const FStr *msg) {\n fflush(stdout);\n fprintf(stderr, \"error: %.*s\\n\", (int)msg->len, msg->ptr ? msg->ptr : \"\");\n finch_str_drop((FStr *)msg);\n}\n\nvoid finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {\n if (start < 0 || end > s->len || start > end) {\n char msg[160];\n snprintf(msg, sizeof msg, \"sub(%lld, %lld) is out of range (the length is %lld)\", (long long)start,\n (long long)end, (long long)s->len);\n finch_panic(file, line, msg);\n }\n str_heap(out, s->ptr + start, end - start);\n}\n\nint64_t finch_str_find(const FStr *s, const FStr *t) {\n if (t->len == 0) return 0;\n for (int64_t i = 0; i + t->len <= s->len; i++)\n if (s->ptr[i] == t->ptr[0] && memcmp(s->ptr + i, t->ptr, (size_t)t->len) == 0) return i;\n return -1;\n}\n\nint finch_str_starts(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr, t->ptr, (size_t)t->len) == 0;\n}\n\nint finch_str_ends(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr + s->len - t->len, t->ptr, (size_t)t->len) == 0;\n}\n\nvoid finch_str_trim(FStr *out, const FStr *s) {\n int64_t a = 0, b = s->len;\n while (a < b && isspace((unsigned char)s->ptr[a])) a++;\n while (b > a && isspace((unsigned char)s->ptr[b - 1])) b--;\n str_heap(out, s->ptr + a, b - a);\n}\n\nvoid finch_str_upper(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)toupper((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_lower(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)tolower((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_replace(FStr *out, const FStr *s, const FStr *from, const FStr *to) {\n if (from->len == 0) {\n str_heap(out, s->ptr, s->len);\n return;\n }\n int64_t count = 0;\n for (int64_t i = 0; i + from->len <= s->len;) {\n if (memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n count++;\n i += from->len;\n } else {\n i++;\n }\n }\n int64_t len = s->len + count * (to->len - from->len);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < s->len;) {\n if (i + from->len <= s->len && memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n memcpy(w, to->ptr, (size_t)to->len);\n w += to->len;\n i += from->len;\n } else {\n *w++ = s->ptr[i++];\n }\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len);\n\n// \"a,b,,c\".split(\",\") -> [\"a\", \"b\", \"\", \"c\"]; split(\"\") gives single characters.\nvoid finch_str_split(FArr *out, const FStr *s, const FStr *sep) {\n out->ptr = NULL;\n out->len = out->cap = 0;\n if (sep->len == 0) {\n for (int64_t i = 0; i < s->len; i++) arr_push_str(out, s->ptr + i, 1);\n return;\n }\n int64_t start = 0;\n for (int64_t i = 0; i + sep->len <= s->len;) {\n if (memcmp(s->ptr + i, sep->ptr, (size_t)sep->len) == 0) {\n arr_push_str(out, s->ptr + start, i - start);\n i += sep->len;\n start = i;\n } else {\n i++;\n }\n }\n arr_push_str(out, s->ptr + start, s->len - start);\n}\n\nvoid finch_str_join(FStr *out, const FArr *parts, const FStr *sep) {\n const FStr *p = parts->ptr;\n int64_t len = 0;\n for (int64_t i = 0; i < parts->len; i++) len += p[i].len + (i ? sep->len : 0);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < parts->len; i++) {\n if (i) {\n memcpy(w, sep->ptr, (size_t)sep->len);\n w += sep->len;\n }\n memcpy(w, p[i].ptr, (size_t)p[i].len);\n w += p[i].len;\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_repeat(FStr *out, const FStr *s, int64_t n) {\n if (n < 0) n = 0;\n int64_t len = s->len * n;\n char *m = finch_alloc(len + 1);\n for (int64_t i = 0; i < n; i++) memcpy(m + i * s->len, s->ptr, (size_t)s->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }\n\n// ---------- building text: print(x) and str(x) of arrays, maps and structs ----------\n// A buffer starts as an empty str and grows on the heap.\n\nvoid finch_buf_add(FStr *b, const char *p, int64_t n) {\n if (b->len + n + 1 > b->cap) {\n int64_t cap = b->cap > 0 ? b->cap * 2 : 32;\n while (cap < b->len + n + 1) cap *= 2;\n b->ptr = must(realloc(b->cap > 0 ? b->ptr : NULL, (size_t)cap));\n b->cap = cap;\n }\n if (n) memcpy(b->ptr + b->len, p, (size_t)n);\n b->len += n;\n b->ptr[b->len] = 0;\n}\n\nstatic void buf_fmt(FStr *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));\nstatic void buf_fmt(FStr *b, const char *fmt, ...) {\n char t[64];\n va_list ap;\n va_start(ap, fmt);\n int n = vsnprintf(t, sizeof t, fmt, ap);\n va_end(ap);\n finch_buf_add(b, t, n);\n}\n\nvoid finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, \"%lld\", (long long)v); }\nvoid finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, \"%llu\", (unsigned long long)v); }\nvoid finch_buf_float(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 0));\n}\nvoid finch_buf_f32(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 1));\n}\nvoid finch_buf_bool(FStr *b, int v) { finch_buf_add(b, v ? \"true\" : \"false\", v ? 4 : 5); }\nvoid finch_buf_ptr(FStr *b, const void *p) {\n if (p) buf_fmt(b, \"0x%llx\", (unsigned long long)(uintptr_t)p);\n else finch_buf_add(b, \"null\", 4);\n}\n\nvoid finch_buf_char(FStr *b, char c, int quoted) {\n if (quoted) finch_buf_add(b, \"'\", 1);\n finch_buf_add(b, &c, 1);\n if (quoted) finch_buf_add(b, \"'\", 1);\n}\n\nvoid finch_buf_str(FStr *b, const FStr *s, int quoted) {\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n finch_buf_add(b, s->ptr, s->len);\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n}\n\n// print the buffer and free it\nvoid finch_buf_print(FStr *b) {\n if (b->len) fwrite(b->ptr, 1, (size_t)b->len, stdout);\n if (b->cap > 0) free(b->ptr);\n}\n\n// ---------- arrays ----------\n\nvoid finch_arr_reserve(FArr *a, int64_t need, int64_t es) {\n if (need <= a->cap) return;\n int64_t cap = a->cap * 2;\n if (cap < 4) cap = 4;\n if (cap < need) cap = need;\n a->ptr = must(realloc(a->cap > 0 ? a->ptr : NULL, (size_t)(cap * es)));\n a->cap = cap;\n}\n\nvoid finch_arr_make(FArr *out, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n out->ptr = len ? finch_zalloc(len * es) : NULL;\n out->len = len;\n out->cap = len;\n}\n\n// grow or shrink to `len`; new elements are zero\nvoid finch_arr_resize(FArr *a, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n finch_arr_reserve(a, len, es);\n if (len > a->len) memset((char *)a->ptr + a->len * es, 0, (size_t)((len - a->len) * es));\n a->len = len;\n}\n\n// a byte-for-byte copy of the elements (the compiler deep-copies owning elements afterwards)\nvoid finch_arr_clone_raw(FArr *out, const FArr *a, int64_t es) {\n out->len = a->len;\n out->cap = a->len;\n out->ptr = a->len ? finch_alloc(a->len * es) : NULL;\n if (a->len) memcpy(out->ptr, a->ptr, (size_t)(a->len * es));\n}\n\nvoid finch_arr_free(FArr *a) {\n if (a->cap > 0) free(a->ptr);\n}\n\n// open a hole at i (0 <= i <= len), len grows by one\nvoid finch_arr_insert_gap(FArr *a, int64_t i, int64_t es, const char *file, int64_t line) {\n if (i < 0 || i > a->len) finch_panic_index(file, line, i, a->len);\n finch_arr_reserve(a, a->len + 1, es);\n char *p = a->ptr;\n memmove(p + (i + 1) * es, p + i * es, (size_t)((a->len - i) * es));\n a->len++;\n}\n\n// close the hole at i (the element was already moved out), len shrinks by one\nvoid finch_arr_remove_gap(FArr *a, int64_t i, int64_t es) {\n char *p = a->ptr;\n memmove(p + i * es, p + (i + 1) * es, (size_t)((a->len - i - 1) * es));\n a->len--;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len) {\n finch_arr_reserve(a, a->len + 1, (int64_t)sizeof(FStr));\n str_heap((FStr *)a->ptr + a->len, p, len);\n a->len++;\n}\n\nvoid finch_bytes_from_str(FArr *out, const FStr *s) {\n out->len = out->cap = s->len;\n out->ptr = s->len ? finch_alloc(s->len) : NULL;\n if (s->len) memcpy(out->ptr, s->ptr, (size_t)s->len);\n}\n\n// ---------- program arguments, input and files ----------\n\nvoid finch_args(FArr *out, int argc, char **argv) {\n out->len = out->cap = argc;\n out->ptr = finch_alloc((int64_t)sizeof(FStr) * (argc ? argc : 1));\n for (int i = 0; i < argc; i++) str_heap((FStr *)out->ptr + i, argv[i], (int64_t)strlen(argv[i]));\n}\n\n// after main: the arguments go\nvoid finch_args_free(FArr *a) {\n for (int64_t i = 0; i < a->len; i++) finch_str_drop((FStr *)a->ptr + i);\n finch_arr_free(a);\n}\n\n// input(\"Name? \") prints the prompt and reads one line, without the newline.\nvoid finch_input(FStr *out, const FStr *prompt) {\n if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);\n fflush(stdout);\n // read one line of any length (getline isn't available everywhere)\n size_t size = 128, n = 0;\n char *line = finch_alloc((int64_t)size);\n int c, got = 0;\n while ((c = fgetc(stdin)) != EOF) {\n got = 1;\n if (c == '\\n') break;\n if (n + 1 >= size) line = must(realloc(line, size *= 2));\n line[n++] = (char)c;\n }\n if (!got) {\n free(line);\n str_static(out, empty);\n return;\n }\n while (n > 0 && line[n - 1] == '\\r') n--;\n line[n] = 0;\n out->ptr = line;\n out->len = (int64_t)n;\n out->cap = (int64_t)size;\n}\n\nint finch_file_exists(const FStr *path) {\n FILE *f = fopen(path->ptr, \"rb\");\n if (f) fclose(f);\n return f != NULL;\n}\n\nint finch_try_read_file(FStr *out, const FStr *path, FStr *err) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n file_error(err, \"read\", path);\n return 0;\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n return 1;\n}\n\nvoid finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't read the file \\\"%.*s\\\": %s\", (int)(path->len > 200 ? 200 : path->len),\n path->ptr, strerror(errno));\n finch_panic(file, line, msg);\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n}\n\n// shell(\"ls -l\"): run a command, give back its exit code\nint64_t finch_shell(const FStr *cmd) {\n fflush(stdout);\n int status = system(cmd->ptr ? cmd->ptr : \"\");\n#ifdef _WIN32\n return status; // Windows gives the exit code directly\n#else\n if (status == -1) return -1;\n if (WIFEXITED(status)) return WEXITSTATUS(status);\n return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);\n#endif\n}\n\nint finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : \"\") == 0; }\n\nint finch_try_delete_file(const FStr *path, FStr *err) {\n if (remove(path->ptr ? path->ptr : \"\") == 0) return 1;\n file_error(err, \"delete\", path);\n return 0;\n}\n\n// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here\nvoid *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; }\n\nint finch_write_file(const FStr *path, const FStr *text) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"wb\");\n if (!f) return 0;\n size_t put = text->len ? fwrite(text->ptr, 1, (size_t)text->len, f) : 0;\n return fclose(f) == 0 && put == (size_t)text->len;\n}\n\nint finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {\n errno = 0;\n if (finch_write_file(path, text)) return 1;\n if (!errno) errno = EIO;\n file_error(err, \"write\", path);\n return 0;\n}\n\n// ---------- folders (std/fs.fch, through std/finch_fs.h) ----------\n\nint finch_fs_is_dir(const char *path) {\n#ifdef _WIN32\n struct _finddata_t d;\n intptr_t h = _findfirst(path, &d);\n if (h == -1) return 0;\n _findclose(h);\n return (d.attrib & _A_SUBDIR) != 0;\n#else\n struct stat st;\n return stat(path, &st) == 0 && S_ISDIR(st.st_mode);\n#endif\n}\n\nint finch_fs_exists(const char *path) {\n#ifdef _WIN32\n return _access(path, 0) == 0;\n#else\n struct stat st;\n return stat(path, &st) == 0;\n#endif\n}\n\nstatic int make_one_dir(const char *p) {\n#ifdef _WIN32\n return _mkdir(p) == 0 || errno == EEXIST;\n#else\n return mkdir(p, 0777) == 0 || errno == EEXIST;\n#endif\n}\n\n// mkdir -p: the parents too; 1 if the folder is there afterwards\nint finch_fs_mkdir(const char *path) {\n size_t n = strlen(path);\n char *p = finch_alloc((int64_t)n + 1);\n memcpy(p, path, n + 1);\n for (size_t i = 1; i < n; i++) {\n if (p[i] == '/' || p[i] == '\\\\') {\n if (p[i - 1] == ':') continue; /* C: is a drive */\n char c = p[i];\n p[i] = 0;\n make_one_dir(p);\n p[i] = c;\n }\n }\n int ok = make_one_dir(p) && finch_fs_is_dir(p);\n free(p);\n return ok;\n}\n\nint finch_fs_remove_dir(const char *path) {\n#ifdef _WIN32\n return _rmdir(path) == 0;\n#else\n return rmdir(path) == 0;\n#endif\n}\n\n// the names in a folder, one per call (\"\" at the end), without . and ..\ntypedef struct {\n#ifdef _WIN32\n intptr_t h;\n struct _finddata_t d;\n int first;\n#else\n DIR *d;\n#endif\n} FsList;\n\nvoid *finch_fs_list_open(const char *path) {\n FsList *l = finch_zalloc((int64_t)sizeof(FsList));\n#ifdef _WIN32\n size_t n = strlen(path);\n char *pat = finch_alloc((int64_t)n + 3);\n memcpy(pat, path, n);\n memcpy(pat + n, \"\\\\*\", 3);\n l->h = _findfirst(pat, &l->d);\n free(pat);\n if (l->h == -1) {\n free(l);\n return NULL;\n }\n l->first = 1;\n#else\n l->d = opendir(path);\n if (!l->d) {\n free(l);\n return NULL;\n }\n#endif\n return l;\n}\n\nconst char *finch_fs_list_next(void *handle) {\n FsList *l = handle;\n for (;;) {\n#ifdef _WIN32\n if (!l->first && _findnext(l->h, &l->d) != 0) return \"\";\n l->first = 0;\n const char *name = l->d.name;\n#else\n struct dirent *e = readdir(l->d);\n if (!e) return \"\";\n const char *name = e->d_name;\n#endif\n if (strcmp(name, \".\") != 0 && strcmp(name, \"..\") != 0) return name;\n }\n}\n\nvoid finch_fs_list_close(void *handle) {\n FsList *l = handle;\n#ifdef _WIN32\n _findclose(l->h);\n#else\n closedir(l->d);\n#endif\n free(l);\n}\n\n// where a program keeps its own files for this user (saves, settings); \"\" if unknown\n// Linux: $XDG_DATA_HOME or ~/.local/share macOS: ~/Library/Application Support Windows: %APPDATA%\nconst char *finch_fs_data_home(void) {\n static char buf[4096];\n const char *v;\n#if defined(_WIN32)\n v = getenv(\"APPDATA\");\n snprintf(buf, sizeof buf, \"%s\", v ? v : \"\");\n#elif defined(__APPLE__)\n v = getenv(\"HOME\");\n if (v) snprintf(buf, sizeof buf, \"%s/Library/Application Support\", v);\n else buf[0] = 0;\n#else\n v = getenv(\"XDG_DATA_HOME\");\n if (v && *v) snprintf(buf, sizeof buf, \"%s\", v);\n else if ((v = getenv(\"HOME\"))) snprintf(buf, sizeof buf, \"%s/.local/share\", v);\n else buf[0] = 0;\n#endif\n return buf;\n}\n\n// ---------- threads (std/thread.fch, through std/finch_thread.h) ----------\n\ntypedef struct {\n void (*fn)(void *);\n void *arg;\n} ThreadStart;\n\n#ifdef _WIN32\nstatic DWORD WINAPI thread_main(LPVOID p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return 0;\n}\n#else\nstatic void *thread_main(void *p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return NULL;\n}\n#endif\n\n// run fn(arg) on a new thread; NULL if it couldn't start\nvoid *finch_thread_spawn(void (*fn)(void *), void *arg) {\n ThreadStart *st = finch_alloc((int64_t)sizeof(ThreadStart));\n st->fn = fn;\n st->arg = arg;\n#ifdef _WIN32\n HANDLE h = CreateThread(NULL, 0, thread_main, st, 0, NULL);\n if (!h) {\n free(st);\n return NULL;\n }\n return h;\n#else\n pthread_t *t = finch_alloc((int64_t)sizeof(pthread_t));\n if (pthread_create(t, NULL, thread_main, st) != 0) {\n free(st);\n free(t);\n return NULL;\n }\n return t;\n#endif\n}\n\n// wait for the thread to end; 1 if that worked\nint finch_thread_join(void *t) {\n if (!t) return 0;\n#ifdef _WIN32\n int ok = WaitForSingleObject((HANDLE)t, INFINITE) == WAIT_OBJECT_0;\n CloseHandle((HANDLE)t);\n return ok;\n#else\n int ok = pthread_join(*(pthread_t *)t, NULL) == 0;\n free(t);\n return ok;\n#endif\n}\n\nvoid *finch_mutex_new(void) {\n#ifdef _WIN32\n CRITICAL_SECTION *m = finch_alloc((int64_t)sizeof(CRITICAL_SECTION));\n InitializeCriticalSection(m);\n#else\n pthread_mutex_t *m = finch_alloc((int64_t)sizeof(pthread_mutex_t));\n pthread_mutex_init(m, NULL);\n#endif\n return m;\n}\n\nvoid finch_mutex_lock(void *m) {\n#ifdef _WIN32\n EnterCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_lock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_unlock(void *m) {\n#ifdef _WIN32\n LeaveCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_unlock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_free(void *m) {\n if (!m) return;\n#ifdef _WIN32\n DeleteCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_destroy((pthread_mutex_t *)m);\n#endif\n free(m);\n}\n\nint finch_cpu_count(void) {\n#ifdef _WIN32\n SYSTEM_INFO si;\n GetSystemInfo(&si);\n return (int)si.dwNumberOfProcessors;\n#else\n long n = sysconf(_SC_NPROCESSORS_ONLN);\n return n > 0 ? (int)n : 1;\n#endif\n}\n\n// ---------- maps ----------\n// map[K]V = { entries; len; used; cap; index; icap; adds }\n// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.\n// A removed entry keeps its slot (hash 0) until the next resize packs the slots.\n// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.\n// adds: how many keys were ever added; a `for` over the map stops with an error if it changes.\n// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.\n// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).\n\ntypedef struct {\n char *entries;\n int64_t len, used, cap;\n int64_t *index;\n int64_t icap;\n int64_t adds;\n} FMap;\n\nstatic uint64_t map_hash(const void *key, int64_t ks, int kstr) {\n const unsigned char *p;\n size_t n;\n if (kstr) {\n const FStr *s = key;\n p = (const unsigned char *)s->ptr;\n n = (size_t)s->len;\n } else {\n p = key;\n n = (size_t)ks;\n }\n uint64_t h = 1469598103934665603ULL; // FNV-1a, then mixed so the low bits spread well\n for (size_t i = 0; i < n; i++) {\n h ^= p[i];\n h *= 1099511628211ULL;\n }\n h ^= h >> 31;\n h *= 0xbf58476d1ce4e5b9ULL;\n h ^= h >> 29;\n return h | 1; // 0 marks a removed entry\n}\n\nstatic int map_key_eq(const char *a, const void *b, int64_t ks, int kstr) {\n if (!kstr) return memcmp(a, b, (size_t)ks) == 0;\n const FStr *x = (const FStr *)a, *y = b;\n return x->len == y->len && (x->len == 0 || memcmp(x->ptr, y->ptr, (size_t)x->len) == 0);\n}\n\nstatic void map_reindex(FMap *m, int64_t es) {\n if (m->len != m->used) { // pack the slots of removed entries away\n int64_t j = 0;\n for (int64_t i = 0; i < m->used; i++) {\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == 0) continue;\n if (i != j) memmove(m->entries + j * es, e, (size_t)es);\n j++;\n }\n m->used = j;\n }\n int64_t icap = 16;\n while (icap < m->cap * 2) icap *= 2;\n free(m->index);\n m->index = finch_alloc(icap * (int64_t)sizeof(int64_t));\n memset(m->index, 0xff, (size_t)icap * sizeof(int64_t));\n m->icap = icap;\n for (int64_t i = 0; i < m->used; i++) {\n uint64_t h = *(uint64_t *)(m->entries + i * es);\n int64_t s = (int64_t)(h & (uint64_t)(icap - 1));\n while (m->index[s] != -1) s = (s + 1) & (icap - 1);\n m->index[s] = i;\n }\n}\n\n// the entry number of `key`, or -1\nint64_t finch_map_find(const FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr) {\n if (m->len == 0) return -1;\n uint64_t h = map_hash(key, ks, kstr);\n for (int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));; s = (s + 1) & (m->icap - 1)) {\n int64_t i = m->index[s];\n if (i == -1) return -1;\n if (i < 0) continue;\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == h && map_key_eq(e + 8, key, ks, kstr)) return i;\n }\n}\n\n// The entry for `key`. A new one is zeroed except its hash (*added = 1); the compiler then stores the key.\nint64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr, int32_t *added) {\n int64_t i = finch_map_find(m, key, es, ks, kstr);\n *added = i < 0;\n if (i >= 0) return i;\n if (m->used == m->cap) {\n if (m->len * 4 < m->cap * 3) { // many removed slots: pack them instead of growing\n map_reindex(m, es);\n } else {\n m->cap = m->cap ? m->cap * 2 : 8;\n m->entries = must(realloc(m->entries, (size_t)(m->cap * es)));\n map_reindex(m, es);\n }\n }\n uint64_t h = map_hash(key, ks, kstr);\n i = m->used++;\n char *e = m->entries + i * es;\n memset(e, 0, (size_t)es);\n *(uint64_t *)e = h;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);\n m->index[s] = i;\n m->len++;\n m->adds++;\n return i;\n}\n\n// Forget entry i (its key and value were already dropped by the compiler).\nvoid finch_map_remove_at(FMap *m, int64_t i, int64_t es) {\n char *e = m->entries + i * es;\n uint64_t h = *(uint64_t *)e;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] != i) s = (s + 1) & (m->icap - 1);\n m->index[s] = -2;\n *(uint64_t *)e = 0;\n if (--m->len == 0) {\n m->used = 0;\n memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n }\n}\n\nvoid finch_map_clear(FMap *m) {\n m->len = m->used = 0;\n if (m->index) memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n}\n\nvoid finch_map_free(FMap *m) {\n free(m->entries);\n free(m->index);\n}\n\n// A copy of the slots and the index; the compiler then copies the keys and values that own memory.\nvoid finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {\n *out = *m;\n if (!m->cap) return;\n out->entries = finch_alloc(m->cap * es);\n memcpy(out->entries, m->entries, (size_t)(m->used * es));\n out->index = finch_alloc(m->icap * (int64_t)sizeof(int64_t));\n memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));\n}\n\n__attribute__((noreturn, cold)) void finch_map_changed(const char *file, int64_t line) {\n finch_panic(file, line, \"a key was added to the map while looping over it (collect the new keys, and add them after the loop)\");\n}\n\n__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {\n char msg[200];\n snprintf(msg, sizeof msg, \"the key %.*s is not in the map (check with .has(key), or use .get(key, default))\",\n (int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : \"\");\n finch_panic(file, line, msg);\n}\n")
+ list.push("// The Finch runtime: string and array helpers, input, files and runtime errors.\n// It is embedded in the compiler, compiled once into a cache folder, and linked\n// statically into every Finch program. Everything here is plain C with no state.\n//\n// Layout shared with the compiler (src/gen.fch):\n// str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated\n// cap > 0 : heap memory owned by the value (cap bytes allocated)\n// cap == 0 : a string literal, valid forever, never written to\n// cap == -1: borrowed from C, must be copied before it is kept\n// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0\n// map[K]V: see \"maps\" below\n\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#ifdef _WIN32\n#define WIN32_LEAN_AND_MEAN\n#define NOMINMAX\n#include \n#include \n#include \n#else\n#include \n#include \n#include \n#include \n#include \n#endif\n\ntypedef struct {\n char *ptr;\n int64_t len;\n int64_t cap;\n} FStr;\n\ntypedef struct {\n void *ptr;\n int64_t len;\n int64_t cap;\n} FArr;\n\nstatic char empty[1] = \"\";\n\n// ---------- runtime errors ----------\n\n// A program that loads Finch libraries (finch build --shared) can take runtime errors over\n// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler\n// gets the message and must not return (longjmp out, or exit itself).\nstatic void (*fatal_hook)(const char *msg);\nvoid finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; }\n\n// The chain of Finch calls, kept by the program itself (see @finch_trace in the compiler's output):\n// each frame is { the caller's frame, the function's name, its file, the line it is at }.\ntypedef struct FFrame {\n struct FFrame *prev;\n const char *name;\n const char *file;\n int64_t line;\n} FFrame;\nstatic void *(*trace_top)(void);\nstatic void (*trace_set)(void *);\nvoid finch_trace_init(void *(*top)(void), void (*set)(void *)) {\n trace_top = top;\n trace_set = set;\n}\n\nstatic void print_trace(void) {\n if (!trace_top) return;\n FFrame *f = trace_top();\n for (int n = 0; f; n++, f = f->prev) {\n if (n == 30) {\n fprintf(stderr, \" ...\\n\");\n break;\n }\n fprintf(stderr, \" in %s (%s:%lld)\\n\", f->name, f->file, (long long)f->line);\n }\n}\n\nstatic __attribute__((noreturn)) void fatal(const char *msg) {\n if (fatal_hook) fatal_hook(msg);\n fflush(stdout);\n fprintf(stderr, \"%s\\n\", msg);\n print_trace();\n exit(1);\n}\n\n__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) {\n char full[1024];\n snprintf(full, sizeof full, \"%s:%lld: runtime error: %s\", file, (long long)line, msg);\n fflush(stdout);\n fatal(full);\n}\n\n__attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {\n char msg[128];\n snprintf(msg, sizeof msg, \"index %lld is out of range (the length is %lld)\", (long long)i, (long long)len);\n finch_panic(file, line, msg);\n}\n\nstatic void *must(void *p) {\n if (!p) {\n fflush(stdout);\n fatal(\"runtime error: out of memory\");\n }\n return p;\n}\n\nvoid *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); }\nvoid *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); }\n\n// ---------- assert and finch test ----------\n\n__attribute__((noreturn, cold)) void finch_assert_failed(const char *file, int64_t line, const FStr *why) {\n char msg[600];\n if (why->len) snprintf(msg, sizeof msg, \"assert failed: %.*s\", (int)(why->len > 500 ? 500 : why->len), why->ptr);\n else snprintf(msg, sizeof msg, \"assert failed\");\n finch_panic(file, line, msg);\n}\n\n// Each test runs on its own: a runtime error (a failed assert too) ends that test, and the next one runs.\nstatic jmp_buf test_jump;\nstatic char test_message[1100];\nstatic int64_t tests_passed, tests_failed;\n\nstatic void test_fatal(const char *msg) {\n snprintf(test_message, sizeof test_message, \"%s\", msg);\n longjmp(test_jump, 1);\n}\n\nvoid finch_test_run(const char *name, void (*fn)(void)) {\n void (*saved)(const char *) = fatal_hook;\n void *chain = trace_top ? trace_top() : NULL;\n test_message[0] = 0;\n fatal_hook = test_fatal;\n int failed = setjmp(test_jump);\n if (!failed) fn();\n fatal_hook = saved;\n if (trace_set) trace_set(chain); // a test that stopped early left its calls on the chain\n fflush(stdout);\n if (failed) {\n tests_failed++;\n printf(\"FAIL %s\\n %s\\n\", name, test_message);\n } else {\n tests_passed++;\n printf(\"ok %s\\n\", name);\n }\n fflush(stdout);\n}\n\nint finch_test_summary(void) {\n printf(\"\\n%lld passed, %lld failed\\n\", (long long)tests_passed, (long long)tests_failed);\n return tests_failed ? 1 : 0;\n}\n\n// ---------- strings ----------\n\nstatic void str_heap(FStr *out, const char *p, int64_t len) {\n char *m = finch_alloc(len + 1);\n if (len) memcpy(m, p, (size_t)len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void str_static(FStr *out, const char *lit) {\n out->ptr = (char *)lit;\n out->len = (int64_t)strlen(lit);\n out->cap = 0;\n}\n\n// A copy that can be kept: literals are shared, everything else is duplicated.\nvoid finch_str_copy(FStr *out, const FStr *s) {\n if (s->cap == 0) *out = *s;\n else str_heap(out, s->ptr, s->len);\n}\n\n// Make `s` writable (before s[i] = c).\nvoid finch_str_own(FStr *s) {\n if (s->cap > 0) return;\n FStr t;\n str_heap(&t, s->ptr, s->len);\n *s = t;\n}\n\nvoid finch_str_drop(FStr *s) {\n if (s->cap > 0) free(s->ptr);\n}\n\nvoid finch_str_from_c(FStr *out, const char *p) {\n if (!p) {\n str_static(out, empty);\n return;\n }\n out->ptr = (char *)p;\n out->len = (int64_t)strlen(p);\n out->cap = -1;\n}\n\nvoid finch_str_concat(FStr *out, const FStr *a, const FStr *b) {\n int64_t len = a->len + b->len;\n char *m = finch_alloc(len + 1);\n memcpy(m, a->ptr, (size_t)a->len);\n memcpy(m + a->len, b->ptr, (size_t)b->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nint finch_str_eq(const FStr *a, const FStr *b) {\n return a->len == b->len && memcmp(a->ptr, b->ptr, (size_t)a->len) == 0;\n}\n\nint64_t finch_str_cmp(const FStr *a, const FStr *b) {\n int64_t n = a->len < b->len ? a->len : b->len;\n int c = memcmp(a->ptr, b->ptr, (size_t)n);\n if (c) return c < 0 ? -1 : 1;\n return a->len < b->len ? -1 : a->len > b->len;\n}\n\nvoid finch_str_from_int(FStr *out, int64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%lld\", (long long)v);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_uint(FStr *out, uint64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%llu\", (unsigned long long)v);\n str_heap(out, buf, n);\n}\n\n// The shortest text that reads back as the same number (as an f32 when `single`):\n// 0.1 -> \"0.1\", 1.0/3 -> \"0.3333333333333333\", 1e8 -> \"100000000\", 1e20 -> \"1e+20\".\nstatic int fmt_float(char *buf, size_t size, double v, int single) {\n if (isnan(v)) return snprintf(buf, size, \"nan\");\n if (isinf(v)) return snprintf(buf, size, v < 0 ? \"-inf\" : \"inf\");\n int p = 1;\n for (; p < 17; p++) {\n snprintf(buf, size, \"%.*e\", p - 1, v);\n double back = strtod(buf, NULL);\n if (single ? (float)back == (float)v : back == v) break;\n }\n int exp = atoi(strchr(buf, 'e') + 1);\n if (exp < -5 || exp >= 17) return snprintf(buf, size, \"%.*g\", p, v);\n int decimals = p - 1 - exp;\n return snprintf(buf, size, \"%.*f\", decimals > 0 ? decimals : 0, v);\n}\n\nvoid finch_str_from_float(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 0);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_f32(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 1);\n str_heap(out, buf, n);\n}\n\n// str(x, 2): a fixed number of digits after the point\nvoid finch_str_fixed(FStr *out, double v, int64_t digits) {\n char buf[400];\n if (digits < 0) digits = 0;\n if (digits > 60) digits = 60;\n int n = snprintf(buf, sizeof buf, \"%.*f\", (int)digits, v);\n str_heap(out, buf, n);\n}\n\nvoid finch_print_float(double v, int32_t single) {\n char buf[64];\n fmt_float(buf, sizeof buf, v, single);\n fputs(buf, stdout);\n}\n\nvoid finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }\n\nvoid finch_str_from_bool(FStr *out, int b) { str_static(out, b ? \"true\" : \"false\"); }\n\nstatic void number_error(char *msg, size_t size, const FStr *s, const char *kind) {\n snprintf(msg, size, \"can't turn \\\"%.*s\\\" into %s\", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : \"\", kind);\n}\n\nstatic int parse_int(const FStr *s, int64_t *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n errno = 0;\n long long v = strtoll(p, &end, 10);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end || errno == ERANGE);\n}\n\nstatic int parse_float(const FStr *s, double *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n double v = strtod(p, &end);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end);\n}\n\nint64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {\n int64_t v;\n if (!parse_int(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\ndouble finch_str_to_float(const FStr *s, const char *file, int64_t line) {\n double v;\n if (!parse_float(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\n// ---------- errors as values: int(s) or 0, try read_file(p) ... ----------\n// Each gives back 1 on success; on failure 0, with the message in *err.\n\nstatic void set_error(FStr *err, const char *msg) { str_heap(err, msg, (int64_t)strlen(msg)); }\n\nint finch_str_try_int(const FStr *s, int64_t *out, FStr *err) {\n if (parse_int(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nint finch_str_try_float(const FStr *s, double *out, FStr *err) {\n if (parse_float(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nstatic void file_error(FStr *err, const char *what, const FStr *path) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't %s the file \\\"%.*s\\\": %s\", what, (int)(path->len > 200 ? 200 : path->len),\n path->ptr ? path->ptr : \"\", strerror(errno));\n set_error(err, msg);\n}\n\n// when `fn main() -> !` fails\nvoid finch_report_error(const FStr *msg) {\n fflush(stdout);\n fprintf(stderr, \"error: %.*s\\n\", (int)msg->len, msg->ptr ? msg->ptr : \"\");\n finch_str_drop((FStr *)msg);\n}\n\nvoid finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {\n if (start < 0 || end > s->len || start > end) {\n char msg[160];\n snprintf(msg, sizeof msg, \"sub(%lld, %lld) is out of range (the length is %lld)\", (long long)start,\n (long long)end, (long long)s->len);\n finch_panic(file, line, msg);\n }\n str_heap(out, s->ptr + start, end - start);\n}\n\nint64_t finch_str_find(const FStr *s, const FStr *t) {\n if (t->len == 0) return 0;\n for (int64_t i = 0; i + t->len <= s->len; i++)\n if (s->ptr[i] == t->ptr[0] && memcmp(s->ptr + i, t->ptr, (size_t)t->len) == 0) return i;\n return -1;\n}\n\nint finch_str_starts(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr, t->ptr, (size_t)t->len) == 0;\n}\n\nint finch_str_ends(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr + s->len - t->len, t->ptr, (size_t)t->len) == 0;\n}\n\nvoid finch_str_trim(FStr *out, const FStr *s) {\n int64_t a = 0, b = s->len;\n while (a < b && isspace((unsigned char)s->ptr[a])) a++;\n while (b > a && isspace((unsigned char)s->ptr[b - 1])) b--;\n str_heap(out, s->ptr + a, b - a);\n}\n\nvoid finch_str_upper(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)toupper((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_lower(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)tolower((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_replace(FStr *out, const FStr *s, const FStr *from, const FStr *to) {\n if (from->len == 0) {\n str_heap(out, s->ptr, s->len);\n return;\n }\n int64_t count = 0;\n for (int64_t i = 0; i + from->len <= s->len;) {\n if (memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n count++;\n i += from->len;\n } else {\n i++;\n }\n }\n int64_t len = s->len + count * (to->len - from->len);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < s->len;) {\n if (i + from->len <= s->len && memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n memcpy(w, to->ptr, (size_t)to->len);\n w += to->len;\n i += from->len;\n } else {\n *w++ = s->ptr[i++];\n }\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len);\n\n// \"a,b,,c\".split(\",\") -> [\"a\", \"b\", \"\", \"c\"]; split(\"\") gives single characters.\nvoid finch_str_split(FArr *out, const FStr *s, const FStr *sep) {\n out->ptr = NULL;\n out->len = out->cap = 0;\n if (sep->len == 0) {\n for (int64_t i = 0; i < s->len; i++) arr_push_str(out, s->ptr + i, 1);\n return;\n }\n int64_t start = 0;\n for (int64_t i = 0; i + sep->len <= s->len;) {\n if (memcmp(s->ptr + i, sep->ptr, (size_t)sep->len) == 0) {\n arr_push_str(out, s->ptr + start, i - start);\n i += sep->len;\n start = i;\n } else {\n i++;\n }\n }\n arr_push_str(out, s->ptr + start, s->len - start);\n}\n\nvoid finch_str_join(FStr *out, const FArr *parts, const FStr *sep) {\n const FStr *p = parts->ptr;\n int64_t len = 0;\n for (int64_t i = 0; i < parts->len; i++) len += p[i].len + (i ? sep->len : 0);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < parts->len; i++) {\n if (i) {\n memcpy(w, sep->ptr, (size_t)sep->len);\n w += sep->len;\n }\n memcpy(w, p[i].ptr, (size_t)p[i].len);\n w += p[i].len;\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_repeat(FStr *out, const FStr *s, int64_t n) {\n if (n < 0) n = 0;\n int64_t len = s->len * n;\n char *m = finch_alloc(len + 1);\n for (int64_t i = 0; i < n; i++) memcpy(m + i * s->len, s->ptr, (size_t)s->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }\n\n// ---------- building text: print(x) and str(x) of arrays, maps and structs ----------\n// A buffer starts as an empty str and grows on the heap.\n\nvoid finch_buf_add(FStr *b, const char *p, int64_t n) {\n if (b->len + n + 1 > b->cap) {\n int64_t cap = b->cap > 0 ? b->cap * 2 : 32;\n while (cap < b->len + n + 1) cap *= 2;\n b->ptr = must(realloc(b->cap > 0 ? b->ptr : NULL, (size_t)cap));\n b->cap = cap;\n }\n if (n) memcpy(b->ptr + b->len, p, (size_t)n);\n b->len += n;\n b->ptr[b->len] = 0;\n}\n\nstatic void buf_fmt(FStr *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));\nstatic void buf_fmt(FStr *b, const char *fmt, ...) {\n char t[64];\n va_list ap;\n va_start(ap, fmt);\n int n = vsnprintf(t, sizeof t, fmt, ap);\n va_end(ap);\n finch_buf_add(b, t, n);\n}\n\nvoid finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, \"%lld\", (long long)v); }\nvoid finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, \"%llu\", (unsigned long long)v); }\nvoid finch_buf_float(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 0));\n}\nvoid finch_buf_f32(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 1));\n}\nvoid finch_buf_bool(FStr *b, int v) { finch_buf_add(b, v ? \"true\" : \"false\", v ? 4 : 5); }\nvoid finch_buf_ptr(FStr *b, const void *p) {\n if (p) buf_fmt(b, \"0x%llx\", (unsigned long long)(uintptr_t)p);\n else finch_buf_add(b, \"null\", 4);\n}\n\nvoid finch_buf_char(FStr *b, char c, int quoted) {\n if (quoted) finch_buf_add(b, \"'\", 1);\n finch_buf_add(b, &c, 1);\n if (quoted) finch_buf_add(b, \"'\", 1);\n}\n\nvoid finch_buf_str(FStr *b, const FStr *s, int quoted) {\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n finch_buf_add(b, s->ptr, s->len);\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n}\n\n// print the buffer and free it\nvoid finch_buf_print(FStr *b) {\n if (b->len) fwrite(b->ptr, 1, (size_t)b->len, stdout);\n if (b->cap > 0) free(b->ptr);\n}\n\n// ---------- arrays ----------\n\nvoid finch_arr_reserve(FArr *a, int64_t need, int64_t es) {\n if (need <= a->cap) return;\n int64_t cap = a->cap * 2;\n if (cap < 4) cap = 4;\n if (cap < need) cap = need;\n a->ptr = must(realloc(a->cap > 0 ? a->ptr : NULL, (size_t)(cap * es)));\n a->cap = cap;\n}\n\nvoid finch_arr_make(FArr *out, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n out->ptr = len ? finch_zalloc(len * es) : NULL;\n out->len = len;\n out->cap = len;\n}\n\n// grow or shrink to `len`; new elements are zero\nvoid finch_arr_resize(FArr *a, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n finch_arr_reserve(a, len, es);\n if (len > a->len) memset((char *)a->ptr + a->len * es, 0, (size_t)((len - a->len) * es));\n a->len = len;\n}\n\n// a byte-for-byte copy of the elements (the compiler deep-copies owning elements afterwards)\nvoid finch_arr_clone_raw(FArr *out, const FArr *a, int64_t es) {\n out->len = a->len;\n out->cap = a->len;\n out->ptr = a->len ? finch_alloc(a->len * es) : NULL;\n if (a->len) memcpy(out->ptr, a->ptr, (size_t)(a->len * es));\n}\n\nvoid finch_arr_free(FArr *a) {\n if (a->cap > 0) free(a->ptr);\n}\n\n// open a hole at i (0 <= i <= len), len grows by one\nvoid finch_arr_insert_gap(FArr *a, int64_t i, int64_t es, const char *file, int64_t line) {\n if (i < 0 || i > a->len) finch_panic_index(file, line, i, a->len);\n finch_arr_reserve(a, a->len + 1, es);\n char *p = a->ptr;\n memmove(p + (i + 1) * es, p + i * es, (size_t)((a->len - i) * es));\n a->len++;\n}\n\n// close the hole at i (the element was already moved out), len shrinks by one\nvoid finch_arr_remove_gap(FArr *a, int64_t i, int64_t es) {\n char *p = a->ptr;\n memmove(p + i * es, p + (i + 1) * es, (size_t)((a->len - i - 1) * es));\n a->len--;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len) {\n finch_arr_reserve(a, a->len + 1, (int64_t)sizeof(FStr));\n str_heap((FStr *)a->ptr + a->len, p, len);\n a->len++;\n}\n\nvoid finch_bytes_from_str(FArr *out, const FStr *s) {\n out->len = out->cap = s->len;\n out->ptr = s->len ? finch_alloc(s->len) : NULL;\n if (s->len) memcpy(out->ptr, s->ptr, (size_t)s->len);\n}\n\n// ---------- program arguments, input and files ----------\n\nvoid finch_args(FArr *out, int argc, char **argv) {\n out->len = out->cap = argc;\n out->ptr = finch_alloc((int64_t)sizeof(FStr) * (argc ? argc : 1));\n for (int i = 0; i < argc; i++) str_heap((FStr *)out->ptr + i, argv[i], (int64_t)strlen(argv[i]));\n}\n\n// after main: the arguments go\nvoid finch_args_free(FArr *a) {\n for (int64_t i = 0; i < a->len; i++) finch_str_drop((FStr *)a->ptr + i);\n finch_arr_free(a);\n}\n\n// input(\"Name? \") prints the prompt and reads one line, without the newline.\nvoid finch_input(FStr *out, const FStr *prompt) {\n if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);\n fflush(stdout);\n // read one line of any length (getline isn't available everywhere)\n size_t size = 128, n = 0;\n char *line = finch_alloc((int64_t)size);\n int c, got = 0;\n while ((c = fgetc(stdin)) != EOF) {\n got = 1;\n if (c == '\\n') break;\n if (n + 1 >= size) line = must(realloc(line, size *= 2));\n line[n++] = (char)c;\n }\n if (!got) {\n free(line);\n str_static(out, empty);\n return;\n }\n while (n > 0 && line[n - 1] == '\\r') n--;\n line[n] = 0;\n out->ptr = line;\n out->len = (int64_t)n;\n out->cap = (int64_t)size;\n}\n\nint finch_file_exists(const FStr *path) {\n FILE *f = fopen(path->ptr, \"rb\");\n if (f) fclose(f);\n return f != NULL;\n}\n\nint finch_try_read_file(FStr *out, const FStr *path, FStr *err) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n file_error(err, \"read\", path);\n return 0;\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n return 1;\n}\n\nvoid finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't read the file \\\"%.*s\\\": %s\", (int)(path->len > 200 ? 200 : path->len),\n path->ptr, strerror(errno));\n finch_panic(file, line, msg);\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n}\n\n// shell(\"ls -l\"): run a command, give back its exit code\nint64_t finch_shell(const FStr *cmd) {\n fflush(stdout);\n int status = system(cmd->ptr ? cmd->ptr : \"\");\n#ifdef _WIN32\n return status; // Windows gives the exit code directly\n#else\n if (status == -1) return -1;\n if (WIFEXITED(status)) return WEXITSTATUS(status);\n return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);\n#endif\n}\n\nint finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : \"\") == 0; }\n\nint finch_try_delete_file(const FStr *path, FStr *err) {\n if (remove(path->ptr ? path->ptr : \"\") == 0) return 1;\n file_error(err, \"delete\", path);\n return 0;\n}\n\n// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here\nvoid *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; }\n\nint finch_write_file(const FStr *path, const FStr *text) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"wb\");\n if (!f) return 0;\n size_t put = text->len ? fwrite(text->ptr, 1, (size_t)text->len, f) : 0;\n return fclose(f) == 0 && put == (size_t)text->len;\n}\n\nint finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {\n errno = 0;\n if (finch_write_file(path, text)) return 1;\n if (!errno) errno = EIO;\n file_error(err, \"write\", path);\n return 0;\n}\n\n// ---------- folders (std/fs.fch, through std/finch_fs.h) ----------\n\nint finch_fs_is_dir(const char *path) {\n#ifdef _WIN32\n struct _finddata_t d;\n intptr_t h = _findfirst(path, &d);\n if (h == -1) return 0;\n _findclose(h);\n return (d.attrib & _A_SUBDIR) != 0;\n#else\n struct stat st;\n return stat(path, &st) == 0 && S_ISDIR(st.st_mode);\n#endif\n}\n\nint finch_fs_exists(const char *path) {\n#ifdef _WIN32\n return _access(path, 0) == 0;\n#else\n struct stat st;\n return stat(path, &st) == 0;\n#endif\n}\n\nstatic int make_one_dir(const char *p) {\n#ifdef _WIN32\n return _mkdir(p) == 0 || errno == EEXIST;\n#else\n return mkdir(p, 0777) == 0 || errno == EEXIST;\n#endif\n}\n\n// mkdir -p: the parents too; 1 if the folder is there afterwards\nint finch_fs_mkdir(const char *path) {\n size_t n = strlen(path);\n char *p = finch_alloc((int64_t)n + 1);\n memcpy(p, path, n + 1);\n for (size_t i = 1; i < n; i++) {\n if (p[i] == '/' || p[i] == '\\\\') {\n if (p[i - 1] == ':') continue; /* C: is a drive */\n char c = p[i];\n p[i] = 0;\n make_one_dir(p);\n p[i] = c;\n }\n }\n int ok = make_one_dir(p) && finch_fs_is_dir(p);\n free(p);\n return ok;\n}\n\nint finch_fs_remove_dir(const char *path) {\n#ifdef _WIN32\n return _rmdir(path) == 0;\n#else\n return rmdir(path) == 0;\n#endif\n}\n\n// the names in a folder, one per call (\"\" at the end), without . and ..\ntypedef struct {\n#ifdef _WIN32\n intptr_t h;\n struct _finddata_t d;\n int first;\n#else\n DIR *d;\n#endif\n} FsList;\n\nvoid *finch_fs_list_open(const char *path) {\n FsList *l = finch_zalloc((int64_t)sizeof(FsList));\n#ifdef _WIN32\n size_t n = strlen(path);\n char *pat = finch_alloc((int64_t)n + 3);\n memcpy(pat, path, n);\n memcpy(pat + n, \"\\\\*\", 3);\n l->h = _findfirst(pat, &l->d);\n free(pat);\n if (l->h == -1) {\n free(l);\n return NULL;\n }\n l->first = 1;\n#else\n l->d = opendir(path);\n if (!l->d) {\n free(l);\n return NULL;\n }\n#endif\n return l;\n}\n\nconst char *finch_fs_list_next(void *handle) {\n FsList *l = handle;\n for (;;) {\n#ifdef _WIN32\n if (!l->first && _findnext(l->h, &l->d) != 0) return \"\";\n l->first = 0;\n const char *name = l->d.name;\n#else\n struct dirent *e = readdir(l->d);\n if (!e) return \"\";\n const char *name = e->d_name;\n#endif\n if (strcmp(name, \".\") != 0 && strcmp(name, \"..\") != 0) return name;\n }\n}\n\nvoid finch_fs_list_close(void *handle) {\n FsList *l = handle;\n#ifdef _WIN32\n _findclose(l->h);\n#else\n closedir(l->d);\n#endif\n free(l);\n}\n\n// where a program keeps its own files for this user (saves, settings); \"\" if unknown\n// Linux: $XDG_DATA_HOME or ~/.local/share macOS: ~/Library/Application Support Windows: %APPDATA%\nconst char *finch_fs_data_home(void) {\n static char buf[4096];\n const char *v;\n#if defined(_WIN32)\n v = getenv(\"APPDATA\");\n snprintf(buf, sizeof buf, \"%s\", v ? v : \"\");\n#elif defined(__APPLE__)\n v = getenv(\"HOME\");\n if (v) snprintf(buf, sizeof buf, \"%s/Library/Application Support\", v);\n else buf[0] = 0;\n#else\n v = getenv(\"XDG_DATA_HOME\");\n if (v && *v) snprintf(buf, sizeof buf, \"%s\", v);\n else if ((v = getenv(\"HOME\"))) snprintf(buf, sizeof buf, \"%s/.local/share\", v);\n else buf[0] = 0;\n#endif\n return buf;\n}\n\n// ---------- threads (std/thread.fch, through std/finch_thread.h) ----------\n\ntypedef struct {\n void (*fn)(void *);\n void *arg;\n} ThreadStart;\n\n#ifdef _WIN32\nstatic DWORD WINAPI thread_main(LPVOID p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return 0;\n}\n#else\nstatic void *thread_main(void *p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return NULL;\n}\n#endif\n\n// run fn(arg) on a new thread; NULL if it couldn't start\nvoid *finch_thread_spawn(void (*fn)(void *), void *arg) {\n ThreadStart *st = finch_alloc((int64_t)sizeof(ThreadStart));\n st->fn = fn;\n st->arg = arg;\n#ifdef _WIN32\n HANDLE h = CreateThread(NULL, 0, thread_main, st, 0, NULL);\n if (!h) {\n free(st);\n return NULL;\n }\n return h;\n#else\n pthread_t *t = finch_alloc((int64_t)sizeof(pthread_t));\n if (pthread_create(t, NULL, thread_main, st) != 0) {\n free(st);\n free(t);\n return NULL;\n }\n return t;\n#endif\n}\n\n// wait for the thread to end; 1 if that worked\nint finch_thread_join(void *t) {\n if (!t) return 0;\n#ifdef _WIN32\n int ok = WaitForSingleObject((HANDLE)t, INFINITE) == WAIT_OBJECT_0;\n CloseHandle((HANDLE)t);\n return ok;\n#else\n int ok = pthread_join(*(pthread_t *)t, NULL) == 0;\n free(t);\n return ok;\n#endif\n}\n\nvoid *finch_mutex_new(void) {\n#ifdef _WIN32\n CRITICAL_SECTION *m = finch_alloc((int64_t)sizeof(CRITICAL_SECTION));\n InitializeCriticalSection(m);\n#else\n pthread_mutex_t *m = finch_alloc((int64_t)sizeof(pthread_mutex_t));\n pthread_mutex_init(m, NULL);\n#endif\n return m;\n}\n\nvoid finch_mutex_lock(void *m) {\n#ifdef _WIN32\n EnterCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_lock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_unlock(void *m) {\n#ifdef _WIN32\n LeaveCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_unlock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_free(void *m) {\n if (!m) return;\n#ifdef _WIN32\n DeleteCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_destroy((pthread_mutex_t *)m);\n#endif\n free(m);\n}\n\nint finch_cpu_count(void) {\n#ifdef _WIN32\n SYSTEM_INFO si;\n GetSystemInfo(&si);\n return (int)si.dwNumberOfProcessors;\n#else\n long n = sysconf(_SC_NPROCESSORS_ONLN);\n return n > 0 ? (int)n : 1;\n#endif\n}\n\n// ---------- maps ----------\n// map[K]V = { entries; len; used; cap; index; icap; adds }\n// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.\n// A removed entry keeps its slot (hash 0) until the next resize packs the slots.\n// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.\n// adds: how many keys were ever added; a `for` over the map stops with an error if it changes.\n// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.\n// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).\n\ntypedef struct {\n char *entries;\n int64_t len, used, cap;\n int64_t *index;\n int64_t icap;\n int64_t adds;\n} FMap;\n\nstatic uint64_t map_hash(const void *key, int64_t ks, int kstr) {\n const unsigned char *p;\n size_t n;\n if (kstr) {\n const FStr *s = key;\n p = (const unsigned char *)s->ptr;\n n = (size_t)s->len;\n } else {\n p = key;\n n = (size_t)ks;\n }\n uint64_t h = 1469598103934665603ULL; // FNV-1a, then mixed so the low bits spread well\n for (size_t i = 0; i < n; i++) {\n h ^= p[i];\n h *= 1099511628211ULL;\n }\n h ^= h >> 31;\n h *= 0xbf58476d1ce4e5b9ULL;\n h ^= h >> 29;\n return h | 1; // 0 marks a removed entry\n}\n\nstatic int map_key_eq(const char *a, const void *b, int64_t ks, int kstr) {\n if (!kstr) return memcmp(a, b, (size_t)ks) == 0;\n const FStr *x = (const FStr *)a, *y = b;\n return x->len == y->len && (x->len == 0 || memcmp(x->ptr, y->ptr, (size_t)x->len) == 0);\n}\n\nstatic void map_reindex(FMap *m, int64_t es) {\n if (m->len != m->used) { // pack the slots of removed entries away\n int64_t j = 0;\n for (int64_t i = 0; i < m->used; i++) {\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == 0) continue;\n if (i != j) memmove(m->entries + j * es, e, (size_t)es);\n j++;\n }\n m->used = j;\n }\n int64_t icap = 16;\n while (icap < m->cap * 2) icap *= 2;\n free(m->index);\n m->index = finch_alloc(icap * (int64_t)sizeof(int64_t));\n memset(m->index, 0xff, (size_t)icap * sizeof(int64_t));\n m->icap = icap;\n for (int64_t i = 0; i < m->used; i++) {\n uint64_t h = *(uint64_t *)(m->entries + i * es);\n int64_t s = (int64_t)(h & (uint64_t)(icap - 1));\n while (m->index[s] != -1) s = (s + 1) & (icap - 1);\n m->index[s] = i;\n }\n}\n\n// the entry number of `key`, or -1\nint64_t finch_map_find(const FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr) {\n if (m->len == 0) return -1;\n uint64_t h = map_hash(key, ks, kstr);\n for (int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));; s = (s + 1) & (m->icap - 1)) {\n int64_t i = m->index[s];\n if (i == -1) return -1;\n if (i < 0) continue;\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == h && map_key_eq(e + 8, key, ks, kstr)) return i;\n }\n}\n\n// The entry for `key`. A new one is zeroed except its hash (*added = 1); the compiler then stores the key.\nint64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr, int32_t *added) {\n int64_t i = finch_map_find(m, key, es, ks, kstr);\n *added = i < 0;\n if (i >= 0) return i;\n if (m->used == m->cap) {\n if (m->len * 4 < m->cap * 3) { // many removed slots: pack them instead of growing\n map_reindex(m, es);\n } else {\n m->cap = m->cap ? m->cap * 2 : 8;\n m->entries = must(realloc(m->entries, (size_t)(m->cap * es)));\n map_reindex(m, es);\n }\n }\n uint64_t h = map_hash(key, ks, kstr);\n i = m->used++;\n char *e = m->entries + i * es;\n memset(e, 0, (size_t)es);\n *(uint64_t *)e = h;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);\n m->index[s] = i;\n m->len++;\n m->adds++;\n return i;\n}\n\n// Forget entry i (its key and value were already dropped by the compiler).\nvoid finch_map_remove_at(FMap *m, int64_t i, int64_t es) {\n char *e = m->entries + i * es;\n uint64_t h = *(uint64_t *)e;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] != i) s = (s + 1) & (m->icap - 1);\n m->index[s] = -2;\n *(uint64_t *)e = 0;\n if (--m->len == 0) {\n m->used = 0;\n memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n }\n}\n\nvoid finch_map_clear(FMap *m) {\n m->len = m->used = 0;\n if (m->index) memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n}\n\nvoid finch_map_free(FMap *m) {\n free(m->entries);\n free(m->index);\n}\n\n// A copy of the slots and the index; the compiler then copies the keys and values that own memory.\nvoid finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {\n *out = *m;\n if (!m->cap) return;\n out->entries = finch_alloc(m->cap * es);\n memcpy(out->entries, m->entries, (size_t)(m->used * es));\n out->index = finch_alloc(m->icap * (int64_t)sizeof(int64_t));\n memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));\n}\n\n__attribute__((noreturn, cold)) void finch_map_changed(const char *file, int64_t line) {\n finch_panic(file, line, \"a key was added to the map while looping over it (collect the new keys, and add them after the loop)\");\n}\n\n__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {\n char msg[200];\n snprintf(msg, sizeof msg, \"the key %.*s is not in the map (check with .has(key), or use .get(key, default))\",\n (int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : \"\");\n finch_panic(file, line, msg);\n}\n\n// ---------- cpu_has(\"avx2\"): what the processor running the program can do ----------\n//\n// x86: CPUID, and for the AVX and AVX-512 families also XGETBV (the system must save those registers, or the\n// instructions fault). ARM64: the kernel's hwcaps on Linux and Android, sysctl on macOS. A name of the other\n// architecture is false.\n\n#if defined(__x86_64__) || defined(__i386__)\n#include \nstatic int x86_leaf(unsigned leaf, unsigned sub, unsigned r[4]) {\n unsigned max = __get_cpuid_max(0, 0);\n if (leaf > max) return 0;\n __cpuid_count(leaf, sub, r[0], r[1], r[2], r[3]);\n return 1;\n}\nstatic unsigned long long x86_xcr0(void) {\n unsigned lo, hi;\n __asm__ volatile(\"xgetbv\" : \"=a\"(lo), \"=d\"(hi) : \"c\"(0));\n return ((unsigned long long)hi << 32) | lo;\n}\nstatic int finch_cpu_has_c(const char *n) {\n unsigned l1[4] = {0}, l7[4] = {0}, l71[4] = {0};\n x86_leaf(1, 0, l1);\n x86_leaf(7, 0, l7);\n x86_leaf(7, 1, l71);\n int osx = (l1[2] >> 27) & 1; // OSXSAVE\n unsigned long long xcr0 = osx ? x86_xcr0() : 0;\n int ymm = osx && (xcr0 & 6) == 6; // the system saves xmm and ymm\n int zmm = ymm && (xcr0 & 0xe0) == 0xe0; // and the AVX-512 registers\n if (!strcmp(n, \"sse3\")) return l1[2] & 1;\n if (!strcmp(n, \"ssse3\")) return (l1[2] >> 9) & 1;\n if (!strcmp(n, \"sse4.1\")) return (l1[2] >> 19) & 1;\n if (!strcmp(n, \"sse4.2\")) return (l1[2] >> 20) & 1;\n if (!strcmp(n, \"popcnt\")) return (l1[2] >> 23) & 1;\n if (!strcmp(n, \"avx\")) return ymm && ((l1[2] >> 28) & 1);\n if (!strcmp(n, \"fma\")) return ymm && ((l1[2] >> 12) & 1);\n if (!strcmp(n, \"f16c\")) return ymm && ((l1[2] >> 29) & 1);\n if (!strcmp(n, \"avx2\")) return ymm && ((l7[1] >> 5) & 1);\n if (!strcmp(n, \"bmi1\")) return (l7[1] >> 3) & 1;\n if (!strcmp(n, \"bmi2\")) return (l7[1] >> 8) & 1;\n if (!strcmp(n, \"avxvnni\")) return ymm && ((l71[0] >> 4) & 1);\n if (!strcmp(n, \"avx512f\")) return zmm && ((l7[1] >> 16) & 1);\n if (!strcmp(n, \"avx512dq\")) return zmm && ((l7[1] >> 17) & 1);\n if (!strcmp(n, \"avx512bw\")) return zmm && ((l7[1] >> 30) & 1);\n if (!strcmp(n, \"avx512vl\")) return zmm && ((l7[1] >> 31) & 1);\n if (!strcmp(n, \"avx512vnni\")) return zmm && ((l7[2] >> 11) & 1);\n if (!strcmp(n, \"avx512fp16\")) return zmm && ((l7[3] >> 23) & 1);\n if (!strcmp(n, \"avx512bf16\")) return zmm && ((l71[0] >> 5) & 1);\n return 0;\n}\n#elif defined(__aarch64__) && defined(__APPLE__)\n#include \nstatic int sysctl_flag(const char *name) {\n int v = 0;\n size_t len = sizeof v;\n if (sysctlbyname(name, &v, &len, NULL, 0) != 0) return 0;\n return v != 0;\n}\nstatic int finch_cpu_has_c(const char *n) {\n if (!strcmp(n, \"neon\")) return 1;\n if (!strcmp(n, \"fp16\")) return sysctl_flag(\"hw.optional.arm.FEAT_FP16\");\n if (!strcmp(n, \"dotprod\")) return sysctl_flag(\"hw.optional.arm.FEAT_DotProd\");\n if (!strcmp(n, \"i8mm\")) return sysctl_flag(\"hw.optional.arm.FEAT_I8MM\");\n if (!strcmp(n, \"bf16\")) return sysctl_flag(\"hw.optional.arm.FEAT_BF16\");\n return 0; // no SVE on Apple chips\n}\n#elif defined(__aarch64__) && defined(__linux__)\n#include \nstatic int finch_cpu_has_c(const char *n) {\n unsigned long h = getauxval(AT_HWCAP), h2 = getauxval(AT_HWCAP2);\n if (!strcmp(n, \"neon\")) return 1;\n if (!strcmp(n, \"fp16\")) return (h >> 10) & 1; // HWCAP_ASIMDHP\n if (!strcmp(n, \"dotprod\")) return (h >> 20) & 1; // HWCAP_ASIMDDP\n if (!strcmp(n, \"sve\")) return (h >> 22) & 1; // HWCAP_SVE\n if (!strcmp(n, \"sve2\")) return (h2 >> 1) & 1; // HWCAP2_SVE2\n if (!strcmp(n, \"i8mm\")) return (h2 >> 13) & 1; // HWCAP2_I8MM\n if (!strcmp(n, \"bf16\")) return (h2 >> 14) & 1; // HWCAP2_BF16\n return 0;\n}\n#else\nstatic int finch_cpu_has_c(const char *n) {\n (void)n;\n return 0;\n}\n#endif\n\nint finch_cpu_has(const FStr *name) {\n char n[32];\n int64_t len = name->len < 31 ? name->len : 31;\n memcpy(n, name->ptr ? name->ptr : \"\", (size_t)len);\n n[len] = 0;\n return finch_cpu_has_c(n) ? 1 : 0;\n}\n")
list.push("// Files and folders: bytes, folders, listing, and a place for a program's own files.\n//\n// import fs\n// data := try fs.read_bytes(\"atlas.png\") the whole file as []u8 (any bytes, NUL included)\n// try fs.write_bytes(\"out.bin\", data)\n// try fs.mkdir(\"saves/slot1\") with the folders above it\n// names := try fs.list(\"saves\") the names in a folder, sorted\n// fs.exists(p) fs.is_dir(p) try fs.remove_dir(p)\n// dir := try fs.data_dir(\"MyGame\") a folder for this user's saves, made if needed\nimport \"finch_fs.h\"\n\nfn read_bytes(str path) -> []u8! {\n text := try read_file(path)\n return text.bytes()\n}\n\nfn write_bytes(str path, []u8 data) -> ! {\n try write_file(path, str(data))\n}\n\nfn exists(str path) -> bool {\n return finch_fs_exists(path) != 0\n}\n\nfn is_dir(str path) -> bool {\n return finch_fs_is_dir(path) != 0\n}\n\nfn mkdir(str path) -> ! {\n if finch_fs_mkdir(path) == 0 {\n return error(\"can't make the folder \\\"\" + path + \"\\\"\")\n }\n}\n\n// only an empty folder\nfn remove_dir(str path) -> ! {\n if finch_fs_remove_dir(path) == 0 {\n return error(\"can't remove the folder \\\"\" + path + \"\\\" (is it empty?)\")\n }\n}\n\nfn list(str path) -> []str! {\n l := finch_fs_list_open(path)\n if l == null {\n return error(\"can't list the folder \\\"\" + path + \"\\\"\")\n }\n []str names\n while true {\n name := finch_fs_list_next(l)\n if name.len == 0 {\n break\n }\n names.push(name)\n }\n finch_fs_list_close(l)\n names.sort()\n return names\n}\n\n// The folder where a program keeps this user's files (saves, settings), made if it isn't there:\n// ~/.local/share/ on Linux, ~/Library/Application Support/ on macOS, %APPDATA%\\ on Windows.\nfn data_dir(str app) -> str! {\n home := finch_fs_data_home()\n if home.len == 0 {\n return error(\"this system doesn't say where programs keep their files\")\n }\n dir := home + \"/\" + app\n try mkdir(dir)\n return dir\n}\n")
list.push("// JSON: read it, build it, write it.\n//\n// import json\n//\n// doc := try json.parse(\"{\\\"name\\\": \\\"Finch\\\", \\\"tags\\\": [\\\"fast\\\", \\\"small\\\"], \\\"stars\\\": 5}\")\n// print(doc.get(\"name\").str_or(\"\")) Finch\n// print(doc.get(\"tags\").at(1).str_or(\"\")) small\n// print(doc.get(\"stars\").int_or(0)) 5\n// print(doc.get(\"missing\").is_null()) true (a missing key is null, so chains never fail)\n//\n// out := json.new_object()\n// out.set(\"ok\", json.new_bool(true))\n// list := json.new_list()\n// list.push(json.new_number(1.5))\n// out.set(\"values\", list)\n// print(json.stringify(out)) {\"ok\":true,\"values\":[1.5]}\n// print(json.pretty(out)) the same, over several lines\n\nenum Kind { Null, Bool, Number, Text, List, Object }\n\n// One JSON value. `kind` says which of the other fields it uses: flag for Bool, num for Number,\n// text for Text, items for List, and items with keys (the same length, in the same order) for Object.\nstruct Json {\n Kind kind\n bool flag\n float num\n str text\n []Json items\n []str keys\n}\n\nfn new_null() -> Json {\n return Json(kind: Kind.Null)\n}\n\nfn new_bool(bool b) -> Json {\n return Json(kind: Kind.Bool, flag: b)\n}\n\nfn new_number(float n) -> Json {\n return Json(kind: Kind.Number, num: n)\n}\n\nfn new_text(str s) -> Json {\n return Json(kind: Kind.Text, text: s)\n}\n\nfn new_list() -> Json {\n return Json(kind: Kind.List)\n}\n\nfn new_object() -> Json {\n return Json(kind: Kind.Object)\n}\n\n// ---------- looking inside ----------\n\nfn Json.is_null() -> bool {\n return self.kind == Kind.Null\n}\n\n// the number of elements of a list, of keys of an object, of bytes of a text; 0 for the rest\nfn Json.len() -> int {\n return match self.kind {\n List, Object -> self.items.len\n Text -> self.text.len\n else -> 0\n }\n}\n\nfn Json.has(str key) -> bool {\n return self.kind == Kind.Object && self.keys.contains(key)\n}\n\n// the value of a key of an object; null if there is none (or this is not an object)\nfn Json.get(str key) -> Json {\n if self.kind != Kind.Object {\n return new_null()\n }\n i := self.keys.find(key)\n if i < 0 {\n return new_null()\n }\n return self.items[i]\n}\n\n// the element of a list; null if there is none\nfn Json.at(int i) -> Json {\n if self.kind != Kind.List || i < 0 || i >= self.items.len {\n return new_null()\n }\n return self.items[i]\n}\n\n// the value if it is of that kind, else what was given\nfn Json.str_or(str fallback) -> str {\n if self.kind == Kind.Text {\n return self.text\n }\n return fallback\n}\n\nfn Json.float_or(float fallback) -> float {\n if self.kind == Kind.Number {\n return self.num\n }\n return fallback\n}\n\nfn Json.int_or(int fallback) -> int {\n if self.kind == Kind.Number {\n return int(self.num)\n }\n return fallback\n}\n\nfn Json.bool_or(bool fallback) -> bool {\n if self.kind == Kind.Bool {\n return self.flag\n }\n return fallback\n}\n\n// the keys of an object, in the order they were written\nfn Json.names() -> []str {\n return self.keys\n}\n\n// ---------- building ----------\n\n// add to the end of a list\nfn Json.push(Json v) {\n self.items.push(v)\n}\n\n// give a key a value in an object; a key that is already there is replaced\nfn Json.set(str key, Json v) {\n i := self.keys.find(key)\n if i >= 0 {\n self.items[i] = v\n return\n }\n self.keys.push(key)\n self.items.push(v)\n}\n\n// ---------- writing ----------\n\nfn quote(str s) -> str {\n []str parts = [\"\\\"\"]\n for c in s {\n n := int(c)\n if c == '\"' {\n parts.push(\"\\\\\\\"\")\n } else if c == '\\\\' {\n parts.push(\"\\\\\\\\\")\n } else if c == '\\n' {\n parts.push(\"\\\\n\")\n } else if c == '\\r' {\n parts.push(\"\\\\r\")\n } else if c == '\\t' {\n parts.push(\"\\\\t\")\n } else if n < 32 {\n parts.push(\"\\\\u00\" + hexdigit(n / 16) + hexdigit(n % 16))\n } else {\n parts.push(str(c))\n }\n }\n parts.push(\"\\\"\")\n return parts.join(\"\")\n}\n\nfn hexdigit(int n) -> str {\n digits := \"0123456789abcdef\"\n return str(digits[n])\n}\n\nfn number_text(float n) -> str {\n if n == float(int(n)) && n > -1000000000000000.0 && n < 1000000000000000.0 {\n return str(int(n))\n }\n return str(n)\n}\n\nstruct Writer {\n []str out\n}\n\nfn write(Json j, str indent, str pad, ptr[Writer] w) {\n nl := \"\"\n inner := \"\"\n sep := \",\"\n colon := \":\"\n if indent.len > 0 {\n nl = \"\\n\"\n inner = pad + indent\n colon = \": \"\n }\n match j.kind {\n Null -> w.out.push(\"null\")\n Bool -> {\n if j.flag {\n w.out.push(\"true\")\n } else {\n w.out.push(\"false\")\n }\n }\n Number -> w.out.push(number_text(j.num))\n Text -> w.out.push(quote(j.text))\n List -> {\n if j.items.len == 0 {\n w.out.push(\"[]\")\n return\n }\n w.out.push(\"[\" + nl)\n for i, item in j.items {\n if i > 0 {\n w.out.push(sep + nl)\n }\n w.out.push(inner)\n write(item, indent, inner, w)\n }\n w.out.push(nl + pad + \"]\")\n }\n Object -> {\n if j.items.len == 0 {\n w.out.push(\"{}\")\n return\n }\n w.out.push(\"{\" + nl)\n for i, item in j.items {\n if i > 0 {\n w.out.push(sep + nl)\n }\n w.out.push(inner + quote(j.keys[i]) + colon)\n write(item, indent, inner, w)\n }\n w.out.push(nl + pad + \"}\")\n }\n }\n}\n\n// the value as text on one line\nfn stringify(Json j) -> str {\n w := Writer()\n write(j, \"\", \"\", addr(w))\n return w.out.join(\"\")\n}\n\n// the value as text over several lines, indented by two spaces\nfn pretty(Json j) -> str {\n w := Writer()\n write(j, \" \", \"\", addr(w))\n return w.out.join(\"\")\n}\n\n// ---------- reading ----------\n\nstruct Reader {\n str src\n int i\n}\n\n// the line and column of a position, for a message\nfn where(ptr[Reader] r) -> str {\n line := 1\n col := 1\n for k in 0..r.i {\n if k < r.src.len && r.src[k] == '\\n' {\n line += 1\n col = 1\n } else {\n col += 1\n }\n }\n return \"line \" + str(line) + \", column \" + str(col)\n}\n\nfn peek(ptr[Reader] r) -> char {\n if r.i < r.src.len {\n return r.src[r.i]\n }\n return char(0)\n}\n\nfn skip_space(ptr[Reader] r) {\n while r.i < r.src.len && (r.src[r.i] == ' ' || r.src[r.i] == '\\n' || r.src[r.i] == '\\t' || r.src[r.i] == '\\r') {\n r.i += 1\n }\n}\n\nfn unexpected(ptr[Reader] r, str wanted) -> str {\n if r.i >= r.src.len {\n return \"json: the text ends where \" + wanted + \" should be (\" + where(r) + \")\"\n }\n return \"json: found '\" + str(r.src[r.i]) + \"' where \" + wanted + \" should be (\" + where(r) + \")\"\n}\n\n// does `word` come next?\nfn comes(ptr[Reader] r, str word) -> bool {\n return r.src.len - r.i >= word.len && r.src.sub(r.i, r.i + word.len) == word\n}\n\nfn expect_word(ptr[Reader] r, str word) -> ! {\n if !comes(r, word) {\n return error(unexpected(r, \"'\" + word + \"'\"))\n }\n r.i += word.len\n}\n\nfn hex_value(char c) -> int {\n if c >= '0' && c <= '9' {\n return int(c) - int('0')\n }\n if c >= 'a' && c <= 'f' {\n return int(c) - int('a') + 10\n }\n if c >= 'A' && c <= 'F' {\n return int(c) - int('A') + 10\n }\n return -1\n}\n\n// a code point as UTF-8\nfn utf8(int cp) -> str {\n []u8 b\n if cp < 128 {\n b.push(u8(cp))\n } else if cp < 2048 {\n b.push(u8(192 + cp / 64))\n b.push(u8(128 + cp % 64))\n } else if cp < 65536 {\n b.push(u8(224 + cp / 4096))\n b.push(u8(128 + (cp / 64) % 64))\n b.push(u8(128 + cp % 64))\n } else {\n b.push(u8(240 + cp / 262144))\n b.push(u8(128 + (cp / 4096) % 64))\n b.push(u8(128 + (cp / 64) % 64))\n b.push(u8(128 + cp % 64))\n }\n return str(b)\n}\n\nfn read_hex4(ptr[Reader] r) -> int! {\n n := 0\n for k in 0..4 {\n if r.i >= r.src.len || hex_value(r.src[r.i]) < 0 {\n return error(unexpected(r, \"a hexadecimal digit\"))\n }\n n = n * 16 + hex_value(r.src[r.i])\n r.i += 1\n }\n return n\n}\n\nfn read_string(ptr[Reader] r) -> str! {\n r.i += 1 // the opening quote\n []str parts\n start := r.i\n while true {\n if r.i >= r.src.len {\n return error(\"json: a text is never closed (\" + where(r) + \")\")\n }\n c := r.src[r.i]\n if c == '\"' {\n parts.push(r.src.sub(start, r.i))\n r.i += 1\n return parts.join(\"\")\n }\n if int(c) < 32 {\n return error(\"json: a line break or control character inside a text (\" + where(r) + \")\")\n }\n if c != '\\\\' {\n r.i += 1\n continue\n }\n parts.push(r.src.sub(start, r.i))\n r.i += 1\n e := peek(r)\n r.i += 1\n if e == 'n' {\n parts.push(\"\\n\")\n } else if e == 't' {\n parts.push(\"\\t\")\n } else if e == 'r' {\n parts.push(\"\\r\")\n } else if e == 'b' {\n parts.push(utf8(8))\n } else if e == 'f' {\n parts.push(utf8(12))\n } else if e == '\"' || e == '\\\\' || e == '/' {\n parts.push(str(e))\n } else if e == 'u' {\n cp := try read_hex4(r)\n // a pair of 😀 stands for one character above U+FFFF\n if cp >= 55296 && cp < 56320 && comes(r, \"\\\\u\") {\n r.i += 2\n low := try read_hex4(r)\n cp = 65536 + (cp - 55296) * 1024 + (low - 56320)\n }\n parts.push(utf8(cp))\n } else {\n return error(\"json: unknown escape \\\\\" + str(e) + \" in a text (\" + where(r) + \")\")\n }\n start = r.i\n }\n}\n\nfn read_number(ptr[Reader] r) -> Json! {\n start := r.i\n if peek(r) == '-' {\n r.i += 1\n }\n while r.i < r.src.len && ((r.src[r.i] >= '0' && r.src[r.i] <= '9') || r.src[r.i] == '.' || r.src[r.i] == 'e' || r.src[r.i] == 'E' || r.src[r.i] == '+' || r.src[r.i] == '-') {\n r.i += 1\n }\n n := float(r.src.sub(start, r.i)) or {\n r.i = start\n return error(unexpected(r, \"a number\"))\n }\n return new_number(n)\n}\n\nfn read_value(ptr[Reader] r) -> Json! {\n skip_space(r)\n c := peek(r)\n if r.i >= r.src.len {\n return error(unexpected(r, \"a value\"))\n }\n if c == '{' {\n obj := new_object()\n r.i += 1\n skip_space(r)\n if peek(r) == '}' {\n r.i += 1\n return obj\n }\n while true {\n skip_space(r)\n if peek(r) != '\"' {\n return error(unexpected(r, \"a key in quotes\"))\n }\n key := try read_string(r)\n skip_space(r)\n if peek(r) != ':' {\n return error(unexpected(r, \"':'\"))\n }\n r.i += 1\n v := try read_value(r)\n obj.set(key, v)\n skip_space(r)\n if peek(r) == ',' {\n r.i += 1\n continue\n }\n if peek(r) == '}' {\n r.i += 1\n return obj\n }\n return error(unexpected(r, \"',' or '}'\"))\n }\n }\n if c == '[' {\n list := new_list()\n r.i += 1\n skip_space(r)\n if peek(r) == ']' {\n r.i += 1\n return list\n }\n while true {\n v := try read_value(r)\n list.push(v)\n skip_space(r)\n if peek(r) == ',' {\n r.i += 1\n continue\n }\n if peek(r) == ']' {\n r.i += 1\n return list\n }\n return error(unexpected(r, \"',' or ']'\"))\n }\n }\n if c == '\"' {\n s := try read_string(r)\n return new_text(s)\n }\n if c == 't' {\n try expect_word(r, \"true\")\n return new_bool(true)\n }\n if c == 'f' {\n try expect_word(r, \"false\")\n return new_bool(false)\n }\n if c == 'n' {\n try expect_word(r, \"null\")\n return new_null()\n }\n if c == '-' || (c >= '0' && c <= '9') {\n return try read_number(r)\n }\n return error(unexpected(r, \"a value\"))\n}\n\n// the JSON in `text`; an error with the line and column where it went wrong\nfn parse(str text) -> Json! {\n r := Reader(src: text)\n v := try read_value(addr(r))\n skip_space(addr(r))\n if r.i < r.src.len {\n return error(unexpected(addr(r), \"the end of the text\"))\n }\n return v\n}\n")
list.push("// Paths, as text. Elements are separated by \"/\".\n//\n// import path\n// path.join(\"a\", \"b.txt\") a/b.txt\n// path.base(\"/tmp/x/a.tar.gz\") a.tar.gz path.stem(...) a.tar path.ext(...) .gz\n// path.dir(\"/tmp/x/a.txt\") /tmp/x\n// path.clean(\"a//b/../c/./d\") a/c/d\n\n// a and b with one separator between; an absolute b replaces a\nfn join(str a, str b) -> str {\n if a.len == 0 {\n return b\n }\n if b.len == 0 {\n return a\n }\n if b[0] == '/' {\n return b\n }\n if a[a.len - 1] == '/' {\n return a + b\n }\n return a + \"/\" + b\n}\n\nfn is_abs(str p) -> bool {\n return p.len > 0 && p[0] == '/'\n}\n\n// the last element: \"a/b.txt\" -> \"b.txt\", \"a/b/\" -> \"b\", \"\" -> \".\", \"/\" -> \"/\"\nfn base(str p) -> str {\n if p.len == 0 {\n return \".\"\n }\n end := p.len\n while end > 1 && p[end - 1] == '/' {\n end -= 1\n }\n if end == 1 && p[0] == '/' {\n return \"/\"\n }\n start := end\n while start > 0 && p[start - 1] != '/' {\n start -= 1\n }\n return p.sub(start, end)\n}\n\n// everything before the last element: \"a/b.txt\" -> \"a\", \"b.txt\" -> \".\", \"/b\" -> \"/\"\nfn dir(str p) -> str {\n end := p.len\n while end > 1 && p[end - 1] == '/' {\n end -= 1\n }\n i := end\n while i > 0 && p[i - 1] != '/' {\n i -= 1\n }\n if i == 0 {\n return \".\"\n }\n while i > 1 && p[i - 1] == '/' {\n i -= 1\n }\n return p.sub(0, i)\n}\n\n// the part of the last element from its last dot: \"a.tar.gz\" -> \".gz\"; \"\" when there is none\nfn ext(str p) -> str {\n b := base(p)\n i := b.len\n while i > 0 && b[i - 1] != '.' {\n i -= 1\n }\n if i <= 1 { // no dot, or a name that only starts with one (.bashrc)\n return \"\"\n }\n return b.sub(i - 1, b.len)\n}\n\n// the last element without its extension\nfn stem(str p) -> str {\n b := base(p)\n e := ext(p)\n return b.sub(0, b.len - e.len)\n}\n\n// the elements of a path: \"/a/b/c\" -> [\"a\", \"b\", \"c\"]\nfn parts(str p) -> []str {\n []str out\n for piece in p.split(\"/\") {\n if piece.len > 0 {\n out.push(piece)\n }\n }\n return out\n}\n\n// the shortest path with the same meaning: no \"//\", no \".\", and \"..\" resolved where it can be\nfn clean(str p) -> str {\n if p.len == 0 {\n return \".\"\n }\n abs := is_abs(p)\n []str out\n for piece in parts(p) {\n if piece == \".\" {\n continue\n }\n if piece == \"..\" {\n if out.len > 0 && out[out.len - 1] != \"..\" {\n out.pop()\n } else if !abs {\n out.push(\"..\")\n }\n continue\n }\n out.push(piece)\n }\n joined := out.join(\"/\")\n if abs {\n return \"/\" + joined\n }\n if joined.len == 0 {\n return \".\"\n }\n return joined\n}\n")
diff --git a/src/gen.fch b/src/gen.fch
index 2d1441a..ad6ebce 100644
--- a/src/gen.fch
+++ b/src/gen.fch
@@ -33,6 +33,7 @@ struct Options {
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.
@@ -218,6 +219,8 @@ struct Gen {
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 ----------
@@ -1460,6 +1463,9 @@ 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 == "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)"
}
@@ -2357,6 +2363,13 @@ fn define(ptr[Gen] g, int si) {
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
@@ -2441,8 +2454,20 @@ fn define(ptr[Gen] g, int si) {
}
}
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 = " !dbg !" + str(dbgsp)
+ 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" +
@@ -3680,7 +3705,7 @@ 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 != "!" {
+ if v.ty.kind == "vec" && (e.name != "!" || v.ty.elem[0].kind == "bool") {
return vecUnary(g, e, v)
}
if e.name == "!" {
@@ -4453,6 +4478,18 @@ 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" && 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)
@@ -5572,6 +5609,15 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
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)
}
@@ -5816,6 +5862,7 @@ fn callFn(ptr[Gen] g, int fi, ast.Node c, int first, str shown, str selfAddr, st
// The call itself, once the arguments are values ("i64 5"); the receiver's address, if any, is already first.
fn emitCall(ptr[Gen] g, FnSig sig, ast.Node c, []str args, str shown) -> Val {
+ checkCpuCall(g, sig, c, shown)
if sig.fallible && g.ftarget != nodeKey(c) {
fail(g, c, "'" + shown + "' can fail, so say what happens then: " + shown + "(...) or , " + shown + "(...) or { ... }, or try " + shown + "(...)")
}
@@ -6504,6 +6551,7 @@ fn runtimeDecls() -> str {
"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)",
@@ -6552,7 +6600,7 @@ fn assemble(ptr[Gen] g) -> str {
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)
+ 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 {
@@ -6743,9 +6791,15 @@ fn vecMake(ptr[Gen] g, ast.Node c, ast.Type vt) -> Val {
ft = first.pl.ty
}
if ft.kind == "array" || ft.kind == "ptr" {
- if c.kids.len != 2 {
+ 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))
}
@@ -6754,6 +6808,10 @@ fn vecMake(ptr[Gen] g, ast.Node c, ast.Type vt) -> Val {
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" {
@@ -6766,8 +6824,22 @@ fn vecMake(ptr[Gen] g, ast.Node c, ast.Type vt) -> Val {
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)
+ 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))
@@ -6816,6 +6888,20 @@ fn vecConvert(ptr[Gen] g, Val v, ast.Type to, ast.Node at) -> Val {
}
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) {
@@ -6849,6 +6935,12 @@ fn vecConvert(ptr[Gen] g, Val v, ast.Type to, ast.Node at) -> Val {
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))
@@ -6869,6 +6961,9 @@ fn vecArith(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val {
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) {
@@ -6891,27 +6986,22 @@ fn vecArith(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val {
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].)
+// 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)
- 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)
+ lane := vt.elem[0]
+ mask := shaped(vt, "bool")
+ if lane.kind == "bool" && o != "==" && o != "!=" {
+ fail(g, at, "masks can only be compared with == and !=")
}
- 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"))
+ 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: all, ty: T("bool"))
+ 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)
@@ -6979,6 +7069,9 @@ fn vecMethod(ptr[Gen] g, ast.Node m, Val v) -> Val {
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
@@ -7009,7 +7102,7 @@ fn vecMethod(ptr[Gen] g, ast.Node m, Val v) -> Val {
need(g, m, 0)
return Val(v: vecIntrinsic(g, lt, n, vt, [lt], [v.v], ""), ty: vt)
}
- if n == "store" {
+ if n == "store" || n == "store_part" {
need(g, m, 2)
dst := ref(g, m.kids[1], true)
dt := dst.val.ty
@@ -7020,7 +7113,10 @@ fn vecMethod(ptr[Gen] g, ast.Node m, Val v) -> Val {
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)")
+ 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 := ""
@@ -7031,14 +7127,31 @@ fn vecMethod(ptr[Gen] g, ast.Node m, Val v) -> Val {
}
} else {
av := load(g, "{ ptr, i64, i64 }", dst.pl.addr)
- vecBounds(g, i, vt.count, op(g, "extractvalue { ptr, i64, i64 } " + av + ", 1"), m)
+ 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"))
}
- 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)")
+ 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
}
@@ -7213,3 +7326,416 @@ fn bitsCall(ptr[Gen] g, ast.Node c) -> Val {
h := op(g, "fptrunc " + il + " " + v.v + " to " + hl)
return Val(v: op(g, "bitcast " + hl + " " + h + " to " + ol), ty: out)
}
+
+// ---------- masks: boolxN ----------
+//
+// A comparison of vectors gives a mask, one true/false a lane (). It picks lanes with select, says
+// whether any or all lanes are true, and loads and stores the end of a list (f32x8(list, i, fill), store_part).
+
+// lanes 0, 1, ... n-1 that are inside a list of `len` from index i (i may be past the end: then none)
+fn tailMask(ptr[Gen] g, str i, int n, str len, ast.Node at) -> str {
+ panicIf(g, op(g, "icmp slt i64 " + i + ", 0"), "a vector's index is below 0", at)
+ rest := op(g, "sub i64 " + len + ", " + i)
+ []str lanes
+ for k in 0..n {
+ lanes.push("i64 " + str(k))
+ }
+ wide := ast.Type(kind: "vec", count: n)
+ wide.elem.push(T("int"))
+ return op(g, "icmp slt <" + str(n) + " x i64> <" + lanes.join(", ") + ">, " + splat(g, rest, wide))
+}
+
+// select(mask, a, b): a's lane where the mask is true, b's where it is false
+fn selectCall(ptr[Gen] g, ast.Node c) -> Val {
+ if g.index {
+ noteRef(g, c, c.col, c.name.len, docs.hover(docs.find(docs.builtins(), "select")), "", 0, 0)
+ }
+ checkArgs(g, c, 0, 3, "select")
+ m := expr(g, c.kids[0])
+ if m.ty.kind != "vec" || m.ty.elem[0].kind != "bool" {
+ fail(g, c.kids[0], "select(mask, a, b) takes a mask first, from a comparison of vectors like v > 0.0; this is " + ast.show(m.ty))
+ }
+ a := expr(g, c.kids[1])
+ b := expr(g, c.kids[2])
+ vt := a.ty
+ if vt.kind != "vec" {
+ vt = b.ty
+ }
+ if vt.kind != "vec" {
+ fail(g, c, "select(mask, a, b): a or b has to be a vector (the other may be a number for every lane)")
+ }
+ if vt.count != m.ty.count {
+ fail(g, c, "the mask has " + str(m.ty.count) + " lanes, but " + ast.show(vt) + " has " + str(vt.count))
+ }
+ av := asVec(g, a, vt, "select", c.kids[1])
+ bv := asVec(g, b, vt, "select", c.kids[2])
+ lt := llty(g, vt)
+ return Val(v: op(g, "select " + llty(g, m.ty) + " " + m.v + ", " + lt + " " + av.v + ", " + lt + " " + bv.v), ty: vt)
+}
+
+fn maskMethod(ptr[Gen] g, ast.Node m, Val v) -> Val {
+ n := m.name
+ ml := llty(g, v.ty)
+ if n == "any" || n == "all" {
+ need(g, m, 0)
+ how := "or"
+ if n == "all" {
+ how = "and"
+ }
+ return Val(v: vecIntrinsic(g, "i1", "vector.reduce." + how, v.ty, [ml], [v.v], ""), ty: T("bool"))
+ }
+ if n == "count" {
+ need(g, m, 0)
+ wide := shaped(v.ty, "i32")
+ wl := llty(g, wide)
+ z := op(g, "zext " + ml + " " + v.v + " to " + wl)
+ sum := vecIntrinsic(g, "i32", "vector.reduce.add", wide, [wl], [z], "")
+ return Val(v: op(g, "zext i32 " + sum + " to i64"), ty: T("int"))
+ }
+ fail(g, m, "a mask (" + ast.show(v.ty) + ") has .len, [i] and the methods any, all, count; pick lanes with select(mask, a, b)")
+ return v
+}
+
+// ---------- moving lanes around ----------
+
+fn indexList([]int idx) -> str {
+ []str parts
+ for k in idx {
+ parts.push("i32 " + str(k))
+ }
+ return "<" + parts.join(", ") + ">"
+}
+
+fn shuffleOf(ptr[Gen] g, Val v, []int idx, str v2) -> Val {
+ vt := v.ty
+ out := ast.Type(kind: "vec", count: idx.len)
+ out.elem.push(vt.elem[0])
+ second := v2
+ if second.len == 0 {
+ second = "poison"
+ }
+ return Val(v: op(g, "shufflevector " + llty(g, vt) + " " + v.v + ", " + llty(g, vt) + " " + second + ", <" + str(idx.len) + " x i32> " + indexList(idx)), ty: out)
+}
+
+fn fitsVector(ptr[Gen] g, ast.Node at, ast.Type lane, int n) {
+ if n != 2 && n != 4 && n != 8 && n != 16 && n != 32 && n != 64 {
+ fail(g, at, "a vector has 2, 4, 8, 16, 32 or 64 lanes, not " + str(n))
+ }
+ bits := bitsOf(lane)
+ if n * bits > 512 {
+ fail(g, at, "that would be " + str(n) + " lanes of " + ast.show(lane) + ", more than 512 bits")
+ }
+}
+
+// v.shuffle(3, 2, 1, 0): lanes picked by number (numbers written in the code); v.lo(), v.hi(): the halves;
+// table.lookup(idx): table[idx % table.len] for every lane of idx (pshufb on x86, tbl on ARM)
+fn shuffleMethod(ptr[Gen] g, ast.Node m, Val v) -> Val {
+ vt := v.ty
+ n := m.name
+ if n == "lo" || n == "hi" {
+ need(g, m, 0)
+ if vt.count < 4 {
+ fail(g, m, "a " + ast.show(vt) + " has only " + str(vt.count) + " lanes; take them with v[0] and v[1]")
+ }
+ []int idx
+ start := 0
+ if n == "hi" {
+ start = vt.count / 2
+ }
+ for k in 0..vt.count / 2 {
+ idx.push(start + k)
+ }
+ return shuffleOf(g, v, idx, "")
+ }
+ if n == "shuffle" {
+ []int idx
+ for q in 1..m.kids.len {
+ x := expr(g, m.kids[q])
+ if !isInt(x.ty) || !x.lit {
+ fail(g, m.kids[q], "shuffle(...) takes lane numbers written in the code (or constants), like v.shuffle(3, 2, 1, 0)")
+ }
+ k := int(x.v) or -1
+ if k < 0 || k >= vt.count {
+ fail(g, m.kids[q], "lane " + x.v + " isn't in a " + ast.show(vt) + " (its lanes are 0 to " + str(vt.count - 1) + ")")
+ }
+ idx.push(k)
+ }
+ fitsVector(g, m, vt.elem[0], idx.len)
+ return shuffleOf(g, v, idx, "")
+ }
+ // lookup
+ need(g, m, 1)
+ ix := expr(g, m.kids[1])
+ if ix.ty.kind != "vec" || !isInt(ix.ty.elem[0]) {
+ fail(g, m.kids[1], "lookup(indices) takes a vector of whole numbers (like u8x16), but this is " + ast.show(ix.ty))
+ }
+ if vt.elem[0].kind == "bool" {
+ fail(g, m, "a mask can't be a lookup table")
+ }
+ fitsVector(g, m.kids[1], vt.elem[0], ix.ty.count)
+ il := llty(g, ix.ty)
+ masked := op(g, "and " + il + " " + ix.v + ", " + splat(g, str(vt.count - 1), ix.ty))
+ out := shaped(ix.ty, vt.elem[0].kind)
+ ol := llty(g, out)
+ el := llty(g, vt.elem[0])
+ xl := llty(g, ix.ty.elem[0])
+ cur := "poison"
+ for k in 0..ix.ty.count {
+ at := op(g, "extractelement " + il + " " + masked + ", i64 " + str(k))
+ lane := op(g, "extractelement " + llty(g, vt) + " " + v.v + ", " + xl + " " + at)
+ cur = op(g, "insertelement " + ol + " " + cur + ", " + el + " " + lane + ", i64 " + str(k))
+ }
+ return Val(v: cur, ty: out)
+}
+
+// concat(a, b): a's lanes, then b's
+fn concatCall(ptr[Gen] g, ast.Node c) -> Val {
+ if g.index {
+ noteRef(g, c, c.col, c.name.len, docs.hover(docs.find(docs.builtins(), "concat")), "", 0, 0)
+ }
+ checkArgs(g, c, 0, 2, "concat")
+ a := expr(g, c.kids[0])
+ b := expr(g, c.kids[1])
+ if a.ty.kind != "vec" || !ast.same(a.ty, b.ty) {
+ fail(g, c, "concat(a, b) joins two vectors of the same type, but these are " + ast.show(a.ty) + " and " + ast.show(b.ty))
+ }
+ fitsVector(g, c, a.ty.elem[0], a.ty.count * 2)
+ []int idx
+ for k in 0..a.ty.count * 2 {
+ idx.push(k)
+ }
+ return shuffleOf(g, a, idx, b.v)
+}
+
+// ---------- processor features: @cpu(...), cpu_has(...) ----------
+//
+// --cpu gives the whole program a processor (main.fch asks clang which features that means); @cpu("avx2,fma") gives one
+// function more, as LLVM's "target-features". Such a function is only called after cpu_has("avx2") said yes. Features of
+// the other architecture are dropped, so one source holds the x86 and the ARM versions side by side.
+
+fn x86Features() -> []str {
+ return ["sse3", "ssse3", "sse4.1", "sse4.2", "popcnt", "avx", "avx2", "fma", "f16c", "bmi1", "bmi2", "avx512f", "avx512bw",
+ "avx512dq", "avx512vl", "avx512vnni", "avx512bf16", "avx512fp16", "avxvnni"]
+}
+
+fn armFeatures() -> []str {
+ return ["neon", "fp16", "dotprod", "i8mm", "bf16", "sve", "sve2"]
+}
+
+// the name LLVM uses for a feature
+fn llvmFeature(str x) -> str {
+ if x == "bmi1" {
+ return "bmi"
+ }
+ if x == "fp16" {
+ return "fullfp16"
+ }
+ return x
+}
+
+// the features of a level: x86-64-v3 is the AVX2 generation, v4 the AVX-512 one
+fn levelFeatures(str name) -> []str {
+ []str out
+ if name == "x86-64-v2" || name == "x86-64-v3" || name == "x86-64-v4" {
+ for x in ["sse3", "ssse3", "sse4.1", "sse4.2", "popcnt"] {
+ out.push(x)
+ }
+ }
+ if name == "x86-64-v3" || name == "x86-64-v4" {
+ for x in ["avx", "avx2", "bmi1", "bmi2", "f16c", "fma"] {
+ out.push(x)
+ }
+ }
+ if name == "x86-64-v4" {
+ for x in ["avx512f", "avx512bw", "avx512dq", "avx512vl"] {
+ out.push(x)
+ }
+ }
+ return out
+}
+
+// a function's own @cpu features, for the architecture it is built for
+fn cpuOf(ptr[Gen] g, ast.Node f) -> []str {
+ []str out
+ if f.cpu.len == 0 {
+ return out
+ }
+ for part in f.cpu.split(",") {
+ x := part.trim()
+ names := levelFeatures(x)
+ if names.len == 0 {
+ if !x86Features().contains(x) && !armFeatures().contains(x) {
+ fail(g, f, "@cpu: '" + x + "' isn't a feature Finch knows (x86: " + x86Features().join(", ") + ", x86-64-v2/v3/v4; ARM: " + armFeatures().join(", ") + ")")
+ }
+ names.push(x)
+ }
+ for y in names {
+ if armFeatures().contains(y) == g.tg.arm64 && !out.contains(y) {
+ out.push(y)
+ }
+ }
+ }
+ return out
+}
+
+// a vector wider than 16 bytes travels in different registers depending on the features (two xmm, or one ymm), so it
+// can't cross from a function with one set of features to a function with another
+fn checkCpuCall(ptr[Gen] g, FnSig sig, ast.Node c, str shown) {
+ if sig.prog < 0 || sig.prog >= g.progs.len || sig.index >= g.progs[sig.prog].fns.len {
+ return
+ }
+ callee := g.progs[sig.prog].fns[sig.index].cpu
+ mine := g.progs[g.cur.prog].fns[g.cur.index].cpu
+ if callee == mine {
+ return
+ }
+ []ast.Type ts = sig.params
+ ts.push(sig.ret)
+ for t in ts {
+ if t.kind == "vec" && t.count * bitsOf(t.elem[0]) > 128 {
+ fail(g, c, "'" + shown + "' has other @cpu features than this function, so a " + ast.show(t) + " can't be passed between them (its registers differ); pass a list or a ptr, or give both the same @cpu")
+ }
+ }
+}
+
+// cpu_has("avx2"): does the processor running the program have it? (false for the other architecture's names)
+fn cpuHasCall(ptr[Gen] g, ast.Node c) -> Val {
+ if g.index {
+ noteRef(g, c, c.col, c.name.len, docs.hover(docs.find(docs.builtins(), "cpu_has")), "", 0, 0)
+ }
+ checkArgs(g, c, 0, 1, "cpu_has")
+ if c.kids[0].kind == "str" {
+ x := c.kids[0].text
+ if !x86Features().contains(x) && !armFeatures().contains(x) {
+ fail(g, c.kids[0], "cpu_has: '" + x + "' isn't a feature Finch knows (x86: " + x86Features().join(", ") + "; ARM: " + armFeatures().join(", ") + ")")
+ }
+ }
+ s := strArg(g, c.kids[0], "the feature's name")
+ r := op(g, "call i32 @finch_cpu_has(ptr " + spill(g, s) + ")")
+ release(g, s)
+ return Val(v: op(g, "icmp ne i32 " + r + ", 0"), ty: T("bool"))
+}
+
+// ---------- acc.dot_add(a, b): the step of an int8 dot product ----------
+//
+// acc is i32xN; a and b have 4N bytes (a u8 or i8, b i8 or u8). Lane k of acc gains a[4k]*b[4k] + ... + a[4k+3]*b[4k+3],
+// exactly (no saturation). That is vpdpbusd (AVX-VNNI, AVX-512 VNNI) and usdot / sdot / udot (ARM), used when the
+// function's features have them; elsewhere the same sum is written out, which LLVM makes into pmaddwd and adds.
+fn dotAdd(ptr[Gen] g, ast.Node m, Val acc) -> Val {
+ need(g, m, 2)
+ at := acc.ty
+ if at.elem[0].kind != "i32" && at.elem[0].kind != "u32" {
+ fail(g, m, "dot_add is called on an i32 vector (the sums), like acc.dot_add(a, b) with acc an i32x8; this is " + ast.show(at))
+ }
+ a := expr(g, m.kids[1])
+ b := expr(g, m.kids[2])
+ for x in [a, b] {
+ if x.ty.kind != "vec" || (x.ty.elem[0].kind != "u8" && x.ty.elem[0].kind != "i8") || x.ty.count != at.count * 4 {
+ fail(g, m, "dot_add(a, b) on an " + ast.show(at) + " takes two vectors of " + str(at.count * 4) + " bytes (u8 or i8), but these are " + ast.show(a.ty) + " and " + ast.show(b.ty))
+ }
+ }
+ au := a.ty.elem[0].kind == "u8"
+ bu := b.ty.elem[0].kind == "u8"
+ if !au && bu {
+ // u8 x i8 is the same sum as i8 x u8: put the unsigned one first
+ t := a
+ a = b
+ b = t
+ au = true
+ bu = false
+ }
+ al := llty(g, at)
+ bits := at.count * 32
+ f := g.curFeatures
+ if !g.tg.arm64 && au && !bu {
+ vnni := (bits == 512 && f.contains("avx512vnni")) || (bits <= 256 && (f.contains("avxvnni") || (f.contains("avx512vnni") && f.contains("avx512vl"))))
+ if vnni {
+ a32 := op(g, "bitcast " + llty(g, a.ty) + " " + a.v + " to " + al)
+ b32 := op(g, "bitcast " + llty(g, b.ty) + " " + b.v + " to " + al)
+ name := "@llvm.x86.avx512.vpdpbusd." + str(bits)
+ declareOnce(g, "declare " + al + " " + name + "(" + al + ", " + al + ", " + al + ")")
+ return Val(v: op(g, "call " + al + " " + name + "(" + al + " " + acc.v + ", " + al + " " + a32 + ", " + al + " " + b32 + ")"), ty: at)
+ }
+ }
+ if g.tg.arm64 && bits % 128 == 0 {
+ which := ""
+ if au && !bu && f.contains("i8mm") {
+ which = "usdot"
+ } else if au && bu && f.contains("dotprod") {
+ which = "udot"
+ } else if !au && !bu && f.contains("dotprod") {
+ which = "sdot"
+ }
+ if which.len > 0 {
+ // one instruction per 16 bytes
+ name := "@llvm.aarch64.neon." + which + ".v4i32.v16i8"
+ declareOnce(g, "declare <4 x i32> " + name + "(<4 x i32>, <16 x i8>, <16 x i8>)")
+ parts := at.count / 4
+ []str outs
+ for q in 0..parts {
+ []int ai
+ for k in 0..4 {
+ ai.push(q * 4 + k)
+ }
+ []int bi
+ for k in 0..16 {
+ bi.push(q * 16 + k)
+ }
+ ac := shuffleOf(g, acc, ai, "").v
+ ap := shuffleOf(g, a, bi, "").v
+ bp := shuffleOf(g, b, bi, "").v
+ outs.push(op(g, "call <4 x i32> " + name + "(<4 x i32> " + ac + ", <16 x i8> " + ap + ", <16 x i8> " + bp + ")"))
+ }
+ return Val(v: joinParts(g, outs, 4), ty: at)
+ }
+ }
+ // written out: widen, multiply, add each group of four
+ w := shaped(a.ty, "i32")
+ wl := llty(g, w)
+ ext := "sext"
+ if au {
+ ext = "zext"
+ }
+ aw := op(g, ext + " " + llty(g, a.ty) + " " + a.v + " to " + wl)
+ ext = "sext"
+ if bu {
+ ext = "zext"
+ }
+ bw := op(g, ext + " " + llty(g, b.ty) + " " + b.v + " to " + wl)
+ prod := Val(v: op(g, "mul " + wl + " " + aw + ", " + bw), ty: w)
+ sum := acc.v
+ for k in 0..4 {
+ []int idx
+ for j in 0..at.count {
+ idx.push(4 * j + k)
+ }
+ sum = op(g, "add " + al + " " + sum + ", " + shuffleOf(g, prod, idx, "").v)
+ }
+ return Val(v: sum, ty: at)
+}
+
+// <4 x i32> pieces back into one vector of n * 4 lanes
+fn joinParts(ptr[Gen] g, []str parts, int each) -> str {
+ if parts.len == 1 {
+ return parts[0]
+ }
+ []str cur = parts
+ size := each
+ while cur.len > 1 {
+ []str next
+ t := ast.Type(kind: "vec", count: size)
+ t.elem.push(T("i32"))
+ k := 0
+ while k < cur.len {
+ []int idx
+ for j in 0..size * 2 {
+ idx.push(j)
+ }
+ next.push(shuffleOf(g, Val(v: cur[k], ty: t), idx, cur[k + 1]).v)
+ k += 2
+ }
+ cur = next
+ size = size * 2
+ }
+ return cur[0]
+}
diff --git a/src/lexer.fch b/src/lexer.fch
index ed4337b..19accef 100644
--- a/src/lexer.fch
+++ b/src/lexer.fch
@@ -70,7 +70,7 @@ fn operator(ptr[Lexer] lx) -> str {
return two
}
}
- ones := "(){}[],.:+-*/%&|^~<>=!"
+ ones := "(){}[],.:+-*/%&|^~<>=!@"
c := lx.src.sub(lx.i, lx.i + 1)
if ones.contains(c) {
return c
diff --git a/src/linker.fch b/src/linker.fch
index 8b452db..9cda7a2 100644
--- a/src/linker.fch
+++ b/src/linker.fch
@@ -371,10 +371,7 @@ struct Build {
// IR -> program. 0 if it worked.
fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
- clang := getenv("FINCH_CLANG")
- if clang.len == 0 {
- clang = "clang"
- }
+ clang := clangCmd()
// the C compiler of the runtime and the linker: the target's own when building for another system
cc := clang
if tg.cross {
@@ -478,3 +475,74 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
}
return 0
}
+
+// clang, as the program is compiled with it. Nix's clang wrapper removes -march=native when NIX_ENFORCE_NO_NATIVE
+// is 1, as it is in nix-shell; --cpu native asks for it on purpose, so the wrapper is told to keep it.
+fn clangCmd() -> str {
+ clang := getenv("FINCH_CLANG")
+ if clang.len == 0 {
+ clang = "clang"
+ }
+ if host.pathSeparator() != ";" {
+ return "NIX_ENFORCE_NO_NATIVE=0 " + clang
+ }
+ return clang
+}
+
+// -march=x86-64-v3 or -mcpu=apple-m1: how clang is told the processor
+fn cpuFlag(platform.Target tg, str cpu) -> str {
+ if cpu.len == 0 {
+ return ""
+ }
+ if tg.arm64 {
+ return " -mcpu=" + quote(cpu)
+ }
+ return " -march=" + quote(cpu)
+}
+
+// What the processor the program is for can do, as Finch's feature names (avx2, avxvnni, i8mm ...): clang says it in
+// the macros it predefines for that target and --cpu. Kept in the cache, one file per clang, target and cpu.
+fn cpuFeatures(platform.Target tg, str cpu) -> []str {
+ clang := clangCmd()
+ flags := cpuFlag(tg, cpu)
+ if tg.cross || tg.windows {
+ flags = " --target=" + tg.triple + flags
+ }
+ dir := host.cacheDir() + "/cpu"
+ host.makeDirs(dir)
+ slot := dir + "/" + fingerprint2(clang + flags)
+ text := read_file(slot + ".txt") or ""
+ if text.len == 0 {
+ empty := slot + ".part" + str(host.pid()) + ".c"
+ out := slot + ".part" + str(host.pid()) + ".txt"
+ write_file(empty, "")
+ host.run(clang + flags + " -dM -E " + quote(empty) + " > " + quote(out) + " 2> " + host.nullDevice())
+ host.remove(empty)
+ text = read_file(out) or ""
+ if text.len == 0 {
+ host.remove(out)
+ []str none
+ return none
+ }
+ host.move(out, slot + ".txt")
+ }
+ macros := ["__SSE3__", "__SSSE3__", "__SSE4_1__", "__SSE4_2__", "__POPCNT__", "__AVX__", "__AVX2__", "__FMA__", "__F16C__",
+ "__BMI__", "__BMI2__", "__AVX512F__", "__AVX512BW__", "__AVX512DQ__", "__AVX512VL__", "__AVX512VNNI__",
+ "__AVX512BF16__", "__AVX512FP16__", "__AVXVNNI__", "__ARM_NEON", "__ARM_FEATURE_FP16_VECTOR_ARITHMETIC",
+ "__ARM_FEATURE_DOTPROD", "__ARM_FEATURE_MATMUL_INT8", "__ARM_FEATURE_BF16_VECTOR_ARITHMETIC", "__ARM_FEATURE_SVE",
+ "__ARM_FEATURE_SVE2"]
+ names := ["sse3", "ssse3", "sse4.1", "sse4.2", "popcnt", "avx", "avx2", "fma", "f16c", "bmi1", "bmi2", "avx512f", "avx512bw",
+ "avx512dq", "avx512vl", "avx512vnni", "avx512bf16", "avx512fp16", "avxvnni", "neon", "fp16", "dotprod", "i8mm", "bf16",
+ "sve", "sve2"]
+ []str have
+ for line in text.split("\n") {
+ parts := line.split(" ")
+ if parts.len >= 2 && parts[0] == "#define" {
+ k := macros.find(parts[1])
+ if k >= 0 && !have.contains(names[k]) {
+ have.push(names[k])
+ }
+ }
+ }
+ return have
+}
diff --git a/src/main.fch b/src/main.fch
index 8bf35bd..6cf7f61 100644
--- a/src/main.fch
+++ b/src/main.fch
@@ -471,7 +471,15 @@ fn main([]str args) -> int {
testModules.push(prog.module) // your own files' tests, not the standard library's
}
}
- ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, index: cmd == "index", cwd: host.cwd(), target: tg, test: cmd == "test", testModules: testModules, trace: trace))
+ // what the processor can do (dot_add and the like pick their instructions by it); not needed for the editor's index
+ []str features
+ if cmd != "index" {
+ features = linker.cpuFeatures(tg, cpu)
+ if cpu == "native" && !tg.arm64 && !features.contains("sse4.2") {
+ print("warning: --cpu native made no difference to clang (a clang wrapper may remove -march=native; try --cpu with a name, like x86-64-v3)")
+ }
+ }
+ ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, index: cmd == "index", cwd: host.cwd(), target: tg, test: cmd == "test", testModules: testModules, trace: trace, features: features))
if cmd == "ir" || cmd == "index" {
print(ir)
return 0
@@ -501,14 +509,7 @@ 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)
- }
- }
+ extra = extra + linker.cpuFlag(tg, cpu) // x86-64 names a processor with -march, ARM with -mcpu
[]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 db97175..7fef3c4 100644
--- a/src/parser.fch
+++ b/src/parser.fch
@@ -289,6 +289,22 @@ fn parseProgram(ptr[Parser] p) -> ast.Program {
if e.flag {
prog.structs.push(sumStruct(e)) // an enum with values inside is stored as a struct (see sumStruct)
}
+ } else if at(p, "@") {
+ // @cpu("avx2,fma") fn ...: a function that may use these processor features (check with cpu_has first)
+ a := next(p)
+ name := expect(p, "ident", "an attribute name, like @cpu(\"avx2\")").text
+ if name != "cpu" {
+ fail(p, a, "the attribute here is @cpu(\"...\"): the processor features a function may use")
+ }
+ expect(p, "(", "'('")
+ feats := expect(p, "string", "the features in quotes, like @cpu(\"avx2,fma\")").text
+ expect(p, ")", "')'")
+ if !at(p, "fn") && !at(p, "export") {
+ fail(p, cur(p), "@cpu(...) goes right before a function")
+ }
+ f := parseFn(p)
+ f.cpu = feats
+ prog.fns.push(f)
} else {
prog.fns.push(parseFn(p))
}
diff --git a/tests/fail/cpu_attr_name.fch b/tests/fail/cpu_attr_name.fch
new file mode 100644
index 0000000..f7702a9
--- /dev/null
+++ b/tests/fail/cpu_attr_name.fch
@@ -0,0 +1,7 @@
+// expect: @cpu: 'avx3' isn't a feature Finch knows
+@cpu("avx2,avx3")
+fn f() {
+}
+fn main() {
+ f()
+}
diff --git a/tests/fail/cpu_has_name.fch b/tests/fail/cpu_has_name.fch
new file mode 100644
index 0000000..c1ba4be
--- /dev/null
+++ b/tests/fail/cpu_has_name.fch
@@ -0,0 +1,4 @@
+// expect: cpu_has: 'avx-512' isn't a feature Finch knows
+fn main() {
+ print(cpu_has("avx-512"))
+}
diff --git a/tests/fail/cpu_wide_vector.fch b/tests/fail/cpu_wide_vector.fch
new file mode 100644
index 0000000..a384b7d
--- /dev/null
+++ b/tests/fail/cpu_wide_vector.fch
@@ -0,0 +1,8 @@
+// expect: can't be passed between them (its registers differ)
+@cpu("avx2")
+fn double(f32x8 v) -> f32x8 {
+ return v * 2.0
+}
+fn main() {
+ print(double(f32x8(1.0)))
+}
diff --git a/tests/fail/vec_less.fch b/tests/fail/vec_less.fch
index e9dadb0..36c975f 100644
--- a/tests/fail/vec_less.fch
+++ b/tests/fail/vec_less.fch
@@ -1,5 +1,7 @@
-// expect: use min(v, w), max(v, w), or compare lanes: v[i] < w[i]
+// expect: use .all() or .any()
fn main() {
a := f32x4(1.0)
- print(a < a)
+ if a < a {
+ print("no")
+ }
}
diff --git a/tests/fail/vec_shuffle_range.fch b/tests/fail/vec_shuffle_range.fch
new file mode 100644
index 0000000..516bac5
--- /dev/null
+++ b/tests/fail/vec_shuffle_range.fch
@@ -0,0 +1,5 @@
+// expect: lane 4 isn't in a f32x4 (its lanes are 0 to 3)
+fn main() {
+ v := f32x4(1.0)
+ print(v.shuffle(0, 1, 2, 4))
+}
diff --git a/tests/fail/vec_shuffle_var.fch b/tests/fail/vec_shuffle_var.fch
new file mode 100644
index 0000000..94c98a2
--- /dev/null
+++ b/tests/fail/vec_shuffle_var.fch
@@ -0,0 +1,6 @@
+// expect: shuffle(...) takes lane numbers written in the code
+fn main() {
+ v := f32x4(1.0)
+ k := 2
+ print(v.shuffle(k, 0, 1, 3))
+}
diff --git a/tests/run/dot_int8.fch b/tests/run/dot_int8.fch
new file mode 100644
index 0000000..3615628
--- /dev/null
+++ b/tests/run/dot_int8.fch
@@ -0,0 +1,69 @@
+// The int8 dot product of quantized models: plain, with dot_add, and in @cpu functions picked by cpu_has
+
+fn plain([]u8 a, []i8 b) -> int {
+ total := 0
+ for i in 0..a.len {
+ total += int(a[i]) * int(b[i])
+ }
+ return total
+}
+
+// 32 bytes a step, 8 sums kept in a vector until the end
+fn with_vectors([]u8 a, []i8 b) -> int {
+ acc := i32x8(0)
+ i := 0
+ while i + 32 <= a.len {
+ acc = acc.dot_add(u8x32(a, i), i8x32(b, i))
+ i += 32
+ }
+ return int(acc.sum())
+}
+
+@cpu("avxvnni")
+fn with_vnni([]u8 a, []i8 b) -> int {
+ acc := i32x8(0)
+ i := 0
+ while i + 32 <= a.len {
+ acc = acc.dot_add(u8x32(a, i), i8x32(b, i))
+ i += 32
+ }
+ return int(acc.sum())
+}
+
+@cpu("avx512f,avx512bw,avx512vl,avx512vnni,i8mm")
+fn with_avx512([]u8 a, []i8 b) -> int {
+ acc := i32x16(0)
+ i := 0
+ while i + 64 <= a.len {
+ acc = acc.dot_add(u8x64(a, i), i8x64(b, i))
+ i += 64
+ }
+ return int(acc.sum())
+}
+
+fn best([]u8 a, []i8 b) -> int {
+ if cpu_has("avx512vnni") && cpu_has("avx512vl") && cpu_has("avx512bw") {
+ return with_avx512(a, b)
+ }
+ if cpu_has("avxvnni") {
+ return with_vnni(a, b)
+ }
+ return with_vectors(a, b)
+}
+
+fn main() {
+ []u8 a
+ []i8 b
+ for k in 0..1024 {
+ a.push(u8((k * 37 + 11) % 256))
+ b.push(i8((k * 53 + 7) % 256 - 128))
+ }
+ want := plain(a, b)
+ print(want, with_vectors(a, b) == want, best(a, b) == want)
+
+ // the extremes: 255 * -128 four times in a lane, no saturation
+ acc := i32x4(1, 2, 3, 4).dot_add(u8x16(255), i8x16(-128))
+ print(acc, i32x4(0).dot_add(i8x16(-128), i8x16(-128)), i32x4(0).dot_add(u8x16(255), u8x16(255)))
+ print(i32x4(0).dot_add(i8x16(-1), u8x16(200)))
+ print(cpu_has("avx2") || cpu_has("neon") || true)
+}
diff --git a/tests/run/dot_int8.out b/tests/run/dot_int8.out
new file mode 100644
index 0000000..28479e1
--- /dev/null
+++ b/tests/run/dot_int8.out
@@ -0,0 +1,4 @@
+-68096 true true
+i32x4(-130559, -130558, -130557, -130556) i32x4(65536, 65536, 65536, 65536) i32x4(260100, 260100, 260100, 260100)
+i32x4(-800, -800, -800, -800)
+true
diff --git a/tests/run/masks.fch b/tests/run/masks.fch
new file mode 100644
index 0000000..38cefed
--- /dev/null
+++ b/tests/run/masks.fch
@@ -0,0 +1,50 @@
+// Masks: comparisons of vectors, select, any/all/count, and the end of a list
+
+fn relu(f32x8 v) -> f32x8 {
+ return select(v > 0.0, v, 0.0)
+}
+
+// the sum of a list of any length: whole vectors, then the last piece with its missing lanes as 0
+fn total([]f32 xs) -> f32 {
+ acc := f32x8(0.0)
+ i := 0
+ while i < xs.len {
+ acc = acc + f32x8(xs, i, 0.0)
+ i += 8
+ }
+ return acc.sum()
+}
+
+// add 1 to every number of a list, in place, the last piece too
+fn bump([]f32 xs) {
+ i := 0
+ while i < xs.len {
+ (f32x8(xs, i, 0.0) + 1.0).store_part(xs, i)
+ i += 8
+ }
+}
+
+fn main() {
+ v := f32x8(-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, -4.0, 5.0)
+ m := v > 0.0
+ print(m, m.count(), m.any(), m.all(), m[3])
+ print(relu(v), select(!m, v, -1.0))
+ print((v >= -1.0) & (v < 3.0), (v == 0.0) | (v == 5.0), m ^ (v > 1.0))
+
+ a := i32x4(1, 5, 3, 7)
+ b := i32x4(4, 4, 4, 4)
+ print(select(a > b, a, b), (a == a).all(), (a != b).all())
+ u := u8x16(200)
+ print((u > u8x16(100)).all(), i32x4(a > b), f32x4(a < b))
+ k := i32x4(0, 2, 0, -1)
+ print(boolx4(k), boolx4(true, false, true, false))
+
+ []f32 xs
+ for n in 0..13 {
+ xs.push(f32(n))
+ }
+ print(total(xs))
+ bump(xs)
+ print(xs)
+ print(f32x8(xs, 8, -1.0), f32x8(xs, 13, 9.0), f32x8(xs, 100, 7.0))
+}
diff --git a/tests/run/masks.out b/tests/run/masks.out
new file mode 100644
index 0000000..a034c35
--- /dev/null
+++ b/tests/run/masks.out
@@ -0,0 +1,9 @@
+boolx8(false, false, false, true, true, true, false, true) 4 true false true
+f32x8(0, 0, 0, 1, 2, 3, 0, 5) f32x8(-2, -1, 0, -1, -1, -1, -4, -1)
+boolx8(false, true, true, true, true, false, false, false) boolx8(false, false, true, false, false, false, false, true) boolx8(false, false, false, true, false, false, false, false)
+i32x4(4, 5, 4, 7) true true
+true i32x4(0, 1, 0, 1) f32x4(1, 0, 1, 0)
+boolx4(false, true, false, true) boolx4(true, false, true, false)
+78
+[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
+f32x8(9, 10, 11, 12, 13, -1, -1, -1) f32x8(9, 9, 9, 9, 9, 9, 9, 9) f32x8(7, 7, 7, 7, 7, 7, 7, 7)
diff --git a/tests/run/shuffles.fch b/tests/run/shuffles.fch
new file mode 100644
index 0000000..c7b06bf
--- /dev/null
+++ b/tests/run/shuffles.fch
@@ -0,0 +1,27 @@
+// Moving lanes: shuffle, lo/hi, concat, and a byte table lookup (how 4-bit weights are unpacked)
+
+// sixteen 4-bit numbers, two to a byte, made into sixteen bytes through a table of what each nibble means
+fn unpack4(u8x8 packed, i8x16 table) -> i8x16 {
+ pairs := packed.shuffle(0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7) // u8x16: each byte twice
+ odd := u8x16(0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1) == 1
+ nibbles := select(odd, pairs >> 4, pairs & 15) // low nibble first, then the high one
+ return table.lookup(nibbles)
+}
+
+fn main() {
+ v := f32x4(1.0, 2.0, 3.0, 4.0)
+ print(v.shuffle(3, 2, 1, 0), v.shuffle(0, 0, 1, 1, 2, 2, 3, 3), v.shuffle(1, 2))
+ w := i32x8(0, 1, 2, 3, 4, 5, 6, 7)
+ print(w.lo(), w.hi(), concat(w.hi(), w.lo()))
+ print(concat(w, w).len, concat(v, v * 10.0).shuffle(0, 4, 1, 5, 2, 6, 3, 7))
+
+ table := u8x16(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25)
+ print(table.lookup(u8x16(15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)))
+ print(table.lookup(u8x16(16, 17, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)).lo())
+ print(table.lookup(u8x32(3)).hi().lo().lo())
+
+ q4 := i8x16(-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7) // what each 4-bit number means
+ print(unpack4(u8x8(0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe), q4))
+ m := f32x4(1.0, -1.0, 2.0, -2.0) > 0.0
+ print(m.shuffle(1, 0, 3, 2), m.lo())
+}
diff --git a/tests/run/shuffles.out b/tests/run/shuffles.out
new file mode 100644
index 0000000..5d724e2
--- /dev/null
+++ b/tests/run/shuffles.out
@@ -0,0 +1,8 @@
+f32x4(4, 3, 2, 1) f32x8(1, 1, 2, 2, 3, 3, 4, 4) f32x2(2, 3)
+i32x4(0, 1, 2, 3) i32x4(4, 5, 6, 7) i32x8(4, 5, 6, 7, 0, 1, 2, 3)
+16 f32x8(1, 10, 2, 20, 3, 30, 4, 40)
+u8x16(25, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24)
+u8x8(10, 11, 25, 10, 10, 10, 10, 10)
+u8x4(13, 13, 13, 13)
+i8x16(-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7)
+boolx4(false, true, false, true) boolx2(true, false)
diff --git a/tests/run/vectors.fch b/tests/run/vectors.fch
index 38eca29..5255421 100644
--- a/tests/run/vectors.fch
+++ b/tests/run/vectors.fch
@@ -36,7 +36,7 @@ fn main() {
// lanes
a[0] = 9.0
print(a[0], a[3], a)
- print(a == f32x4(9.0, 2.0, 3.0, 4.0), a != a)
+ print((a == f32x4(9.0, 2.0, 3.0, 4.0)).all(), (a != a).any())
// whole numbers, bits, conversions
n := i32x8(1, 2, 3, 4, 5, 6, 7, 8)
From 1f45bd8a01676e6fb3b1083317ab45b00f870118 Mon Sep 17 00:00:00 2001
From: Claude
Date: Sun, 1 Nov 2026 15:46:04 +0000
Subject: [PATCH 7/7] docs (en, pl) and editors: masks, tails, shuffles,
lookup, dot_add, @cpu, cpu_has, Nix
- technicians' guides: section 31 rewritten around kernels (masks, the end of a list, moving lanes,
int8 dot products, choosing instructions at build and at run time)
- cheat sheets and engineers' guides: the new operations and their IR; how features are found
- Kate and VS Code: @cpu, boolxN and the new built-ins highlighted
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01Psc42moPLwP43Cmb9oZqSR
---
README.md | 4 +-
README.pl.md | 4 +-
docs/en/cheatsheet.md | 8 +-
docs/en/for-engineers.md | 24 +++-
docs/en/for-technicians.md | 130 +++++++++++++++--
docs/pl/dla-inzynierow.md | 24 +++-
docs/pl/dla-technikow.md | 132 ++++++++++++++++--
docs/pl/sciaga.md | 8 +-
editors/kate/finch.xml | 16 ++-
editors/vscode/CHANGELOG.md | 3 +-
editors/vscode/syntaxes/finch.tmLanguage.json | 10 +-
editors/vscode/test/grammar.js | 2 +
12 files changed, 334 insertions(+), 31 deletions(-)
diff --git a/README.md b/README.md
index 938d072..d53017b 100644
--- a/README.md
+++ b/README.md
@@ -103,7 +103,8 @@ Pick the guide that fits you:
| Arrays | `nums := [1, 2, 3]`, `nums.push(4)`, `nums[0]`, `nums.len`, `for i, n in nums { }` |
| Maps | `ages := ["ann": 31]`, `ages["bob"] = 25`, `ages.has("cy")`, `counts[w] += 1`, `for k, v in ages { }` |
| Errors | `fn parse(str s) -> int! { … return error("bad") }` → `parse(s) or 0`, `parse(s) or { … }`, `try parse(s)` |
-| Vectors (SIMD) | `v := f32x8(list, i)`, `v * 2.0 + w`, `v.sum()`, `v.dot(w)`, `min(v, 1.0)`, `v.store(list, i)`, `v[3]` |
+| Vectors (SIMD) | `v := f32x8(list, i)`, `v * 2.0 + w`, `v.sum()`, `select(v > 0.0, v, 0.0)`, `t.lookup(idx)`, `acc.dot_add(a, b)`, `v.store(list, i)` |
+| Processors | `--cpu native`; `@cpu("avx2") fn …` with `if cpu_has("avx2")`; floats computed as written (`fma`/`muladd` to fuse) |
| Text | `"hp \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
| Control | `if` / `else if` / `else` (also as a value), `while`, `for i in 0..10`, `break`, `continue`, `defer` |
| Memory | automatic for arrays, text and structs; `new(...)` / `free(...)` for your own heap structures |
@@ -161,6 +162,7 @@ Pick the guide that fits you:
- [x] Threads (`import thread`) and atomics; C++ files; `--target android`
- [x] The compiler on every system it builds for: Linux, macOS (Apple Silicon and Intel), Windows; releases made by CI
- [x] Vector types (`f32x8`, `i8x32`, …) for SIMD, and `--cpu native`
+- [x] Masks, shuffles, byte lookup and `dot_add` for kernels; `@cpu` and `cpu_has`; `fma`/`muladd`, float bits, f16 and bf16
## Layout
diff --git a/README.pl.md b/README.pl.md
index 098f317..68dcc85 100644
--- a/README.pl.md
+++ b/README.pl.md
@@ -105,7 +105,8 @@ Wybierz przewodnik dla siebie:
| Tablice | `liczby := [1, 2, 3]`, `liczby.push(4)`, `liczby[0]`, `liczby.len`, `for i, n in liczby { }` |
| Mapy | `wiek := ["ania": 31]`, `wiek["bob"] = 25`, `wiek.has("cyryl")`, `ile[slowo] += 1`, `for k, v in wiek { }` |
| Błędy | `fn parsuj(str s) -> int! { … return error("zła liczba") }` → `parsuj(s) or 0`, `parsuj(s) or { … }`, `try parsuj(s)` |
-| Wektory (SIMD) | `v := f32x8(lista, i)`, `v * 2.0 + w`, `v.sum()`, `v.dot(w)`, `min(v, 1.0)`, `v.store(lista, i)`, `v[3]` |
+| Wektory (SIMD) | `v := f32x8(lista, i)`, `v * 2.0 + w`, `v.sum()`, `select(v > 0.0, v, 0.0)`, `t.lookup(idx)`, `acc.dot_add(a, b)`, `v.store(lista, i)` |
+| Procesory | `--cpu native`; `@cpu("avx2") fn …` razem z `if cpu_has("avx2")`; liczby zmiennoprzecinkowe liczone tak, jak zapisane (`fma`/`muladd` do łączenia) |
| Tekst | `"hp: \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
| Sterowanie | `if` / `else if` / `else` (również jako wartość), `while`, `for i in 0..10`, `break`, `continue`, `defer` |
| Pamięć | tablice, teksty i struktury zwalniają się same; `new(...)` / `free(...)` dla własnych struktur na stercie |
@@ -163,6 +164,7 @@ Wybierz przewodnik dla siebie:
- [x] Wątki (`import thread`) i operacje atomowe; pliki C++; `--target android`
- [x] Kompilator na każdym systemie, na który buduje: Linux, macOS (Apple Silicon i Intel), Windows; wydania robi CI
- [x] Typy wektorowe (`f32x8`, `i8x32`, …) dla SIMD i `--cpu native`
+- [x] Maski, przestawianie, tabele bajtów i `dot_add` dla jąder obliczeniowych; `@cpu` i `cpu_has`; `fma`/`muladd`, bity liczb, f16 i bf16
## Co gdzie leży
diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md
index 86fbf2e..6e21422 100644
--- a/docs/en/cheatsheet.md
+++ b/docs/en/cheatsheet.md
@@ -255,13 +255,19 @@ Types: `f32x4/8/16`, `f64x2/4/8`, `i8x16/32/64`, `i16x8/16/32`, `i32x4/8/16`, `i
| `f32x8(v)` | an 8-lane vector converted lane by lane (`f32x8(ints)`, `i32x8(floats)`) |
| `v + w`, `v * 2.0`, `-v` | lane by lane; a plain number counts for every lane (`/` only on floats) |
| `& \| ^ << >> ~` | lane by lane, whole-number lanes |
-| `v == w`, `v != w` | bool: every lane equal / not |
+| `v < w`, `v == w`, … | a mask `boolx8`: `m.all()`, `m.any()`, `m.count()`, `m & n`, `!m`, `m[i]` |
+| `select(m, a, b)` | lane by lane: `a` where `m` is true, else `b` |
| `v[i]`, `v[i] = x`, `v.len` | a lane; the number of lanes |
| `min(v, w)`, `max`, `abs(v)`, `clamp(v, lo, hi)` | lane by lane |
| `v.sum()`, `v.min()`, `v.max()`, `v.dot(w)` | one number from all the lanes |
| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | every lane (float lanes) |
| `v.fma(b, c)`, `v.muladd(b, c)` | `v * b + c`: one rounding always / the fastest way here |
| `v.store(list, i)` | lanes into `list[i]` … |
+| `f32x8(list, i, fill)`, `v.store_part(list, i)` | the end of a list: only the lanes inside it (the others are `fill`) |
+| `v.shuffle(3, 2, 1, 0)`, `v.lo()`, `v.hi()`, `concat(a, b)` | lanes rearranged (shuffle takes numbers written in the code) |
+| `table.lookup(idx)` | `table[idx % table.len]` per lane (`pshufb`, `tbl`) |
+| `acc.dot_add(a, b)` | `i32x8` sums gain 4 byte products each (`u8x32` × `i8x32`): `vpdpbusd`, `usdot` where there |
+| `@cpu("avx2,fma") fn …`, `cpu_has("avx2")` | a function that may use these features; check at run time before calling it |
---
diff --git a/docs/en/for-engineers.md b/docs/en/for-engineers.md
index 66c90cf..7b055a2 100644
--- a/docs/en/for-engineers.md
+++ b/docs/en/for-engineers.md
@@ -304,7 +304,13 @@ type goes; a call `f32x8(…)` goes to `vecMake()`.
| `f32x8(i32x8 v)` | `sitofp`, `fptosi`, `sext`, `trunc`… on the whole vector |
| `v + w`, `v * 2.0` | `fadd`/`add`… on vectors; a scalar operand is coerced to the lane type, then splatted |
| `v[i]` | a place: `getelementptr , ptr %v, i64 i` after a bounds check; or `extractelement` on a temporary |
-| `v == w` | `fcmp oeq`/`icmp eq` → `` → `llvm.vector.reduce.and` |
+| `v < w`, `v == w` … | `fcmp`/`icmp` → ``, a mask (`boolxN`: `vec` with `bool` lanes) |
+| `select(m, a, b)` | `select ` |
+| `m.any()`, `.all()`, `.count()` | `llvm.vector.reduce.or`/`.and`; `zext` + `llvm.vector.reduce.add` |
+| `f32x8(list, i, fill)`, `v.store_part(list, i)` | `llvm.masked.load`/`llvm.masked.store` with the mask `<0, 1, …> < len - i` |
+| `v.shuffle(…)`, `.lo()`, `.hi()`, `concat(a, b)` | `shufflevector` with a constant mask |
+| `table.lookup(idx)` | `and` with `len - 1`, then `extractelement`/`insertelement` per lane: the pattern the x86 and ARM backends turn into `pshufb`/`tbl` |
+| `acc.dot_add(a, b)` | `llvm.x86.avx512.vpdpbusd.{128,256,512}` or `llvm.aarch64.neon.{usdot,sdot,udot}` when the function's features have them; else `zext`/`sext`, `mul`, four strided `shufflevector`s and `add`s |
| `min`, `max`, `abs` | `llvm.minnum`/`maxnum`, `llvm.smin`/`umin`…, `llvm.fabs`/`llvm.abs` |
| `v.sum()`, `.min()`, `.max()` | `llvm.vector.reduce.fadd` (with `reassoc`), `.add`, `.fmin`, `.smin`… |
| `v.sqrt()`, `.floor()`, `.ceil()`, `.round()` | the `llvm.*` intrinsic of that name |
@@ -323,6 +329,20 @@ aligned address and crash.
**Which instructions** is clang's choice: `--cpu ` adds `-march=` (x86-64) or `-mcpu=` (ARM64) to the
IR's compile, so the same IR becomes SSE2, AVX2 + FMA, AVX-512 or NEON code. Lanes wider than the registers are split.
+Some operations need to know the features at code generation time (`dot_add` picks `vpdpbusd` only where it exists).
+`linker.cpuFeatures()` asks clang for its predefined macros with the same target and `--cpu`
+(`clang -march=X -dM -E empty.c`: `__AVXVNNI__`, `__AVX512VNNI__`, `__ARM_FEATURE_MATMUL_INT8` …), maps them to Finch's
+feature names and caches the answer per clang and flags; they reach `gen` as `Options.features`. `@cpu("…")` on a function
+(`Node.cpu`) adds its own: `define … "target-features"="+avx2,+fma"` (with `+evex512` for any AVX-512 feature, which
+LLVM 18+ needs for 512-bit registers), and `g.curFeatures` is the union while that function is generated. Features of the
+other architecture are dropped (`cpuOf`). `checkCpuCall` refuses a vector wider than 128 bits as a parameter or result
+between functions whose `@cpu` differ, because caller and callee would lower it to different registers.
+`cpu_has(name)` calls `finch_cpu_has` in the runtime: CPUID leaves 1 and 7 with XGETBV for the AVX/AVX-512 state on x86,
+`getauxval(AT_HWCAP/AT_HWCAP2)` on Linux ARM64, `sysctlbyname("hw.optional.arm.FEAT_…")` on macOS.
+
+Clang is always run as `NIX_ENFORCE_NO_NATIVE=0 clang …` on Unix: Nix's cc-wrapper otherwise drops `-march=native`
+inside `nix-shell`. If `--cpu native` still yields no SSE4.2 macro on x86, the driver warns.
+
---
## 8. Ownership and memory
@@ -572,5 +592,5 @@ evaluated twice and temporaries are never pointed to.
- Generics are monomorphised (code size grows with instantiations); there are no trait bounds, a wrong use is an error
inside the instantiated body.
- A failed test's owned memory is not freed (the test runner `longjmp`s out of it).
-- Vectors have no lane masks (`v < w`), shuffles or integer division.
+- Vectors have no integer division, and `lookup` on baseline x86-64 (no SSSE3) is done lane by lane.
diff --git a/docs/en/for-technicians.md b/docs/en/for-technicians.md
index 02d55fe..c3ab1b7 100644
--- a/docs/en/for-technicians.md
+++ b/docs/en/for-technicians.md
@@ -1400,18 +1400,15 @@ arrays, maps and function parameters, and `print` and `str` show it.
| `+ - * /` | lane by lane (`/` on floats only: x86 and ARM can't divide whole numbers lane by lane) |
| `& \| ^ << >> ~` | lane by lane, on whole-number lanes |
| `-v` | every lane negated |
-| `v == w`, `v != w` | `true` if every lane is equal / if any differs |
| `v[i]`, `v[i] = x`, `v.len` | one lane; the number of lanes |
| `min(v, w)`, `max(v, w)`, `abs(v)`, `clamp(v, lo, hi)` | lane by lane; `lo`, `hi` and `w` can be plain numbers |
| `v.sum()`, `v.min()`, `v.max()` | all the lanes made into one number |
| `v.dot(w)` | `(v * w).sum()` |
| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | every lane (float lanes) |
-| `v.fma(b, c)` | `v * b + c` with one rounding, always |
-| `v.muladd(b, c)` | `v * b + c` the fastest way here: one instruction where the processor can |
+| `v.fma(b, c)` | `v * b + c` with one rounding, always (the same on every processor) |
+| `v.muladd(b, c)` | `v * b + c` the fastest way here: one instruction where the processor can (the last bit may differ between processors) |
| `v.store(list, i)` | writes the lanes to `list[i]` … (or through a `ptr`) |
-`<` and the other comparisons would give an answer per lane, so they aren't there; use `min`, `max` or `v[i] < w[i]`.
-
**A real loop.** The usual shape: as many whole vectors as fit, then the rest one by one.
```c
@@ -1433,10 +1430,125 @@ fn dot([]f32 a, []f32 b) -> f32 {
`examples/simd.fch` times this against the plain loop.
-**Which instructions.** By default a program runs on every processor of its system (on x86-64 that means SSE2, so an
-`f32x8` is done as two halves). `--cpu native` (or `cpu = "native"` in `finch.toml`) lets LLVM use everything the processor
-that builds it has: AVX2 and FMA, AVX-512, or NEON's newer parts on ARM. Such a program needs a processor like that one to
-run. To pick a level instead: `--cpu x86-64-v3` (AVX2, most PCs since 2015), `--cpu x86-64-v4` (AVX-512), `--cpu apple-m1`.
+### Masks: comparing, choosing, the end of a list
+
+Comparing vectors compares every lane, so the answer is a **mask**: one `true`/`false` a lane, of type `boolx8` (for 8 lanes).
+
+```c
+v := f32x8(-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, -4.0, 5.0)
+m := v > 0.0 // boolx8(false, false, false, true, true, true, false, true)
+relu := select(m, v, 0.0) // v where the mask is true, 0 elsewhere
+print(m.count(), m.any(), m.all()) // 4 true false
+if (v == w).all() { ... } // a condition needs one bool: all() or any()
+```
+
+| | |
+|---|---|
+| `v < w`, `<=`, `>`, `>=`, `==`, `!=` | a mask (a plain number on one side counts for every lane) |
+| `select(m, a, b)` | lane by lane: `a` where `m` is true, `b` where false (either may be a plain number) |
+| `m.any()`, `m.all()`, `m.count()` | is any lane true, are all, how many |
+| `m & n`, `m \| n`, `m ^ n`, `!m` | masks combined |
+| `m[i]` | one lane, as a `bool` |
+| `i32x8(m)`, `boolx8(v)` | true is 1; a lane that isn't 0 is true |
+
+**The end of a list.** A list is rarely a whole number of vectors long. Two forms touch only the lanes that are inside it:
+
+| | |
+|---|---|
+| `f32x8(list, i, fill)` | `list[i]` … as far as the list goes; the lanes past its end are `fill` |
+| `v.store_part(list, i)` | writes only the lanes that fit in the list |
+
+```c
+fn add_one([]f32 xs) {
+ i := 0
+ while i < xs.len { // no separate loop for the rest
+ (f32x8(xs, i, 0.0) + 1.0).store_part(xs, i)
+ i += 8
+ }
+}
+```
+
+### Moving lanes
+
+| | |
+|---|---|
+| `v.shuffle(3, 2, 1, 0)` | the lanes with these numbers (written in the code), in this order; as many lanes as numbers |
+| `v.lo()`, `v.hi()` | the lower and the upper half |
+| `concat(a, b)` | `a`'s lanes, then `b`'s |
+| `table.lookup(idx)` | for every lane of `idx`: `table[idx % table.len]`. A byte table (`u8x16`, `i8x16`) is one `pshufb` on x86, `tbl` on ARM |
+
+Unpacking 4-bit numbers (two to a byte, as quantized models store them) is shuffle, mask and lookup:
+
+```c
+fn unpack4(u8x8 packed, i8x16 meaning) -> i8x16 {
+ pairs := packed.shuffle(0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7) // each byte twice
+ odd := u8x16(0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1) == 1
+ nibbles := select(odd, pairs >> 4, pairs & 15) // low nibble, then high
+ return meaning.lookup(nibbles) // 0..15 -> what it stands for
+}
+```
+
+### Int8 dot products: dot_add
+
+`acc.dot_add(a, b)` is the step of a quantized model's matrix multiply. `acc` is an `i32x8` of sums; `a` and `b` have four
+times as many bytes (`u8x32` and `i8x32`; `i8` × `i8` and `u8` × `u8` work too). Each lane of `acc` gains the sum of four
+products, `a[4k]*b[4k] + … + a[4k+3]*b[4k+3]`, exactly (no saturation), and the sums stay in the vector:
+
+```c
+acc := i32x8(0)
+i := 0
+while i + 32 <= weights.len {
+ acc = acc.dot_add(u8x32(activations, i), i8x32(weights, i))
+ i += 32
+}
+total := acc.sum() // once, at the end
+```
+
+It is one instruction where the processor has one: `vpdpbusd` with AVX-VNNI (Intel since Alder Lake, AMD Zen 5) or
+AVX-512 VNNI, `usdot`/`sdot`/`udot` on ARM with the dot-product features. Elsewhere it is a few instructions
+(`pmaddwd` and adds), with the same result. Which one is used depends on `--cpu` or the function's `@cpu` (below).
+
+### Choosing the instructions: --cpu, @cpu and cpu_has
+
+By default a program runs on every processor of its system (on x86-64 that means SSE2, so an `f32x8` is done as two
+halves). Three ways to use more:
+
+**`--cpu`, for the whole program.** `--cpu native` (or `cpu = "native"` in `finch.toml`) lets LLVM use everything the
+processor that builds it has: AVX2 and FMA, AVX-512, VNNI, or NEON's newer parts on ARM. The program then needs a processor
+like that one. To pick a level instead: `--cpu x86-64-v3` (AVX2, most PCs since 2015), `--cpu x86-64-v4` (AVX-512),
+`--cpu alderlake`, `--cpu apple-m1`, `--cpu neoverse-v1`.
+
+**`@cpu`, for one function, and `cpu_has` to choose at run time.** One program can carry an AVX-512 version, an AVX2
+version and a plain one, and pick the best the computer it runs on has:
+
+```c
+@cpu("avx512f,avx512bw,avx512vl,avx512vnni")
+fn matmul_avx512([]u8 a, []i8 b, []i32 out) { ... } // may use AVX-512 inside
+
+@cpu("x86-64-v3")
+fn matmul_avx2([]u8 a, []i8 b, []i32 out) { ... } // a level works too
+
+fn matmul([]u8 a, []i8 b, []i32 out) {
+ if cpu_has("avx512vnni") && cpu_has("avx512vl") {
+ matmul_avx512(a, b, out)
+ } else if cpu_has("avx2") {
+ matmul_avx2(a, b, out)
+ } else {
+ matmul_plain(a, b, out)
+ }
+}
+```
+
+- The features are x86's `sse3 ssse3 sse4.1 sse4.2 popcnt avx avx2 fma f16c bmi1 bmi2 avx512f avx512bw avx512dq avx512vl
+ avx512vnni avx512bf16 avx512fp16 avxvnni`, the levels `x86-64-v2`/`v3`/`v4`, and ARM's `neon fp16 dotprod i8mm bf16 sve sve2`.
+- Names of the other architecture are left out when building, so the x86 and ARM versions sit side by side in one file
+ (and `cpu_has` says `false` for them).
+- Call a `@cpu` function only after `cpu_has` said yes: on a processor without the feature it stops with *Illegal instruction*.
+- A vector wider than 16 bytes (`f32x8`, `i32x16` …) can't be passed between functions with different `@cpu`: the two
+ sides would expect it in different registers. Pass the list (or a `ptr`) instead; Finch says so if you try.
Lanes wider than the processor's registers are split by LLVM, and the same code builds for x86-64 and ARM64 alike.
`v.sum()` on float lanes adds them in whatever order is fastest, so the last digits can differ from adding them one by one.
+
+On NixOS, Nix's clang wrapper removes `-march=native` inside `nix-shell`. Finch tells the wrapper to keep it, so
+`--cpu native` works there too; if it ever has no effect, Finch warns.
diff --git a/docs/pl/dla-inzynierow.md b/docs/pl/dla-inzynierow.md
index 65d700f..eefbeb0 100644
--- a/docs/pl/dla-inzynierow.md
+++ b/docs/pl/dla-inzynierow.md
@@ -310,7 +310,13 @@ wszędzie tam, gdzie może stać typ; wywołanie `f32x8(…)` trafia do `vecMake
| `f32x8(i32x8 v)` | `sitofp`, `fptosi`, `sext`, `trunc`… na całym wektorze |
| `v + w`, `v * 2.0` | `fadd`/`add`… na wektorach; argument skalarny jest dopasowywany do typu elementu i rozpowielany |
| `v[i]` | miejsce w pamięci: `getelementptr , ptr %v, i64 i` po sprawdzeniu zakresu; albo `extractelement` na wartości tymczasowej |
-| `v == w` | `fcmp oeq`/`icmp eq` → `` → `llvm.vector.reduce.and` |
+| `v < w`, `v == w` … | `fcmp`/`icmp` → ``, maska (`boolxN`: `vec` z elementami `bool`) |
+| `select(m, a, b)` | `select ` |
+| `m.any()`, `.all()`, `.count()` | `llvm.vector.reduce.or`/`.and`; `zext` + `llvm.vector.reduce.add` |
+| `f32x8(lista, i, wypelnienie)`, `v.store_part(lista, i)` | `llvm.masked.load`/`llvm.masked.store` z maską `<0, 1, …> < len - i` |
+| `v.shuffle(…)`, `.lo()`, `.hi()`, `concat(a, b)` | `shufflevector` ze stałą maską |
+| `tabela.lookup(idx)` | `and` z `len - 1`, potem `extractelement`/`insertelement` dla każdego elementu: wzorzec, który zaplecza x86 i ARM zamieniają na `pshufb`/`tbl` |
+| `acc.dot_add(a, b)` | `llvm.x86.avx512.vpdpbusd.{128,256,512}` albo `llvm.aarch64.neon.{usdot,sdot,udot}`, gdy funkcja ma te rozszerzenia; w przeciwnym razie `zext`/`sext`, `mul`, cztery `shufflevector` co czwarty element i `add` |
| `min`, `max`, `abs` | `llvm.minnum`/`maxnum`, `llvm.smin`/`umin`…, `llvm.fabs`/`llvm.abs` |
| `v.sum()`, `.min()`, `.max()` | `llvm.vector.reduce.fadd` (z `reassoc`), `.add`, `.fmin`, `.smin`… |
| `v.sqrt()`, `.floor()`, `.ceil()`, `.round()` | funkcja wewnętrzna `llvm.*` o tej nazwie |
@@ -331,6 +337,20 @@ użyć `vmovaps` na adresie wyrównanym do 16 i program by się wysypał.
do kompilacji IR, więc to samo IR staje się kodem SSE2, AVX2 + FMA, AVX-512 albo NEON. Wektory szersze niż rejestry
procesora są dzielone na części.
+Niektóre działania muszą znać rozszerzenia procesora już przy generowaniu kodu (`dot_add` wybiera `vpdpbusd` tylko tam,
+gdzie ono istnieje). `linker.cpuFeatures()` pyta clanga o makra, które sam definiuje dla tego samego systemu i `--cpu`
+(`clang -march=X -dM -E pusty.c`: `__AVXVNNI__`, `__AVX512VNNI__`, `__ARM_FEATURE_MATMUL_INT8` …), zamienia je na nazwy
+rozszerzeń Fincha i zapamiętuje odpowiedź dla danego clanga i flag; do `gen` trafiają jako `Options.features`. `@cpu("…")`
+przy funkcji (`Node.cpu`) dodaje własne: `define … "target-features"="+avx2,+fma"` (z `+evex512` przy każdym rozszerzeniu
+AVX-512, którego LLVM 18+ wymaga dla rejestrów 512-bitowych), a `g.curFeatures` to suma obu na czas generowania tej
+funkcji. Rozszerzenia drugiej architektury są pomijane (`cpuOf`). `checkCpuCall` nie pozwala przekazać wektora szerszego
+niż 128 bitów jako parametru albo wyniku między funkcjami o różnym `@cpu`, bo wywołujący i wywoływany umieściliby go
+w innych rejestrach. `cpu_has(nazwa)` woła `finch_cpu_has` z runtime'u: na x86 CPUID (liście 1 i 7) z XGETBV dla stanu
+AVX/AVX-512, na Linuksie ARM64 `getauxval(AT_HWCAP/AT_HWCAP2)`, na macOS `sysctlbyname("hw.optional.arm.FEAT_…")`.
+
+Na Uniksach clang jest zawsze uruchamiany jako `NIX_ENFORCE_NO_NATIVE=0 clang …`: inaczej nakładka cc-wrapper z Niksa
+usuwa `-march=native` w `nix-shell`. Jeśli `--cpu native` mimo to nie daje na x86 makra SSE4.2, sterownik ostrzega.
+
---
## 8. Własność i pamięć
@@ -585,4 +605,4 @@ nie jest obliczane dwa razy, a wskaźnik nigdy nie wskazuje na wartość tymczas
- Typy generyczne są monomorfizowane (kod rośnie z każdą instancją); nie ma ograniczeń na parametry typu, więc złe
użycie daje błąd wewnątrz ciała instancji.
- Pamięć, którą miał nieudany test, nie jest zwalniana (uruchamianie testów wychodzi z niego przez `longjmp`).
-- Wektory nie mają masek (`v < w`), przetasowań ani dzielenia całkowitego.
+- Wektory nie mają dzielenia całkowitego, a `lookup` na podstawowym x86-64 (bez SSSE3) działa element po elemencie.
diff --git a/docs/pl/dla-technikow.md b/docs/pl/dla-technikow.md
index 281a4a5..1285f4a 100644
--- a/docs/pl/dla-technikow.md
+++ b/docs/pl/dla-technikow.md
@@ -1408,18 +1408,15 @@ struktury, elementem tablicy, wartością w mapie i parametrem funkcji, a `print
| `+ - * /` | element po elemencie (`/` tylko na liczbach zmiennoprzecinkowych: x86 i ARM nie dzielą liczb całkowitych element po elemencie) |
| `& \| ^ << >> ~` | element po elemencie, na liczbach całkowitych |
| `-v` | każdy element ze zmienionym znakiem |
-| `v == w`, `v != w` | `true`, jeśli wszystkie elementy są równe / jeśli któryś się różni |
| `v[i]`, `v[i] = x`, `v.len` | jeden element; liczba elementów |
| `min(v, w)`, `max(v, w)`, `abs(v)`, `clamp(v, lo, hi)` | element po elemencie; `lo`, `hi` i `w` mogą być zwykłymi liczbami |
| `v.sum()`, `v.min()`, `v.max()` | wszystkie elementy złożone w jedną liczbę |
| `v.dot(w)` | `(v * w).sum()` |
| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | każdy element (liczby zmiennoprzecinkowe) |
-| `v.fma(b, c)` | `v * b + c` z jednym zaokrągleniem, zawsze |
-| `v.muladd(b, c)` | `v * b + c` najszybciej, jak się da: jedną instrukcją, gdzie procesor to potrafi |
+| `v.fma(b, c)` | `v * b + c` z jednym zaokrągleniem, zawsze (tak samo na każdym procesorze) |
+| `v.muladd(b, c)` | `v * b + c` najszybciej, jak się da: jedną instrukcją, gdzie procesor to potrafi (ostatni bit może się różnić między procesorami) |
| `v.store(lista, i)` | zapisuje elementy do `lista[i]` … (albo przez `ptr`) |
-`<` i inne porównania dałyby osobną odpowiedź dla każdego elementu, więc ich nie ma; użyj `min`, `max` albo `v[i] < w[i]`.
-
**Prawdziwa pętla.** Zwykły schemat: tyle całych wektorów, ile się zmieści, a resztę po jednej liczbie.
```c
@@ -1441,12 +1438,129 @@ fn iloczyn([]f32 a, []f32 b) -> f32 {
`examples/simd.fch` mierzy czas tej pętli i porównuje go ze zwykłą.
-**Jakie instrukcje.** Domyślnie program działa na każdym procesorze swojego systemu (na x86-64 oznacza to SSE2, więc
-`f32x8` liczy się w dwóch połówkach). `--cpu native` (albo `cpu = "native"` w `finch.toml`) pozwala LLVM użyć wszystkiego,
-co ma procesor, na którym budujesz: AVX2 i FMA, AVX-512 albo nowszych części NEON na ARM. Taki program potrzebuje potem
+### Maski: porównywanie, wybieranie, koniec listy
+
+Porównanie wektorów porównuje każdy element, więc odpowiedzią jest **maska**: jedno `true`/`false` na element,
+typu `boolx8` (dla 8 elementów).
+
+```c
+v := f32x8(-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, -4.0, 5.0)
+m := v > 0.0 // boolx8(false, false, false, true, true, true, false, true)
+relu := select(m, v, 0.0) // v tam, gdzie maska jest prawdą, a gdzie indziej 0
+print(m.count(), m.any(), m.all()) // 4 true false
+if (v == w).all() { ... } // warunek potrzebuje jednego bool: all() albo any()
+```
+
+| | |
+|---|---|
+| `v < w`, `<=`, `>`, `>=`, `==`, `!=` | maska (zwykła liczba po jednej stronie działa na każdy element) |
+| `select(m, a, b)` | element po elemencie: `a` tam, gdzie `m` jest prawdą, `b` tam, gdzie fałszem (każde z nich może być zwykłą liczbą) |
+| `m.any()`, `m.all()`, `m.count()` | czy któryś element jest prawdą, czy wszystkie, ile ich jest |
+| `m & n`, `m \| n`, `m ^ n`, `!m` | łączenie masek |
+| `m[i]` | jeden element jako `bool` |
+| `i32x8(m)`, `boolx8(v)` | prawda to 1; element różny od 0 to prawda |
+
+**Koniec listy.** Lista rzadko ma długość będącą wielokrotnością wektora. Dwie formy dotykają tylko elementów,
+które są w liście:
+
+| | |
+|---|---|
+| `f32x8(lista, i, wypelnienie)` | `lista[i]` … tak daleko, jak sięga lista; elementy za jej końcem dostają `wypelnienie` |
+| `v.store_part(lista, i)` | zapisuje tylko elementy, które mieszczą się w liście |
+
+```c
+fn dodaj_jeden([]f32 xs) {
+ i := 0
+ while i < xs.len { // bez osobnej pętli na resztę
+ (f32x8(xs, i, 0.0) + 1.0).store_part(xs, i)
+ i += 8
+ }
+}
+```
+
+### Przestawianie elementów
+
+| | |
+|---|---|
+| `v.shuffle(3, 2, 1, 0)` | elementy o tych numerach (zapisanych w kodzie), w tej kolejności; tyle elementów, ile numerów |
+| `v.lo()`, `v.hi()` | dolna i górna połowa |
+| `concat(a, b)` | elementy `a`, a po nich elementy `b` |
+| `tabela.lookup(idx)` | dla każdego elementu `idx`: `tabela[idx % tabela.len]`. Tabela bajtów (`u8x16`, `i8x16`) to jedno `pshufb` na x86 i `tbl` na ARM |
+
+Rozpakowanie liczb 4-bitowych (po dwie w bajcie, tak jak trzymają je skwantowane modele) to przestawienie, maska i tabela:
+
+```c
+fn rozpakuj4(u8x8 spakowane, i8x16 znaczenie) -> i8x16 {
+ pary := spakowane.shuffle(0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7) // każdy bajt dwa razy
+ nieparzyste := u8x16(0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1) == 1
+ polowki := select(nieparzyste, pary >> 4, pary & 15) // najpierw dolne 4 bity, potem górne
+ return znaczenie.lookup(polowki) // 0..15 -> to, co oznacza
+}
+```
+
+### Iloczyn skalarny na bajtach: dot_add
+
+`acc.dot_add(a, b)` to krok mnożenia macierzy w skwantowanym modelu. `acc` to `i32x8` z sumami; `a` i `b` mają cztery
+razy więcej bajtów (`u8x32` i `i8x32`; działają też `i8` × `i8` i `u8` × `u8`). Każdy element `acc` dostaje sumę czterech
+iloczynów, `a[4k]*b[4k] + … + a[4k+3]*b[4k+3]`, dokładnie (bez nasycania), a sumy zostają w wektorze:
+
+```c
+acc := i32x8(0)
+i := 0
+while i + 32 <= wagi.len {
+ acc = acc.dot_add(u8x32(aktywacje, i), i8x32(wagi, i))
+ i += 32
+}
+suma := acc.sum() // raz, na końcu
+```
+
+To jedna instrukcja tam, gdzie procesor ją ma: `vpdpbusd` z AVX-VNNI (Intel od Alder Lake, AMD Zen 5) albo
+AVX-512 VNNI, `usdot`/`sdot`/`udot` na ARM z rozszerzeniami do iloczynu skalarnego. Gdzie indziej to kilka instrukcji
+(`pmaddwd` i dodawania), z tym samym wynikiem. Wybór zależy od `--cpu` albo `@cpu` funkcji (niżej).
+
+### Wybór instrukcji: --cpu, @cpu i cpu_has
+
+Domyślnie program działa na każdym procesorze swojego systemu (na x86-64 oznacza to SSE2, więc `f32x8` liczy się
+w dwóch połówkach). Są trzy sposoby, żeby użyć więcej:
+
+**`--cpu`, dla całego programu.** `--cpu native` (albo `cpu = "native"` w `finch.toml`) pozwala LLVM użyć wszystkiego,
+co ma procesor, na którym budujesz: AVX2 i FMA, AVX-512, VNNI albo nowszych części NEON na ARM. Program potrzebuje wtedy
podobnego procesora. Można też wybrać poziom: `--cpu x86-64-v3` (AVX2, większość komputerów od 2015 roku),
-`--cpu x86-64-v4` (AVX-512), `--cpu apple-m1`.
+`--cpu x86-64-v4` (AVX-512), `--cpu alderlake`, `--cpu apple-m1`, `--cpu neoverse-v1`.
+
+**`@cpu` dla jednej funkcji i `cpu_has` do wyboru w trakcie działania.** Jeden program może mieć wersję z AVX-512,
+wersję z AVX2 i zwykłą, i wybrać najlepszą, jaką ma komputer, na którym działa:
+
+```c
+@cpu("avx512f,avx512bw,avx512vl,avx512vnni")
+fn mnoz_avx512([]u8 a, []i8 b, []i32 wynik) { ... } // może w środku używać AVX-512
+
+@cpu("x86-64-v3")
+fn mnoz_avx2([]u8 a, []i8 b, []i32 wynik) { ... } // poziom też działa
+
+fn mnoz([]u8 a, []i8 b, []i32 wynik) {
+ if cpu_has("avx512vnni") && cpu_has("avx512vl") {
+ mnoz_avx512(a, b, wynik)
+ } else if cpu_has("avx2") {
+ mnoz_avx2(a, b, wynik)
+ } else {
+ mnoz_zwykle(a, b, wynik)
+ }
+}
+```
+
+- Rozszerzenia to dla x86 `sse3 ssse3 sse4.1 sse4.2 popcnt avx avx2 fma f16c bmi1 bmi2 avx512f avx512bw avx512dq avx512vl
+ avx512vnni avx512bf16 avx512fp16 avxvnni`, poziomy `x86-64-v2`/`v3`/`v4`, a dla ARM `neon fp16 dotprod i8mm bf16 sve sve2`.
+- Nazwy z drugiej architektury są przy budowaniu pomijane, więc wersje dla x86 i ARM mogą stać obok siebie w jednym pliku
+ (a `cpu_has` zwraca dla nich `false`).
+- Funkcję z `@cpu` wołaj dopiero wtedy, gdy `cpu_has` odpowie „tak”: na procesorze bez tego rozszerzenia program
+ zatrzyma się z *Illegal instruction*.
+- Wektora szerszego niż 16 bajtów (`f32x8`, `i32x16` …) nie da się przekazać między funkcjami o różnym `@cpu`: obie strony
+ oczekiwałyby go w innych rejestrach. Przekaż zamiast tego listę (albo `ptr`); Finch o tym powie, jeśli spróbujesz.
Wektory szersze niż rejestry procesora LLVM dzieli na części, a ten sam kod buduje się tak samo na x86-64 i na ARM64.
`v.sum()` na liczbach zmiennoprzecinkowych dodaje elementy w kolejności, która jest najszybsza, więc ostatnie cyfry mogą
się różnić od dodawania ich po kolei.
+
+Na NixOS nakładka Niksa na clanga usuwa `-march=native` w `nix-shell`. Finch mówi jej, żeby to zostawiła, więc
+`--cpu native` działa i tam; gdyby kiedyś nie miało żadnego skutku, Finch ostrzeże.
diff --git a/docs/pl/sciaga.md b/docs/pl/sciaga.md
index fe062ae..1d88ed0 100644
--- a/docs/pl/sciaga.md
+++ b/docs/pl/sciaga.md
@@ -255,13 +255,19 @@ Typy: `f32x4/8/16`, `f64x2/4/8`, `i8x16/32/64`, `i16x8/16/32`, `i32x4/8/16`, `i6
| `f32x8(v)` | 8-elementowy wektor przeliczony element po elemencie (`f32x8(calkowite)`, `i32x8(ulamki)`) |
| `v + w`, `v * 2.0`, `-v` | element po elemencie; zwykła liczba działa na każdy element (`/` tylko dla liczb zmiennoprzecinkowych) |
| `& \| ^ << >> ~` | element po elemencie, dla liczb całkowitych |
-| `v == w`, `v != w` | bool: czy wszystkie elementy są równe / czy któryś się różni |
+| `v < w`, `v == w`, … | maska `boolx8`: `m.all()`, `m.any()`, `m.count()`, `m & n`, `!m`, `m[i]` |
+| `select(m, a, b)` | element po elemencie: `a` tam, gdzie `m` jest prawdą, a gdzie nie, `b` |
| `v[i]`, `v[i] = x`, `v.len` | jeden element; liczba elementów |
| `min(v, w)`, `max`, `abs(v)`, `clamp(v, lo, hi)` | element po elemencie |
| `v.sum()`, `v.min()`, `v.max()`, `v.dot(w)` | jedna liczba ze wszystkich elementów |
| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | każdy element (liczby zmiennoprzecinkowe) |
| `v.fma(b, c)`, `v.muladd(b, c)` | `v * b + c`: zawsze z jednym zaokrągleniem / najszybciej na tym procesorze |
| `v.store(lista, i)` | zapisuje elementy do `lista[i]` … |
+| `f32x8(lista, i, wypelnienie)`, `v.store_part(lista, i)` | koniec listy: tylko elementy, które są w niej (pozostałe to `wypelnienie`) |
+| `v.shuffle(3, 2, 1, 0)`, `v.lo()`, `v.hi()`, `concat(a, b)` | przestawione elementy (shuffle przyjmuje numery zapisane w kodzie) |
+| `tabela.lookup(idx)` | `tabela[idx % tabela.len]` dla każdego elementu (`pshufb`, `tbl`) |
+| `acc.dot_add(a, b)` | sumy `i32x8` dostają po 4 iloczyny bajtów (`u8x32` × `i8x32`): `vpdpbusd`, `usdot` tam, gdzie są |
+| `@cpu("avx2,fma") fn …`, `cpu_has("avx2")` | funkcja, która może używać tych rozszerzeń; przed jej wywołaniem sprawdź procesor |
---
diff --git a/editors/kate/finch.xml b/editors/kate/finch.xml
index f9f2642..b8a9229 100644
--- a/editors/kate/finch.xml
+++ b/editors/kate/finch.xml
@@ -82,6 +82,19 @@
- atomic_store
- atomic_swap
- atomic_cas
+ - select
+ - concat
+ - fma
+ - muladd
+ - cpu_has
+ - f32_bits
+ - f32_from_bits
+ - f64_bits
+ - f64_from_bits
+ - f16_to_f32
+ - f32_to_f16
+ - bf16_to_f32
+ - f32_to_bf16
@@ -104,7 +117,8 @@
-
+
+
diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md
index a0a750c..fc8f613 100644
--- a/editors/vscode/CHANGELOG.md
+++ b/editors/vscode/CHANGELOG.md
@@ -2,7 +2,8 @@
## 2.6.0
-- Vector types (`f32x8`, `i8x32`, `f64x4`, …) highlighted as types; completion of their methods (`sum`, `dot`, `fma`, `store`, …).
+- Vector types (`f32x8`, `i8x32`, `f64x4`, …) and masks (`boolx8`) highlighted as types; completion of their methods (`sum`, `dot_add`, `shuffle`, `lookup`, `store`, …).
+- `@cpu(...)` and the new built-ins (`select`, `concat`, `fma`, `muladd`, `cpu_has`, `f32_bits`, `f16_to_f32`, …) highlighted.
## 2.5.0
diff --git a/editors/vscode/syntaxes/finch.tmLanguage.json b/editors/vscode/syntaxes/finch.tmLanguage.json
index e3c2dea..8ce8dd8 100644
--- a/editors/vscode/syntaxes/finch.tmLanguage.json
+++ b/editors/vscode/syntaxes/finch.tmLanguage.json
@@ -147,11 +147,15 @@
"match": "\\bself\\b"
},
{
- "name": "storage.type.primitive.finch",
- "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"
+ "name": "storage.modifier.attribute.finch",
+ "match": "@cpu\\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*(?=\\()",
+ "name": "storage.type.primitive.finch",
+ "match": "\\b(int|float|bool|char|str|ptr|map|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64|(?:[iuf](?:8|16|32|64)|bool)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|select|concat|fma|muladd|cpu_has|f32_bits|f32_from_bits|f64_bits|f64_from_bits|f16_to_f32|f32_to_f16|bf16_to_f32|f32_to_bf16)\\s*(?=\\()",
"captures": {
"1": {
"name": "support.function.builtin.finch"
diff --git a/editors/vscode/test/grammar.js b/editors/vscode/test/grammar.js
index 0903ec5..ce951a9 100644
--- a/editors/vscode/test/grammar.js
+++ b/editors/vscode/test/grammar.js
@@ -30,6 +30,7 @@ const code = [
' assert(clamp(x, 0, 9) == 3)',
'}',
'fn dot(f32x8 a, i8x32 b) {',
+ '@cpu("avx2") fn k(boolx8 m) { select(m, a, b) }',
];
const expect = [ // [line, token text, scope that must be present]
[0, 'import', 'keyword.control.import'], [0, '"stdio.h"', 'string.quoted.double'],
@@ -51,6 +52,7 @@ const expect = [ // [line, token text, scope that must be present]
[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'],
+ [21, '@cpu', 'storage.modifier.attribute'], [21, 'boolx8', 'storage.type.primitive'], [21, 'select', 'support.function.builtin'],
];
registry.loadGrammar('source.finch').then((g) => {
let state = vsctm.INITIAL, toks = [];