@sprintf macro (based on the $$sprintf builtin) allows compile time format strings #1874.

This commit is contained in:
Christoffer Lerno
2025-06-06 03:18:28 +02:00
parent 9baeca3a8e
commit f2daf2e11e
17 changed files with 168 additions and 69 deletions

View File

@@ -78,6 +78,10 @@ macro Char16[] @char16(String $string) @builtin
return $$wstr16($string)[..^2];
}
macro String @sprintf(String $format, ...) @builtin @const
{
return $$sprintf($format, $vasplat);
}
<*
Return a temporary ZString created using the formatting function.

View File

@@ -13,6 +13,7 @@
- Allow inference across `&&` #2172.
- Added support for custom file extensions in project.json targets.
- `$eval` now also works with `@foo`, `#foo`, `$Foo` and `$foo` parameters #2114.
- `@sprintf` macro (based on the `$$sprintf` builtin) allows compile time format strings #1874.
### Fixes
- `-2147483648`, MIN literals work correctly.

View File

@@ -2273,6 +2273,7 @@ void expr_contract_array(ExprConst *expr_const, ConstKind contract_type);
bool expr_const_will_overflow(const ExprConst *expr, TypeKind kind);
const char *expr_const_to_error_string(const ExprConst *expr);
bool expr_const_float_fits_type(const ExprConst *expr_const, TypeKind kind);
void expr_const_to_scratch_buffer(const ExprConst *expr);
// --- Lexer functions

View File

@@ -500,6 +500,7 @@ typedef enum
BUILTIN_SCATTER,
BUILTIN_SELECT,
BUILTIN_SET_ROUNDING_MODE,
BUILTIN_SPRINTF,
BUILTIN_STR_HASH,
BUILTIN_STR_UPPER,
BUILTIN_STR_LOWER,
@@ -1133,8 +1134,6 @@ typedef enum
// We want to parse #foo separately.
TOKEN_HASH_IDENT, // #foobar
TOKEN_HASH_CONST_IDENT, // #FOOBAR
TOKEN_HASH_TYPE_IDENT, // #Foobar
TOKEN_AT_IDENT, // @macro
TOKEN_AT_CONST_IDENT, // @MACRO
@@ -1259,11 +1258,11 @@ typedef enum
TOKEN_CT_VAEXPR, // $vaexpr,
TOKEN_CT_VASPLAT, // $vasplat,
TOKEN_LAST_KEYWORD = TOKEN_CT_VASPLAT,
TOKEN_DOCS_START, // <*
TOKEN_DOCS_END, // *>
TOKEN_DOCS_START, // <*
TOKEN_DOCS_END, // *>
TOKEN_DOCS_EOL,
TOKEN_EOF, // \n - SHOULD ALWAYS BE THE LAST TOKEN.
TOKEN_EOF, // \n - SHOULD ALWAYS BE THE LAST TOKEN.
TOKEN_LAST = TOKEN_EOF,
} TokenType;

View File

