diff --git a/src/ast.h b/src/ast.h index e1f25da..7979420 100644 --- a/src/ast.h +++ b/src/ast.h @@ -43,6 +43,13 @@ struct Param { std::string Name; TypeIdx Type; ParamMode Mode = ParamMode::Let; + // Declared with the source-level `comp` keyword (e.g. `fn f(comp n: + // u32) ...`). A comp param's value must be known at the call site + // and is bound into the function's substitution map at instantiation + // time, *not* lowered as an LLVM parameter. Each unique comp-arg + // combination produces a fresh monomorphisation via the same machine- + // ry that handles `T: type` generics. + bool isComp = false; }; // Function declaration. The body is a sequence of flat-AST node indices @@ -58,11 +65,23 @@ class FunctionAST { bool isPub; bool isTest; bool isVarArgs; - // Declared with `cfn` instead of `fn` — opts the method into - // the compiler-synthesized-call set (drop / at / default). A - // regular `fn` shaped like one of those names is just a method; - // `cfn` is what wires it to the compiler's hooks. + // Declared with `cfn` instead of `fn`. The keyword has two + // meanings depending on declaration position: + // * Inside a struct body — opts the method into the compiler- + // synthesized-call set (drop / at / default). A regular `fn` + // shaped like one of those names is just a method; `cfn` is + // what wires it to the compiler's hooks. `isCfn=true, + // isCompTimeFn=false`. + // * At top level (free function) — declares a compile-time + // function: the body executes at compile time at each call + // site, with @-emit intrinsics generating runtime code into + // the caller. `isCfn=false, isCompTimeFn=true`. Used by + // `std/fmt.jam` to ship `print` / `eprint` as real Jam + // source. + // The two meanings are mutually exclusive — set by the + // parseFunction-caller based on declaration context. bool isCfn; + bool isCompTimeFn = false; // 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 @@ -101,8 +120,17 @@ class FunctionAST { // supplies concrete type arguments. bool isGeneric() const { if (ReturnType == BuiltinType::Type) return true; + // Compile-time functions are always per-call-site instantiated + // — the body runs at compile time and emits caller-specific + // code. Same dispatch shape as type/comp-value generics. + if (isCompTimeFn) return true; for (const Param &p : Args) { + // Type-parameter generics (`T: type`) and value-parameter + // generics (`comp n: u32`) both monomorphize per call + // site — the same machinery handles both, so they fall + // under the same generic predicate. if (p.Type == BuiltinType::Type) return true; + if (p.isComp) return true; } return false; } @@ -211,20 +239,39 @@ class ConstDeclAST { }; // const std = import("std"); +// +// When written as `pub const X = import(...)` at module scope, the +// handle becomes a re-export: a downstream importer that names *this* +// module can then reach `module.X.member` to descend into the re- +// exported module. Tracked via `isPub`. +// +// `chain` captures trailing `.seg.seg` access on the RHS — e.g. `const +// fmt = import("std").fmt;` parses with `Path="std"` and `chain=["fmt"]`. +// Each segment walks a `pub const X = import(...)` re-export in the +// preceding module's namespace; the final path is what `Name` binds to. class ImportDeclAST { public: std::string Name; std::string Path; + std::vector chain; + bool isPub = false; ImportDeclAST(std::string Name, std::string Path) : Name(std::move(Name)), Path(std::move(Path)) {} }; // const { f1, f2 } = import("mod"); +// +// `chain` captures trailing `.seg.seg` access on the RHS — e.g. +// `const {print} = import("std").fmt;` parses with `Path="std"` and +// `chain=["fmt"]`. Resolution walks pub-import re-exports in each +// intermediate module's namespace, ending at the module each +// destructured name is pulled from. class DestructuringImportDeclAST { public: std::vector Names; std::string Path; + std::vector chain; DestructuringImportDeclAST(std::vector Names, std::string Path) : Names(std::move(Names)), Path(std::move(Path)) {} diff --git a/src/ast_flat.h b/src/ast_flat.h index 50dc836..5071274 100644 --- a/src/ast_flat.h +++ b/src/ast_flat.h @@ -130,12 +130,17 @@ enum class AstTag : uint8_t { // tag extraction. AsCast, - // Comptime intrinsic call: `@name(T)`. Resolved to a constant at - // codegen time; LLVM never sees a call instruction. Stage 1 only - // supports single-TYPE-arg intrinsics (sizeOf, alignOf); generalize - // to multi-arg shapes when user-defined cfn + CTFE lands. - // d.lhs = StringIdx (intrinsic name, e.g. "sizeOf") - // d.rhs = TypeIdx (the type argument) + // Compiler intrinsic call: `@name(arg, …)`. Resolved at astgen + // time; LLVM never sees an AtCall instruction. + // + // Two arg-encoding shapes, discriminated by d.flags bit 0: + // * flags bit 0 = 0 — type-arg single-form. d.rhs = TypeIdx. + // Used by `@sizeOf(T)`, `@alignOf(T)`. + // * flags bit 0 = 1 — expr-arg multi-form. d.rhs = ExtraIdx → + // [argCount, arg0_NodeIdx, arg1_NodeIdx, ...]. Used by + // `@emit*` intrinsics callable from cfn bodies. + // + // d.lhs = StringIdx (intrinsic name). AtCall, // Static method call on a generic-call type receiver: @@ -350,6 +355,17 @@ enum class TypeKind : uint8_t { // via the substitution engine when an LLVM type is requested or // when a binding's static TypeIdx is needed. GenericCall, + // Compile-time module value. Produced by `import(...)`; consumed by + // `MemberAccess` for `mod.foo` resolution. The TypeKey carries the + // module's canonical resolved path in `a` (StringIdx, e.g. + // "collections" or "std"). Has no runtime representation — modules + // are values of this type only at compile time; codegen rejects any + // attempt to lower a Module-typed JIR ref to LLVM. + // + // Mirrors Zig's "file = zero-field struct with namespace" pattern, + // but kept distinct from TypeKind::Struct so module values can't be + // confused with user-defined aggregates. + Module, // Function-typed value: a pointer to a function with a known // signature. Parsed from `fn(T1, T2) Ret` in type position. The // TypeKey carries the return TypeIdx in `a` and an index into @@ -413,6 +429,10 @@ inline bool operator==(const TypeKey &x, const TypeKey &y) { // args index is canonical because the side table interns // args lists. return x.a == y.a && x.b == y.b; + case TypeKind::Module: + // equal iff the resolved module path matches. One TypeIdx + // per unique path (`internModule` consults `idx_`). + return x.a == y.a; } return false; } @@ -576,6 +596,11 @@ class TypePool { TypeIdx internNamed(StringIdx nameId) { return intern(TypeKey{TypeKind::Named, 0, 0, nameId, 0}); } + // Intern a Module TypeIdx keyed on its resolved path. One TypeIdx + // per unique resolved path (e.g. "collections", "std/fmt"). + TypeIdx internModule(StringIdx pathId) { + return intern(TypeKey{TypeKind::Module, 0, 0, pathId, 0}); + } }; #endif // AST_FLAT_H diff --git a/src/astgen.cpp b/src/astgen.cpp index e03a53d..51e4f14 100644 --- a/src/astgen.cpp +++ b/src/astgen.cpp @@ -10,6 +10,8 @@ #include "abi.h" #include "ast.h" #include "codegen.h" +#include "comptime.h" +#include "jir_codegen.h" #include "mangling.h" #include @@ -660,9 +662,18 @@ static void astgenVarDecl(AstGenCtx &gctx, const AstNode &n) { if (r != kNoType) return resolveForCmp(r); } if (k.kind == TypeKind::Named) { - TypeIdx a = gctx.ctx.lookupTypeAlias( - gctx.ctx.getStringPool().get(static_cast(k.a))); + const std::string &nm = gctx.ctx.getStringPool().get( + static_cast(k.a)); + TypeIdx a = gctx.ctx.lookupTypeAlias(nm); if (a != kNoType) return resolveForCmp(a); + // 3+ segment chain through module re-exports — collapse + // `w.leaf.Point` to the canonical `Point` so it matches + // values produced by `w.leaf.makePoint(...)` whose return + // type was registered with the single-segment name. + if (nm.find('.') != std::string::npos) { + TypeIdx c = gctx.ctx.resolveChainedType(nm); + if (c != kNoType) return resolveForCmp(c); + } } return t; }; @@ -737,6 +748,67 @@ static JirRef astgenVariable(AstGenCtx &gctx, const AstNode &n, load.ty = gctx.localTypes[name]; return emit(gctx, load); } + // Comp-param substitution. When the enclosing function was + // instantiated with `comp n: u32` bound to a concrete value, refs + // to `n` in the body lower to that constant directly — no Param / + // Load instruction, no LLVM argument. Locals shadow comp params + // (the local-lookup above runs first); module consts and + // functions are checked after, so a comp-bound name takes + // precedence over a same-named module const. + if (const jam::ComptimeValue *cv = + gctx.ctx.lookupCurrentCompSubst(name)) { + switch (cv->kind) { + case jam::ComptimeValue::Kind::Int: { + JirInst ic{}; + ic.tag = JirTag::Int; + ic.a = static_cast(cv->intVal.bits & 0xFFFFFFFFu); + ic.b = static_cast(cv->intVal.bits >> 32); + ic.ty = gctx.ctx.getTypePool().internInt(cv->intVal.width, + cv->intVal.isSigned); + return emit(gctx, ic); + } + case jam::ComptimeValue::Kind::Bool: { + JirInst ic{}; + ic.tag = JirTag::Bool; + ic.a = cv->boolVal ? 1u : 0u; + ic.ty = BuiltinType::Bool; + return emit(gctx, ic); + } + case jam::ComptimeValue::Kind::Str: { + JirInst si{}; + si.tag = JirTag::Str; + si.a = cv->strVal; + si.ty = gctx.ctx.getTypePool().intern( + TypeKey{TypeKind::Slice, 0, 0, BuiltinType::U8, 0}); + return emit(gctx, si); + } + case jam::ComptimeValue::Kind::Float: { + JirInst fl{}; + fl.tag = JirTag::Float; + uint64_t bits; + __builtin_memcpy(&bits, &cv->floatVal.value, sizeof(bits)); + fl.a = static_cast(bits & 0xFFFFFFFFu); + fl.b = static_cast(bits >> 32); + fl.ty = gctx.ctx.getTypePool().internFloat(cv->floatVal.width); + return emit(gctx, fl); + } + case jam::ComptimeValue::Kind::Type: + // Type values have no runtime representation. Reaching + // here means a value position used a comp param of meta- + // type kind — pure type values shouldn't lower to JIR. + return recoverHere(gctx, + "comp param `" + name + + "` is of type `type` and has no " + "runtime representation", + kNoType); + case jam::ComptimeValue::Kind::Aggregate: + case jam::ComptimeValue::Kind::None: + return recoverHere( + gctx, + "comp param `" + name + "` has no runtime lowering yet", + kNoType); + } + } if (const auto *mc = gctx.ctx.getModuleConst(name)) { return astgenExpr(gctx, mc->initExpr, mc->declaredType); } @@ -3329,6 +3401,609 @@ static JirRef emitCall(AstGenCtx &gctx, const FunctionAST *fn, return callRef; } +// Comp-instantiation dispatcher. When the callee fn has any comp +// params, this evaluates them at compile time, builds a mangled clone +// name, and (on cache miss) registers + declares + defines a +// monomorphised version of the fn with the comp params dropped from +// the signature. Comp param refs in the body resolve to the bound +// values via `astgenVariable`'s comp-subst path. Returns the result +// of a regular Call to the clone, with only the runtime args lowered. +// +// Mirrors the shape of struct-method instantiation in +// `JamCodegenContext::instantiateStructExpr`: two-phase isn't needed +// here because comp-instantiated functions don't have mutual recursion +// to resolve (one fn, one body). +// Ensure POSIX `dprintf(int fd, const char *fmt, ...)` is declared in +// the module so @-emit intrinsics can dispatch through it. Idempotent +// across calls — the registry check skips after the first. +static void ensureDprintfForCfn(AstGenCtx &gctx) { + if (gctx.ctx.getFunctionAST("dprintf") != nullptr) return; + auto fake = std::make_unique( + "dprintf", + std::vector{ + Param{"fd", BuiltinType::I32, ParamMode::Let}, + Param{"fmt", BuiltinType::U64, ParamMode::Let}, + }, + BuiltinType::I32, std::vector{}, /*isExtern=*/true, + /*isExport=*/false, /*isPub=*/false, /*isTest=*/false, + /*isVarArgs=*/true); + gctx.ctx.registerFunctionAST("dprintf", fake.release()); + JamTypeRef i8PtrType = JamLLVMPointerType(gctx.ctx.getInt8Type(), 0); + JamTypeRef paramTypes[2] = {gctx.ctx.getInt32Type(), i8PtrType}; + JamTypeRef ft = JamLLVMFunctionType(gctx.ctx.getInt32Type(), paramTypes, 2, + /*isVarArgs=*/true); + JamFunctionRef pf = JamLLVMAddFunction(gctx.ctx.getModule(), "dprintf", ft); + JamLLVMApplyDefaultFnAttrs(pf, /*isExtern=*/true); +} + +// Emit `dprintf(fd, fmtSpec, arg)` using only one extra arg after the +// format string. Used by the per-type interp lowering and by +// emitPutByte for the "%c" case. +static void emitDprintfSingleArg(AstGenCtx &gctx, JirRef fdRef, + const char *fmtSpec, JirRef arg) { + TypeIdx sliceTy = gctx.ctx.getTypePool().intern( + TypeKey{TypeKind::Slice, 0, 0, BuiltinType::U8, 0}); + TypeIdx u8PtrTy = gctx.ctx.getTypePool().intern( + TypeKey{TypeKind::PtrMany, 0, 0, BuiltinType::U8, 0}); + StringIdx fmtId = gctx.ctx.getStringPool().intern(fmtSpec); + JirInst fs{}; + fs.tag = JirTag::Str; + fs.a = fmtId; + fs.ty = sliceTy; + JirRef fmtSlice = emit(gctx, fs); + JirInst fp{}; + fp.tag = JirTag::ExtractValue; + fp.a = fmtSlice; + fp.b = 0; + fp.ty = u8PtrTy; + JirRef fmtPtr = emit(gctx, fp); + std::vector pp = {3, static_cast(fdRef), + static_cast(fmtPtr), + static_cast(arg)}; + JirExtraIdx pe = gctx.jfn.pushExtra(pp.data(), pp.size()); + JirInst c{}; + c.tag = JirTag::Call; + c.a = gctx.ctx.getStringPool().intern("dprintf"); + c.b = pe; + c.ty = BuiltinType::I32; + emit(gctx, c); +} + +// Materialise a JirRef holding `value` as an LLVM i32. +static JirRef literalI32(AstGenCtx &gctx, uint32_t value) { + JirInst i{}; + i.tag = JirTag::Int; + i.a = value; + i.ty = BuiltinType::I32; + return emit(gctx, i); +} + +// Per-type runtime print dispatch used by `@emitPrintLocalByRange`. +// `val` is the loaded local-variable JirRef; `name` is the source +// identifier (only used for the diagnostic). The dispatcher widens / +// extracts as needed and emits one dprintf call per piece. +static void emitDprintfForValue(AstGenCtx &gctx, jam::Diagnostics &diags, + jam::SrcLoc loc, JirRef fdRef, JirRef val, + const std::string &name) { + TypeIdx ty = gctx.jfn.getInst(val).ty; + const TypeKey &k = gctx.ctx.getTypePool().get(ty); + switch (k.kind) { + case TypeKind::Int: { + bool isSigned = k.b != 0; + uint16_t width = static_cast(k.a); + JirRef wideRef = val; + if (width < 64) { + JirInst ext{}; + ext.tag = isSigned ? JirTag::SExt : JirTag::ZExt; + ext.a = val; + ext.ty = BuiltinType::I64; + wideRef = emit(gctx, ext); + } + emitDprintfSingleArg(gctx, fdRef, isSigned ? "%lld" : "%llu", + wideRef); + return; + } + case TypeKind::Float: { + uint16_t width = static_cast(k.a); + JirRef wideRef = val; + if (width < 64) { + JirInst ext{}; + ext.tag = JirTag::FPExt; + ext.a = val; + ext.ty = BuiltinType::F64; + wideRef = emit(gctx, ext); + } + emitDprintfSingleArg(gctx, fdRef, "%g", wideRef); + return; + } + case TypeKind::Bool: { + // "true"/"false" via CondBr to two write blocks. + JirBlockRef trueB = gctx.jfn.pushBlock("emit.true"); + JirBlockRef falseB = gctx.jfn.pushBlock("emit.false"); + JirBlockRef joinB = gctx.jfn.pushBlock("emit.bool.end"); + emitCondBr(gctx, val, trueB, falseB); + + auto emitStrLit = [&](JirBlockRef block, const char *literal) { + gctx.currentBlock = block; + TypeIdx sliceTy = gctx.ctx.getTypePool().intern( + TypeKey{TypeKind::Slice, 0, 0, BuiltinType::U8, 0}); + TypeIdx u8PtrTy = gctx.ctx.getTypePool().intern( + TypeKey{TypeKind::PtrMany, 0, 0, BuiltinType::U8, 0}); + StringIdx litId = gctx.ctx.getStringPool().intern(literal); + JirInst si{}; + si.tag = JirTag::Str; + si.a = litId; + si.ty = sliceTy; + JirRef sl = emit(gctx, si); + JirInst pi{}; + pi.tag = JirTag::ExtractValue; + pi.a = sl; + pi.b = 0; + pi.ty = u8PtrTy; + JirRef ptr = emit(gctx, pi); + JirInst li{}; + li.tag = JirTag::ExtractValue; + li.a = sl; + li.b = 1; + li.ty = BuiltinType::U64; + JirRef ln = emit(gctx, li); + JirInst tr{}; + tr.tag = JirTag::Trunc; + tr.a = ln; + tr.ty = BuiltinType::I32; + JirRef lnI32 = emit(gctx, tr); + StringIdx fmtId = gctx.ctx.getStringPool().intern("%.*s"); + JirInst fs{}; + fs.tag = JirTag::Str; + fs.a = fmtId; + fs.ty = sliceTy; + JirRef fmtSlice = emit(gctx, fs); + JirInst fp{}; + fp.tag = JirTag::ExtractValue; + fp.a = fmtSlice; + fp.b = 0; + fp.ty = u8PtrTy; + JirRef fmtPtr = emit(gctx, fp); + std::vector pp = { + 4, static_cast(fdRef), + static_cast(fmtPtr), static_cast(lnI32), + static_cast(ptr)}; + JirExtraIdx pe = gctx.jfn.pushExtra(pp.data(), pp.size()); + JirInst c{}; + c.tag = JirTag::Call; + c.a = gctx.ctx.getStringPool().intern("dprintf"); + c.b = pe; + c.ty = BuiltinType::I32; + emit(gctx, c); + emitBr(gctx, joinB); + }; + emitStrLit(trueB, "true"); + emitStrLit(falseB, "false"); + gctx.currentBlock = joinB; + return; + } + case TypeKind::Slice: { + TypeIdx elem = static_cast(k.a); + if (elem != BuiltinType::U8) { + diags.error(loc, "@emit: slice argument `" + name + + "` is not a slice of u8"); + return; + } + TypeIdx sliceTy = gctx.ctx.getTypePool().intern( + TypeKey{TypeKind::Slice, 0, 0, BuiltinType::U8, 0}); + TypeIdx u8PtrTy = gctx.ctx.getTypePool().intern( + TypeKey{TypeKind::PtrMany, 0, 0, BuiltinType::U8, 0}); + JirInst pi{}; + pi.tag = JirTag::ExtractValue; + pi.a = val; + pi.b = 0; + pi.ty = u8PtrTy; + JirRef ptr = emit(gctx, pi); + JirInst li{}; + li.tag = JirTag::ExtractValue; + li.a = val; + li.b = 1; + li.ty = BuiltinType::U64; + JirRef ln = emit(gctx, li); + JirInst tr{}; + tr.tag = JirTag::Trunc; + tr.a = ln; + tr.ty = BuiltinType::I32; + JirRef lnI32 = emit(gctx, tr); + StringIdx fmtId = gctx.ctx.getStringPool().intern("%.*s"); + JirInst fs{}; + fs.tag = JirTag::Str; + fs.a = fmtId; + fs.ty = sliceTy; + JirRef fmtSlice = emit(gctx, fs); + JirInst fp{}; + fp.tag = JirTag::ExtractValue; + fp.a = fmtSlice; + fp.b = 0; + fp.ty = u8PtrTy; + JirRef fmtPtr = emit(gctx, fp); + std::vector pp = { + 4, static_cast(fdRef), static_cast(fmtPtr), + static_cast(lnI32), static_cast(ptr)}; + JirExtraIdx pe = gctx.jfn.pushExtra(pp.data(), pp.size()); + JirInst c{}; + c.tag = JirTag::Call; + c.a = gctx.ctx.getStringPool().intern("dprintf"); + c.b = pe; + c.ty = BuiltinType::I32; + emit(gctx, c); + return; + } + default: + diags.error(loc, "@emit: local `" + name + "` has a type that's " + "not printable (only " + "int / float / bool / " + "slice supported)"); + return; + } +} + +// Astgen-side emitter implementing `CompEmitter`. Owns a reference to +// the caller's `AstGenCtx` so each `@-emit` intrinsic drops JIR into +// the right function. Lives only for the duration of one cfn body +// execution. +class CfnEmitter : public jam::CompEmitter { + public: + explicit CfnEmitter(AstGenCtx &gctx) : gctx_(gctx) {} + + jam::ExecResult + handleAtCall(const std::string &name, + const std::vector &args, + jam::Diagnostics &diags, jam::SrcLoc loc) override { + if (name == "emitPutByte") { + return handlePutByte(args, diags, loc); + } + if (name == "emitPrintLocalByRange") { + return handlePrintLocalByRange(args, diags, loc); + } + if (name == "emitWriteBytes") { + return handleWriteBytes(args, diags, loc); + } + diags.error(loc, "unknown @-emit intrinsic `@" + name + "`"); + return jam::ExecResult::Error; + } + + private: + AstGenCtx &gctx_; + + jam::ExecResult + handlePutByte(const std::vector &args, + jam::Diagnostics &diags, jam::SrcLoc loc) { + if (args.size() != 2 || !args[0].isInt() || !args[1].isInt()) { + diags.error(loc, + "@emitPutByte expects (fd: i32, byte: u8)"); + return jam::ExecResult::Error; + } + ensureDprintfForCfn(gctx_); + JirRef fdRef = literalI32(gctx_, + static_cast(args[0].asU64())); + JirInst byteI{}; + byteI.tag = JirTag::Int; + byteI.a = static_cast(args[1].asU64() & 0xFFu); + byteI.ty = BuiltinType::I32; // %c expects an int + JirRef byteRef = emit(gctx_, byteI); + emitDprintfSingleArg(gctx_, fdRef, "%c", byteRef); + return jam::ExecResult::Continue; + } + + jam::ExecResult + handlePrintLocalByRange(const std::vector &args, + jam::Diagnostics &diags, jam::SrcLoc loc) { + if (args.size() != 4 || !args[0].isInt() || !args[1].isStr() || + !args[2].isInt() || !args[3].isInt()) { + diags.error(loc, + "@emitPrintLocalByRange expects " + "(fd: i32, fmt: str, start: u32, end: u32)"); + return jam::ExecResult::Error; + } + uint32_t fd = static_cast(args[0].asU64()); + const std::string &fmtStr = + gctx_.ctx.getStringPool().get(args[1].strVal); + uint32_t start = static_cast(args[2].asU64()); + uint32_t end = static_cast(args[3].asU64()); + if (start > end || end > fmtStr.length()) { + diags.error(loc, + "@emitPrintLocalByRange: range out of bounds " + "for the format string"); + return jam::ExecResult::Error; + } + std::string name = fmtStr.substr(start, end - start); + auto it = gctx_.locals.find(name); + if (it == gctx_.locals.end()) { + diags.error(loc, "unknown variable `" + name + + "` referenced from cfn format string"); + return jam::ExecResult::Error; + } + ensureDprintfForCfn(gctx_); + // Load the local. + JirInst load{}; + load.tag = JirTag::Load; + load.a = it->second; + load.ty = gctx_.localTypes[name]; + JirRef val = emit(gctx_, load); + JirRef fdRef = literalI32(gctx_, fd); + emitDprintfForValue(gctx_, diags, loc, fdRef, val, name); + return jam::ExecResult::Continue; + } + + // Emit a single `dprintf(fd, "%.*s", len, ptr)` for the byte slice + // `fmt[start..end]`. Lets a cfn batch consecutive literal bytes + // into one runtime write instead of one dprintf per byte. The + // substring is interned into the global StringPool at compile + // time; the runtime code just hands its `.rodata` ptr + len to + // dprintf. + jam::ExecResult + handleWriteBytes(const std::vector &args, + jam::Diagnostics &diags, jam::SrcLoc loc) { + if (args.size() != 4 || !args[0].isInt() || !args[1].isStr() || + !args[2].isInt() || !args[3].isInt()) { + diags.error(loc, + "@emitWriteBytes expects " + "(fd: i32, fmt: str, start: u32, end: u32)"); + return jam::ExecResult::Error; + } + uint32_t fd = static_cast(args[0].asU64()); + const std::string &fmtStr = + gctx_.ctx.getStringPool().get(args[1].strVal); + uint32_t start = static_cast(args[2].asU64()); + uint32_t end = static_cast(args[3].asU64()); + if (start > end || end > fmtStr.length()) { + diags.error(loc, + "@emitWriteBytes: range out of bounds " + "for the format string"); + return jam::ExecResult::Error; + } + if (start == end) { + // Empty span — caller-side guard against this is cheap; + // be permissive so cfn authors don't need to branch. + return jam::ExecResult::Continue; + } + ensureDprintfForCfn(gctx_); + StringIdx litId = gctx_.ctx.getStringPool().intern( + fmtStr.substr(start, end - start)); + TypeIdx sliceTy = gctx_.ctx.getTypePool().intern( + TypeKey{TypeKind::Slice, 0, 0, BuiltinType::U8, 0}); + TypeIdx u8PtrTy = gctx_.ctx.getTypePool().intern( + TypeKey{TypeKind::PtrMany, 0, 0, BuiltinType::U8, 0}); + JirInst sl{}; + sl.tag = JirTag::Str; + sl.a = litId; + sl.ty = sliceTy; + JirRef litSlice = emit(gctx_, sl); + JirInst pi{}; + pi.tag = JirTag::ExtractValue; + pi.a = litSlice; + pi.b = 0; + pi.ty = u8PtrTy; + JirRef ptr = emit(gctx_, pi); + JirInst li{}; + li.tag = JirTag::ExtractValue; + li.a = litSlice; + li.b = 1; + li.ty = BuiltinType::U64; + JirRef ln = emit(gctx_, li); + JirInst tr{}; + tr.tag = JirTag::Trunc; + tr.a = ln; + tr.ty = BuiltinType::I32; + JirRef lnI32 = emit(gctx_, tr); + StringIdx fmtId = gctx_.ctx.getStringPool().intern("%.*s"); + JirInst fs{}; + fs.tag = JirTag::Str; + fs.a = fmtId; + fs.ty = sliceTy; + JirRef fmtSlice = emit(gctx_, fs); + JirInst fp{}; + fp.tag = JirTag::ExtractValue; + fp.a = fmtSlice; + fp.b = 0; + fp.ty = u8PtrTy; + JirRef fmtPtr = emit(gctx_, fp); + JirRef fdRef = literalI32(gctx_, fd); + std::vector pp = { + 4, static_cast(fdRef), static_cast(fmtPtr), + static_cast(lnI32), static_cast(ptr)}; + JirExtraIdx pe = gctx_.jfn.pushExtra(pp.data(), pp.size()); + JirInst c{}; + c.tag = JirTag::Call; + c.a = gctx_.ctx.getStringPool().intern("dprintf"); + c.b = pe; + c.ty = BuiltinType::I32; + emit(gctx_, c); + return jam::ExecResult::Continue; + } +}; + +// Compile-time function call dispatcher. Runs `fn`'s body via the +// ComptimeEvaluator with the call site's args bound into a fresh comp +// scope. The body must evaluate at compile time; any @-emit intrinsic +// it contains (Phase 4) generates JIR into the *caller's* gctx — that's +// how `std.fmt.print` lowers to a sequence of write calls inside the +// user's main() rather than inside print's own LLVM function. +static JirRef astgenCompTimeFnCall(AstGenCtx &gctx, const AstNode &n, + const FunctionAST *fn) { + const NodeStore &ns = gctx.ctx.getNodeStore(); + ExtraIdx argsExtra = static_cast(n.rhs); + uint32_t argCount = ns.getExtra(argsExtra); + + // Arg count must match. cfn doesn't support varargs. + if (argCount != fn->Args.size()) { + return recoverHere(gctx, + "cfn `" + fn->Name + "` expects " + + std::to_string(fn->Args.size()) + + " arg(s), got " + std::to_string(argCount), + kNoType); + } + + // Evaluate each arg at compile time and bind to the param name. + jam::ComptimeEvaluator ev(ns, gctx.ctx.getStringPool(), + gctx.ctx.getTypePool()); + jam::ComptimeScope outer; + jam::Diagnostics &diags = gctx.ctx.diagnostics(); + jam::SrcLoc loc{gctx.ctx.currentFile(), 0}; + for (uint32_t i = 0; i < argCount; i++) { + NodeIdx argIdx = static_cast(ns.getExtra(argsExtra + 1 + i)); + jam::ComptimeValue v = ev.eval(argIdx, outer); + if (v.isNone()) { + return recoverHere(gctx, + "argument to cfn `" + fn->Name + "` (param `" + + fn->Args[i].Name + + "`) must be a compile-time constant", + kNoType); + } + outer.bind(fn->Args[i].Name, std::move(v)); + } + + // Install the emitter so @-emit intrinsics inside the body drop + // JIR into the caller's gctx. The emitter has a reference to + // `gctx` for the duration of the body's execution; clearing it + // after prevents accidental reuse. + CfnEmitter emitter(gctx); + ev.setCallContext(&emitter, &diags, loc); + uint32_t iterCounter = 0; + jam::ComptimeValue returned; + std::vector bodyVec(fn->Body.begin(), fn->Body.end()); + jam::ExecResult r = ev.execBlock(bodyVec.data(), bodyVec.size(), outer, + iterCounter, + jam::ComptimeEvaluator::kDefaultIterCap, + returned, diags, loc); + ev.clearCallContext(); + if (r == jam::ExecResult::Error || r == jam::ExecResult::IterationCap) { + // Diagnostic already pushed; return a poison so the rest of + // the caller still gets analyzed. + return recoverHere(gctx, + "cfn `" + fn->Name + "` failed during compile-" + "time evaluation", + kNoType); + } + // cfn returns void in v1; the value (if any) is discarded for now. + return kNoJirRef; +} + +static JirRef astgenCompInstantiatedCall(AstGenCtx &gctx, const AstNode &n, + const FunctionAST *fn) { + const NodeStore &ns = gctx.ctx.getNodeStore(); + ExtraIdx argsExtra = static_cast(n.rhs); + uint32_t argCount = ns.getExtra(argsExtra); + + jam::ComptimeEvaluator ev(gctx.ctx.getNodeStore(), + gctx.ctx.getStringPool(), + gctx.ctx.getTypePool()); + jam::ComptimeScope scope; + std::unordered_map compSubst; + std::vector runtimeArgs; + std::string mangleSuffix; + + for (uint32_t i = 0; i < argCount; i++) { + NodeIdx argIdx = static_cast(ns.getExtra(argsExtra + 1 + i)); + if (i < fn->Args.size() && fn->Args[i].isComp) { + jam::ComptimeValue v = ev.eval(argIdx, scope); + if (v.isNone()) { + return recoverHere(gctx, + "argument for comp param `" + + fn->Args[i].Name + + "` must be a compile-time constant", + kNoType); + } + compSubst[fn->Args[i].Name] = v; + // Mangle a unique suffix per comp value. + mangleSuffix += "__"; + switch (v.kind) { + case jam::ComptimeValue::Kind::Int: + mangleSuffix += + (v.intVal.isSigned ? "i" : "u") + std::to_string(v.asU64()); + break; + case jam::ComptimeValue::Kind::Bool: + mangleSuffix += v.boolVal ? "true" : "false"; + break; + case jam::ComptimeValue::Kind::Str: { + // Hash to keep the symbol stable / printable for + // arbitrary contents. + std::hash hash; + const std::string &s = gctx.ctx.getStringPool().get(v.strVal); + mangleSuffix += "s" + std::to_string(hash(s)); + break; + } + case jam::ComptimeValue::Kind::Type: { + const TypeKey &tk = gctx.ctx.getTypePool().get(v.typeVal); + if (tk.kind == TypeKind::Int) { + mangleSuffix += + (tk.b ? "i" : "u") + std::to_string(tk.a); + } else if (tk.kind == TypeKind::Bool) { + mangleSuffix += "bool"; + } else { + mangleSuffix += "t" + std::to_string(v.typeVal); + } + break; + } + default: + mangleSuffix += "x"; + break; + } + } else { + runtimeArgs.push_back(argIdx); + } + } + + std::string instName = fn->Name + mangleSuffix; + + // Cache hit? Skip the clone+lower and dispatch to the existing + // instantiation. + const FunctionAST *clone = gctx.ctx.getFunctionAST(instName); + if (clone == nullptr) { + // Build a clone whose signature drops the comp params. Body + // stays as the original NodeStore indices — astgen reads the + // comp subst when it encounters the comp param names. + std::vector instArgs; + instArgs.reserve(fn->Args.size()); + for (const auto &p : fn->Args) { + if (!p.isComp) instArgs.push_back(p); + } + auto cloned = std::make_unique( + instName, std::move(instArgs), fn->ReturnType, fn->Body, + fn->isExtern, fn->isExport, fn->isPub, fn->isTest, fn->isVarArgs, + fn->isCfn); + clone = gctx.ctx.adoptInstantiatedFunction(std::move(cloned)); + gctx.ctx.registerFunctionAST(instName, + const_cast(clone)); + + // Save the builder so we can return to the caller's insertion + // point once the clone body's been emitted. + JamBasicBlockRef savedBB = + JamLLVMGetInsertBlock(gctx.ctx.getBuilder()); + + gctx.ctx.setCurrentCompSubst(compSubst); + JirFunction jfn = astgenMetadata(*clone, gctx.ctx); + jfn.name = clone->Name; + jirDeclarePrototype(jfn, gctx.ctx); + astgenBodyInto(jfn, *clone, gctx.ctx); + jirDefineBody(jfn, gctx.ctx); + gctx.ctx.clearCurrentCompSubst(); + + if (savedBB) { + JamLLVMPositionBuilderAtEnd(gctx.ctx.getBuilder(), savedBB); + } + } + + // Emit a regular Call to the cloned fn, passing only the runtime + // args. lowerArg honours each param's mode (mut/move pass-by-ptr). + std::vector argRefs; + argRefs.reserve(runtimeArgs.size()); + for (size_t i = 0; i < runtimeArgs.size(); i++) { + if (i < clone->Args.size()) { + argRefs.push_back(lowerArg(gctx, runtimeArgs[i], clone->Args[i])); + } else { + argRefs.push_back(astgenExpr(gctx, runtimeArgs[i], kNoType)); + } + } + return emitCall(gctx, clone, argRefs); +} + static JirRef astgenCall(AstGenCtx &gctx, const AstNode &n) { const NodeStore &ns = gctx.ctx.getNodeStore(); ExtraIdx argsExtra = static_cast(n.rhs); @@ -3770,6 +4445,35 @@ static JirRef astgenCall(AstGenCtx &gctx, const AstNode &n) { } const FunctionAST *fn = gctx.ctx.getFunctionAST(callee); + if (fn == nullptr && callee.find('.') != std::string::npos) { + // Chained module access fallback: `std.fmt.print(...)` etc. + // The flat lookup above only knows `handle.X` (single-dot) + // entries; this walks ModuleNamespace.moduleAliases for each + // intermediate segment and resolves the final segment as a + // function in the leaf module's namespace. + fn = gctx.ctx.resolveChainedFunction(callee); + } + // Compile-time function calls: `pub cfn` declared at the top level. + // The cfn body executes at compile time, with each call site + // generating a fresh execution. Args must be comp-known. The body + // is interpreted by `ComptimeEvaluator`; @-emit intrinsics inside + // the body drop JIR into the calling function's gctx (Phase 4 wiring). + if (fn != nullptr && fn->isCompTimeFn) { + return astgenCompTimeFnCall(gctx, n, fn); + } + // Comp-instantiated calls: fn has at least one `comp` param. Each + // unique combination of comp args produces a fresh monomorphisation + // (cached in the function registry under a mangled name). Runtime + // args are passed through to the clone as normal Call args. + if (fn != nullptr) { + bool hasComp = false; + for (const auto &p : fn->Args) { + if (p.isComp) { hasComp = true; break; } + } + if (hasComp) { + return astgenCompInstantiatedCall(gctx, n, fn); + } + } if (fn == nullptr) { // Before erroring, try the fn-pointer-in-local-or-field paths. // Two cases, both producing a Fn-typed JirRef we can call diff --git a/src/codegen.cpp b/src/codegen.cpp index a140135..ea64851 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -182,6 +182,14 @@ JamTypeRef JamCodegenContext::getLLVMType(TypeIdx ty) const { result = getLLVMType(aliasTarget); break; } + // 3+ segment chain through module re-exports. + if (name.find('.') != std::string::npos) { + TypeIdx chained = resolveChainedType(name); + if (chained != kNoType) { + result = getLLVMType(chained); + break; + } + } throw std::runtime_error( formatNamespaceLookupError("user-defined type", name)); } @@ -226,6 +234,16 @@ JamTypeRef JamCodegenContext::getLLVMType(TypeIdx ty) const { result = JamLLVMPointerType(getInt8Type(), 0); break; } + case TypeKind::Module: + // Module values have no runtime representation. Reaching here + // means a module-typed JIR ref leaked to LLVM lowering — the + // MemberAccess / Call paths should consume it before codegen + // sees the value. Diagnostic gives the path-string for + // debugging. + throw std::runtime_error( + "internal: module value `" + + stringPool.get(static_cast(k.a)) + + "` reached LLVM codegen (modules are compile-time only)"); } llvmTypeCache[ty] = result; return result; @@ -272,6 +290,11 @@ JamCodegenContext::lookupStruct(TypeIdx ty) const { // maps `BoxI32` to the instantiated struct's TypeIdx. TypeIdx aliasTarget = lookupTypeAlias(name); if (aliasTarget != kNoType) { return lookupStruct(aliasTarget); } + // 3+ segment chain through module re-exports: `w.lib.Point`. + if (name.find('.') != std::string::npos) { + TypeIdx chained = resolveChainedType(name); + if (chained != kNoType) return lookupStruct(chained); + } return nullptr; } @@ -321,7 +344,12 @@ JamCodegenContext::lookupUnion(TypeIdx ty) const { return nullptr; } const std::string &name = stringPool.get(static_cast(k.a)); - return getUnion(name); + if (const UnionInfo *direct = getUnion(name)) return direct; + if (name.find('.') != std::string::npos) { + TypeIdx chained = resolveChainedType(name); + if (chained != kNoType) return lookupUnion(chained); + } + return nullptr; } TypeIdx @@ -394,6 +422,10 @@ JamCodegenContext::lookupEnum(TypeIdx ty) const { // Option(i32);` maps `OptI32` to the instantiated enum's TypeIdx. TypeIdx aliasTarget = lookupTypeAlias(name); if (aliasTarget != kNoType) { return lookupEnum(aliasTarget); } + if (name.find('.') != std::string::npos) { + TypeIdx chained = resolveChainedType(name); + if (chained != kNoType) return lookupEnum(chained); + } return nullptr; } @@ -453,6 +485,77 @@ JamCodegenContext::getImportHandle(const std::string &handle) const { return (it == importHandles_.end()) ? nullptr : &it->second; } +void JamCodegenContext::registerModuleNamespace(ModuleNamespace ns) { + std::string key = ns.path; + moduleNamespaces_[std::move(key)] = std::move(ns); +} + +const JamCodegenContext::ModuleNamespace * +JamCodegenContext::getModuleNamespace(const std::string &path) const { + auto it = moduleNamespaces_.find(path); + return (it == moduleNamespaces_.end()) ? nullptr : &it->second; +} + +// Split `dotted` on `.` into segments. Shared by chained function / +// type resolution. +static std::vector splitDotted(const std::string &dotted) { + std::vector segs; + size_t start = 0; + for (size_t i = 0; i <= dotted.size(); ++i) { + if (i == dotted.size() || dotted[i] == '.') { + segs.push_back(dotted.substr(start, i - start)); + start = i + 1; + } + } + return segs; +} + +// Walk `segs[0..segs.size()-1]` through ModuleNamespace re-exports +// (`moduleAliases`), starting from the import handle named by +// `segs[0]`. Returns the leaf namespace + the trailing segment name, +// or `{nullptr, ""}` on any miss. Both `resolveChainedFunction` and +// `resolveChainedType` share this prefix walk and only diverge on how +// they look up the final segment. +struct ChainWalkResult { + const JamCodegenContext::ModuleNamespace *leaf; + std::string lastSeg; +}; +static ChainWalkResult +walkChain(const JamCodegenContext &ctx, const std::string &dotted) { + std::vector segs = splitDotted(dotted); + if (segs.size() < 3) return {nullptr, {}}; + const auto *handle = ctx.getImportHandle(segs.front()); + if (!handle) return {nullptr, {}}; + const auto *ns = ctx.getModuleNamespace(handle->modulePath); + if (!ns) return {nullptr, {}}; + for (size_t i = 1; i + 1 < segs.size(); ++i) { + auto it = ns->moduleAliases.find(segs[i]); + if (it == ns->moduleAliases.end()) return {nullptr, {}}; + const TypeKey &k = ctx.getTypePool().get(it->second); + if (k.kind != TypeKind::Module) return {nullptr, {}}; + const std::string &nextPath = + ctx.getStringPool().get(static_cast(k.a)); + ns = ctx.getModuleNamespace(nextPath); + if (!ns) return {nullptr, {}}; + } + return {ns, segs.back()}; +} + +const FunctionAST * +JamCodegenContext::resolveChainedFunction(const std::string &dotted) const { + auto r = walkChain(*this, dotted); + if (!r.leaf) return nullptr; + auto fit = r.leaf->functions.find(r.lastSeg); + return (fit == r.leaf->functions.end()) ? nullptr : fit->second; +} + +TypeIdx JamCodegenContext::resolveChainedType(const std::string &dotted) const { + auto r = walkChain(*this, dotted); + if (!r.leaf) return kNoType; + auto tit = r.leaf->types.find(r.lastSeg); + return (tit == r.leaf->types.end()) ? kNoType : tit->second; +} + std::string JamCodegenContext::formatNamespaceLookupError( const std::string &kind, const std::string &qualified) const { size_t dotPos = qualified.find('.'); @@ -608,6 +711,9 @@ uint64_t JamCodegenContext::typeSize(TypeIdx ty) const { case TypeKind::Fn: // Function value = code pointer = pointer width. return 8; + case TypeKind::Module: + // Modules are compile-time-only values. + return 0; } throw std::runtime_error("typeSize: unhandled type kind"); } @@ -698,6 +804,8 @@ uint64_t JamCodegenContext::typeAlign(TypeIdx ty) const { case TypeKind::Fn: // Function pointer alignment. return 8; + case TypeKind::Module: + return 1; } throw std::runtime_error("typeAlign: unhandled type kind"); } diff --git a/src/codegen.h b/src/codegen.h index 33a7253..2db9898 100644 --- a/src/codegen.h +++ b/src/codegen.h @@ -9,6 +9,7 @@ #define CODEGEN_H #include "ast_flat.h" +#include "comptime.h" #include "decl.h" #include "diagnostics.h" #include "drop_registry.h" @@ -282,9 +283,55 @@ class JamCodegenContext { std::string formatNamespaceLookupError(const std::string &kind, const std::string &qualified) const; + // Per-loaded-module namespace. Indexes the module's `pub` members + // by source-level name so that member access on a Module value + // (e.g. `std.fmt`) can resolve to a concrete FunctionAST, TypeIdx, + // or another Module value (re-exports). + // + // Populated when a module is resolved (Phase 2+). Distinct from + // `importHandles_`, which keys by the *binding-site* handle name + // (e.g. `fmt` from `const fmt = import("fmt");`); ModuleNamespace + // keys by the *resolved canonical path* (e.g. "fmt", "std/fmt") so + // re-exports and aliases all converge on one entry per file. + struct ModuleNamespace { + // Canonical resolved path (e.g. "fmt", "std/fmt"). Same string + // stored in TypeKind::Module's `a` field. + std::string path; + // `pub fn`, `pub cfn`, `pub tfn` by source-level name. + std::unordered_map functions; + // `pub struct`, `pub enum`, `pub union`, `pub const Foo = T` + // by source-level name. TypeIdx points into the global TypePool. + std::unordered_map types; + // `pub const X = import(...)`-style re-exports: name → the + // Module TypeIdx for the re-exported module. + std::unordered_map moduleAliases; + }; + void registerModuleNamespace(ModuleNamespace ns); + const ModuleNamespace *getModuleNamespace(const std::string &path) const; + + // Walk a dotted-name callee through the chained module-namespace + // graph. Used as a fallback when the flat + // `getFunctionAST("handle.X")` lookup misses on a 3+ segment path + // like `std.fmt.print`. Returns nullptr if any segment fails to + // resolve to a Module → Module → ... → Function chain. + const FunctionAST * + resolveChainedFunction(const std::string &dotted) const; + + // Sibling of `resolveChainedFunction` for types. A type annotation + // or struct literal like `w.lib.Point` parses into a Named type + // keyed on the literal string `"w.lib.Point"`. Single-hop + // `handle.X` is registered as a flat alias in main.cpp; for 3+ + // segment chains we walk `moduleAliases` per segment and look up + // the final segment in the leaf module's `types` map. Returns + // `kNoType` on any failure (caller falls through to the standard + // "user-defined type" error). + TypeIdx resolveChainedType(const std::string &dotted) const; + private: std::unordered_map functionAsts; std::unordered_map importHandles_; + // Resolved canonical path → namespace decl table. See ModuleNamespace. + std::unordered_map moduleNamespaces_; // `genericResolutions_` memoizes per-callsite: every unique // `TypeKind::GenericCall` TypeIdx maps to the resolved TypeIdx. @@ -355,6 +402,10 @@ class JamCodegenContext { // __anon_struct_N) consult this map first. Set/cleared around // jirDeclarePrototype + jirDefineBody calls in instantiateStructExpr. mutable std::unordered_map currentSubst_; + // Parallel comp-value substitution context. See + // setCurrentCompSubst. + mutable std::unordered_map + currentCompSubst_; public: // resolve a `TypeKind::GenericCall` TypeIdx to a concrete @@ -399,6 +450,37 @@ class JamCodegenContext { return kNoType; } + // Comp-value substitution context. Parallel to currentSubst_ but + // carries ComptimeValue (Int/Bool/Str/Type/Aggregate) instead of + // just TypeIdx. Active during codegen of a fn body whose comp + // params have been bound (e.g. `fn f(comp n: u32)` called with + // n=7 — astgen reads the value when lowering Variable references + // to `n` and emits the corresponding constant). Set/cleared by + // the comp-fn instantiation path. + void setCurrentCompSubst( + std::unordered_map s) const { + currentCompSubst_ = std::move(s); + } + void clearCurrentCompSubst() const { currentCompSubst_.clear(); } + const jam::ComptimeValue * + lookupCurrentCompSubst(const std::string &name) const { + auto it = currentCompSubst_.find(name); + if (it != currentCompSubst_.end()) return &it->second; + return nullptr; + } + + // Take ownership of a cloned FunctionAST (used by + // comp-instantiation / monomorphisation paths). The clone lives + // for the rest of the compilation so its LLVM symbol stays + // referenceable. Caller is expected to also `registerFunctionAST` + // the clone so name-based lookups find it. + FunctionAST *adoptInstantiatedFunction( + std::unique_ptr cloned) const { + FunctionAST *p = cloned.get(); + instantiatedMethods_.push_back(std::move(cloned)); + return p; + } + private: // instantiate a `struct {...}` expression as the result // of a generic call. Substitutes each field's type with the diff --git a/src/comptime.cpp b/src/comptime.cpp index b589160..96bdcbe 100644 --- a/src/comptime.cpp +++ b/src/comptime.cpp @@ -93,6 +93,23 @@ void ComptimeScope::bind(const std::string &name, ComptimeValue value) { bindings_[name] = std::move(value); } +bool ComptimeScope::set(const std::string &name, ComptimeValue value) { + // Walk up the parent chain to find the scope where `name` was + // originally bound. Mutate there. Returns false if `name` isn't + // bound anywhere in the chain — caller decides how to surface + // (typically a diagnostic). + ComptimeScope *cur = this; + while (cur != nullptr) { + auto it = cur->bindings_.find(name); + if (it != cur->bindings_.end()) { + it->second = std::move(value); + return true; + } + cur = cur->parent_; + } + return false; +} + const ComptimeValue *ComptimeScope::lookup(const std::string &name) const { auto it = bindings_.find(name); if (it != bindings_.end()) return &it->second; @@ -126,6 +143,10 @@ ComptimeValue ComptimeEvaluator::eval(NodeIdx expr, return evalBinaryOp(n, scope); case AstTag::Index: return evalIndex(n, scope); + case AstTag::MemberAccess: + return evalMemberAccess(n, scope); + case AstTag::AtCall: + return evalAtCall(n, scope); default: // Operator / construct we don't fold yet. Returning None keeps // optional-fold callers (peephole constant folding) silent; @@ -134,6 +155,55 @@ ComptimeValue ComptimeEvaluator::eval(NodeIdx expr, } } +ComptimeValue +ComptimeEvaluator::evalAtCall(const AstNode &n, + const ComptimeScope &scope) const { + // Type-arg single form (`@sizeOf(T)` / `@alignOf(T)`) doesn't + // belong here — those produce a value the regular astgen path + // turns into a JIR Int. From inside a cfn body, those still + // return None (caller can do its own dispatch if it wants). + if ((n.flags & 1) == 0) { + return ComptimeValue::makeNone(); + } + + // Expr-arg multi-form: rhs = ExtraIdx → [argCount, arg0, ...]. + ExtraIdx extra = static_cast(n.rhs); + uint32_t argCount = nodes_.getExtra(extra); + std::vector argVals; + argVals.reserve(argCount); + for (uint32_t i = 0; i < argCount; i++) { + NodeIdx argIdx = static_cast(nodes_.getExtra(extra + 1 + i)); + ComptimeValue v = eval(argIdx, scope); + if (v.isNone()) { + if (diags_ != nullptr) { + diags_->error(loc_, + "@-emit argument must be a compile-time " + "constant (arg #" + + std::to_string(i) + ")"); + } + return ComptimeValue::makeNone(); + } + argVals.push_back(std::move(v)); + } + + if (emitter_ == nullptr) { + // No emitter installed — running outside a cfn dispatcher + // context. Silently return None; the caller (test harness, + // peephole folder) decides what to do. + return ComptimeValue::makeNone(); + } + + const std::string &name = strings_.get(static_cast(n.lhs)); + Diagnostics dummyDiags; + Diagnostics &diagsRef = diags_ != nullptr ? *diags_ : dummyDiags; + ExecResult r = emitter_->handleAtCall(name, argVals, diagsRef, loc_); + (void)r; // emit-style intrinsics return Continue on success; + // errors surface via pushed diagnostics + the + // caller's `Diagnostics::hasErrors()` post-check. + // @-emit calls produce side effects, not values. + return ComptimeValue::makeNone(); +} + ComptimeValue ComptimeEvaluator::evalRequired(NodeIdx expr, const ComptimeScope &scope, Diagnostics &diags, @@ -241,13 +311,32 @@ ComptimeEvaluator::evalBinaryOp(const AstNode &n, 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. + // Integer arithmetic + bitwise + comparison. Strict width matching + // for arithmetic / bitwise (catches accidental mixed-width math); + // comparisons coerce mismatched-width int literals so user code + // like `fmt[i] == 123` (u8 vs default-u64 literal) works without + // explicit width casts. Coercion direction: widen the narrower + // operand to the wider one; require matching signedness, otherwise + // give up and return None. if (l.kind == ComptimeValue::Kind::Int && r.kind == ComptimeValue::Kind::Int) { - if (l.intVal.width != r.intVal.width || - l.intVal.isSigned != r.intVal.isSigned) { + bool isComparison = op == BinOp::Eq || op == BinOp::Ne || + op == BinOp::Lt || op == BinOp::Le || + op == BinOp::Gt || op == BinOp::Ge; + if (l.intVal.width != r.intVal.width) { + if (!isComparison || l.intVal.isSigned != r.intVal.isSigned) { + return ComptimeValue::makeNone(); + } + // Widen the narrower operand. Bit-pattern preserved + // because both sides are unsigned-or-signed alike at this + // point. + if (l.intVal.width < r.intVal.width) { + l.intVal.width = r.intVal.width; + } else { + r.intVal.width = l.intVal.width; + } + } + if (l.intVal.isSigned != r.intVal.isSigned) { return ComptimeValue::makeNone(); } uint16_t w = l.intVal.width; @@ -354,6 +443,194 @@ ComptimeEvaluator::evalBinaryOp(const AstNode &n, return ComptimeValue::makeNone(); } +ComptimeValue +ComptimeEvaluator::evalMemberAccess(const AstNode &n, + const ComptimeScope &scope) const { + // Member access on a comp value. v1 supports `.length` on a comp + // str (returns u32 byte count). Future: struct field access on a + // comp-known aggregate. + NodeIdx baseIdx = static_cast(n.lhs); + StringIdx memberId = static_cast(n.rhs); + const std::string &member = strings_.get(memberId); + ComptimeValue base = eval(baseIdx, scope); + if (base.isNone()) return base; + if (base.kind == ComptimeValue::Kind::Str && member == "length") { + const std::string &s = strings_.get(base.strVal); + return ComptimeValue::makeInt(static_cast(s.length()), 64, + /*isSigned=*/false); + } + return ComptimeValue::makeNone(); +} + +ExecResult ComptimeEvaluator::execStmt(NodeIdx stmt, ComptimeScope &scope, + uint32_t &iterCounter, + uint32_t iterCap, + ComptimeValue &outReturnValue, + Diagnostics &diags, + SrcLoc loc) const { + if (stmt == kNoNode) return ExecResult::Continue; + const AstNode &n = nodes_.get(stmt); + + switch (n.tag) { + case AstTag::VarDecl: { + // extra: [name StringIdx, type TypeIdx, init NodeIdx] + ExtraIdx extra = static_cast(n.lhs); + StringIdx nameId = static_cast(nodes_.getExtra(extra)); + // type slot at extra+1 — ignored at comp time; the comp value + // carries its own width/signedness. Width-checking against the + // declared type can come later. + NodeIdx initIdx = static_cast(nodes_.getExtra(extra + 2)); + ComptimeValue v = eval(initIdx, scope); + if (v.isNone()) { + diags.error(loc, + "comp var-decl initializer must be a " + "compile-time-known value"); + return ExecResult::Error; + } + scope.bind(strings_.get(nameId), std::move(v)); + return ExecResult::Continue; + } + + case AstTag::Assign: { + NodeIdx targetIdx = static_cast(n.lhs); + NodeIdx valueIdx = static_cast(n.rhs); + const AstNode &target = nodes_.get(targetIdx); + if (target.tag != AstTag::Variable) { + diags.error(loc, + "comp assignment target must be a bare " + "variable (member-access / index targets are " + "not supported in v1)"); + return ExecResult::Error; + } + StringIdx nameId = static_cast(target.lhs); + const std::string &name = strings_.get(nameId); + ComptimeValue v = eval(valueIdx, scope); + if (v.isNone()) { + diags.error(loc, + "comp assignment value must be a compile-" + "time-known value"); + return ExecResult::Error; + } + if (!scope.set(name, std::move(v))) { + diags.error(loc, + "assignment to undeclared variable `" + name + + "` (declare with `var` first)"); + return ExecResult::Error; + } + return ExecResult::Continue; + } + + case AstTag::IfNode: { + NodeIdx condIdx = static_cast(n.lhs); + ExtraIdx extra = static_cast(n.rhs); + uint32_t thenCount = nodes_.getExtra(extra); + uint32_t elseCount = nodes_.getExtra(extra + 1); + ComptimeValue c = eval(condIdx, scope); + if (c.kind != ComptimeValue::Kind::Bool) { + diags.error(loc, "comp `if` condition must fold to bool"); + return ExecResult::Error; + } + // Run the chosen arm in a nested scope so its locals don't + // leak to the outer block. + ComptimeScope inner(&scope); + if (c.boolVal) { + std::vector stmts; + stmts.reserve(thenCount); + for (uint32_t i = 0; i < thenCount; i++) { + stmts.push_back( + static_cast(nodes_.getExtra(extra + 2 + i))); + } + return execBlock(stmts.data(), stmts.size(), inner, + iterCounter, iterCap, outReturnValue, diags, + loc); + } + std::vector stmts; + stmts.reserve(elseCount); + for (uint32_t i = 0; i < elseCount; i++) { + stmts.push_back(static_cast( + nodes_.getExtra(extra + 2 + thenCount + i))); + } + return execBlock(stmts.data(), stmts.size(), inner, iterCounter, + iterCap, outReturnValue, diags, loc); + } + + case AstTag::WhileNode: { + NodeIdx condIdx = static_cast(n.lhs); + ExtraIdx extra = static_cast(n.rhs); + uint32_t bodyCount = nodes_.getExtra(extra); + std::vector body; + body.reserve(bodyCount); + for (uint32_t i = 0; i < bodyCount; i++) { + body.push_back( + static_cast(nodes_.getExtra(extra + 1 + i))); + } + while (true) { + ComptimeValue c = eval(condIdx, scope); + if (c.kind != ComptimeValue::Kind::Bool) { + diags.error(loc, + "comp `while` condition must fold to bool"); + return ExecResult::Error; + } + if (!c.boolVal) break; + if (++iterCounter > iterCap) { + diags.error(loc, + "comp evaluation iteration cap (" + + std::to_string(iterCap) + + ") exceeded — possible infinite loop"); + return ExecResult::IterationCap; + } + ComptimeScope iter(&scope); + ExecResult r = execBlock(body.data(), body.size(), iter, + iterCounter, iterCap, outReturnValue, + diags, loc); + if (r != ExecResult::Continue) return r; + } + return ExecResult::Continue; + } + + case AstTag::Return: { + NodeIdx valIdx = static_cast(n.lhs); + if (valIdx == kNoNode) { + outReturnValue = ComptimeValue::makeNone(); + } else { + outReturnValue = eval(valIdx, scope); + if (outReturnValue.isNone()) { + diags.error(loc, + "comp `return` expression must fold to a " + "value"); + return ExecResult::Error; + } + } + return ExecResult::Returned; + } + + default: + // Expression statement (e.g. an @-emit intrinsic call once + // Phase 4 lands). v1 evaluator just evaluates and discards; + // when @emit intrinsics arrive they'll be dispatched via a + // caller-context hook before reaching this default branch. + (void)eval(stmt, scope); + return ExecResult::Continue; + } +} + +ExecResult ComptimeEvaluator::execBlock(const NodeIdx *stmts, + std::size_t count, + ComptimeScope &scope, + uint32_t &iterCounter, + uint32_t iterCap, + ComptimeValue &outReturnValue, + Diagnostics &diags, + SrcLoc loc) const { + for (std::size_t i = 0; i < count; i++) { + ExecResult r = + execStmt(stmts[i], scope, iterCounter, iterCap, outReturnValue, + diags, loc); + if (r != ExecResult::Continue) return r; + } + return ExecResult::Continue; +} + ComptimeValue ComptimeEvaluator::evalIndex(const AstNode &n, const ComptimeScope &scope) const { diff --git a/src/comptime.h b/src/comptime.h index 64f04ad..eecbc30 100644 --- a/src/comptime.h +++ b/src/comptime.h @@ -90,24 +90,67 @@ struct ComptimeValue { }; // 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. +// from it when it encounters a Variable node, and the executor mutates +// it on var-decl / assignment statements. +// +// Mutation model: +// * bind(name, value) — declare a new local in THIS scope (var decl). +// Always succeeds; shadows any same-named binding in a parent +// scope. +// * set(name, value) — walk the parent chain, mutate the scope where +// `name` was first bound. Returns false if not found anywhere. +// Used by assignment statements. +// * lookup(name) — read; walks the parent chain. +// +// `parent_` is mutable (non-const) so set() can write through to outer +// scopes. The contract is that callers pass scopes that they own. class ComptimeScope { public: ComptimeScope() = default; - explicit ComptimeScope(const ComptimeScope *parent) : parent_(parent) {} + explicit ComptimeScope(ComptimeScope *parent) : parent_(parent) {} void bind(const std::string &name, ComptimeValue value); + // Walk up looking for `name`. If found, replace its value and + // return true. If not found, return false (caller decides whether + // to surface a diagnostic or implicitly bind in current scope). + bool set(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; + ComptimeScope *parent_ = nullptr; std::unordered_map bindings_; }; +// Outcome of executing a statement (or a block). The evaluator threads +// this through nested control-flow so a `return` inside `while { if { ... } }` +// propagates correctly without throwing. +enum class ExecResult : uint8_t { + Continue, // statement executed normally, keep going + Returned, // a `return` was hit; caller propagates upward + IterationCap, // a while loop or recursive call exceeded the iteration cap + Error, // an unrecoverable failure (diagnostic already pushed) +}; + +// Sink for `@`-emit intrinsics inside a cfn body. The evaluator hits an +// AtCall node while interpreting cfn body code; it evaluates each arg +// to a ComptimeValue and forwards to `handleAtCall`. The astgen-side +// implementation has access to the caller's gctx and uses it to emit +// JIR instructions that lower the cfn's compile-time work into runtime +// code at the call site. +// +// Returning Error from handleAtCall aborts cfn execution and propagates +// up through the evaluator's ExecResult. +class CompEmitter { + public: + virtual ~CompEmitter() = default; + virtual ExecResult handleAtCall(const std::string &name, + const std::vector &args, + Diagnostics &diags, SrcLoc loc) = 0; +}; + // 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 @@ -133,10 +176,62 @@ class ComptimeEvaluator { ComptimeValue evalRequired(NodeIdx expr, const ComptimeScope &scope, Diagnostics &diags, SrcLoc loc) const; + // Execute a statement node. The scope is mutated for var-decls and + // assignments; control-flow statements (if/while) recurse. The + // returned ExecResult tells the caller whether to continue, stop + // (return), or bail (iteration cap / error). `iterCounter` is + // incremented on every loop iteration and recursive cfn call; the + // caller seeds it at 0 and checks it never exceeds `iterCap`. If + // the cap is hit a diagnostic is pushed and IterationCap is + // returned all the way out. + // + // `outReturnValue` receives the value of a `return EXPR;` if one + // fires; otherwise it's left None. + static constexpr uint32_t kDefaultIterCap = 10'000; + ExecResult execStmt(NodeIdx stmt, ComptimeScope &scope, + uint32_t &iterCounter, uint32_t iterCap, + ComptimeValue &outReturnValue, Diagnostics &diags, + SrcLoc loc) const; + + // Execute a sequence of statements in order. Returns the first + // non-Continue result. Wraps a list of NodeIdx from an Extra slice + // like a fn body or if/while body. + ExecResult execBlock(const NodeIdx *stmts, std::size_t count, + ComptimeScope &scope, uint32_t &iterCounter, + uint32_t iterCap, ComptimeValue &outReturnValue, + Diagnostics &diags, SrcLoc loc) const; + + // Install / clear the AtCall emitter + the diagnostic context. + // Active for the duration of a cfn body's execution; cleared + // right after. When emitter is set, AtCall nodes (expr-arg multi- + // form) are forwarded to the emitter. The diags+loc are used by + // the emitter and by `evalAtCall` to push error diagnostics. + void setCallContext(CompEmitter *e, Diagnostics *d, SrcLoc loc) const { + emitter_ = e; + diags_ = d; + loc_ = std::move(loc); + } + void clearCallContext() const { + emitter_ = nullptr; + diags_ = nullptr; + loc_ = {}; + } + private: const NodeStore &nodes_; const StringPool &strings_; const TypePool &types_; + mutable CompEmitter *emitter_ = nullptr; + mutable Diagnostics *diags_ = nullptr; + mutable SrcLoc loc_; + + // Eval an `AtCall` node from inside a cfn body. Type-arg single- + // form (`@sizeOf(T)`) returns None here (caller handles via the + // regular AtCall lowering). Expr-arg multi-form (`@emit*(...)`) + // is forwarded to the installed emitter; returns None as well + // (the @-emit family produces side effects, not values). + ComptimeValue evalAtCall(const AstNode &n, + const ComptimeScope &scope) const; // Per-tag handlers. Each returns None on failure; callers compose. ComptimeValue evalNumberLit(const AstNode &n) const; @@ -150,6 +245,8 @@ class ComptimeEvaluator { const ComptimeScope &scope) const; ComptimeValue evalIndex(const AstNode &n, const ComptimeScope &scope) const; + ComptimeValue evalMemberAccess(const AstNode &n, + const ComptimeScope &scope) const; }; } // namespace jam diff --git a/src/lexer.cpp b/src/lexer.cpp index 2f8f159..b95a510 100644 --- a/src/lexer.cpp +++ b/src/lexer.cpp @@ -150,6 +150,10 @@ void Lexer::identifier() { addToken(TOK_AS); } else if (text == "move") { addToken(TOK_MOVE); + } else if (text == "comp") { + addToken(TOK_COMP); + } else if (text == "inline") { + addToken(TOK_INLINE); } else if (text == "u1" || text == "u8" || text == "u16" || text == "u32" || text == "u64" || text == "i8" || text == "i16" || text == "i32" || text == "i64" || text == "f32" || diff --git a/src/main.cpp b/src/main.cpp index 7d8a80d..314dc92 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -122,27 +122,65 @@ static int compileAndRun(const std::string &filename, resolver.setSharedAnonRegistries(&sharedAnonStructs, &sharedAnonEnums); SymbolTable symbolTable; + // Walk a `import("base").seg.seg` chain through pub-import re- + // exports to the final target module. Each segment names a + // `pub const X = import(...)` alias in the preceding module; we + // recurse so a re-export that itself walks a chain composes. + auto resolveImportChain = + [&](const std::string &basePath, + const std::vector &chain, + auto &self) -> std::pair { + std::string curPath = basePath; + ModuleAST *curMod = resolver.getOrLoadModule(curPath); + if (!curMod) return {curPath, nullptr}; + for (const auto &seg : chain) { + const ImportDeclAST *re = nullptr; + for (auto &imp : curMod->Imports) { + if (imp->isPub && imp->Name == seg) { + re = imp.get(); + break; + } + } + if (!re) return {curPath + "." + seg, nullptr}; + auto sub = self(re->Path, re->chain, self); + if (!sub.second) return sub; + curPath = sub.first; + curMod = sub.second; + } + return {curPath, curMod}; + }; + symbolTable.registerBuiltinSymbol("test", "assert"); for (auto &import : module->Imports) { - if (import->Path == "std" || import->Path == "test") { continue; } + if (import->Path == "test") { continue; } - ModuleAST *importedModule = resolver.getOrLoadModule(import->Path); + auto resolved = + resolveImportChain(import->Path, import->chain, resolveImportChain); + ModuleAST *importedModule = resolved.second; if (!importedModule) { - std::cerr << "Error: Failed to load module: " << import->Path + std::cerr << "Error: Failed to load module: " << resolved.first << std::endl; return 1; } + // Rewrite Path to the chain-resolved canonical so every later + // pass (handle registration, namespace build) sees one path. + import->Path = resolved.first; + import->chain.clear(); symbolTable.registerModule(import->Path, importedModule); } for (auto &destImport : module->DestructuringImports) { - ModuleAST *importedModule = resolver.getOrLoadModule(destImport->Path); + auto resolved = resolveImportChain(destImport->Path, destImport->chain, + resolveImportChain); + ModuleAST *importedModule = resolved.second; if (!importedModule) { - std::cerr << "Error: Failed to load module: " << destImport->Path + std::cerr << "Error: Failed to load module: " << resolved.first << std::endl; return 1; } + destImport->Path = resolved.first; + destImport->chain.clear(); symbolTable.registerModule(destImport->Path, importedModule); for (const auto &name : destImport->Names) { @@ -218,7 +256,6 @@ static int compileAndRun(const std::string &filename, } }; for (const auto &[path, importedModule] : resolver.getLoadedModules()) { - if (path == "std") continue; registerTopLevelDecls(importedModule.get(), /*publicOnly=*/true); } registerTopLevelDecls(module.get(), /*publicOnly=*/false); @@ -271,7 +308,6 @@ static int compileAndRun(const std::string &filename, } }; for (const auto &[path, importedModule] : resolver.getLoadedModules()) { - if (path == "std") continue; declareStructs(importedModule.get(), /*publicOnly=*/true); declareUnions(importedModule.get(), /*publicOnly=*/true); declareEnums(importedModule.get(), /*publicOnly=*/true); @@ -291,7 +327,11 @@ static int compileAndRun(const std::string &filename, // module (e.g. `malloc`), so the externs need prototypes // declared by the time the instantiation runs. for (const auto &[path, importedModule] : resolver.getLoadedModules()) { - if (path == "std") continue; + // Per-loaded-module namespace, keyed by the canonical import + // path (e.g. "fmt", "std/fmt"). This is the table member-access + // on a Module value will consult later. + JamCodegenContext::ModuleNamespace ns; + ns.path = path; for (auto &func : importedModule->Functions) { if (func->isPub && !func->isGeneric()) { JirFunction jfn = astgenMetadata(*func, codegenCtx); @@ -301,56 +341,109 @@ static int compileAndRun(const std::string &filename, } if (func->isPub) { codegenCtx.registerFunctionAST(func->Name, func.get()); - } - } - } - for (auto &import : module->Imports) { - if (import->Path == "std" || import->Path == "test") continue; - const std::string &handle = import->Name; - ModuleAST *importedModule = resolver.getOrLoadModule(import->Path); - if (!importedModule) continue; - codegenCtx.registerImportHandle(handle, import->Path); - auto aliasNamed = [&](const std::string &bare) { - TypeIdx target = codegenCtx.getTypePool().internNamed( - codegenCtx.getStringPool().intern(bare)); - codegenCtx.registerTypeAlias(handle + "." + bare, target); - }; - for (auto &func : importedModule->Functions) { - if (func->isPub) { - codegenCtx.registerFunctionAST(handle + "." + func->Name, - func.get()); - } else { - codegenCtx.registerPrivateName(handle, func->Name); + ns.functions[func->Name] = func.get(); } } for (auto &s : importedModule->Structs) { 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); + ns.types[s->Name] = codegenCtx.getTypePool().internNamed( + codegenCtx.getStringPool().intern(s->Name)); } } for (auto &e : importedModule->Enums) { - if (e->isPub) aliasNamed(e->Name); - else codegenCtx.registerPrivateName(handle, e->Name); + if (e->isPub) { + ns.types[e->Name] = codegenCtx.getTypePool().internNamed( + codegenCtx.getStringPool().intern(e->Name)); + } } for (auto &u : importedModule->Unions) { - if (u->isPub) aliasNamed(u->Name); - else codegenCtx.registerPrivateName(handle, u->Name); + if (u->isPub) { + ns.types[u->Name] = codegenCtx.getTypePool().internNamed( + codegenCtx.getStringPool().intern(u->Name)); + } + } + // `pub const X = import(...)` re-exports — surface the inner + // module as a Module-typed alias on this module's namespace. + for (auto &reexport : importedModule->Imports) { + if (!reexport->isPub) continue; + if (reexport->Path == "test") continue; + TypeIdx modTy = codegenCtx.getTypePool().internModule( + codegenCtx.getStringPool().intern(reexport->Path)); + ns.moduleAliases[reexport->Name] = modTy; + } + codegenCtx.registerModuleNamespace(std::move(ns)); + } + // Register every flat `handle.X` mapping for a given (handle name, + // resolved module). Shared by direct-import bindings and module- + // valued destructuring bindings (`const {fmt} = import("std");`). + auto registerHandleFlats = + [&](const std::string &handle, const std::string &modulePath, + ModuleAST *importedModule) { + codegenCtx.registerImportHandle(handle, modulePath); + auto aliasNamed = [&](const std::string &bare) { + TypeIdx target = codegenCtx.getTypePool().internNamed( + codegenCtx.getStringPool().intern(bare)); + codegenCtx.registerTypeAlias(handle + "." + bare, target); + }; + for (auto &func : importedModule->Functions) { + if (func->isPub) { + codegenCtx.registerFunctionAST(handle + "." + func->Name, + func.get()); + } else { + codegenCtx.registerPrivateName(handle, func->Name); + } + } + for (auto &s : importedModule->Structs) { + if (s->isPub) { + aliasNamed(s->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); + else codegenCtx.registerPrivateName(handle, e->Name); + } + for (auto &u : importedModule->Unions) { + if (u->isPub) aliasNamed(u->Name); + else codegenCtx.registerPrivateName(handle, u->Name); + } + }; + + for (auto &import : module->Imports) { + if (import->Path == "test") continue; + ModuleAST *importedModule = resolver.getOrLoadModule(import->Path); + if (!importedModule) continue; + registerHandleFlats(import->Name, import->Path, importedModule); + } + + // Destructured names that bind a re-exported module value — treat + // them as if the user had written `const X = import("...").X;` so + // `X.member` resolves through the standard handle-flat tables. + for (auto &destImport : module->DestructuringImports) { + if (destImport->Path == "test") continue; + ModuleAST *src = resolver.getOrLoadModule(destImport->Path); + if (!src) continue; + for (const auto &name : destImport->Names) { + const ImportDeclAST *re = nullptr; + for (auto &imp : src->Imports) { + if (imp->isPub && imp->Name == name) { + re = imp.get(); + break; + } + } + if (!re) continue; + auto resolved = + resolveImportChain(re->Path, re->chain, resolveImportChain); + if (!resolved.second) continue; + registerHandleFlats(name, resolved.first, resolved.second); } } @@ -395,7 +488,6 @@ static int compileAndRun(const std::string &filename, if (fn->Name == name) return fn.get(); } for (const auto &kv : resolver.getLoadedModules()) { - if (kv.first == "std") continue; for (auto &fn : kv.second->Functions) { if (fn->isPub && fn->Name == name) return fn.get(); } @@ -487,7 +579,6 @@ static int compileAndRun(const std::string &filename, } }; for (const auto &[path, importedModule] : resolver.getLoadedModules()) { - if (path == "std") continue; registerConsts(importedModule.get()); } registerConsts(module.get()); @@ -614,7 +705,6 @@ static int compileAndRun(const std::string &filename, return 0; }; for (const auto &[path, importedModule] : resolver.getLoadedModules()) { - if (path == "std") continue; int rc = registerStructMethods(importedModule.get(), /*publicOnly=*/true); if (rc != 0) return rc; @@ -675,7 +765,6 @@ static int compileAndRun(const std::string &filename, } } for (const auto &[path, importedModule] : resolver.getLoadedModules()) { - if (path == "std") continue; for (auto &func : importedModule->Functions) { if (func->isPub && !func->isGeneric()) { try { @@ -761,7 +850,6 @@ static int compileAndRun(const std::string &filename, } } for (const auto &kv : resolver.getLoadedModules()) { - if (kv.first == "std") continue; for (auto &fn : kv.second->Functions) { if (fn->isPub) { fnRegistry[fn->Name] = fn.get(); } } diff --git a/src/module_resolver.cpp b/src/module_resolver.cpp index 0984027..19cd1be 100644 --- a/src/module_resolver.cpp +++ b/src/module_resolver.cpp @@ -101,7 +101,10 @@ ModuleResolver::ModuleResolver(const std::string &baseDir, TypePool &typePool_, nodeStore(&nodeStore_) {} std::string ModuleResolver::resolve(const std::string &importPath) const { - if (importPath == "std" || importPath == "test") { return importPath; } + // `test` stays a compiler-builtin namespace (provides `assert`). + // `std` used to short-circuit too, but now resolves to a real + // `std/std.jam` file that re-exports the standard-library modules. + if (importPath == "test") { return importPath; } std::string path = importPath; if (path.substr(0, 2) == "./") { path = path.substr(2); } @@ -180,7 +183,7 @@ ModuleAST *ModuleResolver::getOrLoadModule(const std::string &importPath) { return nullptr; } - if (resolvedPath == "std" || resolvedPath == "test") { + if (resolvedPath == "test") { auto builtinModule = std::make_unique(); loadedModules[importPath] = std::move(builtinModule); return loadedModules[importPath].get(); @@ -210,13 +213,13 @@ ModuleAST *ModuleResolver::getOrLoadModule(const std::string &importPath) { // every resolved module ends up in the shared `loadedModules` map // and gets its `pub` symbols registered by main.cpp. auto loadNested = [&](const std::string &importPath) { - if (importPath == "std") return; + if (importPath == "test") return; fs::path modulePath(resolvedPath); std::string moduleDir = modulePath.parent_path().string(); ModuleResolver nestedResolver(moduleDir, *typePool, *stringPool, *nodeStore); std::string nestedResolved = nestedResolver.resolve(importPath); - if (!nestedResolved.empty() && nestedResolved != "std") { + if (!nestedResolved.empty() && nestedResolved != "test") { getOrLoadModule(importPath); } }; diff --git a/src/parser.cpp b/src/parser.cpp index 9aaacd4..3484583 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -176,16 +176,41 @@ NodeIdx Parser::parsePrimary() { // expression forms; the codegen builds a phi over arm values. if (check(TOK_MATCH)) { return parseMatch(); } - // `@name(arg)` — comptime intrinsic invocation. Resolved to a - // constant at codegen time; LLVM never sees a call. Stage 1 only - // supports compiler-supplied intrinsics that take a single TYPE - // argument (sizeOf, alignOf); the type is parsed via parseType() - // and stored as a TypeIdx in the rhs slot. User-defined cfn bodies - // + arbitrary value args arrive in Stage 2 with CTFE. + // `@name(arg, ...)` — compiler intrinsic invocation. Two encoding + // shapes: + // * Type-arg form (`@sizeOf(T)`, `@alignOf(T)`): single TypeIdx + // stored in rhs. flags=0. + // * Expr-arg multi-form (`@emit*(...)` intrinsics callable from + // cfn bodies): rhs is an ExtraIdx → [argCount, arg0, ...]. + // flags bit 0 = 1. + // The intrinsic name decides which encoding the parser uses. if (match(TOK_AT)) { consume(TOK_IDENTIFIER, "Expected intrinsic name after '@'"); - StringIdx nameId = stringPool->intern(previous().text(source_)); + std::string name(previous().text(source_)); + StringIdx nameId = stringPool->intern(name); consume(TOK_OPEN_PAREN, "Expected '(' after '@name'"); + // Names starting with "emit" are the @-emit family; they take + // expression args. Everything else (sizeOf, alignOf) stays on + // the legacy type-arg path. + bool isEmitFamily = name.length() >= 4 && name.substr(0, 4) == "emit"; + if (isEmitFamily) { + std::vector args; + if (!check(TOK_CLOSE_PAREN)) { + do { + args.push_back(parseLogicalOr()); + } while (match(TOK_COMMA)); + } + consume(TOK_CLOSE_PAREN, + "Expected ')' after '@' intrinsic arguments"); + ExtraIdx extra = nodes->reserveExtra(1 + args.size()); + nodes->setExtra(extra, static_cast(args.size())); + for (std::size_t i = 0; i < args.size(); i++) { + nodes->setExtra(extra + 1 + i, args[i]); + } + AstNode node{AstTag::AtCall, 0, 0, 0, nameId, extra}; + node.flags = 1; // expr-arg multi-form + return emit(node); + } TypeIdx tyArg = parseType(); consume(TOK_CLOSE_PAREN, "Expected ')' after '@' intrinsic argument"); return emit(AstNode{AstTag::AtCall, 0, 0, 0, nameId, @@ -643,15 +668,17 @@ TypeIdx Parser::parseType() { return typePool->internNamed( stringPool->intern(structContextStack.back())); } - // Optional module qualifier: `Handle.TypeName`. main.cpp - // registers each main-module import's pub items under - // `.` so the existing struct/enum/union/type-alias - // and generic-fn lookups resolve the qualified form transparently. + // Optional module qualifier: `Handle.TypeName`, or any longer + // chain `Handle.A.B.…TypeName` when the intermediate segments + // are pub re-exports (`pub const A = import("…")`). main.cpp + // registers single-hop pub items under `.`; for + // 3+ segment chains, codegen falls back to a ModuleNamespace + // walk in `resolveChainedType` when the flat alias misses. std::string ident(firstIdent); - if (match(TOK_DOT)) { + while (match(TOK_DOT)) { consume(TOK_IDENTIFIER, "Expected type name after `.`"); - ident = std::string(firstIdent) + "." + - std::string(previous().text(source_)); + ident += "."; + ident += previous().text(source_); } if (check(TOK_OPEN_PAREN)) { advance(); // consume `(` @@ -909,6 +936,104 @@ NodeIdx Parser::parseMatch() { } NodeIdx Parser::parseExpression() { + // `comp` prefix at statement-start opts the following construct + // into comp-time evaluation: + // comp const X = ...; — comp-bound constant (rhs bit 1 = comp) + // comp var X = ...; — mutable comp binding (rhs bit 1 = comp) + // comp if (cond) { ... } else { ... } — comp-folded branch + // (IfNode flags bit 0 = comp). The + // unchosen arm never reaches astgen. + // The keyword is the source-level marker; the comp evaluator + // implementation lives in src/comptime.h. + if (match(TOK_COMP)) { + if (match(TOK_CONST) || match(TOK_VAR)) { + bool isConst = previous().type == TOK_CONST; + consume(TOK_IDENTIFIER, "Expected variable name"); + StringIdx name = stringPool->intern(previous().text(source_)); + TypeIdx type = kNoType; + if (match(TOK_COLON)) { type = parseType(); } + consume(TOK_EQUAL, + "Expected '=' (every variable must be initialized at " + "declaration)"); + NodeIdx init = parseLogicalOr(); + consume(TOK_SEMI, "Expected ';' after variable declaration"); + ExtraIdx extra = nodes->reserveExtra(3); + nodes->setExtra(extra, name); + nodes->setExtra(extra + 1, type); + nodes->setExtra(extra + 2, init); + uint32_t rhsFlags = (isConst ? 1u : 0u) | 2u; // bit 1 = comp + return emit(AstNode{AstTag::VarDecl, 0, 0, 0, extra, rhsFlags}); + } + if (match(TOK_IF)) { + consume(TOK_OPEN_PAREN, "Expected '(' after `comp if`"); + NodeIdx cond = parseLogicalOr(); + consume(TOK_CLOSE_PAREN, "Expected ')' after `comp if` condition"); + consume(TOK_OPEN_BRACE, "Expected '{' after `comp if` condition"); + std::vector thenBody; + while (!check(TOK_CLOSE_BRACE) && !isAtEnd()) { + thenBody.push_back(parseExpression()); + } + consume(TOK_CLOSE_BRACE, "Expected '}' after `comp if` body"); + std::vector elseBody; + if (match(TOK_ELSE)) { + if (check(TOK_IF) || check(TOK_COMP)) { + elseBody.push_back(parseExpression()); + } else { + consume(TOK_OPEN_BRACE, + "Expected '{' or `if` / `comp if` after 'else'"); + while (!check(TOK_CLOSE_BRACE) && !isAtEnd()) { + elseBody.push_back(parseExpression()); + } + consume(TOK_CLOSE_BRACE, + "Expected '}' after `comp if` else body"); + } + } + ExtraIdx extra = + nodes->reserveExtra(2 + thenBody.size() + elseBody.size()); + nodes->setExtra(extra, static_cast(thenBody.size())); + nodes->setExtra(extra + 1, static_cast(elseBody.size())); + for (size_t i = 0; i < thenBody.size(); i++) { + nodes->setExtra(extra + 2 + i, thenBody[i]); + } + for (size_t i = 0; i < elseBody.size(); i++) { + nodes->setExtra(extra + 2 + thenBody.size() + i, elseBody[i]); + } + AstNode ifNode{AstTag::IfNode, 0, 0, 0, + static_cast(cond), extra}; + ifNode.flags = 1; // bit 0 = comp + return emit(ifNode); + } + parseError("`comp` must be followed by `const`, `var`, or `if`"); + } + // `inline while (cond) { body }` — comp-unrolled loop. The + // analyzer evaluates `cond` each iteration via the comp evaluator + // and re-lowers the body; mutation of comp vars in the body + // updates the comp scope so the next iteration's cond sees the + // new value. Bounded iteration cap prevents bad code from hanging + // the compiler. + if (match(TOK_INLINE)) { + consume(TOK_WHILE, "Expected `while` after `inline`"); + consume(TOK_OPEN_PAREN, "Expected '(' after `inline while`"); + NodeIdx cond = parseLogicalOr(); + consume(TOK_CLOSE_PAREN, + "Expected ')' after `inline while` condition"); + consume(TOK_OPEN_BRACE, + "Expected '{' after `inline while` condition"); + std::vector body; + while (!check(TOK_CLOSE_BRACE) && !isAtEnd()) { + body.push_back(parseExpression()); + } + consume(TOK_CLOSE_BRACE, "Expected '}' after `inline while` body"); + ExtraIdx extra = nodes->reserveExtra(1 + body.size()); + nodes->setExtra(extra, static_cast(body.size())); + for (size_t i = 0; i < body.size(); i++) { + nodes->setExtra(extra + 1 + i, body[i]); + } + AstNode whileNode{AstTag::WhileNode, 0, 0, 0, + static_cast(cond), extra}; + whileNode.flags = 1; // bit 0 = inline + return emit(whileNode); + } if (match(TOK_RETURN)) { if (match(TOK_SEMI)) { return emit(AstNode{AstTag::Return, 0, 0, 0, kNoNode, 0}); @@ -1087,7 +1212,16 @@ NodeIdx Parser::parseExpression() { return expr; } - return parseLogicalOr(); + // Bare expression-statement starting with something other than an + // identifier (notably `@emit*(...)` / `@sizeOf(T)` etc. in + // statement position). parseLogicalOr handles the expression; we + // close with `;` if the result is an AtCall (common shape for + // cfn-body side-effect calls). + NodeIdx expr = parseLogicalOr(); + if (nodes->get(expr).tag == AstTag::AtCall) { + consume(TOK_SEMI, "Expected ';' after `@`-call statement"); + } + return expr; } NodeIdx Parser::parseLogicalOr() { @@ -1336,6 +1470,15 @@ std::unique_ptr Parser::parseFunction() { isVarArgs = true; break; } + // Optional `comp` keyword before the parameter name marks + // this as a compile-time-only parameter — its value must + // be known at the call site and is bound into the + // function's substitution map at monomorphisation time. + // Same per-call instantiation cache as `T: type` generics. + bool isComp = false; + if (match(TOK_COMP)) { + isComp = true; + } consume(TOK_IDENTIFIER, "Expected parameter name"); std::string paramName(previous().text(source_)); @@ -1356,7 +1499,7 @@ std::unique_ptr Parser::parseFunction() { } TypeIdx paramType = parseType(); - args.push_back(Param{std::move(paramName), paramType, mode}); + args.push_back(Param{std::move(paramName), paramType, mode, isComp}); } while (match(TOK_COMMA)); } @@ -1610,10 +1753,19 @@ std::unique_ptr Parser::parseImportDecl() { // doesn't expect. std::string path = previous().lexeme; consume(TOK_CLOSE_PAREN, "Expected ')' after import path"); + + auto decl = std::make_unique(name, path); + // Trailing `.seg.seg` re-export chain. Each segment names a pub + // `import(...)` alias in the preceding module; the final segment's + // resolved path is what `name` will bind to. + while (match(TOK_DOT)) { + consume(TOK_IDENTIFIER, "Expected identifier after `.` in import chain"); + decl->chain.emplace_back(previous().text(source_)); + } consume(TOK_SEMI, "Expected ';' after import declaration"); importHandles.insert(name); - return std::make_unique(name, path); + return decl; } std::unique_ptr Parser::parseDestructuringImport() { @@ -1633,9 +1785,24 @@ std::unique_ptr Parser::parseDestructuringImport() { consume(TOK_STRING_LITERAL, "Expected string literal for import path"); std::string path = previous().lexeme; consume(TOK_CLOSE_PAREN, "Expected ')' after import path"); - consume(TOK_SEMI, "Expected ';' after import declaration"); - return std::make_unique(std::move(names), path); + auto decl = std::make_unique(std::move(names), + std::move(path)); + while (match(TOK_DOT)) { + consume(TOK_IDENTIFIER, "Expected identifier after `.` in import chain"); + decl->chain.emplace_back(previous().text(source_)); + } + consume(TOK_SEMI, "Expected ';' after import declaration"); + // A destructured name MAY be a re-exported module — `const {fmt} + // = import("std");`. The parser has no semantic info to tell that + // apart from a function destructure, so we conservatively register + // every destructured name as a potential import handle. That makes + // the parser route `name.member` as a flat `name.member` callee + // (handled later by the chained-namespace resolver) instead of as + // a struct-field MemberAccess. Cheap for non-module names — the + // flat-key lookup just misses harmlessly. + for (const auto &n : decl->Names) { importHandles.insert(n); } + return decl; } std::unique_ptr Parser::parseConstDecl() { @@ -1713,11 +1880,10 @@ std::unique_ptr Parser::parse() { if (check(TOK_EQUAL)) { advance(); if (check(TOK_IMPORT)) { - if (isPub) { - parseError("`pub` is not allowed on imports"); - } current = saved; - module->Imports.push_back(parseImportDecl()); + auto imp = parseImportDecl(); + imp->isPub = isPub; + module->Imports.push_back(std::move(imp)); continue; } if (check(TOK_STRUCT)) { @@ -1756,7 +1922,25 @@ std::unique_ptr Parser::parse() { // `pub fn …` falls through here; parseFunction handles `pub` // itself (its modifier-loop accepts `pub`/`extern`/`export`/`tfn`). - module->Functions.push_back(parseFunction()); + // `cfn` at top level has two existing meanings: + // * `cfn drop(self: mut X)` — compiler-callable drop method + // (the historical use). First param is `self`. + // * `cfn print(fmt: str)` — compile-time function (new for + // std.fmt.print). No `self` param. + // We disambiguate by inspecting the first param: a leading + // `self` keeps `isCfn` and routes through the existing drop / + // at / setAt machinery; absence of `self` flips to + // `isCompTimeFn` and routes through the comp-time dispatcher. + auto fn = parseFunction(); + if (fn->isCfn) { + bool firstIsSelf = + !fn->Args.empty() && fn->Args.front().Name == "self"; + if (!firstIsSelf) { + fn->isCfn = false; + fn->isCompTimeFn = true; + } + } + module->Functions.push_back(std::move(fn)); } if (!sharedAnonStructs) { module->AnonStructs = std::move(anonStructs); } diff --git a/src/symbol_table.cpp b/src/symbol_table.cpp index 21212f5..afc88c2 100644 --- a/src/symbol_table.cpp +++ b/src/symbol_table.cpp @@ -42,6 +42,13 @@ void SymbolTable::registerModule(const std::string &modulePath, for (auto &c : module->Consts) { if (c->isPub) registerPub(c->Name, nullptr); } + // `pub const X = import(...)` re-exports. Destructuring imports + // (`const { X } = import("std");`) check existence through this + // table; without surfacing re-exports here, every module-typed + // destructure would be rejected as "not exported". + for (auto &i : module->Imports) { + if (i->isPub) registerPub(i->Name, nullptr); + } } void SymbolTable::registerBuiltinSymbol(const std::string &modulePath, diff --git a/src/token.h b/src/token.h index 7ad64f1..f7a5969 100644 --- a/src/token.h +++ b/src/token.h @@ -87,6 +87,15 @@ enum TokenType { TOK_AS, // as keyword (explicit type cast) TOK_AT, // @ — prefix for comptime-function invocations: // `@sizeOf(T)`, `@alignOf(T)`, user-defined cfns + TOK_COMP, // comp keyword — comp param marker (`fn f(comp n: u32) + // ...`); also wraps an expression that must fold at + // compile time (`comp const X = 1 + 1;`). Source- + // level keyword for the broader "compile-time + // evaluation" mechanism implemented in src/comptime.h. + TOK_INLINE, // inline keyword — prefix for `inline while` (loop + // unrolling at compile time, mirrors Zig's + // `inline while`). Required by std.fmt's format- + // string parser. }; // Token structure. diff --git a/std/fmt.jam b/std/fmt.jam new file mode 100644 index 0000000..4b729b2 --- /dev/null +++ b/std/fmt.jam @@ -0,0 +1,121 @@ +// std.fmt — interpolation-based formatted output. +// +// `print` and `eprint` are compile-time functions (`cfn`): the body +// runs at each call site during astgen and emits a sequence of +// `@-emit` intrinsic calls into the caller's JIR. The format string +// is parsed at compile time; per-`{ident}` placeholder, the compiler +// looks up the named local in the caller's scope and dispatches the +// right per-type write call. +// +// Supported placeholder forms (v1): +// {ident} — substitute the named local. Type-dispatched. +// {{ — literal `{`. +// }} — literal `}`. +// +// Supported value types: signed/unsigned int (any width up to 64), +// f32 / f64, bool, and `str` (slice of u8). User types and format +// specifiers (`{x:5.2}` etc.) are deferrable extensions. +// +// Literal-byte batching: consecutive literal bytes between +// placeholders are flushed as a single `@emitWriteBytes` call rather +// than one `@emitPutByte` per byte. For `"hello {x}\n"` the runtime +// binary sees two writes (`"hello "` and `"\n"`) instead of seven. +// Brace escapes (`{{` / `}}`) cut the run and start a new one — the +// flushed range includes the first brace, the second is skipped. + +// `{` is byte 123, `}` is byte 125 — referenced by ordinal because the +// cfn body's `if` comparison runs at compile time and `'{'` char- +// literals haven't landed as a comp form yet. Switch to `'{'` once +// they do. + +pub cfn print(fmt: str) { + var i: u32 = 0; + var runStart: u32 = 0; + while (i < fmt.length) { + if (fmt[i] == 123) { + if (i + 1 < fmt.length) { + if (fmt[i + 1] == 123) { + // `{{` — emit a single literal `{` by extending + // the run through index `i` then resuming after + // the second brace. + @emitWriteBytes(1, fmt, runStart, i + 1); + runStart = i + 2; + i = i + 2; + } else { + // `{name}` — flush the literal run, interp. + @emitWriteBytes(1, fmt, runStart, i); + var nameStart: u32 = i + 1; + var nameEnd: u32 = nameStart; + while (fmt[nameEnd] != 125) { + nameEnd = nameEnd + 1; + } + @emitPrintLocalByRange(1, fmt, nameStart, nameEnd); + i = nameEnd + 1; + runStart = i; + } + } else { + // Trailing `{` with no closer — fold into the run. + i = i + 1; + } + } else if (fmt[i] == 125) { + if (i + 1 < fmt.length) { + if (fmt[i + 1] == 125) { + @emitWriteBytes(1, fmt, runStart, i + 1); + runStart = i + 2; + i = i + 2; + } else { + // Bare `}` — keep it as a literal byte in the run. + i = i + 1; + } + } else { + i = i + 1; + } + } else { + i = i + 1; + } + } + @emitWriteBytes(1, fmt, runStart, fmt.length); +} + +pub cfn eprint(fmt: str) { + var i: u32 = 0; + var runStart: u32 = 0; + while (i < fmt.length) { + if (fmt[i] == 123) { + if (i + 1 < fmt.length) { + if (fmt[i + 1] == 123) { + @emitWriteBytes(2, fmt, runStart, i + 1); + runStart = i + 2; + i = i + 2; + } else { + @emitWriteBytes(2, fmt, runStart, i); + var nameStart: u32 = i + 1; + var nameEnd: u32 = nameStart; + while (fmt[nameEnd] != 125) { + nameEnd = nameEnd + 1; + } + @emitPrintLocalByRange(2, fmt, nameStart, nameEnd); + i = nameEnd + 1; + runStart = i; + } + } else { + i = i + 1; + } + } else if (fmt[i] == 125) { + if (i + 1 < fmt.length) { + if (fmt[i + 1] == 125) { + @emitWriteBytes(2, fmt, runStart, i + 1); + runStart = i + 2; + i = i + 2; + } else { + i = i + 1; + } + } else { + i = i + 1; + } + } else { + i = i + 1; + } + } + @emitWriteBytes(2, fmt, runStart, fmt.length); +} diff --git a/std/std.jam b/std/std.jam new file mode 100644 index 0000000..b1f7828 --- /dev/null +++ b/std/std.jam @@ -0,0 +1,14 @@ +// std — namespace aggregator for the Jam standard library. +// +// Lets user code write `const std = import("std");` once and then +// reach individual modules as `std.fmt`, `std.collections`, etc. +// Each pub `import(...)` here is a re-export: it does not duplicate +// the module, it just adds an alias so chained member access through +// std resolves to the underlying file. +// +// Add a new line here when a new module is added under std/. + +pub const fmt = import("fmt"); +pub const collections = import("collections"); +pub const option = import("option"); +pub const string = import("string"); diff --git a/tests/cpp/test_codegen_errors.cpp b/tests/cpp/test_codegen_errors.cpp index 6bc383c..63d352e 100644 --- a/tests/cpp/test_codegen_errors.cpp +++ b/tests/cpp/test_codegen_errors.cpp @@ -131,8 +131,8 @@ class CodegenErrorTests { testMixedFloatWidthRejected); framework.addTest("Codegen - destructured non-pub symbol rejected", testDestructuredNonPubRejected); - framework.addTest("Codegen - `pub` on import rejected", - testPubOnImportRejected); + framework.addTest("Codegen - `pub` re-export compiles", + testPubReexportCompiles); framework.addTest("Codegen - `pub` on destructuring import rejected", testPubOnDestructuringRejected); framework.addTest("Codegen - namespace access to non-pub type rejected", @@ -308,16 +308,18 @@ fn main() {} ASSERT_TRUE(stderrContains(r, "is not exported from module")); } - // `pub const x = import(...)` is rejected; re-exports aren't a - // feature yet. - static void testPubOnImportRejected() { - auto r = compileSource("must_fail_pub_import", R"( -pub const std = import("std"); + // `pub const X = import(...)` is a re-export. The module that + // writes it surfaces the imported module under its own namespace + // so a downstream importer can chain through (`outer.X.member`). + // Just check that the form parses + compiles cleanly — exercising + // the re-export chain end-to-end is the smoke test in + // `test_print.cpp` and the unit suite. + static void testPubReexportCompiles() { + auto r = compileSource("ok_pub_reexport", R"( +pub const fmt = import("fmt"); fn main() {} )"); - ASSERT_TRUE(r.exitCode != 0); - ASSERT_TRUE(stderrContains(r, "pub")); - ASSERT_TRUE(stderrContains(r, "imports")); + ASSERT_TRUE(r.exitCode == 0); } // `pub const { X } = import(...)` is rejected explicitly so the diff --git a/tests/cpp/test_comptime.cpp b/tests/cpp/test_comptime.cpp index b3eb104..123d305 100644 --- a/tests/cpp/test_comptime.cpp +++ b/tests/cpp/test_comptime.cpp @@ -372,6 +372,287 @@ void testEvalRequiredSilentOnSuccess() { ASSERT_FALSE(diags.hasErrors()); } +// ── Mutable scope ──────────────────────────────────────────────── + +void testScopeSetMutatesExistingBinding() { + jam::ComptimeScope scope; + scope.bind("x", jam::ComptimeValue::makeInt(5, 32, true)); + bool ok = scope.set("x", jam::ComptimeValue::makeInt(10, 32, true)); + ASSERT_TRUE(ok); + ASSERT_EQ(static_cast(10), scope.lookup("x")->asU64()); +} + +void testScopeSetFailsForUnknownName() { + jam::ComptimeScope scope; + bool ok = scope.set("nope", jam::ComptimeValue::makeBool(false)); + ASSERT_FALSE(ok); +} + +void testScopeSetWalksUpToParent() { + jam::ComptimeScope parent; + parent.bind("x", jam::ComptimeValue::makeInt(5, 32, true)); + jam::ComptimeScope child(&parent); + // `x` lives in parent; child.set should mutate parent's binding. + bool ok = child.set("x", jam::ComptimeValue::makeInt(99, 32, true)); + ASSERT_TRUE(ok); + ASSERT_EQ(static_cast(99), parent.lookup("x")->asU64()); +} + +// ── Helper builders for statement nodes ────────────────────────── + +NodeIdx mkVarDecl(NodeStore &ns, StringPool &sp, const std::string &name, + NodeIdx init) { + ExtraIdx extra = ns.reserveExtra(3); + ns.setExtra(extra, sp.intern(name)); + ns.setExtra(extra + 1, kNoType); // type — ignored by comp evaluator + ns.setExtra(extra + 2, init); + AstNode node{}; + node.tag = AstTag::VarDecl; + node.lhs = extra; + node.rhs = 0; // flags + return ns.addNode(node); +} + +NodeIdx mkAssign(NodeStore &ns, NodeIdx target, NodeIdx value) { + AstNode node{}; + node.tag = AstTag::Assign; + node.lhs = target; + node.rhs = value; + return ns.addNode(node); +} + +NodeIdx mkIfNode(NodeStore &ns, NodeIdx cond, + const std::vector &thenStmts, + const std::vector &elseStmts) { + std::size_t total = 2 + thenStmts.size() + elseStmts.size(); + ExtraIdx extra = ns.reserveExtra(total); + ns.setExtra(extra, static_cast(thenStmts.size())); + ns.setExtra(extra + 1, static_cast(elseStmts.size())); + for (std::size_t i = 0; i < thenStmts.size(); i++) { + ns.setExtra(extra + 2 + i, thenStmts[i]); + } + for (std::size_t i = 0; i < elseStmts.size(); i++) { + ns.setExtra(extra + 2 + thenStmts.size() + i, elseStmts[i]); + } + AstNode node{}; + node.tag = AstTag::IfNode; + node.lhs = cond; + node.rhs = extra; + return ns.addNode(node); +} + +NodeIdx mkWhileNode(NodeStore &ns, NodeIdx cond, + const std::vector &body) { + ExtraIdx extra = ns.reserveExtra(1 + body.size()); + ns.setExtra(extra, static_cast(body.size())); + for (std::size_t i = 0; i < body.size(); i++) { + ns.setExtra(extra + 1 + i, body[i]); + } + AstNode node{}; + node.tag = AstTag::WhileNode; + node.lhs = cond; + node.rhs = extra; + return ns.addNode(node); +} + +NodeIdx mkMemberAccess(NodeStore &ns, StringPool &sp, NodeIdx base, + const std::string &member) { + AstNode node{}; + node.tag = AstTag::MemberAccess; + node.lhs = base; + node.rhs = sp.intern(member); + return ns.addNode(node); +} + +// ── execStmt: VarDecl + Assign ─────────────────────────────────── + +void testExecVarDeclAddsBinding() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + jam::Diagnostics diags; + jam::SrcLoc loc{"test.jam", 1}; + uint32_t iter = 0; + jam::ComptimeValue ret; + + // var x = 42; + NodeIdx decl = mkVarDecl(ns, sp, "x", mkInt(ns, 42)); + jam::ExecResult r = e.execStmt(decl, scope, iter, 1000, ret, diags, loc); + ASSERT_TRUE(r == jam::ExecResult::Continue); + ASSERT_TRUE(scope.lookup("x") != nullptr); + ASSERT_EQ(static_cast(42), scope.lookup("x")->asU64()); +} + +void testExecAssignMutatesBinding() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + jam::Diagnostics diags; + jam::SrcLoc loc{"test.jam", 1}; + uint32_t iter = 0; + jam::ComptimeValue ret; + + scope.bind("x", jam::ComptimeValue::makeInt(5, 64, false)); + NodeIdx tgt = mkVar(ns, sp, "x"); + NodeIdx asn = mkAssign(ns, tgt, mkInt(ns, 99)); + jam::ExecResult r = e.execStmt(asn, scope, iter, 1000, ret, diags, loc); + ASSERT_TRUE(r == jam::ExecResult::Continue); + ASSERT_EQ(static_cast(99), scope.lookup("x")->asU64()); +} + +void testExecAssignToUndeclaredErrors() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + jam::Diagnostics diags; + jam::SrcLoc loc{"test.jam", 1}; + uint32_t iter = 0; + jam::ComptimeValue ret; + + NodeIdx tgt = mkVar(ns, sp, "neverDeclared"); + NodeIdx asn = mkAssign(ns, tgt, mkInt(ns, 1)); + jam::ExecResult r = e.execStmt(asn, scope, iter, 1000, ret, diags, loc); + ASSERT_TRUE(r == jam::ExecResult::Error); + ASSERT_TRUE(diags.hasErrors()); +} + +// ── execStmt: control flow ─────────────────────────────────────── + +void testExecIfPicksTrueBranch() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + jam::Diagnostics diags; + jam::SrcLoc loc{"test.jam", 1}; + uint32_t iter = 0; + jam::ComptimeValue ret; + + scope.bind("x", jam::ComptimeValue::makeInt(0, 64, false)); + // if (true) { x = 1; } else { x = 2; } + NodeIdx thenAsn = mkAssign(ns, mkVar(ns, sp, "x"), mkInt(ns, 1)); + NodeIdx elseAsn = mkAssign(ns, mkVar(ns, sp, "x"), mkInt(ns, 2)); + NodeIdx ifNode = + mkIfNode(ns, mkBool(ns, true), {thenAsn}, {elseAsn}); + jam::ExecResult r = e.execStmt(ifNode, scope, iter, 1000, ret, diags, loc); + ASSERT_TRUE(r == jam::ExecResult::Continue); + ASSERT_EQ(static_cast(1), scope.lookup("x")->asU64()); +} + +void testExecIfPicksFalseBranch() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + jam::Diagnostics diags; + jam::SrcLoc loc{"test.jam", 1}; + uint32_t iter = 0; + jam::ComptimeValue ret; + + scope.bind("x", jam::ComptimeValue::makeInt(0, 64, false)); + NodeIdx thenAsn = mkAssign(ns, mkVar(ns, sp, "x"), mkInt(ns, 1)); + NodeIdx elseAsn = mkAssign(ns, mkVar(ns, sp, "x"), mkInt(ns, 2)); + NodeIdx ifNode = + mkIfNode(ns, mkBool(ns, false), {thenAsn}, {elseAsn}); + e.execStmt(ifNode, scope, iter, 1000, ret, diags, loc); + ASSERT_EQ(static_cast(2), scope.lookup("x")->asU64()); +} + +void testExecWhileLoopCountsUp() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + jam::Diagnostics diags; + jam::SrcLoc loc{"test.jam", 1}; + uint32_t iter = 0; + jam::ComptimeValue ret; + + // var i: u64 = 0; + scope.bind("i", jam::ComptimeValue::makeInt(0, 64, false)); + // while (i < 5) { i = i + 1; } + NodeIdx cond = mkBinOp(ns, BinOp::Lt, mkVar(ns, sp, "i"), mkInt(ns, 5)); + NodeIdx inc = mkBinOp(ns, BinOp::Add, mkVar(ns, sp, "i"), mkInt(ns, 1)); + NodeIdx asn = mkAssign(ns, mkVar(ns, sp, "i"), inc); + NodeIdx whileNode = mkWhileNode(ns, cond, {asn}); + jam::ExecResult r = + e.execStmt(whileNode, scope, iter, 1000, ret, diags, loc); + ASSERT_TRUE(r == jam::ExecResult::Continue); + ASSERT_EQ(static_cast(5), scope.lookup("i")->asU64()); +} + +void testExecWhileIterationCapTrips() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + jam::Diagnostics diags; + jam::SrcLoc loc{"test.jam", 1}; + uint32_t iter = 0; + jam::ComptimeValue ret; + + scope.bind("i", jam::ComptimeValue::makeInt(0, 64, false)); + // while (true) { i = i + 1; } — cap at 100 iterations. + NodeIdx inc = mkBinOp(ns, BinOp::Add, mkVar(ns, sp, "i"), mkInt(ns, 1)); + NodeIdx asn = mkAssign(ns, mkVar(ns, sp, "i"), inc); + NodeIdx whileNode = mkWhileNode(ns, mkBool(ns, true), {asn}); + jam::ExecResult r = + e.execStmt(whileNode, scope, iter, /*iterCap=*/100, ret, diags, loc); + ASSERT_TRUE(r == jam::ExecResult::IterationCap); + ASSERT_TRUE(diags.hasErrors()); + // 100 iterations ran (i went 0→1→…→100); the 101st loop attempt + // trips the cap before the body runs. + ASSERT_EQ(static_cast(100), scope.lookup("i")->asU64()); +} + +void testExecBlockShortCircuitsOnError() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + jam::Diagnostics diags; + jam::SrcLoc loc{"test.jam", 1}; + uint32_t iter = 0; + jam::ComptimeValue ret; + + // Block: var x = 0; ; x = 99; + // The bad assign should stop execution before x = 99 runs. + NodeIdx d = mkVarDecl(ns, sp, "x", mkInt(ns, 0)); + NodeIdx bad = mkAssign(ns, mkVar(ns, sp, "y"), mkInt(ns, 1)); + NodeIdx good = mkAssign(ns, mkVar(ns, sp, "x"), mkInt(ns, 99)); + std::vector body = {d, bad, good}; + jam::ExecResult r = e.execBlock(body.data(), body.size(), scope, iter, + 1000, ret, diags, loc); + ASSERT_TRUE(r == jam::ExecResult::Error); + // x stays at 0 (the good assign never ran). + ASSERT_EQ(static_cast(0), scope.lookup("x")->asU64()); +} + +void testMemberAccessStrLength() { + NodeStore ns; + StringPool sp; + TypePool tp; + jam::ComptimeEvaluator e(ns, sp, tp); + jam::ComptimeScope scope; + scope.bind("s", jam::ComptimeValue::makeStr(sp.intern("hello"))); + + NodeIdx access = mkMemberAccess(ns, sp, mkVar(ns, sp, "s"), "length"); + jam::ComptimeValue v = e.eval(access, scope); + ASSERT_TRUE(v.isInt()); + ASSERT_EQ(static_cast(5), v.asU64()); +} + } // namespace int main() { @@ -406,6 +687,30 @@ int main() { framework.addTest( "Comptime - evalRequired pushes diagnostic on fail", testEvalRequiredPushesDiagnosticOnFail); + framework.addTest("Comptime - scope set mutates existing", + testScopeSetMutatesExistingBinding); + framework.addTest("Comptime - scope set returns false for unknown", + testScopeSetFailsForUnknownName); + framework.addTest("Comptime - scope set walks up to parent", + testScopeSetWalksUpToParent); + framework.addTest("Comptime - exec VarDecl adds binding", + testExecVarDeclAddsBinding); + framework.addTest("Comptime - exec Assign mutates", + testExecAssignMutatesBinding); + framework.addTest("Comptime - exec Assign to undeclared errors", + testExecAssignToUndeclaredErrors); + framework.addTest("Comptime - exec If picks true branch", + testExecIfPicksTrueBranch); + framework.addTest("Comptime - exec If picks false branch", + testExecIfPicksFalseBranch); + framework.addTest("Comptime - exec While counts up to bound", + testExecWhileLoopCountsUp); + framework.addTest("Comptime - exec While iteration cap trips", + testExecWhileIterationCapTrips); + framework.addTest("Comptime - exec Block short-circuits on error", + testExecBlockShortCircuitsOnError); + framework.addTest("Comptime - member-access str.length", + testMemberAccessStrLength); framework.addTest("Comptime - evalRequired silent on success", testEvalRequiredSilentOnSuccess); framework.runAll(); diff --git a/tests/cpp/test_print.cpp b/tests/cpp/test_print.cpp new file mode 100644 index 0000000..0d96a9d --- /dev/null +++ b/tests/cpp/test_print.cpp @@ -0,0 +1,401 @@ +// End-to-end tests for the @-emit + cfn-based print pipeline. +// Each test compiles a small Jam program that uses `fmt.print` / +// `fmt.eprint`, runs the resulting binary with stdout + stderr +// redirected to separate files, and asserts on the captured bytes. +// +// stdout/stderr separation lets us verify that `@eprint` actually +// reaches fd=2 — important because the @-emit dispatch passes the fd +// as a comp-known literal and we want to catch any wiring mistake +// that lands stderr output on stdout (or vice versa). + +#include "test_framework.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +struct RunResult { + // Exit code from the compile step. -1 means compile died via signal. + int compileExit; + // Exit code from running the compiled binary. -1 if compile failed + // and we never ran the binary. + int runExit; + // Captured streams. `compileOut` is the compile step's combined + // stdout+stderr; `stdout_` and `stderr_` are the runtime streams. + std::string compileOut; + std::string stdout_; + std::string stderr_; +}; + +// Slurp a whole file into a std::string. Used to read the captured +// stdout/stderr from /tmp after the binary runs. +std::string readFile(const std::string &path) { + std::ifstream in(path); + std::stringstream ss; + ss << in.rdbuf(); + return ss.str(); +} + +// Compile `source` to a binary and run it. Captures the runtime +// stdout/stderr separately via shell redirects so we can assert on +// each independently. +RunResult compileAndRun(const std::string &name, const std::string &source) { + std::string srcPath = "/tmp/jam_print_" + name + ".jam"; + std::string binPath = "/tmp/jam_print_" + name + ".bin"; + std::string compileLog = "/tmp/jam_print_" + name + ".compile"; + std::string stdoutFile = "/tmp/jam_print_" + name + ".out"; + std::string stderrFile = "/tmp/jam_print_" + name + ".err"; + + { + std::ofstream out(srcPath); + out << source; + } + + std::string compileCmd = "./output/jam.out -o " + binPath + " " + srcPath + + " >" + compileLog + " 2>&1"; + int compileStatus = std::system(compileCmd.c_str()); + int compileExit = + WIFEXITED(compileStatus) ? WEXITSTATUS(compileStatus) : -1; + std::string compileOut = readFile(compileLog); + if (compileExit != 0) { + return {compileExit, -1, std::move(compileOut), "", ""}; + } + + std::string runCmd = + binPath + " >" + stdoutFile + " 2>" + stderrFile; + int runStatus = std::system(runCmd.c_str()); + int runExit = WIFEXITED(runStatus) ? WEXITSTATUS(runStatus) : -1; + std::string stdoutContent = readFile(stdoutFile); + std::string stderrContent = readFile(stderrFile); + return {0, runExit, std::move(compileOut), std::move(stdoutContent), + std::move(stderrContent)}; +} + +// ── Per-built-in-type tests ────────────────────────────────────── + +void testPrintIntI32() { + auto r = compileAndRun("int_i32", R"( +const fmt = import("fmt"); +fn main() i32 { + var x: i32 = 42; + fmt.print("x={x}\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("x=42\n"), r.stdout_); + ASSERT_EQ(std::string(""), r.stderr_); +} + +void testPrintIntI64Negative() { + auto r = compileAndRun("int_i64_neg", R"( +const fmt = import("fmt"); +fn main() i32 { + var x: i64 = -123; + fmt.print("{x}\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("-123\n"), r.stdout_); +} + +void testPrintIntU64() { + auto r = compileAndRun("int_u64", R"( +const fmt = import("fmt"); +fn main() i32 { + var x: u64 = 9999999999; + fmt.print("{x}\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("9999999999\n"), r.stdout_); +} + +void testPrintBoolTrue() { + auto r = compileAndRun("bool_true", R"( +const fmt = import("fmt"); +fn main() i32 { + var t: bool = true; + fmt.print("{t}\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("true\n"), r.stdout_); +} + +void testPrintBoolFalse() { + auto r = compileAndRun("bool_false", R"( +const fmt = import("fmt"); +fn main() i32 { + var f: bool = false; + fmt.print("{f}\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("false\n"), r.stdout_); +} + +void testPrintStr() { + auto r = compileAndRun("str", R"( +const fmt = import("fmt"); +fn main() i32 { + var name: str = "Alice"; + fmt.print("hello, {name}\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("hello, Alice\n"), r.stdout_); +} + +// ── Multi-interp + escapes ─────────────────────────────────────── + +void testPrintMultipleInterps() { + auto r = compileAndRun("multi", R"( +const fmt = import("fmt"); +fn main() i32 { + var name: str = "Bob"; + var age: i32 = 30; + var ok: bool = true; + fmt.print("{name} is {age} (active={ok})\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("Bob is 30 (active=true)\n"), r.stdout_); +} + +void testBraceEscapeOpen() { + auto r = compileAndRun("brace_open", R"( +const fmt = import("fmt"); +fn main() i32 { + fmt.print("set is {{here\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("set is {here\n"), r.stdout_); +} + +void testBraceEscapeClose() { + auto r = compileAndRun("brace_close", R"( +const fmt = import("fmt"); +fn main() i32 { + fmt.print("done}}here\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("done}here\n"), r.stdout_); +} + +void testBraceEscapesMixedWithInterp() { + auto r = compileAndRun("brace_mixed", R"( +const fmt = import("fmt"); +fn main() i32 { + var n: i32 = 3; + fmt.print("set is {{1, 2, {n}}}\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("set is {1, 2, 3}\n"), r.stdout_); +} + +// ── stderr routing ─────────────────────────────────────────────── + +void testEprintGoesToStderr() { + auto r = compileAndRun("eprint", R"( +const fmt = import("fmt"); +fn main() i32 { + fmt.print("on stdout\n"); + fmt.eprint("on stderr\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("on stdout\n"), r.stdout_); + ASSERT_EQ(std::string("on stderr\n"), r.stderr_); +} + +void testEprintInterpAndRouting() { + auto r = compileAndRun("eprint_interp", R"( +const fmt = import("fmt"); +fn main() i32 { + var code: i32 = 42; + var msg: str = "boom"; + fmt.eprint("error code={code} msg={msg}\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string(""), r.stdout_); + ASSERT_EQ(std::string("error code=42 msg=boom\n"), r.stderr_); +} + +// ── Nested-module access shapes ────────────────────────────────── +// `std/std.jam` re-exports each std module via `pub const X = +// import(...)`. These tests pin the three user-visible forms for +// reaching `print` through that re-export so a regression in +// chain resolution surfaces as a missed printout. + +void testStdChainedAccess() { + auto r = compileAndRun("std_chain", R"( +const std = import("std"); +fn main() i32 { + std.fmt.print("std.fmt.print\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("std.fmt.print\n"), r.stdout_); +} + +void testStdDestructureFromChain() { + auto r = compileAndRun("std_destruct_chain", R"( +const { print } = import("std").fmt; +fn main() i32 { + print("destructured from std.fmt\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("destructured from std.fmt\n"), r.stdout_); +} + +// Pins literal-batching shape: a long literal run between two +// placeholders, plus leading-only / trailing-only-placeholder +// shapes. The cfn collapses each contiguous literal span into a +// single `@emitWriteBytes` -> dprintf("%.*s", …) call. Run-time +// correctness is the visible signal; the IR-level reduction is a +// secondary win covered by inspection of `--emit-ir`. + +void testLongLiteralRunBatched() { + auto r = compileAndRun("long_run", R"( +const fmt = import("std").fmt; +fn main() i32 { + var x: i32 = 42; + fmt.print("hello, world! the answer is {x}, have a nice day\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string( + "hello, world! the answer is 42, have a nice day\n"), + r.stdout_); +} + +void testLeadingPlaceholderOnly() { + auto r = compileAndRun("lead_ph", R"( +const fmt = import("std").fmt; +fn main() i32 { + var name: str = "Bob"; + fmt.print("{name} is here\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("Bob is here\n"), r.stdout_); +} + +void testTrailingPlaceholderOnly() { + auto r = compileAndRun("trail_ph", R"( +const fmt = import("std").fmt; +fn main() i32 { + var n: i32 = 7; + fmt.print("count: {n}"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("count: 7"), r.stdout_); +} + +void testStdDestructureModule() { + auto r = compileAndRun("std_destruct_mod", R"( +const { fmt } = import("std"); +fn main() i32 { + fmt.print("fmt destructured from std\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("fmt destructured from std\n"), r.stdout_); +} + +// ── Negative tests: compile-time errors ────────────────────────── + +void testUnknownIdentInInterpFails() { + auto r = compileAndRun("unknown_ident", R"( +const fmt = import("fmt"); +fn main() i32 { + fmt.print("hello {nope}\n"); + return 0; +} +)"); + ASSERT_TRUE(r.compileExit != 0); + ASSERT_TRUE(r.compileOut.find("nope") != std::string::npos); +} + +void testMixedTypeIntsAndStr() { + auto r = compileAndRun("mixed", R"( +const fmt = import("fmt"); +fn main() i32 { + var label: str = "count"; + var n: u32 = 7; + var flag: bool = false; + fmt.print("{label}: {n} ({flag})\n"); + return 0; +} +)"); + ASSERT_EQ(0, r.runExit); + ASSERT_EQ(std::string("count: 7 (false)\n"), r.stdout_); +} + +} // namespace + +int main() { + TestFramework framework; + framework.addTest("Print - i32", testPrintIntI32); + framework.addTest("Print - i64 negative", testPrintIntI64Negative); + framework.addTest("Print - u64", testPrintIntU64); + framework.addTest("Print - bool true", testPrintBoolTrue); + framework.addTest("Print - bool false", testPrintBoolFalse); + framework.addTest("Print - str", testPrintStr); + framework.addTest("Print - multiple interps", testPrintMultipleInterps); + framework.addTest("Print - {{ escape", testBraceEscapeOpen); + framework.addTest("Print - }} escape", testBraceEscapeClose); + framework.addTest("Print - escapes + interp mixed", + testBraceEscapesMixedWithInterp); + framework.addTest("Print - eprint routes to stderr", + testEprintGoesToStderr); + framework.addTest("Print - eprint interp + stderr routing", + testEprintInterpAndRouting); + framework.addTest("Print - unknown identifier fails compile", + testUnknownIdentInInterpFails); + framework.addTest("Print - mixed types (str + u32 + bool)", + testMixedTypeIntsAndStr); + framework.addTest("Print - std.fmt.print (chained access)", + testStdChainedAccess); + framework.addTest("Print - const { print } = import(\"std\").fmt", + testStdDestructureFromChain); + framework.addTest("Print - const { fmt } = import(\"std\")", + testStdDestructureModule); + framework.addTest("Print - long literal run batched", + testLongLiteralRunBatched); + framework.addTest("Print - leading placeholder only", + testLeadingPlaceholderOnly); + framework.addTest("Print - trailing placeholder only", + testTrailingPlaceholderOnly); + framework.runAll(); + return framework.allPassed() ? 0 : 1; +} diff --git a/tests/unit/mod_chain_leaf.jam b/tests/unit/mod_chain_leaf.jam new file mode 100644 index 0000000..58aa4bb --- /dev/null +++ b/tests/unit/mod_chain_leaf.jam @@ -0,0 +1,8 @@ +pub const Point = struct { + x: i32, + y: i32, +}; + +pub fn makePoint(a: i32, b: i32) Point { + return Point{ x: a, y: b }; +} diff --git a/tests/unit/mod_chain_wrap.jam b/tests/unit/mod_chain_wrap.jam new file mode 100644 index 0000000..2274c88 --- /dev/null +++ b/tests/unit/mod_chain_wrap.jam @@ -0,0 +1 @@ +pub const leaf = import("mod_chain_leaf"); diff --git a/tests/unit/test_module_chain_types.jam b/tests/unit/test_module_chain_types.jam new file mode 100644 index 0000000..c2469ec --- /dev/null +++ b/tests/unit/test_module_chain_types.jam @@ -0,0 +1,47 @@ +// Multi-dot type access through a pub-import re-export chain. The +// leaf module exposes a `Point` struct; `mod_chain_wrap` re-exports +// the leaf as `pub const leaf = import(...)`. From the test we reach +// the type through `w.leaf.Point` in every position a type can +// appear: variable annotation, struct literal target, fn parameter +// type, and struct field type. +// +// Mirrors Zig's uniform type-as-value path +// (`src/Sema.zig` `fieldVal`/`namespaceLookupVal`): each `.` is a +// namespace lookup so chained access composes naturally regardless of +// whether the result lands in a value or type position. + +const { assert } = import("test"); +const w = import("mod_chain_wrap"); + +const Box = struct { + inner: w.leaf.Point, +}; + +fn takesPoint(p: w.leaf.Point) i32 { + return p.x + p.y; +} + +fn pointFromAnnotation() i32 { + var p: w.leaf.Point = w.leaf.Point{ x: 3, y: 4 }; + return p.x + p.y; +} + +fn pointFromInferredLiteral() i32 { + var p: w.leaf.Point = w.leaf.makePoint(10, 20); + return p.x + p.y; +} + +fn pointAsFnParam() i32 { + var p: w.leaf.Point = w.leaf.Point{ x: 5, y: 6 }; + return takesPoint(p); +} + +fn pointInStructField() i32 { + var b: Box = Box{ inner: w.leaf.Point{ x: 1, y: 2 } }; + return b.inner.x + b.inner.y; +} + +tfn chainedTypeAnnotation() { assert(pointFromAnnotation(), 7); } +tfn chainedTypeAsFnReturn() { assert(pointFromInferredLiteral(), 30); } +tfn chainedTypeAsFnParam() { assert(pointAsFnParam(), 11); } +tfn chainedTypeInStructField() { assert(pointInStructField(), 3); } diff --git a/tests/unit/test_print.jam b/tests/unit/test_print.jam new file mode 100644 index 0000000..834ce4d --- /dev/null +++ b/tests/unit/test_print.jam @@ -0,0 +1,43 @@ +const { assert } = import("test"); +const fmt = import("std").fmt; + +// Smoke tests for the cfn print pipeline. The jam test-runner can't +// capture stdout to assert exact bytes (that's done in +// tests/cpp/test_print.cpp via subprocess + redirects); these tests +// just verify the pipeline compiles and runs without crashing for +// each supported value type. + +fn callsPrintWithIntOK() i32 { + var x: i32 = 42; + fmt.print("x={x}\n"); + return 1; +} + +fn callsPrintWithStrOK() i32 { + var name: str = "Alice"; + fmt.print("hello {name}\n"); + return 1; +} + +fn callsPrintWithBoolOK() i32 { + var t: bool = true; + fmt.print("t={t}\n"); + return 1; +} + +fn callsEprintRoutesOK() i32 { + var n: i32 = 7; + fmt.eprint("err {n}\n"); + return 1; +} + +fn callsEscapesOK() i32 { + fmt.print("set is {{1,2}}\n"); + return 1; +} + +tfn printIntCompiles() { assert(callsPrintWithIntOK(), 1); } +tfn printStrCompiles() { assert(callsPrintWithStrOK(), 1); } +tfn printBoolCompiles() { assert(callsPrintWithBoolOK(), 1); } +tfn eprintCompiles() { assert(callsEprintRoutesOK(), 1); } +tfn escapesCompile() { assert(callsEscapesOK(), 1); }