diff --git a/src/Fable.Transforms/Beam/Prelude.fs b/src/Fable.Transforms/Beam/Prelude.fs index 1a8079ce2..0af5465e9 100644 --- a/src/Fable.Transforms/Beam/Prelude.fs +++ b/src/Fable.Transforms/Beam/Prelude.fs @@ -520,6 +520,39 @@ module Chars = | Some _ -> Char | None -> e.Type +/// Conversion of a value to its `string` form, dispatched on the static type. +module ToString = + open Fable.AST + open Fable.AST.Fable + open Fable.Transforms + open Fable.Transforms.Replacements.Util + + /// The `ToString` of a value whose type the compiler can still see. + /// + /// Two replacement tables reach this: `string x` as an operator, and `x.ToString()` as a + /// conversion. They must not diverge — a fix applied to one spelling and not the other is + /// invisible until someone happens to test the neglected one — so both call here. + /// + /// A char is peeled out of its boxing cast first. `box c |> string` arrives as a char under a + /// cast to `obj` and would otherwise fall through to `fable_convert:to_string`, which sees only + /// the integer and prints the codepoint. Every other type is unaffected, since `tryAsChar` only + /// ever looks through a box around a char. + let toStringByType (com: ICompiler) (r: SourceLocation option) (t: Type) (arg: Expr) = + let arg = Chars.tryAsChar arg |> Option.defaultValue arg + + match arg.Type with + | Type.String -> Some arg + | Type.Char -> emitExpr r t [ arg ] "<<($0)/utf8>>" |> Some + | Type.Number(kind, _) -> + match kind with + | Decimal -> Helper.LibCall(com, "fable_decimal", "to_string", t, [ arg ], ?loc = r) |> Some + | Float16 + | Float32 + | Float64 -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some + | _ -> emitExpr r t [ arg ] "integer_to_binary($0)" |> Some + | Type.Boolean -> emitExpr r t [ arg ] "atom_to_binary($0)" |> Some + | _ -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some + /// Fixed-width integer semantics. Erlang integers are arbitrary precision and never /// overflow, so operations that can leave the width of a .NET sized integer are routed /// through the `fable_int` runtime module to truncate them back (two's complement). diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 63ec0d372..b16d6e0cc 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -392,25 +392,7 @@ let private operators |> Some | _ -> Helper.LibCall(com, "fable_decimal", "from_int", _t, [ arg ], ?loc = r) |> Some | _ -> Helper.LibCall(com, "fable_decimal", "from_int", _t, [ arg ], ?loc = r) |> Some - | "ToString", [ arg ] -> - // `box c |> string` arrives as a char under a boxing cast to `obj`, and would otherwise fall - // through to `fable_convert:to_string`, which sees only the integer and prints the codepoint. - // Peeling the cast puts it back on the `Type.Char` branch below; every other type is - // unaffected, since `tryAsChar` only ever looks through a box around a char. - let arg = Chars.tryAsChar arg |> Option.defaultValue arg - - match arg.Type with - | Type.String -> Some arg - | Type.Char -> emitExpr r _t [ arg ] "<<($0)/utf8>>" |> Some - | Type.Number(kind, _) -> - match kind with - | Decimal -> Helper.LibCall(com, "fable_decimal", "to_string", _t, [ arg ], ?loc = r) |> Some - | Float16 - | Float32 - | Float64 -> Helper.LibCall(com, "fable_convert", "to_string", _t, [ arg ], ?loc = r) |> Some - | _ -> emitExpr r _t [ arg ] "integer_to_binary($0)" |> Some - | Type.Boolean -> emitExpr r _t [ arg ] "atom_to_binary($0)" |> Some - | _ -> Helper.LibCall(com, "fable_convert", "to_string", _t, [ arg ], ?loc = r) |> Some + | "ToString", [ arg ] -> ToString.toStringByType com r _t arg | "ToChar", [ arg ] -> match arg.Type with | Type.String -> emitExpr r _t [ arg ] "binary:first($0)" |> Some @@ -1418,23 +1400,7 @@ let private conversions | [ a ] -> a | _ -> Value(Null Type.Unit, None) - // `box c |> string` reaches here as a char under a boxing cast to `obj`. Peeling the cast - // puts it back on the `Type.Char` branch below, instead of falling through to - // `fable_convert:to_string`, which sees only the integer and prints the codepoint. - let arg = Chars.tryAsChar arg |> Option.defaultValue arg - - match arg.Type with - | Type.String -> Some arg - | Type.Char -> emitExpr r t [ arg ] "<<($0)/utf8>>" |> Some - | Type.Number(kind, _) -> - match kind with - | Decimal -> Helper.LibCall(com, "fable_decimal", "to_string", t, [ arg ], ?loc = r) |> Some - | Float16 - | Float32 - | Float64 -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some - | _ -> emitExpr r t [ arg ] "integer_to_binary($0)" |> Some - | Type.Boolean -> emitExpr r t [ arg ] "atom_to_binary($0)" |> Some - | _ -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some + ToString.toStringByType com r t arg // char(x) → Erlang integers represent chars | "ToChar", [ arg ] -> match arg.Type with