Skip to content

Commit 5de7b69

Browse files
Merge pull request #2190 from fable-compiler/nagareyama-type-test
Bring back type test from Fable 2
2 parents 7df9d89 + 05dd2be commit 5de7b69

File tree

7 files changed

+113
-144
lines changed

7 files changed

+113
-144
lines changed

src/Fable.Transforms/AST/AST.Babel.fs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -482,22 +482,23 @@ type IfStatement(test, consequent, ?alternate, ?loc) =
482482
interface Statement with
483483
member _.Print(printer) =
484484
printer.AddLocation(loc)
485-
match test, alternate with
486-
| :? BooleanLiteral as b, _ when b.Value ->
487-
printer.PrintProductiveStatements(consequent.Body)
488-
| :? BooleanLiteral as b, Some(:? BlockStatement as alternate) when not b.Value ->
489-
printer.PrintProductiveStatements(alternate.Body)
490-
| _ ->
491-
printer.Print("if (", ?loc=loc)
492-
test.Print(printer)
493-
printer.Print(") ")
494-
printer.Print(consequent)
495-
// TODO: Remove else clauses if they become empty after removing null statements (see PrintBlock)
496-
printer.PrintOptional((if printer.Column > 0 then " else " else "else "), alternate)
497-
// If the consequent/alternate is a block
498-
// a new line should already be printed
499-
if printer.Column > 0 then
500-
printer.PrintNewLine()
485+
printer.Print("if (", ?loc=loc)
486+
test.Print(printer)
487+
printer.Print(") ")
488+
printer.Print(consequent)
489+
match alternate with
490+
| None -> ()
491+
| Some alternate ->
492+
if printer.Column > 0 then printer.Print(" ")
493+
printer.Print("else ")
494+
match alternate with
495+
| :? IfStatement
496+
// TODO: Get productive statements and skip else if they're empty
497+
| :? BlockStatement -> printer.Print(alternate)
498+
// Make sure alternate is always printed as block
499+
| _ -> printer.PrintBlock([|alternate|])
500+
if printer.Column > 0 then
501+
printer.PrintNewLine()
501502

502503
/// A case (if test is an Expression) or default (if test === null) clause in the body of a switch statement.
503504
type SwitchCase(consequent, ?test, ?loc) =
@@ -849,6 +850,7 @@ type ConditionalExpression(test, consequent, alternate, ?loc) =
849850
member _.Print(printer) =
850851
printer.AddLocation(loc)
851852
match test with
853+
// TODO: Move this optimization to Fable2Babel as with IfStatement?
852854
| :? BooleanLiteral as b ->
853855
if b.Value then printer.Print(consequent)
854856
else printer.Print(alternate)

src/Fable.Transforms/Fable2Babel.fs

Lines changed: 71 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -247,61 +247,66 @@ module Reflection =
247247
let private ofString s = StringLiteral s :> Expression
248248
let private ofArray babelExprs = ArrayExpression(List.toArray babelExprs) :> Expression
249249

250-
let rec private toTypeTester com ctx r = function
251-
| Fable.Regex -> Identifier "RegExp" :> Expression
252-
| Fable.MetaType -> libValue com ctx "Reflection" "TypeInfo"
253-
| Fable.LambdaType _ | Fable.DelegateType _ -> ofString "function"
254-
| Fable.AnonymousRecordType _ -> ofString "unknown" // Recognize shape? (it's possible in F#)
255-
| Fable.Any -> ofString "any"
256-
| Fable.Unit -> ofString "undefined"
257-
| Fable.Boolean -> ofString "boolean"
258-
| Fable.Char
259-
| Fable.String -> ofString "string"
260-
| Fable.Number _ -> ofString "number"
261-
| Fable.Enum _ -> ofString "number"
262-
| Fable.Option t -> ofArray [ofString "option"; toTypeTester com ctx r t]
263-
| Fable.Array t -> ofArray [ofString "array"; toTypeTester com ctx r t]
264-
| Fable.List t -> ofArray [ofString "list"; toTypeTester com ctx r t]
265-
| Fable.Tuple genArgs ->
266-
let genArgs = List.map (toTypeTester com ctx r) genArgs
267-
ofArray [ofString "tuple"; ofArray genArgs]
268-
| Fable.GenericParam name ->
269-
sprintf "Cannot resolve generic param %s for type testing, evals to true" name |> addWarning com [] r
270-
ofString "any"
271-
| Fable.DeclaredType(ent, _) when ent.IsInterface ->
272-
"Cannot type test interfaces, evals to false" |> addWarning com [] r
273-
ofString "unknown"
274-
| Fable.DeclaredType(ent, genArgs) ->
275-
match tryJsConstructor com ctx ent with
276-
| Some cons ->
277-
if not(List.isEmpty genArgs) then
278-
"Generic args are ignored in type testing" |> addWarning com [] r
279-
cons
280-
| None ->
281-
sprintf "Cannot type test %s, evals to false" ent.FullName |> addWarning com [] r
282-
ofString "unknown"
250+
let transformTypeTest (com: IBabelCompiler) ctx range expr (typ: Fable.Type): Expression =
251+
let warnAndEvalToFalse msg =
252+
"Cannot type test (evals to false): " + msg
253+
|> addWarning com [] range
254+
BooleanLiteral false :> Expression
283255

