From 99c855ddb9cdea73a1924f5ed3e7421cdddde58e Mon Sep 17 00:00:00 2001 From: Gauthier Segay Date: Sun, 15 Oct 2023 11:30:10 +0200 Subject: [PATCH 1/9] draft for exposing DU constructor --- src/Fable.Transforms/Python/Fable2Python.fs | 52 +++++++++++++++------ 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.fs b/src/Fable.Transforms/Python/Fable2Python.fs index e740fdb97e..c4236b08c3 100644 --- a/src/Fable.Transforms/Python/Fable2Python.fs +++ b/src/Fable.Transforms/Python/Fable2Python.fs @@ -3689,6 +3689,16 @@ module Util = let transformUnion (com: IPythonCompiler) ctx (ent: Fable.Entity) (entName: string) classMembers = let fieldIds = getUnionFieldsAsIdents com ctx ent + let genTypeArgument = + let gen = + getGenericTypeParams [ fieldIds[1].Type ] + |> Set.toList + |> List.tryHead + + let ta = Expression.name (gen |> Option.defaultValue "Any") + let id = ident com ctx fieldIds[1] + Arg.arg (id, annotation = ta) + let args, isOptional = let args = fieldIds[0] @@ -3698,20 +3708,8 @@ module Util = Arg.arg (id, annotation = ta)) |> List.singleton - let varargs = - fieldIds[1] - |> ident com ctx - |> (fun id -> - let gen = - getGenericTypeParams [ fieldIds[1].Type ] - |> Set.toList - |> List.tryHead - - let ta = Expression.name (gen |> Option.defaultValue "Any") - Arg.arg (id, annotation = ta)) - let isOptional = Helpers.isOptional fieldIds - Arguments.arguments (args = args, vararg = varargs), isOptional + Arguments.arguments (args = args, vararg = genTypeArgument), isOptional let body = [ yield callSuperAsStatement [] @@ -3752,8 +3750,34 @@ module Util = Statement.functionDef (name, Arguments.arguments (), body = body, returns = returnType, decoratorList = decorators) + let constructors = + [ + for case in ent.UnionCases do + let name = Identifier case.Name + let args = + Arguments.arguments + [ + for field in case.UnionCaseFields do + Arg.arg(com.GetIdentifier(ctx, field.Name)) + ] + let decorators = [Expression.name "staticmethod"] + let body = + // todo + [Statement.return' (Expression.constant 1)] + + let returnType = + match args.VarArg with + | None -> Expression.name entName + | Some _ -> + Expression.subscript(Expression.name entName, Expression.name genTypeArgument.Arg) + Statement.functionDef(name, args, body = body, returns = returnType, decoratorList = decorators) + ] let baseExpr = libValue com ctx "types" "Union" |> Some - let classMembers = List.append [ cases ] classMembers + let classMembers = [ + cases + yield! constructors + yield! classMembers + ] declareType com ctx ent entName args isOptional body baseExpr classMembers let transformClassWithCompilerGeneratedConstructor (com: IPythonCompiler) ctx (ent: Fable.Entity) (entName: string) classMembers = From 1b6fd651bfa679d3aa47e62040bb97013b107d8c Mon Sep 17 00:00:00 2001 From: Gauthier Segay Date: Sun, 15 Oct 2023 11:30:10 +0200 Subject: [PATCH 2/9] draft for exposing DU constructor --- src/Fable.Transforms/Python/Fable2Python.fs | 52 +++++++++++++++------ 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.fs b/src/Fable.Transforms/Python/Fable2Python.fs index 0bdf708696..7e94a18e3f 100644 --- a/src/Fable.Transforms/Python/Fable2Python.fs +++ b/src/Fable.Transforms/Python/Fable2Python.fs @@ -3688,6 +3688,16 @@ module Util = let transformUnion (com: IPythonCompiler) ctx (ent: Fable.Entity) (entName: string) classMembers = let fieldIds = getUnionFieldsAsIdents com ctx ent + let genTypeArgument = + let gen = + getGenericTypeParams [ fieldIds[1].Type ] + |> Set.toList + |> List.tryHead + + let ta = Expression.name (gen |> Option.defaultValue "Any") + let id = ident com ctx fieldIds[1] + Arg.arg (id, annotation = ta) + let args, isOptional = let args = fieldIds[0] @@ -3697,20 +3707,8 @@ module Util = Arg.arg (id, annotation = ta)) |> List.singleton - let varargs = - fieldIds[1] - |> ident com ctx - |> (fun id -> - let gen = - getGenericTypeParams [ fieldIds[1].Type ] - |> Set.toList - |> List.tryHead - - let ta = Expression.name (gen |> Option.defaultValue "Any") - Arg.arg (id, annotation = ta)) - let isOptional = Helpers.isOptional fieldIds - Arguments.arguments (args = args, vararg = varargs), isOptional + Arguments.arguments (args = args, vararg = genTypeArgument), isOptional let body = [ yield callSuperAsStatement [] @@ -3751,8 +3749,34 @@ module Util = Statement.functionDef (name, Arguments.arguments (), body = body, returns = returnType, decoratorList = decorators) + let constructors = + [ + for case in ent.UnionCases do + let name = Identifier case.Name + let args = + Arguments.arguments + [ + for field in case.UnionCaseFields do + Arg.arg(com.GetIdentifier(ctx, field.Name)) + ] + let decorators = [Expression.name "staticmethod"] + let body = + // todo + [Statement.return' (Expression.constant 1)] + + let returnType = + match args.VarArg with + | None -> Expression.name entName + | Some _ -> + Expression.subscript(Expression.name entName, Expression.name genTypeArgument.Arg) + Statement.functionDef(name, args, body = body, returns = returnType, decoratorList = decorators) + ] let baseExpr = libValue com ctx "types" "Union" |> Some - let classMembers = List.append [ cases ] classMembers + let classMembers = [ + cases + yield! constructors + yield! classMembers + ] declareType com ctx ent entName args isOptional body baseExpr classMembers let transformClassWithCompilerGeneratedConstructor (com: IPythonCompiler) ctx (ent: Fable.Entity) (entName: string) classMembers = From 3ecd822e87423b2463da4e2586bfe399d85266a7 Mon Sep 17 00:00:00 2001 From: Gauthier Segay Date: Sat, 21 Oct 2023 13:07:58 +0200 Subject: [PATCH 3/9] generate appropriate constructor expression --- src/Fable.Transforms/Python/Fable2Python.fs | 27 ++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.fs b/src/Fable.Transforms/Python/Fable2Python.fs index 7e94a18e3f..e202006744 100644 --- a/src/Fable.Transforms/Python/Fable2Python.fs +++ b/src/Fable.Transforms/Python/Fable2Python.fs @@ -3751,7 +3751,9 @@ module Util = let constructors = [ - for case in ent.UnionCases do + + for tag, case in ent.UnionCases |> Seq.indexed do + let name = Identifier case.Name let args = Arguments.arguments @@ -3760,15 +3762,34 @@ module Util = Arg.arg(com.GetIdentifier(ctx, field.Name)) ] let decorators = [Expression.name "staticmethod"] + + + let values = + [ + for field in case.UnionCaseFields do + let identifier : Fable.Ident = + { Name = field.Name + Type = field.FieldType + IsMutable = false + IsThisArgument = true + IsCompilerGenerated = false + Range = None } + Fable.Expr.IdentExpr identifier + ] + + let unionExpr,_ = + Fable.Value(Fable.ValueKind.NewUnion(values, tag, ent.Ref, []),None) + |> transformAsExpr com ctx + let body = - // todo - [Statement.return' (Expression.constant 1)] + [Statement.return' unionExpr] let returnType = match args.VarArg with | None -> Expression.name entName | Some _ -> Expression.subscript(Expression.name entName, Expression.name genTypeArgument.Arg) + Statement.functionDef(name, args, body = body, returns = returnType, decoratorList = decorators) ] let baseExpr = libValue com ctx "types" "Union" |> Some From 2522b16f96d8e1729fc9dc49a3122a6d185aca77 Mon Sep 17 00:00:00 2001 From: Gauthier Segay Date: Sat, 21 Oct 2023 13:41:51 +0200 Subject: [PATCH 4/9] add type annotation to the constructor method parameters, add unit tests that emit the python calls and assert they are equivalent to the F# constructed values. --- src/Fable.Transforms/Python/Fable2Python.fs | 3 ++- tests/Python/TestUnionType.fs | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.fs b/src/Fable.Transforms/Python/Fable2Python.fs index 01d0affce5..986b654069 100644 --- a/src/Fable.Transforms/Python/Fable2Python.fs +++ b/src/Fable.Transforms/Python/Fable2Python.fs @@ -3759,7 +3759,8 @@ module Util = Arguments.arguments [ for field in case.UnionCaseFields do - Arg.arg(com.GetIdentifier(ctx, field.Name)) + let ta, _ = typeAnnotation com ctx None field.FieldType + Arg.arg(com.GetIdentifier(ctx, field.Name), ta) ] let decorators = [Expression.name "staticmethod"] diff --git a/tests/Python/TestUnionType.fs b/tests/Python/TestUnionType.fs index d581a27e0b..ed105c3687 100644 --- a/tests/Python/TestUnionType.fs +++ b/tests/Python/TestUnionType.fs @@ -1,5 +1,6 @@ module Fable.Tests.UnionTypes +open Fable.Core open Util.Testing type Gender = Male | Female @@ -205,3 +206,17 @@ let ``test Equality works in filter`` () = |> Array.filter (fun r -> r.Case = MyUnion3.Case1) |> Array.length |> equal 2 + +#if FABLE_COMPILER +[] +let ``test constructor exposed to python code`` () = + let u0 = MyUnion.Case0 + let u1 = MyUnion.Case1 "a" + let u2 = MyUnion.Case2 ("a","b") + let u3 = MyUnion.Case3 ("a","b","c") + let v0 : MyUnion = PyInterop.emitPyExpr () "MyUnion.Case0()" + let v1 : MyUnion = PyInterop.emitPyExpr "a" "MyUnion.Case1($0)" + let v2 : MyUnion = PyInterop.emitPyExpr ("a","b") "MyUnion.Case2($0,$1)" + let v3 : MyUnion = PyInterop.emitPyExpr ("a","b","c") "MyUnion.Case3($0,$1,$2)" + equal [u0;u1;u2;u3] [v0;v1;v2;v3] +#endif From 51bb31238888eed171567f835f666cac1e69d177 Mon Sep 17 00:00:00 2001 From: Gauthier Segay Date: Sat, 21 Oct 2023 13:50:10 +0200 Subject: [PATCH 5/9] (minor) formatting --- src/Fable.Transforms/Python/Fable2Python.fs | 95 ++++++++++----------- src/quicktest/QuickTest.fs | 77 ++--------------- 2 files changed, 51 insertions(+), 121 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.fs b/src/Fable.Transforms/Python/Fable2Python.fs index 986b654069..f5912d9dd0 100644 --- a/src/Fable.Transforms/Python/Fable2Python.fs +++ b/src/Fable.Transforms/Python/Fable2Python.fs @@ -3689,14 +3689,14 @@ module Util = let fieldIds = getUnionFieldsAsIdents com ctx ent let genTypeArgument = - let gen = - getGenericTypeParams [ fieldIds[1].Type ] - |> Set.toList - |> List.tryHead + let gen = + getGenericTypeParams [ fieldIds[1].Type ] + |> Set.toList + |> List.tryHead - let ta = Expression.name (gen |> Option.defaultValue "Any") - let id = ident com ctx fieldIds[1] - Arg.arg (id, annotation = ta) + let ta = Expression.name (gen |> Option.defaultValue "Any") + let id = ident com ctx fieldIds[1] + Arg.arg (id, annotation = ta) let args, isOptional = let args = @@ -3750,53 +3750,46 @@ module Util = Statement.functionDef (name, Arguments.arguments (), body = body, returns = returnType, decoratorList = decorators) let constructors = - [ - - for tag, case in ent.UnionCases |> Seq.indexed do - - let name = Identifier case.Name - let args = - Arguments.arguments - [ - for field in case.UnionCaseFields do - let ta, _ = typeAnnotation com ctx None field.FieldType - Arg.arg(com.GetIdentifier(ctx, field.Name), ta) - ] - let decorators = [Expression.name "staticmethod"] - - let values = - [ - for field in case.UnionCaseFields do - let identifier : Fable.Ident = - { Name = field.Name - Type = field.FieldType - IsMutable = false - IsThisArgument = true - IsCompilerGenerated = false - Range = None } - Fable.Expr.IdentExpr identifier - ] - - let unionExpr,_ = - Fable.Value(Fable.ValueKind.NewUnion(values, tag, ent.Ref, []),None) - |> transformAsExpr com ctx - - let body = - [Statement.return' unionExpr] - - let returnType = - match args.VarArg with - | None -> Expression.name entName - | Some _ -> - Expression.subscript(Expression.name entName, Expression.name genTypeArgument.Arg) - - Statement.functionDef(name, args, body = body, returns = returnType, decoratorList = decorators) + [ + for tag, case in ent.UnionCases |> Seq.indexed do + let name = Identifier case.Name + let args = + Arguments.arguments + [ + for field in case.UnionCaseFields do + let ta, _ = typeAnnotation com ctx None field.FieldType + Arg.arg(com.GetIdentifier(ctx, field.Name), ta) + ] + let decorators = [Expression.name "staticmethod"] + let values = + [ + for field in case.UnionCaseFields do + let identifier : Fable.Ident = + { Name = field.Name + Type = field.FieldType + IsMutable = false + IsThisArgument = true + IsCompilerGenerated = false + Range = None } + Fable.Expr.IdentExpr identifier + ] + let unionExpr,_ = + Fable.Value(Fable.ValueKind.NewUnion(values, tag, ent.Ref, []),None) + |> transformAsExpr com ctx + let body = + [Statement.return' unionExpr] + let returnType = + match args.VarArg with + | None -> Expression.name entName + | Some _ -> + Expression.subscript(Expression.name entName, Expression.name genTypeArgument.Arg) + Statement.functionDef(name, args, body = body, returns = returnType, decoratorList = decorators) ] let baseExpr = libValue com ctx "types" "Union" |> Some let classMembers = [ - cases - yield! constructors - yield! classMembers + cases + yield! constructors + yield! classMembers ] declareType com ctx ent entName args isOptional body baseExpr classMembers diff --git a/src/quicktest/QuickTest.fs b/src/quicktest/QuickTest.fs index 9d5fcc047f..9d0a2c962e 100644 --- a/src/quicktest/QuickTest.fs +++ b/src/quicktest/QuickTest.fs @@ -5,76 +5,13 @@ module QuickTest // When everything works, move the tests to the appropriate file in tests/Main. // Please don't add this file to your commits. -open System -open System.Collections.Generic -open Fable.Core -open Fable.Core.JsInterop -open Fable.Core.Testing +type U = Z of value: int | Y of name: string -let log (o: obj) = - JS.console.log(o) - // printfn "%A" o +let u1 = Z 1 +let u2 = Y "abc" -let equal expected actual = - let areEqual = expected = actual - printfn "%A = %A > %b" expected actual areEqual - if not areEqual then - failwithf "[ASSERT ERROR] Expected %A but got %A" expected actual +let a : U = Fable.Core.PyInterop.emitPyExpr 1 "U.Z($0)" +let b : U = Fable.Core.PyInterop.emitPyExpr "abc" "U.Y($0)" +Fable.Core.Testing.Assert.AreEqual(a, u1) +Fable.Core.Testing.Assert.AreEqual(b, u2) -let throwsError (expected: string) (f: unit -> 'a): unit = - let success = - try - f () |> ignore - true - with e -> - if not <| String.IsNullOrEmpty(expected) then - equal e.Message expected - false - // TODO better error messages - equal false success - -let testCase (msg: string) f: unit = - try - printfn "%s" msg - f () - with ex -> - printfn "%s" ex.Message - if ex.Message <> null && ex.Message.StartsWith("[ASSERT ERROR]") |> not then - printfn "%s" (ex.StackTrace ??= "") - printfn "" - -let testCaseAsync msg f = - testCase msg (fun () -> - async { - try - do! f () - with ex -> - printfn "%s" ex.Message - if ex.Message <> null && ex.Message.StartsWith("[ASSERT ERROR]") |> not then - printfn "%s" (ex.StackTrace ??= "") - } |> Async.StartImmediate) - -let throwsAnyError (f: unit -> 'a): unit = - let success = - try - f() |> ignore - true - with e -> - printfn "Got expected error: %s" e.Message - false - if success then - printfn "[ERROR EXPECTED]" - -let measureTime (f: unit -> unit): unit = emitJsStatement () """ - //js - const startTime = process.hrtime(); - f(); - const elapsed = process.hrtime(startTime); - console.log("Ms:", elapsed[0] * 1e3 + elapsed[1] / 1e6); - //!js -""" - -// Write here your unit test, you can later move it -// to Fable.Tests project. For example: -// testCase "Addition works" <| fun () -> -// 2 + 2 |> equal 4 From d1e44c877dd11558cc7af4978db072b3adecf88b Mon Sep 17 00:00:00 2001 From: Gauthier Segay Date: Sat, 21 Oct 2023 13:55:44 +0200 Subject: [PATCH 6/9] revert QuickTest.fs --- src/quicktest/QuickTest.fs | 77 ++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/src/quicktest/QuickTest.fs b/src/quicktest/QuickTest.fs index 9d0a2c962e..9d5fcc047f 100644 --- a/src/quicktest/QuickTest.fs +++ b/src/quicktest/QuickTest.fs @@ -5,13 +5,76 @@ module QuickTest // When everything works, move the tests to the appropriate file in tests/Main. // Please don't add this file to your commits. -type U = Z of value: int | Y of name: string +open System +open System.Collections.Generic +open Fable.Core +open Fable.Core.JsInterop +open Fable.Core.Testing -let u1 = Z 1 -let u2 = Y "abc" +let log (o: obj) = + JS.console.log(o) + // printfn "%A" o -let a : U = Fable.Core.PyInterop.emitPyExpr 1 "U.Z($0)" -let b : U = Fable.Core.PyInterop.emitPyExpr "abc" "U.Y($0)" -Fable.Core.Testing.Assert.AreEqual(a, u1) -Fable.Core.Testing.Assert.AreEqual(b, u2) +let equal expected actual = + let areEqual = expected = actual + printfn "%A = %A > %b" expected actual areEqual + if not areEqual then + failwithf "[ASSERT ERROR] Expected %A but got %A" expected actual +let throwsError (expected: string) (f: unit -> 'a): unit = + let success = + try + f () |> ignore + true + with e -> + if not <| String.IsNullOrEmpty(expected) then + equal e.Message expected + false + // TODO better error messages + equal false success + +let testCase (msg: string) f: unit = + try + printfn "%s" msg + f () + with ex -> + printfn "%s" ex.Message + if ex.Message <> null && ex.Message.StartsWith("[ASSERT ERROR]") |> not then + printfn "%s" (ex.StackTrace ??= "") + printfn "" + +let testCaseAsync msg f = + testCase msg (fun () -> + async { + try + do! f () + with ex -> + printfn "%s" ex.Message + if ex.Message <> null && ex.Message.StartsWith("[ASSERT ERROR]") |> not then + printfn "%s" (ex.StackTrace ??= "") + } |> Async.StartImmediate) + +let throwsAnyError (f: unit -> 'a): unit = + let success = + try + f() |> ignore + true + with e -> + printfn "Got expected error: %s" e.Message + false + if success then + printfn "[ERROR EXPECTED]" + +let measureTime (f: unit -> unit): unit = emitJsStatement () """ + //js + const startTime = process.hrtime(); + f(); + const elapsed = process.hrtime(startTime); + console.log("Ms:", elapsed[0] * 1e3 + elapsed[1] / 1e6); + //!js +""" + +// Write here your unit test, you can later move it +// to Fable.Tests project. For example: +// testCase "Addition works" <| fun () -> +// 2 + 2 |> equal 4 From 3b9c6382700beef33ee85add609fdbcae69438c1 Mon Sep 17 00:00:00 2001 From: Gauthier Segay Date: Sat, 21 Oct 2023 13:56:16 +0200 Subject: [PATCH 7/9] (minor) formatting --- tests/Python/TestUnionType.fs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/Python/TestUnionType.fs b/tests/Python/TestUnionType.fs index ed105c3687..6eacbf3005 100644 --- a/tests/Python/TestUnionType.fs +++ b/tests/Python/TestUnionType.fs @@ -210,13 +210,13 @@ let ``test Equality works in filter`` () = #if FABLE_COMPILER [] let ``test constructor exposed to python code`` () = - let u0 = MyUnion.Case0 - let u1 = MyUnion.Case1 "a" - let u2 = MyUnion.Case2 ("a","b") - let u3 = MyUnion.Case3 ("a","b","c") - let v0 : MyUnion = PyInterop.emitPyExpr () "MyUnion.Case0()" - let v1 : MyUnion = PyInterop.emitPyExpr "a" "MyUnion.Case1($0)" - let v2 : MyUnion = PyInterop.emitPyExpr ("a","b") "MyUnion.Case2($0,$1)" - let v3 : MyUnion = PyInterop.emitPyExpr ("a","b","c") "MyUnion.Case3($0,$1,$2)" - equal [u0;u1;u2;u3] [v0;v1;v2;v3] + let u0 = MyUnion.Case0 + let u1 = MyUnion.Case1 "a" + let u2 = MyUnion.Case2 ("a","b") + let u3 = MyUnion.Case3 ("a","b","c") + let v0 : MyUnion = PyInterop.emitPyExpr () "MyUnion.Case0()" + let v1 : MyUnion = PyInterop.emitPyExpr "a" "MyUnion.Case1($0)" + let v2 : MyUnion = PyInterop.emitPyExpr ("a","b") "MyUnion.Case2($0,$1)" + let v3 : MyUnion = PyInterop.emitPyExpr ("a","b","c") "MyUnion.Case3($0,$1,$2)" + equal [u0;u1;u2;u3] [v0;v1;v2;v3] #endif From 6419301e685f54aa99e8ff3587fcb145488a1f9f Mon Sep 17 00:00:00 2001 From: Gauthier Segay Date: Sat, 21 Oct 2023 13:57:35 +0200 Subject: [PATCH 8/9] (minor) import Fable.Core only under the #if --- tests/Python/TestUnionType.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Python/TestUnionType.fs b/tests/Python/TestUnionType.fs index 6eacbf3005..56d33b0f78 100644 --- a/tests/Python/TestUnionType.fs +++ b/tests/Python/TestUnionType.fs @@ -1,6 +1,5 @@ module Fable.Tests.UnionTypes -open Fable.Core open Util.Testing type Gender = Male | Female @@ -208,6 +207,7 @@ let ``test Equality works in filter`` () = |> equal 2 #if FABLE_COMPILER +open Fable.Core [] let ``test constructor exposed to python code`` () = let u0 = MyUnion.Case0 From c1d60a02d0e4a80f7ad3bce93768c6d2d2aa0437 Mon Sep 17 00:00:00 2001 From: Gauthier Segay Date: Sat, 21 Oct 2023 16:01:34 +0200 Subject: [PATCH 9/9] Update CHANGELOG.md --- src/Fable.Cli/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index bb3dc222bf..7e7c73fc28 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Remove support for Python 3.9. Add GH testing for Python 3.12 (by @dbrattli) * Support (un)curry up to 20 arguments (by @MangelMaxime) +* Expose discriminated union constructors to Python code (by @smoothdeveloper). #### Dart