mirror of
https://github.com/c3lang/c3c.git
synced 2026-02-27 12:01:16 +00:00
- Added @format attribute for compile time printf validation #2057.
- Bug when printing a boolean value as an integer using printf.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -254,6 +254,7 @@ typedef struct
|
||||
bool is_pure : 1;
|
||||
bool noreturn : 1;
|
||||
bool always_const : 1;
|
||||
uint8_t format : 8;
|
||||
} CalleeAttributes;
|
||||
|
||||
typedef struct
|
||||
|
||||
@@ -275,6 +275,7 @@ typedef enum
|
||||
ATTRIBUTE_EXPORT,
|
||||
ATTRIBUTE_EXTERN,
|
||||
ATTRIBUTE_FINALIZER,
|
||||
ATTRIBUTE_FORMAT,
|
||||
ATTRIBUTE_IF,
|
||||
ATTRIBUTE_INLINE,
|
||||
ATTRIBUTE_INIT,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
19
test/test_suite/attributes/format_attr.c3
Normal file
19
test/test_suite/attributes/format_attr.c3
Normal file
@@ -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
|
||||
{}
|
||||
Reference in New Issue
Block a user