284-
let transformTypeTest (com: IBabelCompiler) ctx range (expr': Fable.Expr) (typ: Fable.Type): Expression =
285-
let (|EntityFullName|) (e: Fable.Entity) = e.FullName
256+
let jsTypeof (primitiveType: string) (Util.TransformExpr com ctx expr): Expression =
257+
let typeof = UnaryExpression(UnaryTypeof, expr)
258+
upcast BinaryExpression(BinaryEqualStrict, typeof, StringLiteral primitiveType, ?loc=range)
286259

287-
let expr = com.TransformAsExpr(ctx, expr')
288-
match typ with
289-
// Special cases
290-
| Fable.DeclaredType(EntityFullName Types.idisposable, _) ->
291-
match expr' with
292-
| MaybeCasted(ExprType(Fable.DeclaredType(ent2, _))) when FSharp2Fable.Util.hasInterface Types.idisposable ent2 ->
293-
upcast BooleanLiteral true
294-
| _ -> libCall com ctx None "Util" "isDisposable" [|expr|]
295-
| Fable.DeclaredType(EntityFullName Types.ienumerable, _) ->
296-
libCall com ctx None "Util" "isIterable" [|expr|]
297-
| Fable.DeclaredType(EntityFullName Types.array, _) -> // Untyped array
298-
libCall com ctx None "Util" "isArrayLike" [|expr|]
299-
| Fable.DeclaredType(EntityFullName Types.exception_, _) ->
300-
libCall com ctx None "Types" "isException" [|expr|]
301-
| _ ->
302-
let typeTester = toTypeTester com ctx range typ
303-
libCall com ctx range "Reflection" "typeTest" [|expr; typeTester|]
260+
let jsInstanceof consExpr (Util.TransformExpr com ctx expr): Expression =
261+
upcast BinaryExpression(BinaryInstanceOf, expr, consExpr, ?loc=range)
304262

263+
match typ with
264+
| Fable.Any -> upcast BooleanLiteral true
265+
| Fable.Unit -> upcast BinaryExpression(BinaryEqual, com.TransformAsExpr(ctx, expr), Util.undefined None, ?loc=range)
266+
| Fable.Boolean -> jsTypeof "boolean" expr
267+
| Fable.Char | Fable.String _ -> jsTypeof "string" expr
268+
| Fable.Number _ | Fable.Enum _ -> jsTypeof "number" expr
269+
| Fable.Regex -> jsInstanceof (Identifier "RegExp") expr
270+
| Fable.LambdaType _ | Fable.DelegateType _ -> jsTypeof "function" expr
271+
| Fable.Array _ | Fable.Tuple _ ->
272+
libCall com ctx None "Util" "isArrayLike" [|com.TransformAsExpr(ctx, expr)|]
273+
| Fable.List _ ->
274+
jsInstanceof (libValue com ctx "Types" "List") expr
275+
| Fable.AnonymousRecordType _ ->
276+
warnAndEvalToFalse "anonymous records"
277+
| Fable.MetaType ->
278+
jsInstanceof (libValue com ctx "Reflection" "TypeInfo") expr
279+
| Fable.Option _ -> warnAndEvalToFalse "options" // TODO
280+
| Fable.GenericParam _ -> warnAndEvalToFalse "generic parameters"
281+
| Fable.DeclaredType (ent, genArgs) ->
282+
match ent.FullName with
283+
| Types.idisposable ->
284+
match expr with
285+
| MaybeCasted(ExprType(Fable.DeclaredType (ent2, _)))
286+
when FSharp2Fable.Util.hasInterface Types.idisposable ent2 ->
287+
upcast BooleanLiteral true
288+
| _ -> libCall com ctx None "Util" "isDisposable" [|com.TransformAsExpr(ctx, expr)|]
289+
| Types.ienumerable ->
290+
[|com.TransformAsExpr(ctx, expr)|]
291+
|> libCall com ctx None "Util" "isIterable"
292+
| Types.array ->
293+
[|com.TransformAsExpr(ctx, expr)|]
294+
|> libCall com ctx None "Util" "isArrayLike"
295+
| Types.exception_ ->
296+
[|com.TransformAsExpr(ctx, expr)|]
297+
|> libCall com ctx None "Types" "isException"
298+
| _ when ent.IsInterface ->
299+
warnAndEvalToFalse "interfaces"
300+
| _ ->
301+
match tryJsConstructor com ctx ent with
302+
| Some cons ->
303+
// TODO: Emit warning only once per file?
304+
if not(List.isEmpty genArgs) then
305+
"Generic args are ignored in type testing"
306+
|> addWarning com [] range
307+
jsInstanceof cons expr
308+
| None ->
309+
warnAndEvalToFalse ent.FullName
305310

306311
// TODO: I'm trying to tell apart the code to generate annotations, but it's not a very clear distinction
307312
// as there are many dependencies from/to the Util module below
@@ -1119,20 +1124,19 @@ module Util =
11191124
[|TryStatement(transformBlock com ctx returnStrategy body,
11201125
?handler=handler, ?finalizer=finalizer, ?loc=r) :> Statement|]
11211126

1122-
// Even if IfStatement doesn't enforce it, compile both branches as blocks
1123-
// to prevent conflict (e.g. `then` doesn't become a block while `else` does)
1124-
let rec transformIfStatement (com: IBabelCompiler) ctx r ret (guardExpr: Expression) thenStmnt elseStmnt =
1125-
let thenStmnt = transformBlock com ctx ret thenStmnt
1126-
match elseStmnt: Fable.Expr with
1127-
| Fable.IfThenElse(TransformExpr com ctx guardExpr', thenStmnt', elseStmnt', r2) ->
1128-
let elseStmnt = transformIfStatement com ctx r2 ret guardExpr' thenStmnt' elseStmnt'
1129-
IfStatement(guardExpr, thenStmnt, elseStmnt, ?loc=r)
1130-
| expr ->
1131-
match com.TransformAsStatements(ctx, ret, expr) with
1132-
| [||] -> IfStatement(guardExpr, thenStmnt, ?loc=r)
1133-
| [|:? ExpressionStatement as e|] when (e.Expression :? NullLiteral) ->
1134-
IfStatement(guardExpr, thenStmnt, ?loc=r)
1135-
| statements -> IfStatement(guardExpr, thenStmnt, BlockStatement statements, ?loc=r)
1127+
let rec transformIfStatement (com: IBabelCompiler) ctx r ret guardExpr thenStmnt elseStmnt =
1128+
match com.TransformAsExpr(ctx, guardExpr) with
1129+
| :? BooleanLiteral as b when b.Value ->
1130+
com.TransformAsStatements(ctx, ret, thenStmnt)
1131+
| :? BooleanLiteral as b when not b.Value ->
1132+
com.TransformAsStatements(ctx, ret, elseStmnt)
1133+
| guardExpr ->
1134+
let thenStmnt = transformBlock com ctx ret thenStmnt
1135+
match com.TransformAsStatements(ctx, ret, elseStmnt) with
1136+
| [||] -> IfStatement(guardExpr, thenStmnt, ?loc=r) :> Statement
1137+
| [|elseStmnt|] -> IfStatement(guardExpr, thenStmnt, elseStmnt, ?loc=r) :> Statement
1138+
| statements -> IfStatement(guardExpr, thenStmnt, BlockStatement statements, ?loc=r) :> Statement
1139+
|> Array.singleton
11361140

11371141
let transformGet (com: IBabelCompiler) ctx range typ fableExpr (getKind: Fable.GetKind) =
11381142
match getKind with
@@ -1579,8 +1583,6 @@ module Util =
15791583
| Some(Fable.FieldKey fi) -> get None expr fi.Name |> Assign
15801584
com.TransformAsStatements(ctx, Some ret, value)
15811585

1582-
// Even if IfStatement doesn't enforce it, compile both branches as blocks
1583-
// to prevent conflicts (e.g. `then` doesn't become a block while `else` does)
15841586
| Fable.IfThenElse(guardExpr, thenExpr, elseExpr, r) ->
15851587
let asStatement =
15861588
match returnStrategy with
@@ -1591,10 +1593,7 @@ module Util =
15911593
Option.isSome ctx.TailCallOpportunity
15921594
|| (isJsStatement ctx false thenExpr) || (isJsStatement ctx false elseExpr)
15931595
if asStatement then
1594-
match com.TransformAsExpr(ctx, guardExpr) with
1595-
// In some situations (like some type tests) the condition may be always true
1596-
| :? BooleanLiteral as e when e.Value -> com.TransformAsStatements(ctx, returnStrategy, thenExpr)
1597-
| guardExpr -> [|transformIfStatement com ctx r returnStrategy guardExpr thenExpr elseExpr :> Statement|]
1596+
transformIfStatement com ctx r returnStrategy guardExpr thenExpr elseExpr
15981597
else
15991598
let guardExpr' = transformAsExpr com ctx guardExpr
16001599
let thenExpr' = transformAsExpr com ctx thenExpr

src/Fable.Transforms/Replacements.fs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,6 @@ let (|ReplaceName|_|) (namesAndReplacements: (string*string) list) name =
223223
namesAndReplacements |> List.tryPick (fun (name2, replacement) ->
224224
if name2 = name then Some replacement else None)
225225

226-
let inline (|ExprType|) (e: Expr) = e.Type
227-
228226
let (|OrDefault|) (def:'T) = function
229227
| Some v -> v
230228
| None -> def

src/fable-library/BigInt.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ module BigInt.Exports
22

33
type bigint = BigInt.BigInteger
44

5+
let isBigInt (x: obj) = x :? bigint
6+
57
let tryParse str res =
68
try
79
res := bigint.Parse str

src/fable-library/Reflection.ts

Lines changed: 2 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { value as getOptionValue } from "./Option.js";
2-
import { FSharpRef, List } from "./Types.js";
3-
import { compareArraysWith, equalArraysWith, isArrayLike, isUnionLike } from "./Util.js";
1+
import { FSharpRef } from "./Types.js";
2+
import { compareArraysWith, equalArraysWith, isUnionLike } from "./Util.js";
43

54
export type FieldInfo = [string, TypeInfo];
65
export type PropertyInfo = FieldInfo;
@@ -417,50 +416,3 @@ export function getCaseFields(x: any): any[] {
417416
assertUnion(x);
418417
return x.fields;
419418
}
420-
421-
type TypeTester =
422-
| "any"
423-
| "unknown"
424-
| "undefined"
425-
| "function"
426-
| "boolean"
427-
| "number"
428-
| "string"
429-
| ["tuple", TypeTester[]]
430-
| ["array", TypeTester | undefined]
431-
| ["list", TypeTester]
432-
| ["option", TypeTester]
433-
| FunctionConstructor
434-
435-
export function typeTest(x: any, typeTester: TypeTester): boolean {
436-
if (typeof typeTester === "string") {
437-
if (typeTester === "any") {
438-
return true;
439-
} else if (typeTester === "unknown") {
440-
return false;
441-
} else {
442-
return typeof x === typeTester;
443-
}
444-
} else if (Array.isArray(typeTester)) {
445-
switch (typeTester[0]) {
446-
case "tuple":
447-
return Array.isArray(x)
448-
&& x.length === typeTester[1].length
449-
&& x.every((x, i) => typeTest(x, typeTester[1][i]));
450-
case "array":
451-
return isArrayLike(x)
452-
&& (x.length === 0
453-
|| typeTester[1] == null
454-
|| typeTest(x[0], typeTester[1]));
455-
case "list":
456-
return x instanceof List
457-
&& (x.tail == null || typeTest(x.head, typeTester[1]));
458-
case "option":
459-
return x == null || typeTest(getOptionValue(x), typeTester[1]);
460-
default:
461-
return false
462-
}
463-
} else {
464-
return x instanceof typeTester;
465-
}
466-
}

src/quicktest/QuickTest.fs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,20 @@ let measureTime (f: unit -> unit) = emitJsStatement () """
6666
// to Fable.Tests project. For example:
6767
// testCase "Addition works" <| fun () ->
6868
// 2 + 2 |> equal 4
69+
70+
let test (exn: exn) =
71+
match exn with
72+
| :? System.NotSupportedException -> ()
73+
| :? System.SystemException -> ()
74+
| Failure _ -> raise exn
75+
| _ -> ()
76+
77+
let test2 () =
78+
printfn "foo"
79+
if 3 > 2 then
80+
"foo"
81+
elif false then
82+
"bar"
83+
else
84+
"baz"
85+

tests/Main/TypeTests.fs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -454,8 +454,7 @@ let tests =
454454
| :? bool -> "boolean"
455455
| :? unit -> "unit"
456456
| :? System.Text.RegularExpressions.Regex -> "RegExp"
457-
| :? (int[]) -> "int array"
458-
| :? (string[]) -> "string array"
457+
| :? (int[]) | :? (string[]) -> "Array"
459458
| _ -> "unknown"
460459
"A" :> obj |> test |> equal "string"
461460
3. :> obj |> test |> equal "number"
@@ -464,8 +463,8 @@ let tests =
464463
// Workaround to make sure Fable is passing the argument
465464
let a = () :> obj in test a |> equal "unit"
466465
System.Text.RegularExpressions.Regex(".") :> obj |> test |> equal "RegExp"
467-
[|"A"|] :> obj |> test |> equal "string array"
468-
[|1;2|] :> obj |> test |> equal "int array"
466+
[|"A"|] :> obj |> test |> equal "Array"
467+
[|1;2|] :> obj |> test |> equal "Array"
469468

470469
testCase "Type test with Date" <| fun () ->
471470
let isDate (x: obj) =

0 commit comments

Comments
 (0)