const, if as a value, """ text over several lines, min/max/abs/clamp, import a/b and import x as y, the fs module

- const NAME = value (numbers, text, other constants; typed: const f32 G = 9.8), also module.NAME
- x := if c { a } else { b }
- """ ... """: the closing quotes' indentation goes from every line
- min, max, abs, clamp for any numbers, unless your own function has the name
- import ui/button (a module in a folder), import path as p
- fs: read_bytes, write_bytes, exists, is_dir, mkdir (with parents), list, remove_dir, data_dir(app)

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:37:41 +01:00
commit f2d6480749
21 changed files with 688 additions and 17 deletions

View file

@ -114,11 +114,13 @@ struct Node {
struct Program {
str module
str path
[]str imports // Finch modules
[]str imports // Finch modules, by the name this file uses: import ui/button -> "button"
[]str modules // which module each is: "ui/button" (import path as p: "path")
[]str cimports // C headers: import "stdio.h"
[]str cwhere // where each was written, for error messages
[]str links // link "glfw", link "mine.c"
[]Node structs
[]Node enums
[]Node fns
[]Node consts // const NAME = value
}

File diff suppressed because one or more lines are too long

View file

@ -163,6 +163,7 @@ struct Ref {
struct Gen {
[]ast.Program progs
[]str constBusy // the constants being worked out: a loop between them is an error
[]str files
[]str head
[]str out
@ -520,7 +521,17 @@ fn progOf(ptr[Gen] g, str module) -> int {
}
fn imports(ptr[Gen] g, str module) -> bool {
return g.progs[progOf(g, g.module)].imports.contains(module)
return modFor(g, module).len > 0
}
// the module a name stands for in this file: button -> "ui/button", p -> "path"; "" if none
fn modFor(ptr[Gen] g, str name) -> str {
pr := g.progs[progOf(g, g.module)]
k := pr.imports.find(name)
if k < 0 {
return ""
}
return pr.modules[k]
}
fn resolve(ptr[Gen] g, ast.Type t, ast.Node at) -> ast.Type {
@ -567,7 +578,7 @@ fn resolve(ptr[Gen] g, ast.Type t, ast.Node at) -> ast.Type {
if !imports(g, t.module) {
fail(g, at, "'" + t.module + "' is not imported here (add: import " + t.module + ")")
}
gm = t.module
gm = modFor(g, t.module)
}
return instStruct(g, gm, t.name, args, at)
}
@ -582,7 +593,7 @@ fn resolve(ptr[Gen] g, ast.Type t, ast.Node at) -> ast.Type {
if !imports(g, t.module) {
fail(g, at, "'" + t.module + "' is not imported here (add: import " + t.module + ")")
}
module = t.module
module = modFor(g, t.module)
}
if findEnum(g, module, t.name) >= 0 {
return ast.Type(kind: "enum", name: t.name, module: module)
@ -606,7 +617,7 @@ fn modName(str m) -> str {
if m.len == 0 {
return "main"
}
return m
return m.replace("/", "$") // ui/button -> ui$button in symbol names
}
fn llty(ptr[Gen] g, ast.Type t) -> str {
@ -2423,6 +2434,9 @@ fn assignStmt(ptr[Gen] g, ast.Node s) {
if root.len > 0 {
id := lookup(g, root)
if id < 0 && target.kind == "var" {
if findConst(g, g.module, root) >= 0 {
fail(g, s, "'" + root + "' is a constant; it can't be changed")
}
fail(g, s, "there is no variable named '" + root + "' (create it with " + root + " := ...)")
}
if id >= 0 && varAt(g, id).readonly {
@ -3266,6 +3280,38 @@ fn matchGen(ptr[Gen] g, ast.Node e, bool asValue) -> Val {
return Val(ty: T("void"))
}
// x := if c { a } else { b }: both branches give the same type
fn ifValue(ptr[Gen] g, ast.Node e, ast.Type want) -> Val {
c := coerce(g, expr(g, e.kids[0]), T("bool"), e.kids[0], "the condition")
yes := newLabel(g, "ifv.yes")
no := newLabel(g, "ifv.no")
end := newLabel(g, "ifv.end")
condBr(g, c.v, yes, no)
label(g, yes)
a := exprWant(g, e.kids[1], want)
if a.ty.kind == "void" || a.ty.kind == "null" {
fail(g, e.kids[1], "this branch has to give a value")
}
if want.kind != "void" {
a = coerce(g, a, want, e.kids[1], "this branch")
}
t := a.ty
av := own(g, a)
fromYes := newLabel(g, "ifv.from")
br(g, fromYes)
label(g, fromYes)
br(g, end)
label(g, no)
b := coerce(g, exprWant(g, e.kids[2], t), t, e.kids[2], "this branch (both branches of the if have to give the same type)")
bv := own(g, b)
fromNo := newLabel(g, "ifv.from")
br(g, fromNo)
label(g, fromNo)
br(g, end)
label(g, end)
return Val(v: op(g, "phi " + llty(g, t) + " " + phiList([av, bv], [fromYes, fromNo])), ty: t, fresh: owning(g, t))
}
fn phiList([]str vals, []str labels) -> str {
[]str parts
for i in 0..vals.len {
@ -3385,6 +3431,9 @@ fn exprWant(ptr[Gen] g, ast.Node e, ast.Type want) -> Val {
if e.kind == "map" {
return mapLit(g, e, want)
}
if e.kind == "ifvalue" {
return ifValue(g, e, want) // f32 x = if c { 1.0 } else { 0.5 }
}
return expr(g, e)
}
@ -3433,6 +3482,9 @@ fn expr(ptr[Gen] g, ast.Node e) -> Val {
if k == "match" {
return matchGen(g, e, true)
}
if k == "ifvalue" {
return ifValue(g, e, T("void"))
}
if k == "try" {
return tryExpr(g, e)
}
@ -3840,6 +3892,12 @@ fn ref(ptr[Gen] g, ast.Node e, bool write) -> Ref {
}
return Ref(isPlace: true, pl: Place(addr: "@" + e.name, ty: gt))
}
if id < 0 && findConst(g, g.module, e.name) >= 0 {
if write {
fail(g, e, "'" + e.name + "' is a constant; it can't be changed")
}
return Ref(isPlace: false, val: constValue(g, g.module, e.name, e))
}
if id < 0 {
if imports(g, e.name) {
fail(g, e, "'" + e.name + "' is a module; use something from it, like " + e.name + ".name(...)")
@ -3878,13 +3936,76 @@ fn llFloat(str text) -> str {
return s
}
// ---------- constants ----------
fn findConst(ptr[Gen] g, str module, str name) -> int {
pi := progOf(g, module)
if pi < 0 {
return -1
}
for i in 0..g.progs[pi].consts.len {
if g.progs[pi].consts[i].name == name {
return i
}
}
return -1
}
// A constant is worked out where it is used, from the code it was written with: numbers,
// text, true/false, chars, other constants, enum variants and operators between them.
fn constValue(ptr[Gen] g, str module, str name, ast.Node at) -> Val {
pi := progOf(g, module)
c := g.progs[pi].consts[findConst(g, module, name)]
key := module + "." + name
if g.constBusy.contains(key) {
fail(g, at, "the constant '" + name + "' is worked out from itself")
}
checkConst(g, c.kids[0])
g.constBusy.push(key)
savedScopes := g.scopes
savedModule := g.module
[]Scope none
g.scopes = none
pushScope(g)
g.module = module
v := Val()
if c.flag {
t := resolve(g, c.ty, c)
v = coerce(g, exprWant(g, c.kids[0], t), t, c.kids[0], "the constant '" + name + "'")
} else {
v = expr(g, c.kids[0])
}
g.module = savedModule
g.scopes = savedScopes
g.constBusy.pop()
noteRef(g, at, at.col, name.len, "```finch\nconst " + ast.show(v.ty) + " " + name + "\n```", g.files[c.file], c.line, c.col)
return v
}
fn checkConst(ptr[Gen] g, ast.Node e) {
k := e.kind
if ["int", "float", "char", "str", "bool", "var", "unary", "binary", "member"].contains(k) {
for kid in e.kids {
checkConst(g, kid)
}
return
}
if k == "call" && numType(e.name).len > 0 {
for kid in e.kids {
checkConst(g, kid)
}
return // f32(0.5), u8(255)
}
fail(g, e, "a constant is made of numbers, text, true/false, chars and other constants (not calls or variables)")
}
// Which enum does `m.kids[0]` name, if it names one? Color, or shapes.Color. -1 if neither.
fn enumNamed(ptr[Gen] g, ast.Node n) -> int {
if n.kind == "var" && lookup(g, n.name) < 0 {
return findEnum(g, g.module, n.name)
}
if n.kind == "member" && n.kids[0].kind == "var" && lookup(g, n.kids[0].name) < 0 && imports(g, n.kids[0].name) {
return findEnum(g, n.kids[0].name, n.name)
return findEnum(g, modFor(g, n.kids[0].name), n.name)
}
return -1
}
@ -3895,7 +4016,7 @@ fn sumNamed(ptr[Gen] g, ast.Node n) -> int {
return findSum(g, g.module, n.name)
}
if n.kind == "member" && n.kids[0].kind == "var" && lookup(g, n.kids[0].name) < 0 && imports(g, n.kids[0].name) {
return findSum(g, n.kids[0].name, n.name)
return findSum(g, modFor(g, n.kids[0].name), n.name)
}
return -1
}
@ -3997,6 +4118,13 @@ fn member(ptr[Gen] g, ast.Node m, bool write) -> Ref {
}
return Ref(isPlace: false, val: enumValue(g, m, ei))
}
mod := m.kids[0]
if mod.kind == "var" && lookup(g, mod.name) < 0 && imports(g, mod.name) && findConst(g, modFor(g, mod.name), m.name) >= 0 {
if write {
fail(g, m, "'" + mod.name + "." + m.name + "' is a constant; it can't be changed")
}
return Ref(isPlace: false, val: constValue(g, modFor(g, mod.name), m.name, m)) // shapes.SIDES
}
obj := ref(g, m.kids[0], write)
t := obj.val.ty
if obj.isPlace {
@ -5198,6 +5326,9 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
if si >= 0 {
return construct(g, si, c.kids, c.names, c)
}
if ["min", "max", "abs", "clamp"].contains(n) {
return mathCall(g, c) // only when nothing of yours (or C's abs) has the name
}
if lookup(g, n) >= 0 {
fail(g, c, "'" + n + "' is a variable, not a function")
}
@ -5205,6 +5336,75 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
return 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
for nm in c.names {
if nm.len > 0 {
fail(g, c, "named arguments (" + nm + ": ...) are only for creating structs")
}
}
[]Val vals
for a in c.kids {
v := expr(g, a)
if !isNumber(v.ty) {
fail(g, a, n + "(...) is for numbers, but this is " + ast.show(v.ty))
}
vals.push(v)
}
if n == "abs" {
checkArgs(g, c, 0, 1, n)
x := vals[0]
if isUnsigned(x.ty) {
return x
}
zero := "0"
if isFloat(x.ty) {
zero = "0.0"
}
neg := arith(g, "-", Val(v: zero, ty: x.ty, lit: true), x, c)
below := compare(g, "<", x, Val(v: zero, ty: x.ty, lit: true), c)
return Val(v: op(g, "select i1 " + below.v + ", " + llty(g, x.ty) + " " + neg.v + ", " + llty(g, x.ty) + " " + x.v), ty: x.ty)
}
if n == "clamp" {
checkArgs(g, c, 0, 3, n)
low := pick(g, "<", vals[0], vals[2], c) // min(x, hi)
return pick(g, ">", low, vals[1], c) // max(that, lo)
}
if vals.len < 2 {
fail(g, c, "'" + n + "' takes two or more numbers")
}
o := "<"
if n == "max" {
o = ">"
}
out := vals[0]
for i in 1..vals.len {
out = pick(g, o, out, vals[i], c)
}
return out
}
// 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 {
l := a
r := b
if !unify(g, addr(l), addr(r)) {
badOperands(g, o, a, b, at)
}
if l.lit && r.lit && l.ty.kind == "int" {
x := int(l.v) or 0
y := int(r.v) or 0
if (o == "<") == (x < y) {
return l
}
return r
}
first := compare(g, o, l, r, at)
t := llty(g, l.ty)
return Val(v: op(g, "select i1 " + first.v + ", " + t + " " + l.v + ", " + t + " " + r.v), ty: l.ty)
}
fn callFn(ptr[Gen] g, int fi, ast.Node c, int first, str shown, str selfAddr, str selfRoot) -> Val {
sig := g.sigs[fi]
if sig.module.len == 0 && sig.name == "main" {
@ -5375,13 +5575,15 @@ fn method(ptr[Gen] g, ast.Node m) -> Val {
if !imports(g, mn) {
fail(g, objNode, "there is no variable or module named '" + mn + "' (to use a module, add import " + mn + " at the top)")
}
shown := mn
mn = modFor(g, mn)
fi := findFn(g, mn, m.name)
if fi >= 0 {
return callFn(g, fi, m, 1, mn + "." + m.name, "", "")
return callFn(g, fi, m, 1, shown + "." + m.name, "", "")
}
mti := findTemplate(g, mn, m.name, "", false)
if mti >= 0 {
return genericCall(g, m, mti, 1, mn + "." + m.name)
return genericCall(g, m, mti, 1, shown + "." + m.name)
}
mei := findEnum(g, mn, m.name)
if mei >= 0 {
@ -5395,7 +5597,7 @@ fn method(ptr[Gen] g, ast.Node m) -> Val {
}
return construct(g, si, args, m.names, m)
}
fail(g, m, "the module '" + mn + "' has no function or struct named '" + m.name + "'")
fail(g, m, "the module '" + shown + "' has no function or struct named '" + m.name + "'")
}
changing := ["push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"]
r := ref(g, objNode, changing.contains(m.name))

View file

@ -24,7 +24,7 @@ fn isHex(char c) -> bool {
fn isKeyword(str w) -> bool {
keywords := ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false",
"null", "import", "link", "struct", "defer", "or", "try", "enum", "match", "export"]
"null", "import", "link", "struct", "defer", "or", "try", "enum", "match", "export", "const"]
return keywords.contains(w)
}
@ -78,6 +78,57 @@ fn operator(ptr[Lexer] lx) -> str {
return ""
}
// """ text over several lines """: the line break after the opening quotes is dropped, and so is
// the indentation of the closing quotes, from every line (the text can be indented with the code).
fn longString(ptr[Lexer] lx, Token t) -> str {
for k in 0..3 {
advance(lx)
}
[]str parts
while !(peek(lx, 0) == '"' && peek(lx, 1) == '"' && peek(lx, 2) == '"') {
if lx.i >= lx.src.len {
fail(lx, t.line, t.col, "text is never closed (missing \"\"\")")
}
ch := peek(lx, 0)
advance(lx)
if ch == '\\' {
e := peek(lx, 0)
advance(lx)
ch = escape(lx, e)
} else if ch == '\r' {
continue
}
parts.push(str(ch))
}
for k in 0..3 {
advance(lx)
}
lx.nl = false // the line breaks inside were text, not the end of a statement
body := parts.join("")
if body.starts_with("\n") {
body = body.sub(1, body.len)
}
last := body.split("\n")
indent := last[last.len - 1]
if last.len > 1 && indent.trim().len == 0 {
// the closing quotes are on their own line: their indentation goes from every line
[]str lines
for i in 0..last.len - 1 {
line := last[i]
if line.starts_with(indent) {
line = line.sub(indent.len, line.len)
} else if line.trim().len > 0 {
fail(lx, t.line, t.col, "a line of this text is indented less than the closing \"\"\"")
} else {
line = ""
}
lines.push(line)
}
return lines.join("\n")
}
return body
}
fn escape(ptr[Lexer] lx, char e) -> char {
if e == 'n' {
return '\n'
@ -185,6 +236,9 @@ fn lex(str src, str path) -> []Token {
if mant.len > 0 {
t.text = (mant + lx.src.sub(expStart, lx.i)).replace("_", "") // LLVM wants the '.'
}
} else if c == '"' && peek(lx, 1) == '"' && peek(lx, 2) == '"' {
t.kind = "string"
t.text = longString(lx, t)
} else if c == '"' || c == '\'' {
quote := c
t.kind = "string"

View file

@ -104,9 +104,9 @@ fn load(ptr[Loader] l, str path, str module) {
prog := parser.parse(lexer.lex(src, path), file, path)
prog.module = module
prog.path = path
imports := prog.imports
modules := prog.modules
l.progs.push(prog)
for name in imports {
for name in modules {
if !l.loaded.contains(name) {
l.loaded.push(name)
load(l, findModule(l, path, name), name)

View file

@ -236,7 +236,23 @@ fn parseProgram(ptr[Parser] p) -> ast.Program {
if !at(p, "ident") {
fail(p, t, "expected a module name or a C header after 'import'")
}
prog.imports.push(next(p).text)
// import ui/button (a folder), import path as p (another name here)
key := next(p).text
name := key
while at(p, "/") && !cur(p).nl {
next(p)
name = expect(p, "ident", "a module name after '/'").text
key = key + "/" + name
}
if at(p, "ident") && cur(p).text == "as" && !cur(p).nl {
next(p)
name = expect(p, "ident", "the name to use for the module, like import path as p").text
}
if prog.imports.contains(name) {
fail(p, t, "the name '" + name + "' is already used by another import (pick one with: import " + key + " as other)")
}
prog.imports.push(name)
prog.modules.push(key)
}
} else if at(p, "link") {
lt := next(p)
@ -248,6 +264,8 @@ fn parseProgram(ptr[Parser] p) -> ast.Program {
prog.links.push(lib)
} else if at(p, "struct") {
prog.structs.push(parseStruct(p))
} else if at(p, "const") {
prog.consts.push(parseConst(p))
} else if at(p, "enum") {
e := parseEnum(p)
prog.enums.push(e)
@ -261,6 +279,24 @@ fn parseProgram(ptr[Parser] p) -> ast.Program {
return prog
}
// const WIDTH = 640 const f32 GRAVITY = 9.8 const TITLE = "Game"
fn parseConst(ptr[Parser] p) -> ast.Node {
c := node(p, "const")
expect(p, "const", "'const'")
if !(at(p, "ident") && peekKind(p, 1) == "=") {
c.ty = parseType(p)
c.flag = true
}
t := expect(p, "ident", "the constant's name, like const WIDTH = 640")
c.name = t.text
c.line = t.line
c.col = t.col
expect(p, "=", "'=' and its value, like const WIDTH = 640")
c.kids.push(parseExpr(p))
endOfStatement(p)
return c
}
// enum Color { Red, Green, Blue } enum Status { Ok = 200, NotFound = 404 }
// The variants are numbered from 0 (or from the last number given) and are written Color.Red.
// A variant can hold values: enum Shape { Circle(float), Rect(float, float), Point } (e.flag is then true).
@ -433,7 +469,7 @@ fn parseFn(ptr[Parser] p) -> ast.Node {
if at(p, "ident") && isTypeName(cur(p).text) {
fail(p, cur(p), "functions start with 'fn', like: fn add(int a, int b) -> int { ... }")
}
expect(p, "fn", "'fn', 'struct' or 'import'")
expect(p, "fn", "'fn', 'struct', 'enum', 'const' or 'import'")
f.name = expect(p, "ident", "a function name").text
if at(p, "[") {
f.tparams = parseTParams(p) // fn max[T](...) or the struct's, in fn Stack[T].push(...)
@ -612,6 +648,33 @@ fn parseIf(ptr[Parser] p) -> ast.Node {
// ---------- expressions ----------
// if as a value: x := if a > b { a } else { b } (each branch is one value; the else is needed)
fn parseIfValue(ptr[Parser] p) -> ast.Node {
e := node(p, "ifvalue")
expect(p, "if", "'if'")
e.kids.push(parseExpr(p))
e.kids.push(branchValue(p))
if !accept(p, "else") {
fail(p, cur(p), "an if that gives a value needs an else: if c { a } else { b }")
}
if at(p, "if") {
e.kids.push(parseIfValue(p))
} else {
e.kids.push(branchValue(p))
}
return e
}
fn branchValue(ptr[Parser] p) -> ast.Node {
expect(p, "{", "'{'")
saved := p.depth
p.depth = 1
v := parseExpr(p)
p.depth = saved
expect(p, "}", "'}' (a branch of an if that gives a value holds just that value)")
return v
}
fn binaryLevel(str op) -> int {
if op == "||" {
return 1
@ -769,6 +832,9 @@ fn parsePrimary(ptr[Parser] p) -> ast.Node {
if k == "match" {
return parseMatch(p, true)
}
if k == "if" {
return parseIfValue(p)
}
if k == "[" {
next(p)
e.kind = "array"