diff --git a/compiler/bootstrap/flowc_stage_a.c b/compiler/bootstrap/flowc_stage_a.c index ef0f1c08c..28c39b047 100644 --- a/compiler/bootstrap/flowc_stage_a.c +++ b/compiler/bootstrap/flowc_stage_a.c @@ -86906,6 +86906,7 @@ const char* sem_node_key(Sem* c, int32_t node); bool sem_domain_escape_to_static(Sem* c, int32_t stmt, int32_t value, const char* target, int32_t target_type); bool sem_domain_escape_to_field(Sem* c, int32_t stmt, int32_t value, int32_t lhs, int32_t target_type); bool sem_domain_escape_to_named_field(Sem* c, int32_t stmt, int32_t value, const char* field_name, int32_t field_domain); +bool sem_domain_escape_to_param_target(Sem* c, int32_t stmt, int32_t value, int32_t lhs); bool sem_is_heap_alloc_expr(Sem* c, int32_t e); bool sem_name_is_heap_root(Sem* c, const char* name); bool sem_expr_is_heap_root(Sem* c, int32_t e); @@ -88402,6 +88403,31 @@ bool sem_domain_escape_to_named_field(Sem* c, int32_t stmt, int32_t value, const return 1; } +bool sem_domain_escape_to_param_target(Sem* c, int32_t stmt, int32_t value, int32_t lhs) { + if (c->current_domain < 0 || lhs < 0) { + return 0; +} + if (sem_lhs_field_domain(c, lhs) >= 0) { + return 0; +} + int32_t pi = sem_root_parameter_index(c, lhs); + if (pi < 0 || (uint8_t*)(c->current_function_name) == NULL) { + return 0; +} + const char* origin = sem_composite_domain_root(c, value); + if ((uint8_t*)(origin) == NULL) { + return 0; +} + int32_t fi = sem_sm_get(c->function_decls, c->current_function_name); + const char* pname = "parameter"; + if (fi >= 0 && pi < sem_fi_nparams(c, fi)) { + pname = sem_fi_param_name(c, fi, pi); +} + sem_sm_put(c->domain_reported, sem_node_key(c, stmt), 1); + sem_err(c, __flowc_str_concatn(9, "lifetime domain escape: `", origin, "` lives in the `", sem_domain_name(c->current_domain), "` domain but is stored through parameter `", pname, "`, whose pointee lifetime is not proven to end within this call ", "(add an explicit field lifetime contract or keep the composite local)", sem_location_suffix(c, stmt))); + return 1; +} + bool sem_is_heap_alloc_expr(Sem* c, int32_t e) { if (e < 0 || sem_is_function_call(c, e) == 0) { return 0; @@ -90299,7 +90325,7 @@ int32_t sem_check_slice_expr(Sem* c, int32_t e) { } int32_t sp = sem_ty_named(c->t, TK_SPAN, __flowc_str_concat(prefix, sem_tstr(c, el))); tys->elem[sp] = el; - { __typeof__(tys->size[sp]) __flowc_st15260 = sem_static_length(c, e); tys->size[sp] = __flowc_st15260; } + { __typeof__(tys->size[sp]) __flowc_st15391 = sem_static_length(c, e); tys->size[sp] = __flowc_st15391; } return sp; } @@ -90470,7 +90496,7 @@ int32_t sem_check_lambda(Sem* c, int32_t e) { p = sem_nnext(c, p); } SemIntVec* saved_sink = (SemIntVec*)(c->return_sink); - { __typeof__(c->return_sink) __flowc_st16009 = sem_iv_new(); c->return_sink = __flowc_st16009; } + { __typeof__(c->return_sink) __flowc_st16140 = sem_iv_new(); c->return_sink = __flowc_st16140; } sem_check_block(c, sem_ncc(c, e)); c->return_sink = saved_sink; sem_walk_lambda_captures(c, sem_ncc(c, e), cname, mark); @@ -90479,9 +90505,9 @@ int32_t sem_check_lambda(Sem* c, int32_t e) { c->current_rt_safe_fn = prev_rt; c->rt_safe_from_domain = prev_rt_dom; if (sem_nb(c, e) != AST_NONE) { - { __typeof__(tys->ret[f]) __flowc_st16059 = sem_parse_type(c, sem_pt_of_ast(c, sem_nb(c, e))); tys->ret[f] = __flowc_st16059; } + { __typeof__(tys->ret[f]) __flowc_st16190 = sem_parse_type(c, sem_pt_of_ast(c, sem_nb(c, e))); tys->ret[f] = __flowc_st16190; } } else { - { __typeof__(tys->ret[f]) __flowc_st16068 = sem_tmk(c, TK_VOID); tys->ret[f] = __flowc_st16068; } + { __typeof__(tys->ret[f]) __flowc_st16199 = sem_tmk(c, TK_VOID); tys->ret[f] = __flowc_st16199; } } return f; } @@ -91023,6 +91049,9 @@ int32_t sem_check_assignment(Sem* c, int32_t s) { } if (reported == 0) { reported = sem_domain_escape_to_field(c, s, value, lhs, tt); +} + if (reported == 0) { + reported = sem_domain_escape_to_param_target(c, s, value, lhs); } if (reported == 0 && (uint8_t*)(base) != NULL && sem_sm_has(c->static_names, base) == 0) { reported = sem_domain_escape_to_heap(c, s, value, base, tt); @@ -91881,17 +91910,17 @@ void sem_check_function(Sem* c, int32_t fi) { SemStrMap* prev_origin = (SemStrMap*)(c->span_origin); SemStrMap* prev_heap = (SemStrMap*)(c->heap_roots); int32_t prev_ret = c->current_return_type; - { __typeof__(c->local_storage) __flowc_st22121 = sem_sv_new(); c->local_storage = __flowc_st22121; } - { __typeof__(c->span_origin) __flowc_st22125 = sem_sm_new(); c->span_origin = __flowc_st22125; } - { __typeof__(c->heap_roots) __flowc_st22129 = sem_sm_new(); c->heap_roots = __flowc_st22129; } - { __typeof__(c->current_return_type) __flowc_st22138 = sem_parse_type(c, sem_iv_get(c->fi_ret, fi)); c->current_return_type = __flowc_st22138; } + { __typeof__(c->local_storage) __flowc_st22264 = sem_sv_new(); c->local_storage = __flowc_st22264; } + { __typeof__(c->span_origin) __flowc_st22268 = sem_sm_new(); c->span_origin = __flowc_st22268; } + { __typeof__(c->heap_roots) __flowc_st22272 = sem_sm_new(); c->heap_roots = __flowc_st22272; } + { __typeof__(c->current_return_type) __flowc_st22281 = sem_parse_type(c, sem_iv_get(c->fi_ret, fi)); c->current_return_type = __flowc_st22281; } i = 0; for (; i < (sem_fi_nparams(c, fi)); i = i + 1) { int32_t pt = sem_parse_type(c, sem_fi_param_type(c, fi, i)); sem_define_var(c, sem_fi_param_name(c, fi, i), pt, 0); } SemIntVec* prev_sink = (SemIntVec*)(c->return_sink); - { __typeof__(c->return_sink) __flowc_st22181 = sem_iv_new(); c->return_sink = __flowc_st22181; } + { __typeof__(c->return_sink) __flowc_st22324 = sem_iv_new(); c->return_sink = __flowc_st22324; } sem_check_block(c, sem_iv_get(c->fi_body, fi)); SemIntVec* returns = (SemIntVec*)(c->return_sink); c->return_sink = prev_sink; diff --git a/compiler/fixtures/mlir/corpus.txt b/compiler/fixtures/mlir/corpus.txt index e06f18f3c..9e72011f1 100644 --- a/compiler/fixtures/mlir/corpus.txt +++ b/compiler/fixtures/mlir/corpus.txt @@ -415,6 +415,8 @@ f3c0f1780d6a2204f761f20b41abd95fa55ab64422cc48d70e8ccdf84693d9df compiler/fixtur c1740c9451405142c8b2213c703aad4215387e1ebd05cd0fa9b0790b0b8485e3 compiler/fixtures/typecheck_rules/domain_ld5_unannotated_field.flow c1740c9451405142c8b2213c703aad4215387e1ebd05cd0fa9b0790b0b8485e3 compiler/fixtures/typecheck_rules/domain_ld5_unannotated_fn.flow f061d484520905d1693c5567431740b70d9ced000bc134328bd87ac89ce27763 compiler/fixtures/typecheck_rules/domain_ld5_value_field.flow +35859f7f3352ce33a7ab5e3af2756e8b9f3837441c6d96ecfe8867ada086e429 compiler/fixtures/typecheck_rules/domain_param_composite_unbounded.flow +5bab87f416406da897449642647480e4c8fa03b534f103abeaf844ea8b832290 compiler/fixtures/typecheck_rules/domain_param_field_unbounded.flow 7dbb1d9f8ea4ba6558c96a5f2ebc1ce18ad9845b96945bd22f29028cde9c13af compiler/fixtures/typecheck_rules/domain_persistent_pointer_static.flow da928172a7bf2e2989c92ea345ff50f46bb789d5f758a32f45a09e5a33bf2122 compiler/fixtures/typecheck_rules/domain_persistent_string_array.flow 8f23d03501493b4442f7b6f4e0b443310f191bca68089b540d95aad17d696785 compiler/fixtures/typecheck_rules/domain_request_call_session.flow @@ -2332,7 +2334,7 @@ b373ac75acc41ccef616dc57987ae160a7bd338a32b8ab654cdfcc0cff5af601 tests/lang/test 059f890a0b1efc9072df83c03fb496c021a29247f85794579fb9b047f91f245b tests/lang/test_deep_tail_recursion.flow ba8141636b8ee78c8fcb698cc1cad34563fce9833dfacb0e1eb01945b7f8dfda tests/lang/test_defer_ordering.flow 3e876e834ff5ce20d090be87b8406aa0699f6b72e95372bf64fd562b899b1172 tests/lang/test_defer_return_order.flow -10eeff24b2150ba69649814c940cb174a374d7570d897e9d1b76c47300800046 tests/lang/test_domain_fields.flow +d3c16bbb4072eaa8d87ed81e23a7e1abe2d8918e743c9407ab08a8045d46addc tests/lang/test_domain_fields.flow 16e0270ae14480e4fcdb56f2164744661902ad333d97bbea4d0f7e6926cfc880 tests/lang/test_dsl_detect.flow 46208f128ccd478092a9a0fd23564b8c775fa55cf688df5a2a88fbde8f9e206d tests/lang/test_dual_operator_sugar.flow 84fa41e915704ac6b562b27aef9ef923277f027dafd3dce8190d4bc7de720003 tests/lang/test_dynamic_invariants.flow diff --git a/compiler/fixtures/typecheck_rules/domain_param_composite_unbounded.flow b/compiler/fixtures/typecheck_rules/domain_param_composite_unbounded.flow new file mode 100644 index 000000000..71883c501 --- /dev/null +++ b/compiler/fixtures/typecheck_rules/domain_param_composite_unbounded.flow @@ -0,0 +1,12 @@ +# expect: lifetime domain escape: `scratch` lives in the `frame` domain but is stored through parameter `out`, whose pointee lifetime is not proven to end within this call (add an explicit field lifetime contract or keep the composite local) at line 9, column 5 +struct Holder { + view: ptr +} + +@lifetime(frame) +function fill(out: ptr) -> void { + let scratch: array = [7, 8] + out[0] = Holder { view: &scratch } +} + +function main() -> i32 { return 0 } diff --git a/compiler/fixtures/typecheck_rules/domain_param_field_unbounded.flow b/compiler/fixtures/typecheck_rules/domain_param_field_unbounded.flow new file mode 100644 index 000000000..682791da8 --- /dev/null +++ b/compiler/fixtures/typecheck_rules/domain_param_field_unbounded.flow @@ -0,0 +1,12 @@ +# expect: lifetime domain escape: `scratch` lives in the `callback` domain but is stored through parameter `out`, whose pointee lifetime is not proven to end within this call (add an explicit field lifetime contract or keep the composite local) at line 9, column 5 +struct Holder { + view: ptr +} + +@lifetime(callback) +function fill(out: ptr) -> void { + let scratch: array = [1, 2, 3, 4] + out[0].view = &scratch +} + +function main() -> i32 { return 0 } diff --git a/compiler/src/sem_check.flow b/compiler/src/sem_check.flow index d77ff6bd9..443db6ebb 100644 --- a/compiler/src/sem_check.flow +++ b/compiler/src/sem_check.flow @@ -1395,6 +1395,39 @@ function sem_domain_escape_to_named_field(c: ptr, stmt: i32, value: i32, fi return true } +# A pointer/span parameter names caller-owned storage. Without an explicit +# field lifetime, the callee cannot prove that pointee storage dies with this +# invocation, so a domain-local reference stored through it must fail closed. +function sem_domain_escape_to_param_target(c: ptr, stmt: i32, value: i32, lhs: i32) -> bool { + if c.current_domain < 0 or lhs < 0 { + return false + } + # A declared field lifetime is a stronger contract and is handled by LD5. + if sem_lhs_field_domain(c, lhs) >= 0 { + return false + } + let pi: i32 = sem_root_parameter_index(c, lhs) + if pi < 0 or c.current_function_name as ptr == null { + return false + } + let origin: string = sem_composite_domain_root(c, value) + if origin as ptr == null { + return false + } + let fi: i32 = sem_sm_get(c.function_decls, c.current_function_name) + let mut pname: string = "parameter" + if fi >= 0 and pi < sem_fi_nparams(c, fi) { + pname = sem_fi_param_name(c, fi, pi) + } + sem_sm_put(c.domain_reported, sem_node_key(c, stmt), 1) + sem_err(c, "lifetime domain escape: `" + origin + "` lives in the `" + + sem_domain_name(c.current_domain) + "` domain but is stored through parameter `" + + pname + "`, whose pointee lifetime is not proven to end within this call " + + "(add an explicit field lifetime contract or keep the composite local)" + + sem_location_suffix(c, stmt)) + return true +} + function sem_is_heap_alloc_expr(c: ptr, e: i32) -> bool { if e < 0 or sem_is_function_call(c, e) == false { return false @@ -4043,6 +4076,9 @@ function sem_check_assignment(c: ptr, s: i32) -> i32 { if reported == false { reported = sem_domain_escape_to_field(c, s, value, lhs, tt) } + if reported == false { + reported = sem_domain_escape_to_param_target(c, s, value, lhs) + } # Store through a heap root (#765): a malloc result or a local # already marked as one lives in the application domain. if reported == false and base as ptr != null and sem_sm_has(c.static_names, base) == false { diff --git a/docs/language/domain-fields.md b/docs/language/domain-fields.md index 1adad515e..75341d255 100644 --- a/docs/language/domain-fields.md +++ b/docs/language/domain-fields.md @@ -114,11 +114,32 @@ function build() -> Holder { } ``` +## Pointer-parameter targets fail closed + +A pointer/span parameter is caller-owned storage whose pointee lifetime is +not bounded by the callee. A domain-local reference therefore cannot be +stored through an **unannotated** parameter target: + +```flow expect-error +struct Holder { view: ptr } + +@lifetime(callback) +function fill(out: ptr) -> void { + let scratch: array = [1, 2, 3, 4] + out[0].view = &scratch +} + +function main() -> i32 { return 0 } +``` + +The same rule covers a whole-composite assignment such as +`out[0] = Holder { view: &scratch }`. An explicit field contract at the +same or shorter domain remains legal: for example an +`@lifetime(callback) view: ptr` field can receive a callback-local +reference through `ptr`. + ## What is still not checked -- A store into an unannotated field of a struct reached only through a - pointer parameter. The checker cannot prove that struct outlives the - writing frame. - Copying a local struct that was previously filled with a short-lived reference (`holder = local`) without a literal on the right-hand side. - Escape through a closure, a function pointer, the heap, or pointer diff --git a/docs/project/Questions.md b/docs/project/Questions.md index 8885d6cba..e240ea265 100644 --- a/docs/project/Questions.md +++ b/docs/project/Questions.md @@ -579,6 +579,20 @@ contract, together with rule LD5. #1358 merges after #1369. **Resolved:** 2026-10-07, decided by the project owner. +### 2026-10-10: Stores through unbounded pointer parameters + +**Context:** PR #1472 follows issue #684. A function with a declared +lifetime domain can store a frame-local reference through an unannotated +pointer or span parameter. The callee cannot establish how long the +caller-owned pointee remains live. + +**Answer:** Reject that store when the value contains a reference rooted in +the writing frame. An explicit field `@lifetime(D)` contract permits the +store when `D` is the same or shorter-lived than the writer. This applies +to field stores and whole-composite assignment through the parameter. + +**Resolved:** 2026-10-10, decided by the project owner. + ### 2026-10-07: Last-use moves without a `move` keyword or COW **Context:** Issue #696 asks for value semantics with compiler-inferred diff --git a/tests/lang/test_domain_fields.flow b/tests/lang/test_domain_fields.flow index 7ada43132..875e34b57 100644 --- a/tests/lang/test_domain_fields.flow +++ b/tests/lang/test_domain_fields.flow @@ -61,6 +61,16 @@ function fill_from_static() -> i32 { return 0 } +@lifetime(callback) +function fill_param_same_domain(out: ptr) -> i32 { + let scratch: array = [12, 13] + out[0].view = &scratch + if out[0].view[1] != 13 { + return 6 + } + return 0 +} + @lifetime(session) function fill_shorter_field() -> i32 { let mut h: BoundHolder = BoundHolder { view: null } @@ -89,5 +99,10 @@ function main() -> i32 { if d != 0 { return d } + let mut h: BoundHolder = BoundHolder { view: null } + let e: i32 = fill_param_same_domain(&h) + if e != 0 { + return e + } return 0 }