Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/Fable.Build/FableLibrary/Python.fs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type BuildFableLibraryPython(?skipCore: bool, ?postFableBuildStage: unit -> unit
[
Path.Combine("src", "fable-library-py", "**", "*.py")
Path.Combine("src", "fable-library-py", "**", "*.fs")
// Rust extension sources, so edits to them trigger a maturin rebuild
Path.Combine("src", "fable-library-py", "**", "*.rs")
Path.Combine("src", "fable-library-ts", "**", "*.fs")
]
)
Expand Down
12 changes: 11 additions & 1 deletion src/Fable.Transforms/Beam/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ let private fsFormat
| _ -> None
| "PrintFormatThen", _, arg :: callee :: _ -> emitExpr r t [ callee; arg ] "(maps:get(cont, $0))($1)" |> Some
| ".ctor", _, str :: (Value(NewArray(ArrayValues templateArgs, _, _), _) as values) :: _ ->
match makeStringTemplateFrom [| "%s"; "%i" |] templateArgs str with
match makeStringTemplateFrom com [| "%s"; "%i" |] templateArgs str with
| Some v -> makeValue r v |> Some
| None ->
Helper.LibCall(com, "fable_string", "interpolate", t, [ str; values ], i.SignatureArgTypes, ?loc = r)
Expand Down Expand Up @@ -1094,7 +1094,17 @@ let private strings
// str.GetEnumerator() → convert string to list of codepoints and create enumerator
| "GetEnumerator", Some c, _ -> emitExpr r t [ c ] "fable_utils:get_enumerator(binary_to_list($0))" |> Some
// String.Format("{0} {1}", arg0, arg1)
// Also String.Format(provider, "{0}", arg0): the optional leading IFormatProvider/CultureInfo
// argument is ignored (the format string is the first argument typed as `String`).
| "Format", None, (fmtStr :: fmtArgs) ->
let fmtStr, fmtArgs =
match fmtStr.Type with
| String -> fmtStr, fmtArgs
| _ ->
match fmtArgs with
| realFmt :: rest -> realFmt, rest
| [] -> fmtStr, fmtArgs

// When a single array argument is passed (e.g. from ParamArray),
// pass it directly — format/2 already handles refs and lists.
let argsList =
Expand Down
2 changes: 1 addition & 1 deletion src/Fable.Transforms/Dart/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,7 @@ let fsFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op
_,
_ -> fsharpModule com ctx r t i thisArg args
| ".ctor", _, str :: (Value(NewArray(ArrayValues templateArgs, _, MutableArray), _) as values) :: _ ->
match makeStringTemplateFrom [| "%s"; "%i" |] templateArgs str with
match makeStringTemplateFrom com [| "%s"; "%i" |] templateArgs str with
| Some v -> makeValue r v |> Some
| None ->
Helper.LibCall(com, "String", "interpolate", t, [ str; values ], i.SignatureArgTypes, ?loc = r)
Expand Down
2 changes: 1 addition & 1 deletion src/Fable.Transforms/Python/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1103,7 +1103,7 @@ let fsFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op
_,
_ -> fsharpModule com ctx r t i thisArg args
| ".ctor", _, str :: (Value(NewArray(ArrayValues templateArgs, _, _), _) as values) :: _ ->
match makeStringTemplateFrom [| "%s"; "%i" |] templateArgs str with
match makeStringTemplateFrom com [| "%s"; "%i" |] templateArgs str with
| Some v -> makeValue r v |> Some
| None ->
Helper.LibCall(com, "string", "interpolate", t, [ str; values ], i.SignatureArgTypes, ?loc = r)
Expand Down
56 changes: 41 additions & 15 deletions src/Fable.Transforms/Replacements.Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -461,17 +461,24 @@ let makeStringTemplate
StringTemplate(tag, parts, values)


// Parses an F# interpolated string format, finds every %P() hole (optionally preceded by a
// Parses an F# interpolated string format, finds every %P(...) hole (optionally preceded by a
// printf format specifier), and builds a StringTemplate from the literal parts and the
// supplied value expressions.
//
// For each hole the caller supplies `handleFormatSpec`:
// - None → the hole has no preceding format spec (or only a simple one from simpleFormats);
// the value is used as-is.
// - Some fmtSpec → the hole has a non-simple format spec; the callback receives the spec
// (e.g. "%0.2f") and the value expression. Returning Some wraps the value;
// returning None aborts the whole template (None is returned for the string).
// A hole takes one of these shapes:
// - `%P()` → no format; the value is used as-is.
// - `%<spec>%P()` → a printf-style format spec (e.g. `%0.2f`); handled by `handleFormatSpec`.
// - `%P(<spec>)` → a .NET format spec (e.g. `X4` from `$"{x:X4}"`).
// - `%<align>P(...)` → an alignment width (e.g. `%6P()` from `$"{x,6}"`, `%-6P(X4)` from
// `$"{x,-6:X4}"`).
// Alignment and/or .NET format holes are always wrapped in a `String.format("{0,align:spec}", value)`
// call (parts omitted when absent), which every target implements.
//
// For each printf hole the caller supplies `handleFormatSpec`:
// - Some fmtSpec → wraps the value (the callback receives the spec, e.g. "%0.2f").
// - None → aborts the whole template (None is returned for the string).
let private makeStringTemplateFromWith
(com: ICompiler)
(simpleFormats: string array)
(handleFormatSpec: string -> Expr -> Expr option)
(values: Expr list)
Expand All @@ -483,7 +490,7 @@ let private makeStringTemplateFromWith
let str = str.Replace("%%", "%")

