From 8f5d5a0bb587d7aa751e2b1df8f8a74a60364b9c Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Thu, 23 Oct 2025 00:42:38 +0200 Subject: [PATCH] "Maybe-deref" subscripting `foo.[i] += 1` #2540. --- releasenotes.md | 1 + src/compiler/compiler_internal.h | 1 + src/compiler/copying.c | 1 + src/compiler/enums.h | 3 +- src/compiler/expr.c | 2 + src/compiler/parse_expr.c | 7 + src/compiler/sema_expr.c | 310 +++++++++++--------- src/compiler/sema_stmts.c | 1 + test/test_suite/expressions/maybe_deref.c3t | 86 ++++++ 9 files changed, 266 insertions(+), 146 deletions(-) create mode 100644 test/test_suite/expressions/maybe_deref.c3t diff --git a/releasenotes.md b/releasenotes.md index 6b1059d13..b9eb9a590 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -11,6 +11,7 @@ - Disallow aliasing of `@local` symbols with a higher visibility in the alias. - Add `--max-macro-iterations` to set macro iteration limit. - Improved generic inference in initializers #2541. +- "Maybe-deref" subscripting `foo.[i] += 1` #2540. ### Fixes - Bug in `io::write_using_write_byte`. diff --git a/src/compiler/compiler_internal.h b/src/compiler/compiler_internal.h index 5e5557ea5..5e5c5e023 100644 --- a/src/compiler/compiler_internal.h +++ b/src/compiler/compiler_internal.h @@ -3808,6 +3808,7 @@ static inline void expr_set_span(Expr *expr, SourceSpan loc) case EXPR_ADDR_CONVERSION: case EXPR_RECAST: case EXPR_LENGTHOF: + case EXPR_MAYBE_DEREF: expr_set_span(expr->inner_expr, loc); return; case EXPR_EXPRESSION_LIST: diff --git a/src/compiler/copying.c b/src/compiler/copying.c index 3db95a3ae..f890bb65d 100644 --- a/src/compiler/copying.c +++ b/src/compiler/copying.c @@ -493,6 +493,7 @@ Expr *copy_expr(CopyStruct *c, Expr *source_expr) case EXPR_RECAST: case EXPR_ADDR_CONVERSION: case EXPR_LENGTHOF: + case EXPR_MAYBE_DEREF: MACRO_COPY_EXPR(expr->inner_expr); return expr; case EXPR_MAKE_ANY: diff --git a/src/compiler/enums.h b/src/compiler/enums.h index 9a572b20a..b9b503957 100644 --- a/src/compiler/enums.h +++ b/src/compiler/enums.h @@ -790,6 +790,7 @@ typedef enum EXPR_MACRO_BODY_EXPANSION, EXPR_MAKE_ANY, EXPR_MAKE_SLICE, + EXPR_MAYBE_DEREF, EXPR_MEMBER_GET, EXPR_MEMBER_SET, EXPR_NAMED_ARGUMENT, @@ -1682,7 +1683,7 @@ typedef enum case EXPR_CT_ARG: case EXPR_TYPEINFO: case EXPR_CT_IDENT: case EXPR_HASH_IDENT: \ case EXPR_COMPILER_CONST: case EXPR_CT_CALL: \ case EXPR_SPLAT: case EXPR_STRINGIFY: case EXPR_TYPECALL: \ - case EXPR_CT_EVAL + case EXPR_CT_EVAL: case EXPR_MAYBE_DEREF static_assert(EXPR_LAST < 128, "Too many expression types"); diff --git a/src/compiler/expr.c b/src/compiler/expr.c index 9d208f971..86498cbb7 100644 --- a/src/compiler/expr.c +++ b/src/compiler/expr.c @@ -355,6 +355,7 @@ bool expr_is_runtime_const(Expr *expr) case EXPR_VECTOR_TO_ARRAY: case EXPR_SLICE_TO_VEC_ARRAY: case EXPR_SCALAR_TO_VECTOR: + case EXPR_MAYBE_DEREF: return expr_is_runtime_const(expr->inner_expr); case EXPR_MAKE_SLICE: expr = expr->make_slice_expr.ptr; @@ -819,6 +820,7 @@ bool expr_is_pure(Expr *expr) case EXPR_RVALUE: case EXPR_RECAST: case EXPR_ADDR_CONVERSION: + case EXPR_MAYBE_DEREF: return expr_is_pure(expr->inner_expr); case EXPR_INT_TO_BOOL: return expr_is_pure(expr->int_to_bool_expr.inner); diff --git a/src/compiler/parse_expr.c b/src/compiler/parse_expr.c index a4471ab3a..7557217b2 100644 --- a/src/compiler/parse_expr.c +++ b/src/compiler/parse_expr.c @@ -1108,11 +1108,18 @@ static Expr *parse_generic_expr(ParseContext *c, Expr *left, SourceSpan lhs_star /** * access_expr ::= '.' primary_expr + * deref_subscript ::= '.' '[' expr ']' */ static Expr *parse_access_expr(ParseContext *c, Expr *left, SourceSpan lhs_start) { ASSERT(left && expr_ok(left)); advance_and_verify(c, TOKEN_DOT); + if (tok_is(c, TOKEN_LBRACKET)) + { + Expr *deref = expr_new(EXPR_MAYBE_DEREF, left->span); + deref->inner_expr = left; + return parse_subscript_expr(c, deref, lhs_start); + } Expr *access_expr = expr_new(EXPR_ACCESS_UNRESOLVED, lhs_start); access_expr->access_unresolved_expr.parent = left; ASSIGN_EXPR_OR_RET(access_expr->access_unresolved_expr.child, parse_precedence(c, PREC_PRIMARY), poisoned_expr); diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index 868f950dc..e356b7e40 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -755,6 +755,7 @@ static bool sema_binary_is_expr_lvalue(SemaContext *context, Expr *top_expr, Exp case EXPR_SUBSCRIPT_ADDR: case EXPR_EXPRESSION_LIST: case EXPR_TWO: + case EXPR_MAYBE_DEREF: goto ERR; } UNREACHABLE @@ -896,6 +897,7 @@ static bool expr_may_ref(Expr *expr) case EXPR_TYPEID_INFO: case EXPR_TYPEINFO: case EXPR_MAKE_ANY: + case EXPR_MAYBE_DEREF: return false; } UNREACHABLE @@ -10751,7 +10753,7 @@ static inline bool sema_expr_analyse_ct_defined(SemaContext *context, Expr *expr active_context->call_env.in_no_eval = true; main_expr = main_expr->expr_other_context.inner; goto RETRY; - case EXPR_LENGTHOF: + case EXPR_LENGTHOF: if (!sema_expr_analyse_lenof(active_context, main_expr, &failed)) { if (!failed) goto FAIL; @@ -10765,41 +10767,41 @@ static inline bool sema_expr_analyse_ct_defined(SemaContext *context, Expr *expr success = false; } break; - case EXPR_UNRESOLVED_IDENTIFIER: - { - Decl *decl = sema_find_path_symbol(active_context, main_expr->unresolved_ident_expr.ident, main_expr->unresolved_ident_expr.path); - if (!decl_ok(decl)) goto FAIL; - success = decl != NULL && !decl_is_defaulted_var(decl); - break; - } - case EXPR_HASH_IDENT: - { - if (unroll_hash) + case EXPR_UNRESOLVED_IDENTIFIER: { - Decl *decl = sema_resolve_symbol(active_context, main_expr->hash_ident_expr.identifier, NULL, main_expr->span); - if (!decl) goto FAIL; - if (decl_is_defaulted_var(decl)) - { - success = false; - break; - } - bool is_ref = main_expr->hash_ident_expr.is_ref; - main_expr = copy_expr_single(decl->var.init_expr); - if (is_ref) expr_set_to_ref(main_expr); - goto RETRY; + Decl *decl = sema_find_path_symbol(active_context, main_expr->unresolved_ident_expr.ident, main_expr->unresolved_ident_expr.path); + if (!decl_ok(decl)) goto FAIL; + success = decl != NULL && !decl_is_defaulted_var(decl); + break; } - Decl *decl = sema_find_symbol(active_context, main_expr->hash_ident_expr.identifier); - if (!decl_ok(decl)) goto FAIL; - success = decl != NULL && !decl_is_defaulted_var(decl); - break; - } - case EXPR_COMPILER_CONST: + case EXPR_HASH_IDENT: + { + if (unroll_hash) + { + Decl *decl = sema_resolve_symbol(active_context, main_expr->hash_ident_expr.identifier, NULL, main_expr->span); + if (!decl) goto FAIL; + if (decl_is_defaulted_var(decl)) + { + success = false; + break; + } + bool is_ref = main_expr->hash_ident_expr.is_ref; + main_expr = copy_expr_single(decl->var.init_expr); + if (is_ref) expr_set_to_ref(main_expr); + goto RETRY; + } + Decl *decl = sema_find_symbol(active_context, main_expr->hash_ident_expr.identifier); + if (!decl_ok(decl)) goto FAIL; + success = decl != NULL && !decl_is_defaulted_var(decl); + break; + } + case EXPR_COMPILER_CONST: success = sema_expr_analyse_compiler_const(active_context, main_expr, false); break; - case EXPR_BUILTIN: + case EXPR_BUILTIN: success = sema_expr_analyse_builtin(active_context, main_expr, false); break; - case EXPR_UNARY: + case EXPR_UNARY: main_expr->resolve_status = RESOLVE_RUNNING; if (!sema_expr_analyse_unary(active_context, main_expr, &failed)) { @@ -10807,142 +10809,142 @@ static inline bool sema_expr_analyse_ct_defined(SemaContext *context, Expr *expr success = false; } break; - case EXPR_TYPEINFO: - { - Type *type = sema_expr_check_type_exists(active_context, main_expr->type_expr); - if (!type_ok(type)) goto FAIL; - success = type != NULL; - break; - } - case EXPR_CT_EVAL: - { - Expr *eval = sema_ct_eval_expr(active_context, "$eval", main_expr->inner_expr, false); - if (!expr_ok(eval)) return false; - if (eval) + case EXPR_TYPEINFO: { - main_expr = eval; - goto RETRY; - } - success = false; - break; - } - case EXPR_SUBSCRIPT: - { - if (!sema_expr_analyse_subscript(active_context, main_expr, &failed)) - { - if (!failed) goto FAIL; - success = false; - } - break; - } - case EXPR_CAST: - { - TypeInfo *typeinfo = type_infoptr(main_expr->cast_expr.type_info); - if (typeinfo->resolve_status == RESOLVE_DONE && typeinfo->type == type_void) - { - main_expr = exprptr(main_expr->cast_expr.expr); - unroll_hash = true; - goto RETRY; - } - if (!sema_expr_analyse_cast(active_context, main_expr, &failed)) - { - if (!failed) goto FAIL; - success = false; - } - break; - } - case EXPR_CT_IDENT: - { - Decl *decl = sema_find_symbol(active_context, main_expr->ct_ident_expr.identifier); - if (!decl_ok(decl)) goto FAIL; - success = decl != NULL && !decl_is_defaulted_var(decl); - break; - } - case EXPR_CALL: - { - bool no_match; - if (!sema_expr_analyse_call(active_context, main_expr, &no_match)) - { - if (!no_match) goto FAIL; - success = false; - } - break; - } - case EXPR_FORCE_UNWRAP: - if (!sema_analyse_expr_rvalue(active_context, main_expr->inner_expr)) goto FAIL; - success = IS_OPTIONAL(main_expr->inner_expr); + Type *type = sema_expr_check_type_exists(active_context, main_expr->type_expr); + if (!type_ok(type)) goto FAIL; + success = type != NULL; break; - case EXPR_RETHROW: - if (!sema_analyse_expr_rvalue(active_context, main_expr->rethrow_expr.inner)) goto FAIL; - success = IS_OPTIONAL(main_expr->rethrow_expr.inner); - break; - case EXPR_OPTIONAL: - if (!sema_expr_analyse_optional(active_context, main_expr, &failed)) + } + case EXPR_CT_EVAL: + { + Expr *eval = sema_ct_eval_expr(active_context, "$eval", main_expr->inner_expr, false); + if (!expr_ok(eval)) return false; + if (eval) { - if (!failed) goto FAIL; - success = false; + main_expr = eval; + goto RETRY; } - break; - case EXPR_POISONED: success = false; break; - case EXPR_CATCH_UNRESOLVED: - case EXPR_CATCH: - case EXPR_COND: - case EXPR_TEST_HOOK: - case EXPR_DESIGNATOR: - case EXPR_BENCHMARK_HOOK: - case EXPR_TRY_UNRESOLVED: - case EXPR_TRY: - case EXPR_TRY_UNWRAP_CHAIN: - case EXPR_OPERATOR_CHARS: - case EXPR_MACRO_BODY_EXPANSION: - case EXPR_BUILTIN_ACCESS: - case EXPR_LAST_FAULT: - case EXPR_DEFAULT_ARG: - case EXPR_IDENTIFIER: - case EXPR_NAMED_ARGUMENT: - case EXPR_ACCESS_RESOLVED: - case EXPR_CT_SUBSCRIPT: - case EXPR_IOTA_DECL: - UNREACHABLE - case EXPR_DECL: - if (!sema_analyse_var_decl(context, main_expr->decl_expr, true, &failed)) + } + case EXPR_SUBSCRIPT: + { + if (!sema_expr_analyse_subscript(active_context, main_expr, &failed)) { if (!failed) goto FAIL; success = false; } break; - case EXPR_BINARY: - main_expr->resolve_status = RESOLVE_RUNNING; - if (!sema_expr_analyse_binary(active_context, NULL, main_expr, &failed)) + } + case EXPR_CAST: + { + TypeInfo *typeinfo = type_infoptr(main_expr->cast_expr.type_info); + if (typeinfo->resolve_status == RESOLVE_DONE && typeinfo->type == type_void) + { + main_expr = exprptr(main_expr->cast_expr.expr); + unroll_hash = true; + goto RETRY; + } + if (!sema_expr_analyse_cast(active_context, main_expr, &failed)) { if (!failed) goto FAIL; success = false; } break; - case EXPR_CT_CALL: - main_expr->resolve_status = RESOLVE_RUNNING; - if (!sema_expr_analyse_ct_call(active_context, main_expr, &failed)) + } + case EXPR_CT_IDENT: + { + Decl *decl = sema_find_symbol(active_context, main_expr->ct_ident_expr.identifier); + if (!decl_ok(decl)) goto FAIL; + success = decl != NULL && !decl_is_defaulted_var(decl); + break; + } + case EXPR_CALL: + { + bool no_match; + if (!sema_expr_analyse_call(active_context, main_expr, &no_match)) { - if (!failed) goto FAIL; + if (!no_match) goto FAIL; success = false; } break; - case EXPR_COMPOUND_LITERAL: - if (!sema_expr_analyse_compound_literal(context, main_expr, &failed)) - { - if (!failed) goto FAIL; + } + case EXPR_FORCE_UNWRAP: + if (!sema_analyse_expr_rvalue(active_context, main_expr->inner_expr)) goto FAIL; + success = IS_OPTIONAL(main_expr->inner_expr); + break; + case EXPR_RETHROW: + if (!sema_analyse_expr_rvalue(active_context, main_expr->rethrow_expr.inner)) goto FAIL; + success = IS_OPTIONAL(main_expr->rethrow_expr.inner); + break; + case EXPR_OPTIONAL: + if (!sema_expr_analyse_optional(active_context, main_expr, &failed)) + { + if (!failed) goto FAIL; + success = false; + } + break; + case EXPR_POISONED: success = false; - } - break; - case EXPR_CT_ARG: - if (!sema_expr_analyse_ct_arg(context, NULL, main_expr, &failed)) - { - if (!failed) goto FAIL; - success = false; - } - break; + break; + case EXPR_CATCH_UNRESOLVED: + case EXPR_CATCH: + case EXPR_COND: + case EXPR_TEST_HOOK: + case EXPR_DESIGNATOR: + case EXPR_BENCHMARK_HOOK: + case EXPR_TRY_UNRESOLVED: + case EXPR_TRY: + case EXPR_TRY_UNWRAP_CHAIN: + case EXPR_OPERATOR_CHARS: + case EXPR_MACRO_BODY_EXPANSION: + case EXPR_BUILTIN_ACCESS: + case EXPR_LAST_FAULT: + case EXPR_DEFAULT_ARG: + case EXPR_IDENTIFIER: + case EXPR_NAMED_ARGUMENT: + case EXPR_ACCESS_RESOLVED: + case EXPR_CT_SUBSCRIPT: + case EXPR_IOTA_DECL: + UNREACHABLE + case EXPR_DECL: + if (!sema_analyse_var_decl(context, main_expr->decl_expr, true, &failed)) + { + if (!failed) goto FAIL; + success = false; + } + break; + case EXPR_BINARY: + main_expr->resolve_status = RESOLVE_RUNNING; + if (!sema_expr_analyse_binary(active_context, NULL, main_expr, &failed)) + { + if (!failed) goto FAIL; + success = false; + } + break; + case EXPR_CT_CALL: + main_expr->resolve_status = RESOLVE_RUNNING; + if (!sema_expr_analyse_ct_call(active_context, main_expr, &failed)) + { + if (!failed) goto FAIL; + success = false; + } + break; + case EXPR_COMPOUND_LITERAL: + if (!sema_expr_analyse_compound_literal(context, main_expr, &failed)) + { + if (!failed) goto FAIL; + success = false; + } + break; + case EXPR_CT_ARG: + if (!sema_expr_analyse_ct_arg(context, NULL, main_expr, &failed)) + { + if (!failed) goto FAIL; + success = false; + } + break; case EXPR_BITACCESS: case EXPR_BITASSIGN: case EXPR_EMBED: @@ -11000,6 +11002,7 @@ static inline bool sema_expr_analyse_ct_defined(SemaContext *context, Expr *expr case EXPR_PTR_TO_INT: case EXPR_MAKE_SLICE: case EXPR_TWO: + case EXPR_MAYBE_DEREF: if (!sema_analyse_expr_rvalue(active_context, main_expr)) goto FAIL; break; @@ -11101,6 +11104,20 @@ NO_MATCH: return false; } +static inline bool sema_expr_analyse_maybe_deref(SemaContext *context, Expr *expr) +{ + Expr *inner = expr->inner_expr; + if (!sema_analyse_expr_rvalue(context, inner)) return false; + if (type_is_pointer(inner->type)) + { + expr->expr_kind = EXPR_UNARY; + expr->unary_expr = (ExprUnary) { .expr = inner, .operator = UNARYOP_DEREF }; + return sema_expr_analyse_unary(context, expr, NULL); + } + expr_replace(expr, inner); + return true; +} + static inline bool sema_expr_analyse_iota_decl(SemaContext *context, Expr *expr) { Decl *decl = expr->iota_decl_expr; @@ -11402,6 +11419,8 @@ static inline bool sema_analyse_expr_dispatch(SemaContext *context, Expr *expr) case EXPR_MAKE_ANY: if (!sema_analyse_expr_rvalue(context, expr->make_any_expr.typeid)) return false; return sema_analyse_expr_rvalue(context, expr->make_any_expr.inner); + case EXPR_MAYBE_DEREF: + return sema_expr_analyse_maybe_deref(context, expr); case EXPR_RVALUE: case EXPR_RECAST: case EXPR_ADDR_CONVERSION: @@ -11974,6 +11993,7 @@ IDENT_CHECK:; case EXPR_VASPLAT: case EXPR_TRY_UNRESOLVED: case EXPR_TWO: + case EXPR_MAYBE_DEREF: break; case EXPR_BITACCESS: case EXPR_SUBSCRIPT_ASSIGN: diff --git a/src/compiler/sema_stmts.c b/src/compiler/sema_stmts.c index 1cce6af59..1d8c6e124 100644 --- a/src/compiler/sema_stmts.c +++ b/src/compiler/sema_stmts.c @@ -852,6 +852,7 @@ static inline bool sema_expr_valid_try_expression(Expr *expr) case EXPR_RECAST: case EXPR_ADDR_CONVERSION: case EXPR_ENUM_FROM_ORD: + case EXPR_MAYBE_DEREF: return true; case EXPR_TWO: return sema_expr_valid_try_expression(expr->two_expr.last); diff --git a/test/test_suite/expressions/maybe_deref.c3t b/test/test_suite/expressions/maybe_deref.c3t new file mode 100644 index 000000000..df31d4c24 --- /dev/null +++ b/test/test_suite/expressions/maybe_deref.c3t @@ -0,0 +1,86 @@ +// #target: macos-x64 +module test; +import std; +alias MyChar = char; + +fn void main() +{ + MyChar[2] my = { '1', '2' }; + MyChar[2]* ptr = &my; + var x1 @safeinfer = ptr.[0]; + ptr.[0] = 2; + var x2 @safeinfer = ptr.[0]; + MyChar* x = &ptr.[1]; + var x3 @safeinfer = *x; + List{int} l; + l.push(42); + List{int}* lptr = &l; + var x4 @safeinfer = lptr.[0]; + var x5 @safeinfer = l.[0]; + assert($defined(lptr.[0])); + assert($defined(l.[0])); + int i = 0; + assert(!$defined(i.[0])); + assert(!$defined((&i).[0])); +} + +/* #expect: test.ll + +entry: + %my = alloca [2 x i8], align 1 + %ptr = alloca ptr, align 8 + %x1 = alloca i8, align 1 + %x2 = alloca i8, align 1 + %x = alloca ptr, align 8 + %x3 = alloca i8, align 1 + %l = alloca %List, align 8 + %lptr = alloca ptr, align 8 + %x4 = alloca i32, align 4 + %self = alloca ptr, align 8 + %x5 = alloca i32, align 4 + %i = alloca i32, align 4 + call void @llvm.memcpy.p0.p0.i32(ptr align 1 %my, ptr align 1 @.__const, i32 2, i1 false) + store ptr %my, ptr %ptr, align 8 + %0 = load ptr, ptr %ptr, align 8 + %1 = load i8, ptr %0, align 1 + store i8 %1, ptr %x1, align 1 + %2 = load ptr, ptr %ptr, align 8 + store i8 2, ptr %2, align 1 + %3 = load ptr, ptr %ptr, align 8 + %4 = load i8, ptr %3, align 1 + store i8 %4, ptr %x2, align 1 + %5 = load ptr, ptr %ptr, align 8 + %ptradd = getelementptr inbounds i8, ptr %5, i64 1 + store ptr %ptradd, ptr %x, align 8 + %6 = load ptr, ptr %x, align 8 + %7 = load i8, ptr %6, align 1 + store i8 %7, ptr %x3, align 1 + call void @llvm.memset.p0.i64(ptr align 8 %l, i8 0, i64 40, i1 false) + call void @"std_collections_list$int$.List.push"(ptr %l, i32 42) #4 + store ptr %l, ptr %lptr, align 8 + %8 = load ptr, ptr %lptr, align 8 + store ptr %8, ptr %self, align 8 + %9 = load ptr, ptr %self, align 8 + %neq = icmp ne ptr %9, null + call void @llvm.assume(i1 %neq) + %10 = load ptr, ptr %self, align 8 + %11 = load i64, ptr %10, align 8 + %lt = icmp ult i64 0, %11 + call void @llvm.assume(i1 %lt) + %12 = load ptr, ptr %self, align 8 + %ptradd1 = getelementptr inbounds i8, ptr %12, i64 32 + %13 = load ptr, ptr %ptradd1, align 8 + %14 = load i32, ptr %13, align 4 + store i32 %14, ptr %x4, align 4 + %neq2 = icmp ne ptr %l, null + call void @llvm.assume(i1 %neq2) + %15 = load i64, ptr %l, align 8 + %lt3 = icmp ult i64 0, %15 + call void @llvm.assume(i1 %lt3) + %ptradd4 = getelementptr inbounds i8, ptr %l, i64 32 + %16 = load ptr, ptr %ptradd4, align 8 + %17 = load i32, ptr %16, align 4 + store i32 %17, ptr %x5, align 4 + store i32 0, ptr %i, align 4 + ret void +} \ No newline at end of file