Add printf format to $assert and $error #2183.

This commit is contained in:
Christoffer Lerno
2025-06-06 23:50:55 +02:00
parent be511b26cd
commit f66cadccd2
8 changed files with 93 additions and 56 deletions

View File

@@ -15,6 +15,7 @@
- `$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.
- Improve error reports when encountering a broken "if-catch".
- Add printf format to `$assert` and `$error` #2183.
### Fixes
- `-2147483648`, MIN literals work correctly.

View File

@@ -1214,7 +1214,7 @@ static inline Ast *parse_assert_stmt(ParseContext *c)
// --- External functions
/**
* ct_assert_stmt ::= CT_ASSERT constant_expression (':' constant_expression) ';'
* ct_assert_stmt ::= CT_ASSERT constant_expression (':' constant_expression (',' constant_expression)*)? ';'
* @param c
* @return
*/
@@ -1227,11 +1227,16 @@ Ast *parse_ct_assert_stmt(ParseContext *c)
{
ASSIGN_EXPRID_OR_RET(ast->assert_stmt.message, parse_constant_expr(c), poisoned_ast);
}
while (try_consume(c, TOKEN_COMMA))
{
ASSIGN_EXPR_OR_RET(Expr *expr, parse_constant_expr(c), poisoned_ast);
vec_add(ast->assert_stmt.args, expr);
}
return consume_eos(c, ast);
}
/**
* ct_error_stmt ::= CT_ERROR constant_expression) ';'
* ct_error_stmt ::= CT_ERROR constant_expression (',' constant_expression)* ';'
* @param c
* @return
*/
@@ -1241,6 +1246,11 @@ Ast *parse_ct_error_stmt(ParseContext *c)
advance_and_verify(c, TOKEN_CT_ERROR);
ast->assert_stmt.expr = 0;
ASSIGN_EXPRID_OR_RET(ast->assert_stmt.message, parse_constant_expr(c), poisoned_ast);
while (try_consume(c, TOKEN_COMMA))
{
ASSIGN_EXPR_OR_RET(Expr *expr, parse_constant_expr(c), poisoned_ast);
vec_add(ast->assert_stmt.args, expr);
}
return consume_eos(c, ast);
}

View File

@@ -38,7 +38,6 @@ 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)
{
@@ -306,56 +305,6 @@ 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];
@@ -584,7 +533,7 @@ bool sema_expr_analyse_builtin_call(SemaContext *context, Expr *expr)
switch (func)
{
case BUILTIN_SPRINTF:
return sema_expr_analyse_sprintf(context, expr);
return sema_expr_analyse_sprintf(context, expr, args[0], &args[1], arg_count - 1);
case BUILTIN_RND:
return sema_expr_analyse_rnd(context, expr);
case BUILTIN_STR_HASH:

View File

@@ -469,6 +469,58 @@ CondResult sema_check_comp_time_bool(SemaContext *context, Expr *expr)
return result;
}
bool sema_expr_analyse_sprintf(SemaContext *context, Expr *expr, Expr *format_string, Expr **args, unsigned num_args)
{
if (!sema_analyse_expr(context, format_string)) return false;
if (!sema_cast_const(format_string))
{
RETURN_SEMA_ERROR(format_string, "Expected a constant format string expression.");
}
for (unsigned i = 0; i < num_args; i++)
{
Expr *e = args[i];
if (!sema_analyse_expr(context, e)) return false;
if (!sema_cast_const(e))
{
RETURN_SEMA_ERROR(e, "Expected a constant expression.");
}
}
if (!expr_is_const_string(format_string))
{
RETURN_SEMA_ERROR(format_string, "Expected a constant format string.");
}
const char *inner_str = format_string->const_expr.bytes.ptr;
ArraySize len = format_string->const_expr.bytes.len;
scratch_buffer_clear();
ArrayIndex current_index = 0;
for (ArraySize i = 0; i < len; i++)
{
char c = inner_str[i];
if (c == '%')
{
i++;
switch (inner_str[i])
{
case 's':
if (current_index == num_args) RETURN_SEMA_ERROR(format_string, "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_string, "Only '%%s' is supported for compile time sprintf.");
}
}
scratch_buffer_append_char(c);
}
if (current_index != num_args)
{
RETURN_SEMA_ERROR(expr, "Too many arguments to sprintf.");
}
expr_rewrite_const_string(expr, scratch_buffer_copy());
return true;
}
static bool sema_binary_is_expr_lvalue(SemaContext *context, Expr *top_expr, Expr *expr, bool *failed_ref)
{

View File

@@ -105,6 +105,7 @@ Expr *sema_expr_analyse_ct_arg_index(SemaContext *context, Expr *index_expr, uns
Expr *sema_ct_eval_expr(SemaContext *context, bool is_type_eval, Expr *inner, bool report_missing);
Expr *sema_resolve_string_ident(SemaContext *context, Expr *inner, bool report_missing);
bool sema_analyse_asm(SemaContext *context, AsmInlineBlock *block, Ast *asm_stmt);
bool sema_expr_analyse_sprintf(SemaContext *context, Expr *expr, Expr *format_string, Expr **args, unsigned num_args);
bool sema_bit_assignment_check(SemaContext *context, Expr *right, Decl *member);
CondResult sema_check_comp_time_bool(SemaContext *context, Expr *expr);

View File

@@ -352,6 +352,19 @@ static inline Expr *sema_dive_into_expression(Expr *expr)
}
}
static bool sema_catch_in_cond(Expr *cond)
{
ASSERT(cond->expr_kind == EXPR_COND && "Assumed cond");
Expr *last = VECLAST(cond->cond_expr);
ASSERT(last);
last = sema_dive_into_expression(last);
// Skip any non-unwraps
return last->expr_kind == EXPR_CATCH;
}
/**
* If we have "if (catch x)", then we want to unwrap x in the else clause.
**/
@@ -1940,7 +1953,11 @@ END:
is_invalid = context->active_scope.is_invalid;
SCOPE_OUTER_END;
if (is_invalid) context->active_scope.is_invalid = is_invalid;
if (!success) return false;
if (!success)
{
if (then_jump && sema_catch_in_cond(cond)) context->active_scope.is_invalid = true;
return false;
}
if (then_jump)
{
sema_unwrappable_from_catch_in_else(context, cond);
@@ -2903,6 +2920,8 @@ bool sema_analyse_ct_assert_stmt(SemaContext *context, Ast *statement)
{
if (message_expr)
{
unsigned len = vec_size(statement->assert_stmt.args);
if (len && !sema_expr_analyse_sprintf(context, message_expr, message_expr, statement->assert_stmt.args, len)) return false;
sema_error_at(context, span, "%.*s", EXPAND_EXPR_STRING(message_expr));
}
else

View File

@@ -0,0 +1,5 @@
fn void main()
{
$error "Foo %s", 1; // #error: Foo 1
$assert 0 > 1 : "Foo %s", 0.0002; // #error: Foo 0.0002
}

View File

@@ -3,7 +3,7 @@ module foo;
fn void foo()
{
int? x;
if (catch err)
if (catch x)
{
z = 1233; // #error: could not be found, did you spell it right
return;