From b5ae2485a7c61bf15a12ebd8db7df4645d9fe77a Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Sat, 4 Oct 2025 11:49:23 +0200 Subject: [PATCH 01/49] Update version to 0.7.7 --- releasenotes.md | 6 ++++++ src/version.h | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/releasenotes.md b/releasenotes.md index 55928c4cd..51171f7da 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -1,5 +1,11 @@ # C3C Release Notes +## 0.7.7 Change list + +### Changes / improvements +### Fixes +### Stdlib changes + ## 0.7.6 Change list ### Changes / improvements diff --git a/src/version.h b/src/version.h index dcab6b987..4f50bec6f 100644 --- a/src/version.h +++ b/src/version.h @@ -1,2 +1,2 @@ -#define COMPILER_VERSION "0.7.6" -#define PRERELEASE 0 +#define COMPILER_VERSION "0.7.7" +#define PRERELEASE 1 From 1eb8c0ced147dc617de2a5f12235e95417f7e380 Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Sun, 5 Oct 2025 00:29:34 +0200 Subject: [PATCH 02/49] Bug in `io::write_using_write_byte`. --- lib/std/io/stream.c3 | 15 +++++++++++++-- releasenotes.md | 3 +++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/std/io/stream.c3 b/lib/std/io/stream.c3 index 77d355baf..962aa5e8d 100644 --- a/lib/std/io/stream.c3 +++ b/lib/std/io/stream.c3 @@ -116,6 +116,9 @@ macro usz? write_all(stream, char[] buffer) return n; } +<* + @require @is_instream(s) +*> macro usz? read_using_read_byte(s, char[] buffer) { usz len = 0; @@ -133,12 +136,18 @@ macro usz? read_using_read_byte(s, char[] buffer) return len; } +<* + @require @is_outstream(s) +*> macro void? write_byte_using_write(s, char c) { char[1] buff = { c }; s.write(&buff)!; } +<* + @require @is_instream(s) +*> macro char? read_byte_using_read(s) { char[1] buffer; @@ -149,10 +158,12 @@ macro char? read_byte_using_read(s) alias ReadByteFn = fn char?(); - +<* + @require @is_outstream(s) +*> macro usz? write_using_write_byte(s, char[] bytes) { - foreach (c : bytes) s.write_byte(self, c)!; + foreach (c : bytes) s.write_byte(c)!; return bytes.len; } diff --git a/releasenotes.md b/releasenotes.md index 51171f7da..32218ec19 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -3,7 +3,10 @@ ## 0.7.7 Change list ### Changes / improvements + ### Fixes +- Bug in `io::write_using_write_byte`. + ### Stdlib changes ## 0.7.6 Change list From 872f63eecc9451c6baafd31bd1429dff242a6402 Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Sun, 5 Oct 2025 22:23:07 +0200 Subject: [PATCH 03/49] - Bitstruct value cannot be used to index a const array in compile time. #2512 --- releasenotes.md | 1 + src/compiler/sema_expr.c | 4 +-- .../bitstruct/bitstruct_to_index.c3t | 25 +++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 test/test_suite/bitstruct/bitstruct_to_index.c3t diff --git a/releasenotes.md b/releasenotes.md index 32218ec19..105816245 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -6,6 +6,7 @@ ### Fixes - Bug in `io::write_using_write_byte`. +- Bitstruct value cannot be used to index a const array in compile time. #2512 ### Stdlib changes diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index 6cad425b3..75c6c5f38 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -3855,7 +3855,7 @@ static inline bool sema_expr_resolve_subscript_index(SemaContext *context, Expr } else { - if (!sema_analyse_expr(context, index)) + if (!sema_analyse_expr_rvalue(context, index)) { expr_poison(index); return false; @@ -3875,7 +3875,7 @@ static inline bool sema_expr_resolve_subscript_index(SemaContext *context, Expr ArrayIndex size; bool check_len = !context->call_env.in_no_eval || current_type == type_untypedlist; Expr *len_expr = current_expr->expr_kind == EXPR_CT_IDENT ? current_expr->ct_ident_expr.decl->var.init_expr : current_expr; - if (expr_is_const_int(index) && (size = sema_len_from_expr(len_expr)) >= 0) + if (sema_cast_const(index)&& expr_is_const_int(index) && (size = sema_len_from_expr(len_expr)) >= 0) { // 4c. And that it's in range. if (int_is_neg(index->const_expr.ixx)) diff --git a/test/test_suite/bitstruct/bitstruct_to_index.c3t b/test/test_suite/bitstruct/bitstruct_to_index.c3t new file mode 100644 index 000000000..844993520 --- /dev/null +++ b/test/test_suite/bitstruct/bitstruct_to_index.c3t @@ -0,0 +1,25 @@ +// #target: macos-x64 +module test; +import std; + +bitstruct Some : char +{ + char v : 0 .. 4; +} + +const int[8] SOME_MAP = some_macro(); + +macro some_macro() +{ + Some $s = { 8 }; + int[8] $map; + $for var $i = 0; $i < 4; $i++: + $s = { .v = $s.v >> 1 }; + $map[$s.v] = 1; + $endfor; + return $map; +} + +/* #expect: test.ll + +@test.SOME_MAP = local_unnamed_addr constant { i32, i32, i32, i32, i32, [3 x i32] } { i32 1, i32 1, i32 1, i32 0, i32 1, [3 x i32] zeroinitializer }, align 16 From e9ec421b3b4a74c42dab45e71c7cda03dab164ef Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Mon, 6 Oct 2025 00:31:27 +0200 Subject: [PATCH 04/49] Compiler fails to stop error print in recursive macro, and also prints unnecessary "inline at" #2513. --- releasenotes.md | 1 + src/compiler/sema_stmts.c | 2 ++ src/compiler/semantic_analyser.c | 4 +++- test/test_suite/macros/macro_recursive_err.c3 | 12 ++++++++++++ test/test_suite/macros/macro_recursive_err2.c3 | 12 ++++++++++++ test/test_suite/macros/macro_untyped_varargs.c3 | 15 +++++++++++++++ 6 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 test/test_suite/macros/macro_recursive_err.c3 create mode 100644 test/test_suite/macros/macro_recursive_err2.c3 diff --git a/releasenotes.md b/releasenotes.md index 105816245..5a36d8c5f 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -7,6 +7,7 @@ ### Fixes - Bug in `io::write_using_write_byte`. - Bitstruct value cannot be used to index a const array in compile time. #2512 +- Compiler fails to stop error print in recursive macro, and also prints unnecessary "inline at" #2513. ### Stdlib changes diff --git a/src/compiler/sema_stmts.c b/src/compiler/sema_stmts.c index cedd7a120..373797f04 100644 --- a/src/compiler/sema_stmts.c +++ b/src/compiler/sema_stmts.c @@ -2339,6 +2339,8 @@ static inline bool sema_analyse_compound_statement_no_scope(SemaContext *context { ast_poison(ast); all_ok = false; + // Don't continue inside a macro, since we get too many "inlined" errors. + if (context->current_macro) break; } } AstId *next = ast ? &ast_last(ast)->next : &compound_statement->compound_stmt.first_stmt; diff --git a/src/compiler/semantic_analyser.c b/src/compiler/semantic_analyser.c index 7676e9dce..7b251d6ef 100644 --- a/src/compiler/semantic_analyser.c +++ b/src/compiler/semantic_analyser.c @@ -584,11 +584,13 @@ void sema_print_inline(SemaContext *context, SourceSpan original) { if (!context) return; InliningSpan *inlined_at = context->inlined_at; + SourceSpan last_span = INVALID_SPAN; while (inlined_at) { - if (inlined_at->span.a != original.a) + if (inlined_at->span.a != original.a && inlined_at->span.a != last_span.a) { sema_note_prev_at(inlined_at->span, "Inlined from here."); + last_span = inlined_at->span; } inlined_at = inlined_at->prev; } diff --git a/test/test_suite/macros/macro_recursive_err.c3 b/test/test_suite/macros/macro_recursive_err.c3 new file mode 100644 index 000000000..87a77393e --- /dev/null +++ b/test/test_suite/macros/macro_recursive_err.c3 @@ -0,0 +1,12 @@ +import std; + +macro @_macro(#i) +{ + if (#i == 0) { return; } + return @_macro(#i - 1); // #error: Failure evaluating macro, max call depth reached +} + +fn void main() +{ + @_macro(1); +} diff --git a/test/test_suite/macros/macro_recursive_err2.c3 b/test/test_suite/macros/macro_recursive_err2.c3 new file mode 100644 index 000000000..299cd3c25 --- /dev/null +++ b/test/test_suite/macros/macro_recursive_err2.c3 @@ -0,0 +1,12 @@ +import std; + +macro @_macro(#i) +{ + if (#i == 0) { return; } + return @_macro(--#i); // #error: You cannot assign to a constant expression +} + +fn void main() +{ + @_macro(1); +} \ No newline at end of file diff --git a/test/test_suite/macros/macro_untyped_varargs.c3 b/test/test_suite/macros/macro_untyped_varargs.c3 index fd3984f6f..6628f3215 100644 --- a/test/test_suite/macros/macro_untyped_varargs.c3 +++ b/test/test_suite/macros/macro_untyped_varargs.c3 @@ -1,9 +1,21 @@ macro foo(...) { $vaarg["hello"]; // #error: Expected the argument index here +} + +macro foo_a(...) +{ int x; $vaarg[x]; // #error: Vararg functions need a constant argument +} + +macro foo_b(...) +{ $vaarg[-1]; // #error: negative +} + +macro foo_c(...) +{ $vaarg[100]; // #error: varargs exist } @@ -22,4 +34,7 @@ fn void main() int x; foo2(x); // #error: This argument needs to be a compile time constant foo3(3); // #error: The argument was not a type. + foo_a(1, -1, 3141, 999 + 1); + foo_b(1, -1, 3141, 999 + 1); + foo_c(1, -1, 3141, 999 + 1); } From 5a1831c98961a7576a14d4e01f905d1cd7e11bb5 Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Mon, 6 Oct 2025 00:45:36 +0200 Subject: [PATCH 05/49] Error when using $vaarg/$vacount/$vasplat and similar in a macro without vaargs #2510. --- releasenotes.md | 1 + src/compiler/compiler_internal.h | 1 + src/compiler/sema_expr.c | 7 ++++--- src/compiler/sema_types.c | 4 ++-- test/test_suite/macros/macro_vaarg_no.c3 | 14 ++++++++++++++ 5 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 test/test_suite/macros/macro_vaarg_no.c3 diff --git a/releasenotes.md b/releasenotes.md index 5a36d8c5f..d2a9e7254 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -3,6 +3,7 @@ ## 0.7.7 Change list ### Changes / improvements +- Error when using $vaarg/$vacount/$vasplat and similar in a macro without vaargs #2510. ### Fixes - Bug in `io::write_using_write_byte`. diff --git a/src/compiler/compiler_internal.h b/src/compiler/compiler_internal.h index 394d793e9..56536615f 100644 --- a/src/compiler/compiler_internal.h +++ b/src/compiler/compiler_internal.h @@ -1800,6 +1800,7 @@ struct SemaContext_ Type *expected_block_type; Ast **block_returns; Expr **macro_varargs; + bool macro_has_vaargs; Decl **macro_params; bool macro_has_ensures; Decl** ct_locals; diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index 75c6c5f38..6e36092a8 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -2695,7 +2695,8 @@ static inline bool sema_expr_setup_call_analysis(SemaContext *context, CalledDec expr->expr_other_context.inner = expr_inner; expr->expr_other_context.context = context; } - macro_context->macro_varargs = callee->macro && callee->signature->variadic == VARIADIC_RAW ? call_expr->call_expr.varargs : NULL; + macro_context->macro_has_vaargs = callee->macro && callee->signature->variadic == VARIADIC_RAW; + macro_context->macro_varargs = macro_context->macro_has_vaargs ? call_expr->call_expr.varargs : NULL; macro_context->original_inline_line = context->original_inline_line ? context->original_inline_line : call_expr->span.row; macro_context->original_module = context->original_module ? context->original_module : context->compilation_unit->module; macro_context->macro_params = params; @@ -10986,9 +10987,9 @@ static inline bool sema_expr_analyse_ct_arg(SemaContext *context, Type *infer_ty { ASSERT_SPAN(expr, expr->resolve_status == RESOLVE_RUNNING); TokenType type = expr->ct_arg_expr.type; - if (!context->current_macro) + if (!context->macro_has_vaargs) { - RETURN_SEMA_ERROR(expr, "'%s' can only be used inside of a macro.", token_type_to_string(type)); + RETURN_SEMA_ERROR(expr, "'%s' can only be used inside of a macro with untyped vaargs.", token_type_to_string(type)); } switch (type) { diff --git a/src/compiler/sema_types.c b/src/compiler/sema_types.c index df16cd5e7..bcb6a9eeb 100644 --- a/src/compiler/sema_types.c +++ b/src/compiler/sema_types.c @@ -403,9 +403,9 @@ INLINE bool sema_resolve_typefrom(SemaContext *context, TypeInfo *type_info, Res // $vatype(...) INLINE bool sema_resolve_vatype(SemaContext *context, TypeInfo *type_info) { - if (!context->current_macro) + if (!context->macro_has_vaargs) { - RETURN_SEMA_ERROR(type_info, "'%s' can only be used inside of a macro.", token_type_to_string(TOKEN_CT_VATYPE)); + RETURN_SEMA_ERROR(type_info, "'%s' can only be used inside of a macro with untyped vaargs.", token_type_to_string(TOKEN_CT_VATYPE)); } ASSIGN_EXPR_OR_RET(Expr *arg_expr, sema_expr_analyse_ct_arg_index(context, type_info->unresolved_type_expr, NULL), false); if (!sema_analyse_expr(context, arg_expr)) return false; diff --git a/test/test_suite/macros/macro_vaarg_no.c3 b/test/test_suite/macros/macro_vaarg_no.c3 new file mode 100644 index 000000000..2e0630300 --- /dev/null +++ b/test/test_suite/macros/macro_vaarg_no.c3 @@ -0,0 +1,14 @@ +import std; + +macro void example(String... x) +{ + $for var $i = 0; $i < $vacount; $i++: // #error: can only be used inside of a macro with untyped + io::printn($vaarg[$i]); + $endfor +} + +fn void main() +{ + example(); +} + From b4b14674b479ee45e0b8fea5963b3965dab518e4 Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Mon, 6 Oct 2025 20:50:56 +0200 Subject: [PATCH 06/49] - Bitstruct truncated constant error escapes `$defined` #2515 --- releasenotes.md | 1 + src/compiler/sema_expr.c | 5 +++-- src/compiler/sema_initializers.c | 4 ++-- src/compiler/sema_internal.h | 2 +- test/test_suite/bitstruct/bitrstruct_defined.c3t | 6 ++++++ 5 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 test/test_suite/bitstruct/bitrstruct_defined.c3t diff --git a/releasenotes.md b/releasenotes.md index d2a9e7254..d47a0fcfb 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -9,6 +9,7 @@ - Bug in `io::write_using_write_byte`. - Bitstruct value cannot be used to index a const array in compile time. #2512 - Compiler fails to stop error print in recursive macro, and also prints unnecessary "inline at" #2513. +- Bitstruct truncated constant error escapes `$defined` #2515 ### Stdlib changes diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index 6e36092a8..2b0d5f68e 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -6905,7 +6905,7 @@ static bool sema_expr_analyse_assign(SemaContext *context, Expr *expr, Expr *lef } if (left->expr_kind == EXPR_BITACCESS) { - if (!sema_bit_assignment_check(context, right, left->access_resolved_expr.ref)) return false; + if (!sema_bit_assignment_check(context, right, left->access_resolved_expr.ref, failed_ref)) return false; expr->expr_kind = EXPR_BITASSIGN; } return true; @@ -12310,7 +12310,7 @@ static inline bool sema_insert_binary_overload(SemaContext *context, Expr *expr, } // Check if the assignment fits -bool sema_bit_assignment_check(SemaContext *context, Expr *right, Decl *member) +bool sema_bit_assignment_check(SemaContext *context, Expr *right, Decl *member, bool *failed_ref) { // Don't check non-consts and non integers. if (!sema_cast_const(right) || !type_is_integer(right->type)) return true; @@ -12322,6 +12322,7 @@ bool sema_bit_assignment_check(SemaContext *context, Expr *right, Decl *member) if (int_bits_needed(right->const_expr.ixx) > bits) { + if (failed_ref) return *failed_ref = true, false; RETURN_SEMA_ERROR(right, "This constant would be truncated if stored in the " "bitstruct, do you need a wider bit range?"); } diff --git a/src/compiler/sema_initializers.c b/src/compiler/sema_initializers.c index b32ef58f5..cc9137af1 100644 --- a/src/compiler/sema_initializers.c +++ b/src/compiler/sema_initializers.c @@ -277,7 +277,7 @@ static inline bool sema_expr_analyse_struct_plain_initializer(SemaContext *conte if (!sema_analyse_expr_rhs(context, members[i]->type, element, true, no_match_ref, false)) return false; if (member->decl_kind == DECL_VAR && member->var.kind == VARDECL_BITMEMBER) { - if (!sema_bit_assignment_check(context, element, members[i])) return false; + if (!sema_bit_assignment_check(context, element, members[i], no_match_ref)) return false; } optional = optional || IS_OPTIONAL(element); } @@ -529,7 +529,7 @@ static bool sema_expr_analyse_designated_initializer(SemaContext *context, Type if (!sema_analyse_expr_rhs(context, result, value, true, no_match_ref, false)) return false; if (is_bitmember) { - if (!sema_bit_assignment_check(context, value, member)) return false; + if (!sema_bit_assignment_check(context, value, member, no_match_ref)) return false; } optional = optional || IS_OPTIONAL(value); expr->resolve_status = RESOLVE_DONE; diff --git a/src/compiler/sema_internal.h b/src/compiler/sema_internal.h index 80f1cf9f6..740f3d249 100644 --- a/src/compiler/sema_internal.h +++ b/src/compiler/sema_internal.h @@ -113,7 +113,7 @@ Expr *sema_resolve_string_ident(SemaContext *context, Expr *inner, bool report_m 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); +bool sema_bit_assignment_check(SemaContext *context, Expr *right, Decl *member, bool *failed_ref); CondResult sema_check_comp_time_bool(SemaContext *context, Expr *expr); bool sema_expr_check_assign(SemaContext *context, Expr *expr, bool *failed_ref); diff --git a/test/test_suite/bitstruct/bitrstruct_defined.c3t b/test/test_suite/bitstruct/bitrstruct_defined.c3t new file mode 100644 index 000000000..a9ab2e5b8 --- /dev/null +++ b/test/test_suite/bitstruct/bitrstruct_defined.c3t @@ -0,0 +1,6 @@ +bitstruct Test : char +{ + char type : 0..2; +} + +$assert !$defined((Test){123}); \ No newline at end of file From 04cd079d4ef553c694d30c7671fcd19249edfa90 Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Tue, 7 Oct 2025 00:12:41 +0200 Subject: [PATCH 07/49] - Compiler segfault when accessing member of number cast to bitstruct #2516. - Additional fix to #2515 - Compiler assert when getting a member of a `bitstruct : char @bigendian` #2517. --- releasenotes.md | 4 +++- src/compiler/sema_decls.c | 5 +++++ src/compiler/sema_expr.c | 8 +++++--- src/compiler/sema_initializers.c | 2 +- test/test_suite/bitstruct/big_endian_char.c3t | 16 ++++++++++++++++ test/test_suite/bitstruct/bitrstruct_defined.c3t | 3 ++- .../cast_bitstruct_as_const_value_fail.c3 | 8 ++++++++ 7 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 test/test_suite/bitstruct/big_endian_char.c3t create mode 100644 test/test_suite/bitstruct/cast_bitstruct_as_const_value_fail.c3 diff --git a/releasenotes.md b/releasenotes.md index d47a0fcfb..385315ba9 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -9,7 +9,9 @@ - Bug in `io::write_using_write_byte`. - Bitstruct value cannot be used to index a const array in compile time. #2512 - Compiler fails to stop error print in recursive macro, and also prints unnecessary "inline at" #2513. -- Bitstruct truncated constant error escapes `$defined` #2515 +- Bitstruct truncated constant error escapes `$defined` #2515. +- Compiler segfault when accessing member of number cast to bitstruct #2516. +- Compiler assert when getting a member of a `bitstruct : char @bigendian` #2517. ### Stdlib changes diff --git a/src/compiler/sema_decls.c b/src/compiler/sema_decls.c index c0f09ba81..3696c219f 100755 --- a/src/compiler/sema_decls.c +++ b/src/compiler/sema_decls.c @@ -1060,6 +1060,11 @@ static bool sema_analyse_bitstruct(SemaContext *context, Decl *decl, bool *erase DEBUG_LOG("Beginning analysis of %s.", decl->name ? decl->name : ".anon"); if (!sema_resolve_type_info(context, decl->strukt.container_type, RESOLVE_TYPE_DEFAULT)) return false; Type *type = type_flatten(decl->strukt.container_type->type->canonical); + if (type_size(type) == 1) + { + decl->strukt.big_endian = false; + decl->strukt.little_endian = false; + } Type *base_type = type->type_kind == TYPE_ARRAY ? type_flatten(type->array.base) : type; if (!type_is_integer(base_type)) { diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index 2b0d5f68e..63ccfb26d 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -12046,18 +12046,20 @@ bool sema_cast_const(Expr *expr) { Expr *parent = expr->access_resolved_expr.parent; Type *flat = type_flatten(parent->type); + if (!sema_cast_const(parent)) return false; switch (flat->type_kind) { case TYPE_UNION: case TYPE_UNTYPED_LIST: - case TYPE_BITSTRUCT: case TYPE_STRUCT: break; + case TYPE_BITSTRUCT: + if (!expr_is_const_initializer(parent)) return false; + break; default: return false; } - if (!sema_cast_const(expr->access_resolved_expr.parent)) return false; - if (!sema_expr_fold_to_member(expr, expr->access_resolved_expr.parent, expr->access_resolved_expr.ref)) return false; + if (!sema_expr_fold_to_member(expr, parent, expr->access_resolved_expr.ref)) return false; return true; } case EXPR_SLICE: diff --git a/src/compiler/sema_initializers.c b/src/compiler/sema_initializers.c index cc9137af1..21a077d96 100644 --- a/src/compiler/sema_initializers.c +++ b/src/compiler/sema_initializers.c @@ -580,7 +580,7 @@ static inline bool sema_expr_analyse_initializer(SemaContext *context, Type *ass // 1. Designated initializer is separately evaluated. if (expr->expr_kind == EXPR_DESIGNATED_INITIALIZER_LIST) { - return sema_expr_analyse_designated_initializer(context, assigned_type, flattened, expr,NULL); + return sema_expr_analyse_designated_initializer(context, assigned_type, flattened, expr, no_match_ref); } if (expr->expr_kind == EXPR_CONST) diff --git a/test/test_suite/bitstruct/big_endian_char.c3t b/test/test_suite/bitstruct/big_endian_char.c3t new file mode 100644 index 000000000..a89f55bac --- /dev/null +++ b/test/test_suite/bitstruct/big_endian_char.c3t @@ -0,0 +1,16 @@ +bitstruct Instruction : char @bigendian +{ + ushort count : 4..7; +} + +bitstruct Instruction2 : char @littleendian +{ + ushort count : 4..7; +} + +fn int main(String[] args) +{ + Instruction a; + Instruction2 b; + return a.count; +} \ No newline at end of file diff --git a/test/test_suite/bitstruct/bitrstruct_defined.c3t b/test/test_suite/bitstruct/bitrstruct_defined.c3t index a9ab2e5b8..f412f1070 100644 --- a/test/test_suite/bitstruct/bitrstruct_defined.c3t +++ b/test/test_suite/bitstruct/bitrstruct_defined.c3t @@ -3,4 +3,5 @@ bitstruct Test : char char type : 0..2; } -$assert !$defined((Test){123}); \ No newline at end of file +$assert !$defined((Test){123}); +$assert !$defined((Test){.type = 123}); \ No newline at end of file diff --git a/test/test_suite/bitstruct/cast_bitstruct_as_const_value_fail.c3 b/test/test_suite/bitstruct/cast_bitstruct_as_const_value_fail.c3 new file mode 100644 index 000000000..ff1657b5e --- /dev/null +++ b/test/test_suite/bitstruct/cast_bitstruct_as_const_value_fail.c3 @@ -0,0 +1,8 @@ +bitstruct Test : char +{ + char type : 0..2; +} + +const TEST = ((Test)char.max).type; // #error: This expression cannot be evaluated at compile + +fn int main() => 0; \ No newline at end of file From d6be1cbf6503401d60d9e8a8907b31c86939d9bb Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Tue, 7 Oct 2025 21:52:15 +0200 Subject: [PATCH 08/49] Incorrect visibility on local globals with public aliases. #2519 --- releasenotes.md | 1 + src/compiler/sema_decls.c | 4 ++++ test/test_suite/define/alias_visibility.c3t | 20 ++++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 test/test_suite/define/alias_visibility.c3t diff --git a/releasenotes.md b/releasenotes.md index 385315ba9..b28bedccf 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -12,6 +12,7 @@ - Bitstruct truncated constant error escapes `$defined` #2515. - Compiler segfault when accessing member of number cast to bitstruct #2516. - Compiler assert when getting a member of a `bitstruct : char @bigendian` #2517. +- Incorrect visibility on local globals with public aliases. #2519 ### Stdlib changes diff --git a/src/compiler/sema_decls.c b/src/compiler/sema_decls.c index 3696c219f..7875c0c95 100755 --- a/src/compiler/sema_decls.c +++ b/src/compiler/sema_decls.c @@ -5302,6 +5302,10 @@ static inline bool sema_analyse_alias(SemaContext *context, Decl *decl, bool *er } decl->type = symbol->type; decl->define_decl.alias = symbol; + if (decl_is_externally_visible(decl) && !decl_is_externally_visible(symbol)) + { + symbol->is_external_visible = true; + } return true; } diff --git a/test/test_suite/define/alias_visibility.c3t b/test/test_suite/define/alias_visibility.c3t new file mode 100644 index 000000000..036aff445 --- /dev/null +++ b/test/test_suite/define/alias_visibility.c3t @@ -0,0 +1,20 @@ +// #target: macos-x64 +module test1; +alias x = y; +int y @local = 1; + +module test; +import test1; + +fn int main(String[] args) +{ + return test1::x; +} + +/* #expect: test1.ll + +@test1.y.10 = local_unnamed_addr global i32 1, align 4 + +// #expect: test.ll + +@test1.y.10 = external global i32, align 4 \ No newline at end of file From fe70f10bcced86e3cc1f1b19a4fee2e625e19cbe Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Tue, 7 Oct 2025 22:43:40 +0200 Subject: [PATCH 09/49] Sorting functions correctly took slices by value, but also other types by value. Now, only slices are accepted by value, other containers are always by ref. --- benchmarks/stdlib/crypto/aes_bench.c3 | 4 +- lib/std/sort/binarysearch.c3 | 111 ++++++++++++++++-------- lib/std/sort/countingsort.c3 | 56 +++++++++---- lib/std/sort/insertionsort.c3 | 39 +++++---- lib/std/sort/quicksort.c3 | 77 +++++++++-------- lib/std/sort/sort.c3 | 112 +++++++++++++++++++++---- lib/std/sort/sorted.c3 | 59 ++++++++++++- releasenotes.md | 1 + src/compiler/sema_expr.c | 2 +- test/unit/stdlib/collections/map.c3 | 2 +- test/unit/stdlib/sort/countingsort.c3 | 4 +- test/unit/stdlib/sort/insertionsort.c3 | 2 +- test/unit/stdlib/sort/quicksort.c3 | 2 +- 13 files changed, 346 insertions(+), 125 deletions(-) diff --git a/benchmarks/stdlib/crypto/aes_bench.c3 b/benchmarks/stdlib/crypto/aes_bench.c3 index 8e148e997..129f9df05 100644 --- a/benchmarks/stdlib/crypto/aes_bench.c3 +++ b/benchmarks/stdlib/crypto/aes_bench.c3 @@ -21,11 +21,11 @@ fn void bench_ctr_xcrypt() @benchmark Aes ctx; // encrypt - ctx.init_with_iv(aes, CTR, key, iv); + ctx.init(aes, key, iv); ctx.encrypt_buffer(text, &out); // decrypt - ctx.init_with_iv(aes, CTR, key, iv); + ctx.init(aes, key, iv); ctx.decrypt_buffer(cipher, &out); } diff --git a/lib/std/sort/binarysearch.c3 b/lib/std/sort/binarysearch.c3 index b2e6bb7e2..6bbadfc30 100644 --- a/lib/std/sort/binarysearch.c3 +++ b/lib/std/sort/binarysearch.c3 @@ -3,6 +3,8 @@ module std::sort; <* Perform a binary search over the sorted array and return the index in [0, array.len) where x would be inserted or cmp(i) is true and cmp(j) is true for j in [i, array.len). + + @require @list_is_by_ref(list) : "Expected a list passed by reference or a slice" @require @is_sortable(list) : "The list must be sortable" @require @is_valid_cmp_fn(cmp, list, context) : "Expected a comparison function which compares values" @require @is_valid_context(cmp, context) : "Expected a valid context" @@ -10,42 +12,81 @@ in [0, array.len) where x would be inserted or cmp(i) is true and cmp(j) is true macro usz binarysearch(list, x, cmp = EMPTY_MACRO_SLOT, context = EMPTY_MACRO_SLOT) @builtin { usz i; - usz len = lengthof(list); var $no_cmp = @is_empty_macro_slot(cmp); var $has_context = @is_valid_macro_slot(context); - for (usz j = len; i < j;) - { - usz half = i + (j - i) / 2; - $if $no_cmp: - switch - { - case greater(list[half], x): j = half; - case less(list[half], x): i = half + 1; - default: return half; - } - $else - $switch: - $case $defined(cmp(list[0], list[0], context)): - int res = cmp(list[half], x, context); - $case $defined(cmp(list[0], list[0])): - assert(!$has_context); - int res = cmp(list[half], x); - $case $defined(cmp(&list[0], &list[0], context)): - int res = cmp(&list[half], &x, context); - $case $defined(cmp(&list[0], &list[0])): - assert(!$has_context); - int res = cmp(&list[half], &x); - $default: - assert(false, "Invalid comparison function"); - $endswitch - switch - { - case res > 0: j = half; - case res < 0: i = half + 1; - default: return half; - } - $endif - } - return i; + $if $kindof(list) == SLICE: + usz len = lengthof(list); + for (usz j = len; i < j;) + { + usz half = i + (j - i) / 2; + $if $no_cmp: + switch + { + case greater(list[half], x): j = half; + case less(list[half], x): i = half + 1; + default: return half; + } + $else + + $switch: + $case $defined(cmp(list[0], list[0], context)): + int res = cmp(list[half], x, context); + $case $defined(cmp(list[0], list[0])): + assert(!$has_context); + int res = cmp(list[half], x); + $case $defined(cmp(&list[0], &list[0], context)): + int res = cmp(&list[half], &x, context); + $case $defined(cmp(&list[0], &list[0])): + assert(!$has_context); + int res = cmp(&list[half], &x); + $default: + assert(false, "Invalid comparison function"); + $endswitch + switch + { + case res > 0: j = half; + case res < 0: i = half + 1; + default: return half; + } + $endif + } + $else + usz len = lengthof(*list); + for (usz j = len; i < j;) + { + usz half = i + (j - i) / 2; + $if $no_cmp: + switch + { + case greater((*list)[half], x): j = half; + case less((*list)[half], x): i = half + 1; + default: return half; + } + $else + + $switch: + $case $defined(cmp((*list)[0], (*list)[0], context)): + int res = cmp(list[half], x, context); + $case $defined(cmp((*list)[0], (*list)[0])): + assert(!$has_context); + int res = cmp((*list)[half], x); + $case $defined(cmp(&(*list)[0], &(*list)[0], context)): + int res = cmp(&(*list)[half], &x, context); + $case $defined(cmp(&(*list)[0], &(*list)[0])): + assert(!$has_context); + int res = cmp(&(*list)[half], &x); + $default: + assert(false, "Invalid comparison function"); + $endswitch + switch + { + case res > 0: j = half; + case res < 0: i = half + 1; + default: return half; + } + $endif + } + $endif + return i; } \ No newline at end of file diff --git a/lib/std/sort/countingsort.c3 b/lib/std/sort/countingsort.c3 index f21e1d8dd..08804144d 100644 --- a/lib/std/sort/countingsort.c3 +++ b/lib/std/sort/countingsort.c3 @@ -4,24 +4,39 @@ import std::sort::cs; import std::sort::qs; <* -Sort list using the counting sort algorithm. + + Sort list using the counting sort algorithm. + + @require @list_is_by_ref(list) : "Expected the list to be passed by reference" @require @is_sortable(list) : "The list must be indexable and support .len or .len()" @require @is_cmp_key_fn(key_fn, list) : "Expected a transformation function which returns an unsigned integer." *> macro void countingsort(list, key_fn = EMPTY_MACRO_SLOT) @builtin { - usz len = lengthof(list); - cs::csort{$typeof(list), $typeof(key_fn)}(list, 0, len, key_fn, ~((uint)0)); + $if $kindof(list) == SLICE: + cs::csort{$typeof(list), $typeof(key_fn)}(list, 0, list.len, key_fn, ~((uint)0)); + $else + cs::csort{$typeof(*list), $typeof(key_fn)}(list, 0, lengthof(*list), key_fn, ~((uint)0)); + $endif } macro void insertionsort_indexed(list, start, end, cmp = EMPTY_MACRO_SLOT, context = EMPTY_MACRO_SLOT) @builtin { - is::isort{$typeof(list), $typeof(cmp), $typeof(context)}(list, (usz)start, (usz)end, cmp, context); + $if $kindof(list) == SLICE: + is::isort{$typeof((list)), $typeof(cmp), $typeof(context)}(list, (usz)start, (usz)end, cmp, context); + $else + is::isort{$typeof((*list)), $typeof(cmp), $typeof(context)}(list, (usz)start, (usz)end, cmp, context); + $endif + } macro void quicksort_indexed(list, start, end, cmp = EMPTY_MACRO_SLOT, context = EMPTY_MACRO_SLOT) @builtin { - qs::qsort{$typeof(list), $typeof(cmp), $typeof(context)}(list, (isz)start, (isz)(end-1), cmp, context); + $if $kindof(list) == SLICE: + qs::qsort{$typeof((list)), $typeof(cmp), $typeof(context)}(list, (isz)start, (isz)(end-1), cmp, context); + $else + qs::qsort{$typeof((*list)), $typeof(cmp), $typeof(context)}(list, (isz)start, (isz)(end-1), cmp, context); + $endif } module std::sort::cs{Type, KeyFn}; @@ -42,7 +57,14 @@ alias CmpCallback @if(!KEY_BY_VALUE && NO_KEY_FN) = fn int(ElementType*, Element alias CmpCallback @if(KEY_BY_VALUE && !NO_KEY_FN) = fn int(ElementType, ElementType, KeyFn); alias CmpCallback @if(!KEY_BY_VALUE && !NO_KEY_FN) = fn int(ElementType*, ElementType*, KeyFn); -fn void csort(Type list, usz low, usz high, KeyFn key_fn, uint byte_idx) +const bool IS_SLICE = Type.kindof == SLICE; +alias ListType = $typefrom(IS_SLICE ??? Type : Type*); +macro list_get(ListType l, i) @if(!IS_SLICE) => (*l)[i]; +macro list_get(ListType l, i) @if(IS_SLICE) => l[i]; +macro list_get_ref(ListType l, i) @if(!IS_SLICE) => &(*l)[i]; +macro list_get_ref(ListType l, i) @if(IS_SLICE) => &l[i]; + +fn void csort(ListType list, usz low, usz high, KeyFn key_fn, uint byte_idx) { if (high <= low) return; $if NO_KEY_FN: @@ -67,13 +89,13 @@ fn void csort(Type list, usz low, usz high, KeyFn key_fn, uint byte_idx) { $switch: $case NO_KEY_FN: - KeyFnReturnType k = list[i]; + KeyFnReturnType k = list_get(list, i); $case KEY_BY_VALUE: - KeyFnReturnType k = key_fn(list[i]); + KeyFnReturnType k = key_fn(list_get(list, i)); $case LIST_HAS_REF: - KeyFnReturnType k = key_fn(&list[i]); + KeyFnReturnType k = key_fn(list_get_ref(list, i)); $default: - KeyFnReturnType k = key_fn(&&list[i]); + KeyFnReturnType k = key_fn(&&list_get(list, i)); $endswitch; char key_byte = (char)((k >> (byte_idx * 8)) & 0xff); @@ -131,17 +153,21 @@ fn void csort(Type list, usz low, usz high, KeyFn key_fn, uint byte_idx) { $switch: $case NO_KEY_FN: - KeyFnReturnType k = list[low + s]; + KeyFnReturnType k = list_get(list, low + s); $case KEY_BY_VALUE: - KeyFnReturnType k = key_fn(list[low + s]); + KeyFnReturnType k = key_fn(list_get(list, low + s)); $case LIST_HAS_REF: - KeyFnReturnType k = key_fn(&list[low + s]); + KeyFnReturnType k = key_fn(list_get_ref(list, low + s)); $default: - KeyFnReturnType k = key_fn(&&list[low + s]); + KeyFnReturnType k = key_fn(&&list_get(list, low + s)); $endswitch; char k_idx = (char)(k >> (byte_idx * 8)); usz target_idx = counts[k_idx]; - @swap(list[low + s], list[low + target_idx]); + $if IS_SLICE: + @swap(list[low + s], list[low + target_idx]); + $else + @swap((*list)[low + s], (*list)[low + target_idx]); + $endif counts[k_idx]++; } } diff --git a/lib/std/sort/insertionsort.c3 b/lib/std/sort/insertionsort.c3 index 56771d623..9c5f71721 100644 --- a/lib/std/sort/insertionsort.c3 +++ b/lib/std/sort/insertionsort.c3 @@ -4,38 +4,41 @@ import std::sort::is; <* Sort list using the quick sort algorithm. + @require @list_is_by_ref(list) : "Expected a list passed by reference, or slice passed by value" @require @is_sortable(list) : "The list must be indexable and support .len or .len()" @require @is_valid_cmp_fn(cmp, list, context) : "Expected a comparison function which compares values" *> macro void insertionsort(list, cmp = EMPTY_MACRO_SLOT, context = EMPTY_MACRO_SLOT) @builtin @safemacro { - $if $kindof(list) == POINTER &&& ($kindof(*list) == ARRAY || $kindof(*list) == VECTOR): - $typeof((*list)[0])[] list2 = list; - is::isort{$typeof(list2), $typeof(cmp), $typeof(context)}(list2, 0, list.len, cmp, context); + $if $kindof(list) == SLICE: + is::isort{$typeof(list), $typeof(cmp), $typeof(context)}(list, 0, lengthof(list), cmp, context); $else - usz len = lengthof(list); - is::isort{$typeof(list), $typeof(cmp), $typeof(context)}(list, 0, (isz)len, cmp, context); + is::isort{$typeof(*list), $typeof(cmp), $typeof(context)}(list, 0, lengthof(*list), cmp, context); $endif } module std::sort::is{Type, CmpFn, Context}; alias ElementType = $typeof(((Type){})[0]); +const bool IS_SLICE = Type.kindof == SLICE; +alias ListType = $typefrom(IS_SLICE ??? Type : Type*); +macro ElementType list_get(ListType l, i) => IS_SLICE ??? l[i] : (*l)[i]; +macro ElementType* list_get_ref(ListType l, i) => IS_SLICE ??? &l[i] : &(*l)[i]; -fn void isort(Type list, usz low, usz high, CmpFn comp, Context context) +fn void isort(ListType list, usz low, usz high, CmpFn comp, Context context) { var $has_cmp = @is_valid_macro_slot(comp); var $has_context = @is_valid_macro_slot(context); - var $cmp_by_value = $has_cmp &&& $defined($typefrom(CmpFn.paramsof[0].type) p = list[0]); - var $has_get_ref = $defined(&list[0]); + var $cmp_by_value = $has_cmp &&& $defined($typefrom(CmpFn.paramsof[0].type) p = list_get(list, 0)); + var $has_get_ref = IS_SLICE ||| $defined(&(*list)[0]); for (usz i = low; i < high; ++i) { usz j = i; for (;j > low;) { $if $has_get_ref: - ElementType *rhs = &list[j]; - ElementType *lhs = &list[--j]; + ElementType *rhs = list_get_ref(list, j); + ElementType *lhs = list_get_ref(list, --j); $switch: $case $cmp_by_value && $has_context: if (comp(*rhs, *lhs, context) >= 0) break; @@ -54,17 +57,21 @@ fn void isort(Type list, usz low, usz high, CmpFn comp, Context context) --j; $switch: $case $cmp_by_value && $has_context: - if (comp(list[r], list[j], context) >= 0) break; + if (comp(list_get(list, r), list_get(j), context) >= 0) break; $case $cmp_by_value: - if (comp(list[r], list[j]) >= 0) break; + if (comp(list_get(list, r), list_get(j)) >= 0) break; $case $has_cmp && $has_context: - if (comp(&list[r], &list[j], context) >= 0) break; + if (comp(list_get_ref(list, r), list_get_ref(j), context) >= 0) break; $case $has_cmp: - if (comp(&list[r], &list[j]) >= 0) break; + if (comp(list_get_ref(list, r), list_get_ref(j)) >= 0) break; $default: - if (!less(list[r], list[j])) break; + if (!less(list_get(list, r), list_get(j))) break; $endswitch - @swap(list[r], list[j]); + $if IS_SLICE: + @swap(list[r], list[j]); + $else + @swap((*list)[r], (*list)[j]); + $endif $endif } } diff --git a/lib/std/sort/quicksort.c3 b/lib/std/sort/quicksort.c3 index 22eff4991..fc2d7c638 100644 --- a/lib/std/sort/quicksort.c3 +++ b/lib/std/sort/quicksort.c3 @@ -4,39 +4,50 @@ import std::sort::qs; <* Sort list using the quick sort algorithm. + @require @list_is_by_ref(list) : "Expected a list passed by reference or be a slice" @require @is_sortable(list) : "The list must be indexable and support .len or .len()" @require @is_valid_cmp_fn(cmp, list, context) : "Expected a comparison function which compares values" @require @is_valid_context(cmp, context) : "Expected a valid context" *> macro void quicksort(list, cmp = EMPTY_MACRO_SLOT, context = EMPTY_MACRO_SLOT) @builtin { - $if $kindof(list) == POINTER &&& ($kindof(*list) == ARRAY || $kindof(*list) == VECTOR): - $typeof((*list)[0])[] list2 = list; - qs::qsort{$typeof(list2), $typeof(cmp), $typeof(context)}(list2, 0, (isz)list.len - 1, cmp, context); + $if $kindof(list) == SLICE: + qs::qsort{$typeof(list), $typeof(cmp), $typeof(context)}(list, 0, (isz)list.len - 1, cmp, context); $else - usz len = lengthof(list); - qs::qsort{$typeof(list), $typeof(cmp), $typeof(context)}(list, 0, (isz)len - 1, cmp, context); + qs::qsort{$typeof(*list), $typeof(cmp), $typeof(context)}(list, 0, (isz)lengthof(*list) - 1, cmp, context); $endif } <* -Select the (k+1)th smallest element in an unordered list using Hoare's -selection algorithm (Quickselect). k should be between 0 and len-1. The data -list will be partially sorted. + Select the (k+1)th smallest element in an unordered list using Hoare's + selection algorithm (Quickselect). k should be between 0 and len-1. The data + list will be partially sorted. + @require @list_is_by_ref(list) : "Expected a list passed by reference or be a slice" @require @is_sortable(list) : "The list must be indexable and support .len or .len()" @require @is_valid_cmp_fn(cmp, list, context) : "expected a comparison function which compares values" @require @is_valid_context(cmp, context) : "Expected a valid context" *> macro quickselect(list, isz k, cmp = EMPTY_MACRO_SLOT, context = EMPTY_MACRO_SLOT) @builtin { - usz len = lengthof(list); - return qs::qselect{$typeof(list), $typeof(cmp), $typeof(context)}(list, 0, (isz)len - 1, k, cmp, context); + $if $kindof(list) == SLICE: + return qs::qselect{$typeof(list), $typeof(cmp), $typeof(context)}(list, 0, (isz)list.len - 1, k, cmp, context); + $else + return qs::qselect{$typeof(*list), $typeof(cmp), $typeof(context)}(list, 0, (isz)lengthof(*list) - 1, k, cmp, context); + $endif } module std::sort::qs{Type, CmpFn, Context}; alias ElementType = $typeof(((Type){})[0]); +const bool IS_SLICE = Type.kindof == SLICE; +alias ListType = $typefrom(IS_SLICE ??? Type : Type*); +macro list_get(ListType l, i) @if(!IS_SLICE) => (*l)[i]; +macro list_get(ListType l, i) @if(IS_SLICE) => l[i]; +macro list_get_ref(ListType l, i) @if(!IS_SLICE) => &(*l)[i]; +macro list_get_ref(ListType l, i) @if(IS_SLICE) => &l[i]; +macro list_set(ListType l, i, v) @if(!IS_SLICE) => (*l)[i] = v; +macro list_set(ListType l, i, v) @if(IS_SLICE) => l[i] = v; struct StackElementItem @private { @@ -48,7 +59,7 @@ alias Stack @private = StackElementItem[64]; // Based on https://alienryderflex.com/quicksort by Darel Rex Finley, Public Domain. -fn void qsort(Type list, isz low, isz high, CmpFn cmp, Context context) +fn void qsort(ListType list, isz low, isz high, CmpFn cmp, Context context) { if (low >= 0 && high >= 0 && low < high) { @@ -86,7 +97,7 @@ fn void qsort(Type list, isz low, isz high, CmpFn cmp, Context context) @require low <= k : "kth smallest element is smaller than lower bounds" @require k <= high : "kth smallest element is larger than upper bounds" *> -fn ElementType? qselect(Type list, isz low, isz high, isz k, CmpFn cmp, Context context) +fn ElementType? qselect(ListType list, isz low, isz high, isz k, CmpFn cmp, Context context) { if (low >= 0 && high >= 0 && low < high) { @@ -98,7 +109,7 @@ fn ElementType? qselect(Type list, isz low, isz high, isz k, CmpFn cmp, Context while (l <= h && max_retries--) { pivot = @partition(list, l, h, cmp, context); - if (k == pivot) return list[k]; + if (k == pivot) return list_get(list, k); if (k < pivot) { h = pivot - 1; @@ -112,40 +123,40 @@ fn ElementType? qselect(Type list, isz low, isz high, isz k, CmpFn cmp, Context return NOT_FOUND?; } -macro isz @partition(Type list, isz l, isz h, CmpFn cmp, Context context) +macro isz @partition(ListType list, isz l, isz h, CmpFn cmp, Context context) { var $has_cmp = @is_valid_macro_slot(cmp); var $has_context = @is_valid_macro_slot(context); - var $cmp_by_value = $has_cmp &&& $defined($typefrom(CmpFn.paramsof[0].type) v = list[0]); + var $cmp_by_value = $has_cmp &&& $defined($typefrom(CmpFn.paramsof[0].type) v = list_get(list, 0)); - ElementType pivot = list[l]; + ElementType pivot = list_get(list, l); while (l < h) { $switch: $case $cmp_by_value && $has_context: - while (cmp(list[h], pivot, context) >= 0 && l < h) h--; - if (l < h) list[l++] = list[h]; - while (cmp(list[l], pivot, context) <= 0 && l < h) l++; + while (cmp(list_get(list, h), pivot, context) >= 0 && l < h) h--; + if (l < h) list_set(list, l++, list_get(list, h)); + while (cmp(list_get(list, l), pivot, context) <= 0 && l < h) l++; $case $cmp_by_value: - while (cmp(list[h], pivot) >= 0 && l < h) h--; - if (l < h) list[l++] = list[h]; - while (cmp(list[l], pivot) <= 0 && l < h) l++; + while (cmp(list_get(list, h), pivot) >= 0 && l < h) h--; + if (l < h) list_set(list, l++, list_get(list, h)); + while (cmp(list_get(list, l), pivot) <= 0 && l < h) l++; $case $has_cmp && $has_context: - while (cmp(&list[h], &pivot, context) >= 0 && l < h) h--; - if (l < h) list[l++] = list[h]; - while (cmp(&list[l], &pivot, context) <= 0 && l < h) l++; + while (cmp(list_get_ref(list, h), &pivot, context) >= 0 && l < h) h--; + if (l < h) list_set(list, l++, list_get(list, h)); + while (cmp(list_get_ref(list, l), &pivot, context) <= 0 && l < h) l++; $case $has_cmp: - while (cmp(&list[h], &pivot) >= 0 && l < h) h--; - if (l < h) list[l++] = list[h]; - while (cmp(&list[l], &pivot) <= 0 && l < h) l++; + while (cmp(list_get_ref(list, h), &pivot) >= 0 && l < h) h--; + if (l < h) list_set(list, l++, list_get(list, h)); + while (cmp(list_get_ref(list, l), &pivot) <= 0 && l < h) l++; $default: - while (greater_eq(list[h], pivot) && l < h) h--; - if (l < h) list[l++] = list[h]; - while (less_eq(list[l], pivot) && l < h) l++; + while (greater_eq(list_get(list, h), pivot) && l < h) h--; + if (l < h) list_set(list, l++, list_get(list, h)); + while (less_eq(list_get(list, l), pivot) && l < h) l++; $endswitch - if (l < h) list[h--] = list[l]; + if (l < h) list_set(list, h--, list_get(list, l)); } - list[l] = pivot; + list_set(list, l, pivot); return l; } diff --git a/lib/std/sort/sort.c3 b/lib/std/sort/sort.c3 index 7ebfa4c5d..fc06ed7f3 100644 --- a/lib/std/sort/sort.c3 +++ b/lib/std/sort/sort.c3 @@ -1,50 +1,132 @@ module std::sort; +macro bool @list_is_by_ref(#list) @const +{ + return $kindof(#list) == SLICE ||| ($kindof(#list) == POINTER &&& $kindof(*#list) != SLICE); +} -macro bool @is_sortable(#list) +<* + @require @list_is_by_ref(#list) : "Expected the list to be passed by ref or be a slice" +*> +macro bool @is_sortable(#list) @const { $switch: - $case !$defined(#list[0]): + $case $kindof(#list) == SLICE: + return true; + $case !$defined(*#list): return false; - $case !$defined(#list.len): + $case !$defined((*#list)[0]): return false; - $case $kindof(#list) == VECTOR || $kindof(#list) == ARRAY: + $case !$defined(lengthof(*#list)): return false; - $case $defined(&#list[0]) &&& !types::is_same($typeof(&#list[0]), $typeof(#list[0])*): + $case $defined(&(*#list)[0]) &&& $typeof(&(*#list)[0]) != $typeof((*#list)[0])*: return false; $default: return true; $endswitch } +macro bool @is_any_sortable(#list) @const +{ + $switch $kindof(#list): + $case SLICE: + return true; + $case POINTER: + return @is_sortable(#list); + $default: + return $defined(#list[0]) &&& $defined(lengthof(#list)) + &&& !($defined(&#list[0]) &&& $typeof(&#list[0]) != $typeof(#list[0])*); + $endswitch +} + macro bool @is_valid_context(#cmp, #context) { return @is_valid_macro_slot(#cmp) || @is_empty_macro_slot(#context); } -macro bool @is_valid_cmp_fn(#cmp, #list, #context) + +<* + @require @list_is_by_ref(#list) : "Expected the list to be passed by ref or be a slice" +*> +macro bool @is_valid_cmp_fn(#cmp, #list, #context) @const { var $Type = $typeof(#cmp); var $no_context = @is_empty_macro_slot(#context); $switch: $case @is_empty_macro_slot(#cmp): return true; $case $Type.kindof != FUNC ||| $Type.returns.kindof != SIGNED_INT: return false; - $case $defined(#cmp(#list[0], #list[0], #context)): return true; - $case $defined(#cmp(#list[0], #list[0])): return $no_context; - $case $defined(#cmp(&#list[0], &#list[0], #context)): return true; - $case $defined(#cmp(&#list[0], &#list[0])): return $no_context; - $default: return false; + $default: + $if $kindof(#list) == SLICE: + $switch: + $case $defined(#cmp((#list)[0], (#list)[0], #context)): return true; + $case $defined(#cmp((#list)[0], (#list)[0])): return $no_context; + $case $defined(#cmp(&(#list)[0], &(#list)[0], #context)): return true; + $case $defined(#cmp(&(#list)[0], &(#list)[0])): return $no_context; + $default: return false; + $endswitch + $else + $switch: + $case $defined(#cmp((*#list)[0], (*#list)[0], #context)): return true; + $case $defined(#cmp((*#list)[0], (*#list)[0])): return $no_context; + $case $defined(#cmp(&(*#list)[0], &(*#list)[0], #context)): return true; + $case $defined(#cmp(&(*#list)[0], &(*#list)[0])): return $no_context; + $default: return false; + $endswitch + $endif $endswitch } -macro bool @is_cmp_key_fn(#key_fn, #list) +macro bool @is_any_valid_cmp_fn(#cmp, #list, #context) @const +{ + var $Type = $typeof(#cmp); + var $no_context = @is_empty_macro_slot(#context); + $switch: + $case @is_empty_macro_slot(#cmp): return true; + $case $Type.kindof != FUNC ||| $Type.returns.kindof != SIGNED_INT: return false; + $default: + $if $kindof(#list) != POINTER: + $switch: + $case $defined(#cmp((#list)[0], (#list)[0], #context)): return true; + $case $defined(#cmp((#list)[0], (#list)[0])): return $no_context; + $case $defined(#cmp(&(#list)[0], &(#list)[0], #context)): return true; + $case $defined(#cmp(&(#list)[0], &(#list)[0])): return $no_context; + $default: return false; + $endswitch + $else + $switch: + $case $defined(#cmp((*#list)[0], (*#list)[0], #context)): return true; + $case $defined(#cmp((*#list)[0], (*#list)[0])): return $no_context; + $case $defined(#cmp(&(*#list)[0], &(*#list)[0], #context)): return true; + $case $defined(#cmp(&(*#list)[0], &(*#list)[0])): return $no_context; + $default: return false; + $endswitch + $endif + $endswitch +} + +<* + @require @list_is_by_ref(#list) : "Expected the list to be passed by ref or be a slice" +*> +macro bool @is_cmp_key_fn(#key_fn, #list) @const { $switch: $case @is_empty_macro_slot(#key_fn): return true; $case $kindof(#key_fn) != FUNC: return false; $case $typeof(#key_fn).returns.kindof != UNSIGNED_INT: return false; - $case $defined(#key_fn(#list[0])): return true; - $case $defined(#key_fn(&&(#list[0]))): return true; - $default: return false; + $default: + $if $kindof(#list) == SLICE: + $switch: + $case $defined(#key_fn((#list)[0])): return true; + $case $defined(#key_fn(&&((#list)[0]))): return true; + $default: return false; + $endswitch + $else + $switch: + $case $defined(#key_fn((*#list)[0])): return true; + $case $defined(#key_fn(&&((*#list)[0]))): return true; + $default: return false; + $endswitch + $endif $endswitch + } \ No newline at end of file diff --git a/lib/std/sort/sorted.c3 b/lib/std/sort/sorted.c3 index fa3cffea1..dc6cc653a 100644 --- a/lib/std/sort/sorted.c3 +++ b/lib/std/sort/sorted.c3 @@ -1,13 +1,15 @@ module std::sort; <* -Returns true if list is sorted in either ascending or descending order. - @require @is_sortable(list) : "The list must be indexable and support .len or .len()" - @require @is_valid_cmp_fn(cmp, list, ctx) : "Expected a comparison function which compares values" + Returns true if list is sorted in either ascending or descending order. + + @require @is_any_sortable(list) : "The list must be indexable and support .len or .len()" + @require @is_any_valid_cmp_fn(cmp, list, ctx) : "Expected a comparison function which compares values" @require @is_valid_context(cmp, ctx) : "Expected a valid context" *> macro bool is_sorted(list, cmp = EMPTY_MACRO_SLOT, ctx = EMPTY_MACRO_SLOT) @builtin { + $if ($kindof(list) != POINTER): var $Type = $typeof(list); usz len = lengthof(list); if (len <= 1) return true; @@ -16,6 +18,33 @@ macro bool is_sorted(list, cmp = EMPTY_MACRO_SLOT, ctx = EMPTY_MACRO_SLOT) @buil usz i; int sort_order; + // determine sort order (ascending or descending) + for (i = start; i < end && sort_order == 0; i++) + { + sort_order = @sort_cmp_slice(list, i, cmp, ctx); + } + + // no sort order found, all elements are the same, consider list sorted + if (sort_order == 0) return true; + + // compare adjacent elements to the sort order + for (; i < end; i++) + { + if (sort_order * @sort_cmp_slice(list, i, cmp, ctx) < 0) return false; + } + return true; + }; + + return check_sort(list, 0, len - 1, cmp, ctx); + $else + var $Type = $typeof(*list); + usz len = lengthof(*list); + if (len <= 1) return true; + var check_sort = fn bool($Type* list, usz start, usz end, $typeof(cmp) cmp, $typeof(ctx) ctx) + { + usz i; + int sort_order; + // determine sort order (ascending or descending) for (i = start; i < end && sort_order == 0; i++) { @@ -33,9 +62,33 @@ macro bool is_sorted(list, cmp = EMPTY_MACRO_SLOT, ctx = EMPTY_MACRO_SLOT) @buil return true; }; return check_sort(list, 0, len - 1, cmp, ctx); + $endif } macro int @sort_cmp(list, pos, cmp, ctx) @local +{ + var $has_cmp = @is_valid_macro_slot(cmp); + var $has_context = @is_valid_macro_slot(ctx); + var $cmp_by_value = $has_cmp &&& $defined($typefrom($typeof(cmp).paramsof[0].type) v = (*list)[0]); + + var a = (*list)[pos]; + var b = (*list)[pos+1]; + + $switch: + $case $cmp_by_value && $has_context: + return cmp(a, b); + $case $cmp_by_value: + return cmp(a, b); + $case $has_cmp && $has_context: + return cmp(&a, &b, ctx); + $case $has_cmp: + return cmp(&a, &b); + $default: + return compare_to(a,b); + $endswitch +} + +macro int @sort_cmp_slice(list, pos, cmp, ctx) @local { var $has_cmp = @is_valid_macro_slot(cmp); var $has_context = @is_valid_macro_slot(ctx); diff --git a/releasenotes.md b/releasenotes.md index b28bedccf..5cf36339a 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -15,6 +15,7 @@ - Incorrect visibility on local globals with public aliases. #2519 ### Stdlib changes +- Sorting functions correctly took slices by value, but also other types by value. Now, only slices are accepted by value, other containers are always by ref. ## 0.7.6 Change list diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index 63ccfb26d..43b02ff5b 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -10693,7 +10693,7 @@ static bool sema_expr_analyse_lenof(SemaContext *context, Expr *expr, bool *miss *missing_ref = true; return false; } - RETURN_SEMA_ERROR(inner, "%s does support lengthof()", type_quoted_error_string(inner->type)); + RETURN_SEMA_ERROR(inner, "%s does not support lengthof()", type_quoted_error_string(inner->type)); } } diff --git a/test/unit/stdlib/collections/map.c3 b/test/unit/stdlib/collections/map.c3 index 91d291976..ab80d2e8c 100644 --- a/test/unit/stdlib/collections/map.c3 +++ b/test/unit/stdlib/collections/map.c3 @@ -47,7 +47,7 @@ fn void map() list.push({key, value}); }; assert(list.len() == tcases.len); - quicksort(list, fn int (MapTest a, MapTest b) => (int)(a.value - b.value)); + quicksort(&list, fn int (MapTest a, MapTest b) => (int)(a.value - b.value)); foreach (i, tc : tcases) { assert(tc.key == list[i].key); diff --git a/test/unit/stdlib/sort/countingsort.c3 b/test/unit/stdlib/sort/countingsort.c3 index 399090266..026cea20b 100644 --- a/test/unit/stdlib/sort/countingsort.c3 +++ b/test/unit/stdlib/sort/countingsort.c3 @@ -79,7 +79,7 @@ fn void countingsort_list() { CountingSortTestList list; list.push_all({ 2, 1, 3}); - sort::countingsort(list, &sort::key_int_value); + sort::countingsort(&list, &sort::key_int_value); assert(check::int_ascending_sort(list.array_view())); } @@ -94,7 +94,7 @@ fn void countingsort_random_large_list() list.push(random.next_int()); } - sort::countingsort(list, &sort::key_int_value); + sort::countingsort(&list, &sort::key_int_value); assert(check::int_ascending_sort(list.array_view())); list.free(); } \ No newline at end of file diff --git a/test/unit/stdlib/sort/insertionsort.c3 b/test/unit/stdlib/sort/insertionsort.c3 index 31d4b1aff..9c044c163 100644 --- a/test/unit/stdlib/sort/insertionsort.c3 +++ b/test/unit/stdlib/sort/insertionsort.c3 @@ -84,7 +84,7 @@ fn void insertionsort_list() { InsertionSortTestList list; list.push_all({ 2, 1, 3}); - sort::insertionsort(list, &sort::cmp_int_value); + sort::insertionsort(&list, &sort::cmp_int_value); assert(check::int_ascending_sort(list.array_view())); } diff --git a/test/unit/stdlib/sort/quicksort.c3 b/test/unit/stdlib/sort/quicksort.c3 index edb81f949..c7eee279d 100644 --- a/test/unit/stdlib/sort/quicksort.c3 +++ b/test/unit/stdlib/sort/quicksort.c3 @@ -84,7 +84,7 @@ fn void quicksort_list() { List2 list; list.push_all({ 2, 1, 3}); - sort::quicksort(list, &sort::cmp_int_value); + sort::quicksort(&list, &sort::cmp_int_value); assert(check::int_sort(list.array_view())); } From e34a26422f3fbd143e72381c6d215d596fdaa139 Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Tue, 7 Oct 2025 23:47:05 +0200 Subject: [PATCH 10/49] Change macro recursion depth to work on MSVC --- src/compiler/compiler_internal.h | 1 + src/compiler/sema_expr.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/compiler/compiler_internal.h b/src/compiler/compiler_internal.h index 56536615f..6412058d6 100644 --- a/src/compiler/compiler_internal.h +++ b/src/compiler/compiler_internal.h @@ -54,6 +54,7 @@ typedef uint16_t FileId; #define MAX_GLOBAL_DECL_STACK (65536) #define MAX_MODULE_NAME 31 #define MAX_MODULE_PATH 63 +#define MAX_MACRO_RECURSION_DEPTH 128 #define MEMCMP_INLINE_REGS 8 #define UINT128_MAX ((Int128) { UINT64_MAX, UINT64_MAX }) #define INT128_MAX ((Int128) { INT64_MAX, UINT64_MAX }) diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index 43b02ff5b..c9a92ac98 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -2736,7 +2736,7 @@ bool sema_expr_analyse_macro_call(SemaContext *context, Expr *call_expr, Expr *s bool is_outer = call_expr->call_expr.is_outer_call; ASSERT_SPAN(call_expr, decl->decl_kind == DECL_MACRO); - if (context->macro_call_depth > 256) + if (context->macro_call_depth > MAX_MACRO_RECURSION_DEPTH) { decl->decl_kind = DECL_POISONED; RETURN_SEMA_ERROR(call_expr, "Failure evaluating macro, max call depth reached, " From 0d85caf21c6ab98d764f3a0bc776f91f857d2f0f Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Thu, 9 Oct 2025 12:45:55 +0200 Subject: [PATCH 11/49] Add splat defaults for designated initialization #2441. Add ??? and +++= to list-precedence. --- releasenotes.md | 2 + src/compiler/compiler.c | 4 +- src/compiler/compiler_internal.h | 12 +++- src/compiler/copying.c | 3 +- src/compiler/expr.c | 2 +- src/compiler/llvm_codegen_expr.c | 64 ++++++++++++++---- src/compiler/parse_expr.c | 16 ++++- src/compiler/sema_initializers.c | 54 ++++++++++++++- src/compiler/sema_liveness.c | 3 +- .../functions/splat_initializer.c3t | 66 +++++++++++++++++++ 10 files changed, 204 insertions(+), 22 deletions(-) create mode 100644 test/test_suite/functions/splat_initializer.c3t diff --git a/releasenotes.md b/releasenotes.md index 5cf36339a..24011fb10 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -4,6 +4,7 @@ ### Changes / improvements - Error when using $vaarg/$vacount/$vasplat and similar in a macro without vaargs #2510. +- Add splat defaults for designated initialization #2441. ### Fixes - Bug in `io::write_using_write_byte`. @@ -13,6 +14,7 @@ - Compiler segfault when accessing member of number cast to bitstruct #2516. - Compiler assert when getting a member of a `bitstruct : char @bigendian` #2517. - Incorrect visibility on local globals with public aliases. #2519 +- Add ??? and +++= to list-precedence. ### Stdlib changes - Sorting functions correctly took slices by value, but also other types by value. Now, only slices are accepted by value, other containers are always by ref. diff --git a/src/compiler/compiler.c b/src/compiler/compiler.c index ea7c732fc..c4188e181 100644 --- a/src/compiler/compiler.c +++ b/src/compiler/compiler.c @@ -1171,8 +1171,8 @@ void print_syntax(BuildOptions *options) puts(" 8. Relational | < > <= >= == !="); puts(" 9. And | && &&&"); puts("10. Or | || |||"); - puts("11. Ternary | ?: ??"); - puts("12. Assign | = *= /= %= -= += |= &= ^= <<= >>="); + puts("11. Ternary | ?: ?? ???"); + puts("12. Assign | = *= /= %= -= += |= &= ^= <<= >>= +++="); } } diff --git a/src/compiler/compiler_internal.h b/src/compiler/compiler_internal.h index 6412058d6..84f81ab11 100644 --- a/src/compiler/compiler_internal.h +++ b/src/compiler/compiler_internal.h @@ -1179,6 +1179,12 @@ typedef struct bool is_ref; } ExprOtherContext; +typedef struct +{ + Expr **list; + Expr *splat; +} ExprDesignatedInit; + typedef struct { Expr *inner; @@ -1231,7 +1237,7 @@ struct Expr_ ExprIdentifierRaw ct_ident_expr; // 24 Decl *decl_expr; // 8 Decl *iota_decl_expr; // 8 - Expr **designated_init_list; // 8 + ExprDesignatedInit designated_init; // 16 ExprDesignator designator_expr; // 16 ExprNamedArgument named_argument_expr; ExprEmbedExpr embed_expr; // 16 @@ -3719,6 +3725,7 @@ static inline void exprid_set_span(ExprId expr_id, SourceSpan loc); static inline void expr_set_span(Expr *expr, SourceSpan loc) { + if (!expr) return; expr->span = loc; switch (expr->expr_kind) { @@ -3755,7 +3762,8 @@ static inline void expr_set_span(Expr *expr, SourceSpan loc) expr_list_set_span(expr->initializer_list, loc); return; case EXPR_DESIGNATED_INITIALIZER_LIST: - expr_list_set_span(expr->designated_init_list, loc); + expr_set_span(expr->designated_init.splat, loc); + expr_list_set_span(expr->designated_init.list, loc); return; case EXPR_MAKE_ANY: expr_set_span(expr->make_any_expr.inner, loc); diff --git a/src/compiler/copying.c b/src/compiler/copying.c index 56712fb7e..3db95a3ae 100644 --- a/src/compiler/copying.c +++ b/src/compiler/copying.c @@ -592,7 +592,8 @@ Expr *copy_expr(CopyStruct *c, Expr *source_expr) MACRO_COPY_EXPR_LIST(expr->initializer_list); return expr; case EXPR_DESIGNATED_INITIALIZER_LIST: - MACRO_COPY_EXPR_LIST(expr->designated_init_list); + MACRO_COPY_EXPR(expr->designated_init.splat); + MACRO_COPY_EXPR_LIST(expr->designated_init.list); return expr; case EXPR_EXPRESSION_LIST: MACRO_COPY_EXPR_LIST(expr->expression_list); diff --git a/src/compiler/expr.c b/src/compiler/expr.c index a52e262c0..9d208f971 100644 --- a/src/compiler/expr.c +++ b/src/compiler/expr.c @@ -420,7 +420,7 @@ bool expr_is_runtime_const(Expr *expr) case EXPR_INITIALIZER_LIST: return expr_list_is_constant_eval(expr->initializer_list); case EXPR_DESIGNATED_INITIALIZER_LIST: - return expr_list_is_constant_eval(expr->designated_init_list); + return (!expr->designated_init.splat || expr_is_const(expr->designated_init.splat)) && expr_list_is_constant_eval(expr->designated_init.list); case EXPR_SLICE: if (!exprid_is_runtime_const(expr->slice_expr.expr)) return false; return expr->slice_expr.range.range_type == RANGE_CONST_RANGE; diff --git a/src/compiler/llvm_codegen_expr.c b/src/compiler/llvm_codegen_expr.c index 75962e9f3..0743e292e 100644 --- a/src/compiler/llvm_codegen_expr.c +++ b/src/compiler/llvm_codegen_expr.c @@ -49,7 +49,7 @@ static void llvm_emit_unary_expr(GenContext *c, BEValue *value, Expr *expr); static inline void llvm_emit_memcmp(GenContext *c, BEValue *be_value, LLVMValueRef ptr, LLVMValueRef other_ptr, LLVMValueRef size); static LLVMTypeRef llvm_find_inner_struct_type_for_coerce(GenContext *c, LLVMTypeRef struct_type, ByteSize dest_size); static void llvm_expand_type_to_args(GenContext *context, Type *param_type, LLVMValueRef expand_ptr, LLVMValueRef *args, unsigned *arg_count_ref, AlignSize alignment); -static inline void llvm_emit_initialize_reference_designated_bitstruct(GenContext *c, BEValue *ref, Decl *bitstruct, Expr **elements); +static inline void llvm_emit_initialize_reference_designated_bitstruct(GenContext *c, BEValue *ref, Decl *bitstruct, Expr **elements, Expr *splat); INLINE LLVMValueRef llvm_emit_bitstruct_value_update(GenContext *c, LLVMValueRef current_val, TypeSize bits, LLVMTypeRef bitstruct_type, Decl *member, LLVMValueRef val); INLINE void llvm_emit_initialize_reference_bitstruct_array(GenContext *c, BEValue *ref, Decl *bitstruct, Expr** elements); #define MAX_AGG 16 @@ -1824,12 +1824,21 @@ static void llvm_emit_initialize_designated_element(GenContext *c, BEValue *ref, } } -static inline void llvm_emit_initialize_reference_designated_bitstruct_array(GenContext *c, BEValue *ref, Decl *bitstruct, Expr **elements) +static inline void llvm_emit_initialize_reference_designated_bitstruct_array(GenContext *c, BEValue *ref, Decl *bitstruct, Expr **elements, Expr *splat) { LLVMTypeRef type = llvm_get_type(c, ref->type); bool is_bitswap = bitstruct_requires_bitswap(bitstruct); llvm_value_addr(c, ref); - llvm_store_zero(c, ref); + if (splat) + { + BEValue splat_val; + llvm_emit_expr(c, &splat_val, splat); + llvm_store(c, ref, &splat_val); + } + else + { + llvm_store_zero(c, ref); + } AlignSize alignment = ref->alignment; LLVMValueRef array_ptr = ref->value; // Now walk through the elements. @@ -1845,16 +1854,27 @@ static inline void llvm_emit_initialize_reference_designated_bitstruct_array(Gen } } -static inline void llvm_emit_initialize_reference_designated_bitstruct(GenContext *c, BEValue *ref, Decl *bitstruct, Expr **elements) +static inline void llvm_emit_initialize_reference_designated_bitstruct(GenContext *c, BEValue *ref, Decl *bitstruct, Expr **elements, Expr *splat) { Type *underlying_type = type_lowering(ref->type); if (underlying_type->type_kind == TYPE_ARRAY) { - llvm_emit_initialize_reference_designated_bitstruct_array(c, ref, bitstruct, elements); + llvm_emit_initialize_reference_designated_bitstruct_array(c, ref, bitstruct, elements, splat); return; } LLVMTypeRef type = llvm_get_type(c, underlying_type); - LLVMValueRef data = LLVMConstNull(type); + LLVMValueRef data; + if (!splat) + { + data = LLVMConstNull(type); + } + else + { + BEValue splat_val; + llvm_emit_expr(c, &splat_val, splat); + llvm_value_rvalue(c, &splat_val); + data = splat_val.value; + } TypeSize bits = type_bit_size(underlying_type); // Now walk through the elements. @@ -1877,13 +1897,15 @@ static inline void llvm_emit_initialize_reference_designated_bitstruct(GenContex static inline void llvm_emit_initialize_reference_designated(GenContext *c, BEValue *ref, Expr *expr) { - Expr **elements = expr->designated_init_list; + + Expr **elements = expr->designated_init.list; + Expr *splat = expr->designated_init.splat; ASSERT(vec_size(elements)); Type *type = type_flatten(expr->type); ASSERT(type->type_kind != TYPE_SLICE); if (type->type_kind == TYPE_BITSTRUCT) { - llvm_emit_initialize_reference_designated_bitstruct(c, ref, type->decl, elements); + llvm_emit_initialize_reference_designated_bitstruct(c, ref, type->decl, elements, splat); return; } @@ -1891,7 +1913,16 @@ static inline void llvm_emit_initialize_reference_designated(GenContext *c, BEVa llvm_value_addr(c, ref); // Clear the memory - llvm_store_zero(c, ref); + if (splat) + { + BEValue splat_value; + llvm_emit_expr(c, &splat_value, splat); + llvm_store(c, ref, &splat_value); + } + else + { + llvm_store_zero(c, ref); + } // Now walk through the elements. FOREACH(Expr *, designator, elements) @@ -6369,8 +6400,19 @@ static inline void llvm_emit_vector_initializer_list(GenContext *c, BEValue *val } else { - vec_value = llvm_get_zero_raw(llvm_type); - Expr **elements = expr->designated_init_list; + Expr **elements = expr->designated_init.list; + Expr *splat = expr->designated_init.splat; + if (splat) + { + BEValue splat_val; + llvm_emit_expr(c, &splat_val, splat); + llvm_value_rvalue(c, &splat_val); + vec_value = splat_val.value; + } + else + { + vec_value = llvm_get_zero_raw(llvm_type); + } FOREACH(Expr *, designator, elements) { diff --git a/src/compiler/parse_expr.c b/src/compiler/parse_expr.c index da440d8c4..a4471ab3a 100644 --- a/src/compiler/parse_expr.c +++ b/src/compiler/parse_expr.c @@ -862,13 +862,18 @@ static Expr *parse_initializer_list(ParseContext *c, Expr *left, SourceSpan lhs_ ASSERT(!left && "Unexpected left hand side"); Expr *initializer_list = EXPR_NEW_TOKEN(EXPR_INITIALIZER_LIST); advance_and_verify(c, TOKEN_LBRACE); + Expr *splat = NULL; if (!try_consume(c, TOKEN_RBRACE)) { Expr **exprs = NULL; if (!parse_init_list(c, &exprs, TOKEN_RBRACE, NULL, true)) return poisoned_expr; int designated = -1; - FOREACH(Expr *, expr, exprs) + FOREACH_IDX(i, Expr *, expr, exprs) { + if (i == 0 && expr->expr_kind == EXPR_SPLAT) + { + splat = expr; + } if (expr->expr_kind == EXPR_DESIGNATOR) { if (designated == 0) @@ -876,10 +881,12 @@ static Expr *parse_initializer_list(ParseContext *c, Expr *left, SourceSpan lhs_ designated = expr->designator_expr.path[0]->kind == DESIGNATOR_FIELD ? 1 : 2; goto ERROR; } + designated = expr->designator_expr.path[0]->kind == DESIGNATOR_FIELD ? 1 : 2; continue; } if (designated > 0) goto ERROR; + if (designated == -1 && splat) continue; designated = 0; continue; ERROR:; @@ -896,7 +903,12 @@ ERROR:; RANGE_EXTEND_PREV(initializer_list); if (designated > 0) { - initializer_list->designated_init_list = exprs; + if (splat) + { + vec_erase_front(exprs, 1); + splat = splat->inner_expr; + } + initializer_list->designated_init = (ExprDesignatedInit) { .splat = splat, .list = exprs }; initializer_list->expr_kind = EXPR_DESIGNATED_INITIALIZER_LIST; } else diff --git a/src/compiler/sema_initializers.c b/src/compiler/sema_initializers.c index 21a077d96..c71bb1182 100644 --- a/src/compiler/sema_initializers.c +++ b/src/compiler/sema_initializers.c @@ -503,7 +503,17 @@ static inline bool sema_expr_analyse_untyped_initializer(SemaContext *context, E static bool sema_expr_analyse_designated_initializer(SemaContext *context, Type *assigned, Type *flattened, Expr *initializer, bool *no_match_ref) { - Expr **init_expressions = initializer->designated_init_list; + Expr **init_expressions = initializer->designated_init.list; + Expr *splat = initializer->designated_init.splat; + if (splat) + { + if (!sema_analyse_expr_rvalue(context, splat)) return false; + sema_cast_const(splat); + if (IS_OPTIONAL(splat)) + { + RETURN_SEMA_ERROR(splat, "An optional splat is not permitted."); + } + } Type *original = flattened->canonical; bool is_bitstruct = original->type_kind == TYPE_BITSTRUCT; bool is_structlike = type_is_union_or_strukt(original) || is_bitstruct; @@ -558,6 +568,36 @@ static bool sema_expr_analyse_designated_initializer(SemaContext *context, Type { type = assigned; } + if (splat && type->canonical != splat->type->canonical) + { + if (type_is_subtype(splat->type->canonical, type->canonical)) + { + Decl *decl = original->decl; + Expr *designator = expr_new(EXPR_DESIGNATOR, initializer->span); + DesignatorElement **elements = NULL; + while (true) + { + DesignatorElement *designator_element = MALLOCS(DesignatorElement); + designator_element->kind = DESIGNATOR_FIELD; + designator_element->index = 0; + vec_add(elements, designator_element); + assert(decl->is_substruct); + Decl *member = decl->strukt.members[0]; + if (member->type->canonical == splat->type) break; + decl = member; + } + designator->resolve_status = RESOLVE_DONE; + designator->designator_expr.path = elements; + designator->designator_expr.value = splat; + designator->type = splat->type; + vec_insert_first(initializer->designated_init.list, designator); + initializer->designated_init.splat = NULL; + } + else + { + RETURN_SEMA_ERROR(splat, "Splat type does not match initializer type."); + } + } initializer->type = type_add_optional(type, optional); initializer->resolve_status = RESOLVE_DONE; if (expr_is_runtime_const(initializer)) @@ -647,7 +687,17 @@ NO_MATCH:; static void sema_create_const_initializer_from_designated_init(ConstInitializer *const_init, Expr *initializer) { // Flatten the type since the external type might be typedef or a distinct type. - const_init_rewrite_to_zero(const_init, type_flatten(initializer->type)); + Type *flattened = type_flatten(initializer->type); + if (initializer->designated_init.splat) + { + Expr *splat = initializer->designated_init.splat; + ASSERT_SPAN(splat, expr_is_const_initializer(splat)); + *const_init = *splat->const_expr.initializer; + } + else + { + const_init_rewrite_to_zero(const_init, flattened); + } ASSERT(type_flatten(initializer->type)->type_kind != TYPE_SLICE); // Loop through the initializers. FOREACH(Expr *, expr, initializer->initializer_list) diff --git a/src/compiler/sema_liveness.c b/src/compiler/sema_liveness.c index 2ec944f0a..53619bf84 100644 --- a/src/compiler/sema_liveness.c +++ b/src/compiler/sema_liveness.c @@ -401,7 +401,8 @@ static void sema_trace_expr_liveness(Expr *expr) sema_trace_expr_list_liveness(expr->expression_list); return; case EXPR_DESIGNATED_INITIALIZER_LIST: - sema_trace_expr_list_liveness(expr->designated_init_list); + sema_trace_expr_liveness(expr->designated_init.splat); + sema_trace_expr_list_liveness(expr->designated_init.list); return; case EXPR_IDENTIFIER: sema_trace_decl_liveness(expr->ident_expr); diff --git a/test/test_suite/functions/splat_initializer.c3t b/test/test_suite/functions/splat_initializer.c3t new file mode 100644 index 000000000..ddea7d81e --- /dev/null +++ b/test/test_suite/functions/splat_initializer.c3t @@ -0,0 +1,66 @@ +// #target: mingw-x64 +module test; +import std; +struct ParentType +{ + int a; + int b; + int x; + int y; +} +struct ChildType +{ + inline ParentType p; + int c; + int ef; + char[10] ff; +} + +const ParentType ABC = { .a = 1, .b = 2, .x = 12312, .y = -1 }; +const ChildType ABC2 = { .a = 1, .b = 1, .x = 12, .y = 0 }; + +ChildType gc = { ...ABC2, .b = 7, .c = 8 }; +ChildType gc2 = { ...ABC, .b = 7, .c = 8 }; + +fn void main() +{ + + ParentType some_p = { .a = 1, .b = 2, .x = 12312, .y = -1 }; + ChildType c = { ...some_p, .b = 7, .c = 8 }; + ParentType t = { ...some_p, .b = 343}; + ChildType d = gc; + ChildType e = gc2; +} + +/* #expect: test.ll + +@"$ct.test.ParentType" = linkonce global %.introspect { i8 10, i64 0, ptr null, i64 16, i64 0, i64 4, [0 x i64] zeroinitializer }, comdat, align 8 +@"$ct.test.ChildType" = linkonce global %.introspect { i8 10, i64 ptrtoint (ptr @"$ct.test.ParentType" to i64), ptr null, i64 36, i64 0, i64 4, [0 x i64] zeroinitializer }, comdat, align 8 +@test.ABC = local_unnamed_addr constant %ParentType { i32 1, i32 7, i32 12312, i32 -1 }, align 4 +@test.ABC2 = local_unnamed_addr constant %ChildType { %ParentType { i32 1, i32 7, i32 12, i32 0 }, i32 8, i32 0, [10 x i8] zeroinitializer }, align 4 +@test.gc = local_unnamed_addr global %ChildType { %ParentType { i32 1, i32 7, i32 12, i32 0 }, i32 8, i32 0, [10 x i8] zeroinitializer }, align 4 +@test.gc2 = local_unnamed_addr global %ChildType { %ParentType { i32 1, i32 7, i32 12312, i32 -1 }, i32 8, i32 0, [10 x i8] zeroinitializer }, align 4 +@.__const = private unnamed_addr constant %ParentType { i32 1, i32 2, i32 12312, i32 -1 }, align 4 + +; Function Attrs: nounwind uwtable +define void @test.main() #0 { +entry: + %some_p = alloca %ParentType, align 4 + %c = alloca %ChildType, align 4 + %t = alloca %ParentType, align 4 + %d = alloca %ChildType, align 4 + %e = alloca %ChildType, align 4 + call void @llvm.memcpy.p0.p0.i32(ptr align 4 %some_p, ptr align 4 @.__const, i32 16, i1 false) + call void @llvm.memset.p0.i64(ptr align 4 %c, i8 0, i64 36, i1 false) + call void @llvm.memcpy.p0.p0.i32(ptr align 4 %c, ptr align 4 %some_p, i32 16, i1 false) + %ptradd = getelementptr inbounds i8, ptr %c, i64 4 + store i32 7, ptr %ptradd, align 4 + %ptradd1 = getelementptr inbounds i8, ptr %c, i64 16 + store i32 8, ptr %ptradd1, align 4 + call void @llvm.memcpy.p0.p0.i32(ptr align 4 %t, ptr align 4 %some_p, i32 16, i1 false) + %ptradd2 = getelementptr inbounds i8, ptr %t, i64 4 + store i32 343, ptr %ptradd2, align 4 + call void @llvm.memcpy.p0.p0.i32(ptr align 4 %d, ptr align 4 @test.gc, i32 36, i1 false) + call void @llvm.memcpy.p0.p0.i32(ptr align 4 %e, ptr align 4 @test.gc2, i32 36, i1 false) + ret void +} From a000ae560acd5a4742683a2f053b1d8f0e0d520c Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Thu, 9 Oct 2025 22:13:59 +0200 Subject: [PATCH 12/49] Add new builtins `$$str_snakecase` `$$str_replace` and `$$str_pascalcase`. Added `@str_snakecase`, `@str_replace` and `@str_pascalcase` builtin compile time macros based on the `$$` builtins. --- lib/std/core/builtin.c3 | 21 ++++ releasenotes.md | 2 + src/compiler/enums.h | 7 +- src/compiler/llvm_codegen_builtins.c | 3 + src/compiler/sema_builtins.c | 125 ++++++++++++++++++- src/compiler/sema_expr.c | 1 + src/compiler/symtab.c | 3 + test/test_suite/builtins/string_builtins.c3t | 29 +++++ 8 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 test/test_suite/builtins/string_builtins.c3t diff --git a/lib/std/core/builtin.c3 b/lib/std/core/builtin.c3 index dc0d430c3..8c6e967bd 100644 --- a/lib/std/core/builtin.c3 +++ b/lib/std/core/builtin.c3 @@ -547,6 +547,27 @@ macro isz @str_find(String $string, String $needle) @builtin => $$str_find($stri macro String @str_upper(String $str) @builtin => $$str_upper($str); macro String @str_lower(String $str) @builtin => $$str_lower($str); macro uint @str_hash(String $str) @builtin => $$str_hash($str); +macro String @str_pascalcase(String $str) @builtin => $$str_pascalcase($str); +macro String @str_snakecase(String $str) @builtin => $$str_snakecase($str); +macro String @str_camelcase(String $str) @builtin => @str_capitalize($$str_pascalcase($str)); +macro String @str_constantcase(String $str) @builtin => @str_upper($$str_snakecase($str)); +macro String @str_replace(String $str, String $pattern, String $replace, uint $limit = 0) @builtin => $$str_replace($str, $pattern, $replace, $limit); +macro String @str_capitalize(String $str) @builtin +{ + $switch $str.len: + $case 0: return $str; + $case 1: return $$str_upper($str); + $default: return $$str_upper($str[0:1]) +++ $str[1..]; + $endswitch +} +macro String @str_uncapitalize(String $str) @builtin +{ + $switch $str.len: + $case 0: return $str; + $case 1: return $$str_lower($str); + $default: return $$str_lower($str[0:1]) +++ $str[1..]; + $endswitch +} macro @generic_hash_core(h, value) { diff --git a/releasenotes.md b/releasenotes.md index 24011fb10..1e2b6dd86 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -5,6 +5,7 @@ ### Changes / improvements - Error when using $vaarg/$vacount/$vasplat and similar in a macro without vaargs #2510. - Add splat defaults for designated initialization #2441. +- Add new builtins `$$str_snakecase` `$$str_replace` and `$$str_pascalcase`. ### Fixes - Bug in `io::write_using_write_byte`. @@ -18,6 +19,7 @@ ### Stdlib changes - Sorting functions correctly took slices by value, but also other types by value. Now, only slices are accepted by value, other containers are always by ref. +- Added `@str_snakecase`, `@str_replace` and `@str_pascalcase` builtin compile time macros based on the `$$` builtins. ## 0.7.6 Change list diff --git a/src/compiler/enums.h b/src/compiler/enums.h index ff43c35b9..9a572b20a 100644 --- a/src/compiler/enums.h +++ b/src/compiler/enums.h @@ -507,10 +507,13 @@ typedef enum BUILTIN_SELECT, BUILTIN_SET_ROUNDING_MODE, BUILTIN_SPRINTF, + BUILTIN_STR_FIND, BUILTIN_STR_HASH, - BUILTIN_STR_UPPER, BUILTIN_STR_LOWER, - BUILTIN_STR_FIND, + BUILTIN_STR_PASCALCASE, + BUILTIN_STR_REPLACE, + BUILTIN_STR_UPPER, + BUILTIN_STR_SNAKECASE, BUILTIN_SWIZZLE, BUILTIN_SWIZZLE2, BUILTIN_SIN, diff --git a/src/compiler/llvm_codegen_builtins.c b/src/compiler/llvm_codegen_builtins.c index 4c8d2a92f..e49eb52e4 100644 --- a/src/compiler/llvm_codegen_builtins.c +++ b/src/compiler/llvm_codegen_builtins.c @@ -1101,6 +1101,9 @@ void llvm_emit_builtin_call(GenContext *c, BEValue *result_value, Expr *expr) case BUILTIN_STR_LOWER: case BUILTIN_STR_UPPER: case BUILTIN_STR_FIND: + case BUILTIN_STR_REPLACE: + case BUILTIN_STR_SNAKECASE: + case BUILTIN_STR_PASCALCASE: case BUILTIN_WIDESTRING_16: case BUILTIN_WIDESTRING_32: case BUILTIN_RND: diff --git a/src/compiler/sema_builtins.c b/src/compiler/sema_builtins.c index 6fa062c09..fd16c1ea3 100644 --- a/src/compiler/sema_builtins.c +++ b/src/compiler/sema_builtins.c @@ -2,8 +2,8 @@ // Use of this source code is governed by a LGPLv3.0 // a copy of which can be found in the LICENSE file. #include - #include "sema_internal.h" +#include typedef enum @@ -305,7 +305,60 @@ bool sema_expr_analyse_rnd(SemaContext *context UNUSED, Expr *expr) return true; } -bool sema_expr_analyse_str_hash(SemaContext *context, Expr *expr) +static bool sema_expr_analyse_str_replace(SemaContext *context, Expr *expr, Expr *arg, Expr *pattern, Expr *replace, Expr *limit) +{ + if (!sema_analyse_expr_rvalue(context, arg)) return false; + if (!sema_cast_const(arg) || !expr_is_const_string(arg)) + { + RETURN_SEMA_ERROR(arg, "Expected a constant string replace a pattern in."); + } + if (!sema_analyse_expr_rvalue(context, pattern)) return false; + if (!sema_cast_const(pattern) || !expr_is_const_string(pattern)) + { + RETURN_SEMA_ERROR(pattern, "Expected a constant pattern to replace."); + } + if (!sema_analyse_expr_rvalue(context, replace)) return false; + if (!sema_cast_const(replace) || !expr_is_const_string(replace)) + { + RETURN_SEMA_ERROR(replace, "Expected a constant replacement string."); + } + if (!sema_analyse_expr_rvalue(context, limit)) return false; + if (!sema_cast_const(limit) || !expr_is_const_int(limit)) + { + RETURN_SEMA_ERROR(limit, "Expected a constant limit."); + } + const char *inner_str = arg->const_expr.bytes.ptr; + ArraySize len = arg->const_expr.bytes.len; + const char *pattern_str = pattern->const_expr.bytes.ptr; + ArraySize pattern_len = pattern->const_expr.bytes.len; + const char *replace_str = replace->const_expr.bytes.ptr; + ArraySize limit_int = int_ucomp(limit->const_expr.ixx, MAX_ARRAY_SIZE, BINARYOP_GT) ? 0 : limit->const_expr.ixx.i.low; + scratch_buffer_clear(); + ArrayIndex index = 0; + if (limit_int == 0) limit_int = UINT64_MAX; + while (index < len) + { + const char *end = strstr(inner_str + index, pattern_str); + if (end == NULL) + { + scratch_buffer_append(inner_str + index); + break; + } + scratch_buffer_append_len(inner_str + index, end - inner_str - index); + scratch_buffer_append(replace_str); + index = end - inner_str + pattern_len; + limit_int--; + if (limit_int == 0) + { + scratch_buffer_append(inner_str + index); + break; + } + } + expr_rewrite_const_string(expr, scratch_buffer_copy()); + return true; +} + +static bool sema_expr_analyse_str_hash(SemaContext *context, Expr *expr) { Expr *inner = expr->call_expr.arguments[0]; if (!sema_analyse_expr_rvalue(context, inner)) return true; @@ -354,7 +407,20 @@ bool sema_expr_analyse_str_conv(SemaContext *context, Expr *expr, BuiltinFunctio expr_replace(expr, inner); return true; } - char *new_string = malloc_string(len + 1); + char *new_string; + if (func == BUILTIN_STR_SNAKECASE) + { + int uppers = 0; + for (ArrayIndex i = 0; i < len; i++) + { + if (isupper(string[i])) uppers++; + } + new_string = malloc_string(len + 1 + uppers); + } + else + { + new_string = malloc_string(len + 1); + } switch (func) { case BUILTIN_STR_LOWER: @@ -364,6 +430,49 @@ bool sema_expr_analyse_str_conv(SemaContext *context, Expr *expr, BuiltinFunctio new_string[i] = (char)(char_is_upper(c) ? (c | 0x20) : c); } break; + case BUILTIN_STR_SNAKECASE: + { + size_t index = 0; + for (ArraySize i = 0; i < len; i++) + { + char c = string[i]; + if (isupper(c)) + { + if (i > 0 && ((islower(string[i - 1]) || isdigit(string[i - 1])) || (i < len - 1 && islower(string[i + 1])))) + { + new_string[index++] = '_'; + } + new_string[index++] = tolower(c); + continue; + } + new_string[index++] = c; + } + len = index; + break; + } + case BUILTIN_STR_PASCALCASE: + { + bool capitalize = true; + size_t j = 0; + for (ArraySize i = 0; i < len; i++) + { + char c = string[i]; + if (!isalpha(c)) + { + capitalize = true; + continue; + } + if (capitalize) + { + new_string[j++] = toupper(c); + capitalize = false; + continue; + } + new_string[j++] = tolower(c); + } + len = j; + break; + } case BUILTIN_STR_UPPER: for (ArraySize i = 0; i < len; i++) { @@ -538,8 +647,12 @@ bool sema_expr_analyse_builtin_call(SemaContext *context, Expr *expr) return sema_expr_analyse_rnd(context, expr); case BUILTIN_STR_HASH: return sema_expr_analyse_str_hash(context, expr); + case BUILTIN_STR_REPLACE: + return sema_expr_analyse_str_replace(context, expr, args[0], args[1], args[2], args[3]); case BUILTIN_STR_UPPER: case BUILTIN_STR_LOWER: + case BUILTIN_STR_PASCALCASE: + case BUILTIN_STR_SNAKECASE: return sema_expr_analyse_str_conv(context, expr, func); case BUILTIN_STR_FIND: return sema_expr_analyse_str_find(context, expr); @@ -592,7 +705,10 @@ bool sema_expr_analyse_builtin_call(SemaContext *context, Expr *expr) case BUILTIN_STR_HASH: case BUILTIN_STR_UPPER: case BUILTIN_STR_LOWER: + case BUILTIN_STR_PASCALCASE: + case BUILTIN_STR_SNAKECASE: case BUILTIN_STR_FIND: + case BUILTIN_STR_REPLACE: case BUILTIN_WIDESTRING_16: case BUILTIN_WIDESTRING_32: case BUILTIN_SPRINTF: @@ -1274,6 +1390,8 @@ static inline int builtin_expected_args(BuiltinFunction func) case BUILTIN_STR_HASH: case BUILTIN_STR_UPPER: case BUILTIN_STR_LOWER: + case BUILTIN_STR_SNAKECASE: + case BUILTIN_STR_PASCALCASE: case BUILTIN_TRUNC: case BUILTIN_VOLATILE_LOAD: case BUILTIN_WASM_MEMORY_SIZE: @@ -1326,6 +1444,7 @@ static inline int builtin_expected_args(BuiltinFunction func) case BUILTIN_MASKED_LOAD: case BUILTIN_GATHER: case BUILTIN_SCATTER: + case BUILTIN_STR_REPLACE: return 4; case BUILTIN_ATOMIC_FETCH_EXCHANGE: case BUILTIN_ATOMIC_FETCH_ADD: diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index c9a92ac98..1009b22f6 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -608,6 +608,7 @@ bool sema_expr_analyse_sprintf(SemaContext *context, Expr *expr, Expr *format_st return true; } + static bool sema_binary_is_expr_lvalue(SemaContext *context, Expr *top_expr, Expr *expr, bool *failed_ref) { if (expr->expr_kind == EXPR_CT_SUBSCRIPT) return true; diff --git a/src/compiler/symtab.c b/src/compiler/symtab.c index aa13abca3..936a882fa 100644 --- a/src/compiler/symtab.c +++ b/src/compiler/symtab.c @@ -280,6 +280,9 @@ void symtab_init(uint32_t capacity) builtin_list[BUILTIN_STR_UPPER] = KW_DEF("str_upper"); builtin_list[BUILTIN_STR_LOWER] = KW_DEF("str_lower"); builtin_list[BUILTIN_STR_FIND] = KW_DEF("str_find"); + builtin_list[BUILTIN_STR_PASCALCASE] = KW_DEF("str_pascalcase"); + builtin_list[BUILTIN_STR_SNAKECASE] = KW_DEF("str_snakecase"); + builtin_list[BUILTIN_STR_REPLACE] = KW_DEF("str_replace"); builtin_list[BUILTIN_SWIZZLE] = KW_DEF("swizzle"); builtin_list[BUILTIN_SWIZZLE2] = KW_DEF("swizzle2"); builtin_list[BUILTIN_SPRINTF] = KW_DEF("sprintf"); diff --git a/test/test_suite/builtins/string_builtins.c3t b/test/test_suite/builtins/string_builtins.c3t new file mode 100644 index 000000000..4cdb1e19c --- /dev/null +++ b/test/test_suite/builtins/string_builtins.c3t @@ -0,0 +1,29 @@ +// #target: macos-x64 +module test; +import std; + +macro void @test($a) +{ + io::printn(@str_snakecase($a)); + io::printn(@str_pascalcase($a)); +} +fn void main() +{ + @test("abc deF"); + @test("abc_def"); + @test("AbcDef"); + @test("parseHTTPResponse"); + io::printn(@str_replace("hello dear! No fear, you are hear!", "ea", "ee", 1)); +} + +/* #expect: test.ll + +@.str = private unnamed_addr constant [9 x i8] c"abc de_f\00", align 1 +@.str.1 = private unnamed_addr constant [7 x i8] c"AbcDef\00", align 1 +@.str.2 = private unnamed_addr constant [8 x i8] c"abc_def\00", align 1 +@.str.3 = private unnamed_addr constant [7 x i8] c"AbcDef\00", align 1 +@.str.4 = private unnamed_addr constant [8 x i8] c"abc_def\00", align 1 +@.str.5 = private unnamed_addr constant [7 x i8] c"Abcdef\00", align 1 +@.str.6 = private unnamed_addr constant [20 x i8] c"parse_http_response\00", align 1 +@.str.7 = private unnamed_addr constant [18 x i8] c"Parsehttpresponse\00", align 1 +@.str.8 = private unnamed_addr constant [35 x i8] c"hello deer! No fear, you are hear!\00", align 1 From f3b7df2ab03ccafea3492136cc0ce66ddc1bbd4f Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Thu, 9 Oct 2025 23:41:58 +0200 Subject: [PATCH 13/49] `"build-dir"` option now available for `project.json`, added to project. #2323 --- releasenotes.md | 1 + src/build/project.c | 3 +++ src/build/project_creation.c | 2 ++ src/build/project_manipulation.c | 1 + 4 files changed, 7 insertions(+) diff --git a/releasenotes.md b/releasenotes.md index 1e2b6dd86..b16344429 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -6,6 +6,7 @@ - Error when using $vaarg/$vacount/$vasplat and similar in a macro without vaargs #2510. - Add splat defaults for designated initialization #2441. - Add new builtins `$$str_snakecase` `$$str_replace` and `$$str_pascalcase`. +- `"build-dir"` option now available for `project.json`, added to project. #2323 ### Fixes - Bug in `io::write_using_write_byte`. diff --git a/src/build/project.c b/src/build/project.c index c844b7ed1..0ac857e72 100644 --- a/src/build/project.c +++ b/src/build/project.c @@ -9,6 +9,7 @@ const char *project_default_keys[][2] = { {"authors", "Authors, optionally with email."}, {"benchfn", "Override the benchmark function."}, + {"build-dir", "Build location, where intermediate files are placed by default, relative to project file."}, {"c-include-dirs", "Set the include directories for C sources."}, {"c-sources", "Set the C sources to be compiled."}, {"cc", "Set C compiler (defaults to 'cc')."}, @@ -77,6 +78,7 @@ const int project_default_keys_count = ELEMENTLEN(project_default_keys); const char* project_deprecated_target_keys[] = { "xxxxxxxxxx" }; const char* project_target_keys[][2] = { {"benchfn", "Override the benchmark function."}, + {"build-dir", "Build location, where intermediate files are placed by default, relative to project file."}, {"c-include-dirs", "C sources include directories for the target."}, {"c-include-dirs-override", "Additional C sources include directories for the target, overriding global settings."}, {"c-sources", "Additional C sources to be compiled for the target."}, @@ -182,6 +184,7 @@ static void load_into_build_target(BuildParseContext context, JSONObject *json, target->run_dir = get_string(context, json, "run-dir", target->run_dir); // The output directory target->output_dir = get_string(context, json, "output", target->output_dir); + target->build_dir = get_string(context, json, "build-dir", target->build_dir); if (context.target) { diff --git a/src/build/project_creation.c b/src/build/project_creation.c index e4dc1a357..6eba5c6dd 100644 --- a/src/build/project_creation.c +++ b/src/build/project_creation.c @@ -27,6 +27,8 @@ const char* JSON_EXE = " // \"c-sources\": [ \"csource/**\" ],\n" " // Include directories for C sources relative to the project file.\n" " // \"c-include-dirs\": [ \"csource/include\" ],\n" + " // Build location, relative to project file.\n" + " \"build-dir\": \"build\",\n" " // Output location, relative to project file.\n" " \"output\": \"build\",\n" " // Architecture and OS target.\n" diff --git a/src/build/project_manipulation.c b/src/build/project_manipulation.c index a75ee4a61..59e042294 100644 --- a/src/build/project_manipulation.c +++ b/src/build/project_manipulation.c @@ -540,6 +540,7 @@ void view_project(BuildOptions *build_options) VIEW_STRING_ARRAY("Source paths", "sources", ", "); VIEW_STRING_ARRAY("C source paths", "c-sources", ", "); VIEW_STRING("Output location", "output"); + VIEW_STRING("Build location", "build-dir"); VIEW_STRING("Output extension", "extension"); VIEW_SETTING("Default optimization level", "opt", optimization_levels); From df67b7dddd08ce3bf9fe38cb75b9bdb1d5718512 Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Fri, 10 Oct 2025 00:34:30 +0200 Subject: [PATCH 14/49] Allow `..` ranges to use "a..a-1" in order to express zero length. --- releasenotes.md | 1 + src/compiler/llvm_codegen_expr.c | 9 +- src/compiler/sema_expr.c | 7 +- test/test_suite/arrays/slice_negative.c3t | 137 +++++++++++++++++++ test/test_suite/slices/slice_negative_len.c3 | 6 +- 5 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 test/test_suite/arrays/slice_negative.c3t diff --git a/releasenotes.md b/releasenotes.md index b16344429..d2b8bee28 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -7,6 +7,7 @@ - Add splat defaults for designated initialization #2441. - Add new builtins `$$str_snakecase` `$$str_replace` and `$$str_pascalcase`. - `"build-dir"` option now available for `project.json`, added to project. #2323 +- Allow `..` ranges to use "a..a-1" in order to express zero length. ### Fixes - Bug in `io::write_using_write_byte`. diff --git a/src/compiler/llvm_codegen_expr.c b/src/compiler/llvm_codegen_expr.c index 0743e292e..2ecc2abdf 100644 --- a/src/compiler/llvm_codegen_expr.c +++ b/src/compiler/llvm_codegen_expr.c @@ -2845,8 +2845,13 @@ static void llvm_emit_slice_values(GenContext *c, Expr *slice, BEValue *parent_r } else { - llvm_emit_int_comp(c, &excess, &start_index, &end_index, BINARYOP_GT); - llvm_emit_panic_if_true(c, &excess, "Negative size", slice->span, "Negative size (start %d is less than end %d)", &start_index, &end_index); + llvm_value_rvalue(c, &start_index); + llvm_value_rvalue(c, &end_index); + LLVMValueRef val = llvm_emit_add_int(c, end_index.type, end_index.value, llvm_const_int(c, end_index.type, 1), slice->span); + BEValue plus_one_end_index; + llvm_value_set(&plus_one_end_index, val, end_index.type); + llvm_emit_int_comp(c, &excess, &start_index, &plus_one_end_index, BINARYOP_GT); + llvm_emit_panic_if_true(c, &excess, "Negative size", slice->span, "Negative size (slice was: [%d..%d])", &start_index, &end_index); if (len.value) { diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index 1009b22f6..3dce12e03 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -4258,6 +4258,7 @@ INLINE bool sema_expr_analyse_range_internal(SemaContext *context, Range *range, if (!sema_analyse_expr_rvalue(context, start)) return false; if (end && !sema_analyse_expr_rvalue(context, end)) return false; + ArrayIndex lowest = range->is_len ? 0 : -1; if (!cast_to_index_len(context, start, false)) return false; if (end && !cast_to_index_len(context, end, false)) return false; Type *end_type = end ? type_no_optional(end->type) : NULL; @@ -4307,14 +4308,14 @@ INLINE bool sema_expr_analyse_range_internal(SemaContext *context, Range *range, // Something like 1 .. ^4 with an unknown length. if (len < 0) return true; // Otherwise we fold the "from end" - if (end_index > len) + if (end_index + lowest > len) { RETURN_SEMA_ERROR(end, "An index may only be negative for pointers (it was: %lld).", len - end_index); } end_index = len - end_index; range->end_from_end = false; } - if (end_index < 0 && env != RANGE_PTR) + if (end_index < lowest && env != RANGE_PTR) { RETURN_SEMA_ERROR(end, "An index may only be negative for pointers (it was: %lld).", end_index); } @@ -4363,7 +4364,7 @@ INLINE bool sema_expr_analyse_range_internal(SemaContext *context, Range *range, if (range->range_type == RANGE_CONST_END) { ArrayIndex end_index = range->const_end; - if (end_index < start_index) RETURN_SEMA_ERROR(start, "The start index (%lld) should not be greater than the end index (%lld).", + if (end_index - lowest < start_index) RETURN_SEMA_ERROR(start, "The start index (%lld) should not be greater than the end index (%lld).", start_index, end_index); range->const_end = end_index + 1 - start_index; range->range_type = RANGE_CONST_LEN; diff --git a/test/test_suite/arrays/slice_negative.c3t b/test/test_suite/arrays/slice_negative.c3t new file mode 100644 index 000000000..a4dcfed6e --- /dev/null +++ b/test/test_suite/arrays/slice_negative.c3t @@ -0,0 +1,137 @@ +// #target: macos-x64 +// #safe: yes +module test; + +fn void main() +{ + int[2] x = { 1, 2 }; + int[] z = x[0..-1]; + int y = 1; + int yy = -1; + int[] w = x[y..yy]; +} + +/* #expect: test.ll + + +define void @test.main() #0 { +entry: + %x = alloca [2 x i32], align 4 + %z = alloca %"int[]", align 8 + %y = alloca i32, align 4 + %yy = alloca i32, align 4 + %w = alloca %"int[]", align 8 + %taddr = alloca i64, align 8 + %taddr1 = alloca i64, align 8 + %varargslots = alloca [2 x %any], align 16 + %indirectarg = alloca %"any[]", align 8 + %taddr3 = alloca i64, align 8 + %varargslots4 = alloca [1 x %any], align 16 + %indirectarg6 = alloca %"any[]", align 8 + %taddr11 = alloca i64, align 8 + %taddr12 = alloca i64, align 8 + %varargslots13 = alloca [2 x %any], align 16 + %indirectarg16 = alloca %"any[]", align 8 + %taddr19 = alloca i64, align 8 + %taddr20 = alloca i64, align 8 + %varargslots21 = alloca [2 x %any], align 16 + %indirectarg24 = alloca %"any[]", align 8 + call void @llvm.memcpy.p0.p0.i32(ptr align 4 %x, ptr align 4 @.__const, i32 8, i1 false) + %0 = insertvalue %"int[]" undef, ptr %x, 0 + %1 = insertvalue %"int[]" %0, i64 0, 1 + store %"int[]" %1, ptr %z, align 8 + store i32 1, ptr %y, align 4 + store i32 -1, ptr %yy, align 4 + %2 = load i32, ptr %y, align 4 + %sext = sext i32 %2 to i64 + %gt = icmp sgt i64 %sext, 2 + %3 = call i1 @llvm.expect.i1(i1 %gt, i1 false) + br i1 %3, label %panic, label %checkok + +checkok: ; preds = %entry + %underflow = icmp slt i64 %sext, 0 + %4 = call i1 @llvm.expect.i1(i1 %underflow, i1 false) + br i1 %4, label %panic2, label %checkok7 + +checkok7: ; preds = %checkok + %5 = load i32, ptr %yy, align 4 + %sext8 = sext i32 %5 to i64 + %add = add i64 %sext8, 1 + %gt9 = icmp sgt i64 %sext, %add + %6 = call i1 @llvm.expect.i1(i1 %gt9, i1 false) + br i1 %6, label %panic10, label %checkok17 + +checkok17: ; preds = %checkok7 + %le = icmp sle i64 2, %sext8 + %7 = call i1 @llvm.expect.i1(i1 %le, i1 false) + br i1 %7, label %panic18, label %checkok25 + +checkok25: ; preds = %checkok17 + %8 = add i64 %sext8, 1 + %size = sub i64 %8, %sext + %ptroffset = getelementptr inbounds [4 x i8], ptr %x, i64 %sext + %9 = insertvalue %"int[]" undef, ptr %ptroffset, 0 + %10 = insertvalue %"int[]" %9, i64 %size, 1 + store %"int[]" %10, ptr %w, align 8 + ret void + +panic: ; preds = %entry + store i64 2, ptr %taddr, align 8 + %11 = insertvalue %any undef, ptr %taddr, 0 + %12 = insertvalue %any %11, i64 ptrtoint (ptr @"$ct.long" to i64), 1 + store i64 %sext, ptr %taddr1, align 8 + %13 = insertvalue %any undef, ptr %taddr1, 0 + %14 = insertvalue %any %13, i64 ptrtoint (ptr @"$ct.long" to i64), 1 + store %any %12, ptr %varargslots, align 16 + %ptradd = getelementptr inbounds i8, ptr %varargslots, i64 16 + store %any %14, ptr %ptradd, align 16 + %15 = insertvalue %"any[]" undef, ptr %varargslots, 0 + %"$$temp" = insertvalue %"any[]" %15, i64 2, 1 + store %"any[]" %"$$temp", ptr %indirectarg, align 8 + call void @std.core.builtin.panicf(ptr @.panic_msg, i64 61, ptr @.file, i64 17, ptr @.func, i64 4, i32 9, ptr byval(%"any[]") align 8 %indirectarg) #3 + unreachable + +panic2: ; preds = %checkok + store i64 %sext, ptr %taddr3, align 8 + %16 = insertvalue %any undef, ptr %taddr3, 0 + %17 = insertvalue %any %16, i64 ptrtoint (ptr @"$ct.long" to i64), 1 + store %any %17, ptr %varargslots4, align 16 + %18 = insertvalue %"any[]" undef, ptr %varargslots4, 0 + %"$$temp5" = insertvalue %"any[]" %18, i64 1, 1 + store %"any[]" %"$$temp5", ptr %indirectarg6, align 8 + call void @std.core.builtin.panicf(ptr @.panic_msg.1, i64 22, ptr @.file, i64 17, ptr @.func, i64 4, i32 9, ptr byval(%"any[]") align 8 %indirectarg6) #3 + unreachable + +panic10: ; preds = %checkok7 + store i64 %sext, ptr %taddr11, align 8 + %19 = insertvalue %any undef, ptr %taddr11, 0 + %20 = insertvalue %any %19, i64 ptrtoint (ptr @"$ct.long" to i64), 1 + store i64 %sext8, ptr %taddr12, align 8 + %21 = insertvalue %any undef, ptr %taddr12, 0 + %22 = insertvalue %any %21, i64 ptrtoint (ptr @"$ct.long" to i64), 1 + store %any %20, ptr %varargslots13, align 16 + %ptradd14 = getelementptr inbounds i8, ptr %varargslots13, i64 16 + store %any %22, ptr %ptradd14, align 16 + %23 = insertvalue %"any[]" undef, ptr %varargslots13, 0 + %"$$temp15" = insertvalue %"any[]" %23, i64 2, 1 + store %"any[]" %"$$temp15", ptr %indirectarg16, align 8 + call void @std.core.builtin.panicf(ptr @.panic_msg.2, i64 35, ptr @.file, i64 17, ptr @.func, i64 4, i32 9, ptr byval(%"any[]") align 8 %indirectarg16) #3 + unreachable + +panic18: ; preds = %checkok17 + store i64 %sext8, ptr %taddr19, align 8 + %24 = insertvalue %any undef, ptr %taddr19, 0 + %25 = insertvalue %any %24, i64 ptrtoint (ptr @"$ct.long" to i64), 1 + store i64 2, ptr %taddr20, align 8 + %26 = insertvalue %any undef, ptr %taddr20, 0 + %27 = insertvalue %any %26, i64 ptrtoint (ptr @"$ct.long" to i64), 1 + store %any %25, ptr %varargslots21, align 16 + %ptradd22 = getelementptr inbounds i8, ptr %varargslots21, i64 16 + store %any %27, ptr %ptradd22, align 16 + %28 = insertvalue %"any[]" undef, ptr %varargslots21, 0 + %"$$temp23" = insertvalue %"any[]" %28, i64 2, 1 + store %"any[]" %"$$temp23", ptr %indirectarg24, align 8 + call void @std.core.builtin.panicf(ptr @.panic_msg.3, i64 60, ptr @.file, i64 17, ptr @.func, i64 4, i32 9, ptr byval(%"any[]") align 8 %indirectarg24) #3 + unreachable +} + diff --git a/test/test_suite/slices/slice_negative_len.c3 b/test/test_suite/slices/slice_negative_len.c3 index 8e1baa8bf..cc15b9549 100644 --- a/test/test_suite/slices/slice_negative_len.c3 +++ b/test/test_suite/slices/slice_negative_len.c3 @@ -2,7 +2,7 @@ fn void test() { int[3] x = { 1, 2, 3}; int[] z = x[2..2]; - z = x[2..1]; // #error: greater than the end index + z = x[2..0]; // #error: greater than the end index } fn void test2() @@ -10,7 +10,7 @@ fn void test2() int[3] x = { 1, 2, 3}; int[] z = x[^2..^2]; z = x[^3..]; - z = x[^1..^2]; // #error: greater than the end index + z = x[^1..^3]; // #error: greater than the end index } fn void test3() @@ -29,7 +29,7 @@ fn void test4() fn void test5() { int[3] x = { 1, 2, 3 }; - int[] z = x[..^4]; // #error: An index may only be negative + int[] z = x[..^5]; // #error: An index may only be negative } fn void test6() From 6f11260a5c444cbb1a9dacbc95653bfb7fbaec02 Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Fri, 10 Oct 2025 14:04:19 +0200 Subject: [PATCH 15/49] Disallow aliasing of `@local` symbols with a higher visibility in the alias. --- releasenotes.md | 5 +++-- src/compiler/context.c | 1 + src/compiler/sema_decls.c | 5 +++++ src/compiler/sema_expr.c | 2 +- src/compiler/sema_name_resolution.c | 2 +- test/test_suite/define/alias_visibility.c3t | 20 +++++++++++++++---- .../define/alias_visibility_forbidden.c3 | 2 ++ 7 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 test/test_suite/define/alias_visibility_forbidden.c3 diff --git a/releasenotes.md b/releasenotes.md index d2b8bee28..cb2f4bbe1 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -8,7 +8,8 @@ - Add new builtins `$$str_snakecase` `$$str_replace` and `$$str_pascalcase`. - `"build-dir"` option now available for `project.json`, added to project. #2323 - Allow `..` ranges to use "a..a-1" in order to express zero length. - +- Disallow aliasing of `@local` symbols with a higher visibility in the alias. + ### Fixes - Bug in `io::write_using_write_byte`. - Bitstruct value cannot be used to index a const array in compile time. #2512 @@ -16,8 +17,8 @@ - Bitstruct truncated constant error escapes `$defined` #2515. - Compiler segfault when accessing member of number cast to bitstruct #2516. - Compiler assert when getting a member of a `bitstruct : char @bigendian` #2517. -- Incorrect visibility on local globals with public aliases. #2519 - Add ??? and +++= to list-precedence. +- Fix issues with linking when using symbol aliases. #2519 ### Stdlib changes - Sorting functions correctly took slices by value, but also other types by value. Now, only slices are accepted by value, other containers are always by ref. diff --git a/src/compiler/context.c b/src/compiler/context.c index 2c34c2228..48865baf2 100644 --- a/src/compiler/context.c +++ b/src/compiler/context.c @@ -142,6 +142,7 @@ bool context_is_macro(SemaContext *context) void unit_register_external_symbol(SemaContext *context, Decl *decl) { + decl = decl_flatten(decl); if (decl->is_external_visible) return; Module *active_module = context->current_macro ? context->original_module : context->compilation_unit->module; if (decl->unit->module == active_module) return; diff --git a/src/compiler/sema_decls.c b/src/compiler/sema_decls.c index 7875c0c95..38f89aebc 100755 --- a/src/compiler/sema_decls.c +++ b/src/compiler/sema_decls.c @@ -5302,6 +5302,11 @@ static inline bool sema_analyse_alias(SemaContext *context, Decl *decl, bool *er } decl->type = symbol->type; decl->define_decl.alias = symbol; + if (symbol->visibility == VISIBLE_LOCAL && decl->visibility < VISIBLE_LOCAL) + { + RETURN_SEMA_ERROR(decl, "A local symbol like '%s' may not be aliased to 'private' or 'public' visibility, please change it to at least be '@private'", symbol->name); + } + // If the symbol is more hidden than the alias, then we must increase the visibility. if (decl_is_externally_visible(decl) && !decl_is_externally_visible(symbol)) { symbol->is_external_visible = true; diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index 3dce12e03..8c97c9a5a 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -10614,7 +10614,7 @@ static inline bool sema_expr_analyse_lambda(SemaContext *context, Type *target_t { decl->var.is_read = true; } - decl->is_external_visible = true; + decl_flatten(decl)->is_external_visible = true; vec_add(unit->module->lambdas_to_evaluate, decl); } else diff --git a/src/compiler/sema_name_resolution.c b/src/compiler/sema_name_resolution.c index f3453d82c..1516b3c22 100644 --- a/src/compiler/sema_name_resolution.c +++ b/src/compiler/sema_name_resolution.c @@ -830,7 +830,7 @@ INLINE bool sema_resolve_symbol_common(SemaContext *context, NameResolve *name_r SEMA_NOTE(found, "'%s' is defined here.", found->name); return false; } - unit_register_external_symbol(context, found); + if (found->decl_kind != DECL_ALIAS) unit_register_external_symbol(context, found); if (found->unit->module->is_generic) { if (name_resolve->is_parameterized) return true; diff --git a/test/test_suite/define/alias_visibility.c3t b/test/test_suite/define/alias_visibility.c3t index 036aff445..ffaf7c53e 100644 --- a/test/test_suite/define/alias_visibility.c3t +++ b/test/test_suite/define/alias_visibility.c3t @@ -1,20 +1,32 @@ // #target: macos-x64 module test1; alias x = y; -int y @local = 1; +int y @private = 1; + +module test2; +int z @private = 1; module test; import test1; +import test2 @public; + +alias z = test2::z; fn int main(String[] args) { - return test1::x; + return test1::x + z; } + /* #expect: test1.ll -@test1.y.10 = local_unnamed_addr global i32 1, align 4 +@test1.y = local_unnamed_addr global i32 1, align 4 + +/* #expect: test2.ll + +@test2.z = local_unnamed_addr global i32 1, align 4 // #expect: test.ll -@test1.y.10 = external global i32, align 4 \ No newline at end of file +@test1.y = external global i32, align 4 +@test2.z = external global i32, align 4 diff --git a/test/test_suite/define/alias_visibility_forbidden.c3 b/test/test_suite/define/alias_visibility_forbidden.c3 new file mode 100644 index 000000000..eb4815bc9 --- /dev/null +++ b/test/test_suite/define/alias_visibility_forbidden.c3 @@ -0,0 +1,2 @@ +alias x @private = y; // #error: A local symbol like +int y @local = 1; From 3430240c2aa467de2df06057f0f95059bb06ebfb Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Sat, 11 Oct 2025 00:47:37 +0200 Subject: [PATCH 16/49] Update readme with OpenBSD --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c100c484..30255cd30 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ Follow the issues [here](https://github.com/c3lang/c3c/issues). If you have suggestions on how to improve the language, either [file an issue](https://github.com/c3lang/c3c/issues) or discuss C3 on its dedicated Discord: [https://discord.gg/qN76R87](https://discord.gg/qN76R87). -The compiler is currently verified to compile on Linux, Windows and MacOS. +The compiler is currently verified to compile on Linux, OpenBSD, Windows and MacOS. **Support matrix** From ae33d1a2060ee719709f7b059b45c12943df1e5f Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Sat, 11 Oct 2025 13:50:06 +0200 Subject: [PATCH 17/49] Fix issue testing if something is global. Remove ScopeId. Adding comments to code. --- src/compiler/ast.c | 49 +++++++++++++++---- src/compiler/compiler_internal.h | 6 +-- src/compiler/llvm_codegen.c | 2 +- src/compiler/llvm_codegen_function.c | 2 +- src/compiler/sema_internal.h | 2 +- src/compiler/sema_passes.c | 1 - src/compiler/sema_stmts.c | 22 ++++++--- src/compiler/semantic_analyser.c | 5 -- .../expressions/grabbing_function_alias.c3t | 7 +++ 9 files changed, 67 insertions(+), 29 deletions(-) create mode 100644 test/test_suite/expressions/grabbing_function_alias.c3t diff --git a/src/compiler/ast.c b/src/compiler/ast.c index 017c0d39e..8bbfde523 100644 --- a/src/compiler/ast.c +++ b/src/compiler/ast.c @@ -35,6 +35,10 @@ Decl *decl_new(DeclKind decl_kind, const char *name, SourceSpan span) return decl; } +bool decl_is_deprecated(Decl *decl) +{ + return decl->resolved_attributes && decl->attrs_resolved && decl->attrs_resolved->deprecated; +} // Check if local or parameter $foo/$Foo bool decl_is_ct_var(Decl *decl) @@ -43,12 +47,12 @@ bool decl_is_ct_var(Decl *decl) return decl_var_kind_is_ct(decl->var.kind); } -Decl *decl_new_with_type(const char *name, SourceSpan loc, DeclKind decl_type) +Decl *decl_new_with_type(const char *name, SourceSpan span, DeclKind decl_type) { Decl *decl = decl_calloc(); decl->decl_kind = decl_type; decl->name = name; - decl->span = loc; + decl->span = span; TypeKind kind = TYPE_POISONED; switch (decl_type) { @@ -95,6 +99,7 @@ const char *decl_safe_name(Decl *decl) if (decl->name) return decl->name; return decl_to_name(decl); } + const char *decl_to_name(Decl *decl) { const char *name = decl_to_a_name(decl); @@ -155,9 +160,9 @@ const char *decl_to_a_name(Decl *decl) } -Decl *decl_new_var(const char *name, SourceSpan loc, TypeInfo *type, VarDeclKind kind) +Decl *decl_new_var(const char *name, SourceSpan span, TypeInfo *type, VarDeclKind kind) { - Decl *decl = decl_new(DECL_VAR, name, loc); + Decl *decl = decl_new(DECL_VAR, name, span); decl->var.kind = kind; decl->var.type_info = type ? type_infoid(type) : 0; return decl; @@ -173,7 +178,7 @@ Decl *decl_new_generated_var(Type *type, VarDeclKind kind, SourceSpan span) decl->var.is_temp = true; decl->type = type; decl->alignment = type ? type_alloca_alignment(type) : 0; - ASSERT(!type || !type_is_user_defined(type) || type->decl->resolve_status == RESOLVE_DONE); + ASSERT_AT(span, !type || !type_is_user_defined(type) || type->decl->resolve_status == RESOLVE_DONE); decl->var.type_info = type_info_id_new_base(type, span); decl->resolve_status = RESOLVE_DONE; return decl; @@ -292,8 +297,8 @@ AttributeType attribute_by_name(const char *name) return ATTRIBUTE_NONE; } - -void decl_append_links_to_global(Decl *decl) +// Look for the @link directive, if a decl is codegen then add it to the linking process. +void decl_append_links_to_global_during_codegen(Decl *decl) { CompilationUnit *unit = decl->unit; if (unit && unit->links) @@ -310,16 +315,22 @@ void decl_append_links_to_global(Decl *decl) } } +/* + * Count the expected number of elements needed for an initializer + * by folding any anonymous structs and unions. + */ int decl_count_elements(Decl *structlike) { int elements = 0; Decl **members = structlike->strukt.members; unsigned member_size = vec_size(members); if (member_size == 0) return 0; + // In the case we have a union, we only count the first element. if (structlike->decl_kind == DECL_UNION) member_size = 1; for (unsigned i = 0; i < member_size; i++) { Decl *member = members[i]; + // Recursively count the anonymous struct/unions if (member->decl_kind != DECL_VAR && !member->name) { elements += decl_count_elements(member); @@ -330,6 +341,7 @@ int decl_count_elements(Decl *structlike) return elements; } +// This is used to check if a macro folds to a single compile time value. bool ast_is_compile_time(Ast *ast) { switch (ast->ast_kind) @@ -341,6 +353,7 @@ bool ast_is_compile_time(Ast *ast) if (!ast->return_stmt.expr) return true; return expr_is_runtime_const(ast->return_stmt.expr); case AST_EXPR_STMT: + // $a; is fine return expr_is_runtime_const(ast->expr_stmt); case AST_CT_COMPOUND_STMT: { @@ -365,14 +378,25 @@ bool ast_is_compile_time(Ast *ast) } } +// Should this declaration be linked externally bool decl_is_externally_visible(Decl *decl) { return decl->is_external_visible || decl->visibility == VISIBLE_PUBLIC || decl->is_export; } +/* + * Is this declartion a global of some sort? + * In other words a static, thread local, global constant or a global + */ bool decl_is_global(Decl *ident) { + switch (ident->decl_kind) + { + case DECL_FUNC: return true; + case DECL_VAR: break; + default: return false; + } switch (ident->var.kind) { case VARDECL_LOCAL: @@ -396,11 +420,13 @@ bool decl_is_global(Decl *ident) UNREACHABLE } +// Is is @local or is it @private but never imported in some other compilation unit (module) bool decl_is_local(Decl *decl) { return !decl->is_external_visible && decl->visibility != VISIBLE_PUBLIC && !decl->is_export; } +// Does the decl need to be prefixed, or is it for some reason like a builtin. bool decl_needs_prefix(Decl *decl) { switch (decl->decl_kind) @@ -418,6 +444,7 @@ bool decl_needs_prefix(Decl *decl) } } +// Find a particular enum by name. Decl *decl_find_enum_constant(Decl *decl, const char *name) { FOREACH(Decl *, enum_constant, decl->enums.values) @@ -427,6 +454,7 @@ Decl *decl_find_enum_constant(Decl *decl, const char *name) return NULL; } + AlignSize decl_find_member_offset(Decl *decl, Decl *member) { static const AlignSize NO_MATCH = ~(AlignSize)0; @@ -442,7 +470,7 @@ AlignSize decl_find_member_offset(Decl *decl, Decl *member) default: return NO_MATCH; } - ASSERT(members); + ASSERT_SPAN(decl, members); unsigned list = vec_size(members); for (unsigned i = 0; i < list; i++) { @@ -460,9 +488,12 @@ AlignSize decl_find_member_offset(Decl *decl, Decl *member) return NO_MATCH; } +// Is it a for statement, AST_FOREACH should not reach here. bool ast_supports_continue(Ast *stmt) { + ASSERT_SPAN(stmt, stmt->ast_kind != AST_FOREACH_STMT); if (stmt->ast_kind != AST_FOR_STMT) return false; + // We don't support `continue` in a `do { };` statement. return stmt->for_stmt.cond || !stmt->flow.skip_first; } @@ -511,7 +542,7 @@ void scratch_buffer_set_extern_decl_name(Decl *decl, bool clear) } if (decl->visibility == VISIBLE_LOCAL) { - assert(module); + ASSERT_SPAN(decl, module); scratch_buffer_printf(".%u", (unsigned)declid(decl)); } } diff --git a/src/compiler/compiler_internal.h b/src/compiler/compiler_internal.h index 84f81ab11..a540ad838 100644 --- a/src/compiler/compiler_internal.h +++ b/src/compiler/compiler_internal.h @@ -19,7 +19,6 @@ typedef int32_t IndexDiff; typedef int64_t ArrayIndex; typedef uint16_t StructIndex; typedef uint32_t AlignSize; -typedef int32_t ScopeId; typedef uint64_t ArraySize; typedef uint64_t BitSize; typedef uint16_t FileId; @@ -1639,7 +1638,6 @@ typedef struct EndJump_ typedef struct DynamicScope_ { - ScopeId scope_id; bool allow_dead_code : 1; bool is_dead : 1; bool is_poisoned : 1; @@ -1789,7 +1787,6 @@ struct SemaContext_ CallEnv call_env; Decl *current_macro; InliningSpan *inlined_at; - ScopeId scope_id; unsigned macro_call_depth; // Jump tracking JumpTarget break_jump; @@ -2308,7 +2305,7 @@ const char *decl_safe_name(Decl *decl); const char *decl_to_name(Decl *decl); const char *decl_to_a_name(Decl *decl); int decl_count_elements(Decl *structlike); -void decl_append_links_to_global(Decl *decl); +void decl_append_links_to_global_during_codegen(Decl *decl); INLINE bool decl_ok(Decl *decl); INLINE bool decl_poison(Decl *decl); @@ -2319,6 +2316,7 @@ static inline Decl *decl_raw(Decl *decl); static inline DeclKind decl_from_token(TokenType type); static inline bool decl_is_var_local(Decl *decl); bool decl_is_ct_var(Decl *decl); +bool decl_is_deprecated(Decl *decl); Decl *decl_find_enum_constant(Decl *decl, const char *name); bool decl_needs_prefix(Decl *decl); AlignSize decl_find_member_offset(Decl *decl, Decl *member); diff --git a/src/compiler/llvm_codegen.c b/src/compiler/llvm_codegen.c index 22faa98f3..bcf182b2d 100644 --- a/src/compiler/llvm_codegen.c +++ b/src/compiler/llvm_codegen.c @@ -541,7 +541,7 @@ void llvm_emit_global_variable_init(GenContext *c, Decl *decl) // Skip real constants. if (!decl->type) return; - decl_append_links_to_global(decl); + decl_append_links_to_global_during_codegen(decl); LLVMValueRef init_value; diff --git a/src/compiler/llvm_codegen_function.c b/src/compiler/llvm_codegen_function.c index 12aae8fde..25848765c 100644 --- a/src/compiler/llvm_codegen_function.c +++ b/src/compiler/llvm_codegen_function.c @@ -673,7 +673,7 @@ void llvm_emit_function_decl(GenContext *c, Decl *decl) { ASSERT_SPAN(decl, decl->decl_kind == DECL_FUNC); // Resolve function backend type for function. - decl_append_links_to_global(decl); + decl_append_links_to_global_during_codegen(decl); LLVMValueRef function = llvm_get_ref(c, decl); decl->backend_ref = function; if (decl->attrs_resolved && decl->attrs_resolved->section) diff --git a/src/compiler/sema_internal.h b/src/compiler/sema_internal.h index 740f3d249..5d0bde043 100644 --- a/src/compiler/sema_internal.h +++ b/src/compiler/sema_internal.h @@ -201,7 +201,7 @@ INLINE Attr* attr_find_kind(Attr **attrs, AttributeType attr_type) INLINE void sema_display_deprecated_warning_on_use(SemaContext *context, Decl *decl, SourceSpan span) { ASSERT(decl->resolve_status == RESOLVE_DONE); - if (!decl->resolved_attributes || !decl->attrs_resolved || !decl->attrs_resolved->deprecated) return; + if (!decl_is_deprecated(decl)) return; if (context->call_env.ignore_deprecation) return; const char *msg = decl->attrs_resolved->deprecated; diff --git a/src/compiler/sema_passes.c b/src/compiler/sema_passes.c index 929ac9fd3..232a2e3f9 100644 --- a/src/compiler/sema_passes.c +++ b/src/compiler/sema_passes.c @@ -659,7 +659,6 @@ void sema_analysis_pass_decls(Module *module) context.active_scope = (DynamicScope) { .depth = 0, - .scope_id = 0, .label_start = 0, .current_local = 0, }; diff --git a/src/compiler/sema_stmts.c b/src/compiler/sema_stmts.c index 373797f04..8c773d1c6 100644 --- a/src/compiler/sema_stmts.c +++ b/src/compiler/sema_stmts.c @@ -298,7 +298,7 @@ static inline bool sema_analyse_continue_stmt(SemaContext *context, Ast *stateme // Continue can only be used with "for" statements, skipping the "do { };" statement if (!ast_supports_continue(jump_target.target)) { - RETURN_SEMA_ERROR(statement, "'continue' may only be used with 'for', 'while' and 'do-while' statements."); + RETURN_SEMA_ERROR(statement, "'continue' may only be used with 'for', 'foreach', 'while' and 'do-while' statements."); } } else @@ -3338,8 +3338,10 @@ bool sema_analyse_contracts(SemaContext *context, AstId doc, AstId **asserts, So bool sema_analyse_function_body(SemaContext *context, Decl *func) { + // Stop if it's already poisoned. if (!decl_ok(func)) return false; + // Check the signature here we test for variadic raw, since we don't support it. Signature *signature = &func->func_decl.signature; if (signature->variadic == VARIADIC_RAW) { @@ -3347,8 +3349,12 @@ bool sema_analyse_function_body(SemaContext *context, Decl *func) " please use typed vaargs on the form 'int... args' or " "untyped vaargs on the form 'args...' instead."); } + + // Pull out the prototype FunctionPrototype *prototype = func->type->function.prototype; - ASSERT(prototype); + ASSERT_SPAN(func, prototype); + + // Set up the context for analysis context->original_inline_line = 0; context->original_module = NULL; context->call_env = (CallEnv) { @@ -3356,23 +3362,23 @@ bool sema_analyse_function_body(SemaContext *context, Decl *func) .is_naked_fn = func->func_decl.attr_naked, .kind = CALL_ENV_FUNCTION, .pure = func->func_decl.signature.attrs.is_pure, - .ignore_deprecation = func->allow_deprecated || (func->resolved_attributes && func->attrs_resolved && func->attrs_resolved->deprecated && func->resolved_attributes && func->attrs_resolved->deprecated) + .ignore_deprecation = func->allow_deprecated || decl_is_deprecated(func) }; + context->rtype = prototype->rtype; context->macro_call_depth = 0; context->active_scope = (DynamicScope) { - .scope_id = 0, .depth = 0, .label_start = 0, .current_local = 0 }; vec_resize(context->ct_locals, 0); - // Clear returns vec_resize(context->block_returns, 0); - context->scope_id = 0; + // Zero out any jumps context->break_jump = context->continue_jump = context->next_jump = (JumpTarget) { .target = NULL }; - ASSERT(func->func_decl.body); + ASSERT_SPAN(func, func->func_decl.body); + Ast *body = astptr(func->func_decl.body); Decl **lambda_params = NULL; SCOPE_START @@ -3383,6 +3389,8 @@ bool sema_analyse_function_body(SemaContext *context, Decl *func) } if (func->func_decl.is_lambda) { + // If we're a lambda we need to pass on the compile time values that will + // be baked into the function. lambda_params = copy_decl_list_single(func->func_decl.lambda_ct_parameters); FOREACH(Decl *, ct_param, lambda_params) { diff --git a/src/compiler/semantic_analyser.c b/src/compiler/semantic_analyser.c index 7b251d6ef..af50a7469 100644 --- a/src/compiler/semantic_analyser.c +++ b/src/compiler/semantic_analyser.c @@ -35,7 +35,6 @@ void context_change_scope_with_flags(SemaContext *context, ScopeFlags flags) unsigned label_start = new_label_scope ? last_local : context->active_scope.label_start; context->active_scope = (DynamicScope) { - .scope_id = ++context->scope_id, .allow_dead_code = false, .is_dead = scope_is_dead, .is_poisoned = scope_is_poisoned, @@ -47,10 +46,6 @@ void context_change_scope_with_flags(SemaContext *context, ScopeFlags flags) .defer_start = parent_defer, .flags = flags, }; - if (context->scope_id == 0) - { - FATAL_ERROR("Too many scopes."); - } } const char *context_filename(SemaContext *context) diff --git a/test/test_suite/expressions/grabbing_function_alias.c3t b/test/test_suite/expressions/grabbing_function_alias.c3t new file mode 100644 index 000000000..e0db11abc --- /dev/null +++ b/test/test_suite/expressions/grabbing_function_alias.c3t @@ -0,0 +1,7 @@ +alias c = main; +void* e = &c; + +fn int main() +{ + return 0; +} \ No newline at end of file From 6eee760239cbd813b5e80980df684a3580840c44 Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Sat, 11 Oct 2025 16:26:07 +0200 Subject: [PATCH 18/49] Add `--max-macro-iterations` to set macro iteration limit. --- releasenotes.md | 3 ++- src/build/build.h | 2 ++ src/build/build_options.c | 11 ++++++++++- src/build/builder.c | 3 ++- src/compiler/compiler_internal.h | 1 - src/compiler/sema_stmts.c | 7 ++++++- src/utils/common.h | 2 ++ test/test_suite/compile_time/ct_for_limit.c3 | 6 ++++++ 8 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 test/test_suite/compile_time/ct_for_limit.c3 diff --git a/releasenotes.md b/releasenotes.md index cb2f4bbe1..1847e46eb 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -9,7 +9,8 @@ - `"build-dir"` option now available for `project.json`, added to project. #2323 - Allow `..` ranges to use "a..a-1" in order to express zero length. - Disallow aliasing of `@local` symbols with a higher visibility in the alias. - +- Add `--max-macro-iterations` to set macro iteration limit. + ### Fixes - Bug in `io::write_using_write_byte`. - Bitstruct value cannot be used to index a const array in compile time. #2512 diff --git a/src/build/build.h b/src/build/build.h index 9db58b8d3..1fdd808f9 100644 --- a/src/build/build.h +++ b/src/build/build.h @@ -586,6 +586,7 @@ typedef struct BuildOptions_ SanitizeMode sanitize_mode; uint32_t max_vector_size; uint32_t max_stack_object_size; + uint32_t max_macro_iterations; bool print_keywords; bool print_attributes; bool print_builtins; @@ -728,6 +729,7 @@ typedef struct uint32_t symtab_size; uint32_t max_vector_size; uint32_t max_stack_object_size; + uint32_t max_macro_iterations; uint32_t switchrange_max_size; uint32_t switchjump_max_size; const char **args; diff --git a/src/build/build_options.c b/src/build/build_options.c index 362c9c05e..eee051b94 100644 --- a/src/build/build_options.c +++ b/src/build/build_options.c @@ -182,6 +182,7 @@ static void usage(bool full) print_opt("--win-debug=