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 5d0a107d4f1..8c67fbfe06e 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -2,6 +2,7 @@ * 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..8a871b57208 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs @@ -361,3 +361,107 @@ let s = $"{f.Invoke(42)}" """ |> compileExeAndRun |> shouldSucceed + + [] + let ``Issue 16696 - let-binding with =$"..." (no space)`` () = + Fsx """ +let n = 42 +let world = "world" +let plain =$"123" +let withHole =$"hello {world}" +let withTypedHole =$"%d{n}" +if plain <> "123" then failwithf "plain: expected 123, got %s" plain +if withHole <> "hello world" then failwithf "withHole: expected 'hello world', got %s" withHole +if withTypedHole <> "42" then failwithf "withTypedHole: expected 42, got %s" withTypedHole + """ + |> compileExeAndRun + |> shouldSucceed + + [] + let ``Issue 16696 - property initialization in constructor call with Name=$"..."`` () = + Fsx """ +type C() = + member val Name = "" with get, set + +let n = 42 +let plain = C(Name=$"123") +let withHole = C(Name=$"items: %d{n}") + +if plain.Name <> "123" then failwithf "plain: expected 123, got %s" plain.Name +if withHole.Name <> "items: 42" then failwithf "withHole: expected 'items: 42', got %s" withHole.Name + """ + |> compileExeAndRun + |> shouldSucceed + + [] + let ``Issue 16696 - record creation with Field=$"..."`` () = + Fsx """ +type R = { Name: string } +let n = 42 +let r1 = { Name=$"abc" } +let r2 = { Name=$"%d{n}" } +if r1.Name <> "abc" then failwithf "r1: expected abc, got %s" r1.Name +if r2.Name <> "42" then failwithf "r2: expected 42, got %s" r2.Name + """ + |> compileExeAndRun + |> shouldSucceed + + [] + let ``Issue 16696 - record copy with Field=$"..."`` () = + Fsx """ +type R = { Name: string; Count: int } +let n = 42 +let r = { Name = ""; Count = 1 } +let r1 = { r with Name=$"abc" } +let r2 = { r with Name=$"%d{n}" } +if r1.Name <> "abc" then failwithf "r1: expected abc, got %s" r1.Name +if r2.Name <> "42" then failwithf "r2: expected 42, got %s" r2.Name + """ + |> compileExeAndRun + |> shouldSucceed + + [] + let ``Issue 16696 - =$ followed by non-quote still rejected as deprecated operator`` () = + Fsx """ +let x =$abc + """ + |> compile + |> shouldFail + |> withDiagnosticMessageMatches "is not permitted as a character in operator names" + + [] + let ``Issue 16696 - defining (=$) as a custom operator is still rejected`` () = + Fsx """ +let (=$) a b = a + b + """ + |> compile + |> shouldFail + |> withDiagnosticMessageMatches "is not permitted as a character in operator names" + + [] + let ``Issue 16696 - =$ used as infix operator is still rejected`` () = + Fsx """ +let f a b = a =$ b + """ + |> compile + |> shouldFail + |> withDiagnosticMessageMatches "is not permitted as a character in operator names" + + [] + let ``Issue 16696 - verbatim interpolation form is still rejected (out of scope)`` () = + Fsx """ +let x =$@"abc" + """ + |> compile + |> shouldFail + |> withDiagnosticMessageMatches "is not permitted as a character in operator names" + + [] + let ``Issue 16696 - = $"..." (with space) still parses unchanged`` () = + Fsx """ +let n = 42 +let x = $"%d{n}" +if x <> "42" then failwithf "expected 42, got %s" x + """ + |> compileExeAndRun + |> shouldSucceed 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 []))