From 4a25bcc5ee4ea25d9ec330e5ae577e8fdffae7d7 Mon Sep 17 00:00:00 2001 From: Christoffer Lerno Date: Mon, 3 Nov 2025 23:49:35 +0100 Subject: [PATCH] Function referencing in `@return?` for simplified fault declarations. Check `@return?` eagerly #2340. --- releasenotes.md | 1 + src/compiler/compiler_internal.h | 10 ++- src/compiler/copying.c | 5 +- src/compiler/parse_global.c | 27 ++++-- src/compiler/sema_decls.c | 87 +++++++++++++++++-- src/compiler/sema_expr.c | 11 ++- src/compiler/sema_liveness.c | 2 +- src/compiler/sema_stmts.c | 60 +++++++++++-- .../contracts/contract_copy_optret.c3 | 21 +++++ test/test_suite/errors/contract_multi.c3 | 2 +- 10 files changed, 200 insertions(+), 26 deletions(-) create mode 100644 test/test_suite/contracts/contract_copy_optret.c3 diff --git a/releasenotes.md b/releasenotes.md index 087547574..0be39d9b4 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -7,6 +7,7 @@ - Missing imports allowed if module `@if` evaluates to false #2251. - Add default exception handler to Win32 #2557. - Accept `"$schema"` as key in `project.json` #2554. +- Function referencing in `@return?` for simplified fault declarations. Check `@return?` eagerly #2340. ### Fixes - `Foo.is_eq` would return false if the type was a `typedef` and had an overload, but the underlying type was not comparable. diff --git a/src/compiler/compiler_internal.h b/src/compiler/compiler_internal.h index ae87fac5c..fe014c4aa 100644 --- a/src/compiler/compiler_internal.h +++ b/src/compiler/compiler_internal.h @@ -600,6 +600,12 @@ typedef struct }; } FuncDecl; +typedef struct +{ + Signature signature; + AstId docs; +} FnTypeDecl; + typedef struct { @@ -740,7 +746,7 @@ typedef struct Decl_ EnumConstantDecl enum_constant; ExecDecl exec_decl; Expr* expand_decl; - Signature fntype_decl; + FnTypeDecl fntype_decl; FuncDecl func_decl; ImportDecl import; IncludeDecl include; @@ -1527,6 +1533,7 @@ typedef struct typedef struct { bool resolved; + bool expanding; union { Expr *expr; @@ -2296,6 +2303,7 @@ bool may_cast(SemaContext *context, Expr *expr, Type *to_type, bool is_explicit, void cast_no_check(Expr *expr, Type *to_type, bool add_optional); + bool cast_to_index_len(SemaContext *context, Expr *index, bool is_len); const char *llvm_codegen(void *context); diff --git a/src/compiler/copying.c b/src/compiler/copying.c index f890bb65d..f34d5acf9 100644 --- a/src/compiler/copying.c +++ b/src/compiler/copying.c @@ -686,7 +686,7 @@ RETRY: case AST_CONTRACT_FAULT: if (ast->contract_fault.resolved) { - MACRO_COPY_DECL(ast->contract_fault.decl); + fixup_decl(c, &ast->contract_fault.decl); } else { @@ -1097,7 +1097,8 @@ Decl *copy_decl(CopyStruct *c, Decl *decl) MACRO_COPY_DECL_LIST(copy->enums.values); break; case DECL_FNTYPE: - copy_signature_deep(c, ©->fntype_decl); + copy_signature_deep(c, ©->fntype_decl.signature); + MACRO_COPY_ASTID(copy->fntype_decl.docs); break; case DECL_FUNC: copy_decl_type(copy); diff --git a/src/compiler/parse_global.c b/src/compiler/parse_global.c index 81a2ab43c..a5addec94 100644 --- a/src/compiler/parse_global.c +++ b/src/compiler/parse_global.c @@ -2181,13 +2181,15 @@ static inline void decl_add_type(Decl *decl, TypeKind kind) type->decl = decl; decl->type = type; } + + /** * typedef_declaration ::= ALIAS TYPE_IDENT attributes? '=' typedef_type ';' * * typedef_type ::= func_typedef | type generic_params? * func_typedef ::= 'fn' optional_type parameter_type_list */ -static inline Decl *parse_alias_type(ParseContext *c) +static inline Decl *parse_alias_type(ParseContext *c, AstId contracts, bool has_real_contracts) { advance_and_verify(c, TOKEN_ALIAS); @@ -2198,7 +2200,7 @@ static inline Decl *parse_alias_type(ParseContext *c) if (token_is_any_type(c->tok)) { PRINT_ERROR_HERE("'%s' is the name of a built-in type and can't be used as an alias.", - token_type_to_string(c->tok)); + token_type_to_string(c->tok)); return poisoned_decl; } if (token_is_some_ident(c->tok)) @@ -2223,8 +2225,9 @@ static inline Decl *parse_alias_type(ParseContext *c) Decl *decl_type = decl_new(DECL_FNTYPE, decl->name, c->prev_span); decl->type_alias_decl.decl = decl_type; ASSIGN_TYPE_OR_RET(TypeInfo *type_info, parse_optional_type(c), poisoned_decl); - decl_type->fntype_decl.rtype = type_infoid(type_info); - if (!parse_fn_parameter_list(c, &(decl_type->fntype_decl))) + decl_type->fntype_decl.signature.rtype = type_infoid(type_info); + decl_type->fntype_decl.docs = contracts; + if (!parse_fn_parameter_list(c, &(decl_type->fntype_decl.signature))) { return poisoned_decl; } @@ -2235,6 +2238,11 @@ static inline Decl *parse_alias_type(ParseContext *c) return decl; } + if (has_real_contracts) + { + RETURN_PRINT_ERROR_AT(poisoned_decl, astptr(contracts), "Contracts are only used for modules, functions and macros."); + } + // 2. Now parse the type which we know is here. ASSIGN_EXPR_OR_RET(Expr *expr, parse_expr(c), poisoned_decl); @@ -2413,13 +2421,17 @@ static inline Decl *parse_attrdef(ParseContext *c) /** * define_decl ::= ALIAS define_type_body */ -static inline Decl *parse_alias(ParseContext *c) +static inline Decl *parse_alias(ParseContext *c, AstId contracts, bool has_real_contracts) { switch (peek(c)) { case TOKEN_TYPE_IDENT: - return parse_alias_type(c); + return parse_alias_type(c, contracts, has_real_contracts); default: + if (has_real_contracts) + { + RETURN_PRINT_ERROR_AT(poisoned_decl, astptr(contracts), "Contracts are only used for modules, functions and macros."); + } return parse_alias_ident(c); } } @@ -3335,8 +3347,7 @@ Decl *parse_top_level_statement(ParseContext *c, ParseContext **context_out) PRINT_ERROR_HERE("There are more than one doc comment in a row, that is not allowed."); return poisoned_decl; case TOKEN_ALIAS: - if (has_real_contracts) goto CONTRACT_NOT_ALLOWED; - decl = parse_alias(c); + decl = parse_alias(c, contracts, has_real_contracts); if (decl->decl_kind == DECL_ALIAS_PATH) { if (!context_out) diff --git a/src/compiler/sema_decls.c b/src/compiler/sema_decls.c index 71dd2d04f..43216a99f 100755 --- a/src/compiler/sema_decls.c +++ b/src/compiler/sema_decls.c @@ -1492,8 +1492,12 @@ static inline bool sema_analyse_fntype(SemaContext *context, Decl *decl, bool *e { if (!sema_analyse_attributes(context, decl, decl->attributes, ATTR_FNTYPE, erase_decl)) return decl_poison(decl); if (*erase_decl) return true; - Signature *sig = &decl->fntype_decl; - return sema_analyse_function_signature(context, decl, NULL, sig->abi, sig); + Signature *sig = &decl->fntype_decl.signature; + if (!sema_analyse_function_signature(context, decl, NULL, sig->abi, sig)) return false; + bool pure = false; + if (!sema_analyse_doc_header(context, decl->fntype_decl.docs, sig->params, NULL, &pure)) return false; + sig->attrs.is_pure = pure; + return true; } static inline bool sema_analyse_type_alias(SemaContext *context, Decl *decl, bool *erase_decl) @@ -1507,7 +1511,7 @@ static inline bool sema_analyse_type_alias(SemaContext *context, Decl *decl, boo Decl *fn_decl = decl->type_alias_decl.decl; fn_decl->is_export = is_export; fn_decl->unit = decl->unit; - fn_decl->type = type_new_func(fn_decl, &fn_decl->fntype_decl); + fn_decl->type = type_new_func(fn_decl, &fn_decl->fntype_decl.signature); decl->type->canonical = type_get_func_ptr(fn_decl->type); return true; } @@ -3035,7 +3039,7 @@ INLINE bool update_abi(Decl *decl, CallABI abi) { if (decl->decl_kind == DECL_FNTYPE) { - decl->fntype_decl.abi = abi; + decl->fntype_decl.signature.abi = abi; return true; } decl->func_decl.signature.abi = abi; @@ -3403,8 +3407,8 @@ static bool sema_analyse_attribute(SemaContext *context, ResolvedAttrData *attr_ decl->func_decl.signature.attrs.format = val + 1; return true; case DECL_FNTYPE: - if (decl->fntype_decl.attrs.format) break; - decl->fntype_decl.attrs.format = val + 1; + if (decl->fntype_decl.signature.attrs.format) break; + decl->fntype_decl.signature.attrs.format = val + 1; return true; default: UNREACHABLE @@ -3764,8 +3768,72 @@ bool sema_analyse_attributes(SemaContext *context, Decl *decl, Attr **attrs, Att return true; } +static bool sema_analyse_optional_returns(SemaContext *context, Ast *directive) +{ + FOREACH(Ast *, ret, directive->contract_stmt.faults) + { + if (ret->contract_fault.expanding) continue; + if (ret->contract_fault.resolved) + { + continue; + } + Expr *expr = ret->contract_fault.expr; + if (expr->expr_kind == EXPR_RETHROW) + { + Expr *inner = expr->rethrow_expr.inner; + if (!sema_analyse_expr(context, inner)) return false; + Decl *decl; + switch (inner->expr_kind) + { + case EXPR_IDENTIFIER: + decl = inner->ident_expr; + break; + case EXPR_TYPEINFO: + { + Type *type = inner->type_expr->type; + if (type->type_kind != TYPE_ALIAS) goto IS_FAULT; + decl = type->decl; + ASSERT(decl->decl_kind == DECL_TYPE_ALIAS); + if (!decl->type_alias_decl.is_func) goto IS_FAULT; + decl = decl->type_alias_decl.decl; + break; + } + default: + goto IS_FAULT;; + } + decl = decl_flatten(decl); + if (decl->decl_kind != DECL_FNTYPE && decl->decl_kind != DECL_FUNC) goto IS_FAULT; + if (!sema_analyse_decl(context, decl)) return false; + AstId docs = decl->decl_kind == DECL_FNTYPE ? decl->fntype_decl.docs : decl->func_decl.docs; + while (docs) + { + Ast *doc = astptr(docs); + docs = doc->next; + if (doc->contract_stmt.kind != CONTRACT_OPTIONALS) continue; + ret->contract_fault.expanding = true; + bool success = sema_analyse_optional_returns(context, doc); + ret->contract_fault.expanding = false; + if (!success) false; + } + continue; + } +IS_FAULT:; + if (!sema_analyse_expr_rvalue(context, expr)) return false; + if (expr->type->canonical != type_fault) + { + RETURN_SEMA_ERROR(expr, "Expected a fault here."); + } + if (!expr_is_const_fault(expr)) RETURN_SEMA_ERROR(expr, "A constant fault is required."); + Decl *decl = expr->const_expr.fault; + if (!decl) RETURN_SEMA_ERROR(expr, "A non-null fault is required."); + ret->contract_fault.decl = decl; + ret->contract_fault.resolved = true; + } + return true; +} + static inline bool sema_analyse_doc_header(SemaContext *context, AstId doc, - Decl **params, Decl **extra_params, bool *pure_ref) + Decl **params, Decl **extra_params, bool *pure_ref) { while (doc) { @@ -3782,6 +3850,10 @@ static inline bool sema_analyse_doc_header(SemaContext *context, AstId doc, *pure_ref = true; continue; } + if (directive_kind == CONTRACT_OPTIONALS) + { + if (!sema_analyse_optional_returns(context, directive)) return false; + } if (directive_kind != CONTRACT_PARAM) continue; const char *param_name = directive->contract_stmt.param.name; Decl *param = NULL; @@ -4258,6 +4330,7 @@ CHECK_DONE: "'extern' or place it in an .c3i file."); } bool pure = false; + if (!sema_analyse_doc_header(context, decl->func_decl.docs, decl->func_decl.signature.params, NULL, &pure)) return false; decl->func_decl.signature.attrs.is_pure = pure; diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index a6e94b635..0901e1c26 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -2463,7 +2463,16 @@ static inline bool sema_call_analyse_func_invocation(SemaContext *context, Decl *any_val = *(any_val->inner_expr); } expr->call_expr.function_contracts = 0; - AstId docs = decl->func_decl.docs; + AstId docs; + if (decl->decl_kind == DECL_FNTYPE) + { + docs = decl->fntype_decl.docs; + } + else + { + docs = decl->func_decl.docs; + } + if (!safe_mode_enabled() || !sema_has_require(docs)) goto SKIP_CONTRACTS; SemaContext temp_context; bool success = false; diff --git a/src/compiler/sema_liveness.c b/src/compiler/sema_liveness.c index 53619bf84..bb48b5a43 100644 --- a/src/compiler/sema_liveness.c +++ b/src/compiler/sema_liveness.c @@ -636,7 +636,7 @@ RETRY: case DECL_MACRO: UNREACHABLE_VOID case DECL_FNTYPE: - sema_trace_func_liveness(&decl->fntype_decl); + sema_trace_func_liveness(&decl->fntype_decl.signature); return; case DECL_FUNC: sema_trace_func_liveness(&decl->func_decl.signature); diff --git a/src/compiler/sema_stmts.c b/src/compiler/sema_stmts.c index 19bc7f118..263738814 100644 --- a/src/compiler/sema_stmts.c +++ b/src/compiler/sema_stmts.c @@ -494,6 +494,11 @@ static inline bool sema_check_return_matches_opt_returns(SemaContext *context, E if (opt == fault) return true; } // No match + FOREACH(Decl *, opt, context->call_env.opt_returns) + { + assert(opt->decl_kind == DECL_FAULT); + if (opt == fault) return true; + } RETURN_SEMA_ERROR(ret_expr, "This value does not match declared optional returns, it needs to be declared with the other optional returns."); } @@ -3283,14 +3288,59 @@ static bool sema_analyse_optional_returns(SemaContext *context, Ast *directive) { FOREACH(Ast *, ret, directive->contract_stmt.faults) { - if (ret->contract_fault.resolved) continue; - Expr *expr = ret->contract_fault.expr; - if (expr->expr_kind != EXPR_UNRESOLVED_IDENTIFIER && !expr->unresolved_ident_expr.is_const) + if (ret->contract_fault.expanding) continue; + if (ret->contract_fault.resolved) { - RETURN_SEMA_ERROR(expr, "Expected a fault name here."); + vec_add(context->call_env.opt_returns, ret->contract_fault.decl); + continue; } + Expr *expr = ret->contract_fault.expr; + if (expr->expr_kind == EXPR_RETHROW) + { + Expr *inner = expr->rethrow_expr.inner; + if (!sema_analyse_expr(context, inner)) return false; + Decl *decl; + switch (inner->expr_kind) + { + case EXPR_IDENTIFIER: + decl = inner->ident_expr; + break; + case EXPR_TYPEINFO: + { + Type *type = inner->type_expr->type; + if (type->type_kind != TYPE_ALIAS) goto IS_FAULT; + decl = type->decl; + ASSERT(decl->decl_kind == DECL_TYPE_ALIAS); + if (!decl->type_alias_decl.is_func) goto IS_FAULT; + decl = decl->type_alias_decl.decl; + break; + } + default: + goto IS_FAULT;; + } + decl = decl_flatten(decl); + if (decl->decl_kind != DECL_FNTYPE && decl->decl_kind != DECL_FUNC) goto IS_FAULT; + if (!sema_analyse_decl(context, decl)) return false; + AstId docs = decl->decl_kind == DECL_FNTYPE ? decl->fntype_decl.docs : decl->func_decl.docs; + while (docs) + { + Ast *doc = astptr(docs); + docs = doc->next; + if (doc->contract_stmt.kind != CONTRACT_OPTIONALS) continue; + ret->contract_fault.expanding = true; + bool success = sema_analyse_optional_returns(context, doc); + ret->contract_fault.expanding = false; + if (!success) false; + } + continue; + } +IS_FAULT:; if (!sema_analyse_expr_rvalue(context, expr)) return false; - if (!expr_is_const_fault(expr)) RETURN_SEMA_ERROR(expr, "A fault is required."); + if (expr->type->canonical != type_fault) + { + RETURN_SEMA_ERROR(expr, "Expected a fault here."); + } + if (!expr_is_const_fault(expr)) RETURN_SEMA_ERROR(expr, "A constant fault is required."); Decl *decl = expr->const_expr.fault; if (!decl) RETURN_SEMA_ERROR(expr, "A non-null fault is required."); ret->contract_fault.decl = decl; diff --git a/test/test_suite/contracts/contract_copy_optret.c3 b/test/test_suite/contracts/contract_copy_optret.c3 new file mode 100644 index 000000000..b76199332 --- /dev/null +++ b/test/test_suite/contracts/contract_copy_optret.c3 @@ -0,0 +1,21 @@ +module test; +import std; + +<* + @return? test! +*> +fn void? test2() +{ + return NOT_FOUND?; +} +<* + @return? io::EOF, Foo! +*> +fn void? test() +{ + return NOT_FOUND?; +} +<* + @return? NOT_FOUND +*> +alias Foo = fn void(); diff --git a/test/test_suite/errors/contract_multi.c3 b/test/test_suite/errors/contract_multi.c3 index d9681c03b..7ce5eeae0 100644 --- a/test/test_suite/errors/contract_multi.c3 +++ b/test/test_suite/errors/contract_multi.c3 @@ -1,5 +1,5 @@ module contract_err_tests; - +import std; <* Hello world @param [in] a