From fe8a72f68032457938d38f2a9e3cf969077dea05 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Tue, 12 May 2026 10:18:55 +0200 Subject: [PATCH] cfn support --- src/ast.cpp | 25 ++++++++ src/ast_flat.h | 8 +++ src/init_analysis.cpp | 5 +- src/lexer.cpp | 7 +++ src/parser.cpp | 18 ++++++ src/token.h | 2 + tests/unit/test_intrinsics.jam | 63 ++++++++++++++++++++ tests/unit/test_match_scrutinee_shapes.jam | 67 ++++++++++++++++++++++ 8 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_intrinsics.jam create mode 100644 tests/unit/test_match_scrutinee_shapes.jam diff --git a/src/ast.cpp b/src/ast.cpp index 73b3df1..23dc4cb 100644 --- a/src/ast.cpp +++ b/src/ast.cpp @@ -571,6 +571,29 @@ static JamValueRef codegenUnaryOp(JamCodegenContext &ctx, const AstNode &n) { } } +// Comptime intrinsic dispatch (`@name(T)`). Stage 1 supports two +// intrinsics — `sizeOf` returns the byte size of a type as u64; +// `alignOf` returns the alignment as u8. Both consult the codegen +// context's typeSize/typeAlign which already compute these for +// struct layout and ABI purposes. The emitted IR is a literal +// constant; LLVM never sees a call. Stage 2 will extend this +// dispatch to user-defined cfn bodies once CTFE lands. +static JamValueRef codegenAtCall(JamCodegenContext &ctx, const AstNode &n) { + const std::string &name = + ctx.getStringPool().get(static_cast(n.lhs)); + TypeIdx tyArg = static_cast(n.rhs); + + if (name == "sizeOf") { + uint64_t bytes = ctx.typeSize(tyArg); + return JamLLVMConstInt(ctx.getInt64Type(), bytes, false); + } + if (name == "alignOf") { + uint64_t a = ctx.typeAlign(tyArg); + return JamLLVMConstInt(ctx.getInt8Type(), a, false); + } + throw std::runtime_error("Unknown comptime intrinsic: @" + name); +} + static JamValueRef codegenCall(JamCodegenContext &ctx, const AstNode &n) { const std::string &callee = ctx.getStringPool().get(static_cast(n.lhs)); @@ -2459,6 +2482,8 @@ JamValueRef codegenNode(JamCodegenContext &ctx, NodeIdx node, return codegenBinaryOp(ctx, n); case AstTag::Call: return codegenCall(ctx, n); + case AstTag::AtCall: + return codegenAtCall(ctx, n); case AstTag::Return: return codegenReturn(ctx, n); case AstTag::Assign: diff --git a/src/ast_flat.h b/src/ast_flat.h index 3918352..48398d4 100644 --- a/src/ast_flat.h +++ b/src/ast_flat.h @@ -122,6 +122,14 @@ 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) + AtCall, + // Pattern atoms — internal nodes used inside MatchNode arms. Never // reachable from regular expression / statement positions. diff --git a/src/init_analysis.cpp b/src/init_analysis.cpp index 7f6dec1..dffeb03 100644 --- a/src/init_analysis.cpp +++ b/src/init_analysis.cpp @@ -273,11 +273,14 @@ Result Analyzer::analyze(NodeIdx idx, NameMap state) { return analyze(countIdx, std::move(r.state)); } - // Literals — no init effect on bindings. + // Literals + comptime intrinsics — no init effect on bindings. + // `@sizeOf(T)` / `@alignOf(T)` etc. are evaluated at codegen + // time; they cannot read or write any runtime binding state. case AstTag::NumberLit: case AstTag::BoolLit: case AstTag::StringLit: case AstTag::ImportLit: + case AstTag::AtCall: // Generics G2: `struct {...}` expression evaluates to a value of // type `type` at compile time. The body lives in ModuleAST and is // processed by the substitution engine — the analyzer doesn't see diff --git a/src/lexer.cpp b/src/lexer.cpp index 5995669..779f1e5 100644 --- a/src/lexer.cpp +++ b/src/lexer.cpp @@ -504,6 +504,13 @@ std::vector Lexer::scanTokens() { } break; + // `@` is the prefix for comptime-function invocations. The + // identifier and arg list are lexed normally; the parser + // recognizes the `@`+IDENTIFIER sequence as an AtCall. + case '@': + addToken(TOK_AT, "@"); + break; + case '<': if (match('=')) { addToken(TOK_LESS_EQUAL, "<="); diff --git a/src/parser.cpp b/src/parser.cpp index 2e61e17..d4a86cc 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -112,6 +112,24 @@ NodeIdx Parser::parsePrimary() { // produce a value (M3). The same call works for both statement and // 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. + if (match(TOK_AT)) { + consume(TOK_IDENTIFIER, "Expected intrinsic name after '@'"); + StringIdx nameId = stringPool->intern(previous().lexeme); + consume(TOK_OPEN_PAREN, "Expected '(' after '@name'"); + TypeIdx tyArg = parseType(); + consume(TOK_CLOSE_PAREN, + "Expected ')' after '@' intrinsic argument"); + return emit(AstNode{AstTag::AtCall, 0, 0, 0, nameId, + static_cast(tyArg)}); + } + if (match(TOK_NUMBER)) { // `parseNumLexeme` returns the magnitude; the sign is recorded in // the node's flags bit 0 and the codegen applies negation. This diff --git a/src/token.h b/src/token.h index e58477f..70ffa73 100644 --- a/src/token.h +++ b/src/token.h @@ -75,6 +75,8 @@ enum TokenType { TOK_MATCH, // match keyword TOK_DOTDOT_EQ, // ..= (inclusive range in match patterns) TOK_AS, // as keyword (explicit type cast) + TOK_AT, // @ — prefix for comptime-function invocations: + // `@sizeOf(T)`, `@alignOf(T)`, user-defined cfns }; // Token structure. diff --git a/tests/unit/test_intrinsics.jam b/tests/unit/test_intrinsics.jam new file mode 100644 index 0000000..9f70376 --- /dev/null +++ b/tests/unit/test_intrinsics.jam @@ -0,0 +1,63 @@ +// `@sizeOf(T)` and `@alignOf(T)` — comptime intrinsics resolved by +// the compiler at codegen time. The `@` prefix marks compile-time +// invocation; the result is substituted as a constant before LLVM +// sees the code. Matches Rust's `mem::size_of::()` semantics +// (the function never exists at runtime). +const { assert } = import("test"); + +// -------------------- Primitive sizes -------------------- + +tfn sizeOfPrimitives() { + assert(@sizeOf(u8) as i32, 1); + assert(@sizeOf(u16) as i32, 2); + assert(@sizeOf(u32) as i32, 4); + assert(@sizeOf(u64) as i32, 8); + assert(@sizeOf(i8) as i32, 1); + assert(@sizeOf(i16) as i32, 2); + assert(@sizeOf(i32) as i32, 4); + assert(@sizeOf(i64) as i32, 8); + assert(@sizeOf(f32) as i32, 4); + assert(@sizeOf(f64) as i32, 8); + assert(@sizeOf(bool) as i32, 1); +} + +// -------------------- Primitive alignments -------------------- + +tfn alignOfPrimitives() { + assert(@alignOf(u8) as i32, 1); + assert(@alignOf(u16) as i32, 2); + assert(@alignOf(u32) as i32, 4); + assert(@alignOf(u64) as i32, 8); + assert(@alignOf(f64) as i32, 8); +} + +// -------------------- Composite types -------------------- + +const Point = struct { + x: i32, + y: i32, +}; + +tfn sizeOfStruct() { + // Two i32 fields, no padding needed. + assert(@sizeOf(Point) as i32, 8); + assert(@alignOf(Point) as i32, 4); +} + +// -------------------- Use in expressions -------------------- + +tfn intrinsicInArithmetic() { + // Comptime constant composes with runtime arithmetic. + var n: u64 = 10; + var total: u64 = n * @sizeOf(u32); + assert(total as i32, 40); +} + +// -------------------- Pointer/slice sizes -------------------- + +tfn sizeOfPointerAndSlice() { + // On a 64-bit target a pointer is 8 bytes; a slice is { ptr, len } + // which is 16 bytes (two 64-bit words). + assert(@sizeOf(*const u8) as i32, 8); + assert(@sizeOf([]u8) as i32, 16); +} diff --git a/tests/unit/test_match_scrutinee_shapes.jam b/tests/unit/test_match_scrutinee_shapes.jam new file mode 100644 index 0000000..1c466a7 --- /dev/null +++ b/tests/unit/test_match_scrutinee_shapes.jam @@ -0,0 +1,67 @@ +// Exercises `Enum.Variant` patterns when the match scrutinee is more +// than a bare variable: +// match (g.field) — struct field access +// match (fn()) — function call result +// match (x as Color) — `as` cast target +const { assert } = import("test"); + +const Color = enum { Red, Green, Blue }; + +const Game = struct { + color: Color, + score: u32, +}; + +fn scoreOfFieldMatch(g: Game) u32 { + return match (g.color) { + Color.Red { 1 } + Color.Green { 2 } + Color.Blue { 3 } + }; +} + +tfn matchOnStructField() { + var g: Game = { color: Color.Red, score: 0 }; + assert(scoreOfFieldMatch(g), 1); + + g.color = Color.Green; + assert(scoreOfFieldMatch(g), 2); + + g.color = Color.Blue; + assert(scoreOfFieldMatch(g), 3); +} + +fn pickColor(seed: u8) Color { + if (seed == 0) { return Color.Red; } + if (seed == 1) { return Color.Green; } + return Color.Blue; +} + +fn scoreOfCallMatch(seed: u8) u32 { + return match (pickColor(seed)) { + Color.Red { 10 } + Color.Green { 20 } + Color.Blue { 30 } + }; +} + +tfn matchOnCallResult() { + assert(scoreOfCallMatch(0), 10); + assert(scoreOfCallMatch(1), 20); + assert(scoreOfCallMatch(2), 30); +} + +fn scoreOfCastMatch(byte: u8) u32 { + // Re-interpret a u8 as a Color via `as` and match on it. + return match (byte as Color) { + Color.Red { 100 } + Color.Green { 200 } + Color.Blue { 50 } + }; +} + +tfn matchOnAsCast() { + assert(scoreOfCastMatch(0), 100); + assert(scoreOfCastMatch(1), 200); + assert(scoreOfCastMatch(2), 50); +} -- 2.51.2