From 0128c8a0b69c2871ceb08d9b0c6b0f24c356e97a Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sat, 10 Oct 2026 02:16:03 +0200 Subject: [PATCH 1/4] Allow interpolated strings in constant expressions An interpolated string whose holes are non-null constant strings without alignment or format specifiers now folds to its text where F# requires a constant: [] values (also in signatures), attribute arguments and type provider static arguments. It was rejected with FS0267, so a constant built from other constants needed '+'. The checker marks those three contexts in TcEnv and folds the string there. Everywhere else, quotations included, interpolated strings are still lowered to String.Concat. A hole that breaks a rule reports FS3925 at the hole; a hole that is not a constant reports FS0267 at the hole. Gated on the ConstantInterpolatedStrings preview feature. Language suggestion #1347 RFC FS-1352 Co-Authored-By: Claude Opus 5.5 --- .../.FSharp.Compiler.Service/11.0.200.md | 1 + docs/release-notes/.Language/preview.md | 2 + src/Compiler/Checking/CheckBasics.fs | 3 + src/Compiler/Checking/CheckBasics.fsi | 3 + src/Compiler/Checking/CheckDeclarations.fs | 1 + .../Checking/Expressions/CheckExpressions.fs | 40 ++++- src/Compiler/FSComp.txt | 2 + src/Compiler/Facilities/LanguageFeatures.fs | 3 + src/Compiler/Facilities/LanguageFeatures.fsi | 1 + src/Compiler/xlf/FSComp.txt.cs.xlf | 10 ++ src/Compiler/xlf/FSComp.txt.de.xlf | 10 ++ src/Compiler/xlf/FSComp.txt.es.xlf | 10 ++ src/Compiler/xlf/FSComp.txt.fr.xlf | 10 ++ src/Compiler/xlf/FSComp.txt.it.xlf | 10 ++ src/Compiler/xlf/FSComp.txt.ja.xlf | 10 ++ src/Compiler/xlf/FSComp.txt.ko.xlf | 10 ++ src/Compiler/xlf/FSComp.txt.pl.xlf | 10 ++ src/Compiler/xlf/FSComp.txt.pt-BR.xlf | 10 ++ src/Compiler/xlf/FSComp.txt.ru.xlf | 10 ++ src/Compiler/xlf/FSComp.txt.tr.xlf | 10 ++ src/Compiler/xlf/FSComp.txt.zh-Hans.xlf | 10 ++ src/Compiler/xlf/FSComp.txt.zh-Hant.xlf | 10 ++ .../FSharp.Compiler.ComponentTests.fsproj | 1 + .../ConstantInterpolatedStringsTests.fs | 138 ++++++++++++++++++ 24 files changed, 322 insertions(+), 3 deletions(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/Language/ConstantInterpolatedStringsTests.fs diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 99464a277dd..b7b05785662 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -1,6 +1,7 @@ ### Added * F# Interactive gains a JSON-RPC server mode, `--fsi-server-jsonrpc:`, in which a host submits interactions over a named pipe and receives structured results — diagnostics with positions, escaping exceptions, the values each interaction bound, and the session's own process id — instead of recovering them by looking for a `SERVER-PROMPT>` marker in the output text. Program output continues to flow through the redirected console streams. The pipe admits only the user running the session; `--fsi-server-client-pid:` names the host process whose exit ends the session. `FsiEvaluationSession` exposes both options as `JsonRpcServerPipeName` and `JsonRpcClientProcessId`. The mode is part of the .NET fsi only. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396)) +* Added the `ConstantInterpolatedStrings` language feature (`--langversion:preview`): an interpolated string whose holes are non-null constant strings without alignment or format specifiers is a constant expression, so `[]` values, attribute arguments and type provider static arguments accept it instead of reporting FS0267. ([Suggestion #1347](https://github.com/fsharp/fslang-suggestions/issues/1347), [RFC FS-1352](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1352-interpolated-strings-in-constant-expressions.md)) ### Fixed diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md index d90fb95fbe7..2b4a55e6b87 100644 --- a/docs/release-notes/.Language/preview.md +++ b/docs/release-notes/.Language/preview.md @@ -4,6 +4,8 @@ Features assigned to F# 11.2 are listed in [11.2](https://fsharp.github.io/fshar ### Added +* Allow interpolated strings in constant expressions: when each hole is a non-null constant string without alignment or format specifiers, as in C#, an interpolated string can define a `[]` value, an attribute argument or a type provider static argument, such as `[]` (`ConstantInterpolatedStrings` preview feature). Outside these contexts interpolated strings, and their quotations, are unchanged. ([Suggestion #1347](https://github.com/fsharp/fslang-suggestions/issues/1347), [RFC FS-1352](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1352-interpolated-strings-in-constant-expressions.md)) + ### Fixed ### Changed diff --git a/src/Compiler/Checking/CheckBasics.fs b/src/Compiler/Checking/CheckBasics.fs index b4185b07b01..2a3ebd0bf81 100644 --- a/src/Compiler/Checking/CheckBasics.fs +++ b/src/Compiler/Checking/CheckBasics.fs @@ -247,6 +247,9 @@ type TcEnv = eInNameOf: bool + /// Checking an expression that must evaluate to a constant: a literal, an attribute argument or a static argument. + eInConstantContext: bool + /// Are we checking the body of an object expression? Such a body has family access to the /// implemented type, but its closures are not nested under that type, so they cannot keep it (#5302). eInObjectExpr: bool diff --git a/src/Compiler/Checking/CheckBasics.fsi b/src/Compiler/Checking/CheckBasics.fsi index 6709b8d92d6..61890cfbfaf 100644 --- a/src/Compiler/Checking/CheckBasics.fsi +++ b/src/Compiler/Checking/CheckBasics.fsi @@ -137,6 +137,9 @@ type TcEnv = /// Resolving a nameof operand, rather than an executable expression. eInNameOf: bool + /// Checking an expression that must evaluate to a constant: a literal, an attribute argument or a static argument. + eInConstantContext: bool + /// Are we checking the body of an object expression? Such a body has family access to the /// implemented type, but its closures are not nested under that type, so they cannot keep it (#5302). eInObjectExpr: bool diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index 05ce83c5f6b..e96aa0a662b 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -6097,6 +6097,7 @@ let emptyTcEnv g = eIsIndexerSetter = false eIsControlFlow = false eInNameOf = false + eInConstantContext = false eInObjectExpr = false eCaughtExceptionVal = ValueNone eCachedImplicitYieldExpressions = HashMultiMap(HashIdentity.Structural, useConcurrentDictionary = true) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index ceb7a4da0aa..a56afd623b5 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -5048,7 +5048,7 @@ and TcStaticConstantParameter (cenv: cenv) (env: TcEnv) tpenv kind (StripParenTy | SynType.StaticConstantExpr(e, _ ) -> // If an error occurs, don't try to recover, since the constant expression will be nothing like what we need - let te, tpenv' = TcExprNoRecover cenv (MustEqual kind) env tpenv e + let te, tpenv' = TcExprNoRecover cenv (MustEqual kind) { env with eInConstantContext = true } tpenv e // Evaluate the constant expression using static attribute argument rules let te = EvalLiteralExprOrAttribArg g te @@ -7802,6 +7802,38 @@ and TcInterpolatedStringViaConcat (cenv: cenv, overallTy: OverallTy, env: TcEnv, TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> resultExpr, tpenv) +/// Fold a string-typed interpolated string in a constant context to its text (RFC FS-1352). Each hole must be +/// a non-null constant string without alignment or format specifiers. +and TcConstantInterpolatedString (cenv: cenv, overallTy: OverallTy, env: TcEnv, m: range, tpenv: UnscopedTyparEnv, parts: SynInterpolatedStringPart list) = + let g = cenv.g + checkLanguageFeatureAndRecover g.langVersion LanguageFeature.ConstantInterpolatedStrings m + + let holeText (fill: Expr) (formatting: SynInterpolationFormatting) (mHole: range) = + let invalidHole () = + errorR (Error(FSComp.SR.tcConstantInterpolatedStringHole (), mHole)) + "" + + match formatting with + | SynInterpolationFormatting.DotNet(None, None) when isStringTy g (tyOfExpr g fill) -> + match EvalLiteralExprOrAttribArg g fill with + | Expr.Const(Const.String text, _, _) -> text + | Expr.Const(Const.Zero, _, _) -> invalidHole () + | _ -> "" // EvalLiteralExprOrAttribArg has reported that the hole is not a constant + | _ -> invalidHole () + + let text = Text.StringBuilder() + let mutable tpenvAcc = tpenv + + for part in parts do + match part with + | SynInterpolatedStringPart.String(s, _) -> text.Append(s.Replace("%%", "%")) |> ignore + | SynInterpolatedStringPart.FillExpr(synFill, formatting) -> + let fill, tpenvAfter = TcExprFlex2 cenv (NewInferenceType g) env false tpenvAcc synFill + tpenvAcc <- tpenvAfter + text.Append(holeText fill formatting synFill.Range) |> ignore + + TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> mkString g m (text.ToString()), tpenvAcc) + /// Check an interpolated string expression and [] warnForFunctionValuesInFillExprs (g: TcGlobals) argTys synFillExprs = match argTys, synFillExprs with @@ -7949,6 +7981,8 @@ and TcInterpolatedStringExpr cenv (overallTy: OverallTy) env m tpenv (parts: Syn else let str = mkString g m printfFormatString mkCallNewFormat g m printerTy printerArgTy printerResidueTy printerResultTy printerTupleTy str, tpenv + elif isString && env.eInConstantContext then + TcConstantInterpolatedString (cenv, overallTy, env, m, tpenv, parts) elif isString then // String-typed interpolation: lower to a reflection-free System.String.Concat of the parts, // type-checking each hole in place (no separate batch, no flat fill-expression list). @@ -11805,7 +11839,7 @@ and TcLiteral (cenv: cenv) overallTy env tpenv (attrs, synLiteralValExpr) = hasFlag valFlags WellKnownValAttributes.LiteralAttribute if hasLiteralAttr then - let literalValExpr, _ = TcExpr cenv (MustEqual overallTy) env tpenv synLiteralValExpr + let literalValExpr, _ = TcExpr cenv (MustEqual overallTy) { env with eInConstantContext = true } tpenv synLiteralValExpr match EvalLiteralExprOrAttribArg g literalValExpr with | Expr.Const (c, _, ty) -> if c = Const.Zero && isStructTy g ty then @@ -12011,7 +12045,7 @@ and TcAttributeEx canFail (cenv: cenv) (env: TcEnv) attrTgt attrEx (synAttr: Syn let meths = minfos |> List.map (fun minfo -> minfo, None) let afterResolution = ForNewConstructors cenv.tcSink env tyId.idRange methodName minfos let (expr, attributeAssignedNamedItems, _), _ = - TcMethodApplication true cenv env tpenv None [] mAttr mAttr methodName None ad PossiblyMutates false meths afterResolution NormalValUse [arg] (MustEqual ty) None [] + TcMethodApplication true cenv { env with eInConstantContext = true } tpenv None [] mAttr mAttr methodName None ad PossiblyMutates false meths afterResolution NormalValUse [arg] (MustEqual ty) None [] UnifyTypes cenv env mAttr ty (tyOfExpr g expr) diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index 1fa6ba59402..6d8d4948d00 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1805,3 +1805,5 @@ featureTypeArgumentDependencyOrdering,"ordering explicit type arguments by their 3923,tcMethodRequiresNamedArguments,"The method '%s' requires named arguments. Use named-argument syntax, e.g. 'MethodName(argumentName = value)'." 3924,chkNativePtrStackallocInHandler,"'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded." featureReraiseInComputationExpressions,"calls to 'reraise' in a computation expression handler" +3925,tcConstantInterpolatedStringHole,"This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers." +featureConstantInterpolatedStrings,"interpolated strings in constant expressions" diff --git a/src/Compiler/Facilities/LanguageFeatures.fs b/src/Compiler/Facilities/LanguageFeatures.fs index 0a34624112e..9ea5d646315 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fs +++ b/src/Compiler/Facilities/LanguageFeatures.fs @@ -67,6 +67,7 @@ type LanguageFeature = | ErrorOnBitwiseOpsOnNonIntegralEnums | OptimizeClosureIfNotInlined | ReraiseInComputationExpressions + | ConstantInterpolatedStrings /// LanguageVersion management type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) = @@ -172,6 +173,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) // F# preview // Unfinished features that still need work before they can be assigned a release language version. LanguageFeature.FromEndSlicing, previewVersion // Unfinished features --- needs work + LanguageFeature.ConstantInterpolatedStrings, previewVersion ] static let defaultLanguageVersion = LanguageVersion("default") @@ -318,6 +320,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) | LanguageFeature.ErrorOnBitwiseOpsOnNonIntegralEnums -> FSComp.SR.featureErrorOnBitwiseOpsOnNonIntegralEnums () | LanguageFeature.OptimizeClosureIfNotInlined -> FSComp.SR.featureOptimizeClosureIfNotInlined () | LanguageFeature.ReraiseInComputationExpressions -> FSComp.SR.featureReraiseInComputationExpressions () + | LanguageFeature.ConstantInterpolatedStrings -> FSComp.SR.featureConstantInterpolatedStrings () /// Get a version string associated with the given feature. static member GetFeatureVersionString feature = diff --git a/src/Compiler/Facilities/LanguageFeatures.fsi b/src/Compiler/Facilities/LanguageFeatures.fsi index 4afa992cde5..1871a6fcd78 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fsi +++ b/src/Compiler/Facilities/LanguageFeatures.fsi @@ -58,6 +58,7 @@ type LanguageFeature = | ErrorOnBitwiseOpsOnNonIntegralEnums | OptimizeClosureIfNotInlined | ReraiseInComputationExpressions + | ConstantInterpolatedStrings /// LanguageVersion management type LanguageVersion = diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 8bd83440050..ee9921bd40f 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -332,6 +332,11 @@ Vyvolá upozornění, pokud je atribut TailCall použit u nerekurzivních funkcí. + + interpolated strings in constant expressions + interpolated strings in constant expressions + + Deprecate places where 'seq' can be omitted Deprecate places where 'seq' can be omitted @@ -1332,6 +1337,11 @@ Atributy nejde použít pro rozšíření typů. + + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + + This copy-and-update record expression changes all fields of record type '{0}'. Consider using the record construction syntax instead. Tento výraz záznamu kopírování a aktualizace mění všechna pole typu záznamu '{0}'. Zvažte použití syntaxe konstrukce záznamu. diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index ab6af560121..87426c781c0 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -332,6 +332,11 @@ Löst Warnungen aus, wenn das Attribut "TailCall" für nicht rekursive Funktionen verwendet wird. + + interpolated strings in constant expressions + interpolated strings in constant expressions + + Deprecate places where 'seq' can be omitted Deprecate places where 'seq' can be omitted @@ -1332,6 +1337,11 @@ Attribute können nicht auf Typerweiterungen angewendet werden. + + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + + This copy-and-update record expression changes all fields of record type '{0}'. Consider using the record construction syntax instead. Dieser Ausdruck zum Kopieren und Aktualisieren von Datensätzen ändert alle Felder des Datensatztyps "{0}". Erwägen Sie stattdessen die Verwendung der Datensatzerstellungssyntax. diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index 3f7773686fd..2cdc61af89b 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -332,6 +332,11 @@ Genera advertencias si el atributo 'TailCall' se usa en funciones no recursivas. + + interpolated strings in constant expressions + interpolated strings in constant expressions + + Deprecate places where 'seq' can be omitted Deprecate places where 'seq' can be omitted @@ -1332,6 +1337,11 @@ Los atributos no se pueden aplicar a las extensiones de tipo. + + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + + This copy-and-update record expression changes all fields of record type '{0}'. Consider using the record construction syntax instead. Esta expresión de copia y actualización de registros cambia todos los campos de tipo de registro "{0}". Es preferible utilizar la sintaxis de construcción de registros. diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 7a3822ab851..1cc521a0eac 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -332,6 +332,11 @@ Émet des avertissements si l’attribut « TailCall » est utilisé sur des fonctions non récursives. + + interpolated strings in constant expressions + interpolated strings in constant expressions + + Deprecate places where 'seq' can be omitted Deprecate places where 'seq' can be omitted @@ -1332,6 +1337,11 @@ Impossible d'appliquer des attributs aux extensions de type. + + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + + This copy-and-update record expression changes all fields of record type '{0}'. Consider using the record construction syntax instead. Cette expression d'enregistrement de copie et de mise à jour modifie tous les champs du type d'enregistrement '{0}'. Envisagez d'utiliser la syntaxe de construction d'enregistrement à la place. diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index 21aa6a838be..2798611b21c 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -332,6 +332,11 @@ Genera avvisi se l'attributo 'TailCall' viene utilizzato in funzioni non ricorsive. + + interpolated strings in constant expressions + interpolated strings in constant expressions + + Deprecate places where 'seq' can be omitted Deprecate places where 'seq' can be omitted @@ -1332,6 +1337,11 @@ Gli attributi non possono essere applicati a estensioni di tipo. + + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + + This copy-and-update record expression changes all fields of record type '{0}'. Consider using the record construction syntax instead. Questa espressione di record di copia e aggiornamento modifica tutti i campi del tipo di record '{0}'. Provare a usare la sintassi di costruzione dei record. diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index c49b5490212..d8c38a0da15 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -332,6 +332,11 @@ 'TailCall' 属性が再帰関数以外で使用されている場合、警告が発せられます。 + + interpolated strings in constant expressions + interpolated strings in constant expressions + + Deprecate places where 'seq' can be omitted Deprecate places where 'seq' can be omitted @@ -1332,6 +1337,11 @@ 属性を型拡張に適用することはできません。 + + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + + This copy-and-update record expression changes all fields of record type '{0}'. Consider using the record construction syntax instead. この copy-and-update レコード式は、レコードの種類が '{0}' であるすべてのフィールドを変更します。代わりにレコード構築構文を使用することを検討してください。 diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index 11cebee14be..a1bb564fc4b 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -332,6 +332,11 @@ 'TailCall' 특성이 비 재귀 함수에 사용되는 경우 경고를 발생합니다. + + interpolated strings in constant expressions + interpolated strings in constant expressions + + Deprecate places where 'seq' can be omitted Deprecate places where 'seq' can be omitted @@ -1332,6 +1337,11 @@ 형식 확장에 특성을 적용할 수 없습니다. + + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + + This copy-and-update record expression changes all fields of record type '{0}'. Consider using the record construction syntax instead. 이 레코드 복사 및 업데이트 식은 '{0}' 레코드 형식의 모든 필드를 변경합니다. 레코드 생성 구문을 대신 사용하는 것이 좋습니다. diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 9aa0452706b..421952ee298 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -332,6 +332,11 @@ Zgłasza ostrzeżenia, jeśli atrybut „TailCall” jest używany w funkcjach niekursywnych. + + interpolated strings in constant expressions + interpolated strings in constant expressions + + Deprecate places where 'seq' can be omitted Deprecate places where 'seq' can be omitted @@ -1332,6 +1337,11 @@ Atrybutów nie można stosować do rozszerzeń typu. + + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + + This copy-and-update record expression changes all fields of record type '{0}'. Consider using the record construction syntax instead. To wyrażenie rekordu kopiowania i aktualizacji zmienia wszystkie pola typu rekordu „{0}”. Zamiast tego rozważ użycie składni konstrukcji rekordu. diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 388d56c9a0c..3feb16dd98a 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -332,6 +332,11 @@ Gera avisos se o atributo "TailCall" for usado em funções não recursivas. + + interpolated strings in constant expressions + interpolated strings in constant expressions + + Deprecate places where 'seq' can be omitted Deprecate places where 'seq' can be omitted @@ -1332,6 +1337,11 @@ Os atributos não podem ser aplicados às extensões de tipo. + + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + + This copy-and-update record expression changes all fields of record type '{0}'. Consider using the record construction syntax instead. Essa expressão de registro copiar e atualizar altera todos os campos do tipo de registro '{0}'. Considere usar a sintaxe de construção de registro em vez disso. diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index ef935e3d593..20a63a25f03 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -332,6 +332,11 @@ Выдает предупреждения, если атрибут TailCall используется в нерекурсивных функциях. + + interpolated strings in constant expressions + interpolated strings in constant expressions + + Deprecate places where 'seq' can be omitted Deprecate places where 'seq' can be omitted @@ -1332,6 +1337,11 @@ Атрибуты не могут быть применены к расширениям типа. + + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + + This copy-and-update record expression changes all fields of record type '{0}'. Consider using the record construction syntax instead. Это выражение записи копирования и обновления изменяет все поля типа записи "{0}". Вместо этого можно использовать синтаксис конструкции записи. diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index 1a917da78aa..d5935b6e619 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -332,6 +332,11 @@ 'TailCall' özniteliği özyinelemeli olmayan işlevlerde kullanılıyorsa uyarılar oluşturur. + + interpolated strings in constant expressions + interpolated strings in constant expressions + + Deprecate places where 'seq' can be omitted Deprecate places where 'seq' can be omitted @@ -1332,6 +1337,11 @@ Öznitelikler tür uzantılarına uygulanamaz. + + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + + This copy-and-update record expression changes all fields of record type '{0}'. Consider using the record construction syntax instead. Bu kopyalama ve güncelleştirme kayıt ifadesi, '{0}' kayıt türündeki tüm alanları değiştirir. Bunun yerine kayıt oluşturma söz dizimini kullanmayı deneyin. diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index f159ad4ee1b..2f978029940 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -332,6 +332,11 @@ 如果在非递归函数上使用“TailCall”属性,则引发警告。 + + interpolated strings in constant expressions + interpolated strings in constant expressions + + Deprecate places where 'seq' can be omitted Deprecate places where 'seq' can be omitted @@ -1332,6 +1337,11 @@ 属性不可应用于类型扩展。 + + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + + This copy-and-update record expression changes all fields of record type '{0}'. Consider using the record construction syntax instead. 此复制和更新记录表达式更改记录类型“{0}”的所有字段。请考虑改用记录构造语法。 diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index abb0041c10d..cea97903cc9 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -332,6 +332,11 @@ 如果 'TailCall' 屬性用於非遞迴函數,則引發警告。 + + interpolated strings in constant expressions + interpolated strings in constant expressions + + Deprecate places where 'seq' can be omitted Deprecate places where 'seq' can be omitted @@ -1332,6 +1337,11 @@ 屬性無法套用到類型延伸模組。 + + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + + This copy-and-update record expression changes all fields of record type '{0}'. Consider using the record construction syntax instead. 此複製和更新記錄運算式將變更記錄類型為 '{0}' 的所有欄位。請考慮改用記錄建構語法。 diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 2b473df5e1b..67ea5bfeb2b 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -386,6 +386,7 @@ + diff --git a/tests/FSharp.Compiler.ComponentTests/Language/ConstantInterpolatedStringsTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/ConstantInterpolatedStringsTests.fs new file mode 100644 index 00000000000..28cb258e1c9 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Language/ConstantInterpolatedStringsTests.fs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Language + +open Xunit +open FSharp.Test.Compiler + +// RFC FS-1352: interpolated strings in constant expressions. +module ConstantInterpolatedStringsTests = + + let private preview source = FSharp source |> withLangVersionPreview + + [] + let ``Literals and attribute arguments take the runtime text of a constant interpolated string`` () = + preview """ +module Program + +[] +let Root = "api/v2" + +[] +let Items = $"{Root}/items" + +[] +let ItemById = $"{Items}/{{id}}" + +[] +let CountFormat = $"{Items}: %%d" + +[] +let Verbatim = $@"\{nameof Root}" + $"{Root}" + +type TagAttribute(tags: string[]) = + inherit System.Attribute() + member _.Tags = tags + member val Name = "" with get, set + +[] +type Tagged() = class end + +let tag = typeof.GetCustomAttributes(typeof, false) |> Array.exactlyOne :?> TagAttribute + +let quotation = + match <@ $"{Root}/items" @> with + | Quotations.Patterns.Value _ -> "folded" + | _ -> "not folded" + +printfn "%s|%s|%s|%s" Items ItemById Verbatim (sprintf CountFormat 3) +printfn "%s|%s|%s" (String.concat "," tag.Tags) tag.Name quotation +""" + |> asExe + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains @"api/v2/items|api/v2/items/{id}|\Rootapi/v2|api/v2/items: 3" + |> withStdOutContains "api/v2/a,api/v2/b|api/v2!|not folded" + + [] + let ``Extended interpolated strings fold with their own delimiters`` () = + // [] let Json = $$"""{"{{Root}}": "100%"}""" + preview "module Program\n[]\nlet Root = \"api\"\n[]\nlet Json = $$\"\"\"{\"{{Root}}\": \"100%\"}\"\"\"\nprintfn \"%s\" Json" + |> asExe + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "{\"api\": \"100%\"}" + + [] + let ``The compiler reads an attribute argument that is a constant interpolated string`` () = + preview """ +module Program + +let getItemV2 (id: int) = id + +[] +let getItem id = getItemV2 id + +let item = getItem 1 +""" + |> typecheck + |> withSingleDiagnostic (Warning 44, Line 9, Col 12, Line 9, Col 19, "This construct is deprecated. Use getItemV2 instead") + + [] + let ``A signature literal can be a constant interpolated string`` () = + Fsi "module M\n[]\nval Root: string = \"api\"\n[]\nval Items: string = $\"{Root}/items\"" + |> withAdditionalSourceFile (FsSource "module M\n[]\nlet Root = \"api\"\n[]\nlet Items = $\"{Root}/items\"") + |> withLangVersionPreview + |> asLibrary + |> compile + |> shouldSucceed + + [] + let ``Each hole must be a non-null constant string without alignment or format specifiers`` () = + preview """ +module Program + +[] +let Root = "api/v2" +[] +let Version = 2 +[] +let NoValue: string = null +let dir = "data" + +[] +let A = $"v{Version}" +[] +let B = $"{Root,10}" +[] +let C = $"%s{Root}" +[] +let D = $"{dir}/x" +[] +let E = $"{NoValue}/x" +""" + |> typecheck + |> shouldFail + |> withDiagnostics [ + (Error 3925, Line 13, Col 13, Line 13, Col 20, "This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") + (Error 3925, Line 15, Col 12, Line 15, Col 16, "This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") + (Error 3925, Line 17, Col 14, Line 17, Col 18, "This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") + (Error 267, Line 19, Col 12, Line 19, Col 15, "This is not a valid constant expression or custom attribute value") + (Error 3925, Line 21, Col 12, Line 21, Col 19, "This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") + ] + + [] + let ``Constant interpolated strings need the preview language version`` () = + FSharp """ +module Program + +[] +let Root = "api/v2" + +[] +let Items = $"{Root}/items" +""" + |> withLangVersion "11.2" + |> typecheck + |> shouldFail + |> withSingleDiagnostic (Error 3350, Line 8, Col 13, Line 8, Col 28, "Feature 'interpolated strings in constant expressions' is not available in F# 11.2. Please use language version 'PREVIEW' or greater.") From 492059a662098068d4dd640fdca9e5d1b95194bb Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sat, 10 Oct 2026 09:10:26 +0200 Subject: [PATCH 2/4] Link the release notes to PR #20753 Co-Authored-By: Claude Opus 5.5 --- docs/release-notes/.FSharp.Compiler.Service/11.0.200.md | 2 +- docs/release-notes/.Language/preview.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index b7b05785662..a2367f7f902 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -1,7 +1,7 @@ ### Added * F# Interactive gains a JSON-RPC server mode, `--fsi-server-jsonrpc:`, in which a host submits interactions over a named pipe and receives structured results — diagnostics with positions, escaping exceptions, the values each interaction bound, and the session's own process id — instead of recovering them by looking for a `SERVER-PROMPT>` marker in the output text. Program output continues to flow through the redirected console streams. The pipe admits only the user running the session; `--fsi-server-client-pid:` names the host process whose exit ends the session. `FsiEvaluationSession` exposes both options as `JsonRpcServerPipeName` and `JsonRpcClientProcessId`. The mode is part of the .NET fsi only. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396)) -* Added the `ConstantInterpolatedStrings` language feature (`--langversion:preview`): an interpolated string whose holes are non-null constant strings without alignment or format specifiers is a constant expression, so `[]` values, attribute arguments and type provider static arguments accept it instead of reporting FS0267. ([Suggestion #1347](https://github.com/fsharp/fslang-suggestions/issues/1347), [RFC FS-1352](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1352-interpolated-strings-in-constant-expressions.md)) +* Added the `ConstantInterpolatedStrings` language feature (`--langversion:preview`): an interpolated string whose holes are non-null constant strings without alignment or format specifiers is a constant expression, so `[]` values, attribute arguments and type provider static arguments accept it instead of reporting FS0267. ([Suggestion #1347](https://github.com/fsharp/fslang-suggestions/issues/1347), [RFC FS-1352](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1352-interpolated-strings-in-constant-expressions.md), [PR #20753](https://github.com/dotnet/fsharp/pull/20753)) ### Fixed diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md index 2b4a55e6b87..58b62759c21 100644 --- a/docs/release-notes/.Language/preview.md +++ b/docs/release-notes/.Language/preview.md @@ -4,7 +4,7 @@ Features assigned to F# 11.2 are listed in [11.2](https://fsharp.github.io/fshar ### Added -* Allow interpolated strings in constant expressions: when each hole is a non-null constant string without alignment or format specifiers, as in C#, an interpolated string can define a `[]` value, an attribute argument or a type provider static argument, such as `[]` (`ConstantInterpolatedStrings` preview feature). Outside these contexts interpolated strings, and their quotations, are unchanged. ([Suggestion #1347](https://github.com/fsharp/fslang-suggestions/issues/1347), [RFC FS-1352](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1352-interpolated-strings-in-constant-expressions.md)) +* Allow interpolated strings in constant expressions: when each hole is a non-null constant string without alignment or format specifiers, as in C#, an interpolated string can define a `[]` value, an attribute argument or a type provider static argument, such as `[]` (`ConstantInterpolatedStrings` preview feature). Outside these contexts interpolated strings, and their quotations, are unchanged. ([Suggestion #1347](https://github.com/fsharp/fslang-suggestions/issues/1347), [RFC FS-1352](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1352-interpolated-strings-in-constant-expressions.md), [PR #20753](https://github.com/dotnet/fsharp/pull/20753)) ### Fixed From 5d28c0ac214639e008ac09093a0f42eea347e4b2 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 11 Oct 2026 02:15:43 +0200 Subject: [PATCH 3/4] Report a constant interpolated string's first invalid hole only RFC FS-1352 reports the rule error on the first hole that breaks a rule. Later holes are still type-checked, but are no longer evaluated as constants, so '$"{1}{2}"' gives FS3925 once instead of once per hole. Reword FS3925 to the RFC's "must be a constant": the message is shown when a hole keeps the string from being one. Cover type provider static arguments, after 'const' and through a literal, with a provider that returns its string static argument. Co-Authored-By: Claude Opus 5.5 --- .../Checking/Expressions/CheckExpressions.fs | 14 ++-- src/Compiler/FSComp.txt | 2 +- src/Compiler/xlf/FSComp.txt.cs.xlf | 4 +- src/Compiler/xlf/FSComp.txt.de.xlf | 4 +- src/Compiler/xlf/FSComp.txt.es.xlf | 4 +- src/Compiler/xlf/FSComp.txt.fr.xlf | 4 +- src/Compiler/xlf/FSComp.txt.it.xlf | 4 +- src/Compiler/xlf/FSComp.txt.ja.xlf | 4 +- src/Compiler/xlf/FSComp.txt.ko.xlf | 4 +- src/Compiler/xlf/FSComp.txt.pl.xlf | 4 +- src/Compiler/xlf/FSComp.txt.pt-BR.xlf | 4 +- src/Compiler/xlf/FSComp.txt.ru.xlf | 4 +- src/Compiler/xlf/FSComp.txt.tr.xlf | 4 +- src/Compiler/xlf/FSComp.txt.zh-Hans.xlf | 4 +- src/Compiler/xlf/FSComp.txt.zh-Hant.xlf | 4 +- .../ConstantInterpolatedStringsTests.fs | 70 +++++++++++++++++-- .../staticStringParameter/provider.fsx | 68 ++++++++++++++++++ 17 files changed, 171 insertions(+), 35 deletions(-) create mode 100644 tests/fsharp/typeProviders/staticStringParameter/provider.fsx diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index a56afd623b5..3dc88b72e71 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -7811,18 +7811,20 @@ and TcConstantInterpolatedString (cenv: cenv, overallTy: OverallTy, env: TcEnv, let holeText (fill: Expr) (formatting: SynInterpolationFormatting) (mHole: range) = let invalidHole () = errorR (Error(FSComp.SR.tcConstantInterpolatedStringHole (), mHole)) - "" + ValueNone match formatting with | SynInterpolationFormatting.DotNet(None, None) when isStringTy g (tyOfExpr g fill) -> match EvalLiteralExprOrAttribArg g fill with - | Expr.Const(Const.String text, _, _) -> text + | Expr.Const(Const.String text, _, _) -> ValueSome text | Expr.Const(Const.Zero, _, _) -> invalidHole () - | _ -> "" // EvalLiteralExprOrAttribArg has reported that the hole is not a constant + | _ -> ValueNone // EvalLiteralExprOrAttribArg has reported that the hole is not a constant | _ -> invalidHole () let text = Text.StringBuilder() let mutable tpenvAcc = tpenv + // Only the first hole that breaks a rule is reported; later holes are still type-checked. + let mutable holesAreConstant = true for part in parts do match part with @@ -7830,7 +7832,11 @@ and TcConstantInterpolatedString (cenv: cenv, overallTy: OverallTy, env: TcEnv, | SynInterpolatedStringPart.FillExpr(synFill, formatting) -> let fill, tpenvAfter = TcExprFlex2 cenv (NewInferenceType g) env false tpenvAcc synFill tpenvAcc <- tpenvAfter - text.Append(holeText fill formatting synFill.Range) |> ignore + + if holesAreConstant then + match holeText fill formatting synFill.Range with + | ValueSome hole -> text.Append(hole: string) |> ignore + | ValueNone -> holesAreConstant <- false TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> mkString g m (text.ToString()), tpenvAcc) diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index 6d8d4948d00..ccf660c5b74 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1805,5 +1805,5 @@ featureTypeArgumentDependencyOrdering,"ordering explicit type arguments by their 3923,tcMethodRequiresNamedArguments,"The method '%s' requires named arguments. Use named-argument syntax, e.g. 'MethodName(argumentName = value)'." 3924,chkNativePtrStackallocInHandler,"'NativePtr.stackalloc' cannot be used inside the 'with' handler, filter or 'finally' block of a 'try' expression. The 'localloc' IL instruction this emits is rejected inside an exception-handling region, producing a 'System.InvalidProgramException' when the method is loaded." featureReraiseInComputationExpressions,"calls to 'reraise' in a computation expression handler" -3925,tcConstantInterpolatedStringHole,"This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers." +3925,tcConstantInterpolatedStringHole,"This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers." featureConstantInterpolatedStrings,"interpolated strings in constant expressions" diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index ee9921bd40f..1e365d9a5ee 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -1338,8 +1338,8 @@ - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index 87426c781c0..4374208d0de 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -1338,8 +1338,8 @@ - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index 2cdc61af89b..cfcccd1f6cf 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -1338,8 +1338,8 @@ - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 1cc521a0eac..7d1d0ac12ba 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -1338,8 +1338,8 @@ - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index 2798611b21c..1569ac1b73e 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -1338,8 +1338,8 @@ - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index d8c38a0da15..06e76bd0178 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -1338,8 +1338,8 @@ - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index a1bb564fc4b..dce920fb63d 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -1338,8 +1338,8 @@ - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 421952ee298..256e18c8587 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -1338,8 +1338,8 @@ - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 3feb16dd98a..b75df2f5950 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -1338,8 +1338,8 @@ - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index 20a63a25f03..a7fdae126b5 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -1338,8 +1338,8 @@ - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index d5935b6e619..f0a2f2f3324 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -1338,8 +1338,8 @@ - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index 2f978029940..82fd4f4a3c0 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -1338,8 +1338,8 @@ - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index cea97903cc9..12d338f6611 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -1338,8 +1338,8 @@ - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. - This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. + This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers. diff --git a/tests/FSharp.Compiler.ComponentTests/Language/ConstantInterpolatedStringsTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/ConstantInterpolatedStringsTests.fs index 28cb258e1c9..6893652bde8 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/ConstantInterpolatedStringsTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/ConstantInterpolatedStringsTests.fs @@ -2,6 +2,7 @@ namespace Language +open System.IO open Xunit open FSharp.Test.Compiler @@ -87,6 +88,42 @@ let item = getItem 1 |> compile |> shouldSucceed + [] + let ``A type provider static argument can be a constant interpolated string`` () = + let providerPath = + Path.Combine(__SOURCE_DIRECTORY__, "../../fsharp/typeProviders/staticStringParameter/provider.fsx") + + let provider = + Fsx $"""#load @"{providerPath}" """ + |> withName "StaticStringParameterProvider" + |> ignoreWarnings + |> compile + |> shouldSucceed + + Fsx( + $"#r @\"{provider.OutputPath.Value}\"\n" + + """ +[] +let Root = "api/v2" + +let fromConst = Provided.Text.Value + +[] +let ItemById = $"{Root}/items/{{id}}" + +let fromLiteral = Provided.Text.Value + +if fromConst <> "api/v2/items" || fromLiteral <> "api/v2/items/{id}" then + failwith $"Unexpected static arguments: '{fromConst}', '{fromLiteral}'" +""" + ) +#if NETCOREAPP + |> withOptions [ "--usesdkrefs-" ] +#endif + |> withLangVersionPreview + |> eval + |> shouldSucceed + [] let ``Each hole must be a non-null constant string without alignment or format specifiers`` () = preview """ @@ -114,11 +151,36 @@ let E = $"{NoValue}/x" |> typecheck |> shouldFail |> withDiagnostics [ - (Error 3925, Line 13, Col 13, Line 13, Col 20, "This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") - (Error 3925, Line 15, Col 12, Line 15, Col 16, "This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") - (Error 3925, Line 17, Col 14, Line 17, Col 18, "This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") + (Error 3925, Line 13, Col 13, Line 13, Col 20, "This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") + (Error 3925, Line 15, Col 12, Line 15, Col 16, "This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") + (Error 3925, Line 17, Col 14, Line 17, Col 18, "This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") (Error 267, Line 19, Col 12, Line 19, Col 15, "This is not a valid constant expression or custom attribute value") - (Error 3925, Line 21, Col 12, Line 21, Col 19, "This interpolated string is a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") + (Error 3925, Line 21, Col 12, Line 21, Col 19, "This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") + ] + + [] + let ``Only the first hole that breaks a rule is reported`` () = + preview """ +module Program + +[] +let Version = 2 +let dir = "data" + +[] +let A = $"{Version}{Version}" +[] +let B = $"{dir}{Version}" +[] +let C = $"{Version}{missing}" +""" + |> typecheck + |> shouldFail + |> withDiagnostics [ + (Error 3925, Line 9, Col 12, Line 9, Col 19, "This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") + (Error 267, Line 11, Col 12, Line 11, Col 15, "This is not a valid constant expression or custom attribute value") + (Error 39, Line 13, Col 21, Line 13, Col 28, "The value or constructor 'missing' is not defined.") + (Error 3925, Line 13, Col 12, Line 13, Col 19, "This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") ] [] diff --git a/tests/fsharp/typeProviders/staticStringParameter/provider.fsx b/tests/fsharp/typeProviders/staticStringParameter/provider.fsx new file mode 100644 index 00000000000..d251cf8e73b --- /dev/null +++ b/tests/fsharp/typeProviders/staticStringParameter/provider.fsx @@ -0,0 +1,68 @@ +namespace StaticStringParameterProvider + +#load "../helloWorld/TypeMagic.fs" + +open System +open System.Collections.Generic +open System.Reflection +open Microsoft.FSharp.Core.CompilerServices +open Microsoft.FSharp.Quotations +open FSharp.TypeMagic + +/// Provides 'Provided.Text', whose static property 'Value' returns the static argument. +[] +type Provider() = + let modul = typeof.Module + let container = TypeContainer.Namespace(modul, "Provided") + let unappliedType = TypeBuilder.CreateType(container, "Text") + let appliedTypes = Dictionary(StringComparer.Ordinal) + + let invalidation = Event() + + interface IProvidedNamespace with + member _.NamespaceName = "Provided" + member _.GetTypes() = [| unappliedType |] + member _.GetNestedNamespaces() = [||] + member _.ResolveTypeName name = if name = unappliedType.Name then unappliedType else null + + interface ITypeProvider with + member this.GetNamespaces() = [| this |] + + member _.GetStaticParameters typeWithoutArguments = + if typeWithoutArguments.Name = unappliedType.Name then + [| TypeBuilder.CreateStaticParameter("Value", typeof, 0) |] + else + [||] + + member _.ApplyStaticArguments(_, typePathWithArguments, staticArguments) = + let name = typePathWithArguments[typePathWithArguments.Length - 1] + + lock appliedTypes (fun () -> + match appliedTypes.TryGetValue name with + | true, struct (appliedType, _) -> appliedType + | _ -> + let appliedType = + TypeBuilder.CreateType( + container, + name, + members = TypeBuilder.CacheMembers(fun declaringType -> + TypeBuilder.JoinPropertiesIntoMemberInfos + [ TypeBuilder.CreateSyntheticProperty(declaringType, "Value", typeof, isStatic = true) ])) + + appliedTypes[name] <- struct (appliedType, staticArguments[0] :?> string) + appliedType) + + member _.GetInvokerExpression(getter, _) = + let struct (_, value) = lock appliedTypes (fun () -> appliedTypes[getter.DeclaringType.Name]) + Expr.Value value + + member _.GetGeneratedAssemblyContents _ = failwith "Only erased types are provided." + + [] + member _.Invalidate = invalidation.Publish + + interface IDisposable with + member _.Dispose() = () + +[] +do () From 4e4506c715a4e6a883ad92b9a8fa46ba5f84c821 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Thu, 15 Oct 2026 02:11:48 +0100 Subject: [PATCH 4/4] Compact constant interpolated strings and run them only when supported Review follow-up. The hole text comes from a top-level tryConstantHoleText, every hole is type-checked before the first one is evaluated (so a later undefined name is still reported), and the constant path runs only when the ConstantInterpolatedStrings feature is supported: before the preview language version an interpolated string in a literal is checked as before, so a branch that is never evaluated, such as false && ($"{1}" = "1"), stays valid. The type provider test uses a small fixture that checks the argument it receives: the helloWorld provider overflows the stack of the test host when it is compiled in process. One language release note remains. Co-Authored-By: Claude Sonnet 5.5 --- .../.FSharp.Compiler.Service/11.0.200.md | 1 - docs/release-notes/.Language/preview.md | 2 +- src/Compiler/Checking/CheckBasics.fs | 1 - src/Compiler/Checking/CheckBasics.fsi | 1 - .../Checking/Expressions/CheckExpressions.fs | 63 +++++------ .../ConstantInterpolatedStringsTests.fs | 105 ++++++------------ .../staticStringParameter/provider.fsx | 68 ------------ .../staticStringValue/provider.fsx | 41 +++++++ 8 files changed, 106 insertions(+), 176 deletions(-) delete mode 100644 tests/fsharp/typeProviders/staticStringParameter/provider.fsx create mode 100644 tests/fsharp/typeProviders/staticStringValue/provider.fsx diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index a2367f7f902..99464a277dd 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -1,7 +1,6 @@ ### Added * F# Interactive gains a JSON-RPC server mode, `--fsi-server-jsonrpc:`, in which a host submits interactions over a named pipe and receives structured results — diagnostics with positions, escaping exceptions, the values each interaction bound, and the session's own process id — instead of recovering them by looking for a `SERVER-PROMPT>` marker in the output text. Program output continues to flow through the redirected console streams. The pipe admits only the user running the session; `--fsi-server-client-pid:` names the host process whose exit ends the session. `FsiEvaluationSession` exposes both options as `JsonRpcServerPipeName` and `JsonRpcClientProcessId`. The mode is part of the .NET fsi only. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396)) -* Added the `ConstantInterpolatedStrings` language feature (`--langversion:preview`): an interpolated string whose holes are non-null constant strings without alignment or format specifiers is a constant expression, so `[]` values, attribute arguments and type provider static arguments accept it instead of reporting FS0267. ([Suggestion #1347](https://github.com/fsharp/fslang-suggestions/issues/1347), [RFC FS-1352](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1352-interpolated-strings-in-constant-expressions.md), [PR #20753](https://github.com/dotnet/fsharp/pull/20753)) ### Fixed diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md index 58b62759c21..d84ca0e28b0 100644 --- a/docs/release-notes/.Language/preview.md +++ b/docs/release-notes/.Language/preview.md @@ -4,7 +4,7 @@ Features assigned to F# 11.2 are listed in [11.2](https://fsharp.github.io/fshar ### Added -* Allow interpolated strings in constant expressions: when each hole is a non-null constant string without alignment or format specifiers, as in C#, an interpolated string can define a `[]` value, an attribute argument or a type provider static argument, such as `[]` (`ConstantInterpolatedStrings` preview feature). Outside these contexts interpolated strings, and their quotations, are unchanged. ([Suggestion #1347](https://github.com/fsharp/fslang-suggestions/issues/1347), [RFC FS-1352](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1352-interpolated-strings-in-constant-expressions.md), [PR #20753](https://github.com/dotnet/fsharp/pull/20753)) +* Allow interpolated strings in constant expressions, such as `[] let Items = $"{Root}/items"` and `[]`, when each hole is a non-null constant string without alignment or format specifier (`ConstantInterpolatedStrings` preview feature). ([Suggestion #1347](https://github.com/fsharp/fslang-suggestions/issues/1347), [RFC FS-1352](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1352-interpolated-strings-in-constant-expressions.md), [PR #20753](https://github.com/dotnet/fsharp/pull/20753)) ### Fixed diff --git a/src/Compiler/Checking/CheckBasics.fs b/src/Compiler/Checking/CheckBasics.fs index 2a3ebd0bf81..6a750425e14 100644 --- a/src/Compiler/Checking/CheckBasics.fs +++ b/src/Compiler/Checking/CheckBasics.fs @@ -247,7 +247,6 @@ type TcEnv = eInNameOf: bool - /// Checking an expression that must evaluate to a constant: a literal, an attribute argument or a static argument. eInConstantContext: bool /// Are we checking the body of an object expression? Such a body has family access to the diff --git a/src/Compiler/Checking/CheckBasics.fsi b/src/Compiler/Checking/CheckBasics.fsi index 61890cfbfaf..1fb6d8a01ce 100644 --- a/src/Compiler/Checking/CheckBasics.fsi +++ b/src/Compiler/Checking/CheckBasics.fsi @@ -137,7 +137,6 @@ type TcEnv = /// Resolving a nameof operand, rather than an executable expression. eInNameOf: bool - /// Checking an expression that must evaluate to a constant: a literal, an attribute argument or a static argument. eInConstantContext: bool /// Are we checking the body of an object expression? Such a body has family access to the diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 3dc88b72e71..d80bbf5287a 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -4202,6 +4202,19 @@ let EliminateNullnessFromInputType (g: TcGlobals) (inputTy: TType) (pat: Pattern | None -> eliminateNull inputTy pat | _ -> inputTy +let tryConstantHoleText (g: TcGlobals) (formatting: SynInterpolationFormatting) (mHole: range) (hole: Expr) = + match formatting with + | SynInterpolationFormatting.DotNet(None, None) when isStringTy g (tyOfExpr g hole) -> + match EvalLiteralExprOrAttribArg g hole with + | Expr.Const(Const.String text, _, _) -> ValueSome text + | Expr.Const(Const.Zero, _, _) -> + errorR (Error(FSComp.SR.tcConstantInterpolatedStringHole (), mHole)) + ValueNone + | _ -> ValueNone + | _ -> + errorR (Error(FSComp.SR.tcConstantInterpolatedStringHole (), mHole)) + ValueNone + //------------------------------------------------------------------------- // Checking types and type constraints //------------------------------------------------------------------------- @@ -7802,43 +7815,25 @@ and TcInterpolatedStringViaConcat (cenv: cenv, overallTy: OverallTy, env: TcEnv, TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> resultExpr, tpenv) -/// Fold a string-typed interpolated string in a constant context to its text (RFC FS-1352). Each hole must be -/// a non-null constant string without alignment or format specifiers. and TcConstantInterpolatedString (cenv: cenv, overallTy: OverallTy, env: TcEnv, m: range, tpenv: UnscopedTyparEnv, parts: SynInterpolatedStringPart list) = let g = cenv.g - checkLanguageFeatureAndRecover g.langVersion LanguageFeature.ConstantInterpolatedStrings m - let holeText (fill: Expr) (formatting: SynInterpolationFormatting) (mHole: range) = - let invalidHole () = - errorR (Error(FSComp.SR.tcConstantInterpolatedStringHole (), mHole)) - ValueNone + let tryPartTexts, tpenv = + (tpenv, parts) ||> List.mapFold (fun tpenv part -> + match part with + | SynInterpolatedStringPart.String(text, _) -> (fun () -> ValueSome(text.Replace("%%", "%"))), tpenv + | SynInterpolatedStringPart.FillExpr(synFill, formatting) -> + let hole, tpenv = TcExprFlex2 cenv (NewInferenceType g) env false tpenv synFill + (fun () -> tryConstantHoleText g formatting synFill.Range hole), tpenv) + + let text = + tryPartTexts + |> Seq.map (fun tryPartText -> tryPartText ()) + |> Seq.takeWhile _.IsSome + |> Seq.map _.Value + |> String.concat "" - match formatting with - | SynInterpolationFormatting.DotNet(None, None) when isStringTy g (tyOfExpr g fill) -> - match EvalLiteralExprOrAttribArg g fill with - | Expr.Const(Const.String text, _, _) -> ValueSome text - | Expr.Const(Const.Zero, _, _) -> invalidHole () - | _ -> ValueNone // EvalLiteralExprOrAttribArg has reported that the hole is not a constant - | _ -> invalidHole () - - let text = Text.StringBuilder() - let mutable tpenvAcc = tpenv - // Only the first hole that breaks a rule is reported; later holes are still type-checked. - let mutable holesAreConstant = true - - for part in parts do - match part with - | SynInterpolatedStringPart.String(s, _) -> text.Append(s.Replace("%%", "%")) |> ignore - | SynInterpolatedStringPart.FillExpr(synFill, formatting) -> - let fill, tpenvAfter = TcExprFlex2 cenv (NewInferenceType g) env false tpenvAcc synFill - tpenvAcc <- tpenvAfter - - if holesAreConstant then - match holeText fill formatting synFill.Range with - | ValueSome hole -> text.Append(hole: string) |> ignore - | ValueNone -> holesAreConstant <- false - - TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> mkString g m (text.ToString()), tpenvAcc) + TcPropagatingExprLeafThenConvert cenv overallTy g.string_ty env m (fun () -> mkString g m text, tpenv) /// Check an interpolated string expression and [] warnForFunctionValuesInFillExprs (g: TcGlobals) argTys synFillExprs = @@ -7987,7 +7982,7 @@ and TcInterpolatedStringExpr cenv (overallTy: OverallTy) env m tpenv (parts: Syn else let str = mkString g m printfFormatString mkCallNewFormat g m printerTy printerArgTy printerResidueTy printerResultTy printerTupleTy str, tpenv - elif isString && env.eInConstantContext then + elif isString && env.eInConstantContext && g.langVersion.SupportsFeature LanguageFeature.ConstantInterpolatedStrings then TcConstantInterpolatedString (cenv, overallTy, env, m, tpenv, parts) elif isString then // String-typed interpolation: lower to a reflection-free System.String.Concat of the parts, diff --git a/tests/FSharp.Compiler.ComponentTests/Language/ConstantInterpolatedStringsTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/ConstantInterpolatedStringsTests.fs index 6893652bde8..0d9c92f5411 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/ConstantInterpolatedStringsTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/ConstantInterpolatedStringsTests.fs @@ -6,13 +6,12 @@ open System.IO open Xunit open FSharp.Test.Compiler -// RFC FS-1352: interpolated strings in constant expressions. module ConstantInterpolatedStringsTests = let private preview source = FSharp source |> withLangVersionPreview [] - let ``Literals and attribute arguments take the runtime text of a constant interpolated string`` () = + let ``Constant interpolated strings take their run-time text`` () = preview """ module Program @@ -40,32 +39,17 @@ type TagAttribute(tags: string[]) = type Tagged() = class end let tag = typeof.GetCustomAttributes(typeof, false) |> Array.exactlyOne :?> TagAttribute +let quotationIsFolded = match <@ $"{Root}/items" @> with Quotations.Patterns.Value _ -> true | _ -> false -let quotation = - match <@ $"{Root}/items" @> with - | Quotations.Patterns.Value _ -> "folded" - | _ -> "not folded" - -printfn "%s|%s|%s|%s" Items ItemById Verbatim (sprintf CountFormat 3) -printfn "%s|%s|%s" (String.concat "," tag.Tags) tag.Name quotation +printfn "%s|%s|%s|%s|%s|%s|%b" Items ItemById Verbatim (sprintf CountFormat 3) (String.concat "," tag.Tags) tag.Name quotationIsFolded """ |> asExe |> compileExeAndRun |> shouldSucceed - |> withStdOutContains @"api/v2/items|api/v2/items/{id}|\Rootapi/v2|api/v2/items: 3" - |> withStdOutContains "api/v2/a,api/v2/b|api/v2!|not folded" + |> withStdOutContains @"api/v2/items|api/v2/items/{id}|\Rootapi/v2|api/v2/items: 3|api/v2/a,api/v2/b|api/v2!|false" [] - let ``Extended interpolated strings fold with their own delimiters`` () = - // [] let Json = $$"""{"{{Root}}": "100%"}""" - preview "module Program\n[]\nlet Root = \"api\"\n[]\nlet Json = $$\"\"\"{\"{{Root}}\": \"100%\"}\"\"\"\nprintfn \"%s\" Json" - |> asExe - |> compileExeAndRun - |> shouldSucceed - |> withStdOutContains "{\"api\": \"100%\"}" - - [] - let ``The compiler reads an attribute argument that is a constant interpolated string`` () = + let ``An Obsolete message can be a constant interpolated string`` () = preview """ module Program @@ -90,33 +74,25 @@ let item = getItem 1 [] let ``A type provider static argument can be a constant interpolated string`` () = - let providerPath = - Path.Combine(__SOURCE_DIRECTORY__, "../../fsharp/typeProviders/staticStringParameter/provider.fsx") + let providerPath = Path.Combine(__SOURCE_DIRECTORY__, "../../fsharp/typeProviders/staticStringValue/provider.fsx") let provider = Fsx $"""#load @"{providerPath}" """ - |> withName "StaticStringParameterProvider" + |> withName "StaticStringValueProvider" |> ignoreWarnings |> compile |> shouldSucceed - Fsx( - $"#r @\"{provider.OutputPath.Value}\"\n" - + """ + Fsx $$"""#r @"{{provider.OutputPath.Value}}" + [] let Root = "api/v2" -let fromConst = Provided.Text.Value - [] -let ItemById = $"{Root}/items/{{id}}" - -let fromLiteral = Provided.Text.Value +let Items = $"{Root}/items" -if fromConst <> "api/v2/items" || fromLiteral <> "api/v2/items/{id}" then - failwith $"Unexpected static arguments: '{fromConst}', '{fromLiteral}'" -""" - ) +type FromConst = Provided.Text +type FromLiteral = Provided.Text""" #if NETCOREAPP |> withOptions [ "--usesdkrefs-" ] #endif @@ -124,9 +100,15 @@ if fromConst <> "api/v2/items" || fromLiteral <> "api/v2/items/{id}" then |> eval |> shouldSucceed - [] - let ``Each hole must be a non-null constant string without alignment or format specifiers`` () = - preview """ + [] + [] + [] + [] + [] + [] + [] + let ``A hole must be a non-null constant string without alignment or format specifier`` (hole: string, errorCode: int) = + preview $""" module Program [] @@ -138,25 +120,11 @@ let NoValue: string = null let dir = "data" [] -let A = $"v{Version}" -[] -let B = $"{Root,10}" -[] -let C = $"%s{Root}" -[] -let D = $"{dir}/x" -[] -let E = $"{NoValue}/x" +let Rejected = $"{hole}" """ |> typecheck |> shouldFail - |> withDiagnostics [ - (Error 3925, Line 13, Col 13, Line 13, Col 20, "This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") - (Error 3925, Line 15, Col 12, Line 15, Col 16, "This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") - (Error 3925, Line 17, Col 14, Line 17, Col 18, "This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") - (Error 267, Line 19, Col 12, Line 19, Col 15, "This is not a valid constant expression or custom attribute value") - (Error 3925, Line 21, Col 12, Line 21, Col 19, "This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") - ] + |> withErrorCode errorCode [] let ``Only the first hole that breaks a rule is reported`` () = @@ -165,36 +133,33 @@ module Program [] let Version = 2 -let dir = "data" [] -let A = $"{Version}{Version}" -[] -let B = $"{dir}{Version}" -[] -let C = $"{Version}{missing}" +let Twice = $"{Version}{Version}" """ |> typecheck - |> shouldFail - |> withDiagnostics [ - (Error 3925, Line 9, Col 12, Line 9, Col 19, "This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") - (Error 267, Line 11, Col 12, Line 11, Col 15, "This is not a valid constant expression or custom attribute value") - (Error 39, Line 13, Col 21, Line 13, Col 28, "The value or constructor 'missing' is not defined.") - (Error 3925, Line 13, Col 12, Line 13, Col 19, "This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") - ] + |> withSingleDiagnostic (Error 3925, Line 8, Col 16, Line 8, Col 23, "This interpolated string must be a constant, so each hole must be a non-null constant expression of type 'string' without alignment or format specifiers.") [] - let ``Constant interpolated strings need the preview language version`` () = + let ``Before the preview language version interpolated strings in literals are unchanged`` () = FSharp """ module Program [] let Root = "api/v2" +[] +let Unevaluated = false && ($"{1}" = "1") + +[] +let Picked = if true then Root else $"{1}" + [] let Items = $"{Root}/items" """ |> withLangVersion "11.2" |> typecheck |> shouldFail - |> withSingleDiagnostic (Error 3350, Line 8, Col 13, Line 8, Col 28, "Feature 'interpolated strings in constant expressions' is not available in F# 11.2. Please use language version 'PREVIEW' or greater.") + |> withDiagnostics + [ Error 267, Line 14, Col 13, Line 14, Col 28, "This is not a valid constant expression or custom attribute value" + Error 837, Line 14, Col 13, Line 14, Col 28, "This is not a valid constant expression" ] diff --git a/tests/fsharp/typeProviders/staticStringParameter/provider.fsx b/tests/fsharp/typeProviders/staticStringParameter/provider.fsx deleted file mode 100644 index d251cf8e73b..00000000000 --- a/tests/fsharp/typeProviders/staticStringParameter/provider.fsx +++ /dev/null @@ -1,68 +0,0 @@ -namespace StaticStringParameterProvider - -#load "../helloWorld/TypeMagic.fs" - -open System -open System.Collections.Generic -open System.Reflection -open Microsoft.FSharp.Core.CompilerServices -open Microsoft.FSharp.Quotations -open FSharp.TypeMagic - -/// Provides 'Provided.Text', whose static property 'Value' returns the static argument. -[] -type Provider() = - let modul = typeof.Module - let container = TypeContainer.Namespace(modul, "Provided") - let unappliedType = TypeBuilder.CreateType(container, "Text") - let appliedTypes = Dictionary(StringComparer.Ordinal) - - let invalidation = Event() - - interface IProvidedNamespace with - member _.NamespaceName = "Provided" - member _.GetTypes() = [| unappliedType |] - member _.GetNestedNamespaces() = [||] - member _.ResolveTypeName name = if name = unappliedType.Name then unappliedType else null - - interface ITypeProvider with - member this.GetNamespaces() = [| this |] - - member _.GetStaticParameters typeWithoutArguments = - if typeWithoutArguments.Name = unappliedType.Name then - [| TypeBuilder.CreateStaticParameter("Value", typeof, 0) |] - else - [||] - - member _.ApplyStaticArguments(_, typePathWithArguments, staticArguments) = - let name = typePathWithArguments[typePathWithArguments.Length - 1] - - lock appliedTypes (fun () -> - match appliedTypes.TryGetValue name with - | true, struct (appliedType, _) -> appliedType - | _ -> - let appliedType = - TypeBuilder.CreateType( - container, - name, - members = TypeBuilder.CacheMembers(fun declaringType -> - TypeBuilder.JoinPropertiesIntoMemberInfos - [ TypeBuilder.CreateSyntheticProperty(declaringType, "Value", typeof, isStatic = true) ])) - - appliedTypes[name] <- struct (appliedType, staticArguments[0] :?> string) - appliedType) - - member _.GetInvokerExpression(getter, _) = - let struct (_, value) = lock appliedTypes (fun () -> appliedTypes[getter.DeclaringType.Name]) - Expr.Value value - - member _.GetGeneratedAssemblyContents _ = failwith "Only erased types are provided." - - [] - member _.Invalidate = invalidation.Publish - - interface IDisposable with - member _.Dispose() = () - -[] -do () diff --git a/tests/fsharp/typeProviders/staticStringValue/provider.fsx b/tests/fsharp/typeProviders/staticStringValue/provider.fsx new file mode 100644 index 00000000000..ca2dc4ef2e0 --- /dev/null +++ b/tests/fsharp/typeProviders/staticStringValue/provider.fsx @@ -0,0 +1,41 @@ +namespace StaticStringValueProvider + +#load "../helloWorld/TypeMagic.fs" + +open System +open Microsoft.FSharp.Core.CompilerServices +open FSharp.TypeMagic + +[] +type Provider() = + let container = TypeContainer.Namespace(typeof.Module, "Provided") + let text = TypeBuilder.CreateType(container, "Text") + let invalidation = Event() + + interface IProvidedNamespace with + member _.NamespaceName = "Provided" + member _.GetTypes() = [| text |] + member _.GetNestedNamespaces() = [||] + member _.ResolveTypeName name = if name = text.Name then text else null + + interface ITypeProvider with + member this.GetNamespaces() = [| this |] + member _.GetStaticParameters _ = [| TypeBuilder.CreateStaticParameter("Value", typeof, 0) |] + + member _.ApplyStaticArguments(_, typePathWithArguments, staticArguments) = + if string staticArguments[0] <> "api/v2/items" then + failwith $"Unexpected static argument '{staticArguments[0]}'" + + TypeBuilder.CreateType(container, typePathWithArguments[typePathWithArguments.Length - 1]) + + member _.GetInvokerExpression(_, _) = failwith "No members are provided." + member _.GetGeneratedAssemblyContents _ = failwith "Only erased types are provided." + + [] + member _.Invalidate = invalidation.Publish + + interface IDisposable with + member _.Dispose() = () + +[] +do ()