diff --git a/README.md b/README.md index edec82a..7f62239 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ 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` (built, not yet tried on a device) +- [x] Threads (`import thread`) and atomics; C++ files; `--target android` (compiles; NDK linking and devices untried) ## Layout diff --git a/README.pl.md b/README.pl.md index b4c2651..52b438d 100644 --- a/README.pl.md +++ b/README.pl.md @@ -151,7 +151,7 @@ Wybierz przewodnik dla siebie: - [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` (budowane, jeszcze nie sprawdzone na urządzeniu) +- [x] Wątki (`import thread`) i operacje atomowe; pliki C++; `--target android` (kompiluje się; linkowanie z NDK i urządzenia niesprawdzone) ## Struktura diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md index 3497dc6..d27262c 100644 --- a/docs/en/cheatsheet.md +++ b/docs/en/cheatsheet.md @@ -327,7 +327,7 @@ Automatic only when nothing is lost: smaller → bigger number, any int → floa - Arrays, text and structs free themselves at the end of their block. No `free` needed. - `b := a` copies; a call's result moves; `return x` moves. -- Passing a struct: `fn f(Body b)` works on its own copy (the caller's stays as it was), `fn f(ptr[Body] b)` changes the caller's (`f(body)` or `f(addr(body))`), a method gets `self` by address, never a copy. +- Passing a struct: `fn f(Body b)` works on its own copy (the caller's stays as it was), `fn f(ptr[Body] b)` changes the caller's (`f(body)` or `f(addr(body))`), a method gets `self` by address, never a copy. A struct with 256+ bytes of numbers passed by value or in `for x in list` gets a warning. - `new(...)` + `free(...)` (often with `defer`) for linked lists, trees, sharing. - C memory: C's own functions (`malloc` / `free`, `fclose`, …). diff --git a/docs/en/for-engineers.md b/docs/en/for-engineers.md index d80aefc..56ce0a9 100644 --- a/docs/en/for-engineers.md +++ b/docs/en/for-engineers.md @@ -503,6 +503,13 @@ makes the C-callable thunk. `atomic_add/sub/swap` lower to `atomicrmw add/sub/xc take a `ptr[T]` to any integer type. Finch's arrays, text and maps are not synchronised; sharing one between threads needs a mutex. +### Warnings + +`warn()` prints `file:line:col: warning: …` to stderr (so `finch ir` stays pure IR) or, with `index`, an `@W` record the +language server publishes with severity 2. One per source position. Two are given: a struct parameter, and the element of +`for x in list`, whose plain data (`byteSize()`: numbers, nested structs, C `[N]T`; `str`/arrays/maps count 0, as only +their handle is copied) is 256 bytes or more. + ### Struct arguments by address `argWant()` (call arguments only, not assignments): when a parameter is `ptr[S]` and the argument is a plain variable diff --git a/docs/en/for-technicians.md b/docs/en/for-technicians.md index 2615007..9e41283 100644 --- a/docs/en/for-technicians.md +++ b/docs/en/for-technicians.md @@ -1134,11 +1134,20 @@ 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. Android is built (`--target android`) but not yet tried on a device. +- 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. -On purpose, not planned: a warning for every big struct that gets copied. A copy in Finch is always visible in the -code (`b := a`, passing a struct to a `Body b` parameter), a method never copies `self`, and `ptr[Body]` (or passing -the variable to a `ptr[Body]` parameter) shares instead. A warning that fires on ordinary code would be noise. +### Warnings about big copies + +A struct with 256 bytes or more of plain data (numbers, structs of numbers, C's `[N]T`) is copied byte by byte when it +is a parameter (`fn f(Body b)`) and in `for x in bodies`. Finch says so, and still builds the program: + +``` +game.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) +game.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 +``` + +Text, arrays and maps inside a struct don't count: passing the struct copies only their handles, not what they hold. +`b := a` is not warned about: it says "copy" in the code. A method never copies `self`. Editors show the warnings in yellow. See the [roadmap in the README](../../README.md#roadmap). @@ -1283,5 +1292,6 @@ 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: it is built and checked here, but not -yet tried on a device. +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. diff --git a/docs/pl/dla-inzynierow.md b/docs/pl/dla-inzynierow.md index a8319f6..c948e86 100644 --- a/docs/pl/dla-inzynierow.md +++ b/docs/pl/dla-inzynierow.md @@ -496,6 +496,13 @@ thunk wołany z C. `atomic_add/sub/swap` to `atomicrmw add/sub/xchg … seq_cst` `ptr[T]` do dowolnej liczby całkowitej. Tablice, teksty i mapy Fincha nie są synchronizowane; dzielenie ich między wątkami wymaga mutexu. +### 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. + ### Struktura jako argument przez adres `argWant()` (tylko argumenty wywołań, nie przypisania): gdy parametr to `ptr[S]`, a argument to zwykła zmienna typu `S`, diff --git a/docs/pl/dla-technikow.md b/docs/pl/dla-technikow.md index cfd1021..4ebceba 100644 --- a/docs/pl/dla-technikow.md +++ b/docs/pl/dla-technikow.md @@ -1138,12 +1138,22 @@ 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. Android jest budowany (`--target android`), ale jeszcze nie - sprawdzony na urządzeniu. +- 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. -Celowo nie planowane: ostrzeżenie przy każdym kopiowaniu dużej struktury. Kopię w Finchu zawsze widać w kodzie -(`b := a`, struktura podana do parametru `Cialo c`), metoda nigdy nie kopiuje `self`, a `ptr[Cialo]` (albo zmienna -podana do parametru `ptr[Cialo]`) współdzieli zamiast kopiować. Ostrzeżenie przy zwykłym kodzie byłoby szumem. +### 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: + +``` +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 +``` + +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ą +ostrzeżenia na żółto. Plan rozwoju jest w [README](../../README.pl.md#plan-rozwoju). @@ -1288,5 +1298,6 @@ Napisz nagłówek (`float path_len(float x, float y);` albo niech napisze go `cb **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ść: jest budowane i sprawdzane tutaj, ale -jeszcze nie na urządzeniu. +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. diff --git a/docs/pl/sciaga.md b/docs/pl/sciaga.md index 6123cf1..cb4e7e7 100644 --- a/docs/pl/sciaga.md +++ b/docs/pl/sciaga.md @@ -327,7 +327,7 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowo - 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ę. +- 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`, …). diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md index 563cf05..195374b 100644 --- a/editors/vscode/CHANGELOG.md +++ b/editors/vscode/CHANGELOG.md @@ -5,6 +5,7 @@ - Highlighting for code inside text (`"hp \(hp)"`), `test "…" { }` blocks, `const`, `"""` text, `1e9`. - Built-ins `min`, `max`, `abs`, `clamp`, `assert` and `atomic_…` highlighted and described on hover. - Format Document (the language server runs `finch fmt` on the open file). +- Warnings in yellow: a big struct copied on every call or in `for x in list`. ## 2.4.0 diff --git a/src/gen.fch b/src/gen.fch index a016f49..4768440 100644 --- a/src/gen.fch +++ b/src/gen.fch @@ -7,6 +7,7 @@ import ast import docs import cimport import platform +import "stdio.h" struct Var { str name @@ -166,6 +167,7 @@ struct Ref { struct Gen { []ast.Program progs + []str warned // file:line:col of the warnings given bool trace // each function keeps a frame { prev, name, file, line } in @finch_trace str frame // the current function's frame ("" in helpers) str frameLine // where its line goes @@ -337,6 +339,58 @@ fn fail(ptr[Gen] g, ast.Node n, str msg) { exit(1) } +// A warning: the program is still built. On stderr (finch ir's output stays the IR); an @W record for editors. +fn warn(ptr[Gen] g, ast.Node n, str msg) { + where := g.files[n.file] + ":" + str(n.line) + ":" + str(n.col) + if g.warned.contains(where) { + return // once per place (a generic function is built more than once) + } + g.warned.push(where) + if g.index { + g.idx.push("@W\t" + g.files[n.file] + "\t" + str(n.line) + "\t" + str(n.col) + "\t" + msg) + return + } + fputs(where + ": warning: " + msg + "\n", stderr) +} + +// About how many bytes of plain data (numbers, structs of them, C's [N]T) a value has, for the copy warnings +fn byteSize(ptr[Gen] g, ast.Type t) -> int { + k := t.kind + if k == "struct" { + sd := g.structs[findStruct(g, t.module, t.name)] + if sd.isC && sd.csize > 0 { + return sd.csize + } + n := 0 + for ft in sd.ftypes { + n += byteSize(g, ft) + } + return n + } + if k == "fixed" && t.elem.len > 0 { + return t.count * byteSize(g, t.elem[0]) + } + if k == "str" || k == "array" || k == "map" { + return 0 // only its { ptr, len, cap } is copied when it is passed: not counted + } + if k == "ptr" || k == "enum" || k == "int" || k == "float" { + return 8 + } + return bitsOf(t) / 8 + int(bitsOf(t) == 1) +} + +// a struct this big passed or looped over by value is copied byte by byte each time +fn bigCopy(ptr[Gen] g, ast.Type t) -> int { + if t.kind != "struct" { + return 0 + } + n := byteSize(g, t) + if n >= 256 { + return n + } + return 0 +} + fn emit(ptr[Gen] g, str line) { // with -g every call in a function that has debug info has to say where it is if g.dbgScope >= 0 && line.contains("call ") { @@ -2306,6 +2360,10 @@ fn define(ptr[Gen] g, int si) { } for k in 0..g.cur.params.len { pt := g.cur.params[k] + big := bigCopy(g, pt) + if big > 0 { + warn(g, f.kids[k], "'" + g.cur.pnames[k] + "' is a " + ast.show(pt) + " (" + str(big) + " bytes), copied on every call to " + f.name + "; take ptr[" + ast.show(pt) + "] to share it instead (callers can still pass the variable as it is)") + } params.push(llty(g, pt) + " %p" + str(k)) v := "%p" + str(k) if owning(g, pt) && !g.cur.borrow[k] { @@ -2714,6 +2772,14 @@ fn eachStmt(ptr[Gen] g, ast.Node s) { et = lt.elem[0] } copyElems := r.isPlace && owning(g, et) && mutates(g, rootVar(s.kids[0]), s.kids[1]) + big := bigCopy(g, et) + if big > 0 && s.text.len == 0 { + list := "list" + if s.kids[0].kind == "var" { + list = s.kids[0].name + } + warn(g, s, "each " + ast.show(et) + " (" + str(big) + " bytes) is copied into '" + s.name + "'; for i in 0.." + list + ".len with " + list + "[i] works on it in place") + } iv := alloca(g, "i64") store(g, "i64", "0", iv) cond := newLabel(g, "each.cond") diff --git a/src/lsp.fch b/src/lsp.fch index ea06340..0bc0fcc 100644 --- a/src/lsp.fch +++ b/src/lsp.fch @@ -83,6 +83,7 @@ struct Analysis { []str imports []str importKeys // the module each import is: btn -> ui/button Diag diag + []Diag warnings // shown in yellow; the program still builds } struct Server { @@ -324,6 +325,8 @@ fn parseOutput(str output) -> Analysis { a.structs[k].types.push(f[4]) } else if tag == "@E" && f.len >= 7 { a.enums.push(EnumRec(module: f[1], name: f[2], file: f[3], line: intOf(f[4]), col: intOf(f[5]), variants: f[6].split(","))) + } else if tag == "@W" && f.len >= 5 { + a.warnings.push(Diag(has: true, file: f[1], line: intOf(f[2]), col: intOf(f[3]), message: f[4])) } else if tag == "@I" && f.len >= 2 { a.imports.push(f[1]) if f.len >= 3 { @@ -399,6 +402,27 @@ fn Server.publish(str path, Analysis a) { list.push(d) now.push(f) } + for w in a.warnings { + if w.file == f { + d := json.new_object() + wl := w.line - 1 + if wl < 0 { + wl = 0 + } + wc := w.col - 1 + if wc < 0 { + wc = 0 + } + d.set("range", range(wl, wc, wc + 1)) + d.set("severity", num(2)) + d.set("source", json.new_text("finch")) + d.set("message", json.new_text(w.message)) + list.push(d) + if !now.contains(f) { + now.push(f) + } + } + } p := json.new_object() p.set("uri", json.new_text(pathToUri(f))) p.set("diagnostics", list) diff --git a/tests/cross.sh b/tests/cross.sh index c4ae87e..8c03d2b 100755 --- a/tests/cross.sh +++ b/tests/cross.sh @@ -34,7 +34,8 @@ for f in run/*.fch new/*.fch; do failed=$((failed + 1)); echo "FAIL (build) $f"; head -5 "$tmp/err"; continue fi got=$(run_exe "$exe" "$input") - if [ "$got" == "$(tr -d '\r' < "${f%.fch}.out")" ]; then + # (warnings come from the build, which is not part of what the program prints) + if [ "$got" == "$(tr -d '\r' < "${f%.fch}.out" | grep -v ': warning: ')" ]; then pass=$((pass + 1)) else failed=$((failed + 1)); echo "FAIL $f"; diff <(echo "$got") "${f%.fch}.out" | head -10 diff --git a/tests/lsp_test.py b/tests/lsp_test.py index ab41ec7..ef579a0 100755 --- a/tests/lsp_test.py +++ b/tests/lsp_test.py @@ -193,6 +193,15 @@ c.notify("textDocument/didChange", {"textDocument": {"uri": uri2, "version": 4}, diags = c.diagnostics(uri2) check("error: unhandled failure", diags and "can fail" in diags[0]["message"], diags) +# a big struct copied on every call: a warning (yellow), not an error +src4 = "struct V {\n" + "".join(" float f%d\n" % i for i in range(40)) + "}\n\nfn len2(V v) -> float {\n return v.f0\n}\n\nfn main() {\n print(len2(V()))\n}\n" +path4 = os.path.join(d, "big.fch") +open(path4, "w").write(src4) +uri4 = pathlib.Path(path4).resolve().as_uri() +c.notify("textDocument/didOpen", {"textDocument": {"uri": uri4, "languageId": "finch", "version": 1, "text": src4}}) +w4 = c.diagnostics(uri4) +check("warning for a big copy", w4 and len(w4) == 1 and w4[0]["severity"] == 2 and "copied on every call" in w4[0]["message"] and w4[0]["range"]["start"]["line"] == 43, w4) + # a module imported under another name: completion after its name src3 = "import path as pt\n\nfn main() {\n print(pt.base(\"a/b\"))\n}\n" path3 = os.path.join(d, "alias.fch") diff --git a/tests/new/big_copies.fch b/tests/new/big_copies.fch new file mode 100644 index 0000000..f4a3eb0 --- /dev/null +++ b/tests/new/big_copies.fch @@ -0,0 +1,52 @@ +// a struct of 256 bytes or more copied without the code saying so: a warning (the program still builds) +struct M4 { + float a0 + float a1 + float a2 + float a3 + float b0 + float b1 + float b2 + float b3 + float c0 + float c1 + float c2 + float c3 + float d0 + float d1 + float d2 + float d3 +} + +struct Body { + M4 world + M4 local + int id +} + +fn weight(Body b) -> float { // warned: 264 bytes copied on every call + return b.world.a0 + float(b.id) +} + +fn nudge(ptr[Body] b) { // shared: no warning + b.world.a0 += 1.0 +} + +fn small(M4 m) -> float { // 128 bytes: no warning + return m.a0 +} + +fn main() { + b := Body(id: 2) + nudge(b) + print(weight(b), small(b.world)) + bodies := [b, b] + total := 0.0 + for x in bodies { // warned: each element copied into x + total += x.world.a0 + } + for i in 0..bodies.len { // in place: no warning + total += bodies[i].world.a0 + } + print(total) +} diff --git a/tests/new/big_copies.out b/tests/new/big_copies.out new file mode 100644 index 0000000..78da95f --- /dev/null +++ b/tests/new/big_copies.out @@ -0,0 +1,4 @@ +new/big_copies.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) +new/big_copies.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 +3 1 +4