diff --git a/Makefile b/Makefile index 993d3ec..7a970f9 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ OUT := output SRC_NAMES := jam_llvm main lexer parser codegen target cabi \ module_resolver symbol_table number_literal \ init_analysis drop_registry abi diagnostics decl \ - analyzer astgen jir_codegen jir_verify + analyzer comptime astgen jir_codegen jir_verify OBJS := $(addprefix $(OUT)/, $(addsuffix .o, $(SRC_NAMES))) # Check if we're on macOS or Linux @@ -164,7 +164,14 @@ test-decl: build test-analyzer: build $(call CXX_TEST_TARGET,analyzer,$(OBJS)) -test: test-unit test-init test-abi test-codegen-errors test-jir test-diagnostics test-decl test-analyzer +test-comptime: build + @echo "" + @echo "Building and running Comptime C++ tests..." + @clang++ -c ./tests/cpp/test_comptime.cpp -o $(OUT)/test_comptime.o `$(LLVM_CONFIG) --cxxflags` -fexceptions $(OPTFLAGS) + @clang++ -o $(OUT)/comptime_tests $(OUT)/test_comptime.o $(OUT)/comptime.o $(OUT)/diagnostics.o + @$(OUT)/comptime_tests + +test: test-unit test-init test-abi test-codegen-errors test-jir test-diagnostics test-decl test-analyzer test-comptime test-release: test test-unit-release fmt: format diff --git a/src/ast.h b/src/ast.h index c907803..e1f25da 100644 --- a/src/ast.h +++ b/src/ast.h @@ -63,6 +63,28 @@ class FunctionAST { // regular `fn` shaped like one of those names is just a method; // `cfn` is what wires it to the compiler's hooks. bool isCfn; + // Name of the enclosing struct, if this is a method declared inside + // a `const T = struct { fn name(...) ... }` body. Empty for free + // functions and for clones of generic-instantiated methods (those + // already carry a qualified Name like `Vec__i32.push`, so the + // mangler keeps that as the LLVM symbol). + // + // The mangler reads this to emit unique LLVM symbols for methods + // across structs — without it, two structs that both define + // `fn init()` would collapse to the same `_init` symbol and the + // linker would silently pick one definition for both call sites. + std::string parentStruct; + + // Path of the module this function was declared in (e.g. "timer" + // for code in timer.jam). Empty for the entry module (matching + // Zig's behavior where the root file scope is the unqualified + // namespace) and for generic clones that already carry qualified + // names. ModuleResolver stamps this when a module is loaded. + // + // Combined with parentStruct, the mangler can emit Zig-style + // dotted LLVM symbols (`timer.Timer.read32`) so same-named + // methods or free fns in different modules don't collide. + std::string modulePath; FunctionAST(std::string Name, std::vector Args, TypeIdx ReturnType, std::vector Body, bool isExtern = false, diff --git a/src/astgen.cpp b/src/astgen.cpp index b883a44..f099416 100644 --- a/src/astgen.cpp +++ b/src/astgen.cpp @@ -47,7 +47,7 @@ struct DropTrack { std::string varName; JirRef slot; // alloca for the variable TypeIdx type; // source-level type - std::string llvmFnName; // canonical drop fn (legacy mangles to `__drop_T`) + std::string llvmFnName; // canonical drop fn — FQN like `T.drop` / `m.T.drop` }; struct AstGenCtx { @@ -483,13 +483,12 @@ static void astgenReturn(AstGenCtx &gctx, const AstNode &n) { } // Resolve the canonical drop function name for a Jam type. Returns -// empty when the type has no drop fn registered. For struct/named -// types, legacy `mangledFunctionName` produces `__drop_` -// at the LLVM level; we mirror that here so the call resolves at -// codegen time even though the FunctionAST::Name is bare "drop". -// Falls back to the codegen context's instantiated-drops table so -// generic struct/enum instantiations (Vec(i32), Holder(i32), ...) -// fire drops too. +// empty when the type has no drop fn registered. Routes through +// `mangledFunctionName` so the LLVM symbol picked here is the same +// one the drop fn's definition gets at codegen time (FQN like +// `T.drop` or `m.T.drop`). Falls back to the codegen context's +// instantiated-drops table so generic struct/enum instantiations +// (Vec(i32), Holder(i32), ...) fire drops too. static std::string lookupDropFnLLVMName(AstGenCtx &gctx, TypeIdx ty) { const TypeKey &k = gctx.ctx.getTypePool().get(ty); std::string typeName; @@ -3408,6 +3407,29 @@ static JirRef astgenCall(AstGenCtx &gctx, const AstNode &n) { // not as a regular function call. if (callee == "assert") { return astgenAssertCall(gctx, n); } + // Multi-dot qualified call: `handle.Struct.method(args)`. Mirrors + // Zig's `container_ty.getNamespace().lookupInNamespace(name)` + // (Sema.zig:5295) — methods on imported structs live under the + // importer's namespace handle, not in a flat global table. The + // registration site in main.cpp puts these under the key + // `handle.Struct.method`; we look them up directly here. + if (callee.find('.') != callee.rfind('.')) { + if (const FunctionAST *method = gctx.ctx.getFunctionAST(callee)) { + std::vector argRefs; + argRefs.reserve(argCount); + for (uint32_t i = 0; i < argCount; i++) { + NodeIdx argIdx = + static_cast(ns.getExtra(argsExtra + 1 + i)); + if (i < method->Args.size()) { + argRefs.push_back(lowerArg(gctx, argIdx, method->Args[i])); + } else { + argRefs.push_back(astgenExpr(gctx, argIdx, kNoType)); + } + } + return emitCall(gctx, method, argRefs); + } + } + // Single-dot qualified call: try in order // 1. `inst.method(args)` — instance dispatch on a local variable // 2. `Type.method(args)` — static method, possibly via type alias diff --git a/src/comptime.cpp b/src/comptime.cpp new file mode 100644 index 0000000..b589160 --- /dev/null +++ b/src/comptime.cpp @@ -0,0 +1,384 @@ +/* + * Copyright (c) 2026-present Raphael Amorim + * + * This file is part of jam. + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions. + */ + +#include "comptime.h" + +namespace jam { + +// ─── ComptimeValue constructors ────────────────────────────────── + +ComptimeValue ComptimeValue::makeNone() { + ComptimeValue v; + v.kind = Kind::None; + return v; +} + +ComptimeValue ComptimeValue::makeInt(uint64_t bits, uint16_t width, + bool isSigned) { + ComptimeValue v; + v.kind = Kind::Int; + v.intVal.bits = bits; + v.intVal.width = width; + v.intVal.isSigned = isSigned; + return v; +} + +ComptimeValue ComptimeValue::makeFloat(double value, uint16_t width) { + ComptimeValue v; + v.kind = Kind::Float; + v.floatVal.value = value; + v.floatVal.width = width; + return v; +} + +ComptimeValue ComptimeValue::makeBool(bool b) { + ComptimeValue v; + v.kind = Kind::Bool; + v.boolVal = b; + return v; +} + +ComptimeValue ComptimeValue::makeStr(StringIdx s) { + ComptimeValue v; + v.kind = Kind::Str; + v.strVal = s; + return v; +} + +ComptimeValue ComptimeValue::makeType(TypeIdx t) { + ComptimeValue v; + v.kind = Kind::Type; + v.typeVal = t; + return v; +} + +ComptimeValue +ComptimeValue::makeAggregate(std::vector fields) { + ComptimeValue v; + v.kind = Kind::Aggregate; + v.aggFields = std::move(fields); + return v; +} + +int64_t ComptimeValue::asI64() const { + if (kind != Kind::Int) return 0; + // Sign-extend from the value's bit-width so `i8(-1)` returns -1 + // (not 255). Unsigned values are returned as-is up to 63 bits. + uint64_t b = intVal.bits; + if (intVal.isSigned && intVal.width < 64) { + uint64_t signBit = 1ULL << (intVal.width - 1); + if (b & signBit) { + uint64_t mask = ~((1ULL << intVal.width) - 1); + b |= mask; + } + } + return static_cast(b); +} + +uint64_t ComptimeValue::asU64() const { + if (kind != Kind::Int) return 0; + // Mask to the value's bit-width so over-wide bit patterns can't + // leak. `i8(-1)` returns 0xFF here, `u8(255)` also returns 0xFF. + if (intVal.width >= 64) return intVal.bits; + return intVal.bits & ((1ULL << intVal.width) - 1); +} + +// ─── ComptimeScope ────────────────────────────────────────────── + +void ComptimeScope::bind(const std::string &name, ComptimeValue value) { + bindings_[name] = std::move(value); +} + +const ComptimeValue *ComptimeScope::lookup(const std::string &name) const { + auto it = bindings_.find(name); + if (it != bindings_.end()) return &it->second; + if (parent_ != nullptr) return parent_->lookup(name); + return nullptr; +} + +// ─── Evaluator ────────────────────────────────────────────────── + +ComptimeEvaluator::ComptimeEvaluator(const NodeStore &nodes, + const StringPool &strings, + const TypePool &types) + : nodes_(nodes), strings_(strings), types_(types) {} + +ComptimeValue ComptimeEvaluator::eval(NodeIdx expr, + const ComptimeScope &scope) const { + if (expr == kNoNode) return ComptimeValue::makeNone(); + const AstNode &n = nodes_.get(expr); + switch (n.tag) { + case AstTag::NumberLit: + return evalNumberLit(n); + case AstTag::BoolLit: + return evalBoolLit(n); + case AstTag::StringLit: + return evalStringLit(n); + case AstTag::Variable: + return evalVariable(n, scope); + case AstTag::UnaryOp: + return evalUnaryOp(n, scope); + case AstTag::BinaryOp: + return evalBinaryOp(n, scope); + case AstTag::Index: + return evalIndex(n, scope); + default: + // Operator / construct we don't fold yet. Returning None keeps + // optional-fold callers (peephole constant folding) silent; + // `evalRequired` will turn it into a diagnostic. + return ComptimeValue::makeNone(); + } +} + +ComptimeValue ComptimeEvaluator::evalRequired(NodeIdx expr, + const ComptimeScope &scope, + Diagnostics &diags, + SrcLoc loc) const { + ComptimeValue v = eval(expr, scope); + if (v.isNone()) { + diags.error(std::move(loc), + "expression cannot be evaluated at compile time"); + } + return v; +} + +ComptimeValue ComptimeEvaluator::evalNumberLit(const AstNode &n) const { + uint64_t bits = static_cast(n.lhs) | + (static_cast(n.rhs) << 32); + bool isNeg = (n.flags & 1) != 0; + bool isFloat = (n.flags & 2) != 0; + if (isFloat) { + double v; + __builtin_memcpy(&v, &bits, sizeof(v)); + if (isNeg) v = -v; + return ComptimeValue::makeFloat(v, 64); + } + // Default integer width: u64 (or i64 if negative). Callers can + // narrow via the surrounding type context, but at this evaluator + // layer we keep the literal at full width to preserve precision + // during folding. + if (isNeg) { + uint64_t magnitude = bits; + uint64_t signedBits = static_cast( + -static_cast(magnitude)); + return ComptimeValue::makeInt(signedBits, 64, /*isSigned=*/true); + } + return ComptimeValue::makeInt(bits, 64, /*isSigned=*/false); +} + +ComptimeValue ComptimeEvaluator::evalBoolLit(const AstNode &n) const { + return ComptimeValue::makeBool(n.lhs != 0); +} + +ComptimeValue ComptimeEvaluator::evalStringLit(const AstNode &n) const { + return ComptimeValue::makeStr(static_cast(n.lhs)); +} + +ComptimeValue +ComptimeEvaluator::evalVariable(const AstNode &n, + const ComptimeScope &scope) const { + const std::string &name = strings_.get(static_cast(n.lhs)); + const ComptimeValue *v = scope.lookup(name); + if (v == nullptr) return ComptimeValue::makeNone(); + return *v; +} + +ComptimeValue +ComptimeEvaluator::evalUnaryOp(const AstNode &n, + const ComptimeScope &scope) const { + NodeIdx operand = static_cast(n.lhs); + ComptimeValue v = eval(operand, scope); + if (v.isNone()) return v; + UnaryOp op = static_cast(n.op); + switch (op) { + case UnaryOp::Neg: + if (v.kind == ComptimeValue::Kind::Int) { + uint64_t neg = static_cast(-static_cast(v.asU64())); + return ComptimeValue::makeInt(neg, v.intVal.width, true); + } + if (v.kind == ComptimeValue::Kind::Float) { + return ComptimeValue::makeFloat(-v.floatVal.value, v.floatVal.width); + } + return ComptimeValue::makeNone(); + case UnaryOp::LogNot: + if (v.kind != ComptimeValue::Kind::Bool) return ComptimeValue::makeNone(); + return ComptimeValue::makeBool(!v.boolVal); + case UnaryOp::BitNot: + if (v.kind != ComptimeValue::Kind::Int) return ComptimeValue::makeNone(); + return ComptimeValue::makeInt(~v.intVal.bits, v.intVal.width, + v.intVal.isSigned); + default: + return ComptimeValue::makeNone(); + } +} + +ComptimeValue +ComptimeEvaluator::evalBinaryOp(const AstNode &n, + const ComptimeScope &scope) const { + NodeIdx lhsIdx = static_cast(n.lhs); + NodeIdx rhsIdx = static_cast(n.rhs); + BinOp op = static_cast(n.op); + + // Short-circuit eval for LogAnd/LogOr — match the runtime semantics + // and avoid evaluating the RHS when the LHS settles the answer. + if (op == BinOp::LogAnd || op == BinOp::LogOr) { + ComptimeValue l = eval(lhsIdx, scope); + if (l.kind != ComptimeValue::Kind::Bool) return ComptimeValue::makeNone(); + bool lb = l.boolVal; + if (op == BinOp::LogAnd && !lb) return ComptimeValue::makeBool(false); + if (op == BinOp::LogOr && lb) return ComptimeValue::makeBool(true); + ComptimeValue r = eval(rhsIdx, scope); + if (r.kind != ComptimeValue::Kind::Bool) return ComptimeValue::makeNone(); + return ComptimeValue::makeBool(r.boolVal); + } + + ComptimeValue l = eval(lhsIdx, scope); + if (l.isNone()) return l; + ComptimeValue r = eval(rhsIdx, scope); + if (r.isNone()) return r; + + // Integer arithmetic + bitwise — both operands must be Int, and + // for now we require matching width/signedness. Mixed-width is a + // codegen-level concern; the comp evaluator stays strict. + if (l.kind == ComptimeValue::Kind::Int && + r.kind == ComptimeValue::Kind::Int) { + if (l.intVal.width != r.intVal.width || + l.intVal.isSigned != r.intVal.isSigned) { + return ComptimeValue::makeNone(); + } + uint16_t w = l.intVal.width; + bool sgn = l.intVal.isSigned; + uint64_t a = l.intVal.bits; + uint64_t b = r.intVal.bits; + switch (op) { + case BinOp::Add: + return ComptimeValue::makeInt(a + b, w, sgn); + case BinOp::Sub: + return ComptimeValue::makeInt(a - b, w, sgn); + case BinOp::Mul: + return ComptimeValue::makeInt(a * b, w, sgn); + case BinOp::Div: + if (b == 0) return ComptimeValue::makeNone(); + if (sgn) { + return ComptimeValue::makeInt( + static_cast(static_cast(a) / + static_cast(b)), + w, sgn); + } + return ComptimeValue::makeInt(a / b, w, sgn); + case BinOp::Mod: + if (b == 0) return ComptimeValue::makeNone(); + if (sgn) { + return ComptimeValue::makeInt( + static_cast(static_cast(a) % + static_cast(b)), + w, sgn); + } + return ComptimeValue::makeInt(a % b, w, sgn); + case BinOp::BitAnd: + return ComptimeValue::makeInt(a & b, w, sgn); + case BinOp::BitOr: + return ComptimeValue::makeInt(a | b, w, sgn); + case BinOp::BitXor: + return ComptimeValue::makeInt(a ^ b, w, sgn); + case BinOp::Shl: + return ComptimeValue::makeInt(a << b, w, sgn); + case BinOp::Shr: + if (sgn) { + return ComptimeValue::makeInt( + static_cast(static_cast(a) >> b), w, sgn); + } + return ComptimeValue::makeInt(a >> b, w, sgn); + case BinOp::Eq: + return ComptimeValue::makeBool(a == b); + case BinOp::Ne: + return ComptimeValue::makeBool(a != b); + case BinOp::Lt: + return sgn + ? ComptimeValue::makeBool(static_cast(a) < + static_cast(b)) + : ComptimeValue::makeBool(a < b); + case BinOp::Le: + return sgn + ? ComptimeValue::makeBool(static_cast(a) <= + static_cast(b)) + : ComptimeValue::makeBool(a <= b); + case BinOp::Gt: + return sgn + ? ComptimeValue::makeBool(static_cast(a) > + static_cast(b)) + : ComptimeValue::makeBool(a > b); + case BinOp::Ge: + return sgn + ? ComptimeValue::makeBool(static_cast(a) >= + static_cast(b)) + : ComptimeValue::makeBool(a >= b); + default: + return ComptimeValue::makeNone(); + } + } + + // String equality — used by format-string parsing (matching + // field names in the args tuple by the parsed `{name}` slice). + if (l.kind == ComptimeValue::Kind::Str && + r.kind == ComptimeValue::Kind::Str) { + bool eq = l.strVal == r.strVal; + if (op == BinOp::Eq) return ComptimeValue::makeBool(eq); + if (op == BinOp::Ne) return ComptimeValue::makeBool(!eq); + return ComptimeValue::makeNone(); + } + + // Bool equality — `comp const flag = true; comp if (flag == false) ...` + if (l.kind == ComptimeValue::Kind::Bool && + r.kind == ComptimeValue::Kind::Bool) { + if (op == BinOp::Eq) + return ComptimeValue::makeBool(l.boolVal == r.boolVal); + if (op == BinOp::Ne) + return ComptimeValue::makeBool(l.boolVal != r.boolVal); + return ComptimeValue::makeNone(); + } + + // Type equality — the linchpin of `comp if (@TypeOf(arg) == i32)`. + if (l.kind == ComptimeValue::Kind::Type && + r.kind == ComptimeValue::Kind::Type) { + bool eq = l.typeVal == r.typeVal; + if (op == BinOp::Eq) return ComptimeValue::makeBool(eq); + if (op == BinOp::Ne) return ComptimeValue::makeBool(!eq); + return ComptimeValue::makeNone(); + } + + return ComptimeValue::makeNone(); +} + +ComptimeValue +ComptimeEvaluator::evalIndex(const AstNode &n, + const ComptimeScope &scope) const { + NodeIdx baseIdx = static_cast(n.lhs); + NodeIdx idxIdx = static_cast(n.rhs); + ComptimeValue base = eval(baseIdx, scope); + if (base.isNone()) return base; + ComptimeValue idx = eval(idxIdx, scope); + if (idx.isNone() || idx.kind != ComptimeValue::Kind::Int) { + return ComptimeValue::makeNone(); + } + uint64_t i = idx.asU64(); + if (base.kind == ComptimeValue::Kind::Str) { + const std::string &s = strings_.get(base.strVal); + if (i >= s.length()) return ComptimeValue::makeNone(); + // Result is the byte value as a u8. + return ComptimeValue::makeInt(static_cast( + static_cast(s[i])), + 8, /*isSigned=*/false); + } + if (base.kind == ComptimeValue::Kind::Aggregate) { + if (i >= base.aggFields.size()) return ComptimeValue::makeNone(); + return base.aggFields[i]; + } + return ComptimeValue::makeNone(); +} + +} // namespace jam diff --git a/src/comptime.h b/src/comptime.h new file mode 100644 index 0000000..64f04ad --- /dev/null +++ b/src/comptime.h @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2026-present Raphael Amorim + * + * This file is part of jam. + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions. + */ + +#ifndef JAM_COMPTIME_H +#define JAM_COMPTIME_H + +#include "ast_flat.h" +#include "diagnostics.h" + +#include +#include +#include +#include + +namespace jam { + +// A value known at compile time. Carries one of the primitive kinds the +// comp evaluator can produce. Aggregate is for tuple / struct values +// constructed at compile time (`.{1, 2, 3}` literal). Type is a value +// of the meta-type (`@TypeOf(x)`, generic-parameter args). +// +// The payload is split: trivially-copyable scalars live in a union; non- +// trivial members (strings, vectors) live alongside. None is the failure +// / "not foldable" state — every operation that can fail returns it +// rather than throwing. +// +// Source-level keyword for the feature is `comp` (e.g. `comp const N = +// 42;`, `comp if (cond) ...`); the compiler-internal naming uses the +// older "comptime" term to refer to the broader compile-time-evaluation +// concept. +struct ComptimeValue { + enum class Kind : uint8_t { + None, + Int, + Float, + Bool, + Str, + Type, + Aggregate, + }; + + Kind kind = Kind::None; + + // Scalar payload (active iff kind is Int / Float / Bool). + union { + struct { + uint64_t bits; + uint16_t width; + bool isSigned; + } intVal; + struct { + double value; + uint16_t width; + } floatVal; + bool boolVal; + uint8_t _pad; // ensure trivially-default-constructible union + }; + + // Non-trivial payload (active per matching kind). + StringIdx strVal = 0; + TypeIdx typeVal = kNoType; + std::vector aggFields; + + ComptimeValue() : kind(Kind::None), _pad(0) {} + + bool isNone() const { return kind == Kind::None; } + bool isInt() const { return kind == Kind::Int; } + bool isBool() const { return kind == Kind::Bool; } + bool isStr() const { return kind == Kind::Str; } + bool isType() const { return kind == Kind::Type; } + + // Construction helpers — clearer than direct field stomping. + static ComptimeValue makeNone(); + static ComptimeValue makeInt(uint64_t bits, uint16_t width, bool isSigned); + static ComptimeValue makeFloat(double v, uint16_t width); + static ComptimeValue makeBool(bool b); + static ComptimeValue makeStr(StringIdx s); + static ComptimeValue makeType(TypeIdx t); + static ComptimeValue makeAggregate(std::vector fields); + + // Convert an Int payload to int64 / uint64 for arithmetic. Both + // helpers truncate to the value's width to keep the bit pattern + // faithful — e.g. `i8(-1)` reads back as 0xFF. + int64_t asI64() const; + uint64_t asU64() const; +}; + +// A lexically-scoped map of name → ComptimeValue. The evaluator reads +// from it when it encounters a Variable node. Bindings can be pushed +// and popped to model nested scopes during inline-for unrolling, where +// the loop variable lives in a transient frame. +class ComptimeScope { + public: + ComptimeScope() = default; + explicit ComptimeScope(const ComptimeScope *parent) : parent_(parent) {} + + void bind(const std::string &name, ComptimeValue value); + + // Returns nullptr if `name` isn't bound here OR in any ancestor. + const ComptimeValue *lookup(const std::string &name) const; + + private: + const ComptimeScope *parent_ = nullptr; + std::unordered_map bindings_; +}; + +// Folds AST expression nodes to compile-time values. Failure modes +// (depends on runtime value, unsupported operator, type mismatch) all +// surface as ComptimeValue::None — the evaluator never throws. Callers +// that *require* a fold (e.g. `comp expr`, `inline for` cond) call +// `evalRequired` which pushes a diagnostic on failure. +// +// The evaluator is stateless across calls: it captures references to +// the node/string/type pools at construction and reads bindings from a +// caller-supplied scope per `eval` invocation. Constructing one per +// call is cheap. +class ComptimeEvaluator { + public: + ComptimeEvaluator(const NodeStore &nodes, const StringPool &strings, + const TypePool &types); + + // Try to fold `expr` to a value. Returns None on any failure. + ComptimeValue eval(NodeIdx expr, const ComptimeScope &scope) const; + + // Same but pushes a diagnostic + returns None when the expression + // can't be folded. Used by `comp expr` (which the user has + // explicitly marked as requiring fold) and by control-flow primitives + // where a non-fold is a hard error. + ComptimeValue evalRequired(NodeIdx expr, const ComptimeScope &scope, + Diagnostics &diags, SrcLoc loc) const; + + private: + const NodeStore &nodes_; + const StringPool &strings_; + const TypePool &types_; + + // Per-tag handlers. Each returns None on failure; callers compose. + ComptimeValue evalNumberLit(const AstNode &n) const; + ComptimeValue evalBoolLit(const AstNode &n) const; + ComptimeValue evalStringLit(const AstNode &n) const; + ComptimeValue evalVariable(const AstNode &n, + const ComptimeScope &scope) const; + ComptimeValue evalUnaryOp(const AstNode &n, + const ComptimeScope &scope) const; + ComptimeValue evalBinaryOp(const AstNode &n, + const ComptimeScope &scope) const; + ComptimeValue evalIndex(const AstNode &n, + const ComptimeScope &scope) const; +}; + +} // namespace jam + +#endif // JAM_COMPTIME_H diff --git a/src/drop_registry.cpp b/src/drop_registry.cpp index 9c5923a..d5b4f4c 100644 --- a/src/drop_registry.cpp +++ b/src/drop_registry.cpp @@ -20,7 +20,10 @@ namespace drops { // `cfn` (not plain `fn`) is required: only methods opted into the // compiler-synthesized-call set get auto-fired at scope exit. A // plain `fn drop(self)` is treated as an ordinary method the user -// invokes explicitly — no implicit destructor call. +// invokes explicitly — no implicit destructor call. This matters +// because a user-callable `fn drop` plus an auto-fire would +// double-drop; the `cfn` opt-in is the signal that the user is +// handing control of the call to the compiler. static void considerDropCandidate(const FunctionAST *fn, const TypePool &types, const StringPool &strings, DropRegistry ®istry) { diff --git a/src/jir.h b/src/jir.h index feda6b2..28e54aa 100644 --- a/src/jir.h +++ b/src/jir.h @@ -222,8 +222,8 @@ enum class JirTag : uint8_t { // Explicit destructor call for a tracked binding. Emitted by // AstGen at scope-exit points (return / break / continue / // fall-through). `a` = the binding's alloca JirRef; `b` = - // StringIdx of the LLVM symbol to call (e.g. `__drop_T` for - // top-level drop fns, `T.drop` for instantiated methods). + // StringIdx of the LLVM symbol to call (e.g. `T.drop` for a + // top-level drop fn, `Vec__i32.drop` for instantiated methods). // Codegen unconditionally lowers to `call void (ptr)` // — no metadata-driven emission, no scope-tracking outside // AstGen. ty is kNoType (void). diff --git a/src/jir_codegen.cpp b/src/jir_codegen.cpp index f472f79..f8b32b7 100644 --- a/src/jir_codegen.cpp +++ b/src/jir_codegen.cpp @@ -597,8 +597,8 @@ static JamValueRef emitInstImpl(JirCodegenCtx &lctx, JirRef r) { case JirTag::Call: { // `inst.a` is the StringIdx of the LLVM symbol name — // astgen has already done the mangling (test functions get - // `__test_`; `fn drop(self: mut T)` becomes `__drop_T`; - // instantiated cloned methods keep their qualified + // `__test_`; methods get dotted FQNs like `T.drop` or + // `m.T.drop`; instantiated cloned methods keep their // `Vec__i32.push` form). Codegen does a single lookup. StringIdx calleeId = static_cast(inst.a); const std::string &symbol = lctx.ctx.getStringPool().get(calleeId); diff --git a/src/main.cpp b/src/main.cpp index 0d870d3..7d8a80d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -324,8 +324,25 @@ static int compileAndRun(const std::string &filename, } } for (auto &s : importedModule->Structs) { - if (s->isPub) aliasNamed(s->Name); - else codegenCtx.registerPrivateName(handle, s->Name); + if (s->isPub) { + aliasNamed(s->Name); + // Also register each pub method under the namespace- + // qualified key `handle.Struct.method` so astgen's + // multi-dot callee lookup can find it. Mirrors the + // free-fn dual registration above (bare + handle.X). + // Inspired by Zig's namespace-scoped Decl lookup + // (Sema.zig:5295) where every type carries a back- + // pointer to its namespace and method resolution walks + // `container_ty.getNamespace().lookupInNamespace(name)`. + for (auto &m : s->Methods) { + if (m->isPub) { + codegenCtx.registerFunctionAST( + handle + "." + s->Name + "." + m->Name, m.get()); + } + } + } else { + codegenCtx.registerPrivateName(handle, s->Name); + } } for (auto &e : importedModule->Enums) { if (e->isPub) aliasNamed(e->Name); @@ -672,6 +689,19 @@ static int compileAndRun(const std::string &filename, } } } + for (auto &s : importedModule->Structs) { + for (auto &m : s->Methods) { + if (!m->isPub) continue; + try { + JirFunction jfn = astgenFunction(*m, codegenCtx); + jfn.name = mangledFunctionName(*m, codegenCtx.getTypePool(), + codegenCtx.getStringPool()); + jirFunctions.push_back(std::move(jfn)); + } catch (const AstGenAnalysisFail &) { + // diagnostic already pushed + } + } + } } // Astgen accumulated every per-decl diagnostic onto the diff --git a/src/mangling.h b/src/mangling.h index 2a000a5..ca6c5b2 100644 --- a/src/mangling.h +++ b/src/mangling.h @@ -14,15 +14,24 @@ #include // Translate a FunctionAST into the LLVM-level symbol the linker sees. +// Modeled on Zig's `Decl.getFullyQualifiedName` (Module.zig:713) — +// dot-separated, walking up the namespace chain. LLVM accepts `.` in +// symbol names, so no further escaping is needed. +// // The rules: // - `tfn t()` → `__test_t` (the harness in main.cpp calls these by // prefixed name). -// - `fn drop(self: mut T)` → `__drop_T` (the legacy mangling, used -// so callers don't have to spell out the full struct name every -// time they `T.drop(&x)`). // - Cloned instantiated methods carry their qualified name already // (`Vec__i32.push`) — we keep it. -// - Everything else → bare `fn.Name`. +// - Free-function `cfn drop(self: mut T)` → `T.drop`. The receiver +// type qualifies it so two top-level drops for different types +// don't collide on the bare name `drop`. Matches what an +// equivalent in-struct `cfn drop` would mangle to. +// - Otherwise: `[modulePath.][parentStruct.]Name` +// free fn in entry module → `name` +// free fn in module `m` → `m.name` +// method on `T` in entry module → `T.name` +// method on `T` in module `m` → `m.T.name` // // Centralised so every site that picks a function's LLVM symbol // (`jirDeclarePrototype`, `astgen::emitCall`, generic instantiation in @@ -33,17 +42,32 @@ inline std::string mangledFunctionName(const FunctionAST &fn, const TypePool &types, const StringPool &strings) { if (fn.isTest) return "__test_" + fn.Name; - if (fn.Name == "drop" && fn.Args.size() == 1) { + // Free-function drop: qualify by receiver type so `cfn drop(self: + // mut A)` and `cfn drop(self: mut B)` get distinct symbols. Only + // kicks in when parentStruct is empty — in-struct `cfn drop` goes + // through the standard FQN path below. + if (fn.parentStruct.empty() && fn.Name == "drop" && + fn.Args.size() == 1) { const Param &p = fn.Args[0]; if (p.Name == "self" && p.Mode == ParamMode::Mut) { const TypeKey &k = types.get(p.Type); if (k.kind == TypeKind::Struct || k.kind == TypeKind::Named) { StringIdx ni = static_cast(k.a); - if (ni != kNoString) { return "__drop_" + strings.get(ni); } + if (ni != kNoString) { return strings.get(ni) + ".drop"; } } } } - return fn.Name; + std::string out; + if (!fn.modulePath.empty()) { + out += fn.modulePath; + out += '.'; + } + if (!fn.parentStruct.empty()) { + out += fn.parentStruct; + out += '.'; + } + out += fn.Name; + return out; } #endif // JAM_MANGLING_H diff --git a/src/module_resolver.cpp b/src/module_resolver.cpp index 55aefa6..0984027 100644 --- a/src/module_resolver.cpp +++ b/src/module_resolver.cpp @@ -225,6 +225,27 @@ ModuleAST *ModuleResolver::getOrLoadModule(const std::string &importPath) { loadNested(destImport->Path); } + // Stamp the import path on every function/method so the mangler + // can build Zig-style FQNs (`timer.Timer.read32`). Without this, + // two modules that both define `pub fn helper()` or `pub const + // Counter = struct { pub fn init() }` would emit the same LLVM + // symbol and the linker would silently merge them. + // + // Extern fns are skipped — they reference libc / external C + // symbols by bare name (`malloc`, `free`, `printf`) and the + // linker has to find those exactly. Same for export — the user + // asked for that exact symbol to be visible to C callers. + for (auto &fn : module->Functions) { + if (fn->isExtern || fn->isExport) continue; + fn->modulePath = importPath; + } + for (auto &s : module->Structs) { + for (auto &m : s->Methods) { + if (m->isExtern || m->isExport) continue; + m->modulePath = importPath; + } + } + currentlyLoading.erase(importPath); loadedModules[importPath] = std::move(module); return loadedModules[importPath].get(); diff --git a/src/parser.cpp b/src/parser.cpp index 68c4de1..9aaacd4 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -1400,7 +1400,16 @@ void Parser::parseStructBody( // consumes the leading keyword(s) itself. if (check(TOK_FN) || check(TOK_CFN) || check(TOK_PUB) || check(TOK_EXTERN) || check(TOK_EXPORT) || check(TOK_TFN)) { - methods.push_back(parseFunction()); + auto methodFn = parseFunction(); + // Stamp the enclosing struct so the mangler can give the + // method a unique LLVM symbol (e.g. `Counter__init`), + // avoiding collisions with same-named methods on other + // structs. `structContextStack.back()` is the struct + // currently being parsed. + if (!structContextStack.empty()) { + methodFn->parentStruct = structContextStack.back(); + } + methods.push_back(std::move(methodFn)); match(TOK_COMMA); // optional trailing comma after a method continue; } diff --git a/std/collections.jam b/std/collections.jam index 81184ff..e42ecd6 100644 --- a/std/collections.jam +++ b/std/collections.jam @@ -170,7 +170,7 @@ pub fn Vec(T: type) type { } // `cfn drop` is what hooks Vec into MVS auto-cleanup. A - // plain `fn drop` would not auto-fire at scope exit; only + // plain `cfn drop` would not auto-fire at scope exit; only // the cfn variant is registered with the drop registry. cfn drop(self: mut Self) { free(self.ptr as *mut[] u8); diff --git a/tests/cpp/test_comptime.cpp b/tests/cpp/test_comptime.cpp new file mode 100644 index 0000000..b3eb104 --- /dev/null +++ b/tests/cpp/test_comptime.cpp @@ -0,0 +1,413 @@ +// Unit tests for src/comptime.h — the comp-time expression evaluator. +// +// Each test builds a small AST by hand (NodeStore + StringPool + +// TypePool), invokes ComptimeEvaluator::eval, and asserts on the +// returned ComptimeValue. No subprocess fork; the evaluator runs in +// the test's address space. + +#include "../../src/ast_flat.h" +#include "../../src/comptime.h" +#include "test_framework.h" + +#include + +namespace { + +// Helper: emit a NumberLit AstNode for `n`. +NodeIdx mkInt(NodeStore &ns, uint64_t n, bool isNeg = false) { + AstNode node{}; + node.tag = AstTag::NumberLit; + node.lhs = static_cast(n & 0xFFFFFFFFu); + node.rhs = static_cast(n >> 32); + if (isNeg) node.flags |= 1; + return ns.addNode(node); +} + +NodeIdx mkBool(NodeStore &ns, bool b) { + AstNode node{}; + node.tag = AstTag::BoolLit; + node.lhs = b ? 1u : 0u; + return ns.addNode(node); +} + +NodeIdx mkStr(NodeStore &ns, StringPool &sp, const std::string &s) { + AstNode node{}; + node.tag = AstTag::StringLit; + node.lhs = static_cast(sp.intern(s)); + return ns.addNode(node); +} + +NodeIdx mkVar(NodeStore &ns, StringPool &sp, const std::string &name) { + AstNode node{}; + node.tag = AstTag::Variable; + node.lhs = static_cast(sp.intern(name)); + return ns.addNode(node); +} + +NodeIdx mkBinOp(NodeStore &ns, BinOp op, NodeIdx l, NodeIdx r) { + AstNode node{}; + node.tag = AstTag::BinaryOp; + node.op = static_cast(op); + node.lhs = static_cast(l); + node.rhs = static_cast(r); + return ns.addNode(node); +} + +NodeIdx mkUnaryOp(NodeStore &ns, UnaryOp op, NodeIdx operand) { + AstNode node{}; + node.tag = AstTag::UnaryOp; + node.op = static_cast(op); + node.lhs = static_cast(operand); + return ns.addNode(node); +} + +NodeIdx mkIndex(NodeStore &ns, NodeIdx base, NodeIdx idx) { + AstNode node{}; + node.tag = AstTag::Index; + node.lhs = static_cast(base); + node.rhs = static_cast(idx); + return ns.addNode(node); +} + +// ── Literal folds ──────────────────────────────────────────────── + +void testNumberLitFoldsToInt() { + NodeStore ns; + StringPool sp; + TypePool tp; + NodeIdx n = mkInt(ns, 42); + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + jam::ComptimeValue v = e.eval(n, scope); + ASSERT_TRUE(v.isInt()); + ASSERT_EQ(static_cast(42), v.asU64()); +} + +void testBoolLitFolds() { + NodeStore ns; + StringPool sp; + TypePool tp; + NodeIdx t = mkBool(ns, true); + NodeIdx f = mkBool(ns, false); + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + jam::ComptimeValue tv = e.eval(t, scope); + jam::ComptimeValue fv = e.eval(f, scope); + ASSERT_TRUE(tv.isBool()); + ASSERT_TRUE(tv.boolVal); + ASSERT_TRUE(fv.isBool()); + ASSERT_FALSE(fv.boolVal); +} + +void testStringLitFolds() { + NodeStore ns; + StringPool sp; + TypePool tp; + NodeIdx s = mkStr(ns, sp, "hello"); + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + jam::ComptimeValue v = e.eval(s, scope); + ASSERT_TRUE(v.isStr()); + ASSERT_EQ(std::string("hello"), sp.get(v.strVal)); +} + +// ── Scope lookups ──────────────────────────────────────────────── + +void testVariableLookupFromScope() { + NodeStore ns; + StringPool sp; + TypePool tp; + NodeIdx v = mkVar(ns, sp, "x"); + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + scope.bind("x", jam::ComptimeValue::makeInt(99, 32, true)); + jam::ComptimeValue r = e.eval(v, scope); + ASSERT_TRUE(r.isInt()); + ASSERT_EQ(static_cast(99), r.asU64()); +} + +void testVariableMissingReturnsNone() { + NodeStore ns; + StringPool sp; + TypePool tp; + NodeIdx v = mkVar(ns, sp, "missing"); + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + jam::ComptimeValue r = e.eval(v, scope); + ASSERT_TRUE(r.isNone()); +} + +void testNestedScopeFallsThroughToParent() { + jam::ComptimeScope parent; + parent.bind("x", jam::ComptimeValue::makeInt(7, 64, false)); + jam::ComptimeScope child(&parent); + child.bind("y", jam::ComptimeValue::makeInt(11, 64, false)); + + const jam::ComptimeValue *x = child.lookup("x"); + const jam::ComptimeValue *y = child.lookup("y"); + const jam::ComptimeValue *z = child.lookup("z"); + ASSERT_TRUE(x != nullptr); + ASSERT_EQ(static_cast(7), x->asU64()); + ASSERT_TRUE(y != nullptr); + ASSERT_EQ(static_cast(11), y->asU64()); + ASSERT_TRUE(z == nullptr); +} + +// ── Arithmetic ─────────────────────────────────────────────────── + +void testIntAddSubMul() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + NodeIdx a = mkInt(ns, 10); + NodeIdx b = mkInt(ns, 3); + ASSERT_EQ(static_cast(13), + e.eval(mkBinOp(ns, BinOp::Add, a, b), scope).asU64()); + ASSERT_EQ(static_cast(7), + e.eval(mkBinOp(ns, BinOp::Sub, a, b), scope).asU64()); + ASSERT_EQ(static_cast(30), + e.eval(mkBinOp(ns, BinOp::Mul, a, b), scope).asU64()); +} + +void testIntDivByZeroIsNone() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + NodeIdx a = mkInt(ns, 10); + NodeIdx b = mkInt(ns, 0); + ASSERT_TRUE(e.eval(mkBinOp(ns, BinOp::Div, a, b), scope).isNone()); + ASSERT_TRUE(e.eval(mkBinOp(ns, BinOp::Mod, a, b), scope).isNone()); +} + +void testIntComparisons() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + NodeIdx a = mkInt(ns, 5); + NodeIdx b = mkInt(ns, 7); + ASSERT_TRUE(e.eval(mkBinOp(ns, BinOp::Lt, a, b), scope).boolVal); + ASSERT_FALSE(e.eval(mkBinOp(ns, BinOp::Gt, a, b), scope).boolVal); + ASSERT_TRUE(e.eval(mkBinOp(ns, BinOp::Ne, a, b), scope).boolVal); + ASSERT_TRUE(e.eval(mkBinOp(ns, BinOp::Eq, a, a), scope).boolVal); + ASSERT_TRUE(e.eval(mkBinOp(ns, BinOp::Le, a, a), scope).boolVal); + ASSERT_TRUE(e.eval(mkBinOp(ns, BinOp::Ge, b, a), scope).boolVal); +} + +void testBitwise() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + NodeIdx a = mkInt(ns, 0b1100); + NodeIdx b = mkInt(ns, 0b1010); + ASSERT_EQ(static_cast(0b1000), + e.eval(mkBinOp(ns, BinOp::BitAnd, a, b), scope).asU64()); + ASSERT_EQ(static_cast(0b1110), + e.eval(mkBinOp(ns, BinOp::BitOr, a, b), scope).asU64()); + ASSERT_EQ(static_cast(0b0110), + e.eval(mkBinOp(ns, BinOp::BitXor, a, b), scope).asU64()); +} + +// ── Logical operators (short-circuit) ──────────────────────────── + +void testLogAndShortCircuits() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + NodeIdx f = mkBool(ns, false); + // RHS references missing variable — would return None if eval'd. + NodeIdx missing = mkVar(ns, sp, "z"); + jam::ComptimeValue v = e.eval(mkBinOp(ns, BinOp::LogAnd, f, missing), scope); + ASSERT_TRUE(v.isBool()); + ASSERT_FALSE(v.boolVal); // LHS=false short-circuits, never touched RHS +} + +void testLogOrShortCircuits() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + NodeIdx t = mkBool(ns, true); + NodeIdx missing = mkVar(ns, sp, "z"); + jam::ComptimeValue v = e.eval(mkBinOp(ns, BinOp::LogOr, t, missing), scope); + ASSERT_TRUE(v.isBool()); + ASSERT_TRUE(v.boolVal); +} + +// ── Unary ──────────────────────────────────────────────────────── + +void testUnaryNegateInt() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + NodeIdx five = mkInt(ns, 5); + jam::ComptimeValue v = e.eval(mkUnaryOp(ns, UnaryOp::Neg, five), scope); + ASSERT_TRUE(v.isInt()); + // asI64 must sign-extend from the value's width. + ASSERT_EQ(static_cast(-5), v.asI64()); +} + +void testUnaryLogNotBool() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + NodeIdx t = mkBool(ns, true); + jam::ComptimeValue v = e.eval(mkUnaryOp(ns, UnaryOp::LogNot, t), scope); + ASSERT_TRUE(v.isBool()); + ASSERT_FALSE(v.boolVal); +} + +// ── String indexing ────────────────────────────────────────────── + +void testStringIndexReturnsByte() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + NodeIdx s = mkStr(ns, sp, "abc"); + NodeIdx i = mkInt(ns, 1); + jam::ComptimeValue v = e.eval(mkIndex(ns, s, i), scope); + ASSERT_TRUE(v.isInt()); + ASSERT_EQ(static_cast('b'), v.asU64()); +} + +void testStringIndexOutOfBoundsIsNone() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + NodeIdx s = mkStr(ns, sp, "abc"); + NodeIdx i = mkInt(ns, 99); + jam::ComptimeValue v = e.eval(mkIndex(ns, s, i), scope); + ASSERT_TRUE(v.isNone()); +} + +// ── Type equality (the @TypeOf-comparison primitive) ──────────── + +void testTypeEquality() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + scope.bind("T", jam::ComptimeValue::makeType(BuiltinType::I32)); + scope.bind("U", jam::ComptimeValue::makeType(BuiltinType::I32)); + scope.bind("V", jam::ComptimeValue::makeType(BuiltinType::F32)); + + NodeIdx t = mkVar(ns, sp, "T"); + NodeIdx u = mkVar(ns, sp, "U"); + NodeIdx v = mkVar(ns, sp, "V"); + ASSERT_TRUE(e.eval(mkBinOp(ns, BinOp::Eq, t, u), scope).boolVal); + ASSERT_FALSE(e.eval(mkBinOp(ns, BinOp::Eq, t, v), scope).boolVal); + ASSERT_TRUE(e.eval(mkBinOp(ns, BinOp::Ne, t, v), scope).boolVal); +} + +// ── Aggregate indexing (for tuples in Phase 4) ────────────────── + +void testAggregateIndexing() { + std::vector fields; + fields.push_back(jam::ComptimeValue::makeInt(10, 32, true)); + fields.push_back(jam::ComptimeValue::makeInt(20, 32, true)); + fields.push_back(jam::ComptimeValue::makeInt(30, 32, true)); + jam::ComptimeValue agg = jam::ComptimeValue::makeAggregate(std::move(fields)); + + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + scope.bind("t", agg); + + NodeIdx t = mkVar(ns, sp, "t"); + NodeIdx i = mkInt(ns, 1); + jam::ComptimeValue v = e.eval(mkIndex(ns, t, i), scope); + ASSERT_TRUE(v.isInt()); + ASSERT_EQ(static_cast(20), v.asU64()); +} + +// ── Required-fold diagnostics ─────────────────────────────────── + +void testEvalRequiredPushesDiagnosticOnFail() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + NodeIdx miss = mkVar(ns, sp, "missing"); + jam::Diagnostics diags; + jam::SrcLoc loc{"test.jam", 1}; + jam::ComptimeValue v = e.evalRequired(miss, scope, diags, loc); + ASSERT_TRUE(v.isNone()); + ASSERT_TRUE(diags.hasErrors()); + ASSERT_EQ(static_cast(1), diags.errorCount()); +} + +void testEvalRequiredSilentOnSuccess() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + NodeIdx n = mkInt(ns, 42); + jam::Diagnostics diags; + jam::SrcLoc loc{"test.jam", 1}; + jam::ComptimeValue v = e.evalRequired(n, scope, diags, loc); + ASSERT_TRUE(v.isInt()); + ASSERT_FALSE(diags.hasErrors()); +} + +} // namespace + +int main() { + TestFramework framework; + framework.addTest("Comptime - NumberLit folds to Int", testNumberLitFoldsToInt); + framework.addTest("Comptime - BoolLit folds", testBoolLitFolds); + framework.addTest("Comptime - StringLit folds", testStringLitFolds); + framework.addTest("Comptime - Variable lookup from scope", + testVariableLookupFromScope); + framework.addTest("Comptime - missing variable returns None", + testVariableMissingReturnsNone); + framework.addTest("Comptime - nested scope falls through to parent", + testNestedScopeFallsThroughToParent); + framework.addTest("Comptime - int add/sub/mul", testIntAddSubMul); + framework.addTest("Comptime - int div-by-zero returns None", + testIntDivByZeroIsNone); + framework.addTest("Comptime - int comparisons", testIntComparisons); + framework.addTest("Comptime - bitwise and/or/xor", testBitwise); + framework.addTest("Comptime - logical and short-circuits", + testLogAndShortCircuits); + framework.addTest("Comptime - logical or short-circuits", + testLogOrShortCircuits); + framework.addTest("Comptime - unary negate int", testUnaryNegateInt); + framework.addTest("Comptime - unary log-not bool", testUnaryLogNotBool); + framework.addTest("Comptime - string index returns byte", + testStringIndexReturnsByte); + framework.addTest("Comptime - string index out-of-bounds None", + testStringIndexOutOfBoundsIsNone); + framework.addTest("Comptime - type equality", testTypeEquality); + framework.addTest("Comptime - aggregate indexing", + testAggregateIndexing); + framework.addTest( + "Comptime - evalRequired pushes diagnostic on fail", + testEvalRequiredPushesDiagnosticOnFail); + framework.addTest("Comptime - evalRequired silent on success", + testEvalRequiredSilentOnSuccess); + framework.runAll(); + return framework.allPassed() ? 0 : 1; +} diff --git a/tests/unit/mod_cross_a.jam b/tests/unit/mod_cross_a.jam new file mode 100644 index 0000000..c6d3426 --- /dev/null +++ b/tests/unit/mod_cross_a.jam @@ -0,0 +1,24 @@ +// Helper module for test_cross_module_mangling.jam — defines a +// `Counter` struct with `init` and `get` and a free `helper` fn, +// intentionally same-named with mod_cross_b.jam to exercise the +// module-prefixed mangling (`mod_cross_a.Counter.init` vs +// `mod_cross_b.Counter.init`). Standalone compile uses the no-op +// `main` below so the test runner can pick this file up too. + +pub const Counter = struct { + value: u32, + + pub fn init() Self { + return Self { value: 100 }; + } + + pub fn get(self: mut Self) u32 { + return self.value; + } +}; + +pub fn helper() u32 { + return 1; +} + +fn main() u8 { return 0; } diff --git a/tests/unit/mod_cross_b.jam b/tests/unit/mod_cross_b.jam new file mode 100644 index 0000000..4c4579b --- /dev/null +++ b/tests/unit/mod_cross_b.jam @@ -0,0 +1,22 @@ +// Sibling of mod_cross_a.jam — same surface API, different bodies. +// The point is that the LLVM symbol for `Counter.init` from a and b +// must NOT collide; module-prefixed mangling makes them +// `mod_cross_a.Counter.init` vs `mod_cross_b.Counter.init`. + +pub const Counter = struct { + value: u32, + + pub fn init() Self { + return Self { value: 200 }; + } + + pub fn get(self: mut Self) u32 { + return self.value; + } +}; + +pub fn helper() u32 { + return 2; +} + +fn main() u8 { return 0; } diff --git a/tests/unit/test_cross_module_mangling.jam b/tests/unit/test_cross_module_mangling.jam new file mode 100644 index 0000000..e21333c --- /dev/null +++ b/tests/unit/test_cross_module_mangling.jam @@ -0,0 +1,32 @@ +// Cross-module symbol uniqueness — imports two sibling modules +// (mod_cross_a, mod_cross_b) that each define `pub fn helper()` and +// `pub const Counter = struct { pub fn init() / pub fn get() }`. The +// LLVM symbols must differ across modules; before the modulePath +// stamping landed, both `helper`s and both `Counter.init`s collapsed +// to the same symbol and the linker silently merged them. +// +// Lookup mirrors Zig's namespace-scoped resolution (Sema.zig:5295): +// methods on imported structs live under the importing handle's +// namespace, not in a flat global table — registered as +// `handle.Struct.method` and resolved via the multi-dot path in +// astgenCall. + +const { assert } = import("test"); +const a = import("mod_cross_a"); +const b = import("mod_cross_b"); + +tfn freeFnsFromDifferentModulesNoClobber() { + assert(a.helper(), 1); + assert(b.helper(), 2); +} + +tfn methodsOnSameNamedImportedStructsNoClobber() { + // Both modules export a `Counter` with `init()` returning a + // module-specific value. Resolution goes via the + // `handle.Struct.method` registry; mangling produces + // `mod_cross_a.Counter.init` vs `mod_cross_b.Counter.init`. + var ca: a.Counter = a.Counter.init(); + var cb: b.Counter = b.Counter.init(); + assert(ca.get(), 100); + assert(cb.get(), 200); +} diff --git a/tests/unit/test_drops.jam b/tests/unit/test_drops.jam index f9d915f..9170fce 100644 --- a/tests/unit/test_drops.jam +++ b/tests/unit/test_drops.jam @@ -10,7 +10,7 @@ const Counter = struct { sink: *mut u32, }; -// `fn drop(self: mut Counter)` is the user-defined destructor. When a +// `cfn drop(self: mut Counter)` is the user-defined destructor. When a // `var c: Counter = ...;` goes out of scope, the codegen synthesizes a // call to drop(&c). We bump the sink-pointed value so we can observe it // from the test harness. diff --git a/tests/unit/test_drops_mangling.jam b/tests/unit/test_drops_mangling.jam index a18b5b0..eafb57f 100644 --- a/tests/unit/test_drops_mangling.jam +++ b/tests/unit/test_drops_mangling.jam @@ -1,6 +1,6 @@ const { assert } = import("test"); -// Two `fn drop(self: mut T)` for different types coexist +// Two `cfn drop(self: mut T)` for different types coexist // because the codegen mangles each to `__drop_` at the LLVM // level. Each fires for its own type at scope exit. diff --git a/tests/unit/test_struct_methods.jam b/tests/unit/test_struct_methods.jam index 195c458..3aa1567 100644 --- a/tests/unit/test_struct_methods.jam +++ b/tests/unit/test_struct_methods.jam @@ -1,7 +1,7 @@ const { assert } = import("test"); // Drop functions can be declared inside the struct body. The form is a -// pure synonym of the free-function `fn drop(self: mut T)`: the drop +// pure synonym of the free-function `cfn drop(self: mut T)`: the drop // registry picks it up the same way, codegen synthesizes the same // `__drop_T` call at scope exit, the side-effect order is identical. diff --git a/tests/unit/test_struct_pub_methods.jam b/tests/unit/test_struct_pub_methods.jam new file mode 100644 index 0000000..b9a01db --- /dev/null +++ b/tests/unit/test_struct_pub_methods.jam @@ -0,0 +1,99 @@ +const { assert } = import("test"); + +// Struct methods can carry the same visibility modifiers as free +// functions: `pub fn` exports the method across module boundaries, +// plain `fn` keeps it private to the defining module. +// +// In a single-file unit test we can't directly exercise the +// cross-module visibility check (no imports), but we *can* verify: +// - `pub fn` parses inside a struct body +// - `pub fn` methods are callable as instance methods and as +// qualified static calls +// - `pub fn init()` works as a constructor convention +// - a struct may freely mix `pub fn`, plain `fn`, and `cfn` in any +// order without confusing the parser + +const Counter = struct { + value: u32, + + pub fn init() Self { + return Self { value: 0 }; + } + + pub fn bump(self: mut Self) { + self.value = self.value + 1; + } + + pub fn get(self: mut Self) u32 { + return self.value; + } + + // Non-pub method — should still be callable from within this + // module. (Visibility only gates cross-module callers.) + fn secret(self: mut Self) u32 { + return self.value * 2; + } +}; + +tfn pubFnInitReturnsFreshInstance() { + var c: Counter = Counter.init(); + assert(c.value, 0); +} + +tfn pubFnBumpMutatesReceiver() { + var c: Counter = Counter.init(); + c.bump(); + c.bump(); + c.bump(); + assert(c.value, 3); +} + +tfn pubFnGetReturnsField() { + var c: Counter = Counter { value: 42 }; + assert(c.get(), 42); +} + +tfn nonPubFnStillCallableInModule() { + var c: Counter = Counter { value: 7 }; + assert(c.secret(), 14); +} + +// Mix of `pub fn`, plain `fn`, and `cfn` in one struct — proves the +// parser accepts the modifiers in any order and codegen wires up the +// right symbol for each. +const Mixed = struct { + n: u32, + sink: *mut u32, + + pub fn init(sink: *mut u32) Self { + return Self { n: 100, sink: sink }; + } + + pub fn add(self: mut Self, x: u32) { + self.n = self.n + x; + } + + fn quiet(self: mut Self) u32 { + return self.n + 1; + } + + cfn drop(self: mut Self) { + var p: *mut u32 = self.sink; + p.* = p.* + 1; + } +}; + +fn dropsOnce(sink: *mut u32) u32 { + var m: Mixed = Mixed.init(sink); + m.add(5); + return m.quiet(); +} + +tfn mixedModifiersCoexist() { + var hits: u32 = 0; + var n: u32 = dropsOnce(&hits); + // 100 + 5 = 105; quiet returns n+1 = 106. + assert(n, 106); + // Drop fired once at scope exit. + assert(hits, 1); +}