Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Accept a nullable string in a '%s' interpolation hole
The hole was checked against 'string', so under --checknulls a nullable string
gave FS3261 where 'sprintf "%s"' does not. Share the specifier's type as
CheckFormatStrings.stringFormatTy and check the hole against that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
  • Loading branch information
charlesroddie and claude committed Jul 30, 2026
commit 71638572259fd1cc6ecd9077b3a07f1f8414b16a
6 changes: 4 additions & 2 deletions src/Compiler/Checking/CheckFormatStrings.fs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ let mkFlexibleDecimalFormatTypar (g: TcGlobals) m =
let mkFlexibleFloatFormatTypar (g: TcGlobals) m =
mkFlexibleFormatTypar g m [ g.float_ty; g.float32_ty; g.decimal_ty ] g.float_ty

let stringFormatTy (g: TcGlobals) =
if g.checkNullness && g.langFeatureNullness then g.string_ty_ambivalent else g.string_ty

type FormatInfoRegister =
{ mutable leftJustify : bool
mutable numPrefixIfPos : char option
Expand Down Expand Up @@ -448,8 +451,7 @@ let parseFormatStringInternal
checkOtherFlags ch
collectSpecifierLocation fragLine fragCol 1
let i = skipPossibleInterpolationHole (i+1)
let stringTy = if g.checkNullness && g.langFeatureNullness then g.string_ty_ambivalent else g.string_ty
parseLoop ((posi, stringTy) :: acc) (i, fragLine, fragCol+1) fragments
parseLoop ((posi, stringFormatTy g) :: acc) (i, fragLine, fragCol+1) fragments

| 'O' ->
checkOtherFlags ch
Expand Down
3 changes: 3 additions & 0 deletions src/Compiler/Checking/CheckFormatStrings.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ val mkFlexibleIntFormatTypar: g: TcGlobals -> m: range -> TType
/// A flexible type variable constrained to 'decimal', as accepted by the '%M' specifier.
val mkFlexibleDecimalFormatTypar: g: TcGlobals -> m: range -> TType

/// The type accepted by the '%s' specifier: ambivalent about nullness when nullness is checked.
val stringFormatTy: g: TcGlobals -> TType

val ParseFormatString:
m: range ->
fragmentRanges: range list ->
Expand Down
3 changes: 2 additions & 1 deletion src/Compiler/Checking/Expressions/CheckExpressions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7672,8 +7672,9 @@ and TcInterpolatedStringViaConcat (cenv: cenv, overallTy: OverallTy, env: TcEnv,
| SynInterpolationFormatting.Printf (spec, _) ->
match spec with
// A bare '%s' requires a string; pass it through (it may be null) instead of formatting via 'sprintf'.
// Its type is the one 'sprintf "%s"' uses, so a nullable string is accepted here too.
| "%s" ->
let fill, tpenv = TcExpr cenv (MustEqual g.string_ty) env tpenv synFill
let fill, tpenv = TcExpr cenv (MustEqual (CheckFormatStrings.stringFormatTy g)) env tpenv synFill
(fill, true), tpenv
| "%c" -> convertViaString g.char_ty
| "%d" | "%i" -> convertViaString (CheckFormatStrings.mkFlexibleIntFormatTypar g m)
Expand Down
Loading