Skip to content

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

Merged
T-Gro merged 17 commits into
dotnet:mainfrom
edgarfgp:fix-16696-equals-dollar-interpolated-string
Jun 18, 2026
Merged

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

Conversation

@edgarfgp

@edgarfgpedgarfgp commented May 26, 2026

Copy link
Copy Markdown
Contributor

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

typeC()=member valName=""with get, set
leta= C(Name="123")// worksletb= C(Name=$"123")// error FS0035: '$' is not permitted as a character in operator namesletc= C(Name= $"123")// works (space before $)
letx=$"123"// error FS0035lety=$"hello {world}"// error FS0035

After

letb= C(Name=$"123")// works → "123"letx=$"hello {world}"// works → "hello world"lety=$"%d{n}"// works → typed holeletz={ Name=$"value"}// works in record creationletw={ r with Name=$"value"}// works in record copylett=$"""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 rewindsLexemeLength 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-internalLexBuffer.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):

letx=$@"abc"// verbatim interpolated ($@)lety=@$"abc"// verbatim interpolated (@$)letz=$$"""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

github-actionsBot commented May 26, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Warning

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

Change pathRelease notes pathDescription
src/Compilerdocs/release-notes/.FSharp.Compiler.Service/11.0.100.mdNo 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
edgarfgpforce-pushed the fix-16696-equals-dollar-interpolated-string branch from 03ecfa7 to 98d38a4CompareMay 26, 2026 20:57
@github-actionsgithub-actionsBot added the AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files label May 26, 2026

@T-GroT-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, minimal fix at exactly the right layer. The approach is sound:

Correctness

  • The 3-char rule '=' '$' '"' wins by longest-match over the 2-char =$ that the INFIX_COMPARE_OP rule would match (since " is not in op_char). This is the key insight that makes the fix safe.
  • The rewind via LexemeLength <- 1 + EndPos adjustment correctly lets the regular interpolated-string lexer take over at $", including triple-quoted forms ($"""...""" — first three chars =$" still trigger the rule, then next scan sees $""" and the existing triple-quote handling kicks in).
  • Negative cases (=, defining (=$), infix =$) all still route through checkExprOp and reject correctly.

API surface

  • LexemeLength setter is exposed in prim-lexing.fsi but LexBuffer<'Char> is ype internal — no public API impact.

CI: All 50 check runs green.

Tests: Excellent coverage — positive cases (let-bindings, named args, records, record-copy, holes, typed holes), negative cases (non-quote, operator definition, infix usage, verbatim out-of-scope), syntax-tree baselines, and the space-separated form unchanged.

One minor suggestion (non-blocking): consider adding a component test that compiles and runs =$"""triple {x}""" alongside the syntax-tree baseline. The baseline proves parsing but not codegen/execution for that form.

Nice work!

@github-project-automationgithub-project-automationBot moved this from New to In Progress in F# Compiler and ToolingMay 27, 2026
@T-GroT-Gro added the AI-reviewed PR reviewed by AI review council label May 27, 2026
@T-Gro
T-Gro self-requested a review May 27, 2026 13:37

@auduchinokauduchinok left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change feels a bit too ad-hoc. It workarounds a specific case arising due to non-idiomatic formatting of the code, i.e. due to the missing spaces around =.

@edgarfgp Since this PR changes what is allowed by the language, have you considered changing the rule that causes the problem in the first place instead of trying to workaround it?

The workaround is done by exposing more lexer internals and doing processing after a token is lexed. Modifying tokens like this is sometimes done in the compiler already, but for operators it's done in LexFilter. Have you compared your approach with the existing one?

The tests look as if they are AI-generated: there's a lot of extra code that hides the parts being tested, and the tests effectively duplicate each other, since they only test simple adjacent = and $, while not testing how different kinds of interpolations interoperate (e.g. normal string interpolations inside triple quote ones), or whether operators are lexed differently due to this change. I think we should deduplicate the syntax tests and add more cases highlighting the lexing.

Comment threadtests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs Outdated
Comment threadsrc/Compiler/lex.fsl
edgarfgpand others added 3 commits May 27, 2026 20:47
Address review notes that the tests were noisy and duplicative:
- collapse the four near-identical let/ctor/record/record-copy cases into
one positive test plus one named-argument/record context test (the lexer
fix is position-agnostic)
- drop the per-variable failwithf scaffolding for terse assertions
- add an explicit Theory proving operator lexing is unchanged ('$' in an
operator still FS0035: =$abc, (=$), a =$ b, <$>, <=$=>)
- use Fsx triple-quoted source throughout; the triple-quote interpolation
form stays covered by the SyntaxTree baseline
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@edgarfgp
edgarfgp requested a review from auduchinokJune 1, 2026 20:53
@edgarfgp

Copy link
Copy Markdown
ContributorAuthor

@auduchinok Are you satisfied with the fix rationale or you have a better way of fixing this ?.

@edgarfgp

Copy link
Copy Markdown
ContributorAuthor

@T-Gro This is ready!. Will raise follow up PRs with the remaining open questions scenarios

@github-actionsgithub-actionsBot added the ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen label Jun 2, 2026
@github-actions

This comment has been minimized.

@github-actionsgithub-actionsBot added the ⚠️ Affects-Bootstrap Tooling check: PR touches compiler bootstrap chain label Jun 5, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Bootstrap, Affects-Compiler-Output
Affects-Bootstrap: modifies lex.fsl (fslex input) and prim-lexing.fsi (lexer buffer API)
Affects-Compiler-Output: lexer change alters token stream for =$" sequences

Generated by PR Tooling Safety Check · opus46 5.8M ·

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-BootstrapTooling check: PR touches compiler bootstrap chain⚠️ Affects-Compiler-OutputTooling check: PR touches IL emission or codegenAI-reviewedPR reviewed by AI review councilAI-Tooling-Check-Scanned-CleanTooling check: diff analyzed, no interesting infrastructure files

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Property initialization doesn't work without space before interpolated string

3 participants

@edgarfgp@auduchinok@T-Gro