Struct defaults apply inside structs; exit() ends an or-block; 1e9 literals; x * 2.0 stays f32; floats print as the shortest exact text; str(x, digits)

Also frees main's args and the message of a failing main (valgrind was not clean on two tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-29 09:14:58 +01:00
commit 6142e47e71
10 changed files with 207 additions and 10 deletions

View file

@ -12,6 +12,7 @@
#include <ctype.h>
#include <errno.h>
#include <math.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
@ -153,12 +154,50 @@ void finch_str_from_uint(FStr *out, uint64_t v) {
str_heap(out, buf, n);
}
// The shortest text that reads back as the same number (as an f32 when `single`):
// 0.1 -> "0.1", 1.0/3 -> "0.3333333333333333", 1e8 -> "100000000", 1e20 -> "1e+20".
static int fmt_float(char *buf, size_t size, double v, int single) {
if (isnan(v)) return snprintf(buf, size, "nan");
if (isinf(v)) return snprintf(buf, size, v < 0 ? "-inf" : "inf");
int p = 1;
for (; p < 17; p++) {
snprintf(buf, size, "%.*e", p - 1, v);
double back = strtod(buf, NULL);
if (single ? (float)back == (float)v : back == v) break;
}
int exp = atoi(strchr(buf, 'e') + 1);
if (exp < -5 || exp >= 17) return snprintf(buf, size, "%.*g", p, v);
int decimals = p - 1 - exp;
return snprintf(buf, size, "%.*f", decimals > 0 ? decimals : 0, v);
}
void finch_str_from_float(FStr *out, double v) {
char buf[64];
int n = snprintf(buf, sizeof buf, "%g", v);
int n = fmt_float(buf, sizeof buf, v, 0);
str_heap(out, buf, n);
}
void finch_str_from_f32(FStr *out, double v) {
char buf[64];
int n = fmt_float(buf, sizeof buf, v, 1);
str_heap(out, buf, n);
}
// str(x, 2): a fixed number of digits after the point
void finch_str_fixed(FStr *out, double v, int64_t digits) {
char buf[400];
if (digits < 0) digits = 0;
if (digits > 60) digits = 60;
int n = snprintf(buf, sizeof buf, "%.*f", (int)digits, v);
str_heap(out, buf, n);
}
void finch_print_float(double v, int32_t single) {
char buf[64];
fmt_float(buf, sizeof buf, v, single);
fputs(buf, stdout);
}
void finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }
void finch_str_from_bool(FStr *out, int b) { str_static(out, b ? "true" : "false"); }
@ -242,6 +281,7 @@ static void file_error(FStr *err, const char *what, const FStr *path) {
void finch_report_error(const FStr *msg) {
fflush(stdout);
fprintf(stderr, "error: %.*s\n", (int)msg->len, msg->ptr ? msg->ptr : "");
finch_str_drop((FStr *)msg);
}
void finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {
@ -399,7 +439,14 @@ static void buf_fmt(FStr *b, const char *fmt, ...) {
void finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, "%lld", (long long)v); }
void finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, "%llu", (unsigned long long)v); }
void finch_buf_float(FStr *b, double v) { buf_fmt(b, "%g", v); }
void finch_buf_float(FStr *b, double v) {
char t[64];
finch_buf_add(b, t, fmt_float(t, sizeof t, v, 0));
}
void finch_buf_f32(FStr *b, double v) {
char t[64];
finch_buf_add(b, t, fmt_float(t, sizeof t, v, 1));
}
void finch_buf_bool(FStr *b, int v) { finch_buf_add(b, v ? "true" : "false", v ? 4 : 5); }
void finch_buf_ptr(FStr *b, const void *p) {
if (p) buf_fmt(b, "0x%llx", (unsigned long long)(uintptr_t)p);
@ -498,6 +545,12 @@ void finch_args(FArr *out, int argc, char **argv) {
for (int i = 0; i < argc; i++) str_heap((FStr *)out->ptr + i, argv[i], (int64_t)strlen(argv[i]));
}
// after main: the arguments go
void finch_args_free(FArr *a) {
for (int64_t i = 0; i < a->len; i++) finch_str_drop((FStr *)a->ptr + i);
finch_arr_free(a);
}
// input("Name? ") prints the prompt and reads one line, without the newline.
void finch_input(FStr *out, const FStr *prompt) {
if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);

File diff suppressed because one or more lines are too long

View file

@ -1020,6 +1020,21 @@ fn own(ptr[Gen] g, Val v) -> str {
// ---------- printing ----------
// f32 prints the shortest text that is the same f32 (0.1, not 0.100000001)
fn floatName(ast.Type t) -> str {
if t.kind == "f32" {
return "f32"
}
return "float"
}
fn single(ast.Type t) -> str {
if t.kind == "f32" {
return "1"
}
return "0"
}
fn printf(ptr[Gen] g, str fmt, str args) {
f := strGlobal(g, fmt)
t := tmpName(g)
@ -1044,7 +1059,7 @@ fn emitPrint(ptr[Gen] g, str v, ast.Type t, bool quoted) {
} else if isUnsigned(t) {
printf(g, "%llu", "i64 " + intCast(g, v, t, T("u64")))
} else if isFloat(t) {
printf(g, "%g", "double " + floatCast(g, v, t, T("float")))
emit(g, "call void @finch_print_float(double " + floatCast(g, v, t, T("float")) + ", i32 " + single(t) + ")")
} else if k == "char" {
z := op(g, "sext i8 " + v + " to i32")
if quoted {
@ -1106,7 +1121,7 @@ fn emitFormat(ptr[Gen] g, str buf, str v, ast.Type t, bool quoted) {
} else if isUnsigned(t) {
emit(g, "call void @finch_buf_uint(ptr " + buf + ", i64 " + intCast(g, v, t, T("u64")) + ")")
} else if isFloat(t) {
emit(g, "call void @finch_buf_float(ptr " + buf + ", double " + floatCast(g, v, t, T("float")) + ")")
emit(g, "call void @finch_buf_" + floatName(t) + "(ptr " + buf + ", double " + floatCast(g, v, t, T("float")) + ")")
} else if k == "char" {
emit(g, "call void @finch_buf_char(ptr " + buf + ", i8 " + v + ", i32 " + q + ")")
} else if k == "bool" {
@ -2254,7 +2269,7 @@ fn block(ptr[Gen] g, ast.Node b, bool scope) {
}
for s in b.kids {
if g.term {
fail(g, s, "this code can never run (it comes after return, break or continue)")
fail(g, s, "this code can never run (it comes after return, break, continue or exit)")
}
stmt(g, s)
}
@ -2357,6 +2372,12 @@ fn hasDefaults(ptr[Gen] g, ast.Type t) -> bool {
return true
}
}
// a field that is itself a struct with defaults gets them too
for ft in sd.ftypes {
if ft.kind == "struct" && hasDefaults(g, ft) {
return true
}
}
return false
}
@ -3492,6 +3513,15 @@ fn unify(ptr[Gen] g, ptr[Val] l, ptr[Val] r) -> bool {
return true
}
if isFloat(l.ty) && isFloat(r.ty) {
// a number written in the code adapts to an f32 on the other side: x * 2.0 stays f32
if l.ty.kind == "f32" && r.lit {
r.value = Val(v: floatCast(g, r.v, r.ty, l.ty), ty: l.ty)
return true
}
if r.ty.kind == "f32" && l.lit {
l.value = Val(v: floatCast(g, l.v, l.ty, r.ty), ty: r.ty)
return true
}
if l.ty.kind == "f32" {
l.value = Val(v: floatCast(g, l.v, l.ty, T("float")), ty: T("float"))
}
@ -4872,6 +4902,19 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
return Val(v: pl.addr, ty: ast.ptrTo(pl.ty))
}
if n == "str" {
if c.kids.len == 2 {
// str(x, 2): a float with that many digits after the point
checkArgs(g, c, 0, 2, n)
x := expr(g, c.kids[0])
if !isNumber(x.ty) {
fail(g, c.kids[0], "str(x, digits) is for numbers, but this is " + ast.show(x.ty))
}
x = coerce(g, x, T("float"), c.kids[0], "the number")
d := coerce(g, expr(g, c.kids[1]), T("int"), c.kids[1], "the number of digits")
fixed := strOut(g)
emit(g, "call void @finch_str_fixed(ptr " + fixed + ", double " + x.v + ", i64 " + d.v + ")")
return Val(v: load(g, "{ ptr, i64, i64 }", fixed), ty: T("str"), fresh: true)
}
checkArgs(g, c, 0, 1, n)
v := expr(g, c.kids[0])
out := strOut(g)
@ -4884,7 +4927,7 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
} else if isUnsigned(v.ty) {
emit(g, "call void @finch_str_from_uint(ptr " + out + ", i64 " + intCast(g, v.v, v.ty, T("u64")) + ")")
} else if isFloat(v.ty) {
emit(g, "call void @finch_str_from_float(ptr " + out + ", double " + floatCast(g, v.v, v.ty, T("float")) + ")")
emit(g, "call void @finch_str_from_" + floatName(v.ty) + "(ptr " + out + ", double " + floatCast(g, v.v, v.ty, T("float")) + ")")
} else if k == "char" {
emit(g, "call void @finch_str_from_char(ptr " + out + ", i8 signext " + v.v + ")")
} else if k == "bool" {
@ -4998,6 +5041,8 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
t := op(g, "trunc i64 " + code.v + " to i32")
f := op(g, "call i32 @fflush(ptr null)")
emit(g, "call void @exit(i32 " + t + ")")
emit(g, "unreachable")
g.term = true // like return: an or-block may end with it
return Val(ty: T("void"))
}
if n == "input" {
@ -5200,6 +5245,8 @@ fn construct(ptr[Gen] g, int si, []ast.Node args, []str names, ast.Node at) -> V
g.tnames = savedTN
g.targs = savedTA
g.scopes = savedScopes
} else if ft.kind == "struct" && hasDefaults(g, ft) {
fv = Val(v: defaultVal(g, ft, at), ty: ft, fresh: owning(g, ft))
} else {
continue
}
@ -5716,6 +5763,10 @@ fn runtimeDecls() -> str {
"declare void @finch_str_from_int(ptr, i64)",
"declare void @finch_str_from_uint(ptr, i64)",
"declare void @finch_str_from_float(ptr, double)",
"declare void @finch_str_from_f32(ptr, double)",
"declare void @finch_str_fixed(ptr, double, i64)",
"declare void @finch_print_float(double, i32)",
"declare void @finch_buf_f32(ptr, double)",
"declare void @finch_str_from_char(ptr, i8 signext)",
"declare void @finch_str_from_bool(ptr, i32)",
"declare i64 @finch_str_to_int(ptr, ptr, i64)",
@ -5747,6 +5798,7 @@ fn runtimeDecls() -> str {
"declare void @finch_arr_remove_gap(ptr, i64, i64)",
"declare void @qsort(ptr, i64, i64, ptr)",
"declare void @finch_args(ptr, i32, ptr)",
"declare void @finch_args_free(ptr)",
"declare void @finch_input(ptr, ptr)",
"declare i32 @finch_file_exists(ptr)",
"declare i32 @finch_str_try_int(ptr, ptr, ptr)",
@ -5846,6 +5898,10 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt)
}
callText = callText + ")"
ret := " ret i32 0\n"
after := ""
if m.params.len == 1 {
after = " call void @finch_args_free(ptr %args)\n"
}
if m.fallible {
// fn main() -> !: a failure is printed, and the exit code is 1
callText = "%r = " + callText
@ -5854,7 +5910,7 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt)
rt := retTy(g, m)
pre = pre + " %ok = extractvalue " + rt + " %r, 0\n br i1 %ok, label %main.ok, label %main.bad\nmain.bad:\n"
pre = pre + " %m = extractvalue " + rt + " %r, 2\n %mp = alloca { ptr, i64, i64 }\n store { ptr, i64, i64 } %m, ptr %mp\n"
pre = pre + " call void @finch_report_error(ptr %mp)\n ret i32 1\nmain.ok:\n"
pre = pre + " call void @finch_report_error(ptr %mp)\n" + after + " ret i32 1\nmain.ok:\n"
if m.ret.kind == "int" {
ret = " %v = extractvalue " + rt + " %r, 1\n %code = trunc i64 %v to i32\n ret i32 %code\n"
}
@ -5862,6 +5918,6 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt)
callText = "%r = " + callText
ret = " %code = trunc i64 %r to i32\n ret i32 %code\n"
}
g.out.push("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n" + pre + " " + callText + "\n" + ret + "}\n")
g.out.push("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n" + pre + " " + callText + "\n" + after + ret + "}\n")
return assemble(g)
}

View file

@ -144,6 +144,8 @@ fn lex(str src, str path) -> []Token {
}
} else if isDigit(c) {
t.kind = "int"
mant := ""
expStart := 0
if c == '0' && (peek(lx, 1) == 'x' || peek(lx, 1) == 'X') {
advance(lx)
advance(lx)
@ -161,8 +163,28 @@ fn lex(str src, str path) -> []Token {
advance(lx)
}
}
// an exponent: 1e9, 2.5e-3
e := peek(lx, 0)
sign := peek(lx, 1) == '+' || peek(lx, 1) == '-'
if (e == 'e' || e == 'E') && (isDigit(peek(lx, 1)) || (sign && isDigit(peek(lx, 2)))) {
if t.kind == "int" {
t.kind = "float"
mant = lx.src.sub(start, lx.i) + ".0"
}
expStart = lx.i
advance(lx)
if sign {
advance(lx)
}
while lx.i < lx.src.len && isDigit(lx.src[lx.i]) {
advance(lx)
}
}
}
t.text = lx.src.sub(start, lx.i).replace("_", "")
if mant.len > 0 {
t.text = (mant + lx.src.sub(expStart, lx.i)).replace("_", "") // LLVM wants the '.'
}
} else if c == '"' || c == '\'' {
quote := c
t.kind = "string"

View file

@ -0,0 +1,43 @@
// struct defaults apply inside structs too; number literals and float text
struct Audio {
int rate = 48000
float volume = 0.8
}
struct Mixer {
Audio audio
[]Audio extra
int voices = 5
}
struct Box[T] {
T item
Audio audio
}
fn make() -> Mixer {
Mixer m
return m
}
fn scale(f32 x) -> f32 {
return x * 2.0 + 1
}
fn main() {
a := Mixer()
Mixer b
c := Mixer(voices: 2)
list := [Mixer()]
m := ["x": Mixer()]
map[str]Mixer counts
counts["new"].voices += 1
bx := Box[int]()
print(a.audio.rate, b.audio.volume, c.audio.rate, c.voices, list[0].audio.rate, m["x"].audio.rate)
print(make().audio.rate, counts["new"].audio.rate, counts["new"].voices, bx.audio.rate)
print(1e3, 2.5e-3, 1E+6, 1_000.5e1, scale(1.5))
print(1.0 / 3.0, 0.1 + 0.2, 100000000.0, 1e20, 1e-7, float(7), f32(0.1))
print(str(3.14159, 2), str(2, 1), str(f32(0.1)), "x=" + str(0.25), [0.5, 1.25])
}

View file

@ -0,0 +1,5 @@
48000 0.8 48000 2 48000 48000
48000 48000 6 48000
1000 0.0025 1000000 10005 4
0.3333333333333333 0.30000000000000004 100000000 1e+20 1e-07 7 0.1
3.14 2.0 0.1 x=0.25 [0.5, 1.25]

12
tests/new/exit_or.fch Normal file
View file

@ -0,0 +1,12 @@
// exit(...) ends an or-block, like return
fn load(str p) -> int! {
return error("no such file")
}
fn main() {
n := load("a.txt") or {
print("stopping:", err)
exit(0)
}
print("never", n)
}

1
tests/new/exit_or.out Normal file
View file

@ -0,0 +1 @@
stopping: no such file

View file

@ -0,0 +1,5 @@
// expect: this code can never run (it comes after return, break, continue or exit)
fn main() {
exit(1)
print("x")
}

View file

@ -1,5 +1,5 @@
5
(4, 5) of length 6.40312
(4, 5) of length 6.4031242374328485
Vec(x: 0, y: 0)
10 Box(corner: Vec(x: 0, y: 0), size: Vec(x: 2, y: 5))
total 3