@@ -351,6 +351,7 @@ EXIT:;
default:
break;
}
assert(type != TOKEN_INVALID_TOKEN);
return new_token(lexer, type, interned_string);
}
@@ -1219,6 +1220,13 @@ EXIT:;
return true;
}
static bool next_is_ident(Lexer *lexer)
{
size_t i = 0;
while (lexer->current[i] == '_') i++;
return char_is_lower(lexer->current[i]);
}
static bool lexer_scan_token_inner(Lexer *lexer)
{
// Now skip the whitespace.
@@ -1249,7 +1257,8 @@ static bool lexer_scan_token_inner(Lexer *lexer)
case '"':
return scan_string(lexer);
case '#':
return scan_ident(lexer, TOKEN_HASH_IDENT, TOKEN_HASH_CONST_IDENT, TOKEN_HASH_TYPE_IDENT, '#');
if (!next_is_ident(lexer)) return new_token(lexer, TOKEN_HASH, "#");
return scan_ident(lexer, TOKEN_HASH_IDENT, TOKEN_INVALID_TOKEN, TOKEN_INVALID_TOKEN, '#');
case '$':
if (match(lexer, '$'))
{

View File

@@ -1052,6 +1052,7 @@ void llvm_emit_builtin_call(GenContext *c, BEValue *result_value, Expr *expr)
case BUILTIN_WIDESTRING_16:
case BUILTIN_WIDESTRING_32:
case BUILTIN_RND:
case BUILTIN_SPRINTF:
UNREACHABLE
case BUILTIN_NONE:
UNREACHABLE

View File

@@ -292,7 +292,68 @@ bool expr_const_will_overflow(const ExprConst *expr, TypeKind kind)
}
void expr_const_to_scratch_buffer(const ExprConst *expr)
{
switch (expr->const_kind)
{
case CONST_POINTER:
if (!expr->ptr)
{
scratch_buffer_append("null");
}
else
{
scratch_buffer_printf("%p", (void *)(intptr_t)expr->ptr);
}
return;
case CONST_BOOL:
scratch_buffer_append(expr->b ? "true" : "false");
return;
case CONST_INTEGER:
scratch_buffer_append(int_to_str(expr->ixx, 10, false));
return;
case CONST_FLOAT:
scratch_buffer_printf("%g", expr->fxx.f);
return;
case CONST_STRING:
scratch_buffer_append_len(expr->bytes.ptr, expr->bytes.len);
return;
case CONST_BYTES:
scratch_buffer_append("<binary data>");
return;
case CONST_REF:
scratch_buffer_append(expr->global_ref->name);
return;
case CONST_FAULT:
scratch_buffer_append(expr->fault->name);
return;
case CONST_ENUM:
scratch_buffer_append(expr->enum_val->name);
return;
case CONST_TYPEID:
scratch_buffer_append(expr->typeid->name);
return;
case CONST_MEMBER:
scratch_buffer_append(expr->member.decl->name);
return;
case CONST_SLICE:
case CONST_INITIALIZER:
scratch_buffer_append("constant list");
return;
case CONST_UNTYPED_LIST:
{
scratch_buffer_append("{");
FOREACH_IDX(i, Expr *, e, expr->untyped_list)
{
if (i != 0) scratch_buffer_append(", ");
expr_const_to_scratch_buffer(&e->const_expr);
}
scratch_buffer_append("}");
return;
}
}
UNREACHABLE
}
const char *expr_const_to_error_string(const ExprConst *expr)
{
switch (expr->const_kind)

View File

@@ -2071,6 +2071,7 @@ ParseRule rules[TOKEN_EOF + 1] = {
[TOKEN_LPAREN] = { parse_grouping_expr, parse_call_expr, PREC_CALL },
[TOKEN_BANGBANG] = { parse_unary_expr, parse_force_unwrap_expr, PREC_CALL },
[TOKEN_LBRACKET] = { NULL, parse_subscript_expr, PREC_CALL },
[TOKEN_LBRACE] = { parse_initializer_list, parse_generic_expr, PREC_PRIMARY },
[TOKEN_MINUS] = { parse_unary_expr, parse_binary, PREC_ADDITIVE },
[TOKEN_PLUS] = { parse_unary_expr, parse_binary, PREC_ADDITIVE },
[TOKEN_DIV] = { NULL, parse_binary, PREC_MULTIPLICATIVE },
@@ -2124,29 +2125,27 @@ ParseRule rules[TOKEN_EOF + 1] = {
[TOKEN_CT_TYPE_IDENT] = { parse_type_identifier, NULL, PREC_NONE },
[TOKEN_HASH_IDENT] = { parse_hash_ident, NULL, PREC_NONE },
[TOKEN_AT_IDENT] = { parse_identifier, NULL, PREC_NONE },
//[TOKEN_HASH_TYPE_IDENT] = { parse_type_identifier, NULL, PREC_NONE }
[TOKEN_ELLIPSIS] = { parse_splat, NULL, PREC_NONE },
[TOKEN_FN] = { parse_lambda, NULL, PREC_NONE },
[TOKEN_CT_SIZEOF] = { parse_ct_sizeof, NULL, PREC_NONE },
[TOKEN_CT_ALIGNOF] = { parse_ct_call, NULL, PREC_NONE },
[TOKEN_CT_ASSIGNABLE] = { parse_ct_castable, NULL, PREC_NONE },
[TOKEN_CT_DEFINED] = { parse_ct_defined, NULL, PREC_NONE },
[TOKEN_CT_IS_CONST] = {parse_ct_is_const, NULL, PREC_NONE },
[TOKEN_CT_EMBED] = { parse_ct_embed, NULL, PREC_NONE },
[TOKEN_CT_EVALTYPE] = { parse_type_expr, NULL, PREC_NONE },
[TOKEN_CT_EVAL] = { parse_ct_eval, NULL, PREC_NONE },
[TOKEN_CT_FEATURE] = { parse_ct_call, NULL, PREC_NONE },
[TOKEN_CT_EXTNAMEOF] = { parse_ct_call, NULL, PREC_NONE },
[TOKEN_CT_OFFSETOF] = { parse_ct_call, NULL, PREC_NONE },
[TOKEN_CT_FEATURE] = { parse_ct_call, NULL, PREC_NONE },
[TOKEN_CT_IS_CONST] = {parse_ct_is_const, NULL, PREC_NONE },
[TOKEN_CT_NAMEOF] = { parse_ct_call, NULL, PREC_NONE },
[TOKEN_CT_OFFSETOF] = { parse_ct_call, NULL, PREC_NONE },
[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_TYPEFROM] = { parse_type_expr, NULL, PREC_NONE },
[TOKEN_CT_TYPEOF] = { parse_type_expr, NULL, PREC_NONE },
[TOKEN_CT_STRINGIFY] = { parse_ct_stringify, NULL, PREC_NONE },
[TOKEN_CT_EVALTYPE] = { parse_type_expr, NULL, PREC_NONE },
[TOKEN_LBRACE] = { parse_initializer_list, parse_generic_expr, PREC_PRIMARY },
[TOKEN_CT_VACOUNT] = { parse_ct_arg, NULL, PREC_NONE },
[TOKEN_CT_VAARG] = { parse_ct_arg, NULL, PREC_NONE },
[TOKEN_CT_VATYPE] = { parse_type_expr, NULL, PREC_NONE },
[TOKEN_CT_VAEXPR] = { parse_ct_arg, NULL, PREC_NONE },
[TOKEN_CT_VACONST] = { parse_ct_arg, NULL, PREC_NONE },
[TOKEN_CT_VACOUNT] = { parse_ct_arg, NULL, PREC_NONE },
[TOKEN_CT_VAEXPR] = { parse_ct_arg, NULL, PREC_NONE },
[TOKEN_CT_VATYPE] = { parse_type_expr, NULL, PREC_NONE },
};

View File

@@ -1574,10 +1574,6 @@ bool parse_parameters(ParseContext *c, Decl ***params_ref, Variadic *variadic, i
ref = true;
param_kind = VARDECL_PARAM;
break;
case TOKEN_HASH_TYPE_IDENT:
// #Foo (not allowed)
PRINT_ERROR_HERE("An unevaluated expression can never be a type, did you mean to use $Type?");
return false;
case TOKEN_HASH_IDENT:
// expression #foo
name = symstr(c);
@@ -2815,8 +2811,6 @@ static inline bool parse_contract_param(ParseContext *c, AstId *docs, AstId **do
case TOKEN_CT_IDENT:
case TOKEN_TYPE_IDENT:
case TOKEN_CT_CONST_IDENT:
case TOKEN_HASH_CONST_IDENT:
case TOKEN_HASH_TYPE_IDENT:
case TOKEN_CT_TYPE_IDENT:
case TOKEN_CONST_IDENT:
case TOKEN_HASH_IDENT:

View File

@@ -1259,8 +1259,6 @@ Ast *parse_stmt(ParseContext *c)
case TOKEN_LBRACE:
return parse_compound_stmt(c);
case TYPELIKE_TOKENS:
case TOKEN_HASH_TYPE_IDENT:
case TOKEN_HASH_CONST_IDENT:
case TOKEN_HASH_IDENT:
case TOKEN_IDENT:
case TOKEN_CONST_IDENT:

View File

@@ -179,7 +179,6 @@ static inline bool parse_next_may_be_type_or_ident(ParseContext *c)
{
case TOKEN_CONST_IDENT:
case TOKEN_IDENT:
case TOKEN_HASH_CONST_IDENT:
case TOKEN_HASH_IDENT:
case TOKEN_CT_IDENT:
case TOKEN_CT_CONST_IDENT:

View File

@@ -38,6 +38,7 @@ static inline bool sema_expr_analyse_swizzle(SemaContext *context, Expr *expr, b
static inline int builtin_expected_args(BuiltinFunction func);
static inline bool is_valid_atomicity(SemaContext *context, Expr *expr);
static bool sema_check_alignment_expression(SemaContext *context, Expr *align);
static bool sema_expr_analyse_sprintf(SemaContext *context, Expr *expr);
static bool sema_expr_is_valid_mask_for_value(SemaContext *context, Expr *expr, Expr *value)
{
@@ -305,6 +306,56 @@ bool sema_expr_analyse_rnd(SemaContext *context UNUSED, Expr *expr)
return true;
}
static bool sema_expr_analyse_sprintf(SemaContext *context, Expr *expr)
{
Expr **args = expr->call_expr.arguments;
FOREACH(Expr *, e, args)
{
if (!sema_analyse_expr(context, e)) return false;
if (!sema_cast_const(e))
{
RETURN_SEMA_ERROR(e, "Expected a constant expression.");
}
}
Expr *format = args[0];
if (!expr_is_const_string(format))
{
RETURN_SEMA_ERROR(format, "Expected a constant format string.");
}
const char *inner_str = format->const_expr.bytes.ptr;
ArraySize len = format->const_expr.bytes.len;
scratch_buffer_clear();
ArrayIndex current_index = 1;
ArraySize param_count = vec_size(args);
for (ArraySize i = 0; i < len; i++)
{
char c = inner_str[i];
if (c == '%')
{
i++;
switch (inner_str[i])
{
case 's':
if (current_index == param_count) RETURN_SEMA_ERROR(format, "Too many arguments in format string.");
expr_const_to_scratch_buffer(&(args[current_index++]->const_expr));
continue;
case '%':
scratch_buffer_append_char('%');
continue;
default:
RETURN_SEMA_ERROR(format, "Only '%%s' is supported for compile time sprintf.");
}
}
scratch_buffer_append_char(c);
}
if (current_index != param_count)
{
RETURN_SEMA_ERROR(format, "Too many arguments to sprintf.");
}
expr_rewrite_const_string(expr, scratch_buffer_copy());
return true;
}
bool sema_expr_analyse_str_hash(SemaContext *context, Expr *expr)
{
Expr *inner = expr->call_expr.arguments[0];
@@ -532,6 +583,8 @@ bool sema_expr_analyse_builtin_call(SemaContext *context, Expr *expr)
switch (func)
{
case BUILTIN_SPRINTF:
return sema_expr_analyse_sprintf(context, expr);
case BUILTIN_RND:
return sema_expr_analyse_rnd(context, expr);
case BUILTIN_STR_HASH:
@@ -593,6 +646,7 @@ bool sema_expr_analyse_builtin_call(SemaContext *context, Expr *expr)
case BUILTIN_STR_FIND:
case BUILTIN_WIDESTRING_16:
case BUILTIN_WIDESTRING_32:
case BUILTIN_SPRINTF:
UNREACHABLE
case BUILTIN_VECCOMPGE:
case BUILTIN_VECCOMPEQ:
@@ -1199,10 +1253,11 @@ static inline int builtin_expected_args(BuiltinFunction func)
{
switch (func)
{
case BUILTIN_SPRINTF:
case BUILTIN_SYSCALL:
case BUILTIN_WIDESTRING_16:
case BUILTIN_WIDESTRING_32:
return -1;
return -1;
case BUILTIN_SWIZZLE:
return -2;
case BUILTIN_SWIZZLE2:

View File

@@ -1205,7 +1205,7 @@ static inline bool sema_analyse_signature(SemaContext *context, Signature *sig,
{
RETURN_SEMA_ERROR(decl, "The format '@format()' index was out of range.");
}
if (sig->variadic != VARIADIC_ANY)
if (sig->variadic != VARIADIC_ANY && !is_macro)
{
RETURN_SEMA_ERROR(decl, "'@format()' is only valid for a function or macro with 'args...' style vaargs.");
}

View File

@@ -2919,42 +2919,9 @@ bool sema_analyse_ct_echo_stmt(SemaContext *context, Ast *statement)
return false;
}
printf("] ");
switch (message->const_expr.const_kind)
{
case CONST_FLOAT:
printf("%f\n", (double)message->const_expr.fxx.f);
break;
case CONST_INTEGER:
puts(int_to_str(message->const_expr.ixx, 10, false));
break;
case CONST_BOOL:
puts(message->const_expr.b ? "true" : "false");
break;
case CONST_REF:
puts(message->const_expr.global_ref->name);
break;
case CONST_FAULT:
puts(message->const_expr.fault->name);
break;
case CONST_ENUM:
puts(message->const_expr.enum_val->name);
break;
case CONST_STRING:
printf("%.*s\n", EXPAND_EXPR_STRING(message));
break;
case CONST_POINTER:
printf("%p\n", (void*)(intptr_t)message->const_expr.ptr);
break;
case CONST_TYPEID:
puts(type_to_error_string(message->const_expr.typeid));
break;
case CONST_BYTES:
case CONST_SLICE:
case CONST_INITIALIZER:
case CONST_UNTYPED_LIST:
case CONST_MEMBER:
RETURN_SEMA_ERROR(message, "Unsupported type for '$echo'");
}
scratch_buffer_clear();
expr_const_to_scratch_buffer(&message->const_expr);
puts(scratch_buffer_to_string());
statement->ast_kind = AST_NOP_STMT;
return true;
}

View File

@@ -280,6 +280,7 @@ void symtab_init(uint32_t capacity)
builtin_list[BUILTIN_STR_FIND] = KW_DEF("str_find");
builtin_list[BUILTIN_SWIZZLE] = KW_DEF("swizzle");
builtin_list[BUILTIN_SWIZZLE2] = KW_DEF("swizzle2");
builtin_list[BUILTIN_SPRINTF] = KW_DEF("sprintf");
builtin_list[BUILTIN_SQRT] = KW_DEF("sqrt");
builtin_list[BUILTIN_SYSCALL] = KW_DEF("syscall");
builtin_list[BUILTIN_SYSCLOCK] = KW_DEF("sysclock");

View File

@@ -152,10 +152,6 @@ const char *token_type_to_string(TokenType type)
return "CT_TYPE_IDENT";
case TOKEN_HASH_IDENT:
return "HASH_IDENT";
case TOKEN_HASH_CONST_IDENT:
return "HASH_CONST_IDENT";
case TOKEN_HASH_TYPE_IDENT:
return "HASH_TYPE_IDENT";
case TOKEN_CONST_IDENT:
return "CONST_IDENT";
case TOKEN_TYPE_IDENT:

View File

@@ -0,0 +1,14 @@
// #target: macos-x64
module test;
fn void main()
{
var $foo = "hello";
var $c = { 1, 3 };
String x = $$sprintf("%s %s", $foo, $c);
String y = @sprintf("%s %s", $c, $foo);
}
/* #expect: test.ll
@.str = private unnamed_addr constant [13 x i8] c"hello {1, 3}\00", align 1
@.str.1 = private unnamed_addr constant [13 x i8] c"{1, 3} hello\00", align 1