diff --git a/.gitignore b/.gitignore index f5378f55bc..a475d735ea 100644 --- a/.gitignore +++ b/.gitignore @@ -201,3 +201,7 @@ docs/tools/FSharp.Formatting.svclog .ionide.debug *.bak project.lock.json + +# Python +.eggs/ +__pycache__/ \ No newline at end of file diff --git a/build.fsx b/build.fsx index cfca3839f3..f81ef641cc 100644 --- a/build.fsx +++ b/build.fsx @@ -170,6 +170,32 @@ let buildLibraryTs() = runInDir buildDirTs "npm run tsc -- --init --target es2020 --module es2020 --allowJs" runInDir buildDirTs ("npm run tsc -- --outDir ../../" + buildDirJs) +let buildLibraryPy() = + let libraryDir = "src/fable-library-py" + let projectDir = libraryDir + "/fable" + let buildDirPy = "build/fable-library-py" + + cleanDirs [buildDirPy] + + runFableWithArgs projectDir [ + "--outDir " + buildDirPy "fable" + "--fableLib " + buildDirPy "fable" + "--lang Python" + "--exclude Fable.Core" + ] + // Copy *.py from projectDir to buildDir + copyDirRecursive libraryDir buildDirPy + copyDirNonRecursive (buildDirPy "fable/fable-library") (buildDirPy "fable") + //copyFile (buildDirPy "fable/fable-library/*.py") (buildDirPy "fable") + copyFile (buildDirPy "fable/system.text.py") (buildDirPy "fable/system_text.py") + copyFile (buildDirPy "fable/fsharp.core.py") (buildDirPy "fable/fsharp_core.py") + copyFile (buildDirPy "fable/fsharp.collections.py") (buildDirPy "fable/fsharp_collections.py") + //copyFile (buildDirPy "fable/async.py") (buildDirPy "fable/async_.py") + removeFile (buildDirPy "fable/system.text.py") + + runInDir buildDirPy ("python3 --version") + runInDir buildDirPy ("python3 ./setup.py develop") + // Like testJs() but doesn't create bundles/packages for fable-standalone & friends // Mainly intended for CI let testJsFast() = @@ -368,6 +394,24 @@ let test() = if envVarOrNone "APPVEYOR" |> Option.isSome then testJsFast() +let testPython() = + buildLibraryIfNotExists() // NOTE: fable-library-py needs to be built seperatly. + + let projectDir = "tests/Python" + let buildDir = "build/tests/Python" + + cleanDirs [buildDir] + runInDir projectDir "dotnet test" + runFableWithArgs projectDir [ + "--outDir " + buildDir + "--exclude Fable.Core" + "--lang Python" + ] + + runInDir buildDir "touch __init__.py" // So relative imports works. + runInDir buildDir "pytest" + + let buildLocalPackageWith pkgDir pkgCommand fsproj action = let version = "3.0.0-local-build-" + DateTime.Now.ToString("yyyyMMdd-HHmm") action version @@ -525,9 +569,16 @@ match argsLower with | "test-react"::_ -> testReact() | "test-compiler"::_ -> testCompiler() | "test-integration"::_ -> testIntegration() +| "test-py"::_ -> testPython() | "quicktest"::_ -> buildLibraryIfNotExists() run "dotnet watch -p src/Fable.Cli run -- watch --cwd ../quicktest --exclude Fable.Core --noCache --runScript" +| "quicktest-py"::_ -> + buildLibraryIfNotExists() + run "dotnet watch -p src/Fable.Cli run -- watch --cwd ../quicktest --lang Python --exclude Fable.Core --noCache" +| "jupyter" :: _ -> + buildLibraryIfNotExists () + run "dotnet watch -p src/Fable.Cli run -- watch --cwd ../Fable.Jupyter/src --lang Python --exclude Fable.Core --noCache 2>> /Users/dbrattli/Developer/GitHub/Fable.Jupyter/src/fable.out" | "run"::_ -> buildLibraryIfNotExists() @@ -546,6 +597,7 @@ match argsLower with | ("watch-library")::_ -> watchLibrary() | ("fable-library"|"library")::_ -> buildLibrary() | ("fable-library-ts"|"library-ts")::_ -> buildLibraryTs() +| ("fable-library-py"|"library-py")::_ -> buildLibraryPy() | ("fable-compiler-js"|"compiler-js")::_ -> buildCompilerJs(minify) | ("fable-standalone"|"standalone")::_ -> buildStandalone {|minify=minify; watch=false|} | "watch-standalone"::_ -> buildStandalone {|minify=false; watch=true|} diff --git a/src/Fable.AST/Plugins.fs b/src/Fable.AST/Plugins.fs index bef25d4d8c..b0f07ce571 100644 --- a/src/Fable.AST/Plugins.fs +++ b/src/Fable.AST/Plugins.fs @@ -12,6 +12,7 @@ type Verbosity = type Language = | JavaScript | TypeScript + | Python | Php type CompilerOptions = diff --git a/src/Fable.Cli/Entry.fs b/src/Fable.Cli/Entry.fs index fc77b19dbf..cb98b749c5 100644 --- a/src/Fable.Cli/Entry.fs +++ b/src/Fable.Cli/Entry.fs @@ -75,8 +75,8 @@ Arguments: --sourceMapsRoot Set the value of the `sourceRoot` property in generated source maps --optimize Compile with optimized F# AST (experimental) - --lang|--language Compile to JavaScript (default), "TypeScript" or "Php". - Support for TypeScript and Php is experimental. + --lang|--language Compile to JavaScript (default), TypeScript, Php or Python. + Support for TypeScript, Php and Python is experimental. Environment variables: DOTNET_USE_POLLING_FILE_WATCHER @@ -92,6 +92,7 @@ let defaultFileExt language args = | None -> CompilerOptionsHelper.DefaultExtension match language with | TypeScript -> Path.replaceExtension ".ts" fileExt + | Python -> Path.replaceExtension ".py" fileExt | Php -> ".php" | _ -> fileExt @@ -102,6 +103,7 @@ let argLanguage args = |> Option.defaultValue "JavaScript" |> (function | "ts" | "typescript" | "TypeScript" -> TypeScript + | "py" | "python" | "Python" -> Python | "php" | "Php" | "PHP" -> Php | _ -> JavaScript) diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index 2e9da7b0e0..0ea7a82ceb 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -166,6 +166,29 @@ module private Util = member _.SourceMap = mapGenerator.Force().toJSON() + type PythonFileWriter(sourcePath: string, targetPath: string, cliArgs: CliArgs, dedupTargetDir) = + let fileExt = ".py" + let targetDir = Path.GetDirectoryName(targetPath) + // PEP8: Modules should have short, all-lowercase names + let fileName = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(targetPath)) + let fileName = Naming.applyCaseRule Core.CaseRules.SnakeCase fileName + // Note that Python modules cannot contain dots or it will be impossible to import them + let targetPath = Path.Combine(targetDir, fileName + fileExt) + + let stream = new IO.StreamWriter(targetPath) + + interface PythonPrinter.Writer with + member _.Write(str) = + stream.WriteAsync(str) |> Async.AwaitTask + member _.MakeImportPath(path) = + let projDir = IO.Path.GetDirectoryName(cliArgs.ProjectFile) + let path = Imports.getImportPath dedupTargetDir sourcePath targetPath projDir cliArgs.OutDir path + if path.EndsWith(".fs") then + let isInFableHiddenDir = Path.Combine(targetDir, path) |> Naming.isInFableHiddenDir + changeFsExtension isInFableHiddenDir path "" // Remove file extension + else path + member _.Dispose() = stream.Dispose() + let compileFile isRecompile (cliArgs: CliArgs) dedupTargetDir (com: CompilerImpl) = async { try let fable = @@ -194,12 +217,18 @@ module private Util = | Php -> let php = fable |> Fable2Php.transformFile com - + use w = new IO.StreamWriter(outPath) let ctx = PhpPrinter.Output.Writer.create w PhpPrinter.Output.writeFile ctx php w.Flush() + | Python -> + let python = fable |> Fable2Python.Compiler.transformFile com + let map = { new PythonPrinter.SourceMapGenerator with + member _.AddMapping(_,_,_,_,_) = () } + let writer = new PythonFileWriter(com.CurrentFile, outPath, cliArgs, dedupTargetDir) + do! PythonPrinter.run writer map python "Compiled " + File.getRelativePathFromCwd com.CurrentFile |> Log.verboseOrIf isRecompile diff --git a/src/Fable.Core/Fable.Core.PY.fs b/src/Fable.Core/Fable.Core.PY.fs new file mode 100644 index 0000000000..e5191c82db --- /dev/null +++ b/src/Fable.Core/Fable.Core.PY.fs @@ -0,0 +1,17 @@ +namespace Fable.Import + +namespace Fable.Core + +open System +open System.Text.RegularExpressions + +module PY = + type [] ArrayConstructor = + [] abstract Create: size: int -> 'T[] + abstract isArray: arg: obj -> bool + abstract from: arg: obj -> 'T[] + + [] + module Constructors = + + let [] Array: ArrayConstructor = pyNative diff --git a/src/Fable.Core/Fable.Core.PyInterop.fs b/src/Fable.Core/Fable.Core.PyInterop.fs new file mode 100644 index 0000000000..c09bcd5d91 --- /dev/null +++ b/src/Fable.Core/Fable.Core.PyInterop.fs @@ -0,0 +1,76 @@ +module Fable.Core.PyInterop + +open System +open Fable.Core + +/// Has same effect as `unbox` (dynamic casting erased in compiled Python code). +/// The casted type can be defined on the call site: `!!myObj?bar(5): float` +let (!!) x: 'T = pyNative + +/// Implicit cast for erased unions (U2, U3...) +let inline (!^) (x:^t1) : ^t2 = ((^t1 or ^t2) : (static member op_ErasedCast : ^t1 -> ^t2) x) + +/// Dynamically access a property of an arbitrary object. +/// `myObj?propA` in Python becomes `myObj.propA` +/// `myObj?(propA)` in Python becomes `myObj[propA]` +let (?) (o: obj) (prop: obj): 'a = pyNative + +/// Dynamically assign a value to a property of an arbitrary object. +/// `myObj?propA <- 5` in Python becomes `myObj.propA = 5` +/// `myObj?(propA) <- 5` in Python becomes `myObj[propA] = 5` +let (?<-) (o: obj) (prop: obj) (v: obj): unit = pyNative + +/// Destructure and apply a tuple to an arbitrary value. +/// E.g. `myFn $ (arg1, arg2)` in Python becomes `myFn(arg1, arg2)` +let ($) (callee: obj) (args: obj): 'a = pyNative + +/// Upcast the right operand to obj (and uncurry it if it's a function) and create a key-value tuple. +/// Mostly convenient when used with `createObj`. +/// E.g. `createObj [ "a" ==> 5 ]` in Python becomes `{ a: 5 }` +let (==>) (key: string) (v: obj): string*obj = pyNative + +/// Destructure a tuple of arguments and applies to literal Python code as with EmitAttribute. +/// E.g. `emitPyExpr (arg1, arg2) "$0 + $1"` in Python becomes `arg1 + arg2` +let emitPyExpr<'T> (args: obj) (pyCode: string): 'T = pyNative + +/// Same as emitPyExpr but intended for Python code that must appear in a statement position +/// E.g. `emitPyExpr aValue "while($0 < 5) doSomething()"` +let emitPyStatement<'T> (args: obj) (pyCode: string): 'T = pyNative + +/// Create a literal Python object from a collection of key-value tuples. +/// E.g. `createObj [ "a" ==> 5 ]` in Python becomes `{ a: 5 }` +let createObj (fields: #seq): obj = pyNative + +/// Create a literal Python object from a collection of union constructors. +/// E.g. `keyValueList CaseRules.LowerFirst [ MyUnion 4 ]` in Python becomes `{ myUnion: 4 }` +let keyValueList (caseRule: CaseRules) (li: 'T seq): obj = pyNative + +/// Create a literal Py object from a mutator lambda. Normally used when +/// the options interface has too many fields to be represented with a Pojo record. +/// E.g. `pyOptions (fun o -> o.foo <- 5)` in Python becomes `{ "foo": 5 }` +let pyOptions<'T> (f: 'T->unit): 'T = pyNative + +/// Create an empty Python object: {} +let createEmpty<'T> : 'T = pyNative + +[] +let pyTypeof (x: obj): string = pyNative + +[] +let pyInstanceof (x: obj) (cons: obj): bool = pyNative + +/// Works like `ImportAttribute` (same semantics as ES6 imports). +/// You can use "*" or "default" selectors. +let import<'T> (selector: string) (path: string):'T = pyNative + +/// F#: let myMember = importMember "myModule" +/// Py: from my_module import my_member +/// Note the import must be immediately assigned to a value in a let binding +let importMember<'T> (path: string):'T = pyNative + +/// F#: let myLib = importAll "myLib" +/// Py: from my_lib import * +let importAll<'T> (path: string):'T = pyNative + +/// Imports a file only for its side effects +let importSideEffects (path: string): unit = pyNative diff --git a/src/Fable.Core/Fable.Core.Util.fs b/src/Fable.Core/Fable.Core.Util.fs index 4ec6e49793..af9d024f10 100644 --- a/src/Fable.Core/Fable.Core.Util.fs +++ b/src/Fable.Core/Fable.Core.Util.fs @@ -9,6 +9,7 @@ module Util = try failwith "JS only" // try/catch is just for padding so it doesn't get optimized with ex -> raise ex + let inline pyNative<'T> : 'T = jsNative let inline phpNative<'T> : 'T = jsNative module Experimental = diff --git a/src/Fable.Core/Fable.Core.fsproj b/src/Fable.Core/Fable.Core.fsproj index 65d5e8c34a..3e1f2d3649 100644 --- a/src/Fable.Core/Fable.Core.fsproj +++ b/src/Fable.Core/Fable.Core.fsproj @@ -12,8 +12,10 @@ + + diff --git a/src/Fable.Transforms/Fable.Transforms.fsproj b/src/Fable.Transforms/Fable.Transforms.fsproj index 99752209a6..51ca895816 100644 --- a/src/Fable.Transforms/Fable.Transforms.fsproj +++ b/src/Fable.Transforms/Fable.Transforms.fsproj @@ -21,6 +21,9 @@ + + + diff --git a/src/Fable.Transforms/Global/Babel.fs b/src/Fable.Transforms/Global/Babel.fs index 4061a21d5a..1916ff2cc4 100644 --- a/src/Fable.Transforms/Global/Babel.fs +++ b/src/Fable.Transforms/Global/Babel.fs @@ -398,7 +398,6 @@ type ImportSpecifier = /// A namespace import specifier, e.g., * as foo in import * as foo from "mod". | ImportNamespaceSpecifier of local: Identifier - /// An exported variable binding, e.g., {foo} in export {foo} or {bar as foo} in export {bar as foo}. /// The exported field refers to the name exported in the module. /// The local field refers to the binding into the local module scope. @@ -409,7 +408,6 @@ type ExportSpecifier = | ExportSpecifier of local: Identifier * exported: Identifier // Type Annotations - type TypeAnnotationInfo = | AnyTypeAnnotation | VoidTypeAnnotation diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 8dfeeba503..4c2db25780 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -14,6 +14,7 @@ type CompilerOptionsHelper = ?clampByteArrays) = let define = defaultArg define [] let isDebug = List.contains "DEBUG" define + { new CompilerOptions with member _.Define = define member _.DebugMode = isDebug diff --git a/src/Fable.Transforms/Python/Fable2Python.fs b/src/Fable.Transforms/Python/Fable2Python.fs new file mode 100644 index 0000000000..d1c6962012 --- /dev/null +++ b/src/Fable.Transforms/Python/Fable2Python.fs @@ -0,0 +1,2421 @@ +module rec Fable.Transforms.Fable2Python + +open Fable +open Fable.AST +open Fable.AST.Python +open System.Collections.Generic +open System.Text.RegularExpressions +open Fable.Naming +open Fable.Core + +type ReturnStrategy = + | Return + | ReturnUnit + | Assign of Expression + | Target of Identifier + +type Import = + { Module: string + LocalIdent: Identifier option + Name: string option } + +type ITailCallOpportunity = + abstract Label: string + abstract Args: string list + abstract IsRecursiveRef: Fable.Expr -> bool + +type UsedNames = + { RootScope: HashSet + DeclarationScopes: HashSet + CurrentDeclarationScope: HashSet } + +/// Python specific, used for keeping track of existing variable bindings to +/// know if we need to declare an identifier as nonlocal or global. +type BoundVars = + { GlobalScope: HashSet + EnclosingScope: HashSet + LocalScope: HashSet } + + member this.EnterScope () = + // printfn "EnterScope" + let enclosingScope = HashSet() + enclosingScope.UnionWith(this.EnclosingScope) + enclosingScope.UnionWith(this.LocalScope) + { this with LocalScope = HashSet (); EnclosingScope = enclosingScope } + + member this.Bind(name: string) = + // printfn "Bind: %A" name + this.LocalScope.Add name |> ignore + + member this.Bind(ids: Identifier list) = + // printfn "Bind: %A" ids + for (Identifier name) in ids do + this.LocalScope.Add name |> ignore + + member this.NonLocals(idents: Identifier list) = + // printfn "NonLocals: %A" (idents, this) + [ + for ident in idents do + let (Identifier name) = ident + if not (this.LocalScope.Contains name) && this.EnclosingScope.Contains name then + yield ident + else + this.Bind(name) + ] +type Context = + { File: Fable.File + UsedNames: UsedNames + BoundVars: BoundVars + DecisionTargets: (Fable.Ident list * Fable.Expr) list + HoistVars: Fable.Ident list -> bool + TailCallOpportunity: ITailCallOpportunity option + OptimizeTailCall: unit -> unit + ScopedTypeParams: Set } + +type IPythonCompiler = + inherit Compiler + abstract GetIdentifier: ctx: Context * name: string -> Python.Identifier + abstract GetIdentifierAsExpr: ctx: Context * name: string -> Python.Expression + abstract GetAllImports: unit -> seq + abstract GetImportExpr: Context * moduleName: string * ?name: string * ?loc: SourceLocation -> Expression + abstract TransformAsExpr: Context * Fable.Expr -> Expression * Statement list + abstract TransformAsStatements: Context * ReturnStrategy option * Fable.Expr -> Statement list + abstract TransformImport: Context * selector:string * path:string -> Expression + abstract TransformFunction: Context * string option * Fable.Ident list * Fable.Expr -> Arguments * Statement list + + abstract WarnOnlyOnce: string * ?range: SourceLocation -> unit + +// TODO: All things that depend on the library should be moved to Replacements +// to become independent of the specific implementation +module Lib = + let libCall (com: IPythonCompiler) ctx r moduleName memberName args = + Expression.call(com.TransformImport(ctx, memberName, getLibPath com moduleName), args, ?loc=r) + + let libConsCall (com: IPythonCompiler) ctx r moduleName memberName args = + Expression.call(com.TransformImport(ctx, memberName, getLibPath com moduleName), args, ?loc=r) + + let libValue (com: IPythonCompiler) ctx moduleName memberName = + com.TransformImport(ctx, memberName, getLibPath com moduleName) + + let tryJsConstructor (com: IPythonCompiler) ctx ent = + match Replacements.tryJsConstructor com ent with + | Some e -> com.TransformAsExpr(ctx, e) |> Some + | None -> None + + let jsConstructor (com: IPythonCompiler) ctx ent = + let entRef = Replacements.jsConstructor com ent + com.TransformAsExpr(ctx, entRef) + +// TODO: This is too implementation-dependent, ideally move it to Replacements +module Reflection = + open Lib + + let private libReflectionCall (com: IPythonCompiler) ctx r memberName args = + libCall com ctx r "Reflection" (memberName + "_type") args + + let private transformRecordReflectionInfo com ctx r (ent: Fable.Entity) generics = + // TODO: Refactor these three bindings to reuse in transformUnionReflectionInfo + let fullname = ent.FullName + let fullnameExpr = Expression.constant(fullname) + let genMap = + let genParamNames = ent.GenericParameters |> List.mapToArray (fun x -> x.Name) |> Seq.toList + List.zip genParamNames generics |> Map + let fields, stmts = + ent.FSharpFields |> Seq.map (fun fi -> + let typeInfo, stmts = transformTypeInfo com ctx r genMap fi.FieldType + (Expression.list([ Expression.constant(fi.Name); typeInfo ])), stmts) + |> Seq.toList + |> Helpers.unzipArgs + let fields = Expression.lambda(Arguments.arguments [], Expression.list(fields)) + let js, stmts' = jsConstructor com ctx ent + [ fullnameExpr; Expression.list(generics); js; fields ] + |> libReflectionCall com ctx None "record", stmts @ stmts' + + let private transformUnionReflectionInfo com ctx r (ent: Fable.Entity) generics = + let fullname = ent.FullName + let fullnameExpr = Expression.constant(fullname) + let genMap = + let genParamNames = ent.GenericParameters |> List.map (fun x -> x.Name) |> Seq.toList + List.zip genParamNames generics |> Map + let cases = + ent.UnionCases |> Seq.map (fun uci -> + uci.UnionCaseFields |> List.map (fun fi -> + Expression.list([ + fi.Name |> Expression.constant + let expr, stmts = transformTypeInfo com ctx r genMap fi.FieldType + expr + ])) + |> Expression.list + ) |> Seq.toList + let cases = Expression.lambda(Arguments.arguments [], Expression.list(cases)) + let js, stmts = jsConstructor com ctx ent + [ fullnameExpr; Expression.list(generics); js; cases ] + |> libReflectionCall com ctx None "union", stmts + + let transformTypeInfo (com: IPythonCompiler) ctx r (genMap: Map) t: Expression * Statement list = + let primitiveTypeInfo name = + libValue com ctx "Reflection" (name + "_type") + let numberInfo kind = + getNumberKindName kind + |> primitiveTypeInfo + let nonGenericTypeInfo fullname = + [ Expression.constant(fullname) ] + |> libReflectionCall com ctx None "class" + let resolveGenerics generics: Expression list * Statement list = + generics |> Array.map (transformTypeInfo com ctx r genMap) |> List.ofArray |> Helpers.unzipArgs + let genericTypeInfo name genArgs = + let resolved, stmts = resolveGenerics genArgs + libReflectionCall com ctx None name resolved, stmts + let genericEntity (fullname: string) (generics: Expression list) = + libReflectionCall com ctx None "class" [ + Expression.constant(fullname) + if not(List.isEmpty generics) then + Expression.list(generics) + ] + match t with + | Fable.Measure _ + | Fable.Any -> primitiveTypeInfo "obj", [] + | Fable.GenericParam(name,_) -> + match Map.tryFind name genMap with + | Some t -> t, [] + | None -> + Replacements.genericTypeInfoError name |> addError com [] r + Expression.none(), [] + | Fable.Unit -> primitiveTypeInfo "unit", [] + | Fable.Boolean -> primitiveTypeInfo "bool", [] + | Fable.Char -> primitiveTypeInfo "char", [] + | Fable.String -> primitiveTypeInfo "string", [] + | Fable.Enum entRef -> + let ent = com.GetEntity(entRef) + let mutable numberKind = Int32 + let cases = + ent.FSharpFields |> Seq.choose (fun fi -> + // F# seems to include a field with this name in the underlying type + match fi.Name with + | "value__" -> + match fi.FieldType with + | Fable.Number(kind,_) -> numberKind <- kind + | _ -> () + None + | name -> + let value = match fi.LiteralValue with Some v -> System.Convert.ToDouble v | None -> 0. + Expression.list([ Expression.constant(name); Expression.constant(value) ]) |> Some) + |> Seq.toList + |> Expression.list + [ Expression.constant(entRef.FullName); numberInfo numberKind; cases ] + |> libReflectionCall com ctx None "enum", [] + | Fable.Number(kind,_) -> + numberInfo kind, [] + | Fable.LambdaType(argType, returnType) -> + genericTypeInfo "lambda" [|argType; returnType|] + | Fable.DelegateType(argTypes, returnType) -> + genericTypeInfo "delegate" ([|yield! argTypes; yield returnType|]) + | Fable.Tuple(genArgs,_) -> genericTypeInfo "tuple" (List.toArray genArgs) + | Fable.Option(genArg,_) -> genericTypeInfo "option" [|genArg|] + | Fable.Array genArg -> genericTypeInfo "array" [|genArg|] + | Fable.List genArg -> genericTypeInfo "list" [|genArg|] + | Fable.Regex -> nonGenericTypeInfo Types.regex, [] + | Fable.MetaType -> nonGenericTypeInfo Types.type_, [] + | Fable.AnonymousRecordType(fieldNames, genArgs) -> + let genArgs, stmts = resolveGenerics (List.toArray genArgs) + List.zip (List.ofArray fieldNames) genArgs + |> List.map (fun (k, t) -> Expression.list[ Expression.constant(k); t ]) + |> libReflectionCall com ctx None "anonRecord", stmts + | Fable.DeclaredType(entRef, generics) -> + let fullName = entRef.FullName + match fullName, generics with + | Replacements.BuiltinEntity kind -> + match kind with + | Replacements.BclGuid + | Replacements.BclTimeSpan + | Replacements.BclDateTime + | Replacements.BclDateTimeOffset + | Replacements.BclTimer + | Replacements.BclInt64 + | Replacements.BclUInt64 + | Replacements.BclDecimal + | Replacements.BclBigInt -> genericEntity fullName [], [] + | Replacements.BclHashSet gen + | Replacements.FSharpSet gen -> + let gens, stmts = transformTypeInfo com ctx r genMap gen + genericEntity fullName [ gens ], stmts + | Replacements.BclDictionary(key, value) + | Replacements.BclKeyValuePair(key, value) + | Replacements.FSharpMap(key, value) -> + let keys, stmts = transformTypeInfo com ctx r genMap key + let values, stmts' = transformTypeInfo com ctx r genMap value + genericEntity fullName [ + keys + values + ], stmts @ stmts' + | Replacements.FSharpResult(ok, err) -> + let ent = com.GetEntity(entRef) + let ok', stmts = transformTypeInfo com ctx r genMap ok + let err', stmts' = transformTypeInfo com ctx r genMap err + let expr, stmts'' =transformUnionReflectionInfo com ctx r ent [ ok'; err' ] + expr, stmts @ stmts' @ stmts'' + | Replacements.FSharpChoice gen -> + let ent = com.GetEntity(entRef) + let gen, stmts = List.map (transformTypeInfo com ctx r genMap) gen |> Helpers.unzipArgs + let expr, stmts' = gen |> transformUnionReflectionInfo com ctx r ent + expr, stmts @ stmts' + | Replacements.FSharpReference gen -> + let ent = com.GetEntity(entRef) + let gen, stmts = transformTypeInfo com ctx r genMap gen + let expr, stmts' = [ gen ] |> transformRecordReflectionInfo com ctx r ent + expr, stmts @ stmts' + | _ -> + let ent = com.GetEntity(entRef) + let generics, stmts = generics |> List.map (transformTypeInfo com ctx r genMap) |> Helpers.unzipArgs + /// Check if the entity is actually declared in JS code + if ent.IsInterface + || FSharp2Fable.Util.isErasedOrStringEnumEntity ent + || FSharp2Fable.Util.isGlobalOrImportedEntity ent + || FSharp2Fable.Util.isReplacementCandidate ent then + genericEntity ent.FullName generics, stmts + else + let reflectionMethodExpr = FSharp2Fable.Util.entityRefWithSuffix com ent Naming.reflectionSuffix + let callee, stmts' = com.TransformAsExpr(ctx, reflectionMethodExpr) + Expression.call(callee, generics), stmts @ stmts' + + let transformReflectionInfo com ctx r (ent: Fable.Entity) generics = + if ent.IsFSharpRecord then + transformRecordReflectionInfo com ctx r ent generics + elif ent.IsFSharpUnion then + transformUnionReflectionInfo com ctx r ent generics + else + let fullname = ent.FullName + let exprs, stmts = + [ + yield Expression.constant(fullname), [] + match generics with + | [] -> yield Util.undefined None, [] + | generics -> yield Expression.list(generics), [] + match tryJsConstructor com ctx ent with + | Some (cons, stmts) -> yield cons, stmts + | None -> () + match ent.BaseType with + | Some d -> + let genMap = + Seq.zip ent.GenericParameters generics + |> Seq.map (fun (p, e) -> p.Name, e) + |> Map + yield Fable.DeclaredType(d.Entity, d.GenericArgs) + |> transformTypeInfo com ctx r genMap + | None -> () + ] + |> Helpers.unzipArgs + exprs + |> libReflectionCall com ctx r "class", stmts + + let private ofString s = Expression.constant(s) + let private ofArray babelExprs = Expression.list(babelExprs) + + let transformTypeTest (com: IPythonCompiler) ctx range expr (typ: Fable.Type): Expression * Statement list = + let warnAndEvalToFalse msg = + "Cannot type test (evals to false): " + msg + |> addWarning com [] range + Expression.constant(false) + + let jsTypeof (primitiveType: string) (Util.TransformExpr com ctx (expr, stmts)): Expression * Statement list = + let typeof = Expression.unaryOp(UnaryTypeof, expr) + Expression.compare(typeof, [ Eq ], [ Expression.constant(primitiveType)], ?loc=range), stmts + + let jsInstanceof consExpr (Util.TransformExpr com ctx (expr, stmts)): Expression * Statement list= + let func = Expression.name (Python.Identifier("isinstance")) + let args = [ expr; consExpr ] + Expression.call (func, args), stmts + + match typ with + | Fable.Measure _ // Dummy, shouldn't be possible to test against a measure type + | Fable.Any -> Expression.constant(true), [] + | Fable.Unit -> + let expr, stmts = com.TransformAsExpr(ctx, expr) + Expression.compare(expr, [ Is ], [ Util.undefined None ], ?loc=range), stmts + | Fable.Boolean -> jsTypeof "boolean" expr + | Fable.Char | Fable.String _ -> jsTypeof "string" expr + | Fable.Number _ | Fable.Enum _ -> jsTypeof "number" expr + | Fable.Regex -> jsInstanceof (Expression.identifier("RegExp")) expr + | Fable.LambdaType _ | Fable.DelegateType _ -> jsTypeof "function" expr + | Fable.Array _ | Fable.Tuple _ -> + let expr, stmts = com.TransformAsExpr(ctx, expr) + libCall com ctx None "Util" "isArrayLike" [ expr ], stmts + | Fable.List _ -> + jsInstanceof (libValue com ctx "List" "FSharpList") expr + | Fable.AnonymousRecordType _ -> + warnAndEvalToFalse "anonymous records", [] + | Fable.MetaType -> + jsInstanceof (libValue com ctx "Reflection" "TypeInfo") expr + | Fable.Option _ -> warnAndEvalToFalse "options", [] // TODO + | Fable.GenericParam _ -> warnAndEvalToFalse "generic parameters", [] + | Fable.DeclaredType (ent, genArgs) -> + match ent.FullName with + | Types.idisposable -> + match expr with + | MaybeCasted(ExprType(Fable.DeclaredType (ent2, _))) + when com.GetEntity(ent2) |> FSharp2Fable.Util.hasInterface Types.idisposable -> + Expression.constant(true), [] + | _ -> + let expr, stmts = com.TransformAsExpr(ctx, expr) + libCall com ctx None "Util" "isDisposable" [ expr ], stmts + | Types.ienumerable -> + let expr, stmts = com.TransformAsExpr(ctx, expr) + [ expr ] + |> libCall com ctx None "Util" "isIterable", stmts + | Types.array -> + let expr, stmts = com.TransformAsExpr(ctx, expr) + [ expr ] + |> libCall com ctx None "Util" "isArrayLike", stmts + | Types.exception_ -> + let expr, stmts = com.TransformAsExpr(ctx, expr) + [ expr ] + |> libCall com ctx None "Types" "isException", stmts + | _ -> + let ent = com.GetEntity(ent) + if ent.IsInterface then + warnAndEvalToFalse "interfaces", [] + else + match tryJsConstructor com ctx ent with + | Some (cons, stmts) -> + if not(List.isEmpty genArgs) then + com.WarnOnlyOnce("Generic args are ignored in type testing", ?range=range) + let expr, stmts' = jsInstanceof cons expr + expr, stmts @ stmts' + | None -> + warnAndEvalToFalse ent.FullName, [] + +module Helpers = + let index = (Seq.initInfinite id).GetEnumerator() + + let getUniqueIdentifier (name: string): Python.Identifier = + do index.MoveNext() |> ignore + let idx = index.Current.ToString() + Python.Identifier($"{name}_{idx}") + + /// Replaces all '$' and `.`with '_' + let clean (name: string) = + //printfn $"clean: {name}" + match name with + | "this" -> "self" + | "async" -> "async_" + | "from" -> "from_" + | "class" -> "class_" + | "for" -> "for_" + | "Math" -> "math" + | "Error" -> "Exception" + | "toString" -> "str" + | "len" -> "len_" + | "Map" -> "dict" + | "Int32Array" -> "list" + | _ -> + name |> String.map(fun c -> if List.contains c ['.'; '$'; '`'; '*'; ' '] then '_' else c) + + let rewriteFableImport moduleName = + //printfn "ModuleName: %s" moduleName + let _reFableLib = + Regex(".*(\/fable-library.*)?\/(?[^\/]*)\.(js|fs)", RegexOptions.Compiled) + + let m = _reFableLib.Match(moduleName) + let dashify = applyCaseRule CaseRules.SnakeCase + + if m.Groups.Count > 1 then + let pymodule = + m.Groups.["module"].Value + |> dashify + |> clean + + let moduleName = String.concat "." [ "fable"; pymodule ] + + //printfn "-> Module: %A" moduleName + moduleName + else + // Modules should have short, all-lowercase names. + let moduleName = + let name = + moduleName.Replace("/", "") + |> dashify + string(name.[0]) + name.[1..].Replace(".", "_") + + //printfn "-> Module: %A" moduleName + moduleName + + let unzipArgs (args: (Python.Expression * Python.Statement list) list): Python.Expression list * Python.Statement list = + let stmts = args |> List.map snd |> List.collect id + let args = args |> List.map fst + args, stmts + + /// A few statements in the generated Python AST do not produce any effect, + /// and should not be printet. + let isProductiveStatement (stmt: Python.Statement) = + let rec hasNoSideEffects (e: Python.Expression) = + //printfn $"hasNoSideEffects: {e}" + + match e with + | Constant _ -> true + | Dict { Keys = keys } -> keys.IsEmpty // Empty object + | Name _ -> true // E.g `void 0` is translated to Name(None) + | _ -> false + + match stmt with + | Expr expr -> + if hasNoSideEffects expr.Value then + None + else + Some stmt + | _ -> Some stmt + + +module Util = + open Lib + open Reflection + + let getIdentifier (com: IPythonCompiler) (ctx: Context) (name: string) = + let name = Helpers.clean name + + match name with + | "math" -> com.GetImportExpr(ctx, "math") |> ignore + | _ -> () + + Python.Identifier name + + let (|TransformExpr|) (com: IPythonCompiler) ctx e : Expression * Statement list = + com.TransformAsExpr(ctx, e) + + let (|Function|_|) = function + | Fable.Lambda(arg, body, _) -> Some([arg], body) + | Fable.Delegate(args, body, _) -> Some(args, body) + | _ -> None + + let (|Lets|_|) = function + | Fable.Let(ident, value, body) -> Some([ident, value], body) + | Fable.LetRec(bindings, body) -> Some(bindings, body) + | _ -> None + + let discardUnitArg (args: Fable.Ident list) = + match args with + | [] -> [] + | [unitArg] when unitArg.Type = Fable.Unit -> [] + | [thisArg; unitArg] when thisArg.IsThisArgument && unitArg.Type = Fable.Unit -> [thisArg] + | args -> args + + let getUniqueNameInRootScope (ctx: Context) name = + let name = Helpers.clean name + let name = (name, Naming.NoMemberPart) ||> Naming.sanitizeIdent (fun name -> + ctx.UsedNames.RootScope.Contains(name) + || ctx.UsedNames.DeclarationScopes.Contains(name)) + ctx.UsedNames.RootScope.Add(name) |> ignore + name + + let getUniqueNameInDeclarationScope (ctx: Context) name = + let name = (name, Naming.NoMemberPart) ||> Naming.sanitizeIdent (fun name -> + ctx.UsedNames.RootScope.Contains(name) || ctx.UsedNames.CurrentDeclarationScope.Contains(name)) + ctx.UsedNames.CurrentDeclarationScope.Add(name) |> ignore + name + + type NamedTailCallOpportunity(_com: Compiler, ctx, name, args: Fable.Ident list) = + // Capture the current argument values to prevent delayed references from getting corrupted, + // for that we use block-scoped ES2015 variable declarations. See #681, #1859 + // TODO: Local unique ident names + let argIds = discardUnitArg args |> List.map (fun arg -> + getUniqueNameInDeclarationScope ctx (arg.Name + "_mut")) + interface ITailCallOpportunity with + member _.Label = name + member _.Args = argIds + member _.IsRecursiveRef(e) = + match e with Fable.IdentExpr id -> name = id.Name | _ -> false + + let getDecisionTarget (ctx: Context) targetIndex = + match List.tryItem targetIndex ctx.DecisionTargets with + | None -> failwithf "Cannot find DecisionTree target %i" targetIndex + | Some(idents, target) -> idents, target + + let rec isPyStatement ctx preferStatement (expr: Fable.Expr) = + match expr with + | Fable.Value _ | Fable.Import _ | Fable.IdentExpr _ + | Fable.Lambda _ | Fable.Delegate _ | Fable.ObjectExpr _ + | Fable.Call _ | Fable.CurriedApply _ | Fable.Operation _ + | Fable.Get _ | Fable.Test _ | Fable.TypeCast _ -> false + + | Fable.TryCatch _ + | Fable.Sequential _ | Fable.Let _ | Fable.LetRec _ | Fable.Set _ + | Fable.ForLoop _ | Fable.WhileLoop _ -> true + | Fable.Extended(kind, _) -> + match kind with + | Fable.Throw _ | Fable.Return _ | Fable.Break _ | Fable.Debugger -> true + | Fable.Curry _ -> false + + // TODO: If IsJsSatement is false, still try to infer it? See #2414 + // /^\s*(break|continue|debugger|while|for|switch|if|try|let|const|var)\b/ + | Fable.Emit(i,_,_) -> i.IsStatement + + | Fable.DecisionTreeSuccess(targetIndex,_, _) -> + getDecisionTarget ctx targetIndex + |> snd |> isPyStatement ctx preferStatement + + // Make it also statement if we have more than, say, 3 targets? + // That would increase the chances to convert it into a switch + | Fable.DecisionTree(_,targets) -> + preferStatement + || List.exists (snd >> (isPyStatement ctx false)) targets + + | Fable.IfThenElse(_,thenExpr,elseExpr,_) -> + preferStatement || isPyStatement ctx false thenExpr || isPyStatement ctx false elseExpr + + let addErrorAndReturnNull (com: Compiler) (range: SourceLocation option) (error: string) = + addError com [] range error + Expression.name (Python.Identifier("None")) + + let ident (com: IPythonCompiler) (ctx: Context) (id: Fable.Ident) = + com.GetIdentifier(ctx, id.Name) + + let identAsExpr (com: IPythonCompiler) (ctx: Context) (id: Fable.Ident) = + com.GetIdentifierAsExpr(ctx, id.Name) + + let thisExpr = + Expression.name("self") + + let ofInt (i: int) = + Expression.constant(float i) + + let ofString (s: string) = + Expression.constant(s) + + let memberFromName (com: IPythonCompiler) (ctx: Context) (memberName: string): Expression * Statement list = + match memberName with + | "ToString" -> Expression.identifier("toString"), [] + | n when n.StartsWith("Symbol.iterator") -> + let name = Identifier "__iter__" + Expression.name(name), [] + | n when Naming.hasIdentForbiddenChars n -> Expression.constant(n), [] + | n -> com.GetIdentifierAsExpr(ctx, n), [] + + let get (com: IPythonCompiler) ctx r left memberName subscript = + // printfn "get: %A" (left, memberName) + match subscript with + | true -> + let expr = Expression.constant(memberName) + Expression.subscript (value = left, slice = expr, ctx = Load) + | _ -> + let expr = com.GetIdentifier(ctx, memberName) + Expression.attribute (value = left, attr = expr, ctx = Load) + + let getExpr com ctx r (object: Expression) (expr: Expression) = + // printfn "getExpr: %A" (object, expr) + match expr with + | Expression.Constant(value=name) when (name :? string) -> + let name = name :?> string |> Identifier + Expression.attribute (value = object, attr = name, ctx = Load), [] + //| Expression.Name({Id=name}) -> + // Expression.attribute (value = object, attr = name, ctx = Load), [] + | e -> + // let func = Expression.name("getattr") + // Expression.call(func=func, args=[object; e]), [] + Expression.subscript(value = object, slice = e, ctx = Load), [] + + let rec getParts com ctx (parts: string list) (expr: Expression) = + match parts with + | [] -> expr + | m::ms -> get com ctx None expr m false |> getParts com ctx ms + + let makeArray (com: IPythonCompiler) ctx exprs = + let expr, stmts = exprs |> List.map (fun e -> com.TransformAsExpr(ctx, e)) |> Helpers.unzipArgs + expr |> Expression.list, stmts + + let makeStringArray strings = + strings + |> List.map (fun x -> Expression.constant(x)) + |> Expression.list + + let makeJsObject com ctx (pairs: seq) = + pairs |> Seq.map (fun (name, value) -> + //let prop, computed = memberFromName com ctx name + let prop = Expression.constant(name) + prop, value) + |> Seq.toList + |> List.unzip + |> Expression.dict + + let assign range left right = + Expression.namedExpr(left, right, ?loc=range) + + /// Immediately Invoked Function Expression + let iife (com: IPythonCompiler) ctx (expr: Fable.Expr) = + let _, body = com.TransformFunction(ctx, None, [], expr) + // Use an arrow function in case we need to capture `this` + let args = Arguments.arguments () + let afe, stmts = makeArrowFunctionExpression args body + Expression.call(afe, []), stmts + + let multiVarDeclaration (ctx: Context) (variables: (Identifier * Expression option) list) = + let ids, values = + variables + |> List.distinctBy (fun (Identifier(name=name), _value) -> name) + |> List.map (function + | i, Some value -> Expression.name(i, Store), value, i + | i, _ -> Expression.name(i, Store), Expression.none (), i) + |> List.unzip3 + |> fun (ids, values, ids') -> + ctx.BoundVars.Bind(ids') + (Expression.tuple(ids), Expression.tuple(values)) + + [ Statement.assign([ids], values) ] + + let varDeclaration (ctx: Context) (var: Expression) (isMutable: bool) value = + match var with + | Name({Id=id}) -> ctx.BoundVars.Bind([id]) + | _ -> () + [ Statement.assign([var], value) ] + + let restElement (var: Python.Identifier) = + let var = Expression.name(var) + Expression.starred(var) + + let callSuper (args: Expression list) = + let super = Expression.name (Python.Identifier("super().__init__")) + Expression.call(super, args) + + let callSuperAsStatement (args: Expression list) = + Statement.expr(callSuper args) + + let makeClassConstructor (args: Arguments) body = + let name = Identifier("__init__") + let self = Arg.arg("self") + let args = { args with Args = self::args.Args } + let body = + match body with + | [] -> [ Pass ] + | _ -> body + + FunctionDef.Create(name, args, body = body) + + let callFunction r funcExpr (args: Expression list) = + Expression.call(funcExpr, args, ?loc=r) + + let callFunctionWithThisContext com ctx r funcExpr (args: Expression list) = + let args = thisExpr::args + Expression.call(get com ctx None funcExpr "call" false, args, ?loc=r) + + let emitExpression range (txt: string) args = + let value = + match txt with + | "$0.join('')" -> "''.join($0)" + | "throw $0" -> "raise $0" + | Naming.StartsWith("void ") value + | Naming.StartsWith("new ") value -> value + | _ -> txt + Expression.emit (value, args, ?loc=range) + + let undefined range: Expression = + Expression.name(identifier = Identifier("None"), ?loc=range) + + let getGenericTypeParams (types: Fable.Type list) = + let rec getGenParams = function + | Fable.GenericParam(name,_) -> [name] + | t -> t.Generics |> List.collect getGenParams + types + |> List.collect getGenParams + |> Set.ofList + + let uncurryLambdaType t = + let rec uncurryLambdaArgs acc = function + | Fable.LambdaType(paramType, returnType) -> + uncurryLambdaArgs (paramType::acc) returnType + | t -> List.rev acc, t + uncurryLambdaArgs [] t + + type MemberKind = + | ClassConstructor + | NonAttached of funcName: string + | Attached of isStatic: bool + + let getMemberArgsAndBody (com: IPythonCompiler) ctx kind hasSpread (args: Fable.Ident list) (body: Fable.Expr) = + // printfn "getMemberArgsAndBody" + let funcName, genTypeParams, args, body = + match kind, args with + | Attached(isStatic=false), (thisArg::args) -> + let genTypeParams = Set.difference (getGenericTypeParams [thisArg.Type]) ctx.ScopedTypeParams + let body = + // TODO: If ident is not captured maybe we can just replace it with "this" + if FableTransforms.isIdentUsed thisArg.Name body then + let thisKeyword = Fable.IdentExpr { thisArg with Name = "self" } + Fable.Let(thisArg, thisKeyword, body) + else body + None, genTypeParams, args, body + | Attached(isStatic=true), _ + | ClassConstructor, _ -> None, ctx.ScopedTypeParams, args, body + | NonAttached funcName, _ -> Some funcName, Set.empty, args, body + | _ -> None, Set.empty, args, body + + let ctx = { ctx with ScopedTypeParams = Set.union ctx.ScopedTypeParams genTypeParams } + let args, body = transformFunction com ctx funcName args body + // TODO: add self argument in this function + + // let args = + // let len = args.Args.Length + // if not hasSpread || len = 0 then args + // else [ + // if len > 1 then + // yield! args.[..len-2] + // // FIXME: yield restElement args.[len-1] + // ] + + args, body + + let getUnionCaseName (uci: Fable.UnionCase) = + match uci.CompiledName with Some cname -> cname | None -> uci.Name + + let getUnionExprTag (com: IPythonCompiler) ctx r (fableExpr: Fable.Expr) = + let expr, stmts = com.TransformAsExpr(ctx, fableExpr) + let expr, stmts' = getExpr com ctx r expr (Expression.constant("tag")) + expr, stmts @ stmts' + + let wrapExprInBlockWithReturn (e, stmts) = + stmts @ [ Statement.return'(e) ] + + let makeArrowFunctionExpression (args: Arguments) (body: Statement list) : Expression * Statement list = + let name = Helpers.getUniqueIdentifier "lifted" + let func = FunctionDef.Create(name = name, args = args, body = body) + Expression.name (name), [ func ] + + let makeFunction name (args, (body: Expression)) : Statement = + let body = wrapExprInBlockWithReturn (body, []) + FunctionDef.Create(name = name, args = args, body = body) + + let makeFunctionExpression (com: IPythonCompiler) ctx name (args, (body: Expression)) : Expression * Statement list= + let name = + name |> Option.map (fun name -> com.GetIdentifier(ctx, name)) + |> Option.defaultValue (Helpers.getUniqueIdentifier "lifted") + let func = makeFunction name (args, body) + Expression.name(name), [ func ] + + let optimizeTailCall (com: IPythonCompiler) (ctx: Context) range (tc: ITailCallOpportunity) args = + let rec checkCrossRefs tempVars allArgs = function + | [] -> tempVars + | (argId, _arg)::rest -> + let found = allArgs |> List.exists (FableTransforms.deepExists (function + | Fable.IdentExpr i -> argId = i.Name + | _ -> false)) + let tempVars = + if found then + let tempVarName = getUniqueNameInDeclarationScope ctx (argId + "_tmp") + Map.add argId tempVarName tempVars + else tempVars + checkCrossRefs tempVars allArgs rest + ctx.OptimizeTailCall() + let zippedArgs = List.zip tc.Args args + let tempVars = checkCrossRefs Map.empty args zippedArgs + let tempVarReplacements = tempVars |> Map.map (fun _ v -> makeIdentExpr v) + [ + // First declare temp variables + for (KeyValue(argId, tempVar)) in tempVars do + yield! varDeclaration ctx (com.GetIdentifierAsExpr(ctx, tempVar)) false (com.GetIdentifierAsExpr(ctx, argId)) + // Then assign argument expressions to the original argument identifiers + // See https://github.com/fable-compiler/Fable/issues/1368#issuecomment-434142713 + for (argId, arg) in zippedArgs do + let arg = FableTransforms.replaceValues tempVarReplacements arg + let arg, stmts = com.TransformAsExpr(ctx, arg) + yield! stmts + yield! assign None (com.GetIdentifierAsExpr(ctx, argId)) arg |> exprAsStatement ctx + yield Statement.continue'(?loc=range) + ] + + let transformImport (com: IPythonCompiler) ctx (r: SourceLocation option) (name: string) (moduleName: string) = + let name, parts = + let parts = Array.toList(name.Split('.')) + parts.Head, parts.Tail + com.GetImportExpr(ctx, moduleName, name) + |> getParts com ctx parts + + let transformCast (com: IPythonCompiler) (ctx: Context) t e: Expression * Statement list = + match t with + // Optimization for (numeric) array or list literals casted to seq + // Done at the very end of the compile pipeline to get more opportunities + // of matching cast and literal expressions after resolving pipes, inlining... + | Fable.DeclaredType(ent,[_]) -> + match ent.FullName, e with + | Types.ienumerableGeneric, Replacements.ArrayOrListLiteral(exprs, _) -> + makeArray com ctx exprs + | _ -> com.TransformAsExpr(ctx, e) + | _ -> com.TransformAsExpr(ctx, e) + + let transformCurry (com: IPythonCompiler) (ctx: Context) expr arity: Expression * Statement list = + com.TransformAsExpr(ctx, Replacements.curryExprAtRuntime com arity expr) + + let transformValue (com: IPythonCompiler) (ctx: Context) r value: Expression * Statement list = + match value with + | Fable.BaseValue(None,_) -> Expression.identifier("super().__init__"), [] + | Fable.BaseValue(Some boundIdent,_) -> identAsExpr com ctx boundIdent, [] + | Fable.ThisValue _ -> Expression.identifier("self"), [] + | Fable.TypeInfo t -> transformTypeInfo com ctx r Map.empty t + | Fable.Null _t -> Expression.identifier("None", ?loc=r), [] + | Fable.UnitConstant -> undefined r, [] + | Fable.BoolConstant x -> Expression.constant(x, ?loc=r), [] + | Fable.CharConstant x -> Expression.constant(string x, ?loc=r), [] + | Fable.StringConstant x -> Expression.constant(x, ?loc=r), [] + | Fable.NumberConstant (x,_,_) -> Expression.constant(x, ?loc=r), [] + //| Fable.RegexConstant (source, flags) -> Expression.regExpLiteral(source, flags, ?loc=r) + | Fable.NewArray (values, typ) -> makeArray com ctx values + | Fable.NewArrayFrom (size, typ) -> + let array, stmts = makeArray com ctx [] + let size, stmts' = com.TransformAsExpr(ctx, size) + Expression.binOp(array, Mult, size), stmts @ stmts' + | Fable.NewTuple(vals,_) -> makeArray com ctx vals + // Optimization for bundle size: compile list literals as List.ofArray + | Fable.NewList (headAndTail, _) -> + let rec getItems acc = function + | None -> List.rev acc, None + | Some(head, Fable.Value(Fable.NewList(tail, _),_)) -> getItems (head::acc) tail + | Some(head, tail) -> List.rev (head::acc), Some tail + match getItems [] headAndTail with + | [], None -> + libCall com ctx r "List" "empty" [], [] + | [TransformExpr com ctx (expr, stmts)], None -> + libCall com ctx r "List" "singleton" [ expr ], stmts + | exprs, None -> + let expr, stmts = makeArray com ctx exprs + [ expr ] + |> libCall com ctx r "List" "ofArray", stmts + | [TransformExpr com ctx (head, stmts)], Some(TransformExpr com ctx (tail, stmts')) -> + libCall com ctx r "List" "cons" [ head; tail], stmts @ stmts' + | exprs, Some(TransformExpr com ctx (tail, stmts)) -> + let expr, stmts' = makeArray com ctx exprs + [ expr; tail ] + |> libCall com ctx r "List" "ofArrayWithTail", stmts @ stmts' + | Fable.NewOption (value, t, _) -> + match value with + | Some (TransformExpr com ctx (e, stmts)) -> + if mustWrapOption t + then libCall com ctx r "Option" "some" [ e ], stmts + else e, stmts + | None -> undefined r, [] + | Fable.EnumConstant(x,_) -> + com.TransformAsExpr(ctx, x) + | Fable.NewRecord(values, ent, genArgs) -> + let ent = com.GetEntity(ent) + let values, stmts = List.map (fun x -> com.TransformAsExpr(ctx, x)) values |> Helpers.unzipArgs + let consRef, stmts' = ent |> jsConstructor com ctx + Expression.call(consRef, values, ?loc=r), stmts @ stmts' + | Fable.NewAnonymousRecord(values, fieldNames, _genArgs) -> + let values, stmts = values |> List.map (fun x -> com.TransformAsExpr(ctx, x)) |> Helpers.unzipArgs + List.zip (List.ofArray fieldNames) values |> makeJsObject com ctx , stmts + | Fable.NewUnion(values, tag, ent, genArgs) -> + let ent = com.GetEntity(ent) + let values, stmts = List.map (fun x -> com.TransformAsExpr(ctx, x)) values |> Helpers.unzipArgs + let consRef, stmts' = ent |> jsConstructor com ctx + // let caseName = ent.UnionCases |> List.item tag |> getUnionCaseName |> ofString + let values = (ofInt tag)::values + Expression.call(consRef, values, ?loc=r), stmts @ stmts' + | _ -> failwith $"transformValue: value {value} not supported!" + + let enumerator2iterator com ctx = + let enumerator = Expression.call(get com ctx None (Expression.identifier("self")) "GetEnumerator" false, []) + [ Statement.return'(libCall com ctx None "Util" "toIterator" [ enumerator ]) ] + + let extractBaseExprFromBaseCall (com: IPythonCompiler) (ctx: Context) (baseType: Fable.DeclaredType option) baseCall = + match baseCall, baseType with + | Some (Fable.Call(baseRef, info, _, _)), _ -> + let baseExpr, stmts = + match baseRef with + | Fable.IdentExpr id -> com.GetIdentifierAsExpr(ctx, id.Name), [] + | _ -> transformAsExpr com ctx baseRef + let args = transformCallArgs com ctx info.HasSpread info.Args + Some (baseExpr, args) + | Some (Fable.Value _), Some baseType -> + // let baseEnt = com.GetEntity(baseType.Entity) + // let entityName = FSharp2Fable.Helpers.getEntityDeclarationName com baseType.Entity + // let entityType = FSharp2Fable.Util.getEntityType baseEnt + // let baseRefId = makeTypedIdent entityType entityName + // let baseExpr = (baseRefId |> typedIdent com ctx) :> Expression + // Some (baseExpr, []) // default base constructor + let range = baseCall |> Option.bind (fun x -> x.Range) + sprintf "Ignoring base call for %s" baseType.Entity.FullName |> addWarning com [] range + None + | Some _, _ -> + let range = baseCall |> Option.bind (fun x -> x.Range) + "Unexpected base call expression, please report" |> addError com [] range + None + | None, _ -> + None + + let transformObjectExpr (com: IPythonCompiler) ctx (members: Fable.MemberDecl list) baseCall: Expression * Statement list = + // printfn "transformObjectExpr" + let compileAsClass = + Option.isSome baseCall || members |> List.exists (fun m -> + // Optimization: Object literals with getters and setters are very slow in V8 + // so use a class expression instead. See https://github.com/fable-compiler/Fable/pull/2165#issuecomment-695835444 + m.Info.IsSetter || (m.Info.IsGetter && canHaveSideEffects m.Body)) + + let makeMethod prop hasSpread args body decorators = + let args, body = + getMemberArgsAndBody com ctx (Attached(isStatic=false)) hasSpread args body + let name = com.GetIdentifier(ctx, prop) + let self = Arg.arg("self") + let args = { args with Args = self::args.Args } + FunctionDef.Create(name, args, body, decorators) + + let members = + members |> List.collect (fun memb -> + let info = memb.Info + let prop, computed = memberFromName com ctx memb.Name + // If compileAsClass is false, it means getters don't have side effects + // and can be compiled as object fields (see condition above) + if info.IsValue || (not compileAsClass && info.IsGetter) then + let expr, stmts = com.TransformAsExpr(ctx, memb.Body) + let stmts = + let decorators = [ Expression.name ("staticmethod") ] + stmts |> List.map (function | FunctionDef(def) -> FunctionDef({ def with DecoratorList = decorators}) | ex -> ex) + stmts @ [ Statement.assign([prop], expr) ] + elif info.IsGetter then + // printfn "IsGetter: %A" prop + let decorators = [ Expression.name("property") ] + [ makeMethod memb.Name false memb.Args memb.Body decorators ] + elif info.IsSetter then + let decorators = [ Expression.name ("property") ] + [ makeMethod memb.Name false memb.Args memb.Body decorators ] + elif info.IsEnumerator then + let method = makeMethod memb.Name info.HasSpread memb.Args memb.Body [] + let iterator = + let body = enumerator2iterator com ctx + let name = com.GetIdentifier(ctx, "__iter__") + let args = Arguments.arguments() + FunctionDef.Create(name = name, args = args, body = body) + [ method; iterator] + else + [ makeMethod memb.Name info.HasSpread memb.Args memb.Body [] ] + ) + + // let classMembers = + // members |> List.choose (function + // | ObjectProperty(key, value, computed) -> + // ClassMember.classProperty(key, value, computed_=computed) |> Some + // | ObjectMethod(kind, key, ``params``, body, computed, returnType, typeParameters, _) -> + // let kind = + // match kind with + // | "get" -> ClassGetter + // | "set" -> ClassSetter + // | _ -> ClassFunction + // ClassMember.classMethod(kind, key, ``params``, body, computed_=computed, + // ?returnType=returnType, ?typeParameters=typeParameters) |> Some) + + let baseExpr, classMembers = + baseCall + |> extractBaseExprFromBaseCall com ctx None + |> Option.map (fun (baseExpr, (baseArgs, stmts)) -> + let consBody = [ callSuperAsStatement baseArgs ] + let args = Arguments.empty + let cons = makeClassConstructor args consBody + Some baseExpr, cons::members + ) + |> Option.defaultValue (None, members) + + let classBody = + match classMembers with + | [] -> [ Pass] + | _ -> classMembers + let name = Helpers.getUniqueIdentifier "ObjectExpr" + let stmt = Statement.classDef(name, body=classBody, bases=(baseExpr |> Option.toList) ) + Expression.call(Expression.name (name)), [ stmt ] + + let transformCallArgs (com: IPythonCompiler) ctx hasSpread args : Expression list * Statement list = + match args with + | [] + | [MaybeCasted(Fable.Value(Fable.UnitConstant,_))] -> [], [] + | args when hasSpread -> + match List.rev args with + | [] -> [], [] + | (Replacements.ArrayOrListLiteral(spreadArgs,_))::rest -> + let rest = List.rev rest |> List.map (fun e -> com.TransformAsExpr(ctx, e)) + rest @ (List.map (fun e -> com.TransformAsExpr(ctx, e)) spreadArgs) |> Helpers.unzipArgs + | last::rest -> + let rest, stmts = List.rev rest |> List.map (fun e -> com.TransformAsExpr(ctx, e)) |> Helpers.unzipArgs + let expr, stmts' = com.TransformAsExpr(ctx, last) + rest @ [ Expression.starred(expr) ], stmts @ stmts' + | args -> List.map (fun e -> com.TransformAsExpr(ctx, e)) args |> Helpers.unzipArgs + + let resolveExpr (ctx: Context) t strategy pyExpr: Statement list = + //printfn "resolveExpr: %A" pyExpr + match strategy with + | None | Some ReturnUnit -> exprAsStatement ctx pyExpr + // TODO: Where to put these int wrappings? Add them also for function arguments? + | Some Return -> [ Statement.return'(pyExpr) ] + | Some(Assign left) -> exprAsStatement ctx (assign None left pyExpr) + | Some(Target left) -> exprAsStatement ctx (assign None (left |> Expression.identifier) pyExpr) + + let transformOperation com ctx range opKind: Expression * Statement list = + //printfn "transformOperation: %A" opKind + match opKind with + | Fable.Unary(UnaryVoid, TransformExpr com ctx (expr, stmts)) -> + expr, stmts + // Transform `~(~(a/b))` to `a // b` + | Fable.Unary(UnaryOperator.UnaryNotBitwise, Fable.Operation(kind=Fable.Unary(UnaryOperator.UnaryNotBitwise, Fable.Operation(kind=Fable.Binary(BinaryOperator.BinaryDivide, TransformExpr com ctx (left, stmts), TransformExpr com ctx (right, stmts')))))) -> + Expression.binOp(left, FloorDiv, right), stmts @ stmts' + | Fable.Unary(op, TransformExpr com ctx (expr, stmts)) -> + Expression.unaryOp(op, expr, ?loc=range), stmts + + | Fable.Binary(BinaryInstanceOf, TransformExpr com ctx (left, stmts), TransformExpr com ctx (right, stmts')) -> + let func = Expression.name (Python.Identifier("isinstance")) + let args = [ left; right ] + Expression.call (func, args), stmts' @ stmts + + | Fable.Binary(op, TransformExpr com ctx (left, stmts), TransformExpr com ctx (right, stmts')) -> + match op with + | BinaryEqualStrict -> + match right, left with + | Expression.Constant(_), _ + | _, Expression.Constant(_) -> + let op = BinaryEqual + Expression.compare(left, op, [right], ?loc=range), stmts @ stmts' + | _ -> + Expression.compare(left, op, [right], ?loc=range), stmts @ stmts' + | BinaryUnequalStrict -> + match right with + | Expression.Constant(_) -> + let op = BinaryUnequal + Expression.compare(left, op, [right], ?loc=range), stmts @ stmts' + | _ -> + Expression.compare(left, op, [right], ?loc=range), stmts @ stmts' + | BinaryEqual -> + match right with + | Expression.Name({Id=Identifier("None")}) -> + let op = BinaryEqualStrict + Expression.compare(left, op, [right], ?loc=range), stmts @ stmts' + | _ -> + Expression.compare(left, op, [right], ?loc=range), stmts @ stmts' + | BinaryUnequal -> + //printfn "Right: %A" right + match right with + | Expression.Name({Id=Identifier("None")}) -> + let op = BinaryUnequalStrict + Expression.compare(left, op, [right], ?loc=range), stmts @ stmts' + | _ -> + Expression.compare(left, op, [right], ?loc=range), stmts @ stmts' + + | BinaryLess + | BinaryLessOrEqual + | BinaryGreater + | BinaryGreaterOrEqual -> + Expression.compare(left, op, [right], ?loc=range), stmts @ stmts' + | _ -> + Expression.binOp(left, op, right, ?loc=range), stmts @ stmts' + + | Fable.Logical(op, TransformExpr com ctx (left, stmts), TransformExpr com ctx (right, stmts')) -> + Expression.boolOp(op, [left; right], ?loc=range), stmts @ stmts' + + let transformEmit (com: IPythonCompiler) ctx range (info: Fable.EmitInfo) = + let macro = info.Macro + let info = info.CallInfo + let thisArg, stmts = info.ThisArg |> Option.map (fun e -> com.TransformAsExpr(ctx, e)) |> Option.toList |> Helpers.unzipArgs + let exprs, stmts' = transformCallArgs com ctx info.HasSpread info.Args + + if macro.StartsWith("functools") then + com.GetImportExpr(ctx, "functools") |> ignore + + let args = + exprs + |> List.append thisArg + emitExpression range macro args, stmts @ stmts' + + let transformCall (com: IPythonCompiler) ctx range callee (callInfo: Fable.CallInfo) : Expression * Statement list = + let callee, stmts = com.TransformAsExpr(ctx, callee) + let args, stmts' = transformCallArgs com ctx callInfo.HasSpread callInfo.Args + match callInfo.ThisArg with + | Some(TransformExpr com ctx (thisArg, stmts'')) -> callFunction range callee (thisArg::args), stmts @ stmts' @ stmts'' + | None when callInfo.IsConstructor -> Expression.call(callee, args, ?loc=range), stmts @ stmts' + | None -> callFunction range callee args, stmts @ stmts' + + let transformCurriedApply com ctx range (TransformExpr com ctx (applied, stmts)) args = + match transformCallArgs com ctx false args with + | [], stmts' -> callFunction range applied [], stmts @ stmts' + | args, stmts' -> (applied, args) ||> List.fold (fun e arg -> callFunction range e [arg]), stmts @ stmts' + + let transformCallAsStatements com ctx range t returnStrategy callee callInfo = + let argsLen (i: Fable.CallInfo) = + List.length i.Args + (if Option.isSome i.ThisArg then 1 else 0) + // Warn when there's a recursive call that couldn't be optimized? + match returnStrategy, ctx.TailCallOpportunity with + | Some(Return|ReturnUnit), Some tc when tc.IsRecursiveRef(callee) + && argsLen callInfo = List.length tc.Args -> + let args = + match callInfo.ThisArg with + | Some thisArg -> thisArg::callInfo.Args + | None -> callInfo.Args + optimizeTailCall com ctx range tc args + | _ -> + let expr, stmts = transformCall com ctx range callee callInfo + stmts @ (expr |> resolveExpr ctx t returnStrategy) + + let transformCurriedApplyAsStatements com ctx range t returnStrategy callee args = + // Warn when there's a recursive call that couldn't be optimized? + match returnStrategy, ctx.TailCallOpportunity with + | Some(Return|ReturnUnit), Some tc when tc.IsRecursiveRef(callee) + && List.sameLength args tc.Args -> + optimizeTailCall com ctx range tc args + | _ -> + let expr, stmts = transformCurriedApply com ctx range callee args + stmts @ (expr |> resolveExpr ctx t returnStrategy) + + let transformBody (com: IPythonCompiler) ctx ret (body: Statement list) : Statement list = + match body with + | [] -> [ Pass ] + | _ -> + let body, nonLocals = + body + |> List.partition (function | Statement.NonLocal _ -> false | _ -> true) + + let nonLocal = + nonLocals + |> List.collect (function | Statement.NonLocal(nl) -> nl.Names | _ -> []) + |> Statement.nonLocal + nonLocal :: body + + // When expecting a block, it's usually not necessary to wrap it + // in a lambda to isolate its variable context + let transformBlock (com: IPythonCompiler) ctx ret (expr: Fable.Expr) : Statement list = + let block = + com.TransformAsStatements(ctx, ret, expr) |> List.choose Helpers.isProductiveStatement + match block with + | [] -> [ Pass ] + | _ -> + block + |> transformBody com ctx ret + + let transformTryCatch com ctx r returnStrategy (body, (catch: option), finalizer) = + // try .. catch statements cannot be tail call optimized + let ctx = { ctx with TailCallOpportunity = None } + let handlers = + catch |> Option.map (fun (param, body) -> + let body = transformBlock com ctx returnStrategy body + let exn = Expression.identifier("Exception") |> Some + let identifier = ident com ctx param + [ ExceptHandler.exceptHandler (``type`` = exn, name = identifier, body = body) ]) + let finalizer, stmts = + match finalizer with + | Some finalizer -> + finalizer |> + transformBlock com ctx None + |> List.partition (function | Statement.NonLocal (_) -> false | _ -> true ) + | None -> [], [] + + stmts @ [ Statement.try'(transformBlock com ctx returnStrategy body, ?handlers=handlers, finalBody=finalizer, ?loc=r) ] + + let rec transformIfStatement (com: IPythonCompiler) ctx r ret guardExpr thenStmnt elseStmnt = + // printfn "transformIfStatement" + let expr, stmts = com.TransformAsExpr(ctx, guardExpr) + match expr with + | Constant(value=value) when (value :? bool) -> + match value with + | :? bool as value when value -> stmts @ com.TransformAsStatements(ctx, ret, thenStmnt) + | _ -> stmts @ com.TransformAsStatements(ctx, ret, elseStmnt) + | guardExpr -> + let thenStmnt, stmts' = + transformBlock com ctx ret thenStmnt + |> List.partition (function | Statement.NonLocal (_) -> false | _ -> true ) + let ifStatement, stmts'' = + let block, stmts = + com.TransformAsStatements(ctx, ret, elseStmnt) + |> List.partition (function | Statement.NonLocal (_) -> false | _ -> true ) + + match block with + | [ ] -> Statement.if'(guardExpr, thenStmnt, ?loc=r), stmts + | [ elseStmnt ] -> Statement.if'(guardExpr, thenStmnt, [ elseStmnt ], ?loc=r), stmts + | statements -> Statement.if'(guardExpr, thenStmnt, statements, ?loc=r), stmts + stmts @ stmts' @ stmts'' @ [ ifStatement ] + + let transformGet (com: IPythonCompiler) ctx range typ (fableExpr: Fable.Expr) kind = + + // printfn "transformGet: %A" fableExpr + // printfn "transformGet: %A" (fableExpr.Type) + + match kind with + | Fable.ExprGet(TransformExpr com ctx (prop, stmts)) -> + let expr, stmts' = com.TransformAsExpr(ctx, fableExpr) + let expr, stmts'' = getExpr com ctx range expr prop + expr, stmts @ stmts' @ stmts'' + + | Fable.FieldGet(fieldName="message") -> + let func = Expression.name("str") + let left, stmts = com.TransformAsExpr(ctx, fableExpr) + Expression.call (func, [ left ]), stmts + | Fable.FieldGet(fieldName="push") -> + let attr = Python.Identifier("append") + let value, stmts = com.TransformAsExpr(ctx, fableExpr) + Expression.attribute (value = value, attr = attr, ctx = Load), stmts + | Fable.FieldGet(fieldName="length") -> + let func = Expression.name("len") + let left, stmts = com.TransformAsExpr(ctx, fableExpr) + Expression.call (func, [ left ]), stmts + | Fable.FieldGet(fieldName="toLocaleUpperCase") -> + let attr = Python.Identifier("upper") + let value, stmts = com.TransformAsExpr(ctx, fableExpr) + Expression.attribute (value = value, attr = attr, ctx = Load), stmts + | Fable.FieldGet(fieldName="toLocaleLowerCase") -> + let attr = Python.Identifier("lower") + let value, stmts = com.TransformAsExpr(ctx, fableExpr) + Expression.attribute (value = value, attr = attr, ctx = Load), stmts + | Fable.FieldGet(fieldName="indexOf") -> + let attr = Python.Identifier("find") + let value, stmts = com.TransformAsExpr(ctx, fableExpr) + Expression.attribute (value = value, attr = attr, ctx = Load), stmts + + | Fable.FieldGet(fieldName,_) -> + let fableExpr = + match fableExpr with + // If we're accessing a virtual member with default implementation (see #701) + // from base class, we can use `super` in JS so we don't need the bound this arg + | Fable.Value(Fable.BaseValue(_,t), r) -> Fable.Value(Fable.BaseValue(None, t), r) + | _ -> fableExpr + let expr, stmts = com.TransformAsExpr(ctx, fableExpr) + let subscript = + match fableExpr.Type with + | Fable.AnonymousRecordType(_) -> true + | _ -> false + // printfn "Fable.FieldGet: %A" fieldName + get com ctx range expr fieldName subscript, stmts + + | Fable.ListHead -> + // get range (com.TransformAsExpr(ctx, fableExpr)) "head" + let expr, stmts = com.TransformAsExpr(ctx, fableExpr) + libCall com ctx range "List" "head" [ expr ], stmts + + | Fable.ListTail -> + // get range (com.TransformAsExpr(ctx, fableExpr)) "tail" + let expr, stmts = com.TransformAsExpr(ctx, fableExpr) + libCall com ctx range "List" "tail" [ expr ], stmts + + | Fable.TupleIndex index -> + match fableExpr with + // TODO: Check the erased expressions don't have side effects? + | Fable.Value(Fable.NewTuple(exprs, _), _) -> + com.TransformAsExpr(ctx, List.item index exprs) + | TransformExpr com ctx (expr, stmts) -> + let expr, stmts' = getExpr com ctx range expr (ofInt index) + expr, stmts @ stmts' + + | Fable.OptionValue -> + let expr, stmts = com.TransformAsExpr(ctx, fableExpr) + if mustWrapOption typ || com.Options.Language = TypeScript + then libCall com ctx None "Option" "value" [ expr ], stmts + else expr, stmts + + | Fable.UnionTag -> + let expr, stmts = getUnionExprTag com ctx range fableExpr + expr, stmts + + | Fable.UnionField(_, fieldIndex) -> + let expr, stmts = com.TransformAsExpr(ctx, fableExpr) + let expr, stmts' = getExpr com ctx None expr (Expression.constant("fields")) + let expr, stmts'' = getExpr com ctx range expr (ofInt fieldIndex) + expr, stmts @ stmts' @ stmts'' + + let transformSet (com: IPythonCompiler) ctx range fableExpr typ (value: Fable.Expr) kind = + // printfn "transformSet: %A" (fableExpr, value) + let expr, stmts = com.TransformAsExpr(ctx, fableExpr) + let value, stmts' = com.TransformAsExpr(ctx, value) + let ret, stmts'' = + match kind with + | Fable.ValueSet -> + expr, [] + | Fable.ExprSet(TransformExpr com ctx (e, stmts'')) -> + let expr, stmts''' = getExpr com ctx None expr e + expr, stmts'' @ stmts''' + | Fable.FieldSet(fieldName) -> + get com ctx None expr fieldName false, [] + assign range ret value, stmts @ stmts' @ stmts'' + + let transformBindingExprBody (com: IPythonCompiler) (ctx: Context) (var: Fable.Ident) (value: Fable.Expr) = + match value with + | Function(args, body) -> + let name = Some var.Name + let args, stmts = transformFunction com ctx name args body + makeArrowFunctionExpression args stmts + | _ -> + com.TransformAsExpr(ctx, value) + + let transformBindingAsExpr (com: IPythonCompiler) ctx (var: Fable.Ident) (value: Fable.Expr) = + //printfn "transformBindingAsExpr: %A" (var, value) + let expr, stmts = transformBindingExprBody com ctx var value + expr |> assign None (identAsExpr com ctx var), stmts + + let transformBindingAsStatements (com: IPythonCompiler) ctx (var: Fable.Ident) (value: Fable.Expr) = + if isPyStatement ctx false value then + let varName, varExpr = Expression.name(var.Name), identAsExpr com ctx var + ctx.BoundVars.Bind(var.Name) + + let decl = Statement.assign([varName], Expression.none ()) + let body = com.TransformAsStatements(ctx, Some(Assign varExpr), value) + List.append [ decl ] body + else + let value, stmts = transformBindingExprBody com ctx var value + let varName = com.GetIdentifierAsExpr(ctx, var.Name) // Expression.name(var.Name) + let decl = varDeclaration ctx varName var.IsMutable value + stmts @ decl + + let transformTest (com: IPythonCompiler) ctx range kind expr: Expression * Statement list = + match kind with + | Fable.TypeTest t -> + transformTypeTest com ctx range expr t + | Fable.OptionTest nonEmpty -> + let op = if nonEmpty then BinaryUnequalStrict else BinaryEqualStrict + let expr, stmts = com.TransformAsExpr(ctx, expr) + Expression.compare(expr, op, [Expression.none()], ?loc=range), stmts + | Fable.ListTest nonEmpty -> + let expr, stmts = com.TransformAsExpr(ctx, expr) + // let op = if nonEmpty then BinaryUnequal else BinaryEqual + // Expression.binaryExpression(op, get None expr "tail", Expression.none(), ?loc=range) + let expr = + let expr = libCall com ctx range "List" "isEmpty" [ expr ] + if nonEmpty then Expression.unaryOp(UnaryNot, expr, ?loc=range) else expr + expr, stmts + | Fable.UnionCaseTest tag -> + let expected = ofInt tag + let actual, stmts = getUnionExprTag com ctx None expr + Expression.compare(actual, [Eq], [ expected ], ?loc=range), stmts + + let transformSwitch (com: IPythonCompiler) ctx useBlocks returnStrategy evalExpr cases defaultCase: Statement list = + let cases = + cases |> List.collect (fun (guards, expr) -> + // Remove empty branches + match returnStrategy, expr, guards with + | None, Fable.Value(Fable.UnitConstant,_), _ + | _, _, [] -> [] + | _, _, guards -> + let guards, lastGuard = List.splitLast guards + let guards = guards |> List.map (fun e -> + let expr, stmts = com.TransformAsExpr(ctx, e) + (stmts, Some expr)) + let caseBody = com.TransformAsStatements(ctx, returnStrategy, expr) + let caseBody = + match returnStrategy with + | Some Return -> caseBody + | _ -> List.append caseBody [ Statement.break'() ] + let expr, stmts = com.TransformAsExpr(ctx, lastGuard) + guards @ [(stmts @ caseBody, Some expr)] + ) + let cases = + match defaultCase with + | Some expr -> + let defaultCaseBody = com.TransformAsStatements(ctx, returnStrategy, expr) + cases @ [(defaultCaseBody, None)] + | None -> cases + + let value, stmts = com.TransformAsExpr(ctx, evalExpr) + + let rec ifThenElse (fallThrough: Python.Expression option) (cases: (Statement list * Expression option) list): Python.Statement list option = + match cases with + | [] -> None + | (body, test) :: cases -> + match test with + | None -> body |> Some + | Some test -> + let expr = Expression.compare (left = value, ops = [ Eq ], comparators = [ test ]) + + let test = + match fallThrough with + | Some ft -> Expression.boolOp (op = Or, values = [ ft; expr ]) + | _ -> expr + + // Check for fallthrough + if body.IsEmpty then + ifThenElse (Some test) cases + else + [ Statement.if' (test = test, body = body, ?orelse = ifThenElse None cases) ] + |> Some + + let result = cases |> ifThenElse None + match result with + | Some ifStmt -> stmts @ ifStmt + | None -> [] + + let matchTargetIdentAndValues idents values = + if List.isEmpty idents then [] + elif List.sameLength idents values then List.zip idents values + else failwith "Target idents/values lengths differ" + + let getDecisionTargetAndBindValues (com: IPythonCompiler) (ctx: Context) targetIndex boundValues = + let idents, target = getDecisionTarget ctx targetIndex + let identsAndValues = matchTargetIdentAndValues idents boundValues + if not com.Options.DebugMode then + let bindings, replacements = + (([], Map.empty), identsAndValues) + ||> List.fold (fun (bindings, replacements) (ident, expr) -> + if canHaveSideEffects expr then + (ident, expr)::bindings, replacements + else + bindings, Map.add ident.Name expr replacements) + let target = FableTransforms.replaceValues replacements target + List.rev bindings, target + else + identsAndValues, target + + let transformDecisionTreeSuccessAsExpr (com: IPythonCompiler) (ctx: Context) targetIndex boundValues = + let bindings, target = getDecisionTargetAndBindValues com ctx targetIndex boundValues + match bindings with + | [] -> com.TransformAsExpr(ctx, target) + | bindings -> + let target = List.rev bindings |> List.fold (fun e (i,v) -> Fable.Let(i,v,e)) target + com.TransformAsExpr(ctx, target) + + let exprAsStatement (ctx: Context) (expr: Expression) : Statement list = + match expr with + | NamedExpr({Target=target; Value=value; Loc=loc }) -> + let nonLocals = + match target with + | Expression.Name({ Id=id }) -> + //printfn "Adding nonlocal for: %A" id + [ ctx.BoundVars.NonLocals([ id ]) |> Statement.nonLocal ] + | _ -> [] + + //printfn "Nonlocals: %A" nonLocals + nonLocals @ [ Statement.assign([target], value) ] + | _ -> [ Statement.expr(expr) ] + + let transformDecisionTreeSuccessAsStatements (com: IPythonCompiler) (ctx: Context) returnStrategy targetIndex boundValues: Statement list = + match returnStrategy with + | Some(Target targetId) as target -> + let idents, _ = getDecisionTarget ctx targetIndex + let assignments = + matchTargetIdentAndValues idents boundValues + |> List.collect (fun (id, TransformExpr com ctx (value, stmts)) -> + let stmts' = assign None (identAsExpr com ctx id) value |> exprAsStatement ctx + stmts @ stmts') + let targetAssignment = assign None (targetId |> Expression.name) (ofInt targetIndex) |> exprAsStatement ctx + targetAssignment @ assignments + | ret -> + let bindings, target = getDecisionTargetAndBindValues com ctx targetIndex boundValues + let bindings = bindings |> Seq.collect (fun (i, v) -> transformBindingAsStatements com ctx i v) |> Seq.toList + bindings @ (com.TransformAsStatements(ctx, ret, target)) + + let transformDecisionTreeAsSwitch expr = + let (|Equals|_|) = function + | Fable.Operation(Fable.Binary(BinaryEqualStrict, expr, right), _, _) -> + Some(expr, right) + | Fable.Test(expr, Fable.UnionCaseTest tag, _) -> + let evalExpr = Fable.Get(expr, Fable.UnionTag, Fable.Number(Int32, None), None) + let right = makeIntConst tag + Some(evalExpr, right) + | _ -> None + let sameEvalExprs evalExpr1 evalExpr2 = + match evalExpr1, evalExpr2 with + | Fable.IdentExpr i1, Fable.IdentExpr i2 + | Fable.Get(Fable.IdentExpr i1,Fable.UnionTag,_,_), Fable.Get(Fable.IdentExpr i2,Fable.UnionTag,_,_) -> + i1.Name = i2.Name + | _ -> false + let rec checkInner cases evalExpr = function + | Fable.IfThenElse(Equals(evalExpr2, caseExpr), + Fable.DecisionTreeSuccess(targetIndex, boundValues, _), treeExpr, _) + when sameEvalExprs evalExpr evalExpr2 -> + match treeExpr with + | Fable.DecisionTreeSuccess(defaultTargetIndex, defaultBoundValues, _) -> + let cases = (caseExpr, targetIndex, boundValues)::cases |> List.rev + Some(evalExpr, cases, (defaultTargetIndex, defaultBoundValues)) + | treeExpr -> checkInner ((caseExpr, targetIndex, boundValues)::cases) evalExpr treeExpr + | _ -> None + match expr with + | Fable.IfThenElse(Equals(evalExpr, caseExpr), + Fable.DecisionTreeSuccess(targetIndex, boundValues, _), treeExpr, _) -> + match checkInner [caseExpr, targetIndex, boundValues] evalExpr treeExpr with + | Some(evalExpr, cases, defaultCase) -> + Some(evalExpr, cases, defaultCase) + | None -> None + | _ -> None + + let transformDecisionTreeAsExpr (com: IPythonCompiler) (ctx: Context) targets expr: Expression * Statement list = + // TODO: Check if some targets are referenced multiple times + let ctx = { ctx with DecisionTargets = targets } + com.TransformAsExpr(ctx, expr) + + let groupSwitchCases t (cases: (Fable.Expr * int * Fable.Expr list) list) (defaultIndex, defaultBoundValues) = + cases + |> List.groupBy (fun (_,idx,boundValues) -> + // Try to group cases with some target index and empty bound values + // If bound values are non-empty use also a non-empty Guid to prevent grouping + if List.isEmpty boundValues + then idx, System.Guid.Empty + else idx, System.Guid.NewGuid()) + |> List.map (fun ((idx,_), cases) -> + let caseExprs = cases |> List.map Tuple3.item1 + // If there are multiple cases, it means boundValues are empty + // (see `groupBy` above), so it doesn't mind which one we take as reference + let boundValues = cases |> List.head |> Tuple3.item3 + caseExprs, Fable.DecisionTreeSuccess(idx, boundValues, t)) + |> function + | [] -> [] + // Check if the last case can also be grouped with the default branch, see #2357 + | cases when List.isEmpty defaultBoundValues -> + match List.splitLast cases with + | cases, (_, Fable.DecisionTreeSuccess(idx, [], _)) + when idx = defaultIndex -> cases + | _ -> cases + | cases -> cases + + let getTargetsWithMultipleReferences expr = + let rec findSuccess (targetRefs: Map) = function + | [] -> targetRefs + | expr::exprs -> + match expr with + // We shouldn't actually see this, but shortcircuit just in case + | Fable.DecisionTree _ -> + findSuccess targetRefs exprs + | Fable.DecisionTreeSuccess(idx,_,_) -> + let count = + Map.tryFind idx targetRefs + |> Option.defaultValue 0 + let targetRefs = Map.add idx (count + 1) targetRefs + findSuccess targetRefs exprs + | expr -> + let exprs2 = FableTransforms.getSubExpressions expr + findSuccess targetRefs (exprs @ exprs2) + findSuccess Map.empty [expr] |> Seq.choose (fun kv -> + if kv.Value > 1 then Some kv.Key else None) |> Seq.toList + + /// When several branches share target create first a switch to get the target index and bind value + /// and another to execute the actual target + let transformDecisionTreeWithTwoSwitches (com: IPythonCompiler) ctx returnStrategy + (targets: (Fable.Ident list * Fable.Expr) list) treeExpr = + // Declare target and bound idents + let targetId = getUniqueNameInDeclarationScope ctx "pattern_matching_result" |> makeIdent + let multiVarDecl = + let boundIdents = + targets |> List.collect (fun (idents,_) -> + idents) |> List.map (fun id -> ident com ctx id, None) + multiVarDeclaration ctx ((ident com ctx targetId,None)::boundIdents) + // Transform targets as switch + let switch2 = + // TODO: Declare the last case as the default case? + let cases = targets |> List.mapi (fun i (_,target) -> [makeIntConst i], target) + transformSwitch com ctx true returnStrategy (targetId |> Fable.IdentExpr) cases None + // Transform decision tree + let targetAssign = Target(ident com ctx targetId) + let ctx = { ctx with DecisionTargets = targets } + match transformDecisionTreeAsSwitch treeExpr with + | Some(evalExpr, cases, (defaultIndex, defaultBoundValues)) -> + let cases = groupSwitchCases (Fable.Number(Int32, None)) cases (defaultIndex, defaultBoundValues) + let defaultCase = Fable.DecisionTreeSuccess(defaultIndex, defaultBoundValues, Fable.Number(Int32, None)) + let switch1 = transformSwitch com ctx false (Some targetAssign) evalExpr cases (Some defaultCase) + multiVarDecl @ switch1 @ switch2 + | None -> + let decisionTree = com.TransformAsStatements(ctx, Some targetAssign, treeExpr) + multiVarDecl @ decisionTree @ switch2 + + let transformDecisionTreeAsStatements (com: IPythonCompiler) (ctx: Context) returnStrategy + (targets: (Fable.Ident list * Fable.Expr) list) (treeExpr: Fable.Expr): Statement list = + // If some targets are referenced multiple times, hoist bound idents, + // resolve the decision index and compile the targets as a switch + let targetsWithMultiRefs = + if com.Options.Language = TypeScript then [] // no hoisting when compiled with types + else getTargetsWithMultipleReferences treeExpr + match targetsWithMultiRefs with + | [] -> + let ctx = { ctx with DecisionTargets = targets } + match transformDecisionTreeAsSwitch treeExpr with + | Some(evalExpr, cases, (defaultIndex, defaultBoundValues)) -> + let t = treeExpr.Type + let cases = cases |> List.map (fun (caseExpr, targetIndex, boundValues) -> + [caseExpr], Fable.DecisionTreeSuccess(targetIndex, boundValues, t)) + let defaultCase = Fable.DecisionTreeSuccess(defaultIndex, defaultBoundValues, t) + transformSwitch com ctx true returnStrategy evalExpr cases (Some defaultCase) + | None -> + com.TransformAsStatements(ctx, returnStrategy, treeExpr) + | targetsWithMultiRefs -> + // If the bound idents are not referenced in the target, remove them + let targets = + targets |> List.map (fun (idents, expr) -> + idents + |> List.exists (fun i -> FableTransforms.isIdentUsed i.Name expr) + |> function + | true -> idents, expr + | false -> [], expr) + let hasAnyTargetWithMultiRefsBoundValues = + targetsWithMultiRefs |> List.exists (fun idx -> + targets.[idx] |> fst |> List.isEmpty |> not) + if not hasAnyTargetWithMultiRefsBoundValues then + match transformDecisionTreeAsSwitch treeExpr with + | Some(evalExpr, cases, (defaultIndex, defaultBoundValues)) -> + let t = treeExpr.Type + let cases = groupSwitchCases t cases (defaultIndex, defaultBoundValues) + let ctx = { ctx with DecisionTargets = targets } + let defaultCase = Fable.DecisionTreeSuccess(defaultIndex, defaultBoundValues, t) + transformSwitch com ctx true returnStrategy evalExpr cases (Some defaultCase) + | None -> + transformDecisionTreeWithTwoSwitches com ctx returnStrategy targets treeExpr + else + transformDecisionTreeWithTwoSwitches com ctx returnStrategy targets treeExpr + + let transformSequenceExpr (com: IPythonCompiler) ctx (exprs: Fable.Expr list) : Expression * Statement list = + //printfn "transformSequenceExpr" + let ctx = { ctx with BoundVars = ctx.BoundVars.EnterScope() } + let body = + exprs + |> List.collecti + (fun i e -> + let expr, stmts = com.TransformAsExpr(ctx, e) + // Return the last expression + if i = exprs.Length - 1 then + Statement.return' (expr) :: stmts + else + exprAsStatement ctx expr @ stmts) + |> transformBody com ctx None + + // let expr = + // Expression.subscript( + // Expression.tuple(exprs), + // Expression.constant(-1)) + // expr, [] + //printfn "transformSequenceExpr, body: %A" body + + let name = Helpers.getUniqueIdentifier ("lifted") + let func = FunctionDef.Create(name = name, args = Arguments.arguments [], body = body) + + let name = Expression.name (name) + Expression.call (name), [ func ] + + let transformSequenceExpr' (com: IPythonCompiler) ctx (exprs: Expression list) (stmts: Statement list) : Expression * Statement list = + //printfn "transformSequenceExpr', exprs: %A" exprs.Length + let ctx = { ctx with BoundVars = ctx.BoundVars.EnterScope() } + + let body = + exprs + |> List.collecti + (fun i expr -> + // Return the last expression + if i = exprs.Length - 1 then + stmts @ [ Statement.return' (expr) ] + else + exprAsStatement ctx expr) + + let name = Helpers.getUniqueIdentifier ("lifted") + let func = FunctionDef.Create(name = name, args = Arguments.arguments [], body = body) + + let name = Expression.name (name) + Expression.call (name), [ func ] + + let rec transformAsExpr (com: IPythonCompiler) ctx (expr: Fable.Expr): Expression * Statement list= + match expr with + | Fable.TypeCast(e,t) -> transformCast com ctx t e + + | Fable.Value(kind, r) -> transformValue com ctx r kind + + | Fable.IdentExpr id -> identAsExpr com ctx id, [] + + | Fable.Import({ Selector = selector; Path = path }, _, r) -> + transformImport com ctx r selector path, [] + + | Fable.Test(expr, kind, range) -> + transformTest com ctx range kind expr + + | Fable.Lambda(arg, body, name) -> + transformFunction com ctx name [arg] body + ||> makeArrowFunctionExpression + + | Fable.Delegate(args, body, name) -> + transformFunction com ctx name args body + ||> makeArrowFunctionExpression + + | Fable.ObjectExpr (members, _, baseCall) -> + transformObjectExpr com ctx members baseCall + + | Fable.Call(Fable.Get(expr, Fable.FieldGet(fieldName="toString"), _, _), info, _, range) -> + let func = Expression.name("str") + let left, stmts = com.TransformAsExpr(ctx, expr) + Expression.call (func, [ left ]), stmts + + | Fable.Call(Fable.Get(expr, Fable.FieldGet(fieldName="split"), _, _), { Args=[Fable.Value(kind=Fable.StringConstant(""))]}, _, range) -> + let func = Expression.name("list") + let value, stmts = com.TransformAsExpr(ctx, expr) + Expression.call (func, [ value ]), stmts + + | Fable.Call(Fable.Get(expr, Fable.FieldGet(fieldName="charCodeAt"), _, _), info, _, range) -> + let func = Expression.name("ord") + let value, stmts = com.TransformAsExpr(ctx, expr) + Expression.call (func, [ value ]), stmts + + | Fable.Call(callee, info, _, range) -> + transformCall com ctx range callee info + + | Fable.CurriedApply(callee, args, _, range) -> + transformCurriedApply com ctx range callee args + + | Fable.Operation(kind, _, range) -> + transformOperation com ctx range kind + + | Fable.Get(Fable.IdentExpr({Name = "String"}), Fable.FieldGet(fieldName="fromCharCode"), _, _) -> + let func = Expression.name("chr") + func, [] + + | Fable.Get(expr, kind, typ, range) -> + transformGet com ctx range typ expr kind + + | Fable.IfThenElse(TransformExpr com ctx (guardExpr, stmts), + TransformExpr com ctx (thenExpr, stmts'), + TransformExpr com ctx (elseExpr, stmts''), r) -> + Expression.ifExp (guardExpr, thenExpr, elseExpr), stmts @ stmts' @ stmts'' + + | Fable.DecisionTree(expr, targets) -> + transformDecisionTreeAsExpr com ctx targets expr + + | Fable.DecisionTreeSuccess(idx, boundValues, _) -> + transformDecisionTreeSuccessAsExpr com ctx idx boundValues + + | Fable.Set(expr, kind, typ, value, range) -> + let expr', stmts = transformSet com ctx range expr typ value kind + match expr' with + | Expression.NamedExpr({ Target = target; Value = _; Loc=_ }) -> + let nonLocals = + match target with + | Expression.Name({Id=id}) -> [ ctx.BoundVars.NonLocals([id]) |> Statement.nonLocal ] + | _ -> [] + expr', nonLocals @ stmts + | _ -> expr', stmts + + | Fable.Let(ident, value, body) -> + //printfn "Fable.Let: %A" (ident, value, body) + if ctx.HoistVars [ident] then + let assignment, stmts = transformBindingAsExpr com ctx ident value + let bodyExpr, stmts' = com.TransformAsExpr(ctx, body) + let expr, stmts'' = transformSequenceExpr' com ctx [ assignment; bodyExpr ] (stmts @ stmts') + expr, stmts'' + else iife com ctx expr + + | Fable.LetRec(bindings, body) -> + if ctx.HoistVars(List.map fst bindings) then + let values, stmts = + bindings + |> List.map (fun (id, value) -> transformBindingAsExpr com ctx id value) + |> List.unzip + |> (fun (e, s) -> (e, List.collect id s)) + + let expr, stmts' = com.TransformAsExpr(ctx, body) + let expr, stmts'' = transformSequenceExpr' com ctx (values @ [expr]) [] + expr, stmts @ stmts' @ stmts'' + else iife com ctx expr + + | Fable.Sequential exprs -> transformSequenceExpr com ctx exprs + + | Fable.Emit(info, _, range) -> + if info.IsStatement then iife com ctx expr + else transformEmit com ctx range info + + // These cannot appear in expression position in JS, must be wrapped in a lambda + | Fable.WhileLoop _ | Fable.ForLoop _ | Fable.TryCatch _ -> + iife com ctx expr + | Fable.Extended(instruction, _) -> + match instruction with + | Fable.Curry(e, arity) -> transformCurry com ctx e arity + | Fable.Throw _ | Fable.Return _ | Fable.Break _ | Fable.Debugger -> iife com ctx expr + + let rec transformAsStatements (com: IPythonCompiler) ctx returnStrategy + (expr: Fable.Expr): Statement list = + match expr with + | Fable.Extended(kind, r) -> + match kind with + | Fable.Curry(e, arity) -> + let expr, stmts = transformCurry com ctx e arity + stmts @ (expr |> resolveExpr ctx e.Type returnStrategy) + | Fable.Throw(TransformExpr com ctx (e, stmts), _) -> stmts @ [Statement.raise(e) ] + | Fable.Return(TransformExpr com ctx (e, stmts)) -> stmts @ [ Statement.return'(e)] + | Fable.Debugger -> [] + | Fable.Break label -> [ Statement.break'() ] + + | Fable.TypeCast(e, t) -> + let expr, stmts = transformCast com ctx t e + stmts @ (expr |> resolveExpr ctx t returnStrategy) + + | Fable.Value(kind, r) -> + let expr, stmts = transformValue com ctx r kind + stmts @ (expr |> resolveExpr ctx kind.Type returnStrategy) + + | Fable.IdentExpr id -> + identAsExpr com ctx id |> resolveExpr ctx id.Type returnStrategy + + | Fable.Import({ Selector = selector; Path = path }, t, r) -> + transformImport com ctx r selector path |> resolveExpr ctx t returnStrategy + + | Fable.Test(expr, kind, range) -> + let expr, stmts = transformTest com ctx range kind expr + stmts @ (expr |> resolveExpr ctx Fable.Boolean returnStrategy) + + | Fable.Lambda(arg, body, name) -> + let args, body = transformFunction com ctx name [arg] body + let expr', stmts = makeArrowFunctionExpression args body + stmts @ (expr' |> resolveExpr ctx expr.Type returnStrategy) + + | Fable.Delegate(args, body, name) -> + let args, body = transformFunction com ctx name args body + let expr', stmts = makeArrowFunctionExpression args body + stmts @ (expr' |> resolveExpr ctx expr.Type returnStrategy) + + | Fable.ObjectExpr (members, t, baseCall) -> + let expr, stmts = transformObjectExpr com ctx members baseCall + stmts @ (expr |> resolveExpr ctx t returnStrategy) + + | Fable.Call(callee, info, typ, range) -> + transformCallAsStatements com ctx range typ returnStrategy callee info + + | Fable.CurriedApply(callee, args, typ, range) -> + transformCurriedApplyAsStatements com ctx range typ returnStrategy callee args + + | Fable.Emit(info, t, range) -> + let e, stmts = transformEmit com ctx range info + if info.IsStatement then + stmts @ [ Statement.expr(e) ] // Ignore the return strategy + else stmts @ resolveExpr ctx t returnStrategy e + + | Fable.Operation(kind, t, range) -> + let expr, stmts = transformOperation com ctx range kind + stmts @ (expr |> resolveExpr ctx t returnStrategy) + + | Fable.Get(expr, kind, t, range) -> + let expr, stmts = transformGet com ctx range t expr kind + stmts @ (expr |> resolveExpr ctx t returnStrategy) + + | Fable.Let(ident, value, body) -> + let binding = transformBindingAsStatements com ctx ident value + List.append binding (transformAsStatements com ctx returnStrategy body) + + | Fable.LetRec(bindings, body) -> + let bindings = bindings |> Seq.collect (fun (i, v) -> transformBindingAsStatements com ctx i v) |> Seq.toList + List.append bindings (transformAsStatements com ctx returnStrategy body) + + | Fable.Set(expr, kind, typ, value, range) -> + let expr', stmts = transformSet com ctx range expr typ value kind + match expr' with + | Expression.NamedExpr({ Target = target; Value = value; Loc=loc }) -> + let nonLocals = + match target with + | Expression.Name({Id=id}) -> [ ctx.BoundVars.NonLocals([id]) |> Statement.nonLocal ] + | _ -> [] + nonLocals @ stmts @ [ Statement.assign([target], value) ] + | _ -> stmts @ (expr' |> resolveExpr ctx expr.Type returnStrategy) + + | Fable.IfThenElse(guardExpr, thenExpr, elseExpr, r) -> + let asStatement = + match returnStrategy with + | None | Some ReturnUnit -> true + | Some(Target _) -> true // Compile as statement so values can be bound + | Some(Assign _) -> (isPyStatement ctx false thenExpr) || (isPyStatement ctx false elseExpr) + | Some Return -> + Option.isSome ctx.TailCallOpportunity + || (isPyStatement ctx false thenExpr) || (isPyStatement ctx false elseExpr) + if asStatement then + transformIfStatement com ctx r returnStrategy guardExpr thenExpr elseExpr + else + let guardExpr', stmts = transformAsExpr com ctx guardExpr + let thenExpr', stmts' = transformAsExpr com ctx thenExpr + let elseExpr', stmts'' = transformAsExpr com ctx elseExpr + stmts + @ stmts' + @ stmts'' + @ (Expression.ifExp(guardExpr', thenExpr', elseExpr', ?loc=r) |> resolveExpr ctx thenExpr.Type returnStrategy) + + | Fable.Sequential statements -> + let lasti = (List.length statements) - 1 + statements |> List.mapiToArray (fun i statement -> + let ret = if i < lasti then None else returnStrategy + com.TransformAsStatements(ctx, ret, statement)) + |> List.concat + + | Fable.TryCatch (body, catch, finalizer, r) -> + transformTryCatch com ctx r returnStrategy (body, catch, finalizer) + + | Fable.DecisionTree(expr, targets) -> + transformDecisionTreeAsStatements com ctx returnStrategy targets expr + + | Fable.DecisionTreeSuccess(idx, boundValues, _) -> + transformDecisionTreeSuccessAsStatements com ctx returnStrategy idx boundValues + + | Fable.WhileLoop(TransformExpr com ctx (guard, stmts), body, label, range) -> + stmts @ [ Statement.while'(guard, transformBlock com ctx None body, ?loc=range) ] + + | Fable.ForLoop (var, TransformExpr com ctx (start, stmts), TransformExpr com ctx (limit, stmts'), body, isUp, range) -> + let limit, step = + if isUp + then + let limit = Expression.binOp (limit, Add, Expression.constant (1)) // Python `range` has exclusive end. + limit, 1 + else + limit, -1 + + let step = Expression.constant(step) + let iter = Expression.call (Expression.name (Python.Identifier "range"), args = [ start; limit; step ]) + let body = transformBlock com ctx None body + let target = com.GetIdentifierAsExpr(ctx, var.Name) + + [ Statement.for'(target = target, iter = iter, body = body) ] + + let transformFunction com ctx name (args: Fable.Ident list) (body: Fable.Expr): Arguments * Statement list = + let tailcallChance = + Option.map (fun name -> + NamedTailCallOpportunity(com, ctx, name, args) :> ITailCallOpportunity) name + let args = discardUnitArg args + let declaredVars = ResizeArray() + let mutable isTailCallOptimized = false + let ctx = + { ctx with TailCallOpportunity = tailcallChance + HoistVars = fun ids -> declaredVars.AddRange(ids); true + OptimizeTailCall = fun () -> isTailCallOptimized <- true + BoundVars = ctx.BoundVars.EnterScope() } + + let body = + if body.Type = Fable.Unit then + transformBlock com ctx (Some ReturnUnit) body + elif isPyStatement ctx (Option.isSome tailcallChance) body then + transformBlock com ctx (Some Return) body + else + transformAsExpr com ctx body |> wrapExprInBlockWithReturn + let args, body = + match isTailCallOptimized, tailcallChance with + | true, Some tc -> + // Replace args, see NamedTailCallOpportunity constructor + let args' = + List.zip args tc.Args + |> List.map (fun (id, tcArg) -> com.GetIdentifier(ctx, tcArg)) + let varDecls = + List.zip args tc.Args + |> List.map (fun (id, tcArg) -> ident com ctx id, Some (com.GetIdentifierAsExpr(ctx, tcArg))) + |> multiVarDeclaration ctx + + let body = varDecls @ body + // Make sure we don't get trapped in an infinite loop, see #1624 + let body = body @ [ Statement.break'() ] + args', Statement.while'(Expression.constant(true), body) + |> List.singleton + | _ -> args |> List.map (ident com ctx), body + let body = + if declaredVars.Count = 0 then body + else + let varDeclStatement = multiVarDeclaration ctx [for v in declaredVars -> ident com ctx v, None] + varDeclStatement @ body + //printfn "Args: %A" (args, body) + let args = Arguments.arguments(args |> List.map Arg.arg) + args, body + + let declareEntryPoint _com _ctx (funcExpr: Expression) = + let argv = emitExpression None "typeof process === 'object' ? process.argv.slice(2) : []" [] + let main = Expression.call(funcExpr, [ argv ]) + // Don't exit the process after leaving main, as there may be a server running + // Statement.expr(emitExpression funcExpr.loc "process.exit($0)" [main], ?loc=funcExpr.loc) + Statement.expr(main) + + let declareModuleMember ctx isPublic (membName: Identifier) isMutable (expr: Expression) = + let membName = Expression.name(membName) + match expr with + // | ClassExpression(body, id, superClass, implements, superTypeParameters, typeParameters, loc) -> + // Declaration.classDeclaration( + // body, + // ?id = Some membName, + // ?superClass = superClass, + // ?superTypeParameters = superTypeParameters, + // ?typeParameters = typeParameters, + // ?implements = implements) + // | FunctionExpression(_, ``params``, body, returnType, typeParameters, _) -> + // Declaration.functionDeclaration( + // ``params``, body, membName, + // ?returnType = returnType, + // ?typeParameters = typeParameters) + | _ -> + varDeclaration ctx membName isMutable expr + // if not isPublic then PrivateModuleDeclaration(decl |> Declaration) + // else ExportNamedDeclaration(decl) + +// let makeEntityTypeParamDecl (com: IPythonCompiler) _ctx (ent: Fable.Entity) = +// if com.Options.Language = TypeScript then +// getEntityGenParams ent |> makeTypeParamDecl +// else +// None + +// let getClassImplements com ctx (ent: Fable.Entity) = +// let mkNative genArgs typeName = +// let id = Identifier.identifier(typeName) +// let typeParamInst = makeGenTypeParamInst com ctx genArgs +// ClassImplements.classImplements(id, ?typeParameters=typeParamInst) |> Some +// // let mkImport genArgs moduleName typeName = +// // let id = makeImportTypeId com ctx moduleName typeName +// // let typeParamInst = makeGenTypeParamInst com ctx genArgs +// // ClassImplements(id, ?typeParameters=typeParamInst) |> Some +// ent.AllInterfaces |> Seq.choose (fun ifc -> +// match ifc.Entity.FullName with +// | "Fable.Core.JS.Set`1" -> mkNative ifc.GenericArgs "Set" +// | "Fable.Core.JS.Map`2" -> mkNative ifc.GenericArgs "Map" +// | _ -> None +// ) + + let getUnionFieldsAsIdents (_com: IPythonCompiler) _ctx (_ent: Fable.Entity) = + let tagId = makeTypedIdent (Fable.Number(Int32, None)) "tag" + let fieldsId = makeTypedIdent (Fable.Array Fable.Any) "fields" + [| tagId; fieldsId |] + + let getEntityFieldsAsIdents _com (ent: Fable.Entity) = + ent.FSharpFields + |> Seq.map (fun field -> + let name = field.Name |> Naming.sanitizeIdentForbiddenChars |> Naming.checkJsKeywords + let typ = field.FieldType + let id: Fable.Ident = { makeTypedIdent typ name with IsMutable = field.IsMutable } + id) + |> Seq.toArray + + let getEntityFieldsAsProps (com: IPythonCompiler) ctx (ent: Fable.Entity) = + if ent.IsFSharpUnion then + getUnionFieldsAsIdents com ctx ent + |> Array.map (identAsExpr com ctx) + else + ent.FSharpFields + |> Seq.map (fun field -> + let prop, computed = memberFromName com ctx field.Name + prop) + |> Seq.toArray + + let declareClassType (com: IPythonCompiler) ctx (ent: Fable.Entity) entName (consArgs: Arguments) (consBody: Statement list) (baseExpr: Expression option) classMembers = + let classCons = makeClassConstructor consArgs consBody + let classFields = Array.empty + let classMembers = List.append [ classCons ] classMembers + //printfn "ClassMembers: %A" classMembers + let classBody = + let body = [ yield! classFields; yield! classMembers ] + //printfn "Body: %A" body + match body with + | [] -> [ Pass ] + | _ -> body + let bases = baseExpr |> Option.toList + let name = com.GetIdentifier(ctx, entName) + let classStmt = Statement.classDef(name, body = classBody, bases = bases) + classStmt + + let declareType (com: IPythonCompiler) ctx (ent: Fable.Entity) entName (consArgs: Arguments) (consBody: Statement list) baseExpr classMembers : Statement list = + let typeDeclaration = declareClassType com ctx ent entName consArgs consBody baseExpr classMembers + let reflectionDeclaration, stmts = + let ta = None + let genArgs = Array.init (ent.GenericParameters.Length) (fun i -> "gen" + string i |> makeIdent) + let generics = genArgs |> Array.mapToList (identAsExpr com ctx) + let body, stmts = transformReflectionInfo com ctx None ent generics + let args = Arguments.arguments(genArgs |> Array.mapToList (ident com ctx >> Arg.arg)) + let expr, stmts' = makeFunctionExpression com ctx None (args, body) + let name = com.GetIdentifier(ctx, entName + Naming.reflectionSuffix) + expr |> declareModuleMember ctx ent.IsPublic name false, stmts @ stmts' + stmts @ [typeDeclaration ] @ reflectionDeclaration + + let transformModuleFunction (com: IPythonCompiler) ctx (info: Fable.MemberInfo) (membName: string) args body = + let args, body = + getMemberArgsAndBody com ctx (NonAttached membName) info.HasSpread args body + + //printfn "Arsg: %A" args + let name = com.GetIdentifier(ctx, membName) //Helpers.getUniqueIdentifier "lifted" + let stmt = FunctionDef.Create(name = name, args = args, body = body) + let expr = Expression.name (name) + info.Attributes + |> Seq.exists (fun att -> att.Entity.FullName = Atts.entryPoint) + |> function + | true -> [ stmt; declareEntryPoint com ctx expr ] + | false -> [ stmt ] //; declareModuleMember info.IsPublic membName false expr ] + + let transformAction (com: IPythonCompiler) ctx expr = + let statements = transformAsStatements com ctx None expr + // let hasVarDeclarations = + // statements |> List.exists (function + // | Declaration(_) -> true + // | _ -> false) + // if hasVarDeclarations then + // [ Expression.call(Expression.functionExpression([||], BlockStatement(statements)), [||]) + // |> Statement.expr |> PrivateModuleDeclaration ] + //else + statements + + let transformAttachedProperty (com: IPythonCompiler) ctx (memb: Fable.MemberDecl) = + let isStatic = not memb.Info.IsInstance + //let kind = if memb.Info.IsGetter then ClassGetter else ClassSetter + let args, body = + getMemberArgsAndBody com ctx (Attached isStatic) false memb.Args memb.Body + let key, computed = memberFromName com ctx memb.Name + let self = Arg.arg("self") + let arguments = { args with Args = self::args.Args } + FunctionDef.Create(com.GetIdentifier(ctx, memb.Name), arguments, body = body) + |> List.singleton + + let transformAttachedMethod (com: IPythonCompiler) ctx (memb: Fable.MemberDecl) = + // printfn "transformAttachedMethod" + let isStatic = not memb.Info.IsInstance + let makeMethod name args body = + let key, computed = memberFromName com ctx name + FunctionDef.Create(com.GetIdentifier(ctx, name), args, body = body) + let args, body = + getMemberArgsAndBody com ctx (Attached isStatic) memb.Info.HasSpread memb.Args memb.Body + let self = Arg.arg("self") + let arguments = { args with Args = self::args.Args } + [ + yield makeMethod memb.Name arguments body + if memb.Info.IsEnumerator then + yield makeMethod "__iter__" (Arguments.arguments [self]) (enumerator2iterator com ctx) + ] + + let transformUnion (com: IPythonCompiler) ctx (ent: Fable.Entity) (entName: string) classMembers = + let fieldIds = getUnionFieldsAsIdents com ctx ent + let args = + let args=fieldIds.[0] |> ident com ctx |> Arg.arg |> List.singleton + let varargs = fieldIds.[1] |> ident com ctx |> Arg.arg + Arguments.arguments(args=args, vararg=varargs) + + let body = + [ + yield callSuperAsStatement [] + yield! fieldIds |> Array.map (fun id -> + let left = get com ctx None thisExpr id.Name false + let right = + match id.Type with + | Fable.Number _ -> + Expression.binOp(identAsExpr com ctx id, BinaryOrBitwise, Expression.constant(0.)) + | _ -> identAsExpr com ctx id + + Statement.assign([left], right)) + //assign None left right |> Statement.expr) + ] + let cases = + let expr, stmts = + ent.UnionCases + |> Seq.map (getUnionCaseName >> makeStrConst) + |> Seq.toList + |> makeArray com ctx + + let body = stmts @ [ Statement.return'(expr) ] + let name = Identifier("cases") + let self = Arg.arg("self") + FunctionDef.Create(name, Arguments.arguments [ self ], body = body) + + let baseExpr = libValue com ctx "Types" "Union" |> Some + let classMembers = List.append [ cases ] classMembers + declareType com ctx ent entName args body baseExpr classMembers + + let transformClassWithCompilerGeneratedConstructor (com: IPythonCompiler) ctx (ent: Fable.Entity) (entName: string) classMembers = + let fieldIds = getEntityFieldsAsIdents com ent + let args = fieldIds |> Array.map (fun id -> com.GetIdentifier(ctx, id.Name) |> Expression.name) + let baseExpr = + if ent.IsFSharpExceptionDeclaration + then libValue com ctx "Types" "FSharpException" |> Some + elif ent.IsFSharpRecord || ent.IsValueType + then libValue com ctx "Types" "Record" |> Some + else None + let body = [ + if Option.isSome baseExpr then + yield callSuperAsStatement [] + + yield! (ent.FSharpFields |> List.collecti (fun i field -> + let left = get com ctx None thisExpr field.Name false + let right = args.[i] + assign None left right |> exprAsStatement ctx)) + ] + let args = + fieldIds + |> Array.mapToList (ident com ctx >> Arg.arg) + |> (fun args -> Arguments.arguments(args=args)) + declareType com ctx ent entName args body baseExpr classMembers + + let transformClassWithImplicitConstructor (com: IPythonCompiler) ctx (classDecl: Fable.ClassDecl) classMembers (cons: Fable.MemberDecl) = + let classEnt = com.GetEntity(classDecl.Entity) + let classIdent = Expression.name(com.GetIdentifier(ctx, classDecl.Name)) + let consArgs, consBody = + getMemberArgsAndBody com ctx ClassConstructor cons.Info.HasSpread cons.Args cons.Body + + let exposedCons = + let argExprs = consArgs.Args |> List.map (fun p -> Expression.identifier(p.Arg)) + let exposedConsBody = Expression.call(classIdent, argExprs) + let name = com.GetIdentifier(ctx, cons.Name) + makeFunction name (consArgs, exposedConsBody) + + let baseExpr, consBody = + classDecl.BaseCall + |> extractBaseExprFromBaseCall com ctx classEnt.BaseType + |> Option.orElseWith (fun () -> + if classEnt.IsValueType then Some(libValue com ctx "Types" "Record", ([], [])) + else None) + |> Option.map (fun (baseExpr, (baseArgs, stmts)) -> + let consBody = + stmts @ consBody + |> List.append [ callSuperAsStatement baseArgs ] + Some baseExpr, consBody) + |> Option.defaultValue (None, consBody) + + [ + yield! declareType com ctx classEnt classDecl.Name consArgs consBody baseExpr classMembers + yield exposedCons + ] + + let rec transformDeclaration (com: IPythonCompiler) ctx decl = + //printfn "transformDeclaration: %A" decl + let withCurrentScope ctx (usedNames: Set) f = + let ctx = { ctx with UsedNames = { ctx.UsedNames with CurrentDeclarationScope = HashSet usedNames } } + let result = f ctx + ctx.UsedNames.DeclarationScopes.UnionWith(ctx.UsedNames.CurrentDeclarationScope) + result + + match decl with + | Fable.ModuleDeclaration decl -> + decl.Members |> List.collect (transformDeclaration com ctx) + + | Fable.ActionDeclaration decl -> + withCurrentScope ctx decl.UsedNames <| fun ctx -> + transformAction com ctx decl.Body + + | Fable.MemberDeclaration decl -> + withCurrentScope ctx decl.UsedNames <| fun ctx -> + let decls = + if decl.Info.IsValue then + let value, stmts = transformAsExpr com ctx decl.Body + let name = com.GetIdentifier(ctx, decl.Name) + stmts @ declareModuleMember ctx decl.Info.IsPublic name decl.Info.IsMutable value + else + transformModuleFunction com ctx decl.Info decl.Name decl.Args decl.Body + + if decl.ExportDefault then + decls //@ [ ExportDefaultDeclaration(Choice2Of2(Expression.identifier(decl.Name))) ] + else + decls + + | Fable.ClassDeclaration decl -> + // printfn "Class: %A" decl + let ent = decl.Entity + + let classMembers = + decl.AttachedMembers + |> List.collect (fun memb -> + withCurrentScope ctx memb.UsedNames <| fun ctx -> + if memb.Info.IsGetter || memb.Info.IsSetter then + transformAttachedProperty com ctx memb + else + transformAttachedMethod com ctx memb) + + match decl.Constructor with + | Some cons -> + withCurrentScope ctx cons.UsedNames <| fun ctx -> + transformClassWithImplicitConstructor com ctx decl classMembers cons + | None -> + let ent = com.GetEntity(ent) + if ent.IsFSharpUnion then transformUnion com ctx ent decl.Name classMembers + else + transformClassWithCompilerGeneratedConstructor com ctx ent decl.Name classMembers + + let transformImports (imports: Import seq) : Statement list = + let statefulImports = ResizeArray() + // printfn "Imports: %A" imports + + [ + for import in imports do + match import with + | { Name = name; LocalIdent = local; Module = moduleName } -> + let moduleName = moduleName |> Helpers.rewriteFableImport + match name with + | Some name -> + let alias = Alias.alias(Identifier(Helpers.clean name), ?asname=local) + Statement.importFrom (Some(Identifier(moduleName)), [ alias ]) + | None -> + let alias = Alias.alias(Identifier(moduleName), ?asname=None) + Statement.import([alias]) + ] + + let getIdentForImport (ctx: Context) (moduleName: string) (name: string option) = + match name with + | None -> + Path.GetFileNameWithoutExtension(moduleName) + |> Python.Identifier + |> Some + | Some name -> + match name with + | "*" + | _ -> name + |> getUniqueNameInRootScope ctx + |> Python.Identifier + |> Some + +module Compiler = + open Util + + type PythonCompiler (com: Compiler) = + let onlyOnceWarnings = HashSet() + let imports = Dictionary() + + interface IPythonCompiler with + member _.WarnOnlyOnce(msg, ?range) = + if onlyOnceWarnings.Add(msg) then + addWarning com [] range msg + + member _.GetImportExpr(ctx, moduleName, ?name, ?r) = + // printfn "GetImportExpr: %A" (moduleName, name) + let cachedName = moduleName + "::" + defaultArg name "module" + match imports.TryGetValue(cachedName) with + | true, i -> + match i.LocalIdent with + | Some localIdent -> Expression.identifier(localIdent) + | None -> Expression.none() + | false, _ -> + let localId = getIdentForImport ctx moduleName name + match name with + | Some name -> + let i = + { Name = + if name = Naming.placeholder then + "`importMember` must be assigned to a variable" + |> addError com [] r; name + else name + |> Some + Module = moduleName + LocalIdent = localId } + imports.Add(cachedName, i) + | None -> + let i = + { Name = None + Module = moduleName + LocalIdent = localId } + imports.Add(cachedName, i) + + match localId with + | Some localId -> Expression.identifier(localId) + | None -> Expression.none() + + member _.GetAllImports() = imports.Values :> _ + member bcom.TransformAsExpr(ctx, e) = transformAsExpr bcom ctx e + member bcom.TransformAsStatements(ctx, ret, e) = transformAsStatements bcom ctx ret e + member bcom.TransformFunction(ctx, name, args, body) = transformFunction bcom ctx name args body + member bcom.TransformImport(ctx, selector, path) = transformImport bcom ctx None selector path + member bcom.GetIdentifier(ctx, name) = getIdentifier bcom ctx name + member bcom.GetIdentifierAsExpr(ctx, name) = getIdentifier bcom ctx name |> Expression.name + + interface Compiler with + member _.Options = com.Options + member _.Plugins = com.Plugins + member _.LibraryDir = com.LibraryDir + member _.CurrentFile = com.CurrentFile + member _.OutputDir = com.OutputDir + member _.ProjectFile = com.ProjectFile + member _.GetEntity(fullName) = com.GetEntity(fullName) + member _.GetImplementationFile(fileName) = com.GetImplementationFile(fileName) + member _.GetRootModule(fileName) = com.GetRootModule(fileName) + member _.GetOrAddInlineExpr(fullName, generate) = com.GetOrAddInlineExpr(fullName, generate) + member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) + member _.AddLog(msg, severity, ?range, ?fileName:string, ?tag: string) = + com.AddLog(msg, severity, ?range=range, ?fileName=fileName, ?tag=tag) + + let makeCompiler com = PythonCompiler(com) + + let transformFile (com: Compiler) (file: Fable.File) = + let com = makeCompiler com :> IPythonCompiler + let declScopes = + let hs = HashSet() + for decl in file.Declarations do + hs.UnionWith(decl.UsedNames) + hs + //printfn "File: %A" file.Declarations + let ctx = + { File = file + UsedNames = { RootScope = HashSet file.UsedNamesInRootScope + DeclarationScopes = declScopes + CurrentDeclarationScope = Unchecked.defaultof<_> } + BoundVars = { GlobalScope = HashSet () + EnclosingScope = HashSet () + LocalScope = HashSet () } + DecisionTargets = [] + HoistVars = fun _ -> false + TailCallOpportunity = None + OptimizeTailCall = fun () -> () + ScopedTypeParams = Set.empty } + + let rootDecls = List.collect (transformDeclaration com ctx) file.Declarations + let importDecls = com.GetAllImports() |> transformImports + let body = importDecls @ rootDecls + Module.module' (body) diff --git a/src/Fable.Transforms/Python/Python.fs b/src/Fable.Transforms/Python/Python.fs new file mode 100644 index 0000000000..1870d5bf8e --- /dev/null +++ b/src/Fable.Transforms/Python/Python.fs @@ -0,0 +1,1103 @@ +// fsharplint:disable MemberNames InterfaceNames +namespace rec Fable.AST.Python + +open Fable.AST + +type Expression = + | Attribute of Attribute + | Subscript of Subscript + | BoolOp of BoolOp + | BinOp of BinOp + /// A yield from expression. Because these are expressions, they must be wrapped in a Expr node if the value sent + /// back is not used. + | YieldFrom of Expression option + /// A yield expression. Because these are expressions, they must be wrapped in a Expr node if the value sent back is + /// not used. + | Yield of Expression option + | Emit of Emit + | IfExp of IfExp + | UnaryOp of UnaryOp + | FormattedValue of FormattedValue + /// A constant value. The value attribute of the Constant literal contains the Python object it represents. The + /// values represented can be simple types such as a number, string or None, but also immutable container types + /// (tuples and frozensets) if all of their elements are constant. + | Constant of value: obj * loc: SourceLocation option + | Call of Call + | Compare of Compare + | Lambda of Lambda + | NamedExpr of NamedExpr + /// A variable name. id holds the name as a string, and ctx is one of the following types. + | Name of Name + | Dict of Dict + | Tuple of Tuple + | Starred of value: Expression * ctx: ExpressionContext + | List of elts: Expression list * ctx: ExpressionContext + +// member val Lineno: int = 0 with get, set +// member val ColOffset: int = 0 with get, set +// member val EndLineno: int option = None with get, set +// member val EndColOffset: int option = None with get, set +type Operator = + | Add + | Sub + | Mult + | Div + | FloorDiv + | Mod + | Pow + | LShift + | RShift + | BitOr + | BitXor + | BitAnd + | MatMult + +type BoolOperator = + | And + | Or + +type ComparisonOperator = + | Eq + | NotEq + | Lt + | LtE + | Gt + | GtE + | Is + | IsNot + | In + | NotIn + +type UnaryOperator = + | Invert + | Not + | UAdd + | USub + +type ExpressionContext = + | Load + | Del + | Store + +type Identifier = + | Identifier of name: string + + member this.Name = + let (Identifier name) = this + name + +type Statement = + | Pass + | Break + | Continue + | If of If + | For of For + | Try of Try + | Expr of Expr + | While of While + | Raise of Raise + | Import of Import + | Assign of Assign + | Return of Return + | Global of Global + | NonLocal of NonLocal + | ClassDef of ClassDef + | AsyncFor of AsyncFor + | ImportFrom of ImportFrom + | FunctionDef of FunctionDef + | AsyncFunctionDef of AsyncFunctionDef + +// member val Lineno: int = 0 with get, set +// member val ColOffset: int = 0 with get, set +// member val EndLineno: int option = None with get, set +// member val EndColOffset: int option = None with get, set + +type Module = { Body: Statement list } + +/// Both parameters are raw strings of the names. asname can be None if the regular name is to be used. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('from ..foo.bar import a as b, c'), indent=4)) +/// Module( +/// body=[ +/// ImportFrom( +/// module='foo.bar', +/// names=[ +/// alias(name='a', asname='b'), +/// alias(name='c')], +/// level=2)], +/// type_ignores=[]) +/// ``` +type Alias = + { Name: Identifier + AsName: Identifier option } + +/// A single except clause. type is the exception type it will match, typically a Name node (or None for a catch-all +/// except: clause). name is a raw string for the name to hold the exception, or None if the clause doesn’t have as foo. +/// body is a list of nodes. +type ExceptHandler = + { Type: Expression option + Name: Identifier option + Body: Statement list + Loc: SourceLocation option } + +/// try blocks. All attributes are list of nodes to execute, except for handlers, which is a list of ExceptHandler +/// nodes. +type Try = + { Body: Statement list + Handlers: ExceptHandler list + OrElse: Statement list + FinalBody: Statement list + Loc: SourceLocation option } + +/// A single argument in a list. arg is a raw string of the argument name, annotation is its annotation, such as a Str +/// or Name node. +/// +/// - type_comment is an optional string with the type annotation as a comment +type Arg = + { Lineno: int + ColOffset: int + EndLineno: int option + EndColOffset: int option + + Arg: Identifier + Annotation: Expression option + TypeComment: string option } + +type Keyword = + { Lineno: int + ColOffset: int + EndLineno: int option + EndColOffset: int option + + Arg: Identifier + Value: Expression } + +/// The arguments for a function. +/// +/// - posonlyargs, args and kwonlyargs are lists of arg nodes. +/// - vararg and kwarg are single arg nodes, referring to the *args, **kwargs parameters. +/// - kwDefaults is a list of default values for keyword-only arguments. If one is None, the corresponding argument is +/// required. +/// - defaults is a list of default values for arguments that can be passed positionally. If there are fewer defaults, +/// they correspond to the last n arguments. +type Arguments = + { PosOnlyArgs: Arg list // https://www.python.org/dev/peps/pep-0570/ + Args: Arg list + VarArg: Arg option + KwOnlyArgs: Arg list + KwDefaults: Expression list + KwArg: Arg option + Defaults: Expression list } + +//#region Statements + +/// An assignment. targets is a list of nodes, and value is a single node. +/// +/// Multiple nodes in targets represents assigning the same value to each. Unpacking is represented by putting a Tuple +/// or List within targets. +/// +/// type_comment is an optional string with the type annotation as a comment. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('a = b = 1'), indent=4)) # Multiple assignment +/// Module( +/// body=[ +/// Assign( +/// targets=[ +/// Name(id='a', ctx=Store()), +/// Name(id='b', ctx=Store())], +/// value=Constant(value=1))], +/// type_ignores=[]) +/// +/// >>> print(ast.dump(ast.parse('a,b = c'), indent=4)) # Unpacking +/// Module( +/// body=[ +/// Assign( +/// targets=[ +/// Tuple( +/// elts=[ +/// Name(id='a', ctx=Store()), +/// Name(id='b', ctx=Store())], +/// ctx=Store())], +/// value=Name(id='c', ctx=Load()))], +/// type_ignores=[]) +/// ``` +type Assign = + { Targets: Expression list + Value: Expression + TypeComment: string option } + +/// When an expression, such as a function call, appears as a statement by itself with its return value not used or +/// stored, it is wrapped in this container. value holds one of the other nodes in this section, a Constant, a Name, a +/// Lambda, a Yield or YieldFrom node. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('-a'), indent=4)) +/// Module( +/// body=[ +/// Expr( +/// value=UnaryOp( +/// op=USub(), +/// operand=Name(id='a', ctx=Load())))], +/// type_ignores=[]) +///``` +type Expr = { Value: Expression } + +/// A for loop. target holds the variable(s) the loop assigns to, as a single Name, Tuple or List node. iter holds the +/// item to be looped over, again as a single node. body and orelse contain lists of nodes to execute. Those in orelse +/// are executed if the loop finishes normally, rather than via a break statement. +/// +/// type_comment is an optional string with the type annotation as a comment. +/// +/// ```py +/// >>> print(ast.dump(ast.parse(""" +/// ... for x in y: +/// ... ... +/// ... else: +/// ... ... +/// ... """), indent=4)) +/// Module( +/// body=[ +/// For( +/// target=Name(id='x', ctx=Store()), +/// iter=Name(id='y', ctx=Load()), +/// body=[ +/// Expr( +/// value=Constant(value=Ellipsis))], +/// orelse=[ +/// Expr( +/// value=Constant(value=Ellipsis))])], +/// type_ignores=[]) +///``` +type For = + { Target: Expression + Iterator: Expression + Body: Statement list + Else: Statement list + TypeComment: string option } + +type AsyncFor = + { Target: Expression + Iterator: Expression + Body: Statement list + Else: Statement list + TypeComment: string option } + +/// A while loop. test holds the condition, such as a Compare node. +/// +/// ```py +/// >> print(ast.dump(ast.parse(""" +/// ... while x: +/// ... ... +/// ... else: +/// ... ... +/// ... """), indent=4)) +/// Module( +/// body=[ +/// While( +/// test=Name(id='x', ctx=Load()), +/// body=[ +/// Expr( +/// value=Constant(value=Ellipsis))], +/// orelse=[ +/// Expr( +/// value=Constant(value=Ellipsis))])], +/// type_ignores=[]) +/// ``` +type While = + { Test: Expression + Body: Statement list + Else: Statement list + Loc: SourceLocation option } + +/// A class definition. +/// +/// - name is a raw string for the class name +/// - bases is a list of nodes for explicitly specified base classes. +/// - keywords is a list of keyword nodes, principally for ‘metaclass’. Other keywords will be passed to the metaclass, +/// as per PEP-3115. +/// - starargs and kwargs are each a single node, as in a function call. starargs will be expanded to join the list of +/// base classes, and kwargs will be passed to the metaclass. +/// - body is a list of nodes representing the code within the class definition. +/// - decorator_list is a list of nodes, as in FunctionDef. +/// +/// ```py +/// >>> print(ast.dump(ast.parse("""\ +/// ... @decorator1 +/// ... @decorator2 +/// ... class Foo(base1, base2, metaclass=meta): +/// ... pass +/// ... """), indent=4)) +/// Module( +/// body=[ +/// ClassDef( +/// name='Foo', +/// bases=[ +/// Name(id='base1', ctx=Load()), +/// Name(id='base2', ctx=Load())], +/// keywords=[ +/// keyword( +/// arg='metaclass', +/// value=Name(id='meta', ctx=Load()))], +/// body=[ +/// Pass()], +/// decorator_list=[ +/// Name(id='decorator1', ctx=Load()), +/// Name(id='decorator2', ctx=Load())])], +/// type_ignores=[]) +///``` +type ClassDef = + { Name: Identifier + Bases: Expression list + Keyword: Keyword list + Body: Statement list + DecoratorList: Expression list + Loc: SourceLocation option } + +/// An if statement. test holds a single node, such as a Compare node. body and orelse each hold a list of nodes. +/// +/// elif clauses don’t have a special representation in the AST, but rather appear as extra If nodes within the orelse +/// section of the previous one. +/// +/// ```py +/// >>> print(ast.dump(ast.parse(""" +/// ... if x: +/// ... ... +/// ... elif y: +/// ... ... +/// ... else: +/// ... ... +/// ... """), indent=4)) +/// Module( +/// body=[ +/// If( +/// test=Name(id='x', ctx=Load()), +/// body=[ +/// Expr( +/// value=Constant(value=Ellipsis))], +/// orelse=[ +/// If( +/// test=Name(id='y', ctx=Load()), +/// body=[ +/// Expr( +/// value=Constant(value=Ellipsis))], +/// orelse=[ +/// Expr( +/// value=Constant(value=Ellipsis))])])], +/// type_ignores=[]) +/// ``` +type If = + { Test: Expression + Body: Statement list + Else: Statement list + Loc: SourceLocation option } + +/// A raise statement. exc is the exception object to be raised, normally a Call or Name, or None for a standalone +/// raise. cause is the optional part for y in raise x from y. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('raise x from y'), indent=4)) +/// Module( +/// body=[ +/// Raise( +/// exc=Name(id='x', ctx=Load()), +/// cause=Name(id='y', ctx=Load()))], +/// type_ignores=[]) +/// ``` +type Raise = + { Exception: Expression + Cause: Expression option } + + static member Create(exc, ?cause) : Statement = { Exception = exc; Cause = cause } |> Raise + +/// A function definition. +/// +/// - name is a raw string of the function name. +/// - args is a arguments node. +/// - body is the list of nodes inside the function. +/// - decorator_list is the list of decorators to be applied, stored outermost first (i.e. the first in the list will be +/// applied last). +/// - returns is the return annotation. +/// - type_comment is an optional string with the type annotation as a comment. +type FunctionDef = + { Name: Identifier + Args: Arguments + Body: Statement list + DecoratorList: Expression list + Returns: Expression option + TypeComment: string option } + + static member Create(name, args, body, ?decoratorList, ?returns, ?typeComment) : Statement = + { Name = name + Args = args + Body = body + DecoratorList = defaultArg decoratorList [] + Returns = returns + TypeComment = typeComment } + |> FunctionDef + +/// global and nonlocal statements. names is a list of raw strings. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('global x,y,z'), indent=4)) +/// Module( +/// body=[ +/// Global( +/// names=[ +/// 'x', +/// 'y', +/// 'z'])], +/// type_ignores=[]) +/// +/// ``` +type Global = + { Names: Identifier list } + + static member Create(names) = { Names = names } + +/// global and nonlocal statements. names is a list of raw strings. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('nonlocal x,y,z'), indent=4)) +/// Module( +/// body=[ +/// Nonlocal( +/// names=[ +/// 'x', +/// 'y', +/// 'z'])], +/// type_ignores=[]) +/// ````` +type NonLocal = + { Names: Identifier list } + + static member Create(names) = { Names = names } + +/// An async function definition. +/// +/// - name is a raw string of the function name. +/// - args is a arguments node. +/// - body is the list of nodes inside the function. +/// - decorator_list is the list of decorators to be applied, stored outermost first (i.e. the first in the list will be +/// applied last). +/// - returns is the return annotation. +/// - type_comment is an optional string with the type annotation as a comment. +type AsyncFunctionDef = + { Name: Identifier + Args: Arguments + Body: Statement list + DecoratorList: Expression list + Returns: Expression option + TypeComment: string option } + + static member Create(name, args, body, decoratorList, ?returns, ?typeComment) = + { Name = name + Args = args + Body = body + DecoratorList = decoratorList + Returns = returns + TypeComment = typeComment } + +/// An import statement. names is a list of alias nodes. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('import x,y,z'), indent=4)) +/// Module( +/// body=[ +/// Import( +/// names=[ +/// alias(name='x'), +/// alias(name='y'), +/// alias(name='z')])], +/// type_ignores=[]) +/// ````` +type Import = { Names: Alias list } + + +/// Represents from x import y. module is a raw string of the ‘from’ name, without any leading dots, or None for +/// statements such as from . import foo. level is an integer holding the level of the relative import (0 means absolute +/// import). +/// +/// ```py +/// >>> print(ast.dump(ast.parse('from y import x,y,z'), indent=4)) +/// Module( +/// body=[ +/// ImportFrom( +/// module='y', +/// names=[ +/// alias(name='x'), +/// alias(name='y'), +/// alias(name='z')], +/// level=0)], +/// type_ignores=[]) +/// ``` +type ImportFrom = + { Module: Identifier option + Names: Alias list + Level: int option } + +/// A return statement. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('return 4'), indent=4)) +/// Module( +/// body=[ +/// Return( +/// value=Constant(value=4))], +/// type_ignores=[]) +/// ``` +type Return = { Value: Expression option } + +//#endregion + +//#region Expressions + +/// Attribute access, e.g. d.keys. value is a node, typically a Name. attr is a bare string giving the name of the +/// attribute, and ctx is Load, Store or Del according to how the attribute is acted on. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('snake.colour', mode='eval'), indent=4)) +/// Expression( +/// body=Attribute( +/// value=Name(id='snake', ctx=Load()), +/// attr='colour', +/// ctx=Load())) +/// ``` +type Attribute = + { Value: Expression + Attr: Identifier + Ctx: ExpressionContext } + +type NamedExpr = + { Target: Expression + Value: Expression + Loc: SourceLocation option } + +/// A subscript, such as l[1]. value is the subscripted object (usually sequence or mapping). slice is an index, slice +/// or key. It can be a Tuple and contain a Slice. ctx is Load, Store or Del according to the action performed with the +/// subscript. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('l[1:2, 3]', mode='eval'), indent=4)) +/// Expression( +/// body=Subscript( +/// value=Name(id='l', ctx=Load()), +/// slice=Tuple( +/// elts=[ +/// Slice( +/// lower=Constant(value=1), +/// upper=Constant(value=2)), +/// Constant(value=3)], +/// ctx=Load()), +/// ctx=Load())) +/// ``` +type Subscript = + { Value: Expression + Slice: Expression + Ctx: ExpressionContext } + +type BinOp = + { Left: Expression + Right: Expression + Operator: Operator + Loc: SourceLocation option } + +type BoolOp = + { Values: Expression list + Operator: BoolOperator + Loc: SourceLocation option } + +/// A comparison of two or more values. left is the first value in the comparison, ops the list of operators, and +/// comparators the list of values after the first element in the comparison. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('1 <= a < 10', mode='eval'), indent=4)) +/// Expression( +/// body=Compare( +/// left=Constant(value=1), +/// ops=[ +/// LtE(), +/// Lt()], +/// comparators=[ +/// Name(id='a', ctx=Load()), +/// Constant(value=10)])) +/// ````` +type Compare = + { Left: Expression + Comparators: Expression list + Ops: ComparisonOperator list + Loc: SourceLocation option } + +/// A unary operation. op is the operator, and operand any expression node. +type UnaryOp = + { Op: UnaryOperator + Operand: Expression + Loc: SourceLocation option } + +/// Node representing a single formatting field in an f-string. If the string contains a single formatting field and +/// nothing else the node can be isolated otherwise it appears in JoinedStr. +/// +/// - value is any expression node (such as a literal, a variable, or a function call). +/// - conversion is an integer: +/// - -1: no formatting +/// - 115: !s string formatting +/// - 114: !r repr formatting +/// - 97: !a ascii formatting +/// - format_spec is a JoinedStr node representing the formatting of the value, or None if no format was specified. Both +/// conversion and format_spec can be set at the same time. +type FormattedValue = + { Value: Expression + Conversion: int option + FormatSpec: Expression option } + +/// A function call. func is the function, which will often be a Name or Attribute object. Of the arguments: +/// +/// args holds a list of the arguments passed by position. +/// +/// keywords holds a list of keyword objects representing arguments passed by keyword. +/// +/// When creating a Call node, args and keywords are required, but they can be empty lists. starargs and kwargs are optional. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('func(a, b=c, *d, **e)', mode='eval'), indent=4)) +/// Expression( +/// body=Call( +/// func=Name(id='func', ctx=Load()), +/// args=[ +/// Name(id='a', ctx=Load()), +/// Starred( +/// value=Name(id='d', ctx=Load()), +/// ctx=Load())], +/// keywords=[ +/// keyword( +/// arg='b', +/// value=Name(id='c', ctx=Load())), +/// keyword( +/// value=Name(id='e', ctx=Load()))])) +/// ``` +type Call = + { Func: Expression + Args: Expression list + Keywords: Keyword list + Loc: SourceLocation option } + +type Emit = + { Value: string + Args: Expression list + Loc: SourceLocation option } + +/// An expression such as a if b else c. Each field holds a single node, so in the following example, all three are Name nodes. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('a if b else c', mode='eval'), indent=4)) +/// Expression( +/// body=IfExp( +/// test=Name(id='b', ctx=Load()), +/// body=Name(id='a', ctx=Load()), +/// orelse=Name(id='c', ctx=Load()))) +/// ``` +type IfExp = + { Test: Expression + Body: Expression + OrElse: Expression + Loc: SourceLocation option } + +/// lambda is a minimal function definition that can be used inside an expression. Unlike FunctionDef, body holds a +/// single node. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('lambda x,y: ...'), indent=4)) +/// Module( +/// body=[ +/// Expr( +/// value=Lambda( +/// args=arguments( +/// posonlyargs=[], +/// args=[ +/// arg(arg='x'), +/// arg(arg='y')], +/// kwonlyargs=[], +/// kw_defaults=[], +/// defaults=[]), +/// body=Constant(value=Ellipsis)))], +/// type_ignores=[]) +/// ``` +type Lambda = { Args: Arguments; Body: Expression } + +/// A tuple. elts holds a list of nodes representing the elements. ctx is Store if the container is an assignment target +/// (i.e. (x,y)=something), and Load otherwise. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('(1, 2, 3)', mode='eval'), indent=4)) +/// Expression( +/// body=Tuple( +/// elts=[ +/// Constant(value=1), +/// Constant(value=2), +/// Constant(value=3)], +/// ctx=Load())) +///``` +type Tuple = + { Elements: Expression list + Loc: SourceLocation option } + +/// A list or tuple. elts holds a list of nodes representing the elements. ctx is Store if the container is an +/// assignment target (i.e. (x,y)=something), and Load otherwise. +/// +/// ```python +/// >>> print(ast.dump(ast.parse('[1, 2, 3]', mode='eval'), indent=4)) +/// Expression( +/// body=List( +/// elts=[ +/// Constant(value=1), +/// Constant(value=2), +/// Constant(value=3)], +/// ctx=Load())) +///``` +type List = { Elements: Expression list } + +/// A set. elts holds a list of nodes representing the set’s elements. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('{1, 2, 3}', mode='eval'), indent=4)) +/// Expression( +/// body=Set( +/// elts=[ +/// Constant(value=1), +/// Constant(value=2), +/// Constant(value=3)])) +/// ``` +type Set = { Elements: Expression list } + +/// A dictionary. keys and values hold lists of nodes representing the keys and the values respectively, in matching +/// order (what would be returned when calling dictionary.keys() and dictionary.values()). +/// +/// When doing dictionary unpacking using dictionary literals the expression to be expanded goes in the values list, +/// with a None at the corresponding position in keys. +/// +/// ```py +/// >>> print(ast.dump(ast.parse('{"a":1, **d}', mode='eval'), indent=4)) +/// Expression( +/// body=Dict( +/// keys=[ +/// Constant(value='a'), +/// None], +/// values=[ +/// Constant(value=1), +/// Name(id='d', ctx=Load())])) +/// ``` +type Dict = + { Keys: Expression list + Values: Expression list } + +/// A variable name. id holds the name as a string, and ctx is one of the following types. +type Name = + { Id: Identifier + Context: ExpressionContext + Loc: SourceLocation option } + +[] +type AST = + | Expression of Expression + | Statement of Statement + | Operator of Operator + | BoolOperator of BoolOperator + | ComparisonOperator of ComparisonOperator + | UnaryOperator of UnaryOperator + | ExpressionContext of ExpressionContext + | Alias of Alias + | Module of Module + | Arguments of Arguments + | Keyword of Keyword + | Arg of Arg + | Identifier of Identifier + +[] +module PythonExtensions = + type Statement with + static member break'() : Statement = Break + static member continue' (?loc) : Statement = Continue + static member import(names) : Statement = Import { Names = names } + static member expr(value) : Statement = { Expr.Value = value } |> Expr + static member raise(value) : Statement = { Exception=value; Cause=None} |> Raise + + static member try'(body, ?handlers, ?orElse, ?finalBody, ?loc) : Statement = + Try.try' (body, ?handlers = handlers, ?orElse = orElse, ?finalBody = finalBody, ?loc = loc) + |> Try + + static member classDef(name, ?bases, ?keywords, ?body, ?decoratorList, ?loc) : Statement = + { Name = name + Bases = defaultArg bases [] + Keyword = defaultArg keywords [] + Body = defaultArg body [] + DecoratorList = defaultArg decoratorList [] + Loc = loc } + |> ClassDef + + static member assign(targets, value, ?typeComment) : Statement = + { Targets = targets + Value = value + TypeComment = typeComment } + |> Assign + + static member return'(?value) : Statement = Return { Value = value } + + static member for'(target, iter, ?body, ?orelse, ?typeComment) : Statement = + For.for' (target, iter, ?body = body, ?orelse = orelse, ?typeComment = typeComment) + |> For + + static member while'(test, body, ?orelse, ?loc) : Statement = + { While.Test = test + Body = body + Else = defaultArg orelse [] + Loc = loc } + |> While + + static member if'(test, body, ?orelse, ?loc) : Statement = + { Test = test + Body = body + Else = defaultArg orelse [] + Loc = loc } + |> If + + static member importFrom(``module``, names, ?level) = + ImportFrom.importFrom (``module``, names, ?level = level) + |> ImportFrom + + static member nonLocal(ids) = NonLocal.Create ids |> Statement.NonLocal + + type Expression with + + static member name(identifier, ?ctx, ?loc) : Expression = + { Id = identifier + Context = defaultArg ctx Load + Loc = loc } + |> Name + + static member name(name, ?ctx) : Expression = Expression.name(Identifier(name), ?ctx = ctx) + static member identifier(name, ?ctx, ?loc) : Expression = Expression.name(Identifier(name), ?ctx = ctx, ?loc = loc) + static member identifier(identifier, ?ctx, ?loc) : Expression = Expression.name(identifier, ?ctx = ctx, ?loc = loc) + + static member dict(keys, values) : Expression = { Keys = keys; Values = values } |> Dict + static member tuple(elts, ?loc) : Expression = { Elements = elts; Loc = loc } |> Tuple + + static member ifExp(test, body, orElse, ?loc) : Expression = + { Test = test + Body = body + OrElse = orElse + Loc = loc } + |> IfExp + + static member lambda(args, body) : Expression = { Args = args; Body = body } |> Lambda + + static member emit(value, ?args, ?loc) : Expression = + { Value = value + Args = defaultArg args [] + Loc = loc } + |> Emit + + static member call(func, ?args, ?kw, ?loc) : Expression = + { Func = func + Args = defaultArg args [] + Keywords = defaultArg kw [] + Loc = loc } + |> Call + + static member compare(left, ops, comparators, ?loc) : Expression = + { Left = left + Comparators = comparators + Ops = ops + Loc = loc } + |> Compare + + static member compare(left, op, comparators, ?loc) : Expression = + let op = + match op with + | BinaryEqual -> Eq + | BinaryEqualStrict -> Is + | BinaryUnequal -> NotEq + | BinaryUnequalStrict -> IsNot + | BinaryLess -> Lt + | BinaryLessOrEqual -> LtE + | BinaryGreater -> Gt + | BinaryGreaterOrEqual -> GtE + | _ -> failwith $"compare: Operator {op} not supported" + Expression.compare(left, [op], comparators) + + static member none() = + Expression.name (Identifier(name="None")) + + static member attribute(value, attr, ?ctx) : Expression = + { Value = value + Attr = attr + Ctx = defaultArg ctx Load } + |> Attribute + + static member unaryOp(op, operand, ?loc) : Expression = + let op = + match op with + | UnaryMinus -> USub + | UnaryPlus -> UAdd + | UnaryNot -> Not + | UnaryNotBitwise -> Invert + // | UnaryTypeof -> "typeof" + // | UnaryVoid -> + // | UnaryDelete -> "delete" + | _ -> failwith $"unaryOp: Operator {op} not supported" + + Expression.unaryOp(op, operand, ?loc=loc) + + static member unaryOp(op, operand, ?loc) : Expression = + { Op = op + Operand = operand + Loc = loc } + |> UnaryOp + + static member namedExpr(target, value, ?loc) = { Target = target; Value = value; Loc=loc } |> NamedExpr + + static member subscript(value, slice, ?ctx) : Expression = + { Value = value + Slice = slice + Ctx = defaultArg ctx Load } + |> Subscript + + static member binOp(left, op, right, ?loc) : Expression = + { Left = left + Right = right + Operator = op + Loc = loc } + |> BinOp + + static member binOp(left, op, right, ?loc) : Expression = + let op = + match op with + | BinaryPlus -> Add + | BinaryMinus -> Sub + | BinaryMultiply -> Mult + | BinaryDivide -> Div + | BinaryModulus -> Mod + | BinaryOrBitwise -> BitOr + | BinaryAndBitwise -> BitAnd + | BinaryShiftLeft -> LShift + | BinaryShiftRightZeroFill -> RShift + | BinaryShiftRightSignPropagating -> RShift + | BinaryXorBitwise -> BitXor + | _ -> failwith $"binOp: Operator {op} not supported" + + Expression.binOp(left, op, right, ?loc=loc) + + static member boolOp(op, values, ?loc) : Expression = { Values = values; Operator = op; Loc=loc } |> BoolOp + static member boolOp(op, values, ?loc) : Expression = + let op = + match op with + | LogicalAnd -> And + | LogicalOr -> Or + + Expression.boolOp(op, values, ?loc=loc) + static member constant(value: obj, ?loc) : Expression = Constant (value=value, loc=loc) + static member starred(value: Expression, ?ctx: ExpressionContext) : Expression = Starred(value, ctx |> Option.defaultValue Load) + static member list(elts: Expression list, ?ctx: ExpressionContext) : Expression = List(elts, ctx |> Option.defaultValue Load) + + type List with + + static member list(elts) = { Elements = elts } + + type ExceptHandler with + + static member exceptHandler(``type``, ?name, ?body, ?loc) = + { Type = ``type`` + Name = name + Body = defaultArg body [] + Loc = loc } + + type Alias with + + static member alias(name, ?asname) = { Name = name; AsName = asname } + + type Try with + + static member try'(body, ?handlers, ?orElse, ?finalBody, ?loc) = + { Body = body + Handlers = defaultArg handlers [] + OrElse = defaultArg orElse [] + FinalBody = defaultArg finalBody [] + Loc = loc } + + type FormattedValue with + + static member formattedValue(value, ?conversion, ?formatSpec) = + { Value = value + Conversion = conversion + FormatSpec = formatSpec } + + type Module with + + static member module'(body) = { Body = body } + + type Arg with + + static member arg(arg, ?annotation, ?typeComment) = + { Lineno = 0 + ColOffset = 0 + EndLineno = None + EndColOffset = None + + Arg = arg + Annotation = annotation + TypeComment = typeComment } + + static member arg(arg, ?annotation, ?typeComment) = + Arg.arg(Identifier(arg), ?annotation=annotation, ?typeComment=typeComment) + + type Keyword with + + static member keyword(arg, value) = + { Lineno = 0 + ColOffset = 0 + EndLineno = None + EndColOffset = None + + Arg = arg + Value = value } + + type Arguments with + + static member arguments(?args, ?posonlyargs, ?vararg, ?kwonlyargs, ?kwDefaults, ?kwarg, ?defaults) = + { PosOnlyArgs = defaultArg posonlyargs [] + Args = defaultArg args [] + VarArg = vararg + KwOnlyArgs = defaultArg kwonlyargs [] + KwDefaults = defaultArg kwDefaults [] + KwArg = kwarg + Defaults = defaultArg defaults [] } + static member empty = Arguments.arguments() + + type For with + + static member for'(target, iter, ?body, ?orelse, ?typeComment) = + { Target = target + Iterator = iter + Body = defaultArg body [] + Else = defaultArg orelse [] + TypeComment = typeComment } + + type AsyncFor with + + static member asyncFor(target, iter, body, ?orelse, ?typeComment) = + { Target = target + Iterator = iter + Body = body + Else = defaultArg orelse [] + TypeComment = typeComment } + + type ImportFrom with + + static member importFrom(``module``, names, ?level) = + { Module = ``module`` + Names = names + Level = level } + + type Expr with + + static member expr(value) : Expr = { Value = value } diff --git a/src/Fable.Transforms/Python/PythonPrinter.fs b/src/Fable.Transforms/Python/PythonPrinter.fs new file mode 100644 index 0000000000..15e667eb3e --- /dev/null +++ b/src/Fable.Transforms/Python/PythonPrinter.fs @@ -0,0 +1,806 @@ +// fsharplint:disable InterfaceNames +module Fable.Transforms.PythonPrinter + +open System +open Fable +open Fable.AST +open Fable.AST.Python + +type SourceMapGenerator = + abstract AddMapping: + originalLine: int + * originalColumn: int + * generatedLine: int + * generatedColumn: int + * ?name: string + -> unit + +type Writer = + inherit IDisposable + abstract MakeImportPath: string -> string + abstract Write: string -> Async + +type Printer = + abstract Line: int + abstract Column: int + abstract PushIndentation: unit -> unit + abstract PopIndentation: unit -> unit + abstract Print: string * ?loc:SourceLocation -> unit + abstract PrintNewLine: unit -> unit + abstract AddLocation: SourceLocation option -> unit + abstract MakeImportPath: string -> string + +type PrinterImpl(writer: Writer, map: SourceMapGenerator) = + // TODO: We can make this configurable later + let indentSpaces = " " + let builder = Text.StringBuilder() + let mutable indent = 0 + let mutable line = 1 + let mutable column = 0 + + let addLoc (loc: SourceLocation option) = + match loc with + | None -> () + | Some loc -> + map.AddMapping(originalLine = loc.start.line, + originalColumn = loc.start.column, + generatedLine = line, + generatedColumn = column, + ?name = loc.identifierName) + + member _.Flush(): Async = + async { + do! writer.Write(builder.ToString()) + builder.Clear() |> ignore + } + + member _.PrintNewLine() = + builder.AppendLine() |> ignore + line <- line + 1 + column <- 0 + + interface IDisposable with + member _.Dispose() = writer.Dispose() + + interface Printer with + member _.Line = line + member _.Column = column + + member _.PushIndentation() = + indent <- indent + 1 + + member _.PopIndentation() = + if indent > 0 then indent <- indent - 1 + + member _.AddLocation(loc) = + addLoc loc + + member _.Print(str, loc) = + addLoc loc + + if column = 0 then + let indent = String.replicate indent indentSpaces + builder.Append(indent) |> ignore + column <- indent.Length + + builder.Append(str) |> ignore + column <- column + str.Length + + member this.PrintNewLine() = + this.PrintNewLine() + + member this.MakeImportPath(path) = + writer.MakeImportPath(path) + + + +module PrinterExtensions = + type Printer with + + member printer.Print(stmt: Statement) = + match stmt with + | AsyncFunctionDef (def) -> printer.Print(def) + | FunctionDef (def) -> printer.Print(def) + | ImportFrom (im) -> printer.Print(im) + | NonLocal (st) -> printer.Print(st) + | ClassDef (st) -> printer.Print(st) + | AsyncFor (st) -> printer.Print(st) + | Return (rtn) -> printer.Print(rtn) + | Global (st) -> printer.Print(st) + | Import (im) -> printer.Print(im) + | Assign (st) -> printer.Print(st) + | While (wh) -> printer.Print(wh) + | Raise (st) -> printer.Print(st) + | Expr (st) -> printer.Print(st) + | For (st) -> printer.Print(st) + | Try (st) -> printer.Print(st) + | If (st) -> printer.Print(st) + | Pass -> printer.Print("pass") + | Break -> printer.Print("break") + | Continue -> printer.Print("continue") + + member printer.Print(node: Try) = + printer.Print("try: ", ?loc = node.Loc) + printer.PrintBlock(node.Body) + + for handler in node.Handlers do + printer.Print(handler) + + if node.OrElse.Length > 0 then + printer.Print("else: ") + printer.PrintBlock(node.OrElse) + + if node.FinalBody.Length > 0 then + printer.Print("finally: ") + printer.PrintBlock(node.FinalBody) + + member printer.Print(arg: Arg) = + let (Identifier name) = arg.Arg + printer.Print(name) + match arg.Annotation with + | Some ann -> + printer.Print("=") + printer.Print(ann) + | _ -> () + + member printer.Print(kw: Keyword) = + let (Identifier name) = kw.Arg + printer.Print(name) + printer.Print(" = ") + printer.Print(kw.Value) + + member printer.Print(arguments: Arguments) = + if not arguments.PosOnlyArgs.IsEmpty then + printer.PrintCommaSeparatedList(arguments.PosOnlyArgs) + printer.Print(", /") + + let args = arguments.Args |> List.map AST.Arg + let defaults = arguments.Defaults + for i = 0 to args.Length - 1 do + printer.Print(args.[i]) + if i >= args.Length - defaults.Length then + printer.Print("=") + printer.Print(defaults.[i-defaults.Length]) + if i < args.Length - 1 then + printer.Print(", ") + + match arguments.Args, arguments.VarArg with + | [], Some vararg -> + printer.Print("*") + printer.Print(vararg) + | args, Some vararg -> + printer.Print(", *") + printer.Print(vararg) + | _ -> () + + member printer.Print(assign: Assign) = + //printer.PrintOperation(targets.[0], "=", value, None) + + for target in assign.Targets do + printer.Print(target) + printer.Print(" = ") + + printer.Print(assign.Value) + + member printer.Print(expr: Expr) = printer.Print(expr.Value) + + member printer.Print(forIn: For) = + printer.Print("for ") + printer.Print(forIn.Target) + printer.Print(" in ") + printer.Print(forIn.Iterator) + printer.Print(":") + printer.PrintNewLine() + printer.PushIndentation() + printer.PrintStatements(forIn.Body) + printer.PopIndentation() + + member printer.Print(asyncFor: AsyncFor) = printer.Print("(AsyncFor)") + + member printer.Print(wh: While) = + printer.Print("while ") + printer.Print(wh.Test) + printer.Print(":") + printer.PrintNewLine() + printer.PushIndentation() + printer.PrintStatements(wh.Body) + printer.PopIndentation() + + member printer.Print(cd: ClassDef) = + let (Identifier name) = cd.Name + printer.Print("class ", ?loc = cd.Loc) + printer.Print(name) + + match cd.Bases with + | [] -> () + | xs -> + printer.Print("(") + printer.PrintCommaSeparatedList(cd.Bases) + printer.Print(")") + + printer.Print(":") + printer.PrintNewLine() + printer.PushIndentation() + printer.PrintStatements(cd.Body) + printer.PopIndentation() + + member printer.Print(ifElse: If) = + let rec printElse stmts = + match stmts with + | [] + | [ Pass ] -> () + | [ If { Test=test; Body=body; Else=els } ] -> + printer.Print("elif ") + printer.Print(test) + printer.Print(":") + printer.PrintBlock(body) + printElse els + | xs -> + printer.Print("else: ") + printer.PrintBlock(xs) + + + printer.Print("if ") + printer.Print(ifElse.Test) + printer.Print(":") + printer.PrintBlock(ifElse.Body) + printElse ifElse.Else + + member printer.Print(ri: Raise) = + printer.Print("raise ") + printer.Print(ri.Exception) + + member printer.Print(func: FunctionDef) = + printer.PrintFunction(Some func.Name, func.Args, func.Body, func.Returns, func.DecoratorList, isDeclaration = true) + printer.PrintNewLine() + + member printer.Print(gl: Global) = + printer.Print("global ") + printer.PrintCommaSeparatedList(gl.Names) + + member printer.Print(nl: NonLocal) = + if not (List.isEmpty nl.Names) then + printer.Print("nonlocal ") + printer.PrintCommaSeparatedList nl.Names + + member printer.Print(af: AsyncFunctionDef) = printer.Print("(AsyncFunctionDef)") + + member printer.Print(im: Import) = + if not (List.isEmpty im.Names) then + printer.Print("import ") + + if List.length im.Names > 1 then + printer.Print("(") + + printer.PrintCommaSeparatedList(im.Names) + + if List.length im.Names > 1 then + printer.Print(")") + + member printer.Print(im: ImportFrom) = + let (Identifier path) = im.Module |> Option.defaultValue (Identifier ".") + + printer.Print("from ") + printer.Print(printer.MakeImportPath(path)) + printer.Print(" import ") + + if not (List.isEmpty im.Names) then + if List.length im.Names > 1 then + printer.Print("(") + + printer.PrintCommaSeparatedList(im.Names) + + if List.length im.Names > 1 then + printer.Print(")") + + member printer.Print(node: Return) = + printer.Print("return ") + printer.PrintOptional(node.Value) + + member printer.Print(node: Attribute) = + printer.Print(node.Value) + printer.Print(".") + printer.Print(node.Attr) + + member printer.Print(ne: NamedExpr) = + printer.Print(ne.Target) + printer.Print(" :=") + printer.Print(ne.Value) + + member printer.Print(node: Subscript) = + printer.Print(node.Value) + printer.Print("[") + printer.Print(node.Slice) + printer.Print("]") + + member printer.Print(node: BinOp) = printer.PrintOperation(node.Left, node.Operator, node.Right) + + member printer.Print(node: BoolOp) = + for i, value in node.Values |> List.indexed do + printer.ComplexExpressionWithParens(value) + + if i < node.Values.Length - 1 then + printer.Print(node.Operator) + + member printer.Print(node: Compare) = + //printer.AddLocation(loc) + printer.ComplexExpressionWithParens(node.Left) + + for op, comparator in List.zip node.Ops node.Comparators do + printer.Print(op) + printer.ComplexExpressionWithParens(comparator) + + member printer.Print(node: UnaryOp) = + printer.AddLocation(node.Loc) + + match node.Op with + | USub + | UAdd + | Not + | Invert -> printer.Print(node.Op) + + printer.ComplexExpressionWithParens(node.Operand) + + member printer.Print(node: FormattedValue) = printer.Print("(FormattedValue)") + + member printer.Print(node: Call) = + printer.Print(node.Func) + printer.Print("(") + printer.PrintCommaSeparatedList(node.Args) + printer.PrintCommaSeparatedList(node.Keywords) + printer.Print(")") + + member printer.Print(node: Emit) = + let inline replace pattern (f: System.Text.RegularExpressions.Match -> string) input = + System.Text.RegularExpressions.Regex.Replace(input, pattern, f) + + let printSegment (printer: Printer) (value: string) segmentStart segmentEnd = + let segmentLength = segmentEnd - segmentStart + + if segmentLength > 0 then + let segment = value.Substring(segmentStart, segmentLength) + + let subSegments = + System.Text.RegularExpressions.Regex.Split(segment, @"\r?\n") + + for i = 1 to subSegments.Length do + let subSegment = + // Remove whitespace in front of new lines, + // indent will be automatically applied + if printer.Column = 0 then + subSegments.[i - 1].TrimStart() + else + subSegments.[i - 1] + + if subSegment.Length > 0 then + printer.Print(subSegment) + + if i < subSegments.Length then + printer.PrintNewLine() + + // Macro transformations + // https://fable.io/docs/communicate/js-from-fable.html#Emit-when-F-is-not-enough + let value = + node.Value + |> replace + @"\$(\d+)\.\.\." + (fun m -> + let rep = ResizeArray() + let i = int m.Groups.[1].Value + + for j = i to node.Args.Length - 1 do + rep.Add("$" + string j) + + String.concat ", " rep) + + |> replace + @"\{\{\s*\$(\d+)\s*\?(.*?)\:(.*?)\}\}" + (fun m -> + let i = int m.Groups.[1].Value + + match node.Args.[i] with + | Constant (value=c) -> m.Groups.[2].Value + | _ -> m.Groups.[3].Value) + + |> replace + @"\{\{([^\}]*\$(\d+).*?)\}\}" + (fun m -> + let i = int m.Groups.[2].Value + + match List.tryItem i node.Args with + | Some _ -> m.Groups.[1].Value + | None -> "") + + let matches = + System.Text.RegularExpressions.Regex.Matches(value, @"\$\d+") + + if matches.Count > 0 then + for i = 0 to matches.Count - 1 do + let m = matches.[i] + + let segmentStart = + if i > 0 then + matches.[i - 1].Index + matches.[i - 1].Length + else + 0 + + printSegment printer value segmentStart m.Index + + let argIndex = int m.Value.[1..] + + match List.tryItem argIndex node.Args with + | Some e -> printer.ComplexExpressionWithParens(e) + | None -> printer.Print("None") + + let lastMatch = matches.[matches.Count - 1] + printSegment printer value (lastMatch.Index + lastMatch.Length) value.Length + else + printSegment printer value 0 value.Length + + member printer.Print(node: IfExp) = + printer.Print(node.Body) + printer.Print(" if ") + printer.WithParens (node.Test) + printer.Print(" else ") + printer.WithParens(node.OrElse) + + member printer.Print(node: Lambda) = + printer.Print("lambda") + + if (List.isEmpty >> not) node.Args.Args then + printer.Print(" ") + + printer.PrintCommaSeparatedList(node.Args.Args) + printer.Print(": ") + + printer.Print(node.Body) + + + member printer.Print(node: Tuple) = + printer.Print("(", ?loc = node.Loc) + printer.PrintCommaSeparatedList(node.Elements) + + if node.Elements.Length = 1 then + printer.Print(",") + + printer.Print(")") + + member printer.Print(node: List) = printer.Print("(List)") + + member printer.Print(node: Set) = printer.Print("(Set)") + + member printer.Print(node: Dict) = + printer.Print("{") + printer.PrintNewLine() + printer.PushIndentation() + + let nodes = + List.zip node.Keys node.Values + |> List.mapi (fun i n -> (i, n)) + + for i, (key, value) in nodes do + printer.Print(key) + printer.Print(": ") + printer.Print(value) + + if i < nodes.Length - 1 then + printer.Print(",") + printer.PrintNewLine() + + printer.PrintNewLine() + printer.PopIndentation() + printer.Print("}") + + member printer.Print(node: Name) = + let (Identifier name) = node.Id + printer.Print(name) + + member printer.Print(node: ExceptHandler) = + printer.Print("except ", ?loc = node.Loc) + printer.PrintOptional(node.Type) + printer.PrintOptional(" as ", node.Name) + printer.Print(":") + + match node.Body with + | [] -> printer.PrintBlock([ Pass ]) + | _ -> printer.PrintBlock(node.Body) + + member printer.Print(node: Alias) = + printer.Print(node.Name) + + match node.AsName with + | Some (Identifier alias) when Identifier alias <> node.Name-> + printer.Print(" as ") + printer.Print(alias) + | _ -> () + + member printer.Print(node: Module) = printer.PrintStatements(node.Body) + + member printer.Print(node: Identifier) = + let (Identifier id) = node + printer.Print(id) + + member printer.Print(node: UnaryOperator) = + let op = + match node with + | Invert -> "~" + | Not -> "not " + | UAdd -> "+" + | USub -> "-" + + printer.Print(op) + + member printer.Print(node: ComparisonOperator) = + let op = + match node with + | Eq -> " == " + | NotEq -> " != " + | Lt -> " < " + | LtE -> " <= " + | Gt -> " > " + | GtE -> " >= " + | Is -> " is " + | IsNot -> " is not " + | In -> " in " + | NotIn -> " not in " + + printer.Print(op) + + member printer.Print(node: BoolOperator) = + let op = + match node with + | And -> " and " + | Or -> " or " + + printer.Print(op) + + member printer.Print(node: Operator) = + let op = + match node with + | Add -> " + " + | Sub -> " - " + | Mult -> " * " + | Div -> " / " + | FloorDiv -> " // " + | Mod -> " % " + | Pow -> " ** " + | LShift -> " << " + | RShift -> " >> " + | BitOr -> " | " + | BitXor -> " ^ " + | BitAnd -> $" & " + | MatMult -> $" @ " + + printer.Print(op) + + member printer.Print(node: Expression) = + match node with + | Attribute (ex) -> printer.Print(ex) + | Subscript (ex) -> printer.Print(ex) + | BoolOp (ex) -> printer.Print(ex) + | BinOp (ex) -> printer.Print(ex) + | Emit (ex) -> printer.Print(ex) + | UnaryOp (ex) -> printer.Print(ex) + | FormattedValue (ex) -> printer.Print(ex) + | Constant (value=value) -> + match box value with + | :? string as str -> + printer.Print("\"") + printer.Print(Web.HttpUtility.JavaScriptStringEncode(string value)) + printer.Print("\"") + | _ -> printer.Print(string value) + + | IfExp (ex) -> printer.Print(ex) + | Call (ex) -> printer.Print(ex) + | Lambda (ex) -> printer.Print(ex) + | NamedExpr (ex) -> printer.Print(ex) + | Name (ex) -> printer.Print(ex) + | Yield (expr) -> printer.Print("(Yield)") + | YieldFrom (expr) -> printer.Print("(Yield)") + | Compare (cp) -> printer.Print(cp) + | Dict (di) -> printer.Print(di) + | Tuple (tu) -> printer.Print(tu) + | Starred (ex, ctx) -> + printer.Print("*") + printer.Print(ex) + | List (elts, ctx) -> + printer.Print("[") + printer.PrintCommaSeparatedList(elts) + printer.Print("]") + + member printer.Print(node: AST) = + match node with + | AST.Expression (ex) -> printer.Print(ex) + | AST.Operator (op) -> printer.Print(op) + | AST.BoolOperator (op) -> printer.Print(op) + | AST.ComparisonOperator (op) -> printer.Print(op) + | AST.UnaryOperator (op) -> printer.Print(op) + | AST.ExpressionContext (_) -> () + | AST.Alias (al) -> printer.Print(al) + | AST.Module ``mod`` -> printer.Print(``mod``) + | AST.Arguments (arg) -> printer.Print(arg) + | AST.Keyword (kw) -> printer.Print(kw) + | AST.Arg (arg) -> printer.Print(arg) + | AST.Statement (st) -> printer.Print(st) + | AST.Identifier (id) -> printer.Print(id) + + member printer.PrintBlock + ( + nodes: 'a list, + printNode: Printer -> 'a -> unit, + printSeparator: Printer -> unit, + ?skipNewLineAtEnd + ) = + let skipNewLineAtEnd = defaultArg skipNewLineAtEnd false + printer.Print("") + printer.PrintNewLine() + printer.PushIndentation() + + for node in nodes do + printNode printer node + printSeparator printer + + printer.PopIndentation() + printer.Print("") + + if not skipNewLineAtEnd then + printer.PrintNewLine() + + member printer.PrintStatementSeparator() = + if printer.Column > 0 then + printer.Print("") + printer.PrintNewLine() + + member printer.PrintStatement(stmt: Statement, ?printSeparator) = + printer.Print(stmt) + + printSeparator + |> Option.iter (fun fn -> fn printer) + + member printer.PrintStatements(statements: Statement list) = + + for stmt in statements do + printer.PrintStatement(stmt, (fun p -> p.PrintStatementSeparator())) + + member printer.PrintBlock(nodes: Statement list, ?skipNewLineAtEnd) = + printer.PrintBlock( + nodes, + (fun p s -> p.PrintStatement(s)), + (fun p -> p.PrintStatementSeparator()), + ?skipNewLineAtEnd = skipNewLineAtEnd + ) + + member printer.PrintOptional(before: string, node: Identifier option) = + match node with + | None -> () + | Some node -> + printer.Print(before) + printer.Print(node) + + member printer.PrintOptional(before: string, node: AST option, after: string) = + match node with + | None -> () + | Some node -> + printer.Print(before) + printer.Print(node) + printer.Print(after) + + member printer.PrintOptional(node: AST option) = + match node with + | None -> () + | Some node -> printer.Print(node) + + member printer.PrintOptional(node: Expression option) = + printer.PrintOptional(node |> Option.map AST.Expression) + member printer.PrintOptional(node: Identifier option) = + match node with + | None -> () + | Some node -> printer.Print(node) + + member printer.PrintList(nodes: 'a list, printNode: Printer -> 'a -> unit, printSeparator: Printer -> unit) = + for i = 0 to nodes.Length - 1 do + printNode printer nodes.[i] + + if i < nodes.Length - 1 then + printSeparator printer + + member printer.PrintCommaSeparatedList(nodes: AST list) = + printer.PrintList(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) + + member printer.PrintCommaSeparatedList(nodes: Expression list) = + printer.PrintList(nodes, (fun p x -> p.SequenceExpressionWithParens(x)), (fun p -> p.Print(", "))) + member printer.PrintCommaSeparatedList(nodes: Arg list) = + printer.PrintCommaSeparatedList(nodes |> List.map AST.Arg) + member printer.PrintCommaSeparatedList(nodes: Keyword list) = + printer.PrintCommaSeparatedList(nodes |> List.map AST.Keyword) + member printer.PrintCommaSeparatedList(nodes: Alias list) = + printer.PrintCommaSeparatedList(nodes |> List.map AST.Alias) + member printer.PrintCommaSeparatedList(nodes: Identifier list) = + printer.PrintCommaSeparatedList(nodes |> List.map AST.Identifier) + + + member printer.PrintFunction + ( + id: Identifier option, + args: Arguments, + body: Statement list, + returnType: Expression option, + decoratorList: Expression list, + ?isDeclaration + ) = + for deco in decoratorList do + printer.Print("@") + printer.Print(deco) + printer.PrintNewLine() + + printer.Print("def ") + printer.PrintOptional(id) + printer.Print("(") + printer.Print(args) + printer.Print(")") + printer.PrintOptional(returnType) + printer.Print(":") + printer.PrintBlock(body, skipNewLineAtEnd = true) + + member printer.WithParens(expr: Expression) = + printer.Print("(") + printer.Print(expr) + printer.Print(")") + + member printer.SequenceExpressionWithParens(expr: Expression) = + match expr with + //| :? SequenceExpression -> printer.WithParens(expr) + | _ -> printer.Print(expr) + + /// Surround with parens anything that can potentially conflict with operator precedence + member printer.ComplexExpressionWithParens(expr: Expression) = + match expr with + | Constant (_) -> printer.Print(expr) + | Name (_) -> printer.Print(expr) + // | :? MemberExpression + // | :? CallExpression + // | :? ThisExpression + // | :? Super + // | :? SpreadElement + // | :? ArrayExpression + // | :? ObjectExpression -> expr.Print(printer) + | _ -> printer.WithParens(expr) + + member printer.PrintOperation(left, operator, right, ?loc) = + printer.AddLocation(loc) + printer.ComplexExpressionWithParens(left) + printer.Print(operator) + printer.ComplexExpressionWithParens(right) + +open PrinterExtensions +let run writer map (program: Module): Async = + + let printDeclWithExtraLine extraLine (printer: Printer) (decl: Statement) = + printer.Print(decl) + + if printer.Column > 0 then + //printer.Print(";") + printer.PrintNewLine() + if extraLine then + printer.PrintNewLine() + + async { + use printer = new PrinterImpl(writer, map) + + let imports, restDecls = + program.Body |> List.splitWhile (function + | Import _ + | ImportFrom _ -> true + | _ -> false) + + for decl in imports do + printDeclWithExtraLine false printer decl + + printer.PrintNewLine() + do! printer.Flush() + + for decl in restDecls do + printDeclWithExtraLine true printer decl + // TODO: Only flush every XXX lines? + do! printer.Flush() + } diff --git a/src/Fable.Transforms/Python/README.md b/src/Fable.Transforms/Python/README.md new file mode 100644 index 0000000000..d891e6c34e --- /dev/null +++ b/src/Fable.Transforms/Python/README.md @@ -0,0 +1,3 @@ +# Fable to Python + +Experimental support for Python transforming the Fable AST into a Python AST. diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index cd1bc78d43..0d80a52229 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1007,7 +1007,9 @@ let injectArg com (ctx: Context) r moduleName methName (genArgs: (string * Type) match genArg with | Number(numberKind,_) when com.Options.TypedArrays -> args @ [getTypedArrayName com numberKind |> makeIdentExpr] - | _ -> args + | _ -> + // TODO: Remove None args in tail position in Fable2Babel + args @ [ Expr.Value(ValueKind.NewOption(None, genArg, false), None) ] | Types.adder -> args @ [makeGenericAdder com ctx genArg] | Types.averager -> @@ -1110,10 +1112,11 @@ let fableCoreLib (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp match i.DeclaringEntityFullName, i.CompiledName with | _, "op_ErasedCast" -> List.tryHead args | _, ".ctor" -> typedObjExpr t [] |> Some + | _, "pyNative" | _, "jsNative" | _, "phpNative" -> // TODO: Fail at compile time? - addWarning com ctx.InlinePath r "jsNative is being compiled without replacement, this will fail at runtime." + addWarning com ctx.InlinePath r $"{i.CompiledName} is being compiled without replacement, this will fail at runtime." let runtimeMsg = "A function supposed to be replaced by JS native code has been called, please check." |> StringConstant |> makeValue None @@ -1201,6 +1204,7 @@ let fableCoreLib (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp | "typedArrays" -> makeBoolConst com.Options.TypedArrays |> Some | "extension" -> makeStrConst com.Options.FileExtension |> Some | _ -> None + | "Fable.Core.PyInterop", _ | "Fable.Core.JsInterop", _ -> match i.CompiledName, args with | "importDynamic", [path] -> diff --git a/src/fable-library-py/.flake8 b/src/fable-library-py/.flake8 new file mode 100644 index 0000000000..28a7341c5c --- /dev/null +++ b/src/fable-library-py/.flake8 @@ -0,0 +1,3 @@ +[flake8] +ignore = E731, T484, T400 # Do not assign a lambda expression, use a def +max-line-length = 121 \ No newline at end of file diff --git a/src/fable-library-py/README.md b/src/fable-library-py/README.md new file mode 100644 index 0000000000..e5a1cf1443 --- /dev/null +++ b/src/fable-library-py/README.md @@ -0,0 +1,4 @@ +# Fable Library for Python + +This module is used as the [Fable](https://fable.io/) library for +Python. \ No newline at end of file diff --git a/src/fable-library-py/fable/Async.fs b/src/fable-library-py/fable/Async.fs new file mode 100644 index 0000000000..75e65e77b8 --- /dev/null +++ b/src/fable-library-py/fable/Async.fs @@ -0,0 +1,52 @@ +module Async + +open System +open AsyncBuilder +open Fable.Core + +let emptyContinuation<'T> (_x: 'T) = + // NOP + () + +let defaultCancellationToken = new CancellationToken() + + +[] +type Async = + static member StartWithContinuations + ( + computation: IAsync<'T>, + continuation, + exceptionContinuation, + cancellationContinuation, + ?cancellationToken + ) : unit = + let trampoline = Trampoline() + + computation ( + { new IAsyncContext<'T> with + member this.onSuccess = continuation + member this.onError = exceptionContinuation + member this.onCancel = cancellationContinuation + member this.cancelToken = defaultArg cancellationToken defaultCancellationToken + member this.trampoline = trampoline } + ) + + static member StartWithContinuations(computation: IAsync<'T>, ?cancellationToken) : unit = + Async.StartWithContinuations( + computation, + emptyContinuation, + emptyContinuation, + emptyContinuation, + ?cancellationToken = cancellationToken + ) + + static member Start(computation, ?cancellationToken) = + Async.StartWithContinuations(computation, ?cancellationToken = cancellationToken) + + + static member StartImmediate(computation: IAsync<'T>, ?cancellationToken) = + Async.Start(computation, ?cancellationToken = cancellationToken) + +let startImmediate(computation: IAsync<'T>) = + Async.StartImmediate(computation, ?cancellationToken=None) diff --git a/src/fable-library-py/fable/AsyncBuilder.fs b/src/fable-library-py/fable/AsyncBuilder.fs new file mode 100644 index 0000000000..b795eb8518 --- /dev/null +++ b/src/fable-library-py/fable/AsyncBuilder.fs @@ -0,0 +1,214 @@ +module AsyncBuilder + +open System +open System.Collections.Generic +open Fable.Core +open Timer + +type Continuation<'T> = 'T -> unit + +type OperationCanceledError () = + inherit Exception ("The operation was canceled") + +type Continuations<'T> = Continuation<'T> * Continuation * Continuation + + +type CancellationToken (cancelled: bool) = + let mutable idx = 0 + let mutable cancelled = cancelled + let listeners = Dictionary unit>() + + new () = CancellationToken(false) + + member this.IsCancelled = cancelled + + member this.Cancel() = + if not cancelled then cancelled <- true + + for KeyValue (_, listener) in listeners do + listener () + + member this.AddListener(f: unit -> unit) = + let id = idx + idx <- idx + 1 + listeners.Add(idx, f) + id + + member this.RemoveListener(id: int) = listeners.Remove(id) + + member this.Register(f: unit -> unit) : IDisposable = + let id = this.AddListener(f) + + { new IDisposable with + member x.Dispose() = this.RemoveListener(id) |> ignore } + + member this.Register(f: obj -> unit, state: obj) : IDisposable = + let id = this.AddListener(fun () -> f (state)) + + { new IDisposable with + member x.Dispose() = this.RemoveListener(id) |> ignore } + +type Trampoline () = + let mutable callCount = 0 + + static member MaxTrampolineCallCount = 2000 + + member this.IncrementAndCheck() = + callCount <- callCount + 1 + callCount > Trampoline.MaxTrampolineCallCount + + member this.Hijack(f: unit -> unit) = + callCount <- 0 + let timer = Timer.Create(0., f) + timer.daemon <- true + timer.start () + +type IAsyncContext<'T> = + abstract member onSuccess : Continuation<'T> + abstract member onError : Continuation + abstract member onCancel : Continuation + + abstract member cancelToken : CancellationToken + abstract member trampoline : Trampoline + +type IAsync<'T> = IAsyncContext<'T> -> unit + +let protectedCont<'T> (f: IAsync<'T>) = + fun (ctx: IAsyncContext<'T>) -> + if ctx.cancelToken.IsCancelled then + ctx.onCancel (new OperationCanceledError()) + else if (ctx.trampoline.IncrementAndCheck()) then + ctx.trampoline.Hijack + (fun () -> + try + f ctx + with err -> ctx.onError (err)) + else + try + f ctx + with err -> ctx.onError (err) + +let protectedBind<'T, 'U> (computation: IAsync<'T>, binder: 'T -> IAsync<'U>) = + protectedCont + (fun (ctx: IAsyncContext<'U>) -> + computation ( + { new IAsyncContext<'T> with + member this.onSuccess = + fun (x: 'T) -> + try + binder (x) (ctx) + with ex -> ctx.onError (ex) + + member this.onError = ctx.onError + member this.onCancel = ctx.onCancel + member this.cancelToken = ctx.cancelToken + member this.trampoline = ctx.trampoline } + )) + +let protectedReturn<'T> (value: 'T) = protectedCont (fun (ctx: IAsyncContext<'T>) -> ctx.onSuccess (value)) + +type IAsyncBuilder = + abstract member Bind<'T, 'U> : IAsync<'T> * ('T -> IAsync<'U>) -> IAsync<'U> + + abstract member Combine<'T> : IAsync * IAsync<'T> -> IAsync<'T> + + abstract member Delay<'T> : (unit -> IAsync<'T>) -> IAsync<'T> + + //abstract member Return<'T> : [] values: 'T [] -> IAsync<'T> + abstract member Return<'T> : value: 'T -> IAsync<'T> + + abstract member While : (unit -> bool) * IAsync -> IAsync + abstract member Zero : unit -> IAsync + + +type AsyncBuilder () = + interface IAsyncBuilder with + + member this.Bind<'T, 'U>(computation: IAsync<'T>, binder: 'T -> IAsync<'U>) = protectedBind (computation, binder) + + member this.Combine<'T>(computation1: IAsync, computation2: IAsync<'T>) = + let self = this :> IAsyncBuilder + self.Bind(computation1, (fun () -> computation2)) + + member x.Delay<'T>(generator: unit -> IAsync<'T>) = protectedCont (fun (ctx: IAsyncContext<'T>) -> generator () (ctx)) + + + // public For(sequence: Iterable, body: (x: T) => IAsync) { + // const iter = sequence[Symbol.iterator](); + // let cur = iter.next(); + // return this.While(() => !cur.done, this.Delay(() => { + // const res = body(cur.value); + // cur = iter.next(); + // return res; + // })); + // } + + member this.Return<'T>(value: 'T) : IAsync<'T> = protectedReturn (unbox value) + // member this.Return<'T>([] value: 'T []) : IAsync<'T> = + // match value with + // | [||] -> protectedReturn (unbox null) + // | [| value |] -> protectedReturn value + // | _ -> failwith "Return takes zero or one argument." + + + // public ReturnFrom(computation: IAsync) { +// return computation; +// } + + // public TryFinally(computation: IAsync, compensation: () => void) { +// return protectedCont((ctx: IAsyncContext) => { +// computation({ +// onSuccess: (x: T) => { +// compensation(); +// ctx.onSuccess(x); +// }, +// onError: (x: any) => { +// compensation(); +// ctx.onError(x); +// }, +// onCancel: (x: any) => { +// compensation(); +// ctx.onCancel(x); +// }, +// cancelToken: ctx.cancelToken, +// trampoline: ctx.trampoline, +// }); +// }); +// } + + // public TryWith(computation: IAsync, catchHandler: (e: any) => IAsync) { +// return protectedCont((ctx: IAsyncContext) => { +// computation({ +// onSuccess: ctx.onSuccess, +// onCancel: ctx.onCancel, +// cancelToken: ctx.cancelToken, +// trampoline: ctx.trampoline, +// onError: (ex: any) => { +// try { +// catchHandler(ex)(ctx); +// } catch (ex2) { +// ctx.onError(ex2); +// } +// }, +// }); +// }); +// } + + // public Using(resource: T, binder: (x: T) => IAsync) { +// return this.TryFinally(binder(resource), () => resource.Dispose()); +// } + + member this.While(guard: unit -> bool, computation: IAsync) : IAsync = + let self = this :> IAsyncBuilder + + if guard () then + self.Bind(computation, (fun () -> self.While(guard, computation))) + else + self.Return() + + // member this.Bind<'T, 'U>(computation: IAsync<'T>, binder: 'T -> IAsync<'U>) = (this :> IAsyncBuilder).Bind(computation, binder) + member this.Zero() : IAsync = protectedCont (fun (ctx: IAsyncContext) -> ctx.onSuccess (())) + +// } + +let singleton : IAsyncBuilder = AsyncBuilder() :> _ diff --git a/src/fable-library-py/fable/Fable.Library.fsproj b/src/fable-library-py/fable/Fable.Library.fsproj new file mode 100644 index 0000000000..12bbb711b9 --- /dev/null +++ b/src/fable-library-py/fable/Fable.Library.fsproj @@ -0,0 +1,42 @@ + + + + netstandard2.0 + $(DefineConstants);FABLE_COMPILER + $(DefineConstants);FX_NO_BIGINT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/fable-library-py/fable/Native.fs b/src/fable-library-py/fable/Native.fs new file mode 100644 index 0000000000..9d5a08678e --- /dev/null +++ b/src/fable-library-py/fable/Native.fs @@ -0,0 +1,97 @@ +module Native + +// Disables warn:1204 raised by use of LanguagePrimitives.ErrorStrings.* +#nowarn "1204" + +open System.Collections.Generic +open Fable.Core +open Fable.Core.PyInterop +open Fable.Import + +type Cons<'T> = + [] + abstract Allocate : len: int -> 'T [] + +module Helpers = + [] + let arrayFrom (xs: 'T seq) : 'T [] = pyNative + + [] + let allocateArray (len: int) : 'T [] = pyNative + + [] + let allocateArrayFrom (xs: 'T []) (len: int) : 'T [] = pyNative + + let allocateArrayFromCons (cons: Cons<'T>) (len: int) : 'T [] = + if isNull (box cons) then + PY.Constructors.Array.Create(len) + else + cons.Allocate(len) + let inline isDynamicArrayImpl arr = PY.Constructors.Array.isArray arr + + // let inline typedArraySetImpl (target: obj) (source: obj) (offset: int): unit = + // !!target?set(source, offset) + + [] + let inline concatImpl (array1: 'T []) (arrays: 'T [] seq) : 'T [] = pyNative + + let inline fillImpl (array: 'T []) (value: 'T) (start: int) (count: int) : 'T [] = !! array?fill (value, start, start + count) + + [] + let foldImpl (folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T []) : 'State = + !! array?reduce (System.Func<'State, 'T, 'State>(folder), state) + + let inline foldIndexedImpl (folder: 'State -> 'T -> int -> 'State) (state: 'State) (array: 'T []) : 'State = + !! array?reduce (System.Func<'State, 'T, int, 'State>(folder), state) + + let inline foldBackImpl (folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T []) : 'State = + !! array?reduceRight (System.Func<'State, 'T, 'State>(folder), state) + + let inline foldBackIndexedImpl (folder: 'State -> 'T -> int -> 'State) (state: 'State) (array: 'T []) : 'State = + !! array?reduceRight (System.Func<'State, 'T, int, 'State>(folder), state) + + // Typed arrays not supported, only dynamic ones do + let inline pushImpl (array: 'T []) (item: 'T) : int = !! array?push (item) + + // Typed arrays not supported, only dynamic ones do + let inline insertImpl (array: 'T []) (index: int) (item: 'T) : 'T [] = !! array?splice (index, 0, item) + + // Typed arrays not supported, only dynamic ones do + let inline spliceImpl (array: 'T []) (start: int) (deleteCount: int) : 'T [] = !! array?splice (start, deleteCount) + + let inline reverseImpl (array: 'T []) : 'T [] = !! array?reverse () + + let inline copyImpl (array: 'T []) : 'T [] = !! array?slice () + + let inline skipImpl (array: 'T []) (count: int) : 'T [] = !! array?slice (count) + + let inline subArrayImpl (array: 'T []) (start: int) (count: int) : 'T [] = !! array?slice (start, start + count) + + let inline indexOfImpl (array: 'T []) (item: 'T) (start: int) : int = !! array?indexOf (item, start) + + let inline findImpl (predicate: 'T -> bool) (array: 'T []) : 'T option = !! array?find (predicate) + + let inline findIndexImpl (predicate: 'T -> bool) (array: 'T []) : int = !! array?findIndex (predicate) + + let inline collectImpl (mapping: 'T -> 'U []) (array: 'T []) : 'U [] = !! array?flatMap (mapping) + + let inline containsImpl (predicate: 'T -> bool) (array: 'T []) : bool = !! array?filter (predicate) + + let inline existsImpl (predicate: 'T -> bool) (array: 'T []) : bool = !! array?some (predicate) + + let inline forAllImpl (predicate: 'T -> bool) (array: 'T []) : bool = !! array?every (predicate) + + let inline filterImpl (predicate: 'T -> bool) (array: 'T []) : 'T [] = !! array?filter (predicate) + + [] + let reduceImpl (reduction: 'T -> 'T -> 'T) (array: 'T []) : 'T = pyNative + + let inline reduceBackImpl (reduction: 'T -> 'T -> 'T) (array: 'T []) : 'T = !! array?reduceRight (reduction) + + // Inlining in combination with dynamic application may cause problems with uncurrying + // Using Emit keeps the argument signature + [] + let sortInPlaceWithImpl (comparer: 'T -> 'T -> int) (array: 'T []) : unit = pyNative //!!array?sort(comparer) + + [] + let copyToTypedArray (src: 'T []) (srci: int) (trg: 'T []) (trgi: int) (cnt: int) : unit = pyNative diff --git a/src/fable-library-py/fable/Timer.fs b/src/fable-library-py/fable/Timer.fs new file mode 100644 index 0000000000..0f951aa772 --- /dev/null +++ b/src/fable-library-py/fable/Timer.fs @@ -0,0 +1,26 @@ +module Timer + +open Fable.Core + +/// This class represents an action that should be run only after a +/// certain amount of time has passed — a timer. Timer is a subclass of +/// Thread and as such also functions as an example of creating custom +/// threads. +type ITimer = + abstract daemon : bool with get, set + + /// Start the thread’s activity. + abstract start : unit -> unit + /// Stop the timer, and cancel the execution of the timer’s action. + /// This will only work if the timer is still in its waiting stage. + abstract cancel : unit -> unit + + /// Create a timer that will run function with arguments args and + /// keyword arguments kwargs, after interval seconds have passed. If + /// args is None (the default) then an empty list will be used. If + /// kwargs is None (the default) then an empty dict will be used. + [] + abstract Create : float * (unit -> unit) -> ITimer + +[] +let Timer : ITimer = pyNative diff --git a/src/fable-library-py/fable/__init__.py b/src/fable-library-py/fable/__init__.py new file mode 100644 index 0000000000..e9c63feb70 --- /dev/null +++ b/src/fable-library-py/fable/__init__.py @@ -0,0 +1,3 @@ +from . import string, types, list + +__all__ = ["list", "string", "types"] diff --git a/src/fable-library-py/fable/array.py b/src/fable-library-py/fable/array.py new file mode 100644 index 0000000000..e9902cf94e --- /dev/null +++ b/src/fable-library-py/fable/array.py @@ -0,0 +1,3 @@ +import builtins + +map = builtins.map diff --git a/src/fable-library-py/fable/async_.py b/src/fable-library-py/fable/async_.py new file mode 100644 index 0000000000..6bd0146315 --- /dev/null +++ b/src/fable-library-py/fable/async_.py @@ -0,0 +1,110 @@ +from threading import Timer +from .async_builder import ( + CancellationToken, + IAsyncContext, + OperationCanceledError, + Trampoline, + protected_bind, + protected_cont, + protected_return, +) +from .choice import Choice_makeChoice1Of2, Choice_makeChoice2Of2 + + +class Async: + pass + + +def empty_continuation(x=None): + pass + + +default_cancellation_token = CancellationToken() + + +def createCancellationToken(arg): + print("createCancellationToken()", arg) + cancelled, number = (arg, False) if isinstance(arg, bool) else (False, True) + token = CancellationToken(cancelled) + if number: + timer = Timer(arg / 1000.0, token.cancel) # type: ignore + timer.start() + + return token + + +def cancel(token: CancellationToken): + print("cancel()") + token.cancel() + + +def cancelAfter(token: CancellationToken, ms: int): + print("cancelAfter()", ms / 1000.0) + timer = Timer(ms / 1000.0, token.cancel) + timer.start() + + +def sleep(millisecondsDueTime: int): + def cont(ctx: IAsyncContext): + def cancel(): + timer.cancel() + ctx.on_cancel(OperationCanceledError()) + + token_id = ctx.cancel_token.add_listener(cancel) + + def timeout(): + ctx.cancel_token.remove_listener(token_id) + ctx.on_success() + + timer = Timer(millisecondsDueTime / 1000.0, timeout) + + return protected_cont(cont) + + +def ignore(computation): + return protected_bind(computation, lambda _x: protected_return()) + + +def catchAsync(work): + def cont(ctx: IAsyncContext): + def on_success(x): + ctx.on_success(Choice_makeChoice1Of2(x)) + + def on_error(err): + ctx.on_success(Choice_makeChoice2Of2(err)) + + ctx_ = IAsyncContext.create(on_success, on_error, ctx.on_cancel, ctx.trampoline, ctx.cancel_token) + work(ctx_) + + return protected_cont(cont) + + +def fromContinuations(f): + def cont(ctx: IAsyncContext): + f([ctx.on_success, ctx.on_error, ctx.on_cancel]) + + return protected_cont(cont) + + +def startWithContinuations( + computation, continuation=None, exception_continuation=None, cancellation_continuation=None, cancellation_token=None +): + trampoline = Trampoline() + + ctx = IAsyncContext.create( + continuation or empty_continuation, + exception_continuation or empty_continuation, + cancellation_continuation or empty_continuation, + trampoline, + cancellation_token or default_cancellation_token, + ) + + return computation(ctx) + + +def start(computation, cancellation_token=None): + return startWithContinuations(computation, cancellation_token=cancellation_token) + + +def startImmediate(computation): + return start(computation) diff --git a/src/fable-library-py/fable/async_builder.py b/src/fable-library-py/fable/async_builder.py new file mode 100644 index 0000000000..6c2acbb7f8 --- /dev/null +++ b/src/fable-library-py/fable/async_builder.py @@ -0,0 +1,297 @@ +from abc import abstractmethod +from collections import deque +from threading import Timer, Lock, RLock +from fable.util import IDisposable + + +class OperationCanceledError(Exception): + def __init__(self): + super().__init__("The operation was canceled") + + +class CancellationToken: + def __init__(self, cancelled: bool = False): + self.cancelled = cancelled + self.listeners = {} + self.idx = 0 + self.lock = RLock() + + @property + def is_cancelled(self): + return self.cancelled + + def cancel(self): + # print("CancellationToken:cancel") + cancel = False + with self.lock: + if not self.cancelled: + cancel = True + self.cancelled = True + + # print("cancel", cancel) + if cancel: + for listener in self.listeners.values(): + listener() + + def add_listener(self, f): + with self.lock: + id = self.idx + self.idx = self.idx + 1 + self.listeners[self.idx] = f + + return id + + def remove_listener(self, id: int): + with self.lock: + del self.listeners[id] + + def register(self, f, state=None): + if state: + id = self.add_listener(lambda _=None: f(state)) + else: + id = self.add_listener(f) + + def dispose(): + self.remove_listener(id) + + IDisposable.create(dispose) + + +class IAsyncContext: + @abstractmethod + def on_success(self, value=None): + ... + + @abstractmethod + def on_error(self, error): + ... + + @abstractmethod + def on_cancel(self, ct): + ... + + @property + @abstractmethod + def trampoline(self) -> "Trampoline": + ... + + @property + @abstractmethod + def cancel_token(self) -> CancellationToken: + ... + + @staticmethod + def create(on_success, on_error, on_cancel, trampoline, cancel_token): + return AnonymousAsyncContext(on_success, on_error, on_cancel, trampoline, cancel_token) + + +class AnonymousAsyncContext: + def __init__(self, on_success=None, on_error=None, on_cancel=None, trampoline=None, cancel_token=None): + self._on_success = on_success + self._on_error = on_error + self._on_cancel = on_cancel + + self.cancel_token = cancel_token + self.trampoline = trampoline + + def on_success(self, value=None): + return self._on_success(value) + + def on_error(self, error): + return self._on_error(error) + + def on_cancel(self, ct): + return self._on_cancel(ct) + + +# type IAsync<'T> = IAsyncContext<'T> -> unit + + +class Trampoline: + MaxTrampolineCallCount = 900 # Max recursion depth: 1000 + + def __init__(self): + self.call_count = 0 + self.lock = Lock() + self.queue = deque() + self.running = False + + def increment_and_check(self): + with self.lock: + self.call_count = self.call_count + 1 + return self.call_count > Trampoline.MaxTrampolineCallCount + + def run(self, action): + + if self.increment_and_check(): + with self.lock: + # print("queueing...") + self.queue.append(action) + + if not self.running: + self.running = True + timer = Timer(0.0, self._run) + timer.start() + else: + action() + + def _run(self): + while len(self.queue): + with self.lock: + self.call_count = 0 + action = self.queue.popleft() + # print("Running action: ", action) + + action() + + self.running = False + + +def protected_cont(f): + # print("protected_cont") + + def _protected_cont(ctx): + # print("_protected_cont") + + if ctx.cancel_token.is_cancelled: + ctx.on_cancel(OperationCanceledError()) + + def fn(): + try: + return f(ctx) + except Exception as err: + print("Exception: ", err) + ctx.on_error(err) + + ctx.trampoline.run(fn) + + return _protected_cont + + +def protected_bind(computation, binder): + # print("protected_bind") + + def cont(ctx): + # print("protected_bind: inner") + + def on_success(x): + # print("protected_bind: x", x, binder) + try: + binder(x)(ctx) + except Exception as err: + print("Exception: ", err) + ctx.on_error(err) + + ctx_ = IAsyncContext.create(on_success, ctx.on_error, ctx.on_cancel, ctx.trampoline, ctx.cancel_token) + return computation(ctx_) + + return protected_cont(cont) + + +def protected_return(value=None): + # print("protected_return:", value) + return protected_cont(lambda ctx: ctx.on_success(value)) + + +class AsyncBuilder: + def Bind(self, computation, binder): + return protected_bind(computation, binder) + + def Combine(self, computation1, computation2): + return self.Bind(computation1, lambda _=None: computation2) + + def Delay(self, generator): + # print("Delay", generator) + return protected_cont(lambda ctx: generator()(ctx)) + + def For(self, sequence, body): + print("For", sequence) + + done = False + it = iter(sequence) + try: + cur = next(it) + except StopIteration: + done = True + + def delay(): + # print("For:delay") + nonlocal cur, done + # print("cur", cur) + res = body(cur) + # print("For:delay:res", res) + try: + cur = next(it) + # print("For:delay:next", cur) + except StopIteration: + print("For:delay:stopIteration") + done = True + return res + + return self.While(lambda: not done, self.Delay(delay)) + + def Return(self, value=None): + # print("Return: ", value) + return protected_return(value) + + def ReturnFrom(self, computation): + return computation + + def TryFinally(self, computation, compensation): + def cont(ctx): + def on_success(x): + compensation() + ctx.on_success(x) + + def on_error(x): + compensation() + ctx.on_error(x) + + def on_cancel(x): + compensation() + ctx.on_cancel(x) + + ctx_ = IAsyncContext.create(on_success, on_error, on_cancel, ctx.trampoline, ctx.cancel_token) + computation(ctx_) + + return protected_cont(cont) + + def TryWith(self, computation, catchHandler): + # print("TryWith") + + def fn(ctx): + def on_error(err): + try: + catchHandler(err)(ctx) + except Exception as ex2: + ctx.on_error(ex2) + + ctx = IAsyncContext.create( + on_success=ctx.on_success, + on_cancel=ctx.on_cancel, + cancel_token=ctx.cancel_token, + trampoline=ctx.trampoline, + on_error=on_error, + ) + + return computation(ctx) + + return protected_cont(fn) + + def Using(self, resource, binder): + return self.TryFinally(binder(resource), lambda _=None: resource.Dispose()) + + def While(self, guard, computation): + # print("while") + if guard(): + # print("gard()") + return self.Bind(computation, lambda _=None: self.While(guard, computation)) + else: + # print("While:return") + return self.Return() + + def Zero(self): + return protected_cont(lambda ctx: ctx.on_success()) + + +singleton = AsyncBuilder() diff --git a/src/fable-library-py/fable/int32.py b/src/fable-library-py/fable/int32.py new file mode 100644 index 0000000000..1bf7e400a3 --- /dev/null +++ b/src/fable-library-py/fable/int32.py @@ -0,0 +1,29 @@ +def parse(string: str, style, unsigned, bitsize, radix: int = 10) -> int: + return int(string) + # const res = isValid(str, style, radix); + # if (res != null) { + # let v = Number.parseInt(res.sign + res.digits, res.radix); + # if (!Number.isNaN(v)) { + # const [umin, umax] = getRange(true, bitsize); + # if (!unsigned && res.radix !== 10 && v >= umin && v <= umax) { + # v = v << (32 - bitsize) >> (32 - bitsize); + # } + # const [min, max] = getRange(unsigned, bitsize); + # if (v >= min && v <= max) { + # return v; + # } + # } + # } + # throw new Error("Input string was not in a correct format."); + + +def op_UnaryNegation_Int8(x): + return x if x == -128 else -x + + +def op_UnaryNegation_Int16(x): + return x if x == -32768 else -x + + +def op_UnaryNegation_Int32(x): + return x if x == -2147483648 else -x diff --git a/src/fable-library-py/fable/long.py b/src/fable-library-py/fable/long.py new file mode 100644 index 0000000000..ce0533ab53 --- /dev/null +++ b/src/fable-library-py/fable/long.py @@ -0,0 +1,33 @@ +from typing import Optional + + +def fromBits(lowBits: int, highBits: int, unsigned: bool): + ret = lowBits + (highBits << 32) + if ret > 0x7FFFFFFFFFFFFFFF: + return ret - 0x10000000000000000 + + return ret + + +def op_LeftShift(self, numBits): + return self << numBits + + +def parse(string: str, style: int, unsigned: bool, _bitsize: int, radix: Optional[int] = None): + return int(string) + # res = isValid(str, style, radix) + # if res: + # def lessOrEqual(x: str, y: str): + # length = max(len(x), len(y)) + # return x.padStart(len, "0") <= y.padStart(len, "0"); + + # isNegative = res.sign == "-" + # maxValue = getMaxValue(unsigned or res.radix != 10, res.radix, isNegative); + # if (lessOrEqual(res.digits.upper(), maxValue)): + # string = res.sign + res.digits if isNegative else res.digits + # return LongLib.fromString(str, unsigned, res.radix); + + # raise Exception("Input string was not in a correct format."); + + +str = str # FIXME: remove str imports in the compiler. diff --git a/src/fable-library-py/fable/map_util.py b/src/fable-library-py/fable/map_util.py new file mode 100644 index 0000000000..f459684825 --- /dev/null +++ b/src/fable-library-py/fable/map_util.py @@ -0,0 +1,19 @@ +from typing import Dict, TypeVar + +K = TypeVar("K") +V = TypeVar("V") + + +def addToSet(v, set): + if set.has(v): + return False + + set.add(v) + return True + + +def addToDict(dict: Dict[K, V], k: K, v: V): + if k in dict: + raise Exception("An item with the same key has already been added. Key: " + str(k)) + + dict[k] = v diff --git a/src/fable-library-py/fable/numeric.py b/src/fable-library-py/fable/numeric.py new file mode 100644 index 0000000000..8815c87765 --- /dev/null +++ b/src/fable-library-py/fable/numeric.py @@ -0,0 +1,30 @@ +def to_fixed(x: float, dp=None) -> str: + if dp is not None: + fmt = "{:.%sf}" % dp + return fmt.format(x) + + return "{}".format(x) + + +def to_precision(x: float, sd=None): + if sd is not None: + fmt = "{:.%se}" % sd + return fmt.format(x) + + return "{}".format(x) + + +def to_exponential(x: float, dp=None): + if dp is not None: + fmt = "{:.%se}" % dp + return fmt.format(x) + + return "{}".format(x) + + +def to_hex(x) -> str: + return "{0:x}".format(x) + + +def multiply(x: int, y: int): + return x * y diff --git a/src/fable-library-py/fable/option.py b/src/fable-library-py/fable/option.py new file mode 100644 index 0000000000..443217ad32 --- /dev/null +++ b/src/fable-library-py/fable/option.py @@ -0,0 +1,36 @@ +from expression.core import option + + +class Some: + def __init__(self, value): + self.value = value + + +def defaultArg(value, default_value): + return option.default_arg(option.of_optional(value), default_value) + + +def defaultArgWith(value, default_value): + return option.default_arg(option.of_optional(value), default_value()) + + +def map(mapping, value): + return option.of_optional(value).map(mapping).default_value(None) + + +def toArray(value): + return option.of_optional(value).to_list() + + +def some(x): + return Some(x) if x is None or isinstance(x, Some) else x + + +def value(x): + if x is None: + raise Exception("Option has no value") + else: + return x.value if isinstance(x, Some) else x + + +__all__ = ["defaultArg", "defaultArgWith", "map", "some", "Some", "toArray", "value"] diff --git a/src/fable-library-py/fable/reflection.py b/src/fable-library-py/fable/reflection.py new file mode 100644 index 0000000000..d87bcdd97b --- /dev/null +++ b/src/fable-library-py/fable/reflection.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable, List, Optional, Type, Union + +from .types import Union as FsUnion + +Constructor = Callable[..., Any] + +EnumCase = Union[str, int] +FieldInfo = Union[str, "TypeInfo"] + + +@dataclass +class CaseInfo: + declaringType: TypeInfo + tag: int + name: str + fields: List[FieldInfo] + + +@dataclass +class TypeInfo: + fullname: str + generics: Optional[List[TypeInfo]] = None + construct: Optional[Constructor] = None + parent: Optional[TypeInfo] = None + fields: Optional[Callable[[], List[FieldInfo]]] = None + cases: Optional[Callable[[], List[CaseInfo]]] = None + enum_cases: Optional[List[EnumCase]] = None + + def __str__(self) -> str: + return full_name(self) + + +def class_type( + fullname: str, + generics: Optional[List[TypeInfo]] = None, + construct: Optional[Constructor] = None, + parent: Optional[TypeInfo] = None, +) -> TypeInfo: + return TypeInfo(fullname, generics, construct, parent) + + +def union_type( + fullname: str, + generics: List[TypeInfo], + construct: Type[FsUnion], + cases: Callable[[], List[List[FieldInfo]]], +) -> TypeInfo: + def fn() -> List[CaseInfo]: + nonlocal construct + + caseNames: List[str] = construct.cases() + mapper: Callable[[int, List[FieldInfo]], CaseInfo] = lambda i, fields: CaseInfo(t, i, caseNames[i], fields) + return [mapper(i, x) for i, x in enumerate(cases())] + + t: TypeInfo = TypeInfo(fullname, generics, construct, None, None, fn, None) + return t + + +def lambda_type(argType: TypeInfo, returnType: TypeInfo): + return TypeInfo("Microsoft.FSharp.Core.FSharpFunc`2", [argType, returnType]) + + +def delegate_type(*generics): + return TypeInfo("System.Func` %d" % len(generics), list(generics)) + + +def record_type( + fullname: str, generics: List[TypeInfo], construct: Constructor, fields: Callable[[], List[FieldInfo]] +) -> TypeInfo: + return TypeInfo(fullname, generics, construct, fields=fields) + + +def option_type(generic: TypeInfo) -> TypeInfo: + return TypeInfo("Microsoft.FSharp.Core.FSharpOption`1", [generic]) + + +def list_type(generic: TypeInfo) -> TypeInfo: + return TypeInfo("Microsoft.FSharp.Collections.FSharpList`1", [generic]) + + +def array_type(generic: TypeInfo) -> TypeInfo: + return TypeInfo(generic.fullname + "[]", [generic]) + + +def tuple_type(*generics: TypeInfo) -> TypeInfo: + return TypeInfo(fullname=f"System.Tuple`{len(generics)}", generics=list(generics)) + + +obj_type: TypeInfo = TypeInfo(fullname="System.Object") +unit_type: TypeInfo = TypeInfo("Microsoft.FSharp.Core.Unit") +char_type: TypeInfo = TypeInfo("System.Char") +string_type: TypeInfo = TypeInfo("System.String") +bool_type: TypeInfo = TypeInfo("System.Boolean") +int8_type: TypeInfo = TypeInfo("System.SByte") +uint8_type: TypeInfo = TypeInfo("System.Byte") +int16_type: TypeInfo = TypeInfo("System.Int16") +uint16_type: TypeInfo = TypeInfo("System.UInt16") +int32_type: TypeInfo = TypeInfo("System.Int32") +uint32_type: TypeInfo = TypeInfo("System.UInt32") +float32_type: TypeInfo = TypeInfo("System.Single") +float64_type: TypeInfo = TypeInfo("System.Double") +decimal_type: TypeInfo = TypeInfo("System.Decimal") + + +def equals(t1: TypeInfo, t2: TypeInfo) -> bool: + return t1 == t2 + + +def isGenericType(t): + return t.generics is not None and len(t.generics) + + +def getGenericTypeDefinition(t): + return t if t.generics is None else TypeInfo(t.fullname, list(map(lambda _: obj_type, t.generics))) + + +def name(info): + if isinstance(info, list): + return info[0] + + elif isinstance(info, CaseInfo): + return info.name + + else: + i = info.fullname.rfind("."); + return info.fullname if i == -1 else info.fullname[i + 1:] + + +def fullName(t): + gen = t.generics if t.generics is not None and not isinstance(t, list) else [] + if len(gen): + gen = ",".join([ fullName(x) for x in gen]) + return f"${t.fullname}[{gen}]" + + else: + return t.fullname + + +def namespace(t): + i = t.fullname.rfind(".") + return "" if i == -1 else t.fullname[0: i] + + +def isArray(t: TypeInfo) -> bool: + return t.fullname.endswith("[]") + + +def getElementType(t: TypeInfo) -> Optional[TypeInfo]: + return t.generics[0] if isArray(t) else None + + +# if (t1.fullname === "") { // Anonymous records +# return t2.fullname === "" +# && equalArraysWith(getRecordElements(t1), +# getRecordElements(t2), +# ([k1, v1], [k2, v2]) => k1 === k2 && equals(v1, v2)); +# } else { +# return t1.fullname === t2.fullname +# && equalArraysWith(getGenerics(t1), getGenerics(t2), equals); diff --git a/src/fable-library-py/fable/string.py b/src/fable-library-py/fable/string.py new file mode 100644 index 0000000000..66a14afae0 --- /dev/null +++ b/src/fable-library-py/fable/string.py @@ -0,0 +1,528 @@ +import re +from abc import ABC +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Callable, Iterable, Match, NoReturn, Optional, Pattern, Union, TypeVar + +# import multiply +# import Numeric +# import toExponential +# import toFixed +# import toHex +# import toPrecision +# import "./Date.js" +# import "./Numeric.js" +# import "./RegExp.js" +# import "./Types.js" +from .numeric import to_fixed, to_precision, to_exponential, to_hex, multiply +from .types import toString + +# import { escape } +# import { toString as dateToString } +# import { toString } + +T = TypeVar("T") + + +fsFormatRegExp: Pattern[str] = re.compile(r"(^|[^%])%([0+\- ]*)(\d+)?(?:\.(\d+))?(\w)") +interpolateRegExp: Pattern[str] = re.compile(r"(?:(^|[^%])%([0+\- ]*)(\d+)?(?:\.(\d+))?(\w))?%P\(\)") +formatRegExp: Pattern[str] = re.compile(r"\{(\d+)(,-?\d+)?(?:\:([a-zA-Z])(\d{0,2})|\:(.+?))?\}") + + +# const enum StringComparison { +# CurrentCulture = 0, +# CurrentCultureIgnoreCase = 1, +# InvariantCulture = 2, +# InvariantCultureIgnoreCase = 3, +# Ordinal = 4, +# OrdinalIgnoreCase = 5, +# } + +# function cmp(x: string, y: string, ic: boolean | StringComparison) { +# function isIgnoreCase(i: boolean | StringComparison) { +# return i === true || +# i === StringComparison.CurrentCultureIgnoreCase || +# i === StringComparison.InvariantCultureIgnoreCase || +# i === StringComparison.OrdinalIgnoreCase; +# } +# function isOrdinal(i: boolean | StringComparison) { +# return i === StringComparison.Ordinal || +# i === StringComparison.OrdinalIgnoreCase; +# } +# if (x == null) { return y == null ? 0 : -1; } +# if (y == null) { return 1; } // everything is bigger than null + +# if (isOrdinal(ic)) { +# if (isIgnoreCase(ic)) { x = x.toLowerCase(); y = y.toLowerCase(); } +# return (x === y) ? 0 : (x < y ? -1 : 1); +# } else { +# if (isIgnoreCase(ic)) { x = x.toLocaleLowerCase(); y = y.toLocaleLowerCase(); } +# return x.localeCompare(y); +# } +# } + +# export function compare(...args: any[]): number { +# switch (args.length) { +# case 2: return cmp(args[0], args[1], false); +# case 3: return cmp(args[0], args[1], args[2]); +# case 4: return cmp(args[0], args[1], args[2] === true); +# case 5: return cmp(args[0].substr(args[1], args[4]), args[2].substr(args[3], args[4]), false); +# case 6: return cmp(args[0].substr(args[1], args[4]), args[2].substr(args[3], args[4]), args[5]); +# case 7: return cmp(args[0].substr(args[1], args[4]), args[2].substr(args[3], args[4]), args[5] === true); +# default: throw new Error("String.compare: Unsupported number of parameters"); +# } +# } + +# export function compareOrdinal(x: string, y: string) { +# return cmp(x, y, StringComparison.Ordinal); +# } + +# export function compareTo(x: string, y: string) { +# return cmp(x, y, StringComparison.CurrentCulture); +# } + +# export function startsWith(str: string, pattern: string, ic: number) { +# if (str.length >= pattern.length) { +# return cmp(str.substr(0, pattern.length), pattern, ic) === 0; +# } +# return false; +# } + +# export function indexOfAny(str: string, anyOf: string[], ...args: number[]) { +# if (str == null || str === "") { +# return -1; +# } +# const startIndex = (args.length > 0) ? args[0] : 0; +# if (startIndex < 0) { +# throw new Error("Start index cannot be negative"); +# } +# const length = (args.length > 1) ? args[1] : str.length - startIndex; +# if (length < 0) { +# throw new Error("Length cannot be negative"); +# } +# if (length > str.length - startIndex) { +# throw new Error("Invalid startIndex and length"); +# } +# str = str.substr(startIndex, length); +# for (const c of anyOf) { +# const index = str.indexOf(c); +# if (index > -1) { +# return index + startIndex; +# } +# } +# return -1; +# } + +IPrintfFormatContinuation = Callable[[Callable[[str], Any]], Callable[[str], Any]] + + +@dataclass +class IPrintfFormat(ABC): + input: str + cont: IPrintfFormatContinuation + + +def printf(input: str) -> IPrintfFormat: + # print("printf: ", input) + format: IPrintfFormatContinuation = fsFormat(input) + return IPrintfFormat(input=input, cont=format) + + +def continuePrint(cont: Callable[[str], Any], arg: Union[IPrintfFormat, str]) -> Union[Any, Callable[[str], Any]]: + # print("continuePrint", cont) + if isinstance(arg, str): + return cont(arg) + + return arg.cont(cont) + + +def toConsole(arg: Union[IPrintfFormat, str]) -> Union[Any, Callable[[str], Any]]: + return continuePrint(print, arg) + + +def toConsoleError(arg: Union[IPrintfFormat, str]): + return continuePrint(lambda x: print(x), arg) + + +def toText(arg: Union[IPrintfFormat, str]) -> Union[str, Callable]: + cont: Callable[[str], Any] = lambda x: x + return continuePrint(cont, arg) + + +def toFail(arg: Union[IPrintfFormat, str]): + def fail(msg: str): + raise Exception(msg) + + return continuePrint(fail, arg) + + +def formatReplacement(rep: Any, flags: Any, padLength: Any, precision: Any, format: Any): + # print("Got here", rep, format) + sign = "" + flags = flags or "" + format = format or "" + + if isinstance(rep, (int, float)): + if format.lower() != "x": + if rep < 0: + rep = rep * -1 + sign = "-" + else: + if flags.find(" ") >= 0: + sign = " " + elif flags.find("+") >= 0: + sign = "+" + + elif format == "x": + rep = to_hex(rep) + elif format == "X": + rep = to_hex(rep).upper() + + precision = None if precision is None else int(precision) + if format in ("f", "F"): + precision = precision if precision is not None else 6 + rep = to_fixed(rep, precision) + elif format in ("g", "G"): + rep = to_precision(rep, precision) if precision is not None else to_precision(rep) + elif format in ("e", "E"): + rep = to_exponential(rep, precision) if precision is not None else to_exponential(rep) + else: # AOid + rep = str(rep) + + elif isinstance(rep, datetime): + rep = dateToString(rep) + else: + rep = toString(rep) + + if padLength is not None: + padLength = int(padLength) + zeroFlag = flags.find("0") >= 0 # Use '0' for left padding + minusFlag = flags.find("-") >= 0 # Right padding + ch = " " if minusFlag or not zeroFlag else "0" + if ch == "0": + rep = padLeft(rep, padLength - len(sign), ch, minusFlag) + rep = sign + rep + else: + rep = padLeft(sign + rep, padLength, ch, minusFlag) + + else: + rep = sign + rep + + return rep + + +def interpolate(string: str, values: Any) -> str: + valIdx = 0 + strIdx = 0 + result = "" + matches = interpolateRegExp.finditer(string) + for match in matches: + # The first group corresponds to the no-escape char (^|[^%]), the actual pattern starts in the next char + # Note: we don't use negative lookbehind because some browsers don't support it yet + matchIndex = match.start() + len(match[1] or "") + result += string[strIdx:matchIndex].replace("%%", "%") + [_, flags, padLength, precision, format] = match.groups() + # print(match.groups()) + result += formatReplacement(values[valIdx], flags, padLength, precision, format) + valIdx += 1 + + strIdx = match.end() + + result += string[strIdx:].replace("%%", "%") + return result + + +def formatOnce(str2: str, rep: Any): + # print("formatOnce: ", str2, rep) + + def match(m: Match[str]): + prefix, flags, padLength, precision, format = m.groups() + # print("prefix: ", [prefix]) + once: str = formatReplacement(rep, flags, padLength, precision, format) + # print("once:", [once]) + return prefix + once.replace("%", "%%") + + ret = fsFormatRegExp.sub(match, str2, count=1) + return ret + + +def createPrinter(string: str, cont: Callable[..., Any]): + # print("createPrinter", string) + + def _(*args: Any): + strCopy: str = string + for arg in args: + # print("Arg: ", [arg]) + strCopy = formatOnce(strCopy, arg) + # print("strCopy", strCopy) + + # print("strCopy", strCopy) + if fsFormatRegExp.search(strCopy): + return createPrinter(strCopy, cont) + return cont(strCopy.replace("%%", "%")) + + return _ + + +def fsFormat(str: str): + # print("fsFormat: ", [str]) + + def _(cont: Callable[..., Any]): + if fsFormatRegExp.search(str): + return createPrinter(str, cont) + return cont(str) + + return _ + + +def format(string: str, *args: Any) -> str: + print("format: ", string, args) + # if (typeof str === "object" and args.length > 0): + # # Called with culture info + # str = args[0] + # args.shift() + + def match(m: Match[str]): + print("Groups: ", m.groups()) + idx, padLength, format, precision_, pattern = list(m.groups()) + rep = args[int(idx)] + print("rep: ", [rep]) + if isinstance(rep, (int, float)): + precision: Optional[int] = None if precision_ is None else int(precision_) + + if format in ["f", "F"]: + precision = precision if precision is not None else 2 + rep = to_fixed(rep, precision) + + elif format in ["g", "G"]: + rep = to_precision(rep, precision) if precision is not None else to_precision(rep) + + elif format in ["e", "E"]: + rep = to_exponential(rep, precision) if precision is not None else to_exponential(rep) + + elif format in ["p", "P"]: + precision = precision if precision is not None else 2 + rep = to_fixed(multiply(rep, 100), precision) + " %" + + elif format in ["d", "D"]: + rep = padLeft(str(rep), precision, "0") if precision is not None else str(rep) + + elif format in ["x", "X"]: + rep = padLeft(to_hex(rep), precision, "0") if precision is not None else to_hex(rep) + if format == "X": + rep = rep.upper() + elif pattern: + sign = "" + + def match(m: Match[str]): + nonlocal sign, rep + + intPart, decimalPart = list(m.groups()) + # print("**************************: ", rep) + if rep < 0: + rep = multiply(rep, -1) + sign = "-" + + rep = to_fixed(rep, len(decimalPart) - 1 if decimalPart else 0) + return padLeft(rep, len(intPart or "") - len(sign) + (len(decimalPart) if decimalPart else 0), "0") + + rep = re.sub(r"(0+)(\.0+)?", match, pattern) + rep = sign + rep + + elif isinstance(rep, datetime): + rep = dateToString(rep, pattern or format) + else: + rep = toString(rep) + + try: + padLength = int((padLength or " ")[1:]) + rep = padLeft(str(rep), abs(padLength), " ", padLength < 0) + except ValueError: + pass + + print("return rep: ", [rep]) + return rep + + ret = formatRegExp.sub(match, string) + print("ret: ", ret) + return ret + + +def endsWith(string: str, search: str) -> bool: + return string.endswith(search) + + +def initialize(n: int, f: Callable[[int], str]) -> str: + if n < 0: + raise Exception("String length must be non-negative") + + xs = [""] * n + for i in range(n): + xs[i] = f(i) + + return "".join(xs) + + +# export function insert(str: string, startIndex: number, value: string) { +# if (startIndex < 0 || startIndex > str.length) { +# throw new Error("startIndex is negative or greater than the length of this instance."); +# } +# return str.substring(0, startIndex) + value + str.substring(startIndex); +# } + + +def isNullOrEmpty(string: Optional[str]): + not isinstance(string, str) or not len(string) + + +def isNullOrWhiteSpace(string: Optional[Any]) -> bool: + return not isinstance(string, str) or string.isspace() + + +def concat(*xs: Iterable[Any]) -> str: + return "".join(map(str, xs)) + + +def join(delimiter: str, xs: Iterable[Any]) -> str: + return delimiter.join(xs) + + +# export function joinWithIndices(delimiter: string, xs: string[], startIndex: number, count: number) { +# const endIndexPlusOne = startIndex + count; +# if (endIndexPlusOne > xs.length) { +# throw new Error("Index and count must refer to a location within the buffer."); +# } +# return xs.slice(startIndex, endIndexPlusOne).join(delimiter); +# } + + +def notSupported(name: str) -> NoReturn: + raise Exception("The environment doesn't support '" + name + "', please use a polyfill.") + + +# export function toBase64String(inArray: number[]) { +# let str = ""; +# for (let i = 0; i < inArray.length; i++) { +# str += String.fromCharCode(inArray[i]); +# } +# return typeof btoa === "function" ? btoa(str) : notSupported("btoa"); +# } + +# export function fromBase64String(b64Encoded: string) { +# const binary = typeof atob === "function" ? atob(b64Encoded) : notSupported("atob"); +# const bytes = new Uint8Array(binary.length); +# for (let i = 0; i < binary.length; i++) { +# bytes[i] = binary.charCodeAt(i); +# } +# return bytes; +# } + + +def padLeft(string: str, length: int, ch: Optional[str] = None, isRight: Optional[bool] = False) -> str: + ch = ch or " " + length = length - len(string) + for i in range(length): + string = string + ch if isRight else ch + string + + return string + + +def padRight(string: str, len: int, ch: Optional[str] = None) -> str: + return padLeft(string, len, ch, True) + + +# export function remove(str: string, startIndex: number, count?: number) { +# if (startIndex >= str.length) { +# throw new Error("startIndex must be less than length of string"); +# } +# if (typeof count === "number" && (startIndex + count) > str.length) { +# throw new Error("Index and count must refer to a location within the string."); +# } +# return str.slice(0, startIndex) + (typeof count === "number" ? str.substr(startIndex + count) : ""); +# } + + +def replace(string: str, search: str, replace: str): + return string.replace(search, replace) + + +def replicate(n: int, x: str) -> str: + return initialize(n, lambda _=0: x) + + +# export function getCharAtIndex(input: string, index: number) { +# if (index < 0 || index >= input.length) { +# throw new Error("Index was outside the bounds of the array."); +# } +# return input[index]; +# } + +# export function split(str: string, splitters: string[], count?: number, removeEmpty?: number) { +# count = typeof count === "number" ? count : undefined; +# removeEmpty = typeof removeEmpty === "number" ? removeEmpty : undefined; +# if (count && count < 0) { +# throw new Error("Count cannot be less than zero"); +# } +# if (count === 0) { +# return []; +# } +# if (!Array.isArray(splitters)) { +# if (removeEmpty === 0) { +# return str.split(splitters, count); +# } +# const len = arguments.length; +# splitters = Array(len - 1); +# for (let key = 1; key < len; key++) { +# splitters[key - 1] = arguments[key]; +# } +# } +# splitters = splitters.map((x) => escape(x)); +# splitters = splitters.length > 0 ? splitters : [" "]; +# let i = 0; +# const splits: string[] = []; +# const reg = new RegExp(splitters.join("|"), "g"); +# while (count == null || count > 1) { +# const m = reg.exec(str); +# if (m === null) { break; } +# if (!removeEmpty || (m.index - i) > 0) { +# count = count != null ? count - 1 : count; +# splits.push(str.substring(i, m.index)); +# } +# i = reg.lastIndex; +# } +# if (!removeEmpty || (str.length - i) > 0) { +# splits.push(str.substring(i)); +# } +# return splits; +# } + +# export function trim(str: string, ...chars: string[]) { +# if (chars.length === 0) { +# return str.trim(); +# } +# const pattern = "[" + escape(chars.join("")) + "]+"; +# return str.replace(new RegExp("^" + pattern), "").replace(new RegExp(pattern + "$"), ""); +# } + +# export function trimStart(str: string, ...chars: string[]) { +# return chars.length === 0 +# ? (str as any).trimStart() +# : str.replace(new RegExp("^[" + escape(chars.join("")) + "]+"), ""); +# } + +# export function trimEnd(str: string, ...chars: string[]) { +# return chars.length === 0 +# ? (str as any).trimEnd() +# : str.replace(new RegExp("[" + escape(chars.join("")) + "]+$"), ""); +# } + +# export function filter(pred: (char: string) => boolean, x: string) { +# return x.split("").filter((c) => pred(c)).join(""); +# } + + +def substring(string: str, startIndex: int, length: Optional[int] = None) -> str: + if length is not None: + return string[startIndex:startIndex + length] + + return string[startIndex:] diff --git a/src/fable-library-py/fable/task.py b/src/fable-library-py/fable/task.py new file mode 100644 index 0000000000..ebaf0d147e --- /dev/null +++ b/src/fable-library-py/fable/task.py @@ -0,0 +1,26 @@ +from expression.core import aiotools +from expression.system import OperationCanceledError, CancellationToken + +# class Trampoline: +# maxTrampolineCallCount = 2000 + +# def __init__(self) -> None: +# self.callCount = 0 + +# def incrementAndCheck(self): +# self.callCount += 1 +# return self.callCount > Trampoline.maxTrampolineCallCount + +# def hijack(self, f: Callable[[], None]): +# self.callCount = 0 +# setTimeout(f, 0) +# asyncio.e + +Continuation = aiotools.Continuation + +sleep = aiotools.sleep +start = aiotools.start +runSynchronously = aiotools.run_synchronously +startImmediate = aiotools.start_immediate + +__all__ = ["sleep", "start", "runSynchronously", "startImmediate"] diff --git a/src/fable-library-py/fable/task_builder.py b/src/fable-library-py/fable/task_builder.py new file mode 100644 index 0000000000..895728664b --- /dev/null +++ b/src/fable-library-py/fable/task_builder.py @@ -0,0 +1,71 @@ +from typing import Any, Callable, Optional, TypeVar, Awaitable, Coroutine +from expression.core.aiotools import from_result + +T = TypeVar("T") +U = TypeVar("U") + +Async = Callable[[], Awaitable[T]] + + +class AsyncBuilder: + def Bind(self, computation: Async[T], binder: Callable[[T], Async[U]]) -> Async[U]: + async def bind() -> U: + print("Bind: bind computation", computation) + t = await computation() + return await binder(t) + + return bind() + + def Combine(self, computation1: Awaitable[None], computation2: Awaitable[T]) -> Awaitable[T]: + return self.Bind(computation1, lambda _: computation2) + + def Delay(self, generator: Callable[[], Async[T]]) -> Async[T]: + async def deferred() -> Async[T]: + print("Delay: deferred: ", generator) + return generator() + + return deferred + + def Return(self, value: Optional[T] = None) -> Awaitable[Optional[T]]: + return from_result(value) + + def ReturnFrom(self, computation: Awaitable[T]) -> Awaitable[T]: + return computation + + def TryFinally(self, computation: Awaitable[T], compensation: Callable[[], None]) -> Awaitable[T]: + async def try_finally() -> T: + try: + t = await computation + finally: + compensation() + return t + + return try_finally() + + def TryWith(self, computation: Awaitable[T], catchHandler: Callable[[Any], Awaitable[T]]) -> Awaitable[T]: + async def try_with() -> T: + try: + t = await computation + except Exception as exn: + t = await catchHandler(exn) + return t + + return try_with() + + def Using(self, resource: T, binder: Callable[[T], Awaitable[U]]) -> Awaitable[U]: + return self.TryFinally(binder(resource), lambda: resource.Dispose()) + + def While(self, guard: Callable[[], bool], computation: Awaitable[None]) -> Awaitable[None]: + print("While") + if guard(): + return self.Bind(computation, lambda _: self.While(guard, computation)) + else: + return self.Return() + + def Zero(self) -> Awaitable[None]: + return from_result(None) + + +singleton = AsyncBuilder() + +__all__ = ["singleton"] diff --git a/src/fable-library-py/fable/types.py b/src/fable-library-py/fable/types.py new file mode 100644 index 0000000000..56970f5fe4 --- /dev/null +++ b/src/fable-library-py/fable/types.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +from abc import abstractstaticmethod +from typing import Any, Generic, Iterable, List, TypeVar, Union as Union_, Callable, Optional, cast + +from .util import IComparable + +T = TypeVar("T") + + +class FSharpRef: + def __init__(self, contentsOrGetter, setter=None) -> None: + + contents = contentsOrGetter + + def set_contents(value): + nonlocal contents + contents = value + + if callable(setter): + self.getter = contentsOrGetter + self.setter = setter + else: + self.getter = lambda: contents + self.setter = set_contents + + @property + def contents(self): + return self.getter() + + @contents.setter + def contents(self, v): + self.setter(v) + + +class Union(IComparable): + def __init__(self): + self.tag: int + self.fields: List[int] = [] + + @abstractstaticmethod + def cases() -> List[str]: + ... + + @property + def name(self) -> str: + return self.cases()[self.tag] + + def to_JSON(self) -> str: + raise NotImplementedError + # return str([self.name] + self.fields) if len(self.fields) else self.name + + def __str__(self) -> str: + if not len(self.fields): + return self.name + + fields = "" + with_parens = True + if len(self.fields) == 1: + field = str(self.fields[0]) + with_parens = field.find(" ") >= 0 + fields = field + else: + fields = ", ".join(map(str, self.fields)) + + return self.name + (" (" if with_parens else " ") + fields + (")" if with_parens else "") + + def __hash__(self) -> int: + hashes = map(hash, self.fields) + return hash([hash(self.tag), *hashes]) + + def __eq__(self, other: Any) -> bool: + if self is other: + return True + if not isinstance(other, Union): + return False + + if self.tag == other.tag: + return self.fields == other.fields + + return False + + def __lt__(self, other: Any) -> bool: + if self.tag == other.tag: + return self.fields < other.fields + + return self.tag < other.tag + + +def recordEquals(self, other): + if self is other: + return True + + return False + + +def recordCompareTo(self, other): + if self is other: + return 0 + + else: + for name in self.keys(): + if self[name] < other.get(name): + return -1 + elif self[name] > other.get(name): + return 1 + + return 0 + + +def recordGetHashCode(self): + return hash(*self.values()) + + +class Record(IComparable): + def toJSON(self) -> str: + return recordToJSON(this) + + def toString(self) -> str: + return recordToString(self) + + def GetHashCode(self) -> int: + return recordGetHashCode(self) + + def Equals(self, other: Record) -> bool: + return recordEquals(self, other) + + def CompareTo(self, other: Record) -> int: + return recordCompareTo(self, other) + + def __lt__(self, other: Any) -> bool: + raise NotImplementedError + + def __eq__(self, other: Any) -> bool: + return self.Equals(other) + + def __hash__(self) -> int: + return recordGetHashCode(self) + + +class Attribute: + pass + + +def seqToString(self): + str = "[" + + for count, x in enumerate(self): + if count == 0: + str += toString(x) + + elif count == 100: + str += "; ..." + break + + else: + str += "; " + toString(x) + + return str + "]" + + +def toString(x, callStack=0): + if x is not None: + # if (typeof x.toString === "function") { + # return x.toString(); + + if isinstance(x, str): + return str(x) + + if isinstance(x, Iterable): + return seqToString(x) + + # else: // TODO: Date? + # const cons = Object.getPrototypeOf(x).constructor; + # return cons === Object && callStack < 10 + # // Same format as recordToString + # ? "{ " + Object.entries(x).map(([k, v]) => k + " = " + toString(v, callStack + 1)).join("\n ") + " }" + # : cons.name; + + return str(x) + + +str = str +Exception = Exception + + +class FSharpException(Exception, IComparable): + def toJSON(self): + return recordToJSON(self) + + def toString(self): + return recordToString(self) + + def GetHashCode(self): + recordGetHashCode(self) + + def Equals(self, other: FSharpException): + return recordEquals(self, other) + + def CompareTo(self, other: FSharpException): + return recordCompareTo(self, other) + + +__all__ = ["Attribute", "Exception", "FSharpRef", "str", "Union"] diff --git a/src/fable-library-py/fable/util.py b/src/fable-library-py/fable/util.py new file mode 100644 index 0000000000..8498ad5fde --- /dev/null +++ b/src/fable-library-py/fable/util.py @@ -0,0 +1,339 @@ +from abc import ABC, abstractmethod +from threading import RLock +from typing import Callable, Iterable, List, TypeVar, Optional + +from libcst import Call + +T = TypeVar("T") + + +class ObjectDisposedException(Exception): + def __init__(self): + super().__init__("Cannot access a disposed object") + + +class IDisposable: + @abstractmethod + def dispose(self) -> None: + ... + + def __enter__(self): + """Enter context management.""" + return self + + def __exit__(self, exctype, excinst, exctb): + """Exit context management.""" + + self.dispose() + return False + + @staticmethod + def create(action): + """Create disposable from action. Will call action when + disposed.""" + return AnonymousDisposable(action) + + +class AnonymousDisposable(IDisposable): + def __init__(self, action): + self._is_disposed = False + self._action = action + self._lock = RLock() + + def dispose(self) -> None: + """Performs the task of cleaning up resources.""" + + dispose = False + with self._lock: + if not self._is_disposed: + dispose = True + self._is_disposed = True + + if dispose: + self._action() + + def __enter__(self): + if self._is_disposed: + raise ObjectDisposedException() + return self + + +class IEquatable(ABC): + def GetHashCode(self): + return hash(self) + + def Equals(self, other): + return self.Equals(other) + + @abstractmethod + def __eq__(self, other): + return NotImplemented + + @abstractmethod + def __hash__(self): + raise NotImplementedError + + +class IComparable(IEquatable): + def CompareTo(self, other): + if self < other: + return -1 + elif self == other: + return 0 + return 1 + + @abstractmethod + def __lt__(self, other): + raise NotImplementedError + + +def equals(a, b): + return a == b + + +def equal(a, b): + return a == b + + +def compare(a, b): + if a == b: + return 0 + if a < b: + return -1 + return 1 + + +def comparePrimitives(x, y) -> int: + return 0 if x == y else (-1 if x < y else 1) + + +def min(comparer, x, y): + return x if comparer(x, y) < 0 else y + + +def max(comparer, x, y): + return x if comparer(x, y) > 0 else y + + +def assertEqual(actual, expected, msg=None) -> None: + if actual != expected: + raise Exception(msg or f"Expected: ${expected} - Actual: ${actual}") + + +def assertNotEqual(actual: T, expected: T, msg: Optional[str] = None) -> None: + if actual == expected: + raise Exception(msg or f"Expected: ${expected} - Actual: ${actual}") + + +def createAtom(value=None): + atom = value + + def _(value=None, isSetter=None): + nonlocal atom + + if not isSetter: + return atom + else: + atom = value + return None + + return _ + + +def createObj(fields): + obj = {} + + for k, v in fields: + obj[k] = v + + return obj + + +def int16ToString(i, radix=10): + if radix == 10: + return "{:d}".format(i) + if radix == 16: + return "{:x}".format(i) + if radix == 2: + return "{:b}".format(i) + return str(i) + + +def int32ToString(i: int, radix: int = 10) -> str: + if radix == 10: + return "{:d}".format(i) + if radix == 16: + return "{:x}".format(i) + if radix == 2: + return "{:b}".format(i) + return str(i) + + +def clear(col): + if isinstance(col, List): + col.clear() + + +class IEnumerator(IDisposable): + @abstractmethod + def Current(self): + ... + + @abstractmethod + def MoveNext(self): + ... + + @abstractmethod + def Reset(self): + ... + + @abstractmethod + def Dispose(self): + ... + + def __getattr__(self, name): + return { + "System_Collections_Generic_IEnumerator_1_get_Current": self.Current, + "System_Collections.IEnumerator_get_Current": self.Current, + "System_Collections_IEnumerator_MoveNext": self.MoveNext, + "System_Collections.IEnumerator_Reset": self.Reset, + }[name] + + +class IEnumerable(Iterable): + @abstractmethod + def GetEnumerator(self): + ... + + +class Enumerator(IEnumerator): + def __init__(self, iter) -> None: + self.iter = iter + self.current = None + + def Current(self): + if self.current is not None: + return self.current + return None + + def MoveNext(self): + try: + cur = next(self.iter) + self.current = cur + return True + except StopIteration: + return False + + def Reset(self): + raise Exception("Python iterators cannot be reset") + + def Dispose(self): + return + + +def getEnumerator(o): + attr = getattr(o, "GetEnumerator", None) + if attr: + return attr() + else: + return Enumerator(iter(o)) + + +CURRIED_KEY = "__CURRIED__" + + +def uncurry(arity: int, f: Callable): + # f may be a function option with None value + if f is None: + return f + + fns = { + 2: lambda a1, a2: f(a1)(a2), + 3: lambda a1, a2, a3: f(a1)(a2)(a3), + 4: lambda a1, a2, a3, a4: f(a1)(a2)(a3)(a4), + 5: lambda a1, a2, a3, a4, a5: f(a1)(a2)(a3)(a4)(a5), + 6: lambda a1, a2, a3, a4, a5, a6: f(a1)(a2)(a3)(a4)(a5)(a6), + 7: lambda a1, a2, a3, a4, a5, a6, a7: f(a1)(a2)(a3)(a4)(a5)(a6)(a7), + 8: lambda a1, a2, a3, a4, a5, a6, a7, a8: f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8), + } + + try: + uncurriedFn = fns[arity] + except Exception: + raise Exception(f"Uncurrying to more than 8-arity is not supported: {arity}") + + setattr(f, CURRIED_KEY, f) + return uncurriedFn + + +def curry(arity: int, f: Callable) -> Callable: + if f is None or arity == 1: + return f + + if hasattr(f, CURRIED_KEY): + return getattr(f, CURRIED_KEY) + + if arity == 2: + return lambda a1: lambda a2: f(a1, a2) + elif arity == 3: + return lambda a1: lambda a2: lambda a3: f(a1, a2, a3) + elif arity == 4: + return lambda a1: lambda a2: lambda a3: lambda a4: f(a1, a2, a3, a4) + elif arity == 4: + return lambda a1: lambda a2: lambda a3: lambda a4: lambda a5: f(a1, a2, a3, a4, a5) + elif arity == 6: + return lambda a1: lambda a2: lambda a3: lambda a4: lambda a5: lambda a6: f(a1, a2, a3, a4, a5, a6) + elif arity == 7: + return lambda a1: lambda a2: lambda a3: lambda a4: lambda a5: lambda a6: lambda a7: f( + a1, a2, a3, a4, a5, a6, a7 + ) + elif arity == 8: + return lambda a1: lambda a2: lambda a3: lambda a4: lambda a5: lambda a6: lambda a7: lambda a8: f( + a1, a2, a3, a4, a5, a6, a7, a8 + ) + else: + raise Exception("Currying to more than 8-arity is not supported: %d" % arity) + + +def isArrayLike(x): + return hasattr(x, "__len__") + + +def isDisposable(x): + return x is not None and isinstance(x, IDisposable) + + +def toIterator(en): + print("toIterator: ", en) + + class Iterator: + def __iter__(self): + return self + + def __next__(self): + has_next = getattr(en, "System_Collections_IEnumerator_MoveNext")() + if not has_next: + raise StopIteration + return getattr(en, "System_Collections_IEnumerator_get_Current")() + + return Iterator() + + +def stringHash(s): + h = 5381 + for c in s: + h = (h * 33) ^ ord(c) + + return h + + +def numberHash(x): + return x * 2654435761 | 0 + + +def structuralHash(x): + return hash(x) + + +def physicalHash(x): + return hash(x) diff --git a/src/fable-library-py/requirements.txt b/src/fable-library-py/requirements.txt new file mode 100644 index 0000000000..0a991ee23b --- /dev/null +++ b/src/fable-library-py/requirements.txt @@ -0,0 +1,4 @@ +expression +pytest +pytest-cov +pytest-asyncio diff --git a/src/fable-library-py/setup.cfg b/src/fable-library-py/setup.cfg new file mode 100644 index 0000000000..2f50783781 --- /dev/null +++ b/src/fable-library-py/setup.cfg @@ -0,0 +1,6 @@ + +[aliases] +test = pytest + +[tool:pytest] +testpaths = tests \ No newline at end of file diff --git a/src/fable-library-py/setup.py b/src/fable-library-py/setup.py new file mode 100644 index 0000000000..89353b603b --- /dev/null +++ b/src/fable-library-py/setup.py @@ -0,0 +1,43 @@ +# coding=utf-8 +# read the contents of your README file +from os import path + +import setuptools + +this_directory = path.abspath(path.dirname(__file__)) +with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: + long_description = f.read() + +setuptools.setup( + name="fable-library", + version="0.1.0", + description="Fable library for Python", + long_description=long_description, + long_description_content_type="text/markdown", + author="Dag Brattli", + author_email="dag@brattli.net", + license="MIT License", + url="https://github.com/fable-compiler/Fable", + download_url="https://github.com/fable-compiler/Fable", + zip_safe=True, + # https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + "Development Status :: 3 - Alpha", + # 'Development Status :: 4 - Beta', + # 'Development Status :: 5 - Production/Stable', + "Environment :: Other Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.8", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + python_requires=">=3.8", + install_requires=["expression"], + setup_requires=["pytest-runner"], + tests_require=["pytest", "pytest-cov", "pytest-asyncio"], + package_data={"fable": ["py.typed"]}, + packages=setuptools.find_packages(), + package_dir={"fable": "fable"}, + include_package_data=True, +) diff --git a/src/fable-library/Array.fs b/src/fable-library/Array.fs index 31467ed6f0..5cb7c92631 100644 --- a/src/fable-library/Array.fs +++ b/src/fable-library/Array.fs @@ -5,119 +5,11 @@ module ArrayModule open System.Collections.Generic open Fable.Core -open Fable.Core.JsInterop +//open Fable.Core.JsInterop open Fable.Import -type Cons<'T> = - [] - abstract Allocate: len: int -> 'T[] - -module Helpers = - [] - let arrayFrom (xs: 'T seq): 'T[] = jsNative - - [] - let allocateArray (len: int): 'T[] = jsNative - - [] - let allocateArrayFrom (xs: 'T[]) (len: int): 'T[] = jsNative - - let allocateArrayFromCons (cons: Cons<'T>) (len: int): 'T[] = - if jsTypeof cons = "function" - then cons.Allocate(len) - else JS.Constructors.Array.Create(len) - - let inline isDynamicArrayImpl arr = - JS.Constructors.Array.isArray arr - - let inline isTypedArrayImpl arr = - JS.Constructors.ArrayBuffer.isView arr - - // let inline typedArraySetImpl (target: obj) (source: obj) (offset: int): unit = - // !!target?set(source, offset) - - [] - let inline concatImpl (array1: 'T[]) (arrays: 'T[] seq): 'T[] = - jsNative - - let inline fillImpl (array: 'T[]) (value: 'T) (start: int) (count: int): 'T[] = - !!array?fill(value, start, start + count) - - let inline foldImpl (folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T[]): 'State = - !!array?reduce(System.Func<'State, 'T, 'State>(folder), state) - - let inline foldIndexedImpl (folder: 'State -> 'T -> int -> 'State) (state: 'State) (array: 'T[]): 'State = - !!array?reduce(System.Func<'State, 'T, int, 'State>(folder), state) - - let inline foldBackImpl (folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T[]): 'State = - !!array?reduceRight(System.Func<'State, 'T, 'State>(folder), state) - - let inline foldBackIndexedImpl (folder: 'State -> 'T -> int -> 'State) (state: 'State) (array: 'T[]): 'State = - !!array?reduceRight(System.Func<'State, 'T, int, 'State>(folder), state) - - // Typed arrays not supported, only dynamic ones do - let inline pushImpl (array: 'T[]) (item: 'T): int = - !!array?push(item) - - // Typed arrays not supported, only dynamic ones do - let inline insertImpl (array: 'T[]) (index: int) (item: 'T): 'T[] = - !!array?splice(index, 0, item) - - // Typed arrays not supported, only dynamic ones do - let inline spliceImpl (array: 'T[]) (start: int) (deleteCount: int): 'T[] = - !!array?splice(start, deleteCount) - - let inline reverseImpl (array: 'T[]): 'T[] = - !!array?reverse() - - let inline copyImpl (array: 'T[]): 'T[] = - !!array?slice() - - let inline skipImpl (array: 'T[]) (count: int): 'T[] = - !!array?slice(count) - - let inline subArrayImpl (array: 'T[]) (start: int) (count: int): 'T[] = - !!array?slice(start, start + count) - - let inline indexOfImpl (array: 'T[]) (item: 'T) (start: int): int = - !!array?indexOf(item, start) - - let inline findImpl (predicate: 'T -> bool) (array: 'T[]): 'T option = - !!array?find(predicate) - - let inline findIndexImpl (predicate: 'T -> bool) (array: 'T[]): int = - !!array?findIndex(predicate) - - let inline collectImpl (mapping: 'T -> 'U[]) (array: 'T[]): 'U[] = - !!array?flatMap(mapping) - - let inline containsImpl (predicate: 'T -> bool) (array: 'T[]): bool = - !!array?filter(predicate) - - let inline existsImpl (predicate: 'T -> bool) (array: 'T[]): bool = - !!array?some(predicate) - - let inline forAllImpl (predicate: 'T -> bool) (array: 'T[]): bool = - !!array?every(predicate) - - let inline filterImpl (predicate: 'T -> bool) (array: 'T[]): 'T[] = - !!array?filter(predicate) - - let inline reduceImpl (reduction: 'T -> 'T -> 'T) (array: 'T[]): 'T = - !!array?reduce(reduction) - - let inline reduceBackImpl (reduction: 'T -> 'T -> 'T) (array: 'T[]): 'T = - !!array?reduceRight(reduction) - - // Inlining in combination with dynamic application may cause problems with uncurrying - // Using Emit keeps the argument signature - [] - let sortInPlaceWithImpl (comparer: 'T -> 'T -> int) (array: 'T[]): unit = jsNative //!!array?sort(comparer) - - [] - let copyToTypedArray (src: 'T[]) (srci: int) (trg: 'T[]) (trgi: int) (cnt: int): unit = jsNative - -open Helpers +open Native +open Native.Helpers let private indexNotFound() = failwith "An index satisfying the predicate was not found in the collection." @@ -539,9 +431,10 @@ let choose (chooser: 'T->'U option) (array: 'T[]) ([] cons: Cons<'U>) = match chooser array.[i] with | None -> () | Some y -> pushImpl res y |> ignore - if jsTypeof cons = "function" - then map id res cons - else res // avoid extra copy + + match box cons with + | null -> res // avoid extra copy + | _ -> map id res cons let foldIndexed folder (state: 'State) (array: 'T[]) = // if isTypedArrayImpl array then diff --git a/src/fable-library/Fable.Library.fsproj b/src/fable-library/Fable.Library.fsproj index 8c638e193e..3126638077 100644 --- a/src/fable-library/Fable.Library.fsproj +++ b/src/fable-library/Fable.Library.fsproj @@ -19,6 +19,7 @@ + diff --git a/src/fable-library/Map.fs b/src/fable-library/Map.fs index 948460f782..92806f7e4f 100644 --- a/src/fable-library/Map.fs +++ b/src/fable-library/Map.fs @@ -521,6 +521,8 @@ module MapTree = then Some(en.Current, en) else None) +open Fable.Core + [] [] [] @@ -861,7 +863,8 @@ let ofList (elements: ('Key * 'Value) list) = Map<_, _>.Create elements // [] -let ofSeq elements = +let ofSeq elements ([] comparer: IComparer<'T>) = + // FIXME: should use comparer Map<_, _>.Create elements // [] diff --git a/src/fable-library/Native.fs b/src/fable-library/Native.fs new file mode 100644 index 0000000000..b3df5784c5 --- /dev/null +++ b/src/fable-library/Native.fs @@ -0,0 +1,118 @@ +module Native + +// Disables warn:1204 raised by use of LanguagePrimitives.ErrorStrings.* +#nowarn "1204" + +open System.Collections.Generic +open Fable.Core +open Fable.Core.JsInterop +open Fable.Import + +type Cons<'T> = + [] + abstract Allocate: len: int -> 'T[] + +module Helpers = + [] + let arrayFrom (xs: 'T seq): 'T[] = jsNative + + [] + let allocateArray (len: int): 'T[] = jsNative + + [] + let allocateArrayFrom (xs: 'T[]) (len: int): 'T[] = jsNative + + let allocateArrayFromCons (cons: Cons<'T>) (len: int): 'T[] = + if jsTypeof cons = "function" + then cons.Allocate(len) + else JS.Constructors.Array.Create(len) + + let inline isDynamicArrayImpl arr = + JS.Constructors.Array.isArray arr + + let inline isTypedArrayImpl arr = + JS.Constructors.ArrayBuffer.isView arr + + // let inline typedArraySetImpl (target: obj) (source: obj) (offset: int): unit = + // !!target?set(source, offset) + + [] + let inline concatImpl (array1: 'T[]) (arrays: 'T[] seq): 'T[] = + jsNative + + let inline fillImpl (array: 'T[]) (value: 'T) (start: int) (count: int): 'T[] = + !!array?fill(value, start, start + count) + + let inline foldImpl (folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T[]): 'State = + !!array?reduce(System.Func<'State, 'T, 'State>(folder), state) + + let inline foldIndexedImpl (folder: 'State -> 'T -> int -> 'State) (state: 'State) (array: 'T[]): 'State = + !!array?reduce(System.Func<'State, 'T, int, 'State>(folder), state) + + let inline foldBackImpl (folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T[]): 'State = + !!array?reduceRight(System.Func<'State, 'T, 'State>(folder), state) + + let inline foldBackIndexedImpl (folder: 'State -> 'T -> int -> 'State) (state: 'State) (array: 'T[]): 'State = + !!array?reduceRight(System.Func<'State, 'T, int, 'State>(folder), state) + + // Typed arrays not supported, only dynamic ones do + let inline pushImpl (array: 'T[]) (item: 'T): int = + !!array?push(item) + + // Typed arrays not supported, only dynamic ones do + let inline insertImpl (array: 'T[]) (index: int) (item: 'T): 'T[] = + !!array?splice(index, 0, item) + + // Typed arrays not supported, only dynamic ones do + let inline spliceImpl (array: 'T[]) (start: int) (deleteCount: int): 'T[] = + !!array?splice(start, deleteCount) + + let inline reverseImpl (array: 'T[]): 'T[] = + !!array?reverse() + + let inline copyImpl (array: 'T[]): 'T[] = + !!array?slice() + + let inline skipImpl (array: 'T[]) (count: int): 'T[] = + !!array?slice(count) + + let inline subArrayImpl (array: 'T[]) (start: int) (count: int): 'T[] = + !!array?slice(start, start + count) + + let inline indexOfImpl (array: 'T[]) (item: 'T) (start: int): int = + !!array?indexOf(item, start) + + let inline findImpl (predicate: 'T -> bool) (array: 'T[]): 'T option = + !!array?find(predicate) + + let inline findIndexImpl (predicate: 'T -> bool) (array: 'T[]): int = + !!array?findIndex(predicate) + + let inline collectImpl (mapping: 'T -> 'U[]) (array: 'T[]): 'U[] = + !!array?flatMap(mapping) + + let inline containsImpl (predicate: 'T -> bool) (array: 'T[]): bool = + !!array?filter(predicate) + + let inline existsImpl (predicate: 'T -> bool) (array: 'T[]): bool = + !!array?some(predicate) + + let inline forAllImpl (predicate: 'T -> bool) (array: 'T[]): bool = + !!array?every(predicate) + + let inline filterImpl (predicate: 'T -> bool) (array: 'T[]): 'T[] = + !!array?filter(predicate) + + let inline reduceImpl (reduction: 'T -> 'T -> 'T) (array: 'T[]): 'T = + !!array?reduce(reduction) + + let inline reduceBackImpl (reduction: 'T -> 'T -> 'T) (array: 'T[]): 'T = + !!array?reduceRight(reduction) + + // Inlining in combination with dynamic application may cause problems with uncurrying + // Using Emit keeps the argument signature + [] + let sortInPlaceWithImpl (comparer: 'T -> 'T -> int) (array: 'T[]): unit = jsNative //!!array?sort(comparer) + + [] + let copyToTypedArray (src: 'T[]) (srci: int) (trg: 'T[]) (trgi: int) (cnt: int): unit = jsNative diff --git a/tests/Python/Fable.Tests.fsproj b/tests/Python/Fable.Tests.fsproj new file mode 100644 index 0000000000..f33d46612e --- /dev/null +++ b/tests/Python/Fable.Tests.fsproj @@ -0,0 +1,39 @@ + + + net5.0 + false + false + true + preview + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Python/Main.fs b/tests/Python/Main.fs new file mode 100644 index 0000000000..ae5f6f6959 --- /dev/null +++ b/tests/Python/Main.fs @@ -0,0 +1,6 @@ +#if FABLE_COMPILER +module Program +() +#else +module Program = let [] main _ = 0 +#endif \ No newline at end of file diff --git a/tests/Python/TestAsync.fs b/tests/Python/TestAsync.fs new file mode 100644 index 0000000000..8c2ed34b54 --- /dev/null +++ b/tests/Python/TestAsync.fs @@ -0,0 +1,181 @@ +module Fable.Tests.Async + +open System +open Util.Testing + +type DisposableAction(f) = + interface IDisposable with + member __.Dispose() = f() + +let sleepAndAssign token res = + Async.StartImmediate(async { + do! Async.Sleep 200 + res := true + }, token) + +let successWork: Async = Async.FromContinuations(fun (onSuccess,_,_) -> onSuccess "success") +let errorWork: Async = Async.FromContinuations(fun (_,onError,_) -> onError (exn "error")) +let cancelWork: Async = Async.FromContinuations(fun (_,_,onCancel) -> + System.OperationCanceledException("cancelled") |> onCancel) + +[] +let ``test Simple async translates without exception`` () = + async { return () } + |> Async.StartImmediate + + +[] +let ``test Async while binding works correctly`` () = + let mutable result = 0 + async { + while result < 10 do + result <- result + 1 + } |> Async.StartImmediate + equal result 10 + +[] +let ``test Async for binding works correctly`` () = + let inputs = [|1; 2; 3|] + let result = ref 0 + async { + for inp in inputs do + result := !result + inp + } |> Async.StartImmediate + equal !result 6 + +[] +let ``test Async exceptions are handled correctly`` () = + let result = ref 0 + let f shouldThrow = + async { + try + if shouldThrow then failwith "boom!" + else result := 12 + with _ -> result := 10 + } |> Async.StartImmediate + !result + f true + f false |> equal 22 + +[] +let ``test Simple async is executed correctly`` () = + let result = ref false + let x = async { return 99 } + async { + let! x = x + let y = 99 + result := x = y + } + |> Async.StartImmediate + equal !result true + +[] +let ``test async use statements should dispose of resources when they go out of scope`` () = + let isDisposed = ref false + let step1ok = ref false + let step2ok = ref false + let resource = async { + return new DisposableAction(fun () -> isDisposed := true) + } + async { + use! r = resource + step1ok := not !isDisposed + } + //TODO: RunSynchronously would make more sense here but in JS I think this will be ok. + |> Async.StartImmediate + step2ok := !isDisposed + (!step1ok && !step2ok) |> equal true + +[] +let ``test Try ... with ... expressions inside async expressions work the same`` () = + let result = ref "" + let throw() : unit = + raise(exn "Boo!") + let append(x) = + result := !result + x + let innerAsync() = + async { + append "b" + try append "c" + throw() + append "1" + with _ -> append "d" + append "e" + } + async { + append "a" + try do! innerAsync() + with _ -> append "2" + append "f" + } |> Async.StartImmediate + equal !result "abcdef" + +// Disable this test for dotnet as it's failing too many times in Appveyor +#if FABLE_COMPILER + +[] +let ``test async cancellation works`` () = + async { + let res1, res2, res3 = ref false, ref false, ref false + let tcs1 = new System.Threading.CancellationTokenSource(50) + let tcs2 = new System.Threading.CancellationTokenSource() + let tcs3 = new System.Threading.CancellationTokenSource() + sleepAndAssign tcs1.Token res1 + sleepAndAssign tcs2.Token res2 + sleepAndAssign tcs3.Token res3 + tcs2.Cancel() + tcs3.CancelAfter(1000) + do! Async.Sleep 500 + equal false !res1 + equal false !res2 + equal true !res3 + } |> Async.StartImmediate + +[] +let ``test CancellationTokenSourceRegister works`` () = + async { + let mutable x = 0 + let res1 = ref false + let tcs1 = new System.Threading.CancellationTokenSource(50) + let foo = tcs1.Token.Register(fun () -> + x <- x + 1) + sleepAndAssign tcs1.Token res1 + do! Async.Sleep 500 + equal false !res1 + equal 1 x + } |> Async.StartImmediate +#endif + +[] +let ``test Async StartWithContinuations works`` () = + let res1, res2, res3 = ref "", ref "", ref "" + Async.StartWithContinuations(successWork, (fun x -> res1 := x), ignore, ignore) + Async.StartWithContinuations(errorWork, ignore, (fun x -> res2 := x.Message), ignore) + Async.StartWithContinuations(cancelWork, ignore, ignore, (fun x -> res3 := x.Message)) + equal "success" !res1 + equal "error" !res2 + equal "cancelled" !res3 + +[] +let ``test Async.Catch works`` () = + let assign res = function + | Choice1Of2 msg -> res := msg + | Choice2Of2 (ex: Exception) -> res := "ERROR: " + ex.Message + let res1 = ref "" + let res2 = ref "" + async { + let! x1 = successWork |> Async.Catch + assign res1 x1 + let! x2 = errorWork |> Async.Catch + assign res2 x2 + } |> Async.StartImmediate + equal "success" !res1 + equal "ERROR: error" !res2 + +[] +let ``test Async.Ignore works`` () = + let res = ref false + async { + do! successWork |> Async.Ignore + res := true + } |> Async.StartImmediate + equal true !res diff --git a/tests/Python/TestFn.fs b/tests/Python/TestFn.fs new file mode 100644 index 0000000000..d889257dd3 --- /dev/null +++ b/tests/Python/TestFn.fs @@ -0,0 +1,40 @@ +module Fable.Tests.Fn + +open Util.Testing + +let add(a, b, cont) = + cont(a + b) + +let square(x, cont) = + cont(x * x) + +let sqrt(x, cont) = + cont(sqrt(x)) + +let pythagoras(a, b, cont) = + square(a, (fun aa -> + square(b, (fun bb -> + add(aa, bb, (fun aabb -> + sqrt(aabb, (fun result -> + cont(result) + )) + )) + )) + )) + +[] +let ``test pythagoras works`` () = + let result = pythagoras(10.0, 2.1, id) + result + |> equal 10.218121158021175 + +[] +let ``test nonlocal works`` () = + let mutable value = 0 + + let fn () = + value <- 42 + + fn () + + value |> equal 42 diff --git a/tests/Python/TestList.fs b/tests/Python/TestList.fs new file mode 100644 index 0000000000..affc9b028f --- /dev/null +++ b/tests/Python/TestList.fs @@ -0,0 +1,38 @@ +module Fable.Tests.List + +open Util.Testing + + +[] +let ``test List.empty works`` () = + let xs = List.empty + List.length xs + |> equal 0 + +[] +let ``test List.length works`` () = + let xs = [1.; 2.; 3.; 4.] + List.length xs + |> equal 4 + +[] +let ``test List.map works`` () = + let xs = [1; 2; 3; 4] + xs + |> List.map string + |> equal ["1"; "2"; "3"; "4"] + + +[] +let ``test List.singleton works`` () = + let xs = List.singleton 42 + xs + |> equal [42] + + +[] +let ``test List.collect works`` () = + let xs = ["a"; "fable"; "bar" ] + xs + |> List.collect (fun a -> [a.Length]) + |> equal [1; 5; 3] diff --git a/tests/Python/TestLoops.fs b/tests/Python/TestLoops.fs new file mode 100644 index 0000000000..2e2d153e25 --- /dev/null +++ b/tests/Python/TestLoops.fs @@ -0,0 +1,35 @@ +module Fable.Tests.Loops + +open Util.Testing + + +[] +let ``test For-loop upto works`` () = + let mutable result = 0 + + for i = 0 to 10 do + result <- result + i + done + + result + |> equal 55 + +[] +let ``test For-loop upto minus one works`` () = + let mutable result = 0 + + for i = 0 to 10 - 1 do + result <- result + i + done + + result + |> equal 45 + +[] +let ``test For-loop downto works`` () = + let mutable result = 0 + for i = 10 downto 0 do + result <- result + i + + result + |> equal 55 diff --git a/tests/Python/TestMap.fs b/tests/Python/TestMap.fs new file mode 100644 index 0000000000..1c515163c5 --- /dev/null +++ b/tests/Python/TestMap.fs @@ -0,0 +1,37 @@ +module Fable.Tests.Maps + +open System.Collections.Generic +open Util.Testing + +type R = { i: int; s: string } +type R2 = { kv: KeyValuePair } + +[] +type R3 = + { Bar: string + Foo: int } + interface System.IComparable with + member this.CompareTo(x) = + match x with + | :? R3 as x -> compare this.Bar x.Bar + | _ -> -1 + override this.GetHashCode() = hash this.Bar + override this.Equals(x) = + match x with + | :? R3 as x -> this.Bar = x.Bar + | _ -> false + +type R4 = + { Bar: string + Baz: int } + +let ``test Map construction from lists works`` () = + let xs = Map [1,1; 2,2] + xs |> Seq.isEmpty + |> equal false + +let ``test Map.isEmpty works`` () = + let xs = Map [] + Map.isEmpty xs |> equal true + let ys = Map [1,1] + Map.isEmpty ys |> equal false diff --git a/tests/Python/TestMath.fs b/tests/Python/TestMath.fs new file mode 100644 index 0000000000..a5224ab35c --- /dev/null +++ b/tests/Python/TestMath.fs @@ -0,0 +1,10 @@ +module Fable.Tests.Math + +open Util.Testing + + +[] +let ``test power works`` () = + let x = 10.0 ** 2. + x + |> equal 100.0 diff --git a/tests/Python/TestOption.fs b/tests/Python/TestOption.fs new file mode 100644 index 0000000000..d69f823b38 --- /dev/null +++ b/tests/Python/TestOption.fs @@ -0,0 +1,73 @@ +module Fable.Tests.Option + +open Util.Testing + + +[] +let ``test defaultArg works`` () = + let f o = defaultArg o 5 + f (Some 2) |> equal 2 + f None |> equal 5 + +[] +let ``test Option.defaultValue works`` () = + let a = Some "MyValue" + let b = None + + a |> Option.defaultValue "" |> equal "MyValue" + b |> Option.defaultValue "default" |> equal "default" + +[] +let ``test Option.defaultValue works II`` () = + Some 5 |> Option.defaultValue 4 |> equal 5 + None |> Option.defaultValue "foo" |> equal "foo" + +[] +let ``test Option.orElse works`` () = + Some 5 |> Option.orElse (Some 4) |> equal (Some 5) + None |> Option.orElse (Some "foo") |> equal (Some "foo") + +[] +let ``test Option.defaultWith works`` () = + Some 5 |> Option.defaultWith (fun () -> 4) |> equal 5 + None |> Option.defaultWith (fun () -> "foo") |> equal "foo" + +[] +let ``test Option.orElseWith works`` () = + Some 5 |> Option.orElseWith (fun () -> Some 4) |> equal (Some 5) + None |> Option.orElseWith (fun () -> Some "foo") |> equal (Some "foo") + +[] +let ``test Option.isSome/isNone works`` () = + let o1 = None + let o2 = Some 5 + Option.isNone o1 |> equal true + Option.isSome o1 |> equal false + Option.isNone o2 |> equal false + Option.isSome o2 |> equal true + +[] +let ``test Option.IsSome/IsNone works II`` () = + let o1 = None + let o2 = Some 5 + o1.IsNone |> equal true + o1.IsSome |> equal false + o2.IsNone |> equal false + o2.IsSome |> equal true + +[] +let ``test Option.iter works`` () = + let mutable res = false + let getOnlyOnce = + let mutable value = Some "Hello" + fun () -> match value with Some x -> value <- None; Some x | None -> None + getOnlyOnce() |> Option.iter (fun s -> if s = "Hello" then res <- true) + equal true res + +[] +let ``test Option.map works`` () = + let getOnlyOnce = + let mutable value = Some "Alfonso" + fun () -> match value with Some x -> value <- None; Some x | None -> None + getOnlyOnce() |> Option.map ((+) "Hello ") |> equal (Some "Hello Alfonso") + getOnlyOnce() |> Option.map ((+) "Hello ") |> equal None diff --git a/tests/Python/TestPyInterop.fs b/tests/Python/TestPyInterop.fs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/Python/TestRecordType.fs b/tests/Python/TestRecordType.fs new file mode 100644 index 0000000000..94c0e5fd28 --- /dev/null +++ b/tests/Python/TestRecordType.fs @@ -0,0 +1,127 @@ +module Fable.Tests.Record + +open Util.Testing + +type RecursiveRecord = + { things : RecursiveRecord list } + +type Person = + { name: string; mutable luckyNumber: int } + member x.LuckyDay = x.luckyNumber % 30 + member x.SignDoc str = str + " by " + x.name + +type JSKiller = + { ``for`` : float; ``class`` : float } + +type JSKiller2 = + { ``s p a c e`` : float; ``s*y*m*b*o*l`` : float } + +type Child = + { a: string; b: int } + member x.Sum() = (int x.a) + x.b + +type Parent = + { children: Child[] } + member x.Sum() = x.children |> Seq.sumBy (fun c -> c.Sum()) + +type MutatingRecord = + { uniqueA: int; uniqueB: int } + +type Id = Id of string + +let inline replaceById< ^t when ^t : (member Id : Id)> (newItem : ^t) (ar: ^t[]) = + Array.map (fun (x: ^t) -> if (^t : (member Id : Id) newItem) = (^t : (member Id : Id) x) then newItem else x) ar + +let makeAnonRec() = + {| X = 5; Y = "Foo"; F = fun x y -> x + y |} + +type Time = + static member inline duration(value: {| from: int; until: int |}) = value.until - value.from + static member inline duration(value: {| from: int |}) = Time.duration {| value with until = 10 |} + +[] +let ``test Anonymous records work`` () = + let r = makeAnonRec() + sprintf "Tell me %s %i times" r.Y (r.F r.X 3) + |> equal "Tell me Foo 8 times" + let x = {| Foo = "baz"; Bar = 23 |} + let y = {| Foo = "baz" |} + x = {| y with Bar = 23 |} |> equal true + // x = {| y with Baz = 23 |} |> equal true // Doesn't compile + x = {| y with Bar = 14 |} |> equal false + +[] +let ``test SRTP works with anonymous records`` () = + let ar = [| {|Id=Id"foo"; Name="Sarah"|}; {|Id=Id"bar"; Name="James"|} |] + replaceById {|Id=Id"ja"; Name="Voll"|} ar |> Seq.head |> fun x -> equal "Sarah" x.Name + replaceById {|Id=Id"foo"; Name="Anna"|} ar |> Seq.head |> fun x -> equal "Anna" x.Name + +[] +let ``test Overloads with anonymous record arguments don't have same mangled name`` () = + Time.duration {| from = 1 |} |> equal 9 + Time.duration {| from = 1; until = 5 |} |> equal 4 + +[] // TODO: Need to handle nonlocal variables in Python +let ``test Anonymous record execution order`` () = + let mutable x = 2 + let record = + {| + C = (x <- x * 3; x) + B = (x <- x + 5; x) + A = (x <- x / 2; x) + |} + record.A |> equal 5 + record.B |> equal 11 + record.C |> equal 6 + +[] +let ``test Recursive record does not cause issues`` () = + let r = { things = [ { things = [] } ] } + equal r.things.Length 1 + +[] +let ``test Record property access can be generated`` () = + let x = { name = "Alfonso"; luckyNumber = 7 } + equal "Alfonso" x.name + equal 7 x.luckyNumber + x.luckyNumber <- 14 + equal 14 x.luckyNumber + +[] +let ``test Record methods can be generated`` () = + let x = { name = "Alfonso"; luckyNumber = 54 } + equal 24 x.LuckyDay + x.SignDoc "Hello World!" + |> equal "Hello World! by Alfonso" + +[] +let ``test RRecord expression constructors can be generated`` () = + let x = { name = "Alfonso"; luckyNumber = 7 } + let y = { x with luckyNumber = 14 } + equal "Alfonso" y.name + equal 14 y.luckyNumber + +[] +let ``test Records with key/reserved words are mapped correctly`` () = + let x = { ``for`` = 1.0; ``class`` = 2.0 } + equal 2. x.``class`` + +[] +let ``test Records with special characters are mapped correctly`` () = + let x = { ``s p a c e`` = 1.0; ``s*y*m*b*o*l`` = 2.0 } + equal 1. x.``s p a c e`` + equal 2. x.``s*y*m*b*o*l`` + +[] +let ``test Mutating records work`` () = + let x = { uniqueA = 10; uniqueB = 20 } + equal 10 x.uniqueA + equal 20 x.uniqueB + let uniqueB' = -x.uniqueB + let x' = { x with uniqueB = uniqueB' } + equal 10 x.uniqueA + equal 10 x'.uniqueA + equal -20 x'.uniqueB + let x'' = { x' with uniqueA = -10 } + equal -10 x''.uniqueA + equal -20 x''.uniqueB diff --git a/tests/Python/TestReflection.fs b/tests/Python/TestReflection.fs new file mode 100644 index 0000000000..8a759b62e5 --- /dev/null +++ b/tests/Python/TestReflection.fs @@ -0,0 +1,221 @@ +module Fable.Tests.Reflection + +open Util.Testing +open FSharp.Data.UnitSystems.SI.UnitSymbols + +#if !OPTIMIZE_FCS + +type MyDelegate = delegate of int * float -> string +type MyGenDelegate<'a,'b> = delegate of 'b * string -> 'a + +// I can declare a type with delegate fields (both C# and F# style). See #1698 +type WithDelegates = + { Del1: MyDelegate + Del2: MyGenDelegate + Del3: System.Func + Del4: System.Func + Del5: System.Action + Del6: System.Action + Del7: System.Action } + +type MyType = + | Union1 of string + +type MyType2 = + | Union2 of string + +type MyType3 = class end +type MyType4 = class end +type MyType5 = class end + +type GenericRecord<'A,'B> = { a: 'A; b: 'B } + +type MyEnum = + | Foo = 1y + | Bar = 5y + | Baz = 8y + +type MyInterface<'T> = + abstract Value: 'T + +type MyClass() = + class end + +type MyClass2() = + class end + +type MyClass3<'T>(v) = + interface MyInterface<'T> with + member _.Value = v + +type MyClass<'T1, 'T2>(v1: 'T1, v2: 'T2) = + inherit MyClass() + member _.Value1 = v1 + member _.Value2 = v2 + +[] +let ``test typedefof works`` () = + let tdef1 = typedefof + let tdef2 = typedefof + equal tdef1 tdef2 + +[] +let ``test IsGenericType works`` () = + typeof.IsGenericType |> equal true + typeof.IsGenericType |> equal false + let t1 = typeof + let t2 = typeof + let t3 = typeof + t1.IsGenericType |> equal true + t2.IsGenericType |> equal false + t3.IsGenericType |> equal false + +[] +let ``test GetGenericTypeDefinition works`` () = + let tdef1 = typedefof + let tdef2 = typeof.GetGenericTypeDefinition() + let t = typeof + let tdef3 = t.GetGenericTypeDefinition() + equal tdef1 tdef2 + equal tdef1 tdef3 + tdef1 = typeof |> equal false + +[] +let ``test Comparing generic types works`` () = + let t1 = typeof> + let t2 = typeof> + let t3 = typeof> + t1 = t2 |> equal true + t1 = t3 |> equal false + +let inline getName<'t> = function + | "namespace" -> typedefof<'t>.Namespace + | "name" -> typedefof<'t>.Name + | _ -> typedefof<'t>.FullName + +let inline getName2 (d:'t) = function + | "namespace" -> typeof<'t>.Namespace + | "name" -> typeof<'t>.Name + | _ -> typeof<'t>.FullName + +let getName3 (t:System.Type) = function + | "namespace" -> t.Namespace + | "name" -> t.Name + | _ -> t.FullName + +type Firm = { name: string } + +let normalize (x: string) = + #if FABLE_COMPILER + x + #else + x.Replace("+",".") + #endif +let inline fullname<'T> () = typeof<'T>.FullName |> normalize + +[] +let ``test Type Namespace`` () = + let x = typeof.Namespace + #if FABLE_COMPILER + equal "Fable.Tests.Reflection" x + #else + equal "Fable.Tests" x + #endif + +[] +let ``test Type FullName`` () = + let x = typeof.FullName + x |> normalize |> equal "Fable.Tests.Reflection.MyType" + +[] +let ``test Type Name`` () = + let x = typeof.Name + equal "MyType" x + + +[] +let ``test Get fullname of generic types with inline function`` () = + fullname() |> equal "Fable.Tests.Reflection.MyType3" + fullname() |> equal "Fable.Tests.Reflection.MyType4" + + +[] +let ``test Type name is accessible`` () = + let x = { name = "" } + getName "name" |> equal "Firm" + getName2 x "name" |> equal "Firm" + getName3 typedefof "name" |> equal "Firm" + // getName4 x "name" |> equal "Firm" + +[] +let ``test Type namespace is accessible`` () = + let test (x: string) = + x.StartsWith("Fable.Tests") |> equal true + let x = { name = "" } + getName "namespace" |> test + getName2 x "namespace" |> test + getName3 typedefof "namespace" |> test + // getName4 x "namespace" |> test + +[] +let ``test Type full name is accessible`` () = + let test (x: string) = + x.Replace("+", ".") |> equal "Fable.Tests.Reflection.Firm" + let x = { name = "" } + getName "fullname" |> test + getName2 x "fullname" |> test + getName3 typedefof "fullname" |> test + // getName4 x "fullname" |> test + +// FSharpType and FSharpValue reflection tests +open FSharp.Reflection + +exception MyException of String: string * Int: int + +let flip f b a = f a b + +type MyRecord = { + String: string + Int: int +} + +type MyUnion = + | StringCase of SomeString: string * string + | IntCase of SomeInt: int + +type RecordF = { F : int -> string } + +type AsyncRecord = { + asyncProp : Async +} + +type MyList<'T> = +| Nil +| Cons of 'T * MyList<'T> + +let inline create<'T when 'T: (new: unit->'T)> () = new 'T() + +type A() = member __.Value = 5 + +type B() = member __.Value = 10 + +type AnonRec1 = {| name: string; child: {| name: string |} |} +type AnonRec2 = {| numbers: int list |} + +type RecordGetValueType = { + Firstname : string + Age : int +} + +[] +let ``test Reflection: Array`` () = + let arType = typeof + let liType = typeof + equal true arType.IsArray + equal false liType.IsArray + let elType = arType.GetElementType() + typeof = elType |> equal true + typeof = elType |> equal false + liType.GetElementType() |> equal null + +#endif diff --git a/tests/Python/TestSeq.fs b/tests/Python/TestSeq.fs new file mode 100644 index 0000000000..3946547f9e --- /dev/null +++ b/tests/Python/TestSeq.fs @@ -0,0 +1,82 @@ +module Fable.Tests.Seqs + +open Util.Testing + +let sumFirstTwo (zs: seq) = + let second = Seq.skip 1 zs |> Seq.head + let first = Seq.head zs + printfn "sumFirstTwo: %A" (first, second) + first + second + +let rec sumFirstSeq (zs: seq) (n: int): float = + match n with + | 0 -> 0. + | 1 -> Seq.head zs + | _ -> (Seq.head zs) + sumFirstSeq (Seq.skip 1 zs) (n-1) + +[] +let ``test Seq.empty works`` () = + let xs = Seq.empty + Seq.length xs + |> equal 0 + +[] +let ``test Seq.length works`` () = + let xs = [1.; 2.; 3.; 4.] + Seq.length xs + |> equal 4 + +[] +let ``test Seq.map works`` () = + let xs = [1; 2; 3; 4] + xs + |> Seq.map string + |> List.ofSeq + |> equal ["1"; "2"; "3"; "4"] + + +[] +let ``test Seq.singleton works`` () = + let xs = Seq.singleton 42 + xs + |> List.ofSeq + |> equal [42] + +[] +let ``test Seq.collect works`` () = + let xs = ["a"; "fable"; "bar" ] + xs + |> Seq.ofList + |> Seq.collect (fun a -> [a.Length]) + |> List.ofSeq + |> equal [1; 5; 3] + +[] +let ``test Seq.collect works II"`` () = + let xs = [[1.]; [2.]; [3.]; [4.]] + let ys = xs |> Seq.collect id + sumFirstTwo ys + |> equal 3. + + let xs1 = [[1.; 2.]; [3.]; [4.; 5.; 6.;]; [7.]] + let ys1 = xs1 |> Seq.collect id + sumFirstSeq ys1 5 + |> equal 15. + +[] +let ``test Seq.collect works with Options`` () = + let xss = [[Some 1; Some 2]; [None; Some 3]] + Seq.collect id xss + |> Seq.sumBy (function + | Some n -> n + | None -> 0 + ) + |> equal 6 + + seq { + for xs in xss do + for x in xs do + x + } + |> Seq.length + |> equal 4 diff --git a/tests/Python/TestSet.fs b/tests/Python/TestSet.fs new file mode 100644 index 0000000000..6ed48c9bd4 --- /dev/null +++ b/tests/Python/TestSet.fs @@ -0,0 +1,41 @@ +module Fable.Tests.Set + +open Util.Testing + +[] +let ``test set function works`` () = + let xs = set [1] + xs |> Set.isEmpty + |> equal false + +[] +let ``test Set.isEmpty works`` () = + let xs = set [] + Set.isEmpty xs |> equal true + let ys = set [1] + Set.isEmpty ys |> equal false + +[] +let ``test Set.IsEmpty works`` () = + let xs = Set.empty + xs.IsEmpty |> equal true + let ys = set [1; 1] + ys.IsEmpty |> equal false + +[] +let ``test Set.Count works`` () = + let xs = Set.empty |> Set.add 1 + xs.Count + |> equal 1 + +[] +let ``test Seq.isEmpty function works on Set`` () = + let xs = set [1] + xs |> Seq.isEmpty + |> equal false + +[] +let ``test Set.add works`` () = + let xs = Set.empty |> Set.add 1 + Set.count xs + |> equal 1 diff --git a/tests/Python/TestString.fs b/tests/Python/TestString.fs new file mode 100644 index 0000000000..16c61b48f7 --- /dev/null +++ b/tests/Python/TestString.fs @@ -0,0 +1,270 @@ +module Fable.Tests.String + +open System +open Util.Testing + +let containsInOrder (substrings: string list) (str: string) = + let mutable lastIndex = -1 + substrings |> List.forall (fun s -> + let i = str.IndexOf(s) + let success = i >= 0 && i > lastIndex + lastIndex <- i + success) + +[] +let ``test sprintf works`` () = + // Immediately applied + sprintf "%.2f %g" 0.5468989 5. + |> equal "0.55 5" + // Curried + let printer = sprintf "Hi %s, good %s!" + let printer = printer "Alfonso" + printer "morning" |> equal "Hi Alfonso, good morning!" + printer "evening" |> equal "Hi Alfonso, good evening!" + +[] +let ``test sprintf works II`` () = + let printer2 = sprintf "Hi %s, good %s%s" "Maxime" + let printer2 = printer2 "afternoon" + printer2 "?" |> equal "Hi Maxime, good afternoon?" + +[] +let ``test sprintf with different decimal digits works`` () = + sprintf "Percent: %.0f%%" 5.0 |> equal "Percent: 5%" + sprintf "Percent: %.2f%%" 5. |> equal "Percent: 5.00%" + sprintf "Percent: %.1f%%" 5.24 |> equal "Percent: 5.2%" + sprintf "Percent: %.2f%%" 5.268 |> equal "Percent: 5.27%" + sprintf "Percent: %f%%" 5.67 |> equal "Percent: 5.670000%" + +[] +let ``sprintf displays sign correctly`` () = + sprintf "%i" 1 |> equal "1" + sprintf "%d" 1 |> equal "1" + sprintf "%d" 1L |> equal "1" + sprintf "%.2f" 1. |> equal "1.00" + sprintf "%i" -1 |> equal "-1" + sprintf "%d" -1 |> equal "-1" + sprintf "%d" -1L |> equal "-1" + sprintf "%.2f" -1. |> equal "-1.00" + +[] +let ``test Print.sprintf works`` () = + let res = Printf.sprintf "%s" "abc" + equal "res: abc" ("res: " + res) + +[] +let ``test sprintf without arguments works`` () = + sprintf "hello" |> equal "hello" + +[] +let ``test input of print format can be retrieved`` () = + let pathScan (pf:PrintfFormat<_,_,_,_,'t>) = + let formatStr = pf.Value + formatStr + + equal "/hello/%s" (pathScan "/hello/%s") + +[] +let ``test interpolate works`` () = + let name = "Phillip" + let age = 29 + $"Name: {name}, Age: %i{age}" + |> equal "Name: Phillip, Age: 29" + +#if FABLE_COMPILER +[] +let ``test string interpolation works with inline expressions`` () = + $"I think {3.0 + 0.14} is close to %.8f{3.14159265}!" + |> equal "I think 3.14 is close to 3.14159265!" +#endif + +[] +let ``test string interpolation works with anonymous records`` () = + let person = + {| Name = "John" + Surname = "Doe" + Age = 32 + Country = "The United Kingdom" |} + $"Hi! My name is %s{person.Name} %s{person.Surname.ToUpper()}. I'm %i{person.Age} years old and I'm from %s{person.Country}!" + |> equal "Hi! My name is John DOE. I'm 32 years old and I'm from The United Kingdom!" + +[] +let ``test interpolated string with double % should be unescaped`` () = + $"{100}%%" |> equal "100%" + +[] +let ``test sprintf \"%A\" with lists works`` () = + let xs = ["Hi"; "Hello"; "Hola"] + (sprintf "%A" xs).Replace("\"", "") |> equal "[Hi; Hello; Hola]" + +[] +let ``test sprintf \"%A\" with nested lists works`` () = + let xs = [["Hi"]; ["Hello"]; ["Hola"]] + (sprintf "%A" xs).Replace("\"", "") |> equal "[[Hi]; [Hello]; [Hola]]" + +[] +let ``test sprintf \"%A\" with sequences works`` () = + let xs = seq { "Hi"; "Hello"; "Hola" } + sprintf "%A" xs |> containsInOrder ["Hi"; "Hello"; "Hola"] |> equal true + +[] +let ``test Storing result of Seq.tail and printing the result several times works. Related to #1996`` () = + let tweets = seq { "Hi"; "Hello"; "Hola" } + let tweetsTailR: seq = tweets |> Seq.tail + + let a = sprintf "%A" (tweetsTailR) + let b = sprintf "%A" (tweetsTailR) + + containsInOrder ["Hello"; "Hola"] a |> equal true + containsInOrder ["Hello"; "Hola"] b |> equal true + +// [] FIXME: we should get this working as well. +// let ``test sprintf \"%X\" works`` () = +// //These should all be the Native JS Versions (except int64 / uint64) +// //See #1530 for more information. + +// sprintf "255: %X" 255 |> equal "255: FF" +// sprintf "255: %x" 255 |> equal "255: ff" +// sprintf "-255: %X" -255 |> equal "-255: FFFFFF01" +// sprintf "4095L: %X" 4095L |> equal "4095L: FFF" +// sprintf "-4095L: %X" -4095L |> equal "-4095L: FFFFFFFFFFFFF001" +// sprintf "1 <<< 31: %x" (1 <<< 31) |> equal "1 <<< 31: 80000000" +// sprintf "1u <<< 31: %x" (1u <<< 31) |> equal "1u <<< 31: 80000000" +// sprintf "2147483649L: %x" 2147483649L |> equal "2147483649L: 80000001" +// sprintf "2147483650uL: %x" 2147483650uL |> equal "2147483650uL: 80000002" +// sprintf "1L <<< 63: %x" (1L <<< 63) |> equal "1L <<< 63: 8000000000000000" +// sprintf "1uL <<< 63: %x" (1uL <<< 63) |> equal "1uL <<< 63: 8000000000000000" + +[] +let ``test sprintf integers with sign and padding works`` () = + sprintf "%+04i" 1 |> equal "+001" + sprintf "%+04i" -1 |> equal "-001" + sprintf "%5d" -5 |> equal " -5" + sprintf "%5d" -5L |> equal " -5" + sprintf "%- 4i" 5 |> equal " 5 " + +// [] +// let ``test parameterized padding works`` () = +// sprintf "[%*s][%*s]" 6 "Hello" 5 "Foo" +// |> equal "[ Hello][ Foo]" + +[] +let ``test String.Format combining padding and zeroes pattern works`` () = + String.Format("{0:++0.00++}", -5000.5657) |> equal "-++5000.57++" + String.Format("{0:000.00}foo", 5) |> equal "005.00foo" + String.Format("{0,-8:000.00}foo", 12.456) |> equal "012.46 foo" + +[] +let ``test StringBuilder works`` () = + let sb = System.Text.StringBuilder() + sb.Append "Hello" |> ignore + sb.AppendLine () |> ignore + sb.AppendLine "World!" |> ignore + let expected = System.Text.StringBuilder() + .AppendFormat("Hello{0}World!{0}", Environment.NewLine) + .ToString() + sb.ToString() |> equal expected + +[] +let ``test StringBuilder.Lengh works`` () = + let sb = System.Text.StringBuilder() + sb.Append("Hello") |> ignore + // We don't test the AppendLine for Length because depending on the OS + // the result is different. Unix \n VS Windows \r\n + // sb.AppendLine() |> ignore + equal 5 sb.Length + +[] +let ``test StringBuilder.ToString works with index and length`` () = + let sb = System.Text.StringBuilder() + sb.Append("Hello") |> ignore + sb.AppendLine() |> ignore + equal "ll" (sb.ToString(2, 2)) + +[] +let ``test StringBuilder.Clear works`` () = + let builder = new System.Text.StringBuilder() + builder.Append("1111") |> ignore + builder.Clear() |> ignore + equal "" (builder.ToString()) + +[] +let ``test StringBuilder.Append works with various overloads`` () = + let builder = Text.StringBuilder() + .Append(Text.StringBuilder "aaa") + .Append("bcd".ToCharArray()) + .Append('/') + .Append(true) + .Append(5.2) + .Append(34) + equal "aaabcd/true5.234" (builder.ToString().ToLower()) + +[] +let ``test Conversion char to int works`` () = + equal 97 (int 'a') + equal 'a' (char 97) + +[] +let ``test Conversion string to char works`` () = + equal 'a' (char "a") + equal "a" (string 'a') + +[] +let ``test Conversion string to negative int8 works`` () = + equal -5y (int8 "-5") + equal "-5" (string -5y) + +[] +let ``test Conversion string to negative int16 works`` () = + equal -5s (int16 "-5") + equal "-5" (string -5s) + +[] +let ``test Conversion string to negative int32 works`` () = + equal -5 (int32 "-5") + equal "-5" (string -5) + +[] +let ``test Conversion string to negative int64 works`` () = + equal -5L (int64 "-5") + equal "-5" (string -5L) + +[] +let ``test Conversion string to int8 works`` () = + equal 5y (int8 "5") + equal "5" (string 5y) + +[] +let ``test Conversion string to int16 works`` () = + equal 5s (int16 "5") + equal "5" (string 5s) + +[] +let ``test Conversion string to int32 works`` () = + equal 5 (int32 "5") + equal "5" (string 5) + +[] +let ``test Conversion string to int64 works`` () = + equal 5L (int64 "5") + equal "5" (string 5L) + +[] +let ``test Conversion string to uint8 works`` () = + equal 5uy (uint8 "5") + equal "5" (string 5uy) + +[] +let ``test Conversion string to uint16 works`` () = + equal 5us (uint16 "5") + equal "5" (string 5us) + +[] +let ``test Conversion string to uint32 works`` () = + equal 5u (uint32 "5") + equal "5" (string 5u) + +[] +let ``test Conversion string to uint64 works`` () = + equal 5uL (uint64 "5") + equal "5" (string 5uL) diff --git a/tests/Python/TestSudoku.fs b/tests/Python/TestSudoku.fs new file mode 100644 index 0000000000..81322933d1 --- /dev/null +++ b/tests/Python/TestSudoku.fs @@ -0,0 +1,99 @@ + +/// This module tests a couple of collection functions in a more complex setting. +module Fable.Tests.Sudoku + +open System.Collections.Generic +open Util.Testing + +type Box = int +type Sudoku = Box array array + +let rows = id +let cols (sudoku:Sudoku) = + sudoku + |> Array.mapi (fun a row -> row |> Array.mapi (fun b cell -> sudoku.[b].[a])) + +let getBoxIndex count row col = + let n = row/count + let m = col/count + n * count + m + +let boxes (sudoku:Sudoku) = + let d = sudoku |> Array.length |> float |> System.Math.Sqrt |> int + let list = new List<_>() + for a in 0..(d*d) - 1 do list.Add(new List<_>()) + + for a in 0..(Array.length sudoku - 1) do + for b in 0..(Array.length sudoku - 1) do + list.[getBoxIndex d a b].Add(sudoku.[a].[b]) + + list + |> Seq.map Seq.toArray + +let toSudoku x : Sudoku = + x + |> Seq.map Seq.toArray + |> Seq.toArray + +let allUnique numbers = + let set = new HashSet<_>() + numbers + |> Seq.filter ((<>) 0) + |> Seq.forall set.Add + +let solvable sudoku = + rows sudoku + |> Seq.append (cols sudoku) + |> Seq.append (boxes sudoku) + |> Seq.forall allUnique + +let replaceAtPos (x:Sudoku) row col newValue :Sudoku = + [| for a in 0..(Array.length x - 1) -> + [| for b in 0..(Array.length x - 1) -> + if a = row && b = col then newValue else x.[a].[b] |] |] + +let rec substitute row col (x:Sudoku) = + let a,b = if col >= Array.length x then row+1,0 else row,col + if a >= Array.length x then seq { yield x } else + if x.[a].[b] = 0 then + [1..Array.length x] + |> Seq.map (replaceAtPos x a b) + |> Seq.filter solvable + |> Seq.map (substitute a (b+1)) + |> Seq.concat + else substitute a (b+1) x + +let getFirstSolution = substitute 0 0 >> Seq.head + +[] +let testsSudoku () = + let solution = + [[0; 0; 8; 3; 0; 0; 6; 0; 0] + [0; 0; 4; 0; 0; 0; 0; 1; 0] + [6; 7; 0; 0; 8; 0; 0; 0; 0] + + [0; 1; 6; 4; 3; 0; 0; 0; 0] + [0; 0; 0; 7; 9; 0; 0; 2; 0] + [0; 9; 0; 0; 0; 0; 4; 0; 1] + + [0; 0; 0; 9; 1; 0; 0; 0; 5] + [0; 0; 3; 0; 5; 0; 0; 0; 2] + [0; 5; 0; 0; 0; 0; 0; 7; 4]] + |> toSudoku + |> getFirstSolution + + let expectedSolution = + [[1; 2; 8; 3; 4; 5; 6; 9; 7] + [5; 3; 4; 6; 7; 9; 2; 1; 8] + [6; 7; 9; 1; 8; 2; 5; 4; 3] + + [2; 1; 6; 4; 3; 8; 7; 5; 9] + [4; 8; 5; 7; 9; 1; 3; 2; 6] + [3; 9; 7; 5; 2; 6; 4; 8; 1] + + [7; 6; 2; 9; 1; 4; 8; 3; 5] + [9; 4; 3; 8; 5; 7; 1; 6; 2] + [8; 5; 1; 2; 6; 3; 9; 7; 4]] + |> toSudoku + + solution = expectedSolution |> equal true diff --git a/tests/Python/TestUnionType.fs b/tests/Python/TestUnionType.fs new file mode 100644 index 0000000000..8b90d32298 --- /dev/null +++ b/tests/Python/TestUnionType.fs @@ -0,0 +1,178 @@ +module Fable.Tests.UnionTypes + +open Util.Testing + +type Gender = Male | Female + +type Either<'TL,'TR> = + | Left of 'TL + | Right of 'TR + member x.AsString() = + match x with + | Left y -> y.ToString() + | Right y -> y.ToString() + +type MyUnion = + | Case0 + | Case1 of string + | Case2 of string * string + | Case3 of string * string * string + + +type MyUnion2 = + | Tag of string + | NewTag of string + +let (|Functional|NonFunctional|) (s: string) = + match s with + | "fsharp" | "haskell" | "ocaml" -> Functional + | _ -> NonFunctional + +let (|Small|Medium|Large|) i = + if i < 3 then Small 5 + elif i >= 3 && i < 6 then Medium "foo" + else Large + +let (|FSharp|_|) (document : string) = + if document = "fsharp" then Some FSharp else None + +let (|A|) n = n + +// type JsonTypeInner = { +// Prop1: string +// Prop2: int +// } + +// type JsonTestUnion = +// | IntType of int +// | StringType of string +// | TupleType of string * int +// | ObjectType of JsonTypeInner + +// type Tree = +// | Leaf of int +// | Branch of Tree[] +// member this.Sum() = +// match this with +// | Leaf i -> i +// | Branch trees -> trees |> Seq.map (fun x -> x.Sum()) |> Seq.sum + +type MyExUnion = MyExUnionCase of exn + +// type Wrapper(s: string) = +// member x.Value = s |> Seq.rev |> Seq.map string |> String.concat "" + +// [] +// type MyUnion = +// | Case1 +// | Case2 +// | Case3 + +// type R = { +// Name: string +// Case: MyUnion +// } + +#if FABLE_COMPILER +open Fable.Core + +[] +#endif +type DU = Int of int | Str of string + +[] +let ``test Union cases matches with no arguments can be generated`` () = + let x = Male + match x with + | Female -> true + | Male -> false + |> equal false + +[] +let ``test Union cases matches with one argument can be generated`` () = + let x = Left "abc" + match x with + | Left data -> data + | Right _ -> failwith "unexpected" + |> equal "abc" + +[] +let ``test Union methods can be generated`` () = + let x = Left 5 + x.AsString() + |> equal "5" + +[] +let ``test Nested pattern matching works`` () = + let x = Right(Left 5) + match x with + | Left _ -> failwith "unexpected" + | Right x -> + match x with + | Left x -> x + | Right _ -> failwith "unexpected" + |> equal 5 + +[] +let ``test Union cases matches with many arguments can be generated`` () = + let x = Case3("a", "b", "c") + match x with + | Case3(a, b, c) -> a + b + c + | _ -> failwith "unexpected" + |> equal "abc" + +[] +let ``test Pattern matching with common targets works`` () = + let x = MyUnion.Case2("a", "b") + match x with + | MyUnion.Case0 -> failwith "unexpected" + | MyUnion.Case1 _ + | MyUnion.Case2 _ -> "a" + | MyUnion.Case3(a, b, c) -> a + b + c + |> equal "a" + +[] +let ``test Union cases called Tag still work (bug due to Tag field)`` () = + let x = Tag "abc" + match x with + | Tag x -> x + | _ -> failwith "unexpected" + |> equal "abc" + +[] +let ``test Comprehensive active patterns work`` () = + let isFunctional = function + | Functional -> true + | NonFunctional -> false + isFunctional "fsharp" |> equal true + isFunctional "csharp" |> equal false + isFunctional "haskell" |> equal true + +[] +let ``test Comprehensive active patterns can return values`` () = + let measure = function + | Small i -> string i + | Medium s -> s + | Large -> "bar" + measure 0 |> equal "5" + measure 10 |> equal "bar" + measure 5 |> equal "foo" + +[] +let ``test Partial active patterns which don't return values work`` () = // See #478 + let isFunctional = function + | FSharp -> "yes" + | "scala" -> "fifty-fifty" + | _ -> "dunno" + isFunctional "scala" |> equal "fifty-fifty" + isFunctional "smalltalk" |> equal "dunno" + isFunctional "fsharp" |> equal "yes" + +[] +let ``test Active patterns can be combined with union case matching`` () = // See #306 + let test = function + | Some(A n, Some(A m)) -> n + m + | _ -> 0 + Some(5, Some 2) |> test |> equal 7 + Some(5, None) |> test |> equal 0 + None |> test |> equal 0 diff --git a/tests/Python/Util.fs b/tests/Python/Util.fs new file mode 100644 index 0000000000..2f182f5cd5 --- /dev/null +++ b/tests/Python/Util.fs @@ -0,0 +1,32 @@ +module Fable.Tests.Util + +open System + +module Testing = +#if FABLE_COMPILER + open Fable.Core + open Fable.Core.PyInterop + + type Assert = + [] + static member AreEqual(actual: 'T, expected: 'T, ?msg: string): unit = pyNative + [] + static member NotEqual(actual: 'T, expected: 'T, ?msg: string): unit = pyNative + + let equal expected actual: unit = Assert.AreEqual(actual, expected) + let notEqual expected actual: unit = Assert.NotEqual(actual, expected) + + type Fact() = inherit System.Attribute() +#else + open Xunit + type FactAttribute = Xunit.FactAttribute + + let equal<'T> (expected: 'T) (actual: 'T): unit = Assert.Equal(expected, actual) + let notEqual<'T> (expected: 'T) (actual: 'T) : unit = Assert.NotEqual(expected, actual) +#endif + + let rec sumFirstSeq (zs: seq) (n: int): float = + match n with + | 0 -> 0. + | 1 -> Seq.head zs + | _ -> (Seq.head zs) + sumFirstSeq (Seq.skip 1 zs) (n-1)