diff --git a/runtime/finch_rt.c b/runtime/finch_rt.c index 1ac70f4..ab806a3 100644 --- a/runtime/finch_rt.c +++ b/runtime/finch_rt.c @@ -20,9 +20,13 @@ #include #include #ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include #include #include #else +#include #include #include #include @@ -881,6 +885,114 @@ const char *finch_fs_data_home(void) { return buf; } +// ---------- threads (std/thread.fch, through std/finch_thread.h) ---------- + +typedef struct { + void (*fn)(void *); + void *arg; +} ThreadStart; + +#ifdef _WIN32 +static DWORD WINAPI thread_main(LPVOID p) { + ThreadStart st = *(ThreadStart *)p; + free(p); + st.fn(st.arg); + return 0; +} +#else +static void *thread_main(void *p) { + ThreadStart st = *(ThreadStart *)p; + free(p); + st.fn(st.arg); + return NULL; +} +#endif + +// run fn(arg) on a new thread; NULL if it couldn't start +void *finch_thread_spawn(void (*fn)(void *), void *arg) { + ThreadStart *st = finch_alloc((int64_t)sizeof(ThreadStart)); + st->fn = fn; + st->arg = arg; +#ifdef _WIN32 + HANDLE h = CreateThread(NULL, 0, thread_main, st, 0, NULL); + if (!h) { + free(st); + return NULL; + } + return h; +#else + pthread_t *t = finch_alloc((int64_t)sizeof(pthread_t)); + if (pthread_create(t, NULL, thread_main, st) != 0) { + free(st); + free(t); + return NULL; + } + return t; +#endif +} + +// wait for the thread to end; 1 if that worked +int finch_thread_join(void *t) { + if (!t) return 0; +#ifdef _WIN32 + int ok = WaitForSingleObject((HANDLE)t, INFINITE) == WAIT_OBJECT_0; + CloseHandle((HANDLE)t); + return ok; +#else + int ok = pthread_join(*(pthread_t *)t, NULL) == 0; + free(t); + return ok; +#endif +} + +void *finch_mutex_new(void) { +#ifdef _WIN32 + CRITICAL_SECTION *m = finch_alloc((int64_t)sizeof(CRITICAL_SECTION)); + InitializeCriticalSection(m); +#else + pthread_mutex_t *m = finch_alloc((int64_t)sizeof(pthread_mutex_t)); + pthread_mutex_init(m, NULL); +#endif + return m; +} + +void finch_mutex_lock(void *m) { +#ifdef _WIN32 + EnterCriticalSection((CRITICAL_SECTION *)m); +#else + pthread_mutex_lock((pthread_mutex_t *)m); +#endif +} + +void finch_mutex_unlock(void *m) { +#ifdef _WIN32 + LeaveCriticalSection((CRITICAL_SECTION *)m); +#else + pthread_mutex_unlock((pthread_mutex_t *)m); +#endif +} + +void finch_mutex_free(void *m) { + if (!m) return; +#ifdef _WIN32 + DeleteCriticalSection((CRITICAL_SECTION *)m); +#else + pthread_mutex_destroy((pthread_mutex_t *)m); +#endif + free(m); +} + +int finch_cpu_count(void) { +#ifdef _WIN32 + SYSTEM_INFO si; + GetSystemInfo(&si); + return (int)si.dwNumberOfProcessors; +#else + long n = sysconf(_SC_NPROCESSORS_ONLN); + return n > 0 ? (int)n : 1; +#endif +} + // ---------- 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/cimport.fch b/src/cimport.fch index 1125536..c4613af 100644 --- a/src/cimport.fch +++ b/src/cimport.fch @@ -28,6 +28,18 @@ struct CConst { // enum values and #define constants str text // a float as written, so no digits are lost: 3.14159265358979323846 bool isStr // #define NAME "text" str s + str structName // #define RAYWHITE (Color){ 245, 245, 245, 255 }: the struct, and its fields in order + []str fields // each as a Finch literal: 245, 0.5 + []bool fieldFloat +} + +// what visitMacroVar looks for inside (Color){ ... } +struct InitFind { + bool found + CXCursor list + bool ok + []str fields + []bool floats } struct CField { @@ -381,7 +393,7 @@ fn macroTokens(ptr[Importer] imp, CXCursor c) -> MacroTokens { u32 n = 0 clang_tokenize(imp.tu, clang_getCursorExtent(c), addr(tokens), addr(n)) out := MacroTokens(safe: true) - allowed := ["(", ")", "+", "-", "*", "/", "%", "<<", ">>", "|", "&", "^", "~", "!", "<", ">", "<=", ">=", "==", "!=", "&&", "||", "?", ":", ","] + allowed := ["(", ")", "+", "-", "*", "/", "%", "<<", ">>", "|", "&", "^", "~", "!", "<", ">", "<=", ">=", "==", "!=", "&&", "||", "?", ":", ",", "{", "}"] words := ["sizeof", "unsigned", "signed", "int", "long", "short", "char", "float", "double", "const", "_Bool"] depth := 0 for k in 1..int(n) { // token 0 is the macro's name @@ -678,6 +690,72 @@ fn evalMacros(ptr[Importer] imp, ptr index, Header h, []ptr[char] argv) { clang_disposeTranslationUnit(tu) } +// a float as a Finch literal: 2.0, not 2 +fn floatText(float v) -> str { + t := str(v) + if !t.contains(".") && !t.contains("e") { + t = t + ".0" + } + return t +} + +// (Color){ 245, 245, 245, 255 }: a struct whose fields are all numbers, in order +fn structMacro(ptr[Importer] imp, str name, CXCursor c, CXType t) { + sname := recordName(imp, t) + if !imp.out.structs.has(sname) { + return + } + find := InitFind(ok: true) + clang_visitChildren(c, addr(visitFindInit), addr(find)) + if !find.found { + return + } + clang_visitChildren(find.list, addr(visitInitField), addr(find)) + if !find.ok || find.fields.len == 0 || find.fields.len > imp.out.structs[sname].fields.len { + return + } + imp.out.consts[name] = CConst(structName: sname, fields: find.fields, fieldFloat: find.floats) +} + +fn visitFindInit(CXCursor c, CXCursor parent, ptr data) -> i32 { + ptr[InitFind] f = data + if clang_getCursorKind(c) == CXCursor_InitListExpr { + f.found = true + f.list = c + return CXChildVisit_Break + } + return CXChildVisit_Recurse +} + +fn visitInitField(CXCursor c, CXCursor parent, ptr data) -> i32 { + ptr[InitFind] f = data + r := clang_Cursor_Evaluate(c) + if r == null { + f.ok = false + return CXChildVisit_Break + } + k := clang_EvalResult_getKind(r) + if k == CXEval_Int { + f.fields.push(str(int(clang_EvalResult_getAsLongLong(r)))) + f.floats.push(false) + } else if k == CXEval_Float { + v := clang_EvalResult_getAsDouble(r) + if v == v && v - v == 0.0 { + f.fields.push(floatText(v)) + f.floats.push(true) + } else { + f.ok = false + } + } else { + f.ok = false // a struct in the struct, a pointer, a text: not a simple constant + } + clang_EvalResult_dispose(r) + if !f.ok { + return CXChildVisit_Break + } + return CXChildVisit_Continue +} + fn visitMacroVar(CXCursor c, CXCursor parent, ptr data) -> i32 { ptr[Importer] imp = data if clang_getCursorKind(c) != CXCursor_VarDecl { @@ -688,8 +766,16 @@ fn visitMacroVar(CXCursor c, CXCursor parent, ptr data) -> i32 { return CXChildVisit_Continue } name = name.sub(10, name.len) + if imp.out.consts.has(name) { + return CXChildVisit_Continue + } + t := clang_getCanonicalType(clang_getCursorType(c)) + if t.kind == CXType_Record { + structMacro(imp, name, c, t) + return CXChildVisit_Continue + } r := clang_Cursor_Evaluate(c) - if r == null || imp.out.consts.has(name) { + if r == null { return CXChildVisit_Continue } k := clang_EvalResult_getKind(r) @@ -698,7 +784,7 @@ fn visitMacroVar(CXCursor c, CXCursor parent, ptr data) -> i32 { } else if k == CXEval_Float { v := clang_EvalResult_getAsDouble(r) if v == v && v - v == 0.0 { // not nan or inf - imp.out.consts[name] = CConst(isFloat: true, f: v, text: str(v)) + imp.out.consts[name] = CConst(isFloat: true, f: v, text: floatText(v)) } } else if k == CXEval_StrLiteral { imp.out.consts[name] = CConst(isStr: true, s: clang_EvalResult_getAsStr(r)) diff --git a/src/docs.fch b/src/docs.fch index 5abd5ab..977f0de 100644 --- a/src/docs.fch +++ b/src/docs.fch @@ -26,6 +26,12 @@ fn builtins() -> []Doc { doc("max", "max(a, b, ...) -> number", "The biggest of the numbers, in the type they all fit."), doc("abs", "abs(x) -> number", "x without its minus sign (any number type)."), doc("clamp", "clamp(x, lo, hi) -> number", "x, but at least lo and at most hi."), + doc("atomic_add", "atomic_add(ptr[int] p, n) -> int", "Adds n to the number at p in one step other threads can't split; gives back what was there before."), + doc("atomic_sub", "atomic_sub(ptr[int] p, n) -> int", "Takes n from the number at p in one step; gives back what was there before."), + doc("atomic_load", "atomic_load(ptr[int] p) -> int", "Reads the number at p, as another thread last wrote it."), + doc("atomic_store", "atomic_store(ptr[int] p, n)", "Writes n to p so other threads see it whole."), + doc("atomic_swap", "atomic_swap(ptr[int] p, n) -> int", "Puts n at p and gives back what was there, in one step."), + doc("atomic_cas", "atomic_cas(ptr[int] p, old, new) -> bool", "Puts new at p only if old is there (compare and swap); true if it did."), doc("addr", "addr(x) -> ptr[T]", "A pointer to a variable, field or element. addr(function) gives C a callback."), doc("new", "new(value) -> ptr[T]", "Puts a copy of the value on the heap. Give it back with free(...)."), doc("free", "free(ptr p)", "Gives back memory from new(...) (or from C), freeing what the value owns."), diff --git a/src/embedded.fch b/src/embedded.fch index 6e774e8..cfef97d 100644 --- a/src/embedded.fch +++ b/src/embedded.fch @@ -9,24 +9,28 @@ fn names() -> []str { list.push("std/path.fch") list.push("std/proc.fch") list.push("std/proc_windows.fch") + list.push("std/thread.fch") list.push("std/time.fch") list.push("std/time_windows.fch") list.push("std/finch_fs.h") + list.push("std/finch_thread.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#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\n// The chain of Finch calls, kept by the program itself (see @finch_trace in the compiler's output):\n// each frame is { the caller's frame, the function's name, its file, the line it is at }.\ntypedef struct FFrame {\n struct FFrame *prev;\n const char *name;\n const char *file;\n int64_t line;\n} FFrame;\nstatic void *(*trace_top)(void);\nstatic void (*trace_set)(void *);\nvoid finch_trace_init(void *(*top)(void), void (*set)(void *)) {\n trace_top = top;\n trace_set = set;\n}\n\nstatic void print_trace(void) {\n if (!trace_top) return;\n FFrame *f = trace_top();\n for (int n = 0; f; n++, f = f->prev) {\n if (n == 30) {\n fprintf(stderr, \" ...\\n\");\n break;\n }\n fprintf(stderr, \" in %s (%s:%lld)\\n\", f->name, f->file, (long long)f->line);\n }\n}\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 print_trace();\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// ---------- assert and finch test ----------\n\n__attribute__((noreturn, cold)) void finch_assert_failed(const char *file, int64_t line, const FStr *why) {\n char msg[600];\n if (why->len) snprintf(msg, sizeof msg, \"assert failed: %.*s\", (int)(why->len > 500 ? 500 : why->len), why->ptr);\n else snprintf(msg, sizeof msg, \"assert failed\");\n finch_panic(file, line, msg);\n}\n\n// Each test runs on its own: a runtime error (a failed assert too) ends that test, and the next one runs.\nstatic jmp_buf test_jump;\nstatic char test_message[1100];\nstatic int64_t tests_passed, tests_failed;\n\nstatic void test_fatal(const char *msg) {\n snprintf(test_message, sizeof test_message, \"%s\", msg);\n longjmp(test_jump, 1);\n}\n\nvoid finch_test_run(const char *name, void (*fn)(void)) {\n void (*saved)(const char *) = fatal_hook;\n void *chain = trace_top ? trace_top() : NULL;\n test_message[0] = 0;\n fatal_hook = test_fatal;\n int failed = setjmp(test_jump);\n if (!failed) fn();\n fatal_hook = saved;\n if (trace_set) trace_set(chain); // a test that stopped early left its calls on the chain\n fflush(stdout);\n if (failed) {\n tests_failed++;\n printf(\"FAIL %s\\n %s\\n\", name, test_message);\n } else {\n tests_passed++;\n printf(\"ok %s\\n\", name);\n }\n fflush(stdout);\n}\n\nint finch_test_summary(void) {\n printf(\"\\n%lld passed, %lld failed\\n\", (long long)tests_passed, (long long)tests_failed);\n return tests_failed ? 1 : 0;\n}\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("// 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#include \n#ifdef _WIN32\n#define WIN32_LEAN_AND_MEAN\n#define NOMINMAX\n#include \n#include \n#include \n#else\n#include \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\n// The chain of Finch calls, kept by the program itself (see @finch_trace in the compiler's output):\n// each frame is { the caller's frame, the function's name, its file, the line it is at }.\ntypedef struct FFrame {\n struct FFrame *prev;\n const char *name;\n const char *file;\n int64_t line;\n} FFrame;\nstatic void *(*trace_top)(void);\nstatic void (*trace_set)(void *);\nvoid finch_trace_init(void *(*top)(void), void (*set)(void *)) {\n trace_top = top;\n trace_set = set;\n}\n\nstatic void print_trace(void) {\n if (!trace_top) return;\n FFrame *f = trace_top();\n for (int n = 0; f; n++, f = f->prev) {\n if (n == 30) {\n fprintf(stderr, \" ...\\n\");\n break;\n }\n fprintf(stderr, \" in %s (%s:%lld)\\n\", f->name, f->file, (long long)f->line);\n }\n}\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 print_trace();\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// ---------- assert and finch test ----------\n\n__attribute__((noreturn, cold)) void finch_assert_failed(const char *file, int64_t line, const FStr *why) {\n char msg[600];\n if (why->len) snprintf(msg, sizeof msg, \"assert failed: %.*s\", (int)(why->len > 500 ? 500 : why->len), why->ptr);\n else snprintf(msg, sizeof msg, \"assert failed\");\n finch_panic(file, line, msg);\n}\n\n// Each test runs on its own: a runtime error (a failed assert too) ends that test, and the next one runs.\nstatic jmp_buf test_jump;\nstatic char test_message[1100];\nstatic int64_t tests_passed, tests_failed;\n\nstatic void test_fatal(const char *msg) {\n snprintf(test_message, sizeof test_message, \"%s\", msg);\n longjmp(test_jump, 1);\n}\n\nvoid finch_test_run(const char *name, void (*fn)(void)) {\n void (*saved)(const char *) = fatal_hook;\n void *chain = trace_top ? trace_top() : NULL;\n test_message[0] = 0;\n fatal_hook = test_fatal;\n int failed = setjmp(test_jump);\n if (!failed) fn();\n fatal_hook = saved;\n if (trace_set) trace_set(chain); // a test that stopped early left its calls on the chain\n fflush(stdout);\n if (failed) {\n tests_failed++;\n printf(\"FAIL %s\\n %s\\n\", name, test_message);\n } else {\n tests_passed++;\n printf(\"ok %s\\n\", name);\n }\n fflush(stdout);\n}\n\nint finch_test_summary(void) {\n printf(\"\\n%lld passed, %lld failed\\n\", (long long)tests_passed, (long long)tests_failed);\n return tests_failed ? 1 : 0;\n}\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// ---------- threads (std/thread.fch, through std/finch_thread.h) ----------\n\ntypedef struct {\n void (*fn)(void *);\n void *arg;\n} ThreadStart;\n\n#ifdef _WIN32\nstatic DWORD WINAPI thread_main(LPVOID p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return 0;\n}\n#else\nstatic void *thread_main(void *p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return NULL;\n}\n#endif\n\n// run fn(arg) on a new thread; NULL if it couldn't start\nvoid *finch_thread_spawn(void (*fn)(void *), void *arg) {\n ThreadStart *st = finch_alloc((int64_t)sizeof(ThreadStart));\n st->fn = fn;\n st->arg = arg;\n#ifdef _WIN32\n HANDLE h = CreateThread(NULL, 0, thread_main, st, 0, NULL);\n if (!h) {\n free(st);\n return NULL;\n }\n return h;\n#else\n pthread_t *t = finch_alloc((int64_t)sizeof(pthread_t));\n if (pthread_create(t, NULL, thread_main, st) != 0) {\n free(st);\n free(t);\n return NULL;\n }\n return t;\n#endif\n}\n\n// wait for the thread to end; 1 if that worked\nint finch_thread_join(void *t) {\n if (!t) return 0;\n#ifdef _WIN32\n int ok = WaitForSingleObject((HANDLE)t, INFINITE) == WAIT_OBJECT_0;\n CloseHandle((HANDLE)t);\n return ok;\n#else\n int ok = pthread_join(*(pthread_t *)t, NULL) == 0;\n free(t);\n return ok;\n#endif\n}\n\nvoid *finch_mutex_new(void) {\n#ifdef _WIN32\n CRITICAL_SECTION *m = finch_alloc((int64_t)sizeof(CRITICAL_SECTION));\n InitializeCriticalSection(m);\n#else\n pthread_mutex_t *m = finch_alloc((int64_t)sizeof(pthread_mutex_t));\n pthread_mutex_init(m, NULL);\n#endif\n return m;\n}\n\nvoid finch_mutex_lock(void *m) {\n#ifdef _WIN32\n EnterCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_lock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_unlock(void *m) {\n#ifdef _WIN32\n LeaveCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_unlock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_free(void *m) {\n if (!m) return;\n#ifdef _WIN32\n DeleteCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_destroy((pthread_mutex_t *)m);\n#endif\n free(m);\n}\n\nint finch_cpu_count(void) {\n#ifdef _WIN32\n SYSTEM_INFO si;\n GetSystemInfo(&si);\n return (int)si.dwNumberOfProcessors;\n#else\n long n = sysconf(_SC_NPROCESSORS_ONLN);\n return n > 0 ? (int)n : 1;\n#endif\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("// Threads: run a function next to the rest of the program, and a lock for what they share.\n//\n// import thread\n// fn work(ptr arg) { a thread's function takes one ptr (to whatever it needs)\n// ptr[Job] job = arg\n// ...\n// }\n// t := try thread.spawn(addr(work), addr(job))\n// t.join() wait for it; `job` must live until then\n//\n// m := thread.mutex()\n// defer m.close()\n// m.lock() ... m.unlock() one thread at a time in between\n// atomic_add(addr(count), 1) (built in) numbers changed by many threads at once\n//\n// What a thread is given is shared, not copied: don't let it outlive the block it comes from, and\n// change it only under a lock (or with atomic_...).\nimport \"finch_thread.h\"\n\nstruct Thread {\n ptr handle\n}\n\nstruct Mutex {\n ptr handle\n}\n\n// start fn(arg) on a new thread; fn is addr(f) of a fn f(ptr arg)\nfn spawn(ptr fn_, ptr arg) -> Thread! {\n h := finch_thread_spawn(fn_, arg)\n if h == null {\n return error(\"can't start a thread\")\n }\n return Thread(handle: h)\n}\n\n// wait until the thread has finished\nfn Thread.join() {\n if self.handle != null {\n finch_thread_join(self.handle)\n self.handle = null\n }\n}\n\nfn mutex() -> Mutex {\n return Mutex(handle: finch_mutex_new())\n}\n\nfn Mutex.lock() {\n finch_mutex_lock(self.handle)\n}\n\nfn Mutex.unlock() {\n finch_mutex_unlock(self.handle)\n}\n\nfn Mutex.close() {\n finch_mutex_free(self.handle)\n self.handle = null\n}\n\n// how many threads the computer runs at the same time\nfn cpus() -> int {\n return int(finch_cpu_count())\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 runtime's thread functions, for std/thread.fch (they are in runtime/finch_rt.c, linked into every program). */\nvoid *finch_thread_spawn(void (*fn)(void *), void *arg);\nint finch_thread_join(void *thread);\nvoid *finch_mutex_new(void);\nvoid finch_mutex_lock(void *mutex);\nvoid finch_mutex_unlock(void *mutex);\nvoid finch_mutex_free(void *mutex);\nint finch_cpu_count(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/fmt.fch b/src/fmt.fch new file mode 100644 index 0000000..29324cb --- /dev/null +++ b/src/fmt.fch @@ -0,0 +1,224 @@ +// finch fmt: the indentation by { }, 4 spaces a level; no spaces at line ends; at most one empty line +// in a row; one line end at the end of the file. Nothing else is moved: lines that go on inside ( or [, +// """ text and /* */ comments keep their shape, shifted along with the line they belong to. + +struct Scan { + int braces // { } open at the start of the line + int parens // ( [ open + bool inLong // inside """ text + bool inBlock // inside /* */ + char last // the last thing of code on the line ('"' for text): + or , means the next line goes on +} + +// how deep a line's leading spaces go (a tab is 4) +fn indentOf(str line) -> int { + n := 0 + for c in line { + if c == ' ' { + n += 1 + } else if c == '\t' { + n += 4 + } else { + break + } + } + return n +} + +// how many spaces and tabs a line starts with +fn leading(str line) -> int { + i := 0 + while i < line.len && (line[i] == ' ' || line[i] == '\t') { + i += 1 + } + return i +} + +fn trimEnd(str line) -> str { + n := line.len + while n > 0 && (line[n - 1] == ' ' || line[n - 1] == '\t' || line[n - 1] == '\r') { + n -= 1 + } + return line.sub(0, n) +} + +fn spaces(int n) -> str { + []str out + for i in 0..n { + out.push(" ") + } + return out.join("") +} + +fn startsWith(str line, int i, str what) -> bool { + return i + what.len <= line.len && line.sub(i, i + what.len) == what +} + +// past one line: what is open at its end +fn scanLine(str line, ptr[Scan] s) { + s.last = ' ' + i := 0 + while i < line.len { + c := line[i] + if s.inLong { + if startsWith(line, i, "\"\"\"") { + s.inLong = false + i += 3 + } else { + i += 1 + } + continue + } + if s.inBlock { + if startsWith(line, i, "*/") { + s.inBlock = false + i += 2 + } else { + i += 1 + } + continue + } + if startsWith(line, i, "//") { + return + } + if startsWith(line, i, "/*") { + s.inBlock = true + i += 2 + continue + } + if startsWith(line, i, "\"\"\"") { + s.inLong = true + i += 3 + continue + } + if c == '"' { + i = pastString(line, i + 1) + s.last = '"' + continue + } + if c == '\'' { + i += 1 + while i < line.len && line[i] != '\'' { + if line[i] == '\\' { + i += 1 + } + i += 1 + } + i += 1 + s.last = '"' + continue + } + if c != ' ' && c != '\t' && c != '\r' { + s.last = c + } + if c == '{' { + s.braces += 1 + } else if c == '}' { + s.braces -= 1 + } else if c == '(' || c == '[' { + s.parens += 1 + } else if c == ')' || c == ']' { + s.parens -= 1 + } + i += 1 + } +} + +// past "text" (i is just after the opening quote), with \(code) that may have its own "text" in it +fn pastString(str line, int start) -> int { + i := start + while i < line.len { + c := line[i] + if c == '"' { + return i + 1 + } + if c == '\\' && i + 1 < line.len && line[i + 1] == '(' { + depth := 1 + i += 2 + while i < line.len && depth > 0 { + if line[i] == '"' { + i = pastString(line, i + 1) + continue + } + if line[i] == '(' { + depth += 1 + } else if line[i] == ')' { + depth -= 1 + } + i += 1 + } + continue + } + if c == '\\' { + i += 1 + } + i += 1 + } + return i +} + +fn format(str text) -> str { + lines := text.replace("\r\n", "\n").split("\n") + []str out + s := Scan() + stmtWas := 0 // where the line that started the statement was, and where it is now + stmtNow := 0 + blank := 0 + for raw in lines { + goesOn := s.last == '+' || s.last == '-' || s.last == '*' || s.last == '/' || s.last == ',' || s.last == '=' || s.last == '|' || s.last == '&' || s.last == '%' + own := !s.inLong && !s.inBlock && s.parens <= 0 && !goesOn // the line starts something of its own + if s.inLong { + // """ text as it is, moved with its statement (its closing line says how much is cut off) + out.push(shifted(raw, stmtNow - stmtWas)) + scanLine(raw, addr(s)) + continue + } + line := trimEnd(raw) + if line.len == 0 { + if s.inBlock { + out.push("") + } else { + blank += 1 + } + continue + } + if blank > 0 && out.len > 0 { + out.push("") + } + blank = 0 + body := line.sub(leading(line), line.len) + if own { + depth := s.braces + if body.starts_with("}") { + depth -= 1 + } + if depth < 0 { + depth = 0 + } + stmtWas = indentOf(line) + stmtNow = depth * 4 + out.push(spaces(stmtNow) + body) + } else { + out.push(shifted(line, stmtNow - stmtWas)) + } + scanLine(line, addr(s)) + } + while out.len > 0 && out[out.len - 1].len == 0 { + out.pop() + } + return out.join("\n") + "\n" +} + +// the line moved right (or left) by `by`, keeping what it has after its indentation +fn shifted(str line, int by) -> str { + n := indentOf(line) + body := line.sub(leading(line), line.len) + if body.len == 0 { + return line + } + to := n + by + if to < 0 { + to = 0 + } + return spaces(to) + body +} diff --git a/src/gen.fch b/src/gen.fch index 7da8365..dfa2aa3 100644 --- a/src/gen.fch +++ b/src/gen.fch @@ -3458,6 +3458,19 @@ fn orElse(ptr[Gen] g, ast.Node e, bool discard) -> Val { return Val(v: op(g, "phi " + llty(g, v.ty) + " [ " + fv + ", %" + badEnd + " ], [ " + v.v + ", %" + ok + " ]"), ty: v.ty, fresh: owning(g, v.ty)) } +// An argument: a struct variable given where a ptr to that struct goes is passed by its address, +// as if it were addr(x) (worldDef for b2CreateWorld(const b2WorldDef*), body for move(ptr[Body] b)) +fn argWant(ptr[Gen] g, ast.Node a, ast.Type want) -> Val { + if a.kind == "var" && want.kind == "ptr" && want.elem.len == 1 && want.elem[0].kind == "struct" { + id := lookup(g, a.name) + if id >= 0 && ast.same(varAt(g, id).ty, want.elem[0]) { + pl := place(g, a, true) + return Val(v: pl.addr, ty: ast.ptrTo(pl.ty)) + } + } + return exprWant(g, a, want) +} + fn exprWant(ptr[Gen] g, ast.Node e, ast.Type want) -> Val { if e.kind == "array" { return arrayLit(g, e, want) @@ -3900,6 +3913,21 @@ fn ref(ptr[Gen] g, ast.Node e, bool write) -> Ref { id := lookup(g, e.name) if id < 0 && g.ci.consts.has(e.name) { c := g.ci.consts[e.name] + if c.structName.len > 0 { + // RAYWHITE: Color(245, 245, 245, 255) + si := findStruct(g, "C", c.structName) + []ast.Node args + []str names + for i in 0..c.fields.len { + kind := "int" + if c.fieldFloat[i] { + kind = "float" + } + args.push(ast.Node(kind: kind, text: c.fields[i], line: e.line, col: e.col, file: e.file)) + names.push(g.structs[si].fnames[i]) // by name: C leaves the fields not given at 0 + } + return Ref(isPlace: false, val: construct(g, si, args, names, e)) + } if c.isStr { return Ref(isPlace: false, val: strConst(g, c.s)) } @@ -4986,7 +5014,7 @@ fn callC(ptr[Gen] g, ast.Node c, cimport.CFunc f) -> Val { if i < f.params.len { pt := ptypes[i] a := pl.params[i] - v := coerce(g, exprWant(g, c.kids[i], pt), pt, c.kids[i], "argument " + str(i + 1) + " of " + f.name) + v := coerce(g, argWant(g, c.kids[i], pt), pt, c.kids[i], "argument " + str(i + 1) + " of " + f.name) temps.push(v) hold(g, v) if a.kind == "memory" { @@ -5391,6 +5419,9 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { if n == "assert" { return assertCall(g, c) } + if ["atomic_add", "atomic_sub", "atomic_load", "atomic_store", "atomic_swap", "atomic_cas"].contains(n) { + return atomicCall(g, c) + } if lookup(g, n) >= 0 { fail(g, c, "'" + n + "' is a variable, not a function") } @@ -5502,6 +5533,55 @@ fn mathCall(ptr[Gen] g, ast.Node c) -> Val { return out } +fn atomicAlign(ptr[Gen] g, ast.Type t) -> str { + return str(bitsOf(t) / 8) +} + +// atomic_add(p, n) and the rest: one step on a whole number that other threads see all at once. +// p is a ptr[int] (or any whole number type) to it; add, sub and swap give back what was there before, +// cas(p, old, new) puts new there only if old is there, and says whether it did. +fn atomicCall(ptr[Gen] g, ast.Node c) -> Val { + n := c.name + if g.index { + noteRef(g, c, c.col, n.len, docs.hover(docs.find(docs.builtins(), n)), "", 0, 0) + } + want := 2 + if n == "atomic_load" { + want = 1 + } else if n == "atomic_cas" { + want = 3 + } + checkArgs(g, c, 0, want, n) + p := expr(g, c.kids[0]) + if p.ty.kind != "ptr" || p.ty.elem.len == 0 || !isInt(p.ty.elem[0]) { + fail(g, c.kids[0], n + "(...) works on a whole number through its address, like " + n + "(addr(count), ...) -- this is " + ast.show(p.ty)) + } + t := p.ty.elem[0] + lt := llty(g, t) + []str vs + for i in 1..c.kids.len { + vs.push(coerce(g, exprWant(g, c.kids[i], t), t, c.kids[i], "the value").v) + } + if n == "atomic_load" { + return Val(v: op(g, "load atomic " + lt + ", ptr " + p.v + " seq_cst, align " + atomicAlign(g, t)), ty: t) + } + if n == "atomic_store" { + emit(g, "store atomic " + lt + " " + vs[0] + ", ptr " + p.v + " seq_cst, align " + atomicAlign(g, t)) + return Val(ty: T("void")) + } + if n == "atomic_cas" { + r := op(g, "cmpxchg ptr " + p.v + ", " + lt + " " + vs[0] + ", " + lt + " " + vs[1] + " seq_cst seq_cst") + return Val(v: op(g, "extractvalue { " + lt + ", i1 } " + r + ", 1"), ty: T("bool")) + } + how := "add" + if n == "atomic_sub" { + how = "sub" + } else if n == "atomic_swap" { + how = "xchg" + } + return Val(v: op(g, "atomicrmw " + how + " ptr " + p.v + ", " + lt + " " + vs[0] + " seq_cst"), ty: t) +} + // 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 @@ -5537,7 +5617,7 @@ fn callFn(ptr[Gen] g, int fi, ast.Node c, int first, str shown, str selfAddr, st mark := g.pending.len for k in 0..sig.params.len { a := c.kids[first + k] - v := coerce(g, exprWant(g, a, sig.params[k]), sig.params[k], a, "argument '" + sig.pnames[k] + "'") + v := coerce(g, argWant(g, a, sig.params[k]), sig.params[k], a, "argument '" + sig.pnames[k] + "'") // the argument lives inside the receiver (c.merge(c)): the method gets its own copy if owning(g, sig.params[k]) && !v.fresh && selfRoot.len > 0 && rootVar(a) == selfRoot { v = Val(v: copyValue(g, v.v, v.ty), ty: v.ty, fresh: true) diff --git a/src/host.fch b/src/host.fch index 1b1b8bd..3a13551 100644 --- a/src/host.fch +++ b/src/host.fch @@ -81,3 +81,18 @@ fn removeTree(str path) { fn move(str from, str to) { shell("mv " + quote(from) + " " + quote(to)) } + +// the files under dir (and its folders) whose names end with ext, like ".fch" +fn listFiles(str dir, str ext) -> []str { + tmp := tempDir() + "/finch_files_" + str(pid()) + ".txt" + run("find " + quote(dir) + " -type f -name " + quote("*" + ext) + " > " + quote(tmp) + " 2> " + nullDevice()) + text := read_file(tmp) or "" + remove(tmp) + []str out + for line in text.replace("\r", "").split("\n") { + if line.ends_with(ext) { + out.push(line) + } + } + return out +} diff --git a/src/host_windows.fch b/src/host_windows.fch index 9801e39..93a2ad9 100644 --- a/src/host_windows.fch +++ b/src/host_windows.fch @@ -102,3 +102,18 @@ fn move(str from, str to) { delete_file(to) rename(from, to) } + +// the files under dir (and its folders) whose names end with ext, like ".fch" +fn listFiles(str dir, str ext) -> []str { + tmp := tempDir() + "/finch_files_" + str(pid()) + ".txt" + run("dir /s /b " + quote(dir.replace("/", "\\") + "\\*" + ext) + " > " + quote(tmp) + " 2> " + nullDevice()) + text := read_file(tmp) or "" + remove(tmp) + []str out + for line in text.replace("\r", "").split("\n") { + if line.ends_with(ext) { + out.push(line) + } + } + return out +} diff --git a/src/linker.fch b/src/linker.fch index b160a79..19223bc 100644 --- a/src/linker.fch +++ b/src/linker.fch @@ -41,6 +41,33 @@ fn quote(str a) -> str { return host.quote(a) } +// a file of the program's own (next to the .fch file), not a library's name +fn isOwnCode(str l) -> bool { + return l.ends_with(".c") || l.ends_with(".o") || l.ends_with(".a") || l.ends_with(".lib") || isCpp(l) +} + +fn isCpp(str l) -> bool { + return l.ends_with(".cpp") || l.ends_with(".cc") || l.ends_with(".cxx") +} + +// the C++ compiler that goes with a C compiler: clang -> clang++, x86_64-w64-mingw32-gcc -> ...-g++, cc -> c++ +fn cxxFor(str cc) -> str { + if getenv("FINCH_CXX").len > 0 { + return getenv("FINCH_CXX") + } + words := cc.split(" ") + first := words[0] + if first.ends_with("clang") { + first = first + "++" + } else if first.ends_with("gcc") { + first = first.sub(0, first.len - 3) + "g++" + } else if first.ends_with("cc") { + first = first.sub(0, first.len - 2) + "c++" + } + words[0] = first + return words.join(" ") +} + // The files a C file was built from, as the compiler's -MMD wrote them: "x.o: x.c x.h \\n y.h" fn depFiles(str dep) -> []str { colon := dep.find(": ") @@ -333,10 +360,22 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int { host.remove(log) return 1 } - cmd := cc + " " + quote(obj) + " " + quote(rtobj) + " -o " + quote(b.out) + // with C++ files the C++ compiler links, so its library comes along + linkWith := cc + for l in b.links { + if isCpp(l) { + linkWith = cxxFor(cc) + } + } + cmd := linkWith + " " + quote(obj) + " " + quote(rtobj) + " -o " + quote(b.out) if !tg.msvc { cmd = cmd + " -lm" } + if tg.android { + cmd = cmd + " -llog" // bionic has threads built in + } else if !tg.windows && !tg.darwin { + cmd = cmd + " -lpthread" // std/thread (in glibc since 2.34, needed before) + } if b.ldflags.len > 0 { cmd = cmd + b.ldflags // finch.toml: libdirs and ldflags } @@ -351,19 +390,23 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int { } } for l in b.links { - if l.ends_with(".c") { - // the program's own C file, built along with it (and kept in the cache until it changes) + if l.ends_with(".c") || isCpp(l) { + // the program's own C or C++ file, built along with it (and kept in the cache until it changes) pic := " -fPIC" if tg.windows { pic = "" } - o := cObject(l, cc, " -O2" + pic + b.cflags, tg, log) or { + compiler := cc + if isCpp(l) { + compiler = cxxFor(cc) + } + o := cObject(l, compiler, " -O2" + pic + b.cflags, tg, log) or { print(err) host.remove(log) return 1 } cmd = cmd + " " + quote(o) - } else if l.ends_with(".o") || l.ends_with(".a") { + } else if l.ends_with(".o") || l.ends_with(".a") || l.ends_with(".lib") { cmd = cmd + " " + quote(l) } else { cmd = cmd + " " + libFlags(l, tg) diff --git a/src/lsp.fch b/src/lsp.fch index 5b5be23..2c52fdb 100644 --- a/src/lsp.fch +++ b/src/lsp.fch @@ -7,6 +7,7 @@ import json import host import docs +import fmt import "stdio.h" import "stdlib.h" @@ -785,6 +786,7 @@ fn Server.answer(json.Json msg) { caps.set("hoverProvider", json.new_bool(true)) caps.set("definitionProvider", json.new_bool(true)) caps.set("documentSymbolProvider", json.new_bool(true)) + caps.set("documentFormattingProvider", json.new_bool(true)) comp := json.new_object() dot := json.new_list() dot.push(json.new_text(".")) @@ -839,6 +841,32 @@ fn Server.answer(json.Json msg) { empty.set("diagnostics", json.new_list()) notify("textDocument/publishDiagnostics", empty) } + "textDocument/formatting" -> { + // finch fmt on the open text: one edit that puts the whole text in its place + path := pathOf(params) + edits := json.new_list() + if self.docs.has(path) { + old := self.docs[path] + neat := fmt.format(old) + if neat != old { + lines := old.split("\n") + whole := json.new_object() + start := json.new_object() + start.set("line", num(0)) + start.set("character", num(0)) + end := json.new_object() + end.set("line", num(lines.len)) + end.set("character", num(0)) + whole.set("start", start) + whole.set("end", end) + edit := json.new_object() + edit.set("range", whole) + edit.set("newText", json.new_text(neat)) + edits.push(edit) + } + } + reply(id, edits) + } "textDocument/hover", "textDocument/definition", "textDocument/completion", "textDocument/documentSymbol", "textDocument/signatureHelp" -> { path := pathOf(params) line := params.get("position").get("line").int_or(0) + 1 diff --git a/src/main.fch b/src/main.fch index a5fb39b..f4fd6f1 100644 --- a/src/main.fch +++ b/src/main.fch @@ -15,6 +15,8 @@ import linker import lsp import host import embedded +import manifest +import fmt import "stdlib.h" fn dirOf(str path) -> str { @@ -160,6 +162,8 @@ fn usage() { print(" finch build [-o name] build a program") print(" finch test run its test \"...\" { } blocks (and those of the modules it imports)") print(" finch ir print the LLVM IR") + print(" finch fmt [--check] [files/dirs] indent .fch files by { }, 4 spaces (--check: only list those it would change)") + print(" (with a finch.toml, run, build and test can leave out the file: its entry is used)") print(" finch version show the version") print(" finch lsp the language server, for editors (VS Code, Kate, ...)") print("options:") @@ -190,23 +194,98 @@ fn unpack() -> str { return dir } +// finch fmt [--check] [files or folders]: the indentation and empty lines of .fch files (all under here by default) +fn format([]str args) -> int { + check := false + []str targets + for i in 2..args.len { + if args[i] == "--check" { + check = true + } else { + targets.push(args[i]) + } + } + if targets.len == 0 { + targets.push(".") + } + []str files + for t in targets { + if t.ends_with(".fch") { + files.push(t) + } else { + for f in host.listFiles(t, ".fch") { + files.push(f) + } + } + } + changed := 0 + for f in files { + text := read_file(f) or { + print("error: can't read " + f) + return 1 + } + neat := fmt.format(text) + if neat != text { + changed += 1 + if check { + print(f) + } else { + write_file(f, neat) + } + } + } + if check && changed > 0 { + print(str(changed) + " file(s) are not formatted (finch fmt changes them)") + return 1 + } + return 0 +} + fn main([]str args) -> int { if args.len == 2 && (args[1] == "version" || args[1] == "--version") { print("Finch 2.4.0 (the compiler is written in Finch)") return 0 } + if args.len >= 2 && args[1] == "fmt" { + return format(args) + } if args.len == 2 && args[1] == "lsp" { return lsp.run(args[0]) // the language server, for editors } - if args.len < 3 { + if args.len < 2 { usage() } cmd := args[1] - file := args[2] if cmd != "build" && cmd != "run" && cmd != "ir" && cmd != "index" && cmd != "test" { usage() } + file := "" + ai := 2 + if args.len > 2 && !args[2].starts_with("-") { + file = args[2] + ai = 3 + } + // finch.toml: next to the file, or above it (with no file: from here, and its entry is the file) + from := host.cwd() + if file.len > 0 { + from = dirOf(absolute(file)) + } + man := manifest.find(from) or { + print(err) + return 1 + } + if file.len == 0 { + entry := man.value("entry", "") + if !man.found || entry.len == 0 { + print("error: which file? (finch " + cmd + " , or a finch.toml with entry = \"main.fch\")") + return 1 + } + file = man.resolve(entry) + } out := stem(file) + if man.found && man.value("name", "").len > 0 { + out = man.value("name", "") + } outGiven := false shared := false debug := false @@ -219,7 +298,6 @@ fn main([]str args) -> int { []str overlayPaths []str overlayFiles []str progArgs - ai := 3 while ai < args.len { a := args[ai] if cmd == "run" && progArgs.len > 0 { // after the first argument of the program, all of them are its @@ -298,6 +376,48 @@ fn main([]str args) -> int { } loader.overlayPaths = overlayPaths loader.overlayFiles = overlayFiles + // what finch.toml adds, for every system and for this one + []str targets + sys := "linux" + if tg.windows { + sys = "windows" + } else if tg.darwin { + sys = "macos" + } else if tg.android { + sys = "android" + } + arch := "x86_64" + if tg.arm64 { + arch = "arm64" + } + targets.push(sys) + targets.push(sys + "-" + arch) + ldflags := "" + cflags := "" + if man.found { + for d in man.list("paths", targets) { + loader.paths.push(man.resolve(d)) + } + for d in man.list("include", targets) { + extraDirs.push(man.resolve(d)) + } + for l in man.list("links", targets) { + if l.ends_with(".c") || l.ends_with(".o") || l.ends_with(".a") || l.ends_with(".cpp") || l.ends_with(".cc") { + extraLibs.push(man.resolve(l)) + } else { + extraLibs.push(l) + } + } + for d in man.list("libdirs", targets) { + ldflags = ldflags + " -L" + linker.quote(man.resolve(d)) + } + for f in man.list("ldflags", targets) { + ldflags = ldflags + " " + linker.quote(f) + } + for f in man.list("cflags", targets) { + cflags = cflags + " " + linker.quote(f) + } + } for d in getenv("FINCH_PATH").split(host.pathSeparator()) { if d.len > 0 { loader.paths.push(d) @@ -322,7 +442,7 @@ fn main([]str args) -> int { } } for l in prog.links { - if l.ends_with(".c") || l.ends_with(".o") || l.ends_with(".a") { + if linker.isOwnCode(l) { links.push(d + "/" + l) // the program's own C code, next to the .fch file } else { links.push(l) @@ -377,7 +497,8 @@ fn main([]str args) -> int { for d in extraDirs { cflagList.push(" -I" + linker.quote(d)) } - if linker.build(linker.Build(out: out, ir: ir, rt: rt, opt: opt, extra: extra, shared: shared, links: links, name: absolute(file), cflags: cflagList.join("")), tg, ci) != 0 { + cflagList.push(cflags) + if linker.build(linker.Build(out: out, ir: ir, rt: rt, opt: opt, extra: extra, shared: shared, links: links, name: absolute(file), cflags: cflagList.join(""), ldflags: ldflags), tg, ci) != 0 { return 1 } if cmd == "run" || cmd == "test" { diff --git a/src/manifest.fch b/src/manifest.fch new file mode 100644 index 0000000..2c846b0 --- /dev/null +++ b/src/manifest.fch @@ -0,0 +1,243 @@ +// finch.toml: what a project needs to build, in one file next to its code (instead of FINCH_PATH, +// C_INCLUDE_PATH, -I, -l and linker flags on the command line). +// +// name = "game" the program finch build makes +// entry = "src/main.fch" what finch run / build / test use when no file is given +// paths = ["src", "lib"] folders with modules +// include = ["vendor/SDL3/include"] folders with C headers (also for link "x.c" files) +// links = ["SDL3"] libraries +// libdirs = ["vendor/SDL3/lib"] folders with libraries +// cflags = ["-DFAST"] for the program's own C files +// ldflags = ["-Wl,-rpath,."] for the linker +// +// [target.windows] added only when building for that system: +// links = ["ws2_32"] windows, linux, macos, android, or with the CPU: linux-arm64 +// +// Folders are relative to finch.toml. Only this part of TOML is read: [sections], key = "text", +// key = ["a", "b"] (over several lines too), numbers, true/false and # comments. + +struct Manifest { + bool found + str path + str dir + map[str]str values // "section.key" (top level: "key") -> a single value + map[str][]str lists // "section.key" -> a list +} + +fn dirOf(str path) -> str { + i := path.len - 1 + while i >= 0 && path[i] != '/' && path[i] != '\\' { + i -= 1 + } + if i < 0 { + return "." + } + if i == 0 { + return "/" + } + return path.sub(0, i) +} + +// finch.toml in `dir` or the folders above it +fn find(str dir) -> Manifest! { + d := dir + while true { + p := d + "/finch.toml" + if d == "/" { + p = "/finch.toml" + } + if file_exists(p) { + return try parse(p, try read_file(p)) + } + up := dirOf(d) + if up == d || d == "." || d.len == 0 { + break + } + d = up + } + return Manifest() +} + +struct Reader { + str text + int i + int line + str path +} + +fn Reader.fail(str msg) -> str { + return self.path + ":" + str(self.line) + ": error: " + msg +} + +fn Reader.at() -> char { + if self.i < self.text.len { + return self.text[self.i] + } + return char(0) +} + +// past spaces, and past line ends and comments too when `lines` +fn Reader.skip(bool lines) { + while self.i < self.text.len { + c := self.text[self.i] + if c == ' ' || c == '\t' || c == '\r' { + self.i += 1 + } else if c == '#' { + while self.i < self.text.len && self.text[self.i] != '\n' { + self.i += 1 + } + } else if c == '\n' && lines { + self.i += 1 + self.line += 1 + } else { + return + } + } +} + +fn isKeyChar(char c) -> bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.' +} + +fn Reader.word() -> str { + start := self.i + while self.i < self.text.len && isKeyChar(self.text[self.i]) { + self.i += 1 + } + return self.text.sub(start, self.i) +} + +// "text" (with \" \\ \n \t) or 'text' (as it is) +fn Reader.quoted() -> str! { + q := self.at() + self.i += 1 + []str out + while self.at() != q { + c := self.at() + if c == char(0) || c == '\n' { + return error(self.fail("this text is never closed")) + } + if c == '\\' && q == '"' { + self.i += 1 + e := self.at() + if e == 'n' { + c = '\n' + } else if e == 't' { + c = '\t' + } else { + c = e + } + } + out.push(str(c)) + self.i += 1 + } + self.i += 1 + return out.join("") +} + +// a single value: text, a number, true or false (all kept as text) +fn Reader.scalar() -> str! { + c := self.at() + if c == '"' || c == '\'' { + return try self.quoted() + } + w := self.word() + if w.len == 0 { + return error(self.fail("expected a value: \"text\", a number, true, false or a [list]")) + } + return w +} + +fn parse(str path, str text) -> Manifest! { + m := Manifest(found: true, path: path, dir: dirOf(path)) + r := Reader(text: text, i: 0, line: 1, path: path) + section := "" + while true { + r.skip(true) + if r.i >= r.text.len { + break + } + if r.at() == '[' { + r.i += 1 + r.skip(false) + section = r.word() + r.skip(false) + if r.at() != ']' || section.len == 0 { + return error(r.fail("a section looks like [name] or [target.windows]")) + } + r.i += 1 + continue + } + key := r.word() + if key.len == 0 { + return error(r.fail("expected a key, like name = \"game\"")) + } + r.skip(false) + if r.at() != '=' { + return error(r.fail("expected '=' after '" + key + "'")) + } + r.i += 1 + r.skip(false) + full := key + if section.len > 0 { + full = section + "." + key + } + if r.at() == '[' { + r.i += 1 + []str items + while true { + r.skip(true) + if r.at() == ']' { + r.i += 1 + break + } + items.push(try r.scalar()) + r.skip(true) + if r.at() == ',' { + r.i += 1 + } else if r.at() != ']' { + return error(r.fail("expected ',' or ']' in the list of '" + key + "'")) + } + } + m.lists[full] = items + } else { + m.values[full] = try r.scalar() + } + r.skip(false) + if r.i < r.text.len && r.at() != '\n' { + return error(r.fail("one key per line")) + } + } + return m +} + +fn Manifest.value(str key, str fallback) -> str { + return self.values.get(key, fallback) +} + +// the list `key` from the top level and from the sections for this system, in that order +fn Manifest.list(str key, []str targets) -> []str { + []str out + if self.lists.has(key) { + for x in self.lists[key] { + out.push(x) + } + } + for t in targets { + k := "target." + t + "." + key + if self.lists.has(k) { + for x in self.lists[k] { + out.push(x) + } + } + } + return out +} + +// a folder or file of the manifest, from where finch.toml is +fn Manifest.resolve(str p) -> str { + if p.starts_with("/") || (p.len > 2 && p[1] == ':') { + return p + } + return self.dir + "/" + p +} diff --git a/src/parser.fch b/src/parser.fch index 53fbbaa..ce3aff3 100644 --- a/src/parser.fch +++ b/src/parser.fch @@ -257,7 +257,7 @@ fn parseProgram(ptr[Parser] p) -> ast.Program { } else if at(p, "link") { lt := next(p) lib := expect(p, "string", "a library name in quotes, like link \"glfw\"").text - ownCode := lib.ends_with(".c") || lib.ends_with(".o") || lib.ends_with(".a") // your own C code, next to the .fch file + ownCode := lib.ends_with(".c") || lib.ends_with(".o") || lib.ends_with(".a") || lib.ends_with(".cpp") || lib.ends_with(".cc") || lib.ends_with(".cxx") || lib.ends_with(".lib") // your own C or C++ code, next to the .fch file if !ownCode && (lib.starts_with("lib") || lib.contains(".so") || lib.contains("/")) { fail(p, lt, "write just the library's name: for libglfw.so that's link \"glfw\"") } diff --git a/src/platform.fch b/src/platform.fch index b7a6992..44a678f 100644 --- a/src/platform.fch +++ b/src/platform.fch @@ -11,6 +11,7 @@ struct Target { bool darwin // macOS bool arm64 // AAPCS64 bool cross // another system than the one the compiler runs on + bool android // Android (bionic): a .so that an app loads str exe // ".exe" or "" str cc // the C compiler that links (the IR is always compiled by clang) str runner // how to run a program for it here: "", "wine ", "qemu-aarch64 " @@ -45,6 +46,8 @@ fn select(str name) -> Target { t = "x86_64-unknown-linux-gnu" } else if name == "arm64" || name == "aarch64" || name == "linux-arm64" { t = "aarch64-unknown-linux-gnu" + } else if name == "android" { + t = "aarch64-linux-android24" } else if name == "macos" { if hostName.starts_with("aarch64") || hostName.starts_with("arm64") { t = "arm64-apple-macosx14.0.0" @@ -60,6 +63,7 @@ fn select(str name) -> Target { tg.msvc = t.contains("msvc") tg.darwin = t.contains("apple") || t.contains("darwin") || t.contains("macos") tg.arm64 = t.starts_with("aarch64") || t.starts_with("arm64") + tg.android = t.contains("android") hostArm := hostName.starts_with("aarch64") || hostName.starts_with("arm64") hostDarwin := hostName.contains("apple") || hostName.contains("darwin") hostWin := hostName.contains("windows") || hostName.contains("mingw") || hostName.contains("msvc") @@ -72,6 +76,8 @@ fn select(str name) -> Target { fc := getenv("FINCH_CC") if fc.len > 0 { tg.cc = fc + } else if tg.android { + tg.cc = androidClang(t) } else if tg.cross && tg.windows { tg.cc = "x86_64-w64-mingw32-gcc" } else if tg.cross && tg.arm64 && !tg.darwin { @@ -85,12 +91,30 @@ fn select(str name) -> Target { } if tg.cross && tg.windows { tg.runner = "wine " - } else if tg.cross && tg.arm64 && !tg.darwin { + } else if tg.cross && tg.arm64 && !tg.darwin && !tg.android { tg.runner = "qemu-aarch64 " } return tg } +// The NDK's clang for this Android triple (ANDROID_NDK_HOME or ANDROID_NDK_ROOT says where the NDK is) +fn androidClang(str triple) -> str { + ndk := getenv("ANDROID_NDK_HOME") + if ndk.len == 0 { + ndk = getenv("ANDROID_NDK_ROOT") + } + if ndk.len == 0 { + return triple + "-clang" + } + for h in ["linux-x86_64", "darwin-x86_64", "windows-x86_64"] { + cc := ndk + "/toolchains/llvm/prebuilt/" + h + "/bin/" + triple + "-clang" + if file_exists(cc) || file_exists(cc + ".cmd") { + return cc + } + } + return triple + "-clang" +} + // C's plain `char`: signed on x86 and Apple, not on ARM64 Linux fn charIsSigned(Target t) -> bool { return !t.arm64 || t.darwin || t.windows diff --git a/std/finch_thread.h b/std/finch_thread.h new file mode 100644 index 0000000..c8a27b4 --- /dev/null +++ b/std/finch_thread.h @@ -0,0 +1,8 @@ +/* The runtime's thread functions, for std/thread.fch (they are in runtime/finch_rt.c, linked into every program). */ +void *finch_thread_spawn(void (*fn)(void *), void *arg); +int finch_thread_join(void *thread); +void *finch_mutex_new(void); +void finch_mutex_lock(void *mutex); +void finch_mutex_unlock(void *mutex); +void finch_mutex_free(void *mutex); +int finch_cpu_count(void); diff --git a/std/thread.fch b/std/thread.fch new file mode 100644 index 0000000..275bc7d --- /dev/null +++ b/std/thread.fch @@ -0,0 +1,65 @@ +// Threads: run a function next to the rest of the program, and a lock for what they share. +// +// import thread +// fn work(ptr arg) { a thread's function takes one ptr (to whatever it needs) +// ptr[Job] job = arg +// ... +// } +// t := try thread.spawn(addr(work), addr(job)) +// t.join() wait for it; `job` must live until then +// +// m := thread.mutex() +// defer m.close() +// m.lock() ... m.unlock() one thread at a time in between +// atomic_add(addr(count), 1) (built in) numbers changed by many threads at once +// +// What a thread is given is shared, not copied: don't let it outlive the block it comes from, and +// change it only under a lock (or with atomic_...). +import "finch_thread.h" + +struct Thread { + ptr handle +} + +struct Mutex { + ptr handle +} + +// start fn(arg) on a new thread; fn is addr(f) of a fn f(ptr arg) +fn spawn(ptr fn_, ptr arg) -> Thread! { + h := finch_thread_spawn(fn_, arg) + if h == null { + return error("can't start a thread") + } + return Thread(handle: h) +} + +// wait until the thread has finished +fn Thread.join() { + if self.handle != null { + finch_thread_join(self.handle) + self.handle = null + } +} + +fn mutex() -> Mutex { + return Mutex(handle: finch_mutex_new()) +} + +fn Mutex.lock() { + finch_mutex_lock(self.handle) +} + +fn Mutex.unlock() { + finch_mutex_unlock(self.handle) +} + +fn Mutex.close() { + finch_mutex_free(self.handle) + self.handle = null +} + +// how many threads the computer runs at the same time +fn cpus() -> int { + return int(finch_cpu_count()) +} diff --git a/tests/lsp_test.py b/tests/lsp_test.py index f9c74b4..c4609d3 100755 --- a/tests/lsp_test.py +++ b/tests/lsp_test.py @@ -124,6 +124,12 @@ syms = c.request("textDocument/documentSymbol", {"textDocument": td}) names = sorted(s["name"] for s in syms) check("document symbols", names == ["Point", "add", "main"], names) +messy = "fn main() {\n x := 1 \n\n\n\tprint(x)\n}" +c.notify("textDocument/didChange", {"textDocument": {"uri": uri, "version": 5}, "contentChanges": [{"text": messy}]}) +fm = c.request("textDocument/formatting", {"textDocument": td, "options": {"tabSize": 4, "insertSpaces": True}}) +check("formatting", fm and fm[0]["newText"] == "fn main() {\n x := 1\n\n print(x)\n}\n", fm) +c.notify("textDocument/didChange", {"textDocument": {"uri": uri, "version": 5}, "contentChanges": [{"text": src}]}) + bad = src.replace("total := add(p.x, p.y)", 'total := add(p.x, "y")') c.notify("textDocument/didChange", {"textDocument": {"uri": uri, "version": 6}, "contentChanges": [{"text": bad}]}) diags = c.diagnostics(uri) diff --git a/tests/new/auto_addr.fch b/tests/new/auto_addr.fch new file mode 100644 index 0000000..ad3a68e --- /dev/null +++ b/tests/new/auto_addr.fch @@ -0,0 +1,23 @@ +struct Body { + float x + float y +} + +fn push(ptr[Body] b, float dx) { + b.x += dx +} + +fn Body.moved(float dx) -> Body { + return Body(x: self.x + dx, y: self.y) +} + +fn main() { + b := Body(x: 1.0, y: 2.0) + push(b, 0.5) // the same as push(addr(b), 0.5) + push(addr(b), 0.5) + print(b.x) + p := addr(b) + push(p, 1.0) // already a ptr: as it is + print(b.x) + print(b.moved(1.0).x) +} diff --git a/tests/new/auto_addr.out b/tests/new/auto_addr.out new file mode 100644 index 0000000..dcf37cd --- /dev/null +++ b/tests/new/auto_addr.out @@ -0,0 +1,3 @@ +2 +3 +4 diff --git a/tests/new/c_unions_macros.fch b/tests/new/c_unions_macros.fch index 4126622..efe07a3 100644 --- a/tests/new/c_unions_macros.fch +++ b/tests/new/c_unions_macros.fch @@ -24,4 +24,6 @@ fn main() { t.as.f = 0.5 print(t.as.f) print(abs(-2.5), abs(-3), min(EV_KEY, 7)) // Finch's abs, not C's int-only one + bg := RAYWHITE // #define RAYWHITE (Color){ 245, 245, 245, 255 } + print(bg.r, bg.a, DARK.g, DARK.a, HALF_UP.x, HALF_UP.y) } diff --git a/tests/new/c_unions_macros.out b/tests/new/c_unions_macros.out index 3e6d42f..95a18c6 100644 --- a/tests/new/c_unions_macros.out +++ b/tests/new/c_unions_macros.out @@ -5,3 +5,4 @@ true 32 32 2 0.5 2.5 3 7 +245 255 20 0 0.5 -1 diff --git a/tests/new/cunion.h b/tests/new/cunion.h index 869cfb3..72b5e1e 100644 --- a/tests/new/cunion.h +++ b/tests/new/cunion.h @@ -22,6 +22,14 @@ typedef struct { int kind; union { int i; float f; } as; } Tagged; #define LONG_NAME "a" "b" #define NOT_A_CONSTANT do { } while (0) +typedef struct { unsigned char r, g, b, a; } Color; +typedef struct { float x, y; } Vec2; +#define CLITERAL(type) (type) +#define RAYWHITE CLITERAL(Color){ 245, 245, 245, 255 } +#define DARK (Color){ 20, 20, 20 } +#define HALF_UP (Vec2){ 0.5f, -1 } +#define NOT_A_STRUCT_CONST (Color){ 1, 2, (unsigned char)(long)&poll_event, 4 } + static inline int fill_key(Event *e, int key) { e->key.type = EV_KEY; e->key.key = key; diff --git a/tests/new/fail/auto_addr_value.fch b/tests/new/fail/auto_addr_value.fch new file mode 100644 index 0000000..a925084 --- /dev/null +++ b/tests/new/fail/auto_addr_value.fch @@ -0,0 +1,13 @@ +// expect: argument 'b' must be ptr[Body], but this is Body +struct Body { + float x +} +fn push(ptr[Body] b) { + b.x += 1.0 +} +fn make() -> Body { + return Body(x: 1.0) +} +fn main() { + push(make()) +} diff --git a/tests/new/interpolation.fch b/tests/new/interpolation.fch new file mode 100644 index 0000000..a95efc8 --- /dev/null +++ b/tests/new/interpolation.fch @@ -0,0 +1,21 @@ +// "\(x)" in text +struct P { + int x + float y +} + +fn main() { + name := "Ada" + hp := 7 + p := P(x: 3, y: 0.25) + print("hi \(name), hp \(hp), next \(hp + 1)") + print("p = (\(p.x), \(p.y, 2)) \("nested \(name)")") + map[str]int m + m["k"] = 5 + print("\(m["k"]) \(str(1.0 / 3.0, 3))") + print("no code here: \\(x) and \t tab") + long := """ + speed \(p.y * 4.0) + """ + print(long) +} diff --git a/tests/new/interpolation.out b/tests/new/interpolation.out new file mode 100644 index 0000000..d1dbb2e --- /dev/null +++ b/tests/new/interpolation.out @@ -0,0 +1,5 @@ +hi Ada, hp 7, next 8 +p = (3, 0.25) nested Ada +5 0.333 +no code here: \(x) and tab +speed 1 diff --git a/tests/new/threads.fch b/tests/new/threads.fch new file mode 100644 index 0000000..b57ad8f --- /dev/null +++ b/tests/new/threads.fch @@ -0,0 +1,55 @@ +import thread + +struct Shared { + []int seen +} + +struct Job { + ptr[int] counter + int adds + ptr[thread.Mutex] lock + ptr[Shared] common + int id +} + +fn work(ptr arg) { + ptr[Job] job = arg + for i in 0..job.adds { + atomic_add(job.counter, 1) + } + job.lock.lock() + job.common.seen.push(job.id) + job.lock.unlock() +} + +fn main() { + count := 0 + m := thread.mutex() + defer m.close() + both := Shared() + []Job jobs + for i in 0..4 { + jobs.push(Job(counter: addr(count), adds: 100000, lock: addr(m), common: addr(both), id: i)) + } + []thread.Thread ts + for i in 0..4 { + ts.push(thread.spawn(addr(work), addr(jobs[i])) or { + print("can't start") + exit(1) + }) + } + for i in 0..4 { + ts[i].join() + } + print(atomic_load(addr(count))) + print(both.seen.len) + print(thread.cpus() > 0) + + u32 flags = 5 + print(atomic_swap(addr(flags), 9)) + print(atomic_cas(addr(flags), 9, 1)) + print(atomic_cas(addr(flags), 9, 2)) + atomic_store(addr(flags), 7) + print(atomic_sub(addr(flags), 2)) + print(flags) +} diff --git a/tests/new/threads.out b/tests/new/threads.out new file mode 100644 index 0000000..f5e1201 --- /dev/null +++ b/tests/new/threads.out @@ -0,0 +1,8 @@ +400000 +4 +true +5 +true +false +7 +5 diff --git a/tests/run.sh b/tests/run.sh index c6176d5..f887c41 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -56,6 +56,24 @@ for f in fail/*.fch new/fail/*.fch; do fi done +# the commands around the compiler: finch test, the chain of calls, finch fmt, a finch.toml project +check_tool() { # name, what it printed, the file with what it should print + if [ "$2" == "$(tr -d '\r' < "$3")" ]; then + pass=$((pass + 1)) + else + failed=$((failed + 1)) + echo "FAIL $1" + diff <(echo "$2") "$3" | head -20 + fi +} +check_tool "finch test" "$(cd tool && "$FINCH" test test_blocks.fch 2>&1 | tr -d '\r')" tool/test_blocks.out +check_tool "the chain of calls" "$(cd tool && "$FINCH" run trace.fch 2>&1 | tr -d '\r')" tool/trace.out +cp tool/fmt_in.fch "$tmp/fmt.fch" +"$FINCH" fmt "$tmp/fmt.fch" +check_tool "finch fmt" "$(cat "$tmp/fmt.fch")" tool/fmt_out.fch +check_tool "finch fmt (again)" "$("$FINCH" fmt --check tool/fmt_out.fch 2>&1)" /dev/null +check_tool "finch.toml" "$(cd tool/project && "$FINCH" run 2>&1 | tr -d '\r')" tool/project/expected.out + # the same programs with debug info (-g -O0): the metadata has to be valid for every kind of thing a program has for f in new/enums.fch new/generics.fch new/std_json.fch run/basics.fch run/methods_basic.fch run/maps.fch run/errors.fch \ run/defer.fch run/c_structs.fch run/memory.fch; do diff --git a/tests/tool/fmt_in.fch b/tests/tool/fmt_in.fch new file mode 100644 index 0000000..05d9628 --- /dev/null +++ b/tests/tool/fmt_in.fch @@ -0,0 +1,24 @@ +struct P { +int x + int y +} + + + +fn main() { + p := P(x: 1, + y: 2) + msg := """ + a { + b + """ + if p.x > 0 { +print("{ \(p.x) }") // } not counted + } else { + print(msg) + } + s := "a" + + "b" + print(s) +} + diff --git a/tests/tool/fmt_out.fch b/tests/tool/fmt_out.fch new file mode 100644 index 0000000..34e6850 --- /dev/null +++ b/tests/tool/fmt_out.fch @@ -0,0 +1,21 @@ +struct P { + int x + int y +} + +fn main() { + p := P(x: 1, + y: 2) + msg := """ + a { + b + """ + if p.x > 0 { + print("{ \(p.x) }") // } not counted + } else { + print(msg) + } + s := "a" + + "b" + print(s) +} diff --git a/tests/tool/project/expected.out b/tests/tool/project/expected.out new file mode 100644 index 0000000..d9a45b2 --- /dev/null +++ b/tests/tool/project/expected.out @@ -0,0 +1 @@ +hello world 42 diff --git a/tests/tool/project/finch.toml b/tests/tool/project/finch.toml new file mode 100644 index 0000000..4b6e207 --- /dev/null +++ b/tests/tool/project/finch.toml @@ -0,0 +1,14 @@ +# a project: finch run / build / test here need no file and no flags +name = "mygame" +entry = "src/main.fch" +paths = ["src"] +include = ["native"] +links = [ + "native/glue.cpp", # C++, built with the C++ compiler +] +cflags = ["-DGLUE_BASE=40"] + +[target.linux] +links = ["m"] +[target.windows] +links = ["ws2_32"] diff --git a/tests/tool/project/native/glue.cpp b/tests/tool/project/native/glue.cpp new file mode 100644 index 0000000..e4323a7 --- /dev/null +++ b/tests/tool/project/native/glue.cpp @@ -0,0 +1,9 @@ +// C++ behind a C header: extern "C" gives the function a name C (and Finch) can call +#include "glue.h" +#include +#include + +extern "C" int glue_answer(void) { + std::vector v = {1, 1}; + return GLUE_BASE + std::accumulate(v.begin(), v.end(), 0); +} diff --git a/tests/tool/project/native/glue.h b/tests/tool/project/native/glue.h new file mode 100644 index 0000000..dc207ab --- /dev/null +++ b/tests/tool/project/native/glue.h @@ -0,0 +1,7 @@ +#ifdef __cplusplus +extern "C" { +#endif +int glue_answer(void); +#ifdef __cplusplus +} +#endif diff --git a/tests/tool/project/src/game/world.fch b/tests/tool/project/src/game/world.fch new file mode 100644 index 0000000..d184de1 --- /dev/null +++ b/tests/tool/project/src/game/world.fch @@ -0,0 +1,3 @@ +fn hello() -> str { + return "world" +} diff --git a/tests/tool/project/src/main.fch b/tests/tool/project/src/main.fch new file mode 100644 index 0000000..18b0f03 --- /dev/null +++ b/tests/tool/project/src/main.fch @@ -0,0 +1,6 @@ +import "glue.h" +import game/world + +fn main() { + print("hello \(world.hello()) \(glue_answer())") +} diff --git a/tests/tool/test_blocks.fch b/tests/tool/test_blocks.fch new file mode 100644 index 0000000..c908c17 --- /dev/null +++ b/tests/tool/test_blocks.fch @@ -0,0 +1,30 @@ +// finch test: each test "..." block runs on its own; a failed assert or a runtime error fails only that one +fn add(int a, int b) -> int { + return a + b +} + +fn second([]int xs) -> int { + return xs[1] +} + +test "add works" { + assert(add(2, 3) == 5) + assert(add(-1, 1) == 0, "zero") +} + +test "a failing assert" { + x := add(1, 2) + assert(x == 4, "x is \(x), not 4") +} + +test "a runtime error fails only this test" { + print(second([1])) +} + +test "still runs after the others" { + assert("\(1.5, 2) and \(add(1, 1))" == "1.50 and 2") +} + +fn main() { + print("main is not run by finch test") +} diff --git a/tests/tool/test_blocks.out b/tests/tool/test_blocks.out new file mode 100644 index 0000000..f948ba9 --- /dev/null +++ b/tests/tool/test_blocks.out @@ -0,0 +1,8 @@ +ok add works +FAIL a failing assert + test_blocks.fch:17: runtime error: assert failed: x is 3, not 4 +FAIL a runtime error fails only this test + test_blocks.fch:7: runtime error: index 1 is out of range (the length is 1) +ok still runs after the others + +2 passed, 2 failed diff --git a/tests/tool/trace.fch b/tests/tool/trace.fch new file mode 100644 index 0000000..278e11a --- /dev/null +++ b/tests/tool/trace.fch @@ -0,0 +1,16 @@ +struct Enemy { + []int path +} + +fn Enemy.next(int i) -> int { + return self.path[i] +} + +fn step(Enemy e) -> int { + return e.next(3) +} + +fn main() { + e := Enemy(path: [1, 2]) + print(step(e)) +} diff --git a/tests/tool/trace.out b/tests/tool/trace.out new file mode 100644 index 0000000..8d0417c --- /dev/null +++ b/tests/tool/trace.out @@ -0,0 +1,4 @@ +trace.fch:6: runtime error: index 3 is out of range (the length is 2) + in Enemy.next (trace.fch:6) + in step (trace.fch:10) + in main (trace.fch:15)