From f66cadccd2de1d81297d120cfa8beea38b5bdb45 Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Fri, 6 Jun 2025 23:50:55 +0200 Subject: [PATCH] Add printf format to `$assert` and `$error` #2183. --- releasenotes.md | 1 + src/compiler/parse_stmt.c | 14 ++++- src/compiler/sema_builtins.c | 53 +------------------ src/compiler/sema_expr.c | 52 ++++++++++++++++++ src/compiler/sema_internal.h | 1 + src/compiler/sema_stmts.c | 21 +++++++- test/test_suite/assert/format_ct_assert.c3 | 5 ++ .../errors/catch_err_single_error.c3 | 2 +- 8 files changed, 93 insertions(+), 56 deletions(-) create mode 100644 test/test_suite/assert/format_ct_assert.c3 diff --git a/releasenotes.md b/releasenotes.md index 39c05d295..8f133bcd2 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -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. diff --git a/src/compiler/parse_stmt.c b/src/compiler/parse_stmt.c index be377dce8..01efe583c 100644 --- a/src/compiler/parse_stmt.c +++ b/src/compiler/parse_stmt.c @@ -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); } diff --git a/src/compiler/sema_builtins.c b/src/compiler/sema_builtins.c index 01a653ca8..15e84520d 100644 --- a/src/compiler/sema_builtins.c +++ b/src/compiler/sema_builtins.c @@ -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: diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index 2fc4bc722..4a590280f 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -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) { diff --git a/src/compiler/sema_internal.h b/src/compiler/sema_internal.h index 15d647b85..943adbe3d 100644 --- a/src/compiler/sema_internal.h +++ b/src/compiler/sema_internal.h @@ -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); diff --git a/src/compiler/sema_stmts.c b/src/compiler/sema_stmts.c index a5a96aad5..bb6c1ff39 100644 --- a/src/compiler/sema_stmts.c +++ b/src/compiler/sema_stmts.c @@ -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 diff --git a/test/test_suite/assert/format_ct_assert.c3 b/test/test_suite/assert/format_ct_assert.c3 new file mode 100644 index 000000000..907661cea --- /dev/null +++ b/test/test_suite/assert/format_ct_assert.c3 @@ -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 +} diff --git a/test/test_suite/errors/catch_err_single_error.c3 b/test/test_suite/errors/catch_err_single_error.c3 index 6cbf30699..91439fa4e 100644 --- a/test/test_suite/errors/catch_err_single_error.c3 +++ b/test/test_suite/errors/catch_err_single_error.c3 @@ -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;