diff --git a/lib/std/io/formatter_private.c3 b/lib/std/io/formatter_private.c3 index dbaf5f035..3a7ed5363 100644 --- a/lib/std/io/formatter_private.c3 +++ b/lib/std/io/formatter_private.c3 @@ -52,7 +52,7 @@ fn uint128? int_from_any(any arg, bool *is_neg) @private switch (arg.type) { case bool: - return *(uint128*)arg; + return (uint128)*(bool*)arg; case ichar: int val = *(ichar*)arg; return (*is_neg = val < 0) ? (~(uint128)val) + 1 : (uint128)val; diff --git a/lib/std/io/io.c3 b/lib/std/io/io.c3 index 8bfe75985..8023b89e3 100644 --- a/lib/std/io/io.c3 +++ b/lib/std/io/io.c3 @@ -139,7 +139,7 @@ macro usz? fprint(out, x) @param [in] format : `The printf-style format string` @return `the number of characters printed` *> -fn usz? fprintf(OutStream out, String format, args...) +fn usz? fprintf(OutStream out, String format, args...) @format(1) { Formatter formatter; formatter.init(&out_putstream_fn, &out); @@ -154,7 +154,7 @@ fn usz? fprintf(OutStream out, String format, args...) @param [in] format : `The printf-style format string` @return `the number of characters printed` *> -fn usz? fprintfn(OutStream out, String format, args...) @maydiscard +fn usz? fprintfn(OutStream out, String format, args...) @format(1) @maydiscard { Formatter formatter; formatter.init(&out_putstream_fn, &out); @@ -249,7 +249,7 @@ fn void? out_putchar_fn(void* data @unused, char c) @private @param [in] format : `The printf-style format string` @return `the number of characters printed` *> -fn usz? printf(String format, args...) @maydiscard +fn usz? printf(String format, args...) @format(0) @maydiscard { Formatter formatter; formatter.init(&out_putchar_fn); @@ -263,7 +263,7 @@ fn usz? printf(String format, args...) @maydiscard @param [in] format : `The printf-style format string` @return `the number of characters printed` *> -fn usz? printfn(String format, args...) @maydiscard +fn usz? printfn(String format, args...) @format(0) @maydiscard { Formatter formatter; formatter.init(&out_putchar_fn); diff --git a/releasenotes.md b/releasenotes.md index f2c0556bd..21744252b 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -43,7 +43,8 @@ - Use `@pool_init()` to set up a temp pool on a thread. Only the main thread has implicit temp pool setup. - `tmem` is now a variable. - Compile test and benchmark functions when invoking `--lsp` #2058. - +- Added `@format` attribute for compile time printf validation #2057. + ### Fixes - Fix address sanitizer to work on MachO targets (e.g. MacOS). - Post and pre-decrement operators switched places for vector elements #2010. @@ -57,6 +58,7 @@ - Bug due to missing cast when doing `$i[$x] = $z`. - Incorrectly allowed getting pointer to a macro #2049. - &self not runtime null-checked in macro #1827. +- Bug when printing a boolean value as an integer using printf. ### Stdlib changes - `new_*` functions in general moved to version without `new_` prefix. diff --git a/src/compiler/compiler_internal.h b/src/compiler/compiler_internal.h index 4048cd02d..d3469013a 100644 --- a/src/compiler/compiler_internal.h +++ b/src/compiler/compiler_internal.h @@ -254,6 +254,7 @@ typedef struct bool is_pure : 1; bool noreturn : 1; bool always_const : 1; + uint8_t format : 8; } CalleeAttributes; typedef struct diff --git a/src/compiler/enums.h b/src/compiler/enums.h index d3c65ca74..85fb351fd 100644 --- a/src/compiler/enums.h +++ b/src/compiler/enums.h @@ -275,6 +275,7 @@ typedef enum ATTRIBUTE_EXPORT, ATTRIBUTE_EXTERN, ATTRIBUTE_FINALIZER, + ATTRIBUTE_FORMAT, ATTRIBUTE_IF, ATTRIBUTE_INLINE, ATTRIBUTE_INIT, diff --git a/src/compiler/sema_decls.c b/src/compiler/sema_decls.c index f20d245de..8005c1d60 100755 --- a/src/compiler/sema_decls.c +++ b/src/compiler/sema_decls.c @@ -1097,6 +1097,7 @@ static inline bool sema_analyse_signature(SemaContext *context, Signature *sig, // Check return type ASSERT(sig->rtype || sig->is_macro); Type *rtype = NULL; + int format_index = (int)sig->attrs.format - 1; if (sig->rtype) { TypeInfo *rtype_info = type_infoptr(sig->rtype); @@ -1180,6 +1181,22 @@ static inline bool sema_analyse_signature(SemaContext *context, Signature *sig, type_to_error_string(method_parent->type), decl->name, type_to_error_string(method_parent->type)); } + if (format_index >= 0) + { + if (format_index >= param_count) + { + RETURN_SEMA_ERROR(decl, "The format '@format()' index was out of range."); + } + if (sig->variadic != VARIADIC_ANY) + { + RETURN_SEMA_ERROR(decl, "'@format()' is only valid for a function or macro with 'args...' style vaargs."); + } + if (sig->vararg_index == format_index) + { + RETURN_SEMA_ERROR(decl, "The format string cannot be a vaarg parameter."); + } + } + // Check parameters for (unsigned i = 0; i < param_count; i++) { @@ -1231,6 +1248,15 @@ static inline bool sema_analyse_signature(SemaContext *context, Signature *sig, : RESOLVE_TYPE_DEFAULT)) return decl_poison(param); param->type = type_info->type; } + if (i == format_index) + { + if (!type_info || type_info->type->canonical != type_string) + { + SourceSpan span = type_info ? type_info->span : param->span; + sema_error_at(context, span, "The '@format()' format string must be be of type 'String'."); + return decl_poison(param); + } + } if (type_info && param->var.no_alias && !type_is_pointer(param->type) && type_flatten(param->type)->type_kind != TYPE_SLICE) { SEMA_ERROR(param, "The parameter was set to @noalias, but it was neither a slice nor a pointer. You need to either remove '@noalias' or use pointer/slice type."); @@ -1239,6 +1265,11 @@ static inline bool sema_analyse_signature(SemaContext *context, Signature *sig, switch (var_kind) { case VARDECL_PARAM_EXPR: + if (i == format_index) + { + SEMA_ERROR(param, "'@format()' cannot be used with lazy arguments, please remove '@format' or make this a regular parameter."); + return decl_poison(param); + } if (!is_macro) { SEMA_ERROR(param, "Only regular parameters are allowed for functions."); @@ -2486,6 +2517,7 @@ static bool sema_analyse_attribute(SemaContext *context, ResolvedAttrData *attr_ [ATTRIBUTE_EXPORT] = ATTR_FUNC | ATTR_GLOBAL | ATTR_CONST | USER_DEFINED_TYPES | ATTR_ALIAS, [ATTRIBUTE_EXTERN] = ATTR_FUNC | ATTR_GLOBAL | ATTR_CONST | USER_DEFINED_TYPES, [ATTRIBUTE_FINALIZER] = ATTR_FUNC, + [ATTRIBUTE_FORMAT] = ATTR_FUNC | ATTR_MACRO | ATTR_FNTYPE, [ATTRIBUTE_IF] = (AttributeDomain)~(ATTR_CALL | ATTR_LOCAL | ATTR_PARAM), [ATTRIBUTE_INIT] = ATTR_FUNC, [ATTRIBUTE_INLINE] = ATTR_FUNC | ATTR_CALL, @@ -2740,6 +2772,37 @@ static bool sema_analyse_attribute(SemaContext *context, ResolvedAttrData *attr_ decl->func_decl.attr_finalizer = true; // Ugly goto PARSE; + case ATTRIBUTE_FORMAT: + if (args != 1) RETURN_SEMA_ERROR(attr, "'@format' expects the index of the format string as the argument, e.g. '@format(1)'."); + if (!sema_analyse_expr(context, expr)) return false; + if (!type_is_integer(expr->type) || !sema_cast_const(expr)) + { + RETURN_SEMA_ERROR(expr, "Expected an integer compile time constant value."); + } + else + { + Int i = expr->const_expr.ixx; + if (int_is_neg(i) || int_icomp(i, 127, BINARYOP_GT)) + { + RETURN_SEMA_ERROR(expr, "The index must be between 0 and 127."); + } + uint16_t val = (uint16_t)i.i.low; + switch (decl->decl_kind) + { + case DECL_FUNC: + case DECL_MACRO: + if (decl->func_decl.signature.attrs.format) break; + decl->func_decl.signature.attrs.format = val + 1; + return true; + case DECL_FNTYPE: + if (decl->fntype_decl.attrs.format) break; + decl->fntype_decl.attrs.format = val + 1; + return true; + default: + UNREACHABLE; + } + RETURN_SEMA_ERROR(attr, "'@format' may not appear twice."); + } case ATTRIBUTE_LINK: if (args < 1) RETURN_SEMA_ERROR(attr, "'@link' requires at least one argument."); Expr *cond = args > 1 ? attr->exprs[0] : NULL; diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index a98f8827a..b87320a45 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -1518,7 +1518,7 @@ INLINE bool sema_call_evaluate_arguments(SemaContext *context, CalledDecl *calle unsigned vaarg_index = sig->vararg_index; Variadic variadic = sig->variadic; Decl **decl_params = callee->params; - + int format_index = (int)sig->attrs.format - 1; // If this is a type call, then we have an implicit first argument. if (callee->struct_var) { @@ -1811,6 +1811,97 @@ SPLAT_NORMAL:; RETURN_SEMA_FUNC_ERROR(callee->definition, call, "The parameter '%s' must be set, did you forget it?", param->name); } call->call_expr.arguments = actual_args; + if (format_index >= 0) goto CHECK_FORMAT; + return true; +CHECK_FORMAT:; + // Check + Expr *expr = actual_args[format_index]; + if (!sema_cast_const(expr) || call->call_expr.va_is_splat) return true; + assert(expr_is_const_string(expr)); + const char *data = expr->const_expr.bytes.ptr; + size_t len = expr->const_expr.bytes.len; + size_t idx = 0; + Expr **vaargs = call->call_expr.varargs; + unsigned vacount = vec_size(vaargs); + for (size_t i = 0; i < len; i++) + { + if (data[i] != '%') continue; + i++; + char c = data[i]; + if (c == '%') continue; + if (idx == vacount) + { + RETURN_SEMA_FUNC_ERROR(callee->definition, call, "Too few arguments provided for the formatting string."); + } + if (c == '.' && data[++i] == '*') + { + idx++; + } + expr = vaargs[idx]; + assert(expr->expr_kind == EXPR_MAKE_ANY); + Type *type = expr->make_any_expr.typeid->const_expr.typeid; + type = type_flatten(type); + while (true) + { + switch (c) + { + case 's': + goto NEXT; + case 'c': + if (!type_is_integer(type)) + { + RETURN_SEMA_ERROR(vaargs[idx], "Expected an integer here."); + } + goto NEXT; + case 'd': + case 'X': + case 'x': + case 'B': + case 'b': + case 'o': + case 'a': + case 'A': + case 'F': + case 'f': + case 'e': + case 'E': + case 'g': + case 'G': + if (!type_is_number_or_bool(type)) + { + RETURN_SEMA_ERROR(vaargs[idx], "Expected a number here, but was %s", type_quoted_error_string(type)); + } + goto NEXT; + case 'p': + if (!type_is_pointer(type) && !type_is_integer(type)) + { + RETURN_SEMA_ERROR(vaargs[idx], "Expected a pointer here."); + } + goto NEXT; + case 'H': + case 'h': + if (!type_flat_is_char_array(type)) + { + RETURN_SEMA_ERROR(vaargs[idx], "Expected a char array here."); + } + goto NEXT; + case '\0': + goto DONE; + case '+': + case '-': + default: + break; + } + c = data[++i]; + } +NEXT: + idx++; + } +DONE: + if (idx < vacount) + { + RETURN_SEMA_FUNC_ERROR(callee->definition, call, "Too many arguments were provided for the formatting string."); + } return true; NO_MATCH_REF: *no_match_ref = true; diff --git a/src/compiler/symtab.c b/src/compiler/symtab.c index ca0e1efa4..8f41d7f57 100644 --- a/src/compiler/symtab.c +++ b/src/compiler/symtab.c @@ -327,6 +327,7 @@ void symtab_init(uint32_t capacity) attribute_list[ATTRIBUTE_EXPORT] = KW_DEF("@export"); attribute_list[ATTRIBUTE_EXTERN] = KW_DEF("@extern"); attribute_list[ATTRIBUTE_FINALIZER] = KW_DEF("@finalizer"); + attribute_list[ATTRIBUTE_FORMAT] = KW_DEF("@format"); attribute_list[ATTRIBUTE_IF] = KW_DEF("@if"); attribute_list[ATTRIBUTE_INIT] = KW_DEF("@init"); attribute_list[ATTRIBUTE_INLINE] = KW_DEF("@inline"); diff --git a/test/src/test_suite_runner.c3 b/test/src/test_suite_runner.c3 index 23acb7167..fdff8f522 100644 --- a/test/src/test_suite_runner.c3 +++ b/test/src/test_suite_runner.c3 @@ -448,7 +448,7 @@ fn void test_file(Path file_path) if (result != 0 && result != 1) { (void)io::copy_to(&&compilation.stdout(), &out); - io::printfn("FAILED - Error(%s): ", result, out); + io::printfn("FAILED - Error(%s): %s", result, out); return; } if (!parse_result(out, settings)) return; diff --git a/test/test_suite/attributes/format_attr.c3 b/test/test_suite/attributes/format_attr.c3 new file mode 100644 index 000000000..c08234c96 --- /dev/null +++ b/test/test_suite/attributes/format_attr.c3 @@ -0,0 +1,19 @@ + +fn void foo(int x) @format(1) // #error: The format '@format()' +{} + +alias Foo = fn void(String format, args...) @format(0); + +fn usz? printfn(String format, args...) @format(1) // #error: The format string cannot +{ + return 0; +} + +fn void foo2(int x) @format(0) // #error: '@format()' is only valid for a function +{} + +fn void foo3(int x, args...) @format(0) // #error: The '@format()' format +{} + +fn void foo4(String #x, args...) @format(0) // #error: '@format()' cannot be used with lazy arguments +{}