Skip to content

Commit 583aa21

Browse files
Wrap erased unions with type cast
1 parent be37563 commit 583aa21

File tree

7 files changed

+198
-158
lines changed

7 files changed

+198
-158
lines changed

build.fsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -534,9 +534,8 @@ let testPython() =
534534
"--lang Python"
535535
]
536536

537-
runInDir buildDir "poetry run pytest -x"
538-
// Testing in Windows
539-
// runInDir buildDir "python -m pytest -x"
537+
if isWindows then runInDir buildDir "python3 -m pytest -x"
538+
else runInDir buildDir "poetry run pytest -x"
540539

541540
type RustTestMode =
542541
| SingleThreaded
@@ -764,7 +763,7 @@ match BUILD_ARGS_LOWER with
764763
| "test-integration"::_ -> testIntegration()
765764
| "test-repos"::_ -> testRepos()
766765
| ("test-ts"|"test-typescript")::_ -> testTypeScript()
767-
| "test-py"::_ -> testPython()
766+
| ("test-py"|"test-python")::_ -> testPython()
768767
| "test-rust"::_ -> testRust SingleThreaded
769768
| "test-rust-default"::_ -> testRust SingleThreaded
770769
| "test-rust-threaded"::_ -> testRust MultiThreaded

src/Fable.Transforms/FSharp2Fable.Util.fs

Lines changed: 33 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,20 +1156,15 @@ module TypeHelpers =
11561156
| Choice1Of2 t -> t
11571157
| Choice2Of2 fullName -> makeRuntimeTypeWithMeasure genArgs fullName
11581158
| _ ->
1159-
let mkDeclType () =
1160-
Fable.DeclaredType(FsEnt.Ref tdef, makeTypeGenArgsWithConstraints withConstraints ctxTypeArgs genArgs)
1161-
// Emit attribute
1162-
if tdef.Attributes |> hasAttribute Atts.emitAttr then
1163-
mkDeclType ()
1164-
else
1165-
// other special attributes
1166-
tdef.Attributes |> tryPickAttribute [
1167-
Atts.stringEnum, Fable.String
1168-
Atts.erase, Fable.Any
1169-
Atts.tsTaggedUnion, Fable.Any
1170-
]
1171-
// Rest of declared types
1172-
|> Option.defaultWith mkDeclType
1159+
let transformAttrs =
1160+
match Compiler.Language with
1161+
| Language.JavaScript | Language.TypeScript -> [ Atts.stringEnum, Fable.String ]
1162+
// Other languages can type erased unions too after fixing tests
1163+
| _ -> [ Atts.stringEnum, Fable.String; Atts.erase, Fable.Any ]
1164+
tdef.Attributes
1165+
|> tryPickAttribute transformAttrs
1166+
|> Option.defaultWith (fun () ->
1167+
Fable.DeclaredType(FsEnt.Ref tdef, makeTypeGenArgsWithConstraints withConstraints ctxTypeArgs genArgs))
11731168

11741169
let rec makeTypeWithConstraints withConstraints (ctxTypeArgs: Map<string, Fable.Type>) (NonAbbreviatedType t) =
11751170
// Generic parameter (try to resolve for inline functions)
@@ -1262,22 +1257,10 @@ module TypeHelpers =
12621257
/// Enums in F# are uint32
12631258
/// -> Allow into all int & uint
12641259
| EnumIntoInt = 0b0001
1265-
/// Erased Unions are reduced to `Any`
1266-
/// -> Cannot distinguish between 'normal' Any (like `obj`) and Erased Union (like Erased Union with string field)
1267-
///
1268-
/// For interface members the FSharp Type is available
1269-
/// -> `Ux<...>` receive special treatment and its types are extracted
1270-
/// -> `abstract Value: U2<int,string>` -> extract `int` & `string`
1271-
/// BUT: for Expressions in Anon Records that's not possible, and `U2<int,string>` is only recognized as `Any`
1272-
/// -> `{| Value = v |}`: `v: int` and `v: string` are recognized as matching,
1273-
/// but `v: U2<int,string>` isn't: only `Any`/`obj` as Type available
1274-
/// To recognize as matching, we must allow all `Any` expressions for `U2` in interface place.
1275-
///
1276-
/// Note: Only `Ux<...>` are currently handled (on interface side), not other Erased Unions!
1277-
| AnyIntoErased = 0b0010
1278-
/// Unlike `AnyIntoErased`, this allows all expressions of type `Any` in all interface properties.
1279-
/// (The other way is always allow: Expression of all Types fits into `Any`)
1280-
| AlwaysAny = 0b0100
1260+
1261+
// We could try to identify all erased unions (without tag) instead of only handling Fable.Core.Ux ones
1262+
// but it's more complex because we cannot simply extra the alternative types from the generics
1263+
let ERASED_UNION = Regex(@"^Fable\.Core\.U\d+`\d+$")
12811264