let matches =
Regex.Matches(str, @"((?<!%)%(?:[0+\- ]*)(?:\d+)?(?:\.\d+)?\w)?%P\(\)")
Regex.Matches(str, @"((?<!%)%(?:[0+\- ]*)(?:\d+)?(?:\.\d+)?\w)?%([0+\- ]*\d*)P\(([^)]*)\)")
|> Seq.cast<Match>

(Some([], values), matches)
Expand All @@ -492,11 +499,28 @@ let private makeStringTemplateFromWith
| None -> None
| Some(_, []) -> None // fewer values than matches
| Some(mapped, value :: restValues) ->
let needsFormat =
m.Groups[1].Success && not (Array.contains m.Groups[1].Value simpleFormats)
let alignment = m.Groups[2].Value
let dotnetFormat = m.Groups[3].Value

let resolved =
if needsFormat then
if alignment <> "" || dotnetFormat <> "" then
// Alignment (`$"{x,6}"`) and/or .NET format spec (`$"{x:X4}"`).
// Wrap value in: String.format("{0[,<align>][:<spec>]}", value)
let alignPart =
if alignment <> "" then
"," + alignment
else
""

let fmtPart =
if dotnetFormat <> "" then
":" + dotnetFormat
else
""

let fmtStr = Value(StringConstant("{0" + alignPart + fmtPart + "}"), None)
Helper.LibCall(com, "String", "format", String, [ fmtStr; value ]) |> Some
elif m.Groups[1].Success && not (Array.contains m.Groups[1].Value simpleFormats) then
handleFormatSpec m.Groups[1].Value value
else
Some value
Expand All @@ -520,18 +544,20 @@ let private makeStringTemplateFromWith
| _ -> None

/// Try to build a StringTemplate from an F# interpolated string.
/// Returns None if any hole carries a format specifier that is not in simpleFormats,
/// Returns None if any hole carries a printf format specifier that is not in simpleFormats,
/// because those require runtime formatting that can't be inlined into a plain template.
let makeStringTemplateFrom simpleFormats values strExpr =
// Abort (return None) for any non-simple format spec.
makeStringTemplateFromWith simpleFormats (fun _ _ -> None) values strExpr
/// Holes carrying a .NET format spec (e.g. `%P(X4)`) are always inlined as a String.format call.
let makeStringTemplateFrom com simpleFormats values strExpr =
// Abort (return None) for any non-simple printf format spec.
makeStringTemplateFromWith com simpleFormats (fun _ _ -> None) values strExpr

