diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index ff1c9aae2dd..93cf58dccdb 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -4,6 +4,7 @@ * Fix FS0421 "The address of the variable cannot be used at this point" incorrectly raised for the discard pattern `let _ = &expr` when `let x = &expr` compiles. ([Issue #18841](https://github.com/dotnet/fsharp/issues/18841), [PR #19811](https://github.com/dotnet/fsharp/pull/19811)) * Honor `--nowarn` and `--warnaserror` for warnings emitted during command-line option parsing ([Issue #19576](https://github.com/dotnet/fsharp/issues/19576), [PR #19776](https://github.com/dotnet/fsharp/pull/19776)) * Fix `[]` prefix attributes being silently dropped on class members, and fix false-positive `AllowMultiple=false` errors when `[]` and `[]` are applied to the same binding. ([Issue #17904](https://github.com/dotnet/fsharp/issues/17904), [Issue #19020](https://github.com/dotnet/fsharp/issues/19020), [PR #19738](https://github.com/dotnet/fsharp/pull/19738)) +* Fix `=` adjacent to an interpolated string (e.g. `C(Name=$"value")`) being lexed as the invalid operator `=$` instead of an assignment followed by an interpolated string. ([Issue #16696](https://github.com/dotnet/fsharp/issues/16696)) * Fix attributes on return type of unparenthesized tuple methods being silently dropped from IL. ([Issue #462](https://github.com/dotnet/fsharp/issues/462), [PR #19714](https://github.com/dotnet/fsharp/pull/19714)) * Fix false-positive nullness warning (FS3261) when pattern matching narrows nullness inside seq/list/array comprehensions. ([Issue #19644](https://github.com/dotnet/fsharp/issues/19644), [PR #19743](https://github.com/dotnet/fsharp/pull/19743)) * Fix internal error FS0073 "Undefined or unsolved type variable" in IlxGen when nested inline SRTP functions with multiple overloads leave unsolved typars in the non-witness codegen path. ([Issue #19709](https://github.com/dotnet/fsharp/issues/19709), [PR #19710](https://github.com/dotnet/fsharp/pull/19710)) diff --git a/src/Compiler/Facilities/prim-lexing.fsi b/src/Compiler/Facilities/prim-lexing.fsi index c95a97a8d42..bcb60fc4977 100644 --- a/src/Compiler/Facilities/prim-lexing.fsi +++ b/src/Compiler/Facilities/prim-lexing.fsi @@ -117,6 +117,11 @@ type internal LexBuffer<'Char> = /// The currently matched text as a Span, it is only valid until the lexer is advanced member LexemeView: System.ReadOnlySpan<'Char> + /// Length of the currently matched lexeme, in characters. Setting this to a value smaller than the + /// actual match effectively rewinds the scanner: the next token will start LexemeLength + /// characters into the previously-matched lexeme. Use with caution. + member LexemeLength: int with get, set + /// Get single character of matched string member LexemeChar: int -> 'Char diff --git a/src/Compiler/lex.fsl b/src/Compiler/lex.fsl index 2e72a9ab201..87c1b269ae4 100644 --- a/src/Compiler/lex.fsl +++ b/src/Compiler/lex.fsl @@ -975,8 +975,17 @@ rule token (args: LexArgs) (skip: bool) = parse | ignored_op_char* ('@'|'^') op_char* { checkExprOp lexbuf; INFIX_AT_HAT_OP(lexeme lexbuf) } + // For '=$"' (property/named-arg initialization with an interpolated string, e.g. C(Name=$"123")): + // match the 3 chars, but consume only the '=' and rewind so the next scan begins at '$"', + // letting the regular interpolated-string lexer process it (including any '{...}' holes). + // See https://github.com/dotnet/fsharp/issues/16696. + | '=' '$' '"' { + lexbuf.LexemeLength <- 1 + lexbuf.EndPos <- lexbuf.StartPos.ShiftColumnBy(1) + EQUALS } + | ignored_op_char* ('=' | "!=" | '<' | '$') op_char* { checkExprOp lexbuf; INFIX_COMPARE_OP(lexeme lexbuf) } - + | ignored_op_char* ('>') op_char* { checkExprGreaterColonOp lexbuf; INFIX_COMPARE_OP(lexeme lexbuf) } | ignored_op_char* ('&') op_char* { checkExprOp lexbuf; INFIX_AMP_OP(lexeme lexbuf) } diff --git a/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs index 61eae80d184..9894fa68126 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs @@ -361,3 +361,49 @@ let s = $"{f.Invoke(42)}" """ |> compileExeAndRun |> shouldSucceed + + // Issue 16696: '=' immediately followed (no space) by an interpolated-string opener was + // greedily lexed as the invalid operator '=$' instead of '=' + an interpolated string. + // The hole {n} proves an interpolated string (not a plain one) is what gets lexed. + [] + let ``Issue 16696 - '=' adjacent to an interpolated string binds it`` () = + Fsx """ +let n = 42 +let x =$"{n}" +if x <> "42" then failwith "expected 42" + """ + |> compileExeAndRun + |> shouldSucceed + + // (The triple-quote form '=$"""..."""' is covered by the SyntaxTree baseline + // SynExprInterpolatedStringAdjacentEqualsTripleQuote.fs; '=$"' is a prefix of '=$"""', so the + // same lexer rule handles it after the rewind.) + + // The reported cases from the issue: named-argument and record-field initialization. + [] + let ``Issue 16696 - '=' adjacent to an interpolated string in named-argument and record contexts`` () = + Fsx """ +type C() = member val Name = "" with get, set +type R = { Name: string } +let n = 42 +let c = C(Name=$"{n}") +let r = { Name=$"{n}" } +if c.Name <> "42" || r.Name <> "42" then failwith "expected 42" + """ + |> compileExeAndRun + |> shouldSucceed + + // Operator lexing is unchanged: a '$' anywhere in an operator is still reserved (FS0035). + // The only thing the fix changes is '=' directly before an interpolated-string opener; + // everything below still lexes as an operator exactly as before. + [] + [] // '=$' not before a quote + [] // defining (=$) + [] // '=$' used as infix + [) f x = f x")>] // '$' inside an operator + [) a b = a")>] // '$' in the middle of an operator + let ``Issue 16696 - operators containing '$' are still rejected (operator lexing unchanged)`` (code: string) = + Fsx code + |> compile + |> shouldFail + |> withDiagnosticMessageMatches "is not permitted as a character in operator names" diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEquals.fs b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEquals.fs new file mode 100644 index 00000000000..49741e61768 --- /dev/null +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEquals.fs @@ -0,0 +1 @@ +let x =$"123" diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEquals.fs.bsl b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEquals.fs.bsl new file mode 100644 index 00000000000..a745892bd42 --- /dev/null +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEquals.fs.bsl @@ -0,0 +1,24 @@ +ImplFile + (ParsedImplFileInput + ("/root/String/SynExprInterpolatedStringAdjacentEquals.fs", false, + QualifiedNameOfFile SynExprInterpolatedStringAdjacentEquals, [], + [SynModuleOrNamespace + ([SynExprInterpolatedStringAdjacentEquals], false, AnonModule, + [Let + (false, + [SynBinding + (None, Normal, false, false, [], + PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector), + SynValData + (None, SynValInfo ([], SynArgInfo ([], false, None)), None), + Named (SynIdent (x, None), false, None, (1,4--1,5)), None, + InterpolatedString + ([String ("123", (1,7--1,13))], Regular, (1,7--1,13)), + (1,4--1,5), Yes (1,0--1,13), { LeadingKeyword = Let (1,0--1,3) + InlineKeyword = None + EqualsRange = Some (1,6--1,7) })], + (1,0--1,13), { InKeyword = None })], PreXmlDocEmpty, [], None, + (1,0--2,0), { LeadingKeyword = None })], (true, true), + { ConditionalDirectives = [] + WarnDirectives = [] + CodeComments = [] }, set [])) diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsTripleQuote.fs b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsTripleQuote.fs new file mode 100644 index 00000000000..c2e5e9481f5 --- /dev/null +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsTripleQuote.fs @@ -0,0 +1 @@ +let x =$"""abc""" diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsTripleQuote.fs.bsl b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsTripleQuote.fs.bsl new file mode 100644 index 00000000000..064e6878fe7 --- /dev/null +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsTripleQuote.fs.bsl @@ -0,0 +1,26 @@ +ImplFile + (ParsedImplFileInput + ("/root/String/SynExprInterpolatedStringAdjacentEqualsTripleQuote.fs", + false, + QualifiedNameOfFile SynExprInterpolatedStringAdjacentEqualsTripleQuote, [], + [SynModuleOrNamespace + ([SynExprInterpolatedStringAdjacentEqualsTripleQuote], false, + AnonModule, + [Let + (false, + [SynBinding + (None, Normal, false, false, [], + PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector), + SynValData + (None, SynValInfo ([], SynArgInfo ([], false, None)), None), + Named (SynIdent (x, None), false, None, (1,4--1,5)), None, + InterpolatedString + ([String ("abc", (1,7--1,17))], TripleQuote, (1,7--1,17)), + (1,4--1,5), Yes (1,0--1,17), { LeadingKeyword = Let (1,0--1,3) + InlineKeyword = None + EqualsRange = Some (1,6--1,7) })], + (1,0--1,17), { InKeyword = None })], PreXmlDocEmpty, [], None, + (1,0--2,0), { LeadingKeyword = None })], (true, true), + { ConditionalDirectives = [] + WarnDirectives = [] + CodeComments = [] }, set [])) diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsWithHole.fs b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsWithHole.fs new file mode 100644 index 00000000000..9f41a3fda7f --- /dev/null +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsWithHole.fs @@ -0,0 +1,2 @@ +let n = 42 +let x =$"{n}" diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsWithHole.fs.bsl b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsWithHole.fs.bsl new file mode 100644 index 00000000000..bcf55431e8f --- /dev/null +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsWithHole.fs.bsl @@ -0,0 +1,38 @@ +ImplFile + (ParsedImplFileInput + ("/root/String/SynExprInterpolatedStringAdjacentEqualsWithHole.fs", false, + QualifiedNameOfFile SynExprInterpolatedStringAdjacentEqualsWithHole, [], + [SynModuleOrNamespace + ([SynExprInterpolatedStringAdjacentEqualsWithHole], false, AnonModule, + [Let + (false, + [SynBinding + (None, Normal, false, false, [], + PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector), + SynValData + (None, SynValInfo ([], SynArgInfo ([], false, None)), None), + Named (SynIdent (n, None), false, None, (1,4--1,5)), None, + Const (Int32 42, (1,8--1,10)), (1,4--1,5), Yes (1,0--1,10), + { LeadingKeyword = Let (1,0--1,3) + InlineKeyword = None + EqualsRange = Some (1,6--1,7) })], (1,0--1,10), + { InKeyword = None }); + Let + (false, + [SynBinding + (None, Normal, false, false, [], + PreXmlDoc ((2,0), FSharp.Compiler.Xml.XmlDocCollector), + SynValData + (None, SynValInfo ([], SynArgInfo ([], false, None)), None), + Named (SynIdent (x, None), false, None, (2,4--2,5)), None, + InterpolatedString + ([String ("", (2,7--2,10)); FillExpr (Ident n, None); + String ("", (2,11--2,13))], Regular, (2,7--2,13)), + (2,4--2,5), Yes (2,0--2,13), { LeadingKeyword = Let (2,0--2,3) + InlineKeyword = None + EqualsRange = Some (2,6--2,7) })], + (2,0--2,13), { InKeyword = None })], PreXmlDocEmpty, [], None, + (1,0--3,0), { LeadingKeyword = None })], (true, true), + { ConditionalDirectives = [] + WarnDirectives = [] + CodeComments = [] }, set []))