Skip to content

Reading a field of a struct that a quotation (or computation expression) captures fails with FS3155 #20376

Description

@marklam

(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

  1. Clone the repo.
  2. dotnet build Repro.slnx — fails with four FS3155 errors.
  3. In Repro/Repro.fs, uncomment the four // let v = v lines.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions