dotnet / dotnet/fsharp

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

Open
#20,376 0 comments 0 reactions 0 assignees View on GitHub
Bug Needs-Triage
Dominant language
F#
Stars
4.3k
Forks
876
Avg merge
4d 22h
Merged PRs (30d)
144

Description

(even though the source contains no assignment and takes no address)

```fsharp
[]
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](https://github.com/fshade/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:

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:

```fsharp
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:

```fsharp
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.

Contributor guide

Open the contributing guide

Research direction

Start by cloning the linked repro and running `dotnet build Repro.slnx`; inspect the four failing cases in `Repro/Repro.fs`, then compare them with `referenceType`, `wholeStruct`, and `viaFunction`. Trace the F# quotation capture and struct field-read handling that produces FS3155. Done means the repro builds without the four `let v = v` rebindings and the quotation does not require an unnecessary extra `Let` node.

Written by the indexing model from the issue text.

Assessment

Tech stack
fsharp
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.