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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
This commit is contained in:
Nexoniarz 2026-10-29 09:52:04 +01:00
commit 3ba2dcd5d6
16 changed files with 152 additions and 18 deletions

View file

@ -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) | | `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) | | `#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).
--- ---

View file

@ -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 `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. 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 `_<os>` 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 ## 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. 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 ## 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)` ### Binary operands: `unify(l, r)`
A literal side adapts to the other; otherwise the side that widens grows; `i32` + `u32` is an error; A literal side adapts to the other (also a float literal next to an `f32`: `x * 2.0` stays `f32`); otherwise the side
int + float → float; f32 + f64 → f64. There is no C integer promotion: `u8 + u8` stays `u8`. 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()` ### Explicit conversions: `convert()`

View file

@ -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 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. `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 `_<system>`, 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 ## 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); - 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). - `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ść. 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)` ### Argumenty operatorów: `unify(l, r)`
Strona-literał dopasowuje się do drugiej; w przeciwnym razie rośnie strona, która się rozszerza; Strona-literał dopasowuje się do drugiej (także literał float obok `f32`: `x * 2.0` zostaje `f32`); w przeciwnym razie
`i32` + `u32` to błąd; całkowita + zmiennoprzecinkowa → zmiennoprzecinkowa; f32 + f64 → f64. rośnie strona, która się rozszerza; `i32` + `u32` to błąd; całkowita + zmiennoprzecinkowa → zmiennoprzecinkowa;
Nie ma promocji całkowitych z C: `u8 + u8` zostaje `u8`. 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()` ### Konwersje jawne: `convert()`

View file

@ -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ść) | | `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 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).
--- ---

View file

@ -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("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("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("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("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("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("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("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("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("char", "char(int n) -> char", "The character with this code."),
@ -86,7 +90,7 @@ fn mapMethods() -> []Doc {
fn keywords() -> []str { fn keywords() -> []str {
return ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false", "null", "import", 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 { fn typeNames() -> []str {

View file

@ -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 // what is known about every function, struct and enum, for completion and the outline
fn indexAll(ptr[Gen] g, []str mainImports) { fn indexAll(ptr[Gen] g, []str mainImports) {
for im in mainImports { for i in 0..mainImports.len {
g.idx.push("@I\t" + im) g.idx.push("@I\t" + mainImports[i] + "\t" + g.progs[0].modules[i]) // the name used here, the module
} }
for sg in g.sigs { for sg in g.sigs {
f := g.progs[sg.prog].fns[sg.index] f := g.progs[sg.prog].fns[sg.index]
@ -5319,8 +5319,8 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
if ei >= 0 { if ei >= 0 {
return enumFrom(g, c, ei) return enumFrom(g, c, ei)
} }
if g.ci.fns.has(n) { if g.ci.fns.has(n) && !["min", "max", "abs", "clamp"].contains(n) {
return callC(g, c, g.ci.fns[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) si = findStruct(g, "C", n)
if si >= 0 { 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 // min(a, b, ...), max(a, b, ...), abs(x), clamp(x, lo, hi) for any numbers
fn mathCall(ptr[Gen] g, ast.Node c) -> Val { fn mathCall(ptr[Gen] g, ast.Node c) -> Val {
n := c.name 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 { for nm in c.names {
if nm.len > 0 { if nm.len > 0 {
fail(g, c, "named arguments (" + nm + ": ...) are only for creating structs") fail(g, c, "named arguments (" + nm + ": ...) are only for creating structs")

View file

@ -80,6 +80,7 @@ struct Analysis {
[]StructRec structs []StructRec structs
[]EnumRec enums []EnumRec enums
[]str imports []str imports
[]str importKeys // the module each import is: btn -> ui/button
Diag diag 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(","))) 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 { } else if tag == "@I" && f.len >= 2 {
a.imports.push(f[1]) 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 { } else if !a.diag.has {
a.diag = parseDiag(line) a.diag = parseDiag(line)
@ -348,6 +354,7 @@ fn Server.analyse(str path) {
a.vars = g.vars a.vars = g.vars
if a.imports.len == 0 { if a.imports.len == 0 {
a.imports = g.imports a.imports = g.imports
a.importKeys = g.importKeys
} }
} }
self.results[path] = a 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 // a module: its functions, structs and enums
if a.imports.contains(owner) { if a.imports.contains(owner) {
owner = a.importKeys[a.imports.find(owner)]
for f in a.fns { for f in a.fns {
if f.module == owner && f.recv.len == 0 { if f.module == owner && f.recv.len == 0 {
items.push(item(f.name, 3, f.signature, "")) items.push(item(f.name, 3, f.signature, ""))

View file

@ -47,6 +47,7 @@ struct Loader {
str os // "windows", "macos" or "linux": a module may have a version for each (time_windows.fch) 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 std // the standard library's folder, or ""
[]str paths // FINCH_PATH: more folders to look for modules in []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 overlayPaths // --overlay path=file: read `path` from `file` (the text an editor has, not saved yet)
[]str overlayFiles []str overlayFiles
} }
@ -89,6 +90,27 @@ fn findModule(ptr[Loader] l, str from, str name) -> str {
return local 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) { fn load(ptr[Loader] l, str path, str module) {
if !file_exists(path) && !l.overlayPaths.contains(path) { if !file_exists(path) && !l.overlayPaths.contains(path) {
if module.len > 0 { 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 := parser.parse(lexer.lex(src, path), file, path)
prog.module = module prog.module = module
prog.path = path 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 modules := prog.modules
l.progs.push(prog) l.progs.push(prog)
for name in modules { for i in 0..modules.len {
if !l.loaded.contains(name) { if !l.loaded.contains(modules[i]) {
l.loaded.push(name) l.loaded.push(modules[i])
load(l, findModule(l, path, name), name) load(l, files[i], modules[i])
} }
} }
} }

View file

@ -186,6 +186,19 @@ c.notify("textDocument/didChange", {"textDocument": {"uri": uri2, "version": 4},
diags = c.diagnostics(uri2) diags = c.diagnostics(uri2)
check("error: unhandled failure", diags and "can fail" in diags[0]["message"], diags) 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.request("shutdown", None)
c.notify("exit", None) c.notify("exit", None)
c.p.wait(timeout=5) c.p.wait(timeout=5)

View file

@ -1,5 +1,6 @@
// C unions (SDL_Event-style) and #defines that are expressions, strings or casts // C unions (SDL_Event-style) and #defines that are expressions, strings or casts
import "cunion.h" import "cunion.h"
import "stdlib.h"
link "cunion.c" link "cunion.c"
fn main() { fn main() {
@ -22,4 +23,5 @@ fn main() {
Tagged t Tagged t
t.as.f = 0.5 t.as.f = 0.5
print(t.as.f) print(t.as.f)
print(abs(-2.5), abs(-3), min(EV_KEY, 7)) // Finch's abs, not C's int-only one
} }

View file

@ -4,3 +4,4 @@ motion 1.5 -2
true 32 true 32
32 2 32 2
0.5 0.5
2.5 3 7

View file

@ -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())
}

View file

@ -0,0 +1 @@
menu 30 ui/util util

11
tests/new/ui/menu.fch Normal file
View file

@ -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()
}

4
tests/new/ui/util.fch Normal file
View file

@ -0,0 +1,4 @@
// for tests/new/modules_by_file.fch: a util.fch in a folder
fn who() -> str {
return "ui/util"
}

4
tests/new/util.fch Normal file
View file

@ -0,0 +1,4 @@
// for tests/new/modules_by_file.fch: the top-level util.fch
fn who() -> str {
return "util"
}