Add compile time ternary $val ??? <expr> : <expr>.

This commit is contained in:
Christoffer Lerno
2025-08-28 01:56:05 +02:00
parent 90d3f429aa
commit 47316dac59
15 changed files with 86 additions and 26 deletions

View File

@@ -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;

View File

@@ -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};

View File

@@ -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;

View File

@@ -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)

View File

@@ -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 ??? <expr> : <expr>`.
- Deprecated `@select` in favor of `???`.
### Stdlib changes
- Add `==` to `Pair`, `Triple` and TzDateTime. Add print to `Pair` and `Triple`.

View File

@@ -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

View File

@@ -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,

View File

@@ -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:

View File

@@ -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, '<'))

View File

@@ -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 },

View File

@@ -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:

View File

@@ -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;

View File

@@ -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:

View File

@@ -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
}

View File

@@ -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");
}