From 3ba2dcd5d60d54a51991e48a40eda6474d3b3086 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Thu, 29 Oct 2026 09:52:04 +0100 Subject: [PATCH] Modules are known by their file: import button from ui/menu.fch and import ui/button are one module, two util.fch are two; min/max/abs/clamp win over C's int-only abs; completion after an import's other name; hover docs; engineers' guides Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL --- docs/en/cheatsheet.md | 2 +- docs/en/for-engineers.md | 25 ++++++++++++++++++++-- docs/pl/dla-inzynierow.md | 27 +++++++++++++++++++++--- docs/pl/sciaga.md | 2 +- src/docs.fch | 10 ++++++--- src/gen.fch | 11 ++++++---- src/lsp.fch | 8 +++++++ src/main.fch | 39 +++++++++++++++++++++++++++++++---- tests/lsp_test.py | 13 ++++++++++++ tests/new/c_unions_macros.fch | 2 ++ tests/new/c_unions_macros.out | 1 + tests/new/modules_by_file.fch | 10 +++++++++ tests/new/modules_by_file.out | 1 + tests/new/ui/menu.fch | 11 ++++++++++ tests/new/ui/util.fch | 4 ++++ tests/new/util.fch | 4 ++++ 16 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 tests/new/modules_by_file.fch create mode 100644 tests/new/modules_by_file.out create mode 100644 tests/new/ui/menu.fch create mode 100644 tests/new/ui/util.fch create mode 100644 tests/new/util.fch diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md index 85ad526..b747bfa 100644 --- a/docs/en/cheatsheet.md +++ b/docs/en/cheatsheet.md @@ -343,7 +343,7 @@ import ui/button as btn // another name: btn.draw() | `union U` | `U`: `U u`, `u.field`, `U(field: v)`, by pointer to C (not by value) | | `#define N 4`, `"text"`, `(1u << 3)`, `((Uint64)2)` | constants (not function-like macros) | -A C function's `char *` result is a `str` already; structs by value work on every target (System V, Windows x64, ARM64). +A C function's `char *` result is a `str` already; structs by value work with the System V, Windows x64 and ARM64 Linux calling conventions (tested in emulators; macOS by CI). --- diff --git a/docs/en/for-engineers.md b/docs/en/for-engineers.md index a1b08b8..53a92a5 100644 --- a/docs/en/for-engineers.md +++ b/docs/en/for-engineers.md @@ -98,6 +98,18 @@ its `endLine` (used by the language server). The old `fn name(...) type` order i `name.fch` there, the folders of `FINCH_PATH`, then the standard library `std/`. Each module is parsed once; its names are used as `module.name`. `import "x.h"` goes to the C importer instead. +A module is identified by its **file**: the key is its path from the main file's folder (or from a `FINCH_PATH` folder or +`std/`) without `.fch` and without an `_` suffix, e.g. `ui/button`. `Program.imports` holds the name each file uses +(`button`, or `btn` for `import ui/button as btn`) and `Program.modules` the key; `modFor()` maps one to the other. +Symbols use the key with `/` turned into `$` (`@finch.ui$button.make`). + +**Constants.** `const NAME = expr` is a top-level node; `constValue()` evaluates the expression at each use, in the +defining module with no local scopes, so it folds to a literal (integer literal operators are folded in `arith`). +`checkConst()` allows only literals, names, operators, members and numeric conversions; `constBusy` catches cycles. + +**Lexer extras.** `1e9` / `2.5e-3` are float tokens (the text is normalised to have a `.` for LLVM); `"""` strings are +dedented by the closing line's indentation in `longString()`. + --- ## 4. Code generation @@ -138,6 +150,14 @@ LLVM IR does not do the C calling convention for you, so `gen.fch` reproduces it The same code is what lets the compiler call libclang, whose API passes `CXCursor`, `CXType` and `CXString` by value. +**Macros.** A `#define` that is one number or a string literal is read from its tokens. One whose tokens could be a +constant expression (no braces, semicolons or statement keywords) is evaluated by clang: a second parse of the header +adds `static const __auto_type __finch_m_NAME = (NAME);` for each, and `clang_Cursor_Evaluate` gives an int, a double +or a string. Those that are not constants only produce errors in that second parse and are dropped. + +**Unions** are a `CStruct` with `isUnion`: the LLVM type is `{ [n x iA] }` (size and alignment from clang), every field has +offset 0, and a field is read through memory. They can't be passed by value to C (`unsupported` says why). + --- ## 6. Driver, linking and targets @@ -201,8 +221,9 @@ from the slot they flow into (`exprWant()`), so `[]f32 xs = [1, 2.5]` works. ### Binary operands: `unify(l, r)` -A literal side adapts to the other; otherwise the side that widens grows; `i32` + `u32` is an error; -int + float → float; f32 + f64 → f64. There is no C integer promotion: `u8 + u8` stays `u8`. +A literal side adapts to the other (also a float literal next to an `f32`: `x * 2.0` stays `f32`); otherwise the side +that widens grows; `i32` + `u32` is an error; int + float → float; f32 + f64 → f64. There is no C integer promotion: +`u8 + u8` stays `u8`. Two integer literals fold to a literal, so `FLAG_A | FLAG_B` still fits a `u64` or `u32`. ### Explicit conversions: `convert()` diff --git a/docs/pl/dla-inzynierow.md b/docs/pl/dla-inzynierow.md index 6378e6d..f56e647 100644 --- a/docs/pl/dla-inzynierow.md +++ b/docs/pl/dla-inzynierow.md @@ -97,6 +97,18 @@ kolejność `fn nazwa(...) typ` to błąd składni z podpowiedzią. 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ł 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`). + +**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. + +**Dodatki leksera.** `1e9` / `2.5e-3` to tokeny float (tekst dostaje `.` dla LLVM); teksty `"""` tracą wcięcie +linii zamykającej w `longString()`. + --- ## 4. Generowanie kodu @@ -130,6 +142,14 @@ LLVM IR nie realizuje za ciebie konwencji wywołań C, więc `gen.fch` ją odtwa - 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). + Ten sam kod pozwala kompilatorowi wołać libclang, którego API przekazuje `CXCursor`, `CXType` i `CXString` przez wartość. --- @@ -194,9 +214,10 @@ Literały tablic biorą typ elementu z miejsca, do którego trafiają (`exprWant ### Argumenty operatorów: `unify(l, r)` -Strona-literał dopasowuje się do drugiej; 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`. +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`. ### Konwersje jawne: `convert()` diff --git a/docs/pl/sciaga.md b/docs/pl/sciaga.md index ff27091..c77f1c2 100644 --- a/docs/pl/sciaga.md +++ b/docs/pl/sciaga.md @@ -342,7 +342,7 @@ import ui/przycisk as p // inna nazwa: p.rysuj() | `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) | -Wynik `char *` z funkcji C to od razu `str`; struktury przez wartość działają na każdym systemie (System V, Windows x64, ARM64). +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). --- diff --git a/src/docs.fch b/src/docs.fch index 2c75a0e..5abd5ab 100644 --- a/src/docs.fch +++ b/src/docs.fch @@ -21,11 +21,15 @@ fn builtins() -> []Doc { doc("delete_file", "delete_file(str path) -> bool", "Deletes the file. true if it worked; with or / try, the reason it didn't."), doc("error", "error(str message)", "The failure a fallible function returns: return error(\"...\"). The caller handles it with or / try."), doc("shell", "shell(str command) -> int", "Runs a command in the system's shell and gives back its exit code."), - doc("exit", "exit(int code)", "Ends the program right away with this exit code."), + doc("exit", "exit(int code)", "Ends the program right away with this exit code (it also ends an or { } block)."), + doc("min", "min(a, b, ...) -> number", "The smallest of the numbers, in the type they all fit."), + 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("addr", "addr(x) -> ptr[T]", "A pointer to a variable, field or element. addr(function) gives C a callback."), doc("new", "new(value) -> ptr[T]", "Puts a copy of the value on the heap. Give it back with free(...)."), doc("free", "free(ptr p)", "Gives back memory from new(...) (or from C), freeing what the value owns."), - doc("str", "str(x) -> str", "Turns a number, bool, char, []u8 or C char pointer into text."), + doc("str", "str(x) -> str", "Turns a number, bool, char, []u8 or C char pointer into text. str(x, 2): a number with 2 digits after the point."), doc("int", "int(x) -> int", "Converts to a whole number: cuts off fractions, parses text (stops the program if it isn't a number, unless handled: int(s) or 0)."), 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."), @@ -86,7 +90,7 @@ fn mapMethods() -> []Doc { 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"] + "link", "struct", "enum", "match", "export", "defer", "or", "try", "self", "const"] } fn typeNames() -> []str { diff --git a/src/gen.fch b/src/gen.fch index b00a825..6df57b0 100644 --- a/src/gen.fch +++ b/src/gen.fch @@ -279,8 +279,8 @@ fn noteCall(ptr[Gen] g, ast.Node c, FnSig sig) { // what is known about every function, struct and enum, for completion and the outline fn indexAll(ptr[Gen] g, []str mainImports) { - for im in mainImports { - g.idx.push("@I\t" + im) + for i in 0..mainImports.len { + g.idx.push("@I\t" + mainImports[i] + "\t" + g.progs[0].modules[i]) // the name used here, the module } for sg in g.sigs { f := g.progs[sg.prog].fns[sg.index] @@ -5319,8 +5319,8 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { if ei >= 0 { return enumFrom(g, c, ei) } - if g.ci.fns.has(n) { - return callC(g, c, g.ci.fns[n]) + if g.ci.fns.has(n) && !["min", "max", "abs", "clamp"].contains(n) { + return callC(g, c, g.ci.fns[n]) // (C's abs only takes an int; Finch's takes any number) } si = findStruct(g, "C", n) if si >= 0 { @@ -5339,6 +5339,9 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { // min(a, b, ...), max(a, b, ...), abs(x), clamp(x, lo, hi) for any numbers fn mathCall(ptr[Gen] g, ast.Node c) -> Val { n := c.name + if g.index { + noteRef(g, c, c.col, n.len, docs.hover(docs.find(docs.builtins(), n)), "", 0, 0) + } for nm in c.names { if nm.len > 0 { fail(g, c, "named arguments (" + nm + ": ...) are only for creating structs") diff --git a/src/lsp.fch b/src/lsp.fch index 6cb075f..5b5be23 100644 --- a/src/lsp.fch +++ b/src/lsp.fch @@ -80,6 +80,7 @@ struct Analysis { []StructRec structs []EnumRec enums []str imports + []str importKeys // the module each import is: btn -> ui/button Diag diag } @@ -315,6 +316,11 @@ fn parseOutput(str output) -> Analysis { 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 == "@I" && f.len >= 2 { a.imports.push(f[1]) + if f.len >= 3 { + a.importKeys.push(f[2]) + } else { + a.importKeys.push(f[1]) + } } } else if !a.diag.has { a.diag = parseDiag(line) @@ -348,6 +354,7 @@ fn Server.analyse(str path) { a.vars = g.vars if a.imports.len == 0 { a.imports = g.imports + a.importKeys = g.importKeys } } self.results[path] = a @@ -537,6 +544,7 @@ fn completionAt(Analysis a, str text, str path, int line, int col) -> json.Json } // a module: its functions, structs and enums if a.imports.contains(owner) { + owner = a.importKeys[a.imports.find(owner)] for f in a.fns { if f.module == owner && f.recv.len == 0 { items.push(item(f.name, 3, f.signature, "")) diff --git a/src/main.fch b/src/main.fch index 410f4bd..13526cc 100644 --- a/src/main.fch +++ b/src/main.fch @@ -47,6 +47,7 @@ struct Loader { str os // "windows", "macos" or "linux": a module may have a version for each (time_windows.fch) str std // the standard library's folder, or "" []str paths // FINCH_PATH: more folders to look for modules in + str root // the main file's folder: modules are named by their path from here (ui/button) []str overlayPaths // --overlay path=file: read `path` from `file` (the text an editor has, not saved yet) []str overlayFiles } @@ -89,6 +90,27 @@ fn findModule(ptr[Loader] l, str from, str name) -> str { return local } +// A module is named by its file, so `import button` in ui/menu.fch and `import ui/button` in main.fch +// are the same module, and two different util.fch files are two modules. +fn moduleKey(ptr[Loader] l, str file, str name) -> str { + []str roots + roots.push(l.root) + for d in l.paths { + roots.push(d) + } + roots.push(l.std) + for r in roots { + if r.len > 0 && file.starts_with(r + "/") && file.ends_with(".fch") { + key := file.sub(r.len + 1, file.len - 4) + if key.ends_with("_" + l.os) { + key = key.sub(0, key.len - l.os.len - 1) // time_windows.fch is the module time + } + return key + } + } + return name +} + fn load(ptr[Loader] l, str path, str module) { if !file_exists(path) && !l.overlayPaths.contains(path) { if module.len > 0 { @@ -104,12 +126,21 @@ fn load(ptr[Loader] l, str path, str module) { prog := parser.parse(lexer.lex(src, path), file, path) prog.module = module prog.path = path + if module.len == 0 { + l.root = dirOf(path) + } + []str files + for i in 0..prog.modules.len { + f := findModule(l, path, prog.modules[i]) + files.push(f) + prog.modules[i] = moduleKey(l, f, prog.modules[i]) + } modules := prog.modules l.progs.push(prog) - for name in modules { - if !l.loaded.contains(name) { - l.loaded.push(name) - load(l, findModule(l, path, name), name) + for i in 0..modules.len { + if !l.loaded.contains(modules[i]) { + l.loaded.push(modules[i]) + load(l, files[i], modules[i]) } } } diff --git a/tests/lsp_test.py b/tests/lsp_test.py index 25ead42..f9c74b4 100755 --- a/tests/lsp_test.py +++ b/tests/lsp_test.py @@ -186,6 +186,19 @@ 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 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") +open(path3, "w").write(src3) +uri3 = pathlib.Path(path3).resolve().as_uri() +c.notify("textDocument/didOpen", {"textDocument": {"uri": uri3, "languageId": "finch", "version": 1, "text": src3}}) +check("import as: compiles", c.diagnostics(uri3) == []) +e3 = src3.replace(" print(", " pt.\n print(") +c.notify("textDocument/didChange", {"textDocument": {"uri": uri3, "version": 2}, "contentChanges": [{"text": e3}]}) +comp = c.request("textDocument/completion", {"textDocument": {"uri": uri3}, "position": {"line": 3, "character": 7}}) +labels = [i["label"] for i in comp["items"]] +check("completion: a module under another name", "base" in labels and "join" in labels, labels) + c.request("shutdown", None) c.notify("exit", None) c.p.wait(timeout=5) diff --git a/tests/new/c_unions_macros.fch b/tests/new/c_unions_macros.fch index be5efc5..4126622 100644 --- a/tests/new/c_unions_macros.fch +++ b/tests/new/c_unions_macros.fch @@ -1,5 +1,6 @@ // C unions (SDL_Event-style) and #defines that are expressions, strings or casts import "cunion.h" +import "stdlib.h" link "cunion.c" fn main() { @@ -22,4 +23,5 @@ fn main() { Tagged t t.as.f = 0.5 print(t.as.f) + print(abs(-2.5), abs(-3), min(EV_KEY, 7)) // Finch's abs, not C's int-only one } diff --git a/tests/new/c_unions_macros.out b/tests/new/c_unions_macros.out index 2fb9461..3e6d42f 100644 --- a/tests/new/c_unions_macros.out +++ b/tests/new/c_unions_macros.out @@ -4,3 +4,4 @@ motion 1.5 -2 true 32 32 2 0.5 +2.5 3 7 diff --git a/tests/new/modules_by_file.fch b/tests/new/modules_by_file.fch new file mode 100644 index 0000000..4d85cef --- /dev/null +++ b/tests/new/modules_by_file.fch @@ -0,0 +1,10 @@ +// a module is its file: ui/menu's "import button" is this file's "import ui/button", +// and ui/util.fch and util.fch are two different modules +import ui/button +import ui/menu +import util + +fn main() { + button.Button b = menu.first() + print(b.label, b.w, menu.whoami(), util.who()) +} diff --git a/tests/new/modules_by_file.out b/tests/new/modules_by_file.out new file mode 100644 index 0000000..e39e691 --- /dev/null +++ b/tests/new/modules_by_file.out @@ -0,0 +1 @@ +menu 30 ui/util util diff --git a/tests/new/ui/menu.fch b/tests/new/ui/menu.fch new file mode 100644 index 0000000..36005d4 --- /dev/null +++ b/tests/new/ui/menu.fch @@ -0,0 +1,11 @@ +// for tests/new/modules_by_file.fch: its sibling, imported without the folder +import button +import util + +fn first() -> button.Button { + return button.make("menu") +} + +fn whoami() -> str { + return util.who() +} diff --git a/tests/new/ui/util.fch b/tests/new/ui/util.fch new file mode 100644 index 0000000..c8dba43 --- /dev/null +++ b/tests/new/ui/util.fch @@ -0,0 +1,4 @@ +// for tests/new/modules_by_file.fch: a util.fch in a folder +fn who() -> str { + return "ui/util" +} diff --git a/tests/new/util.fch b/tests/new/util.fch new file mode 100644 index 0000000..8b03465 --- /dev/null +++ b/tests/new/util.fch @@ -0,0 +1,4 @@ +// for tests/new/modules_by_file.fch: the top-level util.fch +fn who() -> str { + return "util" +}