12821265
let fitsAnonRecordInInterface
12831266
(_com: IFableCompiler)
@@ -1293,60 +1276,20 @@ module TypeHelpers =
12931276
getAllInterfaceMembers interface_
12941277
|> Seq.toList
12951278

1296-
let makeType = makeType Map.empty
12971279
/// Returns for:
12981280
/// * `Ux<...>`: extracted types from `<....>`: `U2<string,int>` -> `[String; Int]`
12991281
/// * `Option<Ux<...>>`: extracted types from `<...>`, then made Optional: `Option<U2<string,int>>` -> `[Option String; Option Int]`
13001282
/// * 'normal' type: `makeType`ed type: `string` -> `[String]`
1301-
/// Note: Erased Unions (except handled `Ux<...>`) are reduced to `Any`
1302-
///
1303-
/// Extracting necessary: Erased Unions are reduced to `Any` -> special handling for `Ux<...>`
13041283
///
13051284
/// Note: nested types aren't handled: `U2<string, U<int, float>>` -> `[Int; Any]`
13061285
let rec collectTypes (ty: FSharpType) : Fable.Type list =
13071286
// Special treatment for Ux<...> and Option<Ux<...>>: extract types in Ux
1308-
// This is necessary because: `makeType` reduces Erased Unions (including Ux) to `Any` -> no type info any more
1309-
//
13101287
// Note: no handling of nested types: `U2<string, U<int, float>>` -> `int` & `float` don't get extract
1311-
match ty with
1312-
| UType tys ->
1313-
tys
1314-
|> List.map makeType
1315-
|> List.distinct
1316-
| OptionType (UType tys, isStruct) ->
1317-
tys
1318-
|> List.map (fun t -> Fable.Option(makeType t, isStruct))
1319-
|> List.distinct
1320-
| _ ->
1321-
makeType ty
1322-
|> List.singleton
1323-
and (|OptionType|_|) (ty: FSharpType) =
1324-
match ty with
1325-
| TypeDefinition tdef ->
1326-
match FsEnt.FullName tdef with
1327-
| Types.valueOption -> Some(ty.GenericArguments[0], true)
1328-
| Types.option -> Some(ty.GenericArguments[0], false)
1329-
| _ -> None
1330-
| _ -> None
1331-
and (|UType|_|) (ty: FSharpType) =
1332-
let (|UName|_|) (tdef: FSharpEntity) =
1333-
if
1334-
tdef.Namespace = Some "Fable.Core"
1335-
&&
1336-
(
1337-
let name = tdef.DisplayName
1338-
name.Length = 2 && name[0] = 'U' && Char.IsDigit name[1]
1339-
)
1340-
then
1341-
Some ()
1342-
else
1343-
None
1344-
match ty with
1345-
| TypeDefinition UName ->
1346-
ty.GenericArguments
1347-
|> Seq.toList
1348-
|> Some
1349-
| _ -> None
1288+
match makeType Map.empty ty with
1289+
| Fable.DeclaredType({ FullName = Naming.Regex ERASED_UNION _ }, genArgs) -> genArgs
1290+
| Fable.Option(Fable.DeclaredType({ FullName = Naming.Regex ERASED_UNION _ }, genArgs), isStruct) ->
1291+
genArgs |> List.map (fun t -> Fable.Option(t, isStruct))
1292+
| t -> [t]
13501293

