diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a89c531..5e28e1f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,13 +2,13 @@ name: CI on: push: - branches: [main] + branches: [main,v2] pull_request: - branches: [main] + branches: [main,v2] workflow_dispatch: jobs: - build: + build-jsoo: strategy: fail-fast: false matrix: @@ -56,12 +56,49 @@ jobs: run: opam install . --deps-only - name: Build and test the project - run: bash fake test + run: bash fake TestJsoo + + build-res: + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + dotnet: + - 6.0.x + node-version: + - 20.x + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Use .NET ${{ matrix.dotnet }} + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ matrix.dotnet }} + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: yarn + + - name: Install .NET Dependencies + run: | + dotnet restore + dotnet tool restore + + - name: Run FAKE + run: bash fake TestRes auto-merge: name: Auto-Merge PRs by Dependabot needs: - - build + - build-jsoo + - build-res runs-on: ubuntu-latest permissions: pull-requests: write diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 85916da7..861a2f6c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -57,7 +57,7 @@ jobs: env: REPO: self BRANCH: jsoo-stdlib - FOLDER: dist_jsoo + FOLDER: dist/jsoo TAG: jsoo-stdlib-${{ github.event.release.tag_name }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MESSAGE: "Build ({sha}) {msg}" diff --git a/.gitignore b/.gitignore index 4de5b3e7..b216a232 100644 --- a/.gitignore +++ b/.gitignore @@ -307,5 +307,5 @@ test/jsoo/src/*.mli test/jsoo/src/stub.js # Dist artifacts -dist -dist_jsoo/src/ts2ocaml_*.ml* +dist/js/ +dist/jsoo/src/ts2ocaml_*.ml* diff --git a/README.md b/README.md index bc484ac9..77c15467 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ An in-browser version may be available in future. For users: - [Common options](docs/common_options.md) among all the targets - [ts2ocaml for js_of_ocaml](docs/js_of_ocaml.md) -- ts2ocaml for ReScript [(ongoing)](https://github.com/ocsigen/ts2ocaml/pull/32) +- [ts2ocaml for ReScript](docs/rescript.md) For developers and contributors: - [Overview for developers](docs/development.md) diff --git a/build/build.fs b/build/build.fs index 3df90a61..748454b6 100644 --- a/build/build.fs +++ b/build/build.fs @@ -27,8 +27,9 @@ let run cmd dir args = failwithf "Error while running '%s' with args: %s " cmd args let platformTool tool = - ProcessUtils.tryFindFileOnPath tool - |> function Some t -> t | _ -> failwithf "%s not found" tool + lazy + ProcessUtils.tryFindFileOnPath tool + |> function Some t -> t | _ -> failwithf "%s not found" tool let dotnetExec cmd args = let result = DotNet.exec id cmd args @@ -36,8 +37,8 @@ let dotnetExec cmd args = failwithf "Error while running 'dotnet %s %s'" cmd args let opamTool = platformTool "opam" -let opam args = run opamTool "./" args -let dune args = run opamTool "./" (sprintf "exec -- dune %s" args) +let opam args = run opamTool.Value "./" args +let dune args = run opamTool.Value "./" (sprintf "exec -- dune %s" args) // Build targets @@ -45,7 +46,7 @@ let setup () = Target.create "Clean" <| fun _ -> !! "src/bin" ++ "src/obj" - ++ distDir + ++ (distDir "js") // clean ts2ocaml.js ++ "src/.fable" |> Seq.iter Shell.cleanDir @@ -74,24 +75,27 @@ let setup () = Target.create "Watch" <| fun _ -> dotnetExec "fable" $"watch {srcDir} --sourceMaps --define DEBUG --run webpack -w --mode=development" - Target.create "TestComplete" ignore + Target.create "Test" ignore - "Clean" ?=> "Build" + Target.create "Publish" ignore - "Clean" - ?=> "YarnInstall" + "YarnInstall" ==> "Restore" ==> "Prepare" - ?=> "Build" "Prepare" - ?=> "BuildForTest" - ?=> "TestComplete" - ?=> "BuildForPublish" + ==> "BuildForTest" ==> "Build" "Prepare" - ?=> "Watch" + ==> "BuildForPublish" + + "Prepare" + ==> "Watch" + + "Clean" + ?=> "BuildForTest" ?=> "Build" ?=> "Test" + ?=> "BuildForPublish" ?=> "Publish" // Test targets @@ -145,6 +149,59 @@ module Test = printfn "* copied to %s" file inDirectory testDir <| fun () -> dune "build" + module Res = + let testDir = testDir "res" + let outputDir = outputDir "test_res" + let srcDir = testDir "src" + let srcGeneratedDir = srcDir "generated" + + let clean () = + !! $"{outputDir}/*" + ++ $"{srcGeneratedDir}/*.res" + ++ $"{srcGeneratedDir}/*.resi" + ++ $"{srcGeneratedDir}/*.bs.js" + |> Seq.iter Shell.rm + + let generateBindings () = + Directory.create outputDir + + let ts2res args files = + Yarn.exec (sprintf "ts2ocaml res %s" (String.concat " " (Seq.append args files))) id + + ts2res ["--create-stdlib"; $"-o {outputDir}"] [] + + let packages = [ + // "full" package involving a lot of inheritance + "full", !! "node_modules/typescript/lib/typescript.d.ts", ["--experimental-tagged-union"]; + + // "full" packages involving a lot of dependencies (which includes some "safe" packages) + "safe", !! "node_modules/@types/scheduler/tracing.d.ts", []; + "full", !! "node_modules/csstype/index.d.ts", []; + "safe", !! "node_modules/@types/prop-types/index.d.ts", []; + "full", !! "node_modules/@types/react/index.d.ts" ++ "node_modules/@types/react/global.d.ts", ["--readable-names"]; + "full", !! "node_modules/@types/react-modal/index.d.ts", ["--readable-names"]; + + // "safe" package which depends on another "safe" package + "safe", !! "node_modules/@types/yargs-parser/index.d.ts", []; + "safe", !! "node_modules/@types/yargs/index.d.ts", []; + + "minimal", !! "node_modules/@types/vscode/index.d.ts", ["--readable-names"]; + ] + + for preset, package, additionalOptions in packages do + ts2res + (["--verbose"; "--nowarn"; "--follow-relative-references"; + $"--preset {preset}"; $"-o {outputDir}"] @ additionalOptions) + package + + let build () = + Shell.mkdir srcGeneratedDir + for file in outputDir |> Shell.copyRecursiveTo true srcGeneratedDir do + printfn "* copied to %s" file + inDirectory testDir <| fun () -> + Yarn.install id + Yarn.exec "rescript" id + let setup () = Target.create "TestJsooClean" <| fun _ -> Jsoo.clean () Target.create "TestJsooGenerateBindings" <| fun _ -> Jsoo.generateBindings () @@ -156,13 +213,18 @@ module Test = ==> "TestJsooGenerateBindings" ==> "TestJsooBuild" ==> "TestJsoo" + ==> "Test" - Target.create "Test" ignore - Target.create "TestOnly" ignore + Target.create "TestResClean" <| fun _ -> Res.clean () + Target.create "TestResGenerateBindings" <| fun _ -> Res.generateBindings () + Target.create "TestResBuild" <| fun _ -> Res.build () + Target.create "TestRes" ignore - "TestJsoo" - ==> "TestOnly" - ==> "TestComplete" + "BuildForTest" + ==> "TestResClean" + ==> "TestResGenerateBindings" + ==> "TestResBuild" + ==> "TestRes" ==> "Test" // Publish targets @@ -177,7 +239,7 @@ module Publish = Yarn.exec $"version --new-version {newVersion} --no-git-tag-version" id module Jsoo = - let targetDir = "./dist_jsoo" + let targetDir = distDir "jsoo" let duneProject = targetDir "dune-project" let copyArtifacts () = @@ -200,10 +262,10 @@ module Publish = if result.Success then let oldVersion = result.Groups.[1].Value if oldVersion <> newVersion then - printfn $"* updating version in dist_jsoo/dune-project from '{oldVersion}' to '{newVersion}'." + printfn $"* updating version in dist/jsoo/dune-project from '{oldVersion}' to '{newVersion}'." content |> String.replace result.Value $"(version {newVersion})" else - printfn $"* version in dist_jsoo/dune-project not updated ('{newVersion}')." + printfn $"* version in dist/jsoo/dune-project not updated ('{newVersion}')." content else content ) @@ -212,9 +274,6 @@ module Publish = inDirectory targetDir <| fun () -> dune "build" let setup () = - Target.create "Publish" <| fun _ -> () - Target.create "PublishOnly" <| fun _ -> () - Target.create "PublishNpm" <| fun _ -> Npm.updateVersion () @@ -226,13 +285,8 @@ module Publish = "BuildForPublish" ==> "PublishNpm" ==> "PublishJsoo" - ==> "PublishOnly" ==> "Publish" - "TestJsoo" ==> "PublishJsoo" - - "Build" ?=> "Test" ?=> "Publish" - // Utility targets module Utility = diff --git a/dist_jsoo/.gitignore b/dist/jsoo/.gitignore similarity index 100% rename from dist_jsoo/.gitignore rename to dist/jsoo/.gitignore diff --git a/dist_jsoo/dune-project b/dist/jsoo/dune-project similarity index 100% rename from dist_jsoo/dune-project rename to dist/jsoo/dune-project diff --git a/dist_jsoo/src/dune b/dist/jsoo/src/dune similarity index 100% rename from dist_jsoo/src/dune rename to dist/jsoo/src/dune diff --git a/dist_jsoo/src/ts2ocaml.ml b/dist/jsoo/src/ts2ocaml.ml similarity index 100% rename from dist_jsoo/src/ts2ocaml.ml rename to dist/jsoo/src/ts2ocaml.ml diff --git a/dist_jsoo/ts2ocaml-jsoo-stdlib.opam b/dist/jsoo/ts2ocaml-jsoo-stdlib.opam similarity index 100% rename from dist_jsoo/ts2ocaml-jsoo-stdlib.opam rename to dist/jsoo/ts2ocaml-jsoo-stdlib.opam diff --git a/dist/res/.gitignore b/dist/res/.gitignore new file mode 100644 index 00000000..3662f0f6 --- /dev/null +++ b/dist/res/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/node_modules/ +/lib/ +.bsb.lock +.merlin + +*.bs.js +Demo.res \ No newline at end of file diff --git a/dist/res/package.json b/dist/res/package.json new file mode 100644 index 00000000..99faf0a8 --- /dev/null +++ b/dist/res/package.json @@ -0,0 +1,20 @@ +{ + "name": "ts2ocaml-rescript-stdlib", + "version": "0.0.0", + "scripts": { + "build": "rescript", + "clean": "rescript clean -with-deps", + "start": "rescript build -w" + }, + "keywords": [ + "rescript" + ], + "author": "", + "license": "Apache-2.0", + "devDependencies": { + "rescript": "11.0.1" + }, + "peerDependencies": { + "rescript": "^11.0.1" + } +} diff --git a/dist/res/rescript.json b/dist/res/rescript.json new file mode 100644 index 00000000..50c249ca --- /dev/null +++ b/dist/res/rescript.json @@ -0,0 +1,18 @@ +{ + "name": "ts2ocaml-rescript-stdlib", + "version": "0.0.0", + "sources": { + "dir" : "src", + "subdirs" : true + }, + "package-specs": { + "module": "commonjs", + "in-source": true + }, + "suffix": ".bs.js", + "bs-dependencies": [ + ], + "warnings": { + "error" : "+101" + } +} diff --git a/dist/res/src/ts2ocaml.res b/dist/res/src/ts2ocaml.res new file mode 100644 index 00000000..ce3c7ae6 --- /dev/null +++ b/dist/res/src/ts2ocaml.res @@ -0,0 +1,225 @@ +@@warning("-27") + +@unboxed type never = { absurd : 'a. 'a } +module Never = { + type t = never + external absurd : t => 'a = "%identity" +} + +@unboxed type rec any = Any('a): any +module Any = { + type t = any + external upcast : 'a => t = "%identity" + external unsafeCast : t => 'a = "%identity" +} + +@unboxed type rec unknown = Unknown('a): unknown +module Unknown = { + type t = unknown + external upcast : 'a => t = "%identity" + external unsafeCast : t => 'a = "%identity" +} + +type true_ = bool +type false_ = bool +type symbol = Js.Types.symbol +type intrinsic = private string +type untypedObject = any +type untypedFunction = any + +module Union = { + type container<+'cases> + + external return1 : 't1 => container<[> #U1('t1)]> = "%identity" + external return2 : 't2 => container<[> #U2('t2)]> = "%identity" + external return3 : 't3 => container<[> #U3('t3)]> = "%identity" + external return4 : 't4 => container<[> #U4('t4)]> = "%identity" + external return5 : 't5 => container<[> #U5('t5)]> = "%identity" + external return6 : 't6 => container<[> #U6('t6)]> = "%identity" + external return7 : 't7 => container<[> #U7('t7)]> = "%identity" + external return8 : 't8 => container<[> #U8('t8)]> = "%identity" + + external getUnsafe1 : container<[> #U1('t1)]> => 't1 = "%identity" + external getUnsafe2 : container<[> #U2('t2)]> => 't2 = "%identity" + external getUnsafe3 : container<[> #U3('t3)]> => 't3 = "%identity" + external getUnsafe4 : container<[> #U4('t4)]> => 't4 = "%identity" + external getUnsafe5 : container<[> #U5('t5)]> => 't5 = "%identity" + external getUnsafe6 : container<[> #U6('t6)]> => 't6 = "%identity" + external getUnsafe7 : container<[> #U7('t7)]> => 't7 = "%identity" + external getUnsafe8 : container<[> #U8('t8)]> => 't8 = "%identity" + + type t2<'t1, 't2> = container<[ #U1('t1) | #U2('t2) ]> + type t3<'t1, 't2, 't3> = container<[ #U1('t1) | #U2('t2) | #U3('t3) ]> + type t4<'t1, 't2, 't3, 't4> = container<[ #U1('t1) | #U2('t2) | #U3('t3) | #U4('t4) ]> + type t5<'t1, 't2, 't3, 't4, 't5> = container<[ #U1('t1) | #U2('t2) | #U3('t3) | #U4('t4) | #U5('t5) ]> + type t6<'t1, 't2, 't3, 't4, 't5, 't6> = container<[ #U1('t1) | #U2('t2) | #U3('t3) | #U4('t4) | #U5('t5) | #U6('t6) ]> + type t7<'t1, 't2, 't3, 't4, 't5, 't6, 't7> = container<[ #U1('t1) | #U2('t2) | #U3('t3) | #U4('t4) | #U5('t5) | #U6('t6) | #U7('t7) ]> + type t8<'t1, 't2, 't3, 't4, 't5, 't6, 't7, 't8> = container<[ #U1('t1) | #U2('t2) | #U3('t3) | #U4('t4) | #U5('t5) | #U6('t6) | #U7('t7) | #U8('t8) ]> +} + +module Intersection = { + type container<-'cases> + + external get1 : container<[> #I1('t1)]> => 't1 = "%identity" + external get2 : container<[> #I2('t2)]> => 't2 = "%identity" + external get3 : container<[> #I3('t3)]> => 't3 = "%identity" + external get4 : container<[> #I4('t4)]> => 't4 = "%identity" + external get5 : container<[> #I5('t5)]> => 't5 = "%identity" + external get6 : container<[> #I6('t6)]> => 't6 = "%identity" + external get7 : container<[> #I7('t7)]> => 't7 = "%identity" + external get8 : container<[> #I8('t8)]> => 't8 = "%identity" + + type t2<'t1, 't2> = container<[ #I1('t1) | #I2('t2) ]> + type t3<'t1, 't2, 't3> = container<[ #I1('t1) | #I2('t2) | #I3('t3) ]> + type t4<'t1, 't2, 't3, 't4> = container<[ #I1('t1) | #I2('t2) | #I3('t3) | #I4('t4) ]> + type t5<'t1, 't2, 't3, 't4, 't5> = container<[ #I1('t1) | #I2('t2) | #I3('t3) | #I4('t4) | #I5('t5) ]> + type t6<'t1, 't2, 't3, 't4, 't5, 't6> = container<[ #I1('t1) | #I2('t2) | #I3('t3) | #I4('t4) | #I5('t5) | #I6('t6) ]> + type t7<'t1, 't2, 't3, 't4, 't5, 't6, 't7> = container<[ #I1('t1) | #I2('t2) | #I3('t3) | #I4('t4) | #I5('t5) | #I6('t6) | #I7('t7) ]> + type t8<'t1, 't2, 't3, 't4, 't5, 't6, 't7, 't8> = container<[ #I1('t1) | #I2('t2) | #I3('t3) | #I4('t4) | #I5('t5) | #I6('t6) | #I7('t7) | #I8('t8) ]> +} + +type intf<-'tags> + +module Primitive = { + type cases<'other> = [ #Null | #Undefined | #String(string) | #Number(float) | #Boolean(bool) | #Symbol(symbol) | #Bigint(Js.Bigint.t) | #Other('other) ] + type t<+'cases> + + let return: ([< cases<'other>] as 'cases) => t<'cases> = x => + %raw(`(function (x) { + switch (x) { + case 'null' | 'Null': return null; + case 'undefined' | 'Undefined': return; + default: return x.VAL; + } + })(x)`) + + let null: t<[> #Null]> = %raw(`null`) + let undefined: t<[> #Undefined]> = %raw(`undefined`) + external string: string => t<[> #String(string)]> = "%identity" + external number: float => t<[> #Number(float)]> = "%identity" + external boolean: bool => t<[> #Boolean(bool)]> = "%identity" + external symbol: symbol => t<[> #Symbol(symbol)]> = "%identity" + external bigint: Js.Bigint.t => t<[> #Bigint(Js.Bigint.t)]> = "%identity" + external other: 'a => t<[> #Other('a)]> = "%identity" + + external fromNull: Js.null<'a> => t<[> #Null | #Other('a) ]> = "%identity" + external toNull: t<[< #Null | #Other('a) ]> => Js.null<'a> = "%identity" + + external fromUndefined: Js.undefined<'a> => t<[> #Undefined | #Other('a) ]> = "%identity" + external toUndefined: t<[< #Undefined | #Other('a) ]> => Js.undefined<'a> = "%identity" + + external fromNullable: Js.nullable<'a> => t<[> #Null | #Undefined | #Other('a) ]> = "%identity" + external toNullable: t<[< #Null | #Undefined | #Other('a) ]> => Js.nullable<'a> = "%identity" + + let classify: t<[< cases<'other>] as 'cases> => 'cases = x => + switch (Js.typeof(x)) { + | "string" => Obj.magic(#String(Obj.magic(x))) + | "number" => Obj.magic(#Number(Obj.magic(x))) + | "boolean" => Obj.magic(#Boolean(Obj.magic(x))) + | "symbol" => Obj.magic(#Symbol(Obj.magic(x))) + | "bigint" => Obj.magic(#Bigint(Obj.magic(x))) + | "undefined" => Obj.magic(#Undefined) + | _ => + if (Js.testAny(x)) { Obj.magic(#Null) } + else { Obj.magic(#Other(x)) } + } +} + +module Newable = { + type t0<'t> + type t1<'arg1, 't> + + @ocaml.doc(`\`'args\` must be a tuple type.`) + type tn<'args, 't> + + let apply0 = (f0: t0<'t>) : 't => %raw(`new f0()`) + let apply1 = (f1: t1<'arg1, 't>, arg1: 'arg1) : 't => %raw(`new f1(arg1)`) + let applyN = (fn: tn<'args, 't>, args: 'args) : 't => %raw(`new fn(...args)`) +} + +module Variadic = { + @ocaml.doc(`\`'variadic\` is expected to be array or some other iterable type.`) + type t0<'variadic, 't> + + @ocaml.doc(`\`'variadic\` is expected to be array or some other iterable type.`) + type t1<'arg1, 'variadic, 't> + + @ocaml.doc(`\`'args\` must be a tuple type. \`'variadic\` is expected to be array or some other iterable type.`) + type tn<'args, 'variadic, 't> + + @ocaml.doc(`\`'variadic\` is expected to be array or some other iterable type.`) + let make0 : ('variadic => 't) => t0<'variadic, 't> = f => %raw(`(function(...args) { return f(args); })`) + + @ocaml.doc(`\`'variadic\` is expected to be array or some other iterable type.`) + let make1 : (('arg1, 'variadic) => 't) => t1<'arg1, 'variadic, 't> = f => %raw(`(function(arg1, ...args) { return f(arg1, args); })`) + + @ocaml.doc(`\`'variadic\` is expected to be array or some other iterable type.`) + let make2 : (('arg1, 'arg2, 'variadic) => 't) => tn<('arg1, 'arg2), 'variadic, 't> = f => %raw(`(function(arg1, arg2, ...args) { return f(arg1, arg2, args); })`) + + @ocaml.doc(`\`'variadic\` is expected to be array or some other iterable type.`) + let make3 : (('arg1, 'arg2, 'arg3, 'variadic) => 't) => tn<('arg1, 'arg2, 'arg3), 'variadic, 't> = f => %raw(`(function(arg1, arg2, arg3, ...args) { return f(arg1, arg2, arg3, args); })`) + + @ocaml.doc(`\`'variadic\` is expected to be array or some other iterable type.`) + let make4 : (('arg1, 'arg2, 'arg3, 'arg4, 'variadic) => 't) => tn<('arg1, 'arg2, 'arg3, 'arg4), 'variadic, 't> = f => %raw(`(function(arg1, arg2, arg3, arg4, ...args) { return f(arg1, arg2, arg3, arg4, args); })`) + + @ocaml.doc(`\`'variadic\` is expected to be array or some other iterable type.`) + let make5 : (('arg1, 'arg2, 'arg3, 'arg4, 'arg5, 'variadic) => 't) => tn<('arg1, 'arg2, 'arg3, 'arg4, 'arg5), 'variadic, 't> = f => %raw(`(function(arg1, arg2, arg3, arg4, arg5, ...args) { return f(arg1, arg2, arg3, arg4, arg5, args); })`) + + @ocaml.doc(`\`'variadic\` is expected to be array or some other iterable type.`) + let make6 : (('arg1, 'arg2, 'arg3, 'arg4, 'arg5, 'arg6, 'variadic) => 't) => tn<('arg1, 'arg2, 'arg3, 'arg4, 'arg5, 'arg6), 'variadic, 't> = f => %raw(`(function(arg1, arg2, arg3, arg4, arg5, arg6, ...args) { return f(arg1, arg2, arg3, arg4, arg5, arg6, args); })`) + + @ocaml.doc(`\`'variadic\` is expected to be array or some other iterable type.`) + let make7 : (('arg1, 'arg2, 'arg3, 'arg4, 'arg5, 'arg6, 'arg7, 'variadic) => 't) => tn<('arg1, 'arg2, 'arg3, 'arg4, 'arg5, 'arg6, 'arg7), 'variadic, 't> = f => %raw(`(function(arg1, arg2, arg3, arg4, arg5, arg6, arg7, ...args) { return f(arg1, arg2, arg3, arg4, arg5, arg6, arg7, args); })`) + + @ocaml.doc(`\`'args\` must be a tuple type. \`'variadic\` is expected to be array or some other iterable type.`) + let makeN : (('args, 'variadic) => 't, int) => tn<'args, 'variadic, 't> = (f, n) => %raw(`(function(...args) { return f(args.slice(0, n), args.slice(n)); })`) + + let apply0 = (f0: t0<'variadic, 't>, variadic: 'variadic) : 't => %raw(`f0(...variadic)`) + let apply1 = (f1: t1<'arg1, 'variadic, 't>, arg1: 'arg1, variadic: 'variadic) : 't => %raw(`f1(arg1, ...variadic)`) + let applyN = (fn: tn<'args, 'variadic, 't>, args: 'args, variadic: 'variadic) : 't => %raw(`fn(...args, ...variadic)`) +} + +module NewableVariadic = { + @ocaml.doc(`\`'variadic\` is expected to be array or some other iterable type.`) + type t0<'variadic, 't> + + @ocaml.doc(`\`'variadic\` is expected to be array or some other iterable type.`) + type t1<'arg1, 'variadic, 't> + + @ocaml.doc(`\`'args\` must be a tuple type. \`'variadic\` is expected to be array or some other iterable type.`) + type tn<'args, 'variadic, 't> + + let apply0 = (f0: t0<'variadic, 't>, variadic: 'variadic) : 't => %raw(`new f0(...variadic)`) + let apply1 = (f1: t1<'arg1, 'variadic, 't>, arg1: 'arg1, variadic: 'variadic) : 't => %raw(`new f1(arg1, ...variadic)`) + let applyN = (fn: tn<'args, 'variadic, 't>, args: 'args, variadic: 'variadic) : 't => %raw(`new fn(...args, ...variadic)`) +} + +// utility type fallbacks +module Partial = { type t<'a> = 'a } +module Required = { type t<'a> = 'a } +module Readonly = { type t<'a> = 'a } +module Pick = { type t<'a, 'keys> = 'a } +module Record = { type t<'keys, 'a> = Js.Dict.t<'a> } +module Exclude = { type t<'a, 'b> = 'a } +module Extract = { type t<'a, 'b> = 'a } +module Omit = { type t<'a, 'keys> = 'a } +module NonNullable = { type t<'a> = 'a } +module Parameters = { type t<'a> } +module ConstructorParameters = { type t<'a> } +module ReturnType = { type t<'a> } +module InstanceType = { type t<'a> } +module ThisParameterType = { type t<'a> } +module OmitThisParameter = { type t<'a> } +module ThisType = { type t<'a> } +module Uppercase = { type t<'s> = private intrinsic } +module Lowercase = { type t<'s> = private intrinsic } +module Capitalize = { type t<'s> = private intrinsic } +module Uncapitalize = { type t<'s> = private intrinsic } + +// utilities for experimental features +module Experimental = { + module Variant = { + let box = (it: 't, tag: string) : 'Variant => %raw(`{ [tag]: it[tag], _0: it }`) + let unbox = (it: 'Variant) : 't => %raw(`it._0`) + } +} \ No newline at end of file diff --git a/dist/res/yarn.lock b/dist/res/yarn.lock new file mode 100644 index 00000000..405d1f49 --- /dev/null +++ b/dist/res/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +rescript@11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/rescript/-/rescript-11.0.1.tgz#c74af134dc8a16d152169b2456d0720324835f54" + integrity sha512-7T4PRp/d0+CBNnY6PYKffFqo9tGZlvnZpboF/n+8SKS+JZ6VvXJO7W538VPZXf3EYx1COGAWWvkF9e/HgSAqHg== diff --git a/docs/development.md b/docs/development.md index a42146ff..eaefbbcf 100644 --- a/docs/development.md +++ b/docs/development.md @@ -5,6 +5,9 @@ Overview for Developers Modules with **\[\\]** does not require `open` to use. +- `build/` ... build scripts + - `BindingUpdater.fs` ... a utility to update bindings in `lib/Bindings/` + - `build.fs` ... the main build script - `lib/` ... target-agnostic part of the tool (will be separated to a different repo in near future) - `Bindings/` ... bindings to JS libraries (typescript, browser-or-node) - `Extensions.fs` ... **\[\\]** extensions for standard library and JS libraries @@ -26,16 +29,19 @@ Modules with **\[\\]** does not require `open` to use. - `Target.fs` ... generic definitions for each targets (`ITarget<_>`) - `Targets/` ... targets should be placed into here - `ParserTest.fs` ... debug target to test parser and typer - - `JsOfOCaml/` ... `js_of_ocaml` target specific codes - - `Common.fs` ... command line options for `js_of_ocaml` target - - `OCamlHelper.fs` ... helper functions to generate OCaml code - - `Writer.fs` ... functions for generating OCaml code from AST - - `Target.fs` ... `ITarget<_>` instance for `js_of_ocaml` target + - `{Target}/` ... target-specific codes + - `Common.fs` ... command line options for the target + - `{Target}Helper.fs` ... helper functions to generate code for the target language + - `Writer.fs` ... functions for generating the code from AST + - `Target.fs` ... `ITarget<_>` instance for the target - `Main.fs` ... entry point - `test/` - - `jsoo/` ... test for `js_of_ocaml` target -- `dist/` ... output directory for NPM packaging -- `dist_jsoo/` ... output directory for OPAM packaging + - `jsoo/` ... test for the `js_of_ocaml` target + - `res/` ... test for the `ReScript` target +- `dist/` + - `js/ `... output directory for NPM packaging + - `jsoo/` ... output directory for OPAM packaging + - `jsoo/` ... output directory for NPM packaging of the stdlib for ReScript - `output/` ... temporary output directory for automated testing, etc ## Requirements @@ -52,9 +58,18 @@ Modules with **\[\\]** does not require `open` to use. - Node 14.0 or higher - [yarn](https://yarnpkg.com/) is required. +- ReScript 11.0.1 or higher + - Installed by `yarn`. + +## Updating TypeScript SDK + +- Run `yarn update --latest typescript` +- Run `./fake UpdateBindings` to update the Fable binding (`lib/Bindings/TypeScript.fs`) +- Run `./fake build` and fix type errors + ## Debugging -`./fake watch` to live update `dist/ts2ocaml.js`. +`./fake watch` to live update `dist/js/ts2ocaml.js`. It will be bundled by Webpack with the `development` mode. @@ -64,9 +79,9 @@ It will be bundled by Webpack with the `development` mode. - `yarn install` to populate `node_modules` - `dotnet restore ts2ocaml.sln` to install required F# libraries - Compile F# source files into JS source files (through Fable) -- Bundle the JS files into `dist/ts2ocaml.js` (through Webpack) +- Bundle the JS files into `dist/js/ts2ocaml.js` (through Webpack) -The resulting `dist/ts2ocaml.js` is then ready to run through `node`. +The resulting `dist/js/ts2ocaml.js` is then ready to run through `node`. ## Testing @@ -88,6 +103,22 @@ The resulting `dist/ts2ocaml.js` is then ready to run through `node`. - Copy the bindings to `test/jsoo/src/` - Perform `dune build` in `test/jsoo/` +### Test the tool for [`ReScript` target](rescript.md) + +- Generate bindings for the following packages: + - TypeScript standard libraries (`node_modules/typescript/lib/lib.*.d.ts`) + - `typescript` with the `full` preset (involving a lot of inheritance) + - `react` with the `full` preset (depending on both `full` packages and `safe` packages) + - `scheduler/tracing` (`safe`) + - `csstype` (`full`) + - `prop-types` (`safe`) + - `react-modal` with the `full` preset (depending on a `full` package) + - `yargs` with the `safe` preset (depending on a `safe` package) + - `yargs-parser` (`safe`) +- The bindings will be placed into `output/test_res/` +- Copy the bindings to `test/res/src/generated/` +- Perform `yarn build` in `test/res/` + > Tests for other targets will be added here ## Publishing @@ -96,12 +127,12 @@ The resulting `dist/ts2ocaml.js` is then ready to run through `node`. ### Prepare for publishing the standard library for [`js_of_ocaml` target](js_of_ocaml.md) to the `jsoo-stdlib` branch -- Copy `ts2ocaml_*.mli` from `output/test_jsoo/` to `dist_jsoo/src/` -- Copy `ts2ocaml_*.ml` from `test/jsoo/_build/default/src/` to `dist_jsoo/src/` -- Set the correct `version` in `dist_jsoo/dune-project` -- Perform `dune build` in `dist_jsoo/` to generate `.opam` file and check if it compiles +- Copy `ts2ocaml_*.mli` from `output/test_jsoo/` to `dist/jsoo/src/` +- Copy `ts2ocaml_*.ml` from `test/jsoo/_build/default/src/` to `dist/jsoo/src/` +- Set the correct `version` in `dist/jsoo/dune-project` +- Perform `dune build` in `dist/jsoo/` to generate `.opam` file and check if it compiles -GitHub Action `publish.yml` is configured to push the `dist_jsoo` directory to the `jsoo-stdlib` branch. +GitHub Action `publish.yml` is configured to push the `dist/jsoo` directory to the `jsoo-stdlib` branch. ### Prepare for publishing the tool to NPM diff --git a/docs/rescript.md b/docs/rescript.md new file mode 100644 index 00000000..956c421d --- /dev/null +++ b/docs/rescript.md @@ -0,0 +1,840 @@ +# ts2ocaml for ReScript + +Generates binding for ReScript. + +# Overview + +`ts2ocaml` is a powerful tool, but there are so many options and also some caverts. + +Therefore, we first provide a walkthrough to use this tool for your project. + +The documentation for the `ts2ocaml` command and its options comes after the walkthrough, starting with the [Usage](#usage) setion. + +## Requirements + +`ts2ocaml` targets ReScript v11 or later. + +## Adding `ts2ocaml.res` + +ReScript has a rich standard library to use JS and DOM APIs and `ts2ocaml` makes use of it as much as possible. In addition to that, `ts2ocaml` uses a small standard library to handle some TypeScript-specific concepts: + +- `intf<'tags>` type, which is used for [tag-based subtyping](#feature-tag). +- TypeScript-specific primitive types, such as `any`, `never`, `unknown`, etc. +- Utility types for handling TypeScript's union types and intersection types. + +Run `ts2ocaml res --create-stdlib` to generate `ts2ocaml.res`. You can safely add it to your project, and even modify it for your needs. + +## Choosing a preset + +`ts2ocaml` has many options, so there is an option [`--preset`](#--preset) to set multiple options at once which is commonly used together. + +- `--preset=minimal` + - A preset to **minimize the output**. + - Intended for library authors, who will modify the output and build a binding library upon it. + - It generates the simplest binding. + - However, it lacks subtyping and it will not compile if the package depends on another package. +- `--preset=safe` + - A preset to generate a code **which just compiles and works**. + - Suited for generating bindings for relatively small packages, which involve less inheritance and slightly depend on other packages. + - e.g. `yargs`, which has a minimal dependency and does not make use of `extends` so much. +- `--preset=full` + - A preset to generate a code with **more type safety** and **more support for package dependency**. + - Suited for generating bindings for large packages, which have many `extends` and/or heavily depend on another package. + - e.g. React component packages, which almost certainly inherits many interfaces from React. + +[`--preset`](#--preset) doesn't override options you explicitly set. +See [`--preset`](#--preset) for the options which will be set by each preset. + +> **Hint:** if a package `foo` depends only on `bar` and `bar` depends on many other packages, +> it's safe to use `--preset=safe` to `foo` and `--preset=full` to `bar`, but not vice versa. + +## Generating and using the bindings + +Once you figure out which preset (and some additional options if any) to use, you are now ready to run `ts2ocaml`. + +``` +ts2ocaml res --preset full --output-dir src node_modules/typescript/lib/typescript.d.ts +``` + +A binding (`typescript.res` and `typescript.resi` in this example) will be generated in the `src` directory. + +The binding has an `Export` module which corresponds to the package's default export (`export default ..` or `export = ..` in TypeScript). + +Define a module alias to "import" the package: + +```rescript +module Ts = Typescript.Export +``` + +Now you can use the binding through the module alias: + +```rescript +let source = "let x: string = 'hello, world!'" + +let result = Ts.transpileModule( + ~input=source, + ~transpileOptions=Ts.TranspileOptions.make( + ~compilerOptions=Ts.CompilerOptions.make(~\"module"=CommonJS), + ), +) + +Js.log(result->Ts.TranspileOutput.get_outputText) +``` + +# Conventions + +Here we describe the coding conventions and file name conventions used by `ts2ocaml` to ensure that multiple bindings work together without problem. If you are not interested, you can skip to the [Usage](#usage) setion. + +## `import` and `export` + +To work with multiple files and packages, `ts2ocaml` has some conventions around the name of the generated OCaml source codes. + +1. If not known, `ts2ocaml` computes the JS module name of the input `.d.ts` file by [heuristics](#how-the-heuristics-work). +2. `ts2ocaml` converts the JS module name to a ReScript module name by the followings: + - Removes `@` at the top of the module name + - Replaces `/` with `__` + - Replaces any other signs (such as `-`) to `_` +3. `ts2ocaml` uses the ReScript module name as the output file name. + +### How the heuristics work + +- If the filename is equal to `types` or `typings` of `package.json`, then `ts2ocaml` will use the package name itself. + - input: `node_modules/typescript/lib/typescript.d.ts` + - `package.json`: `"typings": "./lib/typescript.d.ts",` + - `getJsModuleName`: `typescript` + - output file: `typescript.res` +- If the filename is present in `exports` of `package.json`, then `ts2ocaml` will combine the package name and the exported module name. + - input: `node_modules/@angular/common/http/http.d.ts` + - `package.json`: `"exports": { .., "./http": { "types": "./http/http.d.ts", .. }, .. }` + - `getJsModuleName`: `@angualr/common/http` + - output file: `angular__common__http.res` +- Otherwise, `ts2ocaml` uses a heuristic module name: it will combine the package name and the filename. `index.d.ts` is handled specially. + - input: `node_modules/cassandra-driver/lib/auth/index.d.ts` + - `getJsModuleName`: `cassandra-driver/auth` + - output file: `cassandra_driver__auth.res` + - if `package.json` is not present, the package name is also inferred heuristically from the filename. + +### How the `import` statements are translated + +- `import` of another package from `node_modules` will be converted to an `open` statement or a module alias. + - The ReScript module name of the imported package is computed by the step 2 of [the above](#handling-import-and-export). + +```typescript +// node_modules/@types/react/index.d.ts +import * as CSS from "csstype"; +import { Interaction as SchedulerInteraction } from "scheduler/tracing"; +``` + +```rescript +// react.res +/* import * as CSS from 'csstype'; */ +module CSS = Csstype.Export +/* import { Interaction as SchedulerInteraction } from 'scheduler/tracing'; */ +module SchedulerInteraction = Scheduler__tracing.Export.Interaction +``` + +- `import` of relative path will be converted to an `open` statement or a module alias. + - The OCaml module name of the imported file will also be inferred by [heuristics](#how-the-heuristics-work). + +```typescript +// node_modules/cassandra-driver/index.d.ts +import { auth } from "./lib/auth"; +``` + +```rescript +// cassandra_driver.res +module Auth = Cassandra_driver__auth.Export.Auth +``` + +```typescript +// node_modules/cassandra-driver/lib/mapping/index.d.ts +import { Client } from "../../"; +``` + +```rescript +// cassandra_driver__mapping.res +module Client = Cassandra_driver.Export.Client +``` + +- Indirect `import` using identifiers is not yet be supported. + +```typescript +import { types } from "./lib/types"; +import Uuid = types.Uuid; // we should be able to convert this to `module Uuid = Type.Uuid`, but not yet +``` + +- Direct `export` of an external module **will not be supported**. + +```typescript +export { someFunction } from "./lib/functions"; // this is VERY hard to do in OCaml! +``` + +### How the `export` statements are translated + +`ts2ocaml` will create a module named `Export` to represent the exported definitions. + +- If an export assignment `export = Something` is used, the `Export` module will be an alias to the `Something` module. + +```rescript +/* export = Something */ +module Export = Something +``` + +- If ES6 exports `export interface Foo` or `export { Bar }` are used, the `Export` module will contain the exported modules. + +```rescript +module Export = { + /* export interface Foo */ + module Foo = Foo + /* export { Bar } */ + module Bar = Bar + /* export { Baz as Buzz } */ + module Buzz = Baz +} +``` + +This is why you are advised to use the generated bindings with the following: + +```rescript +/* This is analogous to `import * as TypeScript from "typescript";` */ +module TypeScript = Typescript.Export +``` + +## Optional type parameters + +TypeScript makes use of [generic parameter defaults](https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-parameter-defaults), where you can make a type parameter optional with a default type, which is not supported by ReScript. As such, `ts2ocaml` emits additional type aliases when it encounters such type parameters. + +For example, assume we have `node_modules/foo/index.d.ts` and `node_modules/bar/index.d.ts` as the following: + +```typescript +// foo/index.d.ts + +declare namespace foo { + interface A { ... } + + interface B { ... } +} + +export = foo; +``` + +```typescript +// bar/index.d.ts + +import * as Foo from "foo"; + +declare function useA(a: Foo.A): void; +declare function useB(b: Foo.B): void; +declare function useBDefault(b: Foo.B): void; +``` + +Then the outputs will look like this: + +```rescript +/* foo.res */ + +module Foo = { + module A = { + type t<'T> = intf<[#A(T)]> + + ... + } + + module B = { + type t<'T> = intf<#B(T)> + type t0 = t + + ... + } +} + +/* export = foo; */ +module Export = Foo +``` + +```rescript +/* bar.res */ + +/* import * as Foo from "foo"; */ +module Foo = Foo.Export + +@module("bar") @val external useA: (Foo.A.t<'T>) => unit = "useA" +@module("bar") @val external useB: (Foo.B.t<'T>) => unit = "useB" +@module("bar") @val external useBDefault: (Foo.B.t0) => unit = "useBDefault" +``` + +# Usage + +```bash +$ ts2ocaml res [options] +``` + +> See also [the common options](common_options.md). + +# General Options + +## `--preset` + +Specify the preset to use. + +- `--preset=minimal` + - It sets `--simplify=all``. +- `--preset=safe` + - It sets `--subtyping=cast-function`. + - It also sets all the options `--preset=minimal` sets. +- `--preset=full` + - It sets `--inherit-with-tags=full` and `--subtyping=tag`. + - It also sets all the options `--preset=safe` sets. + +## `--create-stdlib` + +If set, `ts2ocaml` will create `ts2ocaml.res`. + +# Output Options + +## `-o`, `--output-dir` + +The directory to place the generated bindings. +If not set, it will be the current directory. + +## `--no-resi` + +If set, `ts2ocaml` will not generate interface files (`.resi`). + +# JS Module Options + +## `--module` + +Override the JS module type. If not set, it is inferred from the input. + +- `--module=es`: Treats the input as an ES module. +- `--module=cjs`: Treats the input as a CommonJS module. +- `--module=none`: Treats the input as a global definition. + +## `--name` + +Override the JS module name used in the `@module` attribute. +If not set, it is inferred from `package.json`. + +# Typer Options + +## `--int`, `--number-as-int` + +Treat number types as `int`. If not set, `float` will be used. + +## `--subtyping` + +> See also [the detailed docs about modeling TypeScript's subtyping in OCaml](modeling_subtyping.md). + +Turn on subtyping features. + +You can use `--subtyping=foo,bar` to turn on multiple features. Also, use `--subtyping=off` to explicitly disable subtyping features. + +### Feature: `tag` + +Use `intf<'tags>` for class and interface types, which [simulates nominal subtyping](modeling_subtyping.md#phantom-types-with-row-polymorphism-polymorphic-variants) by putting to `'tags` the class names as a polymorphic variant. + +For example, assume we have the following input: + +```typescript +interface A { + methA(a: number): number; +} + +interface B extends A { + methB(a: number, b: number): number; +} + +interface C extends B { + methC(a: number, b: number, c: number): number; +} +``` + +When this feature is used, the resulting binding will look like: + +```rescript +module A = { + type t = intf<[ #A ]> + @send external methA: (t, ~a:float) => float = "methA" + external castFrom: (intf<[> #A ]>) => t = "%identity" +} + +module B = { + type t = intf<[ #A | #B ]> + @send external methB: (t, ~a:float, ~b:float) => float = "methB" + external castFrom: (intf<[> #B ]>) => t = "%identity" +} + +module C = { + type t = intf<[ #A | #B | #C ]> + @send external methC: (t, ~a:float, ~b:float, ~c:float) => float = "methC" + external castFrom: (intf<[> #C ]>) => t = "%identity" +} +``` + +So if we have a `let x : C.t`, you can directly cast it to `A.t` by writing `x :> A.t`. + +Alternatively, you can also write `A.castFrom(x)`, which uses a generic cast function `castFrom`. + +```rescript +let c : C.t = ... + +let a1 : A.t = c :> A.t +let a2 : A.t = A.castFrom(c) +``` + +### Feature: `cast-function` + +Add [`cast` functions](https://github.com/ocsigen/ts2ocaml/blob/bootstrap/docs/modeling_subtyping.md#cast-functions) to cast types around. + +For example, assume we have the following input: + +```typescript +interface A { + methA(a: number): number; +} + +interface B extends A { + methB(a: number, b: number): number; +} + +interface C extends B { + methC(a: number, b: number, c: number): number; +} +``` + +When this feature is used, the resulting binding will look like: + +```rescript +module A = { + type t + @send external methA: (t, ~a:float) => float = "methA" +} + +module B = { + type t + @send external methB: (t, ~a:float, ~b:float) => float = "methB" + external castToA: (t) => A.t = "%identity" +} + +module C = { + type t + @send external methC: (t, ~a:float, ~b:float, ~c:float) => float = "methC" + external castToB: (t) => B.t = "%identity" +} +``` + +So if we have a `let x : C.t`, you can cast it to `A.t` by writing `B.castToA(C.castToB(x))`. + +```rescript +let c : C.t = ... + +let a : A.t = x->C.castToB->B.castToA +``` + +This feature is less powerful than [`tag`](#feature-tag), but it has some use cases [`tag`](#feature-tag) doesn't cover. + +- [`tag`](#feature-tag) [doesn't support diamond inheritance](modeling_subtyping.md#phantom-types-with-row-polymorphism-polymorphic-variants), while `cast-function` does. +- When [`--inherit-with-tags`](#--inherit-with-tags) is not used, [`tag`](#feature-tag) doesn't support casting a type to other from a different package, while `cast-function` does. + +## `--inherit-with-tags` + +> **Note:** This options requires [`--subtyping=tag`](#feature-tag). If the `tag` feature is not specified, it will fail with an error. + +Use `TypeName.tags` type names to inherit types from other packages. + +- `--inherit-with-tags=full` (default) + - It generates `tags` types in the module, and tries to use `tags` type to inherit a type if it is unknown (e.g. from another package). +- `--inherit-with-tags=provide` + - It only generates `tags` types in the module. +- `--inherit-with-tags=consume` + - It only tries to use `tags` type if the inherited type is unknown. +- `--inherit-with-tags=off` + - It disables any usage of `tags` types. + +For example, assume we have `node_modules/foo/index.d.ts` and `node_modules/bar/index.d.ts` as the following: + +```typescript +// foo/index.d.ts + +declare namespace foo { + interface A { ... } +} + +export = foo; +``` + +```typescript +// bar/index.d.ts + +import * as Foo from 'foo'; + +declare namespace bar { + interface B extends A { ... } +} + +export = bar; +``` + +Then the outputs will look like depending on the option you set: + +```rescript +/* foo.res */ + +module Foo = { + module A = { + type t = intf<[ #A ]> + + /* this will be generated if `full` or `provide` is set */ + type tags = [ #A ] + + /* this will be generated regardless of the option */ + type this<'tags> = intf<'tags> constraint 'tags = [> #A ] + external castFrom: (this<'tags>) => t = "%identity" + + ... + } +} + +/* export = foo; */ +module Export = Foo +``` + +```rescript +/* bar.res */ + +/* import * as Foo from "foo"; */ +module Foo = Foo.Export + +module Bar = { + module B = { + /* if `full` or `consume` is set, this will be generated */ + type t = intf<[ #B | Foo.A.tags ]> + /* otherwise, this will be generated */ + type t = intf<[ #B ]> + + /* if `full` is set, this will be generated */ + type tags = [ #B | Foo.A.tags ] + /* else if `provide` is set, this will be generated */ + type tags = [ #B ] + + /* this will be generated regardless of the option */ + type this<'tags> = intf<'tags> constraint 'tags = [> #B ] + external castFrom: (this<'tags>) => t = "%identity" + + ... + } +} + +/* export = bar; */ +module Export = Bar +``` + +If `provide` or `full` is used for `foo.d.ts` and `consume` or `full` is used for `bar.d.ts`, +you will be able to safely cast `B.t` to `A.t`, although they come from different packages. + +```rescript +module Foo = Foo.Export +module Bar = Bar.Export + +let bar : Bar.B.t = ... + +let foo1 : Foo.A.t = bar :> Foo.A.t +let foo2 : Foo.A.t = Foo.A.castFrom(bar) +``` + +Otherwise, you can't safely cast `B.t` to `A.t`. To do it, you will have to + +- set [`--subtyping=cast-function`](#feature-cast-function) to obtain `castToA: (B.t) => A.t`, or +- manually add `#A` to the definition of `B.t` (and `B.tags` if you choose to provide). + +# Code Generator Options + +## `--simplify` + +Turn on simplification features. + +You can use `--simplify=foo,bar` to turn on multiple features. Also, `--simplify=all` enables all the features and `--simplify=off` explicitly disables simplification features. + +### Feature: `immediate-instance` + +Simplifies a value definition of an interface type with the same name **(case sensitive)** to a module. + +Assume we have the following input: + +```typescript +interface Foo = { + someMethod(value: number): void; +} + +declare var Foo: Foo; +``` + +If this option is set, the output will be: + +```rescript +module Foo = { + @module("package") @val @scope("Foo") external someMethod: float => unit = "someMethod" +} + +/* usage */ +Foo.someMethod(42.0) +``` + +Otherwise, the output will be: + +```rescript +module Foo = { + type t + + @send external someMethod: (t, float) => unit = "someMethod" +} + +@module("package") @val external foo: Foo.t = "Foo" + +/* usage */ +foo->Foo.someMethod(42.0) +``` + +A notable example is the `Math` object in ES5 (https://github.com/microsoft/TypeScript/blob/main/lib/lib.es5.d.ts). + +### Feature: `immediate-constructor` + +Simplifies so-called constructor pattern. + +Assume we have the following input: + +```typescript +interface Foo = { + someMethod(value: number): void; +} + +interface FooConstructor { + new(name: string) : Foo; + + staticMethod(): number; +} + +declare var Foo: FooConstructor; +``` + +If this option is set, the output will be: + +```rescript +module Foo = { + type t + @send external someMethod: (t, float) => unit = "someMethod" + + @module("package") @new external create: (string) => t = "Foo" + @module("package") @scope("Foo") @val external staticMethod: () => float = "staticMethod" +} + +/* usage */ +let x = Foo.create("foo") +let num = Foo.staticMethod() +x->Foo.someMethod(num) +``` + +Otherwise, the output will be: + +```rescript +module Foo = { + type t + @send external someMethod: (t, float) => unit = "someMethod" +} + +module FooConstructor = { + type t + @get external create: Newable.t1 = "Foo" + @send external staticMethod: (t, ()) => float = "staticMethod" +} + +@module("package") @val external foo: FooConstructor.t = "Foo" + +/* usage */ +let x = foo->FooConstructor.create->Newable.apply1("foo") +let num = foo->FooConstructor.staticMethod() +x->Foo.someMethod(num) +``` + +A notable example is the `ArrayConstructor` type in ES5 (https://github.com/microsoft/TypeScript/blob/main/lib/lib.es5.d.ts). + +### Feature: `anonymous-interface-value` + +Simplifies a value definition of an anonymous interface type to a module. + +Assume we have the following input: + +```typescript +declare var Foo: { + someMethod(value: number): void; +}; +``` + +If this option is set, the output will be: + +```rescript +module Foo = { + @module("package") @val external someMethod: (float) => unit = "someMethod" +} + +/* usage */ +Foo.someMethod(42.0) +``` + +Otherwise, the output will be: + +```rescript +module AnonymousInterface = { + type t + + @send external someMethod: (t, float) => unit = "someMethod" +} + +@module("package") @val external foo: AnonymousInterface.t = "Foo" + +/* usage */ +foo->AnonymousInterface.someMethod(42.0) +``` + +A notable example is the `Document` variable in DOM (https://github.com/microsoft/TypeScript/blob/main/lib/lib.dom.d.ts). + +### Feature: `named-interface-value` + +> **Note:** [`immediate-instance`](#feature-immediate-instance) and [`immediate-constructor`](#feature-immediate-constructor) will override this feature if the name of the value definition is the same as the corresponding interface. + +Defines additional module with a suffix `Static` for a value definition of some interface type. + +Assume we have the following input: + +```typescript +interface Foo = { + someMethod(value: number): void; +} + +declare var foo: Foo; +``` + +If this option is set, the output will be: + +```rescript +module Foo = { + type t + + @send external someMethod: (t, float) => unit = "someMethod" +} + +module FooStatic = { + @module("package") @scope("Foo") @val external someMethod: float => unit = "someMethod" +} + +@module("package") @val external foo: Foo.t = "Foo" + +/* usage */ +FooStatic.someMethod(42.0) +foo->Foo.someMethod(42.0) // "instance call" is also available +``` + +Otherwise, the output will be: + +```rescript +module Foo = { + type t + + @send external someMethod: (t, float) => unit = "someMethod" +} + +@module("package") @val external foo: Foo.t = "Foo" + +/* usage */ +foo->Foo.someMethod(42.0) +``` + +A notable example is the `document` variable in DOM (https://github.com/microsoft/TypeScript/blob/main/lib/lib.dom.d.ts). + +## `--readable-names` + +Try to use more readable names instead of `AnonymousInterface{N}`. + +- If the anonymous interface is an argument of a function, the name of the argument will be used. +- If the anonymous interface is the type of a field or the return type of a function, the name of the field/function will be used. + +## `--no-types-module` + +TypeScript code often has mutually recursive definitions. ReScript support defining recursive types by `type rec`, but there are some cases where `type rec` is not enough. As such, `ts2ocaml` emits a special recursive module named `Types` that contains all the types used in the file. You can use the `--no-types-module` option to disable this. + +> **Warning:** +> This option is intended for library authors who want a minimalistic output. It will generate a broken code if an input file contains mutually recursive types. A manual modification would be needed! +> +> Also, you wouldn't need this unless you're using the [`--no-resi`](#--no-resi) option, as the `Types` module is hidden by the `.resi` file and won't show up in the editor autocompletion. + +# Experimental Options + +> **Warning:** +> These features are experimental and may be subject to change. + +## `--experimental-tagged-union` + +Emit additional variant type for tagged union. + +Assume we have the following input: + +```typescript +interface Foo { + kind: "foo"; + ... +} + +interface Bar { + kind: "bar"; + ... +} + +type FooBar = Foo | Bar; +``` + +Normally, `ts2ocaml` would generate the following code: + +```rescript +module Foo = { + type t + @get external get_kind: (t) => string = "kind" + ... +} + +module Bar = { + type t + @get external get_kind: (t) => string = "kind" + ... +} + +module FooBar = { + type t = Union.t2 +} +``` + +With this option, `ts2ocaml` will generate an additional type `FooBar.cases` and additional functions `FooBar.box` and `FooBar.unbox`: + +```rescript +module FooBar = { + type t = Union.t2 + + @tag("kind") type cases = + | @as("foo") Foo (Foo.t) + | @as("bar") Bar (Bar.t) + + let box: (t) => cases = ... + let unbox: (cases) => t = ... +} +``` + +Now you can match over the tagged union type by `box`ing it first: + +```rescript +let x : FooBar.t = ... + +switch x->FooBar.box { + | Foo(foo) => ... + | Bar(bar) => ... +} +``` \ No newline at end of file diff --git a/fake b/fake old mode 100644 new mode 100755 diff --git a/lib/Common.fs b/lib/Common.fs index b39d6428..b5318be1 100644 --- a/lib/Common.fs +++ b/lib/Common.fs @@ -35,6 +35,7 @@ type OverloadRenamer(?rename: string -> int -> string, ?used: Set m.[(category, name)] <- i + 1 diff --git a/lib/DataTypes/Graph.fs b/lib/DataTypes/Graph.fs index db138e40..10956540 100644 --- a/lib/DataTypes/Graph.fs +++ b/lib/DataTypes/Graph.fs @@ -127,11 +127,26 @@ module Graph = #endif result -type DependencyTrie<'k when 'k: comparison> = Trie<'k, 'k list list> +type DependencyTrieInfo<'k> = { + isRecursive: bool + scc: 'k list list +} + +type DependencyTrie<'k when 'k: comparison> = Trie<'k, DependencyTrieInfo<'k>> module DependencyTrie = open Ts2Ml.Extensions + let rec isLinear (dt: DependencyTrie<'k>) = + let searchChildren () = + dt.children + |> Map.toSeq + |> Seq.map snd + |> Seq.forall isLinear + match dt.value with + | Some { isRecursive = true } -> false + | _ -> searchChildren () + let ofTrie (getReferences: 'v -> WeakTrie<'k>) (trie: Trie<'k, 'v>) : DependencyTrie<'k> = let refTrieMap = new MutableMap<'k list, WeakTrie<'k>>() let rec getRefTrie nsRev (x: Trie<'k, 'v>) = @@ -158,8 +173,17 @@ module DependencyTrie = |> List.choose (function [x] -> Some (k, x) | _ -> None (* should be impossible *)) refs :: state) [] |> List.rev |> List.concat - let rec go nsRev (x: Trie<'k, 'v>) : DependencyTrie<'k> = + let rec go isRecursive nsRev (x: Trie<'k, 'v>) : DependencyTrie<'k> = let g = getDeps nsRev x |> Graph.ofEdges let scc = Graph.stronglyConnectedComponents g (x.children |> Map.toList |> List.map fst) - { value = Some scc; children = x.children |> Map.map (fun k child -> go (k :: nsRev) child) } - go [] trie + let isRecursiveMap = + scc + |> List.collect (function + | [] -> [] + | [k] -> [k, false] + | ks -> ks |> List.map (fun k -> k, true)) + |> Map.ofList + { value = Some { scc = scc; isRecursive = isRecursive }; + children = x.children |> Map.map (fun k child -> + go (isRecursiveMap |> Map.tryFind k |? false) (k :: nsRev) child) } + go false [] trie diff --git a/lib/Extensions.fs b/lib/Extensions.fs index 7d1aa9c7..fcd9b8c1 100644 --- a/lib/Extensions.fs +++ b/lib/Extensions.fs @@ -105,6 +105,20 @@ module Map = | Some v1 -> m1 |> Map.add k (f v1 v2) ) m1 + let intersectWith f m1 m2 = + let getKeys = Map.keys >> Set.ofSeq + Set.intersect (getKeys m1) (getKeys m2) + |> Set.toSeq + |> Seq.choose (fun key -> + let v1 = m1 |> Map.tryFind key + let v2 = m2 |> Map.tryFind key + match v1, v2 with + | None, None -> None + | Some v, None + | None, Some v -> Some (key, v) + | Some v1, Some v2 -> f v1 v2 |> Option.map (fun v -> key, v)) + |> Map.ofSeq + type MutableMap<'k, 'v> = Collections.Generic.Dictionary<'k, 'v> type MutableSet<'v> = Collections.Generic.HashSet<'v> diff --git a/lib/Syntax.fs b/lib/Syntax.fs index 0e4cb6e8..61fda9ec 100644 --- a/lib/Syntax.fs +++ b/lib/Syntax.fs @@ -91,6 +91,21 @@ and [] Comment = | See of name:string option * text:string list | ESVersion of Ts.ScriptTarget | Other of tag:string * text:string list * orig:Ts.JSDocTag + member x.ToJsDoc() = + let concat (lines: string list) = String.concat "\n" lines + match x with + | Description lines -> "@description " + concat lines + | Summary lines -> "@summary " + concat lines + | Param (name, lines) -> sprintf "@param %s " name + concat lines + | Return lines -> "@returns " + concat lines + | Deprecated lines -> "@deprecated " + concat lines + | Example lines -> "@example" + "\n" + concat lines + | See (Some name, []) -> sprintf "@see %s" name + | See (Some name, lines) -> sprintf "@see {@link %s} " name + concat lines + | See (None, lines) -> "@see " + concat lines + | ESVersion target -> sprintf "@since %s" (Enum.pp target) + | Other (_, _, orig) -> orig.getText() + override x.Equals(yo) = match yo with | :? Comment as y -> true @@ -230,7 +245,19 @@ and [] FullName = { and FieldLike = { name:string; isOptional:bool; value:Type } -and FuncType<'returnType> = { args:Choice list; isVariadic:bool; returnType:'returnType; loc: Location } +and FuncType<'returnType> = { + args:Choice list + isVariadic:bool + returnType:'returnType + loc: Location +} with + member this.map (f: 'returnType -> 'a) = + { + args = this.args + isVariadic = this.isVariadic + returnType = f this.returnType + loc = this.loc + } and Accessibility = Public | Protected | Private and Mutability = ReadOnly | WriteOnly | Mutable @@ -317,43 +344,75 @@ and MemberAttribute = { member this.getComments() = this.comments member this.mapComments f = { this with comments = f this.comments } -and Variable = { +and Variable<'Type> = { name: string - typ: Type + typ: 'Type isConst: bool isExported: Exported accessibility : Accessibility option comments: Comment list loc: Location } with - interface ICommented with + member this.map (f: 'Type -> 'a) = + { + name = this.name + typ = f this.typ + isConst = this.isConst + isExported = this.isExported + accessibility = this.accessibility + comments = this.comments + loc = this.loc + } + interface ICommented> with member this.getComments() = this.comments member this.mapComments f = { this with comments = f this.comments } +and Variable = Variable -and Function = { +and Function<'Type> = { name: string - typ: FuncType + typ: FuncType<'Type> typeParams: TypeParam list isExported: Exported accessibility : Accessibility option comments: Comment list loc: Location } with - interface ICommented with + member this.map (f: 'Type -> 'a) = + { + name = this.name + typ = this.typ.map f + typeParams = this.typeParams + isExported = this.isExported + accessibility = this.accessibility + comments = this.comments + loc = this.loc + } + interface ICommented> with member this.getComments() = this.comments member this.mapComments f = { this with comments = f this.comments } +and Function = Function -and TypeAlias = { +and TypeAlias<'Type> = { name: string typeParams: TypeParam list - target: Type + target: 'Type comments: Comment list isExported: Exported loc: Location } with - interface ICommented with + member this.map (f: 'Type -> 'a) = + { + name = this.name + typeParams = this.typeParams + target = f this.target + comments = this.comments + isExported = this.isExported + loc = this.loc + } + interface ICommented> with member this.getComments() = this.comments member this.mapComments f = { this with comments = f this.comments } +and TypeAlias = TypeAlias and Statement = /// ```ts @@ -404,6 +463,9 @@ and Statement = /// export ... /// ``` | Export of Export + /// ```ts + /// export ... from ... + /// ``` | ReExport of ReExport | Pattern of Pattern | UnknownStatement of {| origText: string option; comments: Comment list; loc: Location |} diff --git a/lib/Typer.fs b/lib/Typer.fs index 380a220a..83e2f2bc 100644 --- a/lib/Typer.fs +++ b/lib/Typer.fs @@ -43,6 +43,11 @@ type TyperOptions = /// ``` abstract replaceNewableFunction: bool with get,set + /// Ignores all `T extends U` while typechecking. + /// + /// Good for targets which don't support constrained type parameters. + abstract noExtendsInTyprm: bool with get,set + type [] Definition = | TypeAlias of TypeAlias | Class of Class @@ -339,6 +344,12 @@ module Type = let mapInIntersection mapping ctx (i: IntersectionType) : IntersectionType = { types = i.types |> List.map (mapping ctx) } + let mapInErased mapping ctx (e: ErasedType) : ErasedType = + match e with + | IndexedAccess (t1, t2) -> IndexedAccess (mapping ctx t1, mapping ctx t2) + | TypeQuery i -> TypeQuery i + | Keyof t -> Keyof (mapping ctx t) + let rec mapIdent f = function | Intrinsic -> Intrinsic | PolymorphicThis -> PolymorphicThis | Ident i -> Ident (f i) @@ -376,45 +387,61 @@ module Type = Erased (e, loc, origText) | UnknownType msg -> UnknownType msg - let rec substTypeVar (subst: Map) _ctx = function - | TypeVar v -> - match subst |> Map.tryFind v with - | Some t -> t - | None -> TypeVar v - | Union u -> Union (mapInUnion (substTypeVar subst) _ctx u) - | Intersection i -> Intersection (mapInIntersection (substTypeVar subst) _ctx i) - | Tuple ts -> Tuple (ts |> mapInTupleType (substTypeVar subst) _ctx) - | AnonymousInterface c -> AnonymousInterface (mapInClass (substTypeVar subst) _ctx c) - | Func (f, typrms, loc) -> - Func (substTypeVarInFunction subst _ctx f, List.map (substTypeVarInTypeParam subst _ctx) typrms, loc) - | NewableFunc (f, typrms, loc) -> - NewableFunc (substTypeVarInFunction subst _ctx f, List.map (substTypeVarInTypeParam subst _ctx) typrms, loc) + let rec mapTypeVar (f: 'Context -> string -> Type) ctx = function + | TypeVar v -> f ctx v + | Union u -> Union (mapInUnion (mapTypeVar f) ctx u) + | Intersection i -> Intersection (mapInIntersection (mapTypeVar f) ctx i) + | Tuple ts -> Tuple (ts |> mapInTupleType (mapTypeVar f) ctx) + | AnonymousInterface c -> AnonymousInterface (mapInClass (mapTypeVar f) ctx c) + | Func (fn, typrms, loc) -> + Func (mapInFuncType (mapTypeVar f) ctx fn, List.map (mapInTypeParam (mapTypeVar f) ctx) typrms, loc) + | NewableFunc (fn, typrms, loc) -> + NewableFunc (mapInFuncType (mapTypeVar f) ctx fn, List.map (mapInTypeParam (mapTypeVar f) ctx) typrms, loc) | App (t, ts, loc) -> let t = match t with - | AAnonymousInterface i -> AAnonymousInterface (mapInClass (substTypeVar subst) _ctx i) + | AAnonymousInterface i -> AAnonymousInterface (mapInClass (mapTypeVar f) ctx i) | _ -> t - App (t, ts |> List.map (substTypeVar subst _ctx), loc) + App (t, ts |> List.map (mapTypeVar f ctx), loc) | Ident i -> Ident i | Prim p -> Prim p | TypeLiteral l -> TypeLiteral l | PolymorphicThis -> PolymorphicThis | Intrinsic -> Intrinsic | Erased (e, loc, origText) -> - let e' = - match e with - | IndexedAccess (t1, t2) -> IndexedAccess (substTypeVar subst _ctx t1, substTypeVar subst _ctx t2) - | TypeQuery i -> TypeQuery i - | Keyof t -> Keyof (substTypeVar subst _ctx t) - Erased (e', loc, origText) + Erased (mapInErased (mapTypeVar f) ctx e, loc, origText) | UnknownType msgo -> UnknownType msgo - and substTypeVarInTypeParam subst _ctx (tp: TypeParam) = - { tp with - extends = Option.map (substTypeVar subst _ctx) tp.extends - defaultType = Option.map (substTypeVar subst _ctx) tp.defaultType } + let substTypeVar (subst: Map) _ctx = + mapTypeVar (fun _ v -> + match subst |> Map.tryFind v with + | Some t -> t + | None -> TypeVar v + ) _ctx - and substTypeVarInFunction subst _ctx f = - { f with - returnType = substTypeVar subst _ctx f.returnType; - args = List.map (mapInArg (substTypeVar subst) _ctx) f.args } + let private mapTypeParamInClassImpl f mtp ctx (c: Class<'a>) = + { c with + implements = c.implements |> List.map (mtp f ctx) + members = c.members |> List.map (mapInMember (mtp f) ctx) + typeParams = c.typeParams |> List.map (f ctx) } + + let rec mapTypeParam (f: 'Context -> TypeParam -> TypeParam) ctx = function + | Intrinsic -> Intrinsic | PolymorphicThis -> PolymorphicThis + | Ident i -> Ident i | Prim p -> Prim p | TypeLiteral l -> TypeLiteral l | TypeVar v -> TypeVar v | UnknownType m -> UnknownType m + | Union u -> Union (mapInUnion (mapTypeParam f) ctx u) + | Intersection i -> Intersection (mapInIntersection (mapTypeParam f) ctx i) + | Tuple ts -> Tuple (ts |> mapInTupleType (mapTypeParam f) ctx) + | AnonymousInterface c -> AnonymousInterface (mapTypeParamInClassImpl f mapTypeParam ctx c) + | Func (fn, typrms, loc) -> + Func (mapInFuncType (mapTypeParam f) ctx fn, typrms |> List.map (f ctx), loc) + | NewableFunc (fn, typrms, loc) -> + NewableFunc (mapInFuncType (mapTypeParam f) ctx fn, typrms |> List.map (f ctx), loc) + | App (lhs, ts, loc) -> + let lhs = + match lhs with + | AIdent _ | APrim _ -> lhs + | AAnonymousInterface c -> AAnonymousInterface (mapTypeParamInClassImpl f mapTypeParam ctx c) + App (lhs, ts |> List.map (mapTypeParam f ctx), loc) + | Erased (e, loc, orig) -> Erased (mapInErased (mapTypeParam f) ctx e, loc, orig) + + let mapTypeParamInClass f ctx c = mapTypeParamInClassImpl f mapTypeParam ctx c type TypeFinder<'State, 'Result> = 'State -> Type -> Type list option * 'State * 'Result seq @@ -737,9 +764,26 @@ module Type = let isSubClass ctx (sub: Type) (super: Type) = Set.isProperSuperset (getAllInheritancesAndSelf ctx super) (getAllInheritancesAndSelf ctx sub) - let getKnownTypes (ctx: TyperContext<_, _>) t = - findTypes (fun state -> function - | Ident { fullName = fns } -> None, state, List.map KnownType.Ident fns + let knownTypeFinder ctx : TypeFinder<_, _> = + fun state -> function + | Ident ({ fullName = fns } & i) & Dummy ts + | App (AIdent ({ fullName = fns } & i), ts, _) -> + let next = + Ident.getDefinitionsWithFullName ctx i + |> List.collect (fun x -> + match x.definition with + | Definition.TypeAlias { typeParams = typrms } | Definition.Class { typeParams = typrms } -> + assignTypeParams i.name i.loc typrms ts + (fun _ ty -> Some ty) + (fun tv -> + match tv.defaultType with + | Some ty -> Some ty + | None -> None) + | _ -> []) + |> List.choose id + |> List.append ts + |> List.distinct + Some next, state, List.map KnownType.Ident fns | AnonymousInterface a -> let info = ctx |> TyperContext.bindCurrentSourceInfo (fun info -> info.anonymousInterfacesMap |> Map.tryFind a) @@ -748,7 +792,9 @@ module Type = | None -> [] | Some info -> [KnownType.AnonymousInterface (a, info)] | _ -> None, state, [] - ) () t |> Set.ofSeq + + let getKnownTypes (ctx: TyperContext<_, _>) t = + findTypes (knownTypeFinder ctx) () t |> Set.ofSeq let rec resolveErasedTypeImpl typeQueries ctx = function | PolymorphicThis -> PolymorphicThis | Intrinsic -> Intrinsic @@ -975,7 +1021,22 @@ module Type = |> String.replace "-" "minus" |> String.replace "." "_" match l with - | LString s -> formatString s + | LString s -> + match s with + | "\r" -> "cr" | "\n" -> "lf" | "\r\n" -> "crlf" | "\t" -> "tab" + | " " -> "whitespace" + | "/" -> "sol" | "\\" -> "bsol" | "|" -> "vert" + | "'" -> "apos" | "\"" -> "quot" | "`" -> "grave" + | "!" -> "excl" | "?" -> "quest" + | "," -> "comma" | "." -> "period" | ":" -> "colon" | ";" -> "semi" + | "+" -> "plus" | "-" -> "minus" | "*" -> "ast" | "^" -> "hat" + | "$" -> "dollar" | "&" -> "amp" | "%" -> "percnt" | "#" -> "num" | "@" -> "commat" | "_" -> "lowbar" + | "[" -> "lbrack" | "]" -> "rbrack" | "(" -> "lpar" | ")" -> "rpar" | "{" -> "lbrace" | "}" -> "rbrace" + | "<" -> "lt" | ">" -> "gt" | "=" -> "equals" + | _ -> + if System.String.IsNullOrEmpty s then "empty" + else if String.forall ((=) ' ') s then $"whitespace{s.Length}" + else formatString s | LInt i -> formatNumber i | LFloat f -> formatNumber f | LBool true -> "true" | LBool false -> "false" @@ -1010,6 +1071,69 @@ module Type = s1 + s2 | UnknownType _ -> "unknown" + module GetAnonymousInterfaces = + let rec treatFuncType (state: {| origin: AnonymousInterfaceOrigin; namespace_: string list |}) (ft: FuncType) tps = + seq { + for arg in ft.args do + let ty, origin = + match arg with + | Choice1Of2 fl -> fl.value, { state.origin with argName = Some fl.name } + | Choice2Of2 t -> t, state.origin + yield! findTypes typeFinder {| state with origin = origin |} ty + yield! findTypes typeFinder state ft.returnType + yield! treatTypeParameters state tps + } + and treatTypeParameters (state: {| origin: AnonymousInterfaceOrigin; namespace_: string list |}) (tps: TypeParam list) = + seq { + for tp in tps do + yield! tp.extends |> Option.map (findTypes typeFinder state) |? Seq.empty + yield! tp.defaultType |> Option.map (findTypes typeFinder state) |? Seq.empty + } + and treatNamed (state: {| origin: AnonymousInterfaceOrigin; namespace_: string list |}) name value = + findTypes typeFinder {| state with origin = { state.origin with valueName = Some name } |} value + and typeFinder (state: {| origin: AnonymousInterfaceOrigin; namespace_: string list |}) ty = + let inline resultMany xs = Some [], state, xs + match ty with + | App (AAnonymousInterface i, _, _) | AnonymousInterface i -> + let inner = + let state = {| state with origin = AnonymousInterfaceOrigin.Empty |} + treatClassLike state (i.MapName(ignore)) + None, {| state with origin = AnonymousInterfaceOrigin.Empty |}, Seq.append [i, state] inner + | Func (ft, tps, _) | NewableFunc (ft, tps, _) -> + treatFuncType state ft tps |> resultMany + | Union { types = types } | Intersection { types = types } -> + Some types, state, Seq.empty + | _ -> None, {| state with origin = AnonymousInterfaceOrigin.Empty |}, Seq.empty + and treatClassLike (state: {| origin: AnonymousInterfaceOrigin; namespace_: string list |}) (c: Class) = + seq { + for _, m in c.members do + match m with + | Method (name, ft, tps) -> + yield! treatFuncType {| state with origin = { state.origin with valueName = Some name } |} ft tps + | Newable (ft, tps) | Callable (ft, tps) -> yield! treatFuncType state ft tps + | Field (fl, _) | Getter fl | Setter fl -> yield! treatNamed state fl.name fl.value + | Indexer (ft, _) -> yield! treatFuncType state ft [] + | SymbolIndexer (name, ft, _) -> + yield! treatFuncType {| state with origin = { state.origin with valueName = Some name } |} ft [] + | Constructor ft -> + for arg in ft.args do + let ty, origin = + match arg with + | Choice1Of2 fl -> fl.value, { state.origin with argName = Some fl.name } + | Choice2Of2 t -> t, state.origin + yield! findTypes typeFinder {| state with origin = origin |} ty + | UnknownMember _ -> () + for t in c.implements do + yield! findTypes typeFinder state t + yield! treatTypeParameters state c.typeParams + } + let getAnonymousInterfaces ty = + let state = {| + origin = AnonymousInterfaceOrigin.Empty + namespace_ = [] + |} + findTypes GetAnonymousInterfaces.typeFinder state ty + module Statement = open Type @@ -1084,62 +1208,6 @@ module Statement = () stmts |> Set.ofSeq let getAnonymousInterfaces stmts : Set = - let rec treatFuncType (state: {| origin: AnonymousInterfaceOrigin; namespace_: string list |}) (ft: FuncType) tps = - seq { - for arg in ft.args do - let ty, origin = - match arg with - | Choice1Of2 fl -> fl.value, { state.origin with argName = Some fl.name } - | Choice2Of2 t -> t, state.origin - yield! findTypes typeFinder {| state with origin = origin |} ty - yield! findTypes typeFinder state ft.returnType - yield! treatTypeParameters state tps - } - and treatTypeParameters (state: {| origin: AnonymousInterfaceOrigin; namespace_: string list |}) (tps: TypeParam list) = - seq { - for tp in tps do - yield! tp.extends |> Option.map (findTypes typeFinder state) |? Seq.empty - yield! tp.defaultType |> Option.map (findTypes typeFinder state) |? Seq.empty - } - and treatNamed (state: {| origin: AnonymousInterfaceOrigin; namespace_: string list |}) name value = - findTypes typeFinder {| state with origin = { state.origin with valueName = Some name } |} value - and typeFinder (state: {| origin: AnonymousInterfaceOrigin; namespace_: string list |}) ty = - let inline resultMany xs = Some [], state, xs - match ty with - | App (AAnonymousInterface i, _, _) | AnonymousInterface i -> - let inner = - let state = {| state with origin = AnonymousInterfaceOrigin.Empty |} - treatClassLike state (i.MapName(ignore)) - None, {| state with origin = AnonymousInterfaceOrigin.Empty |}, Seq.append [i, state] inner - | Func (ft, tps, _) | NewableFunc (ft, tps, _) -> - treatFuncType state ft tps |> resultMany - | Union { types = types } | Intersection { types = types } -> - Some types, state, Seq.empty - | _ -> None, {| state with origin = AnonymousInterfaceOrigin.Empty |}, Seq.empty - and treatClassLike (state: {| origin: AnonymousInterfaceOrigin; namespace_: string list |}) (c: Class) = - seq { - for _, m in c.members do - match m with - | Method (name, ft, tps) -> - yield! treatFuncType {| state with origin = { state.origin with valueName = Some name } |} ft tps - | Newable (ft, tps) | Callable (ft, tps) -> yield! treatFuncType state ft tps - | Field (fl, _) | Getter fl | Setter fl -> yield! treatNamed state fl.name fl.value - | Indexer (ft, _) -> yield! treatFuncType state ft [] - | SymbolIndexer (name, ft, _) -> - yield! treatFuncType {| state with origin = { state.origin with valueName = Some name } |} ft [] - | Constructor ft -> - for arg in ft.args do - let ty, origin = - match arg with - | Choice1Of2 fl -> fl.value, { state.origin with argName = Some fl.name } - | Choice2Of2 t -> t, state.origin - yield! findTypes typeFinder {| state with origin = origin |} ty - | UnknownMember _ -> () - for t in c.implements do - yield! findTypes typeFinder state t - yield! treatTypeParameters state c.typeParams - } - findStatements (fun currentNamespace state stmt -> let inline result_ x = Some [], state, x let state = {| origin = state; namespace_ = currentNamespace |} @@ -1147,18 +1215,18 @@ module Statement = | TypeAlias ta -> let state = {| state with origin = { state.origin with typeName = Some ta.name } |} seq { - yield! findTypes typeFinder state ta.target - yield! treatTypeParameters state ta.typeParams + yield! findTypes GetAnonymousInterfaces.typeFinder state ta.target + yield! GetAnonymousInterfaces.treatTypeParameters state ta.typeParams } |> result_ | Variable v -> - treatNamed state v.name v.typ |> result_ + GetAnonymousInterfaces.treatNamed state v.name v.typ |> result_ | Function f -> - treatFuncType {| state with origin = { state.origin with valueName = Some f.name } |} f.typ f.typeParams |> result_ + GetAnonymousInterfaces.treatFuncType {| state with origin = { state.origin with valueName = Some f.name } |} f.typ f.typeParams |> result_ | Class c -> let typeName = match c.name with Name n -> Some n | _ -> None let state = {| state with namespace_ = currentNamespace; origin = { state.origin with typeName = typeName } |} - treatClassLike state (c.MapName(ignore)) |> result_ + GetAnonymousInterfaces.treatClassLike state (c.MapName(ignore)) |> result_ | _ -> None, state.origin, Seq.empty ) AnonymousInterfaceOrigin.Empty stmts |> Set.ofSeq @@ -1175,19 +1243,7 @@ module Statement = ) () stmts |> Seq.fold (fun state (k, v) -> Trie.addOrUpdate k v Set.union state) Trie.empty let getKnownTypes (ctx: TyperContext<_, _>) stmts = - let (|Dummy|) _ = [] - findTypesInStatements (fun state -> function - | Ident { fullName = fns } -> - None, state, List.map KnownType.Ident fns - | AnonymousInterface a -> - let info = - ctx |> TyperContext.bindCurrentSourceInfo (fun info -> info.anonymousInterfacesMap |> Map.tryFind a) - None, state, - match info with - | None -> [] - | Some info -> [KnownType.AnonymousInterface (a, info)] - | _ -> None, state, [] - ) () stmts |> Set.ofSeq + findTypesInStatements (knownTypeFinder ctx) () stmts |> Set.ofSeq let rec mapTypeWith overrideFunc mapping ctxOfChildNamespace ctxOfRoot ctx stmts = let mapVariable (v: Variable) = { v with typ = mapping ctx v.typ } @@ -1336,10 +1392,20 @@ type ResolvedUnion = { caseUndefined: bool typeofableTypes: Set caseArray: Set option - caseEnum: Set> + caseEnum: Set> discriminatedUnions: Map> otherTypes: Set -} +} with + member this.satisfies(?hasNull, ?hasUndefined, ?hasTypeofable, ?hasArray, ?hasEnum, ?hasDU, ?hasOther) = + let check opt value = + opt |> Option.map (fun x -> x = value) |? true + check hasNull this.caseNull + && check hasUndefined this.caseUndefined + && check hasTypeofable (this.typeofableTypes |> Set.isEmpty |> not) + && check hasArray (this.caseArray |> Option.map (Set.isEmpty >> not) |? false) + && check hasEnum (this.caseEnum |> Set.isEmpty |> not) + && check hasDU (this.discriminatedUnions |> Map.isEmpty |> not) + && check hasOther (this.otherTypes |> Set.isEmpty |> not) module ResolvedUnion = let rec pp (ru: ResolvedUnion) = @@ -1359,8 +1425,8 @@ module ResolvedUnion = ru.caseEnum |> Set.toSeq |> Seq.map (function - | Choice1Of2 ({ name = ty }, { name = name; value = Some value }) -> sprintf "%s.%s=%s" ty name (Literal.toString value) - | Choice1Of2 ({ name = ty }, { name = name; value = None }) -> sprintf "%s.%s=?" ty name + | Choice1Of2 ({ name = ty }, { name = name; value = Some value }, _) -> sprintf "%s.%s=%s" ty name (Literal.toString value) + | Choice1Of2 ({ name = ty }, { name = name; value = None }, _) -> sprintf "%s.%s=?" ty name | Choice2Of2 l -> Literal.toString l) yield sprintf "enum<%s>" (cases |> String.concat " | ") for k, m in ru.discriminatedUnions |> Map.toSeq do @@ -1369,6 +1435,26 @@ module ResolvedUnion = ] cases |> String.concat " | " + let expand ctx (u: UnionType) : UnionType = + let (|Dummy|) _ = [] + let rec go (t: Type) = + match t with + | Union { types = types } -> + let types = types |> List.collect (fun ty -> go ty |? [ty]) + if types |> List.exists (function AnonymousInterface _ -> true | _ -> false) then None + else Some types + | (Ident ({ loc = loc } & i) & Dummy tyargs) + | App (AIdent i, tyargs, loc) -> + let finder = function + | Definition.TypeAlias a -> + let bindings = Type.createBindings i.name loc a.typeParams tyargs + go (a.target |> Type.substTypeVar bindings ()) + | _ -> None + i |> Ident.getDefinitions ctx + |> List.tryPick (finder) + | _ -> None + { u with types = u.types |> List.collect (fun ty -> go ty |? [ty]) |> List.distinct } + let checkNullOrUndefined (u: UnionType) : {| hasNull: bool; hasUndefined: bool; rest: Type list |} = let u = Type.normalizeUnion u let nullOrUndefined, rest = @@ -1377,7 +1463,7 @@ module ResolvedUnion = let hasUndefined = nullOrUndefined |> List.contains (Prim Undefined) {| hasNull = hasNull; hasUndefined = hasUndefined; rest = rest |} - let rec private getEnumFromUnion ctx (u: UnionType) : Set> * UnionType = + let rec private getEnumFromUnion ctx (u: UnionType) : Set> * UnionType = let (|Dummy|) _ = [] let rec go t = @@ -1395,9 +1481,9 @@ module ResolvedUnion = let bindings = Type.createBindings i.name loc a.typeParams tyargs go (a.target |> Type.substTypeVar bindings ()) | Definition.Enum e -> - e.cases |> Seq.map (fun c -> Choice1Of2 (Choice1Of2 (e, c))) + e.cases |> Seq.map (fun c -> Choice1Of2 (Choice1Of2 (e, c, t))) | Definition.EnumCase (c, e) -> - Seq.singleton (Choice1Of2 (Choice1Of2 (e, c))) + Seq.singleton (Choice1Of2 (Choice1Of2 (e, c, t))) | _ -> Seq.empty let result = i |> Ident.getDefinitions ctx @@ -1652,6 +1738,25 @@ let inferEnumCaseValue (stmts: Statement list) : Statement list = | s -> s stmts |> List.map go +let removeExtendsInTyprm = + let remove _ (tp: TypeParam) = { tp with extends = None } + let rec goStmt ctx = function + | Class c -> Type.mapTypeParamInClass remove ctx c |> Class |> Some + | TypeAlias a -> + TypeAlias { + a with + target = a.target |> Type.mapTypeParam remove ctx + typeParams = a.typeParams |> List.map (remove ctx) + } |> Some + | Function f -> + Function { + f with + typ = f.typ |> Type.mapInFuncType (Type.mapTypeParam remove) ctx + typeParams = f.typeParams |> List.map (remove ctx) + } |> Some + | _ -> None + Statement.mapTypeWith goStmt (Type.mapTypeParam remove) (fun _ -> id) id () + let rec mergeStatements (stmts: Statement list) = let mutable result : Choice list = [] @@ -1839,73 +1944,71 @@ type private MemberType = | Method of string * int | Callable of int | Newable of int | Indexer of int | Constructor of int let addParentMembersToClass (ctx: TyperContext<#TyperOptions, _>) (stmts: Statement list) : Statement list = - if not ctx.options.addAllParentMembersToClass then stmts - else - let m = new MutableMap() - let processing = new MutableSet() - let rec addMembers (c: Class) = - match m.TryGetValue(c.loc) with - | true, c -> c - | false, _ when processing.Contains(c.loc) -> c - | false, _ -> - processing.Add(c.loc) |> ignore - // we remove any parent type which is a super type of some other parent type - let implements = - c.implements - |> List.filter (fun t -> c.implements |> List.forall (fun t' -> Type.isSuperClass ctx t t' |> not)) - let getMemberType m = - match m with - | Field (fl, _) | Getter fl -> MemberType.Getter (fl.name |> String.normalize) |> Some - | Setter fl -> MemberType.Setter (fl.name |> String.normalize) |> Some - | Method (name, ft, _) -> MemberType.Method (name |> String.normalize, ft.args.Length) |> Some - | Callable (ft, _) -> MemberType.Callable (ft.args.Length) |> Some - | Newable (ft, _) -> MemberType.Newable (ft.args.Length) |> Some - | Indexer (ft, _) -> MemberType.Indexer (ft.args.Length) |> Some - | Constructor ft -> MemberType.Constructor (ft.args.Length) |> Some - | SymbolIndexer _ | UnknownMember _ -> None - // if a parent member has the same arity as the member in a child, - // we should only keep the one from the child. - let memberTypes : Set = - c.members |> List.choose (snd >> getMemberType) |> Set.ofList - let parentMembers : (MemberAttribute * Member) list = - let (|Dummy|) _ = [] - let rec collector : _ -> _ list = function - | (Ident ({ loc = loc } & i) & Dummy ts) | App (AIdent i, ts, loc) -> - let collect = function - | Definition.TypeAlias a -> - if List.isEmpty ts then collector a.target - else - let bindings = Type.createBindings i.name loc a.typeParams ts - collector a.target |> List.map (Type.mapInMember (Type.substTypeVar bindings) ()) - // we ignore `implements` clauses i.e. interfaces inherited by a class. - | Definition.Class c' when c.isInterface || not c'.isInterface -> - if List.isEmpty ts then (addMembers c').members - else - let members = (addMembers c').members - let bindings = Type.createBindings i.name loc c'.typeParams ts - members |> List.map (Type.mapInMember (Type.substTypeVar bindings) ()) - | _ -> [] - Ident.collectDefinition ctx i collect |> List.distinct - | Intersection i -> i.types |> List.collect collector |> List.distinct - | _ -> [] - implements - |> List.collect collector - |> List.filter (fun (_, m) -> - match getMemberType m with - | None -> false - | Some mt -> memberTypes |> Set.contains mt |> not) - |> List.distinct - let c = { c with members = c.members @ parentMembers } - m[c.loc] <- c - c - let rec go stmts = - stmts |> List.map (function - | Class c when c.isInterface -> Class (addMembers c) - | Namespace m -> Namespace { m with statements = go m.statements } - | AmbientModule m -> AmbientModule { m with statements = go m.statements } - | Global m -> Global { m with statements = go m.statements } - | x -> x) - go stmts + let m = new MutableMap() + let processing = new MutableSet() + let rec addMembers (c: Class) = + match m.TryGetValue(c.loc) with + | true, c -> c + | false, _ when processing.Contains(c.loc) -> c + | false, _ -> + processing.Add(c.loc) |> ignore + // we remove any parent type which is a super type of some other parent type + let implements = + c.implements + |> List.filter (fun t -> c.implements |> List.forall (fun t' -> Type.isSuperClass ctx t t' |> not)) + let getMemberType m = + match m with + | Field (fl, _) | Getter fl -> MemberType.Getter (fl.name |> String.normalize) |> Some + | Setter fl -> MemberType.Setter (fl.name |> String.normalize) |> Some + | Method (name, ft, _) -> MemberType.Method (name |> String.normalize, ft.args.Length) |> Some + | Callable (ft, _) -> MemberType.Callable (ft.args.Length) |> Some + | Newable (ft, _) -> MemberType.Newable (ft.args.Length) |> Some + | Indexer (ft, _) -> MemberType.Indexer (ft.args.Length) |> Some + | Constructor ft -> MemberType.Constructor (ft.args.Length) |> Some + | SymbolIndexer _ | UnknownMember _ -> None + // if a parent member has the same arity as the member in a child, + // we should only keep the one from the child. + let memberTypes : Set = + c.members |> List.choose (snd >> getMemberType) |> Set.ofList + let parentMembers : (MemberAttribute * Member) list = + let (|Dummy|) _ = [] + let rec collector : _ -> _ list = function + | (Ident ({ loc = loc } & i) & Dummy ts) | App (AIdent i, ts, loc) -> + let collect = function + | Definition.TypeAlias a -> + if List.isEmpty ts then collector a.target + else + let bindings = Type.createBindings i.name loc a.typeParams ts + collector a.target |> List.map (Type.mapInMember (Type.substTypeVar bindings) ()) + // we ignore `implements` clauses i.e. interfaces inherited by a class. + | Definition.Class c' when c.isInterface || not c'.isInterface -> + if List.isEmpty ts then (addMembers c').members + else + let members = (addMembers c').members + let bindings = Type.createBindings i.name loc c'.typeParams ts + members |> List.map (Type.mapInMember (Type.substTypeVar bindings) ()) + | _ -> [] + Ident.collectDefinition ctx i collect |> List.distinct + | Intersection i -> i.types |> List.collect collector |> List.distinct + | _ -> [] + implements + |> List.collect collector + |> List.filter (fun (_, m) -> + match getMemberType m with + | None -> false + | Some mt -> memberTypes |> Set.contains mt |> not) + |> List.distinct + let c = { c with members = c.members @ parentMembers } + m[c.loc] <- c + c + let rec go stmts = + stmts |> List.map (function + | Class c when c.isInterface -> Class (addMembers c) + | Namespace m -> Namespace { m with statements = go m.statements } + | AmbientModule m -> AmbientModule { m with statements = go m.statements } + | Global m -> Global { m with statements = go m.statements } + | x -> x) + go stmts let introduceAdditionalInheritance (ctx: IContext<#TyperOptions>) (stmts: Statement list) : Statement list = let opts = ctx.options @@ -2069,10 +2172,7 @@ let replaceAliasToFunction (ctx: #IContext<#TyperOptions>) stmts = } | _ -> TypeAlias ta | x -> x - if ctx.options.replaceAliasToFunction then - List.map go stmts - else - stmts + List.map go stmts let replaceFunctions (ctx: #IContext<#TyperOptions>) (stmts: Statement list) = let rec goType (ctx: #IContext<#TyperOptions>) = function @@ -2089,10 +2189,7 @@ let replaceFunctions (ctx: #IContext<#TyperOptions>) (stmts: Statement list) = | NewableFunc (f, typrms, loc) -> let f = Type.mapInFuncType goType ctx f let typrms = typrms |> List.map (Type.mapInTypeParam goType ctx) - if ctx.options.replaceRankNFunction || ctx.options.replaceNewableFunction then - Type.createFunctionInterface [{| ty = f; typrms = typrms; loc = loc; isNewable = true; comments = [] |}] - else - NewableFunc (f, typrms, loc) + Type.createFunctionInterface [{| ty = f; typrms = typrms; loc = loc; isNewable = true; comments = [] |}] | TypeVar v -> TypeVar v | Union u -> Union (u |> Type.mapInUnion goType ctx) | Intersection i -> Intersection (i |> Type.mapInIntersection goType ctx) @@ -2336,7 +2433,16 @@ let runAll (srcs: SourceFile list) (baseCtx: IContext<#TyperOptions>) = let inline withSourceFileContext ctx f (src: SourceFile) = f (ctx |> TyperContext.ofSourceFileRoot src.fileName) src - let result = srcs |> List.map (mapStatements (inferEnumCaseValue >> mergeStatements)) + let inline onFlag b f = if b then f else id + + let result = + srcs |> List.map ( + mapStatements ( + inferEnumCaseValue + >> onFlag baseCtx.options.noExtendsInTyprm removeExtendsInTyprm + >> mergeStatements + ) + ) // build a context let ctx = createRootContextForTyper result baseCtx @@ -2347,21 +2453,22 @@ let runAll (srcs: SourceFile list) (baseCtx: IContext<#TyperOptions>) = src |> mapStatements (fun stmts -> stmts // add members inherited from parent classes/interfaces to interfaces - |> addParentMembersToClass ctx + |> onFlag ctx.options.addAllParentMembersToClass (addParentMembersToClass ctx) |> Statement.resolveErasedTypes ctx // add common inheritances which tends not to be defined by `extends` or `implements` - |> introduceAdditionalInheritance ctx + |> onFlag (ctx.options.inheritArraylike || ctx.options.inheritIterable || ctx.options.inheritPromiselike) + (introduceAdditionalInheritance ctx) // add default constructors to class if not explicitly defined |> addDefaultConstructorToClass ctx // group statements with pattern |> detectPatterns // replace alias to function type with a function interface - |> replaceAliasToFunction ctx + |> onFlag ctx.options.replaceAliasToFunction (replaceAliasToFunction ctx) // replace N-rank and/or newable function type with an interface - |> replaceFunctions ctx + |> onFlag (ctx.options.replaceRankNFunction || ctx.options.replaceNewableFunction) (replaceFunctions ctx) ))) // rebuild the context because resolveErasedTypes may introduce additional anonymous interfaces let ctx = createRootContext result ctx - ctx, result \ No newline at end of file + ctx, result diff --git a/package.json b/package.json index bebd997e..b5ed4acb 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,13 @@ }, "homepage": "https://github.com/ocsigen/ts2ocaml", "scripts": { - "ts2ocaml": "node ./dist/ts2ocaml.js" + "ts2ocaml": "node ./dist/js/ts2ocaml.js" }, "files": [ - "dist/" + "dist/js/" ], - "main": "./dist/ts2ocaml.js", - "bin": "./dist/ts2ocaml.js", + "main": "./dist/js/ts2ocaml.js", + "bin": "./dist/js/ts2ocaml.js", "dependencies": { "@babel/code-frame": "^7.18.6", "browser-or-node": "^2.0.0", @@ -43,6 +43,7 @@ "cdk8s": "^2.2.41", "monaco-editor": "0.45.0", "react-player": "2.14.0", + "rescript": "11.0.1", "ts2fable": "0.8.0-build.723", "webpack": "5.90.0", "webpack-cli": "5.1.0", diff --git a/src/Common.fs b/src/Common.fs index 886a2c06..8a0ee606 100644 --- a/src/Common.fs +++ b/src/Common.fs @@ -82,4 +82,4 @@ module GlobalOptions = .addFlag("nowarn", (fun (o: GlobalOptions) -> o.nowarn), descr="Do not show warnings") type IContext<'Options when 'Options :> IOptions> = Ts2Ml.Common.IContext<'Options> -type OverloadRenamer = Ts2Ml.Common.OverloadRenamer \ No newline at end of file +type OverloadRenamer = Ts2Ml.Common.OverloadRenamer diff --git a/src/Main.fs b/src/Main.fs index 2bcacbfb..72f9ee58 100644 --- a/src/Main.fs +++ b/src/Main.fs @@ -24,6 +24,7 @@ let main argv = .config() |> GlobalOptions.register |> Target.register parse Targets.JsOfOCaml.Target.target + |> Target.register parse Targets.ReScript.Target.target |> Target.register parse Targets.ParserTest.target yargs.demandCommand(1.0).scriptName("ts2ocaml").help().argv |> ignore 0 diff --git a/src/Targets/JsOfOCaml/Writer.fs b/src/Targets/JsOfOCaml/Writer.fs index 600e2635..ddd76fa6 100644 --- a/src/Targets/JsOfOCaml/Writer.fs +++ b/src/Targets/JsOfOCaml/Writer.fs @@ -161,11 +161,11 @@ module OverrideFunc = | Some text -> Some text | None -> f1 _flags _emitType _ctx ty -let emitEnum (flags: EmitTypeFlags) ctx (cases: Set>) = +let emitEnum (flags: EmitTypeFlags) ctx (cases: Set>) = let forceSkipAttr text = if flags.forceSkipAttributes then empty else text let usedValues = cases - |> Seq.choose (function Choice1Of2 (_, { value = v }) -> v | _ -> None) + |> Seq.choose (function Choice1Of2 (_, { value = v }, _) -> v | _ -> None) |> Set.ofSeq let cases = cases @@ -173,7 +173,7 @@ let emitEnum (flags: EmitTypeFlags) ctx (cases: Set Set.filter (function Choice2Of2 l when usedValues |> Set.contains l -> false | _ -> true) // Convert to identifiers while merging duplicate enum cases |> Set.map (function - | Choice1Of2 (e, c) -> enumCaseToIdentifier e c |> str, c.value + | Choice1Of2 (e, c, _) -> enumCaseToIdentifier e c |> str, c.value | Choice2Of2 l -> "L_" @+ literalToIdentifier ctx l, Some l) between "[" "]" (concat (str " | ") [ for name, value in Set.toSeq cases do @@ -1575,7 +1575,7 @@ module ModuleEmitter = let nonRec _ctx modules = moduleSigNonRec modules let recAll _ctx modules = moduleSigRec modules let recOptimized dt (ctx: Context) = - let scc = dt |> Trie.tryFind ctx.currentNamespace |? [] + let scc = dt |> Trie.tryFind ctx.currentNamespace |> Option.map (fun x -> x.scc) |? [] let sccSet = scc |> List.concat |> Set.ofList fun (modules: TextModuleSig list) -> let modulesMap = modules |> List.fold (fun state x -> state |> Map.add x.origName x) Map.empty diff --git a/src/Targets/ReScript/Common.fs b/src/Targets/ReScript/Common.fs new file mode 100644 index 00000000..7ea7ea39 --- /dev/null +++ b/src/Targets/ReScript/Common.fs @@ -0,0 +1,258 @@ +module Targets.ReScript.Common + +open Fable.Core +open Ts2Ml +open DataTypes + +[] +type Simplify = + | [] All + | [] ImmediateInstance + | [] ImmediateConstructor + | [] AnonymousInterfaceValue + | [] NamedInterfaceValue + | [] Off +with + static member Values = [|All; ImmediateInstance; ImmediateConstructor; AnonymousInterfaceValue; NamedInterfaceValue; Off|] + + static member Has (flags: Simplify list, target: Simplify) = + if flags |> List.contains All then true + else flags |> List.contains target + + static member TryParse (s: string) = + match s with + | "all" -> Some All + | "immediate-instance" -> Some ImmediateInstance + | "immediate-constructor" -> Some ImmediateConstructor + | "anonymous-interface-value" -> Some AnonymousInterfaceValue + | "named-interface-value" -> Some NamedInterfaceValue + | _ -> None + +[] +type Subtyping = + | [] Tag + | [] CastFunction + | [] Off + | [] Default +with + static member Values = [|Tag; CastFunction; Off; Default|] + + static member TryParse (s: string) = + match s with + | "tag" -> Some Tag + | "cast-function" -> Some CastFunction + | _ -> None + +[] +type Preset = + | [] Minimal + | [] Safe + | [] Full +with + static member Values = [|Minimal; Safe; Full|] + +[] +type ModuleKind = + | [] None + | [] ES + | [] CJS + | [] Default +with + static member Values = [|None; ES; CJS; Default|] + +type Options = + inherit GlobalOptions + inherit Typer.TyperOptions + // general options + abstract preset: Preset option with get + abstract createStdlib: bool with get + // output options + abstract outputDir: string option with get + abstract resi: bool with get + // JS options + abstract ``module``: ModuleKind with get + abstract name: string option with get + // typer options + abstract numberAsInt: bool with get, set + abstract subtyping: Subtyping list with get, set + abstract inheritWithTags: FeatureFlag with get, set + // code generator options + abstract simplify: Simplify list with get, set + abstract readableNames: bool with get, set + abstract noTypesModule: bool with get, set + // experimental options + abstract experimentalTaggedUnion: bool with get, set + +module Options = + open Fable.Core.JsInterop + + let validate : Yargs.MiddlewareFunction = + Yargs.MiddlewareFunction(fun opts yargs -> + if isNullOrUndefined opts.subtyping then opts.subtyping <- [] + if isNullOrUndefined opts.simplify then opts.simplify <- [] + + match opts.preset with + | None -> () + | Some p -> + Log.tracef opts "* using the preset '%s'." !!p + + let subtypingIsDefault = + opts.subtyping = [] + + if p = Preset.Minimal || p = Preset.Safe || p = Preset.Full then + if opts.simplify = [] then + opts.simplify <- [Simplify.All] + + if p = Preset.Safe || p = Preset.Full then + if subtypingIsDefault then + opts.subtyping <- Subtyping.CastFunction :: opts.subtyping + + if p = Preset.Full then + if subtypingIsDefault then + opts.subtyping <- Subtyping.Tag :: opts.subtyping + if opts.inheritWithTags = FeatureFlag.Default then + opts.inheritWithTags <- FeatureFlag.Full + + if opts.subtyping |> List.contains Subtyping.Tag |> not + && opts.inheritWithTags <> FeatureFlag.Off + && opts.inheritWithTags <> FeatureFlag.Default then + eprintfn "error: --inherit-with-tags=%s requires --subtyping=tag." !!opts.inheritWithTags + yargs.exit(-1, new System.ArgumentException("--inherit-with-tags requires --subtyping=tag.")) + + !^opts) + + let register (yargs: Yargs.Argv) = + yargs + .group( + !^ResizeArray[ + "create-stdlib"; "preset" + ], + "General Options:" + ) + .addFlag( + "create-stdlib", + (fun (o:Options) -> o.createStdlib), + descr="Create ts2ocaml.res.", + defaultValue=false + ) + .addChoice( + "preset", + Preset.Values, + (fun (o: Options) -> o.preset), + descr="Specify the preset to use." + ) + .group(!^ResizeArray[], "Parser Options:") + .group( + !^ResizeArray[ + "module"; "name" + ], "JS Module Options:") + .addOption( + "name", + (fun (o: Options) -> o.name), + descr="Override the JS module name used in the @module attribute (default: inferred from package.json).", + alias="n" + ) + .addChoice( + "module", + ModuleKind.Values, + (fun (o: Options) -> o.``module``), + descr="Override the JS module type (default: inferred from the input).", + defaultValue=ModuleKind.Default + ) + .group( + !^ResizeArray[ + "output-dir"; "resi" + ], + "Output Options:" + ) + .addOption( + "output-dir", + (fun (o: Options) -> o.outputDir), + descr="The directory to place the generated bindings.\nIf not set, it will be the current directory.", + alias="o") + .addFlag( + "resi", + (fun (o: Options) -> o.resi), + descr = "Generate interface file (.resi) too. --no-resi to disable.", + defaultValue=true + ) + + .group( + !^ResizeArray[ + "number-as-int"; + "subtyping"; + "inherit-with-tags"; + ], + "Typer Options:") + .addFlag( + "number-as-int", + (fun (o: Options) -> o.numberAsInt), + descr="Treat number types as int.\nIf not set, float will be used.", + defaultValue=false, + alias="int") + .addCommaSeparatedStringSet( + "subtyping", + Subtyping.TryParse, + (fun (o: Options) -> o.subtyping), + descr= + sprintf "Turn on subtyping features. Available features: %s" + (Subtyping.Values |> Array.filter ((<>) Subtyping.Default) |> Array.map string |> String.concat ", ")) + .addChoice( + "inherit-with-tags", + FeatureFlag.Values, + (fun (o: Options) -> o.inheritWithTags), + descr="Require --subtyping=tag. Use `TypeName.tags` type names to inherit types from other packages.", + defaultValue=FeatureFlag.Default) + + .group( + !^ResizeArray[ + "simplify"; + "readable-names"; + "no-types-module" + ], + "Code Generator Options:") + .addCommaSeparatedStringSet( + "simplify", + Simplify.TryParse, + (fun (o: Options) -> o.simplify), + descr= + sprintf "Turn on simplification features. Available features: %s" + (Simplify.Values |> Array.map string |> String.concat ", ")) + .addFlag( + "readable-names", + (fun (o: Options) -> o.readableNames), + descr="Try to use more readable names instead of AnonymousInterfaceN.", + defaultValue = false + ) + .addFlag( + "no-types-module", + (fun (o: Options) -> o.noTypesModule), + descr="Unsafe. Do not emit Types module even if there are recursive modules.", + defaultValue = false + ) + + .group( + !^ResizeArray[ + "experimental-tagged-union" + ], + "Experimental Options:" + ) + .addFlag( + "experimental-tagged-union", + (fun (o: Options) -> o.experimentalTaggedUnion), + descr="Experimental. Emit additional variant type for tagged union.", + defaultValue=false + ) + + .middleware(!^validate) + + +type Output = { + baseName: string + /// the content of `.resi` file + resi: text option + /// the content of `.res` file + res: text +} + +let [] stdlib: string = jsNative diff --git a/src/Targets/ReScript/ReScriptHelper.fs b/src/Targets/ReScript/ReScriptHelper.fs new file mode 100644 index 00000000..fe2ed436 --- /dev/null +++ b/src/Targets/ReScript/ReScriptHelper.fs @@ -0,0 +1,588 @@ +module Targets.ReScript.ReScriptHelper + +open System +open Ts2Ml +open Syntax +open Targets.ReScript.Common +open DataTypes +open DataTypes.Text + +module Source = + open Fable.Core + let [] dom: string = jsNative + +let comment text = + if text = empty then empty + else + let inner = + if isMultiLine text then newline + indent text + newline + else between " " " " text + between "/*" "*/" inner +let commentStr text = tprintf "/* %s */" text + +let docComment text = + if text = empty then empty + else + let inner = + if isMultiLine text then newline + indent text + newline + else between " " " " text + between "/**" "*/" inner + +module Attr = + let as_ value = between "@as(" ")" value + + module External = + /// https://rescript-lang.org/docs/manual/latest/import-from-export-to-js#import-from-javascript + let module_ nameOpt = + match nameOpt with + | Some name -> tprintf "@module(\"%s\")" name + | None -> str "@module" + + /// https://rescript-lang.org/docs/manual/latest/bind-to-global-js-values#global-modules + let val_ = str "@val" + + /// https://rescript-lang.org/docs/manual/latest/bind-to-js-function#object-method + let send = str "@send" + + let scope = function + | [] -> failwith "empty scope" + | [s] -> tprintf "@scope(\"%s\")" s + | ss -> + ss |> List.map (tprintf "\"%s\"") + |> concat (str ", ") |> between "@scope((" "))" + + /// https://rescript-lang.org/docs/manual/latest/bind-to-js-object#bind-to-a-js-object-thats-a-class + let new_ = str "@new" + + /// https://rescript-lang.org/docs/manual/latest/bind-to-js-object#bind-using-special-getter-and-setter-attributes + let get_ = str "@get" + /// https://rescript-lang.org/docs/manual/latest/bind-to-js-object#bind-using-special-getter-and-setter-attributes + let set_ = str "@set" + /// https://rescript-lang.org/docs/manual/latest/bind-to-js-object#bind-using-special-getter-and-setter-attributes + let get_index = str "@get_index" + /// https://rescript-lang.org/docs/manual/latest/bind-to-js-object#bind-using-special-getter-and-setter-attributes + let set_index = str "@set_index" + + /// https://rescript-lang.org/docs/manual/latest/generate-converters-accessors#convert-external-into-js-object-creation-function + let obj = str "@obj" + + module ExternalModifier = + /// https://rescript-lang.org/docs/manual/latest/bind-to-js-function#variadic-function-arguments + let variadic = str "@variadic" + + /// https://rescript-lang.org/docs/manual/latest/bind-to-js-function#constrain-arguments-better + let return_nullable = str "@return(nullable)" + + module Doc = + /// https://rescript-lang.org/docs/manual/latest/attribute#usage + let deprecated = function + | None -> str "@deprecated" + | Some msg -> tprintf "@deprecated(\"%s\")" (String.escape msg) + + let floating msg = + tprintf "@@ocaml.text(\"%s\")" (String.escape msg) + + let doc msg = + tprintf "@ocaml.doc(\"%s\")" (String.escape msg) + + module Arrow = + /// https://rescript-lang.org/docs/manual/latest/bind-to-js-function#extra-solution + let uncurry = str "@uncurry" + /// https://rescript-lang.org/docs/manual/latest/bind-to-js-function#modeling-this-based-callbacks + let this = str "@this" + + module Variant = + /// https://rescript-lang.org/blog/improving-interop#tagged-variants + let tag name = + tprintf "@tag(\"%s\")" (String.escape name) + + /// https://rescript-lang.org/blog/improving-interop#untagged-variants + let unboxed = str "@unboxed" + + module PolyVariant = + /// https://rescript-lang.org/docs/manual/latest/bind-to-js-function#constrain-arguments-better + let int = str "@int" + /// https://rescript-lang.org/docs/manual/latest/bind-to-js-function#constrain-arguments-better + let string = str "@string" + /// https://rescript-lang.org/docs/manual/latest/bind-to-js-function#trick-2-polymorphic-variant--unwrap + let unwrap = str "@unwrap" + + module TypeDef = + /// https://rescript-lang.org/docs/manual/latest/unboxed + let unboxed = str "@unboxed" + +module Naming = + let removeInvalidChars (s: string) = + s.Trim('"').ToCharArray() + |> Array.map (fun c -> if Char.isAlphabetOrDigit c || c = '_' || c = '\'' then c else '_') + |> System.String + + let isValid (s: string) = + Char.isAlphabet(s[0]) + && s.ToCharArray() |> Array.forall(fun c -> Char.isAlphabetOrDigit c || c = '_' || c = '\'') + + let keywords = + set [ + "and"; "as"; "assert"; "constraint"; "else"; "exception"; "external"; "export" + "false"; "for"; "if"; "in"; "include"; "lazy"; "let"; "module"; "mutable" + "of"; "open"; "private"; "rec"; "switch"; "true"; "try"; "type"; "when"; "while"; "with" + ] + + let upperFirst (s: string) = + if Char.IsLower s[0] then + sprintf "%c%s" (Char.ToUpper s[0]) s[1..] + else s + + let lowerFirst (s: string) = + if Char.IsUpper s[0] then + sprintf "%c%s" (Char.ToLower s[0]) s[1..] + else s + + let valueName (name: string) = + let check name = + if keywords |> Set.contains name then + String.escape name |> sprintf "\\\"%s\"" + else name + if name = "NaN" then "nan" + else if not (isValid name) then + String.escape name |> sprintf "\\\"%s\"" + else if String.forall (fun c -> Char.IsUpper c || c = '_' || c = '\'') name then + name.ToLower() |> check + else lowerFirst name |> check + + let reservedModuleNames = + Set.ofList [ + "Export"; "Default"; "Types" + ] |> Set.union keywords + + let moduleNameReserved (name: string) = + let name = removeInvalidChars name + if name.[0] = '_' then + "M" + name + else upperFirst name + + let moduleName (name: string) = + let result = moduleNameReserved name + if reservedModuleNames |> Set.contains result then result + "_" else result + + let constructorName (name: string list) = + let s = String.concat "_" name |> removeInvalidChars |> upperFirst + if s.StartsWith("_") then "C" + s + else if keywords |> Set.contains s then s + "_" + else s + + let structured (baseName: string -> string) (name: string list) = + let rec prettify = function + | [] -> "" + | [x] -> baseName x + | x :: xs -> moduleName x + "." + prettify xs + prettify name + + let createTypeNameOfArity arity maxArityOpt name = + match maxArityOpt with + | Some maxArity -> + if arity = maxArity then name + else sprintf "%s%d" name arity + | None -> sprintf "%s%d" name arity + + let jsModuleNameToReScriptName (jsModuleName: string) = + match jsModuleName.TrimStart('@') |> String.splitThenRemoveEmptyEntries "/" |> Array.toList with + | xs -> + xs + |> List.map (fun n -> + n |> Naming.toCase Naming.Case.LowerSnakeCase) + |> String.concat "__" + + let jsModuleNameToFileName (jsModuleName: string) = + let basename = jsModuleName |> jsModuleNameToReScriptName + {| resi = $"{basename}.resi"; res = $"{basename}.res" |} + + let jsModuleNameToReScriptModuleName (jsModuleName: string) = + jsModuleName + |> jsModuleNameToReScriptName + |> moduleName + +module Kind = + let generatesReScriptModule kind = + Set.intersect kind (Set.ofList [Kind.Type; Kind.ClassLike; Kind.Module]) |> Set.isEmpty |> not + +[] +module Type = + let tsUtilityTypes = [ + "Partial", 1; "Required", 1; "Readonly", 1; + "Record", 2; "Pick", 2; "Omit", 2; "Exclude", 2; "Extract", 2; + "NonNullable", 1; + "Parameters", 1; "ConstructorParameters", 1; "ReturnType", 1; "InstanceType", 1; + "ThisParameterType", 1; "OmitThisParameter", 1; "ThisType", 1; + "Uppercase", 1; "Lowercase", 1; "Capitalize", 1; "Uncapitalize", 1; + ] + + /// non-primitive types defined in the standard library + let predefinedTypes = + let builtins = [ + "RegExp", ("Re.t", 0) + "PromiseLike", ("Promise.t", 1) + "Array", ("array", 1) + "ArrayLike", ("Array2.array_like", 1) + "ReadonlyArray", ("array", 1) + "ArrayBuffer", ("TypedArray2.ArrayBuffer.t", 0) + "Error", ("Exn.t", 0) + ] + let typedArrays = + let typedArray name = name, (sprintf "TypedArray2.%s.t" name, 0) + [ + typedArray "DataView" + typedArray "Int8Array" + typedArray "Uint8Array" + typedArray "Uint8ClampedArray" + typedArray "Int16Array" + typedArray "Uint16Array" + typedArray "Int32Array" + typedArray "Uint32Array" + typedArray "Float32Array" + typedArray "Float64Array" + ] + (* + let utilities = + tsUtilityTypes |> List.map (fun (name, arity) -> + name, (Naming.lowerFirst name, arity) + ) + *) + Map.ofList (builtins @ typedArrays) + + /// non-primitive DOM types defined in the standard library + /// + /// `MutableMap` with ignore-case keys, because `dom.ml` has lowered all acronyms (e.g. HTML -> html) + let predefinedDOMTypes = + let types = + Source.dom + |> String.splitManyThenRemoveEmptyEntries ["\n"; "\r"] + |> Array.filter (fun s -> s.StartsWith("type ")) + |> Array.choose (fun s -> s |> String.replace "type " "" |> String.splitMany [" = "; " /*"] |> Array.tryHead) + |> Array.filter (fun s -> s.Length > 0 && s.ToCharArray() |> Array.forall Char.isAlphabet) + |> Array.map (fun s -> Naming.upperFirst s, "Dom." + s) + let ignoreCase = + { new Collections.Generic.IEqualityComparer with + member __.Equals(s1: string, s2: string) = + s1.Equals(s2, StringComparison.InvariantCultureIgnoreCase) + member __.GetHashCode(s: string) = s.ToLowerInvariant().GetHashCode() } + let m = new MutableMap(ignoreCase) + for k, v in types do m.Add(k, v) + m.Add("Storage", "Dom.Storage.t") + m + + // basic type expressions + let var s = tprintf "'%s" s + + let tuple = function + | [] -> failwith "empty tuple" + | _ :: [] -> failwith "1-ary tuple" + | xs -> concat (str ", ") xs |> between "(" ")" + + /// `(t1, t2) => tr` + let arrow args ret = + let lhs = + match args with + | [] -> str "()" + | xs -> concat (str ", ") xs |> between "(" ")" + lhs +@ " => " + ret + + let app t args = + if List.isEmpty args then failwith "type application with empty arguments" + else t + between "<" ">" (concat (str ", ") args) + + let appOpt t args = + if List.isEmpty args then t + else app t args + + let polyVariantBody (cases: {| name:Choice; value:text option; attr: text option |} list) = + let createCase (case: {| name:Choice; value:text option; attr: text option |}) = + let name = + match case.name with + | Choice1Of2 str -> + if Naming.isValid str && Naming.keywords |> Set.contains str |> not then + str + else sprintf "\"%s\"" (String.escape str) + | Choice2Of2 i -> sprintf "%d" i + let attr = + match case.attr with + | None -> empty + | Some a -> a +@ " " + match case.value with + | None -> attr + tprintf "#%s" name + | Some v -> attr + tprintf "#%s(" name + v +@ ")" + cases |> List.map createCase |> concat (str " | ") + + let polyVariant cases = polyVariantBody cases |> between "[" "]" + + // primitive types + let void_ = str "unit" + let string = str "string" + let boolean = str "bool" + let int = str "int" + let float = str "float" + let number (opt: Options) = + if opt.numberAsInt then int + else float + let array = str "array" + let readonlyArray = str "array" + let option t = app (str "option") [t] + + // JS types + // ES5 + let object = str "untypedObject" + let function_ = str "untypedFunction" + let symbol = str "symbol" + let regexp = str "Re.t" + // ES2020 + let bigint = str "Bigint.t" + + // TS types + let never = str "never" + let any = str "any" + let unknown = str "unknown" + let null_or t = app (str "Null.t") [t] + let undefined_or t = app (str "option") [t] + let null_or_undefined_or t = app (str "Nullable.t") [t] + let null_ = str "Null.t" + let undefined = str "unit" + let intrinsic = str "intrinsic" + let true_ = str "true_" + let false_ = str "false_" + + let record isInline (fields: {| name: string; isOptional: bool; attrs: text list; ty: text |} list) = + let body = + fields + |> List.map (fun f -> + let attrs = f.attrs |> List.map (fun x -> x +@ " ") |> join + let name = tprintf "%s%s: " f.name (if f.isOptional then "?" else "") + attrs + name + f.ty) + |> List.map (fun f -> if isInline then f else indent f) + |> concat (if isInline then str ", " else str ",") + if isInline then "{ " @+ body +@ " }" + else "{" @+ newline + body + newline +@ "}" + + // our types + let intf tags = app (str "intf") [tags] + let prim cases = app (str "prim") [cases] + + let rec union = function + | [] -> failwith "union type with zero elements" + | x :: [] -> x + | x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: x7 :: x8 :: rest -> + app (str "Union.t8") [x1; x2; x3; x4; x5; x6; x7; union (x8 :: rest)] + | xs -> app (tprintf "Union.t%i" (List.length xs)) xs + + let rec intersection = function + | [] -> failwith "intersection type with zero elements" + | x :: [] -> x + | x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: x7 :: x8 :: rest -> + app (str "Intersection.t8") [x1; x2; x3; x4; x5; x6; x7; intersection (x8 :: rest)] + | xs -> app (tprintf "Intersection.t%i" (List.length xs)) xs + + let newable args retTy = + match args with + | [] -> app (str "Newable.t0") [retTy] + | [x1] -> app (str "Newable.t1") [x1; retTy] + | xs -> app (str "Newable.tn") [tuple xs; retTy] + + let variadic args variadic retTy = + match args with + | [] -> app (str "Variadic.t0") [variadic; retTy] + | [x1] -> app (str "Variadic.t1") [x1; variadic; retTy] + | xs -> app (str "Variadic.tn") [tuple xs; variadic; retTy] + + let newableVariadic args variadic retTy = + match args with + | [] -> app (str "NewableVariadic.t0") [variadic; retTy] + | [x1] -> app (str "NewableVariadic.t1") [x1; variadic; retTy] + | xs -> app (str "NewableVariadic.tn") [tuple xs; variadic; retTy] + +[] +module Term = + let tuple = function + | [] -> failwith "empty tuple" + | _ :: [] -> failwith "1-ary tuple" + | xs -> concat (str ", ") xs |> between "(" ")" + + let app t us = t + (us |> concat (str ", ") |> between "(" ")") + + /// `(arg1, arg2) => ret` + let arrow args ret = + let lhs = + match args with + | [] -> str "()" + | xs -> concat (str ", ") xs |> between "(" ")" + lhs +@ " => " + ret + + let literal (l: Literal) = + match l with + | LBool true -> str "true" | LBool false -> str "false" + | LInt i -> string i |> str + | LFloat f -> tprintf "%f" f + | LString s -> tprintf "\"%s\"" (String.escape s) + + let raw js = js |> String.escapeWith ["`"] |> str |> between "%raw(`" "`)" + +type TextModule = {| name: string; origName: string; content: text list; comments: text list |} + +let private moduleSigImplBody head oneliner (m: TextModule) = + if List.isEmpty m.content then [ head +@ "{ }" ] + else if oneliner then + [ head +@ "{ " + (concat (str "; ") m.content) +@ " }"] + else [ + yield head + str "{" + yield indent (concat newline m.content) + yield str "}" + ] + +let private moduleSigImplLines (prefix: string) (isRec: bool) (m: TextModule) = + let oneliner = + m.content |> List.forall (isMultiLine >> not) && (m.content |> List.sumBy Text.length) < 60 + let head = + tprintf "%s %s%s : " + prefix + (if isRec then "rec " else "") + m.name + [ + // FIXME: https://github.com/rescript-lang/rescript-compiler/issues/6598 + if prefix <> "and" then + yield! m.comments + + yield! moduleSigImplBody head oneliner m ] + +let private moduleSigImpl (prefix: string) (isRec: bool) (m: TextModule) = + moduleSigImplLines prefix isRec m |> concat newline + +[] +module Statement = + let attr attrs = + if List.isEmpty attrs then empty + else concat (str " ") attrs +@ " " + + let let_ (attrs: text list) name typ value = + attr attrs + tprintf "let %s: " name + typ +@ " = " + value + + let val_ (attrs: text list) name typ = + attr attrs + tprintf "let %s: " name + typ + + let external (attrs: text list) name (typ: text) target = + let result = + attr attrs + tprintf "external %s: " name + typ + tprintf " = \"%s\"" target + if not (Naming.isValidJSIdentifier target) && + [Attr.External.new_; Attr.External.val_] |> List.exists (fun attr -> attrs |> List.contains attr) then + comment result // ReScript doesn't allow exotic names except for get, set, and send. + else result + + let typeAlias isRec name tyargs tyOpt = + let lhs = + str "type " + + (if isRec then str "rec " else empty) + + (if List.isEmpty tyargs then str name else Type.app (str name) tyargs) + match tyOpt with + | None -> lhs + | Some ty -> lhs +@ " = " + ty + + let include_ name = tprintf "include %s" name + let open_ name = tprintf "open %s" name + + let moduleAlias name target = tprintf "module %s = %s" name target + + let moduleSig (m: TextModule) = moduleSigImpl "module" false m + + let moduleSigRec (ms: TextModule list) = + match ms with + | [] -> [] + | [m] -> [moduleSig m] + | m :: ms -> + let content = moduleSigImpl "module" true m :: (ms |> List.map (moduleSigImpl "and" false)) + // make it one liner if possible + if content |> List.forall (isMultiLine >> not) && (content |> List.sumBy Text.length) < 60 then + [content |> concat (str " ")] + else + [content |> concat newline] + + let moduleSigNonRec (ms: TextModule list) = ms |> List.map moduleSig + + let moduleVal (m: TextModule) : text = + concat newline [ + yield! m.comments + yield tprintf "module %s = {" m.name + yield indent (concat newline m.content) + yield str "}" + ] + + let moduleValMany ms = ms |> List.map moduleVal + + let moduleSigRec1 name (content: text list) = + concat newline [ + yield tprintf "module rec %s : {" name + yield indent (concat newline content) + yield tprintf "} = %s" name + ] + + let moduleSCC (dt: DependencyTrie) emitRec emitNonRec (ctx: Typer.TyperContext<_, _>) = + let scc = dt |> Trie.tryFind ctx.currentNamespace |> Option.map (fun x -> x.scc) |? [] + let sccSet = scc |> List.concat |> Set.ofList + fun (modules: TextModule list) -> + let modulesMap = modules |> List.fold (fun state x -> state |> Map.add x.origName x) Map.empty + let sccModules = + scc + |> List.map (fun group -> + group |> List.choose (fun name -> modulesMap |> Map.tryFind name) |> emitRec) + |> List.concat + let otherModules = + modules + |> List.filter (fun x -> sccSet |> Set.contains x.origName |> not) + |> emitNonRec + sccModules @ otherModules + +type [] Binding = + | Let of {| name: string; ty: text; body: text; attrs: text list; comments: text list |} + | Ext of {| name: string; ty: text; target: string; attrs: text list; comments: text list |} + | Unknown of {| msg:text option; comments: text list |} +with + member this.comments = + match this with Let x -> x.comments | Ext x -> x.comments | Unknown x -> x.comments + +module Binding = + let let_ (attrs: text list) comments name ty body = + Binding.Let {| name = name; ty = ty; body = body; attrs = attrs; comments = comments |} + + let ext (attrs: text list) comments name ty target = + Binding.Ext {| name = name; ty = ty; target = target; attrs = attrs; comments = comments |} + + let unknown comments msg = + Binding.Unknown {| msg = msg; comments = comments |} + + let cast comments name ty = + Binding.Ext {| name = name; ty = ty; target = "%identity"; attrs = []; comments = comments |} + + let builder name (fields: {| isOptional: bool; name: string; value: text |} list) (thisType: text) = + let args = + fields + |> List.distinctBy (fun x -> x.name) + |> List.map (fun f -> + let name = f.name |> Naming.valueName + let suffix = + if f.isOptional then "=?" else "" + tprintf "~%s:" name + f.value +@ suffix) + let ty = + Type.arrow args thisType + Binding.Ext {| name = name; ty = ty; target = ""; attrs = [Attr.External.obj]; comments = []|} + + let emitForImplementation (b: Binding) = [ + yield! b.comments + match b with + | Binding.Let x -> yield Statement.let_ x.attrs x.name x.ty x.body + | Binding.Ext x -> yield Statement.external x.attrs x.name x.ty x.target + | Binding.Unknown x -> match x.msg with Some msg -> yield comment msg | None -> () + ] + + let emitForInterface (b: Binding) = [ + yield! b.comments + match b with + | Binding.Let x -> yield Statement.val_ x.attrs x.name x.ty + | Binding.Ext x -> yield Statement.external x.attrs x.name x.ty x.target + | Binding.Unknown x -> match x.msg with Some msg -> yield comment msg | None -> () + ] diff --git a/src/Targets/ReScript/Target.fs b/src/Targets/ReScript/Target.fs new file mode 100644 index 00000000..3bcf2ac2 --- /dev/null +++ b/src/Targets/ReScript/Target.fs @@ -0,0 +1,62 @@ +module Targets.ReScript.Target + +open Ts2Ml +open Syntax +open DataTypes + +open Target +open Targets.ReScript.Common +open Targets.ReScript.Writer + +open Fable.Core.JsInterop + +let private builder (argv: Yargs.Argv) : Yargs.Argv = + argv |> Options.register + +let private run (input: Input) (ctx: IContext) = + let outputDir = + let curdir = Node.Api.``process``.cwd() + match ctx.options.outputDir with + | None -> curdir + | Some dir -> + let path = + if Node.Api.path.isAbsolute dir then dir + else Node.Api.path.join [|curdir; dir|] + let fail () = + failwithf "The output directory '%s' does not exist." path + try + if Node.Api.fs.lstatSync(!^path).isDirectory() then path + else fail () + with + _ -> fail () + + let results = + let result = + if ctx.options.createStdlib then + [{ baseName = "ts2ocaml"; res = Text.str stdlib; resi = None }] + else [] + if List.isEmpty input.sources then result + else + result @ emit input ctx + + if results = [] then + ctx.logger.warnf "no input files are given." + + for result in results do + let fileName = result.baseName + ".res" + let fullPath = Node.Api.path.join[|outputDir; fileName|] + ctx.logger.tracef "* writing the binding to '%s'..." fullPath + Node.Api.fs.writeFileSync(fullPath, Text.toString 2 result.res) + match result.resi with + | None -> () + | Some resi -> + let fileName = result.baseName + ".resi" + let fullPath = Node.Api.path.join[|outputDir; fileName|] + Node.Api.fs.writeFileSync(fullPath, Text.toString 2 resi) + +let target = + { new ITarget with + member __.Command = "res" + member __.Description = "Generate binding for ReScript" + member __.Builder = builder + member __.Run (srcs, options) = run srcs options } \ No newline at end of file diff --git a/src/Targets/ReScript/Writer.fs b/src/Targets/ReScript/Writer.fs new file mode 100644 index 00000000..8ecfcb64 --- /dev/null +++ b/src/Targets/ReScript/Writer.fs @@ -0,0 +1,2139 @@ +module Targets.ReScript.Writer + +open Ts2Ml +open Syntax +open Typer +open Typer.Type +open DataTypes +open DataTypes.Text + +open Fable.Core +open Fable.Core.JsInterop + +open Targets.ReScript.Common +open Targets.ReScript.ReScriptHelper + +let impossibleNone msgf (x: 'a option) = + match x with None -> failwith ("impossible (not None): " + msgf ()) | Some x -> x + +type ScriptTarget = TypeScript.Ts.ScriptTarget + +type State = {| + fileNames: string list + info: Result + referencesCache: MutableMap> +|} +module State = + let create fileNames info : State = + {| fileNames = fileNames + info = info + referencesCache = new MutableMap<_, _>() |} + +type Context = TyperContext +module Context = TyperContext + +type Label = + | Case of text * text list + | TagType of text * text list + +type [] External = + | Root of variadic:bool * nullable:bool + | Return of nullable:bool + | Argument of variadic:bool + | None + +type EmitTypeFlags = { + resolveUnion: bool + needParen: bool + external: External + avoidTheseArgumentNames: Set +} + +module EmitTypeFlags = + let defaultValue = + { + resolveUnion = true + needParen = false + external = External.None + avoidTheseArgumentNames = Set.empty + } + + let noExternal flags = + { flags with external = External.None } + let ofFuncArg isVariadic flags = + { flags with + external = + match flags.external with + | External.Root _ -> External.Argument isVariadic + | _ -> External.None + } + let ofFuncReturn flags = + { flags with + external = + match flags.external with + | External.Root (_, n) -> External.Return n + | _ -> External.None } + +let classifyExternalFunction flags (f: FuncType) = + let isVariadic = + if not f.isVariadic then false + else if List.isEmpty f.args then false + else + let check = function + | App (APrim (Array | ReadonlyArray), _, _) -> true + | _ -> false + match List.last f.args with + | Choice1Of2 x -> check x.value + | Choice2Of2 t -> check t + let isNullable = + match f.returnType with + | Union u -> + let u = ResolvedUnion.checkNullOrUndefined u + u.hasNull || u.hasUndefined + | _ -> false + let flags = { flags with external = External.Root(isVariadic, isNullable) } + let needsWorkaround = f.isVariadic && not isVariadic + {| flags = flags; isVariadic = isVariadic; isNullable = isNullable; needsWorkaround = needsWorkaround |} + +let functionNeedsWorkaround (ft: FuncType) = + let c = classifyExternalFunction EmitTypeFlags.defaultValue ft + c.needsWorkaround + +type TypeEmitter = Context -> Type -> text + +type OverrideFunc = EmitTypeFlags -> TypeEmitter -> Context -> Type -> text option +module OverrideFunc = + let inline noOverride _flags _emitType _ctx _ty = None + let inline combine (f1: OverrideFunc) (f2: OverrideFunc) : OverrideFunc = + fun _flags _emitType _ctx ty -> + match f2 _flags _emitType _ctx ty with + | Some text -> Some text + | None -> f1 _flags _emitType _ctx ty + +let fixme alternative fmt = + Printf.ksprintf (fun msg -> + commentStr (sprintf "FIXME: %s" msg) + alternative + ) fmt + +let anonymousInterfaceModuleName (ctx: Context) (info: AnonymousInterfaceInfo) = + match info.origin.valueName, info.origin.argName with + | _, Some s | Some s, None when ctx.options.readableNames -> + sprintf "%s%d" (Naming.toCase Naming.PascalCase s) info.id + | _, _ -> + sprintf "AnonymousInterface%d" info.id + +let anonymousInterfaceToIdentifier (ctx: Context) (a: AnonymousInterface) : text = + let i = + ctx + |> Context.bindCurrentSourceInfo (fun i -> i.anonymousInterfacesMap |> Map.tryFind a) + |> impossibleNone (fun () -> sprintf "anonymousInterfaceToIdentifier(%s)" a.loc.AsString) + tprintf "%s.t" (anonymousInterfaceModuleName ctx i) + +let rec emitTypeImpl (flags: EmitTypeFlags) (overrideFunc: OverrideFunc) (ctx: Context) (ty: Type) : text = + let treatBuiltinTypes (i: Ident) (tyargs: Type list) = + let contains path = i.fullName |> List.exists (fun fn -> fn.source.Contains(path)) + if contains "node_modules/typescript/lib/lib" then + let len = List.length tyargs + let flagsForArgs = { flags with needParen = true } |> EmitTypeFlags.noExternal + let emitWith ty = Type.appOpt (str ty) (tyargs |> List.map (emitTypeImpl flagsForArgs overrideFunc ctx)) |> Some + match i.name with + | [] | _ :: _ :: _ -> None + | [name] -> + match Type.predefinedTypes |> Map.tryFind name with + | Some (ty, arity) when arity = len -> emitWith ty + | _ -> + if contains "lib.es" then emitWith (sprintf "%s.t" name) + else if contains "lib.dom" || contains "lib.webworker" then + match Type.predefinedDOMTypes.TryGetValue(name) with + | true, ty -> emitWith ty + | _, _ -> None + else None + else None + + let treatIdent (i: Ident) (tyargs: Type list) (loc: Location) = + match treatBuiltinTypes i tyargs with + | Some t -> t + | None -> + let arity = List.length tyargs + let flagsForArgs = { flags with needParen = true } |> EmitTypeFlags.noExternal + let withTyargs ty = + Type.appOpt ty (tyargs |> List.map (emitTypeImpl flagsForArgs overrideFunc ctx)) + let origin = + Ident.pickDefinitionWithFullName ctx i (fun fn -> function + | _ when fn.source <> ctx.currentSourceFile -> None + | Definition.Class { typeParams = tps; loc = loc } + | Definition.TypeAlias { typeParams = tps; loc = loc } -> Some (fn, tps, loc) + | Definition.Enum { loc = loc } + | Definition.EnumCase ({ loc = loc }, _) -> Some (fn, [], loc) + | _ -> None + ) + match origin with + | None -> + let tyName = + let fallback () = + let tyName = + if Option.isSome i.misc.maxArity then + Naming.createTypeNameOfArity arity i.misc.maxArity "t" + else "t" + Naming.structured Naming.moduleName i.name + "." + tyName |> str + match i.name with + | [name] -> + match PrimType.FromJSClassName name with + | Some p -> emitTypeImpl flags overrideFunc ctx (Prim p) + | None -> fallback () + | _ -> fallback () + tyName |> withTyargs + | Some (fn, typrms, origLoc) -> + let result name = + let ts = + assignTypeParams fn.name (origLoc ++ loc) typrms tyargs + (fun _ t -> t) + (fun tv -> + match tv.defaultType with + | Some t -> t + | None -> ctx.logger.errorf "error: insufficient type params for type '%s' at %s" (String.concat "." fn.name) loc.AsString) + Type.appOpt (str name) (ts |> List.map (emitTypeImpl flagsForArgs overrideFunc ctx)) + let fullName = Naming.structured Naming.moduleName fn.name + ".t" + if fn.source <> ctx.currentSourceFile then result fullName + else + match ctx |> Context.getRelativeNameTo fn.name with + | Ok relativeName -> result (Naming.structured Naming.moduleName relativeName + ".t") + | Error [] -> result "t" + | Error diff -> + let fn = String.concat "." fn.name + let selfName = String.concat "." diff + let warnText = $"cannot reference a type {fn} from its sub-namespace {selfName}" + Log.warnf ctx.options "%s at %s" warnText loc.AsString + commentStr warnText + Type.any + + match overrideFunc flags (emitTypeImpl flags overrideFunc) ctx ty with + | Some t -> t + | None -> + match ty with + | App (APrim Array, ts, _) when flags.external = External.Argument true -> + Type.app Type.array (List.map (emitTypeImpl { flags with needParen = true; external = External.None } overrideFunc ctx) ts) + | App (APrim ReadonlyArray, ts, _) when flags.external = External.Argument true -> + Type.app Type.readonlyArray (List.map (emitTypeImpl { flags with needParen = true; external = External.None } overrideFunc ctx) ts) + | _ when flags.external = External.Argument true -> + commentStr (sprintf "FIXME: type '%s' cannot be used for variadic argument" (Type.pp ty)) + Type.app Type.array [Type.any] + | App (t, ts, loc) -> + let flags = flags |> EmitTypeFlags.noExternal + let emit t ts = + Type.appOpt (emitTypeImpl flags overrideFunc ctx t) (List.map (emitTypeImpl { flags with needParen = true } overrideFunc ctx) ts) + match t with + | AIdent i -> treatIdent i ts loc + | APrim _ | AAnonymousInterface _ -> emit (Type.ofAppLeftHandSide t) ts + | Ident i -> treatIdent i [] i.loc + | TypeVar v -> tprintf "'%s" v + | Prim p -> + match p with + | Null -> Type.null_ | Undefined -> Type.undefined + | String -> Type.string | Bool -> Type.boolean + | Number -> Type.number ctx.options + | Object -> Type.object | UntypedFunction -> Type.function_ + | RegExp -> Type.regexp | Symbol _ -> Type.symbol + | Never -> Type.never | Any -> Type.any | Unknown -> Type.unknown | Void -> Type.void_ + | Array -> Type.array | ReadonlyArray -> Type.readonlyArray + | BigInt -> Type.bigint + | TypeLiteral l -> + match l with + | LBool true -> Type.true_ | LBool false -> Type.false_ + | LString s -> Type.polyVariant [{| name = Choice1Of2 s; value = None; attr = None |}] + | LInt i -> + if i >= 0 then Type.polyVariant [{| name = Choice2Of2 i; value = None; attr = None |}] + else fixme (str "int") "%d" i + | LFloat f -> fixme (str "float") "float literal %f" f + | Intersection i -> + let flags = { flags with needParen = true } |> EmitTypeFlags.noExternal + Type.intersection (i.types |> List.distinct |> List.map (emitTypeImpl flags overrideFunc ctx)) + | Union u -> emitUnion flags overrideFunc ctx u + | AnonymousInterface a -> anonymousInterfaceToIdentifier ctx a + | PolymorphicThis -> fixme Type.any "polymorphic 'this' appeared out of context" + | Intrinsic -> Type.intrinsic + | Tuple ts -> + match ts.types with + | [] -> Type.void_ + | [t] -> emitTypeImpl flags overrideFunc ctx t.value + | ts -> Type.tuple (ts |> List.map (fun x -> emitTypeImpl (flags |> EmitTypeFlags.noExternal) overrideFunc ctx x.value)) + | Func (f, [], _) -> emitFuncType flags overrideFunc ctx false f + | NewableFunc (f, [], _) -> emitFuncType flags overrideFunc ctx true f + | Erased (_, loc, origText) -> impossible "emitTypeImpl_erased: %s (%s)" loc.AsString origText + | Func (_, _ :: _, loc) -> impossible "emitTypeImpl_Func_poly: %s (%s)" loc.AsString (Type.pp ty) + | NewableFunc (_, _, loc) -> impossible "emitTypeImpl_NewableFunc_poly: %s (%s)" loc.AsString (Type.pp ty) + | UnknownType msgo -> + match msgo with + | None -> fixme Type.any "unknown type" + | Some msg -> fixme Type.any "unknown type '%s'" msg + +and emitFuncType (flags: EmitTypeFlags) (overrideFunc: OverrideFunc) (ctx: Context) isNewable (f: FuncType) = + let retTy flags = + let argNames = + f.args |> List.choose (function Choice1Of2 x -> Some x.name | Choice2Of2 _ -> None) |> Set.ofList + let flags = { flags with needParen = true; avoidTheseArgumentNames = argNames } |> EmitTypeFlags.ofFuncReturn + emitTypeImpl flags overrideFunc ctx f.returnType + let paren x = + if flags.needParen then between "(" ")" x + else x + let variadicFallback () = + assert f.isVariadic + let retTy = retTy (EmitTypeFlags.noExternal flags) + let args = + let flags = { flags with needParen = true } |> EmitTypeFlags.noExternal + f.args |> List.map (function + | Choice1Of2 x -> + let t = emitTypeImpl flags overrideFunc ctx x.value + if x.isOptional then Type.undefined_or t else t + | Choice2Of2 t -> emitTypeImpl flags overrideFunc ctx t) + let args, variadic = + match List.rev args with + | v :: rest -> List.rev rest, v + | [] -> impossible "emitFuncType_empty_variadic_function" + if isNewable then Type.newableVariadic args variadic retTy |> paren + else Type.variadic args variadic retTy |> paren + let newableFallback () = + let retTy = retTy (EmitTypeFlags.noExternal flags) + let args = + let flags = { flags with needParen = true } |> EmitTypeFlags.noExternal + f.args |> List.map (function + | Choice1Of2 x -> + let t = emitTypeImpl flags overrideFunc ctx x.value + if x.isOptional then Type.undefined_or t else t + | Choice2Of2 t -> emitTypeImpl flags overrideFunc ctx t) + Type.newable args retTy + let args () = + let rec go acc (args: Choice list) = + let flags = { flags with needParen = true } |> EmitTypeFlags.ofFuncArg false + match args with + | [] -> acc + | Choice1Of2 x :: [] when acc = [] && not x.isOptional -> + go acc [Choice2Of2 x.value] + | Choice1Of2 x :: [] when f.isVariadic -> + assert (not x.isOptional) + let t = emitTypeImpl { flags with external = External.Argument true } overrideFunc ctx x.value + (tprintf "~%s:" (Naming.valueName x.name) + t) :: acc + | Choice2Of2 t :: [] -> + let flags = + if f.isVariadic then { flags with external = External.Argument true } else flags + emitTypeImpl flags overrideFunc ctx t :: acc + | Choice1Of2 x :: rest -> + let arg = + let tmp = tprintf "~%s:" (Naming.valueName x.name) + emitTypeImpl flags overrideFunc ctx x.value + if x.isOptional then tmp +@ "=?" else tmp + go (arg :: acc) rest + | Choice2Of2 t :: rest -> + let t = emitTypeImpl flags overrideFunc ctx t + go (t :: acc) rest + go [] f.args |> List.rev + match flags.external with + | _ when isNewable -> + if f.isVariadic then variadicFallback () else newableFallback () + | External.Root (true, _) -> Type.arrow (args ()) (retTy flags) + | _ when f.isVariadic -> variadicFallback () + | External.Root (_, _) + | External.Argument _ + | External.Return _ -> Type.arrow (args ()) (retTy flags) + | _ -> Type.arrow (args ()) (retTy flags) |> paren + +and emitUnion (flags: EmitTypeFlags) (overrideFunc: OverrideFunc) (ctx: Context) (u: UnionType) : text = + if flags.resolveUnion = false then + u.types + |> List.distinct + |> List.map (emitTypeImpl (EmitTypeFlags.noExternal flags) overrideFunc ctx) + |> Type.union + else if flags.external = External.Return true then + let u = ResolvedUnion.checkNullOrUndefined u + let rest = + if List.isEmpty u.rest then Type.never + else + let t = Union { types = u.rest } + emitTypeImpl (EmitTypeFlags.noExternal flags) overrideFunc ctx t + match u.hasNull, u.hasUndefined with + | true, _ | _, true -> Type.option rest + | false, false -> rest + else + let u = ResolvedUnion.resolve ctx u + + let treatEnum (cases: Set>) = + let handleLiteral l attr ty = + match l with + | LString s -> Choice1Of2 {| name = Choice1Of2 s; value = None; attr = attr |} + | LInt i -> Choice1Of2 {| name = Choice2Of2 i; value = None; attr = attr |} + | LFloat _ -> Choice2Of2 (ty |? Type.float) + | LBool _ -> Choice2Of2 (ty |? Type.boolean) + let cases = + List.distinct [ + for c in cases do + match c with + | Choice1Of2 (_, _, ty) -> + let ty = emitTypeImpl (EmitTypeFlags.noExternal flags) overrideFunc ctx ty + yield Choice2Of2 ty + | Choice2Of2 l -> yield handleLiteral l None None + ] + let cases, rest = List.splitChoice2 cases + [ + if List.isEmpty cases |> not then + yield Type.polyVariant cases + yield! rest + ] + + let treatArray (ts: Set) = + // TODO: think how to map multiple array cases properly + let elemT = + let elemT = + match Set.toList ts with + | [t] -> t + | ts -> Union { types = ts } + emitTypeImpl (EmitTypeFlags.noExternal flags) overrideFunc ctx elemT + Type.app Type.array [elemT] + + let treatDUMany du = + // TODO: anonymous DU? + let types = + du + |> Map.toList + |> List.collect (fun (_, cases) -> Map.toList cases) + |> List.map (fun (_, t) -> t) + types + |> List.map (emitTypeImpl (EmitTypeFlags.noExternal { flags with resolveUnion = false }) overrideFunc ctx) + |> List.distinct + + let baseTypes = [ + if not (Set.isEmpty u.caseEnum) then + yield! treatEnum u.caseEnum + if not (Map.isEmpty u.discriminatedUnions) then + yield! treatDUMany u.discriminatedUnions + match u.caseArray with + | Some ts -> yield treatArray ts + | None -> () + for t in u.otherTypes do + yield emitTypeImpl (EmitTypeFlags.noExternal { flags with resolveUnion = false }) overrideFunc ctx t + ] + + let case name value = {| name = Choice1Of2 name; value = value; attr = None |} + let genPoly unwrap = + let cases = [ + for t in u.typeofableTypes do + match t with + | Typeofable.String -> yield case "String" (Some Type.string) + | Typeofable.Number -> yield case "Number" (Some (Type.number ctx.options)) + | Typeofable.Boolean -> yield case "Boolean" (Some Type.boolean) + | Typeofable.Symbol -> yield case "Symbol" (Some Type.symbol) + | Typeofable.BigInt -> yield case "Bigint" (Some Type.bigint) + + if u.caseNull then + yield case "Null" (if unwrap then Some Type.null_ else None) + if u.caseUndefined then + yield case "Undefined" (if unwrap then Some Type.undefined else None) + + match List.distinct baseTypes with + | [] -> () + | [t] -> yield case "Other" (Some t) + | ts -> + if unwrap then + for i, t in ts |> List.indexed do + yield case (sprintf "U%d" (i+1)) (Some t) + else + yield case "Other" (Some (Type.union ts)) + ] + Type.polyVariant cases + + let createNullable isNull isUndefined t = + match isNull, isUndefined with + | false, false -> t + | true, false -> Type.null_or t + | false, true -> Type.undefined_or t + | true, true -> Type.null_or_undefined_or t + + let emitTypeofableType t = emitTypeImpl flags overrideFunc ctx (TypeofableType.toType t) + + let isExternalArg = match flags.external with External.Argument _ -> true | _ -> false + + match baseTypes, Set.toList u.typeofableTypes, u.caseNull, u.caseUndefined with + | [], [], false, false -> impossible "emitUnion_empty_union" + | [], [], true, false -> Type.null_ + | [], [], false, true -> Type.undefined + | [], [], true, true -> Type.null_or_undefined_or Type.never + | [t], [], isNull, isUndefined -> createNullable isNull isUndefined t + | ts, [], isNull, isUndefined when not isExternalArg -> + createNullable isNull isUndefined (Type.union ts) + | [], [t], isNull, isUndefined -> createNullable isNull isUndefined (emitTypeofableType t) + | _, _, _, _ -> + match flags.external with + | External.Argument _ -> Attr.PolyVariant.unwrap +@ " " + genPoly true + | _ -> Type.app (str "Primitive.t") [genPoly false] + +/// `[ #A | #B | ... ]` +and emitLabels (ctx: Context) labels = + emitLabelsBody ctx labels |> between "[" "]" + +/// `#A | #B | ...` +and emitLabelsBody (ctx: Context) labels = + let inline tag t = + if ctx.options.inheritWithTags.HasConsume then t + else empty + let rec go firstCaseEmitted acc = function + | [] -> acc + | Case (c, args) :: rest -> + let text = + match args with + | [] -> "#" @+ c + | _ -> "#" @+ c + between "(" ")" (concat (str ", ") args) + if firstCaseEmitted then + go firstCaseEmitted (acc + str " | " + text) rest + else + go true (acc + text) rest + | TagType (t, args) :: rest -> + let text = Type.appOpt t args + if firstCaseEmitted then + go firstCaseEmitted (acc + tag (" | " @+ text)) rest + else + go ctx.options.inheritWithTags.HasConsume (acc + tag text) rest + go false empty labels + +and getLabelsFromInheritingTypes (flags: EmitTypeFlags) (overrideFunc: OverrideFunc) (ctx: Context) (inheritingTypes: Set) = + let emitType_ = emitTypeImpl flags overrideFunc + let createCase name args = Case (str (Naming.constructorName name), args) + let createTagType name args maxArity = + let arity = List.length args + let tagTypeName = + if Option.isSome maxArity then + Naming.createTypeNameOfArity arity maxArity "tags" + else "tags" + let ty = Naming.structured Naming.moduleName name + "." + tagTypeName + let args = args |> List.map (emitType_ ctx) + TagType (str ty, args) + [ + for e in inheritingTypes do + match e with + | InheritingType.KnownIdent i -> + yield createCase i.fullName.name (i.tyargs |> List.map (emitType_ ctx)) + | InheritingType.UnknownIdent i -> + yield createTagType i.name i.tyargs i.maxArity + | InheritingType.Prim (p, ts) -> + match p.AsJSClassName with + | Some name -> + yield createCase [name] (ts |> List.map (emitType_ ctx)) + | None -> () + | InheritingType.Other _ -> () + ] + +/// `Choice2Of2` when it is an alias to a non-JSable prim type. +and getLabelsOfFullName flags overrideFunc (ctx: Context) (fullName: FullName) (typeParams: TypeParam list) = + getAllInheritancesAndSelfFromName ctx fullName |> getLabelsFromInheritingTypes flags overrideFunc ctx |> List.sort + +and getLabelOfFullName flags overrideFunc (ctx: Context) (fullName: FullName) (typeParams: TypeParam list) = + let inheritingType = InheritingType.KnownIdent {| fullName = fullName; tyargs = typeParams |> List.map (fun tp -> TypeVar tp.name) |} + getLabelsFromInheritingTypes flags overrideFunc ctx (Set.singleton inheritingType) |> Choice1Of2 + +type StructuredTextItemBase<'TypeDefText, 'Binding, 'EnumCaseText> = + /// Will always be emitted at the top of the module. + | ImportText of text + /// Will always be emitted at the next top of the module. + | TypeDefText of 'TypeDefText + | TypeAliasText of text + /// Will be emitted in `.res` and `.resi`, but not in the `Types` module + | Comment of text + /// Will only be emitted in `.res` (not in `.resi` or in the `Types` module) + | Binding of 'Binding + | EnumCaseText of 'EnumCaseText + +and StructuredTextItem = StructuredTextItemBase< + TypeDefText, + (OverloadRenamer -> CurrentScope -> Binding), + {| name: string; comments: Comment list |} +> + +and TypeDefText = { + name: string + tyargs: (TypeParam * text) list + body: text option + isRec: bool + shouldAssert: bool + attrs: text list + comments: text list +} with + static member Create(name, tyargs, body, ?attrs, ?comments, ?isRec, ?shouldAssert) = + TypeDefText { + name = name; tyargs = tyargs; body = body + attrs = attrs |? [] + comments = comments |? [] + isRec = isRec |? false + shouldAssert = shouldAssert |? false + } + +and CurrentScope = { + jsModule: string option + /// reversed list of scope + scopeRev: string list +} + +and [] Scope = + | Default + | Module of string + | Path of string + | Global + | Ignore + +and [] ExportItem = + | Export of {| comments: Comment list; clauses: (ExportClause * Set) list; loc: Location; origText: string |} + | ReExport of {| comments: Comment list; clauses: (ReExportClause * Set) list; loc: Location; specifier: string; origText: string |} + | DefaultUnnamedClass of StructuredTextNode + +and StructuredTextNode = {| + scope: Scope + items: StructuredTextItem list + comments: text list + exports: ExportItem list + openTypesModule: bool + knownTypes: Set + anonymousInterfaces: Set +|} + +and StructuredText = Trie + +module StructuredTextNode = + let empty : StructuredTextNode = + {| scope = Scope.Default; items = []; comments = []; exports = []; knownTypes = Set.empty; anonymousInterfaces = Set.empty; openTypesModule = true |} + + let union (a: StructuredTextNode) (b: StructuredTextNode) : StructuredTextNode = + let mergeScope s1 s2 = + match s1, s2 with + | Scope.Default, s | s, Scope.Default -> s + | _, _ -> impossible "mergeScope(%A, %A)" s1 s2 + {| scope = mergeScope a.scope b.scope + items = List.append a.items b.items + comments = List.append a.comments b.comments + exports = List.append a.exports b.exports + openTypesModule = a.openTypesModule || b.openTypesModule + knownTypes = Set.union a.knownTypes b.knownTypes + anonymousInterfaces = Set.union a.anonymousInterfaces b.anonymousInterfaces |} + + let getReferences (ctx: Context) (v: StructuredTextNode) : WeakTrie = + v.knownTypes + |> Set.fold (fun state -> function + | KnownType.Ident fn when fn.source = ctx.currentSourceFile -> state |> WeakTrie.add fn.name + | KnownType.AnonymousInterface (_, i) -> + state |> WeakTrie.add (i.namespace_ @ [anonymousInterfaceModuleName ctx i]) + | _ -> state + ) WeakTrie.empty + +module StructuredText = + let pp (x: StructuredText) = + let rec go (x: StructuredText) = + concat newline [ + for k, v in x.children |> Map.toArray do + tprintf "- %s" k + indent (go v) + ] + go x + +let removeLabels (xs: Choice list) = + xs |> List.map (function Choice2Of2 t -> Choice2Of2 t | Choice1Of2 fl -> Choice2Of2 fl.value) + +let emitComments (floating: bool) (comments: Comment list) : text list = + if List.isEmpty comments then [] + else + let escape = + String.replace "/*" "/ *" + >> String.replace "*/" "* /" + let emit (c: Comment) = + match c with + | Description lines + | Summary lines -> lines |> List.map escape |> strLines + | c -> c.ToJsDoc() |> escape |> str + let body = comments |> List.map emit |> concat newline + if floating then [comment body] else [docComment body] + +let inline binding (f: (string -> string) -> CurrentScope -> Binding) : StructuredTextItem list = + [Binding (fun renamer scope -> f (renamer.Rename "value") scope)] + +let scopeToAttr (s: CurrentScope) attr = + match s.scopeRev, s.jsModule with + | [], None -> attr + | [], Some m -> Attr.External.module_ (Some m) :: attr + | sr, None -> Attr.External.scope (List.rev sr) :: attr + | sr, Some m -> + Attr.External.module_ (Some m) :: Attr.External.scope (List.rev sr) :: attr + +let tryBindToCurrentScope (s: CurrentScope) attr = + match s.scopeRev, s.jsModule with + | [], None -> None + | [], Some m -> Some {| self = m; attr = Attr.External.module_ None :: attr |} + | s :: [], None -> Some {| self = s; attr = attr |} + | s :: [], Some m -> Some {| self = s; attr = Attr.External.module_ (Some m) :: attr |} + | s :: sr, None -> Some {| self = s; attr = Attr.External.scope (List.rev sr) :: attr |} + | s :: sr, Some m -> Some {| self = s; attr = Attr.External.module_ (Some m) :: Attr.External.scope (List.rev sr) :: attr |} + +let func flags overrideFunc ctx (ft: FuncType) = + Func (ft, [], ft.loc) |> emitTypeImpl flags overrideFunc ctx + +let newableFunc flags overrideFunc ctx (ft: FuncType) = + NewableFunc (ft, [], ft.loc) |> emitTypeImpl flags overrideFunc ctx + +let extFunc flags overrideFunc ctx (ft: FuncType) = + let c = classifyExternalFunction flags ft + let ty = func c.flags overrideFunc ctx ft + let attr = + if c.needsWorkaround then None + else Some [ + if c.isNullable then yield Attr.ExternalModifier.return_nullable + if c.isVariadic then yield Attr.ExternalModifier.variadic + ] + ty, attr + +let extValue flags overrideFunc ctx (t: Type) = + let isNullable = + match t with + | Union u -> + let u = ResolvedUnion.checkNullOrUndefined u + u.hasNull || u.hasUndefined + | _ -> false + let flags = { flags with external = External.Return isNullable } + let attr = + if isNullable then [Attr.ExternalModifier.return_nullable] + else [] + let ty = emitTypeImpl flags overrideFunc ctx t + ty, attr + +let rec emitMembers flags overrideFunc ctx (selfTy: Type) (isExportDefaultClass: bool) (ma: MemberAttribute) m = + let emitType_ = emitTypeImpl flags overrideFunc + + let comments = emitComments false ma.comments + + let inline extFunc ft = extFunc flags overrideFunc ctx ft + let inline extValue t = extValue flags overrideFunc ctx t + let inline func ft = func flags overrideFunc ctx ft + let inline newableFunc ft = newableFunc flags overrideFunc ctx ft + + let createRawCall memberName isVariadic isNewable (args: Choice list) = + let used = + args |> List.choose (function Choice1Of2 f -> Some f.name | Choice2Of2 _ -> None) + |> Set.ofList + let rec rename s = + if used |> Set.contains s |> not then s + else rename (s + "_") + let self = rename "t" + let args = + let rec go index acc = function + | [] -> List.rev acc + | Choice2Of2 _ :: rest -> + let name = sprintf "arg%d" index |> rename + go (index+1) ({| ml = str name; js = name |} :: acc) rest + | Choice1Of2 { name = name; isOptional = isOptional' } :: rest -> + let ml = if isOptional' then sprintf "~%s=?" name else "~" + name + let js = name |> String.replace "'" "$p" + go (index+1) ({| ml = str ml; js = js |} :: acc) rest + go 1 [] args + let body = + let args = + let args = + args |> List.map (fun arg -> arg.js) + if not isVariadic then String.concat ", " args + else + match List.rev args with + | [] -> impossible "emitMembers_createValue" + | last :: [] -> $"...{last}" + | last :: rest -> sprintf "%s, ...%s" (rest |> List.rev |> String.concat ", ") last + let body = + match memberName with + | Some m -> + if Naming.isValidJSIdentifier m then sprintf "%s.%s(%s)" self m args + else sprintf "%s[\"%s\"](%s)" self m args + | None -> sprintf "%s(%s)" self args + if isNewable then "new " + body else body + let args = str self :: (args |> List.map (fun arg -> arg.ml)) + Term.arrow args (Term.raw body) + + let scopeToAttrIf isStatic s attrs = + if isStatic then scopeToAttr s attrs else attrs + + match m with + | Constructor ft -> + let ty, attrs = + let ft = { args = ft.args; isVariadic = ft.isVariadic; returnType = selfTy; loc = ft.loc } + match extFunc ft with + | ty, Some attrs -> ty, Attr.External.new_ :: attrs + | _, None -> + newableFunc { args = ft.args; isVariadic = ft.isVariadic; returnType = selfTy; loc = ft.loc }, + Attr.External.val_ :: [] + binding (fun rename s -> + let target, attrs = + if isExportDefaultClass || List.isEmpty s.scopeRev then + match s.jsModule with + | Some m -> m, Attr.External.module_ None :: Attr.External.new_ :: attrs + | None -> impossible "emitMembers_Constructor_ExportDefaultClass(%s)" ma.loc.AsString + else + match tryBindToCurrentScope s attrs with + | None -> impossible "emitMembers_Constructor(%s)" ma.loc.AsString + | Some x -> x.self, x.attr + let attrs = attrs |> List.rev + Binding.ext attrs comments (rename "make") ty target + ) + | Newable (ft, _typrm) -> + let ty = func { ft with args = Choice2Of2 PolymorphicThis :: ft.args; isVariadic = false } + let value = createRawCall None ft.isVariadic true ft.args + binding (fun rename _ -> Binding.let_ [] comments (rename "make") ty value) + | Callable (ft, _typrm) -> + let ty = func { ft with args = Choice2Of2 PolymorphicThis :: ft.args; isVariadic = false } + let value = createRawCall None ft.isVariadic false ft.args + binding (fun rename _ -> Binding.let_ [] comments (rename "apply") ty value) + | Field ({ name = name; value = Func (ft, _typrm, _); isOptional = false }, _) + | Method (name, ft, _typrm) -> + let origName = name + let ext ty attrs = + binding (fun rename s -> Binding.ext (scopeToAttrIf ma.isStatic s attrs) comments (rename name |> Naming.valueName) ty origName) + if ma.isStatic then + match extFunc ft with + | ty, Some attr -> ext ty (Attr.External.val_ :: attr) + | ty, None -> ext ty (Attr.External.val_ :: []) + else + let ft = { ft with args = Choice2Of2 PolymorphicThis :: ft.args } + match extFunc ft with + | ty, Some attr -> ext ty (Attr.External.send :: attr) + | _, None -> + let ty = func { ft with args = Choice2Of2 PolymorphicThis :: ft.args; isVariadic = false } + let value = createRawCall (Some name) ft.isVariadic false ft.args + binding (fun rename _ -> Binding.let_ [] comments (rename name |> Naming.valueName) ty value) + | Getter fl | Field (fl, ReadOnly) -> + let origName = fl.name + let name = + match m with + | Getter _ -> "get_" + fl.name + | _ -> fl.name + let ty = Member.getActualTypeOfFieldLike fl + if ma.isStatic then + let ty, attrs = + let ty, attrs = extValue ty + ty, Attr.External.val_ :: attrs + binding (fun rename s -> Binding.ext (scopeToAttr s attrs) comments (rename name |> Naming.valueName) ty origName) + else + let ty, attrs = + let args = [Choice2Of2 PolymorphicThis] + let ty, attrs = extFunc { isVariadic = false; args = args; returnType = ty; loc = ma.loc } + ty, Attr.External.get_ :: impossibleNone (fun () -> "emitMembers_Getter") attrs + binding (fun rename _ -> Binding.ext attrs comments (rename name |> Naming.valueName) ty origName) + | Setter fl | Field (fl, WriteOnly) -> + let origName = fl.name + if ma.isStatic then + ctx.logger.warnf "writable global value or static setter '%s' is not supported in ReScript at %s" fl.name ma.loc.AsString + [] + else + let name = + match m with + | Setter _ -> "set_" + fl.name + | _ -> fl.name + let ty, attrs = + let ty = Member.getActualTypeOfFieldLike fl + let args = + if ma.isStatic then [Choice2Of2 ty] + else [Choice2Of2 PolymorphicThis; Choice2Of2 ty] + let ty, attrs = + extFunc { isVariadic = false; args = args; returnType = Prim Void; loc = ma.loc } + ty, Attr.External.set_ :: impossibleNone (fun () -> "emitMembers_Setter") attrs + binding (fun rename s -> Binding.ext (scopeToAttrIf ma.isStatic s attrs) comments (rename name |> Naming.valueName) ty origName) + | Field (fl, Mutable) -> + List.concat [ + emitMembers flags overrideFunc ctx selfTy isExportDefaultClass ma (Getter fl) + emitMembers flags overrideFunc ctx selfTy isExportDefaultClass ma (Setter fl) + ] + | Indexer (ft, ReadOnly) -> + let ty, attrs = + let args = Choice2Of2 PolymorphicThis :: removeLabels ft.args + extFunc { ft with args = args; isVariadic = false } + let attrs = Attr.External.get_index :: impossibleNone (fun () -> "emitMembers_Indexer_Read") attrs + binding (fun rename _ -> Binding.ext attrs comments (rename "get") ty "") + | Indexer (ft, WriteOnly) -> + let ty, attrs = + let args = Choice2Of2 PolymorphicThis :: removeLabels ft.args @ [Choice2Of2 ft.returnType] + let ret = Prim Void + extFunc { ft with args = args; returnType = ret; isVariadic = false } + let attrs = Attr.External.set_index :: impossibleNone (fun () -> "emitMembers_Indexer_Write") attrs + binding (fun rename _ -> Binding.ext attrs comments (rename "set") ty "") + | Indexer (ft, Mutable) -> + List.concat [ + emitMembers flags overrideFunc ctx selfTy isExportDefaultClass ma (Indexer (ft, ReadOnly)) + emitMembers flags overrideFunc ctx selfTy isExportDefaultClass ma (Indexer (ft, WriteOnly)) + ] + | SymbolIndexer (symbol, ft, _) -> + let comments = emitComments true ma.comments + let c = + let ft = func ft + tprintf "external [Symbol.%s]: " symbol + ft + tprintf " = \"[Symbol.%s]\"" symbol + binding (fun _ _ -> Binding.unknown comments (Some c)) + | UnknownMember msgo -> + let comments = emitComments true ma.comments + binding (fun _ _ -> Binding.unknown comments (msgo |> Option.map str)) + +let emitTypeAliasesImpl + (baseName: string) + flags overrideFunc + (ctx: Context) + loc + (typrms: TypeParam list) + (target: text option) + (lines: {| name: string; tyargs:(TypeParam * text) list; target: text option; isOverload: bool |} -> 'a list) = + let emitType_ = emitTypeImpl flags overrideFunc + let tyargs = typrms |> List.map (fun x -> tprintf "'%s" x.name) + [ + yield! lines {| name = baseName; tyargs = List.zip typrms tyargs; target = target; isOverload = false |} + let arities = getPossibleArity typrms + let maxArity = List.length tyargs + for arity in arities |> Set.toSeq |> Seq.sortDescending do + if arity <> maxArity then + let name = Naming.createTypeNameOfArity arity None baseName + let tyargs' = List.take arity tyargs + let typrms' = List.take arity typrms + + let bindings = + createBindings (ctx.currentNamespace @ [name]) loc + (typrms |> List.skip arity) + (typrms |> List.skip arity |> List.map (fun t -> + match t.defaultType with + | None -> impossible "emitTypeAliases" + | Some t -> t + )) + + let target = + Type.appOpt + (str baseName) + [ + for tyarg in tyargs' do yield tyarg + for t in typrms |> List.skip arity do + let t' = repeatUntilEquilibrium (substTypeVar bindings ctx) (TypeVar t.name) + yield emitType_ ctx t' + ] + + yield! lines {| name = name; tyargs = List.zip typrms' tyargs'; target = Some target; isOverload = true |} + ] + +let getTrie name current = + current |> Trie.getSubTrie name |> Option.defaultValue Trie.empty +let setTrie name trie current = + current |> Trie.setSubTrie name trie +let inTrie name f current = + let m = + current + |> Trie.getSubTrie name + |> Option.defaultValue Trie.empty + |> f + current |> Trie.setSubTrie name m +let set node current = current |> Trie.setOrUpdate node StructuredTextNode.union +let add name node current = current |> Trie.addOrUpdate name node StructuredTextNode.union + +let getExportFromStatement (ctx: Context) (name: string) (kind: Kind list) (kindString: string) (s: Statement) : ExportItem option = + let fn = ctx |> Context.getFullName [name] + let ident = { name = [name]; fullName = [fn]; kind = Some (Set.ofList kind); parent = None; loc = s.loc; misc = IdentMiscData.Internal } + match s.isExported.AsExport ident with + | None -> None + | Some clause -> + let prefix = + match clause with + | ES6DefaultExport _ -> "export default" + | _ -> "export" + Some (ExportItem.Export {| comments = []; clauses = [clause, Set.ofList kind]; loc = s.loc; origText = sprintf "%s %s %s" prefix kindString name |}) + +let addExportFromStatement ctx name kind kindString s current = + match getExportFromStatement ctx name kind kindString s with + | None -> current + | Some e -> current |> set {| StructuredTextNode.empty with exports = [e] |} + +type [] ClassKind<'a, 'b, 'c> = + | NormalClass of 'a + | ExportDefaultClass of 'b + | AnonymousInterface of 'c + +let rec emitClass flags overrideFunc (ctx: Context) (current: StructuredText) (c: ClassOrAnonymousInterface) (additionalMembers: Context -> EmitTypeFlags -> OverrideFunc -> list, additionalKnownTypes: Set, forceScope: Scope option) = + let emitType orf ctx ty = emitTypeImpl flags orf ctx ty + + let typrms = List.map (fun (tp: TypeParam) -> TypeVar tp.name) c.typeParams + let kind, selfTy, overrideFunc = + match c.name with + | Choice1Of2 (Name n) -> + let k = ctx |> Context.getFullName [n] + let ident = { name = [n]; fullName = [k]; kind = Some (Set.ofList Kind.OfClass); parent = None; loc = UnknownLocation; misc = IdentMiscData.Internal } + let selfTy = + if List.isEmpty c.typeParams then Ident ident + else App (AIdent ident, typrms, UnknownLocation) + ClassKind.NormalClass {| name = n; orig = c.MapName(fun _ -> Name n) |}, + selfTy, + overrideFunc + | Choice1Of2 ExportDefaultUnnamedClass -> + ClassKind.ExportDefaultClass {| orig = c.MapName(fun _ -> ExportDefaultUnnamedClass) |}, + PolymorphicThis, + overrideFunc + | Choice2Of2 Anonymous -> + let ai = c.MapName (fun _ -> Anonymous) + match ctx |> Context.bindCurrentSourceInfo (fun info -> info.anonymousInterfacesMap |> Map.tryFind ai) with + | None -> impossible "emitClass_unknown_anonymousInterface" + | Some i -> + let selfTy = + if List.isEmpty c.typeParams then AnonymousInterface ai + else App (AAnonymousInterface ai, typrms, UnknownLocation) + let orf _flags _emitType _ctx = function + | AnonymousInterface a when a = ai -> Some (str "t") + | App (AAnonymousInterface a, ts, _) when a = ai -> + Some (Type.appOpt (str "t") (ts |> List.map (_emitType _ctx))) + | _ -> None + ClassKind.AnonymousInterface {| + name = anonymousInterfaceModuleName ctx i + orig = c.MapName(fun _ -> Anonymous) + |}, + selfTy, + OverrideFunc.combine overrideFunc orf + + let isAnonymous, isExportDefaultClass = + match kind with + | ClassKind.AnonymousInterface _ -> true, false + | ClassKind.ExportDefaultClass _ -> false, true + | ClassKind.NormalClass _ -> false, false + + let node = + let ctx, innerCtx = + (), + ctx + |> (match kind with + | ClassKind.NormalClass x -> Context.ofChildNamespace x.name + | ClassKind.AnonymousInterface x -> Context.ofChildNamespace x.name + | ClassKind.ExportDefaultClass _ -> id) + let typrms = List.map (fun (tp: TypeParam) -> tprintf "'%s" tp.name) c.typeParams + let selfTyText = Type.appOpt (str "t") typrms + let currentNamespace = innerCtx |> Context.getFullName [] + let inheritingTypes = c.implements |> List.map (getAllInheritancesAndSelf innerCtx) |> Set.unionMany + + let knownTypes = + Set.unionMany [ + if innerCtx.options.subtyping |> List.contains Subtyping.CastFunction then + yield! c.implements |> List.map (getKnownTypes innerCtx) + else + // We only need the type arguments of the inherited types + yield! + inheritingTypes + |> Set.toList + |> List.collect (function + | InheritingType.KnownIdent x -> x.tyargs + | InheritingType.UnknownIdent x -> x.tyargs + | InheritingType.Prim (_, tyargs) -> tyargs + | InheritingType.Other t -> [t]) + |> List.map (getKnownTypes innerCtx) + + yield + c.members + |> Seq.collect (snd >> findTypesInClassMember (knownTypeFinder innerCtx) ()) + |> Set.ofSeq + + yield! + c.typeParams + |> List.choose (fun c -> c.defaultType) + |> List.map (getKnownTypes innerCtx) + ] |> Set.union additionalKnownTypes + + let labels = + match kind with + | ClassKind.NormalClass _ -> + getLabelsOfFullName flags overrideFunc innerCtx currentNamespace c.typeParams + | ClassKind.ExportDefaultClass _ -> + inheritingTypes |> getLabelsFromInheritingTypes flags overrideFunc innerCtx + | ClassKind.AnonymousInterface _ -> [] + let emittedLabels = emitLabels innerCtx labels + + let useTags = + not isAnonymous + && innerCtx.options.subtyping |> List.contains Subtyping.Tag + && not (List.isEmpty labels) + + let polymorphicThis = + if useTags then + Type.appOpt (str "this") (str "'tags" :: typrms) + else + selfTyText + + let overrideFunc = + OverrideFunc.combine overrideFunc <| + fun _flags _emitType _ctx -> function + | PolymorphicThis -> Some polymorphicThis + | _ -> None + + let emitType_ ctx ty = emitType overrideFunc ctx ty + let members = [ + for ma, m in c.members do + yield! emitMembers flags overrideFunc innerCtx selfTy isExportDefaultClass ma m + yield! additionalMembers innerCtx flags overrideFunc + ] + + let scope = + match kind with + | ClassKind.NormalClass _ -> forceScope |> Option.defaultValue Scope.Default + | _ -> Scope.Ignore + + let comments = c.comments |> emitComments false + + let tagsDefinition = + if useTags && innerCtx.options.inheritWithTags.HasProvide then + let alias = + emitTypeAliasesImpl + "tags" flags overrideFunc innerCtx c.loc c.typeParams (Some emittedLabels) + (fun x -> [Statement.typeAlias false x.name (x.tyargs |> List.map snd) x.target]) + |> concat newline + alias |> TypeAliasText |> Some + else None + + let polymorphicThisDefinition = + if useTags then + let tags = + getLabelOfFullName flags overrideFunc innerCtx currentNamespace c.typeParams + |> function Choice1Of2 xs -> xs | Choice2Of2 (_, x) -> [x] + |> emitLabelsBody innerCtx + |> between "[> " " ]" + Statement.typeAlias false "this" + (str "'tags" :: typrms) + (Type.intf (str "'tags") +@ " constraint 'tags = " + tags |> Some) + |> TypeAliasText |> Some + else None + + let typeDefinition = + let fallback = {| ty = None; isRec = false |} + let getSelfTyText (c: Class) = + match c.name with + | Name name -> + assert (name = List.last innerCtx.currentNamespace) + if innerCtx.options.subtyping |> List.contains Subtyping.Tag then + if List.isEmpty labels then fallback + else + let isRec = + labels |> List.exists (function + | Case (_, args) | TagType (_, args) -> + args |> List.contains (str "t") + ) + {| ty = Type.intf emittedLabels |> Some; isRec = isRec |} + else fallback + | ExportDefaultUnnamedClass -> + if List.isEmpty labels then fallback + else {| ty = Type.intf emittedLabels |> Some; isRec = false |} + let selfTyText = + match kind with + | ClassKind.NormalClass x -> getSelfTyText x.orig + | ClassKind.ExportDefaultClass x -> getSelfTyText x.orig + | ClassKind.AnonymousInterface _ -> fallback + + emitTypeAliasesImpl "t" flags overrideFunc innerCtx c.loc c.typeParams selfTyText.ty (fun x -> + if not x.isOverload then + [TypeDefText.Create(x.name, x.tyargs, x.target, isRec=selfTyText.isRec, comments=emitComments false c.comments)] + else + [TypeAliasText (Statement.typeAlias false x.name (x.tyargs |> List.map snd) x.target)] + ) + + let castFunctions = [ + // add a generic cast function if tag is available + if useTags then + let castTy = + Type.arrow [polymorphicThis] selfTyText + yield! binding (fun _ _ -> Binding.cast [] "castFrom" castTy) + + if innerCtx.options.subtyping |> List.contains Subtyping.CastFunction then + for parent in c.implements do + let ty = Type.arrow [selfTyText] (emitType_ innerCtx parent) + let parentName = getHumanReadableName innerCtx parent + yield! binding (fun rename _ -> Binding.cast [] (rename $"as{parentName}") ty) + ] + + let builder = + let emitType_ ctx ty = + emitTypeImpl { flags with needParen = true } overrideFunc ctx ty + if not c.isPOJO then [] + else + let field (fl: FieldLike) = + let value, isOptional = + match fl.value with + | Prim Null | Prim Undefined -> Prim Never, true + | Union u -> + let nulls, others = + u.types |> List.partition (function Prim Null | Prim Undefined -> true | _ -> false) + if List.isEmpty nulls then fl.value, fl.isOptional + else Union { types = others }, true + | _ -> fl.value, fl.isOptional + {| fl with value = value |> emitType_ innerCtx; isOptional = isOptional |} + let fields = + c.members + |> List.choose (fun (ma, m) -> + match m with + | Field (fl, (Mutable | ReadOnly)) -> Some (field fl) + | Getter fl -> Some (field fl) + (* + | Method (name, ft, _) -> + let value = emitType_ innerCtx (Func (ft, [], ma.loc)) + Some {| isOptional = false; name = name; value = value |} + *) + | _ -> None) + binding (fun rename _ -> Binding.builder (rename "make") fields selfTyText) + + let items = [ + yield! typeDefinition + yield! tagsDefinition |> Option.toList + yield! polymorphicThisDefinition |> Option.toList + yield! members + yield! builder + yield! castFunctions + ] + + {| StructuredTextNode.empty with items = items; comments = comments; knownTypes = knownTypes; scope = scope |} + + let export = + match kind with + | ClassKind.NormalClass x -> + let kind = + if not c.isInterface || node.scope <> Scope.Ignore then Kind.OfClass + else Kind.OfInterface + getExportFromStatement ctx x.name kind (if c.isInterface then "interface" else "class") (Class x.orig) + | _ -> None + + let addAsNode (name: string) = + current + |> add [name] node + |> inTrie [name] (addAnonymousInterface flags ctx node.knownTypes) + |> set {| StructuredTextNode.empty with exports = Option.toList export |} + + match kind with + | ClassKind.NormalClass x -> addAsNode x.name + | ClassKind.AnonymousInterface x -> addAsNode x.name + | ClassKind.ExportDefaultClass _ -> + current + |> set {| StructuredTextNode.empty with exports = [ExportItem.DefaultUnnamedClass node] |} + |> addAnonymousInterface flags ctx node.knownTypes + +and addAnonymousInterfaceExcluding emitTypeFlags (ctx: Context) knownTypes ais (current: StructuredText) = + knownTypes + |> Seq.choose (function KnownType.AnonymousInterface (a, info) -> Some (a, info) | _ -> None) + |> Seq.filter (fun (a, _) -> ais |> List.contains a |> not) + |> Seq.fold (fun (current: StructuredText) (a, _) -> + let shouldSkip = + current.value + |> Option.map (fun v -> v.anonymousInterfaces |> Set.contains a) + |? false + if shouldSkip then current + else + emitClass emitTypeFlags OverrideFunc.noOverride ctx current (a.MapName Choice2Of2) ((fun _ _ _ -> []), Set.empty, None) + |> set {| StructuredTextNode.empty with anonymousInterfaces = Set.singleton a |} + ) current +and addAnonymousInterface emitTypeFlags ctx knownTypes (current: StructuredText) = addAnonymousInterfaceExcluding emitTypeFlags ctx knownTypes [] current + +let emitConstructor name attrs types = + concat (str " ") [ + yield str "|" + yield! attrs + yield str name + if List.isEmpty types |> not then + yield between "(" ")" (concat (str ", ") types) + ] + +let getEnumCaseValue (ctx: Context) (e: Enum) (ec: EnumCase) = + ec.value |> Option.defaultWith (fun () -> + ctx.logger.errorf "error: the case '%s' of enum '%s' has an unknown value, which is not supported at %s" + ec.name e.name ec.loc.AsString + ) + +let emitEnum (ctx: Context) (current: StructuredText) (e: Enum) = + let enumCaseToIdentifier (e: Enum) (ec: EnumCase) = + let duplicateCases = + e.cases |> List.filter (fun ec' -> ec.value = ec'.value) + match duplicateCases with + | [] -> impossible "enumCaseToIdentifier" + | [ec'] -> + assert (ec = ec') + Naming.constructorName [ec.name] + | ecs -> + ecs |> List.map (fun ec -> ec.name) |> Naming.constructorName + + let distinctCases = + e.cases + |> List.map (fun ec -> enumCaseToIdentifier e ec, ec) + |> List.distinctBy (fun (_, ec) -> ec.value) + |> List.map (fun (key, ec) -> key, getEnumCaseValue ctx e ec) + + let childNode (ec: EnumCase) = + EnumCaseText {| name = ec.name; comments = ec.comments |} + + let parentNode = + let casesText = + newline + concat newline [ + for key, value in distinctCases do + yield emitConstructor key [Attr.as_ (Term.literal value)] [] |> indent + ] + let item = TypeDefText.Create("t", [], Some casesText, shouldAssert=true, comments=emitComments false e.comments) + let items = item :: List.map childNode e.cases + let comments = e.comments |> emitComments false + {| StructuredTextNode.empty with items = items; comments = comments |} + + let exports = getExportFromStatement ctx e.name Kind.OfEnum "enum" (Enum e) + + current + |> add [e.name] parentNode + |> set {| StructuredTextNode.empty with exports = Option.toList exports |} + +let rec emitTypeAlias flags overrideFunc (ctx: Context) (current: StructuredText) (ta: TypeAlias) : StructuredText = + let emitType = emitTypeImpl flags overrideFunc + + let comments = (ta :> ICommented<_>).getComments() |> emitComments false + let knownTypes = Statement.getKnownTypes ctx [TypeAlias ta] + + let renamer = new OverloadRenamer() + let inline rename s = renamer.Rename "ctor" s + let items = + let ctx = ctx |> Context.ofChildNamespace ta.name + let isRec = knownTypes |> Set.contains (KnownType.Ident (ctx |> Context.getFullNameOfCurrentNamespace)) + let emitTypeAliases attrs shouldAssert target = + emitTypeAliasesImpl "t" flags OverrideFunc.noOverride ctx ta.loc ta.typeParams (Some target) (fun x -> + if not x.isOverload then + [TypeDefText.Create ( + x.name, x.tyargs, x.target, + isRec=isRec, attrs=attrs, shouldAssert=shouldAssert, + comments=comments + )] + else + [TypeAliasText (Statement.typeAlias false x.name (x.tyargs |> List.map snd) x.target)] + ) + let fallback () = emitTypeAliases [] false (emitType ctx ta.target) + let nameFromType t = + Naming.constructorName [getHumanReadableName ctx t] |> rename + + match ta.target with + | Union u -> // emit as variant if possible + let ru = ResolvedUnion.resolve ctx (ResolvedUnion.expand ctx u) + let isEnumOrUnboxed = + ru.satisfies(hasDU=false, hasOther=false) + && ru.typeofableTypes |> Set.contains Typeofable.BigInt |> not // not supported by res + && ru.typeofableTypes |> Set.contains Typeofable.Symbol |> not // not supported by res + + let isTagged = + ru.satisfies(hasDU=true, hasTypeofable=false, hasArray=false, hasOther=false) + && Map.count ru.discriminatedUnions = 1 + + let commonCases () = [ + if ru.caseNull then + yield emitConstructor (rename "Null") [Attr.as_ (str "null")] [] + if ru.caseUndefined then + yield emitConstructor (rename "Undefined") [Attr.as_ (str "undefined")] [] + for e in ru.caseEnum do + match e with + | Choice1Of2 (e, ec, _) -> + let value = + ec.value |> Option.defaultWith (fun () -> + ctx.logger.errorf "error: the case '%s' of enum '%s' has an unknown value, which is not supported at %s" + ec.name e.name ec.loc.AsString + ) + yield + emitConstructor + (Naming.constructorName [ec.name] |> rename) + [Attr.as_ (Term.literal value)] + [] + | Choice2Of2 l -> + yield emitConstructor (nameFromType (TypeLiteral l)) [Attr.as_ (Term.literal l)] [] + ] + if isEnumOrUnboxed then + let attrs = + if Set.isEmpty ru.typeofableTypes && Option.isNone ru.caseArray then [] + else [Attr.Variant.unboxed] + emitTypeAliases attrs true ( + newline + indent (concat newline [ + yield! commonCases () + match ru.caseArray with + | None -> () + | Some ts -> + yield emitConstructor (rename "Array") [] [ + Type.app Type.array [emitType ctx (Union { types = Set.toList ts })] + ] + for t in ru.typeofableTypes do + match t with + | Typeofable.String -> yield emitConstructor (rename "String") [] [Type.string] + | Typeofable.Number -> yield emitConstructor (rename "Number") [] [Type.number ctx.options] + | Typeofable.Boolean -> yield emitConstructor (rename "Boolean") [] [Type.boolean] + | _ -> () + ] + )) + + else if isTagged && ctx.options.experimentalTaggedUnion then + let tagName, cases = ru.discriminatedUnions |> Map.toSeq |> Seq.head + // skip if the result would contain anonymous interfaces + if cases |> Map.exists (fun _ t -> getAnonymousInterfaces t |> Seq.isEmpty |> not) then + fallback () + else + let tps = ta.typeParams |> List.map (fun x -> x, tprintf "'%s" x.name) + let variant = + let body = + newline + indent (concat newline [ + for i, (tag, t) in cases |> Map.toSeq |> Seq.indexed do + let name = + let rec go = function + | Ident { name = name } + | App (AIdent({ name = name }), _, _) -> + List.last name |> Some + | Union { types = types } -> + let names = types |> List.choose go |> List.distinct + match names with + | [name] -> Some name + | _ -> None + | _ -> None + match go t with + | Some name -> Naming.constructorName [name] + | None -> $"Case{i + 1}" + yield emitConstructor + (rename name) + [Attr.as_ (Term.literal tag)] + [emitType ctx t] + ]) + TypeDefText.Create ("cases", tps, Some body, isRec=isRec, attrs=[Attr.Variant.tag tagName], comments=comments, shouldAssert=true) + [ + yield! fallback () + yield variant + yield! + binding (fun rename _ -> + Binding.let_ [] [] (rename "box") + (Type.arrow + [Type.appOpt (str "t") (tps |> List.map snd)] + (Type.appOpt (str "cases") (tps |> List.map snd))) + (Term.arrow [str "it"] ( + Term.app (str "Experimental.Variant.box") [str "it"; Term.literal (LString tagName)])) + ) + yield! + binding (fun rename _ -> + Binding.let_ [] [] (rename "unbox") + (Type.arrow + [Type.appOpt (str "cases") (tps |> List.map snd)] + (Type.appOpt (str "t") (tps |> List.map snd))) + (Term.arrow [str "it"] ( + Term.app (str "Experimental.Variant.unbox") [str "it"])) + ) + ] + else fallback () + + | TypeLiteral l -> // emit as single-case variant + emitTypeAliases [] true ( + emitConstructor (nameFromType (TypeLiteral l)) [Attr.as_ (Term.literal l)] [] + ) + + | _ -> fallback () + + let node = {| StructuredTextNode.empty with items = items; comments = comments; knownTypes = knownTypes |} + current + |> inTrie [ta.name] (set node) + |> addExportFromStatement ctx ta.name Kind.OfTypeAlias "type" (TypeAlias ta) + |> inTrie [ta.name] (addAnonymousInterface flags ctx knownTypes) + +let private createExternalForValue (ctx: Context) (rename: string -> string) (s: CurrentScope) attr comments name ty = + let fallback () = + Binding.ext (scopeToAttr s attr) comments (rename name |> Naming.valueName) ty name + let jsModule () = + match s.jsModule with + | None -> impossible "createExternalForValue" + | Some jsModule -> jsModule + match ctx |> Context.getExportTypeOfName [name] with + | None | Some (ExportType.Child _) | Some (ExportType.ES6 None) -> fallback () + | Some ExportType.CommonJS -> + Binding.ext (Attr.External.module_ None :: attr) comments (rename name |> Naming.valueName) ty (jsModule ()) + | Some ExportType.ES6Default -> + Binding.ext (scopeToAttr s attr) comments (rename name |> Naming.valueName) ty "default" + | Some (ExportType.ES6 (Some renameAs)) -> + Binding.ext (scopeToAttr s attr) comments (rename name |> Naming.valueName) ty renameAs + +let rec emitFunction flags overrideFunc ctx (f: Function) = + if functionNeedsWorkaround f.typ then + emitVariable flags overrideFunc ctx + { accessibility = f.accessibility; comments = f.comments; isExported = f.isExported; + loc = f.loc; name = f.name; isConst = true; typ = Func (f.typ, [], f.loc) } + else + let inline extFunc ft = extFunc flags overrideFunc ctx ft + let ty, attr = extFunc f.typ + let attr = attr |> impossibleNone (fun () -> "emitFunction") + let comments = emitComments false f.comments + binding (fun rename s -> createExternalForValue ctx rename s (Attr.External.val_ :: attr) comments f.name ty) + +and emitVariable flags overrideFunc ctx (v: Variable) = + match v.typ with + | Func (ft, tps, _) when not (functionNeedsWorkaround ft) -> + emitFunction flags overrideFunc ctx + { accessibility = v.accessibility; comments = v.comments; isExported = v.isExported; + loc = v.loc; name = v.name; typ = ft; typeParams = tps } + | _ -> + let emitType = emitTypeImpl flags + let emitType_ = emitType overrideFunc + let ty, attr = emitType_ ctx v.typ, [Attr.External.val_] + let comments = emitComments false v.comments + binding (fun rename s -> createExternalForValue ctx rename s attr comments v.name ty) + +let emitImport (ctx: Context) (i: Import) : StructuredTextItem list = + let emitImportClause (c: ImportClause) = + let getModuleName (specifier: string) = + if specifier.StartsWith(".") |> not then Naming.jsModuleNameToReScriptModuleName specifier |> Some + else + match JsHelper.tryGetActualFileNameFromRelativeImportPath ctx.currentSourceFile ctx.state.fileNames specifier with + | Some _ -> None // if the imported file is included in the input files, skip emitting it + | None -> + JsHelper.resolveRelativeImportPath (ctx.state.info |> Result.toOption) ctx.currentSourceFile ctx.state.fileNames specifier + |> JsHelper.InferenceResult.tryUnwrap + |> Option.defaultValue specifier + |> Naming.jsModuleNameToReScriptModuleName + |> Some + + let isModule (name: string) (kind: Set option) = + i.isTypeOnly + || kind |> Option.map Kind.generatesReScriptModule + |> Option.defaultValue false + || ctx |> Context.tryCurrentSourceInfo (fun i -> i.unknownIdentTypes |> Trie.containsKey [name]) + |> Option.defaultValue false + || name |> Naming.isCase Naming.PascalCase + + match c with + | LocalImport x -> + let shouldEmit = + match x.kind with + | Some kind -> kind |> Kind.generatesReScriptModule + | None -> x.target |> Ident.getKind ctx |> Kind.generatesReScriptModule + if shouldEmit then + [Statement.moduleAlias (Naming.moduleName x.name) (x.target.name |> Naming.structured Naming.moduleName) |> ImportText] + else [] + | NamespaceImport x when isModule x.name x.kind -> + getModuleName x.specifier + |> Option.map (fun moduleName -> + [Statement.moduleAlias (Naming.moduleName x.name) (sprintf "%s.Export" moduleName) |> ImportText]) + |> Option.defaultValue [] + | ES6WildcardImport s -> + getModuleName s + |> Option.map (fun moduleName -> [Statement.open_ (sprintf "%s.Export" moduleName) |> ImportText]) + |> Option.defaultValue [] + | ES6DefaultImport x when isModule x.name x.kind -> + getModuleName x.specifier + |> Option.map (fun moduleName -> + [Statement.moduleAlias (Naming.moduleName x.name) (sprintf "%s.Export.Default" moduleName) |> ImportText]) + |> Option.defaultValue [] + | ES6Import x when isModule x.name x.kind -> + let name = + match x.renameAs with + | Some name -> Naming.moduleName name + | None -> Naming.moduleName x.name + getModuleName x.specifier + |> Option.map (fun moduleName -> + [Statement.moduleAlias name (sprintf "%s.Export.%s" moduleName (Naming.moduleName x.name)) |> ImportText]) + |> Option.defaultValue [] + | NamespaceImport _ | ES6DefaultImport _ | ES6Import _ -> [] + + [ yield! emitComments true i.comments |> List.map ImportText + yield commentStr i.origText |> ImportText + for c in i.clauses do + yield! emitImportClause c] + +let createStructuredText (rootCtx: Context) (stmts: Statement list) : StructuredText = + let emitTypeFlags = EmitTypeFlags.defaultValue + let overrideFunc = OverrideFunc.noOverride + let emitType = emitTypeImpl emitTypeFlags + let emitSelfType = emitTypeImpl emitTypeFlags overrideFunc + + /// convert interface members to appropriate statements + let intfToStmts (moduleIntf: Class<_>) ctx flags overrideFunc = + let inline extFunc ft = extFunc flags overrideFunc ctx ft + let inline func ft = func flags overrideFunc ctx ft + let inline newableFunc ft = newableFunc flags overrideFunc ctx ft + let emitAsVariable name typ isConst (memberAttr: MemberAttribute) = + let v = + { name = name; typ = typ; + isConst = isConst; isExported = Exported.No; accessibility = Some memberAttr.accessibility; + comments = memberAttr.comments; loc = memberAttr.loc } + emitVariable flags overrideFunc ctx v + let emitAsFunction name typ typrms (memberAttr: MemberAttribute) = + let f = + { name = name; typ = typ; typeParams = typrms; + isExported = Exported.No; accessibility = Some memberAttr.accessibility; + comments = memberAttr.comments; loc = memberAttr.loc } + emitFunction flags overrideFunc ctx f + [ for ma, m in moduleIntf.members do + let comments = emitComments false ma.comments + match m with + | Field (fl, mt) -> + yield! emitAsVariable fl.name fl.value (mt = ReadOnly) ma + | Getter fl -> + yield! emitAsVariable fl.name fl.value true ma + | Setter _ -> () + | Method (name, ft, tps) -> + yield! emitAsFunction name ft tps ma + | Newable (ft, _tps) -> + let ty, attrs = + match extFunc ft with + | ty, Some attrs -> ty, Attr.External.new_ :: attrs + | _, None -> newableFunc ft, Attr.External.val_ :: [] + yield! + binding (fun rename s -> + let target, attrs = + match tryBindToCurrentScope s attrs with + | Some x -> x.self, x.attr + | None -> impossible "intfToStmts_Newable(%s)" ma.loc.AsString + let attrs = attrs |> List.rev + Binding.ext attrs comments (rename "make") ty target + ) + | Callable (ft, _tps) -> + let ty, attrs = + match extFunc ft with + | ty, Some attrs -> ty, Attr.External.new_ :: attrs + | _, None -> func ft, Attr.External.val_ :: [] + yield! + binding (fun rename s -> + let target, attrs = + match tryBindToCurrentScope s attrs with + | Some x -> x.self, x.attr + | None -> impossible "intfToStmts_Callable(%s)" ma.loc.AsString + let attrs = attrs |> List.rev + Binding.ext attrs comments (rename "apply") ty target + ) + | Constructor _ -> impossible "emitStructuredDefinition_Pattern_intfToModule_Constructor" // because interface! + | Indexer (ft, _) -> + let ty = func ft + yield! binding (fun _ _ -> Binding.unknown comments (Some ("unsupported indexer of type: " @+ ty))) + | UnknownMember (Some msg) -> + yield! binding (fun _ _ -> Binding.unknown comments (Some (str msg))) + | SymbolIndexer _ | UnknownMember None -> () ] + + let rec folder ctx (current: StructuredText) (s: Statement) : StructuredText = + let comments = (s :> ICommented<_>).getComments() |> emitComments false + + let knownTypes () = Statement.getKnownTypes ctx [s] + let addExport name kind kindString current = + addExportFromStatement ctx name kind kindString s current + let addAnonymousInterfaceWithKnownTypes knownTypes current = addAnonymousInterface emitTypeFlags ctx knownTypes current + let addAnonymousInterface current = addAnonymousInterfaceWithKnownTypes (knownTypes ()) current + let addAnonymousInterfaceExcludingWithKnownTypes knownTypes ais current = addAnonymousInterfaceExcluding emitTypeFlags ctx knownTypes ais current + let addAnonymousInterfaceExcluding ais current = addAnonymousInterfaceExcludingWithKnownTypes (knownTypes ()) ais current + + match s with + | Namespace m -> + let module' = + let node = {| StructuredTextNode.empty with comments = comments; scope = Scope.Default; knownTypes = knownTypes () |} + let module' = current |> getTrie [m.name] |> set node + let ctx = ctx |> Context.ofChildNamespace m.name + m.statements |> List.fold (folder ctx) module' + let current = current |> setTrie [m.name] module' + match module'.value with + | None -> current + | Some _ -> current |> addExport m.name Kind.OfNamespace "namespace" + | AmbientModule m -> + let module' = + let node = {| StructuredTextNode.empty with comments = comments; scope = Scope.Module m.name.unquoted; knownTypes = knownTypes () |} + let module' = current |> getTrie [m.name.orig] |> set node + let ctx = ctx |> Context.ofChildNamespace m.name.orig + m.statements |> List.fold (folder ctx) module' + current |> setTrie [m.name.orig] module' + | Global m -> + current |> inTrie ["global"] (fun g -> + let node = {| StructuredTextNode.empty with scope = Scope.Global |} + m.statements |> List.fold (folder ctx) (set node g) + ) + | Class c -> + emitClass emitTypeFlags OverrideFunc.noOverride ctx current (c.MapName Choice1Of2) ((fun _ _ _ -> []), Set.empty, None) + | Enum e -> + emitEnum ctx current e + | TypeAlias ta -> + emitTypeAlias emitTypeFlags overrideFunc ctx current ta + | Pattern p -> + let fallback current = + p.underlyingStatements + |> List.fold (folder ctx) current + |> addAnonymousInterface + match p with + | ImmediateInstance (intf & { name = Name intfName }, value) when Simplify.Has(ctx.options.simplify, Simplify.ImmediateInstance) -> + let knownTypesInMembers = Statement.getKnownTypes ctx [Class intf] + let createModule () = + let items = intfToStmts intf (ctx |> Context.ofChildNamespace value.name) emitTypeFlags overrideFunc + {| StructuredTextNode.empty with items = items; scope = Scope.Path value.name; knownTypes = knownTypesInMembers; openTypesModule = false |} + if knownTypesInMembers |> Set.contains (KnownType.Ident (ctx |> Context.getFullName [intfName])) then + fallback current + else + current + |> inTrie [value.name] (set (createModule ())) + |> addExport value.name Kind.OfClass "interface" + |> inTrie [value.name] addAnonymousInterface + | ImmediateConstructor (baseIntf, ctorIntf, ctorValue) when Simplify.Has(ctx.options.simplify, Simplify.ImmediateConstructor) -> + emitClass emitTypeFlags OverrideFunc.noOverride ctx current (baseIntf.MapName Choice1Of2) (intfToStmts ctorIntf, Statement.getKnownTypes ctx [Class ctorIntf], Some (Scope.Path ctorValue.name)) + | _ -> fallback current + | Function func -> + let node = + {| StructuredTextNode.empty with + items = emitFunction emitTypeFlags overrideFunc ctx func + knownTypes = knownTypes () |} + current + |> set node + |> addExport func.name Kind.OfValue "function" + |> addAnonymousInterface + | Variable value -> + let knownTypes = knownTypes () + let fallback current = + let node = + {| StructuredTextNode.empty with + items = emitVariable emitTypeFlags overrideFunc ctx value + knownTypes = knownTypes |} + current + |> set node + |> addExport value.name Kind.OfValue (if value.isConst then "const" else "let") + |> addAnonymousInterface + let inline (|Dummy|) _ = [] + match value.typ with + | AnonymousInterface intf when Simplify.Has(ctx.options.simplify, Simplify.AnonymousInterfaceValue) -> + let items = intfToStmts intf (ctx |> Context.ofChildNamespace value.name) emitTypeFlags overrideFunc + current + |> inTrie [value.name] + (set + {| StructuredTextNode.empty with + items = items + scope = Scope.Path value.name + knownTypes = + knownTypes |> Set.filter (function KnownType.AnonymousInterface (ai, _) -> ai.loc <> intf.loc | _ -> true) + openTypesModule = false |}) + |> addExport value.name Kind.OfClass (if value.isConst then "const" else "let") + |> inTrie [value.name] (addAnonymousInterfaceExcluding [intf]) + | Ident (i & { loc = loc }) & Dummy tyargs + | App (AIdent i, tyargs, loc) when Simplify.Has(ctx.options.simplify, Simplify.NamedInterfaceValue) -> + let intf = + Ident.pickDefinition ctx i (function Definition.Class c when c.isInterface -> Some c | _ -> None) + match intf with + | None -> fallback current + | Some intf -> + let bindings = createBindings i.name loc intf.typeParams tyargs + let intf = intf |> mapInClass (substTypeVar bindings) ctx + let name = value.name + "Static" + let knownTypesInMembers = Statement.getKnownTypes ctx [Class intf] + let createModule () = + let items = intfToStmts intf ctx emitTypeFlags overrideFunc + {| StructuredTextNode.empty with items = items; scope = Scope.Path value.name; knownTypes = knownTypesInMembers; openTypesModule = false |} + current + |> inTrie [name] (set (createModule ())) + |> addExport name Kind.OfClass (if value.isConst then "const" else "let") + |> inTrie [name] (addAnonymousInterfaceWithKnownTypes knownTypesInMembers) + |> fallback + | _ -> fallback current + | Import i -> + current |> set {| StructuredTextNode.empty with items = emitImport ctx i |} + | Export e -> + let getKind = function + | CommonJsExport i | ES6DefaultExport i -> i |> Ident.getKind ctx + | ES6Export x -> x.target |> Ident.getKind ctx + | NamespaceExport _ -> Set.empty + current + |> set + {| StructuredTextNode.empty with + exports = [ExportItem.Export {| e with clauses = e.clauses |> List.map (fun c -> c, getKind c) |}] |} + | ReExport e -> + let getKind = function + | ES6ReExport x -> x.target |> Ident.getKind ctx + | ES6NamespaceReExport _ | ES6WildcardReExport -> Set.empty + current + |> set + {| StructuredTextNode.empty with + exports = [ExportItem.ReExport {| e with clauses = e.clauses |> List.map (fun c -> c, getKind c) |}] |} + | UnknownStatement u -> + let cmt = + match u.origText with + | Some s -> commentStr s | None -> commentStr "unknown statement" + current |> set {| StructuredTextNode.empty with items = [Comment cmt] |} + | FloatingComment c -> + let cmt = c.comments |> emitComments true |> List.map Comment + current |> set {| StructuredTextNode.empty with items = Comment empty :: cmt |} + + stmts |> List.fold (folder rootCtx) Trie.empty + +type EmitModuleFlags = {| + /// The module being emitted is a reserved one (e.g. `Export`) + isReservedModule: bool + jsModule: string option + scopeRev: string list +|} + +type EmitModuleResult = {| + imports: text list + /// The `Types` module + types: text list + /// The content of the `.res` file + impl: text list + /// The content of the `.resi` file + intf: text list + comments: text list +|} + +module EmitModuleResult = + let empty : EmitModuleResult = + {| imports = []; types = []; impl = []; intf = []; comments = [] |} + +let rec emitModule (dt: DependencyTrie) flags (ctx: Context) st = + let isLinear = ctx.options.noTypesModule || DependencyTrie.isLinear dt // compute only once + let rec go (flags: EmitModuleFlags) (ctx: Context) (st: StructuredText) : EmitModuleResult = + let renamer = new OverloadRenamer() + let children = + st.children + |> Map.toList + |> List.map (fun (k, v) -> + let name = + let name = + if flags.isReservedModule then Naming.moduleNameReserved k + else Naming.moduleName k + name |> renamer.Rename "module" + let scopeRev, jsModule = + let overrideScope name = + match ctx |> Context.getExportTypeOfName [name] with + | None + | Some (ExportType.Child _) -> name :: flags.scopeRev + | Some ExportType.CommonJS -> [] + | Some (ExportType.ES6 None) -> [name] + | Some ExportType.ES6Default -> ["default"] + | Some (ExportType.ES6 (Some name)) -> [name] + match v.value with + | None -> k :: flags.scopeRev, flags.jsModule + | Some v -> + match v.scope with + | Scope.Default -> overrideScope k, flags.jsModule + | Scope.Path p -> overrideScope p, flags.jsModule + | Scope.Module m -> [], Some m + | Scope.Global -> [], None + | Scope.Ignore -> flags.scopeRev, flags.jsModule + let flags = {| flags with scopeRev = scopeRev; jsModule = jsModule |} + let ctx = ctx |> Context.ofChildNamespace k + let result = go flags ctx v + let openTypesModule = + if flags.isReservedModule then false + else + let hasTypeDefinitions = result.types |> List.isEmpty |> not + v.value + |> Option.map (fun v -> hasTypeDefinitions && v.openTypesModule) + |> Option.defaultValue hasTypeDefinitions + {| name = name; origName = k |}, openTypesModule, result) + + let items = + let currentScope : CurrentScope = !!flags + + let emitEnumCase (e: {| name: string; comments: Comment list |}) = + let moduleName = Naming.moduleName e.name + let types = + tprintf "module %s : " moduleName +@ "{ type nonrec t = t }" + let intf = [ + yield str $"type nonrec t = t" + ] + let impl = [ + yield Statement.open_ moduleName + yield str "type nonrec t = t" + ] + let m content = {| name = moduleName; origName = e.name; content = content; comments = emitComments false e.comments |} + {| types = types + intf = Statement.moduleSig (m intf) + impl = Statement.moduleVal (m (if isLinear then intf else impl)) + |} + + let emitTypeDefText (e: TypeDefText) = + // TODO: emit comments + let attrs = e.attrs |> List.map (fun x -> x +@ " ") |> join + let actual = attrs + Statement.typeAlias e.isRec e.name (e.tyargs |> List.map snd) e.body + let alias = + let tmp = + Statement.typeAlias false e.name (e.tyargs |> List.map snd) + (Type.appOpt (str e.name) (e.tyargs |> List.map snd) |> Some) + match e.body, e.shouldAssert with + | _, false | None, _ -> tmp + | Some b, true -> attrs + tmp +@ " = " + b + {| types = actual; intf = actual; impl = alias |} + + let rec f = function + | ImportText t -> ImportText t + | TypeAliasText t -> TypeAliasText t + | TypeDefText d -> TypeDefText (emitTypeDefText d) + | Binding b -> Binding (b renamer currentScope) + | EnumCaseText e -> EnumCaseText (emitEnumCase e) + | Comment c -> Comment c + match st.value with None -> [] | Some v -> v.items |> List.map f + + let imports = + items |> List.choose (function ImportText t -> Some t | _ -> None) + + let types = + if isLinear then [] + else + let items = + items |> List.choose (function + | TypeDefText x | EnumCaseText x -> Some x.types + | _ -> None) + let children = + children + |> List.filter (fun (_, _, c) -> c.types |> List.isEmpty |> not) + |> List.map (fun (k, _, c) -> {| k with content = c.imports @ c.types; comments = [] |}) + |> Statement.moduleSCC dt Statement.moduleSigRec Statement.moduleSigNonRec ctx + children @ items + + let exports = + st.value + |> Option.map (fun m -> m.exports |> emitExportModule ctx) + |> Option.defaultValue EmitModuleResult.empty + + let intf = + let children = + children + |> List.filter (fun (_, _, c) -> c.intf |> List.isEmpty |> not) + |> List.map (fun (k, _, c) -> + let content = c.imports @ c.intf + {| k with content = content; comments = c.comments |}) + |> Statement.moduleSigRec + let typeDefs = + items |> List.choose (function + | TypeAliasText t -> Some t + | TypeDefText x | EnumCaseText x -> Some x.intf + | _ -> None) + [ + yield! children + yield! typeDefs + for item in items do + match item with + | Binding b -> yield! Binding.emitForInterface b + | Comment c -> yield c + | _ -> () + yield! exports.intf + ] + + let impl = + let fixmeRecursiveModules (ms: TextModule list) = + match ms with + | [] -> [] + | [m] -> [Statement.moduleVal m] + | _ when ctx.options.noTypesModule -> + [ yield + commentStr ( + sprintf "FIXME: start of recursive definitions (%s)" + (ms |> List.map (fun m -> m.name) |> String.concat ", ") + ) + yield! Statement.moduleValMany ms + yield commentStr "FIXME: end of recursive definitions" ] + | _ -> Statement.moduleValMany ms + + let children = + children + |> List.filter (fun (_, _, c) -> c.impl |> List.isEmpty |> not) + |> List.map (fun (k, openTypesModule, c) -> + let content = + if not isLinear && openTypesModule then + Statement.open_ k.name :: c.imports @ c.impl + else + c.imports @ c.impl + {| k with content = content; comments = c.comments |}) + |> Statement.moduleSCC dt fixmeRecursiveModules Statement.moduleValMany ctx + let typeDefs = + items |> List.choose (function + | TypeAliasText t -> Some t + | TypeDefText d -> if isLinear then Some d.types else Some d.impl + | EnumCaseText x -> Some x.impl + | _ -> None) + [ + yield! children + yield! typeDefs + for item in items do + match item with + | Binding b -> yield! Binding.emitForImplementation b + | Comment c -> yield c + | _ -> () + yield! exports.impl + ] + + let comments = + match st.value with None -> [] | Some v -> v.comments + + {| imports = imports; types = types; intf = intf; impl = impl; comments = comments |} + go flags ctx st + +and emitExportModule (ctx: Context) (exports: ExportItem list) : EmitModuleResult = + let emitModuleAlias name (i: Ident) = + if i.kind |> Option.map Kind.generatesReScriptModule |> Option.defaultValue false then + [ Statement.moduleAlias + (name |> Naming.moduleNameReserved) + (i.name |> Naming.structured Naming.moduleName) |> TypeAliasText ] + else [] + + let addItems items (acc: StructuredText) = + acc |> Trie.setOrUpdate {| StructuredTextNode.empty with items = items |} StructuredTextNode.union + + let setItems path items (acc: StructuredText) = + acc |> Trie.addOrUpdate path {| StructuredTextNode.empty with items = items |} StructuredTextNode.union + + let rec go isFirst (acc: StructuredText) (exports: ExportItem list) = + match exports with + | [] -> acc + | ExportItem.DefaultUnnamedClass node :: rest -> + go false (acc |> Trie.addOrUpdate ["Export"; "Default"] node StructuredTextNode.union) rest + | ExportItem.Export export :: rest -> + let clauses = export.clauses |> List.map fst + let rec go' acc = function + | [] -> acc + | NamespaceExport _ :: rest -> go' acc rest + | CommonJsExport i :: rest -> + go' (acc |> addItems (emitModuleAlias "Export" i)) rest + | ES6DefaultExport e :: rest -> + go' (acc |> setItems ["Export"] (emitModuleAlias "Default" e)) rest + | ES6Export e :: rest -> + let name = e.renameAs |> Option.defaultValue (e.target.name |> List.last) + go' (acc |> setItems ["Export"] (emitModuleAlias name e.target)) rest + go false (go' acc clauses) rest + | ExportItem.ReExport export :: rest -> + // TODO + go isFirst acc rest + + let st = go true Trie.empty exports + st |> emitModule Trie.empty {| isReservedModule = true; jsModule = None; scopeRev = [] |} ctx + +let header = [ + str "@@uncurried" + str "@@warning(\"-27-32-33-44\")" +] + +let setTyperOptions (ctx: IContext) = + ctx.options.inheritArraylike <- true + ctx.options.inheritIterable <- true + ctx.options.inheritPromiselike <- true + ctx.options.replaceAliasToFunction <- false + ctx.options.replaceNewableFunction <- false + ctx.options.replaceRankNFunction <- true + ctx.options.addAllParentMembersToClass <- true + ctx.options.noExtendsInTyprm <- true + +let emitTypes (types: text list) : text list = + if List.isEmpty types then [] + else + [ + Statement.moduleSigRec1 "Types" types + Statement.open_ "Types" + ] + +let emitReferenceTypeDirectives (ctx: Context) (src: SourceFile) : text list = + let refs = + src.references + |> List.choose (function TypeReference r -> Some r | _ -> None) + if List.isEmpty refs then [] + else + let comments = + refs + |> List.map (sprintf "") + |> List.map commentStr + let openRefs = + refs + |> List.map Naming.jsModuleNameToReScriptModuleName + |> List.map Statement.open_ + empty :: comments @ openRefs + +let emitReferenceFileDirectives (ctx: Context) (src: SourceFile) : text list = + let refs = + src.references + |> List.choose (function FileReference r -> Some r | _ -> None) + if List.isEmpty refs then [] + else + // if the referenced file is included in the input files, skip emitting it + let validRefs = + refs + |> List.choose (fun ref -> + let relativePath = Path.join [Path.dirname src.fileName; ref] + if ctx.state.fileNames |> List.contains relativePath |> not then + Some {| path = ref; relativePath = relativePath |} + else None) + let comments = + refs + |> List.map (sprintf "") + |> List.map commentStr + let openRefs = + validRefs + |> List.choose (fun x -> + JsHelper.deriveModuleName (Result.toOption ctx.state.info) [x.relativePath] + |> JsHelper.InferenceResult.tryUnwrap + |> Option.map Naming.jsModuleNameToReScriptModuleName) + |> List.map Statement.open_ + empty :: comments @ openRefs + +let private emitImpl (sources: SourceFile list) (info: PackageInfo option) (ctx: IContext) = + let moduleName = + match ctx.options.name with + | Some name -> name + | None -> + JsHelper.deriveModuleName info (sources |> List.map (fun s -> s.fileName)) + |> JsHelper.InferenceResult.unwrap "package" + + let outputBaseName = + match ctx.options.name with + | Some name -> name + | None -> + let inline log x = + ctx.logger.tracef "* the inferred output file name is '%s.res'" x + x + JsHelper.deriveModuleName info (sources |> List.map (fun s -> s.fileName)) + |> JsHelper.InferenceResult.tryUnwrap + |> Option.map (Naming.jsModuleNameToReScriptName >> log) + |> Option.defaultWith (fun () -> + ctx.logger.warnf "* the output file name cannot be inferred. 'output.res' is used instead." + "output") + + let fileNames = sources |> List.map (fun s -> s.fileName) + + let info = + match info with + | Some info -> Ok info + | None -> Error (Some moduleName) + + let sources, mergedFileName = + match sources with + | [] -> impossible "emitImpl (empty sources)" + | [src] -> [src], src.fileName + | _ -> [mergeSources "input.d.ts" sources], "input.d.ts" + + ctx.logger.tracef "* running typer..." + setTyperOptions ctx + let ctx, sources = runAll sources ctx + let ctx = + ctx + |> Context.mapState (fun _ -> State.create fileNames info) + |> Context.ofSourceFileRoot mergedFileName + let stmts = sources |> List.collect (fun x -> x.statements) + + ctx.logger.tracef "* emitting a binding to '%s' for rescript..." moduleName + let st = createStructuredText ctx stmts + let flags : EmitModuleFlags = + let jsModule = + match ctx.options.``module`` with + | ModuleKind.None -> None + | ModuleKind.ES | ModuleKind.CJS -> Some moduleName + | ModuleKind.Default -> + let hasExport = + ctx.info |> Map.exists (fun _ v -> v.exportMap |> Trie.isEmpty |> not) + if hasExport then Some moduleName else None + {| jsModule = jsModule; scopeRev = []; isReservedModule = false |} + let dt = DependencyTrie.ofTrie (StructuredTextNode.getReferences ctx) st + let m = emitModule dt flags ctx st + + let opens = [ + yield Statement.open_ "Js" + yield Statement.open_ "Ts2ocaml" + for src in sources do + yield! emitReferenceTypeDirectives ctx src + yield! emitReferenceFileDirectives ctx src + ] + + let res = + concat newline [ + yield! header + yield! m.comments + yield! opens + yield! m.imports + yield! emitTypes m.types + yield! m.impl + ] + let resi = + if ctx.options.resi then + concat newline [ + yield! header + yield! m.comments + yield! opens + yield! m.imports + yield! m.intf + ] |> Some + else None + + { baseName = outputBaseName; resi = resi; res = res} + +let emit (input: Input) (ctx: IContext) : Output list = + if ctx.options.merge then + [emitImpl input.sources input.info ctx] + else + input.sources + |> List.map (fun source -> emitImpl [source] input.info ctx) diff --git a/src/ts2ocaml.fsproj b/src/ts2ocaml.fsproj index 4bb7f2ab..5d99e282 100644 --- a/src/ts2ocaml.fsproj +++ b/src/ts2ocaml.fsproj @@ -13,6 +13,10 @@ + + + + diff --git a/test/res/.gitignore b/test/res/.gitignore new file mode 100644 index 00000000..3bc4f81c --- /dev/null +++ b/test/res/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/node_modules/ +/lib/ +.bsb.lock +.merlin +/src/generated +*.bs.js diff --git a/test/res/package.json b/test/res/package.json new file mode 100644 index 00000000..d0843ca5 --- /dev/null +++ b/test/res/package.json @@ -0,0 +1,20 @@ +{ + "name": "rescript-project-template", + "version": "0.0.1", + "scripts": { + "build": "rescript", + "clean": "rescript clean -with-deps", + "start": "rescript build -w" + }, + "keywords": [ + "rescript" + ], + "author": "", + "license": "MIT", + "devDependencies": { + "@ocsigen/ts2ocaml": "link:../../" + }, + "dependencies": { + "rescript": "11.0.1" + } +} diff --git a/test/res/rescript.json b/test/res/rescript.json new file mode 100644 index 00000000..0dde58eb --- /dev/null +++ b/test/res/rescript.json @@ -0,0 +1,14 @@ +{ + "name": "rescript-project-template", + "version": "0.0.1", + "sources": { + "dir" : "src", + "subdirs" : true + }, + "package-specs": { + "module": "commonjs", + "in-source": true + }, + "suffix": ".bs.js", + "uncurried": true +} diff --git a/test/res/src/main.res b/test/res/src/main.res new file mode 100644 index 00000000..11aa340f --- /dev/null +++ b/test/res/src/main.res @@ -0,0 +1,12 @@ +module Ts = Typescript.Export + +let source = "let x: string = 'hello, world!'" + +let result = Ts.transpileModule( + ~input=source, + ~transpileOptions=Ts.TranspileOptions.make( + ~compilerOptions=Ts.CompilerOptions.make(~\"module"=CommonJS), + ), +) + +Js.log(result->Ts.TranspileOutput.get_outputText) diff --git a/test/res/src/placeholders/Iterable.res b/test/res/src/placeholders/Iterable.res new file mode 100644 index 00000000..9bbaf1d1 --- /dev/null +++ b/test/res/src/placeholders/Iterable.res @@ -0,0 +1 @@ +type t<'a> \ No newline at end of file diff --git a/test/res/src/placeholders/Map.res b/test/res/src/placeholders/Map.res new file mode 100644 index 00000000..77030897 --- /dev/null +++ b/test/res/src/placeholders/Map.res @@ -0,0 +1 @@ +type t<'k, 'v> \ No newline at end of file diff --git a/test/res/src/placeholders/ReadonlyMap.res b/test/res/src/placeholders/ReadonlyMap.res new file mode 100644 index 00000000..77030897 --- /dev/null +++ b/test/res/src/placeholders/ReadonlyMap.res @@ -0,0 +1 @@ +type t<'k, 'v> \ No newline at end of file diff --git a/test/res/src/placeholders/Set.res b/test/res/src/placeholders/Set.res new file mode 100644 index 00000000..9bbaf1d1 --- /dev/null +++ b/test/res/src/placeholders/Set.res @@ -0,0 +1 @@ +type t<'a> \ No newline at end of file diff --git a/test/res/src/placeholders/WeakMap.res b/test/res/src/placeholders/WeakMap.res new file mode 100644 index 00000000..77030897 --- /dev/null +++ b/test/res/src/placeholders/WeakMap.res @@ -0,0 +1 @@ +type t<'k, 'v> \ No newline at end of file diff --git a/test/res/src/placeholders/WeakSet.res b/test/res/src/placeholders/WeakSet.res new file mode 100644 index 00000000..9bbaf1d1 --- /dev/null +++ b/test/res/src/placeholders/WeakSet.res @@ -0,0 +1 @@ +type t<'a> \ No newline at end of file diff --git a/test/res/yarn.lock b/test/res/yarn.lock new file mode 100644 index 00000000..dcddde29 --- /dev/null +++ b/test/res/yarn.lock @@ -0,0 +1,204 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/helper-validator-identifier@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" + integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@ocsigen/ts2ocaml@link:../..": + version "0.0.0" + uid "" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +browser-or-node@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/browser-or-node/-/browser-or-node-2.0.0.tgz#808ea90282a670931cdc0ea98166538a50dd0d89" + integrity sha512-3Lrks/Okgof+/cRguUNG+qRXSeq79SO3hY4QrXJayJofwJwHiGC0qi99uDjsfTwULUFSr1OGVsBkdIkygKjTUA== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6" + integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w== + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +rescript@11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/rescript/-/rescript-11.0.1.tgz#c74af134dc8a16d152169b2456d0720324835f54" + integrity sha512-7T4PRp/d0+CBNnY6PYKffFqo9tGZlvnZpboF/n+8SKS+JZ6VvXJO7W538VPZXf3EYx1COGAWWvkF9e/HgSAqHg== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +typescript@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.5.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" diff --git a/webpack.config.js b/webpack.config.js index 34d78ca3..77be6bcf 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,7 +6,7 @@ const webpack = require('webpack'); var CONFIG = { fsharpEntry: './src/Main.fs.js', - outputDir: './dist', + outputDir: './dist/js', } var path = require("path"); @@ -39,6 +39,14 @@ module.exports = { path: path.join(__dirname, CONFIG.outputDir), filename: 'ts2ocaml.js' }, + module: { + rules: [ + { + resourceQuery: /raw/, + type: 'asset/source' + } + ] + }, plugins: [ new webpack.BannerPlugin({ banner: "#!/usr/bin/env node", diff --git a/yarn.lock b/yarn.lock index 9767aa8f..212802a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2676,6 +2676,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +rescript@11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/rescript/-/rescript-11.0.1.tgz#c74af134dc8a16d152169b2456d0720324835f54" + integrity sha512-7T4PRp/d0+CBNnY6PYKffFqo9tGZlvnZpboF/n+8SKS+JZ6VvXJO7W538VPZXf3EYx1COGAWWvkF9e/HgSAqHg== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"