(even though the source contains no assignment and takes no address)
[<Struct>]
type Vertex = { Position : float; Normal : float }
let quoted (v : Vertex) = <@ v.Position + v.Normal @>
// error FS3155: A quotation may not involve an assignment to or taking the address
// of a captured local variable
Reading v.Position calls a property getter, which needs this by address, and v is a captured local — so the restriction applies to entirely read-only code. The only workaround is to rebind the value inside the quotation, which reads as a no-op.
This is not limited to explicit <@ @> quotations: it happens inside query { } too, and in any computation expression whose builder quotes its body. It bites hard with such libraries — a codebase of mine using FShade shaders carries nine of these rebindings, one for every shader body, because each shader takes its input as a struct and reads fields off it.
Repro steps
Repro repository: https://github.com/marklam/fs3155-struct-capture-repro
- Clone the repo.
dotnet build Repro.slnx — fails with four FS3155 errors.
- In
Repro/Repro.fs, uncomment the four // let v = v lines.
dotnet build Repro.slnx — succeeds. dotnet run --project Repro/Repro.fsproj then runs.
There is a single project and a single source file. Every let v = v in it is commented out, so the difference between broken and working is four lines that appear to do nothing.
Expected behavior
The code compiles. Nothing in the source assigns to the captured value or takes its address; it only reads fields.
The repro file also contains viaFunction, which compiles today:
let position (v : Vertex) = v.Position
let viaFunction (v : Vertex) = <@ position v @>
so the compiler is already willing to pass the captured struct by value. The manual let v = v is a source-level spelling of that same copy, placed inside the quotation instead of at the call.
Actual behavior
Four errors, all FS3155: A quotation may not involve an assignment to or taking the address of a captured local variable:
| Function |
Error at |
Case |
quoted |
Repro.fs(20,9) |
A bare <@ @> quotation. |
queried |
Repro.fs(30,30) |
query { } — a computation expression whose builder quotes its body. No explicit <@ @>, and no third-party library, is needed to hit this. |
localStruct |
Repro.fs(40,9) |
A struct local in the enclosing scope, rather than a parameter. |
hoisted |
Repro.fs(50,9) |
let v = v placed just before the quotation — see below. |
For what it is worth, the diagnostic text is itself part of the problem: it describes an assignment or address-of that appears nowhere in the user's source, so it gives no hint of what to change.
For contrast, the same file contains three cases that compile as it stands, which locate the boundary:
| Function |
Case |
referenceType |
Reference-type record, field read — no address needed. |
wholeStruct |
Struct captured whole, <@ v @> — only the field read needs an address. |
viaFunction |
Struct field read through a function — passed by value. |
So the failure is specific to a field or property read on a captured struct local.
Known workarounds
Rebind the value inside the quotation:
let quoted (v : Vertex) =
<@
let v = v
v.Position + v.Normal
@>
Two caveats:
- It has to be inside. Hoisting it to just before the quotation does not help, because the copy is captured in turn — that is the
hoisted case above.
- It is not erased. With the rebindings uncommented,
dotnet run --project Repro/Repro.fsproj prints the quotation, and the copy survives as an extra Let node that every consumer of the quotation then has to see through:
Let (v, ValueWithName ({ Position = 1.0
Normal = 2.0 }, v),
Call (None, op_Addition,
[PropertyGet (Some (v), Position, []),
PropertyGet (Some (v), Normal, [])]))
Related information
- Operating system: Windows 11 Pro 24H2 (10.0.26200)
- .NET Runtime kind: .NET 10, SDK 10.0.400, F# 10. Reproduces identically under SDK 9.0.317 targeting net9.0 and SDK 8.0.424 targeting net8.0, so this is long-standing behaviour rather than a regression.
- Editing Tools: Visual Studio Enterprise 2026 (18.9.1). The errors above are from
dotnet build at the command line, so this is not an IDE artifact.
(even though the source contains no assignment and takes no address)
Reading
v.Positioncalls a property getter, which needsthisby address, andvis a captured local — so the restriction applies to entirely read-only code. The only workaround is to rebind the value inside the quotation, which reads as a no-op.This is not limited to explicit
<@ @>quotations: it happens insidequery { }too, and in any computation expression whose builder quotes its body. It bites hard with such libraries — a codebase of mine using FShade shaders carries nine of these rebindings, one for every shader body, because each shader takes its input as a struct and reads fields off it.Repro steps
Repro repository: https://github.com/marklam/fs3155-struct-capture-repro
dotnet build Repro.slnx— fails with four FS3155 errors.Repro/Repro.fs, uncomment the four// let v = vlines.dotnet build Repro.slnx— succeeds.dotnet run --project Repro/Repro.fsprojthen runs.There is a single project and a single source file. Every
let v = vin it is commented out, so the difference between broken and working is four lines that appear to do nothing.Expected behavior
The code compiles. Nothing in the source assigns to the captured value or takes its address; it only reads fields.
The repro file also contains
viaFunction, which compiles today:so the compiler is already willing to pass the captured struct by value. The manual
let v = vis a source-level spelling of that same copy, placed inside the quotation instead of at the call.Actual behavior
Four errors, all
FS3155: A quotation may not involve an assignment to or taking the address of a captured local variable:quotedRepro.fs(20,9)<@ @>quotation.queriedRepro.fs(30,30)query { }— a computation expression whose builder quotes its body. No explicit<@ @>, and no third-party library, is needed to hit this.localStructRepro.fs(40,9)hoistedRepro.fs(50,9)let v = vplaced just before the quotation — see below.For what it is worth, the diagnostic text is itself part of the problem: it describes an assignment or address-of that appears nowhere in the user's source, so it gives no hint of what to change.
For contrast, the same file contains three cases that compile as it stands, which locate the boundary:
referenceTypewholeStruct<@ v @>— only the field read needs an address.viaFunctionSo the failure is specific to a field or property read on a captured struct local.
Known workarounds
Rebind the value inside the quotation:
Two caveats:
hoistedcase above.dotnet run --project Repro/Repro.fsprojprints the quotation, and the copy survives as an extraLetnode that every consumer of the quotation then has to see through:Related information
dotnet buildat the command line, so this is not an IDE artifact.