13511294
/// Special Rules mostly for Indexers:
13521295
/// For direct interface member implementation we want to be precise (-> exact_ish match)
@@ -1358,14 +1301,10 @@ module TypeHelpers =
13581301
function
13591302
| Fable.Number((Int8 | UInt8 | Int16 | UInt16 | Int32 | UInt32), _) -> Some ()
13601303
| _ -> None
1304+
13611305
let fitsIntoSingle (rules: Allow) (expected: Fable.Type) (actual: Fable.Type) =
13621306
match expected, actual with
13631307
| Fable.Any, _ -> true
1364-
| _, Fable.Any when rules.HasFlag Allow.AlwaysAny ->
1365-
// Erased Unions are reduced to `Any`
1366-
// -> cannot distinguish between 'normal' Any (like 'obj')
1367-
// and Erased Union (like Erased Union with string field)
1368-
true
13691308
| IntNumber, Fable.Number(_, Fable.NumberInfo.IsEnum _) when rules.HasFlag Allow.EnumIntoInt ->
13701309
// the underlying type of enum in F# is uint32
13711310
// For practicality: allow in all uint & int fields
@@ -1374,22 +1313,14 @@ module TypeHelpers =
13741313
| Fable.Option(t1,_), t2
13751314
| t1, t2 ->
13761315
typeEquals false t1 t2
1316+
13771317
let fitsIntoMulti (rules: Allow) (expected: Fable.Type list) (actual: Fable.Type) =
13781318
expected |> List.contains Fable.Any
1379-
||
1380-
(
1381-
// special treatment for actual=Any & multiple expected:
1382-
// multiple expected -> `Ux<...>` -> extracted types
1383-
// BUT: in actual that's not possible -> in actual `Ux<...>` = `Any`
1384-
// -> no way to distinguish Ux (or other Erased Unions) from 'normal` Any (like obj)
1385-
rules.HasFlag Allow.AnyIntoErased
1386-
&&
1387-
expected |> List.isMultiple
1388-
&&
1389-
actual = Fable.Any
1390-
)
1391-
||
1392-
expected |> List.exists (fun expected -> fitsIntoSingle rules expected actual)
1319+
|| (match actual with
1320+
| Fable.DeclaredType({ FullName = Naming.Regex ERASED_UNION _ }, actual) when List.sameLength expected actual ->
1321+
List.zip expected actual |> List.forall (fun (expected, actual) -> fitsIntoSingle rules expected actual)
1322+
| _ -> false)
1323+
|| expected |> List.exists (fun expected -> fitsIntoSingle rules expected actual)
13931324

13941325
fitsIntoMulti rules expected actual
13951326

@@ -1460,10 +1391,10 @@ module TypeHelpers =
14601391
| [] -> unreachable ()
14611392
| [expectedType] ->
14621393
let expectedType = expectedType |> formatType
1463-
$"Expected type '{expectedType}' for field '{fieldName}' because of Indexer '{indexerName}' in interface '{interfaceName}', but is '{actualType}'"
1394+
$"Expected type '{expectedType}' for field '{fieldName}' because of indexer '{indexerName}' in interface '{interfaceName}', but is '{actualType}'"
14641395
| _ ->
14651396
let expectedTypes = expectedTypes |> formatTypes
1466-
$"Expected any type of [{expectedTypes}] for field '{fieldName}' because of Indexer '{indexerName}' in interface '{interfaceName}', but is '{actualType}'"
1397+
$"Expected any type of [{expectedTypes}] for field '{fieldName}' because of indexer '{indexerName}' in interface '{interfaceName}', but is '{actualType}'"
14671398
| _ ->
14681399
let indexerNames =
14691400
indexers
@@ -1473,10 +1404,10 @@ module TypeHelpers =
14731404
| [] -> unreachable ()
14741405
| [expectedType] ->
14751406
let expectedType = expectedType |> formatType
1476-
$"Expected type '{expectedType}' for field '{fieldName}' because of Indexers [{indexerNames}] in interface '{interfaceName}', but is '{actualType}'"
1407+
$"Expected type '{expectedType}' for field '{fieldName}' because of indexers [{indexerNames}] in interface '{interfaceName}', but is '{actualType}'"
14771408
| _ ->
14781409
let expectedTypes = expectedTypes |> formatTypes
1479-
$"Expected any type of [{expectedTypes}] for field '{fieldName}' because of Indexers [{indexerNames}] in interface '{interfaceName}', but is '{actualType}'"
1410+
$"Expected any type of [{expectedTypes}] for field '{fieldName}' because of indexers [{indexerNames}] in interface '{interfaceName}', but is '{actualType}'"
14801411

14811412
let r = r |> Option.orElse range // fall back to anon record range
14821413

