From 4f262997272d74eadb3e2d307d0021e5208f1bfe Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 8 Sep 2026 11:41:40 +0200 Subject: [PATCH 1/8] Reject NativePtr.stackalloc inside a try handler/finally (FS3916) NativePtr.stackalloc emits the 'localloc' IL instruction, which the JIT rejects inside an exception-handling region, causing InvalidProgramException at method load. Detect this during PostInferenceChecks and emit compile-time error FS3916 when stackalloc is applied inside a 'with' handler or 'finally' block. A withinHandler env flag is set on the handler/finally bodies and reset at closure and method boundaries so stackalloc in a lambda or object-expression method defined in a handler stays legal. Part of issue #20295 (Case 1). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Checking/PostInferenceChecks.fs | 17 +++- src/Compiler/FSComp.txt | 1 + src/Compiler/TypedTree/TcGlobals.fs | 2 + src/Compiler/TypedTree/TcGlobals.fsi | 2 + src/Compiler/xlf/FSComp.txt.cs.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.de.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.es.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.fr.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.it.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.ja.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.ko.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.pl.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.pt-BR.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.ru.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.tr.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.zh-Hans.xlf | 5 ++ src/Compiler/xlf/FSComp.txt.zh-Hant.xlf | 5 ++ .../Libraries/NativeInterop.fs | 82 +++++++++++++++++++ 19 files changed, 167 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index ed1e382d6c7..4e20ee59fe9 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,5 +1,6 @@ ### Fixed +* Fix `NativePtr.stackalloc` used inside the `with` handler, filter or `finally` block of a `try` expression producing an assembly that throws `InvalidProgramException` at method load. The `localloc` IL instruction it emits is rejected by the runtime inside an exception-handling region, so this is now reported at compile time as error FS3916. ([Issue #20295](https://github.com/dotnet/fsharp/issues/20295)) * Fix `NativePtr.stackalloc` nested in a larger expression (e.g. a call argument or the right of an assignment) producing an assembly that throws `InvalidProgramException` at load. ([Issue #8083](https://github.com/dotnet/fsharp/issues/8083), [PR #20302](https://github.com/dotnet/fsharp/pull/20302)) * Fix internal error "Unexpected generalized type variables when compiling an active pattern" when an active pattern is used in a `let` binding whose right-hand side is a generic value, e.g. `let (T) = id`. Such a binding is now checked like the equivalent `match` and is not generalized. ([Issue #16856](https://github.com/dotnet/fsharp/issues/16856), [PR #20383](https://github.com/dotnet/fsharp/pull/20383)) * Fix Release-only (`--optimize+`) `System.InvalidProgramException` from `Seq.collect` / `yield!` over a value-type (struct) collection implementing `seq<'T>` (e.g. `ImmutableArray<_>`) when materialised with `List.ofSeq` / `Seq.toList` / `Seq.toArray` or a list/array comprehension. The collector lowering now boxes a struct sub-collection to `seq<'T>` before calling `AddMany`/`AddManyAndClose` (matching the coercion the type checker already inserts for `yield!`), and uses `unit` as the try/finally result type instead of the body type (removing a spurious `ldnull` store). ([Issue #20203](https://github.com/dotnet/fsharp/issues/20203)) diff --git a/src/Compiler/Checking/PostInferenceChecks.fs b/src/Compiler/Checking/PostInferenceChecks.fs index 1d5798320b4..80f41d00dc4 100644 --- a/src/Compiler/Checking/PostInferenceChecks.fs +++ b/src/Compiler/Checking/PostInferenceChecks.fs @@ -100,6 +100,9 @@ type env = /// Are we expecting a resumable code block etc resumableCode: Resumable + + /// Are we inside the 'with' handler, filter, or 'finally' block of a 'try'? + withinHandler: bool } override _.ToString() = "" @@ -1248,6 +1251,11 @@ and CheckExpr (cenv: cenv) (env: env) origExpr (ctxt: PermitByRefExpr) : Limit = // Check an application | Expr.App (f, _fty, tyargs, argsl, m) -> + (match f with + | OptionalCoerce(Expr.Val (vref, _, _)) + when env.withinHandler && cenv.reportErrors && valRefEq g vref g.nativeptr_stackalloc_vref -> + errorR(Error(FSComp.SR.chkNativePtrStackallocInHandler(), m)) + | _ -> ()) CheckApplication cenv env expr (f, tyargs, argsl, m) ctxt | Expr.Lambda (_, _, _, argvs, _, m, bodyTy) -> @@ -1473,6 +1481,7 @@ and CheckMethod cenv env baseValOpt ty (TObjExprMethod(_, attribs, tps, vs, body { env with resumableCode = Resumable.ResumableExpr false } else { env with resumableCode = Resumable.None } + let env = { env with withinHandler = false } CheckAttribs cenv env attribs CheckNoReraise cenv None body CheckEscapes cenv true m (match baseValOpt with Some x -> x :: vs | None -> vs) body |> ignore @@ -1514,7 +1523,7 @@ and CheckExprOp cenv env (op, tyargs, args, m) ctxt expr = | TOp.TryFinally _, [_], [Expr.Lambda (_, _, _, [_], e1, _, _); Expr.Lambda (_, _, _, [_], e2, _, _)] -> CheckTypeInstNoInnerByrefs cenv env m tyargs // result of a try/finally can be a byref let limit = CheckExpr cenv env e1 ctxt // result of a try/finally can be a byref if in a position where the overall expression is can be a byref - CheckExprNoByrefs cenv env e2 + CheckExprNoByrefs cenv { env with withinHandler = true } e2 limit | TOp.IntegerForLoop _, _, [Expr.Lambda (_, _, _, [_], e1, _, _);Expr.Lambda (_, _, _, [_], e2, _, _);Expr.Lambda (_, _, _, [_], e3, _, _)] -> @@ -1525,7 +1534,7 @@ and CheckExprOp cenv env (op, tyargs, args, m) ctxt expr = CheckTypeInstNoInnerByrefs cenv env m tyargs // result of a try/catch can be a byref let limit1 = CheckExpr cenv env e1 ctxt // result of a try/catch can be a byref if in a position where the overall expression is can be a byref // [(* e2; -- don't check filter body - duplicates logic in 'catch' body *) e3] - let limit2 = CheckExpr cenv env e3 ctxt // result of a try/catch can be a byref if in a position where the overall expression is can be a byref + let limit2 = CheckExpr cenv { env with withinHandler = true } e3 ctxt // result of a try/catch can be a byref if in a position where the overall expression is can be a byref CombineTwoLimits limit1 limit2 | TOp.ILCall (_, _, _, _, _, _, _, ilMethRef, enclTypeInst, methInst, retTypes), _, _ -> @@ -1810,6 +1819,7 @@ and CheckLambdas isTop (memberVal: Val option) cenv env inlined valReprInfo alwa let restArgs = List.concat vsl let syntacticArgs = thisAndBase @ restArgs let env = BindArgVals env restArgs + let env = { env with withinHandler = false } match memInfo with | None -> () @@ -2852,7 +2862,8 @@ let CheckImplFile (g, amap, reportErrors, infoReader, internalsVisibleToPaths, v external=false returnScope = 0 isInAppExpr = false - resumableCode = Resumable.None } + resumableCode = Resumable.None + withinHandler = false } CheckImplFileContents cenv env implFileTy implFileContents CheckAttribs cenv env extraAttribs diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index 7a53eae36f3..06c4e93c112 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1821,3 +1821,4 @@ featureRecordSpreads,"record type and expression spreads" 3913,tcExtendedLayoutCannotBeUsedOnUnions,"The 'ExtendedLayoutAttribute' cannot be applied to discriminated unions" 3914,tcExtendedLayoutStructMustHaveInstanceField,"A struct with the 'ExtendedLayoutAttribute' must have at least one instance field" 3915,tcTupleTypeExtensionTooManyElements,"Tuple type extensions are supported only for tuples of up to 7 elements, but this tuple type has %d elements. Extensions of larger tuples are not supported." +3916,chkNativePtrStackallocInHandler,"'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded." diff --git a/src/Compiler/TypedTree/TcGlobals.fs b/src/Compiler/TypedTree/TcGlobals.fs index 24b2e645bfb..210849e0067 100644 --- a/src/Compiler/TypedTree/TcGlobals.fs +++ b/src/Compiler/TypedTree/TcGlobals.fs @@ -885,6 +885,7 @@ type TcGlobals( let v_option_defaultValue_info = makeIntrinsicValRef(fslib_MFOptionModule_nleref, "defaultValue" , None , Some "DefaultValue" , [vara], ([[varaTy]; [mkOptionTy varaTy]], varaTy)) let v_nativeptr_tobyref_info = makeIntrinsicValRef(fslib_MFNativePtrModule_nleref, "toByRef" , None , Some "ToByRefInlined", [vara], ([[mkNativePtrTy varaTy]], mkByrefTy varaTy)) + let v_nativeptr_stackalloc_info = makeIntrinsicValRef(fslib_MFNativePtrModule_nleref, "stackalloc" , None , Some "StackAllocate", [vara], ([[v_int32_ty]], mkNativePtrTy varaTy)) let v_seq_collect_info = makeIntrinsicValRef(fslib_MFSeqModule_nleref, "collect" , None , Some "Collect", [vara;varb;varc], ([[varaTy --> varbTy]; [mkSeqTy varaTy]], mkSeqTy varcTy)) let v_seq_delay_info = makeIntrinsicValRef(fslib_MFSeqModule_nleref, "delay" , None , Some "Delay" , [varb], ([[v_unit_ty --> mkSeqTy varbTy]], mkSeqTy varbTy)) @@ -1757,6 +1758,7 @@ type TcGlobals( member val seq_singleton_vref = ValRefForIntrinsic v_seq_singleton_info member val seq_collect_vref = ValRefForIntrinsic v_seq_collect_info member val nativeptr_tobyref_vref = ValRefForIntrinsic v_nativeptr_tobyref_info + member val nativeptr_stackalloc_vref = ValRefForIntrinsic v_nativeptr_stackalloc_info member val seq_using_vref = ValRefForIntrinsic v_seq_using_info member val seq_delay_vref = ValRefForIntrinsic v_seq_delay_info member val seq_append_vref = ValRefForIntrinsic v_seq_append_info diff --git a/src/Compiler/TypedTree/TcGlobals.fsi b/src/Compiler/TypedTree/TcGlobals.fsi index 8356b16ccfc..a8f1dc89357 100644 --- a/src/Compiler/TypedTree/TcGlobals.fsi +++ b/src/Compiler/TypedTree/TcGlobals.fsi @@ -791,6 +791,8 @@ type internal TcGlobals = member nativeptr_tobyref_vref: TypedTree.ValRef + member nativeptr_stackalloc_vref: TypedTree.ValRef + member new_decimal_info: IntrinsicValRef member new_format_info: IntrinsicValRef diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 9649b05a722..8ada5a8834d 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -132,6 +132,11 @@ The interface '{0}' cannot be used as a type argument because the static abstract member '{1}' does not have a most specific implementation in the interface. + + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + + The member or function '{0}' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. Člen nebo funkce „{0}“ má atribut „TailCallAttribute“, ale nepoužívá se koncovým (tail) rekurzivním způsobem. diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index 80c89f42d06..a1f16d17807 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -132,6 +132,11 @@ The interface '{0}' cannot be used as a type argument because the static abstract member '{1}' does not have a most specific implementation in the interface. + + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + + The member or function '{0}' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. Der Member oder die Funktion "{0}" weist das Attribut "TailCallAttribute" auf, wird jedoch nicht endrekursiv verwendet. diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index 3452a9f4074..afb4e5f07af 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -132,6 +132,11 @@ The interface '{0}' cannot be used as a type argument because the static abstract member '{1}' does not have a most specific implementation in the interface. + + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + + The member or function '{0}' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. El miembro o la función “{0}” tiene el atributo “TailCallAttribute”, pero no se usa de forma de recursión de cola. diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 1c029f7d57c..b7828962564 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -132,6 +132,11 @@ The interface '{0}' cannot be used as a type argument because the static abstract member '{1}' does not have a most specific implementation in the interface. + + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + + The member or function '{0}' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. Le membre ou la fonction « {0} » possède l'attribut « TailCallAttribute », mais n'est pas utilisé de manière récursive. diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index 8006c24faa0..94a7c0fbab6 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -132,6 +132,11 @@ The interface '{0}' cannot be used as a type argument because the static abstract member '{1}' does not have a most specific implementation in the interface. + + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + + The member or function '{0}' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. Il membro o la funzione "{0}" ha l'attributo "TailCallAttribute", ma non è in uso in modo ricorsivo finale. diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index 5dffc6c4d0a..2a8e7665298 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -132,6 +132,11 @@ The interface '{0}' cannot be used as a type argument because the static abstract member '{1}' does not have a most specific implementation in the interface. + + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + + The member or function '{0}' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. メンバーまたは関数 '{0}' には 'TailCallAttribute' 属性がありますが、末尾の再帰的な方法では使用されていません。 diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index 841f583de03..8677a6549f9 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -132,6 +132,11 @@ The interface '{0}' cannot be used as a type argument because the static abstract member '{1}' does not have a most specific implementation in the interface. + + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + + The member or function '{0}' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. 멤버 또는 함수 '{0}'에 'TailCallAttribute' 특성이 있지만 비상 재귀적인 방식으로 사용되고 있지 않습니다. diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 0696a3f904f..53c7cb6f5e9 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -132,6 +132,11 @@ The interface '{0}' cannot be used as a type argument because the static abstract member '{1}' does not have a most specific implementation in the interface. + + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + + The member or function '{0}' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. Składowa lub funkcja „{0}” ma atrybut „TailCallAttribute”, ale nie jest używana w sposób cykliczny końca. diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 92fc3e6c918..48a7a706712 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -132,6 +132,11 @@ The interface '{0}' cannot be used as a type argument because the static abstract member '{1}' does not have a most specific implementation in the interface. + + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + + The member or function '{0}' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. O membro ou a função "{0}" tem o atributo "TailCallAttribute", mas não está sendo usado de maneira recursiva em cauda. diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index e4fefadb0bf..6962d93e1ff 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -132,6 +132,11 @@ The interface '{0}' cannot be used as a type argument because the static abstract member '{1}' does not have a most specific implementation in the interface. + + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + + The member or function '{0}' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. Элемент или функция "{0}" содержит атрибут "TailCallAttribute", но не используется в рекурсивном хвостовом режиме. diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index fbcca933879..986fca815dc 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -132,6 +132,11 @@ The interface '{0}' cannot be used as a type argument because the static abstract member '{1}' does not have a most specific implementation in the interface. + + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + + The member or function '{0}' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. Üye veya '{0}' işlevi, 'TailCallAttribute' özniteliğine sahip ancak kuyruk özyinelemeli bir şekilde kullanılmıyor. diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index e38cf4f9d73..2f509732719 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -132,6 +132,11 @@ The interface '{0}' cannot be used as a type argument because the static abstract member '{1}' does not have a most specific implementation in the interface. + + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + + The member or function '{0}' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. 成员或函数“{0}”具有 "TailCallAttribute" 属性,但未以尾递归方式使用。 diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index cafc3b32a6a..836c32e0f30 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -132,6 +132,11 @@ The interface '{0}' cannot be used as a type argument because the static abstract member '{1}' does not have a most specific implementation in the interface. + + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + 'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded. + + The member or function '{0}' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way. 成員或函式 '{0}' 具有 'TailCallAttribute' 屬性,但未以尾遞迴方式使用。 diff --git a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs index f44304fad85..4cd241556f5 100644 --- a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs +++ b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs @@ -171,3 +171,85 @@ let call (s: Sink) = IL_000f: callvirt instance int32 Test/Sink::Put(native int) IL_0014: ret }""" ] + + // Regression tests for https://github.com/dotnet/fsharp/issues/20295 (Case 1): 'NativePtr.stackalloc' + // emits the 'localloc' IL instruction, which the JIT rejects inside an exception-handling region. + // Such code used to compile and then throw InvalidProgramException at method load; it must now be + // rejected at compile time with FS3916. + [] + [ NativePtr.stackalloc 1 |> ignore")>] + [ NativePtr.stackalloc 1 |> ignore")>] + [ 1 |> ignore")>] + [ (try () with _ -> NativePtr.stackalloc 1 |> ignore)")>] + let ``stackalloc in a handler is rejected`` (handler: string) = + $""" +module Test +open Microsoft.FSharp.NativeInterop +let f () = {handler} +""" + |> FSharp + |> withNoWarn 9 + |> compile + |> shouldFail + |> withErrorCode 3916 + + [] + let ``stackalloc in the try body is allowed`` () = + FSharp """ +module Test +open Microsoft.FSharp.NativeInterop +let f () = try NativePtr.stackalloc 1 |> ignore with _ -> () +""" + |> withNoWarn 9 + |> compile + |> shouldSucceed + + [] + let ``stackalloc in a lambda inside a handler is allowed`` () = + FSharp """ +module Test +open Microsoft.FSharp.NativeInterop +let f () = try () with _ -> (fun () -> NativePtr.stackalloc 1 |> ignore) () +""" + |> withNoWarn 9 + |> compile + |> shouldSucceed + + [] + let ``stackalloc in an object-expression method inside a handler is allowed`` () = + FSharp """ +module Test +open Microsoft.FSharp.NativeInterop +let f () = + try () + with _ -> + let d = { new System.IDisposable with member _.Dispose() = NativePtr.stackalloc 1 |> ignore } + d.Dispose() +""" + |> withNoWarn 9 + |> compile + |> shouldSucceed + + [] + let ``stackalloc outside any try compiles and runs`` () = + FSharp """ +module Test +open Microsoft.FSharp.NativeInterop +[] +let main _ = + NativePtr.stackalloc 1 |> ignore + printfn "ok" + 0 +""" + |> withNoWarn 9 + |> compileExeAndRun + |> shouldSucceed + + [] + let ``handler without stackalloc is unaffected`` () = + FSharp """ +module Test +let f () = try () with _ -> printfn "handled" +""" + |> compile + |> shouldSucceed From 455dee5b7fba43f4fc0331fd632bc6faeff37def Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 8 Sep 2026 12:48:52 +0200 Subject: [PATCH 2/8] Reject NativePtr.stackalloc in an inlinable lambda inside a handler (FS3916) An immediately-applied lambda in a 'try' handler is inlined into the handler's IL region by the optimizer, so its 'localloc' still lands inside the exception-handling region and throws InvalidProgramException under --optimize+. The CheckLambdas 'withinHandler = false' reset was based on the false premise that every lambda becomes a separate method; remove it so lambda bodies inside a handler are conservatively checked. Genuine method boundaries (object-expression/interface methods) keep their reset via CheckMethod and remain legal (verified by compileExeAndRun). Part of issue #20295 (Case 1). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/PostInferenceChecks.fs | 1 - .../Libraries/NativeInterop.fs | 22 +++++++++---------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Compiler/Checking/PostInferenceChecks.fs b/src/Compiler/Checking/PostInferenceChecks.fs index 80f41d00dc4..ab29dcc6670 100644 --- a/src/Compiler/Checking/PostInferenceChecks.fs +++ b/src/Compiler/Checking/PostInferenceChecks.fs @@ -1819,7 +1819,6 @@ and CheckLambdas isTop (memberVal: Val option) cenv env inlined valReprInfo alwa let restArgs = List.concat vsl let syntacticArgs = thisAndBase @ restArgs let env = BindArgVals env restArgs - let env = { env with withinHandler = false } match memInfo with | None -> () diff --git a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs index 4cd241556f5..a057135c7e5 100644 --- a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs +++ b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs @@ -181,6 +181,9 @@ let call (s: Sink) = [ NativePtr.stackalloc 1 |> ignore")>] [ 1 |> ignore")>] [ (try () with _ -> NativePtr.stackalloc 1 |> ignore)")>] + // An immediately-applied lambda in a handler is inlined into the handler's IL region by the + // optimizer, so its 'localloc' still lands inside the exception region and must be rejected. + [ (fun () -> NativePtr.stackalloc 1 |> ignore) ()")>] let ``stackalloc in a handler is rejected`` (handler: string) = $""" module Test @@ -199,17 +202,6 @@ let f () = {handler} module Test open Microsoft.FSharp.NativeInterop let f () = try NativePtr.stackalloc 1 |> ignore with _ -> () -""" - |> withNoWarn 9 - |> compile - |> shouldSucceed - - [] - let ``stackalloc in a lambda inside a handler is allowed`` () = - FSharp """ -module Test -open Microsoft.FSharp.NativeInterop -let f () = try () with _ -> (fun () -> NativePtr.stackalloc 1 |> ignore) () """ |> withNoWarn 9 |> compile @@ -225,10 +217,16 @@ let f () = with _ -> let d = { new System.IDisposable with member _.Dispose() = NativePtr.stackalloc 1 |> ignore } d.Dispose() +[] +let main _ = + f () + printfn "ok" + 0 """ |> withNoWarn 9 - |> compile + |> compileExeAndRun |> shouldSucceed + |> withStdOutContains "ok" [] let ``stackalloc outside any try compiles and runs`` () = From 59c362369133cd5a65782cf122aa85c67f1a11de Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 8 Sep 2026 14:17:53 +0200 Subject: [PATCH 3/8] Hoist localloc base-ctor args before uninitialized 'this' (issue #20295 Case 2) NativePtr.stackalloc used as a chained base-constructor argument emitted 'localloc' while an uninitialized 'this' was pending on the evaluation stack, which the JIT rejects (InvalidProgramException at method load). The uninitialized 'this' cannot be spilled to a local, so the existing spill mechanism could not clear the stack. When a base/self-ctor argument may emit 'localloc', evaluate the arguments into locals first (at a clean stack), then push 'this' and reload them. Left-to-right evaluation order is preserved and ordinary base ctors emit byte-identical IL. The fix is applied in both GenApp and GenILCall. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/CodeGen/IlxGen.fs | 97 +++++++++++++--- .../Libraries/NativeInterop.fs | 105 ++++++++++++++++++ 2 files changed, 186 insertions(+), 16 deletions(-) diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index ceeec0a69ec..6bc4f60a212 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -3166,6 +3166,21 @@ let ComputeDebugPointForBinding g bind = // Generate expressions //------------------------------------------------------------------------- +/// True if evaluating this expression may emit the 'localloc' IL instruction (e.g. NativePtr.stackalloc, +/// which the optimizer inlines to inline IL containing 'localloc' before IlxGen runs). +let exprMayLocalloc expr = + (false, expr) + ||> FoldExpr + { ExprFolder0 with + exprIntercept = + (fun _exprF noInterceptF z expr -> + z + || (match expr with + | Expr.Op(TOp.ILAsm(instrs, _), _, _, _) -> instrs |> List.contains I_localloc + | _ -> false) + || noInterceptF false expr) + } + let rec GenExpr cenv cgbuf eenv (expr: Expr) sequel = cenv.stackGuard.Guard(fun () -> @@ -4717,21 +4732,47 @@ and GenApp (cenv: cenv) cgbuf eenv (f, fty, tyargs, curriedArgs, m) sequel = else mspec.DeclaringType - if isSuperInit || isSelfInit then - CG.EmitInstr cgbuf (pop 0) (Push [ thisTy ]) mkLdarg0 - let pendingUninitializedThis = (isSuperInit || isSelfInit) && not valu - if pendingUninitializedThis then + let genArgs () = + if not cenv.g.generateWitnesses || witnessInfos.IsEmpty then + () // no witness args + else + let _ctyargs, mtyargs = List.splitAt ctps.Length tyargs + GenWitnessArgs cenv cgbuf eenv m mtps mtyargs + + GenUntupledArgsDiscardingLoneUnit cenv cgbuf eenv m vref.NumObjArgs curriedArgInfos nowArgs + + // An uninitialized 'this' cannot be spilled, so a 'localloc' emitted by a base/self-ctor + // argument while 'this' is pending on the stack yields invalid IL (InvalidProgramException at + // load). When that can happen, evaluate the args into locals first (at a clean stack), then + // push 'this' and reload them; left-to-right evaluation order is preserved and ordinary ctors + // are unaffected. + let hoistArgsBeforeThis = + pendingUninitializedThis && List.exists exprMayLocalloc nowArgs + + if hoistArgsBeforeThis then + let stackBefore = cgbuf.GetCurrentStack() + genArgs () + + let argTys = + let stackAfter = cgbuf.GetCurrentStack() + stackAfter |> List.truncate (stackAfter.Length - stackBefore.Length) + + let argLocals = [ for ty in argTys -> cgbuf.SpillToLocal(ty, false) ] + CG.EmitInstr cgbuf (pop 0) (Push [ thisTy ]) mkLdarg0 cgbuf.StartUninitializedThisOnStack() - if not cenv.g.generateWitnesses || witnessInfos.IsEmpty then - () // no witness args + for local in List.rev argLocals do + cgbuf.ReloadFromLocal local else - let _ctyargs, mtyargs = List.splitAt ctps.Length tyargs - GenWitnessArgs cenv cgbuf eenv m mtps mtyargs + if isSuperInit || isSelfInit then + CG.EmitInstr cgbuf (pop 0) (Push [ thisTy ]) mkLdarg0 + + if pendingUninitializedThis then + cgbuf.StartUninitializedThisOnStack() - GenUntupledArgsDiscardingLoneUnit cenv cgbuf eenv m vref.NumObjArgs curriedArgInfos nowArgs + genArgs () // Generate laterArgs (for effects) and save LocalScope "callstack" cgbuf (fun scopeMarks -> @@ -5809,17 +5850,41 @@ and GenILCall else ilMethSpec.DeclaringType - // Load the 'this' pointer to pass to the superclass constructor. This argument is not - // in the expression tree since it can't be treated like an ordinary value - if isSuperInit then - CG.EmitInstr cgbuf (pop 0) (Push [ thisTy ]) mkLdarg0 - + // An uninitialized 'this' cannot be spilled, so a 'localloc' emitted by a base-ctor argument while + // 'this' is pending on the stack produces invalid IL (InvalidProgramException at load). When that + // can happen, evaluate the args into locals first (at a clean stack), then push 'this' and reload + // them. Left-to-right evaluation order is preserved; ordinary base ctors are unaffected. let pendingUninitializedThis = isSuperInit && not valu - if pendingUninitializedThis then + let hoistArgsBeforeThis = + pendingUninitializedThis && List.exists exprMayLocalloc argExprs + + if hoistArgsBeforeThis then + let g = cenv.g + + let argLocals = + [ + for argExpr in argExprs -> + let ilTy = argExpr |> tyOfExpr g |> GenType cenv m eenv.tyenv + GenExpr cenv cgbuf eenv argExpr Continue + cgbuf.SpillToLocal(ilTy, false) + ] + + CG.EmitInstr cgbuf (pop 0) (Push [ thisTy ]) mkLdarg0 cgbuf.StartUninitializedThisOnStack() - GenExprs cenv cgbuf eenv argExprs + for local in argLocals do + cgbuf.ReloadFromLocal local + else + // Load the 'this' pointer to pass to the superclass constructor. This argument is not + // in the expression tree since it can't be treated like an ordinary value + if isSuperInit then + CG.EmitInstr cgbuf (pop 0) (Push [ thisTy ]) mkLdarg0 + + if pendingUninitializedThis then + cgbuf.StartUninitializedThisOnStack() + + GenExprs cenv cgbuf eenv argExprs let il = if newobj then diff --git a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs index a057135c7e5..323969a116d 100644 --- a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs +++ b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs @@ -251,3 +251,108 @@ let f () = try () with _ -> printfn "handled" """ |> compile |> shouldSucceed + + // Regression tests for https://github.com/dotnet/fsharp/issues/20295 (Case 2): a 'NativePtr.stackalloc' + // used as a chained base-constructor argument loads the uninitialized 'this' before evaluating the + // argument, so its 'localloc' ran with 'this' pending on the stack and could not be spilled - the + // emitted IL threw InvalidProgramException at load. The args are now hoisted into locals before 'this'. + [] + let ``stackalloc as a base-ctor argument`` () = + FSharp """ +module Test +open Microsoft.FSharp.NativeInterop +type A(p: nativeptr) = class end +type B() = inherit A(NativePtr.stackalloc 1) +[] +let main _ = + B() |> ignore + printfn "ok" + 0 +""" + |> withNoWarn 9 + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "ok" + + [] + let ``stackalloc as one of several base-ctor arguments`` () = + FSharp """ +module Test +open Microsoft.FSharp.NativeInterop +type A(n: int, p: nativeptr) = class end +type B() = inherit A(1, NativePtr.stackalloc 1) +[] +let main _ = + B() |> ignore + printfn "ok" + 0 +""" + |> withNoWarn 9 + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "ok" + + [] + let ``stackalloc as a generic base-ctor argument`` () = + FSharp """ +module Test +open Microsoft.FSharp.NativeInterop +type A<'T when 'T: unmanaged>(p: nativeptr<'T>) = class end +type B() = inherit A(NativePtr.stackalloc 1) +[] +let main _ = + B() |> ignore + printfn "ok" + 0 +""" + |> withNoWarn 9 + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "ok" + + // The hoist evaluates the base-ctor args left-to-right into locals before pushing 'this'; a + // side-effecting normal arg before the stackalloc arg must still run first. + [] + let ``stackalloc base-ctor argument preserves left-to-right order`` () = + FSharp """ +module Test +open Microsoft.FSharp.NativeInterop +let trace = System.Text.StringBuilder() +let step (name: string) x = trace.Append name |> ignore; x +type A(n: int, p: nativeptr) = class end +type B() = inherit A(step "a" 1, step "b" (NativePtr.stackalloc 1)) +[] +let main _ = + B() |> ignore + if string trace <> "ab" then failwithf "wrong order: %O" trace + printfn "ok" + 0 +""" + |> withNoWarn 9 + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "ok" + + // No-regression: an ordinary base ctor without a 'localloc' argument must not hoist - the arg is + // pushed directly onto 'this', with no extra local introduced by the hoist. + [] + let ``ordinary base-ctor argument is not hoisted`` () = + FSharp """ +module Test +type A(n: int) = class end +type B() = inherit A(1) +""" + |> compile + |> shouldSucceed + |> verifyILContains [ + """.method public specialname rtspecialname instance void .ctor() cil managed + { + + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldc.i4.1 + IL_0002: callvirt instance void Test/A::.ctor(int32) + IL_0007: ldarg.0 + IL_0008: pop + IL_0009: ret + }""" ] From 917fa7ba2e998e8a40f9433f6dcab460c99910d4 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 8 Sep 2026 15:12:11 +0200 Subject: [PATCH 4/8] Consolidate Case-2 base-ctor stackalloc run tests into one theory (#20295) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Libraries/NativeInterop.fs | 56 +++++-------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs index 323969a116d..0740d201750 100644 --- a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs +++ b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs @@ -256,55 +256,29 @@ let f () = try () with _ -> printfn "handled" // used as a chained base-constructor argument loads the uninitialized 'this' before evaluating the // argument, so its 'localloc' ran with 'this' pending on the stack and could not be spilled - the // emitted IL threw InvalidProgramException at load. The args are now hoisted into locals before 'this'. - [] - let ``stackalloc as a base-ctor argument`` () = - FSharp """ -module Test -open Microsoft.FSharp.NativeInterop -type A(p: nativeptr) = class end -type B() = inherit A(NativePtr.stackalloc 1) -[] -let main _ = - B() |> ignore - printfn "ok" - 0 -""" - |> withNoWarn 9 - |> compileExeAndRun - |> shouldSucceed - |> withStdOutContains "ok" - - [] - let ``stackalloc as one of several base-ctor arguments`` () = - FSharp """ -module Test -open Microsoft.FSharp.NativeInterop -type A(n: int, p: nativeptr) = class end -type B() = inherit A(1, NativePtr.stackalloc 1) -[] -let main _ = - B() |> ignore - printfn "ok" - 0 -""" - |> withNoWarn 9 - |> compileExeAndRun - |> shouldSucceed - |> withStdOutContains "ok" - - [] - let ``stackalloc as a generic base-ctor argument`` () = - FSharp """ + [] + // simple nativeptr base-ctor arg + [) = class end", + "type B() = inherit A(NativePtr.stackalloc 1)")>] + // stackalloc as one of several base-ctor args + [) = class end", + "type B() = inherit A(1, NativePtr.stackalloc 1)")>] + // generic base type instantiated concretely + [(p: nativeptr<'T>) = class end", + "type B() = inherit A(NativePtr.stackalloc 1)")>] + let ``stackalloc as a base-ctor argument compiles and runs`` (baseType: string) (derived: string) = + $""" module Test open Microsoft.FSharp.NativeInterop -type A<'T when 'T: unmanaged>(p: nativeptr<'T>) = class end -type B() = inherit A(NativePtr.stackalloc 1) +{baseType} +{derived} [] let main _ = B() |> ignore printfn "ok" 0 """ + |> FSharp |> withNoWarn 9 |> compileExeAndRun |> shouldSucceed From 8f53ed68065cfd35e8559f6d015913e702113d00 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 8 Sep 2026 15:30:37 +0200 Subject: [PATCH 5/8] Finalize #20295: release note for NativePtr.stackalloc base-ctor hoist Adds a Fixed release note for the Case-2 base-constructor argument hoist. Verified xlf/surface-area baselines are in sync and formatting is clean; ran full regression (NativeInterop 17/17, Byref 138/138, Language RegressionTests 29/29, SurfaceAreaTest green). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 4e20ee59fe9..439f1eb82c0 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,6 +1,7 @@ ### Fixed * Fix `NativePtr.stackalloc` used inside the `with` handler, filter or `finally` block of a `try` expression producing an assembly that throws `InvalidProgramException` at method load. The `localloc` IL instruction it emits is rejected by the runtime inside an exception-handling region, so this is now reported at compile time as error FS3916. ([Issue #20295](https://github.com/dotnet/fsharp/issues/20295)) +* Fix `NativePtr.stackalloc` used as a chained base-constructor argument producing an assembly that throws `InvalidProgramException` at method load; the base-constructor arguments are now hoisted into locals before the uninitialized `this` is pushed. ([Issue #20295](https://github.com/dotnet/fsharp/issues/20295)) * Fix `NativePtr.stackalloc` nested in a larger expression (e.g. a call argument or the right of an assignment) producing an assembly that throws `InvalidProgramException` at load. ([Issue #8083](https://github.com/dotnet/fsharp/issues/8083), [PR #20302](https://github.com/dotnet/fsharp/pull/20302)) * Fix internal error "Unexpected generalized type variables when compiling an active pattern" when an active pattern is used in a `let` binding whose right-hand side is a generic value, e.g. `let (T) = id`. Such a binding is now checked like the equivalent `match` and is not generalized. ([Issue #16856](https://github.com/dotnet/fsharp/issues/16856), [PR #20383](https://github.com/dotnet/fsharp/pull/20383)) * Fix Release-only (`--optimize+`) `System.InvalidProgramException` from `Seq.collect` / `yield!` over a value-type (struct) collection implementing `seq<'T>` (e.g. `ImmutableArray<_>`) when materialised with `List.ofSeq` / `Seq.toList` / `Seq.toArray` or a list/array comprehension. The collector lowering now boxes a struct sub-collection to `seq<'T>` before calling `AddMany`/`AddManyAndClose` (matching the coercion the type checker already inserts for `yield!`), and uses `unit` as the try/finally result type instead of the body type (removing a spurious `ldnull` store). ([Issue #20203](https://github.com/dotnet/fsharp/issues/20203)) From 6e87905b0fd0e01113d7b6d38143fd04536128b1 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 8 Sep 2026 17:43:00 +0200 Subject: [PATCH 6/8] Move NativePtr.stackalloc-in-handler check to codegen (FS3916, issue #20295 Case 1) The syntactic PostInferenceChecks detection ran before inlining and closure conversion, so it could not tell an escaping closure (whose 'localloc' lands in its own method and is legal) from an inlined/immediately-applied lambda or a 'let inline' wrapper (whose 'localloc' lands in the handler region and is illegal). This produced both a false positive (rejecting legal escaping closures) and a false negative ('let inline' wrapper compiled and threw InvalidProgramException at load). Detect 'localloc' emission inside a catch/filter/finally/fault region at codegen instead, via a new eenv.withinExnHandler flag set on the handler bodies in GenTryWith/GenTryFinally and reset at method/closure boundaries. By that point inlining and closure conversion have run, so the true exception-handling region is known: escaping closures stay legal, inlined localloc is rejected. The try body keeps 'localloc' legal per ECMA-335. Removes the now-unused withinHandler plumbing and the nativeptr_stackalloc_vref intrinsic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/PostInferenceChecks.fs | 16 ++------ src/Compiler/CodeGen/IlxGen.fs | 18 +++++++++ src/Compiler/TypedTree/TcGlobals.fs | 2 - src/Compiler/TypedTree/TcGlobals.fsi | 2 - .../Libraries/NativeInterop.fs | 39 ++++++++++++++++++- 5 files changed, 59 insertions(+), 18 deletions(-) diff --git a/src/Compiler/Checking/PostInferenceChecks.fs b/src/Compiler/Checking/PostInferenceChecks.fs index ab29dcc6670..1d5798320b4 100644 --- a/src/Compiler/Checking/PostInferenceChecks.fs +++ b/src/Compiler/Checking/PostInferenceChecks.fs @@ -100,9 +100,6 @@ type env = /// Are we expecting a resumable code block etc resumableCode: Resumable - - /// Are we inside the 'with' handler, filter, or 'finally' block of a 'try'? - withinHandler: bool } override _.ToString() = "" @@ -1251,11 +1248,6 @@ and CheckExpr (cenv: cenv) (env: env) origExpr (ctxt: PermitByRefExpr) : Limit = // Check an application | Expr.App (f, _fty, tyargs, argsl, m) -> - (match f with - | OptionalCoerce(Expr.Val (vref, _, _)) - when env.withinHandler && cenv.reportErrors && valRefEq g vref g.nativeptr_stackalloc_vref -> - errorR(Error(FSComp.SR.chkNativePtrStackallocInHandler(), m)) - | _ -> ()) CheckApplication cenv env expr (f, tyargs, argsl, m) ctxt | Expr.Lambda (_, _, _, argvs, _, m, bodyTy) -> @@ -1481,7 +1473,6 @@ and CheckMethod cenv env baseValOpt ty (TObjExprMethod(_, attribs, tps, vs, body { env with resumableCode = Resumable.ResumableExpr false } else { env with resumableCode = Resumable.None } - let env = { env with withinHandler = false } CheckAttribs cenv env attribs CheckNoReraise cenv None body CheckEscapes cenv true m (match baseValOpt with Some x -> x :: vs | None -> vs) body |> ignore @@ -1523,7 +1514,7 @@ and CheckExprOp cenv env (op, tyargs, args, m) ctxt expr = | TOp.TryFinally _, [_], [Expr.Lambda (_, _, _, [_], e1, _, _); Expr.Lambda (_, _, _, [_], e2, _, _)] -> CheckTypeInstNoInnerByrefs cenv env m tyargs // result of a try/finally can be a byref let limit = CheckExpr cenv env e1 ctxt // result of a try/finally can be a byref if in a position where the overall expression is can be a byref - CheckExprNoByrefs cenv { env with withinHandler = true } e2 + CheckExprNoByrefs cenv env e2 limit | TOp.IntegerForLoop _, _, [Expr.Lambda (_, _, _, [_], e1, _, _);Expr.Lambda (_, _, _, [_], e2, _, _);Expr.Lambda (_, _, _, [_], e3, _, _)] -> @@ -1534,7 +1525,7 @@ and CheckExprOp cenv env (op, tyargs, args, m) ctxt expr = CheckTypeInstNoInnerByrefs cenv env m tyargs // result of a try/catch can be a byref let limit1 = CheckExpr cenv env e1 ctxt // result of a try/catch can be a byref if in a position where the overall expression is can be a byref // [(* e2; -- don't check filter body - duplicates logic in 'catch' body *) e3] - let limit2 = CheckExpr cenv { env with withinHandler = true } e3 ctxt // result of a try/catch can be a byref if in a position where the overall expression is can be a byref + let limit2 = CheckExpr cenv env e3 ctxt // result of a try/catch can be a byref if in a position where the overall expression is can be a byref CombineTwoLimits limit1 limit2 | TOp.ILCall (_, _, _, _, _, _, _, ilMethRef, enclTypeInst, methInst, retTypes), _, _ -> @@ -2861,8 +2852,7 @@ let CheckImplFile (g, amap, reportErrors, infoReader, internalsVisibleToPaths, v external=false returnScope = 0 isInAppExpr = false - resumableCode = Resumable.None - withinHandler = false } + resumableCode = Resumable.None } CheckImplFileContents cenv env implFileTy implFileContents CheckAttribs cenv env extraAttribs diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index 6bc4f60a212..b38575123f7 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -1261,6 +1261,12 @@ and IlxGenEnv = /// Are we under the scope of a try, catch or finally? If so we can't tailcall. SEH = structured exception handling withinSEH: bool + /// Are we within the 'with'/filter/'finally'/fault handler region of a 'try' (but not merely its try body)? + /// The JIT rejects the 'localloc' IL instruction (emitted by NativePtr.stackalloc) inside such a region, so + /// emitting it here is reported as error FS3916. This is checked at codegen, after inlining and closure + /// conversion, so an escaping closure whose 'localloc' lives in its own method stays legal. + withinExnHandler: bool + /// Suppresses filter block emission inside finally/fault handlers (workaround for dotnet/runtime#112406). insideFinallyOrFaultHandler: bool @@ -3055,6 +3061,7 @@ let CodeGenThen (cenv: cenv) mgbuf (entryPointInfo, methodName, eenv, alreadyUse cgbuf { eenv with withinSEH = false + withinExnHandler = false insideFinallyOrFaultHandler = false liveLocals = IntMap.empty () innerVals = innerVals @@ -5219,6 +5226,7 @@ and GenTryWith cenv cgbuf eenv (e1, valForFilter: Val, filterExpr, valForHandler let eenvinner = { eenvinner with + withinExnHandler = true exitSequel = sequelOnBranches } // We emit the debug point for the 'with' keyword span on the start of the filter @@ -5289,6 +5297,7 @@ and GenTryWith cenv cgbuf eenv (e1, valForFilter: Val, filterExpr, valForHandler let eenvinner = { eenvinner with + withinExnHandler = true exitSequel = exitSequel } @@ -5334,6 +5343,7 @@ and GenTryFinally cenv cgbuf eenv (bodyExpr, handlerExpr, m, resTy, spTry, spFin let eenvHandler = { eenvinner with + withinExnHandler = true insideFinallyOrFaultHandler = true } @@ -5641,6 +5651,13 @@ and GenAsmCode cenv cgbuf eenv (il, tyargs, args, returnTys, m) sequel = && ilReturnTys |> List.forall (fun ty -> ty <> ILType.Void) -> + // The JIT rejects 'localloc' inside an exception-handling region, producing an + // InvalidProgramException at method load. By this point inlining and closure conversion have run, + // so eenv.withinExnHandler reflects the true handler region: an escaping closure carrying the + // 'localloc' into its own method has had the flag reset and stays legal. + if eenv.withinExnHandler then + errorR (Error(FSComp.SR.chkNativePtrStackallocInHandler (), m)) + CG.EmitLocallocCode cgbuf (fun () -> GenExprs cenv cgbuf eenv args CG.EmitInstrs cgbuf (pop args.Length) (Push ilReturnTys) ilAfterInst) @@ -13131,6 +13148,7 @@ let GetEmptyIlxGenEnv (g: TcGlobals) ccu = innerVals = [] sigToImplRemapInfo = [] (* "module remap info" *) withinSEH = false + withinExnHandler = false insideFinallyOrFaultHandler = false isInLoop = false initLocals = true diff --git a/src/Compiler/TypedTree/TcGlobals.fs b/src/Compiler/TypedTree/TcGlobals.fs index 210849e0067..24b2e645bfb 100644 --- a/src/Compiler/TypedTree/TcGlobals.fs +++ b/src/Compiler/TypedTree/TcGlobals.fs @@ -885,7 +885,6 @@ type TcGlobals( let v_option_defaultValue_info = makeIntrinsicValRef(fslib_MFOptionModule_nleref, "defaultValue" , None , Some "DefaultValue" , [vara], ([[varaTy]; [mkOptionTy varaTy]], varaTy)) let v_nativeptr_tobyref_info = makeIntrinsicValRef(fslib_MFNativePtrModule_nleref, "toByRef" , None , Some "ToByRefInlined", [vara], ([[mkNativePtrTy varaTy]], mkByrefTy varaTy)) - let v_nativeptr_stackalloc_info = makeIntrinsicValRef(fslib_MFNativePtrModule_nleref, "stackalloc" , None , Some "StackAllocate", [vara], ([[v_int32_ty]], mkNativePtrTy varaTy)) let v_seq_collect_info = makeIntrinsicValRef(fslib_MFSeqModule_nleref, "collect" , None , Some "Collect", [vara;varb;varc], ([[varaTy --> varbTy]; [mkSeqTy varaTy]], mkSeqTy varcTy)) let v_seq_delay_info = makeIntrinsicValRef(fslib_MFSeqModule_nleref, "delay" , None , Some "Delay" , [varb], ([[v_unit_ty --> mkSeqTy varbTy]], mkSeqTy varbTy)) @@ -1758,7 +1757,6 @@ type TcGlobals( member val seq_singleton_vref = ValRefForIntrinsic v_seq_singleton_info member val seq_collect_vref = ValRefForIntrinsic v_seq_collect_info member val nativeptr_tobyref_vref = ValRefForIntrinsic v_nativeptr_tobyref_info - member val nativeptr_stackalloc_vref = ValRefForIntrinsic v_nativeptr_stackalloc_info member val seq_using_vref = ValRefForIntrinsic v_seq_using_info member val seq_delay_vref = ValRefForIntrinsic v_seq_delay_info member val seq_append_vref = ValRefForIntrinsic v_seq_append_info diff --git a/src/Compiler/TypedTree/TcGlobals.fsi b/src/Compiler/TypedTree/TcGlobals.fsi index a8f1dc89357..8356b16ccfc 100644 --- a/src/Compiler/TypedTree/TcGlobals.fsi +++ b/src/Compiler/TypedTree/TcGlobals.fsi @@ -791,8 +791,6 @@ type internal TcGlobals = member nativeptr_tobyref_vref: TypedTree.ValRef - member nativeptr_stackalloc_vref: TypedTree.ValRef - member new_decimal_info: IntrinsicValRef member new_format_info: IntrinsicValRef diff --git a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs index 0740d201750..5b6e4e9641c 100644 --- a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs +++ b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs @@ -196,8 +196,45 @@ let f () = {handler} |> shouldFail |> withErrorCode 3916 + // A 'let inline' wrapper around 'stackalloc' is inlined into the handler's IL region, so its + // 'localloc' still lands inside the exception region and must be rejected. The pre-codegen syntactic + // check missed this because the wrapper hid the 'stackalloc' call behind an inlinable function. [] - let ``stackalloc in the try body is allowed`` () = + let ``stackalloc via an inline wrapper inside a handler is rejected`` () = + FSharp """ +module Test +open Microsoft.FSharp.NativeInterop +let inline alloc () = NativePtr.stackalloc 1 |> ignore +let f () = try () with _ -> alloc () +""" + |> withNoWarn 9 + |> compile + |> shouldFail + |> withErrorCode 3916 + + // An escaping closure defined in a handler is compiled to its own method, so its 'localloc' lives + // outside the exception region and is legal. Such code must not be rejected (regression guard against + // the pre-codegen syntactic check's false positive). + [] + let ``stackalloc in an escaping closure inside a handler is allowed`` () = + FSharp """ +module Test +open Microsoft.FSharp.NativeInterop +let f () = + try () + with _ -> + let g = fun () -> NativePtr.stackalloc 1 |> ignore + System.Action(g).Invoke() +[] +let main _ = + f () + printfn "ran-closure" + 0 +""" + |> withNoWarn 9 + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "ran-closure" FSharp """ module Test open Microsoft.FSharp.NativeInterop From f5245514db5882c1e9fc9e7b7a38cfa277f9468c Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 8 Sep 2026 18:18:18 +0200 Subject: [PATCH 7/8] Restore dropped 'stackalloc in the try body is allowed' test header (#20295) The prior commit's test edit stripped the [] let header, orphaning the try-body assertion inside the escaping-closure test and breaking the ComponentTests build with FS0020. Restore it as its own Fact. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Libraries/NativeInterop.fs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs index 5b6e4e9641c..df5f07573e5 100644 --- a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs +++ b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs @@ -235,6 +235,11 @@ let main _ = |> compileExeAndRun |> shouldSucceed |> withStdOutContains "ran-closure" + + // 'localloc' is legal in the protected 'try' body itself (only handler/filter/finally/fault + // regions reject it), so 'stackalloc' directly inside a 'try' must still compile. + [] + let ``stackalloc in the try body is allowed`` () = FSharp """ module Test open Microsoft.FSharp.NativeInterop From f0587a9f973453393aa162d5063f3a87dc50f1b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:08:56 +0000 Subject: [PATCH 8/8] Fix net472 stackalloc test action type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs index df5f07573e5..593d8ebcf08 100644 --- a/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs +++ b/tests/FSharp.Compiler.ComponentTests/Libraries/NativeInterop.fs @@ -224,7 +224,7 @@ let f () = try () with _ -> let g = fun () -> NativePtr.stackalloc 1 |> ignore - System.Action(g).Invoke() + System.Action(g).Invoke() [] let main _ = f ()