From f2d64807496187addef8572b8f45eb9345b86a4b Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Thu, 29 Oct 2026 09:37:41 +0100 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL --- runtime/finch_rt.c | 149 +++++++++++++++++++- src/ast.fch | 4 +- src/embedded.fch | 6 +- src/gen.fch | 220 ++++++++++++++++++++++++++++-- src/lexer.fch | 56 +++++++- src/main.fch | 4 +- src/parser.fch | 70 +++++++++- std/finch_fs.h | 9 ++ std/fs.fch | 70 ++++++++++ tests/new/fail/const_assign.fch | 5 + tests/new/fail/const_call.fch | 6 + tests/new/fail/const_loop.fch | 6 + tests/new/fail/ifvalue_else.fch | 4 + tests/new/fail/ifvalue_types.fch | 4 + tests/new/fail/import_clash.fch | 5 + tests/new/fail/longstr_indent.fch | 6 + tests/new/lang_additions.fch | 33 +++++ tests/new/lang_additions.out | 11 ++ tests/new/std_fs.fch | 23 ++++ tests/new/std_fs.out | 7 + tests/new/ui/button.fch | 7 + 21 files changed, 688 insertions(+), 17 deletions(-) create mode 100644 std/finch_fs.h create mode 100644 std/fs.fch create mode 100644 tests/new/fail/const_assign.fch create mode 100644 tests/new/fail/const_call.fch create mode 100644 tests/new/fail/const_loop.fch create mode 100644 tests/new/fail/ifvalue_else.fch create mode 100644 tests/new/fail/ifvalue_types.fch create mode 100644 tests/new/fail/import_clash.fch create mode 100644 tests/new/fail/longstr_indent.fch create mode 100644 tests/new/lang_additions.fch create mode 100644 tests/new/lang_additions.out create mode 100644 tests/new/std_fs.fch create mode 100644 tests/new/std_fs.out create mode 100644 tests/new/ui/button.fch diff --git a/runtime/finch_rt.c b/runtime/finch_rt.c index 99b2f7f..662ee89 100644 --- a/runtime/finch_rt.c +++ b/runtime/finch_rt.c @@ -18,7 +18,13 @@ #include #include #include -#ifndef _WIN32 +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include #include #endif @@ -661,6 +667,147 @@ int finch_try_write_file(const FStr *path, const FStr *text, FStr *err) { return 0; } +// ---------- folders (std/fs.fch, through std/finch_fs.h) ---------- + +int finch_fs_is_dir(const char *path) { +#ifdef _WIN32 + struct _finddata_t d; + intptr_t h = _findfirst(path, &d); + if (h == -1) return 0; + _findclose(h); + return (d.attrib & _A_SUBDIR) != 0; +#else + struct stat st; + return stat(path, &st) == 0 && S_ISDIR(st.st_mode); +#endif +} + +int finch_fs_exists(const char *path) { +#ifdef _WIN32 + return _access(path, 0) == 0; +#else + struct stat st; + return stat(path, &st) == 0; +#endif +} + +static int make_one_dir(const char *p) { +#ifdef _WIN32 + return _mkdir(p) == 0 || errno == EEXIST; +#else + return mkdir(p, 0777) == 0 || errno == EEXIST; +#endif +} + +// mkdir -p: the parents too; 1 if the folder is there afterwards +int finch_fs_mkdir(const char *path) { + size_t n = strlen(path); + char *p = finch_alloc((int64_t)n + 1); + memcpy(p, path, n + 1); + for (size_t i = 1; i < n; i++) { + if (p[i] == '/' || p[i] == '\\') { + if (p[i - 1] == ':') continue; /* C: is a drive */ + char c = p[i]; + p[i] = 0; + make_one_dir(p); + p[i] = c; + } + } + int ok = make_one_dir(p) && finch_fs_is_dir(p); + free(p); + return ok; +} + +int finch_fs_remove_dir(const char *path) { +#ifdef _WIN32 + return _rmdir(path) == 0; +#else + return rmdir(path) == 0; +#endif +} + +// the names in a folder, one per call ("" at the end), without . and .. +typedef struct { +#ifdef _WIN32 + intptr_t h; + struct _finddata_t d; + int first; +#else + DIR *d; +#endif +} FsList; + +void *finch_fs_list_open(const char *path) { + FsList *l = finch_zalloc((int64_t)sizeof(FsList)); +#ifdef _WIN32 + size_t n = strlen(path); + char *pat = finch_alloc((int64_t)n + 3); + memcpy(pat, path, n); + memcpy(pat + n, "\\*", 3); + l->h = _findfirst(pat, &l->d); + free(pat); + if (l->h == -1) { + free(l); + return NULL; + } + l->first = 1; +#else + l->d = opendir(path); + if (!l->d) { + free(l); + return NULL; + } +#endif + return l; +} + +const char *finch_fs_list_next(void *handle) { + FsList *l = handle; + for (;;) { +#ifdef _WIN32 + if (!l->first && _findnext(l->h, &l->d) != 0) return ""; + l->first = 0; + const char *name = l->d.name; +#else + struct dirent *e = readdir(l->d); + if (!e) return ""; + const char *name = e->d_name; +#endif + if (strcmp(name, ".") != 0 && strcmp(name, "..") != 0) return name; + } +} + +void finch_fs_list_close(void *handle) { + FsList *l = handle; +#ifdef _WIN32 + _findclose(l->h); +#else + closedir(l->d); +#endif + free(l); +} + +// where a program keeps its own files for this user (saves, settings); "" if unknown +// Linux: $XDG_DATA_HOME or ~/.local/share macOS: ~/Library/Application Support Windows: %APPDATA% +const char *finch_fs_data_home(void) { + static char buf[4096]; + const char *v; +#if defined(_WIN32) + v = getenv("APPDATA"); + snprintf(buf, sizeof buf, "%s", v ? v : ""); +#elif defined(__APPLE__) + v = getenv("HOME"); + if (v) snprintf(buf, sizeof buf, "%s/Library/Application Support", v); + else buf[0] = 0; +#else + v = getenv("XDG_DATA_HOME"); + if (v && *v) snprintf(buf, sizeof buf, "%s", v); + else if ((v = getenv("HOME"))) snprintf(buf, sizeof buf, "%s/.local/share", v); + else buf[0] = 0; +#endif + return buf; +} + // ---------- maps ---------- // map[K]V = { entries; len; used; cap; index; icap; adds } // entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order. diff --git a/src/ast.fch b/src/ast.fch index 28bb900..8e760c8 100644 --- a/src/ast.fch +++ b/src/ast.fch @@ -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 } diff --git a/src/embedded.fch b/src/embedded.fch index 6f332ae..9531c8c 100644 --- a/src/embedded.fch +++ b/src/embedded.fch @@ -4,25 +4,29 @@ fn names() -> []str { []str list list.push("runtime/finch_rt.c") + list.push("std/fs.fch") list.push("std/json.fch") list.push("std/path.fch") list.push("std/proc.fch") list.push("std/proc_windows.fch") list.push("std/time.fch") list.push("std/time_windows.fch") + list.push("std/finch_fs.h") list.push("std/finch_win.h") return list } fn texts() -> []str { []str list - list.push("// The Finch runtime: string and array helpers, input, files and runtime errors.\n// It is embedded in the compiler, compiled once into a cache folder, and linked\n// statically into every Finch program. Everything here is plain C with no state.\n//\n// Layout shared with the compiler (src/codegen.cpp):\n// str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated\n// cap > 0 : heap memory owned by the value (cap bytes allocated)\n// cap == 0 : a string literal, valid forever, never written to\n// cap == -1: borrowed from C, must be copied before it is kept\n// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0\n// map[K]V: see \"maps\" below\n\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#ifndef _WIN32\n#include \n#endif\n\ntypedef struct {\n char *ptr;\n int64_t len;\n int64_t cap;\n} FStr;\n\ntypedef struct {\n void *ptr;\n int64_t len;\n int64_t cap;\n} FArr;\n\nstatic char empty[1] = \"\";\n\n// ---------- runtime errors ----------\n\n// A program that loads Finch libraries (finch build --shared) can take runtime errors over\n// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler\n// gets the message and must not return (longjmp out, or exit itself).\nstatic void (*fatal_hook)(const char *msg);\nvoid finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; }\n\nstatic __attribute__((noreturn)) void fatal(const char *msg) {\n if (fatal_hook) fatal_hook(msg);\n fflush(stdout);\n fprintf(stderr, \"%s\\n\", msg);\n exit(1);\n}\n\n__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) {\n char full[1024];\n snprintf(full, sizeof full, \"%s:%lld: runtime error: %s\", file, (long long)line, msg);\n fflush(stdout);\n fatal(full);\n}\n\n__attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {\n char msg[128];\n snprintf(msg, sizeof msg, \"index %lld is out of range (the length is %lld)\", (long long)i, (long long)len);\n finch_panic(file, line, msg);\n}\n\nstatic void *must(void *p) {\n if (!p) {\n fflush(stdout);\n fatal(\"runtime error: out of memory\");\n }\n return p;\n}\n\nvoid *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); }\nvoid *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); }\n\n// ---------- strings ----------\n\nstatic void str_heap(FStr *out, const char *p, int64_t len) {\n char *m = finch_alloc(len + 1);\n if (len) memcpy(m, p, (size_t)len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void str_static(FStr *out, const char *lit) {\n out->ptr = (char *)lit;\n out->len = (int64_t)strlen(lit);\n out->cap = 0;\n}\n\n// A copy that can be kept: literals are shared, everything else is duplicated.\nvoid finch_str_copy(FStr *out, const FStr *s) {\n if (s->cap == 0) *out = *s;\n else str_heap(out, s->ptr, s->len);\n}\n\n// Make `s` writable (before s[i] = c).\nvoid finch_str_own(FStr *s) {\n if (s->cap > 0) return;\n FStr t;\n str_heap(&t, s->ptr, s->len);\n *s = t;\n}\n\nvoid finch_str_drop(FStr *s) {\n if (s->cap > 0) free(s->ptr);\n}\n\nvoid finch_str_from_c(FStr *out, const char *p) {\n if (!p) {\n str_static(out, empty);\n return;\n }\n out->ptr = (char *)p;\n out->len = (int64_t)strlen(p);\n out->cap = -1;\n}\n\nvoid finch_str_concat(FStr *out, const FStr *a, const FStr *b) {\n int64_t len = a->len + b->len;\n char *m = finch_alloc(len + 1);\n memcpy(m, a->ptr, (size_t)a->len);\n memcpy(m + a->len, b->ptr, (size_t)b->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nint finch_str_eq(const FStr *a, const FStr *b) {\n return a->len == b->len && memcmp(a->ptr, b->ptr, (size_t)a->len) == 0;\n}\n\nint64_t finch_str_cmp(const FStr *a, const FStr *b) {\n int64_t n = a->len < b->len ? a->len : b->len;\n int c = memcmp(a->ptr, b->ptr, (size_t)n);\n if (c) return c < 0 ? -1 : 1;\n return a->len < b->len ? -1 : a->len > b->len;\n}\n\nvoid finch_str_from_int(FStr *out, int64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%lld\", (long long)v);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_uint(FStr *out, uint64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%llu\", (unsigned long long)v);\n str_heap(out, buf, n);\n}\n\n// The shortest text that reads back as the same number (as an f32 when `single`):\n// 0.1 -> \"0.1\", 1.0/3 -> \"0.3333333333333333\", 1e8 -> \"100000000\", 1e20 -> \"1e+20\".\nstatic int fmt_float(char *buf, size_t size, double v, int single) {\n if (isnan(v)) return snprintf(buf, size, \"nan\");\n if (isinf(v)) return snprintf(buf, size, v < 0 ? \"-inf\" : \"inf\");\n int p = 1;\n for (; p < 17; p++) {\n snprintf(buf, size, \"%.*e\", p - 1, v);\n double back = strtod(buf, NULL);\n if (single ? (float)back == (float)v : back == v) break;\n }\n int exp = atoi(strchr(buf, 'e') + 1);\n if (exp < -5 || exp >= 17) return snprintf(buf, size, \"%.*g\", p, v);\n int decimals = p - 1 - exp;\n return snprintf(buf, size, \"%.*f\", decimals > 0 ? decimals : 0, v);\n}\n\nvoid finch_str_from_float(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 0);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_f32(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 1);\n str_heap(out, buf, n);\n}\n\n// str(x, 2): a fixed number of digits after the point\nvoid finch_str_fixed(FStr *out, double v, int64_t digits) {\n char buf[400];\n if (digits < 0) digits = 0;\n if (digits > 60) digits = 60;\n int n = snprintf(buf, sizeof buf, \"%.*f\", (int)digits, v);\n str_heap(out, buf, n);\n}\n\nvoid finch_print_float(double v, int32_t single) {\n char buf[64];\n fmt_float(buf, sizeof buf, v, single);\n fputs(buf, stdout);\n}\n\nvoid finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }\n\nvoid finch_str_from_bool(FStr *out, int b) { str_static(out, b ? \"true\" : \"false\"); }\n\nstatic void number_error(char *msg, size_t size, const FStr *s, const char *kind) {\n snprintf(msg, size, \"can't turn \\\"%.*s\\\" into %s\", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : \"\", kind);\n}\n\nstatic int parse_int(const FStr *s, int64_t *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n errno = 0;\n long long v = strtoll(p, &end, 10);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end || errno == ERANGE);\n}\n\nstatic int parse_float(const FStr *s, double *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n double v = strtod(p, &end);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end);\n}\n\nint64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {\n int64_t v;\n if (!parse_int(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\ndouble finch_str_to_float(const FStr *s, const char *file, int64_t line) {\n double v;\n if (!parse_float(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\n// ---------- errors as values: int(s) or 0, try read_file(p) ... ----------\n// Each gives back 1 on success; on failure 0, with the message in *err.\n\nstatic void set_error(FStr *err, const char *msg) { str_heap(err, msg, (int64_t)strlen(msg)); }\n\nint finch_str_try_int(const FStr *s, int64_t *out, FStr *err) {\n if (parse_int(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nint finch_str_try_float(const FStr *s, double *out, FStr *err) {\n if (parse_float(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nstatic void file_error(FStr *err, const char *what, const FStr *path) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't %s the file \\\"%.*s\\\": %s\", what, (int)(path->len > 200 ? 200 : path->len),\n path->ptr ? path->ptr : \"\", strerror(errno));\n set_error(err, msg);\n}\n\n// when `fn main() -> !` fails\nvoid finch_report_error(const FStr *msg) {\n fflush(stdout);\n fprintf(stderr, \"error: %.*s\\n\", (int)msg->len, msg->ptr ? msg->ptr : \"\");\n finch_str_drop((FStr *)msg);\n}\n\nvoid finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {\n if (start < 0 || end > s->len || start > end) {\n char msg[160];\n snprintf(msg, sizeof msg, \"sub(%lld, %lld) is out of range (the length is %lld)\", (long long)start,\n (long long)end, (long long)s->len);\n finch_panic(file, line, msg);\n }\n str_heap(out, s->ptr + start, end - start);\n}\n\nint64_t finch_str_find(const FStr *s, const FStr *t) {\n if (t->len == 0) return 0;\n for (int64_t i = 0; i + t->len <= s->len; i++)\n if (s->ptr[i] == t->ptr[0] && memcmp(s->ptr + i, t->ptr, (size_t)t->len) == 0) return i;\n return -1;\n}\n\nint finch_str_starts(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr, t->ptr, (size_t)t->len) == 0;\n}\n\nint finch_str_ends(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr + s->len - t->len, t->ptr, (size_t)t->len) == 0;\n}\n\nvoid finch_str_trim(FStr *out, const FStr *s) {\n int64_t a = 0, b = s->len;\n while (a < b && isspace((unsigned char)s->ptr[a])) a++;\n while (b > a && isspace((unsigned char)s->ptr[b - 1])) b--;\n str_heap(out, s->ptr + a, b - a);\n}\n\nvoid finch_str_upper(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)toupper((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_lower(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)tolower((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_replace(FStr *out, const FStr *s, const FStr *from, const FStr *to) {\n if (from->len == 0) {\n str_heap(out, s->ptr, s->len);\n return;\n }\n int64_t count = 0;\n for (int64_t i = 0; i + from->len <= s->len;) {\n if (memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n count++;\n i += from->len;\n } else {\n i++;\n }\n }\n int64_t len = s->len + count * (to->len - from->len);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < s->len;) {\n if (i + from->len <= s->len && memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n memcpy(w, to->ptr, (size_t)to->len);\n w += to->len;\n i += from->len;\n } else {\n *w++ = s->ptr[i++];\n }\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len);\n\n// \"a,b,,c\".split(\",\") -> [\"a\", \"b\", \"\", \"c\"]; split(\"\") gives single characters.\nvoid finch_str_split(FArr *out, const FStr *s, const FStr *sep) {\n out->ptr = NULL;\n out->len = out->cap = 0;\n if (sep->len == 0) {\n for (int64_t i = 0; i < s->len; i++) arr_push_str(out, s->ptr + i, 1);\n return;\n }\n int64_t start = 0;\n for (int64_t i = 0; i + sep->len <= s->len;) {\n if (memcmp(s->ptr + i, sep->ptr, (size_t)sep->len) == 0) {\n arr_push_str(out, s->ptr + start, i - start);\n i += sep->len;\n start = i;\n } else {\n i++;\n }\n }\n arr_push_str(out, s->ptr + start, s->len - start);\n}\n\nvoid finch_str_join(FStr *out, const FArr *parts, const FStr *sep) {\n const FStr *p = parts->ptr;\n int64_t len = 0;\n for (int64_t i = 0; i < parts->len; i++) len += p[i].len + (i ? sep->len : 0);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < parts->len; i++) {\n if (i) {\n memcpy(w, sep->ptr, (size_t)sep->len);\n w += sep->len;\n }\n memcpy(w, p[i].ptr, (size_t)p[i].len);\n w += p[i].len;\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_repeat(FStr *out, const FStr *s, int64_t n) {\n if (n < 0) n = 0;\n int64_t len = s->len * n;\n char *m = finch_alloc(len + 1);\n for (int64_t i = 0; i < n; i++) memcpy(m + i * s->len, s->ptr, (size_t)s->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }\n\n// ---------- building text: print(x) and str(x) of arrays, maps and structs ----------\n// A buffer starts as an empty str and grows on the heap.\n\nvoid finch_buf_add(FStr *b, const char *p, int64_t n) {\n if (b->len + n + 1 > b->cap) {\n int64_t cap = b->cap > 0 ? b->cap * 2 : 32;\n while (cap < b->len + n + 1) cap *= 2;\n b->ptr = must(realloc(b->cap > 0 ? b->ptr : NULL, (size_t)cap));\n b->cap = cap;\n }\n if (n) memcpy(b->ptr + b->len, p, (size_t)n);\n b->len += n;\n b->ptr[b->len] = 0;\n}\n\nstatic void buf_fmt(FStr *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));\nstatic void buf_fmt(FStr *b, const char *fmt, ...) {\n char t[64];\n va_list ap;\n va_start(ap, fmt);\n int n = vsnprintf(t, sizeof t, fmt, ap);\n va_end(ap);\n finch_buf_add(b, t, n);\n}\n\nvoid finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, \"%lld\", (long long)v); }\nvoid finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, \"%llu\", (unsigned long long)v); }\nvoid finch_buf_float(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 0));\n}\nvoid finch_buf_f32(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 1));\n}\nvoid finch_buf_bool(FStr *b, int v) { finch_buf_add(b, v ? \"true\" : \"false\", v ? 4 : 5); }\nvoid finch_buf_ptr(FStr *b, const void *p) {\n if (p) buf_fmt(b, \"0x%llx\", (unsigned long long)(uintptr_t)p);\n else finch_buf_add(b, \"null\", 4);\n}\n\nvoid finch_buf_char(FStr *b, char c, int quoted) {\n if (quoted) finch_buf_add(b, \"'\", 1);\n finch_buf_add(b, &c, 1);\n if (quoted) finch_buf_add(b, \"'\", 1);\n}\n\nvoid finch_buf_str(FStr *b, const FStr *s, int quoted) {\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n finch_buf_add(b, s->ptr, s->len);\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n}\n\n// print the buffer and free it\nvoid finch_buf_print(FStr *b) {\n if (b->len) fwrite(b->ptr, 1, (size_t)b->len, stdout);\n if (b->cap > 0) free(b->ptr);\n}\n\n// ---------- arrays ----------\n\nvoid finch_arr_reserve(FArr *a, int64_t need, int64_t es) {\n if (need <= a->cap) return;\n int64_t cap = a->cap * 2;\n if (cap < 4) cap = 4;\n if (cap < need) cap = need;\n a->ptr = must(realloc(a->cap > 0 ? a->ptr : NULL, (size_t)(cap * es)));\n a->cap = cap;\n}\n\nvoid finch_arr_make(FArr *out, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n out->ptr = len ? finch_zalloc(len * es) : NULL;\n out->len = len;\n out->cap = len;\n}\n\n// grow or shrink to `len`; new elements are zero\nvoid finch_arr_resize(FArr *a, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n finch_arr_reserve(a, len, es);\n if (len > a->len) memset((char *)a->ptr + a->len * es, 0, (size_t)((len - a->len) * es));\n a->len = len;\n}\n\n// a byte-for-byte copy of the elements (the compiler deep-copies owning elements afterwards)\nvoid finch_arr_clone_raw(FArr *out, const FArr *a, int64_t es) {\n out->len = a->len;\n out->cap = a->len;\n out->ptr = a->len ? finch_alloc(a->len * es) : NULL;\n if (a->len) memcpy(out->ptr, a->ptr, (size_t)(a->len * es));\n}\n\nvoid finch_arr_free(FArr *a) {\n if (a->cap > 0) free(a->ptr);\n}\n\n// open a hole at i (0 <= i <= len), len grows by one\nvoid finch_arr_insert_gap(FArr *a, int64_t i, int64_t es, const char *file, int64_t line) {\n if (i < 0 || i > a->len) finch_panic_index(file, line, i, a->len);\n finch_arr_reserve(a, a->len + 1, es);\n char *p = a->ptr;\n memmove(p + (i + 1) * es, p + i * es, (size_t)((a->len - i) * es));\n a->len++;\n}\n\n// close the hole at i (the element was already moved out), len shrinks by one\nvoid finch_arr_remove_gap(FArr *a, int64_t i, int64_t es) {\n char *p = a->ptr;\n memmove(p + i * es, p + (i + 1) * es, (size_t)((a->len - i - 1) * es));\n a->len--;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len) {\n finch_arr_reserve(a, a->len + 1, (int64_t)sizeof(FStr));\n str_heap((FStr *)a->ptr + a->len, p, len);\n a->len++;\n}\n\nvoid finch_bytes_from_str(FArr *out, const FStr *s) {\n out->len = out->cap = s->len;\n out->ptr = s->len ? finch_alloc(s->len) : NULL;\n if (s->len) memcpy(out->ptr, s->ptr, (size_t)s->len);\n}\n\n// ---------- program arguments, input and files ----------\n\nvoid finch_args(FArr *out, int argc, char **argv) {\n out->len = out->cap = argc;\n out->ptr = finch_alloc((int64_t)sizeof(FStr) * (argc ? argc : 1));\n for (int i = 0; i < argc; i++) str_heap((FStr *)out->ptr + i, argv[i], (int64_t)strlen(argv[i]));\n}\n\n// after main: the arguments go\nvoid finch_args_free(FArr *a) {\n for (int64_t i = 0; i < a->len; i++) finch_str_drop((FStr *)a->ptr + i);\n finch_arr_free(a);\n}\n\n// input(\"Name? \") prints the prompt and reads one line, without the newline.\nvoid finch_input(FStr *out, const FStr *prompt) {\n if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);\n fflush(stdout);\n // read one line of any length (getline isn't available everywhere)\n size_t size = 128, n = 0;\n char *line = finch_alloc((int64_t)size);\n int c, got = 0;\n while ((c = fgetc(stdin)) != EOF) {\n got = 1;\n if (c == '\\n') break;\n if (n + 1 >= size) line = must(realloc(line, size *= 2));\n line[n++] = (char)c;\n }\n if (!got) {\n free(line);\n str_static(out, empty);\n return;\n }\n while (n > 0 && line[n - 1] == '\\r') n--;\n line[n] = 0;\n out->ptr = line;\n out->len = (int64_t)n;\n out->cap = (int64_t)size;\n}\n\nint finch_file_exists(const FStr *path) {\n FILE *f = fopen(path->ptr, \"rb\");\n if (f) fclose(f);\n return f != NULL;\n}\n\nint finch_try_read_file(FStr *out, const FStr *path, FStr *err) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n file_error(err, \"read\", path);\n return 0;\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n return 1;\n}\n\nvoid finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't read the file \\\"%.*s\\\": %s\", (int)(path->len > 200 ? 200 : path->len),\n path->ptr, strerror(errno));\n finch_panic(file, line, msg);\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n}\n\n// shell(\"ls -l\"): run a command, give back its exit code\nint64_t finch_shell(const FStr *cmd) {\n fflush(stdout);\n int status = system(cmd->ptr ? cmd->ptr : \"\");\n#ifdef _WIN32\n return status; // Windows gives the exit code directly\n#else\n if (status == -1) return -1;\n if (WIFEXITED(status)) return WEXITSTATUS(status);\n return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);\n#endif\n}\n\nint finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : \"\") == 0; }\n\nint finch_try_delete_file(const FStr *path, FStr *err) {\n if (remove(path->ptr ? path->ptr : \"\") == 0) return 1;\n file_error(err, \"delete\", path);\n return 0;\n}\n\n// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here\nvoid *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; }\n\nint finch_write_file(const FStr *path, const FStr *text) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"wb\");\n if (!f) return 0;\n size_t put = text->len ? fwrite(text->ptr, 1, (size_t)text->len, f) : 0;\n return fclose(f) == 0 && put == (size_t)text->len;\n}\n\nint finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {\n errno = 0;\n if (finch_write_file(path, text)) return 1;\n if (!errno) errno = EIO;\n file_error(err, \"write\", path);\n return 0;\n}\n\n// ---------- maps ----------\n// map[K]V = { entries; len; used; cap; index; icap; adds }\n// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.\n// A removed entry keeps its slot (hash 0) until the next resize packs the slots.\n// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.\n// adds: how many keys were ever added; a `for` over the map stops with an error if it changes.\n// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.\n// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).\n\ntypedef struct {\n char *entries;\n int64_t len, used, cap;\n int64_t *index;\n int64_t icap;\n int64_t adds;\n} FMap;\n\nstatic uint64_t map_hash(const void *key, int64_t ks, int kstr) {\n const unsigned char *p;\n size_t n;\n if (kstr) {\n const FStr *s = key;\n p = (const unsigned char *)s->ptr;\n n = (size_t)s->len;\n } else {\n p = key;\n n = (size_t)ks;\n }\n uint64_t h = 1469598103934665603ULL; // FNV-1a, then mixed so the low bits spread well\n for (size_t i = 0; i < n; i++) {\n h ^= p[i];\n h *= 1099511628211ULL;\n }\n h ^= h >> 31;\n h *= 0xbf58476d1ce4e5b9ULL;\n h ^= h >> 29;\n return h | 1; // 0 marks a removed entry\n}\n\nstatic int map_key_eq(const char *a, const void *b, int64_t ks, int kstr) {\n if (!kstr) return memcmp(a, b, (size_t)ks) == 0;\n const FStr *x = (const FStr *)a, *y = b;\n return x->len == y->len && (x->len == 0 || memcmp(x->ptr, y->ptr, (size_t)x->len) == 0);\n}\n\nstatic void map_reindex(FMap *m, int64_t es) {\n if (m->len != m->used) { // pack the slots of removed entries away\n int64_t j = 0;\n for (int64_t i = 0; i < m->used; i++) {\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == 0) continue;\n if (i != j) memmove(m->entries + j * es, e, (size_t)es);\n j++;\n }\n m->used = j;\n }\n int64_t icap = 16;\n while (icap < m->cap * 2) icap *= 2;\n free(m->index);\n m->index = finch_alloc(icap * (int64_t)sizeof(int64_t));\n memset(m->index, 0xff, (size_t)icap * sizeof(int64_t));\n m->icap = icap;\n for (int64_t i = 0; i < m->used; i++) {\n uint64_t h = *(uint64_t *)(m->entries + i * es);\n int64_t s = (int64_t)(h & (uint64_t)(icap - 1));\n while (m->index[s] != -1) s = (s + 1) & (icap - 1);\n m->index[s] = i;\n }\n}\n\n// the entry number of `key`, or -1\nint64_t finch_map_find(const FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr) {\n if (m->len == 0) return -1;\n uint64_t h = map_hash(key, ks, kstr);\n for (int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));; s = (s + 1) & (m->icap - 1)) {\n int64_t i = m->index[s];\n if (i == -1) return -1;\n if (i < 0) continue;\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == h && map_key_eq(e + 8, key, ks, kstr)) return i;\n }\n}\n\n// The entry for `key`. A new one is zeroed except its hash (*added = 1); the compiler then stores the key.\nint64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr, int32_t *added) {\n int64_t i = finch_map_find(m, key, es, ks, kstr);\n *added = i < 0;\n if (i >= 0) return i;\n if (m->used == m->cap) {\n if (m->len * 4 < m->cap * 3) { // many removed slots: pack them instead of growing\n map_reindex(m, es);\n } else {\n m->cap = m->cap ? m->cap * 2 : 8;\n m->entries = must(realloc(m->entries, (size_t)(m->cap * es)));\n map_reindex(m, es);\n }\n }\n uint64_t h = map_hash(key, ks, kstr);\n i = m->used++;\n char *e = m->entries + i * es;\n memset(e, 0, (size_t)es);\n *(uint64_t *)e = h;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);\n m->index[s] = i;\n m->len++;\n m->adds++;\n return i;\n}\n\n// Forget entry i (its key and value were already dropped by the compiler).\nvoid finch_map_remove_at(FMap *m, int64_t i, int64_t es) {\n char *e = m->entries + i * es;\n uint64_t h = *(uint64_t *)e;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] != i) s = (s + 1) & (m->icap - 1);\n m->index[s] = -2;\n *(uint64_t *)e = 0;\n if (--m->len == 0) {\n m->used = 0;\n memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n }\n}\n\nvoid finch_map_clear(FMap *m) {\n m->len = m->used = 0;\n if (m->index) memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n}\n\nvoid finch_map_free(FMap *m) {\n free(m->entries);\n free(m->index);\n}\n\n// A copy of the slots and the index; the compiler then copies the keys and values that own memory.\nvoid finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {\n *out = *m;\n if (!m->cap) return;\n out->entries = finch_alloc(m->cap * es);\n memcpy(out->entries, m->entries, (size_t)(m->used * es));\n out->index = finch_alloc(m->icap * (int64_t)sizeof(int64_t));\n memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));\n}\n\n__attribute__((noreturn, cold)) void finch_map_changed(const char *file, int64_t line) {\n finch_panic(file, line, \"a key was added to the map while looping over it (collect the new keys, and add them after the loop)\");\n}\n\n__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {\n char msg[200];\n snprintf(msg, sizeof msg, \"the key %.*s is not in the map (check with .has(key), or use .get(key, default))\",\n (int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : \"\");\n finch_panic(file, line, msg);\n}\n") + list.push("// The Finch runtime: string and array helpers, input, files and runtime errors.\n// It is embedded in the compiler, compiled once into a cache folder, and linked\n// statically into every Finch program. Everything here is plain C with no state.\n//\n// Layout shared with the compiler (src/codegen.cpp):\n// str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated\n// cap > 0 : heap memory owned by the value (cap bytes allocated)\n// cap == 0 : a string literal, valid forever, never written to\n// cap == -1: borrowed from C, must be copied before it is kept\n// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0\n// map[K]V: see \"maps\" below\n\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#ifdef _WIN32\n#include \n#include \n#else\n#include \n#include \n#include \n#include \n#endif\n\ntypedef struct {\n char *ptr;\n int64_t len;\n int64_t cap;\n} FStr;\n\ntypedef struct {\n void *ptr;\n int64_t len;\n int64_t cap;\n} FArr;\n\nstatic char empty[1] = \"\";\n\n// ---------- runtime errors ----------\n\n// A program that loads Finch libraries (finch build --shared) can take runtime errors over\n// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler\n// gets the message and must not return (longjmp out, or exit itself).\nstatic void (*fatal_hook)(const char *msg);\nvoid finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; }\n\nstatic __attribute__((noreturn)) void fatal(const char *msg) {\n if (fatal_hook) fatal_hook(msg);\n fflush(stdout);\n fprintf(stderr, \"%s\\n\", msg);\n exit(1);\n}\n\n__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) {\n char full[1024];\n snprintf(full, sizeof full, \"%s:%lld: runtime error: %s\", file, (long long)line, msg);\n fflush(stdout);\n fatal(full);\n}\n\n__attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {\n char msg[128];\n snprintf(msg, sizeof msg, \"index %lld is out of range (the length is %lld)\", (long long)i, (long long)len);\n finch_panic(file, line, msg);\n}\n\nstatic void *must(void *p) {\n if (!p) {\n fflush(stdout);\n fatal(\"runtime error: out of memory\");\n }\n return p;\n}\n\nvoid *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); }\nvoid *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); }\n\n// ---------- strings ----------\n\nstatic void str_heap(FStr *out, const char *p, int64_t len) {\n char *m = finch_alloc(len + 1);\n if (len) memcpy(m, p, (size_t)len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void str_static(FStr *out, const char *lit) {\n out->ptr = (char *)lit;\n out->len = (int64_t)strlen(lit);\n out->cap = 0;\n}\n\n// A copy that can be kept: literals are shared, everything else is duplicated.\nvoid finch_str_copy(FStr *out, const FStr *s) {\n if (s->cap == 0) *out = *s;\n else str_heap(out, s->ptr, s->len);\n}\n\n// Make `s` writable (before s[i] = c).\nvoid finch_str_own(FStr *s) {\n if (s->cap > 0) return;\n FStr t;\n str_heap(&t, s->ptr, s->len);\n *s = t;\n}\n\nvoid finch_str_drop(FStr *s) {\n if (s->cap > 0) free(s->ptr);\n}\n\nvoid finch_str_from_c(FStr *out, const char *p) {\n if (!p) {\n str_static(out, empty);\n return;\n }\n out->ptr = (char *)p;\n out->len = (int64_t)strlen(p);\n out->cap = -1;\n}\n\nvoid finch_str_concat(FStr *out, const FStr *a, const FStr *b) {\n int64_t len = a->len + b->len;\n char *m = finch_alloc(len + 1);\n memcpy(m, a->ptr, (size_t)a->len);\n memcpy(m + a->len, b->ptr, (size_t)b->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nint finch_str_eq(const FStr *a, const FStr *b) {\n return a->len == b->len && memcmp(a->ptr, b->ptr, (size_t)a->len) == 0;\n}\n\nint64_t finch_str_cmp(const FStr *a, const FStr *b) {\n int64_t n = a->len < b->len ? a->len : b->len;\n int c = memcmp(a->ptr, b->ptr, (size_t)n);\n if (c) return c < 0 ? -1 : 1;\n return a->len < b->len ? -1 : a->len > b->len;\n}\n\nvoid finch_str_from_int(FStr *out, int64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%lld\", (long long)v);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_uint(FStr *out, uint64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%llu\", (unsigned long long)v);\n str_heap(out, buf, n);\n}\n\n// The shortest text that reads back as the same number (as an f32 when `single`):\n// 0.1 -> \"0.1\", 1.0/3 -> \"0.3333333333333333\", 1e8 -> \"100000000\", 1e20 -> \"1e+20\".\nstatic int fmt_float(char *buf, size_t size, double v, int single) {\n if (isnan(v)) return snprintf(buf, size, \"nan\");\n if (isinf(v)) return snprintf(buf, size, v < 0 ? \"-inf\" : \"inf\");\n int p = 1;\n for (; p < 17; p++) {\n snprintf(buf, size, \"%.*e\", p - 1, v);\n double back = strtod(buf, NULL);\n if (single ? (float)back == (float)v : back == v) break;\n }\n int exp = atoi(strchr(buf, 'e') + 1);\n if (exp < -5 || exp >= 17) return snprintf(buf, size, \"%.*g\", p, v);\n int decimals = p - 1 - exp;\n return snprintf(buf, size, \"%.*f\", decimals > 0 ? decimals : 0, v);\n}\n\nvoid finch_str_from_float(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 0);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_f32(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 1);\n str_heap(out, buf, n);\n}\n\n// str(x, 2): a fixed number of digits after the point\nvoid finch_str_fixed(FStr *out, double v, int64_t digits) {\n char buf[400];\n if (digits < 0) digits = 0;\n if (digits > 60) digits = 60;\n int n = snprintf(buf, sizeof buf, \"%.*f\", (int)digits, v);\n str_heap(out, buf, n);\n}\n\nvoid finch_print_float(double v, int32_t single) {\n char buf[64];\n fmt_float(buf, sizeof buf, v, single);\n fputs(buf, stdout);\n}\n\nvoid finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }\n\nvoid finch_str_from_bool(FStr *out, int b) { str_static(out, b ? \"true\" : \"false\"); }\n\nstatic void number_error(char *msg, size_t size, const FStr *s, const char *kind) {\n snprintf(msg, size, \"can't turn \\\"%.*s\\\" into %s\", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : \"\", kind);\n}\n\nstatic int parse_int(const FStr *s, int64_t *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n errno = 0;\n long long v = strtoll(p, &end, 10);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end || errno == ERANGE);\n}\n\nstatic int parse_float(const FStr *s, double *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n double v = strtod(p, &end);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end);\n}\n\nint64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {\n int64_t v;\n if (!parse_int(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\ndouble finch_str_to_float(const FStr *s, const char *file, int64_t line) {\n double v;\n if (!parse_float(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\n// ---------- errors as values: int(s) or 0, try read_file(p) ... ----------\n// Each gives back 1 on success; on failure 0, with the message in *err.\n\nstatic void set_error(FStr *err, const char *msg) { str_heap(err, msg, (int64_t)strlen(msg)); }\n\nint finch_str_try_int(const FStr *s, int64_t *out, FStr *err) {\n if (parse_int(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nint finch_str_try_float(const FStr *s, double *out, FStr *err) {\n if (parse_float(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nstatic void file_error(FStr *err, const char *what, const FStr *path) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't %s the file \\\"%.*s\\\": %s\", what, (int)(path->len > 200 ? 200 : path->len),\n path->ptr ? path->ptr : \"\", strerror(errno));\n set_error(err, msg);\n}\n\n// when `fn main() -> !` fails\nvoid finch_report_error(const FStr *msg) {\n fflush(stdout);\n fprintf(stderr, \"error: %.*s\\n\", (int)msg->len, msg->ptr ? msg->ptr : \"\");\n finch_str_drop((FStr *)msg);\n}\n\nvoid finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {\n if (start < 0 || end > s->len || start > end) {\n char msg[160];\n snprintf(msg, sizeof msg, \"sub(%lld, %lld) is out of range (the length is %lld)\", (long long)start,\n (long long)end, (long long)s->len);\n finch_panic(file, line, msg);\n }\n str_heap(out, s->ptr + start, end - start);\n}\n\nint64_t finch_str_find(const FStr *s, const FStr *t) {\n if (t->len == 0) return 0;\n for (int64_t i = 0; i + t->len <= s->len; i++)\n if (s->ptr[i] == t->ptr[0] && memcmp(s->ptr + i, t->ptr, (size_t)t->len) == 0) return i;\n return -1;\n}\n\nint finch_str_starts(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr, t->ptr, (size_t)t->len) == 0;\n}\n\nint finch_str_ends(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr + s->len - t->len, t->ptr, (size_t)t->len) == 0;\n}\n\nvoid finch_str_trim(FStr *out, const FStr *s) {\n int64_t a = 0, b = s->len;\n while (a < b && isspace((unsigned char)s->ptr[a])) a++;\n while (b > a && isspace((unsigned char)s->ptr[b - 1])) b--;\n str_heap(out, s->ptr + a, b - a);\n}\n\nvoid finch_str_upper(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)toupper((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_lower(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)tolower((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_replace(FStr *out, const FStr *s, const FStr *from, const FStr *to) {\n if (from->len == 0) {\n str_heap(out, s->ptr, s->len);\n return;\n }\n int64_t count = 0;\n for (int64_t i = 0; i + from->len <= s->len;) {\n if (memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n count++;\n i += from->len;\n } else {\n i++;\n }\n }\n int64_t len = s->len + count * (to->len - from->len);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < s->len;) {\n if (i + from->len <= s->len && memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n memcpy(w, to->ptr, (size_t)to->len);\n w += to->len;\n i += from->len;\n } else {\n *w++ = s->ptr[i++];\n }\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len);\n\n// \"a,b,,c\".split(\",\") -> [\"a\", \"b\", \"\", \"c\"]; split(\"\") gives single characters.\nvoid finch_str_split(FArr *out, const FStr *s, const FStr *sep) {\n out->ptr = NULL;\n out->len = out->cap = 0;\n if (sep->len == 0) {\n for (int64_t i = 0; i < s->len; i++) arr_push_str(out, s->ptr + i, 1);\n return;\n }\n int64_t start = 0;\n for (int64_t i = 0; i + sep->len <= s->len;) {\n if (memcmp(s->ptr + i, sep->ptr, (size_t)sep->len) == 0) {\n arr_push_str(out, s->ptr + start, i - start);\n i += sep->len;\n start = i;\n } else {\n i++;\n }\n }\n arr_push_str(out, s->ptr + start, s->len - start);\n}\n\nvoid finch_str_join(FStr *out, const FArr *parts, const FStr *sep) {\n const FStr *p = parts->ptr;\n int64_t len = 0;\n for (int64_t i = 0; i < parts->len; i++) len += p[i].len + (i ? sep->len : 0);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < parts->len; i++) {\n if (i) {\n memcpy(w, sep->ptr, (size_t)sep->len);\n w += sep->len;\n }\n memcpy(w, p[i].ptr, (size_t)p[i].len);\n w += p[i].len;\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_repeat(FStr *out, const FStr *s, int64_t n) {\n if (n < 0) n = 0;\n int64_t len = s->len * n;\n char *m = finch_alloc(len + 1);\n for (int64_t i = 0; i < n; i++) memcpy(m + i * s->len, s->ptr, (size_t)s->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }\n\n// ---------- building text: print(x) and str(x) of arrays, maps and structs ----------\n// A buffer starts as an empty str and grows on the heap.\n\nvoid finch_buf_add(FStr *b, const char *p, int64_t n) {\n if (b->len + n + 1 > b->cap) {\n int64_t cap = b->cap > 0 ? b->cap * 2 : 32;\n while (cap < b->len + n + 1) cap *= 2;\n b->ptr = must(realloc(b->cap > 0 ? b->ptr : NULL, (size_t)cap));\n b->cap = cap;\n }\n if (n) memcpy(b->ptr + b->len, p, (size_t)n);\n b->len += n;\n b->ptr[b->len] = 0;\n}\n\nstatic void buf_fmt(FStr *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));\nstatic void buf_fmt(FStr *b, const char *fmt, ...) {\n char t[64];\n va_list ap;\n va_start(ap, fmt);\n int n = vsnprintf(t, sizeof t, fmt, ap);\n va_end(ap);\n finch_buf_add(b, t, n);\n}\n\nvoid finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, \"%lld\", (long long)v); }\nvoid finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, \"%llu\", (unsigned long long)v); }\nvoid finch_buf_float(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 0));\n}\nvoid finch_buf_f32(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 1));\n}\nvoid finch_buf_bool(FStr *b, int v) { finch_buf_add(b, v ? \"true\" : \"false\", v ? 4 : 5); }\nvoid finch_buf_ptr(FStr *b, const void *p) {\n if (p) buf_fmt(b, \"0x%llx\", (unsigned long long)(uintptr_t)p);\n else finch_buf_add(b, \"null\", 4);\n}\n\nvoid finch_buf_char(FStr *b, char c, int quoted) {\n if (quoted) finch_buf_add(b, \"'\", 1);\n finch_buf_add(b, &c, 1);\n if (quoted) finch_buf_add(b, \"'\", 1);\n}\n\nvoid finch_buf_str(FStr *b, const FStr *s, int quoted) {\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n finch_buf_add(b, s->ptr, s->len);\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n}\n\n// print the buffer and free it\nvoid finch_buf_print(FStr *b) {\n if (b->len) fwrite(b->ptr, 1, (size_t)b->len, stdout);\n if (b->cap > 0) free(b->ptr);\n}\n\n// ---------- arrays ----------\n\nvoid finch_arr_reserve(FArr *a, int64_t need, int64_t es) {\n if (need <= a->cap) return;\n int64_t cap = a->cap * 2;\n if (cap < 4) cap = 4;\n if (cap < need) cap = need;\n a->ptr = must(realloc(a->cap > 0 ? a->ptr : NULL, (size_t)(cap * es)));\n a->cap = cap;\n}\n\nvoid finch_arr_make(FArr *out, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n out->ptr = len ? finch_zalloc(len * es) : NULL;\n out->len = len;\n out->cap = len;\n}\n\n// grow or shrink to `len`; new elements are zero\nvoid finch_arr_resize(FArr *a, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n finch_arr_reserve(a, len, es);\n if (len > a->len) memset((char *)a->ptr + a->len * es, 0, (size_t)((len - a->len) * es));\n a->len = len;\n}\n\n// a byte-for-byte copy of the elements (the compiler deep-copies owning elements afterwards)\nvoid finch_arr_clone_raw(FArr *out, const FArr *a, int64_t es) {\n out->len = a->len;\n out->cap = a->len;\n out->ptr = a->len ? finch_alloc(a->len * es) : NULL;\n if (a->len) memcpy(out->ptr, a->ptr, (size_t)(a->len * es));\n}\n\nvoid finch_arr_free(FArr *a) {\n if (a->cap > 0) free(a->ptr);\n}\n\n// open a hole at i (0 <= i <= len), len grows by one\nvoid finch_arr_insert_gap(FArr *a, int64_t i, int64_t es, const char *file, int64_t line) {\n if (i < 0 || i > a->len) finch_panic_index(file, line, i, a->len);\n finch_arr_reserve(a, a->len + 1, es);\n char *p = a->ptr;\n memmove(p + (i + 1) * es, p + i * es, (size_t)((a->len - i) * es));\n a->len++;\n}\n\n// close the hole at i (the element was already moved out), len shrinks by one\nvoid finch_arr_remove_gap(FArr *a, int64_t i, int64_t es) {\n char *p = a->ptr;\n memmove(p + i * es, p + (i + 1) * es, (size_t)((a->len - i - 1) * es));\n a->len--;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len) {\n finch_arr_reserve(a, a->len + 1, (int64_t)sizeof(FStr));\n str_heap((FStr *)a->ptr + a->len, p, len);\n a->len++;\n}\n\nvoid finch_bytes_from_str(FArr *out, const FStr *s) {\n out->len = out->cap = s->len;\n out->ptr = s->len ? finch_alloc(s->len) : NULL;\n if (s->len) memcpy(out->ptr, s->ptr, (size_t)s->len);\n}\n\n// ---------- program arguments, input and files ----------\n\nvoid finch_args(FArr *out, int argc, char **argv) {\n out->len = out->cap = argc;\n out->ptr = finch_alloc((int64_t)sizeof(FStr) * (argc ? argc : 1));\n for (int i = 0; i < argc; i++) str_heap((FStr *)out->ptr + i, argv[i], (int64_t)strlen(argv[i]));\n}\n\n// after main: the arguments go\nvoid finch_args_free(FArr *a) {\n for (int64_t i = 0; i < a->len; i++) finch_str_drop((FStr *)a->ptr + i);\n finch_arr_free(a);\n}\n\n// input(\"Name? \") prints the prompt and reads one line, without the newline.\nvoid finch_input(FStr *out, const FStr *prompt) {\n if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);\n fflush(stdout);\n // read one line of any length (getline isn't available everywhere)\n size_t size = 128, n = 0;\n char *line = finch_alloc((int64_t)size);\n int c, got = 0;\n while ((c = fgetc(stdin)) != EOF) {\n got = 1;\n if (c == '\\n') break;\n if (n + 1 >= size) line = must(realloc(line, size *= 2));\n line[n++] = (char)c;\n }\n if (!got) {\n free(line);\n str_static(out, empty);\n return;\n }\n while (n > 0 && line[n - 1] == '\\r') n--;\n line[n] = 0;\n out->ptr = line;\n out->len = (int64_t)n;\n out->cap = (int64_t)size;\n}\n\nint finch_file_exists(const FStr *path) {\n FILE *f = fopen(path->ptr, \"rb\");\n if (f) fclose(f);\n return f != NULL;\n}\n\nint finch_try_read_file(FStr *out, const FStr *path, FStr *err) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n file_error(err, \"read\", path);\n return 0;\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n return 1;\n}\n\nvoid finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't read the file \\\"%.*s\\\": %s\", (int)(path->len > 200 ? 200 : path->len),\n path->ptr, strerror(errno));\n finch_panic(file, line, msg);\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n}\n\n// shell(\"ls -l\"): run a command, give back its exit code\nint64_t finch_shell(const FStr *cmd) {\n fflush(stdout);\n int status = system(cmd->ptr ? cmd->ptr : \"\");\n#ifdef _WIN32\n return status; // Windows gives the exit code directly\n#else\n if (status == -1) return -1;\n if (WIFEXITED(status)) return WEXITSTATUS(status);\n return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);\n#endif\n}\n\nint finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : \"\") == 0; }\n\nint finch_try_delete_file(const FStr *path, FStr *err) {\n if (remove(path->ptr ? path->ptr : \"\") == 0) return 1;\n file_error(err, \"delete\", path);\n return 0;\n}\n\n// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here\nvoid *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; }\n\nint finch_write_file(const FStr *path, const FStr *text) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"wb\");\n if (!f) return 0;\n size_t put = text->len ? fwrite(text->ptr, 1, (size_t)text->len, f) : 0;\n return fclose(f) == 0 && put == (size_t)text->len;\n}\n\nint finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {\n errno = 0;\n if (finch_write_file(path, text)) return 1;\n if (!errno) errno = EIO;\n file_error(err, \"write\", path);\n return 0;\n}\n\n// ---------- folders (std/fs.fch, through std/finch_fs.h) ----------\n\nint finch_fs_is_dir(const char *path) {\n#ifdef _WIN32\n struct _finddata_t d;\n intptr_t h = _findfirst(path, &d);\n if (h == -1) return 0;\n _findclose(h);\n return (d.attrib & _A_SUBDIR) != 0;\n#else\n struct stat st;\n return stat(path, &st) == 0 && S_ISDIR(st.st_mode);\n#endif\n}\n\nint finch_fs_exists(const char *path) {\n#ifdef _WIN32\n return _access(path, 0) == 0;\n#else\n struct stat st;\n return stat(path, &st) == 0;\n#endif\n}\n\nstatic int make_one_dir(const char *p) {\n#ifdef _WIN32\n return _mkdir(p) == 0 || errno == EEXIST;\n#else\n return mkdir(p, 0777) == 0 || errno == EEXIST;\n#endif\n}\n\n// mkdir -p: the parents too; 1 if the folder is there afterwards\nint finch_fs_mkdir(const char *path) {\n size_t n = strlen(path);\n char *p = finch_alloc((int64_t)n + 1);\n memcpy(p, path, n + 1);\n for (size_t i = 1; i < n; i++) {\n if (p[i] == '/' || p[i] == '\\\\') {\n if (p[i - 1] == ':') continue; /* C: is a drive */\n char c = p[i];\n p[i] = 0;\n make_one_dir(p);\n p[i] = c;\n }\n }\n int ok = make_one_dir(p) && finch_fs_is_dir(p);\n free(p);\n return ok;\n}\n\nint finch_fs_remove_dir(const char *path) {\n#ifdef _WIN32\n return _rmdir(path) == 0;\n#else\n return rmdir(path) == 0;\n#endif\n}\n\n// the names in a folder, one per call (\"\" at the end), without . and ..\ntypedef struct {\n#ifdef _WIN32\n intptr_t h;\n struct _finddata_t d;\n int first;\n#else\n DIR *d;\n#endif\n} FsList;\n\nvoid *finch_fs_list_open(const char *path) {\n FsList *l = finch_zalloc((int64_t)sizeof(FsList));\n#ifdef _WIN32\n size_t n = strlen(path);\n char *pat = finch_alloc((int64_t)n + 3);\n memcpy(pat, path, n);\n memcpy(pat + n, \"\\\\*\", 3);\n l->h = _findfirst(pat, &l->d);\n free(pat);\n if (l->h == -1) {\n free(l);\n return NULL;\n }\n l->first = 1;\n#else\n l->d = opendir(path);\n if (!l->d) {\n free(l);\n return NULL;\n }\n#endif\n return l;\n}\n\nconst char *finch_fs_list_next(void *handle) {\n FsList *l = handle;\n for (;;) {\n#ifdef _WIN32\n if (!l->first && _findnext(l->h, &l->d) != 0) return \"\";\n l->first = 0;\n const char *name = l->d.name;\n#else\n struct dirent *e = readdir(l->d);\n if (!e) return \"\";\n const char *name = e->d_name;\n#endif\n if (strcmp(name, \".\") != 0 && strcmp(name, \"..\") != 0) return name;\n }\n}\n\nvoid finch_fs_list_close(void *handle) {\n FsList *l = handle;\n#ifdef _WIN32\n _findclose(l->h);\n#else\n closedir(l->d);\n#endif\n free(l);\n}\n\n// where a program keeps its own files for this user (saves, settings); \"\" if unknown\n// Linux: $XDG_DATA_HOME or ~/.local/share macOS: ~/Library/Application Support Windows: %APPDATA%\nconst char *finch_fs_data_home(void) {\n static char buf[4096];\n const char *v;\n#if defined(_WIN32)\n v = getenv(\"APPDATA\");\n snprintf(buf, sizeof buf, \"%s\", v ? v : \"\");\n#elif defined(__APPLE__)\n v = getenv(\"HOME\");\n if (v) snprintf(buf, sizeof buf, \"%s/Library/Application Support\", v);\n else buf[0] = 0;\n#else\n v = getenv(\"XDG_DATA_HOME\");\n if (v && *v) snprintf(buf, sizeof buf, \"%s\", v);\n else if ((v = getenv(\"HOME\"))) snprintf(buf, sizeof buf, \"%s/.local/share\", v);\n else buf[0] = 0;\n#endif\n return buf;\n}\n\n// ---------- maps ----------\n// map[K]V = { entries; len; used; cap; index; icap; adds }\n// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.\n// A removed entry keeps its slot (hash 0) until the next resize packs the slots.\n// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.\n// adds: how many keys were ever added; a `for` over the map stops with an error if it changes.\n// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.\n// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).\n\ntypedef struct {\n char *entries;\n int64_t len, used, cap;\n int64_t *index;\n int64_t icap;\n int64_t adds;\n} FMap;\n\nstatic uint64_t map_hash(const void *key, int64_t ks, int kstr) {\n const unsigned char *p;\n size_t n;\n if (kstr) {\n const FStr *s = key;\n p = (const unsigned char *)s->ptr;\n n = (size_t)s->len;\n } else {\n p = key;\n n = (size_t)ks;\n }\n uint64_t h = 1469598103934665603ULL; // FNV-1a, then mixed so the low bits spread well\n for (size_t i = 0; i < n; i++) {\n h ^= p[i];\n h *= 1099511628211ULL;\n }\n h ^= h >> 31;\n h *= 0xbf58476d1ce4e5b9ULL;\n h ^= h >> 29;\n return h | 1; // 0 marks a removed entry\n}\n\nstatic int map_key_eq(const char *a, const void *b, int64_t ks, int kstr) {\n if (!kstr) return memcmp(a, b, (size_t)ks) == 0;\n const FStr *x = (const FStr *)a, *y = b;\n return x->len == y->len && (x->len == 0 || memcmp(x->ptr, y->ptr, (size_t)x->len) == 0);\n}\n\nstatic void map_reindex(FMap *m, int64_t es) {\n if (m->len != m->used) { // pack the slots of removed entries away\n int64_t j = 0;\n for (int64_t i = 0; i < m->used; i++) {\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == 0) continue;\n if (i != j) memmove(m->entries + j * es, e, (size_t)es);\n j++;\n }\n m->used = j;\n }\n int64_t icap = 16;\n while (icap < m->cap * 2) icap *= 2;\n free(m->index);\n m->index = finch_alloc(icap * (int64_t)sizeof(int64_t));\n memset(m->index, 0xff, (size_t)icap * sizeof(int64_t));\n m->icap = icap;\n for (int64_t i = 0; i < m->used; i++) {\n uint64_t h = *(uint64_t *)(m->entries + i * es);\n int64_t s = (int64_t)(h & (uint64_t)(icap - 1));\n while (m->index[s] != -1) s = (s + 1) & (icap - 1);\n m->index[s] = i;\n }\n}\n\n// the entry number of `key`, or -1\nint64_t finch_map_find(const FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr) {\n if (m->len == 0) return -1;\n uint64_t h = map_hash(key, ks, kstr);\n for (int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));; s = (s + 1) & (m->icap - 1)) {\n int64_t i = m->index[s];\n if (i == -1) return -1;\n if (i < 0) continue;\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == h && map_key_eq(e + 8, key, ks, kstr)) return i;\n }\n}\n\n// The entry for `key`. A new one is zeroed except its hash (*added = 1); the compiler then stores the key.\nint64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr, int32_t *added) {\n int64_t i = finch_map_find(m, key, es, ks, kstr);\n *added = i < 0;\n if (i >= 0) return i;\n if (m->used == m->cap) {\n if (m->len * 4 < m->cap * 3) { // many removed slots: pack them instead of growing\n map_reindex(m, es);\n } else {\n m->cap = m->cap ? m->cap * 2 : 8;\n m->entries = must(realloc(m->entries, (size_t)(m->cap * es)));\n map_reindex(m, es);\n }\n }\n uint64_t h = map_hash(key, ks, kstr);\n i = m->used++;\n char *e = m->entries + i * es;\n memset(e, 0, (size_t)es);\n *(uint64_t *)e = h;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);\n m->index[s] = i;\n m->len++;\n m->adds++;\n return i;\n}\n\n// Forget entry i (its key and value were already dropped by the compiler).\nvoid finch_map_remove_at(FMap *m, int64_t i, int64_t es) {\n char *e = m->entries + i * es;\n uint64_t h = *(uint64_t *)e;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] != i) s = (s + 1) & (m->icap - 1);\n m->index[s] = -2;\n *(uint64_t *)e = 0;\n if (--m->len == 0) {\n m->used = 0;\n memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n }\n}\n\nvoid finch_map_clear(FMap *m) {\n m->len = m->used = 0;\n if (m->index) memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n}\n\nvoid finch_map_free(FMap *m) {\n free(m->entries);\n free(m->index);\n}\n\n// A copy of the slots and the index; the compiler then copies the keys and values that own memory.\nvoid finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {\n *out = *m;\n if (!m->cap) return;\n out->entries = finch_alloc(m->cap * es);\n memcpy(out->entries, m->entries, (size_t)(m->used * es));\n out->index = finch_alloc(m->icap * (int64_t)sizeof(int64_t));\n memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));\n}\n\n__attribute__((noreturn, cold)) void finch_map_changed(const char *file, int64_t line) {\n finch_panic(file, line, \"a key was added to the map while looping over it (collect the new keys, and add them after the loop)\");\n}\n\n__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {\n char msg[200];\n snprintf(msg, sizeof msg, \"the key %.*s is not in the map (check with .has(key), or use .get(key, default))\",\n (int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : \"\");\n finch_panic(file, line, msg);\n}\n") + list.push("// Files and folders: bytes, folders, listing, and a place for a program's own files.\n//\n// import fs\n// data := try fs.read_bytes(\"atlas.png\") the whole file as []u8 (any bytes, NUL included)\n// try fs.write_bytes(\"out.bin\", data)\n// try fs.mkdir(\"saves/slot1\") with the folders above it\n// names := try fs.list(\"saves\") the names in a folder, sorted\n// fs.exists(p) fs.is_dir(p) try fs.remove_dir(p)\n// dir := try fs.data_dir(\"MyGame\") a folder for this user's saves, made if needed\nimport \"finch_fs.h\"\n\nfn read_bytes(str path) -> []u8! {\n text := try read_file(path)\n return text.bytes()\n}\n\nfn write_bytes(str path, []u8 data) -> ! {\n try write_file(path, str(data))\n}\n\nfn exists(str path) -> bool {\n return finch_fs_exists(path) != 0\n}\n\nfn is_dir(str path) -> bool {\n return finch_fs_is_dir(path) != 0\n}\n\nfn mkdir(str path) -> ! {\n if finch_fs_mkdir(path) == 0 {\n return error(\"can't make the folder \\\"\" + path + \"\\\"\")\n }\n}\n\n// only an empty folder\nfn remove_dir(str path) -> ! {\n if finch_fs_remove_dir(path) == 0 {\n return error(\"can't remove the folder \\\"\" + path + \"\\\" (is it empty?)\")\n }\n}\n\nfn list(str path) -> []str! {\n l := finch_fs_list_open(path)\n if l == null {\n return error(\"can't list the folder \\\"\" + path + \"\\\"\")\n }\n []str names\n while true {\n name := finch_fs_list_next(l)\n if name.len == 0 {\n break\n }\n names.push(name)\n }\n finch_fs_list_close(l)\n names.sort()\n return names\n}\n\n// The folder where a program keeps this user's files (saves, settings), made if it isn't there:\n// ~/.local/share/ on Linux, ~/Library/Application Support/ on macOS, %APPDATA%\\ on Windows.\nfn data_dir(str app) -> str! {\n home := finch_fs_data_home()\n if home.len == 0 {\n return error(\"this system doesn't say where programs keep their files\")\n }\n dir := home + \"/\" + app\n try mkdir(dir)\n return dir\n}\n") list.push("// JSON: read it, build it, write it.\n//\n// import json\n//\n// doc := try json.parse(\"{\\\"name\\\": \\\"Finch\\\", \\\"tags\\\": [\\\"fast\\\", \\\"small\\\"], \\\"stars\\\": 5}\")\n// print(doc.get(\"name\").str_or(\"\")) Finch\n// print(doc.get(\"tags\").at(1).str_or(\"\")) small\n// print(doc.get(\"stars\").int_or(0)) 5\n// print(doc.get(\"missing\").is_null()) true (a missing key is null, so chains never fail)\n//\n// out := json.new_object()\n// out.set(\"ok\", json.new_bool(true))\n// list := json.new_list()\n// list.push(json.new_number(1.5))\n// out.set(\"values\", list)\n// print(json.stringify(out)) {\"ok\":true,\"values\":[1.5]}\n// print(json.pretty(out)) the same, over several lines\n\nenum Kind { Null, Bool, Number, Text, List, Object }\n\n// One JSON value. `kind` says which of the other fields it uses: flag for Bool, num for Number,\n// text for Text, items for List, and items with keys (the same length, in the same order) for Object.\nstruct Json {\n Kind kind\n bool flag\n float num\n str text\n []Json items\n []str keys\n}\n\nfn new_null() -> Json {\n return Json(kind: Kind.Null)\n}\n\nfn new_bool(bool b) -> Json {\n return Json(kind: Kind.Bool, flag: b)\n}\n\nfn new_number(float n) -> Json {\n return Json(kind: Kind.Number, num: n)\n}\n\nfn new_text(str s) -> Json {\n return Json(kind: Kind.Text, text: s)\n}\n\nfn new_list() -> Json {\n return Json(kind: Kind.List)\n}\n\nfn new_object() -> Json {\n return Json(kind: Kind.Object)\n}\n\n// ---------- looking inside ----------\n\nfn Json.is_null() -> bool {\n return self.kind == Kind.Null\n}\n\n// the number of elements of a list, of keys of an object, of bytes of a text; 0 for the rest\nfn Json.len() -> int {\n return match self.kind {\n List, Object -> self.items.len\n Text -> self.text.len\n else -> 0\n }\n}\n\nfn Json.has(str key) -> bool {\n return self.kind == Kind.Object && self.keys.contains(key)\n}\n\n// the value of a key of an object; null if there is none (or this is not an object)\nfn Json.get(str key) -> Json {\n if self.kind != Kind.Object {\n return new_null()\n }\n i := self.keys.find(key)\n if i < 0 {\n return new_null()\n }\n return self.items[i]\n}\n\n// the element of a list; null if there is none\nfn Json.at(int i) -> Json {\n if self.kind != Kind.List || i < 0 || i >= self.items.len {\n return new_null()\n }\n return self.items[i]\n}\n\n// the value if it is of that kind, else what was given\nfn Json.str_or(str fallback) -> str {\n if self.kind == Kind.Text {\n return self.text\n }\n return fallback\n}\n\nfn Json.float_or(float fallback) -> float {\n if self.kind == Kind.Number {\n return self.num\n }\n return fallback\n}\n\nfn Json.int_or(int fallback) -> int {\n if self.kind == Kind.Number {\n return int(self.num)\n }\n return fallback\n}\n\nfn Json.bool_or(bool fallback) -> bool {\n if self.kind == Kind.Bool {\n return self.flag\n }\n return fallback\n}\n\n// the keys of an object, in the order they were written\nfn Json.names() -> []str {\n return self.keys\n}\n\n// ---------- building ----------\n\n// add to the end of a list\nfn Json.push(Json v) {\n self.items.push(v)\n}\n\n// give a key a value in an object; a key that is already there is replaced\nfn Json.set(str key, Json v) {\n i := self.keys.find(key)\n if i >= 0 {\n self.items[i] = v\n return\n }\n self.keys.push(key)\n self.items.push(v)\n}\n\n// ---------- writing ----------\n\nfn quote(str s) -> str {\n []str parts = [\"\\\"\"]\n for c in s {\n n := int(c)\n if c == '\"' {\n parts.push(\"\\\\\\\"\")\n } else if c == '\\\\' {\n parts.push(\"\\\\\\\\\")\n } else if c == '\\n' {\n parts.push(\"\\\\n\")\n } else if c == '\\r' {\n parts.push(\"\\\\r\")\n } else if c == '\\t' {\n parts.push(\"\\\\t\")\n } else if n < 32 {\n parts.push(\"\\\\u00\" + hexdigit(n / 16) + hexdigit(n % 16))\n } else {\n parts.push(str(c))\n }\n }\n parts.push(\"\\\"\")\n return parts.join(\"\")\n}\n\nfn hexdigit(int n) -> str {\n digits := \"0123456789abcdef\"\n return str(digits[n])\n}\n\nfn number_text(float n) -> str {\n if n == float(int(n)) && n > -1000000000000000.0 && n < 1000000000000000.0 {\n return str(int(n))\n }\n return str(n)\n}\n\nstruct Writer {\n []str out\n}\n\nfn write(Json j, str indent, str pad, ptr[Writer] w) {\n nl := \"\"\n inner := \"\"\n sep := \",\"\n colon := \":\"\n if indent.len > 0 {\n nl = \"\\n\"\n inner = pad + indent\n colon = \": \"\n }\n match j.kind {\n Null -> w.out.push(\"null\")\n Bool -> {\n if j.flag {\n w.out.push(\"true\")\n } else {\n w.out.push(\"false\")\n }\n }\n Number -> w.out.push(number_text(j.num))\n Text -> w.out.push(quote(j.text))\n List -> {\n if j.items.len == 0 {\n w.out.push(\"[]\")\n return\n }\n w.out.push(\"[\" + nl)\n for i, item in j.items {\n if i > 0 {\n w.out.push(sep + nl)\n }\n w.out.push(inner)\n write(item, indent, inner, w)\n }\n w.out.push(nl + pad + \"]\")\n }\n Object -> {\n if j.items.len == 0 {\n w.out.push(\"{}\")\n return\n }\n w.out.push(\"{\" + nl)\n for i, item in j.items {\n if i > 0 {\n w.out.push(sep + nl)\n }\n w.out.push(inner + quote(j.keys[i]) + colon)\n write(item, indent, inner, w)\n }\n w.out.push(nl + pad + \"}\")\n }\n }\n}\n\n// the value as text on one line\nfn stringify(Json j) -> str {\n w := Writer()\n write(j, \"\", \"\", addr(w))\n return w.out.join(\"\")\n}\n\n// the value as text over several lines, indented by two spaces\nfn pretty(Json j) -> str {\n w := Writer()\n write(j, \" \", \"\", addr(w))\n return w.out.join(\"\")\n}\n\n// ---------- reading ----------\n\nstruct Reader {\n str src\n int i\n}\n\n// the line and column of a position, for a message\nfn where(ptr[Reader] r) -> str {\n line := 1\n col := 1\n for k in 0..r.i {\n if k < r.src.len && r.src[k] == '\\n' {\n line += 1\n col = 1\n } else {\n col += 1\n }\n }\n return \"line \" + str(line) + \", column \" + str(col)\n}\n\nfn peek(ptr[Reader] r) -> char {\n if r.i < r.src.len {\n return r.src[r.i]\n }\n return char(0)\n}\n\nfn skip_space(ptr[Reader] r) {\n while r.i < r.src.len && (r.src[r.i] == ' ' || r.src[r.i] == '\\n' || r.src[r.i] == '\\t' || r.src[r.i] == '\\r') {\n r.i += 1\n }\n}\n\nfn unexpected(ptr[Reader] r, str wanted) -> str {\n if r.i >= r.src.len {\n return \"json: the text ends where \" + wanted + \" should be (\" + where(r) + \")\"\n }\n return \"json: found '\" + str(r.src[r.i]) + \"' where \" + wanted + \" should be (\" + where(r) + \")\"\n}\n\n// does `word` come next?\nfn comes(ptr[Reader] r, str word) -> bool {\n return r.src.len - r.i >= word.len && r.src.sub(r.i, r.i + word.len) == word\n}\n\nfn expect_word(ptr[Reader] r, str word) -> ! {\n if !comes(r, word) {\n return error(unexpected(r, \"'\" + word + \"'\"))\n }\n r.i += word.len\n}\n\nfn hex_value(char c) -> int {\n if c >= '0' && c <= '9' {\n return int(c) - int('0')\n }\n if c >= 'a' && c <= 'f' {\n return int(c) - int('a') + 10\n }\n if c >= 'A' && c <= 'F' {\n return int(c) - int('A') + 10\n }\n return -1\n}\n\n// a code point as UTF-8\nfn utf8(int cp) -> str {\n []u8 b\n if cp < 128 {\n b.push(u8(cp))\n } else if cp < 2048 {\n b.push(u8(192 + cp / 64))\n b.push(u8(128 + cp % 64))\n } else if cp < 65536 {\n b.push(u8(224 + cp / 4096))\n b.push(u8(128 + (cp / 64) % 64))\n b.push(u8(128 + cp % 64))\n } else {\n b.push(u8(240 + cp / 262144))\n b.push(u8(128 + (cp / 4096) % 64))\n b.push(u8(128 + (cp / 64) % 64))\n b.push(u8(128 + cp % 64))\n }\n return str(b)\n}\n\nfn read_hex4(ptr[Reader] r) -> int! {\n n := 0\n for k in 0..4 {\n if r.i >= r.src.len || hex_value(r.src[r.i]) < 0 {\n return error(unexpected(r, \"a hexadecimal digit\"))\n }\n n = n * 16 + hex_value(r.src[r.i])\n r.i += 1\n }\n return n\n}\n\nfn read_string(ptr[Reader] r) -> str! {\n r.i += 1 // the opening quote\n []str parts\n start := r.i\n while true {\n if r.i >= r.src.len {\n return error(\"json: a text is never closed (\" + where(r) + \")\")\n }\n c := r.src[r.i]\n if c == '\"' {\n parts.push(r.src.sub(start, r.i))\n r.i += 1\n return parts.join(\"\")\n }\n if int(c) < 32 {\n return error(\"json: a line break or control character inside a text (\" + where(r) + \")\")\n }\n if c != '\\\\' {\n r.i += 1\n continue\n }\n parts.push(r.src.sub(start, r.i))\n r.i += 1\n e := peek(r)\n r.i += 1\n if e == 'n' {\n parts.push(\"\\n\")\n } else if e == 't' {\n parts.push(\"\\t\")\n } else if e == 'r' {\n parts.push(\"\\r\")\n } else if e == 'b' {\n parts.push(utf8(8))\n } else if e == 'f' {\n parts.push(utf8(12))\n } else if e == '\"' || e == '\\\\' || e == '/' {\n parts.push(str(e))\n } else if e == 'u' {\n cp := try read_hex4(r)\n // a pair of 😀 stands for one character above U+FFFF\n if cp >= 55296 && cp < 56320 && comes(r, \"\\\\u\") {\n r.i += 2\n low := try read_hex4(r)\n cp = 65536 + (cp - 55296) * 1024 + (low - 56320)\n }\n parts.push(utf8(cp))\n } else {\n return error(\"json: unknown escape \\\\\" + str(e) + \" in a text (\" + where(r) + \")\")\n }\n start = r.i\n }\n}\n\nfn read_number(ptr[Reader] r) -> Json! {\n start := r.i\n if peek(r) == '-' {\n r.i += 1\n }\n while r.i < r.src.len && ((r.src[r.i] >= '0' && r.src[r.i] <= '9') || r.src[r.i] == '.' || r.src[r.i] == 'e' || r.src[r.i] == 'E' || r.src[r.i] == '+' || r.src[r.i] == '-') {\n r.i += 1\n }\n n := float(r.src.sub(start, r.i)) or {\n r.i = start\n return error(unexpected(r, \"a number\"))\n }\n return new_number(n)\n}\n\nfn read_value(ptr[Reader] r) -> Json! {\n skip_space(r)\n c := peek(r)\n if r.i >= r.src.len {\n return error(unexpected(r, \"a value\"))\n }\n if c == '{' {\n obj := new_object()\n r.i += 1\n skip_space(r)\n if peek(r) == '}' {\n r.i += 1\n return obj\n }\n while true {\n skip_space(r)\n if peek(r) != '\"' {\n return error(unexpected(r, \"a key in quotes\"))\n }\n key := try read_string(r)\n skip_space(r)\n if peek(r) != ':' {\n return error(unexpected(r, \"':'\"))\n }\n r.i += 1\n v := try read_value(r)\n obj.set(key, v)\n skip_space(r)\n if peek(r) == ',' {\n r.i += 1\n continue\n }\n if peek(r) == '}' {\n r.i += 1\n return obj\n }\n return error(unexpected(r, \"',' or '}'\"))\n }\n }\n if c == '[' {\n list := new_list()\n r.i += 1\n skip_space(r)\n if peek(r) == ']' {\n r.i += 1\n return list\n }\n while true {\n v := try read_value(r)\n list.push(v)\n skip_space(r)\n if peek(r) == ',' {\n r.i += 1\n continue\n }\n if peek(r) == ']' {\n r.i += 1\n return list\n }\n return error(unexpected(r, \"',' or ']'\"))\n }\n }\n if c == '\"' {\n s := try read_string(r)\n return new_text(s)\n }\n if c == 't' {\n try expect_word(r, \"true\")\n return new_bool(true)\n }\n if c == 'f' {\n try expect_word(r, \"false\")\n return new_bool(false)\n }\n if c == 'n' {\n try expect_word(r, \"null\")\n return new_null()\n }\n if c == '-' || (c >= '0' && c <= '9') {\n return try read_number(r)\n }\n return error(unexpected(r, \"a value\"))\n}\n\n// the JSON in `text`; an error with the line and column where it went wrong\nfn parse(str text) -> Json! {\n r := Reader(src: text)\n v := try read_value(addr(r))\n skip_space(addr(r))\n if r.i < r.src.len {\n return error(unexpected(addr(r), \"the end of the text\"))\n }\n return v\n}\n") list.push("// Paths, as text. Elements are separated by \"/\".\n//\n// import path\n// path.join(\"a\", \"b.txt\") a/b.txt\n// path.base(\"/tmp/x/a.tar.gz\") a.tar.gz path.stem(...) a.tar path.ext(...) .gz\n// path.dir(\"/tmp/x/a.txt\") /tmp/x\n// path.clean(\"a//b/../c/./d\") a/c/d\n\n// a and b with one separator between; an absolute b replaces a\nfn join(str a, str b) -> str {\n if a.len == 0 {\n return b\n }\n if b.len == 0 {\n return a\n }\n if b[0] == '/' {\n return b\n }\n if a[a.len - 1] == '/' {\n return a + b\n }\n return a + \"/\" + b\n}\n\nfn is_abs(str p) -> bool {\n return p.len > 0 && p[0] == '/'\n}\n\n// the last element: \"a/b.txt\" -> \"b.txt\", \"a/b/\" -> \"b\", \"\" -> \".\", \"/\" -> \"/\"\nfn base(str p) -> str {\n if p.len == 0 {\n return \".\"\n }\n end := p.len\n while end > 1 && p[end - 1] == '/' {\n end -= 1\n }\n if end == 1 && p[0] == '/' {\n return \"/\"\n }\n start := end\n while start > 0 && p[start - 1] != '/' {\n start -= 1\n }\n return p.sub(start, end)\n}\n\n// everything before the last element: \"a/b.txt\" -> \"a\", \"b.txt\" -> \".\", \"/b\" -> \"/\"\nfn dir(str p) -> str {\n end := p.len\n while end > 1 && p[end - 1] == '/' {\n end -= 1\n }\n i := end\n while i > 0 && p[i - 1] != '/' {\n i -= 1\n }\n if i == 0 {\n return \".\"\n }\n while i > 1 && p[i - 1] == '/' {\n i -= 1\n }\n return p.sub(0, i)\n}\n\n// the part of the last element from its last dot: \"a.tar.gz\" -> \".gz\"; \"\" when there is none\nfn ext(str p) -> str {\n b := base(p)\n i := b.len\n while i > 0 && b[i - 1] != '.' {\n i -= 1\n }\n if i <= 1 { // no dot, or a name that only starts with one (.bashrc)\n return \"\"\n }\n return b.sub(i - 1, b.len)\n}\n\n// the last element without its extension\nfn stem(str p) -> str {\n b := base(p)\n e := ext(p)\n return b.sub(0, b.len - e.len)\n}\n\n// the elements of a path: \"/a/b/c\" -> [\"a\", \"b\", \"c\"]\nfn parts(str p) -> []str {\n []str out\n for piece in p.split(\"/\") {\n if piece.len > 0 {\n out.push(piece)\n }\n }\n return out\n}\n\n// the shortest path with the same meaning: no \"//\", no \".\", and \"..\" resolved where it can be\nfn clean(str p) -> str {\n if p.len == 0 {\n return \".\"\n }\n abs := is_abs(p)\n []str out\n for piece in parts(p) {\n if piece == \".\" {\n continue\n }\n if piece == \"..\" {\n if out.len > 0 && out[out.len - 1] != \"..\" {\n out.pop()\n } else if !abs {\n out.push(\"..\")\n }\n continue\n }\n out.push(piece)\n }\n joined := out.join(\"/\")\n if abs {\n return \"/\" + joined\n }\n if joined.len == 0 {\n return \".\"\n }\n return joined\n}\n") list.push("// Programs: run a command, read what it printed, look at the environment.\n//\n// import proc\n// code := proc.run(\"make\") the exit code; what the command prints goes to the screen\n// text := try proc.output(\"git describe\") what the command printed; an error if it failed\n// proc.env(\"HOME\", \"/\") an environment variable, or the second value if it is not set\nimport \"stdlib.h\"\nimport \"unistd.h\"\nimport \"time.h\"\n\n// run a command; its output goes to the screen. Gives its exit code.\nfn run(str command) -> int {\n return shell(command)\n}\n\n// run a command and give what it printed. Fails, with its output as the message, if it exits with an error.\nfn output(str command) -> str! {\n tmp := \"/tmp/finch_out_\" + str(getpid()) + \"_\" + str(int(time(null)))\n code := shell(\"(\" + command + \") > '\" + tmp + \"' 2>&1\")\n text := read_file(tmp) or \"\"\n delete_file(tmp)\n if code != 0 {\n return error(\"'\" + command + \"' exited with code \" + str(code) + \": \" + text.trim())\n }\n return text\n}\n\nfn env(str name, str fallback) -> str {\n v := getenv(name)\n if v.len == 0 {\n return fallback\n }\n return v\n}\n\nfn pid() -> int {\n return int(getpid())\n}\n\n// the folder the program is running in\nfn cwd() -> str {\n []char buf\n for i in 0..4096 {\n buf.push(char(0))\n }\n getcwd(buf.ptr, 4096)\n return str(buf.ptr)\n}\n") list.push("// Programs, for Windows. The same functions as proc.fch (that file is for Linux and macOS).\nimport \"stdlib.h\"\nimport \"process.h\"\nimport \"direct.h\"\nimport \"time.h\"\n\nfn run(str command) -> int {\n return shell(command)\n}\n\nfn output(str command) -> str! {\n tmp := proc_env(\"TEMP\", \".\") + \"\\\\finch_out_\" + str(getpid()) + \"_\" + str(int(time(null)))\n code := shell(command + \" > \\\"\" + tmp + \"\\\" 2>&1\")\n text := read_file(tmp) or \"\"\n delete_file(tmp)\n if code != 0 {\n return error(\"'\" + command + \"' exited with code \" + str(code) + \": \" + text.trim())\n }\n return text\n}\n\nfn proc_env(str name, str fallback) -> str {\n v := getenv(name)\n if v.len == 0 {\n return fallback\n }\n return v\n}\n\nfn env(str name, str fallback) -> str {\n return proc_env(name, fallback)\n}\n\nfn pid() -> int {\n return int(getpid())\n}\n\nfn cwd() -> str {\n []char buf\n for i in 0..4096 {\n buf.push(char(0))\n }\n getcwd(buf.ptr, 4096)\n return str(buf.ptr)\n}\n") list.push("// Time: the clock, sleeping, and dates.\n//\n// import time\n// start := time.millis() milliseconds from a clock that only goes forward (to measure with)\n// time.sleep(250) wait a quarter of a second\n// time.now() seconds since 1970-01-01 00:00 UTC\n// d := time.date(time.now()) the parts: d.year, d.month, d.day, d.hour, d.minute, d.second\n// time.format(time.now(), \"%Y-%m-%d %H:%M\") as text, in the computer's time zone\nimport \"time.h\"\n\nstruct Date {\n int year\n int month // 1 to 12\n int day // 1 to 31\n int hour\n int minute\n int second\n int weekday // 0 is Sunday\n int yearday // 1 to 366\n}\n\n// seconds since 1970-01-01 00:00 UTC\nfn now() -> int {\n return int(time(null))\n}\n\n// milliseconds on a clock that only goes forward: for measuring how long something took\nfn millis() -> int {\n timespec ts\n clock_gettime(CLOCK_MONOTONIC, addr(ts))\n return int(ts.tv_sec) * 1000 + int(ts.tv_nsec) / 1000000\n}\n\nfn sleep(int ms) {\n timespec ts\n ts.tv_sec = ms / 1000\n ts.tv_nsec = (ms % 1000) * 1000000\n nanosleep(addr(ts), null)\n}\n\nfn parts(tm t) -> Date {\n return Date(year: int(t.tm_year) + 1900, month: int(t.tm_mon) + 1, day: int(t.tm_mday), hour: int(t.tm_hour),\n minute: int(t.tm_min), second: int(t.tm_sec), weekday: int(t.tm_wday), yearday: int(t.tm_yday) + 1)\n}\n\n// the date and time of a moment, in UTC\nfn date(int unix) -> Date {\n tm t\n i64 stamp = unix\n gmtime_r(addr(stamp), addr(t))\n return parts(t)\n}\n\n// the date and time of a moment, in the computer's time zone\nfn local(int unix) -> Date {\n tm t\n i64 stamp = unix\n localtime_r(addr(stamp), addr(t))\n return parts(t)\n}\n\n// a moment as text, in the computer's time zone: %Y year, %m month, %d day, %H hour, %M minute, %S second\n// (the rest of C's strftime works too: %A weekday name, %B month name, ...)\nfn format(int unix, str pattern) -> str {\n tm t\n i64 stamp = unix\n localtime_r(addr(stamp), addr(t))\n []char buf\n for i in 0..256 {\n buf.push(char(0))\n }\n strftime(buf.ptr, 256, pattern, addr(t))\n return str(buf.ptr)\n}\n\n// the number of days in a month (month is 1 to 12)\nfn days_in_month(int year, int month) -> int {\n if month == 2 {\n if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {\n return 29\n }\n return 28\n }\n if month == 4 || month == 6 || month == 9 || month == 11 {\n return 30\n }\n return 31\n}\n") list.push("// Time, for Windows. The same functions as time.fch (that file is for Linux and macOS).\nimport \"time.h\"\nimport \"finch_win.h\"\n\nstruct Date {\n int year\n int month // 1 to 12\n int day // 1 to 31\n int hour\n int minute\n int second\n int weekday // 0 is Sunday\n int yearday // 1 to 366\n}\n\nfn now() -> int {\n return int(time(null))\n}\n\n// milliseconds on a clock that only goes forward (the performance counter: GetTickCount64 ticks every 15 ms)\nfn millis() -> int {\n i64 count = 0\n i64 freq = 1\n QueryPerformanceFrequency(addr(freq))\n QueryPerformanceCounter(addr(count))\n return int(count / freq) * 1000 + int(count % freq) * 1000 / int(freq)\n}\n\nfn sleep(int ms) {\n Sleep(u32(ms))\n}\n\nfn parts(tm t) -> Date {\n return Date(year: int(t.tm_year) + 1900, month: int(t.tm_mon) + 1, day: int(t.tm_mday), hour: int(t.tm_hour),\n minute: int(t.tm_min), second: int(t.tm_sec), weekday: int(t.tm_wday), yearday: int(t.tm_yday) + 1)\n}\n\nfn date(int unix) -> Date {\n tm t\n i64 stamp = unix\n _gmtime64_s(addr(t), addr(stamp))\n return parts(t)\n}\n\nfn local(int unix) -> Date {\n tm t\n i64 stamp = unix\n _localtime64_s(addr(t), addr(stamp))\n return parts(t)\n}\n\nfn format(int unix, str pattern) -> str {\n tm t\n i64 stamp = unix\n _localtime64_s(addr(t), addr(stamp))\n []char buf\n for i in 0..256 {\n buf.push(char(0))\n }\n strftime(buf.ptr, 256, pattern, addr(t))\n return str(buf.ptr)\n}\n\nfn days_in_month(int year, int month) -> int {\n if month == 2 {\n if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {\n return 29\n }\n return 28\n }\n if month == 4 || month == 6 || month == 9 || month == 11 {\n return 30\n }\n return 31\n}\n") + list.push("/* The runtime's folder functions, for std/fs.fch (they are in runtime/finch_rt.c, linked into every program). */\nint finch_fs_is_dir(const char *path);\nint finch_fs_exists(const char *path);\nint finch_fs_mkdir(const char *path);\nint finch_fs_remove_dir(const char *path);\nvoid *finch_fs_list_open(const char *path);\nconst char *finch_fs_list_next(void *list);\nvoid finch_fs_list_close(void *list);\nconst char *finch_fs_data_home(void);\n") list.push("/* The Windows functions std/time_windows.fch needs. Declared here, not by including :\n libclang can't parse that header with MinGW's intrinsics. Both are in kernel32. */\nunsigned long long GetTickCount64(void);\nvoid Sleep(unsigned long milliseconds);\nint QueryPerformanceCounter(long long *count);\nint QueryPerformanceFrequency(long long *frequency);\n") return list } diff --git a/src/gen.fch b/src/gen.fch index af2341a..b00a825 100644 --- a/src/gen.fch +++ b/src/gen.fch @@ -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)) diff --git a/src/lexer.fch b/src/lexer.fch index 95e6c02..bc716e2 100644 --- a/src/lexer.fch +++ b/src/lexer.fch @@ -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" diff --git a/src/main.fch b/src/main.fch index 858271f..410f4bd 100644 --- a/src/main.fch +++ b/src/main.fch @@ -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) diff --git a/src/parser.fch b/src/parser.fch index 82c54ad..a352ae3 100644 --- a/src/parser.fch +++ b/src/parser.fch @@ -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" diff --git a/std/finch_fs.h b/std/finch_fs.h new file mode 100644 index 0000000..f3df1df --- /dev/null +++ b/std/finch_fs.h @@ -0,0 +1,9 @@ +/* The runtime's folder functions, for std/fs.fch (they are in runtime/finch_rt.c, linked into every program). */ +int finch_fs_is_dir(const char *path); +int finch_fs_exists(const char *path); +int finch_fs_mkdir(const char *path); +int finch_fs_remove_dir(const char *path); +void *finch_fs_list_open(const char *path); +const char *finch_fs_list_next(void *list); +void finch_fs_list_close(void *list); +const char *finch_fs_data_home(void); diff --git a/std/fs.fch b/std/fs.fch new file mode 100644 index 0000000..afb6196 --- /dev/null +++ b/std/fs.fch @@ -0,0 +1,70 @@ +// Files and folders: bytes, folders, listing, and a place for a program's own files. +// +// import fs +// data := try fs.read_bytes("atlas.png") the whole file as []u8 (any bytes, NUL included) +// try fs.write_bytes("out.bin", data) +// try fs.mkdir("saves/slot1") with the folders above it +// names := try fs.list("saves") the names in a folder, sorted +// fs.exists(p) fs.is_dir(p) try fs.remove_dir(p) +// dir := try fs.data_dir("MyGame") a folder for this user's saves, made if needed +import "finch_fs.h" + +fn read_bytes(str path) -> []u8! { + text := try read_file(path) + return text.bytes() +} + +fn write_bytes(str path, []u8 data) -> ! { + try write_file(path, str(data)) +} + +fn exists(str path) -> bool { + return finch_fs_exists(path) != 0 +} + +fn is_dir(str path) -> bool { + return finch_fs_is_dir(path) != 0 +} + +fn mkdir(str path) -> ! { + if finch_fs_mkdir(path) == 0 { + return error("can't make the folder \"" + path + "\"") + } +} + +// only an empty folder +fn remove_dir(str path) -> ! { + if finch_fs_remove_dir(path) == 0 { + return error("can't remove the folder \"" + path + "\" (is it empty?)") + } +} + +fn list(str path) -> []str! { + l := finch_fs_list_open(path) + if l == null { + return error("can't list the folder \"" + path + "\"") + } + []str names + while true { + name := finch_fs_list_next(l) + if name.len == 0 { + break + } + names.push(name) + } + finch_fs_list_close(l) + names.sort() + return names +} + +// The folder where a program keeps this user's files (saves, settings), made if it isn't there: +// ~/.local/share/ on Linux, ~/Library/Application Support/ on macOS, %APPDATA%\ on Windows. +fn data_dir(str app) -> str! { + home := finch_fs_data_home() + if home.len == 0 { + return error("this system doesn't say where programs keep their files") + } + dir := home + "/" + app + try mkdir(dir) + return dir +} diff --git a/tests/new/fail/const_assign.fch b/tests/new/fail/const_assign.fch new file mode 100644 index 0000000..50bfb71 --- /dev/null +++ b/tests/new/fail/const_assign.fch @@ -0,0 +1,5 @@ +// expect: is a constant; it can't be changed +const W = 3 +fn main() { + W = 4 +} diff --git a/tests/new/fail/const_call.fch b/tests/new/fail/const_call.fch new file mode 100644 index 0000000..336d6eb --- /dev/null +++ b/tests/new/fail/const_call.fch @@ -0,0 +1,6 @@ +// expect: a constant is made of numbers, text +fn f() -> int { return 1 } +const W = f() +fn main() { + print(W) +} diff --git a/tests/new/fail/const_loop.fch b/tests/new/fail/const_loop.fch new file mode 100644 index 0000000..de8965e --- /dev/null +++ b/tests/new/fail/const_loop.fch @@ -0,0 +1,6 @@ +// expect: is worked out from itself +const A = B + 1 +const B = A +fn main() { + print(A) +} diff --git a/tests/new/fail/ifvalue_else.fch b/tests/new/fail/ifvalue_else.fch new file mode 100644 index 0000000..e91fc59 --- /dev/null +++ b/tests/new/fail/ifvalue_else.fch @@ -0,0 +1,4 @@ +// expect: an if that gives a value needs an else +fn main() { + x := if true { 1 } +} diff --git a/tests/new/fail/ifvalue_types.fch b/tests/new/fail/ifvalue_types.fch new file mode 100644 index 0000000..b23e893 --- /dev/null +++ b/tests/new/fail/ifvalue_types.fch @@ -0,0 +1,4 @@ +// expect: both branches of the if have to give the same type +fn main() { + x := if true { 1 } else { "one" } +} diff --git a/tests/new/fail/import_clash.fch b/tests/new/fail/import_clash.fch new file mode 100644 index 0000000..46e6e49 --- /dev/null +++ b/tests/new/fail/import_clash.fch @@ -0,0 +1,5 @@ +// expect: is already used by another import +import path +import time as path +fn main() { +} diff --git a/tests/new/fail/longstr_indent.fch b/tests/new/fail/longstr_indent.fch new file mode 100644 index 0000000..82b5a0a --- /dev/null +++ b/tests/new/fail/longstr_indent.fch @@ -0,0 +1,6 @@ +// expect: is indented less than the closing +fn main() { + s := """ + a + """ +} diff --git a/tests/new/lang_additions.fch b/tests/new/lang_additions.fch new file mode 100644 index 0000000..d4462b7 --- /dev/null +++ b/tests/new/lang_additions.fch @@ -0,0 +1,33 @@ +// const, if as a value, min/max/abs/clamp, import a/b and import x as y, """ text +import ui/button +import path as p +import ui/button as b2 + +const W = 640 +const H = W / 2 +const f32 G = 9.8 +const TITLE = "Game " + "One" +const NEG = -W + +fn main() { + print(W, H, G, TITLE, NEG, button.SIZE) + bt := button.make("ok") + button.Button other + print(bt.label, bt.w, other.w, b2.make("x").w) + print(p.base("/a/b.txt")) + x := 7 + v := if x > 5 { "big" } else if x > 2 { "mid" } else { "small" } + f32 s = if x > 5 { 1.5 } else { 0.5 } + print(v, s, if x == 7 { x * 2 } else { 0 }) + print(min(3, 9), max(2.5, 1), abs(-4), abs(-2.5), clamp(15, 0, 10), clamp(-3, 0, 10), min(5, 2, 8)) + f32 sp = 2.0 + print(clamp(sp * 3.0, 0.0, 5.0), max(u8(3), u8(200))) + shader := """ + #version 330 + void main() { + gl_Position = vec4(0.0); + } + """ + print(shader) + print("""one "quoted" line""") +} diff --git a/tests/new/lang_additions.out b/tests/new/lang_additions.out new file mode 100644 index 0000000..9f67b4f --- /dev/null +++ b/tests/new/lang_additions.out @@ -0,0 +1,11 @@ +640 320 9.8 Game One -640 3 +ok 30 30 30 +b.txt +big 1.5 14 +3 2.5 4 2.5 10 0 2 +5 200 +#version 330 +void main() { + gl_Position = vec4(0.0); +} +one "quoted" line diff --git a/tests/new/std_fs.fch b/tests/new/std_fs.fch new file mode 100644 index 0000000..40eb2ff --- /dev/null +++ b/tests/new/std_fs.fch @@ -0,0 +1,23 @@ +// the fs module: bytes with NUL in them, folders, listing +import fs + +fn main() -> ! { + root := "fs_test_dir" + try fs.mkdir(root + "/a/b") + print(fs.exists(root), fs.is_dir(root + "/a/b"), fs.is_dir("nope")) + data := [u8(0), u8(255), u8(13), u8(10), u8(65)] + try fs.write_bytes(root + "/a/blob.bin", data) + back := try fs.read_bytes(root + "/a/blob.bin") + print(back.len, back[0], back[1], back[4], str(back) == str(data)) + write_file(root + "/z.txt", "z") + print(try fs.list(root)) + print(try fs.list(root + "/a")) + print(fs.list("no_such_dir") or ["(none)"]) + delete_file(root + "/a/blob.bin") + delete_file(root + "/z.txt") + try fs.remove_dir(root + "/a/b") + try fs.remove_dir(root + "/a") + try fs.remove_dir(root) + print(fs.exists(root)) + fs.remove_dir(root) or { print("gone:", err) } +} diff --git a/tests/new/std_fs.out b/tests/new/std_fs.out new file mode 100644 index 0000000..84c5f28 --- /dev/null +++ b/tests/new/std_fs.out @@ -0,0 +1,7 @@ +true true false +5 0 255 65 true +["a", "z.txt"] +["b", "blob.bin"] +["(none)"] +false +gone: can't remove the folder "fs_test_dir" (is it empty?) diff --git a/tests/new/ui/button.fch b/tests/new/ui/button.fch new file mode 100644 index 0000000..46db929 --- /dev/null +++ b/tests/new/ui/button.fch @@ -0,0 +1,7 @@ +// a module in a folder, for tests/new/lang_additions.fch (import ui/button) +const SIZE = 3 +struct Button { + str label + int w = SIZE * 10 +} +fn make(str l) -> Button { return Button(label: l) }