@@ -1504,7 +1435,7 @@ module TypeHelpers =
15041435
| Some i ->
15051436
let expr = List.item i argExprs
15061437
let ty = expr.Type
1507-
if ty |> fitsInto (Allow.TheUsual ||| Allow.AnyIntoErased) expectedTypes then
1438+
if ty |> fitsInto Allow.TheUsual expectedTypes then
15081439
None
15091440
else
15101441
formatUnexpectedTypeError None m.DisplayName expectedTypes ty expr.Range
@@ -1537,7 +1468,7 @@ module TypeHelpers =
15371468
|> List.filter (fun (fieldName, _) -> fieldsToIgnore |> Set.contains fieldName |> not )
15381469
|> List.choose (fun (name, expr) ->
15391470
let ty = expr.Type
1540-
if fitsInto (Allow.TheUsual ||| Allow.EnumIntoInt ||| Allow.AnyIntoErased) validTypes ty then
1471+
if fitsInto (Allow.TheUsual ||| Allow.EnumIntoInt) validTypes ty then
15411472
None
15421473
else
15431474
formatUnexpectedTypeError (Some indexers) name validTypes ty expr.Range
@@ -1804,13 +1735,13 @@ module Util =
18041735
let isErasedOrStringEnumEntity (ent: Fable.Entity) =
18051736
ent.Attributes |> Seq.exists (fun att ->
18061737
match att.Entity.FullName with
1807-
| Atts.erase | Atts.stringEnum | Atts.tsTaggedUnion -> true
1738+
| Atts.erase | Atts.stringEnum | Atts.tsTaggedUnion | Atts.emit -> true
18081739
| _ -> false)
18091740

18101741
let isErasedOrStringEnumFSharpEntity (ent: FSharpEntity) =
18111742
ent.Attributes |> Seq.exists (fun att ->
18121743
match (nonAbbreviatedDefinition att.AttributeType).TryFullName with
1813-
| Some(Atts.erase | Atts.stringEnum | Atts.tsTaggedUnion) -> true
1744+
| Some(Atts.erase | Atts.stringEnum | Atts.tsTaggedUnion | Atts.emit) -> true
18141745
| _ -> false)
18151746

18161747
let isGlobalOrImportedEntity (ent: Fable.Entity) =

src/Fable.Transforms/FSharp2Fable.fs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,25 @@ let private transformNewUnion com ctx r fsType (unionCase: FSharpUnionCase) (arg
4646
match getUnionPattern fsType unionCase with
4747
| ErasedUnionCase ->
4848
makeTuple r false argExprs
49-
// TODO: Wrap erased unions in type cast so type info is not lost
50-
| ErasedUnion(tdef, _genArgs, rule, tag) ->
51-
if tag then
52-
(transformStringEnum rule unionCase)::argExprs |> makeTuple r false
53-
else
54-
match argExprs with
55-
| [] -> transformStringEnum rule unionCase
56-
| [argExpr] -> argExpr
57-
| _ when tdef.UnionCases.Count > 1 ->
58-
$"Erased unions with multiple fields must have one single case: {getFsTypeFullName fsType}. " +
59-
"To allow multiple cases pass tag argument, e.g.: [<Erase(tag=true)>]"
60-
|> addErrorAndReturnNull com ctx.InlinePath r
61-
| argExprs -> makeTuple r false argExprs
49+
| ErasedUnion(tdef, genArgs, rule, tag) ->
50+
let unionExpr =
51+
if tag then
52+
(transformStringEnum rule unionCase)::argExprs |> makeTuple r false
53+
else
54+
match argExprs with
55+
| [] -> transformStringEnum rule unionCase
56+
| [argExpr] -> argExpr
57+
| _ when tdef.UnionCases.Count > 1 ->
58+
$"Erased unions with multiple fields must have one single case: {getFsTypeFullName fsType}. " +
59+
"To allow multiple cases pass tag argument, e.g.: [<Erase(tag=true)>]"
60+
|> addErrorAndReturnNull com ctx.InlinePath r
61+
| argExprs -> makeTuple r false argExprs
62+
match com.Options.Language with
63+
// Tests are failing for Rust if we wrap erased unions
64+
| Language.Rust -> unionExpr
65+
| _ ->
66+
let genArgs = makeTypeGenArgs ctx.GenericArgs genArgs
67+
Fable.TypeCast(unionExpr, Fable.DeclaredType(FsEnt.Ref tdef, genArgs))
6268
| TypeScriptTaggedUnion _ ->
6369
match argExprs with
6470
| [argExpr] -> argExpr

0 commit comments

Comments
 (0)