Skip to content

Allow interpolated string adjacent to '=' (e.g. C(Name=$"value"))#19820

Open
edgarfgp wants to merge 2 commits into
dotnet:mainfrom
edgarfgp:fix-16696-equals-dollar-interpolated-string
Open

Allow interpolated string adjacent to '=' (e.g. C(Name=$"value"))#19820
edgarfgp wants to merge 2 commits into
dotnet:mainfrom
edgarfgp:fix-16696-equals-dollar-interpolated-string

Conversation

@edgarfgp
Copy link
Copy Markdown
Contributor

@edgarfgp edgarfgp commented May 26, 2026

Fixes #16696.

Problem

When an interpolated string immediately follows = with no space, the lexer greedily matches =$ as a single INFIX_COMPARE_OP token (since $ is an operator character). checkExprOp then rejects it with FS0035 ('$' is not permitted as a character in operator names), so property/named-argument initialization with an interpolated string fails to parse.

Before

type C() =
    member val Name = "" with get, set

let a = C(Name="123")     // works
let b = C(Name=$"123")    // error FS0035: '$' is not permitted as a character in operator names
let c = C(Name= $"123")   // works (space before $)
let x =$"123"             // error FS0035
let y =$"hello {world}"   // error FS0035

After

let b = C(Name=$"123")            // works → "123"
let x =$"hello {world}"           // works → "hello world"
let y =$"%d{n}"                   // works → typed hole
let z = { Name=$"value" }         // works in record creation
let w = { r with Name=$"value" }  // works in record copy
let t =$"""triple {x}"""          // works for triple-quoted

The produced AST is identical to the spaced form = $"...", so all downstream tooling sees the same tree.

Solution

A lexer-level rule for the exact 3-character sequence =$":

| '=' '$' '"' {
      lexbuf.LexemeLength <- 1
      lexbuf.EndPos <- lexbuf.StartPos.ShiftColumnBy(1)
      EQUALS }

It matches =$" (winning over the 2-char operator rule via longest-match), emits EQUALS, then rewinds LexemeLength to 1 so the next scan begins at $". The existing interpolated-string lexer then processes the rest — including interpolation holes and triple-quoted forms. Because the fix is in the lexer, it applies in every position where = $"..." is valid (let-bindings, named args, record creation/copy), not just one grammar rule.

The previously-internal LexBuffer.LexemeLength setter is exposed in prim-lexing.fsi (no change to the public API surface — the type is internal).

Scope

Limited to the $" opening sequence (single- and triple-quoted). Defining or using =$ as a custom operator remains rejected (FS0035), preserving the "reserved for future use" semantics.

Open question — should we also handle the remaining variants?

These forms still require a space and continue to error as before (regression tests assert this):

let x =$@"abc"        // verbatim interpolated   ($@)
let y =@$"abc"        // verbatim interpolated   (@$)
let z =$$"""abc"""    // extended interpolated   ($$)

They're out of scope for the reported issue (which only shows the $" form), and the workaround is a single space. They could be added with the same lexer technique (longer =$@", =@$", =$$""" rules with a larger rewind), at the cost of more lexer rules. Do we want to cover them in this PR, defer to a follow-up, or leave them as-is?

@github-actions
Copy link
Copy Markdown
Contributor

❗ Release notes required


✅ Found changes and release notes in following paths:

Warning

No PR link found in some release notes, please consider adding it.

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/11.0.100.md No current pull request URL (#19820) found, please consider adding it

The lexer greedily matched '=$' as a single INFIX_COMPARE_OP, so
'C(Name=$"123")' and 'let x =$"123"' failed with FS0035 ('$' not
permitted in operator names). Add a lexer rule that matches '=$"',
consumes only the '=', and rewinds so the next scan begins at '$"' —
letting the regular interpolated-string lexer handle the rest, including
interpolation holes and triple-quoted forms.

Fix is position-agnostic (works in let-bindings, named args, record
creation/copy). Scoped to '=$"'; the '$@', '@$' and '$$' verbatim/extended
forms still require a space, as before.
@edgarfgp edgarfgp force-pushed the fix-16696-equals-dollar-interpolated-string branch from 03ecfa7 to 98d38a4 Compare May 26, 2026 20:57
@github-actions github-actions Bot added the AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files label May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

Property initialization doesn't work without space before interpolated string

1 participant