From 94b8330ac5a59b4ee55208d52c765bf3e5cbcddb Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Sun, 6 Apr 2025 15:28:10 +0200 Subject: [PATCH] Function `@require` checks are added to the caller in safe mode. #186 --- releasenotes.md | 1 + src/compiler/compiler_internal.h | 8 +- src/compiler/copying.c | 12 +- src/compiler/expr.c | 10 ++ src/compiler/llvm_codegen_expr.c | 5 + src/compiler/llvm_codegen_stmt.c | 2 +- src/compiler/sema_decls.c | 2 +- src/compiler/sema_expr.c | 212 +++++++++++++++++++++++++------ src/compiler/sema_liveness.c | 1 + src/compiler/sema_stmts.c | 9 +- 10 files changed, 215 insertions(+), 47 deletions(-) diff --git a/releasenotes.md b/releasenotes.md index a672ab458..f455e7e9c 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -5,6 +5,7 @@ ### Changes / improvements - Better errors on some common casting mistakes (pointer->slice, String->ZString, deref pointer->array) #2064. - Better errors trying to convert an enum to an int and vice versa. +- Function `@require` checks are added to the caller in safe mode. #186 ### Fixes - Trying to cast an enum to int and back caused the compiler to crash. diff --git a/src/compiler/compiler_internal.h b/src/compiler/compiler_internal.h index d3469013a..e05a842a4 100644 --- a/src/compiler/compiler_internal.h +++ b/src/compiler/compiler_internal.h @@ -424,6 +424,7 @@ typedef struct VarDecl_ bool bit_is_expr : 1; bool is_self : 1; bool is_temp : 1; + bool copy_const : 1; union { Expr *init_expr; @@ -739,7 +740,11 @@ typedef struct ExprId function; DeclId func_ref; }; - ExprId macro_body; + union + { + ExprId macro_body; + AstId function_contracts; + }; bool is_type_method : 1; bool is_pointer_call : 1; bool attr_force_inline : 1; @@ -2198,6 +2203,7 @@ Expr *expr_new_const_typeid(SourceSpan span, Type *type); Expr *expr_new_const_string(SourceSpan span, const char *string); Expr *expr_new_const_null(SourceSpan span, Type *type); Expr *expr_new_const_initializer(SourceSpan span, Type *type, ConstInitializer *initializer); +Expr *expr_new_expr_list_resolved(SourceSpan span, Type *type, Expr **expressions); const char *expr_kind_to_string(ExprKind kind); bool expr_is_simple(Expr *expr, bool to_float); bool expr_is_pure(Expr *expr); diff --git a/src/compiler/copying.c b/src/compiler/copying.c index 52a831459..2dbfd7ed4 100644 --- a/src/compiler/copying.c +++ b/src/compiler/copying.c @@ -551,11 +551,19 @@ Expr *copy_expr(CopyStruct *c, Expr *source_expr) } else { - MACRO_COPY_EXPRID(expr->call_expr.function); } - MACRO_COPY_EXPRID(expr->call_expr.macro_body); + // Arguments must be copied before contracts are copied + // or things will break. MACRO_COPY_EXPR_LIST(expr->call_expr.arguments); + if (expr->resolve_status == RESOLVE_DONE) + { + MACRO_COPY_ASTID(expr->call_expr.function_contracts); + } + else + { + MACRO_COPY_EXPRID(expr->call_expr.macro_body); + } if (expr->call_expr.varargs) { if (expr->call_expr.va_is_splat) diff --git a/src/compiler/expr.c b/src/compiler/expr.c index 8ee3ea16d..2ce062511 100644 --- a/src/compiler/expr.c +++ b/src/compiler/expr.c @@ -1010,6 +1010,16 @@ Expr *expr_new_const_typeid(SourceSpan span, Type *type) return expr; } +Expr *expr_new_expr_list_resolved(SourceSpan span, Type *type, Expr **expressions) +{ + Expr *expr = expr_calloc(); + expr->expr_kind = EXPR_EXPRESSION_LIST; + expr->span = span; + expr->type = type; + expr->resolve_status = RESOLVE_DONE; + expr->expression_list = expressions; + return expr; +} Expr *expr_new_const_initializer(SourceSpan span, Type *type, ConstInitializer *initializer) { Expr *expr = expr_calloc(); diff --git a/src/compiler/llvm_codegen_expr.c b/src/compiler/llvm_codegen_expr.c index f2687c923..db5c74980 100644 --- a/src/compiler/llvm_codegen_expr.c +++ b/src/compiler/llvm_codegen_expr.c @@ -5839,6 +5839,11 @@ static void llvm_emit_call_expr(GenContext *c, BEValue *result_value, Expr *expr } } + if (safe_mode_enabled()) + { + llvm_emit_statement_chain(c, expr->call_expr.function_contracts); + } + // 1. Dynamic dispatch. if (expr->call_expr.is_dynamic_dispatch) { diff --git a/src/compiler/llvm_codegen_stmt.c b/src/compiler/llvm_codegen_stmt.c index 4089e2d3f..1ba70526d 100644 --- a/src/compiler/llvm_codegen_stmt.c +++ b/src/compiler/llvm_codegen_stmt.c @@ -144,7 +144,7 @@ void llvm_emit_local_decl(GenContext *c, Decl *decl, BEValue *value) llvm_value_set_decl_address(c, value, decl); value->kind = BE_ADDRESS; BEValue res = llvm_emit_assign_expr(c, value, decl->var.init_expr, decl->var.optional_ref, true); - if (!is_optional) *value = res; + if (!is_optional && res.value) *value = res; return; } diff --git a/src/compiler/sema_decls.c b/src/compiler/sema_decls.c index 432bb6090..63092cad0 100755 --- a/src/compiler/sema_decls.c +++ b/src/compiler/sema_decls.c @@ -951,7 +951,7 @@ static bool sema_analyse_interface(SemaContext *context, Decl *decl, bool *erase bool erase = false; // Insert the first parameter, which is the implicit `void*` - Decl *first = decl_new_var(kw_self, decl->span, NULL, VARDECL_PARAM); + Decl *first = decl_new_var(NULL, decl->span, NULL, VARDECL_PARAM); first->type = type_voidptr; first->var.kind = VARDECL_PARAM; first->unit = context->unit; diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index eeba597b6..245ef0ba5 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -150,6 +150,11 @@ static inline bool sema_expr_analyse_var_call(SemaContext *context, Expr *expr, static inline bool sema_expr_analyse_func_call(SemaContext *context, Expr *expr, Decl *decl, Expr *struct_var, bool optional, bool *no_match_ref); +static inline bool sema_expr_setup_call_analysis(SemaContext *context, CalledDecl * + callee, + SemaContext *macro_context, Expr *call_expr, Type *rtype, + Ast *yield_body, + Decl **yield_params, Decl **params, BlockExit **block_exit_ref, InliningSpan *span_ref); static inline bool sema_call_analyse_func_invocation(SemaContext *context, Decl *decl, Type *type, Expr *expr, Expr *struct_var, bool optional, const char *name, bool *no_match_ref); @@ -877,9 +882,16 @@ static inline bool sema_cast_ident_rvalue(SemaContext *context, Expr *expr) case VARDECL_REWRAPPED: // Impossible to reach this, they are already unfolded UNREACHABLE + case VARDECL_LOCAL: + if (decl->var.copy_const && decl->var.init_expr && expr_is_const(decl->var.init_expr)) + { + assert(decl->var.init_expr->resolve_status == RESOLVE_DONE); + expr_replace(expr, copy_expr_single(decl->var.init_expr)); + return true; + } + FALLTHROUGH; case VARDECL_PARAM: case VARDECL_GLOBAL: - case VARDECL_LOCAL: case VARDECL_UNWRAPPED: return true; case VARDECL_BITMEMBER: @@ -1942,6 +1954,30 @@ static inline bool sema_call_check_contract_param_match(SemaContext *context, De return true; } +static inline bool sema_has_require(AstId doc_id) +{ + if (!doc_id) return false; + Ast *docs = astptr(doc_id); + while (docs) + { + switch (docs->contract_stmt.kind) + { + case CONTRACT_UNKNOWN: + case CONTRACT_COMMENT: + case CONTRACT_PURE: + case CONTRACT_PARAM: + case CONTRACT_OPTIONALS: + case CONTRACT_ENSURE: + docs = astptrzero(docs->next); + continue; + case CONTRACT_REQUIRE: + return true; + } + UNREACHABLE + } + return false; +} + static inline bool sema_call_analyse_func_invocation(SemaContext *context, Decl *decl, Type *type, Expr *expr, Expr *struct_var, bool optional, const char *name, bool *no_match_ref) @@ -1968,7 +2004,81 @@ static inline bool sema_call_analyse_func_invocation(SemaContext *context, Decl if (!sema_call_evaluate_arguments(context, &callee, expr, &optional, no_match_ref)) return false; Type *rtype = type->function.prototype->rtype; + if (expr->call_expr.is_dynamic_dispatch) + { + Expr *any_val = expr->call_expr.arguments[0]; + ASSERT(any_val->expr_kind == EXPR_PTR_ACCESS); + *any_val = *(any_val->inner_expr); + } + expr->call_expr.function_contracts = 0; + AstId docs = decl->func_decl.docs; + if (!safe_mode_enabled() || !sema_has_require(docs)) goto SKIP_CONTRACTS; + SemaContext temp_context; + bool success = false; + if (!sema_expr_setup_call_analysis(context, &callee, &temp_context, + expr, NULL, NULL, NULL, NULL, + NULL, NULL)) + { + goto END_CONTRACT; + } + FOREACH_IDX(i, Decl *, param, sig->params) + { + if (!param || !param->name) continue; + Expr *arg = expr->call_expr.arguments[i]; + if (!arg) + { + assert(i == sig->vararg_index); + if (expr->call_expr.va_is_splat) + { + arg = expr->call_expr.vasplat; + } + else + { + Expr **exprs = expr->call_expr.varargs; + Expr *init_list = expr_new_expr(EXPR_INITIALIZER_LIST, expr); + init_list->initializer_list = exprs; + init_list->type = param->type; + Expr *compound_init = expr_new_expr(EXPR_COMPOUND_LITERAL, expr); + compound_init->expr_compound_literal.initializer = init_list; + compound_init->expr_compound_literal.type_info = type_info_new_base(param->type, param->span); + if (!sema_analyse_expr(context, compound_init)) goto END_CONTRACT; + arg = compound_init; + expr->call_expr.va_is_splat = true; + } + } + Decl *new_param = decl_new_generated_var(arg->type, VARDECL_LOCAL, expr->span); + new_param->name = param->name; + new_param->unit = context->unit; + new_param->var.copy_const = true; + Expr *new_arg = expr_generate_decl(new_param, arg); + new_arg->resolve_status = RESOLVE_DONE; + new_arg->type = arg->type; + if (!sema_add_local(&temp_context, new_param)) goto END_CONTRACT; + if (IS_OPTIONAL(new_param)) + { + sema_unwrap_var(&temp_context, new_param); + } + if (i == sig->vararg_index) + { + expr->call_expr.vasplat = new_arg; + continue; + } + expr->call_expr.arguments[i] = new_arg; + } + AstId assert_first = 0; + AstId* next = &assert_first; + + if (!sema_analyse_contracts(&temp_context, docs, &next, expr->span, NULL)) return false; + + expr->call_expr.function_contracts = assert_first; + + success = true; +END_CONTRACT: + sema_context_destroy(&temp_context); + if (!success) return false; + +SKIP_CONTRACTS: expr->call_expr.has_optional_arg = optional; if (!type_is_void(rtype)) @@ -1978,12 +2088,6 @@ static inline bool sema_call_analyse_func_invocation(SemaContext *context, Decl expr->call_expr.must_use = sig->attrs.nodiscard || (is_optional_return && !sig->attrs.maydiscard); } expr->type = type_add_optional(rtype, optional); - if (expr->call_expr.is_dynamic_dispatch) - { - Expr *any_val = expr->call_expr.arguments[0]; - ASSERT(any_val->expr_kind == EXPR_PTR_ACCESS); - *any_val = *(any_val->inner_expr); - } return true; } @@ -2121,7 +2225,53 @@ static inline bool sema_expr_analyse_func_call(SemaContext *context, Expr *expr, decl->name, no_match_ref); } +static inline bool sema_expr_setup_call_analysis(SemaContext *context, CalledDecl *callee, + SemaContext *macro_context, Expr *call_expr, + Type *rtype, + Ast *yield_body, + Decl **yield_params, Decl **params, + BlockExit **block_exit_ref, InliningSpan *span_ref) +{ + Decl *decl = callee->definition; + sema_context_init(macro_context, decl->unit); + macro_context->compilation_unit = context->unit; + macro_context->macro_call_depth = context->macro_call_depth + 1; + macro_context->call_env = context->call_env; + macro_context->expected_block_type = rtype; + if (span_ref) + { + *span_ref = (InliningSpan){ call_expr->span, context->inlined_at }; + } + else + { + span_ref = context->inlined_at; + } + macro_context->inlined_at = span_ref; + macro_context->current_macro = callee->macro ? decl : NULL; + macro_context->yield_body = yield_body; + macro_context->yield_params = yield_params; + macro_context->yield_context = context; + FOREACH(Expr *, expr, call_expr->call_expr.varargs) + { + if (expr->resolve_status == RESOLVE_DONE) continue; + Expr *expr_inner = expr_copy(expr); + expr->expr_kind = EXPR_OTHER_CONTEXT; + expr->expr_other_context.inner = expr_inner; + expr->expr_other_context.context = context; + } + macro_context->macro_varargs = callee->macro ? 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; + + macro_context->block_exit_ref = block_exit_ref; + + context_change_scope_with_flags(macro_context, SCOPE_MACRO); + macro_context->block_return_defer = macro_context->active_scope.defer_last; + + return true; +} bool sema_expr_analyse_macro_call(SemaContext *context, Expr *call_expr, Expr *struct_var, Decl *decl, bool call_var_optional, bool *no_match_ref) { @@ -2183,7 +2333,7 @@ bool sema_expr_analyse_macro_call(SemaContext *context, Expr *call_expr, Expr *s } } param->var.init_expr = args[i]; - // Ref arguments doesn't affect optional arg. + // Lazy arguments doesn't affect optional arg. if (param->var.kind == VARDECL_PARAM_EXPR) continue; has_optional_arg = has_optional_arg || IS_OPTIONAL(args[i]); } @@ -2272,44 +2422,24 @@ bool sema_expr_analyse_macro_call(SemaContext *context, Expr *call_expr, Expr *s DynamicScope old_scope = context->active_scope; + // Create a scope, since the macro itself will not. context_change_scope_with_flags(context, SCOPE_NONE); - SemaContext macro_context; - Type *rtype = NULL; - sema_context_init(¯o_context, decl->unit); - macro_context.compilation_unit = context->unit; - macro_context.macro_call_depth = context->macro_call_depth + 1; - macro_context.call_env = context->call_env; - rtype = typeget(sig->rtype); + Type *rtype = typeget(sig->rtype); bool optional_return = rtype && type_is_optional(rtype); bool may_be_optional = !rtype || optional_return; if (rtype) rtype = type_no_optional(rtype); - macro_context.expected_block_type = rtype; - context_change_scope_with_flags(¯o_context, SCOPE_MACRO); - - macro_context.block_return_defer = macro_context.active_scope.defer_last; - InliningSpan span = { call_expr->span, context->inlined_at }; - macro_context.inlined_at = &span; - macro_context.current_macro = decl; - macro_context.yield_body = macro_body ? macro_body->macro_body_expr.body : NULL; - macro_context.yield_params = body_params; - macro_context.yield_context = context; - FOREACH(Expr *, expr, call_expr->call_expr.varargs) - { - if (expr->resolve_status == RESOLVE_DONE) continue; - Expr *expr_inner = expr_copy(expr); - expr->expr_kind = EXPR_OTHER_CONTEXT; - expr->expr_other_context.inner = expr_inner; - expr->expr_other_context.context = context; - } - macro_context.macro_varargs = call_expr->call_expr.varargs; - 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; BlockExit** block_exit_ref = CALLOCS(BlockExit*); - macro_context.block_exit_ref = block_exit_ref; + + InliningSpan span; + if (!sema_expr_setup_call_analysis(context, &callee, ¯o_context, + call_expr, rtype, macro_body ? macro_body->macro_body_expr.body : NULL, body_params, params, block_exit_ref, + &span)) + { + goto EXIT_FAIL; + } AstId assert_first = 0; AstId* next = &assert_first; @@ -2344,10 +2474,10 @@ bool sema_expr_analyse_macro_call(SemaContext *context, Expr *call_expr, Expr *s } bool has_ensures = false; - if (!sema_analyse_contracts(¯o_context, docs, &next, call_expr->span, &has_ensures)) goto EXIT_FAIL; - sema_append_contract_asserts(assert_first, body); + if (!sema_analyse_contracts(¯o_context, docs, &next, call_expr->span, &has_ensures)) return false; macro_context.macro_has_ensures = has_ensures; + sema_append_contract_asserts(assert_first, body); if (!sema_analyse_statement(¯o_context, body)) goto EXIT_FAIL; ASSERT_SPAN(call_expr, macro_context.active_scope.depth == 1); bool implicit_void_return = !macro_context.active_scope.jump_end; @@ -2513,6 +2643,7 @@ EXIT: } return true; EXIT_FAIL: + context->active_scope = old_scope; sema_context_destroy(¯o_context); return SCOPE_POP_ERROR(); NO_MATCH_REF: @@ -2787,6 +2918,7 @@ INLINE bool sema_call_may_not_have_attributes(SemaContext *context, Expr *expr) } return true; } + static inline bool sema_call_analyse_member_get(SemaContext *context, Expr *expr) { if (vec_size(expr->call_expr.arguments) != 1) diff --git a/src/compiler/sema_liveness.c b/src/compiler/sema_liveness.c index 3f2ac7bf7..9c35151f0 100644 --- a/src/compiler/sema_liveness.c +++ b/src/compiler/sema_liveness.c @@ -321,6 +321,7 @@ RETRY: sema_trace_expr_liveness(exprptr(expr->call_expr.function)); return; } + sema_trace_astid_liveness(expr->call_expr.function_contracts); sema_trace_decl_liveness(declptr(expr->call_expr.func_ref)); return; } diff --git a/src/compiler/sema_stmts.c b/src/compiler/sema_stmts.c index 26986b914..29eb45938 100644 --- a/src/compiler/sema_stmts.c +++ b/src/compiler/sema_stmts.c @@ -382,18 +382,21 @@ static inline bool assert_create_from_contract(SemaContext *context, Ast *direct if (expr->expr_kind == EXPR_DECL) RETURN_SEMA_ERROR(expr, "Only expressions are allowed in contracts."); if (!sema_analyse_expr_rhs(context, type_bool, expr, false, NULL, false)) return false; + if (evaluation_location.a) expr->span = evaluation_location; + const char *comment = directive->contract_stmt.contract.comment; if (!comment) comment = directive->contract_stmt.contract.expr_string; if (expr_is_const_bool(expr)) { if (expr->const_expr.b) continue; - sema_error_at(context, evaluation_location.a ? evaluation_location : expr->span, "%s", comment); + sema_error_at(context, expr->span, "%s", comment); return false; } + Ast *assert = new_ast(AST_ASSERT_STMT, expr->span); assert->assert_stmt.is_ensure = true; assert->assert_stmt.expr = exprid(expr); - Expr *comment_expr = expr_new_const_string(expr->span, comment); + Expr *comment_expr = expr_new_const_string(assert->span, comment); assert->assert_stmt.message = exprid(comment_expr); ast_append(asserts, assert); } @@ -3095,9 +3098,11 @@ bool sema_analyse_contracts(SemaContext *context, AstId doc, AstId **asserts, So case CONTRACT_PARAM: break; case CONTRACT_OPTIONALS: + if (!has_ensures) break; if (!sema_analyse_optional_returns(context, directive)) return false; break; case CONTRACT_ENSURE: + if (!has_ensures) break; if (!sema_analyse_ensure(context, directive)) return false; *has_ensures = true; break;