diff --git a/src/astgen.cpp b/src/astgen.cpp index b089a18..e4baa99 100644 --- a/src/astgen.cpp +++ b/src/astgen.cpp @@ -13,6 +13,7 @@ #include "comptime.h" #include "jir_codegen.h" #include "mangling.h" +#include "target.h" #include #include @@ -3446,6 +3447,55 @@ static JirRef astgenAtCall(AstGenCtx &gctx, const AstNode &n) { const std::string &name = gctx.ctx.getStringPool().get(static_cast(n.lhs)); TypeIdx tyArg = static_cast(n.rhs); + if (name == "isDarwin" || name == "isLinux" || name == "isWindows" || + name == "isUnix") { + // Target-OS predicates, resolved at astgen time to a `Bool` + // constant. Because the result is a `JirTag::Bool` literal, + // `emitCondBr` folds `if (@isLinux())` to an unconditional branch + // and jirDefineBody's reachability pass drops the dead arm — so a + // macOS-only `_NSGetArgv` call never reaches codegen (or the + // linker) on Linux. Sourced from the host target, which is the + // only target today; switch to the selected target once + // cross-compilation lands. + // `isDarwin` follows Zig (std.Target.Os.Tag.isDarwin = the Apple + // family). Jam's OS enum only has MacOS today, so it reduces to + // that and widens automatically if iOS/tvOS/etc. are ever added. + // `isUnix` is "not Windows" for the current enum. + jam::OS os = jam::Target::getHostTarget().os; + bool v = (name == "isDarwin") ? (os == jam::OS::MacOS) + : (name == "isLinux") ? (os == jam::OS::Linux) + : (name == "isWindows") + ? (os == jam::OS::Windows) + : (os == jam::OS::MacOS || os == jam::OS::Linux || + os == jam::OS::FreeBSD); // isUnix + JirInst inst{}; + inst.tag = JirTag::Bool; + inst.a = v ? 1u : 0u; + inst.ty = BuiltinType::Bool; + return emit(gctx, inst); + } + if (name == "os") { + // `@os()` → the OS name as a `[]u8`, mirroring Zig's + // `@tagName(builtin.os.tag)` (e.g. "macos", "linux"). This is the + // display form: unlike the boolean predicates it does NOT fold a + // branch (string `==` isn't comptime-evaluated), so keep using + // `@isDarwin()` & co. for conditional compilation / dead-code + // elimination. Emits the same JIR a string literal would. + jam::OS os = jam::Target::getHostTarget().os; + const char *osName = (os == jam::OS::MacOS) ? "macos" + : (os == jam::OS::Linux) ? "linux" + : (os == jam::OS::Windows) ? "windows" + : (os == jam::OS::FreeBSD) ? "freebsd" + : "unknown"; + StringIdx s = gctx.ctx.getStringPool().intern(osName); + TypeIdx sliceTy = gctx.ctx.getTypePool().intern( + TypeKey{TypeKind::Slice, 0, 0, BuiltinType::U8, 0}); + JirInst inst{}; + inst.tag = JirTag::Str; + inst.a = s; + inst.ty = sliceTy; + return emit(gctx, inst); + } if (name == "sizeOf") { uint64_t bytes = gctx.ctx.typeSize(tyArg); JirInst inst{}; diff --git a/src/jir_codegen.cpp b/src/jir_codegen.cpp index 7853920..0df50ad 100644 --- a/src/jir_codegen.cpp +++ b/src/jir_codegen.cpp @@ -1010,6 +1010,63 @@ void jirDeclarePrototype(const JirFunction &jfn, JamCodegenContext &ctx) { } } +// Mark every block reachable from the entry block (1) by walking +// terminator successors (Br / CondBr / Switch). Blocks left unmarked are +// dead — they arise when `emitCondBr` folds a constant-`Bool` condition +// (e.g. `if (@isLinux())`) to an unconditional branch, orphaning the +// untaken arm. Skipping them in codegen is frontend dead-code +// elimination: the arm's instructions (and any extern *calls* they +// contain) never reach LLVM, so a macOS-only `_NSGetArgv` reference is +// gone on Linux rather than becoming an undefined-symbol link error. +// Terminator decoding mirrors `predecessorCount` in astgen.cpp. +static std::vector computeReachableBlocks(const JirFunction &jfn) { + std::vector reachable(jfn.blocks.size(), false); + if (jfn.blocks.size() <= 1) return reachable; // sentinel only + std::vector stack; + reachable[1] = true; // entry block + stack.push_back(1); + auto visit = [&](uint32_t t) { + if (t >= 1 && t < jfn.blocks.size() && !reachable[t]) { + reachable[t] = true; + stack.push_back(static_cast(t)); + } + }; + while (!stack.empty()) { + JirBlockRef b = stack.back(); + stack.pop_back(); + const JirBlock &blk = jfn.getBlock(b); + if (blk.insts.empty()) continue; + const JirInst &last = jfn.getInst(blk.insts.back()); + switch (last.tag) { + case JirTag::Br: + visit(last.a); + break; + case JirTag::CondBr: { + uint32_t ex = last.b; + if (ex + 2 <= jfn.extra.size()) { + visit(jfn.extra[ex]); + visit(jfn.extra[ex + 1]); + } + break; + } + case JirTag::Switch: { + uint32_t ex = last.b; + if (ex + 2 > jfn.extra.size()) break; + visit(jfn.extra[ex]); // default + uint32_t caseCount = jfn.extra[ex + 1]; + for (uint32_t i = 0; i < caseCount; i++) { + uint32_t caseSlot = ex + 2 + i * 4 + 3; + if (caseSlot < jfn.extra.size()) visit(jfn.extra[caseSlot]); + } + break; + } + default: + break; // Ret / Unreachable: no successors + } + } + return reachable; +} + void jirDefineBody(const JirFunction &jfn, JamCodegenContext &ctx) { if (jfn.isExtern) return; @@ -1021,9 +1078,17 @@ void jirDefineBody(const JirFunction &jfn, JamCodegenContext &ctx) { JirCodegenCtx lctx{jfn, ctx, {}, {}}; + // Dead-code elimination: only blocks reachable from entry get + // lowered. A constant-folded `if (@isLinux())` leaves the untaken + // arm orphaned (zero predecessors); skipping it here keeps its + // instructions — including extern calls like `_NSGetArgv` on a + // non-macOS target — out of the LLVM module entirely. + std::vector reachable = computeReachableBlocks(jfn); + // Create LLVM blocks first so terminators can resolve forward - // references. Skip the sentinel block at index 0. + // references. Skip the sentinel block at index 0 and dead blocks. for (JirBlockRef b = 1; b < jfn.blocks.size(); b++) { + if (!reachable[b]) continue; JamBasicBlockRef bb = JamLLVMAppendBasicBlock(f, jfn.getBlock(b).name.c_str()); lctx.blockMap[b] = bb; @@ -1031,6 +1096,7 @@ void jirDefineBody(const JirFunction &jfn, JamCodegenContext &ctx) { // Emit instructions block-by-block. for (JirBlockRef b = 1; b < jfn.blocks.size(); b++) { + if (!reachable[b]) continue; JamLLVMPositionBuilderAtEnd(ctx.getBuilder(), lctx.blockMap[b]); for (JirRef r : jfn.getBlock(b).insts) { JamValueRef v = emitInst(lctx, r); diff --git a/src/parser.cpp b/src/parser.cpp index 92c5072..97e667b 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -211,6 +211,13 @@ NodeIdx Parser::parsePrimary() { node.flags = 1; // expr-arg multi-form return emit(node); } + // No-arg form (`@isMacOS()`, `@isLinux()`, `@isUnix()`, + // `@isWindows()`, ...): empty parens. rhs is unused — astgenAtCall + // dispatches purely by name. + if (check(TOK_CLOSE_PAREN)) { + consume(TOK_CLOSE_PAREN, "Expected ')' after '@' intrinsic name"); + return emit(AstNode{AstTag::AtCall, 0, 0, 0, nameId, 0}); + } TypeIdx tyArg = parseType(); consume(TOK_CLOSE_PAREN, "Expected ')' after '@' intrinsic argument"); return emit(AstNode{AstTag::AtCall, 0, 0, 0, nameId, diff --git a/tests/unit/test_os_intrinsics.jam b/tests/unit/test_os_intrinsics.jam new file mode 100644 index 0000000..593c3d5 --- /dev/null +++ b/tests/unit/test_os_intrinsics.jam @@ -0,0 +1,52 @@ +// Target-OS comptime intrinsics: @os(), @isDarwin(), @isLinux(), +// @isUnix(), @isWindows(). +// +// These are target-dependent, so the suite must pass on macOS AND Linux +// CI — we cannot hardcode one OS. Two techniques keep it portable: +// 1. assert *target-independent invariants* (implications, mutual +// exclusion) that hold on every target; +// 2. lean on the comptime folding itself — only the matching `@isX()` +// arm survives to codegen, so each arm's expectation is correct on +// whatever target it compiles for. +// +// Note: logical `&&`/`||` aren't accepted bare in a call argument (call +// args parse at comparison precedence), so the boolean combinations are +// wrapped in parens. + +const { assert } = import("test"); +const std = import("std"); + +// @os() agrees with whichever predicate is active on this target. Only +// the matching arm compiles (the rest fold away), so this is correct +// everywhere without naming a specific OS. +tfn osNameMatchesActivePredicate() { + if (@isDarwin()) { + assert(std.string.eq(@os(), "macos"), true); + } else if (@isLinux()) { + assert(std.string.eq(@os(), "linux"), true); + } else if (@isWindows()) { + assert(std.string.eq(@os(), "windows"), true); + } +} + +// @os() is never empty (even an unrecognized target yields "unknown"). +tfn osNameNonEmpty() { + var name: []u8 = @os(); + assert(name.len > 0, true); +} + +// Windows and Unix are mutually exclusive on any target. +tfn windowsAndUnixExclusive() { + assert((@isWindows() && @isUnix()), false); +} + +// Family implications — hold regardless of which OS we build for. +tfn darwinImpliesUnix() { + assert((!@isDarwin() || @isUnix()), true); +} +tfn linuxImpliesUnix() { + assert((!@isLinux() || @isUnix()), true); +} +tfn windowsExcludesUnix() { + assert((!@isWindows() || !@isUnix()), true); +}