/// Like makeStringTemplateFrom but always succeeds for string literals.
/// Values with format specifiers not in simpleFormats are individually wrapped in a
/// String.interpolate call so they evaluate to strings.
/// Used for JSX templates, where every hole must produce a value (formatted or not).
let makeStringTemplateFromAllowingFormat (com: ICompiler) simpleFormats (values: Expr list) strExpr =
makeStringTemplateFromWith
com
simpleFormats
(fun fmtSpec value ->
// Wrap value in: String.interpolate("%fmtspec%P()", [| value |])
Expand Down
2 changes: 1 addition & 1 deletion src/Fable.Transforms/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,7 @@ let fsFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op
_,
_ -> fsharpModule com ctx r t i thisArg args
| ".ctor", _, str :: (Value(NewArray(ArrayValues templateArgs, _, _), _) as values) :: _ ->
match makeStringTemplateFrom [| "%s"; "%i" |] templateArgs str with
match makeStringTemplateFrom com [| "%s"; "%i" |] templateArgs str with
| Some v -> makeValue r v |> Some
| None ->
// Try to build a StringTemplate where formatted values are individually wrapped
Expand Down
156 changes: 126 additions & 30 deletions src/Fable.Transforms/Rust/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -809,10 +809,78 @@ let fsharpModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (this
Helper.LibCall(com, moduleName, memberName, t, args, i.SignatureArgTypes, ?loc = r)
|> Some

// Maps a .NET standard numeric format specifier (e.g. "X4", "D5", "F2") to a
// Rust format spec body { flags; width; precision; type } so it can be merged
// into a `format!` placeholder. Returns None for specifiers Rust's `format!`
// can't express (N, C, P, custom numeric patterns) so the value is emitted plain.
let private mapDotnetSpecToRust (spec: string) =
match Regex.Match(spec, @"^([A-Za-z])(\d*)$") with
| m when m.Success ->
let typ = m.Groups[1].Value
let n = m.Groups[2].Value
// A width on hex/binary/octal/decimal means zero-pad to that width.
let zeroPad =
if n <> "" then
"0"
else
""

match typ with
| "X"
| "x" -> Some(zeroPad, n, "", typ)
| "B"
| "b" -> Some(zeroPad, n, "", "b")
| "O"
| "o" -> Some(zeroPad, n, "", "o")
| "D"
| "d" -> Some(zeroPad, n, "", "")
| "F"
| "f" ->
Some(
"",
"",
"."
+ (if n = "" then
"2"
else
n),
""
)
| "E" ->
Some(
"",
"",
"."
+ (if n = "" then
"6"
else
n),
"E"
)
| "e" ->
Some(
"",
"",
"."
+ (if n = "" then
"6"
else
n),
"e"
)
| "G"
| "g"
| "R"
| "r" -> Some("", "", "", "")
| _ -> None
| _ -> None

let makeRustFormatString interpolated (fmt: string) =
let pattern1 = @"([^%]?)%([0+\- ]*)(\*|\d+)?(\.\d+)?(\w)"
let pattern1 =
@"(?<pre>[^%]?)%(?<flags>[0+\- ]*)(?<width>\*|\d+)?(?<prec>\.\d+)?(?<type>\w)"

let pattern2 = @"([^%]?)%([0+\- ]*)(\*|\d+)?(\.\d+)?(?:P\(\)|(\w)(?:%P\(\))?)"
let pattern2 =
@"(?<pre>[^%]?)%(?<flags>[0+\- ]*)(?<width>\*|\d+)?(?<prec>\.\d+)?(?:P\((?<dotnet>[^)]*)\)|(?<type>\w)(?:%P\(\))?)"

let pattern =
if interpolated then
Expand Down Expand Up @@ -844,35 +912,63 @@ let makeRustFormatString interpolated (fmt: string) =
pattern,
fun m ->
argCount <- argCount + 1
let g1 = m.Groups[1].Value
let g2 = m.Groups[2].Value |> formatFlags
let g3 = m.Groups[3].Value.Replace("*", "$") // width parameter
let g4 = m.Groups[4].Value
let g5 = m.Groups[5].Value

let g4 =
if String.IsNullOrEmpty(g4) && (g5 = "f" || g5 = "F") then
".6"
else
g4

let g5 =
match g5 with
| "A" -> "?"
| "B" -> "b"
| ("b" | "c" | "d" | "i" | "s" | "u") -> ""
| ("o" | "x" | "X" | "e" | "E") as t -> t
| _ -> ""

let argFmt =
let formatting = g2 + g3 + g4 + g5

if String.IsNullOrEmpty(formatting) then
g1 + "{}"
let pre = m.Groups["pre"].Value
let flags = m.Groups["flags"].Value |> formatFlags
let width = m.Groups["width"].Value.Replace("*", "$") // width parameter
let prec = m.Groups["prec"].Value

let formatting =
if m.Groups["dotnet"].Success then
// .NET interpolation hole: %<align>P(<spec>) from e.g. $"{x,6:X4}"
let dotnet = m.Groups["dotnet"].Value

if dotnet = "" then
// Plain or alignment-only hole, e.g. %P() or %6P()
flags + width + prec
else
match mapDotnetSpecToRust dotnet with
| Some(specFlags, specWidth, specPrec, specType) ->
// An explicit alignment width takes the field width (space-padded);
// otherwise use the specifier's own (zero-padded) width.
let f =
if width <> "" then
flags
else
specFlags

let w =
if width <> "" then
width
else
specWidth

f + w + specPrec + specType
| None ->
// Unsupported specifier: keep any alignment, drop the rest.
flags + width + prec
else
g1 + "{:" + formatting + "}"

argFmt
let typ = m.Groups["type"].Value

let prec =
if String.IsNullOrEmpty(prec) && (typ = "f" || typ = "F") then
".6"
else
prec

let typ =
match typ with
| "A" -> "?"
| "B" -> "b"
| ("b" | "c" | "d" | "i" | "s" | "u") -> ""
| ("o" | "x" | "X" | "e" | "E") as t -> t
| _ -> ""

flags + width + prec + typ

if String.IsNullOrEmpty(formatting) then
pre + "{}"
else
pre + "{:" + formatting + "}"
)

rustFmt, argCount
Expand Down
Loading