From f9838dfe03eb8954d570eb5d3450f5b0155c86f7 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Fri, 9 Oct 2026 01:26:53 +0200 Subject: [PATCH 1/3] Fix the two known limits: adding map keys while looping over the map is a runtime error; temporaries held by an expression are freed when try / or-blocks leave early. err usable after or, inner err shadows outer. Co-Authored-By: Claude Opus 5.5 --- runtime/finch_rt.c | 9 ++- src/codegen.cpp | 64 ++++++++++++++++--- src/codegen_builtins.cpp | 24 ++++++- src/codegen_impl.h | 21 +++++- src/codegen_map.cpp | 4 ++ src/codegen_types.cpp | 8 +-- tests/fail/map_add_while_looping.fch | 7 ++ tests/run/early_exit.fch | 96 ++++++++++++++++++++++++++++ tests/run/early_exit.out | 12 ++++ 9 files changed, 228 insertions(+), 17 deletions(-) create mode 100644 tests/fail/map_add_while_looping.fch create mode 100644 tests/run/early_exit.fch create mode 100644 tests/run/early_exit.out diff --git a/runtime/finch_rt.c b/runtime/finch_rt.c index 89e6519..4cb2d13 100644 --- a/runtime/finch_rt.c +++ b/runtime/finch_rt.c @@ -596,10 +596,11 @@ int finch_try_write_file(const FStr *path, const FStr *text, FStr *err) { } // ---------- maps ---------- -// map[K]V = { entries; len; used; cap; index; icap } +// 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. // A removed entry keeps its slot (hash 0) until the next resize packs the slots. // index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number. +// adds: how many keys were ever added; a `for` over the map stops with an error if it changes. // The compiler stores, copies and drops keys and values; the runtime only finds and places entries. // Keys are compared byte for byte (numbers, chars, bools) or as text (kstr). @@ -608,6 +609,7 @@ typedef struct { int64_t len, used, cap; int64_t *index; int64_t icap; + int64_t adds; } FMap; static uint64_t map_hash(const void *key, int64_t ks, int kstr) { @@ -699,6 +701,7 @@ int64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1); m->index[s] = i; m->len++; + m->adds++; return i; } @@ -736,6 +739,10 @@ void finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) { memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t)); } +__attribute__((noreturn, cold)) void finch_map_changed(const char *file, int64_t line) { + 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)"); +} + __attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) { char msg[200]; snprintf(msg, sizeof msg, "the key %.*s is not in the map (check with .has(key), or use .get(key, default))", diff --git a/src/codegen.cpp b/src/codegen.cpp index aac0585..e0b4906 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -296,6 +296,21 @@ void Codegen::define(Fn &fn) { void Codegen::pushScope() { scopes.emplace_back(); } +void Codegen::hold(const Value_ &v) { + if (v.fresh && v.v && !isa(v.v) && owning(v.type)) pending.push_back({v.v, v.type, false}); +} + +void Codegen::holdAddr(Value *addr, const FType &t) { + if (owning(t)) pending.push_back({addr, t, true}); +} + +void Codegen::dropPending(size_t downTo) { + for (size_t i = pending.size(); i-- > downTo;) { + if (pending[i].isAddr) dropAt(pending[i].v, pending[i].type); + else dropValue(pending[i].v, pending[i].type); + } +} + void Codegen::popScope() { if (!terminated()) emitScopeCleanups(scopes.size() - 1); scopes.pop_back(); @@ -352,7 +367,8 @@ Var *Codegen::lookup(const std::string &name) { Var &Codegen::addVar(Pos p, const std::string &name, const FType &t, Value *init, bool owned, bool readonly, unsigned argNo) { - if (lookup(name)) failAt(p.file, p.line, p.col, "a variable named '" + name + "' already exists here"); + // `err` of an inner `or` hides the outer one: f() or g() or err + if (lookup(name) && name != "err") failAt(p.file, p.line, p.col, "a variable named '" + name + "' already exists here"); ModuleScope &m = modules[curModule]; if (m.fns.count(name)) failAt(p.file, p.line, p.col, "'" + name + "' is already the name of a function"); if (m.imports.count(name)) failAt(p.file, p.line, p.col, "'" + name + "' is already the name of an imported module"); @@ -427,6 +443,7 @@ void Codegen::jump(bool isBreak, Pos p) { if (inDefer) failAt(p.file, p.line, p.col, std::string(word) + " can't be used inside defer"); if (loops.empty()) failAt(p.file, p.line, p.col, std::string(word) + " can only be used inside a loop"); Loop l = loops.back(); + dropPending(l.pendingDepth); emitCleanups(l.scopeDepth); b.CreateBr(isBreak ? l.breakTo : l.continueTo); } @@ -466,6 +483,8 @@ void Codegen::assign(const AssignStmt &s) { if (Var *var = lookup(root)) { want = var->type; known = true; } } v = known ? exprWant(*s.value, want) : expr(*s.value); + Held held(*this); + hold(v); // while the target is computed (a[try f()] = ...) Place dst = place(*s.target, true); v = coerce(v, dst.type, s.value->pos, what); Value *nv = own(v); @@ -481,6 +500,8 @@ void Codegen::assign(const AssignStmt &s) { BinOp op = s.op == '+' ? BinOp::Add : s.op == '-' ? BinOp::Sub : s.op == '*' ? BinOp::Mul : s.op == '/' ? BinOp::Div : BinOp::Mod; Value_ rhs = expr(*s.value); + Held held(*this); + hold(rhs); Place dst = place(*s.target, true); Value_ cur{b.CreateLoad(ty(dst.type), dst.addr), dst.type}; Value_ v = coerce(arith(op, cur, rhs, s.pos), dst.type, s.pos, what); @@ -538,7 +559,7 @@ void Codegen::whileStmt(const WhileStmt &s) { else b.CreateCondBr(cond, body, end); b.SetInsertPoint(body); - loops.push_back({condBB, end, scopes.size()}); + loops.push_back({condBB, end, scopes.size(), pending.size()}); block(*s.body); loops.pop_back(); if (!terminated()) b.CreateBr(condBB); @@ -564,7 +585,7 @@ void Codegen::forStmt(const ForStmt &s) { b.CreateCondBr(b.CreateICmpSLT(b.CreateLoad(b.getInt64Ty(), i), to), body, end); b.SetInsertPoint(body); - loops.push_back({step, end, scopes.size()}); + loops.push_back({step, end, scopes.size(), pending.size()}); block(*s.body); loops.pop_back(); if (!terminated()) b.CreateBr(step); @@ -600,10 +621,20 @@ void Codegen::forEachStmt(const ForEachStmt &s) { bool copyKV = live && mutates(rootVar(*s.list), *s.body); Value *i = slot(FType::I64, "$i"); b.CreateStore(b.getInt64(0), i); + // adding keys inside the loop could reorder the entries: that stops the program (removing is fine) + Value *adds = live ? b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(MT, addr, 6)) : nullptr; BasicBlock *condBB = newBlock("map.cond"), *check = newBlock("map.check"), *body = newBlock("map.body"), *step = newBlock("map.step"), *end = newBlock("map.end"); b.CreateBr(condBB); b.SetInsertPoint(condBB); + if (adds && mutates(rootVar(*s.list), *s.body)) { + BasicBlock *changed = newBlock("map.changed"), *same = newBlock("map.same"); + b.CreateCondBr(b.CreateICmpNE(b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(MT, addr, 6)), adds), changed, same); + b.SetInsertPoint(changed); + b.CreateCall(rt("finch_map_changed"), {fileName(s.pos), b.getInt64(s.pos.line)}); + b.CreateUnreachable(); + b.SetInsertPoint(same); + } Value *iv = b.CreateLoad(b.getInt64Ty(), i); b.CreateCondBr(b.CreateICmpSLT(iv, b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(MT, addr, 2))), check, end); b.SetInsertPoint(check); @@ -611,7 +642,7 @@ void Codegen::forEachStmt(const ForEachStmt &s) { Value *hash = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ET, entry, 0)); b.CreateCondBr(b.CreateICmpEQ(hash, b.getInt64(0)), step, body); // a removed entry b.SetInsertPoint(body); - loops.push_back({step, end, scopes.size()}); + loops.push_back({step, end, scopes.size(), pending.size()}); pushScope(); std::string why = " can't be changed (to change the map, use m[key] = ...)"; Value *kv = b.CreateLoad(ty(kt), b.CreateStructGEP(ET, entry, 1)); @@ -662,7 +693,7 @@ void Codegen::forEachStmt(const ForEachStmt &s) { elemAddr = b.CreateInBoundsGEP(ty(et), data, iv); } Value *elem = b.CreateLoad(ty(et), elemAddr); - loops.push_back({step, end, scopes.size()}); + loops.push_back({step, end, scopes.size(), pending.size()}); pushScope(); std::string elemVar = s.var; Pos elemPos = s.varPos; @@ -718,6 +749,7 @@ void Codegen::returnStmt(const ReturnStmt &s) { if (std::string(bd.name) == "error") note(c.pos, 5, "```finch\n" + std::string(bd.signature) + "\n```\n" + bd.doc); checkArgs(c.args, c.argNames, 1, c.pos, "error"); Value *msg = own(coerce(expr(*c.args[0]), FType::Str, c.args[0]->pos, "the error message")); + dropPending(0); emitCleanups(0); retError(msg); return; @@ -725,6 +757,7 @@ void Codegen::returnStmt(const ReturnStmt &s) { if (curFn->ret.kind == FType::Void) { if (s.value) failAt(s.value->pos.file, s.value->pos.line, s.value->pos.col, "'" + d.name + "' doesn't return a value (it has no '-> type')"); + dropPending(0); emitCleanups(0); retValue(nullptr); return; @@ -740,6 +773,7 @@ void Codegen::returnStmt(const ReturnStmt &s) { if (it->second.owned && it->second.type == curFn->ret) { Value *v = b.CreateLoad(ty(it->second.type), it->second.slot); it->second.moved = true; + dropPending(0); emitCleanups(0); scopes[idx].vars[name].moved = false; retValue(v); @@ -750,6 +784,7 @@ void Codegen::returnStmt(const ReturnStmt &s) { } Value_ v = coerce(exprWant(*s.value, curFn->ret), curFn->ret, s.value->pos, "the returned value"); Value *out = own(v); + dropPending(0); emitCleanups(0); retValue(out); } @@ -889,6 +924,7 @@ Value_ Codegen::tryExpr(const TryExpr &t) { BasicBlock *bad = newBlock("try.failed"), *ok = newBlock("try.ok"); b.CreateCondBr(failed, bad, ok); b.SetInsertPoint(bad); + dropPending(0); // what the expressions around this `try` were holding emitCleanups(0); retError(msg); b.SetInsertPoint(ok); @@ -927,8 +963,10 @@ Value_ Codegen::orElse(const OrElseExpr &e, bool discard) { Pos fp = e.fallback->pos; if (v.type.kind == FType::Void) failAt(fp.file, fp.line, fp.col, "this call gives back no value, so there is nothing to replace; handle the failure with or { ... }"); - dropValue(msg, FType::Str); + pushScope(); // the fallback can use the message too: f(x) or err + addVar(fp, "err", FType::Str, msg, true); Value *fv = own(coerce(exprWant(*e.fallback, v.type), v.type, fp, "the value after 'or'")); + popScope(); BasicBlock *badEnd = b.GetInsertBlock(); BasicBlock *join = newBlock("or.end"); b.CreateBr(join); @@ -1126,6 +1164,8 @@ LRef Codegen::index(const IndexExpr &e, bool forWrite) { Pos p = e.pos; LRef obj = ref(*e.obj, forWrite); FType t = obj.isPlace ? obj.pl.type : obj.val.type; + Held held(*this); + if (!obj.isPlace) hold(obj.val); // while the index is computed if (t.kind == FType::Map) return mapIndex(e, obj, t, forWrite); Value *i = coerce(expr(*e.index), FType::I64, e.index->pos, "an index").v; @@ -1180,6 +1220,7 @@ Value_ Codegen::arrayLit(const ArrayLitExpr &a, const FType *want) { if (allNum && anyFloat) et = FType::F64; } std::vector vals; + Held held(*this); for (size_t k = 0; k < a.elems.size(); k++) { Value_ v = et.kind == FType::Void ? exprWant(*a.elems[k], et) : exprWant(*a.elems[k], et); if (et.kind == FType::Void) { @@ -1189,6 +1230,7 @@ Value_ Codegen::arrayLit(const ArrayLitExpr &a, const FType *want) { } v = coerce(v, et, a.elems[k]->pos, "every element of this array"); vals.push_back(own(v)); + hold({vals.back(), et, false, true}); } FType at = FType::arrayOf(et); Value *n = b.getInt64(vals.size()); @@ -1404,11 +1446,17 @@ Value_ Codegen::binary(const BinaryExpr &e) { case BinOp::Greater: case BinOp::GreaterEq: { Value_ l = expr(*e.lhs); - return compare(e.op, l, expr(*e.rhs), e.pos); + Held held(*this); + hold(l); + Value_ r = expr(*e.rhs); + return compare(e.op, l, r, e.pos); } default: { Value_ l = expr(*e.lhs); - return arith(e.op, l, expr(*e.rhs), e.pos); + Held held(*this); + hold(l); + Value_ r = expr(*e.rhs); + return arith(e.op, l, r, e.pos); } } } diff --git a/src/codegen_builtins.cpp b/src/codegen_builtins.cpp index caa8c46..649a22b 100644 --- a/src/codegen_builtins.cpp +++ b/src/codegen_builtins.cpp @@ -108,6 +108,7 @@ Function *Codegen::rt(const std::string &name) { {"finch_map_free", {V, {P}}}, {"finch_map_clone_raw", {V, {P, P, I}}}, {"finch_map_missing", {V, {P, I, P}}}, + {"finch_map_changed", {V, {P, I}}}, {"finch_buf_add", {V, {P, P, I}}}, {"finch_buf_int", {V, {P, I}}}, {"finch_buf_uint", {V, {P, I}}}, @@ -125,7 +126,7 @@ Function *Codegen::rt(const std::string &name) { } Function *f = Function::Create(FunctionType::get(it->second.ret, it->second.params, false), Function::ExternalLinkage, name, mod.get()); - if (name == "finch_panic" || name == "finch_panic_index" || name == "finch_map_missing") { + if (name == "finch_panic" || name == "finch_panic_index" || name == "finch_map_missing" || name == "finch_map_changed") { f->addFnAttr(Attribute::NoReturn); f->addFnAttr(Attribute::Cold); } @@ -309,6 +310,8 @@ Value_ Codegen::call(const CallExpr &c) { if (n == "write_file") { checkArgs(c.args, c.argNames, 2, p, n); Value_ path = coerce(expr(*c.args[0]), FType::Str, c.args[0]->pos, "the file name"); + Held held(*this); + hold(path); Value_ text = coerce(expr(*c.args[1]), FType::Str, c.args[1]->pos, "the text"); if (canFail) { Value *err = tmp(zero(FType::Str)); @@ -360,6 +363,7 @@ Value_ Codegen::callFinch(const Fn &fn, const std::vector &args, const checkArgs(args, names, fn.params.size(), p, shown); std::vector vals; std::vector fresh; + Held held(*this); if (self) vals.push_back(self); for (size_t i = 0; i < args.size(); i++) { Value_ v = coerce(exprWant(*args[i], fn.params[i]), fn.params[i], args[i]->pos, "argument '" + d.params[i].name + "'"); @@ -368,6 +372,7 @@ Value_ Codegen::callFinch(const Fn &fn, const std::vector &args, const v = {copyValue(v.v, v.type), v.type, false, true}; vals.push_back(v.v); // owning values are lent: the function never drops them if (v.fresh) fresh.push_back(v); + hold(v); } setLoc(p); Value *r = b.CreateCall(fn.llvm, vals); @@ -412,6 +417,7 @@ Value_ Codegen::construct(StructInfo *s, const std::vector &args, const } Value *v = zero(t); + Held held(*this); for (size_t k = 0; k < fields.size(); k++) { const StructInfo::F &f = *fields[k]; Value_ fv; @@ -432,7 +438,9 @@ Value_ Codegen::construct(StructInfo *s, const std::vector &args, const } else { continue; // zero } - v = b.CreateInsertValue(v, own(fv), f.llvmIndex); + Value *fo = own(fv); + hold({fo, f.type, false, true}); + v = b.CreateInsertValue(v, fo, f.llvmIndex); } return {v, t, false, owning(t)}; } @@ -441,7 +449,9 @@ Value_ Codegen::callC(const CFunc &f, const std::vector &args, Pos p) { if (!f.unsupported.empty()) failAt(p.file, p.line, p.col, "the C function '" + f.name + "' can't be used from Finch yet: it " + f.unsupported); std::vector vals; + Held held(*this); for (size_t i = 0; i < args.size(); i++) { + if (!vals.empty()) hold(vals.back()); if (i < f.params.size()) { FType pt = resolve(f.params[i], p); vals.push_back(coerce(exprWant(*args[i], pt), pt, args[i]->pos, "argument " + std::to_string(i + 1) + " of " + f.name)); @@ -592,8 +602,10 @@ Value_ Codegen::method(const MethodExpr &m) { std::string root = rootVar(*m.obj); if (Var *v = root.empty() ? nullptr : lookup(root); v && v->readonly) failAt(p.file, p.line, p.col, v->readonlyWhy); } + Held held(*this); if (t.kind == FType::Map) { Value_ recv = r.isPlace ? Value_{r.pl.addr, t} : Value_{tmp(r.val.v), t, false, r.val.fresh}; + if (!r.isPlace && r.val.fresh) holdAddr(recv.v, t); Value_ out = mapMethod(m, &recv); if (!r.isPlace && r.val.fresh) dropAt(recv.v, t); return out; @@ -611,6 +623,7 @@ Value_ Codegen::method(const MethodExpr &m) { if (!r.isPlace) { if (changing.count(m.name)) failAt(p.file, p.line, p.col, m.name + "() would change a temporary array; store it in a variable first"); recv = {tmp(r.val.v), t, false, r.val.fresh}; + if (r.val.fresh) holdAddr(recv.v, t); } Value_ out = arrayMethod(m, &recv); if (!r.isPlace && r.val.fresh) dropAt(recv.v, t); @@ -619,8 +632,11 @@ Value_ Codegen::method(const MethodExpr &m) { if (t.kind == FType::Str) { Value_ sv = r.isPlace ? Value_{b.CreateLoad(ty(t), r.pl.addr), t} : r.val; Value *addr = tmp(sv.v); // the runtime takes strings by address + if (!r.isPlace) hold(sv); auto argStr = [&](size_t i, const char *what) { - return coerce(expr(*m.args[i]), FType::Str, m.args[i]->pos, what); + Value_ a = coerce(expr(*m.args[i]), FType::Str, m.args[i]->pos, what); + hold(a); + return a; }; auto argInt = [&](size_t i, const char *what) { return coerce(expr(*m.args[i]), FType::I64, m.args[i]->pos, what).v; @@ -714,6 +730,8 @@ Value_ Codegen::structMethod(const MethodExpr &m, LRef &r, const FType &t) { self = tmp(r.val.v); // a temporary struct: Point(1, 2).length() temp = true; } + Held held(*this); + if (temp && r.val.fresh) holdAddr(self, st); Value_ out = callFinch(*fn, m.args, m.argNames, p, st.name + "." + m.name, &m, self, root); if (temp && r.val.fresh && owning(st)) dropAt(self, st); return out; diff --git a/src/codegen_impl.h b/src/codegen_impl.h index b87ea9f..4d3b8f8 100644 --- a/src/codegen_impl.h +++ b/src/codegen_impl.h @@ -87,7 +87,16 @@ struct Fn { struct Loop { llvm::BasicBlock *continueTo, *breakTo; - size_t scopeDepth; // scopes opened inside the loop are cleaned up by break/continue + size_t scopeDepth; // scopes opened inside the loop are cleaned up by break/continue + size_t pendingDepth; // and so are temporaries held by expressions around it (see Codegen::pending) +}; + +// A fresh value an expression is holding while it computes another part (f(a + b, try g())): if that +// part leaves early (try, or a return/break/continue in an `or { }` block), it must be dropped. +struct Pending { + llvm::Value *v; + FType type; + bool isAddr; // v is the value's address }; struct Place { // something that can be assigned to @@ -136,6 +145,7 @@ public: std::vector> structStore; std::vector scopes; std::vector loops; + std::vector pending; const Fn *curFn = nullptr; std::string curModule; int varOrder = 0; @@ -183,6 +193,15 @@ public: bool terminated(); llvm::BasicBlock *newBlock(const char *name); + void hold(const Value_ &v); // see Pending + void holdAddr(llvm::Value *addr, const FType &t); + void dropPending(size_t downTo); // emit drops for pending[downTo..] + struct Held { // forgets what was held when the expression is done + Codegen &cg; + size_t mark; + explicit Held(Codegen &c) : cg(c), mark(c.pending.size()) {} + ~Held() { cg.pending.erase(cg.pending.begin() + mark, cg.pending.end()); } + }; void pushScope(); void popScope(); // emits the scope's cleanups if reachable void emitCleanups(size_t downTo); // run cleanups of scopes [downTo, end) without popping diff --git a/src/codegen_map.cpp b/src/codegen_map.cpp index d404fd2..182d396 100644 --- a/src/codegen_map.cpp +++ b/src/codegen_map.cpp @@ -138,9 +138,13 @@ Value_ Codegen::mapLit(const MapLitExpr &e, const FType *want) { } Value *map = tmp(zero(t)); StructType *ET = mapEntryTy(t); + Held held(*this); + holdAddr(map, t); // the entries so far, if a later one leaves early for (size_t i = 0; i < e.keys.size(); i++) { + Held entry(*this); Value_ k = i == 0 && first ? k0 : exprWant(*e.keys[i], *t.key); k = coerce(k, *t.key, e.keys[i]->pos, "every key of this map"); + hold(k); Value_ v = i == 0 && first ? v0 : exprWant(*e.values[i], *t.elem); Value *vv = own(coerce(v, *t.elem, e.values[i]->pos, "every value of this map")); Value *valueAddr = b.CreateStructGEP(ET, mapSlot(map, t, k, p, false), 2); diff --git a/src/codegen_types.cpp b/src/codegen_types.cpp index 311675c..4879997 100644 --- a/src/codegen_types.cpp +++ b/src/codegen_types.cpp @@ -27,7 +27,7 @@ llvm::Type *Codegen::ty(const FType &t) { case FType::Array: return StructType::get(ctx, {b.getPtrTy(), b.getInt64Ty(), b.getInt64Ty()}); case FType::Map: { llvm::Type *P = b.getPtrTy(), *I = b.getInt64Ty(); - return StructType::get(ctx, {P, I, I, I, P, I}); // entries, len, used, cap, index, icap + return StructType::get(ctx, {P, I, I, I, P, I, I}); // entries, len, used, cap, index, icap, adds } case FType::Ptr: case FType::Null: return b.getPtrTy(); @@ -546,11 +546,11 @@ DIType *Codegen::diType(const FType &t) { case FType::Map: { // shown by its parts; the entries are behind `entries` DIType *i64 = diType(FType::I64), *pt = di->createPointerType(nullptr, 64); DIFile *f = diFiles[0]; - const char *names[] = {"entries", "len", "used", "cap", "index", "icap"}; + const char *names[] = {"entries", "len", "used", "cap", "index", "icap", "adds"}; std::vector els; - for (unsigned k = 0; k < 6; k++) + for (unsigned k = 0; k < 7; k++) els.push_back(di->createMemberType(diUnit, names[k], f, 0, 64, 64, 64 * k, DINode::FlagZero, k == 0 || k == 4 ? pt : i64)); - d = di->createStructType(diUnit, t.show(), f, 0, 384, 64, DINode::FlagZero, nullptr, di->getOrCreateArray(els)); + d = di->createStructType(diUnit, t.show(), f, 0, 448, 64, DINode::FlagZero, nullptr, di->getOrCreateArray(els)); break; } case FType::Struct: { diff --git a/tests/fail/map_add_while_looping.fch b/tests/fail/map_add_while_looping.fch new file mode 100644 index 0000000..3e95002 --- /dev/null +++ b/tests/fail/map_add_while_looping.fch @@ -0,0 +1,7 @@ +// expect: a key was added to the map while looping over it +fn main() { + m := ["a": 1] + for k in m { + m[k + "!"] = 2 + } +} diff --git a/tests/run/early_exit.fch b/tests/run/early_exit.fch new file mode 100644 index 0000000..464af92 --- /dev/null +++ b/tests/run/early_exit.fch @@ -0,0 +1,96 @@ +// Leaving an expression half way (try, or { break / continue / return }) frees what it was holding. +// MEMCHECK=1 tests/run.sh checks this with valgrind. + +struct Pair { + str a + str b +} + +fn check(int n) -> str! { + if n > 0 { + return error("failed at " + str(n)) + } + return "ok" +} + +fn join3(str a, str b, str c) -> str { + return a + b + c +} + +fn Pair.both() -> str { + return self.a + self.b +} + +fn args(int n) -> str! { + s := "x".repeat(3) + return join3(s + "1", try check(n), s + "2") +} + +fn array(int n) -> []str! { + s := "y".repeat(2) + return [s + "a", s + "b", try check(n)] +} + +fn pair(int n) -> Pair! { + s := "z".repeat(2) + return Pair(s + "a", try check(n)) +} + +fn concat(int n) -> str! { + s := "w".repeat(2) + return s + "1" + (try check(n)) + "2" +} + +fn method_on_temp(int n) -> str! { + s := "v".repeat(2) + return (s + "!").replace("v", try check(n)) +} + +fn struct_method(int n) -> str! { + s := "u".repeat(2) + t := Pair(s, s + "?").both() + (try check(n)) + return t +} + +fn map_literal(int n) -> map[str]str! { + s := "k".repeat(2) + return [s: s + "1", s + "x": try check(n)] +} + +fn index_temp(int n) -> str! { + s := "i".repeat(2) + return [s + "0", s + "1"][int(try check(n)) or 0] +} + +fn main() { + for n in 0..2 { + print(args(n) or err, "|", array(n) or ["-"], "|", pair(n) or Pair("-", "-")) + print(concat(n) or err, "|", method_on_temp(n) or err, "|", struct_method(n) or err) + print(map_literal(n) or ["-": "-"]) + } + print(index_temp(1) or "index: failed") + + // break / continue out of an or { } block in the middle of a call + s := "q".repeat(2) + for i in 0..4 { + t := join3(s + "a", check(i % 2) or { continue }, s + "b") + print(i, t) + } + for i in 0..4 { + t := join3(s + "a", check(i) or { break }, s + "b") + print("before break:", i, t) + } + + // removing keys while looping over a map is fine + m := ["a": 1, "b": 2, "c": 3, "d": 4] + for k, v in m { + if v % 2 == 0 { + m.remove(k) + } + } + print(m) + for k in m { + m[k] = m[k] * 10 // changing values is fine too + } + print(m) +} diff --git a/tests/run/early_exit.out b/tests/run/early_exit.out new file mode 100644 index 0000000..e547282 --- /dev/null +++ b/tests/run/early_exit.out @@ -0,0 +1,12 @@ +xxx1okxxx2 | ["yya", "yyb", "ok"] | Pair(a: "zza", b: "ok") +ww1ok2 | okok! | uuuu?ok +{"kk": "kk1", "kkx": "ok"} +failed at 1 | ["-"] | Pair(a: "-", b: "-") +failed at 1 | failed at 1 | failed at 1 +{"-": "-"} +index: failed +0 qqaokqqb +2 qqaokqqb +before break: 0 qqaokqqb +{"a": 1, "c": 3} +{"a": 10, "c": 30} From 68637a35a7bdef972856d91a479a3adfaf2ad298 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 11 Oct 2026 02:14:09 +0200 Subject: [PATCH 2/3] export fn and finch build --shared: libraries a C program loads (game scripts) export fn name(...) is a C function by that name (numbers, bool, char, pointers in and out). --shared builds lib.so / .dll with no main. finch_module_on_fatal(handler) lets the host take runtime errors over (longjmp out) instead of the process exiting. tests/shared.sh: a C host loads one, calls it and survives an index error. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 ++ docs/en/cheatsheet.md | 1 + runtime/finch_rt.c | 21 ++++++++++++--- src/ast.h | 1 + src/codegen.cpp | 58 ++++++++++++++++++++++++++++++++++++++-- src/codegen_builtins.cpp | 1 + src/codegen_impl.h | 2 ++ src/lexer.cpp | 3 ++- src/lexer.h | 2 +- src/main.cpp | 6 ++++- src/parser.cpp | 3 ++- src/target.h | 1 + tests/shared.sh | 17 ++++++++++++ tests/shared/host.c | 17 ++++++++++++ tests/shared/host.h | 1 + tests/shared/mod.fch | 22 +++++++++++++++ 16 files changed, 148 insertions(+), 10 deletions(-) create mode 100755 tests/shared.sh create mode 100644 tests/shared/host.c create mode 100644 tests/shared/host.h create mode 100644 tests/shared/mod.fch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3e6484..9d05a54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,8 @@ jobs: build/finch version - name: Tests run: tests/run.sh + - name: Shared library + run: tests/shared.sh - name: Tests under valgrind if: matrix.arch == 'x64' run: MEMCHECK=1 tests/run.sh diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md index a9f7408..42dfe2a 100644 --- a/docs/en/cheatsheet.md +++ b/docs/en/cheatsheet.md @@ -12,6 +12,7 @@ Everything in Finch on one page. For explanations, see the [guides](../../README |---|---| | `finch run file.fch [args…]` | compile and run (args go to the program) | | `finch build file.fch [-o name]` | make a program (`name.exe` on Windows) | +| `finch build file.fch --shared` | make a library (`libfile.so`, `file.dll`) that a C program loads; its `export fn`s are its C functions (numbers, bool, char, pointers) | | `finch ir file.fch` | print the LLVM IR | | `finch lsp` | the language server (editors use it) | | `finch version` | version | diff --git a/runtime/finch_rt.c b/runtime/finch_rt.c index 4cb2d13..d825f64 100644 --- a/runtime/finch_rt.c +++ b/runtime/finch_rt.c @@ -37,12 +37,26 @@ static char empty[1] = ""; // ---------- runtime errors ---------- -__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) { +// A program that loads Finch libraries (finch build --shared) can take runtime errors over +// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler +// gets the message and must not return (longjmp out, or exit itself). +static void (*fatal_hook)(const char *msg); +void finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; } + +static __attribute__((noreturn)) void fatal(const char *msg) { + if (fatal_hook) fatal_hook(msg); fflush(stdout); - fprintf(stderr, "%s:%lld: runtime error: %s\n", file, (long long)line, msg); + fprintf(stderr, "%s\n", msg); exit(1); } +__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) { + char full[1024]; + snprintf(full, sizeof full, "%s:%lld: runtime error: %s", file, (long long)line, msg); + fflush(stdout); + fatal(full); +} + __attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) { char msg[128]; snprintf(msg, sizeof msg, "index %lld is out of range (the length is %lld)", (long long)i, (long long)len); @@ -52,8 +66,7 @@ __attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t static void *must(void *p) { if (!p) { fflush(stdout); - fprintf(stderr, "runtime error: out of memory\n"); - exit(1); + fatal("runtime error: out of memory"); } return p; } diff --git a/src/ast.h b/src/ast.h index 87c3828..9651207 100644 --- a/src/ast.h +++ b/src/ast.h @@ -315,6 +315,7 @@ struct FnDecl { Pos recvPos; Type ret; // Void when there is no `-> type` bool fallible = false; // -> int! / -> ! : can fail with an error + bool exported = false; // export fn: callable from C by its own name (a library's entry point) std::vector params; BlockPtr body; }; diff --git a/src/codegen.cpp b/src/codegen.cpp index e0b4906..01241b6 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -4,6 +4,7 @@ #include "codegen_impl.h" #include "builtins_doc.h" +#include "target.h" using namespace llvm; @@ -36,12 +37,29 @@ Codegen::Codegen(LLVMContext &c, const std::vector &ps, const CImports std::unique_ptr Codegen::run() { declareStructs(); declareFns(); - if (!modules[""].fns.count("main")) failAt(0, 1, 1, "there is no main function (add 'fn main() { ... }')"); + if (g_target.shared) { + if (modules[""].fns.count("main")) { + const Pos &pos = modules[""].fns["main"].decl->namePos; + failAt(pos.file, pos.line, pos.col, "a library (--shared) has no main: the program that loads it calls its 'export fn's"); + } + } else if (!modules[""].fns.count("main")) { + failAt(0, 1, 1, "there is no main function (add 'fn main() { ... }')"); + } for (auto &[name, m] : modules) for (auto &[fname, fn] : m.fns) define(fn); for (auto &[s, ms] : methods) for (auto &[mname, fn] : ms) define(fn); - defineMainWrapper(); + if (!g_target.shared) defineMainWrapper(); + for (const auto &[module, name] : exportNames) defineExport(modules[module].fns[name]); + if (g_target.shared) { // finch_module_on_fatal(handler): the host's say on runtime errors (see finch_rt.c) + Function *w = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy()}, false), Function::ExternalLinkage, + "finch_module_on_fatal", mod.get()); + if (g_target.windows) w->setDLLStorageClass(GlobalValue::DLLExportStorageClass); + b.SetInsertPoint(BasicBlock::Create(ctx, "entry", w)); + b.SetCurrentDebugLocation(DebugLoc()); + b.CreateCall(rt("finch_on_fatal"), {w->getArg(0)}); + b.CreateRetVoid(); + } if (di) di->finalize(); std::string err; @@ -188,8 +206,44 @@ void Codegen::declare(const Program &p, const FnDecl &fn) { // User functions get a prefix so they never clash with C library names. std::string name = "finch." + (p.module.empty() ? "" : p.module + ".") + (recv ? recv->name + "." : "") + fn.name; f.llvm = Function::Create(FunctionType::get(retType(f), params, false), Function::InternalLinkage, name, mod.get()); + if (fn.exported) { + if (recv) failAt(pos.file, pos.line, pos.col, "methods can't be exported (export a plain fn that calls it)"); + if (f.fallible) failAt(pos.file, pos.line, pos.col, "an export fn can't fail (C has no errors as values): handle them inside it"); + auto cType = [](const FType &t) { return t.isNumber() || t.kind == FType::Bool || t.kind == FType::Char || t.kind == FType::Ptr; }; + for (size_t i = 0; i < f.params.size(); i++) + if (!cType(f.params[i])) + failAt(fn.params[i].pos.file, fn.params[i].pos.line, fn.params[i].pos.col, + "an export fn takes numbers, bool, char and pointers only (this is " + f.params[i].show() + + "; pass text as ptr[char] and make it a str inside)"); + if (f.ret.kind != FType::Void && !cType(f.ret)) + failAt(pos.file, pos.line, pos.col, "an export fn returns nothing, a number, bool, char or a pointer (not " + f.ret.show() + ")"); + for (const auto &[mname, other] : modules) + if (const auto it = other.fns.find(fn.name); it != other.fns.end() && it->second.decl->exported) + failAt(pos.file, pos.line, pos.col, "'" + fn.name + "' is exported twice (from two modules)"); + } if (recv) methods[recv][fn.name] = f; else m.fns[fn.name] = f; + if (fn.exported) exportNames.push_back({p.module, fn.name}); +} + +// export fn name(...): a C function called `name` that calls the Finch one (bool is a C byte). +void Codegen::defineExport(Fn &fn) { + auto cTy = [&](const FType &t) -> llvm::Type * { return t.kind == FType::Bool ? b.getInt8Ty() : ty(t); }; + std::vector params; + for (const FType &t : fn.params) params.push_back(cTy(t)); + llvm::Type *ret = fn.ret.kind == FType::Void ? b.getVoidTy() : cTy(fn.ret); + Function *w = Function::Create(FunctionType::get(ret, params, false), Function::ExternalLinkage, fn.decl->name, mod.get()); + if (g_target.windows) w->setDLLStorageClass(GlobalValue::DLLExportStorageClass); + b.SetInsertPoint(BasicBlock::Create(ctx, "entry", w)); + b.SetCurrentDebugLocation(DebugLoc()); + std::vector args; + for (size_t i = 0; i < fn.params.size(); i++) { + Value *a = w->getArg(unsigned(i)); + args.push_back(fn.params[i].kind == FType::Bool ? b.CreateICmpNE(a, b.getInt8(0)) : a); + } + Value *r = b.CreateCall(fn.llvm, args); + if (fn.ret.kind == FType::Void) b.CreateRetVoid(); + else b.CreateRet(fn.ret.kind == FType::Bool ? b.CreateZExt(r, b.getInt8Ty()) : r); } // The real C `main` calls the Finch one and turns its result into an exit code. diff --git a/src/codegen_builtins.cpp b/src/codegen_builtins.cpp index 649a22b..bfec91f 100644 --- a/src/codegen_builtins.cpp +++ b/src/codegen_builtins.cpp @@ -50,6 +50,7 @@ Function *Codegen::rt(const std::string &name) { struct Sig { llvm::Type *ret; std::vector params; }; std::map sigs = { {"finch_panic", {V, {P, I, P}}}, + {"finch_on_fatal", {V, {P}}}, {"finch_panic_index", {V, {P, I, I, I}}}, {"finch_alloc", {P, {I}}}, {"finch_zalloc", {P, {I}}}, diff --git a/src/codegen_impl.h b/src/codegen_impl.h index 4d3b8f8..4d838c4 100644 --- a/src/codegen_impl.h +++ b/src/codegen_impl.h @@ -174,6 +174,8 @@ public: void declare(const Program &p, const FnDecl &fn); void define(Fn &fn); void defineMainWrapper(); + void defineExport(Fn &fn); + std::vector> exportNames; // (module, name) of each export fn void block(const BlockStmt &blk); void blockBody(const BlockStmt &blk); void stmt(const Stmt &s); diff --git a/src/lexer.cpp b/src/lexer.cpp index ffe367f..fc7ea12 100644 --- a/src/lexer.cpp +++ b/src/lexer.cpp @@ -5,7 +5,7 @@ #include static const std::unordered_map keywords = { - {"fn", Tok::Fn}, {"return", Tok::Return}, {"if", Tok::If}, + {"fn", Tok::Fn}, {"export", Tok::Export}, {"return", Tok::Return}, {"if", Tok::If}, {"else", Tok::Else}, {"while", Tok::While}, {"for", Tok::For}, {"in", Tok::In}, {"break", Tok::Break}, {"continue", Tok::Continue}, {"true", Tok::True}, {"false", Tok::False}, {"null", Tok::Null}, @@ -154,6 +154,7 @@ const char *tokName(Tok t) { case Tok::String: return "a string"; case Tok::Char: return "a char"; case Tok::Fn: return "'fn'"; + case Tok::Export: return "'export'"; case Tok::Return: return "'return'"; case Tok::If: return "'if'"; case Tok::Else: return "'else'"; diff --git a/src/lexer.h b/src/lexer.h index 23761b8..491f064 100644 --- a/src/lexer.h +++ b/src/lexer.h @@ -8,7 +8,7 @@ enum class Tok { Ident, Int, Float, String, Char, // keywords - Fn, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link, Struct, Defer, + Fn, Export, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link, Struct, Defer, OrElse, Try, // or, try // punctuation diff --git a/src/main.cpp b/src/main.cpp index 73ec72c..fe8f2f0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -42,6 +42,7 @@ static void usage() { " finch lsp the language server, for editors (VS Code, Kate, ...)\n" "\n" " -l link a C library, same as link \"lib\" in the file\n" + " --shared (build) a library with no main: lib.so / .dll, its export fn's callable from C\n" " --target build for another system: windows, linux, arm64 (Linux), macos,\n" " or an LLVM triple\n" " -g add debug info (gdb / lldb / Visual Studio)\n" @@ -230,6 +231,7 @@ static bool endsWith(const std::string &s, const char *x) { static void link(const std::string &obj, const std::string &exe, const std::vector &libs, const CImports &c) { std::string cmd = g_target.cc + " " + shellQuote(obj) + " " + shellQuote(runtimeObject()) + " -o " + shellQuote(exe); + if (g_target.shared) cmd += g_target.darwin ? " -dynamiclib" : " -shared"; if (!g_target.msvc) cmd += " -lm"; std::vector temps; for (const std::string &l : libs) { @@ -289,6 +291,7 @@ int main(int argc, char **argv) { else if (a == "--target" && i + 1 < argc) targetName = argv[++i]; else if (a == "-O0") optimizeCode = false; else if (a == "-g") debug = true; + else if (a == "--shared" && cmd == "build") g_target.shared = true; else if (a == "-l" && i + 1 < argc) libs.push_back(argv[++i]); else if (a.rfind("-l", 0) == 0 && a.size() > 2) libs.push_back(a.substr(2)); else if (cmd == "run") programArgs.push_back(a); @@ -296,7 +299,8 @@ int main(int argc, char **argv) { } setTarget(targetName); - if (!outGiven) out += g_target.exe; + if (!outGiven) out = g_target.shared ? (g_target.windows ? out + ".dll" : "lib" + out + (g_target.darwin ? ".dylib" : ".so")) + : out + g_target.exe; Loader loader; loader.load(file, "", Pos{}); diff --git a/src/parser.cpp b/src/parser.cpp index 4a76616..a276cce 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -180,9 +180,10 @@ private: FnDecl function() { FnDecl fn; fn.pos = pos(); + fn.exported = accept(Tok::Export); if (at(Tok::Ident) && isTypeName(cur().text)) fail(fn.pos.line, fn.pos.col, "functions start with 'fn', like: fn add(int a, int b) -> int { ... }"); - expect(Tok::Fn, "'fn', 'struct', 'import' or 'link' (every function starts with fn)"); + expect(Tok::Fn, fn.exported ? "'fn' (export fn name(...))" : "'fn', 'struct', 'import' or 'link' (every function starts with fn)"); fn.namePos = pos(); fn.name = expect(Tok::Ident, "a function name").text; diff --git a/src/target.h b/src/target.h index eb18c34..115eab1 100644 --- a/src/target.h +++ b/src/target.h @@ -14,6 +14,7 @@ struct TargetInfo { bool cross = false; // compiling for another system than the one we run on std::string cc; // the C compiler that links programs and builds the runtime std::string exe; // ".exe" or "" + bool shared = false; // finch build --shared: a library (.so / .dll / .dylib) with no main }; inline TargetInfo g_target; diff --git a/tests/shared.sh b/tests/shared.sh new file mode 100755 index 0000000..83ea020 --- /dev/null +++ b/tests/shared.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# finch build --shared: a library a C program loads, calls by name, and that hands runtime +# errors to the host instead of exiting. +set -e +cd "$(dirname "$0")/shared" +FINCH=${FINCH:-../../build/finch} +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +"$FINCH" build mod.fch --shared -o "$tmp/libmod.so" +${CC:-cc} host.c -rdynamic -o "$tmp/host" -ldl +out=$("$tmp/host" "$tmp/libmod.so") +expected='add 42 +even 1 0 +host_print: hello Rakya +caught: mod.fch:17: runtime error: index 7 is out of range (the length is 3) +still alive' +if [ "$out" = "$expected" ]; then echo "shared library: passed"; else printf 'shared library: FAILED\n%s\n' "$out"; exit 1; fi diff --git a/tests/shared/host.c b/tests/shared/host.c new file mode 100644 index 0000000..9c5dfbb --- /dev/null +++ b/tests/shared/host.c @@ -0,0 +1,17 @@ +#include +#include +#include +static jmp_buf jb; +void host_print(const char *s) { printf("host_print: %s\n", s); } +static void onFatal(const char *m) { printf("caught: %s\n", m); longjmp(jb, 1); } +int main(int argc, char **argv) { + void *h = dlopen(argc > 1 ? argv[1] : "./libmod.so", RTLD_NOW | RTLD_LOCAL); + if (!h) { printf("%s\n", dlerror()); return 1; } + ((void (*)(void (*)(const char *)))dlsym(h, "finch_module_on_fatal"))(onFatal); + printf("add %ld\n", ((long (*)(long, long))dlsym(h, "add"))(2, 40)); + printf("even %d %d\n", ((_Bool (*)(long))dlsym(h, "is_even"))(4), ((_Bool (*)(long))dlsym(h, "is_even"))(5)); + ((void (*)(const char *))dlsym(h, "hello"))("Rakya"); + if (!setjmp(jb)) ((long (*)(long))dlsym(h, "crash"))(7); + printf("still alive\n"); + return 0; +} diff --git a/tests/shared/host.h b/tests/shared/host.h new file mode 100644 index 0000000..13358a4 --- /dev/null +++ b/tests/shared/host.h @@ -0,0 +1 @@ +void host_print(const char *s); diff --git a/tests/shared/mod.fch b/tests/shared/mod.fch new file mode 100644 index 0000000..19cb22a --- /dev/null +++ b/tests/shared/mod.fch @@ -0,0 +1,22 @@ +import "host.h" + +fn greet(str who) -> str { + return "hello " + who +} + +export fn add(int a, int b) -> int { + return a + b +} + +export fn hello(ptr[char] name) { + host_print(greet(str(name))) +} + +export fn crash(int i) -> int { + xs := [1, 2, 3] + return xs[i] +} + +export fn is_even(int n) -> bool { + return n % 2 == 0 +} From 5f2ba99f2d3f604f12d1471abd92f9fe632b67e9 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 11 Oct 2026 02:20:42 +0200 Subject: [PATCH 3/3] -I : look for imported C headers there too (an SDK's headers) Co-Authored-By: Claude Opus 5.5 --- docs/en/cheatsheet.md | 2 +- src/main.cpp | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md index 42dfe2a..ec86711 100644 --- a/docs/en/cheatsheet.md +++ b/docs/en/cheatsheet.md @@ -12,7 +12,7 @@ Everything in Finch on one page. For explanations, see the [guides](../../README |---|---| | `finch run file.fch [args…]` | compile and run (args go to the program) | | `finch build file.fch [-o name]` | make a program (`name.exe` on Windows) | -| `finch build file.fch --shared` | make a library (`libfile.so`, `file.dll`) that a C program loads; its `export fn`s are its C functions (numbers, bool, char, pointers) | +| `finch build file.fch --shared [-I dir]` | make a library (`libfile.so`, `file.dll`) that a C program loads; its `export fn`s are its C functions (numbers, bool, char, pointers) | | `finch ir file.fch` | print the LLVM IR | | `finch lsp` | the language server (editors use it) | | `finch version` | version | diff --git a/src/main.cpp b/src/main.cpp index fe8f2f0..70c088e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -42,6 +42,7 @@ static void usage() { " finch lsp the language server, for editors (VS Code, Kate, ...)\n" "\n" " -l link a C library, same as link \"lib\" in the file\n" + " -I also look for imported C headers in dir\n" " --shared (build) a library with no main: lib.so / .dll, its export fn's callable from C\n" " --target build for another system: windows, linux, arm64 (Linux), macos,\n" " or an LLVM triple\n" @@ -281,7 +282,7 @@ int main(int argc, char **argv) { std::string cmd = argv[1], file = argv[2]; if (cmd != "run" && cmd != "build" && cmd != "ir") usage(); std::string out = stem(file); - std::vector libs, programArgs; + std::vector libs, programArgs, includeDirs; std::string targetName; bool optimizeCode = true, debug = false, outGiven = false; for (int i = 3; i < argc; i++) { @@ -292,6 +293,8 @@ int main(int argc, char **argv) { else if (a == "-O0") optimizeCode = false; else if (a == "-g") debug = true; else if (a == "--shared" && cmd == "build") g_target.shared = true; + else if (a == "-I" && i + 1 < argc) includeDirs.push_back(argv[++i]); + else if (a.rfind("-I", 0) == 0 && a.size() > 2) includeDirs.push_back(a.substr(2)); else if (a == "-l" && i + 1 < argc) libs.push_back(argv[++i]); else if (a.rfind("-l", 0) == 0 && a.size() > 2) libs.push_back(a.substr(2)); else if (cmd == "run") programArgs.push_back(a); @@ -316,6 +319,7 @@ int main(int argc, char **argv) { } if (std::find(dirs.begin(), dirs.end(), dirOf(p.path)) == dirs.end()) dirs.push_back(dirOf(p.path)); } + dirs.insert(dirs.end(), includeDirs.begin(), includeDirs.end()); // -I: after the files' own folders CImports c = importHeaders(allImports, dirs); LLVMContext ctx; TargetMachine *tm = targetMachine();