From 47316dac59a63c9455d0d1084c0602b051167bc1 Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Thu, 28 Aug 2025 01:56:05 +0200 Subject: [PATCH] Add compile time ternary `$val ??? : `. --- lib/std/core/array.c3 | 13 +++------ lib/std/core/logging.c3 | 4 +-- lib/std/core/values.c3 | 2 +- lib/std/net/socket.c3 | 6 ++--- releasenotes.md | 2 ++ src/compiler/compiler_internal.h | 1 + src/compiler/enums.h | 1 + src/compiler/expr.c | 2 +- src/compiler/lexer.c | 2 +- src/compiler/parse_expr.c | 14 +++++++--- src/compiler/parse_stmt.c | 1 + src/compiler/sema_expr.c | 33 +++++++++++++++++++++-- src/compiler/tokens.c | 2 ++ test/test_suite/statements/ct_ternary.c3t | 25 +++++++++++++++++ test/unit/stdlib/core/values.c3 | 4 +-- 15 files changed, 86 insertions(+), 26 deletions(-) create mode 100644 test/test_suite/statements/ct_ternary.c3t diff --git a/lib/std/core/array.c3 b/lib/std/core/array.c3 index b82851403..323f69d18 100644 --- a/lib/std/core/array.c3 +++ b/lib/std/core/array.c3 @@ -546,17 +546,10 @@ macro bool @is_valid_list(#expr) @const macro bool @is_valid_fill(left, right, fill_with) { if (@is_empty_macro_slot(fill_with)) return true; - usz left_len = @select($defined(left.len()), left.len(), left.len); - usz right_len = @select($defined(right.len()), right.len(), right.len); + usz left_len = $defined(left.len()) ??? left.len() : left.len; + usz right_len = $defined(right.len()) ??? right.len() : right.len; if (left_len == right_len) return true; return left_len > right_len ? $defined(($typeof(right[0]))fill_with) : $defined(($typeof(left[0]))fill_with); } -macro usz find_len(list) -{ - $if $defined(list.len()): - return list.len(); - $else - return list.len; - $endif -} +macro usz find_len(list) => $defined(list.len()) ??? list.len() : list.len; \ No newline at end of file diff --git a/lib/std/core/logging.c3 b/lib/std/core/logging.c3 index c1bec7f28..892e59597 100644 --- a/lib/std/core/logging.c3 +++ b/lib/std/core/logging.c3 @@ -204,10 +204,10 @@ fn void StderrLogger.log(&self, LogPriority priority, LogCategory category, LogT } alias LogFn = fn void(void*, LogPriority priority, LogCategory category, LogTag tag, String file, String function, int line, String fmt, any[] args); -LogFn current_logfn = @select(env::LIBC, (LogFn)&StderrLogger.log, (LogFn)&NullLogger.log); +LogFn current_logfn = env::LIBC ??? (LogFn)&StderrLogger.log : (LogFn)&NullLogger.log; OnceFlag log_init; Mutex logger_mutex; -Logger current_logger = @select(env::LIBC, &stderr_logger, &null_logger); +Logger current_logger = env::LIBC ??? &stderr_logger : &null_logger; StderrLogger stderr_logger @if (env::LIBC); NullLogger null_logger; LogPriority[256] config_priorities = { [0..255] = ERROR, [CATEGORY_APPLICATION] = INFO, [CATEGORY_TEST] = VERBOSE, [CATEGORY_ASSERT] = WARN}; diff --git a/lib/std/core/values.c3 b/lib/std/core/values.c3 index cef4abcc4..25f0d327b 100644 --- a/lib/std/core/values.c3 +++ b/lib/std/core/values.c3 @@ -43,7 +43,7 @@ macro promote_int(x) @param #value_2 @returns `The selected value.` *> -macro @select(bool $bool, #value_1, #value_2) @builtin +macro @select(bool $bool, #value_1, #value_2) @builtin @deprecated("Use '$bool ? #value_1 : #value_2' instead.") { $if $bool: return #value_1; diff --git a/lib/std/net/socket.c3 b/lib/std/net/socket.c3 index 2aab51f34..8566d36a2 100644 --- a/lib/std/net/socket.c3 +++ b/lib/std/net/socket.c3 @@ -193,9 +193,9 @@ fn usz? Socket.peek(&self, char[] bytes) @dynamic enum SocketShutdownHow : (CInt native_value) { - RECEIVE = @select(env::WIN32, libc::SD_RECEIVE, libc::SHUT_RD), - SEND = @select(env::WIN32, libc::SD_SEND, libc::SHUT_WR), - BOTH = @select(env::WIN32, libc::SD_BOTH, libc::SHUT_RDWR), + RECEIVE = env::WIN32 ??? libc::SD_RECEIVE : libc::SHUT_RD, + SEND = env::WIN32 ??? libc::SD_SEND : libc::SHUT_WR, + BOTH = env::WIN32 ??? libc::SD_BOTH : libc::SHUT_RDWR, } fn void? Socket.shutdown(&self, SocketShutdownHow how) diff --git a/releasenotes.md b/releasenotes.md index 42f6e3d08..2b34b952e 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -82,6 +82,8 @@ - Fix correct `?` after optional function name when reporting type errors. - Make `log` and `exp` no-strip. - `@test`/`@benchmark` on module would attach to interface and regular methods. +- Add compile time ternary `$val ??? : `. +- Deprecated `@select` in favor of `???`. ### Stdlib changes - Add `==` to `Pair`, `Triple` and TzDateTime. Add print to `Pair` and `Triple`. diff --git a/src/compiler/compiler_internal.h b/src/compiler/compiler_internal.h index dd0a4a98f..2442f93fc 100644 --- a/src/compiler/compiler_internal.h +++ b/src/compiler/compiler_internal.h @@ -781,6 +781,7 @@ typedef struct ExprId then_expr; // May be null for elvis! ExprId else_expr; bool grouped : 1; + bool is_const : 1; } ExprTernary; typedef struct diff --git a/src/compiler/enums.h b/src/compiler/enums.h index 15c160269..441c33ec4 100644 --- a/src/compiler/enums.h +++ b/src/compiler/enums.h @@ -1145,6 +1145,7 @@ typedef enum TOKEN_CT_AND, // &&& TOKEN_CT_CONCAT, // +++ TOKEN_CT_OR, // ||| + TOKEN_CT_TERNARY, // ??? // Literals. TOKEN_IDENT, // Any normal ident. TOKEN_CONST_IDENT, // Any purely uppercase ident, diff --git a/src/compiler/expr.c b/src/compiler/expr.c index 6e878a122..1c93261ad 100644 --- a/src/compiler/expr.c +++ b/src/compiler/expr.c @@ -448,7 +448,7 @@ bool expr_is_runtime_const(Expr *expr) } goto RETRY; case EXPR_TERNARY: - ASSERT(!exprid_is_runtime_const(expr->ternary_expr.cond)); + ASSERT(!exprid_is_runtime_const(expr->ternary_expr.cond) && !expr->ternary_expr.is_const); return false; case EXPR_FORCE_UNWRAP: case EXPR_LAST_FAULT: diff --git a/src/compiler/lexer.c b/src/compiler/lexer.c index a3920ab09..e45fef417 100644 --- a/src/compiler/lexer.c +++ b/src/compiler/lexer.c @@ -1314,7 +1314,7 @@ static bool lexer_scan_token_inner(Lexer *lexer) case '^': return match(lexer, '=') ? new_token(lexer, TOKEN_BIT_XOR_ASSIGN, "^=") : new_token(lexer, TOKEN_BIT_XOR, "^"); case '?': - if (match(lexer, '?')) return new_token(lexer, TOKEN_QUESTQUEST, "??"); + if (match(lexer, '?')) return match(lexer, '?') ? new_token(lexer, TOKEN_CT_TERNARY, "???") : new_token(lexer, TOKEN_QUESTQUEST, "??"); return match(lexer, ':') ? new_token(lexer, TOKEN_ELVIS, "?:") : new_token(lexer, TOKEN_QUESTION, "?"); case '<': if (match(lexer, '<')) diff --git a/src/compiler/parse_expr.c b/src/compiler/parse_expr.c index 2f9a1c543..51888ddb8 100644 --- a/src/compiler/parse_expr.c +++ b/src/compiler/parse_expr.c @@ -761,12 +761,17 @@ static Expr *parse_ternary_expr(ParseContext *c, Expr *left_side, SourceSpan lhs ASSERT(expr_ok(left_side)); Expr *expr = expr_new(EXPR_TERNARY, lhs_start); - advance_and_verify(c, TOKEN_QUESTION); + bool is_const = tok_is(c, TOKEN_CT_TERNARY); + advance(c); - // If we have no expression following *or* it is a '!' followed by no expression - // in this case it's an optional expression. - if (!rules[c->tok].prefix || ((c->tok == TOKEN_BANG || c->tok == TOKEN_BANGBANG) && !rules[peek(c)].prefix)) + if (is_const) { + expr->ternary_expr.is_const = true; + } + else if (!rules[c->tok].prefix || ((c->tok == TOKEN_BANG || c->tok == TOKEN_BANGBANG) && !rules[peek(c)].prefix)) + { + // If we have no expression following *or* it is a '!' followed by no expression + // in this case it's an optional expression. expr->expr_kind = EXPR_OPTIONAL; expr->inner_expr = left_side; RANGE_EXTEND_PREV(expr); @@ -2197,6 +2202,7 @@ ParseRule rules[TOKEN_EOF + 1] = { [TOKEN_CT_QNAMEOF] = { parse_ct_call, NULL, PREC_NONE }, [TOKEN_CT_SIZEOF] = { parse_ct_sizeof, NULL, PREC_NONE }, [TOKEN_CT_STRINGIFY] = { parse_ct_stringify, NULL, PREC_NONE }, + [TOKEN_CT_TERNARY] = { NULL, parse_ternary_expr, PREC_TERNARY }, [TOKEN_CT_TYPEFROM] = { parse_type_expr, NULL, PREC_NONE }, [TOKEN_CT_TYPEOF] = { parse_type_expr, NULL, PREC_NONE }, [TOKEN_CT_VAARG] = { parse_ct_arg, NULL, PREC_NONE }, diff --git a/src/compiler/parse_stmt.c b/src/compiler/parse_stmt.c index ddb8d61ee..646d03e79 100644 --- a/src/compiler/parse_stmt.c +++ b/src/compiler/parse_stmt.c @@ -1394,6 +1394,7 @@ Ast *parse_stmt(ParseContext *c) case TOKEN_CT_NAMEOF: case TOKEN_CT_OFFSETOF: case TOKEN_CT_OR: + case TOKEN_CT_TERNARY: case TOKEN_CT_QNAMEOF: case TOKEN_CT_SIZEOF: case TOKEN_CT_STRINGIFY: diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index 7c962401b..028c956a0 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -1027,11 +1027,20 @@ static inline bool sema_expr_analyse_ternary(SemaContext *context, Type *infer_t Expr *left = exprptrzero(expr->ternary_expr.then_expr); Expr *cond = exprptr(expr->ternary_expr.cond); CondResult path = COND_MISSING; + bool is_const = expr->ternary_expr.is_const; // Normal if (left) { if (!sema_analyse_cond_expr(context, cond, &path)) return expr_poison(expr); - if (!sema_analyse_maybe_dead_expr(context, left, path == COND_FALSE, infer_type)) return expr_poison(expr); + if (is_const && path == COND_MISSING) + { + RETURN_SEMA_ERROR(cond, "When using '\?\?\?' the cond expression must evaluate to a constant."); + } + bool is_left = path == COND_TRUE; + if (!is_const || is_left) + { + if (!sema_analyse_maybe_dead_expr(context, left, !is_left, infer_type)) return expr_poison(expr); + } } else { @@ -1069,8 +1078,28 @@ static inline bool sema_expr_analyse_ternary(SemaContext *context, Type *infer_t } } + bool is_right = path == COND_FALSE; + Expr *right = exprptr(expr->ternary_expr.else_expr); - if (!sema_analyse_maybe_dead_expr(context, right, path == COND_TRUE, infer_type)) return expr_poison(expr); + if (!is_const || is_right) + { + if (!sema_analyse_maybe_dead_expr(context, right, !is_right, infer_type)) return expr_poison(expr); + } + + if (is_const) + { + switch (path) + { + case COND_TRUE: + expr_replace(expr, left); + return true; + case COND_FALSE: + expr_replace(expr, right); + return true; + default: + UNREACHABLE + } + } Type *left_canonical = left->type->canonical; Type *right_canonical = right->type->canonical; diff --git a/src/compiler/tokens.c b/src/compiler/tokens.c index 13056f603..f61003456 100644 --- a/src/compiler/tokens.c +++ b/src/compiler/tokens.c @@ -88,6 +88,8 @@ const char *token_type_to_string(TokenType type) return "|||"; case TOKEN_CT_CONCAT: return "+++"; + case TOKEN_CT_TERNARY: + return "???"; case TOKEN_DIV_ASSIGN: return "/="; case TOKEN_DOTDOT: diff --git a/test/test_suite/statements/ct_ternary.c3t b/test/test_suite/statements/ct_ternary.c3t new file mode 100644 index 000000000..d9d9ba2a0 --- /dev/null +++ b/test/test_suite/statements/ct_ternary.c3t @@ -0,0 +1,25 @@ +// #target: macos-x64 +module test; +import std; + +fn int main() +{ + int aa = false ??? 1 + a : 2; + int x = 42; + int bb = true ??? x : 1 + a; + return 0; +} + +/* #expect: test.ll + +define i32 @main() #0 { +entry: + %aa = alloca i32, align 4 + %x = alloca i32, align 4 + %bb = alloca i32, align 4 + store i32 2, ptr %aa, align 4 + store i32 42, ptr %x, align 4 + %0 = load i32, ptr %x, align 4 + store i32 %0, ptr %bb, align 4 + ret i32 0 +} diff --git a/test/unit/stdlib/core/values.c3 b/test/unit/stdlib/core/values.c3 index b4644dce5..0b7395510 100644 --- a/test/unit/stdlib/core/values.c3 +++ b/test/unit/stdlib/core/values.c3 @@ -2,8 +2,8 @@ module std::core::values @test; fn void test_select() { - const int X = @select(true, 1, "Hello"); - const String Y = @select(false, 1, "Hello"); + const int X = true ??? 1 : "Hello"; + const String Y = false ??? 1 : "Hello"; test::eq(X, 1); test::eq(Y, "Hello"); } \ No newline at end of file