From d4f0e2048f5239e5bf61ba919be50ee96fd30349 Mon Sep 17 00:00:00 2001 From: Charlie Gordon Date: Wed, 27 Aug 2025 00:18:22 +0200 Subject: [PATCH] assert: output more detailed messages * output values of non-literal assert expression comparison operands * output values of non-literal static_assert operands * support `static_assert(expression)` and `static_assert(expression, error_string)` * add tests --- analyser/module_analyser.c2 | 106 ++++++++++++++++-- analyser/module_analyser_stmt.c2 | 58 +++++++++- ast/expr.c2 | 22 ++++ ast/integer_literal.c2 | 2 - ast/static_assert.c2 | 4 +- generator/ast_visitor.c2 | 4 +- libs/libc/c2_assert.c2i | 10 +- parser/c2_parser.c2 | 7 +- test/c_generator/stmts/assert.c2t | 13 ++- .../static_asserts/static_assert_int.c2 | 26 ++++- .../static_asserts/static_assert_ok.c2 | 8 ++ 11 files changed, 229 insertions(+), 31 deletions(-) diff --git a/analyser/module_analyser.c2 b/analyser/module_analyser.c2 index 78611a14..f0a5106d 100644 --- a/analyser/module_analyser.c2 +++ b/analyser/module_analyser.c2 @@ -28,6 +28,7 @@ import label_vector local; import name_vector local; import src_loc local; import scope; +import string_buffer; import string_pool; import warning_flags; import struct_func_list as sf_list; @@ -37,6 +38,7 @@ import value_type local; import ctype; import stdarg local; import stdlib; +import string; const u32 MaxDepth = 8; const u32 LHS = 0x01; @@ -509,37 +511,117 @@ fn void Analyser.handleTypeDecl(void* arg, Decl* d) { ma.analyseGlobalDecl(d); } +fn void Analyser.printLiteral(Analyser* ma, string_buffer.Buf *buf, + const char* s, const Expr* e, Value val, + bool as_is = false) +{ + buf.add(s); + u32 off = buf.size(); + e.printLiteral(buf); + const char* str = val.str(); + if (as_is) { + buf.print(" is %s", str); + } else + if (string.strcmp((const char*)buf.data() + off, str)) { + buf.print(" (%s)", str); + } +} + +fn bool isTrueLiteral(Expr* e) { + switch (e.getKind()) { + case IntegerLiteral: return ((IntegerLiteral*)e).isDecimal(); + case FloatLiteral: return true; + case BooleanLiteral: return true; + case CharLiteral: return false; + case StringLiteral: return false; + case Nil: return false; + default: return false; + } +} + fn void Analyser.handleStaticAssert(void* arg, StaticAssert* sa) { Analyser* ma = arg; - - Expr* lhs = sa.getLhs(); - Expr* rhs = sa.getRhs(); + Expr* lhs = sa.getLHS(); + Expr* rhs = sa.getRHS(); + const char* msg = "static_assert failed"; + if (rhs && rhs.isStringLiteral()) { + StringLiteral* s = (StringLiteral*)rhs; + msg = ma.astPool.idx2str(s.getTextIndex()); + rhs = nil; + } // push a dummy declaration so ma.popCheck() does not set ma.scope to nil if (!ma.pushCheck(nil, sa.getAST().getPtr(), nil)) return; QualType t1 = ma.analyseConstExpr(&lhs, true); - QualType t2 = t1.isInvalid() ? t1 : ma.analyseConstExpr(&rhs, true); + QualType t2 = t1; + if (!t1.isInvalid() && rhs) t2 = ma.analyseConstExpr(&rhs, true); ma.popCheck(); if (t1.isInvalid() || t2.isInvalid()) return; - bool error = false; if (!lhs.isCtv()) { ma.errorRange(lhs.getLoc(), lhs.getRange(), "static_assert element is not a compile-time value"); - error = true; + return; } - if (!rhs.isCtv()) { + if (rhs && !rhs.isCtv()) { ma.errorRange(rhs.getLoc(), rhs.getRange(), "static_assert element is not a compile-time value"); - error = true; + return; } - if (error) return; + Expr* e = lhs; + BinaryOpcode opcode = Equal; + if (lhs.isComparison() && !rhs) { + BinaryOperator* b = (BinaryOperator*)e; + opcode = b.getOpcode(); + lhs = b.getLHS(); + if (lhs.isImplicitCast()) lhs = ((ImplicitCastExpr*)lhs).getInner(); + rhs = b.getRHS(); + if (rhs.isImplicitCast()) rhs = ((ImplicitCastExpr*)rhs).getInner(); + } Value val1 = ast.evalExpr(lhs); - Value val2 = ast.evalExpr(rhs); + Value val2 = {} + if (rhs) { + val2 = ast.evalExpr(rhs); + if (val1.is_equal(&val2)) return; + } else { + if (!val1.isZero()) return; + } - if (!val1.is_equal(&val2)) { - ma.errorRange(rhs.getStartLoc(), rhs.getRange(), "static_assert failed, expected %s, got %s", val1.str(), val2.str()); + char[256] tmp; + string_buffer.Buf buf.init(tmp, elemsof(tmp), true, false, 0); + + if (rhs) { + if (opcode == Equal) { + if (!lhs.isLiteral() && rhs.isLiteral()) { + ma.printLiteral(&buf, "", lhs, val1, true); + ma.printLiteral(&buf, ", expected ", rhs, val2); + } else + if (lhs.isLiteral() && !rhs.isLiteral()) { + ma.printLiteral(&buf, "", rhs, val2, true); + ma.printLiteral(&buf, ", expected ", lhs, val1); + } else { + ma.printLiteral(&buf, "values differ: ", lhs, val1); + ma.printLiteral(&buf, " <> ", rhs, val2); + } + } else { + buf.add("expected "); + lhs.printLiteral(&buf); + buf.print(" %s ", opcode.str()); + rhs.printLiteral(&buf); + if (!isTrueLiteral(lhs)) { + ma.printLiteral(&buf, ", ", lhs, val1, true); + } + if (!isTrueLiteral(rhs)) { + ma.printLiteral(&buf, ", ", rhs, val2, true); + } + } + } else { + buf.add("expected "); + e.printLiteral(&buf); + buf.add(" != 0"); } + ma.errorRange(e.getStartLoc(), e.getRange(), "%s: %s", msg, buf.str()); + buf.deinit(); } fn void Analyser.handleVarDecl(void* arg, VarDecl* v) { diff --git a/analyser/module_analyser_stmt.c2 b/analyser/module_analyser_stmt.c2 index b753c39a..fdcf6faa 100644 --- a/analyser/module_analyser_stmt.c2 +++ b/analyser/module_analyser_stmt.c2 @@ -445,24 +445,70 @@ fn void Analyser.analyseAssertStmt(Analyser* ma, Stmt* s) { // add c2_assert.fail func designator Expr* func = ma.builder.actOnMemberExpr(nil, ma.c2_assert_idx, 0, loc, ma.c2_assert_fail_idx); // encode expression as a string + u32 num_args = 1; + Expr*[3] args; char[128] tmp; string_buffer.Buf buf.init(tmp, elemsof(tmp), true, false, 0); - inner.printLiteral(&buf); - u32 msg_len = buf.size(); - u32 msg_idx = ma.astPool.add(buf.data(), msg_len, true); + string_buffer.Buf* out = &buf; + encode_expression(out, inner); + if (inner.isComparison()) { + BinaryOperator* b = (BinaryOperator*)inner; + Expr* lhs = b.getLHS(); + const char* fmt1 = lhs.isLiteral() ? nil : get_type_format(lhs); + if (fmt1) { + out.add(", "); + encode_expression(out, lhs); + out.print(": %%%s", fmt1); + args[num_args++] = lhs; + } + Expr* rhs = b.getRHS(); + const char* fmt2 = rhs.isLiteral() ? nil : get_type_format(rhs); + if (fmt2) { + out.add(", "); + encode_expression(out, rhs); + out.print(": %%%s", fmt2); + args[num_args++] = rhs; + } + } + u32 msg_len = out.size(); + u32 msg_idx = ma.astPool.add(out.data(), msg_len, true); buf.deinit(); SrcLoc eloc = inner.getStartLoc(); SrcLoc eloc_end = inner.getEndLoc(); - Expr* arg = ma.builder.actOnStringLiteral(eloc, eloc_end - eloc, msg_idx, msg_len); + args[0] = ma.builder.actOnStringLiteral(eloc, eloc_end - eloc, msg_idx, msg_len); - // create call expression `c2_assert.c2_assert_fail("expression") + // create call expression `c2_assert.c2_assert_fail("expression...", ...) Expr** callp = a.getCall2(); - *callp = ma.builder.actOnCallExpr(loc, eloc_end + 1, func, &arg, 1); + *callp = ma.builder.actOnCallExpr(loc, eloc_end + 1, func, args, num_args); qt = ma.analyseExpr(a.getCall2(), true, RHS); if (qt.isInvalid()) return; } } +// encode expression as part of a format string +fn void encode_expression(string_buffer.Buf* out, Expr* e) { + char[128] tmp; + string_buffer.Buf buf.init(tmp, elemsof(tmp), false, false, 0); + e.printLiteral(&buf); + const char* s = buf.data(); + for (const char* p = s; *p; p++) { + if (*p == '%') out.add1('%'); + out.add1(*p); + } + buf.deinit(); +} + +fn const char* get_type_format(Expr* e) { + QualType qt = e.getType(); + QualType canon = qt.getCanonicalType(); + if (canon.isPointer() || canon.isFunction()) return "p"; + if (canon.isBuiltin()) { + return canon.isFloat() ? "g" : "d"; + } + if (canon.isEnum()) return "d"; + return nil; +} + fn void Analyser.analyseReturnStmt(Analyser* ma, Stmt* s) { ReturnStmt* r = (ReturnStmt*)s; diff --git a/ast/expr.c2 b/ast/expr.c2 index 9812dfd5..f774ba1e 100644 --- a/ast/expr.c2 +++ b/ast/expr.c2 @@ -202,6 +202,28 @@ public fn bool Expr.isStringLiteral(const Expr* e) { return e.getKind() == StringLiteral; } +public fn bool Expr.isLiteral(const Expr* e) { + switch (e.getKind()) { + case IntegerLiteral: + case FloatLiteral: + case BooleanLiteral: + case CharLiteral: + case StringLiteral: + case Nil: + return true; + default: + break; + } + return false; +} + +public fn bool Expr.isComparison(const Expr* e) { + if (e.getKind() != BinaryOperator) return false; + BinaryOperator* binop = (BinaryOperator*)e; + BinaryOpcode opcode = binop.getOpcode(); + return opcode.isComparison(); +} + public fn bool Expr.isNil(const Expr* e) { return e.getKind() == Nil; } diff --git a/ast/integer_literal.c2 b/ast/integer_literal.c2 index 0484c9b8..a91884e3 100644 --- a/ast/integer_literal.c2 +++ b/ast/integer_literal.c2 @@ -58,11 +58,9 @@ fn u32 IntegerLiteral.getSrcLen(const IntegerLiteral* e) { return e.base.base.integerLiteralBits.src_len; } -/* public fn bool IntegerLiteral.isDecimal(const IntegerLiteral* e) { return e.base.base.integerLiteralBits.radix == Default; } -*/ fn void printBinary(string_buffer.Buf* out, u64 value, u32 len) { char[2+64+1] tmp; diff --git a/ast/static_assert.c2 b/ast/static_assert.c2 index f5fd0f04..80292678 100644 --- a/ast/static_assert.c2 +++ b/ast/static_assert.c2 @@ -47,11 +47,11 @@ public fn AST* StaticAssert.getAST(const StaticAssert* d) { return idx2ast(d.ast_idx); } -public fn Expr* StaticAssert.getLhs(const StaticAssert* d) { +public fn Expr* StaticAssert.getLHS(const StaticAssert* d) { return d.lhs; } -public fn Expr* StaticAssert.getRhs(const StaticAssert* d) { +public fn Expr* StaticAssert.getRHS(const StaticAssert* d) { return d.rhs; } diff --git a/generator/ast_visitor.c2 b/generator/ast_visitor.c2 index e55a653f..f7dc6907 100644 --- a/generator/ast_visitor.c2 +++ b/generator/ast_visitor.c2 @@ -41,8 +41,8 @@ public fn void Visitor.free(Visitor* v) { // only used by plugins public fn void Visitor.handleAssert(Visitor* v, StaticAssert* a) @(unused) { - v.handleExpr(a.getLhs()); - v.handleExpr(a.getRhs()); + v.handleExpr(a.getLHS()); + v.handleExpr(a.getRHS()); } public fn void Visitor.handle(Visitor* v, Decl* d) { diff --git a/libs/libc/c2_assert.c2i b/libs/libc/c2_assert.c2i index b98de7df..cd5367bd 100644 --- a/libs/libc/c2_assert.c2i +++ b/libs/libc/c2_assert.c2i @@ -1,14 +1,20 @@ module c2_assert; import stdio local; +import stdarg local; import stdlib local; public fn i32 c2_assert_fail(const char* filename @(auto_file), u32 line @(auto_line), const char* funcname @(auto_func), - const char* condstr) + const char* fmt @(printf_format), ...) { - dprintf(2, "%s:%d: function %s: Assertion failed: %s\n", filename, line, funcname, condstr); + va_list args; + va_start(args, fmt); + dprintf(2, "%s:%d: function %s: assertion failed: ", filename, line, funcname); + vdprintf(2, fmt, args); + dprintf(2, "\n"); + va_end(args); abort(); return 0; } diff --git a/parser/c2_parser.c2 b/parser/c2_parser.c2 index b6fce041..f29535be 100644 --- a/parser/c2_parser.c2 +++ b/parser/c2_parser.c2 @@ -832,8 +832,11 @@ fn void Parser.parseStaticAssert(Parser* p) { p.expectAndConsume(LParen); Expr* lhs = p.parseExpr(); - p.expectAndConsume(Comma); - Expr* rhs = p.parseExpr(); + Expr* rhs = nil; + if (p.tok.kind == Comma) { + p.consumeToken(); + rhs = p.parseExpr(); + } p.expectAndConsume(RParen); p.expectAndConsume(Semicolon); p.builder.actOnStaticAssert(loc, lhs, rhs); diff --git a/test/c_generator/stmts/assert.c2t b/test/c_generator/stmts/assert.c2t index 68b183e7..29588c4e 100644 --- a/test/c_generator/stmts/assert.c2t +++ b/test/c_generator/stmts/assert.c2t @@ -6,11 +6,16 @@ module test; i32 a = 10; +i32 b = 20; void* p = nil; public fn i32 main() { - assert(a); assert(p); + assert(a); + assert(a == 10); + assert(a != 10); + assert(a > 10); + assert(a == b); return 0; } @@ -19,8 +24,12 @@ int main(void); int main(void) { - (test_a) || c2_assert_fail("file1.c2", 7, "test.main", "a"); (test_p) || c2_assert_fail("file1.c2", 8, "test.main", "p"); + (test_a) || c2_assert_fail("file1.c2", 9, "test.main", "a"); + (test_a == 10) || c2_assert_fail("file1.c2", 10, "test.main", "a == 10, a: %d", test_a); + (test_a != 10) || c2_assert_fail("file1.c2", 11, "test.main", "a != 10, a: %d", test_a); + (test_a > 10) || c2_assert_fail("file1.c2", 12, "test.main", "a > 10, a: %d", test_a); + (test_a == test_b) || c2_assert_fail("file1.c2", 13, "test.main", "a == b, a: %d, b: %d", test_a, test_b); return 0; } diff --git a/test/globals/static_asserts/static_assert_int.c2 b/test/globals/static_asserts/static_assert_int.c2 index 300613ce..af94f093 100644 --- a/test/globals/static_asserts/static_assert_int.c2 +++ b/test/globals/static_asserts/static_assert_int.c2 @@ -1,5 +1,29 @@ // @warnings{no-unused} module test; -static_assert(3, 4); // @error{static_assert failed, expected 3, got 4} +type S struct { i32 x, y; } +type T struct { u32 x; } +static_assert(3, 4); // @error{static_assert failed: values differ: 3 <> 4} +static_assert(0xff, 0b11111110); // @error{static_assert failed: values differ: 0xFF (255) <> 0b11111110 (254)} +static_assert(sizeof(S), 4); // @error{static_assert failed: sizeof(S) is 8, expected 4} +static_assert(4, sizeof(S)); // @error{static_assert failed: sizeof(S) is 8, expected 4} +static_assert(sizeof(T), sizeof(S)); // @error{static_assert failed: values differ: sizeof(T) (4) <> sizeof(S) (8)} + +static_assert(3 == 4); // @error{static_assert failed: values differ: 3 <> 4} +static_assert(0xff == 0b11111110); // @error{static_assert failed: values differ: 0xFF (255) <> 0b11111110 (254)} +static_assert(sizeof(S) == 4); // @error{static_assert failed: sizeof(S) is 8, expected 4} +static_assert(4 == sizeof(S)); // @error{static_assert failed: sizeof(S) is 8, expected 4} +static_assert(sizeof(T) == sizeof(S)); // @error{static_assert failed: values differ: sizeof(T) (4) <> sizeof(S) (8)} + +static_assert(3 >= 4); // @error{static_assert failed: expected 3 >= 4} +static_assert(0xff <= 0b11111110); // @error{static_assert failed: expected 0xFF <= 0b11111110, 0xFF is 255, 0b11111110 is 254} +static_assert(sizeof(S) <= 4); // @error{static_assert failed: expected sizeof(S) <= 4, sizeof(S) is 8} +static_assert(4 >= sizeof(S)); // @error{static_assert failed: expected 4 >= sizeof(S), sizeof(S) is 8} +static_assert(sizeof(T) > sizeof(S)); // @error{static_assert failed: expected sizeof(T) > sizeof(S), sizeof(T) is 4, sizeof(S) is 8} + +static_assert(1.5 >= 4); // @error{static_assert failed: expected 1.5 >= 4} +static_assert(true, 0); // @error{static_assert failed: values differ: true (1) <> 0} +static_assert(true, false); // @error{static_assert failed: values differ: true (1) <> false (0)} +static_assert(true, "unexpected failure"); +static_assert(false, "expected failure"); // @error{expected failure: expected false != 0} diff --git a/test/globals/static_asserts/static_assert_ok.c2 b/test/globals/static_asserts/static_assert_ok.c2 index 17cef1c4..29c55b21 100644 --- a/test/globals/static_asserts/static_assert_ok.c2 +++ b/test/globals/static_asserts/static_assert_ok.c2 @@ -2,13 +2,21 @@ module test; static_assert(8, 8); +static_assert(8 == 8); static_assert(8, 4+4); +static_assert(8 == 4+4); static_assert(2+2*3, 4+4); +static_assert(2+2*3 == 4+4); static_assert(true, true); +static_assert(true == true); static_assert(false, false); +static_assert(false == false); static_assert(1, true); +static_assert(1 == true); static_assert(true, 1); +static_assert(true == 1); static_assert('c', 0x63); +static_assert('c' == 0x63); type Struct struct { i32 a;