From e8791433a061272ee30960bc22345957587aaf59 Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Sat, 21 Aug 2021 10:50:18 +0100 Subject: [PATCH 01/41] checkpoint --- build.fsx | 32 + global.json | 6 - package-lock.json | 1579 +---------------- src/Fable.AST/Plugins.fs | 1 + src/Fable.Cli/Entry.fs | 2 + src/Fable.Cli/Fable.Cli.fsproj | 2 +- src/Fable.Cli/Pipeline.fs | 37 + src/Fable.Transforms/Fable.Transforms.fsproj | 1 + src/Fable.Transforms/Global/Compiler.fs | 2 +- src/Fable.Transforms/Lua/LuaPrinter.fs | 431 +++++ src/fable-library-lua/README.md | 4 + src/fable-library-lua/fable/Async.fs | 52 + src/fable-library-lua/fable/AsyncBuilder.fs | 214 +++ .../fable/Fable.Library.fsproj | 42 + src/fable-library-lua/fable/Native.fs | 104 ++ src/fable-library-lua/fable/Timer.fs | 26 + src/quicktest/QuickTest.fs | 98 +- src/quicktest/QuickTest.lua | 440 +++++ src/quicktest/run.lua | 4 + 19 files changed, 1454 insertions(+), 1623 deletions(-) delete mode 100644 global.json create mode 100644 src/Fable.Transforms/Lua/LuaPrinter.fs create mode 100644 src/fable-library-lua/README.md create mode 100644 src/fable-library-lua/fable/Async.fs create mode 100644 src/fable-library-lua/fable/AsyncBuilder.fs create mode 100644 src/fable-library-lua/fable/Fable.Library.fsproj create mode 100644 src/fable-library-lua/fable/Native.fs create mode 100644 src/fable-library-lua/fable/Timer.fs create mode 100644 src/quicktest/QuickTest.lua create mode 100644 src/quicktest/run.lua diff --git a/build.fsx b/build.fsx index 731e50bbd5..04b9cfbc77 100644 --- a/build.fsx +++ b/build.fsx @@ -201,6 +201,34 @@ let buildLibraryPy() = runInDir buildDirPy ("python3 --version") runInDir buildDirPy ("python3 ./setup.py develop") +let buildLibraryLua() = + let libraryDir = "src/fable-library-lua" + let projectDir = libraryDir + "/fable" + let buildDirLua = "build/fable-library-lua" + + cleanDirs [buildDirLua] + + runFableWithArgs projectDir [ + "--outDir " + buildDirLua "fable" + "--fableLib " + buildDirLua "fable" + "--lang Lua" + "--exclude Fable.Core" + "--define FABLE_LIBRARY" + ] + // Copy *.py from projectDir to buildDir + copyDirRecursive libraryDir buildDirLua + //copyDirNonRecursive (buildDirLua "fable/fable-library") (buildDirLua "fable") + //copyFile (buildDirPy "fable/fable-library/*.py") (buildDirPy "fable") + // copyFile (buildDirLua "fable/system.text.lua") (buildDirLua "fable/system_text.lua") + // copyFile (buildDirLua "fable/fsharp.core.lua") (buildDirLua "fable/fsharp_core.lua") + // copyFile (buildDirLua "fable/fsharp.collections.lua") (buildDirLua "fable/fsharp_collections.lua") + // copyFile (buildDirLua "fable/system.collections.generic.lua") (buildDirLua "fable/system_collections_generic.lua") + //copyFile (buildDirPy "fable/async.lua") (buildDirPy "fable/async_.lua") + //removeFile (buildDirLua "fable/system.text.lua") + + runInDir buildDirLua ("lua -v") + //runInDir buildDirLua ("lua ./setup.lua develop") + let buildPyLibraryIfNotExists() = let baseDir = __SOURCE_DIRECTORY__ if not (pathExists (baseDir "build/fable-library-py")) then @@ -607,6 +635,9 @@ match argsLower with | "quicktest-py"::_ -> buildPyLibraryIfNotExists() run "dotnet watch -p src/Fable.Cli run -- watch --cwd ../quicktest --lang Python --exclude Fable.Core --noCache" +| "quicktest-lua"::_ -> + buildPyLibraryIfNotExists() + run "dotnet watch -p src/Fable.Cli run -- watch --cwd ../quicktest --lang Lua --exclude Fable.Core --noCache" | "jupyter" :: _ -> buildPyLibraryIfNotExists () run "dotnet watch -p src/Fable.Cli run -- watch --cwd ../Fable.Jupyter/src --lang Python --exclude Fable.Core --noCache 2>> ../Fable.Jupyter/src/fable.out" @@ -629,6 +660,7 @@ match argsLower with | ("fable-library"|"library")::_ -> buildLibraryJs() | ("fable-library-ts"|"library-ts")::_ -> buildLibraryTs() | ("fable-library-py"|"library-py")::_ -> buildLibraryPy() +| ("fable-library-lua" | "library-lua")::_ -> buildLibraryLua() | ("fable-compiler-js"|"compiler-js")::_ -> buildCompilerJs(minify) | ("fable-standalone"|"standalone")::_ -> buildStandalone {|minify=minify; watch=false|} | "watch-standalone"::_ -> buildStandalone {|minify=false; watch=true|} diff --git a/global.json b/global.json deleted file mode 100644 index acd6055a84..0000000000 --- a/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "5.0.300", - "rollForward": "minor" - } -} diff --git a/package-lock.json b/package-lock.json index bb5bd34f2a..45a93046a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1567 +1,6 @@ { - "name": "Fable", - "lockfileVersion": 2, "requires": true, - "packages": { - "": { - "dependencies": { - "@types/node": "^14.14.40", - "esm": "^3.2.25", - "ghreleases": "^3.0.2", - "mocha": "^8.3.2", - "rollup": "^2.45.2", - "terser": "^5.6.1", - "tslint": "^6.1.3", - "typescript": "^4.2.4" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", - "dependencies": { - "@babel/highlight": "^7.12.13" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" - }, - "node_modules/@babel/highlight": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", - "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.12.11", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "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" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@types/node": { - "version": "14.14.40", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.40.tgz", - "integrity": "sha512-2HoZZGylcnz19ZSbvWhgWHqvprw1ZGHanxIrDWYykPD4CauLW4gcyLzCVfUN2kv/1t1F3CurQIdi+s1l9+XgEA==" - }, - "node_modules/@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==" - }, - "node_modules/after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" - }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "engines": { - "node": ">=4" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/bl": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-3.0.1.tgz", - "integrity": "sha512-jrCW5ZhfQ/Vt07WX1Ngs+yn9BDqPL/gw28S7s9H6QK/gupnizNzJAss5akW20ISgOrbLTlXOOCTJeNUQqruAWQ==", - "dependencies": { - "readable-stream": "^3.0.1" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" - }, - "node_modules/buffer-from": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", - "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==" - }, - "node_modules/builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", - "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", - "dependencies": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.1" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dependencies": { - "ansi-regex": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/duplexer2": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", - "dependencies": { - "readable-stream": "~1.1.9" - } - }, - "node_modules/duplexer2/node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/duplexer2/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/ghreleases": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/ghreleases/-/ghreleases-3.0.2.tgz", - "integrity": "sha512-QiR9mIYvRG7hd8JuQYoxeBNOelVuTp2DpdiByRywbCDBSJufK9Vq7VuhD8B+5uviMxZx2AEkCzye61Us9gYgnw==", - "dependencies": { - "after": "~0.8.1", - "ghrepos": "~2.1.0", - "ghutils": "~3.2.0", - "lodash.uniq": "^4.5.0", - "simple-mime": "~0.1.0", - "url-template": "~2.0.6" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/ghrepos": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ghrepos/-/ghrepos-2.1.0.tgz", - "integrity": "sha512-6GM0ohSDTAv7xD6GsKfxJiV/CajoofRyUwu0E8l29d1o6lFAUxmmyMP/FH33afA20ZrXzxxcTtN6TsYvudMoAg==", - "dependencies": { - "ghutils": "~3.2.0" - } - }, - "node_modules/ghutils": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/ghutils/-/ghutils-3.2.6.tgz", - "integrity": "sha512-WpYHgLQkqU7Cv147wKUEThyj6qKHCdnAG2CL9RRsRQImVdLGdVqblJ3JUnj3ToQwgm1ALPS+FXgR0448AgGPUg==", - "dependencies": { - "jsonist": "~2.1.0", - "xtend": "~4.0.1" - } - }, - "node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "engines": { - "node": ">=4.x" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "bin": { - "he": "bin/he" - } - }, - "node_modules/hyperquest": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/hyperquest/-/hyperquest-2.1.3.tgz", - "integrity": "sha512-fUuDOrB47PqNK/BAMOS13v41UoaqIxqSLHX6CAbOD7OfT+/GCWO1/vPLfTNutOeXrv1ikuaZ3yux+33Z9vh+rw==", - "dependencies": { - "buffer-from": "^0.1.1", - "duplexer2": "~0.0.2", - "through2": "~0.6.3" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", - "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "engines": { - "node": ">=4" - } - }, - "node_modules/is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", - "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "node_modules/jsonist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/jsonist/-/jsonist-2.1.2.tgz", - "integrity": "sha512-8yqmWJAC2VaYoSKQAbsfgCpGY5o/1etWzx6ZxaZrC4iGaHrHUZEo+a2MyF8w+2uTavTlHdLWaZUoR19UfBstxQ==", - "dependencies": { - "bl": "~3.0.0", - "hyperquest": "~2.1.3", - "json-stringify-safe": "~5.0.1", - "xtend": "~4.0.1" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" - }, - "node_modules/log-symbols": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", - "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", - "dependencies": { - "chalk": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mocha": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.3.2.tgz", - "integrity": "sha512-UdmISwr/5w+uXLPKspgoV7/RXZwKRTiTjJ2/AC5ZiEztIoOYdfKb19+9jNmEInzx5pBsCyJQzarAxqIGBNYJhg==", - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.1", - "debug": "4.3.1", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.0.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.20", - "serialize-javascript": "5.0.1", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 10.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/nanoid": { - "version": "3.1.20", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", - "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "node_modules/picomatch": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", - "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/rollup": { - "version": "2.45.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.45.2.tgz", - "integrity": "sha512-kRRU7wXzFHUzBIv0GfoFFIN3m9oteY4uAsKllIpQDId5cfnkWF2J130l+27dzDju0E6MScKiV0ZM5Bw8m4blYQ==", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.1" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/serialize-javascript": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", - "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/simple-mime": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/simple-mime/-/simple-mime-0.1.0.tgz", - "integrity": "sha1-lfUXxPRm18/1YacfydqyWW6p7y4=", - "engines": [ - "node >= 0.2.0" - ] - }, - "node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/terser": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.6.1.tgz", - "integrity": "sha512-yv9YLFQQ+3ZqgWCUk+pvNJwgUTdlIxUk1WTN+RnaFJe2L7ipG2csPT0ra2XRm7Cs8cxN7QXmK1rFzEwYEQkzXw==", - "dependencies": { - "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.19" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "dependencies": { - "readable-stream": ">=1.0.33-1 <1.1.0-0", - "xtend": ">=4.0.0 <4.1.0-0" - } - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/through2/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/tslint": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", - "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", - "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^4.0.1", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.3", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.13.0", - "tsutils": "^2.29.0" - }, - "bin": { - "tslint": "bin/tslint" - }, - "engines": { - "node": ">=4.8.0" - }, - "peerDependencies": { - "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" - } - }, - "node_modules/tslint/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tslint/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/tslint/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "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" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tslint/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/tslint/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "node_modules/tslint/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/tslint/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/tslint/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "engines": { - "node": ">=4" - } - }, - "node_modules/tslint/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/tslint/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "dependencies": { - "tslib": "^1.8.1" - }, - "peerDependencies": { - "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" - } - }, - "node_modules/typescript": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/url-template": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha1-/FZaPMy/93MMd19WQflVV5FDnyE=" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dependencies": { - "string-width": "^1.0.2 || 2" - } - }, - "node_modules/workerpool": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz", - "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dependencies": { - "ansi-regex": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dependencies": { - "ansi-regex": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - }, + "lockfileVersion": 1, "dependencies": { "@babel/code-frame": { "version": "7.12.13", @@ -2376,14 +815,6 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -2393,6 +824,14 @@ "strip-ansi": "^4.0.0" } }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", diff --git a/src/Fable.AST/Plugins.fs b/src/Fable.AST/Plugins.fs index 83c39996cc..ef0987ca85 100644 --- a/src/Fable.AST/Plugins.fs +++ b/src/Fable.AST/Plugins.fs @@ -15,6 +15,7 @@ type Language = | Python | Php | Dart + | Lua type CompilerOptions = abstract TypedArrays: bool diff --git a/src/Fable.Cli/Entry.fs b/src/Fable.Cli/Entry.fs index c244483d04..c5868a2a6c 100644 --- a/src/Fable.Cli/Entry.fs +++ b/src/Fable.Cli/Entry.fs @@ -95,6 +95,7 @@ let defaultFileExt language args = | Python -> Path.replaceExtension ".py" fileExt | Php -> ".php" | Dart -> ".dart" + | Lua -> ".lua" | _ -> fileExt let argLanguage args = @@ -107,6 +108,7 @@ let argLanguage args = | "py" | "python" | "Python" -> Python | "php" | "Php" | "PHP" -> Php | "dart" -> Dart + | "lua" | "Lua" -> Lua | _ -> JavaScript) type Runner = diff --git a/src/Fable.Cli/Fable.Cli.fsproj b/src/Fable.Cli/Fable.Cli.fsproj index d0cebbef49..158c4b3fa0 100644 --- a/src/Fable.Cli/Fable.Cli.fsproj +++ b/src/Fable.Cli/Fable.Cli.fsproj @@ -4,7 +4,7 @@ Exe net5.0 3.2.9 - 3.2.9 + 3.0.0-local-build-20210819-1229 * Don't print JS files in watch mode if there're F# errors * Fix SRTP with local inline functions diff --git a/src/Fable.Cli/Pipeline.fs b/src/Fable.Cli/Pipeline.fs index e0d6b51bd7..11945064b4 100644 --- a/src/Fable.Cli/Pipeline.fs +++ b/src/Fable.Cli/Pipeline.fs @@ -141,9 +141,46 @@ module Dart = do! DartPrinter.run writer fable } +module Lua = + open Fable.Transforms.Lua + type LuaWriter(com: Compiler, cliArgs: CliArgs, dedupTargetDir, targetPath: string) = + let sourcePath = com.CurrentFile + let fileExt = cliArgs.CompilerOptions.FileExtension + let targetDir = Path.GetDirectoryName(targetPath) + let stream = new IO.StreamWriter(targetPath) + interface LuaPrinter.Writer with + member _.Write(str) = + stream.WriteAsync(str) |> Async.AwaitTask + member _.EscapeStringLiteral(str) = + // TODO: Check if this works for Dart + Web.HttpUtility.JavaScriptStringEncode(str) + member _.MakeImportPath(path) = + let projDir = IO.Path.GetDirectoryName(cliArgs.ProjectFile) + let path = Imports.getImportPath dedupTargetDir sourcePath targetPath projDir cliArgs.OutDir path + if path.EndsWith(".fs") then + let isInFableHiddenDir = Path.Combine(targetDir, path) |> Naming.isInFableHiddenDir + File.changeFsExtension isInFableHiddenDir path fileExt + else path + member _.AddLog(msg, severity, ?range) = + com.AddLog(msg, severity, ?range=range, fileName=com.CurrentFile) + member _.Dispose() = stream.Dispose() + + let compileFile (com: Compiler) (cliArgs: CliArgs) dedupTargetDir (outPath: string) = async { + let _imports, fable = + FSharp2Fable.Compiler.transformFile com + |> FableTransforms.transformFile com + |> Fable2Extended.Compiler.transformFile com + + use writer = new LuaWriter(com, cliArgs, dedupTargetDir, outPath) + //do! (writer :> LuaPrinter.Writer).Write(sprintf "AST: %A" fable.Declarations) + do! LuaPrinter.run writer fable + } + + let compileFile (com: Compiler) (cliArgs: CliArgs) dedupTargetDir (outPath: string) = match com.Options.Language with | JavaScript | TypeScript -> Js.compileFile com cliArgs dedupTargetDir outPath | Python -> Python.compileFile com cliArgs dedupTargetDir outPath | Php -> Php.compileFile com outPath | Dart -> Dart.compileFile com cliArgs dedupTargetDir outPath + | Lua -> Lua.compileFile com cliArgs dedupTargetDir outPath diff --git a/src/Fable.Transforms/Fable.Transforms.fsproj b/src/Fable.Transforms/Fable.Transforms.fsproj index 590cd037d3..6c7d6055b1 100644 --- a/src/Fable.Transforms/Fable.Transforms.fsproj +++ b/src/Fable.Transforms/Fable.Transforms.fsproj @@ -27,6 +27,7 @@ + diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index d23b23f7c7..7ff4b76aa3 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -1,7 +1,7 @@ namespace Fable module Literals = - let [] VERSION = "3.2.9" + let [] VERSION = "3.0.0-local-build-20210819-1229" type CompilerOptionsHelper = static member DefaultExtension = ".fs.js" diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs new file mode 100644 index 0000000000..06415b8004 --- /dev/null +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -0,0 +1,431 @@ +module Fable.Transforms.Lua.LuaPrinter + +open System +open Fable.AST +open Fable.AST.Fable + +type Writer = + inherit IDisposable + abstract Write: string -> Async + abstract EscapeStringLiteral: string -> string + abstract MakeImportPath: string -> string + abstract AddLog: msg:string * severity: Fable.Severity * ?range: SourceLocation -> unit + +type Printer = + abstract Line: int + abstract Column: int + abstract PushIndentation: unit -> unit + abstract PopIndentation: unit -> unit + abstract Print: string -> unit + abstract PrintNewLine: unit -> unit + abstract EscapeStringLiteral: string -> string + abstract MakeImportPath: string -> string + abstract AddLog: msg:string * severity: Fable.Severity * ?range: SourceLocation -> unit + +type PrinterImpl(writer: Writer) = + // TODO: We can make this configurable later + let indentSpaces = " " + let builder = Text.StringBuilder() + let mutable indent = 0 + let mutable line = 1 + let mutable column = 0 + + member _.Flush(): Async = + async { + do! writer.Write(builder.ToString()) + builder.Clear() |> ignore + } + + interface IDisposable with + member _.Dispose() = writer.Dispose() + + interface Printer with + member _.Line = line + member _.Column = column + + member _.PrintNewLine() = + builder.AppendLine() |> ignore + line <- line + 1 + column <- 0 + + member _.PushIndentation() = + indent <- indent + 1 + + member _.PopIndentation() = + if indent > 0 then indent <- indent - 1 + + member _.Print(str: string) = + if column = 0 then + let indent = String.replicate indent indentSpaces + builder.Append(indent) |> ignore + column <- indent.Length + + builder.Append(str) |> ignore + column <- column + str.Length + + member this.EscapeStringLiteral(str) = + writer.EscapeStringLiteral(str) + + member this.MakeImportPath(path) = + writer.MakeImportPath(path) + + member this.AddLog(msg, severity, ?range) = + writer.AddLog(msg, severity, ?range=range) + +module PrinterExtensions = + type Printer with + member this.AddError(msg, ?range) = + this.AddLog(msg, Fable.Severity.Error, ?range=range) + + member this.AddWarning(msg, ?range) = + this.AddLog(msg, Fable.Severity.Warning , ?range=range) + + member printer.PrintBlock(nodes: 'a array, printNode: Printer -> 'a -> unit, printSeparator: Printer -> unit, ?skipNewLineAtEnd) = + let skipNewLineAtEnd = defaultArg skipNewLineAtEnd false + printer.PrintNewLine() + printer.PushIndentation() + // for node in nodes do + // printNode printer node + // printSeparator printer + // printer.PopIndentation() + // printer.Print("end") + match nodes |> Array.toList |> List.rev with + | h::t -> + for node in t |> List.rev do + printNode printer node + printSeparator printer + printSeparator printer + printer.Print("return ") + printNode printer h + | _ -> () + if not skipNewLineAtEnd then + printer.PrintNewLine() + printer.PopIndentation() + + member printer.PrintBlock(nodes: Expr list, ?skipNewLineAtEnd) = + printer.PrintBlock(List.toArray nodes, + (fun p s -> p.Print(s)), // TODO: p.PrintProductiveStatement(s)), + (fun p -> p.PrintStatementSeparator()), + ?skipNewLineAtEnd=skipNewLineAtEnd) + + member printer.PrintStatementSeparator() = + if printer.Column > 0 then + printer.PrintNewLine() + + // TODO: Use Transforms.AST.canHaveSideEffects? + member this.HasSideEffects(e: Expr) = + match e with + | _ -> true + + member this.IsProductiveStatement(s: Expr) = + this.HasSideEffects(s) + + member printer.PrintProductiveStatement(s: Expr, ?printSeparator) = + if printer.IsProductiveStatement(s) then + printer.Print(s) + printSeparator |> Option.iter (fun f -> f printer) + + member printer.Print(t: Type) = + // match t with + // | Any -> printer.Print("Object") + // | Unit -> printer.Print("null") + // | Boolean -> printer.Print("bool") + // | Char -> printer.Print("null") + // | String -> printer.Print("String") + // | Number(kind,_) -> + // match kind with + // | Int8 | UInt8 | Int16 | UInt16 | Int32 | UInt32 -> printer.Print("int") + // | Float32 | Float64 -> printer.Print("double") + // | _ -> printer.AddError("TODO: Print type") + () + + // TODO + member printer.ComplexExpressionWithParens(expr: Expr) = + printer.Print(expr) + + member printer.PrintOperation(kind) = + match kind with + | Binary(operator, left, right) -> + printer.ComplexExpressionWithParens(left) + // TODO: review + match operator with + | BinaryEqual | BinaryEqualStrict -> printer.Print(" == ") + | BinaryUnequal | BinaryUnequalStrict -> printer.Print(" != ") + | BinaryLess -> printer.Print(" < ") + | BinaryLessOrEqual -> printer.Print(" <= ") + | BinaryGreater -> printer.Print(" > ") + | BinaryGreaterOrEqual -> printer.Print(" >= ") + | BinaryShiftLeft -> printer.Print(" << ") + | BinaryShiftRightSignPropagating -> printer.Print(" >> ") + | BinaryShiftRightZeroFill -> printer.Print(" >>> ") + | BinaryMinus -> printer.Print(" - ") + | BinaryPlus -> printer.Print(" + ") + | BinaryMultiply -> printer.Print(" * ") + | BinaryDivide -> printer.Print(" / ") + | BinaryModulus -> printer.Print(" % ") + | BinaryExponent -> printer.Print(" ** ") + | BinaryOrBitwise -> printer.Print(" | ") + | BinaryXorBitwise -> printer.Print(" ^ ") + | BinaryAndBitwise -> printer.Print(" & ") + | BinaryIn | BinaryInstanceOf -> printer.AddError($"Operator not supported {operator}") + printer.ComplexExpressionWithParens(right) + + | Logical(operator, left, right) -> + printer.ComplexExpressionWithParens(left) + match operator with + | LogicalOr -> printer.Print(" || ") + | LogicalAnd -> printer.Print(" && ") + printer.ComplexExpressionWithParens(right) + + | Unary(operator, operand) -> + match operator with + | UnaryMinus -> printer.Print("-") + | UnaryPlus -> printer.Print("+") + | UnaryNot -> printer.Print("!") + | UnaryNotBitwise -> printer.Print("~") + | UnaryTypeof | UnaryVoid | UnaryDelete -> printer.AddError($"Operator not supported {operator}") + printer.ComplexExpressionWithParens(operand) + + member printer.PrintValue(kind: ValueKind) = + match kind with + | BoolConstant v -> printer.Print((if v then "true" else "false")) + | StringConstant value -> + printer.Print("\"") + printer.Print(printer.EscapeStringLiteral(value)) + printer.Print("\"") +// | CharConstant of value: char + | NumberConstant(value,_,_) -> + let value = + match value.ToString(System.Globalization.CultureInfo.InvariantCulture) with + | "∞" -> "double.infinity" + | "-∞" -> "-double.infinity" + | value -> value + printer.Print(value) + | ThisValue _ -> printer.Print("this") + | Null _ | UnitConstant -> printer.Print("null") +// | NewArray of values: Expr list * typ: Type +// | NewArrayFrom of value: Expr * typ: Type +// | BaseValue of boundIdent: Ident option * typ: Type +// | TypeInfo of typ: Type +// | RegexConstant of source: string * flags: RegexFlag list +// | EnumConstant of value: Expr * ref: EntityRef +// | NewOption of value: Expr option * typ: Type * isStruct: bool +// | NewList of headAndTail: (Expr * Expr) option * typ: Type +// | NewTuple of values: Expr list * isStruct: bool +// | NewRecord of values: Expr list * ref: EntityRef * genArgs: Type list +// | NewAnonymousRecord of values: Expr list * fieldNames: string [] * genArgs: Type list +// | NewUnion of values: Expr list * tag: int * ref: EntityRef * genArgs: Type list + | _ -> printer.AddError("TODO: Print value") + + member printer.Print(expr: Expr) = + match expr with + | IdentExpr i -> printer.Print(i.Name) + | Value(kind,_) -> printer.PrintValue(kind) + | Operation(kind,_,_) -> printer.PrintOperation(kind) + | Extended(kind,_) -> + match kind with + //| Return (Value (UnitConstant, _)) -> () + | Return e -> + printer.Print("return ") + //printer.PrintSelfExecutingFnOpen() + printer.Print(e) + //printer.PrintSelfExecutingFnClose() + | _ -> printer.AddError("TODO: Print extended set") + | Import ({ Selector = "toConsole" }, b, c) -> + printer.Print("print") + | Import ({ Selector = "printfn" }, b, c) -> + printer.Print("") + | Import ({ Selector = "printf" }, b, c) -> + printer.Print("") + | Import (importInfo, b, c) -> + printer.Print("require(\"") + printer.Print(importInfo.Path) + printer.Print("""")""") + printer.Print(".") + printer.Print(importInfo.Selector) + //printer.PrintNewLine() + //printer.Print(sprintf "require(%A)" a) + | Call (expr, callInfo, t, d) -> + //printer.Print(sprintf "%A %A" expr callInfo) + match callInfo.CallMemberInfo with + | Some m -> + printer.Print(m.CompiledName) + printer.Print(".") + printer.Print(expr) + + | None -> + printer.Print(expr) + //printer.Print(sprintf "(%A)" expr) + printer.Print("(") + callInfo.Args |> Seq.toArray |> printer.PrintCommaSeparatedArgsArray + printer.Print(")") + | Lambda(arg, body, name) -> + printer.Print("function(") + printer.Print(arg.Name) + printer.Print(")") + printer.PushIndentation() + printer.PrintNewLine() + printer.Print(body) + printer.PopIndentation() + printer.PrintNewLine() + printer.Print("end") + | Delegate(args, body, name) -> printer.Print("todo delegate") + | ObjectExpr(members, typ, baseCall) -> printer.Print("todo object") + | TypeCast(expr, _) -> + //printer.Print(sprintf "cast %A" expr) + printer.Print(expr) + () + | Test(expr, kind, range) -> printer.Print("todo test") + | CurriedApply(applied, args, _, _) -> + printer.Print(applied) + printer.Print("(") + args |> Seq.toArray |> printer.PrintCommaSeparatedArgsArray + printer.Print(")") + //printer.Print("todoApply") + // | CurriedApply(applied, args, typ, range) -> + // printer.Print(applied) + // args |> Seq.toArray |> printer.PrintCommaSeparatedArgsArray + // printer.Print(sprintf "_curriedApply(%A)" typ) + | Emit(info, typ, range) -> + //printer.Print(info.Macro) + printer.AddError("Emit macros not supported") + | DecisionTree(expr, targets) -> printer.Print("todo decision tree") + | DecisionTreeSuccess(targetIndex, boundValues, typ) -> printer.Print("todo decision tree success") + | Let(ident, value, body) -> + printer.Print(ident.Name) + // printer.Print(value) + printer.Print(" = ") + printer.Print(value) + printer.PrintNewLine() + //printer.Print("(") + printer.Print(body) + //printer.Print(")") + //printer.Print(sprintf "let %A %A %A" ident value body) + | LetRec(bindings, body) -> printer.Print("todo let rec") + | Get(expr, kind, typ, range) -> + printer.Print(expr) + //printer.Print("todo get") + | Set(expr, kind, typ, value, range) -> + printer.Print(expr) + printer.Print(" = ") + printer.Print(value) + | Sequential(exprs) -> + match exprs |> List.rev with + | h::t -> + for e in t |> List.rev do + printer.PrintNewLine() + printer.Print(e) + printer.PrintNewLine() + printer.Print("return ") + printer.Print(h) + printer.PrintNewLine() + | _ -> () + | WhileLoop(guard, body, label, range) -> printer.Print("todo while") + | ForLoop(ident, start, limit, body, isUp, range) -> printer.Print("todo for") + | TryCatch(body, catch, finalizer, range) -> + printer.PrintSelfExecutingFnOpen() + printer.Print("local status, retval = pcall(function()") + printer.PushIndentation() + printer.PrintNewLine() + printer.Print body + printer.PopIndentation() + //if !status then catch else return retval + printer.Print("return retval") + printer.PrintNewLine() + printer.Print("end)") + printer.PrintSelfExecutingFnClose() + | IfThenElse(guardExpr, thenExpr, elseExpr, range) -> + printer.PrintSelfExecutingFnOpen() + printer.Print "if " + printer.Print guardExpr + printer.Print " then" + printer.PushIndentation() + printer.PrintNewLine() + printer.Print("return ") + printer.Print thenExpr + printer.PopIndentation() + printer.PrintNewLine() + printer.Print "else " + printer.PushIndentation() + printer.PrintNewLine() + printer.Print("return ") + printer.Print elseExpr + printer.PopIndentation() + printer.PrintSelfExecutingFnClose() + // | x -> + // printer.Print("todo") + //printer.Print($"TODO: Print expression {x}") + //printer.AddError("TODO: Print expression") + + member printer.PrintArray(items: 'a array, printItem: Printer -> 'a -> unit, printSeparator: Printer -> unit) = + for i = 0 to items.Length - 1 do + printItem printer items.[i] + if i < items.Length - 1 then + printSeparator printer + + member printer.PrintCommaSeparatedArray(nodes: Ident array) = + printer.PrintArray(nodes, (fun p x -> + // p.Print(x.Type) + // p.Print(" ") + p.Print(x.Name) + ), (fun p -> p.Print(", "))) + member printer.PrintCommaSeparatedArgsArray(nodes: Expr array) = + printer.PrintArray(nodes, fun p x -> + //p.Print(sprintf "%A" x) + p.Print(x) + , fun p -> p.Print(", ")) + + member printer.PrintFunctionDeclaration(name: string, args: Ident list, body: Expr) = + printer.Print("function ") + printer.Print("mod.") //function should be always declared in the local module scope, so they can be imported correctly + printer.Print(body.Type) + printer.Print("") + printer.Print(name) + printer.Print(" (") + printer.PrintCommaSeparatedArray(List.toArray args) + printer.Print(") ") + + printer.PrintBlock([body], skipNewLineAtEnd=false) + printer.Print("end") + member printer.PrintSelfExecutingFnOpen() = + printer.Print("(function ()") + printer.PushIndentation() + printer.PrintNewLine() + member printer.PrintSelfExecutingFnClose() = + printer.PopIndentation() + printer.PrintNewLine() + printer.Print("end)()") + + member printer.Print(md: Declaration) = + match md with + | ModuleDeclaration _ -> printer.AddError("Nested modules are not supported") + | ActionDeclaration _ -> printer.AddError("TODO: Action declaration") + | ClassDeclaration _ -> printer.AddError("TODO: Class declaration") + | MemberDeclaration m -> + printer.PrintFunctionDeclaration(m.Name, m.Args, m.Body) + +open PrinterExtensions + +let run (writer: Writer) (file: File): Async = + let printDeclWithExtraLine extraLine (printer: Printer) (decl: Declaration) = + printer.Print(decl) + + if extraLine then + printer.PrintNewLine() + + async { + use printer = new PrinterImpl(writer) + + // TODO: Imports + (printer :> Printer).Print("local mod = {}") + (printer :> Printer).PrintNewLine() + do! printer.Flush() + + for decl in file.Declarations do + printDeclWithExtraLine true printer decl + do! printer.Flush() + + (printer :> Printer).Print("return mod") + (printer :> Printer).PrintNewLine() + do! printer.Flush() + } diff --git a/src/fable-library-lua/README.md b/src/fable-library-lua/README.md new file mode 100644 index 0000000000..45b44c862f --- /dev/null +++ b/src/fable-library-lua/README.md @@ -0,0 +1,4 @@ +# Fable Library for Lua + +This module is used as the [Fable](https://fable.io/) library for +Lua. \ No newline at end of file diff --git a/src/fable-library-lua/fable/Async.fs b/src/fable-library-lua/fable/Async.fs new file mode 100644 index 0000000000..75e65e77b8 --- /dev/null +++ b/src/fable-library-lua/fable/Async.fs @@ -0,0 +1,52 @@ +module Async + +open System +open AsyncBuilder +open Fable.Core + +let emptyContinuation<'T> (_x: 'T) = + // NOP + () + +let defaultCancellationToken = new CancellationToken() + + +[] +type Async = + static member StartWithContinuations + ( + computation: IAsync<'T>, + continuation, + exceptionContinuation, + cancellationContinuation, + ?cancellationToken + ) : unit = + let trampoline = Trampoline() + + computation ( + { new IAsyncContext<'T> with + member this.onSuccess = continuation + member this.onError = exceptionContinuation + member this.onCancel = cancellationContinuation + member this.cancelToken = defaultArg cancellationToken defaultCancellationToken + member this.trampoline = trampoline } + ) + + static member StartWithContinuations(computation: IAsync<'T>, ?cancellationToken) : unit = + Async.StartWithContinuations( + computation, + emptyContinuation, + emptyContinuation, + emptyContinuation, + ?cancellationToken = cancellationToken + ) + + static member Start(computation, ?cancellationToken) = + Async.StartWithContinuations(computation, ?cancellationToken = cancellationToken) + + + static member StartImmediate(computation: IAsync<'T>, ?cancellationToken) = + Async.Start(computation, ?cancellationToken = cancellationToken) + +let startImmediate(computation: IAsync<'T>) = + Async.StartImmediate(computation, ?cancellationToken=None) diff --git a/src/fable-library-lua/fable/AsyncBuilder.fs b/src/fable-library-lua/fable/AsyncBuilder.fs new file mode 100644 index 0000000000..b795eb8518 --- /dev/null +++ b/src/fable-library-lua/fable/AsyncBuilder.fs @@ -0,0 +1,214 @@ +module AsyncBuilder + +open System +open System.Collections.Generic +open Fable.Core +open Timer + +type Continuation<'T> = 'T -> unit + +type OperationCanceledError () = + inherit Exception ("The operation was canceled") + +type Continuations<'T> = Continuation<'T> * Continuation * Continuation + + +type CancellationToken (cancelled: bool) = + let mutable idx = 0 + let mutable cancelled = cancelled + let listeners = Dictionary unit>() + + new () = CancellationToken(false) + + member this.IsCancelled = cancelled + + member this.Cancel() = + if not cancelled then cancelled <- true + + for KeyValue (_, listener) in listeners do + listener () + + member this.AddListener(f: unit -> unit) = + let id = idx + idx <- idx + 1 + listeners.Add(idx, f) + id + + member this.RemoveListener(id: int) = listeners.Remove(id) + + member this.Register(f: unit -> unit) : IDisposable = + let id = this.AddListener(f) + + { new IDisposable with + member x.Dispose() = this.RemoveListener(id) |> ignore } + + member this.Register(f: obj -> unit, state: obj) : IDisposable = + let id = this.AddListener(fun () -> f (state)) + + { new IDisposable with + member x.Dispose() = this.RemoveListener(id) |> ignore } + +type Trampoline () = + let mutable callCount = 0 + + static member MaxTrampolineCallCount = 2000 + + member this.IncrementAndCheck() = + callCount <- callCount + 1 + callCount > Trampoline.MaxTrampolineCallCount + + member this.Hijack(f: unit -> unit) = + callCount <- 0 + let timer = Timer.Create(0., f) + timer.daemon <- true + timer.start () + +type IAsyncContext<'T> = + abstract member onSuccess : Continuation<'T> + abstract member onError : Continuation + abstract member onCancel : Continuation + + abstract member cancelToken : CancellationToken + abstract member trampoline : Trampoline + +type IAsync<'T> = IAsyncContext<'T> -> unit + +let protectedCont<'T> (f: IAsync<'T>) = + fun (ctx: IAsyncContext<'T>) -> + if ctx.cancelToken.IsCancelled then + ctx.onCancel (new OperationCanceledError()) + else if (ctx.trampoline.IncrementAndCheck()) then + ctx.trampoline.Hijack + (fun () -> + try + f ctx + with err -> ctx.onError (err)) + else + try + f ctx + with err -> ctx.onError (err) + +let protectedBind<'T, 'U> (computation: IAsync<'T>, binder: 'T -> IAsync<'U>) = + protectedCont + (fun (ctx: IAsyncContext<'U>) -> + computation ( + { new IAsyncContext<'T> with + member this.onSuccess = + fun (x: 'T) -> + try + binder (x) (ctx) + with ex -> ctx.onError (ex) + + member this.onError = ctx.onError + member this.onCancel = ctx.onCancel + member this.cancelToken = ctx.cancelToken + member this.trampoline = ctx.trampoline } + )) + +let protectedReturn<'T> (value: 'T) = protectedCont (fun (ctx: IAsyncContext<'T>) -> ctx.onSuccess (value)) + +type IAsyncBuilder = + abstract member Bind<'T, 'U> : IAsync<'T> * ('T -> IAsync<'U>) -> IAsync<'U> + + abstract member Combine<'T> : IAsync * IAsync<'T> -> IAsync<'T> + + abstract member Delay<'T> : (unit -> IAsync<'T>) -> IAsync<'T> + + //abstract member Return<'T> : [] values: 'T [] -> IAsync<'T> + abstract member Return<'T> : value: 'T -> IAsync<'T> + + abstract member While : (unit -> bool) * IAsync -> IAsync + abstract member Zero : unit -> IAsync + + +type AsyncBuilder () = + interface IAsyncBuilder with + + member this.Bind<'T, 'U>(computation: IAsync<'T>, binder: 'T -> IAsync<'U>) = protectedBind (computation, binder) + + member this.Combine<'T>(computation1: IAsync, computation2: IAsync<'T>) = + let self = this :> IAsyncBuilder + self.Bind(computation1, (fun () -> computation2)) + + member x.Delay<'T>(generator: unit -> IAsync<'T>) = protectedCont (fun (ctx: IAsyncContext<'T>) -> generator () (ctx)) + + + // public For(sequence: Iterable, body: (x: T) => IAsync) { + // const iter = sequence[Symbol.iterator](); + // let cur = iter.next(); + // return this.While(() => !cur.done, this.Delay(() => { + // const res = body(cur.value); + // cur = iter.next(); + // return res; + // })); + // } + + member this.Return<'T>(value: 'T) : IAsync<'T> = protectedReturn (unbox value) + // member this.Return<'T>([] value: 'T []) : IAsync<'T> = + // match value with + // | [||] -> protectedReturn (unbox null) + // | [| value |] -> protectedReturn value + // | _ -> failwith "Return takes zero or one argument." + + + // public ReturnFrom(computation: IAsync) { +// return computation; +// } + + // public TryFinally(computation: IAsync, compensation: () => void) { +// return protectedCont((ctx: IAsyncContext) => { +// computation({ +// onSuccess: (x: T) => { +// compensation(); +// ctx.onSuccess(x); +// }, +// onError: (x: any) => { +// compensation(); +// ctx.onError(x); +// }, +// onCancel: (x: any) => { +// compensation(); +// ctx.onCancel(x); +// }, +// cancelToken: ctx.cancelToken, +// trampoline: ctx.trampoline, +// }); +// }); +// } + + // public TryWith(computation: IAsync, catchHandler: (e: any) => IAsync) { +// return protectedCont((ctx: IAsyncContext) => { +// computation({ +// onSuccess: ctx.onSuccess, +// onCancel: ctx.onCancel, +// cancelToken: ctx.cancelToken, +// trampoline: ctx.trampoline, +// onError: (ex: any) => { +// try { +// catchHandler(ex)(ctx); +// } catch (ex2) { +// ctx.onError(ex2); +// } +// }, +// }); +// }); +// } + + // public Using(resource: T, binder: (x: T) => IAsync) { +// return this.TryFinally(binder(resource), () => resource.Dispose()); +// } + + member this.While(guard: unit -> bool, computation: IAsync) : IAsync = + let self = this :> IAsyncBuilder + + if guard () then + self.Bind(computation, (fun () -> self.While(guard, computation))) + else + self.Return() + + // member this.Bind<'T, 'U>(computation: IAsync<'T>, binder: 'T -> IAsync<'U>) = (this :> IAsyncBuilder).Bind(computation, binder) + member this.Zero() : IAsync = protectedCont (fun (ctx: IAsyncContext) -> ctx.onSuccess (())) + +// } + +let singleton : IAsyncBuilder = AsyncBuilder() :> _ diff --git a/src/fable-library-lua/fable/Fable.Library.fsproj b/src/fable-library-lua/fable/Fable.Library.fsproj new file mode 100644 index 0000000000..fa985a71f1 --- /dev/null +++ b/src/fable-library-lua/fable/Fable.Library.fsproj @@ -0,0 +1,42 @@ + + + + netstandard2.0 + $(DefineConstants);FABLE_COMPILER + $(DefineConstants);FX_NO_BIGINT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/fable-library-lua/fable/Native.fs b/src/fable-library-lua/fable/Native.fs new file mode 100644 index 0000000000..71a719a9e6 --- /dev/null +++ b/src/fable-library-lua/fable/Native.fs @@ -0,0 +1,104 @@ +module Native + +// Disables warn:1204 raised by use of LanguagePrimitives.ErrorStrings.* +#nowarn "1204" + +open System.Collections.Generic +open Fable.Core +open Fable.Core.PyInterop +open Fable.Import + +[] +type Cons<'T> = + [] + abstract Allocate : len: int -> 'T [] + +module Helpers = + [] + let arrayFrom (xs: 'T seq) : 'T [] = nativeOnly + + [] + let allocateArray (len: int) : 'T [] = nativeOnly + + [] + let allocateArrayFrom (xs: 'T []) (len: int) : 'T [] = nativeOnly + + let allocateArrayFromCons (cons: Cons<'T>) (len: int) : 'T [] = + if isNull cons then + PY.Constructors.Array.Create(len) + else + cons.Allocate(len) + let inline isDynamicArrayImpl arr = PY.Constructors.Array.isArray arr + + // let inline typedArraySetImpl (target: obj) (source: obj) (offset: int): unit = + // !!target?set(source, offset) + + [] + let concatImpl (array1: 'T []) (arrays: 'T [] seq) : 'T [] = nativeOnly + + let fillImpl (array: 'T []) (value: 'T) (start: int) (count: int) : 'T [] = + for i = 0 to count - 1 do + array.[i+start] <- value + array + + [] + let foldImpl (folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T []) : 'State = nativeOnly + + let inline foldIndexedImpl (folder: 'State -> 'T -> int -> 'State) (state: 'State) (array: 'T []) : 'State = + !! array?reduce (System.Func<'State, 'T, int, 'State>(folder), state) + + let inline foldBackImpl (folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T []) : 'State = + !! array?reduceRight (System.Func<'State, 'T, 'State>(folder), state) + + let inline foldBackIndexedImpl (folder: 'State -> 'T -> int -> 'State) (state: 'State) (array: 'T []) : 'State = + !! array?reduceRight (System.Func<'State, 'T, int, 'State>(folder), state) + + // Typed arrays not supported, only dynamic ones do + let inline pushImpl (array: 'T []) (item: 'T) : int = !! array?append (item) + + // Typed arrays not supported, only dynamic ones do + let inline insertImpl (array: 'T []) (index: int) (item: 'T) : 'T [] = !! array?splice (index, 0, item) + + // Typed arrays not supported, only dynamic ones do + let inline spliceImpl (array: 'T []) (start: int) (deleteCount: int) : 'T [] = !! array?splice (start, deleteCount) + + [] + let reverseImpl (array: 'T []) : 'T [] = nativeOnly + + [] + let copyImpl (array: 'T []) : 'T [] = nativeOnly + + [] + let skipImpl (array: 'T []) (count: int) : 'T [] = nativeOnly + + [] + let subArrayImpl (array: 'T []) (start: int) (count: int) : 'T [] = nativeOnly + + let inline indexOfImpl (array: 'T []) (item: 'T) (start: int) : int = !! array?indexOf (item, start) + + let inline findImpl (predicate: 'T -> bool) (array: 'T []) : 'T option = !! array?find (predicate) + + let inline findIndexImpl (predicate: 'T -> bool) (array: 'T []) : int = !! array?findIndex (predicate) + + let inline collectImpl (mapping: 'T -> 'U []) (array: 'T []) : 'U [] = !! array?flatMap (mapping) + + let inline containsImpl (predicate: 'T -> bool) (array: 'T []) : bool = !! array?filter (predicate) + + let inline existsImpl (predicate: 'T -> bool) (array: 'T []) : bool = !! array?some (predicate) + + let inline forAllImpl (predicate: 'T -> bool) (array: 'T []) : bool = !! array?every (predicate) + + let inline filterImpl (predicate: 'T -> bool) (array: 'T []) : 'T [] = !! array?filter (predicate) + + [] + let reduceImpl (reduction: 'T -> 'T -> 'T) (array: 'T []) : 'T = nativeOnly + + let inline reduceBackImpl (reduction: 'T -> 'T -> 'T) (array: 'T []) : 'T = !! array?reduceRight (reduction) + + // Inlining in combination with dynamic application may cause problems with uncurrying + // Using Emit keeps the argument signature. Note: Python cannot take an argument here. + [] + let sortInPlaceWithImpl (comparer: 'T -> 'T -> int) (array: 'T []) : unit = nativeOnly //!!array?sort(comparer) + + [] + let copyToTypedArray (src: 'T []) (srci: int) (trg: 'T []) (trgi: int) (cnt: int) : unit = nativeOnly diff --git a/src/fable-library-lua/fable/Timer.fs b/src/fable-library-lua/fable/Timer.fs new file mode 100644 index 0000000000..51fb7fe1ba --- /dev/null +++ b/src/fable-library-lua/fable/Timer.fs @@ -0,0 +1,26 @@ +module Timer + +open Fable.Core + +/// This class represents an action that should be run only after a +/// certain amount of time has passed — a timer. Timer is a subclass of +/// Thread and as such also functions as an example of creating custom +/// threads. +type ITimer = + abstract daemon : bool with get, set + + /// Start the thread’s activity. + abstract start : unit -> unit + /// Stop the timer, and cancel the execution of the timer’s action. + /// This will only work if the timer is still in its waiting stage. + abstract cancel : unit -> unit + + /// Create a timer that will run function with arguments args and + /// keyword arguments kwargs, after interval seconds have passed. If + /// args is None (the default) then an empty list will be used. If + /// kwargs is None (the default) then an empty dict will be used. + [] + abstract Create : float * (unit -> unit) -> ITimer + +[] +let Timer : ITimer = nativeOnly diff --git a/src/quicktest/QuickTest.fs b/src/quicktest/QuickTest.fs index a61381c5c7..7e2f993d07 100644 --- a/src/quicktest/QuickTest.fs +++ b/src/quicktest/QuickTest.fs @@ -11,56 +11,64 @@ open Fable.Core open Fable.Core.JsInterop open Fable.Core.Testing -let log (o: obj) = - printfn "%A" o +// let log (o: obj) = +// printfn "%A" o -let equal expected actual = - let areEqual = expected = actual - printfn "%A = %A > %b" expected actual areEqual - if not areEqual then - failwithf "[ASSERT ERROR] Expected %A but got %A" expected actual +let hello = printfn "hello world" +let a = 2 + 2 +let b = 3 - 1 +let c = a + b +let execute () = + a.ToString() |> printf "%s" + printf "c" -let throwsError (expected: string) (f: unit -> 'a): unit = - let success = - try - f () |> ignore - true - with e -> - if not <| String.IsNullOrEmpty(expected) then - equal e.Message expected - false - // TODO better error messages - equal false success +// let equal expected actual = +// let areEqual = expected = actual +// printfn "%A = %A > %b" expected actual areEqual +// if not areEqual then +// failwithf "[ASSERT ERROR] Expected %A but got %A" expected actual -let testCase (msg: string) f: unit = - try - printfn "%s" msg - f () - with ex -> - printfn "%s" ex.Message - if ex.Message <> null && ex.Message.StartsWith("[ASSERT ERROR]") |> not then - printfn "%s" ex.StackTrace - printfn "" +// let throwsError (expected: string) (f: unit -> 'a): unit = +// let success = +// try +// f () |> ignore +// true +// with e -> +// if not <| String.IsNullOrEmpty(expected) then +// equal e.Message expected +// false +// // TODO better error messages +// equal false success -let testCaseAsync msg f = - testCase msg (fun () -> - async { - try - do! f () - with ex -> - printfn "%s" ex.Message - if ex.Message <> null && ex.Message.StartsWith("[ASSERT ERROR]") |> not then - printfn "%s" ex.StackTrace - } |> Async.StartImmediate) +// let testCase (msg: string) f: unit = +// try +// printfn "%s" msg +// f () +// with ex -> +// printfn "%s" ex.Message +// if ex.Message <> null && ex.Message.StartsWith("[ASSERT ERROR]") |> not then +// printfn "%s" ex.StackTrace +// printfn "" -let measureTime (f: unit -> unit) = emitJsStatement () """ - //js - const startTime = process.hrtime(); - f(); - const elapsed = process.hrtime(startTime); - console.log("Ms:", elapsed[0] * 1e3 + elapsed[1] / 1e6); - //!js -""" +// let testCaseAsync msg f = +// testCase msg (fun () -> +// async { +// try +// do! f () +// with ex -> +// printfn "%s" ex.Message +// if ex.Message <> null && ex.Message.StartsWith("[ASSERT ERROR]") |> not then +// printfn "%s" ex.StackTrace +// } |> Async.StartImmediate) + +// let measureTime (f: unit -> unit) = emitJsStatement () """ +// //js +// const startTime = process.hrtime(); +// f(); +// const elapsed = process.hrtime(startTime); +// console.log("Ms:", elapsed[0] * 1e3 + elapsed[1] / 1e6); +// //!js +// """ // Write here your unit test, you can later move it // to Fable.Tests project. For example: diff --git a/src/quicktest/QuickTest.lua b/src/quicktest/QuickTest.lua new file mode 100644 index 0000000000..6c245d79e5 --- /dev/null +++ b/src/quicktest/QuickTest.lua @@ -0,0 +1,440 @@ +AST: [MemberDeclaration + { Name = "hello" + FullDisplayName = "QuickTest.hello" + Args = [] + Body = + Call + (Import + ({ Selector = "toConsole" + Path = + "./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js" + Kind = LibraryImport }, Any, None), + { ThisArg = None + Args = + [TypeCast + (Call + (Import + ({ Selector = "printf" + Path = + "./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js" + Kind = LibraryImport }, Any, None), + { ThisArg = None + Args = + [Value + (StringConstant "hello world", + Some { start = { line = 17 + column = 20 } + end = { line = 17 + column = 33 } + identifierName = None })] + SignatureArgTypes = [String] + CallMemberInfo = None + HasSpread = false + IsConstructor = false + OptimizableInto = None }, + DeclaredType + ({ FullName = "Microsoft.FSharp.Core.PrintfFormat`5" + Path = + AssemblyPath + "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, + [Unit; + DeclaredType + ({ FullName = "System.IO.TextWriter" + Path = CoreAssemblyName "System.Runtime" }, []); + Unit; Unit; Unit]), Some { start = { line = 17 + column = 20 } + end = { line = 17 + column = 33 } + identifierName = None }), + DeclaredType + ({ FullName = "Microsoft.FSharp.Core.PrintfFormat`4" + Path = + AssemblyPath + "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, + [Unit; + DeclaredType + ({ FullName = "System.IO.TextWriter" + Path = CoreAssemblyName "System.Runtime" }, []); Unit; + Unit]))] + SignatureArgTypes = + [DeclaredType + ({ FullName = "Microsoft.FSharp.Core.PrintfFormat`4" + Path = + AssemblyPath + "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, + [GenericParam ("T", []); + DeclaredType ({ FullName = "System.IO.TextWriter" + Path = CoreAssemblyName "System.Runtime" }, []); + Unit; Unit])] + CallMemberInfo = None + HasSpread = false + IsConstructor = false + OptimizableInto = None }, Unit, Some { start = { line = 17 + column = 12 } + end = { line = 17 + column = 33 } + identifierName = None }) + Info = Fable.Transforms.FSharp2Fable.MemberInfo + UsedNames = set [] + ExportDefault = false }; + MemberDeclaration + { Name = "a" + FullDisplayName = "QuickTest.a" + Args = [] + Body = + Operation + (Binary + (BinaryPlus, + Value + (NumberConstant (2.0, Int32, None), Some { start = { line = 18 + column = 8 } + end = { line = 18 + column = 9 } + identifierName = None }), + Value + (NumberConstant (2.0, Int32, None), Some { start = { line = 18 + column = 12 } + end = { line = 18 + column = 13 } + identifierName = None })), + Number (Int32, None), Some { start = { line = 18 + column = 8 } + end = { line = 18 + column = 13 } + identifierName = None }) + Info = Fable.Transforms.FSharp2Fable.MemberInfo + UsedNames = set ["arg0_0"; "arg1_0"] + ExportDefault = false }; + MemberDeclaration + { Name = "b" + FullDisplayName = "QuickTest.b" + Args = [] + Body = + Operation + (Binary + (BinaryMinus, + Value + (NumberConstant (3.0, Int32, None), Some { start = { line = 19 + column = 8 } + end = { line = 19 + column = 9 } + identifierName = None }), + Value + (NumberConstant (1.0, Int32, None), Some { start = { line = 19 + column = 12 } + end = { line = 19 + column = 13 } + identifierName = None })), + Number (Int32, None), Some { start = { line = 19 + column = 8 } + end = { line = 19 + column = 13 } + identifierName = None }) + Info = Fable.Transforms.FSharp2Fable.MemberInfo + UsedNames = set ["arg0_0"; "arg1_0"] + ExportDefault = false }; + MemberDeclaration + { Name = "c" + FullDisplayName = "QuickTest.c" + Args = [] + Body = + Operation + (Binary + (BinaryPlus, IdentExpr { Name = "a" + Type = Number (Int32, None) + IsMutable = false + IsThisArgument = false + IsCompilerGenerated = true + Range = Some { start = { line = 20 + column = 8 } + end = { line = 20 + column = 9 } + identifierName = Some "a" } }, + IdentExpr { Name = "b" + Type = Number (Int32, None) + IsMutable = false + IsThisArgument = false + IsCompilerGenerated = true + Range = Some { start = { line = 20 + column = 12 } + end = { line = 20 + column = 13 } + identifierName = Some "b" } }), + Number (Int32, None), Some { start = { line = 20 + column = 8 } + end = { line = 20 + column = 13 } + identifierName = None }) + Info = Fable.Transforms.FSharp2Fable.MemberInfo + UsedNames = set ["arg0_0"; "arg1_0"] + ExportDefault = false }; + MemberDeclaration + { Name = "execute" + FullDisplayName = "QuickTest.execute" + Args = [{ Name = "unitVar0" + Type = Unit + IsMutable = false + IsThisArgument = false + IsCompilerGenerated = true + Range = Some { start = { line = 21 + column = 12 } + end = { line = 21 + column = 14 } + identifierName = Some "unitVar0" } }] + Body = + Extended + (Return + (Sequential + [Let + ({ Name = "arg10" + Type = String + IsMutable = false + IsThisArgument = false + IsCompilerGenerated = true + Range = Some { start = { line = 22 + column = 20 } + end = { line = 22 + column = 31 } + identifierName = Some "arg10" } }, + Let + ({ Name = "copyOfStruct" + Type = Number (Int32, None) + IsMutable = true + IsThisArgument = false + IsCompilerGenerated = true + Range = Some { start = { line = 22 + column = 4 } + end = { line = 22 + column = 16 } + identifierName = Some "copyOfStruct" } }, + IdentExpr { Name = "a" + Type = Number (Int32, None) + IsMutable = false + IsThisArgument = false + IsCompilerGenerated = true + Range = Some { start = { line = 22 + column = 4 } + end = { line = 22 + column = 5 } + identifierName = Some "a" } }, + Call + (Import + ({ Selector = "int32ToString" + Path = + "./.fable/fable-library.3.0.0-local-build-20210819-1229/Util.js" + Kind = LibraryImport }, Any, None), + { ThisArg = None + Args = + [IdentExpr + { Name = "copyOfStruct" + Type = Number (Int32, None) + IsMutable = true + IsThisArgument = false + IsCompilerGenerated = true + Range = + Some { start = { line = 22 + column = 4 } + end = { line = 22 + column = 16 } + identifierName = Some "copyOfStruct" } }] + SignatureArgTypes = [] + CallMemberInfo = None + HasSpread = false + IsConstructor = false + OptimizableInto = None }, String, None)), + CurriedApply + (Call + (Import + ({ Selector = "toConsole" + Path = + "./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js" + Kind = LibraryImport }, Any, None), + { ThisArg = None + Args = + [TypeCast + (Call + (Import + ({ Selector = "printf" + Path = + "./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js" + Kind = LibraryImport }, Any, None), + { ThisArg = None + Args = + [Value + (StringConstant "%s", + Some { start = { line = 22 + column = 27 } + end = { line = 22 + column = 31 } + identifierName = None })] + SignatureArgTypes = [String] + CallMemberInfo = None + HasSpread = false + IsConstructor = false + OptimizableInto = None }, + DeclaredType + ({ FullName = + "Microsoft.FSharp.Core.PrintfFormat`5" + Path = + AssemblyPath + "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, + [LambdaType (String, Unit); + DeclaredType + ({ FullName = "System.IO.TextWriter" + Path = + CoreAssemblyName "System.Runtime" }, + []); Unit; Unit; String]), + Some { start = { line = 22 + column = 27 } + end = { line = 22 + column = 31 } + identifierName = None }), + DeclaredType + ({ FullName = + "Microsoft.FSharp.Core.PrintfFormat`4" + Path = + AssemblyPath + "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, + [LambdaType (String, Unit); + DeclaredType + ({ FullName = "System.IO.TextWriter" + Path = CoreAssemblyName "System.Runtime" }, + []); Unit; Unit]))] + SignatureArgTypes = + [DeclaredType + ({ FullName = + "Microsoft.FSharp.Core.PrintfFormat`4" + Path = + AssemblyPath + "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, + [GenericParam ("T", []); + DeclaredType + ({ FullName = "System.IO.TextWriter" + Path = CoreAssemblyName "System.Runtime" }, + []); Unit; Unit])] + CallMemberInfo = None + HasSpread = false + IsConstructor = false + OptimizableInto = None }, LambdaType (String, Unit), + Some { start = { line = 22 + column = 20 } + end = { line = 22 + column = 31 } + identifierName = None }), + [IdentExpr + { Name = "arg10" + Type = String + IsMutable = false + IsThisArgument = false + IsCompilerGenerated = true + Range = Some { start = { line = 22 + column = 20 } + end = { line = 22 + column = 31 } + identifierName = Some "arg10" } }], + Unit, Some { start = { line = 22 + column = 20 } + end = { line = 22 + column = 31 } + identifierName = None })); + Call + (Import + ({ Selector = "toConsole" + Path = + "./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js" + Kind = LibraryImport }, Any, None), + { ThisArg = None + Args = + [TypeCast + (Call + (Import + ({ Selector = "printf" + Path = + "./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js" + Kind = LibraryImport }, Any, None), + { ThisArg = None + Args = + [Value + (StringConstant "c", + Some { start = { line = 23 + column = 11 } + end = { line = 23 + column = 14 } + identifierName = None })] + SignatureArgTypes = [String] + CallMemberInfo = None + HasSpread = false + IsConstructor = false + OptimizableInto = None }, + DeclaredType + ({ FullName = + "Microsoft.FSharp.Core.PrintfFormat`5" + Path = + AssemblyPath + "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, + [Unit; + DeclaredType + ({ FullName = "System.IO.TextWriter" + Path = CoreAssemblyName "System.Runtime" }, + []); Unit; Unit; Unit]), + Some { start = { line = 23 + column = 11 } + end = { line = 23 + column = 14 } + identifierName = None }), + DeclaredType + ({ FullName = "Microsoft.FSharp.Core.PrintfFormat`4" + Path = + AssemblyPath + "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, + [Unit; + DeclaredType + ({ FullName = "System.IO.TextWriter" + Path = CoreAssemblyName "System.Runtime" }, []); + Unit; Unit]))] + SignatureArgTypes = + [DeclaredType + ({ FullName = "Microsoft.FSharp.Core.PrintfFormat`4" + Path = + AssemblyPath + "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, + [GenericParam ("T", []); + DeclaredType + ({ FullName = "System.IO.TextWriter" + Path = CoreAssemblyName "System.Runtime" }, []); + Unit; Unit])] + CallMemberInfo = None + HasSpread = false + IsConstructor = false + OptimizableInto = None }, Unit, + Some { start = { line = 23 + column = 4 } + end = { line = 23 + column = 14 } + identifierName = None })]), None) + Info = Fable.Transforms.FSharp2Fable.MemberInfo + UsedNames = set ["arg10"; "clo1"; "copyOfStruct"; "unitVar0"] + ExportDefault = false }]local mod = {} +function mod.hello () + print(("hello world")) +end +function mod.a () + 2 + 2 +end +function mod.b () + 3 - 1 +end +function mod.c () + a + b +end +function mod.execute (unitVar0) + return (function () + + arg10 = copyOfStruct = a + require("./.fable/fable-library.3.0.0-local-build-20210819-1229/Util.js").int32ToString(copyOfStruct) + print(("%s"))(arg10) + return print(("c")) + + end)() +end +return mod diff --git a/src/quicktest/run.lua b/src/quicktest/run.lua new file mode 100644 index 0000000000..3c7da75282 --- /dev/null +++ b/src/quicktest/run.lua @@ -0,0 +1,4 @@ +-- local m = require './QuickTest' +require('./QuickTest').hello() +require('./QuickTest').a() +require('./QuickTest').b() \ No newline at end of file From 236131f6e1a46fd7f47d30667f1ac6e134f9fda1 Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Sat, 21 Aug 2021 17:23:55 +0100 Subject: [PATCH 02/41] Using babel ast --- src/Fable.Cli/Pipeline.fs | 14 +- src/Fable.Transforms/Lua/LuaPrinter.fs | 1276 +++++++++++++---- src/fable-library-lua/fable/Async.fs | 52 - src/fable-library-lua/fable/AsyncBuilder.fs | 214 --- .../fable/Fable.Library.fsproj | 2 +- src/quicktest/QuickTest.fs | 3 + src/quicktest/QuickTest.lua | 458 +----- 7 files changed, 996 insertions(+), 1023 deletions(-) delete mode 100644 src/fable-library-lua/fable/Async.fs delete mode 100644 src/fable-library-lua/fable/AsyncBuilder.fs diff --git a/src/Fable.Cli/Pipeline.fs b/src/Fable.Cli/Pipeline.fs index 11945064b4..46aafefaaf 100644 --- a/src/Fable.Cli/Pipeline.fs +++ b/src/Fable.Cli/Pipeline.fs @@ -148,11 +148,10 @@ module Lua = let fileExt = cliArgs.CompilerOptions.FileExtension let targetDir = Path.GetDirectoryName(targetPath) let stream = new IO.StreamWriter(targetPath) - interface LuaPrinter.Writer with + interface Writer with member _.Write(str) = stream.WriteAsync(str) |> Async.AwaitTask - member _.EscapeStringLiteral(str) = - // TODO: Check if this works for Dart + member _.EscapeJsStringLiteral(str) = Web.HttpUtility.JavaScriptStringEncode(str) member _.MakeImportPath(path) = let projDir = IO.Path.GetDirectoryName(cliArgs.ProjectFile) @@ -161,19 +160,18 @@ module Lua = let isInFableHiddenDir = Path.Combine(targetDir, path) |> Naming.isInFableHiddenDir File.changeFsExtension isInFableHiddenDir path fileExt else path - member _.AddLog(msg, severity, ?range) = - com.AddLog(msg, severity, ?range=range, fileName=com.CurrentFile) + member _.AddSourceMapping((srcLine, srcCol, genLine, genCol, name)) = () member _.Dispose() = stream.Dispose() let compileFile (com: Compiler) (cliArgs: CliArgs) dedupTargetDir (outPath: string) = async { - let _imports, fable = + let babel = FSharp2Fable.Compiler.transformFile com |> FableTransforms.transformFile com - |> Fable2Extended.Compiler.transformFile com + |> Fable2Babel.Compiler.transformFile com use writer = new LuaWriter(com, cliArgs, dedupTargetDir, outPath) //do! (writer :> LuaPrinter.Writer).Write(sprintf "AST: %A" fable.Declarations) - do! LuaPrinter.run writer fable + do! run writer babel } diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index 06415b8004..6218ff465b 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -1,26 +1,31 @@ -module Fable.Transforms.Lua.LuaPrinter +// fsharplint:disable InterfaceNames +module Fable.Transforms.Lua open System +open Fable open Fable.AST -open Fable.AST.Fable +open Fable.AST.Babel + +type SourceMapping = + int * int * int * int * string option type Writer = inherit IDisposable - abstract Write: string -> Async - abstract EscapeStringLiteral: string -> string + abstract AddSourceMapping: SourceMapping -> unit + abstract EscapeJsStringLiteral: string -> string abstract MakeImportPath: string -> string - abstract AddLog: msg:string * severity: Fable.Severity * ?range: SourceLocation -> unit + abstract Write: string -> Async type Printer = abstract Line: int abstract Column: int abstract PushIndentation: unit -> unit abstract PopIndentation: unit -> unit - abstract Print: string -> unit + abstract Print: string * ?loc: SourceLocation -> unit abstract PrintNewLine: unit -> unit - abstract EscapeStringLiteral: string -> string + abstract AddLocation: SourceLocation option -> unit + abstract EscapeJsStringLiteral: string -> string abstract MakeImportPath: string -> string - abstract AddLog: msg:string * severity: Fable.Severity * ?range: SourceLocation -> unit type PrinterImpl(writer: Writer) = // TODO: We can make this configurable later @@ -30,6 +35,17 @@ type PrinterImpl(writer: Writer) = let mutable line = 1 let mutable column = 0 + let addLoc (loc: SourceLocation option) = + match loc with + | None -> () + | Some loc -> + writer.AddSourceMapping( + loc.start.line, + loc.start.column, + line, + column, + loc.identifierName) + member _.Flush(): Async = async { do! writer.Write(builder.ToString()) @@ -54,7 +70,12 @@ type PrinterImpl(writer: Writer) = member _.PopIndentation() = if indent > 0 then indent <- indent - 1 - member _.Print(str: string) = + member _.AddLocation(loc) = + addLoc loc + + member _.Print(str: string, ?loc) = + addLoc loc + if column = 0 then let indent = String.replicate indent indentSpaces builder.Append(indent) |> ignore @@ -63,368 +84,999 @@ type PrinterImpl(writer: Writer) = builder.Append(str) |> ignore column <- column + str.Length - member this.EscapeStringLiteral(str) = - writer.EscapeStringLiteral(str) + member this.EscapeJsStringLiteral(str) = + writer.EscapeJsStringLiteral(str) member this.MakeImportPath(path) = writer.MakeImportPath(path) - member this.AddLog(msg, severity, ?range) = - writer.AddLog(msg, severity, ?range=range) module PrinterExtensions = type Printer with - member this.AddError(msg, ?range) = - this.AddLog(msg, Fable.Severity.Error, ?range=range) - - member this.AddWarning(msg, ?range) = - this.AddLog(msg, Fable.Severity.Warning , ?range=range) - member printer.PrintBlock(nodes: 'a array, printNode: Printer -> 'a -> unit, printSeparator: Printer -> unit, ?skipNewLineAtEnd) = let skipNewLineAtEnd = defaultArg skipNewLineAtEnd false + // printer.Print("") printer.PrintNewLine() printer.PushIndentation() - // for node in nodes do - // printNode printer node - // printSeparator printer - // printer.PopIndentation() - // printer.Print("end") - match nodes |> Array.toList |> List.rev with - | h::t -> - for node in t |> List.rev do - printNode printer node - printSeparator printer + for node in nodes do + printNode printer node printSeparator printer - printer.Print("return ") - printNode printer h - | _ -> () + printer.PopIndentation() + printer.Print("end") if not skipNewLineAtEnd then printer.PrintNewLine() - printer.PopIndentation() - - member printer.PrintBlock(nodes: Expr list, ?skipNewLineAtEnd) = - printer.PrintBlock(List.toArray nodes, - (fun p s -> p.Print(s)), // TODO: p.PrintProductiveStatement(s)), - (fun p -> p.PrintStatementSeparator()), - ?skipNewLineAtEnd=skipNewLineAtEnd) member printer.PrintStatementSeparator() = if printer.Column > 0 then + printer.Print(";") printer.PrintNewLine() - // TODO: Use Transforms.AST.canHaveSideEffects? - member this.HasSideEffects(e: Expr) = + member this.HasSideEffects(e: Expression) = match e with + | Undefined(_) + | Literal(NullLiteral(_)) + | Literal(Literal.StringLiteral(_)) + | Literal(BooleanLiteral(_)) + | Literal(NumericLiteral(_)) -> false + // Constructors of classes deriving from System.Object add an empty object at the end + | ObjectExpression(properties, loc) -> properties.Length > 0 + | UnaryExpression(prefix, argument, operator, loc) when operator = "void" -> this.HasSideEffects(argument) + // Some identifiers may be stranded as the result of imports + // intended only for side effects, see #2228 + | Expression.Identifier(_) -> false + // Sometimes empty IIFE remain in the AST + | CallExpression(ArrowFunctionExpression(_,(BlockStatement body),_,_,_),_,_) -> + body |> Array.exists this.IsProductiveStatement | _ -> true - member this.IsProductiveStatement(s: Expr) = - this.HasSideEffects(s) + member this.IsProductiveStatement(s: Statement) = + match s with + | ExpressionStatement(expr) -> this.HasSideEffects(expr) + | _ -> true - member printer.PrintProductiveStatement(s: Expr, ?printSeparator) = + member printer.PrintProductiveStatement(s: Statement, ?printSeparator) = if printer.IsProductiveStatement(s) then printer.Print(s) printSeparator |> Option.iter (fun f -> f printer) - member printer.Print(t: Type) = - // match t with - // | Any -> printer.Print("Object") - // | Unit -> printer.Print("null") - // | Boolean -> printer.Print("bool") - // | Char -> printer.Print("null") - // | String -> printer.Print("String") - // | Number(kind,_) -> - // match kind with - // | Int8 | UInt8 | Int16 | UInt16 | Int32 | UInt32 -> printer.Print("int") - // | Float32 | Float64 -> printer.Print("double") - // | _ -> printer.AddError("TODO: Print type") - () - - // TODO - member printer.ComplexExpressionWithParens(expr: Expr) = + member printer.PrintProductiveStatements(statements: Statement[]) = + for s in statements do + printer.PrintProductiveStatement(s, (fun p -> p.PrintStatementSeparator())) + + member printer.PrintBlock(nodes: Statement array, ?skipNewLineAtEnd) = + printer.PrintBlock(nodes, + (fun p s -> p.PrintProductiveStatement(s)), + (fun p -> p.PrintStatementSeparator()), + ?skipNewLineAtEnd=skipNewLineAtEnd) + + member printer.PrintOptional(node: Node option, ?before: string) = + match node with + | None -> () + | Some node -> + match before with + | Some before -> + printer.Print(before) + | _ -> () + printer.Print(node) + + member printer.PrintOptional(node: Expression option, ?before: string) = + printer.PrintOptional(node |> Option.map Expression, ?before=before) + member printer.PrintOptional(node: TypeParameterDeclaration option, ?before: string) = + printer.PrintOptional(node |> Option.map Node.TypeParameterDeclaration, ?before=before) + member printer.PrintOptional(node: TypeAnnotation option, ?before: string) = + printer.PrintOptional(node |> Option.map Node.TypeAnnotation, ?before=before) + member printer.PrintOptional(node: Identifier option, ?before: string) = + printer.PrintOptional(node |> Option.map Expression.Identifier, ?before=before) + member printer.PrintOptional(node: Literal option, ?before: string) = + printer.PrintOptional(node |> Option.map Literal, ?before=before) + member printer.PrintOptional(node: StringLiteral option, ?before: string) = + printer.PrintOptional(node |> Option.map Literal.StringLiteral, ?before=before) + member printer.PrintOptional(node: TypeParameterInstantiation option, ?before: string) = + printer.PrintOptional(node |> Option.map Node.TypeParameterInstantiation, ?before=before) + member printer.PrintOptional(node: Statement option, ?before: string) = + printer.PrintOptional(node |> Option.map Statement, ?before=before) + member printer.PrintOptional(node: Declaration option, ?before: string) = + printer.PrintOptional(node |> Option.map Declaration, ?before=before) + member printer.PrintOptional(node: VariableDeclaration option, ?before: string) = + printer.PrintOptional(node |> Option.map Declaration.VariableDeclaration, ?before=before) + member printer.PrintOptional(node: CatchClause option, ?before: string) = + printer.PrintOptional(node |> Option.map Node.CatchClause, ?before=before) + member printer.PrintOptional(node: BlockStatement option, ?before: string) = + printer.PrintOptional(node |> Option.map Statement.BlockStatement, ?before=before) + + member printer.PrintArray(nodes: 'a array, printNode: Printer -> 'a -> unit, printSeparator: Printer -> unit) = + for i = 0 to nodes.Length - 1 do + printNode printer nodes.[i] + if i < nodes.Length - 1 then + printSeparator printer + + member printer.PrintCommaSeparatedArray(nodes: Node array) = + printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) + member printer.PrintCommaSeparatedArray(nodes: Pattern array) = + printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) + member printer.PrintCommaSeparatedArray(nodes: ImportSpecifier array) = + printer.PrintArray(nodes, (fun p x -> + match x with + | ImportMemberSpecifier(local, imported) -> p.PrintImportMemberSpecific(local, imported) + | ImportDefaultSpecifier(local) -> printer.Print(local) + | ImportNamespaceSpecifier(local) -> printer.PrintImportNamespaceSpecifier(local) + ), (fun p -> p.Print(", "))) + member printer.PrintCommaSeparatedArray(nodes: ExportSpecifier array) = + printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) + member printer.PrintCommaSeparatedArray(nodes: FunctionTypeParam array) = + printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) + member printer.PrintCommaSeparatedArray(nodes: TypeAnnotationInfo array) = + printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) + member printer.PrintCommaSeparatedArray(nodes: TypeParameter array) = + printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) + + member printer.PrintCommaSeparatedArray(nodes: Expression array) = + printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) + + + // TODO: (super) type parameters, implements + member printer.PrintClass(id: Identifier option, superClass: Expression option, + superTypeParameters: TypeParameterInstantiation option, + typeParameters: TypeParameterDeclaration option, + implements: ClassImplements array option, body: ClassBody, loc) = + printer.Print("class", ?loc=loc) + printer.PrintOptional(id, " ") + printer.PrintOptional(typeParameters) + match superClass with + | Some (Expression.Identifier(Identifier(typeAnnotation=Some(typeAnnotation)))) -> + printer.Print(" extends ") + printer.Print(typeAnnotation) + | _ -> printer.PrintOptional(superClass, " extends ") + // printer.PrintOptional(superTypeParameters) + match implements with + | Some implements when not (Array.isEmpty implements) -> + printer.Print(" implements ") + printer.PrintArray(implements, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) + | _ -> () + printer.Print(" ") + printer.Print(body) + + member printer.PrintFunction(id: Identifier option, parameters: Pattern array, body: BlockStatement, + typeParameters: TypeParameterDeclaration option, returnType: TypeAnnotation option, loc, ?isDeclaration, ?isArrow) = + let areEqualPassedAndAppliedArgs (passedArgs: Pattern[]) (appliedAgs: Expression[]) = + Array.zip passedArgs appliedAgs + |> Array.forall (function + | RestElement(name=name), Expression.Identifier(Identifier(name=idName)) -> name = idName + | _ -> false) + + let isDeclaration = defaultArg isDeclaration false + let isArrow = defaultArg isArrow false + + printer.AddLocation(loc) + + // Check if we can remove the function + let skipExpr = + match body.Body with + | [| ReturnStatement(argument, loc) |] when not isDeclaration -> + match argument with + | CallExpression(callee, arguments, loc) when parameters.Length = arguments.Length -> + // To be sure we're not running side effects when deleting the function, + // check the callee is an identifier (accept non-computed member expressions too?) + match callee with + | Expression.Identifier(_) when areEqualPassedAndAppliedArgs parameters arguments -> + Some callee + | _ -> None + | _ -> None + | _ -> None + + match skipExpr with + | Some e -> printer.Print(e) + | None -> + if isArrow then + // Remove parens if we only have one argument? (and no annotation) + printer.PrintOptional(typeParameters) + printer.Print("(") + printer.PrintCommaSeparatedArray(parameters) + printer.Print(")") + printer.PrintOptional(returnType) + printer.Print(" => ") + match body.Body with + | [| ReturnStatement(argument, loc) |] -> + match argument with + | ObjectExpression(_) -> printer.WithParens(argument) + | MemberExpression(name, object, property, computed, loc) -> + match object with + | ObjectExpression(_) -> printer.PrintMemberExpression(name, object, property, computed, loc, objectWithParens=true) + | _ -> printer.Print(argument) + | _ -> printer.ComplexExpressionWithParens(argument) + | _ -> printer.PrintBlock(body.Body, skipNewLineAtEnd=true) + else + printer.Print("function ") + printer.PrintOptional(id) + printer.PrintOptional(typeParameters) + printer.Print("(") + printer.PrintCommaSeparatedArray(parameters) + printer.Print(")") + printer.PrintOptional(returnType) + printer.Print(" ") + printer.PrintBlock(body.Body, skipNewLineAtEnd=true) + + member printer.WithParens(expr: Expression) = + printer.Print("(") printer.Print(expr) + printer.Print(")") - member printer.PrintOperation(kind) = - match kind with - | Binary(operator, left, right) -> - printer.ComplexExpressionWithParens(left) - // TODO: review - match operator with - | BinaryEqual | BinaryEqualStrict -> printer.Print(" == ") - | BinaryUnequal | BinaryUnequalStrict -> printer.Print(" != ") - | BinaryLess -> printer.Print(" < ") - | BinaryLessOrEqual -> printer.Print(" <= ") - | BinaryGreater -> printer.Print(" > ") - | BinaryGreaterOrEqual -> printer.Print(" >= ") - | BinaryShiftLeft -> printer.Print(" << ") - | BinaryShiftRightSignPropagating -> printer.Print(" >> ") - | BinaryShiftRightZeroFill -> printer.Print(" >>> ") - | BinaryMinus -> printer.Print(" - ") - | BinaryPlus -> printer.Print(" + ") - | BinaryMultiply -> printer.Print(" * ") - | BinaryDivide -> printer.Print(" / ") - | BinaryModulus -> printer.Print(" % ") - | BinaryExponent -> printer.Print(" ** ") - | BinaryOrBitwise -> printer.Print(" | ") - | BinaryXorBitwise -> printer.Print(" ^ ") - | BinaryAndBitwise -> printer.Print(" & ") - | BinaryIn | BinaryInstanceOf -> printer.AddError($"Operator not supported {operator}") - printer.ComplexExpressionWithParens(right) - - | Logical(operator, left, right) -> - printer.ComplexExpressionWithParens(left) - match operator with - | LogicalOr -> printer.Print(" || ") - | LogicalAnd -> printer.Print(" && ") - printer.ComplexExpressionWithParens(right) - - | Unary(operator, operand) -> - match operator with - | UnaryMinus -> printer.Print("-") - | UnaryPlus -> printer.Print("+") - | UnaryNot -> printer.Print("!") - | UnaryNotBitwise -> printer.Print("~") - | UnaryTypeof | UnaryVoid | UnaryDelete -> printer.AddError($"Operator not supported {operator}") - printer.ComplexExpressionWithParens(operand) - - member printer.PrintValue(kind: ValueKind) = - match kind with - | BoolConstant v -> printer.Print((if v then "true" else "false")) - | StringConstant value -> - printer.Print("\"") - printer.Print(printer.EscapeStringLiteral(value)) - printer.Print("\"") -// | CharConstant of value: char - | NumberConstant(value,_,_) -> - let value = - match value.ToString(System.Globalization.CultureInfo.InvariantCulture) with - | "∞" -> "double.infinity" - | "-∞" -> "-double.infinity" - | value -> value - printer.Print(value) - | ThisValue _ -> printer.Print("this") - | Null _ | UnitConstant -> printer.Print("null") -// | NewArray of values: Expr list * typ: Type -// | NewArrayFrom of value: Expr * typ: Type -// | BaseValue of boundIdent: Ident option * typ: Type -// | TypeInfo of typ: Type -// | RegexConstant of source: string * flags: RegexFlag list -// | EnumConstant of value: Expr * ref: EntityRef -// | NewOption of value: Expr option * typ: Type * isStruct: bool -// | NewList of headAndTail: (Expr * Expr) option * typ: Type -// | NewTuple of values: Expr list * isStruct: bool -// | NewRecord of values: Expr list * ref: EntityRef * genArgs: Type list -// | NewAnonymousRecord of values: Expr list * fieldNames: string [] * genArgs: Type list -// | NewUnion of values: Expr list * tag: int * ref: EntityRef * genArgs: Type list - | _ -> printer.AddError("TODO: Print value") - - member printer.Print(expr: Expr) = + /// Surround with parens anything that can potentially conflict with operator precedence + member printer.ComplexExpressionWithParens(expr: Expression) = match expr with - | IdentExpr i -> printer.Print(i.Name) - | Value(kind,_) -> printer.PrintValue(kind) - | Operation(kind,_,_) -> printer.PrintOperation(kind) - | Extended(kind,_) -> - match kind with - //| Return (Value (UnitConstant, _)) -> () - | Return e -> - printer.Print("return ") - //printer.PrintSelfExecutingFnOpen() - printer.Print(e) - //printer.PrintSelfExecutingFnClose() - | _ -> printer.AddError("TODO: Print extended set") - | Import ({ Selector = "toConsole" }, b, c) -> - printer.Print("print") - | Import ({ Selector = "printfn" }, b, c) -> - printer.Print("") - | Import ({ Selector = "printf" }, b, c) -> - printer.Print("") - | Import (importInfo, b, c) -> - printer.Print("require(\"") - printer.Print(importInfo.Path) - printer.Print("""")""") - printer.Print(".") - printer.Print(importInfo.Selector) - //printer.PrintNewLine() - //printer.Print(sprintf "require(%A)" a) - | Call (expr, callInfo, t, d) -> - //printer.Print(sprintf "%A %A" expr callInfo) - match callInfo.CallMemberInfo with - | Some m -> - printer.Print(m.CompiledName) - printer.Print(".") - printer.Print(expr) - - | None -> - printer.Print(expr) - //printer.Print(sprintf "(%A)" expr) - printer.Print("(") - callInfo.Args |> Seq.toArray |> printer.PrintCommaSeparatedArgsArray - printer.Print(")") - | Lambda(arg, body, name) -> - printer.Print("function(") - printer.Print(arg.Name) - printer.Print(")") - printer.PushIndentation() - printer.PrintNewLine() - printer.Print(body) - printer.PopIndentation() - printer.PrintNewLine() - printer.Print("end") - | Delegate(args, body, name) -> printer.Print("todo delegate") - | ObjectExpr(members, typ, baseCall) -> printer.Print("todo object") - | TypeCast(expr, _) -> - //printer.Print(sprintf "cast %A" expr) - printer.Print(expr) - () - | Test(expr, kind, range) -> printer.Print("todo test") - | CurriedApply(applied, args, _, _) -> - printer.Print(applied) + | Undefined(_) + | Literal(NullLiteral(_)) + | Literal(Literal.StringLiteral(_)) + | Literal(BooleanLiteral(_)) + | Literal(NumericLiteral(_)) + | Expression.Identifier(_) + | MemberExpression(_) + | CallExpression(_) + | ThisExpression(_) + | Super(_) + | SpreadElement(_) + | ArrayExpression(_) + | ObjectExpression(_) -> printer.Print(expr) + | _ -> printer.WithParens(expr) + + member printer.PrintOperation(left, operator, right, loc) = + printer.AddLocation(loc) + printer.ComplexExpressionWithParens(left) + printer.Print(" " + operator + " ") + printer.ComplexExpressionWithParens(right) + + member printer.Print(node: Node) = + match node with + | Pattern(n) -> printer.Print(n) + | Statement(n) -> printer.Print(n) + | Node.ClassBody(n) -> printer.Print(n) + | Expression(n) -> printer.Print(n) + | Node.SwitchCase(n) -> printer.Print(n) + | Node.CatchClause(n) -> printer.Print(n) + | ObjectMember(n) -> printer.Print(n) + | Node.TypeParameter(n) -> printer.Print(n) + | Node.TypeAnnotation(n) -> ()//printer.Print(n) + | Node.ExportSpecifier(n) -> printer.Print(n) + | Node.InterfaceExtends(n) -> printer.Print(n) + | ModuleDeclaration(n) -> printer.Print(n) + | Node.FunctionTypeParam(n) -> printer.Print(n) + | Node.ObjectTypeProperty(n) -> printer.Print(n) + | Node.TypeAnnotationInfo(n) -> printer.Print(n) + | Node.TypeParameterDeclaration(n) -> printer.Print(n) + | Node.TypeParameterInstantiation(n) -> printer.Print(n) + | Node.Program(_) + | Directive(_) + | ImportSpecifier(_) + | Node.ObjectTypeIndexer(_) + | Node.VariableDeclarator(_) + | Node.ObjectTypeCallProperty(_) + | Node.ObjectTypeInternalSlot(_) -> failwith "Not implemented" + + member printer.Print(expr: Expression) = + match expr with + | Super(loc) -> printer.Print("super", ?loc = loc) + | Literal(n) -> printer.Print(n) + | Undefined(loc) -> printer.Print("undefined", ?loc=loc) + | Expression.Identifier(n) -> printer.Print(n) + | NewExpression(callee, arguments, typeArguments, loc) -> printer.PrintNewExpression(callee, arguments, typeArguments, loc) + | SpreadElement(argument, loc) -> + printer.Print("...", ?loc = loc) + printer.ComplexExpressionWithParens(argument) + | ThisExpression(loc) -> printer.Print("this", ?loc = loc) + | CallExpression(callee, arguments, loc) -> printer.PrintCallExpression(callee, arguments, loc) + | EmitExpression(value, args, loc) -> printer.PrintEmitExpression(value, args, loc) + | ArrayExpression(elements, loc) -> + printer.Print("[", ?loc = loc) + printer.PrintCommaSeparatedArray(elements) + printer.Print("]") + | ClassExpression(body, id, superClass, implements, superTypeParameters, typeParameters, loc) -> + printer.PrintClass(id, superClass, superTypeParameters, typeParameters, implements, body, loc) + | Expression.ClassImplements(n) -> printer.Print(n) + | UnaryExpression(prefix, argument, operator, loc) -> printer.PrintUnaryExpression(prefix, argument, operator, loc) + | UpdateExpression(prefix, argument, operator, loc) -> printer.PrintUpdateExpression(prefix, argument, operator, loc) + | ObjectExpression(properties, loc) -> printer.PrintObjectExpression(properties, loc) + | BinaryExpression(left, right, operator, loc) -> printer.PrintOperation(left, operator, right, loc) + | MemberExpression(name, object, property, computed, loc) -> printer.PrintMemberExpression(name, object, property, computed, loc) + | LogicalExpression(left, operator, right, loc) -> printer.PrintOperation(left, operator, right, loc) + | SequenceExpression(expressions, loc) -> + // A comma-separated sequence of expressions. + printer.AddLocation(loc) + // TODO: Remove parens if we end up with only one expression + // (when the ones before last don't have side effects) printer.Print("(") - args |> Seq.toArray |> printer.PrintCommaSeparatedArgsArray - printer.Print(")") - //printer.Print("todoApply") - // | CurriedApply(applied, args, typ, range) -> - // printer.Print(applied) - // args |> Seq.toArray |> printer.PrintCommaSeparatedArgsArray - // printer.Print(sprintf "_curriedApply(%A)" typ) - | Emit(info, typ, range) -> - //printer.Print(info.Macro) - printer.AddError("Emit macros not supported") - | DecisionTree(expr, targets) -> printer.Print("todo decision tree") - | DecisionTreeSuccess(targetIndex, boundValues, typ) -> printer.Print("todo decision tree success") - | Let(ident, value, body) -> - printer.Print(ident.Name) - // printer.Print(value) - printer.Print(" = ") - printer.Print(value) - printer.PrintNewLine() - //printer.Print("(") - printer.Print(body) - //printer.Print(")") - //printer.Print(sprintf "let %A %A %A" ident value body) - | LetRec(bindings, body) -> printer.Print("todo let rec") - | Get(expr, kind, typ, range) -> - printer.Print(expr) - //printer.Print("todo get") - | Set(expr, kind, typ, value, range) -> - printer.Print(expr) - printer.Print(" = ") - printer.Print(value) - | Sequential(exprs) -> - match exprs |> List.rev with - | h::t -> - for e in t |> List.rev do - printer.PrintNewLine() + let last = expressions.Length - 1 + for i = 0 to last do + let e = expressions.[i] + if i = last then printer.Print(e) - printer.PrintNewLine() - printer.Print("return ") - printer.Print(h) - printer.PrintNewLine() - | _ -> () - | WhileLoop(guard, body, label, range) -> printer.Print("todo while") - | ForLoop(ident, start, limit, body, isUp, range) -> printer.Print("todo for") - | TryCatch(body, catch, finalizer, range) -> - printer.PrintSelfExecutingFnOpen() - printer.Print("local status, retval = pcall(function()") - printer.PushIndentation() - printer.PrintNewLine() - printer.Print body - printer.PopIndentation() - //if !status then catch else return retval - printer.Print("return retval") - printer.PrintNewLine() - printer.Print("end)") - printer.PrintSelfExecutingFnClose() - | IfThenElse(guardExpr, thenExpr, elseExpr, range) -> - printer.PrintSelfExecutingFnOpen() - printer.Print "if " - printer.Print guardExpr - printer.Print " then" - printer.PushIndentation() - printer.PrintNewLine() - printer.Print("return ") - printer.Print thenExpr - printer.PopIndentation() + elif printer.HasSideEffects(e) then + printer.Print(e) + printer.Print(", ") + printer.Print(")") + | FunctionExpression(id, ``params``, body, typeParameters, returnType, loc) -> + printer.PrintFunction(id, ``params``, body, returnType, typeParameters, loc) + | AssignmentExpression(left, right, operator, loc) -> printer.PrintOperation(left, operator, right, loc) + | ConditionalExpression(test, consequent, alternate, loc) -> printer.PrintConditionalExpression(test, consequent, alternate, loc) + | ArrowFunctionExpression(``params``, body, returnType, typeParameters, loc) -> + printer.PrintArrowFunctionExpression(``params``, body, returnType, typeParameters, loc) + + member printer.Print(pattern: Pattern) = + match pattern with + | Pattern.Identifier(p) -> printer.Print(p) + | RestElement(name, argument, typeAnnotation, loc) -> + printer.Print("...", ?loc=loc) + printer.Print(argument) + printer.PrintOptional(typeAnnotation) + member printer.Print(literal: Literal) = + match literal with + | RegExp(pattern, flags, loc) -> printer.PrintRegExp(pattern, flags, loc) + | NullLiteral(loc) -> printer.Print("null", ?loc=loc) + | Literal.StringLiteral(l) -> printer.Print(l) + | BooleanLiteral(value, loc) -> printer.Print((if value then "true" else "false"), ?loc=loc) + | NumericLiteral(value, loc) -> printer.PrintNumeric(value, loc) + | Literal.DirectiveLiteral(l) -> failwith "not implemented" + + member printer.Print(stmt: Statement) = + match stmt with + | Declaration(s) -> printer.Print(s) + | IfStatement(test, consequent, alternate, loc) -> printer.PrintIfStatment(test, consequent, alternate, loc) + | TryStatement(block, handler, finalizer, loc) -> printer.PrintTryStatement(block, handler, finalizer, loc) + | ForStatement(body, init, test, update, loc) -> printer.PrintForStatement(body, init, test, update, loc) + | BreakStatement(label, loc) -> printer.Print("break", ?loc = loc) + | WhileStatement(test, body, loc) -> printer.PrintWhileStatment(test, body, loc) + | ThrowStatement(argument, loc) -> + printer.Print("throw ", ?loc = loc) + printer.Print(argument) + | Statement.BlockStatement(s) -> printer.Print(s) + | ReturnStatement(argument, loc) -> + printer.Print("return ", ?loc = loc) + printer.Print(argument) + | SwitchStatement(discriminant, cases, loc) -> printer.PrintSwitchStatement(discriminant, cases, loc) + | LabeledStatement(body, label) -> printer.PrintLabeledStatement(body, label) + | DebuggerStatement(loc) -> printer.Print("debugger", ?loc = loc) + | ContinueStatement(label, loc) -> + printer.Print("continue", ?loc=loc) + printer.PrintOptional(label, " ") + + | ExpressionStatement(expr) -> printer.Print(expr) + + member printer.Print(decl: Declaration) = + match decl with + | ClassDeclaration(body, id, superClass, implements, superTypeParameters, typeParameters, loc) -> + printer.PrintClass(id, superClass, superTypeParameters, typeParameters, implements, body, loc) + | Declaration.VariableDeclaration(d) -> printer.Print(d) + | FunctionDeclaration(``params``, body, id, returnType, typeParameters, loc) -> + printer.PrintFunction(Some id, ``params``, body, typeParameters, returnType, loc, isDeclaration=true) printer.PrintNewLine() - printer.Print "else " - printer.PushIndentation() + | InterfaceDeclaration(id, body, extends, implements, typeParameters) -> + printer.PrintInterfaceDeclaration(id, body, extends, implements, typeParameters) + + member printer.Print(md: ModuleDeclaration) = + match md with + | ImportDeclaration(specifiers, source) -> printer.PrintImportDeclaration(specifiers, source) + | ExportNamedReferences(specifiers, source) -> + // printer.Print("export ") + // printer.Print("{ ") + printer.PrintCommaSeparatedArray(specifiers) + // printer.Print(" }") + // printer.PrintOptional(source, " from ") + specifiers + |> Array.iter(fun s -> + printer.Print("mod.") + printer.Print(s)) + | ExportNamedDeclaration(declaration) -> + printer.Print("mod.") + printer.Print(declaration) + | ExportAllDeclaration(source, loc) -> + printer.Print("Not supported!") + // printer.Print("export * from ", ?loc=loc) + // printer.Print(source) + | PrivateModuleDeclaration(statement) -> + if printer.IsProductiveStatement(statement) then + printer.Print(statement) + | ExportDefaultDeclaration(declaration) -> + printer.Print("mod.") + match declaration with + | Choice1Of2 x -> printer.Print(x) + | Choice2Of2 x -> printer.Print(x) + + member printer.PrintEmitExpression(value, args, loc) = + printer.AddLocation(loc) + + let inline replace pattern (f: System.Text.RegularExpressions.Match -> string) input = + System.Text.RegularExpressions.Regex.Replace(input, pattern, f) + + let printSegment (printer: Printer) (value: string) segmentStart segmentEnd = + let segmentLength = segmentEnd - segmentStart + if segmentLength > 0 then + let segment = value.Substring(segmentStart, segmentLength) + let subSegments = System.Text.RegularExpressions.Regex.Split(segment, @"\r?\n") + for i = 1 to subSegments.Length do + let subSegment = + // Remove whitespace in front of new lines, + // indent will be automatically applied + if printer.Column = 0 then subSegments.[i - 1].TrimStart() + else subSegments.[i - 1] + if subSegment.Length > 0 then + printer.Print(subSegment) + if i < subSegments.Length then + printer.PrintNewLine() + + // Macro transformations + // https://fable.io/docs/communicate/js-from-fable.html#Emit-when-F-is-not-enough + let value = + value + |> replace @"\$(\d+)\.\.\." (fun m -> + let rep = ResizeArray() + let i = int m.Groups.[1].Value + for j = i to args.Length - 1 do + rep.Add("$" + string j) + String.concat ", " rep) + + |> replace @"\{\{\s*\$(\d+)\s*\?(.*?)\:(.*?)\}\}" (fun m -> + let i = int m.Groups.[1].Value + match args.[i] with + | Literal(BooleanLiteral(value=value)) when value -> m.Groups.[2].Value + | _ -> m.Groups.[3].Value) + + |> replace @"\{\{([^\}]*\$(\d+).*?)\}\}" (fun m -> + let i = int m.Groups.[2].Value + match Array.tryItem i args with + | Some _ -> m.Groups.[1].Value + | None -> "") + + // This is to emit string literals as JS, I think it's no really + // used and it shouldn't be necessary with the new emitJsExpr + // |> replace @"\$(\d+)!" (fun m -> + // let i = int m.Groups.[1].Value + // match Array.tryItem i args with + // | Some(:? StringLiteral as s) -> s.Value + // | _ -> "") + + let matches = System.Text.RegularExpressions.Regex.Matches(value, @"\$\d+") + if matches.Count > 0 then + for i = 0 to matches.Count - 1 do + let m = matches.[i] + + let segmentStart = + if i > 0 then matches.[i-1].Index + matches.[i-1].Length + else 0 + + printSegment printer value segmentStart m.Index + + let argIndex = int m.Value.[1..] + match Array.tryItem argIndex args with + | Some e -> printer.ComplexExpressionWithParens(e) + | None -> printer.Print("undefined") + + let lastMatch = matches.[matches.Count - 1] + printSegment printer value (lastMatch.Index + lastMatch.Length) value.Length + else + printSegment printer value 0 value.Length + + member printer.Print(identifier: Identifier) = + let (Identifier(name, optional, typeAnnotation, loc)) = identifier + printer.Print(name, ?loc=loc) + if optional = Some true then + printer.Print("?") + printer.PrintOptional(typeAnnotation) + + member printer.PrintRegExp(pattern, flags, loc) = + printer.Print("/", ?loc=loc) + printer.Print(pattern) + printer.Print("/") + printer.Print(flags) + + member printer.Print(node: StringLiteral) = + let (StringLiteral(value, loc)) = node + printer.Print("\"", ?loc=loc) + printer.Print(printer.EscapeJsStringLiteral(value)) + printer.Print("\"") + + member printer.PrintNumeric(value, loc) = + let value = + match value.ToString(System.Globalization.CultureInfo.InvariantCulture) with + | "∞" -> "Infinity" + | "-∞" -> "-Infinity" + | value -> value + printer.Print(value, ?loc=loc) + + member printer.Print(node: BlockStatement) = + printer.PrintBlock(node.Body) + + member printer.PrintLabeledStatement(body, label) = + printer.Print(label) + printer.Print(":") + printer.PrintNewLine() + // Don't push indent + printer.Print(body) + +// Control Flow + member printer.PrintIfStatment(test, consequent, alternate, loc) = + printer.AddLocation(loc) + printer.Print("if (", ?loc=loc) + printer.Print(test) + printer.Print(") ") + printer.Print(consequent) + match alternate with + | None -> () + | Some alternate -> + if printer.Column > 0 then printer.Print(" ") + match alternate with + | IfStatement(test, consequent, alternate, loc) -> + printer.Print("else ") + printer.PrintIfStatment(test, consequent, alternate, loc) + | alternate -> + let statements = + match alternate with + | Statement.BlockStatement(b) -> b.Body + | alternate -> [|alternate|] + // Get productive statements and skip `else` if they're empty + statements + |> Array.filter printer.IsProductiveStatement + |> function + | [||] -> () + | statements -> + printer.Print("else ") + printer.PrintBlock(statements) + if printer.Column > 0 then printer.PrintNewLine() - printer.Print("return ") - printer.Print elseExpr - printer.PopIndentation() - printer.PrintSelfExecutingFnClose() - // | x -> - // printer.Print("todo") - //printer.Print($"TODO: Print expression {x}") - //printer.AddError("TODO: Print expression") - - member printer.PrintArray(items: 'a array, printItem: Printer -> 'a -> unit, printSeparator: Printer -> unit) = - for i = 0 to items.Length - 1 do - printItem printer items.[i] - if i < items.Length - 1 then - printSeparator printer - member printer.PrintCommaSeparatedArray(nodes: Ident array) = - printer.PrintArray(nodes, (fun p x -> - // p.Print(x.Type) - // p.Print(" ") - p.Print(x.Name) - ), (fun p -> p.Print(", "))) - member printer.PrintCommaSeparatedArgsArray(nodes: Expr array) = - printer.PrintArray(nodes, fun p x -> - //p.Print(sprintf "%A" x) - p.Print(x) - , fun p -> p.Print(", ")) - - member printer.PrintFunctionDeclaration(name: string, args: Ident list, body: Expr) = - printer.Print("function ") - printer.Print("mod.") //function should be always declared in the local module scope, so they can be imported correctly - printer.Print(body.Type) - printer.Print("") - printer.Print(name) - printer.Print(" (") - printer.PrintCommaSeparatedArray(List.toArray args) + /// A case (if test is an Expression) or default (if test === null) clause in the body of a switch statement. + member printer.Print(node: SwitchCase) = + let (SwitchCase(test, consequent, loc)) = node + printer.AddLocation(loc) + + match test with + | None -> printer.Print("default") + | Some test -> + printer.Print("case ") + printer.Print(test) + + printer.Print(":") + + match consequent.Length with + | 0 -> printer.PrintNewLine() + | 1 -> + printer.Print(" ") + printer.Print(consequent.[0]) + | _ -> + printer.Print(" ") + printer.PrintBlock(consequent) + + member printer.PrintSwitchStatement(discriminant, cases, loc) = + printer.Print("switch (", ?loc=loc) + printer.Print(discriminant) printer.Print(") ") + printer.PrintBlock(cases, (fun p x -> p.Print(x)), fun _ -> ()) - printer.PrintBlock([body], skipNewLineAtEnd=false) - printer.Print("end") - member printer.PrintSelfExecutingFnOpen() = - printer.Print("(function ()") +// Exceptions + member printer.Print(node: CatchClause) = + let (CatchClause(param, body, loc)) = node + // "catch" is being printed by TryStatement + printer.Print("(", ?loc = loc) + printer.Print(param) + printer.Print(") ") + printer.Print(body) + + member printer.PrintTryStatement(block, handler, finalizer, loc) = + printer.Print("try ", ?loc = loc) + printer.Print(block) + printer.PrintOptional(handler, "catch ") + printer.PrintOptional(finalizer, "finally ") + +// Declarations + + member printer.Print(node: VariableDeclaration) = + let (VariableDeclaration(declarations, kind, loc)) = node + printer.Print(kind + " ", ?loc = loc) + let canConflict = declarations.Length > 1 + + for i = 0 to declarations.Length - 1 do + let (VariableDeclarator(id, init)) = declarations.[i] + printer.Print(id) + + match init with + | None -> () + | Some e -> + printer.Print(" = ") + if canConflict then printer.ComplexExpressionWithParens(e) + else printer.Print(e) + if i < declarations.Length - 1 then + printer.Print(", ") + + member printer.PrintWhileStatment(test, body, loc) = + printer.Print("while (", ?loc = loc) + printer.Print(test) + printer.Print(") ") + printer.Print(body) + + member printer.PrintForStatement(body, init, test, update, loc) = + printer.Print("for (", ?loc = loc) + printer.PrintOptional(init) + printer.Print("; ") + printer.PrintOptional(test) + printer.Print("; ") + printer.PrintOptional(update) + printer.Print(") ") + printer.Print(body) + + /// A fat arrow function expression, e.g., let foo = (bar) => { /* body */ }. + member printer.PrintArrowFunctionExpression(``params``, body, returnType, typeParameters, loc) = + printer.PrintFunction( + None, + ``params``, + body, + typeParameters, + returnType, + loc, + isArrow = true + ) + + member printer.Print(node: ObjectMember) = + match node with + | ObjectProperty(key, value, computed) -> printer.PrintObjectProperty(key, value, computed) + | ObjectMethod(kind, key, ``params``, body, computed, returnType, typeParameters, loc) -> + printer.PrintObjectMethod(kind, key, ``params``, body, computed, returnType, typeParameters, loc) + + member printer.PrintObjectProperty(key, value, computed) = + if computed then + printer.Print("[") + printer.Print(key) + printer.Print("]") + else + printer.Print(key) + printer.Print(": ") + printer.Print(value) + + member printer.PrintObjectMethod(kind, key, ``params``, body, computed, returnType, typeParameters, loc) = + printer.AddLocation(loc) + + if kind <> "method" then + printer.Print(kind + " ") + + if computed then + printer.Print("[") + printer.Print(key) + printer.Print("]") + else + printer.Print(key) + + printer.PrintOptional(typeParameters) + printer.Print("(") + printer.PrintCommaSeparatedArray(``params``) + printer.Print(")") + printer.PrintOptional(returnType) + printer.Print(" ") + + printer.PrintBlock(body.Body, skipNewLineAtEnd=true) + + member printer.PrintMemberExpression(name, object, property, computed, loc, ?objectWithParens: bool) = + printer.AddLocation(loc) + match objectWithParens, object with + | Some true, _ | _, Literal(NumericLiteral(_)) -> printer.WithParens(object) + | _ -> printer.ComplexExpressionWithParens(object) + if computed then + printer.Print("[") + printer.Print(property) + printer.Print("]") + else + printer.Print(".") + printer.Print(property) + + member printer.PrintObjectExpression(properties, loc) = + let printSeparator(p: Printer) = + p.Print(",") + p.PrintNewLine() + + printer.AddLocation(loc) + if Array.isEmpty properties then printer.Print("{}") + else printer.PrintBlock(properties, (fun p x -> p.Print(x)), printSeparator, skipNewLineAtEnd=true) + + member printer.PrintConditionalExpression(test, consequent, alternate, loc) = + printer.AddLocation(loc) + match test with + // TODO: Move node optimization to Fable2Babel as with IfStatement? + | Literal(BooleanLiteral(value=value)) -> + if value then printer.Print(consequent) + else printer.Print(alternate) + | _ -> + printer.ComplexExpressionWithParens(test) + printer.Print(" ? ") + printer.ComplexExpressionWithParens(consequent) + printer.Print(" : ") + printer.ComplexExpressionWithParens(alternate) + + member printer.PrintCallExpression(callee, arguments, loc) = + printer.AddLocation(loc) + printer.ComplexExpressionWithParens(callee) + printer.Print("(") + printer.PrintCommaSeparatedArray(arguments) + printer.Print(")") + + member printer.PrintNewExpression(callee, arguments, typeArguments, loc) = + printer.Print("new ", ?loc=loc) + printer.ComplexExpressionWithParens(callee) + printer.Print("(") + printer.PrintCommaSeparatedArray(arguments) + printer.Print(")") + + member printer.PrintUnaryExpression(prefix, argument, operator, loc) = + printer.AddLocation(loc) + match operator with + | "-" | "+" | "!" | "~" -> printer.Print(operator) + | _ -> printer.Print(operator + " ") + printer.ComplexExpressionWithParens(argument) + + member printer.PrintUpdateExpression(prefix, argument, operator, loc) = + printer.AddLocation(loc) + if prefix then + printer.Print(operator) + printer.ComplexExpressionWithParens(argument) + else + printer.ComplexExpressionWithParens(argument) + printer.Print(operator) + +// Binary Operations + + member printer.Print(node: ClassMember) = + match node with + | ClassMethod(kind, key, ``params``, body, computed, ``static``, ``abstract``, returnType, typeParameters, loc) -> + printer.PrintClassMethod(kind, key, ``params``, body, computed, ``static``, ``abstract``, returnType, typeParameters, loc) + | ClassProperty(key, value, computed, ``static``, optional, typeAnnotation, loc) -> printer.PrintClassProperty(key, value, computed, ``static``, optional, typeAnnotation, loc) + + member printer.PrintClassMethod(kind, key, ``params``, body, computed, ``static``, ``abstract``, returnType, typeParameters, loc) = + printer.AddLocation(loc) + + let keywords = [ + if ``static`` = Some true then yield "static" + if ``abstract`` = Some true then yield "abstract" + if kind = "get" || kind = "set" then yield kind + ] + + if not (List.isEmpty keywords) then + printer.Print((String.concat " " keywords) + " ") + + if computed then + printer.Print("[") + printer.Print(key) + printer.Print("]") + else + printer.Print(key) + + printer.PrintOptional(typeParameters) + printer.Print("(") + printer.PrintCommaSeparatedArray(``params``) + printer.Print(")") + printer.PrintOptional(returnType) + printer.Print(" ") + + printer.Print(body) + + member printer.PrintClassProperty(key, value, computed, ``static``, optional, typeAnnotation, loc) = + printer.AddLocation(loc) + if ``static`` then + printer.Print("static ") + if computed then + printer.Print("[") + printer.Print(key) + printer.Print("]") + else + printer.Print(key) + if optional then + printer.Print("?") + printer.PrintOptional(typeAnnotation) + printer.PrintOptional(value, ": ") + + member printer.Print(node: ClassImplements) = + let (ClassImplements(id, typeParameters)) = node + printer.Print(id) + printer.PrintOptional(typeParameters) + + member printer.Print(node: ClassBody) = + let (ClassBody(body, loc)) = node + printer.AddLocation(loc) + printer.PrintBlock(body, (fun p x -> p.Print(x)), (fun p -> p.PrintStatementSeparator())) + + member printer.PrintImportMemberSpecific(local, imported) = + // Don't print the braces, node will be done in the import declaration + printer.Print(imported) + if imported.Name <> local.Name then + printer.Print(" as ") + printer.Print(local) + + member printer.PrintImportNamespaceSpecifier(local) = + printer.Print("* as ") + printer.Print(local) + + member printer.PrintImportDeclaration(specifiers, source) = + let members = specifiers |> Array.choose (function ImportMemberSpecifier(local, imported) -> Some (ImportMemberSpecifier(local, imported)) | _ -> None) + let defaults = specifiers|> Array.choose (function ImportDefaultSpecifier(local) -> Some (ImportDefaultSpecifier(local)) | _ -> None) + let namespaces = specifiers |> Array.choose (function ImportNamespaceSpecifier(local) -> Some (ImportNamespaceSpecifier(local)) | _ -> None) + + // printer.Print("import ") + + if not(Array.isEmpty defaults) then + printer.PrintCommaSeparatedArray(defaults) + if not(Array.isEmpty namespaces && Array.isEmpty members) then + printer.Print(", ") + + if not(Array.isEmpty namespaces) then + printer.PrintCommaSeparatedArray(namespaces) + if not(Array.isEmpty members) then + printer.Print(", ") + + if not(Array.isEmpty members) then + //printer.Print("{ ") + printer.PrintCommaSeparatedArray(members) + //printer.Print(" }") + + printer.Print(" = ") + printer.Print("require") + printer.Print("(") + + // if not(Array.isEmpty defaults && Array.isEmpty namespaces && Array.isEmpty members) then + // printer.Print(" from ") + + printer.Print("\"") + let (StringLiteral(value, _)) = source + printer.Print(printer.MakeImportPath(value)) + printer.Print("\"") + printer.Print(")") + + member printer.Print(node: ExportSpecifier) = + let (ExportSpecifier (local, exported)) = node + // Don't print the braces, node will be done in the export declaration + printer.Print(local) + if exported.Name <> local.Name then + printer.Print(" as ") + printer.Print(exported) + + member printer.Print(node: TypeAnnotationInfo) = + match node with + | StringTypeAnnotation -> printer.Print("string") + | NumberTypeAnnotation -> printer.Print("number") + | TypeAnnotationInfo(an) -> printer.Print(an) + | BooleanTypeAnnotation -> printer.Print("boolean") + | AnyTypeAnnotation -> printer.Print("any") + | VoidTypeAnnotation -> printer.Print("void") + | TupleTypeAnnotation(types) -> + printer.Print("[") + printer.PrintCommaSeparatedArray(types) + printer.Print("]") + | UnionTypeAnnotation(types) -> + printer.PrintArray(types, (fun p x -> p.Print(x)), (fun p -> p.Print(" | "))) + | FunctionTypeAnnotation(``params``, returnType, typeParameters, rest) -> printer.PrintFunctionTypeAnnotation(``params``, returnType, typeParameters, rest) + | NullableTypeAnnotation(typeAnnotation) -> printer.Print(typeAnnotation) + | GenericTypeAnnotation(id, typeParameters) -> + printer.Print(id) + printer.PrintOptional(typeParameters) + | TypeAnnotationInfo.ObjectTypeAnnotation(an) -> printer.Print(an) + + member printer.Print((TypeAnnotation info): TypeAnnotation) = + printer.Print(": ") + printer.Print(info) + + member printer.Print((TypeParameter(name=name)): TypeParameter) = + printer.Print(name) + // printer.PrintOptional(bound) + // printer.PrintOptional(``default``) + + member printer.Print((TypeParameterDeclaration ``params``): TypeParameterDeclaration) = + printer.Print("<") + printer.PrintCommaSeparatedArray(``params``) + printer.Print(">") + + member printer.Print((TypeParameterInstantiation ``params``) : TypeParameterInstantiation) = + printer.Print("<") + printer.PrintCommaSeparatedArray(``params``) + printer.Print(">") + + member printer.Print(node: FunctionTypeParam) = + let (FunctionTypeParam(name, typeAnnotation, optional)) = node + printer.Print(name) + if optional = Some true then + printer.Print("?") + printer.Print(": ") + printer.Print(typeAnnotation) + + member printer.PrintFunctionTypeAnnotation(``params``, returnType, typeParameters, rest) = + printer.PrintOptional(typeParameters) + printer.Print("(") + printer.PrintCommaSeparatedArray(``params``) + if Option.isSome rest then + printer.Print("...") + printer.Print(rest.Value) + printer.Print(") => ") + printer.Print(returnType) + + member printer.Print(node: ObjectTypeProperty) = + let (ObjectTypeProperty(key, value, kind, computed, ``static``, optional, proto, method)) = node + + if ``static`` then + printer.Print("static ") + if Option.isSome kind then + printer.Print(kind.Value + " ") + if computed then + printer.Print("[") + printer.Print(key) + printer.Print("]") + else + printer.Print(key) + if optional then + printer.Print("?") + // TODO: proto, method + printer.Print(": ") + printer.Print(value) + + member printer.Print(node: ObjectTypeAnnotation) = + let (ObjectTypeAnnotation(properties, indexers, callProperties, internalSlots, exact)) = node + // printer.Print("{") + printer.PrintNewLine() printer.PushIndentation() + printer.PrintArray(properties, (fun p x -> p.Print(x)), (fun p -> p.PrintStatementSeparator())) + printer.PrintArray(indexers, (fun p x -> p.Print(x |> Node.ObjectTypeIndexer)), (fun p -> p.PrintStatementSeparator())) + printer.PrintArray(callProperties, (fun p x -> p.Print(x |> Node.ObjectTypeCallProperty)), (fun p -> p.PrintStatementSeparator())) + printer.PrintArray(internalSlots, (fun p x -> p.Print(x |> Node.ObjectTypeInternalSlot)), (fun p -> p.PrintStatementSeparator())) printer.PrintNewLine() - member printer.PrintSelfExecutingFnClose() = printer.PopIndentation() + printer.Print("end") printer.PrintNewLine() - printer.Print("end)()") - member printer.Print(md: Declaration) = - match md with - | ModuleDeclaration _ -> printer.AddError("Nested modules are not supported") - | ActionDeclaration _ -> printer.AddError("TODO: Action declaration") - | ClassDeclaration _ -> printer.AddError("TODO: Class declaration") - | MemberDeclaration m -> - printer.PrintFunctionDeclaration(m.Name, m.Args, m.Body) + member printer.Print(node: InterfaceExtends) = + let (InterfaceExtends(id, typeParameters)) = node + printer.Print(id) + printer.PrintOptional(typeParameters) + + member printer.PrintInterfaceDeclaration(id, body, extends, implements, typeParameters) = + printer.Print("interface ") + printer.Print(id) + printer.PrintOptional(typeParameters) + + if not (Array.isEmpty extends) then + printer.Print(" extends ") + printer.PrintArray(extends, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) + + if not (Array.isEmpty implements) then + printer.Print(" implements ") + printer.PrintArray(implements, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) + + printer.Print(" ") + printer.Print(body) open PrinterExtensions -let run (writer: Writer) (file: File): Async = - let printDeclWithExtraLine extraLine (printer: Printer) (decl: Declaration) = +let run writer (program: Program): Async = + let printDeclWithExtraLine extraLine (printer: Printer) (decl: ModuleDeclaration) = printer.Print(decl) + if printer.Column > 0 then + printer.Print(";") + printer.PrintNewLine() if extraLine then printer.PrintNewLine() async { use printer = new PrinterImpl(writer) - // TODO: Imports + let imports, restDecls = + program.Body |> Array.splitWhile (function + | ImportDeclaration(_) -> true + | _ -> false) + (printer :> Printer).Print("local mod = {}") (printer :> Printer).PrintNewLine() + + for decl in imports do + printDeclWithExtraLine false printer decl + + (printer :> Printer).PrintNewLine() do! printer.Flush() - for decl in file.Declarations do + for decl in restDecls do printDeclWithExtraLine true printer decl + // TODO: Only flush every XXX lines? do! printer.Flush() - (printer :> Printer).Print("return mod") (printer :> Printer).PrintNewLine() do! printer.Flush() diff --git a/src/fable-library-lua/fable/Async.fs b/src/fable-library-lua/fable/Async.fs deleted file mode 100644 index 75e65e77b8..0000000000 --- a/src/fable-library-lua/fable/Async.fs +++ /dev/null @@ -1,52 +0,0 @@ -module Async - -open System -open AsyncBuilder -open Fable.Core - -let emptyContinuation<'T> (_x: 'T) = - // NOP - () - -let defaultCancellationToken = new CancellationToken() - - -[] -type Async = - static member StartWithContinuations - ( - computation: IAsync<'T>, - continuation, - exceptionContinuation, - cancellationContinuation, - ?cancellationToken - ) : unit = - let trampoline = Trampoline() - - computation ( - { new IAsyncContext<'T> with - member this.onSuccess = continuation - member this.onError = exceptionContinuation - member this.onCancel = cancellationContinuation - member this.cancelToken = defaultArg cancellationToken defaultCancellationToken - member this.trampoline = trampoline } - ) - - static member StartWithContinuations(computation: IAsync<'T>, ?cancellationToken) : unit = - Async.StartWithContinuations( - computation, - emptyContinuation, - emptyContinuation, - emptyContinuation, - ?cancellationToken = cancellationToken - ) - - static member Start(computation, ?cancellationToken) = - Async.StartWithContinuations(computation, ?cancellationToken = cancellationToken) - - - static member StartImmediate(computation: IAsync<'T>, ?cancellationToken) = - Async.Start(computation, ?cancellationToken = cancellationToken) - -let startImmediate(computation: IAsync<'T>) = - Async.StartImmediate(computation, ?cancellationToken=None) diff --git a/src/fable-library-lua/fable/AsyncBuilder.fs b/src/fable-library-lua/fable/AsyncBuilder.fs deleted file mode 100644 index b795eb8518..0000000000 --- a/src/fable-library-lua/fable/AsyncBuilder.fs +++ /dev/null @@ -1,214 +0,0 @@ -module AsyncBuilder - -open System -open System.Collections.Generic -open Fable.Core -open Timer - -type Continuation<'T> = 'T -> unit - -type OperationCanceledError () = - inherit Exception ("The operation was canceled") - -type Continuations<'T> = Continuation<'T> * Continuation * Continuation - - -type CancellationToken (cancelled: bool) = - let mutable idx = 0 - let mutable cancelled = cancelled - let listeners = Dictionary unit>() - - new () = CancellationToken(false) - - member this.IsCancelled = cancelled - - member this.Cancel() = - if not cancelled then cancelled <- true - - for KeyValue (_, listener) in listeners do - listener () - - member this.AddListener(f: unit -> unit) = - let id = idx - idx <- idx + 1 - listeners.Add(idx, f) - id - - member this.RemoveListener(id: int) = listeners.Remove(id) - - member this.Register(f: unit -> unit) : IDisposable = - let id = this.AddListener(f) - - { new IDisposable with - member x.Dispose() = this.RemoveListener(id) |> ignore } - - member this.Register(f: obj -> unit, state: obj) : IDisposable = - let id = this.AddListener(fun () -> f (state)) - - { new IDisposable with - member x.Dispose() = this.RemoveListener(id) |> ignore } - -type Trampoline () = - let mutable callCount = 0 - - static member MaxTrampolineCallCount = 2000 - - member this.IncrementAndCheck() = - callCount <- callCount + 1 - callCount > Trampoline.MaxTrampolineCallCount - - member this.Hijack(f: unit -> unit) = - callCount <- 0 - let timer = Timer.Create(0., f) - timer.daemon <- true - timer.start () - -type IAsyncContext<'T> = - abstract member onSuccess : Continuation<'T> - abstract member onError : Continuation - abstract member onCancel : Continuation - - abstract member cancelToken : CancellationToken - abstract member trampoline : Trampoline - -type IAsync<'T> = IAsyncContext<'T> -> unit - -let protectedCont<'T> (f: IAsync<'T>) = - fun (ctx: IAsyncContext<'T>) -> - if ctx.cancelToken.IsCancelled then - ctx.onCancel (new OperationCanceledError()) - else if (ctx.trampoline.IncrementAndCheck()) then - ctx.trampoline.Hijack - (fun () -> - try - f ctx - with err -> ctx.onError (err)) - else - try - f ctx - with err -> ctx.onError (err) - -let protectedBind<'T, 'U> (computation: IAsync<'T>, binder: 'T -> IAsync<'U>) = - protectedCont - (fun (ctx: IAsyncContext<'U>) -> - computation ( - { new IAsyncContext<'T> with - member this.onSuccess = - fun (x: 'T) -> - try - binder (x) (ctx) - with ex -> ctx.onError (ex) - - member this.onError = ctx.onError - member this.onCancel = ctx.onCancel - member this.cancelToken = ctx.cancelToken - member this.trampoline = ctx.trampoline } - )) - -let protectedReturn<'T> (value: 'T) = protectedCont (fun (ctx: IAsyncContext<'T>) -> ctx.onSuccess (value)) - -type IAsyncBuilder = - abstract member Bind<'T, 'U> : IAsync<'T> * ('T -> IAsync<'U>) -> IAsync<'U> - - abstract member Combine<'T> : IAsync * IAsync<'T> -> IAsync<'T> - - abstract member Delay<'T> : (unit -> IAsync<'T>) -> IAsync<'T> - - //abstract member Return<'T> : [] values: 'T [] -> IAsync<'T> - abstract member Return<'T> : value: 'T -> IAsync<'T> - - abstract member While : (unit -> bool) * IAsync -> IAsync - abstract member Zero : unit -> IAsync - - -type AsyncBuilder () = - interface IAsyncBuilder with - - member this.Bind<'T, 'U>(computation: IAsync<'T>, binder: 'T -> IAsync<'U>) = protectedBind (computation, binder) - - member this.Combine<'T>(computation1: IAsync, computation2: IAsync<'T>) = - let self = this :> IAsyncBuilder - self.Bind(computation1, (fun () -> computation2)) - - member x.Delay<'T>(generator: unit -> IAsync<'T>) = protectedCont (fun (ctx: IAsyncContext<'T>) -> generator () (ctx)) - - - // public For(sequence: Iterable, body: (x: T) => IAsync) { - // const iter = sequence[Symbol.iterator](); - // let cur = iter.next(); - // return this.While(() => !cur.done, this.Delay(() => { - // const res = body(cur.value); - // cur = iter.next(); - // return res; - // })); - // } - - member this.Return<'T>(value: 'T) : IAsync<'T> = protectedReturn (unbox value) - // member this.Return<'T>([] value: 'T []) : IAsync<'T> = - // match value with - // | [||] -> protectedReturn (unbox null) - // | [| value |] -> protectedReturn value - // | _ -> failwith "Return takes zero or one argument." - - - // public ReturnFrom(computation: IAsync) { -// return computation; -// } - - // public TryFinally(computation: IAsync, compensation: () => void) { -// return protectedCont((ctx: IAsyncContext) => { -// computation({ -// onSuccess: (x: T) => { -// compensation(); -// ctx.onSuccess(x); -// }, -// onError: (x: any) => { -// compensation(); -// ctx.onError(x); -// }, -// onCancel: (x: any) => { -// compensation(); -// ctx.onCancel(x); -// }, -// cancelToken: ctx.cancelToken, -// trampoline: ctx.trampoline, -// }); -// }); -// } - - // public TryWith(computation: IAsync, catchHandler: (e: any) => IAsync) { -// return protectedCont((ctx: IAsyncContext) => { -// computation({ -// onSuccess: ctx.onSuccess, -// onCancel: ctx.onCancel, -// cancelToken: ctx.cancelToken, -// trampoline: ctx.trampoline, -// onError: (ex: any) => { -// try { -// catchHandler(ex)(ctx); -// } catch (ex2) { -// ctx.onError(ex2); -// } -// }, -// }); -// }); -// } - - // public Using(resource: T, binder: (x: T) => IAsync) { -// return this.TryFinally(binder(resource), () => resource.Dispose()); -// } - - member this.While(guard: unit -> bool, computation: IAsync) : IAsync = - let self = this :> IAsyncBuilder - - if guard () then - self.Bind(computation, (fun () -> self.While(guard, computation))) - else - self.Return() - - // member this.Bind<'T, 'U>(computation: IAsync<'T>, binder: 'T -> IAsync<'U>) = (this :> IAsyncBuilder).Bind(computation, binder) - member this.Zero() : IAsync = protectedCont (fun (ctx: IAsyncContext) -> ctx.onSuccess (())) - -// } - -let singleton : IAsyncBuilder = AsyncBuilder() :> _ diff --git a/src/fable-library-py/fable/Fable.Library.fsproj b/src/fable-library-py/fable/Fable.Library.fsproj index fa985a71f1..9a34b5f562 100644 --- a/src/fable-library-py/fable/Fable.Library.fsproj +++ b/src/fable-library-py/fable/Fable.Library.fsproj @@ -7,7 +7,7 @@ - + diff --git a/src/quicktest/QuickTest.fs b/src/quicktest/QuickTest.fs index 7e2f993d07..472506ab8d 100644 --- a/src/quicktest/QuickTest.fs +++ b/src/quicktest/QuickTest.fs @@ -18,6 +18,9 @@ let hello = printfn "hello world" let a = 2 + 2 let b = 3 - 1 let c = a + b +let fn a b c = + let d = a + b + c + printf "%A %A" b d let execute () = a.ToString() |> printf "%s" printf "c" diff --git a/src/quicktest/QuickTest.lua b/src/quicktest/QuickTest.lua index 6c245d79e5..ddf9ddffdb 100644 --- a/src/quicktest/QuickTest.lua +++ b/src/quicktest/QuickTest.lua @@ -1,440 +1,26 @@ -AST: [MemberDeclaration - { Name = "hello" - FullDisplayName = "QuickTest.hello" - Args = [] - Body = - Call - (Import - ({ Selector = "toConsole" - Path = - "./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js" - Kind = LibraryImport }, Any, None), - { ThisArg = None - Args = - [TypeCast - (Call - (Import - ({ Selector = "printf" - Path = - "./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js" - Kind = LibraryImport }, Any, None), - { ThisArg = None - Args = - [Value - (StringConstant "hello world", - Some { start = { line = 17 - column = 20 } - end = { line = 17 - column = 33 } - identifierName = None })] - SignatureArgTypes = [String] - CallMemberInfo = None - HasSpread = false - IsConstructor = false - OptimizableInto = None }, - DeclaredType - ({ FullName = "Microsoft.FSharp.Core.PrintfFormat`5" - Path = - AssemblyPath - "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, - [Unit; - DeclaredType - ({ FullName = "System.IO.TextWriter" - Path = CoreAssemblyName "System.Runtime" }, []); - Unit; Unit; Unit]), Some { start = { line = 17 - column = 20 } - end = { line = 17 - column = 33 } - identifierName = None }), - DeclaredType - ({ FullName = "Microsoft.FSharp.Core.PrintfFormat`4" - Path = - AssemblyPath - "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, - [Unit; - DeclaredType - ({ FullName = "System.IO.TextWriter" - Path = CoreAssemblyName "System.Runtime" }, []); Unit; - Unit]))] - SignatureArgTypes = - [DeclaredType - ({ FullName = "Microsoft.FSharp.Core.PrintfFormat`4" - Path = - AssemblyPath - "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, - [GenericParam ("T", []); - DeclaredType ({ FullName = "System.IO.TextWriter" - Path = CoreAssemblyName "System.Runtime" }, []); - Unit; Unit])] - CallMemberInfo = None - HasSpread = false - IsConstructor = false - OptimizableInto = None }, Unit, Some { start = { line = 17 - column = 12 } - end = { line = 17 - column = 33 } - identifierName = None }) - Info = Fable.Transforms.FSharp2Fable.MemberInfo - UsedNames = set [] - ExportDefault = false }; - MemberDeclaration - { Name = "a" - FullDisplayName = "QuickTest.a" - Args = [] - Body = - Operation - (Binary - (BinaryPlus, - Value - (NumberConstant (2.0, Int32, None), Some { start = { line = 18 - column = 8 } - end = { line = 18 - column = 9 } - identifierName = None }), - Value - (NumberConstant (2.0, Int32, None), Some { start = { line = 18 - column = 12 } - end = { line = 18 - column = 13 } - identifierName = None })), - Number (Int32, None), Some { start = { line = 18 - column = 8 } - end = { line = 18 - column = 13 } - identifierName = None }) - Info = Fable.Transforms.FSharp2Fable.MemberInfo - UsedNames = set ["arg0_0"; "arg1_0"] - ExportDefault = false }; - MemberDeclaration - { Name = "b" - FullDisplayName = "QuickTest.b" - Args = [] - Body = - Operation - (Binary - (BinaryMinus, - Value - (NumberConstant (3.0, Int32, None), Some { start = { line = 19 - column = 8 } - end = { line = 19 - column = 9 } - identifierName = None }), - Value - (NumberConstant (1.0, Int32, None), Some { start = { line = 19 - column = 12 } - end = { line = 19 - column = 13 } - identifierName = None })), - Number (Int32, None), Some { start = { line = 19 - column = 8 } - end = { line = 19 - column = 13 } - identifierName = None }) - Info = Fable.Transforms.FSharp2Fable.MemberInfo - UsedNames = set ["arg0_0"; "arg1_0"] - ExportDefault = false }; - MemberDeclaration - { Name = "c" - FullDisplayName = "QuickTest.c" - Args = [] - Body = - Operation - (Binary - (BinaryPlus, IdentExpr { Name = "a" - Type = Number (Int32, None) - IsMutable = false - IsThisArgument = false - IsCompilerGenerated = true - Range = Some { start = { line = 20 - column = 8 } - end = { line = 20 - column = 9 } - identifierName = Some "a" } }, - IdentExpr { Name = "b" - Type = Number (Int32, None) - IsMutable = false - IsThisArgument = false - IsCompilerGenerated = true - Range = Some { start = { line = 20 - column = 12 } - end = { line = 20 - column = 13 } - identifierName = Some "b" } }), - Number (Int32, None), Some { start = { line = 20 - column = 8 } - end = { line = 20 - column = 13 } - identifierName = None }) - Info = Fable.Transforms.FSharp2Fable.MemberInfo - UsedNames = set ["arg0_0"; "arg1_0"] - ExportDefault = false }; - MemberDeclaration - { Name = "execute" - FullDisplayName = "QuickTest.execute" - Args = [{ Name = "unitVar0" - Type = Unit - IsMutable = false - IsThisArgument = false - IsCompilerGenerated = true - Range = Some { start = { line = 21 - column = 12 } - end = { line = 21 - column = 14 } - identifierName = Some "unitVar0" } }] - Body = - Extended - (Return - (Sequential - [Let - ({ Name = "arg10" - Type = String - IsMutable = false - IsThisArgument = false - IsCompilerGenerated = true - Range = Some { start = { line = 22 - column = 20 } - end = { line = 22 - column = 31 } - identifierName = Some "arg10" } }, - Let - ({ Name = "copyOfStruct" - Type = Number (Int32, None) - IsMutable = true - IsThisArgument = false - IsCompilerGenerated = true - Range = Some { start = { line = 22 - column = 4 } - end = { line = 22 - column = 16 } - identifierName = Some "copyOfStruct" } }, - IdentExpr { Name = "a" - Type = Number (Int32, None) - IsMutable = false - IsThisArgument = false - IsCompilerGenerated = true - Range = Some { start = { line = 22 - column = 4 } - end = { line = 22 - column = 5 } - identifierName = Some "a" } }, - Call - (Import - ({ Selector = "int32ToString" - Path = - "./.fable/fable-library.3.0.0-local-build-20210819-1229/Util.js" - Kind = LibraryImport }, Any, None), - { ThisArg = None - Args = - [IdentExpr - { Name = "copyOfStruct" - Type = Number (Int32, None) - IsMutable = true - IsThisArgument = false - IsCompilerGenerated = true - Range = - Some { start = { line = 22 - column = 4 } - end = { line = 22 - column = 16 } - identifierName = Some "copyOfStruct" } }] - SignatureArgTypes = [] - CallMemberInfo = None - HasSpread = false - IsConstructor = false - OptimizableInto = None }, String, None)), - CurriedApply - (Call - (Import - ({ Selector = "toConsole" - Path = - "./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js" - Kind = LibraryImport }, Any, None), - { ThisArg = None - Args = - [TypeCast - (Call - (Import - ({ Selector = "printf" - Path = - "./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js" - Kind = LibraryImport }, Any, None), - { ThisArg = None - Args = - [Value - (StringConstant "%s", - Some { start = { line = 22 - column = 27 } - end = { line = 22 - column = 31 } - identifierName = None })] - SignatureArgTypes = [String] - CallMemberInfo = None - HasSpread = false - IsConstructor = false - OptimizableInto = None }, - DeclaredType - ({ FullName = - "Microsoft.FSharp.Core.PrintfFormat`5" - Path = - AssemblyPath - "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, - [LambdaType (String, Unit); - DeclaredType - ({ FullName = "System.IO.TextWriter" - Path = - CoreAssemblyName "System.Runtime" }, - []); Unit; Unit; String]), - Some { start = { line = 22 - column = 27 } - end = { line = 22 - column = 31 } - identifierName = None }), - DeclaredType - ({ FullName = - "Microsoft.FSharp.Core.PrintfFormat`4" - Path = - AssemblyPath - "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, - [LambdaType (String, Unit); - DeclaredType - ({ FullName = "System.IO.TextWriter" - Path = CoreAssemblyName "System.Runtime" }, - []); Unit; Unit]))] - SignatureArgTypes = - [DeclaredType - ({ FullName = - "Microsoft.FSharp.Core.PrintfFormat`4" - Path = - AssemblyPath - "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, - [GenericParam ("T", []); - DeclaredType - ({ FullName = "System.IO.TextWriter" - Path = CoreAssemblyName "System.Runtime" }, - []); Unit; Unit])] - CallMemberInfo = None - HasSpread = false - IsConstructor = false - OptimizableInto = None }, LambdaType (String, Unit), - Some { start = { line = 22 - column = 20 } - end = { line = 22 - column = 31 } - identifierName = None }), - [IdentExpr - { Name = "arg10" - Type = String - IsMutable = false - IsThisArgument = false - IsCompilerGenerated = true - Range = Some { start = { line = 22 - column = 20 } - end = { line = 22 - column = 31 } - identifierName = Some "arg10" } }], - Unit, Some { start = { line = 22 - column = 20 } - end = { line = 22 - column = 31 } - identifierName = None })); - Call - (Import - ({ Selector = "toConsole" - Path = - "./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js" - Kind = LibraryImport }, Any, None), - { ThisArg = None - Args = - [TypeCast - (Call - (Import - ({ Selector = "printf" - Path = - "./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js" - Kind = LibraryImport }, Any, None), - { ThisArg = None - Args = - [Value - (StringConstant "c", - Some { start = { line = 23 - column = 11 } - end = { line = 23 - column = 14 } - identifierName = None })] - SignatureArgTypes = [String] - CallMemberInfo = None - HasSpread = false - IsConstructor = false - OptimizableInto = None }, - DeclaredType - ({ FullName = - "Microsoft.FSharp.Core.PrintfFormat`5" - Path = - AssemblyPath - "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, - [Unit; - DeclaredType - ({ FullName = "System.IO.TextWriter" - Path = CoreAssemblyName "System.Runtime" }, - []); Unit; Unit; Unit]), - Some { start = { line = 23 - column = 11 } - end = { line = 23 - column = 14 } - identifierName = None }), - DeclaredType - ({ FullName = "Microsoft.FSharp.Core.PrintfFormat`4" - Path = - AssemblyPath - "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, - [Unit; - DeclaredType - ({ FullName = "System.IO.TextWriter" - Path = CoreAssemblyName "System.Runtime" }, []); - Unit; Unit]))] - SignatureArgTypes = - [DeclaredType - ({ FullName = "Microsoft.FSharp.Core.PrintfFormat`4" - Path = - AssemblyPath - "C:/Users/metal/.nuget/packages/fsharp.core/5.0.1/lib/netstandard2.0/FSharp.Core.dll" }, - [GenericParam ("T", []); - DeclaredType - ({ FullName = "System.IO.TextWriter" - Path = CoreAssemblyName "System.Runtime" }, []); - Unit; Unit])] - CallMemberInfo = None - HasSpread = false - IsConstructor = false - OptimizableInto = None }, Unit, - Some { start = { line = 23 - column = 4 } - end = { line = 23 - column = 14 } - identifierName = None })]), None) - Info = Fable.Transforms.FSharp2Fable.MemberInfo - UsedNames = set ["arg10"; "clo1"; "copyOfStruct"; "unitVar0"] - ExportDefault = false }]local mod = {} -function mod.hello () - print(("hello world")) -end -function mod.a () - 2 + 2 -end -function mod.b () - 3 - 1 -end -function mod.c () - a + b -end -function mod.execute (unitVar0) - return (function () +local mod = {} +printf, toConsole = require("./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js"); +int32ToString = require("./.fable/fable-library.3.0.0-local-build-20210819-1229/Util.js"); + +mod.const hello = toConsole(printf("hello world")); - arg10 = copyOfStruct = a - require("./.fable/fable-library.3.0.0-local-build-20210819-1229/Util.js").int32ToString(copyOfStruct) - print(("%s"))(arg10) - return print(("c")) +mod.const a = 2 + 2; - end)() +mod.const b = 3 - 1; + +mod.const c = a + b; + +mod.function fn(a_1, b_1, c_1) + const d = ((a_1 + b_1) + c_1) | 0; + toConsole(printf("%A %A"))(b_1)(d); end + +mod.function execute() + let arg10; + let copyOfStruct = a; + arg10 = int32ToString(copyOfStruct); + toConsole(printf("%s"))(arg10); + toConsole(printf("c")); +end + return mod From ab0bdd2a074e8f8cc8bff5fdad3b82efd52e0228 Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Mon, 23 Aug 2021 18:35:48 +0100 Subject: [PATCH 03/41] checkpoint --- build.fsx | 31 +- src/Fable.Cli/Pipeline.fs | 32 +- src/Fable.Transforms/Fable.Transforms.fsproj | 2 + src/Fable.Transforms/Lua/Fable2Lua.fs | 116 ++ src/Fable.Transforms/Lua/Lua.fs | 52 + src/Fable.Transforms/Lua/LuaPrinter.fs | 1225 +++--------------- src/quicktest/QuickTest.lua | 37 +- tests/Lua/Fable.Tests.Lua.fsproj | 27 + tests/Lua/Main.fs | 6 + tests/Lua/TestArithmetic.fs | 101 ++ tests/Lua/Util.fs | 32 + tests/Lua/runtests.lua | 8 + 12 files changed, 533 insertions(+), 1136 deletions(-) create mode 100644 src/Fable.Transforms/Lua/Fable2Lua.fs create mode 100644 src/Fable.Transforms/Lua/Lua.fs create mode 100644 tests/Lua/Fable.Tests.Lua.fsproj create mode 100644 tests/Lua/Main.fs create mode 100644 tests/Lua/TestArithmetic.fs create mode 100644 tests/Lua/Util.fs create mode 100644 tests/Lua/runtests.lua diff --git a/build.fsx b/build.fsx index 04b9cfbc77..5f640994d9 100644 --- a/build.fsx +++ b/build.fsx @@ -217,14 +217,6 @@ let buildLibraryLua() = ] // Copy *.py from projectDir to buildDir copyDirRecursive libraryDir buildDirLua - //copyDirNonRecursive (buildDirLua "fable/fable-library") (buildDirLua "fable") - //copyFile (buildDirPy "fable/fable-library/*.py") (buildDirPy "fable") - // copyFile (buildDirLua "fable/system.text.lua") (buildDirLua "fable/system_text.lua") - // copyFile (buildDirLua "fable/fsharp.core.lua") (buildDirLua "fable/fsharp_core.lua") - // copyFile (buildDirLua "fable/fsharp.collections.lua") (buildDirLua "fable/fsharp_collections.lua") - // copyFile (buildDirLua "fable/system.collections.generic.lua") (buildDirLua "fable/system_collections_generic.lua") - //copyFile (buildDirPy "fable/async.lua") (buildDirPy "fable/async_.lua") - //removeFile (buildDirLua "fable/system.text.lua") runInDir buildDirLua ("lua -v") //runInDir buildDirLua ("lua ./setup.lua develop") @@ -234,6 +226,12 @@ let buildPyLibraryIfNotExists() = if not (pathExists (baseDir "build/fable-library-py")) then buildLibraryPy() +let buildLuaLibraryIfNotExists() = + let baseDir = __SOURCE_DIRECTORY__ + if not (pathExists (baseDir "build/fable-library-lua")) then + buildLibraryLua() + + // Like testJs() but doesn't create bundles/packages for fable-standalone & friends // Mainly intended for CI let testJsFast() = @@ -469,6 +467,22 @@ let testPython() = runInDir buildDir "touch __init__.py" // So relative imports works. runInDir buildDir "pytest" +let testLua() = + buildLuaLibraryIfNotExists() // NOTE: fable-library-py needs to be built separately. + + let projectDir = "tests/Lua" + let buildDir = "build/tests/Lua" + + cleanDirs [buildDir] + runInDir projectDir "dotnet test" + runFableWithArgs projectDir [ + "--outDir " + buildDir + "--exclude Fable.Core" + "--lang Lua" + ] + + copyFile (projectDir "runtests.lua") (buildDir "runtests.lua") + runInDir buildDir "lua runtests.lua" let buildLocalPackageWith pkgDir pkgCommand fsproj action = let version = "3.0.0-local-build-" + DateTime.Now.ToString("yyyyMMdd-HHmm") @@ -629,6 +643,7 @@ match argsLower with | "test-compiler"::_ -> testCompiler() | "test-integration"::_ -> testIntegration() | "test-py"::_ -> testPython() +| "test-lua"::_ -> testLua() | "quicktest"::_ -> buildLibraryJsIfNotExists() run "dotnet watch -p src/Fable.Cli run -- watch --cwd ../quicktest --exclude Fable.Core --noCache --runScript" diff --git a/src/Fable.Cli/Pipeline.fs b/src/Fable.Cli/Pipeline.fs index 46aafefaaf..48402e3a2e 100644 --- a/src/Fable.Cli/Pipeline.fs +++ b/src/Fable.Cli/Pipeline.fs @@ -142,36 +142,20 @@ module Dart = } module Lua = - open Fable.Transforms.Lua - type LuaWriter(com: Compiler, cliArgs: CliArgs, dedupTargetDir, targetPath: string) = - let sourcePath = com.CurrentFile - let fileExt = cliArgs.CompilerOptions.FileExtension - let targetDir = Path.GetDirectoryName(targetPath) - let stream = new IO.StreamWriter(targetPath) - interface Writer with - member _.Write(str) = - stream.WriteAsync(str) |> Async.AwaitTask - member _.EscapeJsStringLiteral(str) = - Web.HttpUtility.JavaScriptStringEncode(str) - member _.MakeImportPath(path) = - let projDir = IO.Path.GetDirectoryName(cliArgs.ProjectFile) - let path = Imports.getImportPath dedupTargetDir sourcePath targetPath projDir cliArgs.OutDir path - if path.EndsWith(".fs") then - let isInFableHiddenDir = Path.Combine(targetDir, path) |> Naming.isInFableHiddenDir - File.changeFsExtension isInFableHiddenDir path fileExt - else path - member _.AddSourceMapping((srcLine, srcCol, genLine, genCol, name)) = () - member _.Dispose() = stream.Dispose() + open Fable.Transforms let compileFile (com: Compiler) (cliArgs: CliArgs) dedupTargetDir (outPath: string) = async { - let babel = + let program = FSharp2Fable.Compiler.transformFile com |> FableTransforms.transformFile com - |> Fable2Babel.Compiler.transformFile com + |> Fable2Lua.transformFile com - use writer = new LuaWriter(com, cliArgs, dedupTargetDir, outPath) + use w = new IO.StreamWriter(outPath) + let ctx = LuaPrinter.Output.Writer.create w + LuaPrinter.Output.writeFile ctx program + //use writer = new LuaWriter(com, cliArgs, dedupTargetDir, outPath) //do! (writer :> LuaPrinter.Writer).Write(sprintf "AST: %A" fable.Declarations) - do! run writer babel + //do! run writer program } diff --git a/src/Fable.Transforms/Fable.Transforms.fsproj b/src/Fable.Transforms/Fable.Transforms.fsproj index 6c7d6055b1..4f16a22001 100644 --- a/src/Fable.Transforms/Fable.Transforms.fsproj +++ b/src/Fable.Transforms/Fable.Transforms.fsproj @@ -27,6 +27,8 @@ + + diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs new file mode 100644 index 0000000000..bbf4e60fb6 --- /dev/null +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -0,0 +1,116 @@ +module rec Fable.Transforms.Fable2Lua + +//cloned from FableToBabel + +open System +open System.Collections.Generic +open System.Text.RegularExpressions + +open Fable +open Fable.AST +open Fable.AST.Lua +open Fable.Naming +open Fable.Core + +// type ILuaCompiler = +// inherit Compiler + +// type LuaCompiler(com: Fable.Compiler) = +// interface ILuaCompiler // with + //member this.AddType(entref, Type: LuaType) = this.AddType(entref, phpType) +module Transforms = + module Helpers = + let transformStatements transformStatements transformReturn exprs = [ + match exprs |> List.rev with + | h::t -> + for x in t |> List.rev do + yield transformStatements x + yield transformReturn h + | [] -> () + ] + let transformValueKind = function + | Fable.NumberConstant(v,_,_) -> + Const(ConstNumber v) + | Fable.StringConstant(s) -> + Const(ConstString s) + | Fable.BoolConstant(b) -> + Const(ConstBool b) + | Fable.UnitConstant -> + Const(ConstNull) + | Fable.CharConstant(c) -> + Const(ConstString (string c)) + // | Fable.EnumConstant(e,ref) -> + // convertExpr com e + | Fable.Null _ -> + Const(ConstNull) + | _ -> Const ConstNull + let transformOp = function + | Fable.OperationKind.Binary (op, left, right) -> + let op = match op with + | BinaryMultiply -> Multiply + | BinaryDivide -> Divide + | BinaryEqual -> Equals + | BinaryPlus -> Plus + | BinaryEqualStrict -> Equals + | x -> sprintf "%A" x |> BinaryTodo + Binary(op, transformExpr left, transformExpr right ) + | x -> Unknown(sprintf "%A" x) + let asSingleExprIife = function + | [] -> NoOp + | [h] -> + transformExpr h + | exprs -> + let statements = + Helpers.transformStatements + (transformExpr >> Do) + (transformExpr >> Return) + exprs + FunctionCall(AnonymousFunc([], statements), []) + let transformExpr = function + | Fable.Expr.Value(value, _) -> transformValueKind value + | Fable.Expr.Call(expr, callInfo, t, r) -> + //Unknown(sprintf "call %A %A" expr callInfo) + FunctionCall(transformExpr expr, List.map transformExpr callInfo.Args) + | Fable.Expr.Import (info, t, r) -> + let path = info.Path.Replace(".fs", ".lua") //todo - make less brittle + let rcall = FunctionCall(Ident { Namespace=None; Name= "require" }, [Const (ConstString path)]) + match info.Selector with + | "" -> rcall + | s -> Get(rcall, FieldGet s) + | Fable.Expr.IdentExpr(i) when i.Name <> "" -> + Ident {Namespace = None; Name = i.Name } + | Fable.Expr.Operation (kind, _, _) -> + transformOp kind + | Fable.Expr.Get(expr, Fable.GetKind.FieldGet(fieldName, isMut), _, _) -> + Get(transformExpr expr, FieldGet(fieldName)) + | Fable.Expr.Sequential exprs -> + asSingleExprIife exprs + | Fable.Expr.Let (ident, value, body) -> + Let(ident.Name, transformExpr body) + | Fable.Expr.Emit(m, _, _) -> + Macro(m.Macro, m.CallInfo.Args |> List.map transformExpr) + | Fable.Expr.DecisionTree(expr, lst) -> + transformExpr expr + | Fable.Expr.DecisionTreeSuccess(i, exprs, _) -> + asSingleExprIife exprs + | Fable.Expr.IfThenElse (guardExpr, thenExpr, elseExpr, _) -> + IfThenElse(transformExpr guardExpr, transformExpr thenExpr, transformExpr elseExpr) + | x -> Unknown (sprintf "%A" x) + + let transformDeclarations = function + | Fable.ModuleDeclaration m -> + Assignment("moduleDecTest", Expr.Const (ConstString "moduledectest")) + | Fable.MemberDeclaration m -> + if m.Args.Length = 0 then + Assignment(m.Name, transformExpr m.Body) + else + FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), [transformExpr m.Body |> Return]) + | x -> sprintf "%A" x |> Unknown |> Do + +let transformFile com (file: Fable.File): File = + //let comp = LuaCompiler(com) :> ILuaCompiler + { + Filename = "abc" + Statements = file.Declarations |> List.map Transforms.transformDeclarations + ASTDebug = sprintf "%A" file.Declarations + } \ No newline at end of file diff --git a/src/Fable.Transforms/Lua/Lua.fs b/src/Fable.Transforms/Lua/Lua.fs new file mode 100644 index 0000000000..02864f2bc7 --- /dev/null +++ b/src/Fable.Transforms/Lua/Lua.fs @@ -0,0 +1,52 @@ +// fsharplint:disable MemberNames InterfaceNames +namespace rec Fable.AST.Lua + + +type Const = + | ConstNumber of float + | ConstString of string + | ConstBool of bool + | ConstNull + +type LuaIdentity = + { Namespace: string option + Name: string + } + +type UnaryOp = + | Not +type BinaryOp = + | Equals + | Multiply + | Divide + | Plus + | Minus + | BinaryTodo of string + +type GetKind = + | FieldGet of fieldName: string + +type Expr = + | Ident of LuaIdentity + | Const of Const + | Unary of UnaryOp * Expr + | Binary of BinaryOp * Expr * Expr + | Get of Expr * kind: GetKind + | FunctionCall of f: Expr * args: Expr list + | AnonymousFunc of args: string list * body: Statement list + | Unknown of string + | Let of name: string * Expr + | Macro of string * args: Expr list + | IfThenElse of guardExpr: Expr * thenExpr: Expr * elseExpr: Expr + | NoOp + +type Statement = + | Assignment of name: string * Expr + | FunctionDeclaration of name: string * args: string list * body: Statement list + | Return of Expr + | Do of Expr + +type File = + { Filename: string + Statements: (Statement) list + ASTDebug: string } \ No newline at end of file diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index 6218ff465b..f05ccdf006 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -1,1083 +1,152 @@ // fsharplint:disable InterfaceNames -module Fable.Transforms.Lua +module Fable.Transforms.LuaPrinter open System +open System.IO open Fable open Fable.AST -open Fable.AST.Babel - -type SourceMapping = - int * int * int * int * string option - -type Writer = - inherit IDisposable - abstract AddSourceMapping: SourceMapping -> unit - abstract EscapeJsStringLiteral: string -> string - abstract MakeImportPath: string -> string - abstract Write: string -> Async - -type Printer = - abstract Line: int - abstract Column: int - abstract PushIndentation: unit -> unit - abstract PopIndentation: unit -> unit - abstract Print: string * ?loc: SourceLocation -> unit - abstract PrintNewLine: unit -> unit - abstract AddLocation: SourceLocation option -> unit - abstract EscapeJsStringLiteral: string -> string - abstract MakeImportPath: string -> string - -type PrinterImpl(writer: Writer) = - // TODO: We can make this configurable later - let indentSpaces = " " - let builder = Text.StringBuilder() - let mutable indent = 0 - let mutable line = 1 - let mutable column = 0 - - let addLoc (loc: SourceLocation option) = - match loc with - | None -> () - | Some loc -> - writer.AddSourceMapping( - loc.start.line, - loc.start.column, - line, - column, - loc.identifierName) - - member _.Flush(): Async = - async { - do! writer.Write(builder.ToString()) - builder.Clear() |> ignore - } - - interface IDisposable with - member _.Dispose() = writer.Dispose() - - interface Printer with - member _.Line = line - member _.Column = column - - member _.PrintNewLine() = - builder.AppendLine() |> ignore - line <- line + 1 - column <- 0 - - member _.PushIndentation() = - indent <- indent + 1 - - member _.PopIndentation() = - if indent > 0 then indent <- indent - 1 - - member _.AddLocation(loc) = - addLoc loc - - member _.Print(str: string, ?loc) = - addLoc loc - - if column = 0 then - let indent = String.replicate indent indentSpaces - builder.Append(indent) |> ignore - column <- indent.Length - - builder.Append(str) |> ignore - column <- column + str.Length - - member this.EscapeJsStringLiteral(str) = - writer.EscapeJsStringLiteral(str) - - member this.MakeImportPath(path) = - writer.MakeImportPath(path) - - -module PrinterExtensions = - type Printer with - member printer.PrintBlock(nodes: 'a array, printNode: Printer -> 'a -> unit, printSeparator: Printer -> unit, ?skipNewLineAtEnd) = - let skipNewLineAtEnd = defaultArg skipNewLineAtEnd false - // printer.Print("") - printer.PrintNewLine() - printer.PushIndentation() - for node in nodes do - printNode printer node - printSeparator printer - printer.PopIndentation() - printer.Print("end") - if not skipNewLineAtEnd then - printer.PrintNewLine() - - member printer.PrintStatementSeparator() = - if printer.Column > 0 then - printer.Print(";") - printer.PrintNewLine() - - member this.HasSideEffects(e: Expression) = - match e with - | Undefined(_) - | Literal(NullLiteral(_)) - | Literal(Literal.StringLiteral(_)) - | Literal(BooleanLiteral(_)) - | Literal(NumericLiteral(_)) -> false - // Constructors of classes deriving from System.Object add an empty object at the end - | ObjectExpression(properties, loc) -> properties.Length > 0 - | UnaryExpression(prefix, argument, operator, loc) when operator = "void" -> this.HasSideEffects(argument) - // Some identifiers may be stranded as the result of imports - // intended only for side effects, see #2228 - | Expression.Identifier(_) -> false - // Sometimes empty IIFE remain in the AST - | CallExpression(ArrowFunctionExpression(_,(BlockStatement body),_,_,_),_,_) -> - body |> Array.exists this.IsProductiveStatement - | _ -> true - - member this.IsProductiveStatement(s: Statement) = - match s with - | ExpressionStatement(expr) -> this.HasSideEffects(expr) - | _ -> true - - member printer.PrintProductiveStatement(s: Statement, ?printSeparator) = - if printer.IsProductiveStatement(s) then - printer.Print(s) - printSeparator |> Option.iter (fun f -> f printer) - - member printer.PrintProductiveStatements(statements: Statement[]) = - for s in statements do - printer.PrintProductiveStatement(s, (fun p -> p.PrintStatementSeparator())) - - member printer.PrintBlock(nodes: Statement array, ?skipNewLineAtEnd) = - printer.PrintBlock(nodes, - (fun p s -> p.PrintProductiveStatement(s)), - (fun p -> p.PrintStatementSeparator()), - ?skipNewLineAtEnd=skipNewLineAtEnd) - - member printer.PrintOptional(node: Node option, ?before: string) = - match node with - | None -> () - | Some node -> - match before with - | Some before -> - printer.Print(before) - | _ -> () - printer.Print(node) - - member printer.PrintOptional(node: Expression option, ?before: string) = - printer.PrintOptional(node |> Option.map Expression, ?before=before) - member printer.PrintOptional(node: TypeParameterDeclaration option, ?before: string) = - printer.PrintOptional(node |> Option.map Node.TypeParameterDeclaration, ?before=before) - member printer.PrintOptional(node: TypeAnnotation option, ?before: string) = - printer.PrintOptional(node |> Option.map Node.TypeAnnotation, ?before=before) - member printer.PrintOptional(node: Identifier option, ?before: string) = - printer.PrintOptional(node |> Option.map Expression.Identifier, ?before=before) - member printer.PrintOptional(node: Literal option, ?before: string) = - printer.PrintOptional(node |> Option.map Literal, ?before=before) - member printer.PrintOptional(node: StringLiteral option, ?before: string) = - printer.PrintOptional(node |> Option.map Literal.StringLiteral, ?before=before) - member printer.PrintOptional(node: TypeParameterInstantiation option, ?before: string) = - printer.PrintOptional(node |> Option.map Node.TypeParameterInstantiation, ?before=before) - member printer.PrintOptional(node: Statement option, ?before: string) = - printer.PrintOptional(node |> Option.map Statement, ?before=before) - member printer.PrintOptional(node: Declaration option, ?before: string) = - printer.PrintOptional(node |> Option.map Declaration, ?before=before) - member printer.PrintOptional(node: VariableDeclaration option, ?before: string) = - printer.PrintOptional(node |> Option.map Declaration.VariableDeclaration, ?before=before) - member printer.PrintOptional(node: CatchClause option, ?before: string) = - printer.PrintOptional(node |> Option.map Node.CatchClause, ?before=before) - member printer.PrintOptional(node: BlockStatement option, ?before: string) = - printer.PrintOptional(node |> Option.map Statement.BlockStatement, ?before=before) - - member printer.PrintArray(nodes: 'a array, printNode: Printer -> 'a -> unit, printSeparator: Printer -> unit) = - for i = 0 to nodes.Length - 1 do - printNode printer nodes.[i] - if i < nodes.Length - 1 then - printSeparator printer - - member printer.PrintCommaSeparatedArray(nodes: Node array) = - printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) - member printer.PrintCommaSeparatedArray(nodes: Pattern array) = - printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) - member printer.PrintCommaSeparatedArray(nodes: ImportSpecifier array) = - printer.PrintArray(nodes, (fun p x -> - match x with - | ImportMemberSpecifier(local, imported) -> p.PrintImportMemberSpecific(local, imported) - | ImportDefaultSpecifier(local) -> printer.Print(local) - | ImportNamespaceSpecifier(local) -> printer.PrintImportNamespaceSpecifier(local) - ), (fun p -> p.Print(", "))) - member printer.PrintCommaSeparatedArray(nodes: ExportSpecifier array) = - printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) - member printer.PrintCommaSeparatedArray(nodes: FunctionTypeParam array) = - printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) - member printer.PrintCommaSeparatedArray(nodes: TypeAnnotationInfo array) = - printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) - member printer.PrintCommaSeparatedArray(nodes: TypeParameter array) = - printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) - - member printer.PrintCommaSeparatedArray(nodes: Expression array) = - printer.PrintArray(nodes, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) - - - // TODO: (super) type parameters, implements - member printer.PrintClass(id: Identifier option, superClass: Expression option, - superTypeParameters: TypeParameterInstantiation option, - typeParameters: TypeParameterDeclaration option, - implements: ClassImplements array option, body: ClassBody, loc) = - printer.Print("class", ?loc=loc) - printer.PrintOptional(id, " ") - printer.PrintOptional(typeParameters) - match superClass with - | Some (Expression.Identifier(Identifier(typeAnnotation=Some(typeAnnotation)))) -> - printer.Print(" extends ") - printer.Print(typeAnnotation) - | _ -> printer.PrintOptional(superClass, " extends ") - // printer.PrintOptional(superTypeParameters) - match implements with - | Some implements when not (Array.isEmpty implements) -> - printer.Print(" implements ") - printer.PrintArray(implements, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) - | _ -> () - printer.Print(" ") - printer.Print(body) - - member printer.PrintFunction(id: Identifier option, parameters: Pattern array, body: BlockStatement, - typeParameters: TypeParameterDeclaration option, returnType: TypeAnnotation option, loc, ?isDeclaration, ?isArrow) = - let areEqualPassedAndAppliedArgs (passedArgs: Pattern[]) (appliedAgs: Expression[]) = - Array.zip passedArgs appliedAgs - |> Array.forall (function - | RestElement(name=name), Expression.Identifier(Identifier(name=idName)) -> name = idName - | _ -> false) - - let isDeclaration = defaultArg isDeclaration false - let isArrow = defaultArg isArrow false - - printer.AddLocation(loc) - - // Check if we can remove the function - let skipExpr = - match body.Body with - | [| ReturnStatement(argument, loc) |] when not isDeclaration -> - match argument with - | CallExpression(callee, arguments, loc) when parameters.Length = arguments.Length -> - // To be sure we're not running side effects when deleting the function, - // check the callee is an identifier (accept non-computed member expressions too?) - match callee with - | Expression.Identifier(_) when areEqualPassedAndAppliedArgs parameters arguments -> - Some callee - | _ -> None - | _ -> None - | _ -> None - - match skipExpr with - | Some e -> printer.Print(e) - | None -> - if isArrow then - // Remove parens if we only have one argument? (and no annotation) - printer.PrintOptional(typeParameters) - printer.Print("(") - printer.PrintCommaSeparatedArray(parameters) - printer.Print(")") - printer.PrintOptional(returnType) - printer.Print(" => ") - match body.Body with - | [| ReturnStatement(argument, loc) |] -> - match argument with - | ObjectExpression(_) -> printer.WithParens(argument) - | MemberExpression(name, object, property, computed, loc) -> - match object with - | ObjectExpression(_) -> printer.PrintMemberExpression(name, object, property, computed, loc, objectWithParens=true) - | _ -> printer.Print(argument) - | _ -> printer.ComplexExpressionWithParens(argument) - | _ -> printer.PrintBlock(body.Body, skipNewLineAtEnd=true) - else - printer.Print("function ") - printer.PrintOptional(id) - printer.PrintOptional(typeParameters) - printer.Print("(") - printer.PrintCommaSeparatedArray(parameters) - printer.Print(")") - printer.PrintOptional(returnType) - printer.Print(" ") - printer.PrintBlock(body.Body, skipNewLineAtEnd=true) - - member printer.WithParens(expr: Expression) = - printer.Print("(") - printer.Print(expr) - printer.Print(")") - - /// Surround with parens anything that can potentially conflict with operator precedence - member printer.ComplexExpressionWithParens(expr: Expression) = - match expr with - | Undefined(_) - | Literal(NullLiteral(_)) - | Literal(Literal.StringLiteral(_)) - | Literal(BooleanLiteral(_)) - | Literal(NumericLiteral(_)) - | Expression.Identifier(_) - | MemberExpression(_) - | CallExpression(_) - | ThisExpression(_) - | Super(_) - | SpreadElement(_) - | ArrayExpression(_) - | ObjectExpression(_) -> printer.Print(expr) - | _ -> printer.WithParens(expr) - - member printer.PrintOperation(left, operator, right, loc) = - printer.AddLocation(loc) - printer.ComplexExpressionWithParens(left) - printer.Print(" " + operator + " ") - printer.ComplexExpressionWithParens(right) - - member printer.Print(node: Node) = - match node with - | Pattern(n) -> printer.Print(n) - | Statement(n) -> printer.Print(n) - | Node.ClassBody(n) -> printer.Print(n) - | Expression(n) -> printer.Print(n) - | Node.SwitchCase(n) -> printer.Print(n) - | Node.CatchClause(n) -> printer.Print(n) - | ObjectMember(n) -> printer.Print(n) - | Node.TypeParameter(n) -> printer.Print(n) - | Node.TypeAnnotation(n) -> ()//printer.Print(n) - | Node.ExportSpecifier(n) -> printer.Print(n) - | Node.InterfaceExtends(n) -> printer.Print(n) - | ModuleDeclaration(n) -> printer.Print(n) - | Node.FunctionTypeParam(n) -> printer.Print(n) - | Node.ObjectTypeProperty(n) -> printer.Print(n) - | Node.TypeAnnotationInfo(n) -> printer.Print(n) - | Node.TypeParameterDeclaration(n) -> printer.Print(n) - | Node.TypeParameterInstantiation(n) -> printer.Print(n) - | Node.Program(_) - | Directive(_) - | ImportSpecifier(_) - | Node.ObjectTypeIndexer(_) - | Node.VariableDeclarator(_) - | Node.ObjectTypeCallProperty(_) - | Node.ObjectTypeInternalSlot(_) -> failwith "Not implemented" - - member printer.Print(expr: Expression) = - match expr with - | Super(loc) -> printer.Print("super", ?loc = loc) - | Literal(n) -> printer.Print(n) - | Undefined(loc) -> printer.Print("undefined", ?loc=loc) - | Expression.Identifier(n) -> printer.Print(n) - | NewExpression(callee, arguments, typeArguments, loc) -> printer.PrintNewExpression(callee, arguments, typeArguments, loc) - | SpreadElement(argument, loc) -> - printer.Print("...", ?loc = loc) - printer.ComplexExpressionWithParens(argument) - | ThisExpression(loc) -> printer.Print("this", ?loc = loc) - | CallExpression(callee, arguments, loc) -> printer.PrintCallExpression(callee, arguments, loc) - | EmitExpression(value, args, loc) -> printer.PrintEmitExpression(value, args, loc) - | ArrayExpression(elements, loc) -> - printer.Print("[", ?loc = loc) - printer.PrintCommaSeparatedArray(elements) - printer.Print("]") - | ClassExpression(body, id, superClass, implements, superTypeParameters, typeParameters, loc) -> - printer.PrintClass(id, superClass, superTypeParameters, typeParameters, implements, body, loc) - | Expression.ClassImplements(n) -> printer.Print(n) - | UnaryExpression(prefix, argument, operator, loc) -> printer.PrintUnaryExpression(prefix, argument, operator, loc) - | UpdateExpression(prefix, argument, operator, loc) -> printer.PrintUpdateExpression(prefix, argument, operator, loc) - | ObjectExpression(properties, loc) -> printer.PrintObjectExpression(properties, loc) - | BinaryExpression(left, right, operator, loc) -> printer.PrintOperation(left, operator, right, loc) - | MemberExpression(name, object, property, computed, loc) -> printer.PrintMemberExpression(name, object, property, computed, loc) - | LogicalExpression(left, operator, right, loc) -> printer.PrintOperation(left, operator, right, loc) - | SequenceExpression(expressions, loc) -> - // A comma-separated sequence of expressions. - printer.AddLocation(loc) - // TODO: Remove parens if we end up with only one expression - // (when the ones before last don't have side effects) - printer.Print("(") - let last = expressions.Length - 1 - for i = 0 to last do - let e = expressions.[i] - if i = last then - printer.Print(e) - elif printer.HasSideEffects(e) then - printer.Print(e) - printer.Print(", ") - printer.Print(")") - | FunctionExpression(id, ``params``, body, typeParameters, returnType, loc) -> - printer.PrintFunction(id, ``params``, body, returnType, typeParameters, loc) - | AssignmentExpression(left, right, operator, loc) -> printer.PrintOperation(left, operator, right, loc) - | ConditionalExpression(test, consequent, alternate, loc) -> printer.PrintConditionalExpression(test, consequent, alternate, loc) - | ArrowFunctionExpression(``params``, body, returnType, typeParameters, loc) -> - printer.PrintArrowFunctionExpression(``params``, body, returnType, typeParameters, loc) - - member printer.Print(pattern: Pattern) = - match pattern with - | Pattern.Identifier(p) -> printer.Print(p) - | RestElement(name, argument, typeAnnotation, loc) -> - printer.Print("...", ?loc=loc) - printer.Print(argument) - printer.PrintOptional(typeAnnotation) - member printer.Print(literal: Literal) = - match literal with - | RegExp(pattern, flags, loc) -> printer.PrintRegExp(pattern, flags, loc) - | NullLiteral(loc) -> printer.Print("null", ?loc=loc) - | Literal.StringLiteral(l) -> printer.Print(l) - | BooleanLiteral(value, loc) -> printer.Print((if value then "true" else "false"), ?loc=loc) - | NumericLiteral(value, loc) -> printer.PrintNumeric(value, loc) - | Literal.DirectiveLiteral(l) -> failwith "not implemented" - - member printer.Print(stmt: Statement) = - match stmt with - | Declaration(s) -> printer.Print(s) - | IfStatement(test, consequent, alternate, loc) -> printer.PrintIfStatment(test, consequent, alternate, loc) - | TryStatement(block, handler, finalizer, loc) -> printer.PrintTryStatement(block, handler, finalizer, loc) - | ForStatement(body, init, test, update, loc) -> printer.PrintForStatement(body, init, test, update, loc) - | BreakStatement(label, loc) -> printer.Print("break", ?loc = loc) - | WhileStatement(test, body, loc) -> printer.PrintWhileStatment(test, body, loc) - | ThrowStatement(argument, loc) -> - printer.Print("throw ", ?loc = loc) - printer.Print(argument) - | Statement.BlockStatement(s) -> printer.Print(s) - | ReturnStatement(argument, loc) -> - printer.Print("return ", ?loc = loc) - printer.Print(argument) - | SwitchStatement(discriminant, cases, loc) -> printer.PrintSwitchStatement(discriminant, cases, loc) - | LabeledStatement(body, label) -> printer.PrintLabeledStatement(body, label) - | DebuggerStatement(loc) -> printer.Print("debugger", ?loc = loc) - | ContinueStatement(label, loc) -> - printer.Print("continue", ?loc=loc) - printer.PrintOptional(label, " ") - - | ExpressionStatement(expr) -> printer.Print(expr) - - member printer.Print(decl: Declaration) = - match decl with - | ClassDeclaration(body, id, superClass, implements, superTypeParameters, typeParameters, loc) -> - printer.PrintClass(id, superClass, superTypeParameters, typeParameters, implements, body, loc) - | Declaration.VariableDeclaration(d) -> printer.Print(d) - | FunctionDeclaration(``params``, body, id, returnType, typeParameters, loc) -> - printer.PrintFunction(Some id, ``params``, body, typeParameters, returnType, loc, isDeclaration=true) - printer.PrintNewLine() - | InterfaceDeclaration(id, body, extends, implements, typeParameters) -> - printer.PrintInterfaceDeclaration(id, body, extends, implements, typeParameters) - - member printer.Print(md: ModuleDeclaration) = - match md with - | ImportDeclaration(specifiers, source) -> printer.PrintImportDeclaration(specifiers, source) - | ExportNamedReferences(specifiers, source) -> - // printer.Print("export ") - // printer.Print("{ ") - printer.PrintCommaSeparatedArray(specifiers) - // printer.Print(" }") - // printer.PrintOptional(source, " from ") - specifiers - |> Array.iter(fun s -> - printer.Print("mod.") - printer.Print(s)) - | ExportNamedDeclaration(declaration) -> - printer.Print("mod.") - printer.Print(declaration) - | ExportAllDeclaration(source, loc) -> - printer.Print("Not supported!") - // printer.Print("export * from ", ?loc=loc) - // printer.Print(source) - | PrivateModuleDeclaration(statement) -> - if printer.IsProductiveStatement(statement) then - printer.Print(statement) - | ExportDefaultDeclaration(declaration) -> - printer.Print("mod.") - match declaration with - | Choice1Of2 x -> printer.Print(x) - | Choice2Of2 x -> printer.Print(x) - - member printer.PrintEmitExpression(value, args, loc) = - printer.AddLocation(loc) - - let inline replace pattern (f: System.Text.RegularExpressions.Match -> string) input = - System.Text.RegularExpressions.Regex.Replace(input, pattern, f) - - let printSegment (printer: Printer) (value: string) segmentStart segmentEnd = - let segmentLength = segmentEnd - segmentStart - if segmentLength > 0 then - let segment = value.Substring(segmentStart, segmentLength) - let subSegments = System.Text.RegularExpressions.Regex.Split(segment, @"\r?\n") - for i = 1 to subSegments.Length do - let subSegment = - // Remove whitespace in front of new lines, - // indent will be automatically applied - if printer.Column = 0 then subSegments.[i - 1].TrimStart() - else subSegments.[i - 1] - if subSegment.Length > 0 then - printer.Print(subSegment) - if i < subSegments.Length then - printer.PrintNewLine() - - // Macro transformations - // https://fable.io/docs/communicate/js-from-fable.html#Emit-when-F-is-not-enough - let value = - value - |> replace @"\$(\d+)\.\.\." (fun m -> - let rep = ResizeArray() - let i = int m.Groups.[1].Value - for j = i to args.Length - 1 do - rep.Add("$" + string j) - String.concat ", " rep) - - |> replace @"\{\{\s*\$(\d+)\s*\?(.*?)\:(.*?)\}\}" (fun m -> - let i = int m.Groups.[1].Value - match args.[i] with - | Literal(BooleanLiteral(value=value)) when value -> m.Groups.[2].Value - | _ -> m.Groups.[3].Value) - - |> replace @"\{\{([^\}]*\$(\d+).*?)\}\}" (fun m -> - let i = int m.Groups.[2].Value - match Array.tryItem i args with - | Some _ -> m.Groups.[1].Value - | None -> "") - - // This is to emit string literals as JS, I think it's no really - // used and it shouldn't be necessary with the new emitJsExpr - // |> replace @"\$(\d+)!" (fun m -> - // let i = int m.Groups.[1].Value - // match Array.tryItem i args with - // | Some(:? StringLiteral as s) -> s.Value - // | _ -> "") - - let matches = System.Text.RegularExpressions.Regex.Matches(value, @"\$\d+") - if matches.Count > 0 then - for i = 0 to matches.Count - 1 do - let m = matches.[i] - - let segmentStart = - if i > 0 then matches.[i-1].Index + matches.[i-1].Length - else 0 - - printSegment printer value segmentStart m.Index - - let argIndex = int m.Value.[1..] - match Array.tryItem argIndex args with - | Some e -> printer.ComplexExpressionWithParens(e) - | None -> printer.Print("undefined") - - let lastMatch = matches.[matches.Count - 1] - printSegment printer value (lastMatch.Index + lastMatch.Length) value.Length - else - printSegment printer value 0 value.Length - - member printer.Print(identifier: Identifier) = - let (Identifier(name, optional, typeAnnotation, loc)) = identifier - printer.Print(name, ?loc=loc) - if optional = Some true then - printer.Print("?") - printer.PrintOptional(typeAnnotation) - - member printer.PrintRegExp(pattern, flags, loc) = - printer.Print("/", ?loc=loc) - printer.Print(pattern) - printer.Print("/") - printer.Print(flags) - - member printer.Print(node: StringLiteral) = - let (StringLiteral(value, loc)) = node - printer.Print("\"", ?loc=loc) - printer.Print(printer.EscapeJsStringLiteral(value)) - printer.Print("\"") - - member printer.PrintNumeric(value, loc) = - let value = - match value.ToString(System.Globalization.CultureInfo.InvariantCulture) with - | "∞" -> "Infinity" - | "-∞" -> "-Infinity" - | value -> value - printer.Print(value, ?loc=loc) - - member printer.Print(node: BlockStatement) = - printer.PrintBlock(node.Body) - - member printer.PrintLabeledStatement(body, label) = - printer.Print(label) - printer.Print(":") - printer.PrintNewLine() - // Don't push indent - printer.Print(body) - -// Control Flow - member printer.PrintIfStatment(test, consequent, alternate, loc) = - printer.AddLocation(loc) - printer.Print("if (", ?loc=loc) - printer.Print(test) - printer.Print(") ") - printer.Print(consequent) - match alternate with - | None -> () - | Some alternate -> - if printer.Column > 0 then printer.Print(" ") - match alternate with - | IfStatement(test, consequent, alternate, loc) -> - printer.Print("else ") - printer.PrintIfStatment(test, consequent, alternate, loc) - | alternate -> - let statements = - match alternate with - | Statement.BlockStatement(b) -> b.Body - | alternate -> [|alternate|] - // Get productive statements and skip `else` if they're empty - statements - |> Array.filter printer.IsProductiveStatement - |> function - | [||] -> () - | statements -> - printer.Print("else ") - printer.PrintBlock(statements) - if printer.Column > 0 then - printer.PrintNewLine() - - /// A case (if test is an Expression) or default (if test === null) clause in the body of a switch statement. - member printer.Print(node: SwitchCase) = - let (SwitchCase(test, consequent, loc)) = node - printer.AddLocation(loc) - - match test with - | None -> printer.Print("default") - | Some test -> - printer.Print("case ") - printer.Print(test) - - printer.Print(":") - - match consequent.Length with - | 0 -> printer.PrintNewLine() - | 1 -> - printer.Print(" ") - printer.Print(consequent.[0]) - | _ -> - printer.Print(" ") - printer.PrintBlock(consequent) - - member printer.PrintSwitchStatement(discriminant, cases, loc) = - printer.Print("switch (", ?loc=loc) - printer.Print(discriminant) - printer.Print(") ") - printer.PrintBlock(cases, (fun p x -> p.Print(x)), fun _ -> ()) - -// Exceptions - member printer.Print(node: CatchClause) = - let (CatchClause(param, body, loc)) = node - // "catch" is being printed by TryStatement - printer.Print("(", ?loc = loc) - printer.Print(param) - printer.Print(") ") - printer.Print(body) - - member printer.PrintTryStatement(block, handler, finalizer, loc) = - printer.Print("try ", ?loc = loc) - printer.Print(block) - printer.PrintOptional(handler, "catch ") - printer.PrintOptional(finalizer, "finally ") - -// Declarations - - member printer.Print(node: VariableDeclaration) = - let (VariableDeclaration(declarations, kind, loc)) = node - printer.Print(kind + " ", ?loc = loc) - let canConflict = declarations.Length > 1 - - for i = 0 to declarations.Length - 1 do - let (VariableDeclarator(id, init)) = declarations.[i] - printer.Print(id) - - match init with - | None -> () - | Some e -> - printer.Print(" = ") - if canConflict then printer.ComplexExpressionWithParens(e) - else printer.Print(e) - if i < declarations.Length - 1 then - printer.Print(", ") - - member printer.PrintWhileStatment(test, body, loc) = - printer.Print("while (", ?loc = loc) - printer.Print(test) - printer.Print(") ") - printer.Print(body) - - member printer.PrintForStatement(body, init, test, update, loc) = - printer.Print("for (", ?loc = loc) - printer.PrintOptional(init) - printer.Print("; ") - printer.PrintOptional(test) - printer.Print("; ") - printer.PrintOptional(update) - printer.Print(") ") - printer.Print(body) - - /// A fat arrow function expression, e.g., let foo = (bar) => { /* body */ }. - member printer.PrintArrowFunctionExpression(``params``, body, returnType, typeParameters, loc) = - printer.PrintFunction( - None, - ``params``, - body, - typeParameters, - returnType, - loc, - isArrow = true - ) - - member printer.Print(node: ObjectMember) = - match node with - | ObjectProperty(key, value, computed) -> printer.PrintObjectProperty(key, value, computed) - | ObjectMethod(kind, key, ``params``, body, computed, returnType, typeParameters, loc) -> - printer.PrintObjectMethod(kind, key, ``params``, body, computed, returnType, typeParameters, loc) - - member printer.PrintObjectProperty(key, value, computed) = - if computed then - printer.Print("[") - printer.Print(key) - printer.Print("]") - else - printer.Print(key) - printer.Print(": ") - printer.Print(value) - - member printer.PrintObjectMethod(kind, key, ``params``, body, computed, returnType, typeParameters, loc) = - printer.AddLocation(loc) - - if kind <> "method" then - printer.Print(kind + " ") - - if computed then - printer.Print("[") - printer.Print(key) - printer.Print("]") - else - printer.Print(key) - - printer.PrintOptional(typeParameters) - printer.Print("(") - printer.PrintCommaSeparatedArray(``params``) - printer.Print(")") - printer.PrintOptional(returnType) - printer.Print(" ") - - printer.PrintBlock(body.Body, skipNewLineAtEnd=true) - - member printer.PrintMemberExpression(name, object, property, computed, loc, ?objectWithParens: bool) = - printer.AddLocation(loc) - match objectWithParens, object with - | Some true, _ | _, Literal(NumericLiteral(_)) -> printer.WithParens(object) - | _ -> printer.ComplexExpressionWithParens(object) - if computed then - printer.Print("[") - printer.Print(property) - printer.Print("]") - else - printer.Print(".") - printer.Print(property) - - member printer.PrintObjectExpression(properties, loc) = - let printSeparator(p: Printer) = - p.Print(",") - p.PrintNewLine() - - printer.AddLocation(loc) - if Array.isEmpty properties then printer.Print("{}") - else printer.PrintBlock(properties, (fun p x -> p.Print(x)), printSeparator, skipNewLineAtEnd=true) - - member printer.PrintConditionalExpression(test, consequent, alternate, loc) = - printer.AddLocation(loc) - match test with - // TODO: Move node optimization to Fable2Babel as with IfStatement? - | Literal(BooleanLiteral(value=value)) -> - if value then printer.Print(consequent) - else printer.Print(alternate) - | _ -> - printer.ComplexExpressionWithParens(test) - printer.Print(" ? ") - printer.ComplexExpressionWithParens(consequent) - printer.Print(" : ") - printer.ComplexExpressionWithParens(alternate) - - member printer.PrintCallExpression(callee, arguments, loc) = - printer.AddLocation(loc) - printer.ComplexExpressionWithParens(callee) - printer.Print("(") - printer.PrintCommaSeparatedArray(arguments) - printer.Print(")") - - member printer.PrintNewExpression(callee, arguments, typeArguments, loc) = - printer.Print("new ", ?loc=loc) - printer.ComplexExpressionWithParens(callee) - printer.Print("(") - printer.PrintCommaSeparatedArray(arguments) - printer.Print(")") - - member printer.PrintUnaryExpression(prefix, argument, operator, loc) = - printer.AddLocation(loc) - match operator with - | "-" | "+" | "!" | "~" -> printer.Print(operator) - | _ -> printer.Print(operator + " ") - printer.ComplexExpressionWithParens(argument) - - member printer.PrintUpdateExpression(prefix, argument, operator, loc) = - printer.AddLocation(loc) - if prefix then - printer.Print(operator) - printer.ComplexExpressionWithParens(argument) - else - printer.ComplexExpressionWithParens(argument) - printer.Print(operator) - -// Binary Operations - - member printer.Print(node: ClassMember) = - match node with - | ClassMethod(kind, key, ``params``, body, computed, ``static``, ``abstract``, returnType, typeParameters, loc) -> - printer.PrintClassMethod(kind, key, ``params``, body, computed, ``static``, ``abstract``, returnType, typeParameters, loc) - | ClassProperty(key, value, computed, ``static``, optional, typeAnnotation, loc) -> printer.PrintClassProperty(key, value, computed, ``static``, optional, typeAnnotation, loc) - - member printer.PrintClassMethod(kind, key, ``params``, body, computed, ``static``, ``abstract``, returnType, typeParameters, loc) = - printer.AddLocation(loc) - - let keywords = [ - if ``static`` = Some true then yield "static" - if ``abstract`` = Some true then yield "abstract" - if kind = "get" || kind = "set" then yield kind - ] - - if not (List.isEmpty keywords) then - printer.Print((String.concat " " keywords) + " ") - - if computed then - printer.Print("[") - printer.Print(key) - printer.Print("]") - else - printer.Print(key) - - printer.PrintOptional(typeParameters) - printer.Print("(") - printer.PrintCommaSeparatedArray(``params``) - printer.Print(")") - printer.PrintOptional(returnType) - printer.Print(" ") - - printer.Print(body) - - member printer.PrintClassProperty(key, value, computed, ``static``, optional, typeAnnotation, loc) = - printer.AddLocation(loc) - if ``static`` then - printer.Print("static ") - if computed then - printer.Print("[") - printer.Print(key) - printer.Print("]") - else - printer.Print(key) - if optional then - printer.Print("?") - printer.PrintOptional(typeAnnotation) - printer.PrintOptional(value, ": ") - - member printer.Print(node: ClassImplements) = - let (ClassImplements(id, typeParameters)) = node - printer.Print(id) - printer.PrintOptional(typeParameters) - - member printer.Print(node: ClassBody) = - let (ClassBody(body, loc)) = node - printer.AddLocation(loc) - printer.PrintBlock(body, (fun p x -> p.Print(x)), (fun p -> p.PrintStatementSeparator())) - - member printer.PrintImportMemberSpecific(local, imported) = - // Don't print the braces, node will be done in the import declaration - printer.Print(imported) - if imported.Name <> local.Name then - printer.Print(" as ") - printer.Print(local) - - member printer.PrintImportNamespaceSpecifier(local) = - printer.Print("* as ") - printer.Print(local) - - member printer.PrintImportDeclaration(specifiers, source) = - let members = specifiers |> Array.choose (function ImportMemberSpecifier(local, imported) -> Some (ImportMemberSpecifier(local, imported)) | _ -> None) - let defaults = specifiers|> Array.choose (function ImportDefaultSpecifier(local) -> Some (ImportDefaultSpecifier(local)) | _ -> None) - let namespaces = specifiers |> Array.choose (function ImportNamespaceSpecifier(local) -> Some (ImportNamespaceSpecifier(local)) | _ -> None) - - // printer.Print("import ") - - if not(Array.isEmpty defaults) then - printer.PrintCommaSeparatedArray(defaults) - if not(Array.isEmpty namespaces && Array.isEmpty members) then - printer.Print(", ") - - if not(Array.isEmpty namespaces) then - printer.PrintCommaSeparatedArray(namespaces) - if not(Array.isEmpty members) then - printer.Print(", ") - - if not(Array.isEmpty members) then - //printer.Print("{ ") - printer.PrintCommaSeparatedArray(members) - //printer.Print(" }") - - printer.Print(" = ") - printer.Print("require") - printer.Print("(") - - // if not(Array.isEmpty defaults && Array.isEmpty namespaces && Array.isEmpty members) then - // printer.Print(" from ") - - printer.Print("\"") - let (StringLiteral(value, _)) = source - printer.Print(printer.MakeImportPath(value)) - printer.Print("\"") - printer.Print(")") - - member printer.Print(node: ExportSpecifier) = - let (ExportSpecifier (local, exported)) = node - // Don't print the braces, node will be done in the export declaration - printer.Print(local) - if exported.Name <> local.Name then - printer.Print(" as ") - printer.Print(exported) - - member printer.Print(node: TypeAnnotationInfo) = - match node with - | StringTypeAnnotation -> printer.Print("string") - | NumberTypeAnnotation -> printer.Print("number") - | TypeAnnotationInfo(an) -> printer.Print(an) - | BooleanTypeAnnotation -> printer.Print("boolean") - | AnyTypeAnnotation -> printer.Print("any") - | VoidTypeAnnotation -> printer.Print("void") - | TupleTypeAnnotation(types) -> - printer.Print("[") - printer.PrintCommaSeparatedArray(types) - printer.Print("]") - | UnionTypeAnnotation(types) -> - printer.PrintArray(types, (fun p x -> p.Print(x)), (fun p -> p.Print(" | "))) - | FunctionTypeAnnotation(``params``, returnType, typeParameters, rest) -> printer.PrintFunctionTypeAnnotation(``params``, returnType, typeParameters, rest) - | NullableTypeAnnotation(typeAnnotation) -> printer.Print(typeAnnotation) - | GenericTypeAnnotation(id, typeParameters) -> - printer.Print(id) - printer.PrintOptional(typeParameters) - | TypeAnnotationInfo.ObjectTypeAnnotation(an) -> printer.Print(an) - - member printer.Print((TypeAnnotation info): TypeAnnotation) = - printer.Print(": ") - printer.Print(info) - - member printer.Print((TypeParameter(name=name)): TypeParameter) = - printer.Print(name) - // printer.PrintOptional(bound) - // printer.PrintOptional(``default``) - - member printer.Print((TypeParameterDeclaration ``params``): TypeParameterDeclaration) = - printer.Print("<") - printer.PrintCommaSeparatedArray(``params``) - printer.Print(">") - - member printer.Print((TypeParameterInstantiation ``params``) : TypeParameterInstantiation) = - printer.Print("<") - printer.PrintCommaSeparatedArray(``params``) - printer.Print(">") - - member printer.Print(node: FunctionTypeParam) = - let (FunctionTypeParam(name, typeAnnotation, optional)) = node - printer.Print(name) - if optional = Some true then - printer.Print("?") - printer.Print(": ") - printer.Print(typeAnnotation) - - member printer.PrintFunctionTypeAnnotation(``params``, returnType, typeParameters, rest) = - printer.PrintOptional(typeParameters) - printer.Print("(") - printer.PrintCommaSeparatedArray(``params``) - if Option.isSome rest then - printer.Print("...") - printer.Print(rest.Value) - printer.Print(") => ") - printer.Print(returnType) - - member printer.Print(node: ObjectTypeProperty) = - let (ObjectTypeProperty(key, value, kind, computed, ``static``, optional, proto, method)) = node - - if ``static`` then - printer.Print("static ") - if Option.isSome kind then - printer.Print(kind.Value + " ") - if computed then - printer.Print("[") - printer.Print(key) - printer.Print("]") - else - printer.Print(key) - if optional then - printer.Print("?") - // TODO: proto, method - printer.Print(": ") - printer.Print(value) - - member printer.Print(node: ObjectTypeAnnotation) = - let (ObjectTypeAnnotation(properties, indexers, callProperties, internalSlots, exact)) = node - // printer.Print("{") - printer.PrintNewLine() - printer.PushIndentation() - printer.PrintArray(properties, (fun p x -> p.Print(x)), (fun p -> p.PrintStatementSeparator())) - printer.PrintArray(indexers, (fun p x -> p.Print(x |> Node.ObjectTypeIndexer)), (fun p -> p.PrintStatementSeparator())) - printer.PrintArray(callProperties, (fun p x -> p.Print(x |> Node.ObjectTypeCallProperty)), (fun p -> p.PrintStatementSeparator())) - printer.PrintArray(internalSlots, (fun p x -> p.Print(x |> Node.ObjectTypeInternalSlot)), (fun p -> p.PrintStatementSeparator())) - printer.PrintNewLine() - printer.PopIndentation() - printer.Print("end") - printer.PrintNewLine() - - member printer.Print(node: InterfaceExtends) = - let (InterfaceExtends(id, typeParameters)) = node - printer.Print(id) - printer.PrintOptional(typeParameters) - - member printer.PrintInterfaceDeclaration(id, body, extends, implements, typeParameters) = - printer.Print("interface ") - printer.Print(id) - printer.PrintOptional(typeParameters) - - if not (Array.isEmpty extends) then - printer.Print(" extends ") - printer.PrintArray(extends, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) - - if not (Array.isEmpty implements) then - printer.Print(" implements ") - printer.PrintArray(implements, (fun p x -> p.Print(x)), (fun p -> p.Print(", "))) - - printer.Print(" ") - printer.Print(body) - -open PrinterExtensions - -let run writer (program: Program): Async = - let printDeclWithExtraLine extraLine (printer: Printer) (decl: ModuleDeclaration) = - printer.Print(decl) - - if printer.Column > 0 then - printer.Print(";") - printer.PrintNewLine() - if extraLine then - printer.PrintNewLine() - - async { - use printer = new PrinterImpl(writer) - - let imports, restDecls = - program.Body |> Array.splitWhile (function - | ImportDeclaration(_) -> true - | _ -> false) - - (printer :> Printer).Print("local mod = {}") - (printer :> Printer).PrintNewLine() - - for decl in imports do - printDeclWithExtraLine false printer decl - - (printer :> Printer).PrintNewLine() - do! printer.Flush() - - for decl in restDecls do - printDeclWithExtraLine true printer decl - // TODO: Only flush every XXX lines? - do! printer.Flush() - (printer :> Printer).Print("return mod") - (printer :> Printer).PrintNewLine() - do! printer.Flush() - } +open Fable.AST.Lua + + +module Output = + type Writer = + { Writer: TextWriter + Indent: int + Precedence: int + CurrentNamespace: string option } + + module Helper = + let separateWithCommas = function + | [] -> "" + | [x] -> x + | lst -> lst |> List.reduce (fun acc item -> acc + " ," + item) + + let indent ctx = + { ctx with Indent = ctx.Indent + 1} + + module Writer = + let create w = + { Writer = w; Indent = 0; Precedence = Int32.MaxValue; CurrentNamespace = None } + + let writeIndent ctx = + for _ in 1 .. ctx.Indent do + ctx.Writer.Write(" ") + + let write ctx txt = + ctx.Writer.Write(txt: string) + + let writei ctx txt = + writeIndent ctx + write ctx txt + + let writeln ctx txt = + ctx.Writer.WriteLine(txt: string) + let writeCommented ctx help txt = + writeln ctx "--[[" + write ctx help + writeln ctx txt + writeln ctx " --]]" + let writeOp ctx = function + | Multiply -> write ctx "*" + | Equals -> write ctx "==" + | Divide -> write ctx """/""" + | Plus -> write ctx "+" + | Minus -> write ctx "-" + | BinaryTodo x -> writeCommented ctx "binary todo" x + let rec writeExpr ctx = function + | Ident i -> + write ctx i.Name + | Const c -> + match c with + | ConstString s -> s |> sprintf "'%s'" |> write ctx + | ConstNumber n -> n |> sprintf "%f" |> write ctx + | ConstBool b -> b |> sprintf "%b" |> write ctx + | ConstNull -> write ctx "null" + | FunctionCall(e, args) -> + writeExpr ctx e + write ctx "(" + args |> writeExprs ctx + write ctx ")" + | AnonymousFunc(args, body) -> + write ctx "(function " + write ctx "(" + args |> Helper.separateWithCommas |> write ctx + write ctx ")" + writeln ctx "" + let ctxI = indent ctx + for b in body do + writeStatement ctxI b + writei ctx "end)" + | Binary (op, left, right) -> + writeExpr ctx left + write ctx " " + writeOp ctx op + write ctx " " + writeExpr ctx right + | Get(expr, FieldGet(fieldName)) -> + writeExpr ctx expr + write ctx "." + write ctx fieldName + | Let(name, expr) -> + writei ctx name + write ctx " = " + writeExpr ctx expr + writeln ctx "" + | IfThenElse(guardExpr, thenExpr, elseExpr) -> + writei ctx "if " + writeExpr ctx guardExpr + write ctx " then" + let ctxI = indent ctx + writeExpr ctxI thenExpr + writei ctx "else " + writeExpr ctxI elseExpr + writei ctx "end" + | Macro (s, args) -> + writei ctx s + | Unknown x -> + writeCommented ctx "unknown" x + | x -> sprintf "%A" x |> writeCommented ctx "todo" + and writeExprs ctx = function + | [] -> () + | h::t -> + writeExpr ctx h + for item in t do + write ctx ", " + writeExpr ctx item + + and writeStatement ctx = function + | Assignment(name, expr) -> + writei ctx name + write ctx " = " + writeExpr ctx expr + writeln ctx "" + | FunctionDeclaration(name, args, body) -> + writei ctx "function " + write ctx name + write ctx "(" + args |> Helper.separateWithCommas |> write ctx + write ctx ")" + let ctxI = indent ctx + writeln ctxI "" + body |> List.iter (writeStatement ctxI) + writeln ctx "end" + | Return expr -> + writei ctx "return " + writeExpr ctx expr + writeln ctx "" + | Do expr -> + writei ctx "" + writeExpr ctx expr + writeln ctx "" + + let writeFile ctx (file: File) = + write ctx "test" + for s in file.Statements do + writeStatement ctx s + + //debugging + //writeln ctx "--[[" + //sprintf "%s" file.ASTDebug |> write ctx + //writeln ctx "rabbit" + //sprintf "%A" file.Statements |> write ctx + //writeln ctx " --]]" \ No newline at end of file diff --git a/src/quicktest/QuickTest.lua b/src/quicktest/QuickTest.lua index ddf9ddffdb..22b6a002ed 100644 --- a/src/quicktest/QuickTest.lua +++ b/src/quicktest/QuickTest.lua @@ -1,26 +1,11 @@ -local mod = {} -printf, toConsole = require("./.fable/fable-library.3.0.0-local-build-20210819-1229/String.js"); -int32ToString = require("./.fable/fable-library.3.0.0-local-build-20210819-1229/Util.js"); - -mod.const hello = toConsole(printf("hello world")); - -mod.const a = 2 + 2; - -mod.const b = 3 - 1; - -mod.const c = a + b; - -mod.function fn(a_1, b_1, c_1) - const d = ((a_1 + b_1) + c_1) | 0; - toConsole(printf("%A %A"))(b_1)(d); -end - -mod.function execute() - let arg10; - let copyOfStruct = a; - arg10 = int32ToString(copyOfStruct); - toConsole(printf("%s"))(arg10); - toConsole(printf("c")); -end - -return mod +testhello = +a = +b = +c = +fn = +execute = + +Fable.AST.Fable.File +[Assignment ("hello", Const ConstNull); Assignment ("a", Const ConstNull); + Assignment ("b", Const ConstNull); Assignment ("c", Const ConstNull); + Assignment ("fn", Const ConstNull); Assignment ("execute", Const ConstNull)] \ No newline at end of file diff --git a/tests/Lua/Fable.Tests.Lua.fsproj b/tests/Lua/Fable.Tests.Lua.fsproj new file mode 100644 index 0000000000..3429c37060 --- /dev/null +++ b/tests/Lua/Fable.Tests.Lua.fsproj @@ -0,0 +1,27 @@ + + + net5.0 + false + false + true + preview + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/tests/Lua/Main.fs b/tests/Lua/Main.fs new file mode 100644 index 0000000000..ae5f6f6959 --- /dev/null +++ b/tests/Lua/Main.fs @@ -0,0 +1,6 @@ +#if FABLE_COMPILER +module Program +() +#else +module Program = let [] main _ = 0 +#endif \ No newline at end of file diff --git a/tests/Lua/TestArithmetic.fs b/tests/Lua/TestArithmetic.fs new file mode 100644 index 0000000000..8928c9c251 --- /dev/null +++ b/tests/Lua/TestArithmetic.fs @@ -0,0 +1,101 @@ +module Fable.Tests.Arithmetic + +open System +open Util.Testing + +[] +let testTwoPlusTwo = + 2 + 2 |> equal 4 +// let [] aLiteral = 5 +// let notALiteral = 5 +// let [] literalNegativeValue = -345 + +// let checkTo3dp (expected: float) actual = +// floor (actual * 1000.) |> equal expected + +// // let positiveInfinity = System.Double.PositiveInfinity +// // let negativeInfinity = System.Double.NegativeInfinity +// //let isNaN = fun x -> System.Double.IsNaN(x) + +// let equals (x:'a) (y:'a) = x = y +// let compareTo (x:'a) (y:'a) = compare x y + +// let decimalOne = 1M +// let decimalTwo = 2M + +// [] +// let ``test Infix add can be generated`` () = +// 4 + 2 |> equal 6 + +// [] +// let ``test Int32 literal addition is optimized`` () = +// aLiteral + 7 |> equal 12 +// notALiteral + 7 |> equal 12 + +// // FIXME +// // [] +// // let ``test Unary negation with negative literal values works`` () = +// // -literalNegativeValue |> equal 345 + +// [] +// let ``test Unary negation with integer MinValue works`` () = +// -(-128y) |> equal System.SByte.MinValue +// -(-32768s) |> equal System.Int16.MinValue +// -(-2147483648) |> equal System.Int32.MinValue +// // FIXME -(-9223372036854775808L) |> equal System.Int64.MinValue + +// [] +// let ``test Infix subtract can be generated`` () = +// 4 - 2 |> equal 2 + +// [] +// let ``test Infix multiply can be generated`` () = +// 4 * 2 |> equal 8 + +// [] +// let ``test Infix divide can be generated`` () = +// 4 / 2 |> equal 2 + +// [] +// let ``test Integer division doesn't produce floats`` () = +// 5. / 2. |> equal 2.5 +// 5 / 2 |> equal 2 +// 5 / 3 |> equal 1 + +// [] +// let ``test Infix modulo can be generated`` () = +// 4 % 3 |> equal 1 + +// [] +// let ``test Evaluation order is preserved by generated code`` () = +// (4 - 2) * 2 + 1 |> equal 5 + +// [] +// let ``test Decimal.ToString works`` () = +// string 001.23456M |> equal "1.23456" +// string 1.23456M |> equal "1.23456" +// string 0.12345M |> equal "0.12345" +// string 0.01234M |> equal "0.01234" +// string 0.00123M |> equal "0.00123" +// string 0.00012M |> equal "0.00012" +// string 0.00001M |> equal "0.00001" +// // FIXME: +// // string 0.00000M |> equal "0.00000" +// // string 0.12300M |> equal "0.12300" +// // string 0.0M |> equal "0.0" +// string 0M |> equal "0" +// string 1M |> equal "1" +// string -1M |> equal "-1" +// string 00000000000000000000000000000.M |> equal "0" +// // string 0.0000000000000000000000000000M |> equal "0.0000000000000000000000000000" +// string 79228162514264337593543950335M |> equal "79228162514264337593543950335" +// string -79228162514264337593543950335M |> equal "-79228162514264337593543950335" + +// [] +// let ``test Decimal precision is kept`` () = +// let items = [ 290.8M +// 290.8M +// 337.12M +// 6.08M +// -924.8M ] +// List.sum items |> equal 0M diff --git a/tests/Lua/Util.fs b/tests/Lua/Util.fs new file mode 100644 index 0000000000..93858d0c20 --- /dev/null +++ b/tests/Lua/Util.fs @@ -0,0 +1,32 @@ +module Fable.Tests.Util + +open System + +module Testing = +#if FABLE_COMPILER + open Fable.Core + open Fable.Core.PyInterop + + type Assert = + [] + static member AreEqual(actual: 'T, expected: 'T, ?msg: string): unit = nativeOnly + [] + static member NotEqual(actual: 'T, expected: 'T, ?msg: string): unit = nativeOnly + + let equal expected actual: unit = Assert.AreEqual(actual, expected) + let notEqual expected actual: unit = Assert.NotEqual(actual, expected) + + type Fact() = inherit System.Attribute() +#else + open Xunit + type FactAttribute = Xunit.FactAttribute + + let equal<'T> (expected: 'T) (actual: 'T): unit = Assert.Equal(expected, actual) + let notEqual<'T> (expected: 'T) (actual: 'T) : unit = Assert.NotEqual(expected, actual) +#endif + + // let rec sumFirstSeq (zs: seq) (n: int): float = + // match n with + // | 0 -> 0. + // | 1 -> Seq.head zs + // | _ -> (Seq.head zs) + sumFirstSeq (Seq.skip 1 zs) (n-1) diff --git a/tests/Lua/runtests.lua b/tests/Lua/runtests.lua new file mode 100644 index 0000000000..375cb5bdd1 --- /dev/null +++ b/tests/Lua/runtests.lua @@ -0,0 +1,8 @@ +luaunit = require('luaunit') +require('TestArithmetic') +TestMod = {} +function TestMod.testHello() + assertEquals(1, 1) +end + +luaunit.run() From 5b336c9c33f3e5139baf473ceba17354881feeed Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Tue, 24 Aug 2021 21:51:34 +0100 Subject: [PATCH 04/41] basic stuff working --- src/Fable.Transforms/Lua/Fable2Lua.fs | 36 ++++++++++++++---- src/Fable.Transforms/Lua/Lua.fs | 4 +- src/Fable.Transforms/Lua/LuaPrinter.fs | 29 ++++++++++++-- tests/Lua/TestArithmetic.fs | 52 +++++++++++++++++++++++++- tests/Lua/runtests.lua | 4 +- 5 files changed, 111 insertions(+), 14 deletions(-) diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index bbf4e60fb6..88fc691dfc 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -51,28 +51,34 @@ module Transforms = | BinaryDivide -> Divide | BinaryEqual -> Equals | BinaryPlus -> Plus + | BinaryMinus -> Minus | BinaryEqualStrict -> Equals | x -> sprintf "%A" x |> BinaryTodo Binary(op, transformExpr left, transformExpr right ) + | Fable.OperationKind.Unary (op, expr) -> + match op with + | UnaryNotBitwise -> transformExpr expr //not sure why this is being added + | _ -> sprintf "%A %A" op expr |> Unknown | x -> Unknown(sprintf "%A" x) let asSingleExprIife = function | [] -> NoOp | [h] -> - transformExpr h + h | exprs -> let statements = Helpers.transformStatements - (transformExpr >> Do) - (transformExpr >> Return) + (Do) + (Return) exprs FunctionCall(AnonymousFunc([], statements), []) + let asSingleExprIifeTr = List.map transformExpr >> asSingleExprIife let transformExpr = function | Fable.Expr.Value(value, _) -> transformValueKind value | Fable.Expr.Call(expr, callInfo, t, r) -> //Unknown(sprintf "call %A %A" expr callInfo) FunctionCall(transformExpr expr, List.map transformExpr callInfo.Args) | Fable.Expr.Import (info, t, r) -> - let path = info.Path.Replace(".fs", ".lua") //todo - make less brittle + let path = info.Path.Replace(".fs", "") //todo - make less brittle let rcall = FunctionCall(Ident { Namespace=None; Name= "require" }, [Const (ConstString path)]) match info.Selector with | "" -> rcall @@ -84,15 +90,25 @@ module Transforms = | Fable.Expr.Get(expr, Fable.GetKind.FieldGet(fieldName, isMut), _, _) -> Get(transformExpr expr, FieldGet(fieldName)) | Fable.Expr.Sequential exprs -> - asSingleExprIife exprs + asSingleExprIifeTr exprs | Fable.Expr.Let (ident, value, body) -> Let(ident.Name, transformExpr body) | Fable.Expr.Emit(m, _, _) -> + // let argsExprs = m.CallInfo.Args |> List.map transformExpr + // let macroExpr = Macro(m.Macro, argsExprs) + // let exprs = + // argsExprs + // @ [macroExpr] + // asSingleExprIife exprs Macro(m.Macro, m.CallInfo.Args |> List.map transformExpr) | Fable.Expr.DecisionTree(expr, lst) -> transformExpr expr | Fable.Expr.DecisionTreeSuccess(i, exprs, _) -> - asSingleExprIife exprs + asSingleExprIifeTr exprs + | Fable.Expr.Lambda(arg, body, name) -> + Function([arg.Name], [transformExpr body |> Return]) + | Fable.Expr.CurriedApply(applied, args, _, _) -> + FunctionCall(transformExpr applied, args |> List.map transformExpr) | Fable.Expr.IfThenElse (guardExpr, thenExpr, elseExpr, _) -> IfThenElse(transformExpr guardExpr, transformExpr thenExpr, transformExpr elseExpr) | x -> Unknown (sprintf "%A" x) @@ -104,7 +120,13 @@ module Transforms = if m.Args.Length = 0 then Assignment(m.Name, transformExpr m.Body) else - FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), [transformExpr m.Body |> Return]) + // let body = + // match m.Body with + // | Fable.Expr.Emit(mChild, _, _) -> + // [Macro(mChild.Macro, m.Args |> List.map (fun a -> Ident { Name = a.Name; Namespace = None })) |> Return] + // | _ -> [transformExpr m.Body |> Return] + // FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), body, m.Info.IsPublic) + FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), [transformExpr m.Body |> Return], m.Info.IsPublic) | x -> sprintf "%A" x |> Unknown |> Do let transformFile com (file: Fable.File): File = diff --git a/src/Fable.Transforms/Lua/Lua.fs b/src/Fable.Transforms/Lua/Lua.fs index 02864f2bc7..c5c3bf7853 100644 --- a/src/Fable.Transforms/Lua/Lua.fs +++ b/src/Fable.Transforms/Lua/Lua.fs @@ -15,6 +15,7 @@ type LuaIdentity = type UnaryOp = | Not + | NotBitwise type BinaryOp = | Equals | Multiply @@ -39,10 +40,11 @@ type Expr = | Macro of string * args: Expr list | IfThenElse of guardExpr: Expr * thenExpr: Expr * elseExpr: Expr | NoOp + | Function of args: string list * body: Statement list type Statement = | Assignment of name: string * Expr - | FunctionDeclaration of name: string * args: string list * body: Statement list + | FunctionDeclaration of name: string * args: string list * body: Statement list * exportToMod: bool | Return of Expr | Do of Expr diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index f05ccdf006..68b97afbc6 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -53,6 +53,9 @@ module Output = | Plus -> write ctx "+" | Minus -> write ctx "-" | BinaryTodo x -> writeCommented ctx "binary todo" x + let sprintExprSimple = function + | Ident i -> i.Name + | _ -> "" let rec writeExpr ctx = function | Ident i -> write ctx i.Name @@ -102,7 +105,19 @@ module Output = writeExpr ctxI elseExpr writei ctx "end" | Macro (s, args) -> - writei ctx s + let subbedMacro = + (s, args |> List.mapi(fun i x -> i.ToString(), sprintExprSimple x)) + ||> List.fold (fun acc (i, arg) -> acc.Replace("$"+i, arg) ) + writei ctx subbedMacro + | Function(args, body) -> + write ctx "function " + write ctx "(" + args |> Helper.separateWithCommas |> write ctx + write ctx ")" + let ctxI = indent ctx + writeln ctxI "" + body |> List.iter (writeStatement ctxI) + writeln ctx "end" | Unknown x -> writeCommented ctx "unknown" x | x -> sprintf "%A" x |> writeCommented ctx "todo" @@ -120,7 +135,7 @@ module Output = write ctx " = " writeExpr ctx expr writeln ctx "" - | FunctionDeclaration(name, args, body) -> + | FunctionDeclaration(name, args, body, exportToMod) -> writei ctx "function " write ctx name write ctx "(" @@ -130,6 +145,12 @@ module Output = writeln ctxI "" body |> List.iter (writeStatement ctxI) writeln ctx "end" + if exportToMod then + writei ctx "mod." + write ctx name + write ctx " = " + write ctx name + writeln ctxI "" | Return expr -> writei ctx "return " writeExpr ctx expr @@ -140,10 +161,10 @@ module Output = writeln ctx "" let writeFile ctx (file: File) = - write ctx "test" + writeln ctx "mod = {}" for s in file.Statements do writeStatement ctx s - + write ctx "return mod" //debugging //writeln ctx "--[[" //sprintf "%s" file.ASTDebug |> write ctx diff --git a/tests/Lua/TestArithmetic.fs b/tests/Lua/TestArithmetic.fs index 8928c9c251..2189e3e0f8 100644 --- a/tests/Lua/TestArithmetic.fs +++ b/tests/Lua/TestArithmetic.fs @@ -4,8 +4,58 @@ open System open Util.Testing [] -let testTwoPlusTwo = +let testTwoPlusTwo () = 2 + 2 |> equal 4 + +[] +let testMinus () = + 7 - 2 |> equal 5 + +[] +let testMultiply () = + 3 * 2 |> equal 6 + +[] +let testDivide () = + 10 / 2 |> equal 5 + +[] +let testFloatAdd () = + 3.141 + 2.85 |> equal 5.991 + +let private addFn a b = a + b + +[] +let testAddThroughTrivialFn () = + addFn 2 2 |> equal 4 + +[] +let testLocalsWithFcalls () = + let a = addFn 1 0 + let b = 2 + let c = addFn 3 0 + a + b + c |> equal 6 + +[] +let testAddStrings () = + let a () = "hello" + let b () = "world" + a() + " " + b() |> equal "hello world" + +[] +let testLocalFunction () = + let locAdd1 a = + addFn 1 a + locAdd1 2 |> equal 3 + +[] +let testInlineLambda () = + 1 |> fun x -> x + 1 |> fun x -> x - 3 |> equal (-1) + +let add42 = addFn 42 +[] +let testPartialApply () = + add42 3 |> equal 45 // let [] aLiteral = 5 // let notALiteral = 5 // let [] literalNegativeValue = -345 diff --git a/tests/Lua/runtests.lua b/tests/Lua/runtests.lua index 375cb5bdd1..d0d86ffc0a 100644 --- a/tests/Lua/runtests.lua +++ b/tests/Lua/runtests.lua @@ -1,8 +1,10 @@ luaunit = require('luaunit') -require('TestArithmetic') + TestMod = {} function TestMod.testHello() assertEquals(1, 1) end +TestArithmetic = require('TestArithmetic') + luaunit.run() From 915bb97f538f26ae1fea34728a9191c68b10e844 Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Wed, 25 Aug 2021 20:04:15 +0100 Subject: [PATCH 05/41] Records --- src/Fable.Transforms/Fable.Transforms.fsproj | 1 + src/Fable.Transforms/Lua/Compiler.fs | 12 ++++ src/Fable.Transforms/Lua/Fable2Lua.fs | 74 ++++++++++++++++---- src/Fable.Transforms/Lua/Lua.fs | 4 +- src/Fable.Transforms/Lua/LuaPrinter.fs | 31 ++++++-- tests/Lua/Fable.Tests.Lua.fsproj | 1 + tests/Lua/TestRecords.fs | 34 +++++++++ tests/Lua/runtests.lua | 1 + 8 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 src/Fable.Transforms/Lua/Compiler.fs create mode 100644 tests/Lua/TestRecords.fs diff --git a/src/Fable.Transforms/Fable.Transforms.fsproj b/src/Fable.Transforms/Fable.Transforms.fsproj index 4f16a22001..8f606bb3ac 100644 --- a/src/Fable.Transforms/Fable.Transforms.fsproj +++ b/src/Fable.Transforms/Fable.Transforms.fsproj @@ -28,6 +28,7 @@ + diff --git a/src/Fable.Transforms/Lua/Compiler.fs b/src/Fable.Transforms/Lua/Compiler.fs new file mode 100644 index 0000000000..33985d74f8 --- /dev/null +++ b/src/Fable.Transforms/Lua/Compiler.fs @@ -0,0 +1,12 @@ +module rec Fable.Compilers.Lua + +open Fable.AST +open Fable.AST.Fable + +type LuaCompiler(com: Fable.Compiler) = + let mutable types = Map.empty + member this.Com = com + member this.AddClassDecl (c: ClassDecl) = + types <- types |> Map.add c.Entity c + member this.GetByRef (e: EntityRef) = + types |> Map.tryFind e diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index 88fc691dfc..266ca615b4 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -9,6 +9,7 @@ open System.Text.RegularExpressions open Fable open Fable.AST open Fable.AST.Lua +open Fable.Compilers.Lua open Fable.Naming open Fable.Core @@ -28,7 +29,7 @@ module Transforms = yield transformReturn h | [] -> () ] - let transformValueKind = function + let transformValueKind (com: LuaCompiler) = function | Fable.NumberConstant(v,_,_) -> Const(ConstNumber v) | Fable.StringConstant(s) -> @@ -41,10 +42,28 @@ module Transforms = Const(ConstString (string c)) // | Fable.EnumConstant(e,ref) -> // convertExpr com e + | Fable.NewRecord(values, ref, args) -> + let entity = com.Com.GetEntity(ref) + if entity.IsFSharpRecord then + let names = entity.FSharpFields |> List.map(fun f -> f.Name) + let values = values |> List.map (transformExpr com) + let pairs = List.zip names values + NewObj(pairs) + else sprintf "unknown ety %A %A %A %A" values ref args entity |> Unknown + // match com.GetByRef ref with + // | Some c -> + // let names = c.AttachedMembers |> List.filter(fun q -> q.Info.IsPublic && q.Info.IsInstance) |> List.map (fun m -> m.Name) + // let values = values |> List.map (transformExpr com) + // let pairs = List.zip names values + // NewObj(pairs) + // com.Com.GetEntity(ref) + // | x -> sprintf "unknown %A" x |> ConstString |> Const | Fable.Null _ -> Const(ConstNull) - | _ -> Const ConstNull - let transformOp = function + | x -> sprintf "unknown %A" x |> ConstString |> Const + let transformOp com = + let transformExpr = transformExpr com + function | Fable.OperationKind.Binary (op, left, right) -> let op = match op with | BinaryMultiply -> Multiply @@ -60,7 +79,8 @@ module Transforms = | UnaryNotBitwise -> transformExpr expr //not sure why this is being added | _ -> sprintf "%A %A" op expr |> Unknown | x -> Unknown(sprintf "%A" x) - let asSingleExprIife = function + let asSingleExprIife (exprs: Expr list): Expr= //function + match exprs with | [] -> NoOp | [h] -> h @@ -71,9 +91,24 @@ module Transforms = (Return) exprs FunctionCall(AnonymousFunc([], statements), []) - let asSingleExprIifeTr = List.map transformExpr >> asSingleExprIife - let transformExpr = function - | Fable.Expr.Value(value, _) -> transformValueKind value + let flattenReturnIifes e = + let rec collectStatementsRec = + function + | Return (FunctionCall(AnonymousFunc([], statements), [])) -> //self executing functions only + statements |> List.collect collectStatementsRec + | x -> [x] + let statements = collectStatementsRec e + match statements with + | [Return s] -> s |> Return + | [] -> NoOp |> Do + | _ -> FunctionCall(AnonymousFunc([], statements), []) |> Return + + let asSingleExprIifeTr com : Fable.Expr list -> Expr = List.map (transformExpr com) >> asSingleExprIife + let transformExpr (com: LuaCompiler) expr= + let transformExpr = transformExpr com + let transformOp = transformOp com + match expr with + | Fable.Expr.Value(value, _) -> transformValueKind com value | Fable.Expr.Call(expr, callInfo, t, r) -> //Unknown(sprintf "call %A %A" expr callInfo) FunctionCall(transformExpr expr, List.map transformExpr callInfo.Args) @@ -90,9 +125,13 @@ module Transforms = | Fable.Expr.Get(expr, Fable.GetKind.FieldGet(fieldName, isMut), _, _) -> Get(transformExpr expr, FieldGet(fieldName)) | Fable.Expr.Sequential exprs -> - asSingleExprIifeTr exprs + asSingleExprIifeTr com exprs | Fable.Expr.Let (ident, value, body) -> - Let(ident.Name, transformExpr body) + //Let(ident.Name, transformExpr value, transformExpr body) + asSingleExprIife [ + Let(ident.Name, transformExpr value, NoOp) + transformExpr body + ] | Fable.Expr.Emit(m, _, _) -> // let argsExprs = m.CallInfo.Args |> List.map transformExpr // let macroExpr = Macro(m.Macro, argsExprs) @@ -104,7 +143,7 @@ module Transforms = | Fable.Expr.DecisionTree(expr, lst) -> transformExpr expr | Fable.Expr.DecisionTreeSuccess(i, exprs, _) -> - asSingleExprIifeTr exprs + asSingleExprIifeTr com exprs | Fable.Expr.Lambda(arg, body, name) -> Function([arg.Name], [transformExpr body |> Return]) | Fable.Expr.CurriedApply(applied, args, _, _) -> @@ -113,12 +152,12 @@ module Transforms = IfThenElse(transformExpr guardExpr, transformExpr thenExpr, transformExpr elseExpr) | x -> Unknown (sprintf "%A" x) - let transformDeclarations = function + let transformDeclarations (com: LuaCompiler) = function | Fable.ModuleDeclaration m -> Assignment("moduleDecTest", Expr.Const (ConstString "moduledectest")) | Fable.MemberDeclaration m -> if m.Args.Length = 0 then - Assignment(m.Name, transformExpr m.Body) + Assignment(m.Name, transformExpr com m.Body) else // let body = // match m.Body with @@ -126,13 +165,18 @@ module Transforms = // [Macro(mChild.Macro, m.Args |> List.map (fun a -> Ident { Name = a.Name; Namespace = None })) |> Return] // | _ -> [transformExpr m.Body |> Return] // FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), body, m.Info.IsPublic) - FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), [transformExpr m.Body |> Return], m.Info.IsPublic) + FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), [transformExpr com m.Body |> Return |> flattenReturnIifes], m.Info.IsPublic) + | Fable.ClassDeclaration(d) -> + com.AddClassDecl d + //todo - build prototype members out + //SNoOp + sprintf "ClassDeclaration %A" d |> Unknown |> Do | x -> sprintf "%A" x |> Unknown |> Do let transformFile com (file: Fable.File): File = - //let comp = LuaCompiler(com) :> ILuaCompiler + let comp = LuaCompiler(com) { Filename = "abc" - Statements = file.Declarations |> List.map Transforms.transformDeclarations + Statements = file.Declarations |> List.map (Transforms.transformDeclarations comp) ASTDebug = sprintf "%A" file.Declarations } \ No newline at end of file diff --git a/src/Fable.Transforms/Lua/Lua.fs b/src/Fable.Transforms/Lua/Lua.fs index c5c3bf7853..7de6602a46 100644 --- a/src/Fable.Transforms/Lua/Lua.fs +++ b/src/Fable.Transforms/Lua/Lua.fs @@ -36,17 +36,19 @@ type Expr = | FunctionCall of f: Expr * args: Expr list | AnonymousFunc of args: string list * body: Statement list | Unknown of string - | Let of name: string * Expr + | Let of name: string * value: Expr * body: Expr | Macro of string * args: Expr list | IfThenElse of guardExpr: Expr * thenExpr: Expr * elseExpr: Expr | NoOp | Function of args: string list * body: Statement list + | NewObj of values: (string * Expr) list type Statement = | Assignment of name: string * Expr | FunctionDeclaration of name: string * args: string list * body: Statement list * exportToMod: bool | Return of Expr | Do of Expr + | SNoOp type File = { Filename: string diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index 68b97afbc6..f7976eb400 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -90,9 +90,10 @@ module Output = writeExpr ctx expr write ctx "." write ctx fieldName - | Let(name, expr) -> - writei ctx name + | Let(name, value, expr) -> + write ctx name write ctx " = " + writeExpr ctx value writeExpr ctx expr writeln ctx "" | IfThenElse(guardExpr, thenExpr, elseExpr) -> @@ -118,6 +119,21 @@ module Output = writeln ctxI "" body |> List.iter (writeStatement ctxI) writeln ctx "end" + | NewObj(args) -> + write ctx "{" + let ctxI = indent ctx + writeln ctxI "" + for idx, (name, expr) in args |> List.mapi (fun i x -> i, x) do + writei ctxI name + write ctxI " = " + writeExpr ctxI expr + if idx < args.Length - 1 then + writeln ctxI "," + //writeExprs ctxI args + writeln ctx "" + writei ctx "" + writeln ctx "}" + | NoOp -> () | Unknown x -> writeCommented ctx "unknown" x | x -> sprintf "%A" x |> writeCommented ctx "todo" @@ -159,6 +175,7 @@ module Output = writei ctx "" writeExpr ctx expr writeln ctx "" + | SNoOp -> () let writeFile ctx (file: File) = writeln ctx "mod = {}" @@ -166,8 +183,8 @@ module Output = writeStatement ctx s write ctx "return mod" //debugging - //writeln ctx "--[[" - //sprintf "%s" file.ASTDebug |> write ctx - //writeln ctx "rabbit" - //sprintf "%A" file.Statements |> write ctx - //writeln ctx " --]]" \ No newline at end of file + writeln ctx "" + // writeln ctx "--[[" + // sprintf "%s" file.ASTDebug |> write ctx + // sprintf "%A" file.Statements |> write ctx + // writeln ctx " --]]" \ No newline at end of file diff --git a/tests/Lua/Fable.Tests.Lua.fsproj b/tests/Lua/Fable.Tests.Lua.fsproj index 3429c37060..a32620812f 100644 --- a/tests/Lua/Fable.Tests.Lua.fsproj +++ b/tests/Lua/Fable.Tests.Lua.fsproj @@ -21,6 +21,7 @@ + diff --git a/tests/Lua/TestRecords.fs b/tests/Lua/TestRecords.fs new file mode 100644 index 0000000000..b0427aa7d9 --- /dev/null +++ b/tests/Lua/TestRecords.fs @@ -0,0 +1,34 @@ +module Fable.Tests.Record + +open Util.Testing + +type Simple = { + one: string + two: int +} + +type Parent = { + a: Simple + b: Simple +} + +// type Recursive = { +// x: Recursive +// } + +[] +let testMakeRecord () = + let r = { one="string_one"; two=2} + r.one |> equal "string_one" + r.two |> equal 2 + +[] +let testMakeNestedRecord () = + let r = { + a = { one = "a"; two = 2} + b = { one = "b"; two = 4} + } + r.a.one |> equal "a" + r.b.one |> equal "b" + r.a.two |> equal 2 + r.b.two |> equal 4 \ No newline at end of file diff --git a/tests/Lua/runtests.lua b/tests/Lua/runtests.lua index d0d86ffc0a..7d600e5b27 100644 --- a/tests/Lua/runtests.lua +++ b/tests/Lua/runtests.lua @@ -6,5 +6,6 @@ function TestMod.testHello() end TestArithmetic = require('TestArithmetic') +TestRecords = require('TestRecords') luaunit.run() From 3874c074b3d49097aa4fe320cb9fe37381997249 Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Wed, 25 Aug 2021 20:15:28 +0100 Subject: [PATCH 06/41] Anon records --- src/Fable.Transforms/Lua/Fable2Lua.fs | 30 +++++++++++++-------------- tests/Lua/TestRecords.fs | 8 ++++++- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index 266ca615b4..a05fcdc924 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -50,14 +50,10 @@ module Transforms = let pairs = List.zip names values NewObj(pairs) else sprintf "unknown ety %A %A %A %A" values ref args entity |> Unknown - // match com.GetByRef ref with - // | Some c -> - // let names = c.AttachedMembers |> List.filter(fun q -> q.Info.IsPublic && q.Info.IsInstance) |> List.map (fun m -> m.Name) - // let values = values |> List.map (transformExpr com) - // let pairs = List.zip names values - // NewObj(pairs) - // com.Com.GetEntity(ref) - // | x -> sprintf "unknown %A" x |> ConstString |> Const + | Fable.NewAnonymousRecord(values, names, _) -> + let transformedValues = values |> List.map (transformExpr com) + let pairs = List.zip (names |> Array.toList) transformedValues + NewObj(pairs) | Fable.Null _ -> Const(ConstNull) | x -> sprintf "unknown %A" x |> ConstString |> Const @@ -94,12 +90,14 @@ module Transforms = let flattenReturnIifes e = let rec collectStatementsRec = function + | Return (FunctionCall(AnonymousFunc([], [Return s]), [])) -> + [Return s] | Return (FunctionCall(AnonymousFunc([], statements), [])) -> //self executing functions only statements |> List.collect collectStatementsRec | x -> [x] let statements = collectStatementsRec e match statements with - | [Return s] -> s |> Return + | [Return s] -> Return s | [] -> NoOp |> Do | _ -> FunctionCall(AnonymousFunc([], statements), []) |> Return @@ -159,13 +157,13 @@ module Transforms = if m.Args.Length = 0 then Assignment(m.Name, transformExpr com m.Body) else - // let body = - // match m.Body with - // | Fable.Expr.Emit(mChild, _, _) -> - // [Macro(mChild.Macro, m.Args |> List.map (fun a -> Ident { Name = a.Name; Namespace = None })) |> Return] - // | _ -> [transformExpr m.Body |> Return] - // FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), body, m.Info.IsPublic) - FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), [transformExpr com m.Body |> Return |> flattenReturnIifes], m.Info.IsPublic) + + let unwrapSelfExStatements = + match transformExpr com m.Body |> Return |> flattenReturnIifes with + | Return (FunctionCall(AnonymousFunc([], statements), [])) -> + statements + | s -> [s] + FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), unwrapSelfExStatements, m.Info.IsPublic) | Fable.ClassDeclaration(d) -> com.AddClassDecl d //todo - build prototype members out diff --git a/tests/Lua/TestRecords.fs b/tests/Lua/TestRecords.fs index b0427aa7d9..2386bf01a3 100644 --- a/tests/Lua/TestRecords.fs +++ b/tests/Lua/TestRecords.fs @@ -31,4 +31,10 @@ let testMakeNestedRecord () = r.a.one |> equal "a" r.b.one |> equal "b" r.a.two |> equal 2 - r.b.two |> equal 4 \ No newline at end of file + r.b.two |> equal 4 + +[] +let testMakeAnonRecord () = + let r = {| x = 3.142; y = true |} + r.x |> equal 3.142 + r.y |> equal true From 4b3b0bad66f4aebb27f55e19a4eb31a4530d99e9 Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Wed, 25 Aug 2021 20:55:29 +0100 Subject: [PATCH 07/41] ternary --- src/Fable.Transforms/Lua/Fable2Lua.fs | 2 +- src/Fable.Transforms/Lua/Lua.fs | 2 +- src/Fable.Transforms/Lua/LuaPrinter.fs | 22 ++++++++++++---------- tests/Lua/Fable.Tests.Lua.fsproj | 1 + tests/Lua/TestControlFlow.fs | 25 +++++++++++++++++++++++++ tests/Lua/runtests.lua | 1 + 6 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 tests/Lua/TestControlFlow.fs diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index a05fcdc924..ff3d7a9b66 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -147,7 +147,7 @@ module Transforms = | Fable.Expr.CurriedApply(applied, args, _, _) -> FunctionCall(transformExpr applied, args |> List.map transformExpr) | Fable.Expr.IfThenElse (guardExpr, thenExpr, elseExpr, _) -> - IfThenElse(transformExpr guardExpr, transformExpr thenExpr, transformExpr elseExpr) + Ternary(transformExpr guardExpr, transformExpr thenExpr, transformExpr elseExpr) | x -> Unknown (sprintf "%A" x) let transformDeclarations (com: LuaCompiler) = function diff --git a/src/Fable.Transforms/Lua/Lua.fs b/src/Fable.Transforms/Lua/Lua.fs index 7de6602a46..ee98385ff0 100644 --- a/src/Fable.Transforms/Lua/Lua.fs +++ b/src/Fable.Transforms/Lua/Lua.fs @@ -38,7 +38,7 @@ type Expr = | Unknown of string | Let of name: string * value: Expr * body: Expr | Macro of string * args: Expr list - | IfThenElse of guardExpr: Expr * thenExpr: Expr * elseExpr: Expr + | Ternary of guardExpr: Expr * thenExpr: Expr * elseExpr: Expr | NoOp | Function of args: string list * body: Statement list | NewObj of values: (string * Expr) list diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index f7976eb400..59151048e9 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -95,16 +95,19 @@ module Output = write ctx " = " writeExpr ctx value writeExpr ctx expr + | Ternary(guardExpr, thenExpr, elseExpr) -> writeln ctx "" - | IfThenElse(guardExpr, thenExpr, elseExpr) -> - writei ctx "if " - writeExpr ctx guardExpr - write ctx " then" - let ctxI = indent ctx + let ctxA = indent ctx + writei ctxA "" + writeExpr ctxA guardExpr + writeln ctxA "" + let ctxI = indent ctxA + writei ctxI " and " writeExpr ctxI thenExpr - writei ctx "else " + writeln ctxI "" + writei ctxI " or " writeExpr ctxI elseExpr - writei ctx "end" + write ctx "" | Macro (s, args) -> let subbedMacro = (s, args |> List.mapi(fun i x -> i.ToString(), sprintExprSimple x)) @@ -118,7 +121,7 @@ module Output = let ctxI = indent ctx writeln ctxI "" body |> List.iter (writeStatement ctxI) - writeln ctx "end" + writei ctx "end" | NewObj(args) -> write ctx "{" let ctxI = indent ctx @@ -131,8 +134,7 @@ module Output = writeln ctxI "," //writeExprs ctxI args writeln ctx "" - writei ctx "" - writeln ctx "}" + writei ctx "}" | NoOp -> () | Unknown x -> writeCommented ctx "unknown" x diff --git a/tests/Lua/Fable.Tests.Lua.fsproj b/tests/Lua/Fable.Tests.Lua.fsproj index a32620812f..ac514702ad 100644 --- a/tests/Lua/Fable.Tests.Lua.fsproj +++ b/tests/Lua/Fable.Tests.Lua.fsproj @@ -22,6 +22,7 @@ + diff --git a/tests/Lua/TestControlFlow.fs b/tests/Lua/TestControlFlow.fs new file mode 100644 index 0000000000..f0cdd03859 --- /dev/null +++ b/tests/Lua/TestControlFlow.fs @@ -0,0 +1,25 @@ +module Fable.Tests.TestControlFlow + +open System +open Util.Testing + +[] +let testIfElse () = + let r = + if true then 4 else 6 + r |> equal 4 +[] +let testIfElse2 () = + let r = + if false then 4 else 6 + r |> equal 6 + +let bfn x a b= + if x then a else b + +[] +let testIfElseFn1 () = + bfn true 1 2 |> equal 1 +[] +let testIfElseFn2 () = + bfn false 3 4 |> equal 4 \ No newline at end of file diff --git a/tests/Lua/runtests.lua b/tests/Lua/runtests.lua index 7d600e5b27..9d4a0ce43c 100644 --- a/tests/Lua/runtests.lua +++ b/tests/Lua/runtests.lua @@ -7,5 +7,6 @@ end TestArithmetic = require('TestArithmetic') TestRecords = require('TestRecords') +TestControlFlow = require('TestControlFlow') luaunit.run() From e79bab02999b22faae4107c9ff8e3332945a03de Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Wed, 25 Aug 2021 21:11:01 +0100 Subject: [PATCH 08/41] unions --- src/Fable.Transforms/Lua/Fable2Lua.fs | 3 +++ tests/Lua/Fable.Tests.Lua.fsproj | 1 + tests/Lua/TestUnionType.fs | 35 +++++++++++++++++++++++++++ tests/Lua/runtests.lua | 1 + 4 files changed, 40 insertions(+) create mode 100644 tests/Lua/TestUnionType.fs diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index ff3d7a9b66..70ff53745d 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -54,6 +54,9 @@ module Transforms = let transformedValues = values |> List.map (transformExpr com) let pairs = List.zip (names |> Array.toList) transformedValues NewObj(pairs) + | Fable.NewUnion(values, tag, _, _) -> + let values = values |> List.map(transformExpr com) |> List.mapi(fun i x -> sprintf "p%i" i, x) + NewObj(("tag", tag |> float |> ConstNumber |> Const)::values) | Fable.Null _ -> Const(ConstNull) | x -> sprintf "unknown %A" x |> ConstString |> Const diff --git a/tests/Lua/Fable.Tests.Lua.fsproj b/tests/Lua/Fable.Tests.Lua.fsproj index ac514702ad..8d66668e23 100644 --- a/tests/Lua/Fable.Tests.Lua.fsproj +++ b/tests/Lua/Fable.Tests.Lua.fsproj @@ -23,6 +23,7 @@ + diff --git a/tests/Lua/TestUnionType.fs b/tests/Lua/TestUnionType.fs new file mode 100644 index 0000000000..ba0d0acd19 --- /dev/null +++ b/tests/Lua/TestUnionType.fs @@ -0,0 +1,35 @@ +module Fable.Tests.UnionTypes + +open Util.Testing + +type Gender = Male | Female + +[] +let testMakeUnion () = + let r = Male + r |> equal Male + +[] +let testMakeUnion2 () = + let r = Female + r |> equal Female + +type Stuff = + | A of string * int + | B + | C of bool + +[] +let testMakeUnionContent () = + let r = A ("abc", 42) + r |> equal (A ("abc", 42)) + +[] +let testMakeUnionContent2 () = + let r = C true + r |> equal (C true) + +[] +let testMakeUnionContent3 () = + let r = B + r |> equal B \ No newline at end of file diff --git a/tests/Lua/runtests.lua b/tests/Lua/runtests.lua index 9d4a0ce43c..f7dae0b4a6 100644 --- a/tests/Lua/runtests.lua +++ b/tests/Lua/runtests.lua @@ -8,5 +8,6 @@ end TestArithmetic = require('TestArithmetic') TestRecords = require('TestRecords') TestControlFlow = require('TestControlFlow') +TestUnionType = require('TestUnionType') luaunit.run() From aea3275f4a1cddbe5654c7b07322af81a8efbe48 Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Thu, 26 Aug 2021 21:06:57 +0100 Subject: [PATCH 09/41] checkpoint --- build.fsx | 2 +- src/Fable.Transforms/Lua/Fable2Lua.fs | 2 +- src/fable-library-lua/fable/Util.lua | 5 +++++ tests/Lua/TestControlFlow.fs | 14 +++++++++++++- tests/Lua/TestRecords.fs | 9 +++++++++ 5 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 src/fable-library-lua/fable/Util.lua diff --git a/build.fsx b/build.fsx index 5f640994d9..d409864128 100644 --- a/build.fsx +++ b/build.fsx @@ -215,7 +215,7 @@ let buildLibraryLua() = "--exclude Fable.Core" "--define FABLE_LIBRARY" ] - // Copy *.py from projectDir to buildDir + // Copy *.lua from projectDir to buildDir copyDirRecursive libraryDir buildDirLua runInDir buildDirLua ("lua -v") diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index 70ff53745d..cc6cefa6e3 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -114,7 +114,7 @@ module Transforms = //Unknown(sprintf "call %A %A" expr callInfo) FunctionCall(transformExpr expr, List.map transformExpr callInfo.Args) | Fable.Expr.Import (info, t, r) -> - let path = info.Path.Replace(".fs", "") //todo - make less brittle + let path = info.Path.Replace(".fs", "").Replace(".js", "") //todo - make less brittle let rcall = FunctionCall(Ident { Namespace=None; Name= "require" }, [Const (ConstString path)]) match info.Selector with | "" -> rcall diff --git a/src/fable-library-lua/fable/Util.lua b/src/fable-library-lua/fable/Util.lua new file mode 100644 index 0000000000..21e149c7f8 --- /dev/null +++ b/src/fable-library-lua/fable/Util.lua @@ -0,0 +1,5 @@ +mod = {} +function mod.equals(a, b) + return false +end +return mod \ No newline at end of file diff --git a/tests/Lua/TestControlFlow.fs b/tests/Lua/TestControlFlow.fs index f0cdd03859..7555148970 100644 --- a/tests/Lua/TestControlFlow.fs +++ b/tests/Lua/TestControlFlow.fs @@ -22,4 +22,16 @@ let testIfElseFn1 () = bfn true 1 2 |> equal 1 [] let testIfElseFn2 () = - bfn false 3 4 |> equal 4 \ No newline at end of file + bfn false 3 4 |> equal 4 + +[] +let testIfElseIf () = + let a x = + if x = 1 then + 1 + else if x = 2 then + 2 + else 3 + a 1 |> equal 1 + a 2 |> equal 2 + a 3 |> equal 3 \ No newline at end of file diff --git a/tests/Lua/TestRecords.fs b/tests/Lua/TestRecords.fs index 2386bf01a3..a6e6960ba9 100644 --- a/tests/Lua/TestRecords.fs +++ b/tests/Lua/TestRecords.fs @@ -33,6 +33,15 @@ let testMakeNestedRecord () = r.a.two |> equal 2 r.b.two |> equal 4 +// [] +// let testStructuralCompareRecords () = +// let a = { one="string_one"; two=2} +// let b = { one="string_one"; two=2} +// let c = { one="string_two"; two=4} +// a = a |> equal true +// a = b |> equal true +// a = c |> equal false + [] let testMakeAnonRecord () = let r = {| x = 3.142; y = true |} From 45522da8e287f1db967d0a22db7fb96c79e6a27c Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Fri, 27 Aug 2021 08:42:59 +0100 Subject: [PATCH 10/41] Add lib path --- build.fsx | 1 + 1 file changed, 1 insertion(+) diff --git a/build.fsx b/build.fsx index d409864128..a06e0a3cbb 100644 --- a/build.fsx +++ b/build.fsx @@ -479,6 +479,7 @@ let testLua() = "--outDir " + buildDir "--exclude Fable.Core" "--lang Lua" + "--fableLib " + "fable-library-lua" "fable" //cannot use relative paths in lua. Copy to subfolder? ] copyFile (projectDir "runtests.lua") (buildDir "runtests.lua") From 6a794d40758919969c3ce042608a8a038f6f125d Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Fri, 27 Aug 2021 08:50:56 +0100 Subject: [PATCH 11/41] cleanup --- .../fable/Fable.Library.fsproj | 2 +- src/quicktest/QuickTest.fs | 125 +++++++----------- src/quicktest/QuickTest.lua | 11 -- src/quicktest/run.lua | 4 - 4 files changed, 52 insertions(+), 90 deletions(-) delete mode 100644 src/quicktest/QuickTest.lua delete mode 100644 src/quicktest/run.lua diff --git a/src/fable-library-py/fable/Fable.Library.fsproj b/src/fable-library-py/fable/Fable.Library.fsproj index 9a34b5f562..fa985a71f1 100644 --- a/src/fable-library-py/fable/Fable.Library.fsproj +++ b/src/fable-library-py/fable/Fable.Library.fsproj @@ -7,7 +7,7 @@ - + diff --git a/src/quicktest/QuickTest.fs b/src/quicktest/QuickTest.fs index 472506ab8d..36b1cf55be 100644 --- a/src/quicktest/QuickTest.fs +++ b/src/quicktest/QuickTest.fs @@ -1,79 +1,56 @@ -module QuickTest - -// Run `dotnet fsi build.fsx quicktest` and then add tests to this file, -// when you save they will be run automatically with latest changes in compiler. -// When everything works, move the tests to the appropriate file in tests/Main. -// Please don't add this file to your commits. - -open System -open System.Collections.Generic -open Fable.Core open Fable.Core.JsInterop open Fable.Core.Testing -// let log (o: obj) = -// printfn "%A" o - -let hello = printfn "hello world" -let a = 2 + 2 -let b = 3 - 1 -let c = a + b -let fn a b c = - let d = a + b + c - printf "%A %A" b d -let execute () = - a.ToString() |> printf "%s" - printf "c" - -// let equal expected actual = -// let areEqual = expected = actual -// printfn "%A = %A > %b" expected actual areEqual -// if not areEqual then -// failwithf "[ASSERT ERROR] Expected %A but got %A" expected actual - -// let throwsError (expected: string) (f: unit -> 'a): unit = -// let success = -// try -// f () |> ignore -// true -// with e -> -// if not <| String.IsNullOrEmpty(expected) then -// equal e.Message expected -// false -// // TODO better error messages -// equal false success - -// let testCase (msg: string) f: unit = -// try -// printfn "%s" msg -// f () -// with ex -> -// printfn "%s" ex.Message -// if ex.Message <> null && ex.Message.StartsWith("[ASSERT ERROR]") |> not then -// printfn "%s" ex.StackTrace -// printfn "" - -// let testCaseAsync msg f = -// testCase msg (fun () -> -// async { -// try -// do! f () -// with ex -> -// printfn "%s" ex.Message -// if ex.Message <> null && ex.Message.StartsWith("[ASSERT ERROR]") |> not then -// printfn "%s" ex.StackTrace -// } |> Async.StartImmediate) - -// let measureTime (f: unit -> unit) = emitJsStatement () """ -// //js -// const startTime = process.hrtime(); -// f(); -// const elapsed = process.hrtime(startTime); -// console.log("Ms:", elapsed[0] * 1e3 + elapsed[1] / 1e6); -// //!js -// """ +let log (o: obj) = + printfn "%A" o + +let equal expected actual = + let areEqual = expected = actual + printfn "%A = %A > %b" expected actual areEqual + if not areEqual then + failwithf "[ASSERT ERROR] Expected %A but got %A" expected actual + +let throwsError (expected: string) (f: unit -> 'a): unit = + let success = + try + f () |> ignore + true + with e -> + if not <| String.IsNullOrEmpty(expected) then + equal e.Message expected + false + // TODO better error messages + equal false success + +let testCase (msg: string) f: unit = + try + printfn "%s" msg + f () + with ex -> + printfn "%s" ex.Message + if ex.Message <> null && ex.Message.StartsWith("[ASSERT ERROR]") |> not then + printfn "%s" ex.StackTrace + printfn "" + +let testCaseAsync msg f = + testCase msg (fun () -> + async { + try + do! f () + with ex -> + printfn "%s" ex.Message + if ex.Message <> null && ex.Message.StartsWith("[ASSERT ERROR]") |> not then + printfn "%s" ex.StackTrace + } |> Async.StartImmediate) + +let measureTime (f: unit -> unit) = emitJsStatement () """ + //js + const startTime = process.hrtime(); + f(); + const elapsed = process.hrtime(startTime); + console.log("Ms:", elapsed[0] * 1e3 + elapsed[1] / 1e6); + //!js +""" // Write here your unit test, you can later move it -// to Fable.Tests project. For example: -// testCase "Addition works" <| fun () -> -// 2 + 2 |> equal 4 +// to Fable.Tests project. For example: \ No newline at end of file diff --git a/src/quicktest/QuickTest.lua b/src/quicktest/QuickTest.lua deleted file mode 100644 index 22b6a002ed..0000000000 --- a/src/quicktest/QuickTest.lua +++ /dev/null @@ -1,11 +0,0 @@ -testhello = -a = -b = -c = -fn = -execute = - -Fable.AST.Fable.File -[Assignment ("hello", Const ConstNull); Assignment ("a", Const ConstNull); - Assignment ("b", Const ConstNull); Assignment ("c", Const ConstNull); - Assignment ("fn", Const ConstNull); Assignment ("execute", Const ConstNull)] \ No newline at end of file diff --git a/src/quicktest/run.lua b/src/quicktest/run.lua deleted file mode 100644 index 3c7da75282..0000000000 --- a/src/quicktest/run.lua +++ /dev/null @@ -1,4 +0,0 @@ --- local m = require './QuickTest' -require('./QuickTest').hello() -require('./QuickTest').a() -require('./QuickTest').b() \ No newline at end of file From 686e62f75f4fbb70f4e95ea0974203c77f4a3402 Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Sat, 28 Aug 2021 12:23:06 +0100 Subject: [PATCH 12/41] Structural equality --- build.fsx | 6 ++---- src/Fable.Transforms/Lua/Fable2Lua.fs | 12 +++++++++++- src/fable-library-lua/fable/Util.lua | 24 +++++++++++++++++++++++- tests/Lua/TestRecords.fs | 16 ++++++++-------- 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/build.fsx b/build.fsx index a06e0a3cbb..e3d89e204c 100644 --- a/build.fsx +++ b/build.fsx @@ -474,12 +474,13 @@ let testLua() = let buildDir = "build/tests/Lua" cleanDirs [buildDir] + copyDirRecursive ("build" "fable-library-lua" "fable") (buildDir "fable-lib") runInDir projectDir "dotnet test" runFableWithArgs projectDir [ "--outDir " + buildDir "--exclude Fable.Core" "--lang Lua" - "--fableLib " + "fable-library-lua" "fable" //cannot use relative paths in lua. Copy to subfolder? + "--fableLib " + buildDir "fable-lib" //("fable-library-lua" "fable") //cannot use relative paths in lua. Copy to subfolder? ] copyFile (projectDir "runtests.lua") (buildDir "runtests.lua") @@ -651,9 +652,6 @@ match argsLower with | "quicktest-py"::_ -> buildPyLibraryIfNotExists() run "dotnet watch -p src/Fable.Cli run -- watch --cwd ../quicktest --lang Python --exclude Fable.Core --noCache" -| "quicktest-lua"::_ -> - buildPyLibraryIfNotExists() - run "dotnet watch -p src/Fable.Cli run -- watch --cwd ../quicktest --lang Lua --exclude Fable.Core --noCache" | "jupyter" :: _ -> buildPyLibraryIfNotExists () run "dotnet watch -p src/Fable.Cli run -- watch --cwd ../Fable.Jupyter/src --lang Python --exclude Fable.Core --noCache 2>> ../Fable.Jupyter/src/fable.out" diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index cc6cefa6e3..becfe11fc2 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -105,6 +105,11 @@ module Transforms = | _ -> FunctionCall(AnonymousFunc([], statements), []) |> Return let asSingleExprIifeTr com : Fable.Expr list -> Expr = List.map (transformExpr com) >> asSingleExprIife + let (|Regex|_|) pattern input = + let m = Regex.Match(input, pattern) + if m.Success then Some(List.tail [ for g in m.Groups -> g.Value ]) + else None + let transformExpr (com: LuaCompiler) expr= let transformExpr = transformExpr com let transformOp = transformOp com @@ -114,7 +119,12 @@ module Transforms = //Unknown(sprintf "call %A %A" expr callInfo) FunctionCall(transformExpr expr, List.map transformExpr callInfo.Args) | Fable.Expr.Import (info, t, r) -> - let path = info.Path.Replace(".fs", "").Replace(".js", "") //todo - make less brittle + let path = + match info.Kind, info.Path with + | LibraryImport, Regex "fable-lib\/(\w+).(?:fs|js)" [name] -> + "fable-lib/" + name + | _ -> + info.Path.Replace(".fs", "").Replace(".js", "") //todo - make less brittle let rcall = FunctionCall(Ident { Namespace=None; Name= "require" }, [Const (ConstString path)]) match info.Selector with | "" -> rcall diff --git a/src/fable-library-lua/fable/Util.lua b/src/fable-library-lua/fable/Util.lua index 21e149c7f8..8ed680575a 100644 --- a/src/fable-library-lua/fable/Util.lua +++ b/src/fable-library-lua/fable/Util.lua @@ -1,5 +1,27 @@ mod = {} + +-- https://web.archive.org/web/20131225070434/http://snippets.luacode.org/snippets/Deep_Comparison_of_Two_Values_3 +function deepcompare(t1,t2,ignore_mt) + local ty1 = type(t1) + local ty2 = type(t2) + if ty1 ~= ty2 then return false end + -- non-table types can be directly compared + if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end + -- as well as tables which have the metamethod __eq + local mt = getmetatable(t1) + if not ignore_mt and mt and mt.__eq then return t1 == t2 end + for k1,v1 in pairs(t1) do + local v2 = t2[k1] + if v2 == nil or not deepcompare(v1,v2) then return false end + end + for k2,v2 in pairs(t2) do + local v1 = t1[k2] + if v1 == nil or not deepcompare(v1,v2) then return false end + end + return true +end + function mod.equals(a, b) - return false + return deepcompare(a, b, true) end return mod \ No newline at end of file diff --git a/tests/Lua/TestRecords.fs b/tests/Lua/TestRecords.fs index a6e6960ba9..71138fefb8 100644 --- a/tests/Lua/TestRecords.fs +++ b/tests/Lua/TestRecords.fs @@ -33,14 +33,14 @@ let testMakeNestedRecord () = r.a.two |> equal 2 r.b.two |> equal 4 -// [] -// let testStructuralCompareRecords () = -// let a = { one="string_one"; two=2} -// let b = { one="string_one"; two=2} -// let c = { one="string_two"; two=4} -// a = a |> equal true -// a = b |> equal true -// a = c |> equal false +[] +let testStructuralCompareRecords () = + let a = { one="string_one"; two=2} + let b = { one="string_one"; two=2} + let c = { one="string_two"; two=4} + a = a |> equal true + a = b |> equal true + a = c |> equal false [] let testMakeAnonRecord () = From 20cf8799aae159cfc03b6288f7c7c3347da3ee59 Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Sat, 28 Aug 2021 14:24:55 +0100 Subject: [PATCH 13/41] Some/none and match unions --- src/Fable.Transforms/Lua/Compiler.fs | 4 +++ src/Fable.Transforms/Lua/Fable2Lua.fs | 40 +++++++++++++++++++++++--- src/Fable.Transforms/Lua/LuaPrinter.fs | 29 ++++++++++--------- tests/Lua/TestUnionType.fs | 11 ++++++- 4 files changed, 65 insertions(+), 19 deletions(-) diff --git a/src/Fable.Transforms/Lua/Compiler.fs b/src/Fable.Transforms/Lua/Compiler.fs index 33985d74f8..c7b44da7ea 100644 --- a/src/Fable.Transforms/Lua/Compiler.fs +++ b/src/Fable.Transforms/Lua/Compiler.fs @@ -5,8 +5,12 @@ open Fable.AST.Fable type LuaCompiler(com: Fable.Compiler) = let mutable types = Map.empty + let mutable decisionTreeTargets = [] member this.Com = com member this.AddClassDecl (c: ClassDecl) = types <- types |> Map.add c.Entity c member this.GetByRef (e: EntityRef) = types |> Map.tryFind e + member this.DecisionTreeTargets (exprs: (list * Expr) list) = + decisionTreeTargets <- exprs + member this.GetDecisionTreeTargets (idx: int) = decisionTreeTargets.[idx] \ No newline at end of file diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index becfe11fc2..d3928b67d2 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -29,6 +29,12 @@ module Transforms = yield transformReturn h | [] -> () ] + let ident name = Ident {Name = name; Namespace = None} + let iife statements = FunctionCall(AnonymousFunc([], statements), []) + let maybeIife = function + | [] -> NoOp + | [Return expr] -> expr + | statements -> iife statements let transformValueKind (com: LuaCompiler) = function | Fable.NumberConstant(v,_,_) -> Const(ConstNumber v) @@ -55,8 +61,13 @@ module Transforms = let pairs = List.zip (names |> Array.toList) transformedValues NewObj(pairs) | Fable.NewUnion(values, tag, _, _) -> - let values = values |> List.map(transformExpr com) |> List.mapi(fun i x -> sprintf "p%i" i, x) + let values = values |> List.map(transformExpr com) |> List.mapi(fun i x -> sprintf "p_%i" i, x) NewObj(("tag", tag |> float |> ConstNumber |> Const)::values) + | Fable.NewOption (value, t, _) -> + value |> Option.map (transformExpr com) |> Option.defaultValue (Const ConstNull) + | Fable.NewTuple(values, isStruct) -> + let fields = values |> List.mapi(fun i x -> sprintf "p_%i" i, transformExpr com x) + NewObj(fields) | Fable.Null _ -> Const(ConstNull) | x -> sprintf "unknown %A" x |> ConstString |> Const @@ -89,7 +100,7 @@ module Transforms = (Do) (Return) exprs - FunctionCall(AnonymousFunc([], statements), []) + statements |> Helpers.maybeIife let flattenReturnIifes e = let rec collectStatementsRec = function @@ -135,6 +146,8 @@ module Transforms = transformOp kind | Fable.Expr.Get(expr, Fable.GetKind.FieldGet(fieldName, isMut), _, _) -> Get(transformExpr expr, FieldGet(fieldName)) + | Fable.Expr.Get(expr, Fable.GetKind.UnionField(caseIdx, fieldIdx), _, _) -> + Get(transformExpr expr, FieldGet(sprintf "p_%i" fieldIdx)) | Fable.Expr.Sequential exprs -> asSingleExprIifeTr com exprs | Fable.Expr.Let (ident, value, body) -> @@ -152,15 +165,34 @@ module Transforms = // asSingleExprIife exprs Macro(m.Macro, m.CallInfo.Args |> List.map transformExpr) | Fable.Expr.DecisionTree(expr, lst) -> + com.DecisionTreeTargets(lst) transformExpr expr - | Fable.Expr.DecisionTreeSuccess(i, exprs, _) -> - asSingleExprIifeTr com exprs + | Fable.Expr.DecisionTreeSuccess(i, boundValues, _) -> + let idents,target = com.GetDecisionTreeTargets(i) + let statements = + [ for (ident, value) in List.zip idents boundValues do + yield Assignment(ident.Name, transformExpr value) + yield transformExpr target |> Return + ] + statements + |> Helpers.maybeIife | Fable.Expr.Lambda(arg, body, name) -> Function([arg.Name], [transformExpr body |> Return]) | Fable.Expr.CurriedApply(applied, args, _, _) -> FunctionCall(transformExpr applied, args |> List.map transformExpr) | Fable.Expr.IfThenElse (guardExpr, thenExpr, elseExpr, _) -> Ternary(transformExpr guardExpr, transformExpr thenExpr, transformExpr elseExpr) + | Fable.Test(expr, kind, b) -> + match kind with + | Fable.UnionCaseTest i-> + Binary(Equals, Get(transformExpr expr, FieldGet "tag") , Const (ConstNumber (float i))) + | _ -> + Unknown(sprintf "test %A %A" expr kind) + | Fable.Extended(Fable.ExtendedSet.Throw(expr, _), t) -> + let errorExpr = + //Const (ConstString "There was an error") + transformExpr expr + FunctionCall(Helpers.ident "error", [errorExpr]) | x -> Unknown (sprintf "%A" x) let transformDeclarations (com: LuaCompiler) = function diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index 59151048e9..30b49a4938 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -96,18 +96,19 @@ module Output = writeExpr ctx value writeExpr ctx expr | Ternary(guardExpr, thenExpr, elseExpr) -> - writeln ctx "" - let ctxA = indent ctx - writei ctxA "" - writeExpr ctxA guardExpr - writeln ctxA "" - let ctxI = indent ctxA - writei ctxI " and " + //let ctxA = indent ctx + write ctx "(" + writeExpr ctx guardExpr + //writeln ctx "" + let ctxI = indent ctx + write ctx " and " + //writei ctx "and " writeExpr ctxI thenExpr - writeln ctxI "" - writei ctxI " or " + //writeln ctx "" + write ctx " or " + //writei ctx "or " writeExpr ctxI elseExpr - write ctx "" + write ctx ")" | Macro (s, args) -> let subbedMacro = (s, args |> List.mapi(fun i x -> i.ToString(), sprintExprSimple x)) @@ -186,7 +187,7 @@ module Output = write ctx "return mod" //debugging writeln ctx "" - // writeln ctx "--[[" - // sprintf "%s" file.ASTDebug |> write ctx - // sprintf "%A" file.Statements |> write ctx - // writeln ctx " --]]" \ No newline at end of file + //writeln ctx "--[[" + //sprintf "%s" file.ASTDebug |> write ctx + //sprintf "%A" file.Statements |> write ctx + //writeln ctx " --]]" \ No newline at end of file diff --git a/tests/Lua/TestUnionType.fs b/tests/Lua/TestUnionType.fs index ba0d0acd19..073d32940d 100644 --- a/tests/Lua/TestUnionType.fs +++ b/tests/Lua/TestUnionType.fs @@ -32,4 +32,13 @@ let testMakeUnionContent2 () = [] let testMakeUnionContent3 () = let r = B - r |> equal B \ No newline at end of file + r |> equal B + +[] +let testMatch1 () = + let thing = A("abc", 123) + let res = + match thing with + | A(s, i) -> Some(s, i) + | B -> None + res |> equal (Some("abc", 123)) \ No newline at end of file From d82be52ea7a469ee98a3b7461037932c85e33346 Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Tue, 31 Aug 2021 20:38:50 +0100 Subject: [PATCH 14/41] progress --- src/Fable.Transforms/Lua/Fable2Lua.fs | 69 +++++++++++++------ src/Fable.Transforms/Lua/Lua.fs | 11 +-- src/Fable.Transforms/Lua/LuaPrinter.fs | 48 ++++++++++++- .../fable/Fable.Library.fsproj | 20 +++--- tests/Lua/Fable.Tests.Lua.fsproj | 1 + tests/Lua/TestArray.fs | 31 +++++++++ tests/Lua/TestControlFlow.fs | 9 ++- tests/Lua/TestUnionType.fs | 1 + tests/Lua/runtests.lua | 1 + 9 files changed, 151 insertions(+), 40 deletions(-) create mode 100644 tests/Lua/TestArray.fs diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index d3928b67d2..3ccfa395c5 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -35,6 +35,11 @@ module Transforms = | [] -> NoOp | [Return expr] -> expr | statements -> iife statements + let tryNewObj (names: string list) (values: Expr list) = + if names.Length = values.Length then + let pairs = List.zip names values + NewObj(pairs) + else sprintf "Names and values do not match %A %A" names values |> Unknown let transformValueKind (com: LuaCompiler) = function | Fable.NumberConstant(v,_,_) -> Const(ConstNumber v) @@ -53,21 +58,22 @@ module Transforms = if entity.IsFSharpRecord then let names = entity.FSharpFields |> List.map(fun f -> f.Name) let values = values |> List.map (transformExpr com) - let pairs = List.zip names values - NewObj(pairs) + Helpers.tryNewObj names values else sprintf "unknown ety %A %A %A %A" values ref args entity |> Unknown | Fable.NewAnonymousRecord(values, names, _) -> let transformedValues = values |> List.map (transformExpr com) - let pairs = List.zip (names |> Array.toList) transformedValues - NewObj(pairs) + Helpers.tryNewObj (Array.toList names) transformedValues | Fable.NewUnion(values, tag, _, _) -> let values = values |> List.map(transformExpr com) |> List.mapi(fun i x -> sprintf "p_%i" i, x) NewObj(("tag", tag |> float |> ConstNumber |> Const)::values) | Fable.NewOption (value, t, _) -> value |> Option.map (transformExpr com) |> Option.defaultValue (Const ConstNull) | Fable.NewTuple(values, isStruct) -> - let fields = values |> List.mapi(fun i x -> sprintf "p_%i" i, transformExpr com x) - NewObj(fields) + // let fields = values |> List.mapi(fun i x -> sprintf "p_%i" i, transformExpr com x) + // NewObj(fields) + NewArr(values |> List.map (transformExpr com)) + | Fable.NewArray(values, t) -> + NewArr(values |> List.map (transformExpr com)) | Fable.Null _ -> Const(ConstNull) | x -> sprintf "unknown %A" x |> ConstString |> Const @@ -87,6 +93,7 @@ module Transforms = | Fable.OperationKind.Unary (op, expr) -> match op with | UnaryNotBitwise -> transformExpr expr //not sure why this is being added + | UnaryNot -> Unary(Not, transformExpr expr) | _ -> sprintf "%A %A" op expr |> Unknown | x -> Unknown(sprintf "%A" x) let asSingleExprIife (exprs: Expr list): Expr= //function @@ -133,29 +140,39 @@ module Transforms = let path = match info.Kind, info.Path with | LibraryImport, Regex "fable-lib\/(\w+).(?:fs|js)" [name] -> + let name = //fudge - not sure why these are being nested + match name with + | "Array" -> "fable-library/" + name + | _ -> name "fable-lib/" + name | _ -> info.Path.Replace(".fs", "").Replace(".js", "") //todo - make less brittle let rcall = FunctionCall(Ident { Namespace=None; Name= "require" }, [Const (ConstString path)]) match info.Selector with | "" -> rcall - | s -> Get(rcall, FieldGet s) + | s -> GetField(rcall, s) | Fable.Expr.IdentExpr(i) when i.Name <> "" -> Ident {Namespace = None; Name = i.Name } | Fable.Expr.Operation (kind, _, _) -> transformOp kind | Fable.Expr.Get(expr, Fable.GetKind.FieldGet(fieldName, isMut), _, _) -> - Get(transformExpr expr, FieldGet(fieldName)) + GetField(transformExpr expr, fieldName) | Fable.Expr.Get(expr, Fable.GetKind.UnionField(caseIdx, fieldIdx), _, _) -> - Get(transformExpr expr, FieldGet(sprintf "p_%i" fieldIdx)) + GetField(transformExpr expr, sprintf "p_%i" fieldIdx) + | Fable.Expr.Get(expr, Fable.GetKind.ExprGet(e), _, _) -> + GetAtIndex(transformExpr expr, transformExpr e) + | Fable.Expr.Set(expr, Fable.SetKind.ValueSet, t, value, _) -> + SetValue(transformExpr expr, transformExpr value) + | Fable.Expr.Set(expr, Fable.SetKind.ExprSet(e), t, value, _) -> + SetExpr(transformExpr expr, transformExpr e, transformExpr value) | Fable.Expr.Sequential exprs -> asSingleExprIifeTr com exprs | Fable.Expr.Let (ident, value, body) -> - //Let(ident.Name, transformExpr value, transformExpr body) - asSingleExprIife [ - Let(ident.Name, transformExpr value, NoOp) - transformExpr body + let statements = [ + Assignment(ident.Name, transformExpr value) + transformExpr body |> Return ] + Helpers.maybeIife statements | Fable.Expr.Emit(m, _, _) -> // let argsExprs = m.CallInfo.Args |> List.map transformExpr // let macroExpr = Macro(m.Macro, argsExprs) @@ -169,13 +186,15 @@ module Transforms = transformExpr expr | Fable.Expr.DecisionTreeSuccess(i, boundValues, _) -> let idents,target = com.GetDecisionTreeTargets(i) - let statements = - [ for (ident, value) in List.zip idents boundValues do - yield Assignment(ident.Name, transformExpr value) - yield transformExpr target |> Return - ] - statements - |> Helpers.maybeIife + if idents.Length = boundValues.Length then + let statements = + [ for (ident, value) in List.zip idents boundValues do + yield Assignment(ident.Name, transformExpr value) + yield transformExpr target |> Return + ] + statements + |> Helpers.maybeIife + else sprintf "not equal lengths %A %A" idents boundValues |> Unknown | Fable.Expr.Lambda(arg, body, name) -> Function([arg.Name], [transformExpr body |> Return]) | Fable.Expr.CurriedApply(applied, args, _, _) -> @@ -185,7 +204,7 @@ module Transforms = | Fable.Test(expr, kind, b) -> match kind with | Fable.UnionCaseTest i-> - Binary(Equals, Get(transformExpr expr, FieldGet "tag") , Const (ConstNumber (float i))) + Binary(Equals, GetField(transformExpr expr, "tag") , Const (ConstNumber (float i))) | _ -> Unknown(sprintf "test %A %A" expr kind) | Fable.Extended(Fable.ExtendedSet.Throw(expr, _), t) -> @@ -193,6 +212,14 @@ module Transforms = //Const (ConstString "There was an error") transformExpr expr FunctionCall(Helpers.ident "error", [errorExpr]) + | Fable.Delegate(idents, body, _) -> + Function(idents |> List.map(fun i -> i.Name), [transformExpr body |> Return |> flattenReturnIifes]) //can be flattened + | Fable.ForLoop(ident, start, limit, body, isUp, _) -> + Helpers.maybeIife [ + ForLoop(ident.Name, transformExpr start, transformExpr limit, [transformExpr body |> Do]) + ] + | Fable.TypeCast(expr, t) -> + transformExpr expr //typecasts are meaningless | x -> Unknown (sprintf "%A" x) let transformDeclarations (com: LuaCompiler) = function diff --git a/src/Fable.Transforms/Lua/Lua.fs b/src/Fable.Transforms/Lua/Lua.fs index ee98385ff0..d590695c4d 100644 --- a/src/Fable.Transforms/Lua/Lua.fs +++ b/src/Fable.Transforms/Lua/Lua.fs @@ -24,24 +24,24 @@ type BinaryOp = | Minus | BinaryTodo of string -type GetKind = - | FieldGet of fieldName: string - type Expr = | Ident of LuaIdentity | Const of Const | Unary of UnaryOp * Expr | Binary of BinaryOp * Expr * Expr - | Get of Expr * kind: GetKind + | GetField of Expr * name: string + | GetAtIndex of Expr * idx: Expr + | SetValue of Expr * value: Expr + | SetExpr of Expr * Expr * value: Expr | FunctionCall of f: Expr * args: Expr list | AnonymousFunc of args: string list * body: Statement list | Unknown of string - | Let of name: string * value: Expr * body: Expr | Macro of string * args: Expr list | Ternary of guardExpr: Expr * thenExpr: Expr * elseExpr: Expr | NoOp | Function of args: string list * body: Statement list | NewObj of values: (string * Expr) list + | NewArr of values: Expr list type Statement = | Assignment of name: string * Expr @@ -49,6 +49,7 @@ type Statement = | Return of Expr | Do of Expr | SNoOp + | ForLoop of string * start: Expr* limit: Expr* body: Statement list type File = { Filename: string diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index 30b49a4938..a401a556cd 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -80,21 +80,36 @@ module Output = for b in body do writeStatement ctxI b writei ctx "end)" + | Unary(Not, expr) -> + write ctx "not " + writeExpr ctx expr | Binary (op, left, right) -> writeExpr ctx left write ctx " " writeOp ctx op write ctx " " writeExpr ctx right - | Get(expr, FieldGet(fieldName)) -> + | GetField(expr, fieldName) -> writeExpr ctx expr write ctx "." write ctx fieldName - | Let(name, value, expr) -> - write ctx name + | GetAtIndex(expr, idx) -> + writeExpr ctx expr + write ctx "[" + //hack alert - lua indexers are 1-based and not 0-based, so we need to "add1". Probably correct soln here is to simplify ast after +1 if possible + let add1 = Binary(BinaryOp.Plus, Const (ConstNumber 1.0), idx) + writeExpr ctx add1 + write ctx "]" + | SetValue(expr, value) -> + writeExpr ctx expr write ctx " = " writeExpr ctx value + | SetExpr(expr, a, value) -> writeExpr ctx expr + write ctx " = " + // writeExpr ctx a + // write ctx " " + writeExpr ctx value | Ternary(guardExpr, thenExpr, elseExpr) -> //let ctxA = indent ctx write ctx "(" @@ -136,6 +151,18 @@ module Output = //writeExprs ctxI args writeln ctx "" writei ctx "}" + | NewArr(args) -> + write ctx "{" + let ctxI = indent ctx + writeln ctxI "" + for idx, expr in args |> List.mapi (fun i x -> i, x) do + writei ctxI "" + writeExpr ctxI expr + if idx < args.Length - 1 then + writeln ctxI "," + //writeExprs ctxI args + writeln ctx "" + writei ctx "}" | NoOp -> () | Unknown x -> writeCommented ctx "unknown" x @@ -178,6 +205,21 @@ module Output = writei ctx "" writeExpr ctx expr writeln ctx "" + | ForLoop (name, start, limit, body) -> + writei ctx "for " + write ctx name + write ctx "=" + writeExpr ctx start + write ctx ", " + writeExpr ctx limit + write ctx " do" + let ctxI = indent ctx + for statement in body do + writeln ctxI "" + writeStatement ctxI statement + writeln ctx "" + writei ctx "end" + writeln ctx "" | SNoOp -> () let writeFile ctx (file: File) = diff --git a/src/fable-library-lua/fable/Fable.Library.fsproj b/src/fable-library-lua/fable/Fable.Library.fsproj index fa985a71f1..7899f66221 100644 --- a/src/fable-library-lua/fable/Fable.Library.fsproj +++ b/src/fable-library-lua/fable/Fable.Library.fsproj @@ -16,21 +16,21 @@ - + - - + - - - - - - + + + + + diff --git a/tests/Lua/Fable.Tests.Lua.fsproj b/tests/Lua/Fable.Tests.Lua.fsproj index 8d66668e23..2c8cce6b21 100644 --- a/tests/Lua/Fable.Tests.Lua.fsproj +++ b/tests/Lua/Fable.Tests.Lua.fsproj @@ -21,6 +21,7 @@ + diff --git a/tests/Lua/TestArray.fs b/tests/Lua/TestArray.fs new file mode 100644 index 0000000000..7f9ead6555 --- /dev/null +++ b/tests/Lua/TestArray.fs @@ -0,0 +1,31 @@ +module Fable.Tests.Array + +open System +open Util.Testing + +[] +let testCreateArray () = + let arr = [||] + arr |> equal [||] + +[] +let testCreateArray2 () = + let arr = [|1;2;3|] + arr |> equal [|1;2;3|] + +[] +let testDeref1 () = + let arr = [|1;2;3|] + arr.[0] |> equal 1 + arr.[1] |> equal 2 + arr.[2] |> equal 3 + +// [] +// let testDestructure () = +// let x = [|1;2|] +// let res = +// match x with +// | [|a; b|] -> +// Some (a, b) +// | _ -> None +// res |> equal (Some(1, 2)) \ No newline at end of file diff --git a/tests/Lua/TestControlFlow.fs b/tests/Lua/TestControlFlow.fs index 7555148970..cc5a261338 100644 --- a/tests/Lua/TestControlFlow.fs +++ b/tests/Lua/TestControlFlow.fs @@ -34,4 +34,11 @@ let testIfElseIf () = else 3 a 1 |> equal 1 a 2 |> equal 2 - a 3 |> equal 3 \ No newline at end of file + a 3 |> equal 3 + +[] +let testForEach1 () = + let mutable a = 42 + for i in 0..5 do + a <- i + a + a |> equal 57 \ No newline at end of file diff --git a/tests/Lua/TestUnionType.fs b/tests/Lua/TestUnionType.fs index 073d32940d..9b4be18fc1 100644 --- a/tests/Lua/TestUnionType.fs +++ b/tests/Lua/TestUnionType.fs @@ -41,4 +41,5 @@ let testMatch1 () = match thing with | A(s, i) -> Some(s, i) | B -> None + | C(_) -> None res |> equal (Some("abc", 123)) \ No newline at end of file diff --git a/tests/Lua/runtests.lua b/tests/Lua/runtests.lua index f7dae0b4a6..febb5c20a5 100644 --- a/tests/Lua/runtests.lua +++ b/tests/Lua/runtests.lua @@ -6,6 +6,7 @@ function TestMod.testHello() end TestArithmetic = require('TestArithmetic') +TestArray = require('TestArray') TestRecords = require('TestRecords') TestControlFlow = require('TestControlFlow') TestUnionType = require('TestUnionType') From b7d3052abf7f2b6cc2d9024308999118cc4d232a Mon Sep 17 00:00:00 2001 From: Alex Swan Date: Fri, 3 Sep 2021 08:45:44 +0100 Subject: [PATCH 15/41] imports --- src/Fable.Transforms/Lua/Fable2Lua.fs | 8 +++--- src/Fable.Transforms/Lua/LuaPrinter.fs | 36 ++++++++++++++++++++++---- src/fable-library-lua/fable/Native.fs | 14 +++++++--- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index 3ccfa395c5..a66d7c93f3 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -140,10 +140,10 @@ module Transforms = let path = match info.Kind, info.Path with | LibraryImport, Regex "fable-lib\/(\w+).(?:fs|js)" [name] -> - let name = //fudge - not sure why these are being nested - match name with - | "Array" -> "fable-library/" + name - | _ -> name + "fable-lib/" + name + | LibraryImport, Regex"fable-library-lua\/fable\/fable-library\/(\w+).(?:fs|js)" [name] -> + "fable-lib/fable-library" + name + | LibraryImport, Regex"fable-library-lua\/fable\/(\w+).(?:fs|js)" [name] -> "fable-lib/" + name | _ -> info.Path.Replace(".fs", "").Replace(".js", "") //todo - make less brittle diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index a401a556cd..4f294738f1 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -124,11 +124,37 @@ module Output = //writei ctx "or " writeExpr ctxI elseExpr write ctx ")" - | Macro (s, args) -> - let subbedMacro = - (s, args |> List.mapi(fun i x -> i.ToString(), sprintExprSimple x)) - ||> List.fold (fun acc (i, arg) -> acc.Replace("$"+i, arg) ) - writei ctx subbedMacro + | Macro (macro, args) -> + + // let subbedMacro = + // (s, args |> List.mapi(fun i x -> i.ToString(), sprintExprSimple x)) + // ||> List.fold (fun acc (i, arg) -> acc.Replace("$"+i, arg) ) + // writei ctx subbedMacro + let regex = System.Text.RegularExpressions.Regex("\$(?\d)(?\.\.\.)?") + let matches = regex.Matches(macro) + let mutable pos = 0 + for m in matches do + let n = int m.Groups.["n"].Value + write ctx (macro.Substring(pos,m.Index-pos)) + if m.Groups.["s"].Success then + if n < args.Length then + match args.[n] with + | NewArr items -> + let mutable first = true + for value in items do + if first then + first <- false + else + write ctx ", " + writeExpr ctx value + | _ -> + writeExpr ctx args.[n] + + elif n < args.Length then + writeExpr ctx args.[n] + + pos <- m.Index + m.Length + write ctx (macro.Substring(pos)) | Function(args, body) -> write ctx "function " write ctx "(" diff --git a/src/fable-library-lua/fable/Native.fs b/src/fable-library-lua/fable/Native.fs index 71a719a9e6..0d73a5d911 100644 --- a/src/fable-library-lua/fable/Native.fs +++ b/src/fable-library-lua/fable/Native.fs @@ -10,17 +10,17 @@ open Fable.Import [] type Cons<'T> = - [] + [] abstract Allocate : len: int -> 'T [] module Helpers = [] let arrayFrom (xs: 'T seq) : 'T [] = nativeOnly - [] + [] let allocateArray (len: int) : 'T [] = nativeOnly - [] + [] let allocateArrayFrom (xs: 'T []) (len: int) : 'T [] = nativeOnly let allocateArrayFromCons (cons: Cons<'T>) (len: int) : 'T [] = @@ -33,7 +33,13 @@ module Helpers = // let inline typedArraySetImpl (target: obj) (source: obj) (offset: int): unit = // !!target?set(source, offset) - [] + [] let concatImpl (array1: 'T []) (arrays: 'T [] seq) : 'T [] = nativeOnly let fillImpl (array: 'T []) (value: 'T) (start: int) (count: int) : 'T [] = From 7048aac82a0c6da17dc50387be778bf384f9385e Mon Sep 17 00:00:00 2001 From: Alex Swan <1506553+alexswan10k@users.noreply.github.com> Date: Mon, 6 Sep 2021 18:47:15 +0100 Subject: [PATCH 16/41] ProjectCracker default path added --- src/Fable.Cli/ProjectCracker.fs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Fable.Cli/ProjectCracker.fs b/src/Fable.Cli/ProjectCracker.fs index fe106b447c..37c4fdcf61 100644 --- a/src/Fable.Cli/ProjectCracker.fs +++ b/src/Fable.Cli/ProjectCracker.fs @@ -488,6 +488,9 @@ let copyFableLibraryAndPackageSources (opts: CrackerOptions) (pkgs: FablePackage | Python -> [ "../../../fable-library-py/" // running from nuget tools package "../../../../../build/fable-library-py/" ] // running from bin/Release/netcoreapp3.1 + | Lua -> + [ "../../../fable-library-lua/" // running from nuget tools package + "../../../../../build/fable-library-lua/" ] // running from bin/Release/netcoreapp3.1 | _ -> [ "../../../fable-library/" // running from nuget tools package "../../../../../build/fable-library/" ] // running from bin/Release/netcoreapp3.1 From 1fae7d5fb20cf5612db24a951b3ab552f4088b7c Mon Sep 17 00:00:00 2001 From: Alex Swan <1506553+alexswan10k@users.noreply.github.com> Date: Mon, 6 Sep 2021 18:53:00 +0100 Subject: [PATCH 17/41] Equality operators --- src/Fable.Transforms/Lua/Fable2Lua.fs | 5 +++++ src/Fable.Transforms/Lua/Lua.fs | 5 +++++ src/Fable.Transforms/Lua/LuaPrinter.fs | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index a66d7c93f3..e031973220 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -88,6 +88,11 @@ module Transforms = | BinaryPlus -> Plus | BinaryMinus -> Minus | BinaryEqualStrict -> Equals + | BinaryUnequal -> Unequal + | BinaryLess -> Less + | BinaryGreater -> Greater + | BinaryLessOrEqual -> LessOrEqual + | BinaryGreaterOrEqual -> GreaterOrEqual | x -> sprintf "%A" x |> BinaryTodo Binary(op, transformExpr left, transformExpr right ) | Fable.OperationKind.Unary (op, expr) -> diff --git a/src/Fable.Transforms/Lua/Lua.fs b/src/Fable.Transforms/Lua/Lua.fs index d590695c4d..8125055e0d 100644 --- a/src/Fable.Transforms/Lua/Lua.fs +++ b/src/Fable.Transforms/Lua/Lua.fs @@ -18,6 +18,11 @@ type UnaryOp = | NotBitwise type BinaryOp = | Equals + | Unequal + | Less + | LessOrEqual + | Greater + | GreaterOrEqual | Multiply | Divide | Plus diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index 4f294738f1..ac52d9f21f 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -49,6 +49,11 @@ module Output = let writeOp ctx = function | Multiply -> write ctx "*" | Equals -> write ctx "==" + | Unequal -> write ctx "~=" + | Less -> write ctx "<" + | LessOrEqual -> write ctx "<=" + | Greater -> write ctx ">" + | GreaterOrEqual -> write ctx ">=" | Divide -> write ctx """/""" | Plus -> write ctx "+" | Minus -> write ctx "-" From 25750e08ecdc651860a09694d00ee71d5bad43dc Mon Sep 17 00:00:00 2001 From: Alex Swan <1506553+alexswan10k@users.noreply.github.com> Date: Mon, 6 Sep 2021 19:02:24 +0100 Subject: [PATCH 18/41] Implement while loop --- src/Fable.Transforms/Lua/Fable2Lua.fs | 4 ++++ src/Fable.Transforms/Lua/Lua.fs | 1 + src/Fable.Transforms/Lua/LuaPrinter.fs | 11 +++++++++++ tests/Lua/TestControlFlow.fs | 9 ++++++++- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index e031973220..b59a8fee7a 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -225,6 +225,10 @@ module Transforms = ] | Fable.TypeCast(expr, t) -> transformExpr expr //typecasts are meaningless + | Fable.WhileLoop(guard, body, label, range) -> + Helpers.maybeIife [ + WhileLoop(transformExpr guard, [transformExpr body |> Do]) + ] | x -> Unknown (sprintf "%A" x) let transformDeclarations (com: LuaCompiler) = function diff --git a/src/Fable.Transforms/Lua/Lua.fs b/src/Fable.Transforms/Lua/Lua.fs index 8125055e0d..0bd90cda09 100644 --- a/src/Fable.Transforms/Lua/Lua.fs +++ b/src/Fable.Transforms/Lua/Lua.fs @@ -55,6 +55,7 @@ type Statement = | Do of Expr | SNoOp | ForLoop of string * start: Expr* limit: Expr* body: Statement list + | WhileLoop of guard: Expr * body: Statement list type File = { Filename: string diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index ac52d9f21f..a4a75aea18 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -251,6 +251,17 @@ module Output = writeln ctx "" writei ctx "end" writeln ctx "" + | WhileLoop (guard, body) -> + writei ctx "while " + writeExpr ctx guard + write ctx " do" + let ctxI = indent ctx + for statement in body do + writeln ctxI "" + writeStatement ctxI statement + writeln ctx "" + writei ctx "end" + writeln ctx "" | SNoOp -> () let writeFile ctx (file: File) = diff --git a/tests/Lua/TestControlFlow.fs b/tests/Lua/TestControlFlow.fs index cc5a261338..1d5ac2c451 100644 --- a/tests/Lua/TestControlFlow.fs +++ b/tests/Lua/TestControlFlow.fs @@ -41,4 +41,11 @@ let testForEach1 () = let mutable a = 42 for i in 0..5 do a <- i + a - a |> equal 57 \ No newline at end of file + a |> equal 57 + +[] +let testWhile1 () = + let mutable a = 1 + while a < 3 do + a <- a + 1 + a |> equal 3 \ No newline at end of file From 8d5301ca49ebcdcd34af6e2453bc3e4bff31dcb0 Mon Sep 17 00:00:00 2001 From: Alex Swan <1506553+alexswan10k@users.noreply.github.com> Date: Mon, 6 Sep 2021 21:48:29 +0100 Subject: [PATCH 19/41] ex control flow (excluding ex pattern matching) --- src/Fable.Transforms/Lua/Fable2Lua.fs | 34 +++++++++++++++++++++----- src/Fable.Transforms/Lua/Lua.fs | 3 ++- src/Fable.Transforms/Lua/LuaPrinter.fs | 21 ++++++++++++++-- tests/Lua/TestControlFlow.fs | 21 +++++++++++++++- 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index b59a8fee7a..fa924e5e16 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -99,6 +99,7 @@ module Transforms = match op with | UnaryNotBitwise -> transformExpr expr //not sure why this is being added | UnaryNot -> Unary(Not, transformExpr expr) + | UnaryVoid -> NoOp | _ -> sprintf "%A %A" op expr |> Unknown | x -> Unknown(sprintf "%A" x) let asSingleExprIife (exprs: Expr list): Expr= //function @@ -174,7 +175,7 @@ module Transforms = asSingleExprIifeTr com exprs | Fable.Expr.Let (ident, value, body) -> let statements = [ - Assignment(ident.Name, transformExpr value) + Assignment([ident.Name], transformExpr value) transformExpr body |> Return ] Helpers.maybeIife statements @@ -194,7 +195,7 @@ module Transforms = if idents.Length = boundValues.Length then let statements = [ for (ident, value) in List.zip idents boundValues do - yield Assignment(ident.Name, transformExpr value) + yield Assignment([ident.Name], transformExpr value) yield transformExpr target |> Return ] statements @@ -214,8 +215,8 @@ module Transforms = Unknown(sprintf "test %A %A" expr kind) | Fable.Extended(Fable.ExtendedSet.Throw(expr, _), t) -> let errorExpr = - //Const (ConstString "There was an error") - transformExpr expr + Const (ConstString "There was an error, todo") + //transformExpr expr FunctionCall(Helpers.ident "error", [errorExpr]) | Fable.Delegate(idents, body, _) -> Function(idents |> List.map(fun i -> i.Name), [transformExpr body |> Return |> flattenReturnIifes]) //can be flattened @@ -229,14 +230,35 @@ module Transforms = Helpers.maybeIife [ WhileLoop(transformExpr guard, [transformExpr body |> Do]) ] + | Fable.TryCatch(body, catch, finalizer, _) -> + Helpers.maybeIife [ + Assignment(["status"; "resOrErr"], FunctionCall(Helpers.ident "pcall", [ + Function([], [ + transformExpr body |> Return + ]) + ])) + let finalizer = finalizer |> Option.map transformExpr + let catch = catch |> Option.map (fun (ident, expr) -> ident.Name, transformExpr expr) + IfThenElse(Helpers.ident "status", [ + match finalizer with + | Some finalizer -> yield Do finalizer + | None -> () + yield Helpers.ident "resOrErr" |> Return + ], [ + match catch with + | Some(ident, expr) -> + yield expr |> Return + | _ -> () + ]) + ] | x -> Unknown (sprintf "%A" x) let transformDeclarations (com: LuaCompiler) = function | Fable.ModuleDeclaration m -> - Assignment("moduleDecTest", Expr.Const (ConstString "moduledectest")) + Assignment(["moduleDecTest"], Expr.Const (ConstString "moduledectest")) | Fable.MemberDeclaration m -> if m.Args.Length = 0 then - Assignment(m.Name, transformExpr com m.Body) + Assignment([m.Name], transformExpr com m.Body) else let unwrapSelfExStatements = diff --git a/src/Fable.Transforms/Lua/Lua.fs b/src/Fable.Transforms/Lua/Lua.fs index 0bd90cda09..b7cfdd0c27 100644 --- a/src/Fable.Transforms/Lua/Lua.fs +++ b/src/Fable.Transforms/Lua/Lua.fs @@ -49,13 +49,14 @@ type Expr = | NewArr of values: Expr list type Statement = - | Assignment of name: string * Expr + | Assignment of names: string list * Expr | FunctionDeclaration of name: string * args: string list * body: Statement list * exportToMod: bool | Return of Expr | Do of Expr | SNoOp | ForLoop of string * start: Expr* limit: Expr* body: Statement list | WhileLoop of guard: Expr * body: Statement list + | IfThenElse of guard: Expr * thenSt: Statement list * elseSt: Statement list type File = { Filename: string diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index a4a75aea18..6d0f00d2fa 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -207,8 +207,9 @@ module Output = writeExpr ctx item and writeStatement ctx = function - | Assignment(name, expr) -> - writei ctx name + | Assignment(names, expr) -> + let names = names |> Helper.separateWithCommas + writei ctx names write ctx " = " writeExpr ctx expr writeln ctx "" @@ -262,6 +263,22 @@ module Output = writeln ctx "" writei ctx "end" writeln ctx "" + | IfThenElse(guard, thenSt, elseSt) -> + writei ctx "if " + writeExpr ctx guard + write ctx " then" + let ctxI = indent ctx + for statement in thenSt do + writeln ctxI "" + writeStatement ctxI statement + writeln ctx "" + writei ctx "else" + for statement in elseSt do + writeln ctxI "" + writeStatement ctxI statement + writeln ctx "" + writei ctx "end" + writeln ctx "" | SNoOp -> () let writeFile ctx (file: File) = diff --git a/tests/Lua/TestControlFlow.fs b/tests/Lua/TestControlFlow.fs index 1d5ac2c451..e3dfa46d36 100644 --- a/tests/Lua/TestControlFlow.fs +++ b/tests/Lua/TestControlFlow.fs @@ -48,4 +48,23 @@ let testWhile1 () = let mutable a = 1 while a < 3 do a <- a + 1 - a |> equal 3 \ No newline at end of file + a |> equal 3 + +[] +let testExHappy() = + let a = + try + 3 + with ex -> + 4 + a |> equal 3 + +[] +let testExThrow() = + let a = + try + failwith "boom" + 3 + with ex -> + 4 + a |> equal 4 \ No newline at end of file From 9d7e875803635e066bf4d3240099e8583b39498e Mon Sep 17 00:00:00 2001 From: Alex Swan <1506553+alexswan10k@users.noreply.github.com> Date: Fri, 10 Sep 2021 08:30:51 +0100 Subject: [PATCH 20/41] Migrate to lua 5.2. Util generated from ts --- build.fsx | 5 +- share/lua/5.1/luaunit.lua | 3453 ++++++++++++++++++++++++ src/Fable.Transforms/Lua/Fable2Lua.fs | 56 +- src/Fable.Transforms/Lua/Lua.fs | 3 + src/Fable.Transforms/Lua/LuaPrinter.fs | 13 +- src/fable-library-lua/README.md | 11 +- src/fable-library-lua/fable/Util.lua | 1604 ++++++++++- tests/Lua/Util.fs | 4 +- tests/Lua/luaunit.lua | 3372 +++++++++++++++++++++++ tests/Lua/runtests.lua | 8 +- 10 files changed, 8490 insertions(+), 39 deletions(-) create mode 100644 share/lua/5.1/luaunit.lua create mode 100644 tests/Lua/luaunit.lua diff --git a/build.fsx b/build.fsx index 9344f0b70e..b47be94c43 100644 --- a/build.fsx +++ b/build.fsx @@ -217,7 +217,7 @@ let buildLibraryLua() = // Copy *.lua from projectDir to buildDir copyDirRecursive libraryDir buildDirLua - runInDir buildDirLua ("lua -v") + runInDir buildDirLua ("lua52 -v") //runInDir buildDirLua ("lua ./setup.lua develop") let buildPyLibraryIfNotExists() = @@ -482,8 +482,9 @@ let testLua() = "--fableLib " + buildDir "fable-lib" //("fable-library-lua" "fable") //cannot use relative paths in lua. Copy to subfolder? ] + copyFile (projectDir "luaunit.lua") (buildDir "luaunit.lua") copyFile (projectDir "runtests.lua") (buildDir "runtests.lua") - runInDir buildDir "lua runtests.lua" + runInDir buildDir "lua52 runtests.lua" let buildLocalPackageWith pkgDir pkgCommand fsproj action = let version = "3.0.0-local-build-" + DateTime.Now.ToString("yyyyMMdd-HHmm") diff --git a/share/lua/5.1/luaunit.lua b/share/lua/5.1/luaunit.lua new file mode 100644 index 0000000000..4741478bc7 --- /dev/null +++ b/share/lua/5.1/luaunit.lua @@ -0,0 +1,3453 @@ +--[[ + luaunit.lua + +Description: A unit testing framework +Homepage: https://github.com/bluebird75/luaunit +Development by Philippe Fremy +Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit) +License: BSD License, see LICENSE.txt +]]-- + +require("math") +local M={} + +-- private exported functions (for testing) +M.private = {} + +M.VERSION='3.4' +M._VERSION=M.VERSION -- For LuaUnit v2 compatibility + +-- a version which distinguish between regular Lua and LuaJit +M._LUAVERSION = (jit and jit.version) or _VERSION + +--[[ Some people like assertEquals( actual, expected ) and some people prefer +assertEquals( expected, actual ). +]]-- +M.ORDER_ACTUAL_EXPECTED = true +M.PRINT_TABLE_REF_IN_ERROR_MSG = false +M.LINE_LENGTH = 80 +M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items +M.LIST_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items + +-- this setting allow to remove entries from the stack-trace, for +-- example to hide a call to a framework which would be calling luaunit +M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE = 0 + +--[[ EPS is meant to help with Lua's floating point math in simple corner +cases like almostEquals(1.1-0.1, 1), which may not work as-is (e.g. on numbers +with rational binary representation) if the user doesn't provide some explicit +error margin. + +The default margin used by almostEquals() in such cases is EPS; and since +Lua may be compiled with different numeric precisions (single vs. double), we +try to select a useful default for it dynamically. Note: If the initial value +is not acceptable, it can be changed by the user to better suit specific needs. + +See also: https://en.wikipedia.org/wiki/Machine_epsilon +]] +M.EPS = 2^-52 -- = machine epsilon for "double", ~2.22E-16 +if math.abs(1.1 - 1 - 0.1) > M.EPS then + -- rounding error is above EPS, assume single precision + M.EPS = 2^-23 -- = machine epsilon for "float", ~1.19E-07 +end + +-- set this to false to debug luaunit +local STRIP_LUAUNIT_FROM_STACKTRACE = true + +M.VERBOSITY_DEFAULT = 10 +M.VERBOSITY_LOW = 1 +M.VERBOSITY_QUIET = 0 +M.VERBOSITY_VERBOSE = 20 +M.DEFAULT_DEEP_ANALYSIS = nil +M.FORCE_DEEP_ANALYSIS = true +M.DISABLE_DEEP_ANALYSIS = false + +-- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values +-- EXPORT_ASSERT_TO_GLOBALS = true + +-- we need to keep a copy of the script args before it is overriden +local cmdline_argv = rawget(_G, "arg") + +M.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests +M.SUCCESS_PREFIX = 'LuaUnit test SUCCESS: ' -- prefix string for successful tests finished early +M.SKIP_PREFIX = 'LuaUnit test SKIP: ' -- prefix string for skipped tests + + + +M.USAGE=[[Usage: lua [options] [testname1 [testname2] ... ] +Options: + -h, --help: Print this help + --version: Print version information + -v, --verbose: Increase verbosity + -q, --quiet: Set verbosity to minimum + -e, --error: Stop on first error + -f, --failure: Stop on first failure or error + -s, --shuffle: Shuffle tests before running them + -o, --output OUTPUT: Set output type to OUTPUT + Possible values: text, tap, junit, nil + -n, --name NAME: For junit only, mandatory name of xml file + -r, --repeat NUM: Execute all tests NUM times, e.g. to trig the JIT + -p, --pattern PATTERN: Execute all test names matching the Lua PATTERN + May be repeated to include several patterns + Make sure you escape magic chars like +? with % + -x, --exclude PATTERN: Exclude all test names matching the Lua PATTERN + May be repeated to exclude several patterns + Make sure you escape magic chars like +? with % + testname1, testname2, ... : tests to run in the form of testFunction, + TestClass or TestClass.testMethod + +You may also control LuaUnit options with the following environment variables: +* LUAUNIT_OUTPUT: same as --output +* LUAUNIT_JUNIT_FNAME: same as --name ]] + +---------------------------------------------------------------- +-- +-- general utility functions +-- +---------------------------------------------------------------- + +--[[ Note on catching exit + +I have seen the case where running a big suite of test cases and one of them would +perform a os.exit(0), making the outside world think that the full test suite was executed +successfully. + +This is an attempt to mitigate this problem: we override os.exit() to now let a test +exit the framework while we are running. When we are not running, it behaves normally. +]] + +M.oldOsExit = os.exit +os.exit = function(...) + if M.LuaUnit and #M.LuaUnit.instances ~= 0 then + local msg = [[You are trying to exit but there is still a running instance of LuaUnit. +LuaUnit expects to run until the end before exiting with a complete status of successful/failed tests. + +To force exit LuaUnit while running, please call before os.exit (assuming lu is the luaunit module loaded): + + lu.unregisterCurrentSuite() + +]] + M.private.error_fmt(2, msg) + end + M.oldOsExit(...) +end + +local function pcall_or_abort(func, ...) + -- unpack is a global function for Lua 5.1, otherwise use table.unpack + local unpack = rawget(_G, "unpack") or table.unpack + local result = {pcall(func, ...)} + if not result[1] then + -- an error occurred + print(result[2]) -- error message + print() + print(M.USAGE) + os.exit(-1) + end + return unpack(result, 2) +end + +local crossTypeOrdering = { + number = 1, boolean = 2, string = 3, table = 4, other = 5 +} +local crossTypeComparison = { + number = function(a, b) return a < b end, + string = function(a, b) return a < b end, + other = function(a, b) return tostring(a) < tostring(b) end, +} + +local function crossTypeSort(a, b) + local type_a, type_b = type(a), type(b) + if type_a == type_b then + local func = crossTypeComparison[type_a] or crossTypeComparison.other + return func(a, b) + end + type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other + type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other + return type_a < type_b +end + +local function __genSortedIndex( t ) + -- Returns a sequence consisting of t's keys, sorted. + local sortedIndex = {} + + for key,_ in pairs(t) do + table.insert(sortedIndex, key) + end + + table.sort(sortedIndex, crossTypeSort) + return sortedIndex +end +M.private.__genSortedIndex = __genSortedIndex + +local function sortedNext(state, control) + -- Equivalent of the next() function of table iteration, but returns the + -- keys in sorted order (see __genSortedIndex and crossTypeSort). + -- The state is a temporary variable during iteration and contains the + -- sorted key table (state.sortedIdx). It also stores the last index (into + -- the keys) used by the iteration, to find the next one quickly. + local key + + --print("sortedNext: control = "..tostring(control) ) + if control == nil then + -- start of iteration + state.count = #state.sortedIdx + state.lastIdx = 1 + key = state.sortedIdx[1] + return key, state.t[key] + end + + -- normally, we expect the control variable to match the last key used + if control ~= state.sortedIdx[state.lastIdx] then + -- strange, we have to find the next value by ourselves + -- the key table is sorted in crossTypeSort() order! -> use bisection + local lower, upper = 1, state.count + repeat + state.lastIdx = math.modf((lower + upper) / 2) + key = state.sortedIdx[state.lastIdx] + if key == control then + break -- key found (and thus prev index) + end + if crossTypeSort(key, control) then + -- key < control, continue search "right" (towards upper bound) + lower = state.lastIdx + 1 + else + -- key > control, continue search "left" (towards lower bound) + upper = state.lastIdx - 1 + end + until lower > upper + if lower > upper then -- only true if the key wasn't found, ... + state.lastIdx = state.count -- ... so ensure no match in code below + end + end + + -- proceed by retrieving the next value (or nil) from the sorted keys + state.lastIdx = state.lastIdx + 1 + key = state.sortedIdx[state.lastIdx] + if key then + return key, state.t[key] + end + + -- getting here means returning `nil`, which will end the iteration +end + +local function sortedPairs(tbl) + -- Equivalent of the pairs() function on tables. Allows to iterate in + -- sorted order. As required by "generic for" loops, this will return the + -- iterator (function), an "invariant state", and the initial control value. + -- (see http://www.lua.org/pil/7.2.html) + return sortedNext, {t = tbl, sortedIdx = __genSortedIndex(tbl)}, nil +end +M.private.sortedPairs = sortedPairs + +-- seed the random with a strongly varying seed +math.randomseed(math.floor(os.clock()*1E11)) + +local function randomizeTable( t ) + -- randomize the item orders of the table t + for i = #t, 2, -1 do + local j = math.random(i) + if i ~= j then + t[i], t[j] = t[j], t[i] + end + end +end +M.private.randomizeTable = randomizeTable + +local function strsplit(delimiter, text) +-- Split text into a list consisting of the strings in text, separated +-- by strings matching delimiter (which may _NOT_ be a pattern). +-- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores") + if delimiter == "" or delimiter == nil then -- this would result in endless loops + error("delimiter is nil or empty string!") + end + if text == nil then + return nil + end + + local list, pos, first, last = {}, 1 + while true do + first, last = text:find(delimiter, pos, true) + if first then -- found? + table.insert(list, text:sub(pos, first - 1)) + pos = last + 1 + else + table.insert(list, text:sub(pos)) + break + end + end + return list +end +M.private.strsplit = strsplit + +local function hasNewLine( s ) + -- return true if s has a newline + return (string.find(s, '\n', 1, true) ~= nil) +end +M.private.hasNewLine = hasNewLine + +local function prefixString( prefix, s ) + -- Prefix all the lines of s with prefix + return prefix .. string.gsub(s, '\n', '\n' .. prefix) +end +M.private.prefixString = prefixString + +local function strMatch(s, pattern, start, final ) + -- return true if s matches completely the pattern from index start to index end + -- return false in every other cases + -- if start is nil, matches from the beginning of the string + -- if final is nil, matches to the end of the string + start = start or 1 + final = final or string.len(s) + + local foundStart, foundEnd = string.find(s, pattern, start, false) + return foundStart == start and foundEnd == final +end +M.private.strMatch = strMatch + +local function patternFilter(patterns, expr) + -- Run `expr` through the inclusion and exclusion rules defined in patterns + -- and return true if expr shall be included, false for excluded. + -- Inclusion pattern are defined as normal patterns, exclusions + -- patterns start with `!` and are followed by a normal pattern + + -- result: nil = UNKNOWN (not matched yet), true = ACCEPT, false = REJECT + -- default: true if no explicit "include" is found, set to false otherwise + local default, result = true, nil + + if patterns ~= nil then + for _, pattern in ipairs(patterns) do + local exclude = pattern:sub(1,1) == '!' + if exclude then + pattern = pattern:sub(2) + else + -- at least one include pattern specified, a match is required + default = false + end + -- print('pattern: ',pattern) + -- print('exclude: ',exclude) + -- print('default: ',default) + + if string.find(expr, pattern) then + -- set result to false when excluding, true otherwise + result = not exclude + end + end + end + + if result ~= nil then + return result + end + return default +end +M.private.patternFilter = patternFilter + +local function xmlEscape( s ) + -- Return s escaped for XML attributes + -- escapes table: + -- " " + -- ' ' + -- < < + -- > > + -- & & + + return string.gsub( s, '.', { + ['&'] = "&", + ['"'] = """, + ["'"] = "'", + ['<'] = "<", + ['>'] = ">", + } ) +end +M.private.xmlEscape = xmlEscape + +local function xmlCDataEscape( s ) + -- Return s escaped for CData section, escapes: "]]>" + return string.gsub( s, ']]>', ']]>' ) +end +M.private.xmlCDataEscape = xmlCDataEscape + + +local function lstrip( s ) + --[[Return s with all leading white spaces and tabs removed]] + local idx = 0 + while idx < s:len() do + idx = idx + 1 + local c = s:sub(idx,idx) + if c ~= ' ' and c ~= '\t' then + break + end + end + return s:sub(idx) +end +M.private.lstrip = lstrip + +local function extractFileLineInfo( s ) + --[[ From a string in the form "(leading spaces) dir1/dir2\dir3\file.lua:linenb: msg" + + Return the "file.lua:linenb" information + ]] + local s2 = lstrip(s) + local firstColon = s2:find(':', 1, true) + if firstColon == nil then + -- string is not in the format file:line: + return s + end + local secondColon = s2:find(':', firstColon+1, true) + if secondColon == nil then + -- string is not in the format file:line: + return s + end + + return s2:sub(1, secondColon-1) +end +M.private.extractFileLineInfo = extractFileLineInfo + + +local function stripLuaunitTrace2( stackTrace, errMsg ) + --[[ + -- Example of a traceback: + < + [C]: in function 'xpcall' + ./luaunit.lua:1449: in function 'protectedCall' + ./luaunit.lua:1508: in function 'execOneFunction' + ./luaunit.lua:1596: in function 'runSuiteByInstances' + ./luaunit.lua:1660: in function 'runSuiteByNames' + ./luaunit.lua:1736: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + Other example: + < + [C]: in function 'xpcall' + ./luaunit.lua:1517: in function 'protectedCall' + ./luaunit.lua:1578: in function 'execOneFunction' + ./luaunit.lua:1677: in function 'runSuiteByInstances' + ./luaunit.lua:1730: in function 'runSuiteByNames' + ./luaunit.lua:1806: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + < + [C]: in function 'xpcall' + luaunit2/luaunit.lua:1532: in function 'protectedCall' + luaunit2/luaunit.lua:1591: in function 'execOneFunction' + luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances' + luaunit2/luaunit.lua:1743: in function 'runSuiteByNames' + luaunit2/luaunit.lua:1819: in function 'runSuite' + luaunit2/example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + + -- first line is "stack traceback": KEEP + -- next line may be luaunit line: REMOVE + -- next lines are call in the program under testOk: REMOVE + -- next lines are calls from luaunit to call the program under test: KEEP + + -- Strategy: + -- keep first line + -- remove lines that are part of luaunit + -- kepp lines until we hit a luaunit line + + The strategy for stripping is: + * keep first line "stack traceback:" + * part1: + * analyse all lines of the stack from bottom to top of the stack (first line to last line) + * extract the "file:line:" part of the line + * compare it with the "file:line" part of the error message + * if it does not match strip the line + * if it matches, keep the line and move to part 2 + * part2: + * anything NOT starting with luaunit.lua is the interesting part of the stack trace + * anything starting again with luaunit.lua is part of the test launcher and should be stripped out + ]] + + local function isLuaunitInternalLine( s ) + -- return true if line of stack trace comes from inside luaunit + return s:find('[/\\]luaunit%.lua:%d+: ') ~= nil + end + + -- print( '<<'..stackTrace..'>>' ) + + local t = strsplit( '\n', stackTrace ) + -- print( prettystr(t) ) + + local idx = 2 + + local errMsgFileLine = extractFileLineInfo(errMsg) + -- print('emfi="'..errMsgFileLine..'"') + + -- remove lines that are still part of luaunit + while t[idx] and extractFileLineInfo(t[idx]) ~= errMsgFileLine do + -- print('Removing : '..t[idx] ) + table.remove(t, idx) + end + + -- keep lines until we hit luaunit again + while t[idx] and (not isLuaunitInternalLine(t[idx])) do + -- print('Keeping : '..t[idx] ) + idx = idx + 1 + end + + -- remove remaining luaunit lines + while t[idx] do + -- print('Removing2 : '..t[idx] ) + table.remove(t, idx) + end + + -- print( prettystr(t) ) + return table.concat( t, '\n') + +end +M.private.stripLuaunitTrace2 = stripLuaunitTrace2 + + +local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable ) + local type_v = type(v) + if "string" == type_v then + -- use clever delimiters according to content: + -- enclose with single quotes if string contains ", but no ' + if v:find('"', 1, true) and not v:find("'", 1, true) then + return "'" .. v .. "'" + end + -- use double quotes otherwise, escape embedded " + return '"' .. v:gsub('"', '\\"') .. '"' + + elseif "table" == type_v then + --if v.__class__ then + -- return string.gsub( tostring(v), 'table', v.__class__ ) + --end + return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable) + + elseif "number" == type_v then + -- eliminate differences in formatting between various Lua versions + if v ~= v then + return "#NaN" -- "not a number" + end + if v == math.huge then + return "#Inf" -- "infinite" + end + if v == -math.huge then + return "-#Inf" + end + if _VERSION == "Lua 5.3" then + local i = math.tointeger(v) + if i then + return tostring(i) + end + end + end + + return tostring(v) +end + +local function prettystr( v ) + --[[ Pretty string conversion, to display the full content of a variable of any type. + + * string are enclosed with " by default, or with ' if string contains a " + * tables are expanded to show their full content, with indentation in case of nested tables + ]]-- + local cycleDetectTable = {} + local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable) + if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then + -- some table contain recursive references, + -- so we must recompute the value by including all table references + -- else the result looks like crap + cycleDetectTable = {} + s = prettystr_sub(v, 1, true, cycleDetectTable) + end + return s +end +M.prettystr = prettystr + +function M.adjust_err_msg_with_iter( err_msg, iter_msg ) + --[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed, + add the iteration message if any and return the result. + + err_msg: string, error message captured with pcall + iter_msg: a string describing the current iteration ("iteration N") or nil + if there is no iteration in this test. + + Returns: (new_err_msg, test_status) + new_err_msg: string, adjusted error message, or nil in case of success + test_status: M.NodeStatus.FAIL, SUCCESS or ERROR according to the information + contained in the error message. + ]] + if iter_msg then + iter_msg = iter_msg..', ' + else + iter_msg = '' + end + + local RE_FILE_LINE = '.*:%d+: ' + + -- error message is not necessarily a string, + -- so convert the value to string with prettystr() + if type( err_msg ) ~= 'string' then + err_msg = prettystr( err_msg ) + end + + if (err_msg:find( M.SUCCESS_PREFIX ) == 1) or err_msg:match( '('..RE_FILE_LINE..')' .. M.SUCCESS_PREFIX .. ".*" ) then + -- test finished early with success() + return nil, M.NodeStatus.SUCCESS + end + + if (err_msg:find( M.SKIP_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.SKIP_PREFIX .. ".*" ) ~= nil) then + -- substitute prefix by iteration message + err_msg = err_msg:gsub('.*'..M.SKIP_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.SKIP + end + + if (err_msg:find( M.FAILURE_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.FAILURE_PREFIX .. ".*" ) ~= nil) then + -- substitute prefix by iteration message + err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.FAIL + end + + + + -- print("error detected") + -- regular error, not a failure + if iter_msg then + local match + -- "./test\\test_luaunit.lua:2241: some error msg + match = err_msg:match( '(.*:%d+: ).*' ) + if match then + err_msg = err_msg:gsub( match, match .. iter_msg ) + else + -- no file:line: infromation, just add the iteration info at the beginning of the line + err_msg = iter_msg .. err_msg + end + end + return err_msg, M.NodeStatus.ERROR +end + +local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis, margin ) + --[[ + Prepares a nice error message when comparing tables, performing a deeper + analysis. + + Arguments: + * table_a, table_b: tables to be compared + * doDeepAnalysis: + M.DEFAULT_DEEP_ANALYSIS: (the default if not specified) perform deep analysis only for big lists and big dictionnaries + M.FORCE_DEEP_ANALYSIS : always perform deep analysis + M.DISABLE_DEEP_ANALYSIS: never perform deep analysis + * margin: supplied only for almost equality + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- check if table_a & table_b are suitable for deep analysis + if type(table_a) ~= 'table' or type(table_b) ~= 'table' then + return false + end + + if doDeepAnalysis == M.DISABLE_DEEP_ANALYSIS then + return false + end + + local len_a, len_b, isPureList = #table_a, #table_b, true + + for k1, v1 in pairs(table_a) do + if type(k1) ~= 'number' or k1 > len_a then + -- this table a mapping + isPureList = false + break + end + end + + if isPureList then + for k2, v2 in pairs(table_b) do + if type(k2) ~= 'number' or k2 > len_b then + -- this table a mapping + isPureList = false + break + end + end + end + + if isPureList and math.min(len_a, len_b) < M.LIST_DIFF_ANALYSIS_THRESHOLD then + if not (doDeepAnalysis == M.FORCE_DEEP_ANALYSIS) then + return false + end + end + + if isPureList then + return M.private.mismatchFormattingPureList( table_a, table_b, margin ) + else + -- only work on mapping for the moment + -- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) + return false + end +end +M.private.tryMismatchFormatting = tryMismatchFormatting + +local function getTaTbDescr() + if not M.ORDER_ACTUAL_EXPECTED then + return 'expected', 'actual' + end + return 'actual', 'expected' +end + +local function extendWithStrFmt( res, ... ) + table.insert( res, string.format( ... ) ) +end + +local function mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) + --[[ + Prepares a nice error message when comparing tables which are not pure lists, performing a deeper + analysis. + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- disable for the moment + --[[ + local result = {} + local descrTa, descrTb = getTaTbDescr() + + local keysCommon = {} + local keysOnlyTa = {} + local keysOnlyTb = {} + local keysDiffTaTb = {} + + local k, v + + for k,v in pairs( table_a ) do + if is_equal( v, table_b[k] ) then + table.insert( keysCommon, k ) + else + if table_b[k] == nil then + table.insert( keysOnlyTa, k ) + else + table.insert( keysDiffTaTb, k ) + end + end + end + + for k,v in pairs( table_b ) do + if not is_equal( v, table_a[k] ) and table_a[k] == nil then + table.insert( keysOnlyTb, k ) + end + end + + local len_a = #keysCommon + #keysDiffTaTb + #keysOnlyTa + local len_b = #keysCommon + #keysDiffTaTb + #keysOnlyTb + local limited_display = (len_a < 5 or len_b < 5) + + if math.min(len_a, len_b) < M.TABLE_DIFF_ANALYSIS_THRESHOLD then + return false + end + + if not limited_display then + if len_a == len_b then + extendWithStrFmt( result, 'Table A (%s) and B (%s) both have %d items', descrTa, descrTb, len_a ) + else + extendWithStrFmt( result, 'Table A (%s) has %d items and table B (%s) has %d items', descrTa, len_a, descrTb, len_b ) + end + + if #keysCommon == 0 and #keysDiffTaTb == 0 then + table.insert( result, 'Table A and B have no keys in common, they are totally different') + else + local s_other = 'other ' + if #keysCommon then + extendWithStrFmt( result, 'Table A and B have %d identical items', #keysCommon ) + else + table.insert( result, 'Table A and B have no identical items' ) + s_other = '' + end + + if #keysDiffTaTb ~= 0 then + result[#result] = string.format( '%s and %d items differing present in both tables', result[#result], #keysDiffTaTb) + else + result[#result] = string.format( '%s and no %sitems differing present in both tables', result[#result], s_other, #keysDiffTaTb) + end + end + + extendWithStrFmt( result, 'Table A has %d keys not present in table B and table B has %d keys not present in table A', #keysOnlyTa, #keysOnlyTb ) + end + + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr(k) + end + + if #keysDiffTaTb ~= 0 then + table.insert( result, 'Items differing in A and B:') + for k,v in sortedPairs( keysDiffTaTb ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + + if #keysOnlyTa ~= 0 then + table.insert( result, 'Items only in table A:' ) + for k,v in sortedPairs( keysOnlyTa ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + + if #keysOnlyTb ~= 0 then + table.insert( result, 'Items only in table B:' ) + for k,v in sortedPairs( keysOnlyTb ) do + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + + if #keysCommon ~= 0 then + table.insert( result, 'Items common to A and B:') + for k,v in sortedPairs( keysCommon ) do + extendWithStrFmt( result, ' = A and B [%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + + return true, table.concat( result, '\n') + ]] +end +M.private.mismatchFormattingMapping = mismatchFormattingMapping + +local function mismatchFormattingPureList( table_a, table_b, margin ) + --[[ + Prepares a nice error message when comparing tables which are lists, performing a deeper + analysis. + + margin is supplied only for almost equality + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + local result, descrTa, descrTb = {}, getTaTbDescr() + + local len_a, len_b, refa, refb = #table_a, #table_b, '', '' + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + refa, refb = string.format( '<%s> ', M.private.table_ref(table_a)), string.format('<%s> ', M.private.table_ref(table_b) ) + end + local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b) + local deltalv = longest - shortest + + local commonUntil = shortest + for i = 1, shortest do + if not M.private.is_table_equals(table_a[i], table_b[i], margin) then + commonUntil = i - 1 + break + end + end + + local commonBackTo = shortest - 1 + for i = 0, shortest - 1 do + if not M.private.is_table_equals(table_a[len_a-i], table_b[len_b-i], margin) then + commonBackTo = i - 1 + break + end + end + + + table.insert( result, 'List difference analysis:' ) + if len_a == len_b then + -- TODO: handle expected/actual naming + extendWithStrFmt( result, '* lists %sA (%s) and %sB (%s) have the same size', refa, descrTa, refb, descrTb ) + else + extendWithStrFmt( result, '* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items', refa, descrTa, len_a, refb, descrTb, len_b ) + end + + extendWithStrFmt( result, '* lists A and B start differing at index %d', commonUntil+1 ) + if commonBackTo >= 0 then + if deltalv > 0 then + extendWithStrFmt( result, '* lists A and B are equal again from index %d for A, %d for B', len_a-commonBackTo, len_b-commonBackTo ) + else + extendWithStrFmt( result, '* lists A and B are equal again from index %d', len_a-commonBackTo ) + end + end + + local function insertABValue(ai, bi) + bi = bi or ai + if M.private.is_table_equals( table_a[ai], table_b[bi], margin) then + return extendWithStrFmt( result, ' = A[%d], B[%d]: %s', ai, bi, prettystr(table_a[ai]) ) + else + extendWithStrFmt( result, ' - A[%d]: %s', ai, prettystr(table_a[ai])) + extendWithStrFmt( result, ' + B[%d]: %s', bi, prettystr(table_b[bi])) + end + end + + -- common parts to list A & B, at the beginning + if commonUntil > 0 then + table.insert( result, '* Common parts:' ) + for i = 1, commonUntil do + insertABValue( i ) + end + end + + -- diffing parts to list A & B + if commonUntil < shortest - commonBackTo - 1 then + table.insert( result, '* Differing parts:' ) + for i = commonUntil + 1, shortest - commonBackTo - 1 do + insertABValue( i ) + end + end + + -- display indexes of one list, with no match on other list + if shortest - commonBackTo <= longest - commonBackTo - 1 then + table.insert( result, '* Present only in one list:' ) + for i = shortest - commonBackTo, longest - commonBackTo - 1 do + if len_a > len_b then + extendWithStrFmt( result, ' - A[%d]: %s', i, prettystr(table_a[i]) ) + -- table.insert( result, '+ (no matching B index)') + else + -- table.insert( result, '- no matching A index') + extendWithStrFmt( result, ' + B[%d]: %s', i, prettystr(table_b[i]) ) + end + end + end + + -- common parts to list A & B, at the end + if commonBackTo >= 0 then + table.insert( result, '* Common parts at the end of the lists' ) + for i = longest - commonBackTo, longest do + if len_a > len_b then + insertABValue( i, i-deltalv ) + else + insertABValue( i-deltalv, i ) + end + end + end + + return true, table.concat( result, '\n') +end +M.private.mismatchFormattingPureList = mismatchFormattingPureList + +local function prettystrPairs(value1, value2, suffix_a, suffix_b) + --[[ + This function helps with the recurring task of constructing the "expected + vs. actual" error messages. It takes two arbitrary values and formats + corresponding strings with prettystr(). + + To keep the (possibly complex) output more readable in case the resulting + strings contain line breaks, they get automatically prefixed with additional + newlines. Both suffixes are optional (default to empty strings), and get + appended to the "value1" string. "suffix_a" is used if line breaks were + encountered, "suffix_b" otherwise. + + Returns the two formatted strings (including padding/newlines). + ]] + local str1, str2 = prettystr(value1), prettystr(value2) + if hasNewLine(str1) or hasNewLine(str2) then + -- line break(s) detected, add padding + return "\n" .. str1 .. (suffix_a or ""), "\n" .. str2 + end + return str1 .. (suffix_b or ""), str2 +end +M.private.prettystrPairs = prettystrPairs + +local UNKNOWN_REF = 'table 00-unknown ref' +local ref_generator = { value=1, [UNKNOWN_REF]=0 } + +local function table_ref( t ) + -- return the default tostring() for tables, with the table ID, even if the table has a metatable + -- with the __tostring converter + local ref = '' + local mt = getmetatable( t ) + if mt == nil then + ref = tostring(t) + else + local success, result + success, result = pcall(setmetatable, t, nil) + if not success then + -- protected table, if __tostring is defined, we can + -- not get the reference. And we can not know in advance. + ref = tostring(t) + if not ref:match( 'table: 0?x?[%x]+' ) then + return UNKNOWN_REF + end + else + ref = tostring(t) + setmetatable( t, mt ) + end + end + -- strip the "table: " part + ref = ref:sub(8) + if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then + -- Create a new reference number + ref_generator[ref] = ref_generator.value + ref_generator.value = ref_generator.value+1 + end + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + return string.format('table %02d-%s', ref_generator[ref], ref) + else + return string.format('table %02d', ref_generator[ref]) + end +end +M.private.table_ref = table_ref + +local TABLE_TOSTRING_SEP = ", " +local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP) + +local function _table_tostring( tbl, indentLevel, printTableRefs, cycleDetectTable ) + printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG + cycleDetectTable = cycleDetectTable or {} + cycleDetectTable[tbl] = true + + local result, dispOnMultLines = {}, false + + -- like prettystr but do not enclose with "" if the string is just alphanumerical + -- this is better for displaying table keys who are often simple strings + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr_sub(k, indentLevel+1, printTableRefs, cycleDetectTable) + end + + local mt = getmetatable( tbl ) + + if mt and mt.__tostring then + -- if table has a __tostring() function in its metatable, use it to display the table + -- else, compute a regular table + result = tostring(tbl) + if type(result) ~= 'string' then + return string.format( '', prettystr(result) ) + end + result = strsplit( '\n', result ) + return M.private._table_tostring_format_multiline_string( result, indentLevel ) + + else + -- no metatable, compute the table representation + + local entry, count, seq_index = nil, 0, 1 + for k, v in sortedPairs( tbl ) do + + -- key part + if k == seq_index then + -- for the sequential part of tables, we'll skip the "=" output + entry = '' + seq_index = seq_index + 1 + elseif cycleDetectTable[k] then + -- recursion in the key detected + cycleDetectTable.detected = true + entry = "<"..table_ref(k)..">=" + else + entry = keytostring(k) .. "=" + end + + -- value part + if cycleDetectTable[v] then + -- recursion in the value detected! + cycleDetectTable.detected = true + entry = entry .. "<"..table_ref(v)..">" + else + entry = entry .. + prettystr_sub( v, indentLevel+1, printTableRefs, cycleDetectTable ) + end + count = count + 1 + result[count] = entry + end + return M.private._table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) + end + +end +M.private._table_tostring = _table_tostring -- prettystr_sub() needs it + +local function _table_tostring_format_multiline_string( tbl_str, indentLevel ) + local indentString = '\n'..string.rep(" ", indentLevel - 1) + return table.concat( tbl_str, indentString ) + +end +M.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string + + +local function _table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) + -- final function called in _table_to_string() to format the resulting list of + -- string describing the table. + + local dispOnMultLines = false + + -- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values + local totalLength = 0 + for k, v in ipairs( result ) do + totalLength = totalLength + string.len( v ) + if totalLength >= M.LINE_LENGTH then + dispOnMultLines = true + break + end + end + + -- set dispOnMultLines to true if the max LINE_LENGTH would be exceeded + -- with the values and the separators. + if not dispOnMultLines then + -- adjust with length of separator(s): + -- two items need 1 sep, three items two seps, ... plus len of '{}' + if #result > 0 then + totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (#result - 1) + end + dispOnMultLines = (totalLength + 2 >= M.LINE_LENGTH) + end + + -- now reformat the result table (currently holding element strings) + if dispOnMultLines then + local indentString = string.rep(" ", indentLevel - 1) + result = { + "{\n ", + indentString, + table.concat(result, ",\n " .. indentString), + "\n", + indentString, + "}" + } + else + result = {"{", table.concat(result, TABLE_TOSTRING_SEP), "}"} + end + if printTableRefs then + table.insert(result, 1, "<"..table_ref(tbl).."> ") -- prepend table ref + end + return table.concat(result) +end +M.private._table_tostring_format_result = _table_tostring_format_result -- prettystr_sub() needs it + +local function table_findkeyof(t, element) + -- Return the key k of the given element in table t, so that t[k] == element + -- (or `nil` if element is not present within t). Note that we use our + -- 'general' is_equal comparison for matching, so this function should + -- handle table-type elements gracefully and consistently. + if type(t) == "table" then + for k, v in pairs(t) do + if M.private.is_table_equals(v, element) then + return k + end + end + end + return nil +end + +local function _is_table_items_equals(actual, expected ) + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false + + elseif (type_a == 'table') --[[and (type_e == 'table')]] then + for k, v in pairs(actual) do + if table_findkeyof(expected, v) == nil then + return false -- v not contained in expected + end + end + for k, v in pairs(expected) do + if table_findkeyof(actual, v) == nil then + return false -- v not contained in actual + end + end + return true + + elseif actual ~= expected then + return false + end + + return true +end + +--[[ +This is a specialized metatable to help with the bookkeeping of recursions +in _is_table_equals(). It provides an __index table that implements utility +functions for easier management of the table. The "cached" method queries +the state of a specific (actual,expected) pair; and the "store" method sets +this state to the given value. The state of pairs not "seen" / visited is +assumed to be `nil`. +]] +local _recursion_cache_MT = { + __index = { + -- Return the cached value for an (actual,expected) pair (or `nil`) + cached = function(t, actual, expected) + local subtable = t[actual] or {} + return subtable[expected] + end, + + -- Store cached value for a specific (actual,expected) pair. + -- Returns the value, so it's easy to use for a "tailcall" (return ...). + store = function(t, actual, expected, value, asymmetric) + local subtable = t[actual] + if not subtable then + subtable = {} + t[actual] = subtable + end + subtable[expected] = value + + -- Unless explicitly marked "asymmetric": Consider the recursion + -- on (expected,actual) to be equivalent to (actual,expected) by + -- default, and thus cache the value for both. + if not asymmetric then + t:store(expected, actual, value, true) + end + + return value + end + } +} + +local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlmostEqual) + --[[Returns true if both table are equal. + + If argument marginForAlmostEqual is suppied, number comparison is done using alomstEqual instead + of strict equality. + + cycleDetectTable is an internal argument used during recursion on tables. + ]] + --print('_is_table_equals( \n '..prettystr(actual)..'\n , '..prettystr(expected).. + -- '\n , '..prettystr(cycleDetectTable)..'\n , '..prettystr(marginForAlmostEqual)..' )') + + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false -- different types won't match + end + + if type_a == 'number' then + if marginForAlmostEqual ~= nil then + return M.almostEquals(actual, expected, marginForAlmostEqual) + else + return actual == expected + end + elseif type_a ~= 'table' then + -- other types compare directly + return actual == expected + end + + cycleDetectTable = cycleDetectTable or { actual={}, expected={} } + if cycleDetectTable.actual[ actual ] then + -- oh, we hit a cycle in actual + if cycleDetectTable.expected[ expected ] then + -- uh, we hit a cycle at the same time in expected + -- so the two tables have similar structure + return true + end + + -- cycle was hit only in actual, the structure differs from expected + return false + end + + if cycleDetectTable.expected[ expected ] then + -- no cycle in actual, but cycle in expected + -- the structure differ + return false + end + + -- at this point, no table cycle detected, we are + -- seeing this table for the first time + + -- mark the cycle detection + cycleDetectTable.actual[ actual ] = true + cycleDetectTable.expected[ expected ] = true + + + local actualKeysMatched = {} + for k, v in pairs(actual) do + actualKeysMatched[k] = true -- Keep track of matched keys + if not _is_table_equals(v, expected[k], cycleDetectTable, marginForAlmostEqual) then + -- table differs on this key + -- clear the cycle detection before returning + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return false + end + end + + for k, v in pairs(expected) do + if not actualKeysMatched[k] then + -- Found a key that we did not see in "actual" -> mismatch + -- clear the cycle detection before returning + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return false + end + -- Otherwise actual[k] was already matched against v = expected[k]. + end + + -- all key match, we have a match ! + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return true +end +M.private._is_table_equals = _is_table_equals + +local function failure(main_msg, extra_msg_or_nil, level) + -- raise an error indicating a test failure + -- for error() compatibility we adjust "level" here (by +1), to report the + -- calling context + local msg + if type(extra_msg_or_nil) == 'string' and extra_msg_or_nil:len() > 0 then + msg = extra_msg_or_nil .. '\n' .. main_msg + else + msg = main_msg + end + error(M.FAILURE_PREFIX .. msg, (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE) +end + +local function is_table_equals(actual, expected, marginForAlmostEqual) + return _is_table_equals(actual, expected, nil, marginForAlmostEqual) +end +M.private.is_table_equals = is_table_equals + +local function fail_fmt(level, extra_msg_or_nil, ...) + -- failure with printf-style formatted message and given error level + failure(string.format(...), extra_msg_or_nil, (level or 1) + 1) +end +M.private.fail_fmt = fail_fmt + +local function error_fmt(level, ...) + -- printf-style error() + error(string.format(...), (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE) +end +M.private.error_fmt = error_fmt + +---------------------------------------------------------------- +-- +-- assertions +-- +---------------------------------------------------------------- + +local function errorMsgEquality(actual, expected, doDeepAnalysis, margin) + -- margin is supplied only for almost equal verification + + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + if type(expected) == 'string' or type(expected) == 'table' then + local strExpected, strActual = prettystrPairs(expected, actual) + local result = string.format("expected: %s\nactual: %s", strExpected, strActual) + if margin then + result = result .. '\nwere not equal by the margin of: '..prettystr(margin) + end + + -- extend with mismatch analysis if possible: + local success, mismatchResult + success, mismatchResult = tryMismatchFormatting( actual, expected, doDeepAnalysis, margin ) + if success then + result = table.concat( { result, mismatchResult }, '\n' ) + end + return result + end + return string.format("expected: %s, actual: %s", + prettystr(expected), prettystr(actual)) +end + +function M.assertError(f, ...) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + if pcall( f, ... ) then + failure( "Expected an error when calling function but no error generated", nil, 2 ) + end +end + +function M.fail( msg ) + -- stops a test due to a failure + failure( msg, nil, 2 ) +end + +function M.failIf( cond, msg ) + -- Fails a test with "msg" if condition is true + if cond then + failure( msg, nil, 2 ) + end +end + +function M.skip(msg) + -- skip a running test + error_fmt(2, M.SKIP_PREFIX .. msg) +end + +function M.skipIf( cond, msg ) + -- skip a running test if condition is met + if cond then + error_fmt(2, M.SKIP_PREFIX .. msg) + end +end + +function M.runOnlyIf( cond, msg ) + -- continue a running test if condition is met, else skip it + if not cond then + error_fmt(2, M.SKIP_PREFIX .. prettystr(msg)) + end +end + +function M.success() + -- stops a test with a success + error_fmt(2, M.SUCCESS_PREFIX) +end + +function M.successIf( cond ) + -- stops a test with a success if condition is met + if cond then + error_fmt(2, M.SUCCESS_PREFIX) + end +end + + +------------------------------------------------------------------ +-- Equality assertions +------------------------------------------------------------------ + +function M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis) + if type(actual) == 'table' and type(expected) == 'table' then + if not is_table_equals(actual, expected) then + failure( errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2 ) + end + elseif type(actual) ~= type(expected) then + failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + elseif actual ~= expected then + failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + end +end + +function M.almostEquals( actual, expected, margin ) + if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then + error_fmt(3, 'almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s', + prettystr(actual), prettystr(expected), prettystr(margin)) + end + if margin < 0 then + error_fmt(3, 'almostEquals: margin must not be negative, current value is ' .. margin) + end + return math.abs(expected - actual) <= margin +end + +function M.assertAlmostEquals( actual, expected, margin, extra_msg_or_nil ) + -- check that two floats are close by margin + margin = margin or M.EPS + if type(margin) ~= 'number' then + error_fmt(2, 'almostEquals: margin must be a number, not %s', prettystr(margin)) + end + + if type(actual) == 'table' and type(expected) == 'table' then + -- handle almost equals for table + if not is_table_equals(actual, expected, margin) then + failure( errorMsgEquality(actual, expected, nil, margin), extra_msg_or_nil, 2 ) + end + elseif type(actual) == 'number' and type(expected) == 'number' and type(margin) == 'number' then + if not M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt(2, extra_msg_or_nil, 'Values are not almost equal\n' .. + 'Actual: %s, expected: %s, delta %s above margin of %s', + actual, expected, delta, margin) + end + else + error_fmt(3, 'almostEquals: must supply only number or table arguments.\nArguments supplied: %s, %s, %s', + prettystr(actual), prettystr(expected), prettystr(margin)) + end +end + +function M.assertNotEquals(actual, expected, extra_msg_or_nil) + if type(actual) ~= type(expected) then + return + end + + if type(actual) == 'table' and type(expected) == 'table' then + if not is_table_equals(actual, expected) then + return + end + elseif actual ~= expected then + return + end + fail_fmt(2, extra_msg_or_nil, 'Received the not expected value: %s', prettystr(actual)) +end + +function M.assertNotAlmostEquals( actual, expected, margin, extra_msg_or_nil ) + -- check that two floats are not close by margin + margin = margin or M.EPS + if M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt(2, extra_msg_or_nil, 'Values are almost equal\nActual: %s, expected: %s' .. + ', delta %s below margin of %s', + actual, expected, delta, margin) + end +end + +function M.assertItemsEquals(actual, expected, extra_msg_or_nil) + -- checks that the items of table expected + -- are contained in table actual. Warning, this function + -- is at least O(n^2) + if not _is_table_items_equals(actual, expected ) then + expected, actual = prettystrPairs(expected, actual) + fail_fmt(2, extra_msg_or_nil, 'Content of the tables are not identical:\nExpected: %s\nActual: %s', + expected, actual) + end +end + +------------------------------------------------------------------ +-- String assertion +------------------------------------------------------------------ + +function M.assertStrContains( str, sub, isPattern, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + -- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) ) + -- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) ) + if not string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not find %s %s in string %s', + isPattern and 'pattern' or 'substring', sub, str) + end +end + +function M.assertStrIContains( str, sub, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if not string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not find (case insensitively) substring %s in string %s', + sub, str) + end +end + +function M.assertNotStrContains( str, sub, isPattern, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Found the not expected %s %s in string %s', + isPattern and 'pattern' or 'substring', sub, str) + end +end + +function M.assertNotStrIContains( str, sub, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Found (case insensitively) the not expected substring %s in string %s', + sub, str) + end +end + +function M.assertStrMatches( str, pattern, start, final, extra_msg_or_nil ) + -- Verify a full match for the string + if not strMatch( str, pattern, start, final ) then + pattern, str = prettystrPairs(pattern, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not match pattern %s with string %s', + pattern, str) + end +end + +local function _assertErrorMsgEquals( stripFileAndLine, expectedMsg, func, ... ) + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error: '..M.prettystr(expectedMsg), nil, 3 ) + end + if type(expectedMsg) == "string" and type(error_msg) ~= "string" then + -- table are converted to string automatically + error_msg = tostring(error_msg) + end + local differ = false + if stripFileAndLine then + if error_msg:gsub("^.+:%d+: ", "") ~= expectedMsg then + differ = true + end + else + if error_msg ~= expectedMsg then + local tr = type(error_msg) + local te = type(expectedMsg) + if te == 'table' then + if tr ~= 'table' then + differ = true + else + local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg) + if not ok then + differ = true + end + end + else + differ = true + end + end + end + + if differ then + error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg) + fail_fmt(3, nil, 'Error message expected: %s\nError message received: %s\n', + expectedMsg, error_msg) + end +end + +function M.assertErrorMsgEquals( expectedMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + _assertErrorMsgEquals(false, expectedMsg, func, ...) +end + +function M.assertErrorMsgContentEquals(expectedMsg, func, ...) + _assertErrorMsgEquals(true, expectedMsg, func, ...) +end + +function M.assertErrorMsgContains( partialMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error containing: '..prettystr(partialMsg), nil, 2 ) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not string.find( error_msg, partialMsg, nil, true ) then + error_msg, partialMsg = prettystrPairs(error_msg, partialMsg) + fail_fmt(2, nil, 'Error message does not contain: %s\nError message received: %s\n', + partialMsg, error_msg) + end +end + +function M.assertErrorMsgMatches( expectedMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error matching: "'..expectedMsg..'"', nil, 2 ) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not strMatch( error_msg, expectedMsg ) then + expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg) + fail_fmt(2, nil, 'Error message does not match pattern: %s\nError message received: %s\n', + expectedMsg, error_msg) + end +end + +------------------------------------------------------------------ +-- Type assertions +------------------------------------------------------------------ + +function M.assertEvalToTrue(value, extra_msg_or_nil) + if not value then + failure("expected: a value evaluating to true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertEvalToFalse(value, extra_msg_or_nil) + if value then + failure("expected: false or nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsTrue(value, extra_msg_or_nil) + if value ~= true then + failure("expected: true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsTrue(value, extra_msg_or_nil) + if value == true then + failure("expected: not true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsFalse(value, extra_msg_or_nil) + if value ~= false then + failure("expected: false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsFalse(value, extra_msg_or_nil) + if value == false then + failure("expected: not false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsNil(value, extra_msg_or_nil) + if value ~= nil then + failure("expected: nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNil(value, extra_msg_or_nil) + if value == nil then + failure("expected: not nil, actual: nil", extra_msg_or_nil, 2) + end +end + +--[[ +Add type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type matches the +expected string (derived from the function name): + +M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx" +]] +for _, funcName in ipairs( + {'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean', + 'assertIsFunction', 'assertIsUserdata', 'assertIsThread'} +) do + local typeExpected = funcName:match("^assertIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeExpected = typeExpected and typeExpected:lower() + or error("bad function name '"..funcName.."' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) ~= typeExpected then + if type(value) == 'nil' then + fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: nil', + typeExpected, type(value), prettystrPairs(value)) + else + fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: type %s, value %s', + typeExpected, type(value), prettystrPairs(value)) + end + end + end +end + +--[[ +Add shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility) +M.isXxx(value) -> returns true if type(value) conforms to "xxx" +]] +for _, typeExpected in ipairs( + {'Number', 'String', 'Table', 'Boolean', + 'Function', 'Userdata', 'Thread', 'Nil' } +) do + local typeExpectedLower = typeExpected:lower() + local isType = function(value) + return (type(value) == typeExpectedLower) + end + M['is'..typeExpected] = isType + M['is_'..typeExpectedLower] = isType +end + +--[[ +Add non-type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type differs from the +expected string (derived from the function name): + +M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx" +]] +for _, funcName in ipairs( + {'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean', + 'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread'} +) do + local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeUnexpected = typeUnexpected and typeUnexpected:lower() + or error("bad function name '"..funcName.."' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) == typeUnexpected then + fail_fmt(2, extra_msg_or_nil, 'expected: not a %s type, actual: value %s', + typeUnexpected, prettystrPairs(value)) + end + end +end + +function M.assertIs(actual, expected, extra_msg_or_nil) + if actual ~= expected then + if not M.ORDER_ACTUAL_EXPECTED then + actual, expected = expected, actual + end + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + expected, actual = prettystrPairs(expected, actual, '\n', '') + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt(2, extra_msg_or_nil, 'expected and actual object should not be different\nExpected: %s\nReceived: %s', + expected, actual) + end +end + +function M.assertNotIs(actual, expected, extra_msg_or_nil) + if actual == expected then + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + local s_expected + if not M.ORDER_ACTUAL_EXPECTED then + s_expected = prettystrPairs(actual) + else + s_expected = prettystrPairs(expected) + end + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt(2, extra_msg_or_nil, 'expected and actual object should be different: %s', s_expected ) + end +end + + +------------------------------------------------------------------ +-- Scientific assertions +------------------------------------------------------------------ + + +function M.assertIsNaN(value, extra_msg_or_nil) + if type(value) ~= "number" or value == value then + failure("expected: NaN, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNaN(value, extra_msg_or_nil) + if type(value) == "number" and value ~= value then + failure("expected: not NaN, actual: NaN", extra_msg_or_nil, 2) + end +end + +function M.assertIsInf(value, extra_msg_or_nil) + if type(value) ~= "number" or math.abs(value) ~= math.huge then + failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= math.huge then + failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsMinusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= -math.huge then + failure("expected: -#Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsPlusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == math.huge then + failure("expected: not #Inf, actual: #Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == -math.huge then + failure("expected: not -#Inf, actual: -#Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsInf(value, extra_msg_or_nil) + if type(value) == "number" and math.abs(value) == math.huge then + failure("expected: not infinity, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusZero(value, extra_msg_or_nil) + if type(value) ~= 'number' or value ~= 0 then + failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + else if (1/value == -math.huge) then + -- more precise error diagnosis + failure("expected: +0.0, actual: -0.0", extra_msg_or_nil, 2) + else if (1/value ~= math.huge) then + -- strange, case should have already been covered + failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertIsMinusZero(value, extra_msg_or_nil) + if type(value) ~= 'number' or value ~= 0 then + failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + else if (1/value == math.huge) then + -- more precise error diagnosis + failure("expected: -0.0, actual: +0.0", extra_msg_or_nil, 2) + else if (1/value ~= -math.huge) then + -- strange, case should have already been covered + failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertNotIsPlusZero(value, extra_msg_or_nil) + if type(value) == 'number' and (1/value == math.huge) then + failure("expected: not +0.0, actual: +0.0", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusZero(value, extra_msg_or_nil) + if type(value) == 'number' and (1/value == -math.huge) then + failure("expected: not -0.0, actual: -0.0", extra_msg_or_nil, 2) + end +end + +function M.assertTableContains(t, expected, extra_msg_or_nil) + -- checks that table t contains the expected element + if table_findkeyof(t, expected) == nil then + t, expected = prettystrPairs(t, expected) + fail_fmt(2, extra_msg_or_nil, 'Table %s does NOT contain the expected element %s', + t, expected) + end +end + +function M.assertNotTableContains(t, expected, extra_msg_or_nil) + -- checks that table t doesn't contain the expected element + local k = table_findkeyof(t, expected) + if k ~= nil then + t, expected = prettystrPairs(t, expected) + fail_fmt(2, extra_msg_or_nil, 'Table %s DOES contain the unwanted element %s (at key %s)', + t, expected, prettystr(k)) + end +end + +---------------------------------------------------------------- +-- Compatibility layer +---------------------------------------------------------------- + +-- for compatibility with LuaUnit v2.x +function M.wrapFunctions() + -- In LuaUnit version <= 2.1 , this function was necessary to include + -- a test function inside the global test suite. Nowadays, the functions + -- are simply run directly as part of the test discovery process. + -- so just do nothing ! + io.stderr:write[[Use of WrapFunctions() is no longer needed. +Just prefix your test function names with "test" or "Test" and they +will be picked up and run by LuaUnit. +]] +end + +local list_of_funcs = { + -- { official function name , alias } + + -- general assertions + { 'assertEquals' , 'assert_equals' }, + { 'assertItemsEquals' , 'assert_items_equals' }, + { 'assertNotEquals' , 'assert_not_equals' }, + { 'assertAlmostEquals' , 'assert_almost_equals' }, + { 'assertNotAlmostEquals' , 'assert_not_almost_equals' }, + { 'assertEvalToTrue' , 'assert_eval_to_true' }, + { 'assertEvalToFalse' , 'assert_eval_to_false' }, + { 'assertStrContains' , 'assert_str_contains' }, + { 'assertStrIContains' , 'assert_str_icontains' }, + { 'assertNotStrContains' , 'assert_not_str_contains' }, + { 'assertNotStrIContains' , 'assert_not_str_icontains' }, + { 'assertStrMatches' , 'assert_str_matches' }, + { 'assertError' , 'assert_error' }, + { 'assertErrorMsgEquals' , 'assert_error_msg_equals' }, + { 'assertErrorMsgContains' , 'assert_error_msg_contains' }, + { 'assertErrorMsgMatches' , 'assert_error_msg_matches' }, + { 'assertErrorMsgContentEquals', 'assert_error_msg_content_equals' }, + { 'assertIs' , 'assert_is' }, + { 'assertNotIs' , 'assert_not_is' }, + { 'assertTableContains' , 'assert_table_contains' }, + { 'assertNotTableContains' , 'assert_not_table_contains' }, + { 'wrapFunctions' , 'WrapFunctions' }, + { 'wrapFunctions' , 'wrap_functions' }, + + -- type assertions: assertIsXXX -> assert_is_xxx + { 'assertIsNumber' , 'assert_is_number' }, + { 'assertIsString' , 'assert_is_string' }, + { 'assertIsTable' , 'assert_is_table' }, + { 'assertIsBoolean' , 'assert_is_boolean' }, + { 'assertIsNil' , 'assert_is_nil' }, + { 'assertIsTrue' , 'assert_is_true' }, + { 'assertIsFalse' , 'assert_is_false' }, + { 'assertIsNaN' , 'assert_is_nan' }, + { 'assertIsInf' , 'assert_is_inf' }, + { 'assertIsPlusInf' , 'assert_is_plus_inf' }, + { 'assertIsMinusInf' , 'assert_is_minus_inf' }, + { 'assertIsPlusZero' , 'assert_is_plus_zero' }, + { 'assertIsMinusZero' , 'assert_is_minus_zero' }, + { 'assertIsFunction' , 'assert_is_function' }, + { 'assertIsThread' , 'assert_is_thread' }, + { 'assertIsUserdata' , 'assert_is_userdata' }, + + -- type assertions: assertIsXXX -> assertXxx + { 'assertIsNumber' , 'assertNumber' }, + { 'assertIsString' , 'assertString' }, + { 'assertIsTable' , 'assertTable' }, + { 'assertIsBoolean' , 'assertBoolean' }, + { 'assertIsNil' , 'assertNil' }, + { 'assertIsTrue' , 'assertTrue' }, + { 'assertIsFalse' , 'assertFalse' }, + { 'assertIsNaN' , 'assertNaN' }, + { 'assertIsInf' , 'assertInf' }, + { 'assertIsPlusInf' , 'assertPlusInf' }, + { 'assertIsMinusInf' , 'assertMinusInf' }, + { 'assertIsPlusZero' , 'assertPlusZero' }, + { 'assertIsMinusZero' , 'assertMinusZero'}, + { 'assertIsFunction' , 'assertFunction' }, + { 'assertIsThread' , 'assertThread' }, + { 'assertIsUserdata' , 'assertUserdata' }, + + -- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat) + { 'assertIsNumber' , 'assert_number' }, + { 'assertIsString' , 'assert_string' }, + { 'assertIsTable' , 'assert_table' }, + { 'assertIsBoolean' , 'assert_boolean' }, + { 'assertIsNil' , 'assert_nil' }, + { 'assertIsTrue' , 'assert_true' }, + { 'assertIsFalse' , 'assert_false' }, + { 'assertIsNaN' , 'assert_nan' }, + { 'assertIsInf' , 'assert_inf' }, + { 'assertIsPlusInf' , 'assert_plus_inf' }, + { 'assertIsMinusInf' , 'assert_minus_inf' }, + { 'assertIsPlusZero' , 'assert_plus_zero' }, + { 'assertIsMinusZero' , 'assert_minus_zero' }, + { 'assertIsFunction' , 'assert_function' }, + { 'assertIsThread' , 'assert_thread' }, + { 'assertIsUserdata' , 'assert_userdata' }, + + -- type assertions: assertNotIsXXX -> assert_not_is_xxx + { 'assertNotIsNumber' , 'assert_not_is_number' }, + { 'assertNotIsString' , 'assert_not_is_string' }, + { 'assertNotIsTable' , 'assert_not_is_table' }, + { 'assertNotIsBoolean' , 'assert_not_is_boolean' }, + { 'assertNotIsNil' , 'assert_not_is_nil' }, + { 'assertNotIsTrue' , 'assert_not_is_true' }, + { 'assertNotIsFalse' , 'assert_not_is_false' }, + { 'assertNotIsNaN' , 'assert_not_is_nan' }, + { 'assertNotIsInf' , 'assert_not_is_inf' }, + { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, + { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, + { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, + { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, + { 'assertNotIsFunction' , 'assert_not_is_function' }, + { 'assertNotIsThread' , 'assert_not_is_thread' }, + { 'assertNotIsUserdata' , 'assert_not_is_userdata' }, + + -- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat) + { 'assertNotIsNumber' , 'assertNotNumber' }, + { 'assertNotIsString' , 'assertNotString' }, + { 'assertNotIsTable' , 'assertNotTable' }, + { 'assertNotIsBoolean' , 'assertNotBoolean' }, + { 'assertNotIsNil' , 'assertNotNil' }, + { 'assertNotIsTrue' , 'assertNotTrue' }, + { 'assertNotIsFalse' , 'assertNotFalse' }, + { 'assertNotIsNaN' , 'assertNotNaN' }, + { 'assertNotIsInf' , 'assertNotInf' }, + { 'assertNotIsPlusInf' , 'assertNotPlusInf' }, + { 'assertNotIsMinusInf' , 'assertNotMinusInf' }, + { 'assertNotIsPlusZero' , 'assertNotPlusZero' }, + { 'assertNotIsMinusZero' , 'assertNotMinusZero' }, + { 'assertNotIsFunction' , 'assertNotFunction' }, + { 'assertNotIsThread' , 'assertNotThread' }, + { 'assertNotIsUserdata' , 'assertNotUserdata' }, + + -- type assertions: assertNotIsXXX -> assert_not_xxx + { 'assertNotIsNumber' , 'assert_not_number' }, + { 'assertNotIsString' , 'assert_not_string' }, + { 'assertNotIsTable' , 'assert_not_table' }, + { 'assertNotIsBoolean' , 'assert_not_boolean' }, + { 'assertNotIsNil' , 'assert_not_nil' }, + { 'assertNotIsTrue' , 'assert_not_true' }, + { 'assertNotIsFalse' , 'assert_not_false' }, + { 'assertNotIsNaN' , 'assert_not_nan' }, + { 'assertNotIsInf' , 'assert_not_inf' }, + { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, + { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, + { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, + { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, + { 'assertNotIsFunction' , 'assert_not_function' }, + { 'assertNotIsThread' , 'assert_not_thread' }, + { 'assertNotIsUserdata' , 'assert_not_userdata' }, + + -- all assertions with Coroutine duplicate Thread assertions + { 'assertIsThread' , 'assertIsCoroutine' }, + { 'assertIsThread' , 'assertCoroutine' }, + { 'assertIsThread' , 'assert_is_coroutine' }, + { 'assertIsThread' , 'assert_coroutine' }, + { 'assertNotIsThread' , 'assertNotIsCoroutine' }, + { 'assertNotIsThread' , 'assertNotCoroutine' }, + { 'assertNotIsThread' , 'assert_not_is_coroutine' }, + { 'assertNotIsThread' , 'assert_not_coroutine' }, +} + +-- Create all aliases in M +for _,v in ipairs( list_of_funcs ) do + local funcname, alias = v[1], v[2] + M[alias] = M[funcname] + + if EXPORT_ASSERT_TO_GLOBALS then + _G[funcname] = M[funcname] + _G[alias] = M[funcname] + end +end + +---------------------------------------------------------------- +-- +-- Outputters +-- +---------------------------------------------------------------- + +-- A common "base" class for outputters +-- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html + +local genericOutput = { __class__ = 'genericOutput' } -- class +local genericOutput_MT = { __index = genericOutput } -- metatable +M.genericOutput = genericOutput -- publish, so that custom classes may derive from it + +function genericOutput.new(runner, default_verbosity) + -- runner is the "parent" object controlling the output, usually a LuaUnit instance + local t = { runner = runner } + if runner then + t.result = runner.result + t.verbosity = runner.verbosity or default_verbosity + t.fname = runner.fname + else + t.verbosity = default_verbosity + end + return setmetatable( t, genericOutput_MT) +end + +-- abstract ("empty") methods +function genericOutput:startSuite() + -- Called once, when the suite is started +end + +function genericOutput:startClass(className) + -- Called each time a new test class is started +end + +function genericOutput:startTest(testName) + -- called each time a new test is started, right before the setUp() + -- the current test status node is already created and available in: self.result.currentNode +end + +function genericOutput:updateStatus(node) + -- called with status failed or error as soon as the error/failure is encountered + -- this method is NOT called for a successful test because a test is marked as successful by default + -- and does not need to be updated +end + +function genericOutput:endTest(node) + -- called when the test is finished, after the tearDown() method +end + +function genericOutput:endClass() + -- called when executing the class is finished, before moving on to the next class of at the end of the test execution +end + +function genericOutput:endSuite() + -- called at the end of the test suite execution +end + + +---------------------------------------------------------------- +-- class TapOutput +---------------------------------------------------------------- + +local TapOutput = genericOutput.new() -- derived class +local TapOutput_MT = { __index = TapOutput } -- metatable +TapOutput.__class__ = 'TapOutput' + + -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html + + function TapOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + return setmetatable( t, TapOutput_MT) + end + function TapOutput:startSuite() + print("1.."..self.result.selectedCount) + print('# Started on '..self.result.startDate) + end + function TapOutput:startClass(className) + if className ~= '[TestFunctions]' then + print('# Starting class: '..className) + end + end + + function TapOutput:updateStatus( node ) + if node:isSkipped() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t# SKIP ", node.msg, "\n" ) + return + end + + io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + if self.verbosity > M.VERBOSITY_LOW then + print( prefixString( '# ', node.msg ) ) + end + if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then + print( prefixString( '# ', node.stackTrace ) ) + end + end + + function TapOutput:endTest( node ) + if node:isSuccess() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + end + end + + function TapOutput:endSuite() + print( '# '..M.LuaUnit.statusLine( self.result ) ) + return self.result.notSuccessCount + end + + +-- class TapOutput end + +---------------------------------------------------------------- +-- class JUnitOutput +---------------------------------------------------------------- + +-- See directory junitxml for more information about the junit format +local JUnitOutput = genericOutput.new() -- derived class +local JUnitOutput_MT = { __index = JUnitOutput } -- metatable +JUnitOutput.__class__ = 'JUnitOutput' + + function JUnitOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + t.testList = {} + return setmetatable( t, JUnitOutput_MT ) + end + + function JUnitOutput:startSuite() + -- open xml file early to deal with errors + if self.fname == nil then + error('With Junit, an output filename must be supplied with --name!') + end + if string.sub(self.fname,-4) ~= '.xml' then + self.fname = self.fname..'.xml' + end + self.fd = io.open(self.fname, "w") + if self.fd == nil then + error("Could not open file for writing: "..self.fname) + end + + print('# XML output to '..self.fname) + print('# Started on '..self.result.startDate) + end + function JUnitOutput:startClass(className) + if className ~= '[TestFunctions]' then + print('# Starting class: '..className) + end + end + function JUnitOutput:startTest(testName) + print('# Starting test: '..testName) + end + + function JUnitOutput:updateStatus( node ) + if node:isFailure() then + print( '# Failure: ' .. prefixString( '# ', node.msg ):sub(4, nil) ) + -- print('# ' .. node.stackTrace) + elseif node:isError() then + print( '# Error: ' .. prefixString( '# ' , node.msg ):sub(4, nil) ) + -- print('# ' .. node.stackTrace) + end + end + + function JUnitOutput:endSuite() + print( '# '..M.LuaUnit.statusLine(self.result)) + + -- XML file writing + self.fd:write('\n') + self.fd:write('\n') + self.fd:write(string.format( + ' \n', + self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount, self.result.skippedCount )) + self.fd:write(" \n") + self.fd:write(string.format(' \n', _VERSION ) ) + self.fd:write(string.format(' \n', M.VERSION) ) + -- XXX please include system name and version if possible + self.fd:write(" \n") + + for i,node in ipairs(self.result.allTests) do + self.fd:write(string.format(' \n', + node.className, node.testName, node.duration ) ) + if node:isNotSuccess() then + self.fd:write(node:statusXML()) + end + self.fd:write(' \n') + end + + -- Next two lines are needed to validate junit ANT xsd, but really not useful in general: + self.fd:write(' \n') + self.fd:write(' \n') + + self.fd:write(' \n') + self.fd:write('\n') + self.fd:close() + return self.result.notSuccessCount + end + + +-- class TapOutput end + +---------------------------------------------------------------- +-- class TextOutput +---------------------------------------------------------------- + +--[[ Example of other unit-tests suite text output + +-- Python Non verbose: + +For each test: . or F or E + +If some failed tests: + ============== + ERROR / FAILURE: TestName (testfile.testclass) + --------- + Stack trace + + +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) + +-- Python Verbose: +testname (filename.classname) ... ok +testname (filename.classname) ... FAIL +testname (filename.classname) ... ERROR + +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) + +-- Ruby: +Started + . + Finished in 0.002695 seconds. + + 1 tests, 2 assertions, 0 failures, 0 errors + +-- Ruby: +>> ruby tc_simple_number2.rb +Loaded suite tc_simple_number2 +Started +F.. +Finished in 0.038617 seconds. + + 1) Failure: +test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]: +Adding doesn't work. +<3> expected but was +<4>. + +3 tests, 4 assertions, 1 failures, 0 errors + +-- Java Junit +.......F. +Time: 0,003 +There was 1 failure: +1) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError + at junit.samples.VectorTest.testCapacity(VectorTest.java:87) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + +FAILURES!!! +Tests run: 8, Failures: 1, Errors: 0 + + +-- Maven + +# mvn test +------------------------------------------------------- + T E S T S +------------------------------------------------------- +Running math.AdditionTest +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: +0.03 sec <<< FAILURE! + +Results : + +Failed tests: + testLireSymbole(math.AdditionTest) + +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 + + +-- LuaUnit +---- non verbose +* display . or F or E when running tests +---- verbose +* display test name + ok/fail +---- +* blank line +* number) ERROR or FAILURE: TestName + Stack trace +* blank line +* number) ERROR or FAILURE: TestName + Stack trace + +then -------------- +then "Ran x tests in 0.000s (%d not selected, %d skipped)" +then OK or FAILED (failures=1, error=1) + + +]] + +local TextOutput = genericOutput.new() -- derived class +local TextOutput_MT = { __index = TextOutput } -- metatable +TextOutput.__class__ = 'TextOutput' + + function TextOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT) + t.errorList = {} + return setmetatable( t, TextOutput_MT ) + end + + function TextOutput:startSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print( 'Started on '.. self.result.startDate ) + end + end + + function TextOutput:startTest(testName) + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write( " ", self.result.currentNode.testName, " ... " ) + end + end + + function TextOutput:endTest( node ) + if node:isSuccess() then + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write("Ok\n") + else + io.stdout:write(".") + io.stdout:flush() + end + else + if self.verbosity > M.VERBOSITY_DEFAULT then + print( node.status ) + print( node.msg ) + --[[ + -- find out when to do this: + if self.verbosity > M.VERBOSITY_DEFAULT then + print( node.stackTrace ) + end + ]] + else + -- write only the first character of status E, F or S + io.stdout:write(string.sub(node.status, 1, 1)) + io.stdout:flush() + end + end + end + + function TextOutput:displayOneFailedTest( index, fail ) + print(index..") "..fail.testName ) + print( fail.msg ) + print( fail.stackTrace ) + print() + end + + function TextOutput:displayErroredTests() + if #self.result.errorTests ~= 0 then + print("Tests with errors:") + print("------------------") + for i, v in ipairs(self.result.errorTests) do + self:displayOneFailedTest(i, v) + end + end + end + + function TextOutput:displayFailedTests() + if #self.result.failedTests ~= 0 then + print("Failed tests:") + print("-------------") + for i, v in ipairs(self.result.failedTests) do + self:displayOneFailedTest(i, v) + end + end + end + + function TextOutput:endSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print("=========================================================") + else + print() + end + self:displayErroredTests() + self:displayFailedTests() + print( M.LuaUnit.statusLine( self.result ) ) + if self.result.notSuccessCount == 0 then + print('OK') + end + end + +-- class TextOutput end + + +---------------------------------------------------------------- +-- class NilOutput +---------------------------------------------------------------- + +local function nopCallable() + --print(42) + return nopCallable +end + +local NilOutput = { __class__ = 'NilOuptut' } -- class +local NilOutput_MT = { __index = nopCallable } -- metatable + +function NilOutput.new(runner) + return setmetatable( { __class__ = 'NilOutput' }, NilOutput_MT ) +end + +---------------------------------------------------------------- +-- +-- class LuaUnit +-- +---------------------------------------------------------------- + +M.LuaUnit = { + outputType = TextOutput, + verbosity = M.VERBOSITY_DEFAULT, + __class__ = 'LuaUnit', + instances = {} +} +local LuaUnit_MT = { __index = M.LuaUnit } + +if EXPORT_ASSERT_TO_GLOBALS then + LuaUnit = M.LuaUnit +end + + function M.LuaUnit.new() + local newInstance = setmetatable( {}, LuaUnit_MT ) + return newInstance + end + + -----------------[[ Utility methods ]]--------------------- + + function M.LuaUnit.asFunction(aObject) + -- return "aObject" if it is a function, and nil otherwise + if 'function' == type(aObject) then + return aObject + end + end + + function M.LuaUnit.splitClassMethod(someName) + --[[ + Return a pair of className, methodName strings for a name in the form + "class.method". If no class part (or separator) is found, will return + nil, someName instead (the latter being unchanged). + + This convention thus also replaces the older isClassMethod() test: + You just have to check for a non-nil className (return) value. + ]] + local separator = string.find(someName, '.', 1, true) + if separator then + return someName:sub(1, separator - 1), someName:sub(separator + 1) + end + return nil, someName + end + + function M.LuaUnit.isMethodTestName( s ) + -- return true is the name matches the name of a test method + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == 'test' + end + + function M.LuaUnit.isTestName( s ) + -- return true is the name matches the name of a test + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == 'test' + end + + function M.LuaUnit.collectTests() + -- return a list of all test names in the global namespace + -- that match LuaUnit.isTestName + + local testNames = {} + for k, _ in pairs(_G) do + if type(k) == "string" and M.LuaUnit.isTestName( k ) then + table.insert( testNames , k ) + end + end + table.sort( testNames ) + return testNames + end + + function M.LuaUnit.parseCmdLine( cmdLine ) + -- parse the command line + -- Supported command line parameters: + -- --verbose, -v: increase verbosity + -- --quiet, -q: silence output + -- --error, -e: treat errors as fatal (quit program) + -- --output, -o, + name: select output type + -- --pattern, -p, + pattern: run test matching pattern, may be repeated + -- --exclude, -x, + pattern: run test not matching pattern, may be repeated + -- --shuffle, -s, : shuffle tests before reunning them + -- --name, -n, + fname: name of output file for junit, default to stdout + -- --repeat, -r, + num: number of times to execute each test + -- [testnames, ...]: run selected test names + -- + -- Returns a table with the following fields: + -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE + -- output: nil, 'tap', 'junit', 'text', 'nil' + -- testNames: nil or a list of test names to run + -- exeRepeat: num or 1 + -- pattern: nil or a list of patterns + -- exclude: nil or a list of patterns + + local result, state = {}, nil + local SET_OUTPUT = 1 + local SET_PATTERN = 2 + local SET_EXCLUDE = 3 + local SET_FNAME = 4 + local SET_REPEAT = 5 + + if cmdLine == nil then + return result + end + + local function parseOption( option ) + if option == '--help' or option == '-h' then + result['help'] = true + return + elseif option == '--version' then + result['version'] = true + return + elseif option == '--verbose' or option == '-v' then + result['verbosity'] = M.VERBOSITY_VERBOSE + return + elseif option == '--quiet' or option == '-q' then + result['verbosity'] = M.VERBOSITY_QUIET + return + elseif option == '--error' or option == '-e' then + result['quitOnError'] = true + return + elseif option == '--failure' or option == '-f' then + result['quitOnFailure'] = true + return + elseif option == '--shuffle' or option == '-s' then + result['shuffle'] = true + return + elseif option == '--output' or option == '-o' then + state = SET_OUTPUT + return state + elseif option == '--name' or option == '-n' then + state = SET_FNAME + return state + elseif option == '--repeat' or option == '-r' then + state = SET_REPEAT + return state + elseif option == '--pattern' or option == '-p' then + state = SET_PATTERN + return state + elseif option == '--exclude' or option == '-x' then + state = SET_EXCLUDE + return state + end + error('Unknown option: '..option,3) + end + + local function setArg( cmdArg, state ) + if state == SET_OUTPUT then + result['output'] = cmdArg + return + elseif state == SET_FNAME then + result['fname'] = cmdArg + return + elseif state == SET_REPEAT then + result['exeRepeat'] = tonumber(cmdArg) + or error('Malformed -r argument: '..cmdArg) + return + elseif state == SET_PATTERN then + if result['pattern'] then + table.insert( result['pattern'], cmdArg ) + else + result['pattern'] = { cmdArg } + end + return + elseif state == SET_EXCLUDE then + local notArg = '!'..cmdArg + if result['pattern'] then + table.insert( result['pattern'], notArg ) + else + result['pattern'] = { notArg } + end + return + end + error('Unknown parse state: '.. state) + end + + + for i, cmdArg in ipairs(cmdLine) do + if state ~= nil then + setArg( cmdArg, state, result ) + state = nil + else + if cmdArg:sub(1,1) == '-' then + state = parseOption( cmdArg ) + else + if result['testNames'] then + table.insert( result['testNames'], cmdArg ) + else + result['testNames'] = { cmdArg } + end + end + end + end + + if result['help'] then + M.LuaUnit.help() + end + + if result['version'] then + M.LuaUnit.version() + end + + if state ~= nil then + error('Missing argument after '..cmdLine[ #cmdLine ],2 ) + end + + return result + end + + function M.LuaUnit.help() + print(M.USAGE) + os.exit(0) + end + + function M.LuaUnit.version() + print('LuaUnit v'..M.VERSION..' by Philippe Fremy ') + os.exit(0) + end + +---------------------------------------------------------------- +-- class NodeStatus +---------------------------------------------------------------- + + local NodeStatus = { __class__ = 'NodeStatus' } -- class + local NodeStatus_MT = { __index = NodeStatus } -- metatable + M.NodeStatus = NodeStatus + + -- values of status + NodeStatus.SUCCESS = 'SUCCESS' + NodeStatus.SKIP = 'SKIP' + NodeStatus.FAIL = 'FAIL' + NodeStatus.ERROR = 'ERROR' + + function NodeStatus.new( number, testName, className ) + -- default constructor, test are PASS by default + local t = { number = number, testName = testName, className = className } + setmetatable( t, NodeStatus_MT ) + t:success() + return t + end + + function NodeStatus:success() + self.status = self.SUCCESS + -- useless because lua does this for us, but it helps me remembering the relevant field names + self.msg = nil + self.stackTrace = nil + end + + function NodeStatus:skip(msg) + self.status = self.SKIP + self.msg = msg + self.stackTrace = nil + end + + function NodeStatus:fail(msg, stackTrace) + self.status = self.FAIL + self.msg = msg + self.stackTrace = stackTrace + end + + function NodeStatus:error(msg, stackTrace) + self.status = self.ERROR + self.msg = msg + self.stackTrace = stackTrace + end + + function NodeStatus:isSuccess() + return self.status == NodeStatus.SUCCESS + end + + function NodeStatus:isNotSuccess() + -- Return true if node is either failure or error or skip + return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP) + end + + function NodeStatus:isSkipped() + return self.status == NodeStatus.SKIP + end + + function NodeStatus:isFailure() + return self.status == NodeStatus.FAIL + end + + function NodeStatus:isError() + return self.status == NodeStatus.ERROR + end + + function NodeStatus:statusXML() + if self:isError() then + return table.concat( + {' \n', + ' \n'}) + elseif self:isFailure() then + return table.concat( + {' \n', + ' \n'}) + elseif self:isSkipped() then + return table.concat({' ', xmlEscape(self.msg),'\n' } ) + end + return ' \n' -- (not XSD-compliant! normally shouldn't get here) + end + + --------------[[ Output methods ]]------------------------- + + local function conditional_plural(number, singular) + -- returns a grammatically well-formed string "%d " + local suffix = '' + if number ~= 1 then -- use plural + suffix = (singular:sub(-2) == 'ss') and 'es' or 's' + end + return string.format('%d %s%s', number, singular, suffix) + end + + function M.LuaUnit.statusLine(result) + -- return status line string according to results + local s = { + string.format('Ran %d tests in %0.3f seconds', + result.runCount, result.duration), + conditional_plural(result.successCount, 'success'), + } + if result.notSuccessCount > 0 then + if result.failureCount > 0 then + table.insert(s, conditional_plural(result.failureCount, 'failure')) + end + if result.errorCount > 0 then + table.insert(s, conditional_plural(result.errorCount, 'error')) + end + else + table.insert(s, '0 failures') + end + if result.skippedCount > 0 then + table.insert(s, string.format("%d skipped", result.skippedCount)) + end + if result.nonSelectedCount > 0 then + table.insert(s, string.format("%d non-selected", result.nonSelectedCount)) + end + return table.concat(s, ', ') + end + + function M.LuaUnit:startSuite(selectedCount, nonSelectedCount) + self.result = { + selectedCount = selectedCount, + nonSelectedCount = nonSelectedCount, + successCount = 0, + runCount = 0, + currentTestNumber = 0, + currentClassName = "", + currentNode = nil, + suiteStarted = true, + startTime = os.clock(), + startDate = os.date(os.getenv('LUAUNIT_DATEFMT')), + startIsodate = os.date('%Y-%m-%dT%H:%M:%S'), + patternIncludeFilter = self.patternIncludeFilter, + + -- list of test node status + allTests = {}, + failedTests = {}, + errorTests = {}, + skippedTests = {}, + + failureCount = 0, + errorCount = 0, + notSuccessCount = 0, + skippedCount = 0, + } + + self.outputType = self.outputType or TextOutput + self.output = self.outputType.new(self) + self.output:startSuite() + end + + function M.LuaUnit:startClass( className, classInstance ) + self.result.currentClassName = className + self.output:startClass( className ) + self:setupClass( className, classInstance ) + end + + function M.LuaUnit:startTest( testName ) + self.result.currentTestNumber = self.result.currentTestNumber + 1 + self.result.runCount = self.result.runCount + 1 + self.result.currentNode = NodeStatus.new( + self.result.currentTestNumber, + testName, + self.result.currentClassName + ) + self.result.currentNode.startTime = os.clock() + table.insert( self.result.allTests, self.result.currentNode ) + self.output:startTest( testName ) + end + + function M.LuaUnit:updateStatus( err ) + -- "err" is expected to be a table / result from protectedCall() + if err.status == NodeStatus.SUCCESS then + return + end + + local node = self.result.currentNode + + --[[ As a first approach, we will report only one error or one failure for one test. + + However, we can have the case where the test is in failure, and the teardown is in error. + In such case, it's a good idea to report both a failure and an error in the test suite. This is + what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for + example, there could be more (failures + errors) count that tests. What happens to the current node ? + + We will do this more intelligent version later. + ]] + + -- if the node is already in failure/error, just don't report the new error (see above) + if node.status ~= NodeStatus.SUCCESS then + return + end + + if err.status == NodeStatus.FAIL then + node:fail( err.msg, err.trace ) + table.insert( self.result.failedTests, node ) + elseif err.status == NodeStatus.ERROR then + node:error( err.msg, err.trace ) + table.insert( self.result.errorTests, node ) + elseif err.status == NodeStatus.SKIP then + node:skip( err.msg ) + table.insert( self.result.skippedTests, node ) + else + error('No such status: ' .. prettystr(err.status)) + end + + self.output:updateStatus( node ) + end + + function M.LuaUnit:endTest() + local node = self.result.currentNode + -- print( 'endTest() '..prettystr(node)) + -- print( 'endTest() '..prettystr(node:isNotSuccess())) + node.duration = os.clock() - node.startTime + node.startTime = nil + self.output:endTest( node ) + + if node:isSuccess() then + self.result.successCount = self.result.successCount + 1 + elseif node:isError() then + if self.quitOnError or self.quitOnFailure then + -- Runtime error - abort test execution as requested by + -- "--error" option. This is done by setting a special + -- flag that gets handled in internalRunSuiteByInstances(). + print("\nERROR during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isFailure() then + if self.quitOnFailure then + -- Failure - abort test execution as requested by + -- "--failure" option. This is done by setting a special + -- flag that gets handled in internalRunSuiteByInstances(). + print("\nFailure during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isSkipped() then + self.result.runCount = self.result.runCount - 1 + else + error('No such node status: ' .. prettystr(node.status)) + end + self.result.currentNode = nil + end + + function M.LuaUnit:endClass() + self:teardownClass( self.lastClassName, self.lastClassInstance ) + self.output:endClass() + end + + function M.LuaUnit:endSuite() + if self.result.suiteStarted == false then + error('LuaUnit:endSuite() -- suite was already ended' ) + end + self.result.duration = os.clock()-self.result.startTime + self.result.suiteStarted = false + + -- Expose test counts for outputter's endSuite(). This could be managed + -- internally instead by using the length of the lists of failed tests + -- but unit tests rely on these fields being present. + self.result.failureCount = #self.result.failedTests + self.result.errorCount = #self.result.errorTests + self.result.notSuccessCount = self.result.failureCount + self.result.errorCount + self.result.skippedCount = #self.result.skippedTests + + self.output:endSuite() + end + + function M.LuaUnit:setOutputType(outputType, fname) + -- Configures LuaUnit runner output + -- outputType is one of: NIL, TAP, JUNIT, TEXT + -- when outputType is junit, the additional argument fname is used to set the name of junit output file + -- for other formats, fname is ignored + if outputType:upper() == "NIL" then + self.outputType = NilOutput + return + end + if outputType:upper() == "TAP" then + self.outputType = TapOutput + return + end + if outputType:upper() == "JUNIT" then + self.outputType = JUnitOutput + if fname then + self.fname = fname + end + return + end + if outputType:upper() == "TEXT" then + self.outputType = TextOutput + return + end + error( 'No such format: '..outputType,2) + end + + --------------[[ Runner ]]----------------- + + function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName) + -- if classInstance is nil, this is just a function call + -- else, it's method of a class being called. + + local function err_handler(e) + -- transform error into a table, adding the traceback information + return { + status = NodeStatus.ERROR, + msg = e, + trace = string.sub(debug.traceback("", 1), 2) + } + end + + local ok, err + if classInstance then + -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround + ok, err = xpcall( function () methodInstance(classInstance) end, err_handler ) + else + ok, err = xpcall( function () methodInstance() end, err_handler ) + end + if ok then + return {status = NodeStatus.SUCCESS} + end + -- print('ok="'..prettystr(ok)..'" err="'..prettystr(err)..'"') + + local iter_msg + iter_msg = self.exeRepeat and 'iteration '..self.currentCount + + err.msg, err.status = M.adjust_err_msg_with_iter( err.msg, iter_msg ) + + if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then + err.trace = nil + return err + end + + -- reformat / improve the stack trace + if prettyFuncName then -- we do have the real method name + err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '"..prettyFuncName.."'") + end + if STRIP_LUAUNIT_FROM_STACKTRACE then + err.trace = stripLuaunitTrace2(err.trace, err.msg) + end + + return err -- return the error "object" (table) + end + + + function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance) + -- When executing a test function, className and classInstance must be nil + -- When executing a class method, all parameters must be set + + if type(methodInstance) ~= 'function' then + self:unregisterSuite() + error( tostring(methodName)..' must be a function, not '..type(methodInstance)) + end + + local prettyFuncName + if className == nil then + className = '[TestFunctions]' + prettyFuncName = methodName + else + prettyFuncName = className..'.'..methodName + end + + if self.lastClassName ~= className then + if self.lastClassName ~= nil then + self:endClass() + end + self:startClass( className, classInstance ) + self.lastClassName = className + self.lastClassInstance = classInstance + end + + self:startTest(prettyFuncName) + + local node = self.result.currentNode + for iter_n = 1, self.exeRepeat or 1 do + if node:isNotSuccess() then + break + end + self.currentCount = iter_n + + -- run setUp first (if any) + if classInstance then + local func = self.asFunction( classInstance.setUp ) or + self.asFunction( classInstance.Setup ) or + self.asFunction( classInstance.setup ) or + self.asFunction( classInstance.SetUp ) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className..'.setUp')) + end + end + + -- run testMethod() + if node:isSuccess() then + self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName)) + end + + -- lastly, run tearDown (if any) + if classInstance then + local func = self.asFunction( classInstance.tearDown ) or + self.asFunction( classInstance.TearDown ) or + self.asFunction( classInstance.teardown ) or + self.asFunction( classInstance.Teardown ) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className..'.tearDown')) + end + end + end + + self:endTest() + end + + function M.LuaUnit.expandOneClass( result, className, classInstance ) + --[[ + Input: a list of { name, instance }, a class name, a class instance + Ouptut: modify result to add all test method instance in the form: + { className.methodName, classInstance } + ]] + for methodName, methodInstance in sortedPairs(classInstance) do + if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName( methodName ) then + table.insert( result, { className..'.'..methodName, classInstance } ) + end + end + end + + function M.LuaUnit.expandClasses( listOfNameAndInst ) + --[[ + -- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance} + -- functions and methods remain untouched + + Input: a list of { name, instance } + + Output: + * { function name, function instance } : do nothing + * { class.method name, class instance }: do nothing + * { class name, class instance } : add all method names in the form of (className.methodName, classInstance) + ]] + local result = {} + + for i,v in ipairs( listOfNameAndInst ) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + table.insert( result, { name, instance } ) + else + if type(instance) ~= 'table' then + error( 'Instance must be a table or a function, not a '..type(instance)..' with value '..prettystr(instance)) + end + local className, methodName = M.LuaUnit.splitClassMethod( name ) + if className then + local methodInstance = instance[methodName] + if methodInstance == nil then + error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) + end + table.insert( result, { name, instance } ) + else + M.LuaUnit.expandOneClass( result, name, instance ) + end + end + end + + return result + end + + function M.LuaUnit.applyPatternFilter( patternIncFilter, listOfNameAndInst ) + local included, excluded = {}, {} + for i, v in ipairs( listOfNameAndInst ) do + -- local name, instance = v[1], v[2] + if patternFilter( patternIncFilter, v[1] ) then + table.insert( included, v ) + else + table.insert( excluded, v ) + end + end + return included, excluded + end + + local function getKeyInListWithGlobalFallback( key, listOfNameAndInst ) + local result = nil + for i,v in ipairs( listOfNameAndInst ) do + if(listOfNameAndInst[i][1] == key) then + result = listOfNameAndInst[i][2] + break + end + end + if(not M.LuaUnit.asFunction( result ) ) then + result = _G[key] + end + return result + end + + function M.LuaUnit:setupSuite( listOfNameAndInst ) + local setupSuite = getKeyInListWithGlobalFallback("setupSuite", listOfNameAndInst) + if self.asFunction( setupSuite ) then + self:updateStatus( self:protectedCall( nil, setupSuite, 'setupSuite' ) ) + end + end + + function M.LuaUnit:teardownSuite(listOfNameAndInst) + local teardownSuite = getKeyInListWithGlobalFallback("teardownSuite", listOfNameAndInst) + if self.asFunction( teardownSuite ) then + self:updateStatus( self:protectedCall( nil, teardownSuite, 'teardownSuite') ) + end + end + + function M.LuaUnit:setupClass( className, instance ) + if type( instance ) == 'table' and self.asFunction( instance.setupClass ) then + self:updateStatus( self:protectedCall( instance, instance.setupClass, className..'.setupClass' ) ) + end + end + + function M.LuaUnit:teardownClass( className, instance ) + if type( instance ) == 'table' and self.asFunction( instance.teardownClass ) then + self:updateStatus( self:protectedCall( instance, instance.teardownClass, className..'.teardownClass' ) ) + end + end + + function M.LuaUnit:internalRunSuiteByInstances( listOfNameAndInst ) + --[[ Run an explicit list of tests. Each item of the list must be one of: + * { function name, function instance } + * { class name, class instance } + * { class.method name, class instance } + + This function is internal to LuaUnit. The official API to perform this action is runSuiteByInstances() + ]] + + local expandedList = self.expandClasses( listOfNameAndInst ) + if self.shuffle then + randomizeTable( expandedList ) + end + local filteredList, filteredOutList = self.applyPatternFilter( + self.patternIncludeFilter, expandedList ) + + self:startSuite( #filteredList, #filteredOutList ) + self:setupSuite( listOfNameAndInst ) + + for i,v in ipairs( filteredList ) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + self:execOneFunction( nil, name, nil, instance ) + else + -- expandClasses() should have already taken care of sanitizing the input + assert( type(instance) == 'table' ) + local className, methodName = M.LuaUnit.splitClassMethod( name ) + assert( className ~= nil ) + local methodInstance = instance[methodName] + assert(methodInstance ~= nil) + self:execOneFunction( className, methodName, instance, methodInstance ) + end + if self.result.aborted then + break -- "--error" or "--failure" option triggered + end + end + + if self.lastClassName ~= nil then + self:endClass() + end + + self:teardownSuite( listOfNameAndInst ) + self:endSuite() + + if self.result.aborted then + print("LuaUnit ABORTED (as requested by --error or --failure option)") + self:unregisterSuite() + os.exit(-2) + end + end + + function M.LuaUnit:internalRunSuiteByNames( listOfName ) + --[[ Run LuaUnit with a list of generic names, coming either from command-line or from global + namespace analysis. Convert the list into a list of (name, valid instances (table or function)) + and calls internalRunSuiteByInstances. + ]] + + local instanceName, instance + local listOfNameAndInst = {} + + for i,name in ipairs( listOfName ) do + local className, methodName = M.LuaUnit.splitClassMethod( name ) + if className then + instanceName = className + instance = _G[instanceName] + + if instance == nil then + self:unregisterSuite() + error( "No such name in global space: "..instanceName ) + end + + if type(instance) ~= 'table' then + self:unregisterSuite() + error( 'Instance of '..instanceName..' must be a table, not '..type(instance)) + end + + local methodInstance = instance[methodName] + if methodInstance == nil then + self:unregisterSuite() + error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) + end + + else + -- for functions and classes + instanceName = name + instance = _G[instanceName] + end + + if instance == nil then + self:unregisterSuite() + error( "No such name in global space: "..instanceName ) + end + + if (type(instance) ~= 'table' and type(instance) ~= 'function') then + self:unregisterSuite() + error( 'Name must match a function or a table: '..instanceName ) + end + + table.insert( listOfNameAndInst, { name, instance } ) + end + + self:internalRunSuiteByInstances( listOfNameAndInst ) + end + + function M.LuaUnit.run(...) + -- Run some specific test classes. + -- If no arguments are passed, run the class names specified on the + -- command line. If no class name is specified on the command line + -- run all classes whose name starts with 'Test' + -- + -- If arguments are passed, they must be strings of the class names + -- that you want to run or generic command line arguments (-o, -p, -v, ...) + local runner = M.LuaUnit.new() + return runner:runSuite(...) + end + + function M.LuaUnit:registerSuite() + -- register the current instance into our global array of instances + -- print('-> Register suite') + M.LuaUnit.instances[ #M.LuaUnit.instances+1 ] = self + end + + function M.unregisterCurrentSuite() + -- force unregister the last registered suite + table.remove(M.LuaUnit.instances, #M.LuaUnit.instances) + end + + function M.LuaUnit:unregisterSuite() + -- print('<- Unregister suite') + -- remove our current instqances from the global array of instances + local instanceIdx = nil + for i, instance in ipairs(M.LuaUnit.instances) do + if instance == self then + instanceIdx = i + break + end + end + + if instanceIdx ~= nil then + table.remove(M.LuaUnit.instances, instanceIdx) + -- print('Unregister done') + end + + end + + function M.LuaUnit:initFromArguments( ... ) + --[[Parses all arguments from either command-line or direct call and set internal + flags of LuaUnit runner according to it. + + Return the list of names which were possibly passed on the command-line or as arguments + ]] + local args = {...} + if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then + -- run was called with the syntax M.LuaUnit:runSuite() + -- we support both M.LuaUnit.run() and M.LuaUnit:run() + -- strip out the first argument self to make it a command-line argument list + table.remove(args,1) + end + + if #args == 0 then + args = cmdline_argv + end + + local options = pcall_or_abort( M.LuaUnit.parseCmdLine, args ) + + -- We expect these option fields to be either `nil` or contain + -- valid values, so it's safe to always copy them directly. + self.verbosity = options.verbosity + self.quitOnError = options.quitOnError + self.quitOnFailure = options.quitOnFailure + + self.exeRepeat = options.exeRepeat + self.patternIncludeFilter = options.pattern + self.shuffle = options.shuffle + + options.output = options.output or os.getenv('LUAUNIT_OUTPUT') + options.fname = options.fname or os.getenv('LUAUNIT_JUNIT_FNAME') + + if options.output then + if options.output:lower() == 'junit' and options.fname == nil then + print('With junit output, a filename must be supplied with -n or --name') + os.exit(-1) + end + pcall_or_abort(self.setOutputType, self, options.output, options.fname) + end + + return options.testNames + end + + function M.LuaUnit:runSuite( ... ) + testNames = self:initFromArguments(...) + self:registerSuite() + self:internalRunSuiteByNames( testNames or M.LuaUnit.collectTests() ) + self:unregisterSuite() + return self.result.notSuccessCount + end + + function M.LuaUnit:runSuiteByInstances( listOfNameAndInst, commandLineArguments ) + --[[ + Run all test functions or tables provided as input. + + Input: a list of { name, instance } + instance can either be a function or a table containing test functions starting with the prefix "test" + + return the number of failures and errors, 0 meaning success + ]] + -- parse the command-line arguments + testNames = self:initFromArguments( commandLineArguments ) + self:registerSuite() + self:internalRunSuiteByInstances( listOfNameAndInst ) + self:unregisterSuite() + return self.result.notSuccessCount + end + + + +-- class LuaUnit + +-- For compatbility with LuaUnit v2 +M.run = M.LuaUnit.run +M.Run = M.LuaUnit.run + +function M:setVerbosity( verbosity ) + -- set the verbosity value (as integer) + M.LuaUnit.verbosity = verbosity +end +M.set_verbosity = M.setVerbosity +M.SetVerbosity = M.setVerbosity + + +return M + diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index fa924e5e16..cd19df032d 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -30,7 +30,11 @@ module Transforms = | [] -> () ] let ident name = Ident {Name = name; Namespace = None} + let fcall args expr= FunctionCall(expr, args) let iife statements = FunctionCall(AnonymousFunc([], statements), []) + let debugLog expr = FunctionCall(Helpers.ident "print", [expr]) |> Do + let libEquality a b= + FunctionCall(GetObjMethod(FunctionCall(Helpers.ident "require", [ConstString "./fable-lib/Util" |> Const]), "equals"), [a; b]) let maybeIife = function | [] -> NoOp | [Return expr] -> expr @@ -38,7 +42,20 @@ module Transforms = let tryNewObj (names: string list) (values: Expr list) = if names.Length = values.Length then let pairs = List.zip names values - NewObj(pairs) + let compareExprs = names + |> List.map (fun name -> + libEquality + (GetField(Helpers.ident "self", name)) + (GetField(Helpers.ident "toCompare", name))) + let compareExprAcc = compareExprs |> List.reduce (fun acc item -> Binary(And, acc, item) ) + let equality = "Equals", Function (["self"; "toCompare"], [ + //yield debugLog (ConstString "Calling equality" |> Const) + // debugLog (Helpers.ident "self") + // debugLog (Helpers.ident "toCompare") + //yield! compareExprs |> List.map debugLog + Return compareExprAcc + ]) + NewObj(equality::pairs) else sprintf "Names and values do not match %A %A" names values |> Unknown let transformValueKind (com: LuaCompiler) = function | Fable.NumberConstant(v,_,_) -> @@ -80,6 +97,8 @@ module Transforms = let transformOp com = let transformExpr = transformExpr com function + | Fable.OperationKind.Binary(BinaryModulus, left, right) -> + GetField(Helpers.ident "math", "fmod") |> Helpers.fcall [transformExpr left; transformExpr right] | Fable.OperationKind.Binary (op, left, right) -> let op = match op with | BinaryMultiply -> Multiply @@ -89,6 +108,7 @@ module Transforms = | BinaryMinus -> Minus | BinaryEqualStrict -> Equals | BinaryUnequal -> Unequal + | BinaryUnequalStrict -> Unequal | BinaryLess -> Less | BinaryGreater -> Greater | BinaryLessOrEqual -> LessOrEqual @@ -137,11 +157,20 @@ module Transforms = let transformExpr (com: LuaCompiler) expr= let transformExpr = transformExpr com let transformOp = transformOp com + match expr with | Fable.Expr.Value(value, _) -> transformValueKind com value | Fable.Expr.Call(expr, callInfo, t, r) -> - //Unknown(sprintf "call %A %A" expr callInfo) - FunctionCall(transformExpr expr, List.map transformExpr callInfo.Args) + let lhs = + match expr with + | Fable.Expr.Get(expr, Fable.GetKind.FieldGet(fieldName, isMut), t, _) -> + match t with + | Fable.DeclaredType(_, _) + | Fable.AnonymousRecordType(_, _) -> + GetObjMethod(transformExpr expr, fieldName) + | _ -> transformExpr expr + | _ -> transformExpr expr + FunctionCall(lhs, List.map transformExpr callInfo.Args) | Fable.Expr.Import (info, t, r) -> let path = match info.Kind, info.Path with @@ -156,17 +185,21 @@ module Transforms = let rcall = FunctionCall(Ident { Namespace=None; Name= "require" }, [Const (ConstString path)]) match info.Selector with | "" -> rcall - | s -> GetField(rcall, s) + | s -> GetObjMethod(rcall, s) | Fable.Expr.IdentExpr(i) when i.Name <> "" -> Ident {Namespace = None; Name = i.Name } | Fable.Expr.Operation (kind, _, _) -> transformOp kind - | Fable.Expr.Get(expr, Fable.GetKind.FieldGet(fieldName, isMut), _, _) -> + | Fable.Expr.Get(expr, Fable.GetKind.FieldGet(fieldName, isMut), t, _) -> GetField(transformExpr expr, fieldName) | Fable.Expr.Get(expr, Fable.GetKind.UnionField(caseIdx, fieldIdx), _, _) -> GetField(transformExpr expr, sprintf "p_%i" fieldIdx) | Fable.Expr.Get(expr, Fable.GetKind.ExprGet(e), _, _) -> GetAtIndex(transformExpr expr, transformExpr e) + | Fable.Expr.Get(expr, Fable.GetKind.TupleIndex(i), _, _) -> + GetAtIndex(transformExpr expr, Const (ConstNumber (float i))) + | Fable.Expr.Get(expr, Fable.GetKind.OptionValue, _, _) -> + transformExpr expr //todo null check, throw if null? | Fable.Expr.Set(expr, Fable.SetKind.ValueSet, t, value, _) -> SetValue(transformExpr expr, transformExpr value) | Fable.Expr.Set(expr, Fable.SetKind.ExprSet(e), t, value, _) -> @@ -211,6 +244,17 @@ module Transforms = match kind with | Fable.UnionCaseTest i-> Binary(Equals, GetField(transformExpr expr, "tag") , Const (ConstNumber (float i))) + | Fable.OptionTest isSome -> + if isSome then Binary(Unequal, Const ConstNull, transformExpr expr) else Binary(Equals, Const ConstNull, transformExpr expr) + | Fable.TestKind.TypeTest t -> + // match t with + // | Fable.DeclaredType (ent, genArgs) -> + // match ent.FullName with + // | Fable.Transforms.Types.ienumerable -> //isArrayLike + // | Fable.Transforms.Types.array + // | _ -> + // | _ -> () + Binary(Equals, GetField(transformExpr expr, "type"), Const (t.ToString() |> ConstString)) | _ -> Unknown(sprintf "test %A %A" expr kind) | Fable.Extended(Fable.ExtendedSet.Throw(expr, _), t) -> @@ -218,6 +262,8 @@ module Transforms = Const (ConstString "There was an error, todo") //transformExpr expr FunctionCall(Helpers.ident "error", [errorExpr]) + | Fable.Extended(Fable.ExtendedSet.Curry(expr, d), _) -> + transformExpr expr |> sprintf "todo curry %A" |> Unknown | Fable.Delegate(idents, body, _) -> Function(idents |> List.map(fun i -> i.Name), [transformExpr body |> Return |> flattenReturnIifes]) //can be flattened | Fable.ForLoop(ident, start, limit, body, isUp, _) -> diff --git a/src/Fable.Transforms/Lua/Lua.fs b/src/Fable.Transforms/Lua/Lua.fs index b7cfdd0c27..f7046f9dd9 100644 --- a/src/Fable.Transforms/Lua/Lua.fs +++ b/src/Fable.Transforms/Lua/Lua.fs @@ -28,6 +28,8 @@ type BinaryOp = | Plus | Minus | BinaryTodo of string + | And + | Or type Expr = | Ident of LuaIdentity @@ -35,6 +37,7 @@ type Expr = | Unary of UnaryOp * Expr | Binary of BinaryOp * Expr * Expr | GetField of Expr * name: string + | GetObjMethod of Expr * name: string | GetAtIndex of Expr * idx: Expr | SetValue of Expr * value: Expr | SetExpr of Expr * Expr * value: Expr diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index 6d0f00d2fa..fe24264fd4 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -57,6 +57,8 @@ module Output = | Divide -> write ctx """/""" | Plus -> write ctx "+" | Minus -> write ctx "-" + | And -> write ctx "and" + | Or -> write ctx "or" | BinaryTodo x -> writeCommented ctx "binary todo" x let sprintExprSimple = function | Ident i -> i.Name @@ -69,7 +71,7 @@ module Output = | ConstString s -> s |> sprintf "'%s'" |> write ctx | ConstNumber n -> n |> sprintf "%f" |> write ctx | ConstBool b -> b |> sprintf "%b" |> write ctx - | ConstNull -> write ctx "null" + | ConstNull -> write ctx "nil" | FunctionCall(e, args) -> writeExpr ctx e write ctx "(" @@ -98,6 +100,10 @@ module Output = writeExpr ctx expr write ctx "." write ctx fieldName + | GetObjMethod(expr, fieldName) -> + writeExpr ctx expr + write ctx ":" + write ctx fieldName | GetAtIndex(expr, idx) -> writeExpr ctx expr write ctx "[" @@ -217,6 +223,7 @@ module Output = writei ctx "function " write ctx name write ctx "(" + // let args = if exportToMod then "self"::args else args args |> Helper.separateWithCommas |> write ctx write ctx ")" let ctxI = indent ctx @@ -226,8 +233,10 @@ module Output = if exportToMod then writei ctx "mod." write ctx name - write ctx " = " + write ctx " = function(self, ...) " write ctx name + write ctx "(...)" + write ctx " end" writeln ctxI "" | Return expr -> writei ctx "return " diff --git a/src/fable-library-lua/README.md b/src/fable-library-lua/README.md index 45b44c862f..b472dd39ea 100644 --- a/src/fable-library-lua/README.md +++ b/src/fable-library-lua/README.md @@ -1,4 +1,13 @@ # Fable Library for Lua This module is used as the [Fable](https://fable.io/) library for -Lua. \ No newline at end of file +Lua. + +On windows, testing was done against lua 5.2.4, which you can get through chocolatey: + +https://community.chocolatey.org/packages/lua52 +or direct, although it needs to be in the path: +http://luabinaries.sourceforge.net/download.html + + +choco install lua52 \ No newline at end of file diff --git a/src/fable-library-lua/fable/Util.lua b/src/fable-library-lua/fable/Util.lua index 8ed680575a..655f1f85d3 100644 --- a/src/fable-library-lua/fable/Util.lua +++ b/src/fable-library-lua/fable/Util.lua @@ -1,27 +1,1585 @@ -mod = {} - --- https://web.archive.org/web/20131225070434/http://snippets.luacode.org/snippets/Deep_Comparison_of_Two_Values_3 -function deepcompare(t1,t2,ignore_mt) - local ty1 = type(t1) - local ty2 = type(t2) - if ty1 ~= ty2 then return false end - -- non-table types can be directly compared - if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end - -- as well as tables which have the metamethod __eq - local mt = getmetatable(t1) - if not ignore_mt and mt and mt.__eq then return t1 == t2 end - for k1,v1 in pairs(t1) do - local v2 = t2[k1] - if v2 == nil or not deepcompare(v1,v2) then return false end - end - for k2,v2 in pairs(t2) do - local v1 = t1[k2] - if v1 == nil or not deepcompare(v1,v2) then return false end +-- mod = {} + +-- -- https://web.archive.org/web/20131225070434/http://snippets.luacode.org/snippets/Deep_Comparison_of_Two_Values_3 +-- function deepcompare(t1,t2,ignore_mt) +-- local ty1 = type(t1) +-- local ty2 = type(t2) +-- if ty1 ~= ty2 then return false end +-- -- non-table types can be directly compared +-- if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end +-- -- as well as tables which have the metamethod __eq +-- local mt = getmetatable(t1) +-- if not ignore_mt and mt and mt.__eq then return t1 == t2 end +-- for k1,v1 in pairs(t1) do +-- local v2 = t2[k1] +-- if v2 == nil or not deepcompare(v1,v2) then return false end +-- end +-- for k2,v2 in pairs(t2) do +-- local v1 = t1[k2] +-- if v1 == nil or not deepcompare(v1,v2) then return false end +-- end +-- return true +-- end + +-- function mod.equals(a, b) +-- return deepcompare(a, b, true) +-- end +-- return mod + +-- https://stackoverflow.com/questions/5977654/how-do-i-use-the-bitwise-operator-xor-in-lua +local function BitXOR(a,b)--Bitwise xor + local p,c=1,0 + while a>0 and b>0 do + local ra,rb=a%2,b%2 + if ra~=rb then c=c+p end + a,b,p=(a-ra)/2,(b-rb)/2,p*2 end - return true + if a0 do + local ra=a%2 + if ra>0 then c=c+p end + a,p=(a-ra)/2,p*2 + end + return c +end + +local function BitOR(a,b)--Bitwise or + local p,c=1,0 + while a+b>0 do + local ra,rb=a%2,b%2 + if ra+rb>0 then c=c+p end + a,b,p=(a-ra)/2,(b-rb)/2,p*2 + end + return c +end + +local function BitAND(a,b)--Bitwise and + local p,c=1,0 + while a>0 and b>0 do + local ra,rb=a%2,b%2 + if ra+rb>1 then c=c+p end + a,b,p=(a-ra)/2,(b-rb)/2,p*2 + end + return c +end + +function lshift(x, by) + return x * 2 ^ by + end + + function rshift(x, by) + return math.floor(x / 2 ^ by) + end + + +--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] +-- Lua Library inline imports +____symbolMetatable = { + __tostring = function(self) + return ("Symbol(" .. (self.description or "")) .. ")" + end +} +function __TS__Symbol(description) + return setmetatable({description = description}, ____symbolMetatable) +end +Symbol = { + iterator = __TS__Symbol("Symbol.iterator"), + hasInstance = __TS__Symbol("Symbol.hasInstance"), + species = __TS__Symbol("Symbol.species"), + toStringTag = __TS__Symbol("Symbol.toStringTag") +} + +function __TS__ArrayIsArray(value) + return (type(value) == "table") and ((value[1] ~= nil) or (next(value, nil) == nil)) +end + +function __TS__Class(self) + local c = {prototype = {}} + c.prototype.__index = c.prototype + c.prototype.constructor = c + return c +end + +function __TS__ClassExtends(target, base) + target.____super = base + local staticMetatable = setmetatable({__index = base}, base) + setmetatable(target, staticMetatable) + local baseMetatable = getmetatable(base) + if baseMetatable then + if type(baseMetatable.__index) == "function" then + staticMetatable.__index = baseMetatable.__index + end + if type(baseMetatable.__newindex) == "function" then + staticMetatable.__newindex = baseMetatable.__newindex + end + end + setmetatable(target.prototype, base.prototype) + if type(base.prototype.__index) == "function" then + target.prototype.__index = base.prototype.__index + end + if type(base.prototype.__newindex) == "function" then + target.prototype.__newindex = base.prototype.__newindex + end + if type(base.prototype.__tostring) == "function" then + target.prototype.__tostring = base.prototype.__tostring + end +end + +function __TS__New(target, ...) + local instance = setmetatable({}, target.prototype) + instance:____constructor(...) + return instance +end + +function __TS__GetErrorStack(self, constructor) + local level = 1 + while true do + local info = debug.getinfo(level, "f") + level = level + 1 + if not info then + level = 1 + break + elseif info.func == constructor then + break + end + end + return debug.traceback(nil, level) +end +function __TS__WrapErrorToString(self, getDescription) + return function(self) + local description = getDescription(self) + local caller = debug.getinfo(3, "f") + if (_VERSION == "Lua 5.1") or (caller and (caller.func ~= error)) then + return description + else + return (tostring(description) .. "\n") .. self.stack + end + end +end +function __TS__InitErrorClass(self, Type, name) + Type.name = name + return setmetatable( + Type, + { + __call = function(____, _self, message) return __TS__New(Type, message) end + } + ) +end +Error = __TS__InitErrorClass( + _G, + (function() + local ____ = __TS__Class() + ____.name = "" + function ____.prototype.____constructor(self, message) + if message == nil then + message = "" + end + self.message = message + self.name = "Error" + self.stack = __TS__GetErrorStack(_G, self.constructor.new) + local metatable = getmetatable(self) + if not metatable.__errorToStringPatched then + metatable.__errorToStringPatched = true + metatable.__tostring = __TS__WrapErrorToString(_G, metatable.__tostring) + end + end + function ____.prototype.__tostring(self) + return (((self.message ~= "") and (function() return (self.name .. ": ") .. self.message end)) or (function() return self.name end))() + end + return ____ + end)(), + "Error" +) +for ____, errorName in ipairs({"RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError"}) do + _G[errorName] = __TS__InitErrorClass( + _G, + (function() + local ____ = __TS__Class() + ____.name = ____.name + __TS__ClassExtends(____, Error) + function ____.prototype.____constructor(self, ...) + Error.prototype.____constructor(self, ...) + self.name = errorName + end + return ____ + end)(), + errorName + ) end -function mod.equals(a, b) - return deepcompare(a, b, true) +function __TS__ObjectAssign(to, ...) + local sources = {...} + if to == nil then + return to + end + for ____, source in ipairs(sources) do + for key in pairs(source) do + to[key] = source[key] + end + end + return to +end + +function __TS__CloneDescriptor(____bindingPattern0) + local enumerable + enumerable = ____bindingPattern0.enumerable + local configurable + configurable = ____bindingPattern0.configurable + local get + get = ____bindingPattern0.get + local set + set = ____bindingPattern0.set + local writable + writable = ____bindingPattern0.writable + local value + value = ____bindingPattern0.value + local descriptor = {enumerable = enumerable == true, configurable = configurable == true} + local hasGetterOrSetter = (get ~= nil) or (set ~= nil) + local hasValueOrWritableAttribute = (writable ~= nil) or (value ~= nil) + if hasGetterOrSetter and hasValueOrWritableAttribute then + error("Invalid property descriptor. Cannot both specify accessors and a value or writable attribute.", 0) + end + if get or set then + descriptor.get = get + descriptor.set = set + else + descriptor.value = value + descriptor.writable = writable == true + end + return descriptor +end + +function ____descriptorIndex(self, key) + local value = rawget(self, key) + if value ~= nil then + return value + end + local metatable = getmetatable(self) + while metatable do + local rawResult = rawget(metatable, key) + if rawResult ~= nil then + return rawResult + end + local descriptors = rawget(metatable, "_descriptors") + if descriptors then + local descriptor = descriptors[key] + if descriptor then + if descriptor.get then + return descriptor.get(self) + end + return descriptor.value + end + end + metatable = getmetatable(metatable) + end +end +function ____descriptorNewindex(self, key, value) + local metatable = getmetatable(self) + while metatable do + local descriptors = rawget(metatable, "_descriptors") + if descriptors then + local descriptor = descriptors[key] + if descriptor then + if descriptor.set then + descriptor.set(self, value) + else + if descriptor.writable == false then + error( + ((("Cannot assign to read only property '" .. key) .. "' of object '") .. tostring(self)) .. "'", + 0 + ) + end + descriptor.value = value + end + return + end + end + metatable = getmetatable(metatable) + end + rawset(self, key, value) +end +function __TS__SetDescriptor(target, key, desc, isPrototype) + if isPrototype == nil then + isPrototype = false + end + local metatable = ((isPrototype and (function() return target end)) or (function() return getmetatable(target) end))() + if not metatable then + metatable = {} + setmetatable(target, metatable) + end + local value = rawget(target, key) + if value ~= nil then + rawset(target, key, nil) + end + if not rawget(metatable, "_descriptors") then + metatable._descriptors = {} + end + local descriptor = __TS__CloneDescriptor(desc) + metatable._descriptors[key] = descriptor + metatable.__index = ____descriptorIndex + metatable.__newindex = ____descriptorNewindex +end + +function __TS__StringAccess(self, index) + if (index >= 0) and (index < #self) then + return string.sub(self, index + 1, index + 1) + end +end + +____radixChars = "0123456789abcdefghijklmnopqrstuvwxyz" +function __TS__NumberToString(self, radix) + if ((((radix == nil) or (radix == 10)) or (self == math.huge)) or (self == -math.huge)) or (self ~= self) then + return tostring(self) + end + radix = math.floor(radix) + if (radix < 2) or (radix > 36) then + error("toString() radix argument must be between 2 and 36", 0) + end + local integer, fraction = math.modf( + math.abs(self) + ) + local result = "" + if radix == 8 then + result = string.format("%o", integer) + elseif radix == 16 then + result = string.format("%x", integer) + else + repeat + do + result = __TS__StringAccess(____radixChars, integer % radix) .. result + integer = math.floor(integer / radix) + end + until not (integer ~= 0) + end + if fraction ~= 0 then + result = result .. "." + local delta = 1e-16 + repeat + do + fraction = fraction * radix + delta = delta * radix + local digit = math.floor(fraction) + result = result .. __TS__StringAccess(____radixChars, digit) + fraction = fraction - digit + end + until not (fraction >= delta) + end + if self < 0 then + result = "-" .. result + end + return result +end + +function __TS__InstanceOf(obj, classTbl) + if type(classTbl) ~= "table" then + error("Right-hand side of 'instanceof' is not an object", 0) + end + if classTbl[Symbol.hasInstance] ~= nil then + return not (not classTbl[Symbol.hasInstance](classTbl, obj)) + end + if type(obj) == "table" then + local luaClass = obj.constructor + while luaClass ~= nil do + if luaClass == classTbl then + return true + end + luaClass = luaClass.____super + end + end + return false +end + +function __TS__IteratorGeneratorStep(self) + local co = self.____coroutine + local status, value = coroutine.resume(co) + if not status then + error(value, 0) + end + if coroutine.status(co) == "dead" then + return + end + return true, value +end +function __TS__IteratorIteratorStep(self) + local result = self:next() + if result.done then + return + end + return true, result.value +end +function __TS__IteratorStringStep(self, index) + index = index + 1 + if index > #self then + return + end + return index, string.sub(self, index, index) +end +function __TS__Iterator(iterable) + if type(iterable) == "string" then + return __TS__IteratorStringStep, iterable, 0 + elseif iterable.____coroutine ~= nil then + return __TS__IteratorGeneratorStep, iterable + elseif iterable[Symbol.iterator] then + local iterator = iterable[Symbol.iterator](iterable) + return __TS__IteratorIteratorStep, iterator + else + return ipairs(iterable) + end +end + +WeakMap = (function() + local WeakMap = __TS__Class() + WeakMap.name = "WeakMap" + function WeakMap.prototype.____constructor(self, entries) + self[Symbol.toStringTag] = "WeakMap" + self.items = {} + setmetatable(self.items, {__mode = "k"}) + if entries == nil then + return + end + local iterable = entries + if iterable[Symbol.iterator] then + local iterator = iterable[Symbol.iterator](iterable) + while true do + local result = iterator:next() + if result.done then + break + end + local value = result.value + self.items[value[1]] = value[2] + end + else + for ____, kvp in ipairs(entries) do + self.items[kvp[1]] = kvp[2] + end + end + end + function WeakMap.prototype.delete(self, key) + local contains = self:has(key) + self.items[key] = nil + return contains + end + function WeakMap.prototype.get(self, key) + return self.items[key] + end + function WeakMap.prototype.has(self, key) + return self.items[key] ~= nil + end + function WeakMap.prototype.set(self, key, value) + self.items[key] = value + return self + end + WeakMap[Symbol.species] = WeakMap + return WeakMap +end)() + +function __TS__StringCharCodeAt(self, index) + if index ~= index then + index = 0 + end + if index < 0 then + return 0 / 0 + end + return string.byte(self, index + 1) or (0 / 0) +end + +function __TS__ArrayReduce(arr, callbackFn, ...) + local len = #arr + local k = 0 + local accumulator = nil + if select("#", ...) ~= 0 then + accumulator = select(1, ...) + elseif len > 0 then + accumulator = arr[1] + k = 1 + else + error("Reduce of empty array with no initial value", 0) + end + for i = k, len - 1 do + accumulator = callbackFn(_G, accumulator, arr[i + 1], i, arr) + end + return accumulator +end + +function __TS__TypeOf(value) + local luaType = type(value) + if luaType == "table" then + return "object" + elseif luaType == "nil" then + return "undefined" + else + return luaType + end +end + +function __TS__ObjectValues(obj) + local result = {} + for key in pairs(obj) do + result[#result + 1] = obj[key] + end + return result +end + +function __TS__ArrayMap(arr, callbackfn) + local newArray = {} + do + local i = 0 + while i < #arr do + newArray[i + 1] = callbackfn(_G, arr[i + 1], i, arr) + i = i + 1 + end + end + return newArray +end + +function __TS__ObjectKeys(obj) + local result = {} + for key in pairs(obj) do + result[#result + 1] = key + end + return result +end + +function __TS__ArraySort(arr, compareFn) + if compareFn ~= nil then + table.sort( + arr, + function(a, b) return compareFn(_G, a, b) < 0 end + ) + else + table.sort(arr) + end + return arr +end + +function __TS__StringReplace(source, searchValue, replaceValue) + searchValue = string.gsub(searchValue, "[%%%(%)%.%+%-%*%?%[%^%$]", "%%%1") + if type(replaceValue) == "string" then + replaceValue = string.gsub(replaceValue, "%%", "%%%%") + local result = string.gsub(source, searchValue, replaceValue, 1) + return result + else + local result = string.gsub( + source, + searchValue, + function(match) return replaceValue(_G, match) end, + 1 + ) + return result + end +end + +function __TS__ArraySplice(list, ...) + local len = #list + local actualArgumentCount = select("#", ...) + local start = select(1, ...) + local deleteCount = select(2, ...) + local actualStart + if start < 0 then + actualStart = math.max(len + start, 0) + else + actualStart = math.min(start, len) + end + local itemCount = math.max(actualArgumentCount - 2, 0) + local actualDeleteCount + if actualArgumentCount == 0 then + actualDeleteCount = 0 + elseif actualArgumentCount == 1 then + actualDeleteCount = len - actualStart + else + actualDeleteCount = math.min( + math.max(deleteCount or 0, 0), + len - actualStart + ) + end + local out = {} + do + local k = 0 + while k < actualDeleteCount do + local from = actualStart + k + if list[from + 1] then + out[k + 1] = list[from + 1] + end + k = k + 1 + end + end + if itemCount < actualDeleteCount then + do + local k = actualStart + while k < (len - actualDeleteCount) do + local from = k + actualDeleteCount + local to = k + itemCount + if list[from + 1] then + list[to + 1] = list[from + 1] + else + list[to + 1] = nil + end + k = k + 1 + end + end + do + local k = len + while k > ((len - actualDeleteCount) + itemCount) do + list[k] = nil + k = k - 1 + end + end + elseif itemCount > actualDeleteCount then + do + local k = len - actualDeleteCount + while k > actualStart do + local from = (k + actualDeleteCount) - 1 + local to = (k + itemCount) - 1 + if list[from + 1] then + list[to + 1] = list[from + 1] + else + list[to + 1] = nil + end + k = k - 1 + end + end + end + local j = actualStart + for i = 3, actualArgumentCount do + list[j + 1] = select(i, ...) + j = j + 1 + end + do + local k = #list - 1 + while k >= ((len - actualDeleteCount) + itemCount) do + list[k + 1] = nil + k = k - 1 + end + end + return out +end + +function __TS__ArrayConcat(arr1, ...) + local args = {...} + local out = {} + for ____, val in ipairs(arr1) do + out[#out + 1] = val + end + for ____, arg in ipairs(args) do + if __TS__ArrayIsArray(arg) then + local argAsArray = arg + for ____, val in ipairs(argAsArray) do + out[#out + 1] = val + end + else + out[#out + 1] = arg + end + end + return out +end + +local ____exports = {} +local isComparable, isEquatable, isHashable, equalObjects, compareObjects +function ____exports.isArrayLike(self, x) + return __TS__ArrayIsArray(x) or ArrayBuffer:isView(x) +end +function isComparable(self, x) + return type(x.CompareTo) == "function" +end +function isEquatable(self, x) + return type(x.Equals) == "function" +end +function isHashable(self, x) + return type(x.GetHashCode) == "function" +end +function ____exports.dateOffset(self, date) + local date1 = date + return ((type(date1.offset) == "number") and date1.offset) or (((date.kind == 1) and 0) or (date:getTimezoneOffset() * -60000)) +end +function ____exports.stringHash(self, s) + local i = 0 + local h = 5381 + local len = #s + while i < len do + h = BitXOR((h * 33), __TS__StringCharCodeAt( + s, + (function() + local ____tmp = i + i = ____tmp + 1 + return ____tmp + end)() + )) + end + return h +end +function ____exports.numberHash(self, x) + return BitOR((x * 2654435761), 0) +end +function ____exports.combineHashCodes(self, hashes) + if #hashes == 0 then + return 0 + end + return __TS__ArrayReduce( + hashes, + function(____, h1, h2) + return BitXOR((lshift(h1, 5) + h1), h2) + end + ) +end +function ____exports.dateHash(self, x) + return x:getTime() +end +function ____exports.arrayHash(self, x) + local len = x.length + local hashes = __TS__New(Array, len) + do + local i = 0 + while i < len do + hashes[i + 1] = ____exports.structuralHash(nil, x[i]) + i = i + 1 + end + end + return ____exports.combineHashCodes(nil, hashes) +end +function ____exports.structuralHash(self, x) + if x == nil then + return 0 + end + local ____switch68 = __TS__TypeOf(x) + if ____switch68 == "boolean" then + goto ____switch68_case_0 + elseif ____switch68 == "number" then + goto ____switch68_case_1 + elseif ____switch68 == "string" then + goto ____switch68_case_2 + end + goto ____switch68_case_default + ::____switch68_case_0:: + do + return (x and 1) or 0 + end + ::____switch68_case_1:: + do + return ____exports.numberHash(nil, x) + end + ::____switch68_case_2:: + do + return ____exports.stringHash(nil, x) + end + ::____switch68_case_default:: + do + do + if isHashable(nil, x) then + return x:GetHashCode() + elseif ____exports.isArrayLike(nil, x) then + return ____exports.arrayHash(nil, x) + elseif __TS__InstanceOf(x, Date) then + return ____exports.dateHash(nil, x) + elseif Object:getPrototypeOf(x).constructor == Object then + local hashes = __TS__ArrayMap( + __TS__ObjectValues(x), + function(____, v) return ____exports.structuralHash(nil, v) end + ) + return ____exports.combineHashCodes(nil, hashes) + else + return ____exports.numberHash( + nil, + ____exports.ObjectRef:id(x) + ) + end + end + end + ::____switch68_end:: +end +function ____exports.equalArraysWith(self, x, y, eq) + if x == nil then + return y == nil + end + if y == nil then + return false + end + if x.length ~= y.length then + return false + end + do + local i = 0 + while i < x.length do + if not eq(nil, x[i], y[i]) then + return false + end + i = i + 1 + end + end + return true +end +function ____exports.equalArrays(self, x, y) + return ____exports.equalArraysWith(nil, x, y, ____exports.equals) +end +function equalObjects(self, x, y) + local xKeys = __TS__ObjectKeys(x) + local yKeys = __TS__ObjectKeys(y) + if #xKeys ~= #yKeys then + return false + end + __TS__ArraySort(xKeys) + __TS__ArraySort(yKeys) + do + local i = 0 + while i < #xKeys do + if (xKeys[i + 1] ~= yKeys[i + 1]) or (not ____exports.equals(nil, x[xKeys[i + 1]], y[yKeys[i + 1]])) then + return false + end + i = i + 1 + end + end + return true +end +function ____exports.equals(self, x, y) + if x == y then + return true + elseif x == nil then + return y == nil + elseif y == nil then + return false + elseif type(x) ~= "table" then + return false + elseif isEquatable(nil, x) then + return x:Equals(y) + elseif ____exports.isArrayLike(nil, x) then + return ____exports.isArrayLike(nil, y) and ____exports.equalArrays(nil, x, y) + elseif __TS__InstanceOf(x, Date) then + return __TS__InstanceOf(y, Date) and (____exports.compareDates(nil, x, y) == 0) + else + return (Object:getPrototypeOf(x).constructor == Object) and equalObjects(nil, x, y) + end +end +function ____exports.compareDates(self, x, y) + local xtime + local ytime + if (x.offset ~= nil) and (y.offset ~= nil) then + xtime = x:getTime() + ytime = y:getTime() + else + xtime = x:getTime() + ____exports.dateOffset(nil, x) + ytime = y:getTime() + ____exports.dateOffset(nil, y) + end + return ((xtime == ytime) and 0) or (((xtime < ytime) and -1) or 1) +end +function ____exports.compareArraysWith(self, x, y, comp) + if x == nil then + return ((y == nil) and 0) or 1 + end + if y == nil then + return -1 + end + if x.length ~= y.length then + return ((x.length < y.length) and -1) or 1 + end + do + local i = 0 + local j = 0 + while i < x.length do + j = comp(nil, x[i], y[i]) + if j ~= 0 then + return j + end + i = i + 1 + end + end + return 0 +end +function ____exports.compareArrays(self, x, y) + return ____exports.compareArraysWith(nil, x, y, ____exports.compare) +end +function compareObjects(self, x, y) + local xKeys = __TS__ObjectKeys(x) + local yKeys = __TS__ObjectKeys(y) + if #xKeys ~= #yKeys then + return ((#xKeys < #yKeys) and -1) or 1 + end + __TS__ArraySort(xKeys) + __TS__ArraySort(yKeys) + do + local i = 0 + local j = 0 + while i < #xKeys do + local key = xKeys[i + 1] + if key ~= yKeys[i + 1] then + return ((key < yKeys[i + 1]) and -1) or 1 + else + j = ____exports.compare(nil, x[key], y[key]) + if j ~= 0 then + return j + end + end + i = i + 1 + end + end + return 0 +end +function ____exports.compare(self, x, y) + if x == y then + return 0 + elseif x == nil then + return ((y == nil) and 0) or -1 + elseif y == nil then + return 1 + elseif type(x) ~= "table" then + return ((x < y) and -1) or 1 + elseif isComparable(nil, x) then + return x:CompareTo(y) + elseif ____exports.isArrayLike(nil, x) then + return (____exports.isArrayLike(nil, y) and ____exports.compareArrays(nil, x, y)) or -1 + elseif __TS__InstanceOf(x, Date) then + return (__TS__InstanceOf(y, Date) and ____exports.compareDates(nil, x, y)) or -1 + else + return ((Object:getPrototypeOf(x).constructor == Object) and compareObjects(nil, x, y)) or -1 + end +end +function ____exports.isIterable(self, x) + return ((x ~= nil) and (type(x) == "table")) and (x[Symbol.iterator] ~= nil) +end +local function isComparer(self, x) + return type(x.Compare) == "function" +end +function ____exports.isDisposable(self, x) + return (x ~= nil) and (type(x.Dispose) == "function") +end +function ____exports.sameConstructor(self, x, y) + return Object:getPrototypeOf(x).constructor == Object:getPrototypeOf(y).constructor +end +____exports.Enumerator = __TS__Class() +local Enumerator = ____exports.Enumerator +Enumerator.name = "Enumerator" +function Enumerator.prototype.____constructor(self, iter) + self.iter = iter +end +Enumerator.prototype["System.Collections.Generic.IEnumerator`1.get_Current"] = function(self) + return self.current +end +Enumerator.prototype["System.Collections.IEnumerator.get_Current"] = function(self) + return self.current +end +Enumerator.prototype["System.Collections.IEnumerator.MoveNext"] = function(self) + local cur = self.iter:next() + self.current = cur.value + return not cur.done +end +Enumerator.prototype["System.Collections.IEnumerator.Reset"] = function(self) + error( + __TS__New(Error, "JS iterators cannot be reset"), + 0 + ) +end +function Enumerator.prototype.Dispose(self) + return +end +function ____exports.getEnumerator(self, o) + return ((type(o.GetEnumerator) == "function") and o:GetEnumerator()) or __TS__New( + ____exports.Enumerator, + o[Symbol.iterator](o) + ) +end +function ____exports.toIterator(self, en) + return { + [Symbol.iterator] = function(self) + return self + end, + next = function(self) + local hasNext = en["System.Collections.IEnumerator.MoveNext"](en) + local current = ((hasNext and (function() return en["System.Collections.IEnumerator.get_Current"](en) end)) or (function() return nil end))() + return {done = not hasNext, value = current} + end + } +end +____exports.Comparer = __TS__Class() +local Comparer = ____exports.Comparer +Comparer.name = "Comparer" +function Comparer.prototype.____constructor(self, f) + self.Compare = f or ____exports.compare +end +function ____exports.comparerFromEqualityComparer(self, comparer) + if isComparer(nil, comparer) then + return __TS__New(____exports.Comparer, comparer.Compare) + else + return __TS__New( + ____exports.Comparer, + function(____, x, y) + local xhash = comparer:GetHashCode(x) + local yhash = comparer:GetHashCode(y) + if xhash == yhash then + return (comparer:Equals(x, y) and 0) or -1 + else + return ((xhash < yhash) and -1) or 1 + end + end + ) + end +end +function ____exports.assertEqual(self, actual, expected, msg) + if not ____exports.equals(nil, actual, expected) then + error( + __TS__ObjectAssign( + __TS__New( + Error, + msg or ((("Expected: " .. tostring(expected)) .. " - Actual: ") .. tostring(actual)) + ), + {actual = actual, expected = expected} + ), + 0 + ) + end +end +function ____exports.assertNotEqual(self, actual, expected, msg) + if ____exports.equals(nil, actual, expected) then + error( + __TS__ObjectAssign( + __TS__New( + Error, + msg or ((("Expected: " .. tostring(expected)) .. " - Actual: ") .. tostring(actual)) + ), + {actual = actual, expected = expected} + ), + 0 + ) + end +end +____exports.Lazy = __TS__Class() +local Lazy = ____exports.Lazy +Lazy.name = "Lazy" +function Lazy.prototype.____constructor(self, factory) + self.factory = factory + self.isValueCreated = false +end +__TS__SetDescriptor( + Lazy.prototype, + "Value", + { + get = function(self) + if not self.isValueCreated then + self.createdValue = self:factory() + self.isValueCreated = true + end + return self.createdValue + end + }, + true +) +__TS__SetDescriptor( + Lazy.prototype, + "IsValueCreated", + { + get = function(self) + return self.isValueCreated + end + }, + true +) +function ____exports.lazyFromValue(self, v) + return __TS__New( + ____exports.Lazy, + function() return v end + ) +end +function ____exports.padWithZeros(self, i, length) + local str = __TS__NumberToString(i, 10) + while #str < length do + str = "0" .. str + end + return str +end +function ____exports.padLeftAndRightWithZeros(self, i, lengthLeft, lengthRight) + local str = __TS__NumberToString(i, 10) + while #str < lengthLeft do + str = "0" .. str + end + while #str < lengthRight do + str = str .. "0" + end + return str +end +function ____exports.int16ToString(self, i, radix) + i = ((((i < 0) and (radix ~= nil)) and (radix ~= 10)) and ((65535 + i) + 1)) or i + return __TS__NumberToString(i, radix) +end +function ____exports.int32ToString(self, i, radix) + i = ((((i < 0) and (radix ~= nil)) and (radix ~= 10)) and ((4294967295 + i) + 1)) or i + return __TS__NumberToString(i, radix) +end +____exports.ObjectRef = __TS__Class() +local ObjectRef = ____exports.ObjectRef +ObjectRef.name = "ObjectRef" +function ObjectRef.prototype.____constructor(self) +end +function ObjectRef.id(self, o) + if not ____exports.ObjectRef.idMap:has(o) then + ____exports.ObjectRef.idMap:set( + o, + (function() + local ____tmp = ____exports.ObjectRef.count + 1 + ____exports.ObjectRef.count = ____tmp + return ____tmp + end)() + ) + end + return ____exports.ObjectRef.idMap:get(o) +end +ObjectRef.idMap = __TS__New(WeakMap) +ObjectRef.count = 0 +function ____exports.physicalHash(self, x) + if x == nil then + return 0 + end + local ____switch58 = __TS__TypeOf(x) + if ____switch58 == "boolean" then + goto ____switch58_case_0 + elseif ____switch58 == "number" then + goto ____switch58_case_1 + elseif ____switch58 == "string" then + goto ____switch58_case_2 + end + goto ____switch58_case_default + ::____switch58_case_0:: + do + return (x and 1) or 0 + end + ::____switch58_case_1:: + do + return ____exports.numberHash(nil, x) + end + ::____switch58_case_2:: + do + return ____exports.stringHash(nil, x) + end + ::____switch58_case_default:: + do + return ____exports.numberHash( + nil, + ____exports.ObjectRef:id(x) + ) + end + ::____switch58_end:: +end +function ____exports.identityHash(self, x) + if x == nil then + return 0 + elseif isHashable(nil, x) then + return x:GetHashCode() + else + return ____exports.physicalHash(nil, x) + end +end +function ____exports.fastStructuralHash(self, x) + return ____exports.stringHash( + nil, + String(nil, x) + ) +end +function ____exports.safeHash(self, x) + return ((x == nil) and 0) or ((isHashable(nil, x) and x:GetHashCode()) or ____exports.numberHash( + nil, + ____exports.ObjectRef:id(x) + )) +end +function ____exports.comparePrimitives(self, x, y) + return ((x == y) and 0) or (((x < y) and -1) or 1) +end +function ____exports.min(self, comparer, x, y) + return ((comparer(nil, x, y) < 0) and x) or y +end +function ____exports.max(self, comparer, x, y) + return ((comparer(nil, x, y) > 0) and x) or y +end +function ____exports.clamp(self, comparer, value, min, max) + return ((comparer(nil, value, min) < 0) and min) or (((comparer(nil, value, max) > 0) and max) or value) +end +function ____exports.createAtom(self, value) + local atom = value + return function(____, value, isSetter) + if not isSetter then + return atom + else + atom = value + return nil + end + end +end +function ____exports.createObj(self, fields) + local obj = {} + for ____, kv in __TS__Iterator(fields) do + obj[kv[1]] = kv[2] + end + return obj +end +function ____exports.jsOptions(self, mutator) + local opts = {} + mutator(nil, opts) + return opts +end +function ____exports.round(self, value, digits) + if digits == nil then + digits = 0 + end + local m = math.pow(10, digits) + local n = ((digits and (value * m)) or value):toFixed(8) + local i = math.floor(n) + local f = n - i + local e = 1e-8 + local r = (((f > (0.5 - e)) and (f < (0.5 + e))) and ((((i % 2) == 0) and i) or (i + 1))) or math.floor(n + 0.5) + return (digits and (r / m)) or r +end +function ____exports.sign(self, x) + return ((x > 0) and 1) or (((x < 0) and -1) or 0) +end +function ____exports.randomNext(self, min, max) + return math.floor( + math.random() * (max - min) + ) + min +end +function ____exports.randomBytes(self, buffer) + if buffer == nil then + error( + __TS__New(Error, "Buffer cannot be null"), + 0 + ) + end + do + local i = 0 + while i < buffer.length do + local r = math.floor( + math.random() * 281474976710656 + ) + local rhi = math.floor(r / 16777216) + do + local j = 0 + while (j < 6) and ((i + j) < buffer.length) do + if j == 3 then + r = rhi + end + buffer[i + j] = BitAND(r, 255) + r = rshift(r, 8) + j = j + 1 + end + end + i = i + 6 + end + end +end +function ____exports.unescapeDataString(self, s) + return decodeURIComponent( + nil, + __TS__StringReplace(s, nil, "%20") + ) +end +function ____exports.escapeDataString(self, s) + return __TS__StringReplace( + __TS__StringReplace( + __TS__StringReplace( + __TS__StringReplace( + __TS__StringReplace( + encodeURIComponent(nil, s), + nil, + "%21" + ), + nil, + "%27" + ), + nil, + "%28" + ), + nil, + "%29" + ), + nil, + "%2A" + ) +end +function ____exports.escapeUriString(self, s) + return encodeURI(nil, s) +end +function ____exports.count(self, col) + if ____exports.isArrayLike(nil, col) then + return col.length + else + local count = 0 + for ____, _ in __TS__Iterator(col) do + count = count + 1 + end + return count + end +end +function ____exports.clear(self, col) + if ____exports.isArrayLike(nil, col) then + __TS__ArraySplice(col, 0) + else + col:clear() + end +end +local CURRIED_KEY = "__CURRIED__" +function ____exports.uncurry(self, arity, f) + if (f == nil) or (f.length > 1) then + return f + end + local uncurriedFn + local ____switch154 = arity + if ____switch154 == 2 then + goto ____switch154_case_0 + elseif ____switch154 == 3 then + goto ____switch154_case_1 + elseif ____switch154 == 4 then + goto ____switch154_case_2 + elseif ____switch154 == 5 then + goto ____switch154_case_3 + elseif ____switch154 == 6 then + goto ____switch154_case_4 + elseif ____switch154 == 7 then + goto ____switch154_case_5 + elseif ____switch154 == 8 then + goto ____switch154_case_6 + end + goto ____switch154_case_default + ::____switch154_case_0:: + do + uncurriedFn = function(____, a1, a2) return f(nil, a1)(nil, a2) end + goto ____switch154_end + end + ::____switch154_case_1:: + do + uncurriedFn = function(____, a1, a2, a3) return f(nil, a1)(nil, a2)(nil, a3) end + goto ____switch154_end + end + ::____switch154_case_2:: + do + uncurriedFn = function(____, a1, a2, a3, a4) return f(nil, a1)(nil, a2)(nil, a3)(nil, a4) end + goto ____switch154_end + end + ::____switch154_case_3:: + do + uncurriedFn = function(____, a1, a2, a3, a4, a5) return f(nil, a1)(nil, a2)(nil, a3)(nil, a4)(nil, a5) end + goto ____switch154_end + end + ::____switch154_case_4:: + do + uncurriedFn = function(____, a1, a2, a3, a4, a5, a6) return f(nil, a1)(nil, a2)(nil, a3)(nil, a4)(nil, a5)(nil, a6) end + goto ____switch154_end + end + ::____switch154_case_5:: + do + uncurriedFn = function(____, a1, a2, a3, a4, a5, a6, a7) return f(nil, a1)(nil, a2)(nil, a3)(nil, a4)(nil, a5)(nil, a6)(nil, a7) end + goto ____switch154_end + end + ::____switch154_case_6:: + do + uncurriedFn = function(____, a1, a2, a3, a4, a5, a6, a7, a8) return f(nil, a1)(nil, a2)(nil, a3)(nil, a4)(nil, a5)(nil, a6)(nil, a7)(nil, a8) end + goto ____switch154_end + end + ::____switch154_case_default:: + do + error( + __TS__New( + Error, + "Uncurrying to more than 8-arity is not supported: " .. tostring(arity) + ), + 0 + ) + end + ::____switch154_end:: + uncurriedFn[CURRIED_KEY] = f + return uncurriedFn +end +function ____exports.curry(self, arity, f) + if (f == nil) or (f.length == 1) then + return f + end + if f[CURRIED_KEY] ~= nil then + return f[CURRIED_KEY] + end + local ____switch165 = arity + if ____switch165 == 2 then + goto ____switch165_case_0 + elseif ____switch165 == 3 then + goto ____switch165_case_1 + elseif ____switch165 == 4 then + goto ____switch165_case_2 + elseif ____switch165 == 5 then + goto ____switch165_case_3 + elseif ____switch165 == 6 then + goto ____switch165_case_4 + elseif ____switch165 == 7 then + goto ____switch165_case_5 + elseif ____switch165 == 8 then + goto ____switch165_case_6 + end + goto ____switch165_case_default + ::____switch165_case_0:: + do + return function(____, a1) return function(____, a2) return f(nil, a1, a2) end end + end + ::____switch165_case_1:: + do + return function(____, a1) return function(____, a2) return function(____, a3) return f(nil, a1, a2, a3) end end end + end + ::____switch165_case_2:: + do + return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return f(nil, a1, a2, a3, a4) end end end end + end + ::____switch165_case_3:: + do + return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return f(nil, a1, a2, a3, a4, a5) end end end end end + end + ::____switch165_case_4:: + do + return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return function(____, a6) return f(nil, a1, a2, a3, a4, a5, a6) end end end end end end + end + ::____switch165_case_5:: + do + return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return function(____, a6) return function(____, a7) return f(nil, a1, a2, a3, a4, a5, a6, a7) end end end end end end end + end + ::____switch165_case_6:: + do + return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return function(____, a6) return function(____, a7) return function(____, a8) return f(nil, a1, a2, a3, a4, a5, a6, a7, a8) end end end end end end end end + end + ::____switch165_case_default:: + do + error( + __TS__New( + Error, + "Currying to more than 8-arity is not supported: " .. tostring(arity) + ), + 0 + ) + end + ::____switch165_end:: +end +function ____exports.checkArity(self, arity, f) + return ((f.length > arity) and (function(____, ...) + local args1 = {...} + return function(____, ...) + local args2 = {...} + return f:apply( + nil, + __TS__ArrayConcat(args1, args2) + ) + end + end)) or f +end +function ____exports.partialApply(self, arity, f, args) + if f == nil then + return nil + elseif f[CURRIED_KEY] ~= nil then + f = f[CURRIED_KEY] + do + local i = 0 + while i < #args do + f = f(nil, args[i + 1]) + i = i + 1 + end + end + return f + else + local ____switch209 = arity + if ____switch209 == 1 then + goto ____switch209_case_0 + elseif ____switch209 == 2 then + goto ____switch209_case_1 + elseif ____switch209 == 3 then + goto ____switch209_case_2 + elseif ____switch209 == 4 then + goto ____switch209_case_3 + elseif ____switch209 == 5 then + goto ____switch209_case_4 + elseif ____switch209 == 6 then + goto ____switch209_case_5 + elseif ____switch209 == 7 then + goto ____switch209_case_6 + elseif ____switch209 == 8 then + goto ____switch209_case_7 + end + goto ____switch209_case_default + ::____switch209_case_0:: + do + return function(____, a1) return f:apply( + nil, + __TS__ArrayConcat(args, {a1}) + ) end + end + ::____switch209_case_1:: + do + return function(____, a1) return function(____, a2) return f:apply( + nil, + __TS__ArrayConcat(args, {a1, a2}) + ) end end + end + ::____switch209_case_2:: + do + return function(____, a1) return function(____, a2) return function(____, a3) return f:apply( + nil, + __TS__ArrayConcat(args, {a1, a2, a3}) + ) end end end + end + ::____switch209_case_3:: + do + return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return f:apply( + nil, + __TS__ArrayConcat(args, {a1, a2, a3, a4}) + ) end end end end + end + ::____switch209_case_4:: + do + return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return f:apply( + nil, + __TS__ArrayConcat(args, {a1, a2, a3, a4, a5}) + ) end end end end end + end + ::____switch209_case_5:: + do + return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return function(____, a6) return f:apply( + nil, + __TS__ArrayConcat(args, {a1, a2, a3, a4, a5, a6}) + ) end end end end end end + end + ::____switch209_case_6:: + do + return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return function(____, a6) return function(____, a7) return f:apply( + nil, + __TS__ArrayConcat(args, {a1, a2, a3, a4, a5, a6, a7}) + ) end end end end end end end + end + ::____switch209_case_7:: + do + return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return function(____, a6) return function(____, a7) return function(____, a8) return f:apply( + nil, + __TS__ArrayConcat(args, {a1, a2, a3, a4, a5, a6, a7, a8}) + ) end end end end end end end end + end + ::____switch209_case_default:: + do + error( + __TS__New( + Error, + "Partially applying to more than 8-arity is not supported: " .. tostring(arity) + ), + 0 + ) + end + ::____switch209_end:: + end +end +function ____exports.mapCurriedArgs(self, fn, mappings) + local function mapArg(self, fn, arg, mappings, idx) + local mapping = mappings[idx + 1] + if mapping ~= 0 then + local expectedArity = mapping[1] + local actualArity = mapping[2] + if expectedArity > 1 then + arg = ____exports.curry(nil, expectedArity, arg) + end + if actualArity > 1 then + arg = ____exports.uncurry(nil, actualArity, arg) + end + end + local res = fn(nil, arg) + if (idx + 1) == #mappings then + return res + else + return function(____, arg) return mapArg(nil, res, arg, mappings, idx + 1) end + end + end + return function(____, arg) return mapArg(nil, fn, arg, mappings, 0) end end -return mod \ No newline at end of file +return ____exports \ No newline at end of file diff --git a/tests/Lua/Util.fs b/tests/Lua/Util.fs index 93858d0c20..3e73f61d40 100644 --- a/tests/Lua/Util.fs +++ b/tests/Lua/Util.fs @@ -8,9 +8,9 @@ module Testing = open Fable.Core.PyInterop type Assert = - [] + [] static member AreEqual(actual: 'T, expected: 'T, ?msg: string): unit = nativeOnly - [] + [] static member NotEqual(actual: 'T, expected: 'T, ?msg: string): unit = nativeOnly let equal expected actual: unit = Assert.AreEqual(actual, expected) diff --git a/tests/Lua/luaunit.lua b/tests/Lua/luaunit.lua new file mode 100644 index 0000000000..937c0f90a6 --- /dev/null +++ b/tests/Lua/luaunit.lua @@ -0,0 +1,3372 @@ +--[[ + luaunit.lua +Description: A unit testing framework +Homepage: https://github.com/bluebird75/luaunit +Development by Philippe Fremy +Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit) +License: BSD License, see LICENSE.txt +]]-- + +require("math") +local M={} + +-- private exported functions (for testing) +M.private = {} + +M.VERSION='3.4' +M._VERSION=M.VERSION -- For LuaUnit v2 compatibility + +-- a version which distinguish between regular Lua and LuaJit +M._LUAVERSION = (jit and jit.version) or _VERSION + +--[[ Some people like assertEquals( actual, expected ) and some people prefer +assertEquals( expected, actual ). +]]-- +M.ORDER_ACTUAL_EXPECTED = true +M.PRINT_TABLE_REF_IN_ERROR_MSG = false +M.LINE_LENGTH = 80 +M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items +M.LIST_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items + +-- this setting allow to remove entries from the stack-trace, for +-- example to hide a call to a framework which would be calling luaunit +M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE = 0 + +--[[ EPS is meant to help with Lua's floating point math in simple corner +cases like almostEquals(1.1-0.1, 1), which may not work as-is (e.g. on numbers +with rational binary representation) if the user doesn't provide some explicit +error margin. +The default margin used by almostEquals() in such cases is EPS; and since +Lua may be compiled with different numeric precisions (single vs. double), we +try to select a useful default for it dynamically. Note: If the initial value +is not acceptable, it can be changed by the user to better suit specific needs. +See also: https://en.wikipedia.org/wiki/Machine_epsilon +]] +M.EPS = 2^-52 -- = machine epsilon for "double", ~2.22E-16 +if math.abs(1.1 - 1 - 0.1) > M.EPS then + -- rounding error is above EPS, assume single precision + M.EPS = 2^-23 -- = machine epsilon for "float", ~1.19E-07 +end + +-- set this to false to debug luaunit +local STRIP_LUAUNIT_FROM_STACKTRACE = true + +M.VERBOSITY_DEFAULT = 10 +M.VERBOSITY_LOW = 1 +M.VERBOSITY_QUIET = 0 +M.VERBOSITY_VERBOSE = 20 +M.DEFAULT_DEEP_ANALYSIS = nil +M.FORCE_DEEP_ANALYSIS = true +M.DISABLE_DEEP_ANALYSIS = false + +-- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values +-- EXPORT_ASSERT_TO_GLOBALS = true + +-- we need to keep a copy of the script args before it is overriden +local cmdline_argv = rawget(_G, "arg") + +M.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests +M.SUCCESS_PREFIX = 'LuaUnit test SUCCESS: ' -- prefix string for successful tests finished early +M.SKIP_PREFIX = 'LuaUnit test SKIP: ' -- prefix string for skipped tests + + + +M.USAGE=[[Usage: lua [options] [testname1 [testname2] ... ] +Options: + -h, --help: Print this help + --version: Print version information + -v, --verbose: Increase verbosity + -q, --quiet: Set verbosity to minimum + -e, --error: Stop on first error + -f, --failure: Stop on first failure or error + -s, --shuffle: Shuffle tests before running them + -o, --output OUTPUT: Set output type to OUTPUT + Possible values: text, tap, junit, nil + -n, --name NAME: For junit only, mandatory name of xml file + -r, --repeat NUM: Execute all tests NUM times, e.g. to trig the JIT + -p, --pattern PATTERN: Execute all test names matching the Lua PATTERN + May be repeated to include several patterns + Make sure you escape magic chars like +? with % + -x, --exclude PATTERN: Exclude all test names matching the Lua PATTERN + May be repeated to exclude several patterns + Make sure you escape magic chars like +? with % + testname1, testname2, ... : tests to run in the form of testFunction, + TestClass or TestClass.testMethod +You may also control LuaUnit options with the following environment variables: +* LUAUNIT_OUTPUT: same as --output +* LUAUNIT_JUNIT_FNAME: same as --name ]] + +---------------------------------------------------------------- +-- +-- general utility functions +-- +---------------------------------------------------------------- + +--[[ Note on catching exit +I have seen the case where running a big suite of test cases and one of them would +perform a os.exit(0), making the outside world think that the full test suite was executed +successfully. +This is an attempt to mitigate this problem: we override os.exit() to now let a test +exit the framework while we are running. When we are not running, it behaves normally. +]] + +M.oldOsExit = os.exit +os.exit = function(...) + if M.LuaUnit and #M.LuaUnit.instances ~= 0 then + local msg = [[You are trying to exit but there is still a running instance of LuaUnit. +LuaUnit expects to run until the end before exiting with a complete status of successful/failed tests. +To force exit LuaUnit while running, please call before os.exit (assuming lu is the luaunit module loaded): + lu.unregisterCurrentSuite() +]] + M.private.error_fmt(2, msg) + end + M.oldOsExit(...) +end + +local function pcall_or_abort(func, ...) + -- unpack is a global function for Lua 5.1, otherwise use table.unpack + local unpack = rawget(_G, "unpack") or table.unpack + local result = {pcall(func, ...)} + if not result[1] then + -- an error occurred + print(result[2]) -- error message + print() + print(M.USAGE) + os.exit(-1) + end + return unpack(result, 2) +end + +local crossTypeOrdering = { + number = 1, boolean = 2, string = 3, table = 4, other = 5 +} +local crossTypeComparison = { + number = function(a, b) return a < b end, + string = function(a, b) return a < b end, + other = function(a, b) return tostring(a) < tostring(b) end, +} + +local function crossTypeSort(a, b) + local type_a, type_b = type(a), type(b) + if type_a == type_b then + local func = crossTypeComparison[type_a] or crossTypeComparison.other + return func(a, b) + end + type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other + type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other + return type_a < type_b +end + +local function __genSortedIndex( t ) + -- Returns a sequence consisting of t's keys, sorted. + local sortedIndex = {} + + for key,_ in pairs(t) do + table.insert(sortedIndex, key) + end + + table.sort(sortedIndex, crossTypeSort) + return sortedIndex +end +M.private.__genSortedIndex = __genSortedIndex + +local function sortedNext(state, control) + -- Equivalent of the next() function of table iteration, but returns the + -- keys in sorted order (see __genSortedIndex and crossTypeSort). + -- The state is a temporary variable during iteration and contains the + -- sorted key table (state.sortedIdx). It also stores the last index (into + -- the keys) used by the iteration, to find the next one quickly. + local key + + --print("sortedNext: control = "..tostring(control) ) + if control == nil then + -- start of iteration + state.count = #state.sortedIdx + state.lastIdx = 1 + key = state.sortedIdx[1] + return key, state.t[key] + end + + -- normally, we expect the control variable to match the last key used + if control ~= state.sortedIdx[state.lastIdx] then + -- strange, we have to find the next value by ourselves + -- the key table is sorted in crossTypeSort() order! -> use bisection + local lower, upper = 1, state.count + repeat + state.lastIdx = math.modf((lower + upper) / 2) + key = state.sortedIdx[state.lastIdx] + if key == control then + break -- key found (and thus prev index) + end + if crossTypeSort(key, control) then + -- key < control, continue search "right" (towards upper bound) + lower = state.lastIdx + 1 + else + -- key > control, continue search "left" (towards lower bound) + upper = state.lastIdx - 1 + end + until lower > upper + if lower > upper then -- only true if the key wasn't found, ... + state.lastIdx = state.count -- ... so ensure no match in code below + end + end + + -- proceed by retrieving the next value (or nil) from the sorted keys + state.lastIdx = state.lastIdx + 1 + key = state.sortedIdx[state.lastIdx] + if key then + return key, state.t[key] + end + + -- getting here means returning `nil`, which will end the iteration +end + +local function sortedPairs(tbl) + -- Equivalent of the pairs() function on tables. Allows to iterate in + -- sorted order. As required by "generic for" loops, this will return the + -- iterator (function), an "invariant state", and the initial control value. + -- (see http://www.lua.org/pil/7.2.html) + return sortedNext, {t = tbl, sortedIdx = __genSortedIndex(tbl)}, nil +end +M.private.sortedPairs = sortedPairs + +-- seed the random with a strongly varying seed +math.randomseed(math.floor(os.clock()*1E11)) + +local function randomizeTable( t ) + -- randomize the item orders of the table t + for i = #t, 2, -1 do + local j = math.random(i) + if i ~= j then + t[i], t[j] = t[j], t[i] + end + end +end +M.private.randomizeTable = randomizeTable + +local function strsplit(delimiter, text) +-- Split text into a list consisting of the strings in text, separated +-- by strings matching delimiter (which may _NOT_ be a pattern). +-- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores") + if delimiter == "" or delimiter == nil then -- this would result in endless loops + error("delimiter is nil or empty string!") + end + if text == nil then + return nil + end + + local list, pos, first, last = {}, 1 + while true do + first, last = text:find(delimiter, pos, true) + if first then -- found? + table.insert(list, text:sub(pos, first - 1)) + pos = last + 1 + else + table.insert(list, text:sub(pos)) + break + end + end + return list +end +M.private.strsplit = strsplit + +local function hasNewLine( s ) + -- return true if s has a newline + return (string.find(s, '\n', 1, true) ~= nil) +end +M.private.hasNewLine = hasNewLine + +local function prefixString( prefix, s ) + -- Prefix all the lines of s with prefix + return prefix .. string.gsub(s, '\n', '\n' .. prefix) +end +M.private.prefixString = prefixString + +local function strMatch(s, pattern, start, final ) + -- return true if s matches completely the pattern from index start to index end + -- return false in every other cases + -- if start is nil, matches from the beginning of the string + -- if final is nil, matches to the end of the string + start = start or 1 + final = final or string.len(s) + + local foundStart, foundEnd = string.find(s, pattern, start, false) + return foundStart == start and foundEnd == final +end +M.private.strMatch = strMatch + +local function patternFilter(patterns, expr) + -- Run `expr` through the inclusion and exclusion rules defined in patterns + -- and return true if expr shall be included, false for excluded. + -- Inclusion pattern are defined as normal patterns, exclusions + -- patterns start with `!` and are followed by a normal pattern + + -- result: nil = UNKNOWN (not matched yet), true = ACCEPT, false = REJECT + -- default: true if no explicit "include" is found, set to false otherwise + local default, result = true, nil + + if patterns ~= nil then + for _, pattern in ipairs(patterns) do + local exclude = pattern:sub(1,1) == '!' + if exclude then + pattern = pattern:sub(2) + else + -- at least one include pattern specified, a match is required + default = false + end + -- print('pattern: ',pattern) + -- print('exclude: ',exclude) + -- print('default: ',default) + + if string.find(expr, pattern) then + -- set result to false when excluding, true otherwise + result = not exclude + end + end + end + + if result ~= nil then + return result + end + return default +end +M.private.patternFilter = patternFilter + +local function xmlEscape( s ) + -- Return s escaped for XML attributes + -- escapes table: + -- " " + -- ' ' + -- < < + -- > > + -- & & + + return string.gsub( s, '.', { + ['&'] = "&", + ['"'] = """, + ["'"] = "'", + ['<'] = "<", + ['>'] = ">", + } ) +end +M.private.xmlEscape = xmlEscape + +local function xmlCDataEscape( s ) + -- Return s escaped for CData section, escapes: "]]>" + return string.gsub( s, ']]>', ']]>' ) +end +M.private.xmlCDataEscape = xmlCDataEscape + + +local function lstrip( s ) + --[[Return s with all leading white spaces and tabs removed]] + local idx = 0 + while idx < s:len() do + idx = idx + 1 + local c = s:sub(idx,idx) + if c ~= ' ' and c ~= '\t' then + break + end + end + return s:sub(idx) +end +M.private.lstrip = lstrip + +local function extractFileLineInfo( s ) + --[[ From a string in the form "(leading spaces) dir1/dir2\dir3\file.lua:linenb: msg" + Return the "file.lua:linenb" information + ]] + local s2 = lstrip(s) + local firstColon = s2:find(':', 1, true) + if firstColon == nil then + -- string is not in the format file:line: + return s + end + local secondColon = s2:find(':', firstColon+1, true) + if secondColon == nil then + -- string is not in the format file:line: + return s + end + + return s2:sub(1, secondColon-1) +end +M.private.extractFileLineInfo = extractFileLineInfo + + +local function stripLuaunitTrace2( stackTrace, errMsg ) + --[[ + -- Example of a traceback: + < + [C]: in function 'xpcall' + ./luaunit.lua:1449: in function 'protectedCall' + ./luaunit.lua:1508: in function 'execOneFunction' + ./luaunit.lua:1596: in function 'runSuiteByInstances' + ./luaunit.lua:1660: in function 'runSuiteByNames' + ./luaunit.lua:1736: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + Other example: + < + [C]: in function 'xpcall' + ./luaunit.lua:1517: in function 'protectedCall' + ./luaunit.lua:1578: in function 'execOneFunction' + ./luaunit.lua:1677: in function 'runSuiteByInstances' + ./luaunit.lua:1730: in function 'runSuiteByNames' + ./luaunit.lua:1806: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + < + [C]: in function 'xpcall' + luaunit2/luaunit.lua:1532: in function 'protectedCall' + luaunit2/luaunit.lua:1591: in function 'execOneFunction' + luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances' + luaunit2/luaunit.lua:1743: in function 'runSuiteByNames' + luaunit2/luaunit.lua:1819: in function 'runSuite' + luaunit2/example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + -- first line is "stack traceback": KEEP + -- next line may be luaunit line: REMOVE + -- next lines are call in the program under testOk: REMOVE + -- next lines are calls from luaunit to call the program under test: KEEP + -- Strategy: + -- keep first line + -- remove lines that are part of luaunit + -- kepp lines until we hit a luaunit line + The strategy for stripping is: + * keep first line "stack traceback:" + * part1: + * analyse all lines of the stack from bottom to top of the stack (first line to last line) + * extract the "file:line:" part of the line + * compare it with the "file:line" part of the error message + * if it does not match strip the line + * if it matches, keep the line and move to part 2 + * part2: + * anything NOT starting with luaunit.lua is the interesting part of the stack trace + * anything starting again with luaunit.lua is part of the test launcher and should be stripped out + ]] + + local function isLuaunitInternalLine( s ) + -- return true if line of stack trace comes from inside luaunit + return s:find('[/\\]luaunit%.lua:%d+: ') ~= nil + end + + -- print( '<<'..stackTrace..'>>' ) + + local t = strsplit( '\n', stackTrace ) + -- print( prettystr(t) ) + + local idx = 2 + + local errMsgFileLine = extractFileLineInfo(errMsg) + -- print('emfi="'..errMsgFileLine..'"') + + -- remove lines that are still part of luaunit + while t[idx] and extractFileLineInfo(t[idx]) ~= errMsgFileLine do + -- print('Removing : '..t[idx] ) + table.remove(t, idx) + end + + -- keep lines until we hit luaunit again + while t[idx] and (not isLuaunitInternalLine(t[idx])) do + -- print('Keeping : '..t[idx] ) + idx = idx + 1 + end + + -- remove remaining luaunit lines + while t[idx] do + -- print('Removing2 : '..t[idx] ) + table.remove(t, idx) + end + + -- print( prettystr(t) ) + return table.concat( t, '\n') + +end +M.private.stripLuaunitTrace2 = stripLuaunitTrace2 + + +local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable ) + local type_v = type(v) + if "string" == type_v then + -- use clever delimiters according to content: + -- enclose with single quotes if string contains ", but no ' + if v:find('"', 1, true) and not v:find("'", 1, true) then + return "'" .. v .. "'" + end + -- use double quotes otherwise, escape embedded " + return '"' .. v:gsub('"', '\\"') .. '"' + + elseif "table" == type_v then + --if v.__class__ then + -- return string.gsub( tostring(v), 'table', v.__class__ ) + --end + return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable) + + elseif "number" == type_v then + -- eliminate differences in formatting between various Lua versions + if v ~= v then + return "#NaN" -- "not a number" + end + if v == math.huge then + return "#Inf" -- "infinite" + end + if v == -math.huge then + return "-#Inf" + end + if _VERSION == "Lua 5.3" then + local i = math.tointeger(v) + if i then + return tostring(i) + end + end + end + + return tostring(v) +end + +local function prettystr( v ) + --[[ Pretty string conversion, to display the full content of a variable of any type. + * string are enclosed with " by default, or with ' if string contains a " + * tables are expanded to show their full content, with indentation in case of nested tables + ]]-- + local cycleDetectTable = {} + local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable) + if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then + -- some table contain recursive references, + -- so we must recompute the value by including all table references + -- else the result looks like crap + cycleDetectTable = {} + s = prettystr_sub(v, 1, true, cycleDetectTable) + end + return s +end +M.prettystr = prettystr + +function M.adjust_err_msg_with_iter( err_msg, iter_msg ) + --[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed, + add the iteration message if any and return the result. + err_msg: string, error message captured with pcall + iter_msg: a string describing the current iteration ("iteration N") or nil + if there is no iteration in this test. + Returns: (new_err_msg, test_status) + new_err_msg: string, adjusted error message, or nil in case of success + test_status: M.NodeStatus.FAIL, SUCCESS or ERROR according to the information + contained in the error message. + ]] + if iter_msg then + iter_msg = iter_msg..', ' + else + iter_msg = '' + end + + local RE_FILE_LINE = '.*:%d+: ' + + -- error message is not necessarily a string, + -- so convert the value to string with prettystr() + if type( err_msg ) ~= 'string' then + err_msg = prettystr( err_msg ) + end + + if (err_msg:find( M.SUCCESS_PREFIX ) == 1) or err_msg:match( '('..RE_FILE_LINE..')' .. M.SUCCESS_PREFIX .. ".*" ) then + -- test finished early with success() + return nil, M.NodeStatus.SUCCESS + end + + if (err_msg:find( M.SKIP_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.SKIP_PREFIX .. ".*" ) ~= nil) then + -- substitute prefix by iteration message + err_msg = err_msg:gsub('.*'..M.SKIP_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.SKIP + end + + if (err_msg:find( M.FAILURE_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.FAILURE_PREFIX .. ".*" ) ~= nil) then + -- substitute prefix by iteration message + err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.FAIL + end + + + + -- print("error detected") + -- regular error, not a failure + if iter_msg then + local match + -- "./test\\test_luaunit.lua:2241: some error msg + match = err_msg:match( '(.*:%d+: ).*' ) + if match then + err_msg = err_msg:gsub( match, match .. iter_msg ) + else + -- no file:line: infromation, just add the iteration info at the beginning of the line + err_msg = iter_msg .. err_msg + end + end + return err_msg, M.NodeStatus.ERROR +end + +local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis, margin ) + --[[ + Prepares a nice error message when comparing tables, performing a deeper + analysis. + Arguments: + * table_a, table_b: tables to be compared + * doDeepAnalysis: + M.DEFAULT_DEEP_ANALYSIS: (the default if not specified) perform deep analysis only for big lists and big dictionnaries + M.FORCE_DEEP_ANALYSIS : always perform deep analysis + M.DISABLE_DEEP_ANALYSIS: never perform deep analysis + * margin: supplied only for almost equality + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- check if table_a & table_b are suitable for deep analysis + if type(table_a) ~= 'table' or type(table_b) ~= 'table' then + return false + end + + if doDeepAnalysis == M.DISABLE_DEEP_ANALYSIS then + return false + end + + local len_a, len_b, isPureList = #table_a, #table_b, true + + for k1, v1 in pairs(table_a) do + if type(k1) ~= 'number' or k1 > len_a then + -- this table a mapping + isPureList = false + break + end + end + + if isPureList then + for k2, v2 in pairs(table_b) do + if type(k2) ~= 'number' or k2 > len_b then + -- this table a mapping + isPureList = false + break + end + end + end + + if isPureList and math.min(len_a, len_b) < M.LIST_DIFF_ANALYSIS_THRESHOLD then + if not (doDeepAnalysis == M.FORCE_DEEP_ANALYSIS) then + return false + end + end + + if isPureList then + return M.private.mismatchFormattingPureList( table_a, table_b, margin ) + else + -- only work on mapping for the moment + -- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) + return false + end +end +M.private.tryMismatchFormatting = tryMismatchFormatting + +local function getTaTbDescr() + if not M.ORDER_ACTUAL_EXPECTED then + return 'expected', 'actual' + end + return 'actual', 'expected' +end + +local function extendWithStrFmt( res, ... ) + table.insert( res, string.format( ... ) ) +end + +local function mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) + --[[ + Prepares a nice error message when comparing tables which are not pure lists, performing a deeper + analysis. + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- disable for the moment + --[[ + local result = {} + local descrTa, descrTb = getTaTbDescr() + local keysCommon = {} + local keysOnlyTa = {} + local keysOnlyTb = {} + local keysDiffTaTb = {} + local k, v + for k,v in pairs( table_a ) do + if is_equal( v, table_b[k] ) then + table.insert( keysCommon, k ) + else + if table_b[k] == nil then + table.insert( keysOnlyTa, k ) + else + table.insert( keysDiffTaTb, k ) + end + end + end + for k,v in pairs( table_b ) do + if not is_equal( v, table_a[k] ) and table_a[k] == nil then + table.insert( keysOnlyTb, k ) + end + end + local len_a = #keysCommon + #keysDiffTaTb + #keysOnlyTa + local len_b = #keysCommon + #keysDiffTaTb + #keysOnlyTb + local limited_display = (len_a < 5 or len_b < 5) + if math.min(len_a, len_b) < M.TABLE_DIFF_ANALYSIS_THRESHOLD then + return false + end + if not limited_display then + if len_a == len_b then + extendWithStrFmt( result, 'Table A (%s) and B (%s) both have %d items', descrTa, descrTb, len_a ) + else + extendWithStrFmt( result, 'Table A (%s) has %d items and table B (%s) has %d items', descrTa, len_a, descrTb, len_b ) + end + if #keysCommon == 0 and #keysDiffTaTb == 0 then + table.insert( result, 'Table A and B have no keys in common, they are totally different') + else + local s_other = 'other ' + if #keysCommon then + extendWithStrFmt( result, 'Table A and B have %d identical items', #keysCommon ) + else + table.insert( result, 'Table A and B have no identical items' ) + s_other = '' + end + if #keysDiffTaTb ~= 0 then + result[#result] = string.format( '%s and %d items differing present in both tables', result[#result], #keysDiffTaTb) + else + result[#result] = string.format( '%s and no %sitems differing present in both tables', result[#result], s_other, #keysDiffTaTb) + end + end + extendWithStrFmt( result, 'Table A has %d keys not present in table B and table B has %d keys not present in table A', #keysOnlyTa, #keysOnlyTb ) + end + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr(k) + end + if #keysDiffTaTb ~= 0 then + table.insert( result, 'Items differing in A and B:') + for k,v in sortedPairs( keysDiffTaTb ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + if #keysOnlyTa ~= 0 then + table.insert( result, 'Items only in table A:' ) + for k,v in sortedPairs( keysOnlyTa ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + if #keysOnlyTb ~= 0 then + table.insert( result, 'Items only in table B:' ) + for k,v in sortedPairs( keysOnlyTb ) do + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + if #keysCommon ~= 0 then + table.insert( result, 'Items common to A and B:') + for k,v in sortedPairs( keysCommon ) do + extendWithStrFmt( result, ' = A and B [%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + return true, table.concat( result, '\n') + ]] +end +M.private.mismatchFormattingMapping = mismatchFormattingMapping + +local function mismatchFormattingPureList( table_a, table_b, margin ) + --[[ + Prepares a nice error message when comparing tables which are lists, performing a deeper + analysis. + margin is supplied only for almost equality + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + local result, descrTa, descrTb = {}, getTaTbDescr() + + local len_a, len_b, refa, refb = #table_a, #table_b, '', '' + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + refa, refb = string.format( '<%s> ', M.private.table_ref(table_a)), string.format('<%s> ', M.private.table_ref(table_b) ) + end + local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b) + local deltalv = longest - shortest + + local commonUntil = shortest + for i = 1, shortest do + if not M.private.is_table_equals(table_a[i], table_b[i], margin) then + commonUntil = i - 1 + break + end + end + + local commonBackTo = shortest - 1 + for i = 0, shortest - 1 do + if not M.private.is_table_equals(table_a[len_a-i], table_b[len_b-i], margin) then + commonBackTo = i - 1 + break + end + end + + + table.insert( result, 'List difference analysis:' ) + if len_a == len_b then + -- TODO: handle expected/actual naming + extendWithStrFmt( result, '* lists %sA (%s) and %sB (%s) have the same size', refa, descrTa, refb, descrTb ) + else + extendWithStrFmt( result, '* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items', refa, descrTa, len_a, refb, descrTb, len_b ) + end + + extendWithStrFmt( result, '* lists A and B start differing at index %d', commonUntil+1 ) + if commonBackTo >= 0 then + if deltalv > 0 then + extendWithStrFmt( result, '* lists A and B are equal again from index %d for A, %d for B', len_a-commonBackTo, len_b-commonBackTo ) + else + extendWithStrFmt( result, '* lists A and B are equal again from index %d', len_a-commonBackTo ) + end + end + + local function insertABValue(ai, bi) + bi = bi or ai + if M.private.is_table_equals( table_a[ai], table_b[bi], margin) then + return extendWithStrFmt( result, ' = A[%d], B[%d]: %s', ai, bi, prettystr(table_a[ai]) ) + else + extendWithStrFmt( result, ' - A[%d]: %s', ai, prettystr(table_a[ai])) + extendWithStrFmt( result, ' + B[%d]: %s', bi, prettystr(table_b[bi])) + end + end + + -- common parts to list A & B, at the beginning + if commonUntil > 0 then + table.insert( result, '* Common parts:' ) + for i = 1, commonUntil do + insertABValue( i ) + end + end + + -- diffing parts to list A & B + if commonUntil < shortest - commonBackTo - 1 then + table.insert( result, '* Differing parts:' ) + for i = commonUntil + 1, shortest - commonBackTo - 1 do + insertABValue( i ) + end + end + + -- display indexes of one list, with no match on other list + if shortest - commonBackTo <= longest - commonBackTo - 1 then + table.insert( result, '* Present only in one list:' ) + for i = shortest - commonBackTo, longest - commonBackTo - 1 do + if len_a > len_b then + extendWithStrFmt( result, ' - A[%d]: %s', i, prettystr(table_a[i]) ) + -- table.insert( result, '+ (no matching B index)') + else + -- table.insert( result, '- no matching A index') + extendWithStrFmt( result, ' + B[%d]: %s', i, prettystr(table_b[i]) ) + end + end + end + + -- common parts to list A & B, at the end + if commonBackTo >= 0 then + table.insert( result, '* Common parts at the end of the lists' ) + for i = longest - commonBackTo, longest do + if len_a > len_b then + insertABValue( i, i-deltalv ) + else + insertABValue( i-deltalv, i ) + end + end + end + + return true, table.concat( result, '\n') +end +M.private.mismatchFormattingPureList = mismatchFormattingPureList + +local function prettystrPairs(value1, value2, suffix_a, suffix_b) + --[[ + This function helps with the recurring task of constructing the "expected + vs. actual" error messages. It takes two arbitrary values and formats + corresponding strings with prettystr(). + To keep the (possibly complex) output more readable in case the resulting + strings contain line breaks, they get automatically prefixed with additional + newlines. Both suffixes are optional (default to empty strings), and get + appended to the "value1" string. "suffix_a" is used if line breaks were + encountered, "suffix_b" otherwise. + Returns the two formatted strings (including padding/newlines). + ]] + local str1, str2 = prettystr(value1), prettystr(value2) + if hasNewLine(str1) or hasNewLine(str2) then + -- line break(s) detected, add padding + return "\n" .. str1 .. (suffix_a or ""), "\n" .. str2 + end + return str1 .. (suffix_b or ""), str2 +end +M.private.prettystrPairs = prettystrPairs + +local UNKNOWN_REF = 'table 00-unknown ref' +local ref_generator = { value=1, [UNKNOWN_REF]=0 } + +local function table_ref( t ) + -- return the default tostring() for tables, with the table ID, even if the table has a metatable + -- with the __tostring converter + local ref = '' + local mt = getmetatable( t ) + if mt == nil then + ref = tostring(t) + else + local success, result + success, result = pcall(setmetatable, t, nil) + if not success then + -- protected table, if __tostring is defined, we can + -- not get the reference. And we can not know in advance. + ref = tostring(t) + if not ref:match( 'table: 0?x?[%x]+' ) then + return UNKNOWN_REF + end + else + ref = tostring(t) + setmetatable( t, mt ) + end + end + -- strip the "table: " part + ref = ref:sub(8) + if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then + -- Create a new reference number + ref_generator[ref] = ref_generator.value + ref_generator.value = ref_generator.value+1 + end + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + return string.format('table %02d-%s', ref_generator[ref], ref) + else + return string.format('table %02d', ref_generator[ref]) + end +end +M.private.table_ref = table_ref + +local TABLE_TOSTRING_SEP = ", " +local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP) + +local function _table_tostring( tbl, indentLevel, printTableRefs, cycleDetectTable ) + printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG + cycleDetectTable = cycleDetectTable or {} + cycleDetectTable[tbl] = true + + local result, dispOnMultLines = {}, false + + -- like prettystr but do not enclose with "" if the string is just alphanumerical + -- this is better for displaying table keys who are often simple strings + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr_sub(k, indentLevel+1, printTableRefs, cycleDetectTable) + end + + local mt = getmetatable( tbl ) + + if mt and mt.__tostring then + -- if table has a __tostring() function in its metatable, use it to display the table + -- else, compute a regular table + result = tostring(tbl) + if type(result) ~= 'string' then + return string.format( '', prettystr(result) ) + end + result = strsplit( '\n', result ) + return M.private._table_tostring_format_multiline_string( result, indentLevel ) + + else + -- no metatable, compute the table representation + + local entry, count, seq_index = nil, 0, 1 + for k, v in sortedPairs( tbl ) do + + -- key part + if k == seq_index then + -- for the sequential part of tables, we'll skip the "=" output + entry = '' + seq_index = seq_index + 1 + elseif cycleDetectTable[k] then + -- recursion in the key detected + cycleDetectTable.detected = true + entry = "<"..table_ref(k)..">=" + else + entry = keytostring(k) .. "=" + end + + -- value part + if cycleDetectTable[v] then + -- recursion in the value detected! + cycleDetectTable.detected = true + entry = entry .. "<"..table_ref(v)..">" + else + entry = entry .. + prettystr_sub( v, indentLevel+1, printTableRefs, cycleDetectTable ) + end + count = count + 1 + result[count] = entry + end + return M.private._table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) + end + +end +M.private._table_tostring = _table_tostring -- prettystr_sub() needs it + +local function _table_tostring_format_multiline_string( tbl_str, indentLevel ) + local indentString = '\n'..string.rep(" ", indentLevel - 1) + return table.concat( tbl_str, indentString ) + +end +M.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string + + +local function _table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) + -- final function called in _table_to_string() to format the resulting list of + -- string describing the table. + + local dispOnMultLines = false + + -- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values + local totalLength = 0 + for k, v in ipairs( result ) do + totalLength = totalLength + string.len( v ) + if totalLength >= M.LINE_LENGTH then + dispOnMultLines = true + break + end + end + + -- set dispOnMultLines to true if the max LINE_LENGTH would be exceeded + -- with the values and the separators. + if not dispOnMultLines then + -- adjust with length of separator(s): + -- two items need 1 sep, three items two seps, ... plus len of '{}' + if #result > 0 then + totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (#result - 1) + end + dispOnMultLines = (totalLength + 2 >= M.LINE_LENGTH) + end + + -- now reformat the result table (currently holding element strings) + if dispOnMultLines then + local indentString = string.rep(" ", indentLevel - 1) + result = { + "{\n ", + indentString, + table.concat(result, ",\n " .. indentString), + "\n", + indentString, + "}" + } + else + result = {"{", table.concat(result, TABLE_TOSTRING_SEP), "}"} + end + if printTableRefs then + table.insert(result, 1, "<"..table_ref(tbl).."> ") -- prepend table ref + end + return table.concat(result) +end +M.private._table_tostring_format_result = _table_tostring_format_result -- prettystr_sub() needs it + +local function table_findkeyof(t, element) + -- Return the key k of the given element in table t, so that t[k] == element + -- (or `nil` if element is not present within t). Note that we use our + -- 'general' is_equal comparison for matching, so this function should + -- handle table-type elements gracefully and consistently. + if type(t) == "table" then + for k, v in pairs(t) do + if M.private.is_table_equals(v, element) then + return k + end + end + end + return nil +end + +local function _is_table_items_equals(actual, expected ) + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false + + elseif (type_a == 'table') --[[and (type_e == 'table')]] then + for k, v in pairs(actual) do + if table_findkeyof(expected, v) == nil then + return false -- v not contained in expected + end + end + for k, v in pairs(expected) do + if table_findkeyof(actual, v) == nil then + return false -- v not contained in actual + end + end + return true + + elseif actual ~= expected then + return false + end + + return true +end + +--[[ +This is a specialized metatable to help with the bookkeeping of recursions +in _is_table_equals(). It provides an __index table that implements utility +functions for easier management of the table. The "cached" method queries +the state of a specific (actual,expected) pair; and the "store" method sets +this state to the given value. The state of pairs not "seen" / visited is +assumed to be `nil`. +]] +local _recursion_cache_MT = { + __index = { + -- Return the cached value for an (actual,expected) pair (or `nil`) + cached = function(t, actual, expected) + local subtable = t[actual] or {} + return subtable[expected] + end, + + -- Store cached value for a specific (actual,expected) pair. + -- Returns the value, so it's easy to use for a "tailcall" (return ...). + store = function(t, actual, expected, value, asymmetric) + local subtable = t[actual] + if not subtable then + subtable = {} + t[actual] = subtable + end + subtable[expected] = value + + -- Unless explicitly marked "asymmetric": Consider the recursion + -- on (expected,actual) to be equivalent to (actual,expected) by + -- default, and thus cache the value for both. + if not asymmetric then + t:store(expected, actual, value, true) + end + + return value + end + } +} + +local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlmostEqual) + --[[Returns true if both table are equal. + If argument marginForAlmostEqual is suppied, number comparison is done using alomstEqual instead + of strict equality. + cycleDetectTable is an internal argument used during recursion on tables. + ]] + --print('_is_table_equals( \n '..prettystr(actual)..'\n , '..prettystr(expected).. + -- '\n , '..prettystr(cycleDetectTable)..'\n , '..prettystr(marginForAlmostEqual)..' )') + + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false -- different types won't match + end + + if type_a == 'number' then + if marginForAlmostEqual ~= nil then + return M.almostEquals(actual, expected, marginForAlmostEqual) + else + return actual == expected + end + elseif type_a ~= 'table' then + -- other types compare directly + return actual == expected + end + + cycleDetectTable = cycleDetectTable or { actual={}, expected={} } + if cycleDetectTable.actual[ actual ] then + -- oh, we hit a cycle in actual + if cycleDetectTable.expected[ expected ] then + -- uh, we hit a cycle at the same time in expected + -- so the two tables have similar structure + return true + end + + -- cycle was hit only in actual, the structure differs from expected + return false + end + + if cycleDetectTable.expected[ expected ] then + -- no cycle in actual, but cycle in expected + -- the structure differ + return false + end + + -- at this point, no table cycle detected, we are + -- seeing this table for the first time + + -- mark the cycle detection + cycleDetectTable.actual[ actual ] = true + cycleDetectTable.expected[ expected ] = true + + + local actualKeysMatched = {} + for k, v in pairs(actual) do + actualKeysMatched[k] = true -- Keep track of matched keys + if not _is_table_equals(v, expected[k], cycleDetectTable, marginForAlmostEqual) then + -- table differs on this key + -- clear the cycle detection before returning + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return false + end + end + + for k, v in pairs(expected) do + if not actualKeysMatched[k] then + -- Found a key that we did not see in "actual" -> mismatch + -- clear the cycle detection before returning + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return false + end + -- Otherwise actual[k] was already matched against v = expected[k]. + end + + -- all key match, we have a match ! + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return true +end +M.private._is_table_equals = _is_table_equals + +local function failure(main_msg, extra_msg_or_nil, level) + -- raise an error indicating a test failure + -- for error() compatibility we adjust "level" here (by +1), to report the + -- calling context + local msg + if type(extra_msg_or_nil) == 'string' and extra_msg_or_nil:len() > 0 then + msg = extra_msg_or_nil .. '\n' .. main_msg + else + msg = main_msg + end + error(M.FAILURE_PREFIX .. msg, (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE) +end + +local function is_table_equals(actual, expected, marginForAlmostEqual) + return _is_table_equals(actual, expected, nil, marginForAlmostEqual) +end +M.private.is_table_equals = is_table_equals + +local function fail_fmt(level, extra_msg_or_nil, ...) + -- failure with printf-style formatted message and given error level + failure(string.format(...), extra_msg_or_nil, (level or 1) + 1) +end +M.private.fail_fmt = fail_fmt + +local function error_fmt(level, ...) + -- printf-style error() + error(string.format(...), (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE) +end +M.private.error_fmt = error_fmt + +---------------------------------------------------------------- +-- +-- assertions +-- +---------------------------------------------------------------- + +local function errorMsgEquality(actual, expected, doDeepAnalysis, margin) + -- margin is supplied only for almost equal verification + + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + if type(expected) == 'string' or type(expected) == 'table' then + local strExpected, strActual = prettystrPairs(expected, actual) + local result = string.format("expected: %s\nactual: %s", strExpected, strActual) + if margin then + result = result .. '\nwere not equal by the margin of: '..prettystr(margin) + end + + -- extend with mismatch analysis if possible: + local success, mismatchResult + success, mismatchResult = tryMismatchFormatting( actual, expected, doDeepAnalysis, margin ) + if success then + result = table.concat( { result, mismatchResult }, '\n' ) + end + return result + end + return string.format("expected: %s, actual: %s", + prettystr(expected), prettystr(actual)) +end + +function M.assertError(f, ...) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + if pcall( f, ... ) then + failure( "Expected an error when calling function but no error generated", nil, 2 ) + end +end + +function M.fail( msg ) + -- stops a test due to a failure + failure( msg, nil, 2 ) +end + +function M.failIf( cond, msg ) + -- Fails a test with "msg" if condition is true + if cond then + failure( msg, nil, 2 ) + end +end + +function M.skip(msg) + -- skip a running test + error_fmt(2, M.SKIP_PREFIX .. msg) +end + +function M.skipIf( cond, msg ) + -- skip a running test if condition is met + if cond then + error_fmt(2, M.SKIP_PREFIX .. msg) + end +end + +function M.runOnlyIf( cond, msg ) + -- continue a running test if condition is met, else skip it + if not cond then + error_fmt(2, M.SKIP_PREFIX .. prettystr(msg)) + end +end + +function M.success() + -- stops a test with a success + error_fmt(2, M.SUCCESS_PREFIX) +end + +function M.successIf( cond ) + -- stops a test with a success if condition is met + if cond then + error_fmt(2, M.SUCCESS_PREFIX) + end +end + + +------------------------------------------------------------------ +-- Equality assertions +------------------------------------------------------------------ + +function M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis) + if type(actual) == 'table' and type(expected) == 'table' then + if not is_table_equals(actual, expected) then + failure( errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2 ) + end + elseif type(actual) ~= type(expected) then + failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + elseif actual ~= expected then + failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + end +end + +function M.almostEquals( actual, expected, margin ) + if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then + error_fmt(3, 'almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s', + prettystr(actual), prettystr(expected), prettystr(margin)) + end + if margin < 0 then + error_fmt(3, 'almostEquals: margin must not be negative, current value is ' .. margin) + end + return math.abs(expected - actual) <= margin +end + +function M.assertAlmostEquals( actual, expected, margin, extra_msg_or_nil ) + -- check that two floats are close by margin + margin = margin or M.EPS + if type(margin) ~= 'number' then + error_fmt(2, 'almostEquals: margin must be a number, not %s', prettystr(margin)) + end + + if type(actual) == 'table' and type(expected) == 'table' then + -- handle almost equals for table + if not is_table_equals(actual, expected, margin) then + failure( errorMsgEquality(actual, expected, nil, margin), extra_msg_or_nil, 2 ) + end + elseif type(actual) == 'number' and type(expected) == 'number' and type(margin) == 'number' then + if not M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt(2, extra_msg_or_nil, 'Values are not almost equal\n' .. + 'Actual: %s, expected: %s, delta %s above margin of %s', + actual, expected, delta, margin) + end + else + error_fmt(3, 'almostEquals: must supply only number or table arguments.\nArguments supplied: %s, %s, %s', + prettystr(actual), prettystr(expected), prettystr(margin)) + end +end + +function M.assertNotEquals(actual, expected, extra_msg_or_nil) + if type(actual) ~= type(expected) then + return + end + + if type(actual) == 'table' and type(expected) == 'table' then + if not is_table_equals(actual, expected) then + return + end + elseif actual ~= expected then + return + end + fail_fmt(2, extra_msg_or_nil, 'Received the not expected value: %s', prettystr(actual)) +end + +function M.assertNotAlmostEquals( actual, expected, margin, extra_msg_or_nil ) + -- check that two floats are not close by margin + margin = margin or M.EPS + if M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt(2, extra_msg_or_nil, 'Values are almost equal\nActual: %s, expected: %s' .. + ', delta %s below margin of %s', + actual, expected, delta, margin) + end +end + +function M.assertItemsEquals(actual, expected, extra_msg_or_nil) + -- checks that the items of table expected + -- are contained in table actual. Warning, this function + -- is at least O(n^2) + if not _is_table_items_equals(actual, expected ) then + expected, actual = prettystrPairs(expected, actual) + fail_fmt(2, extra_msg_or_nil, 'Content of the tables are not identical:\nExpected: %s\nActual: %s', + expected, actual) + end +end + +------------------------------------------------------------------ +-- String assertion +------------------------------------------------------------------ + +function M.assertStrContains( str, sub, isPattern, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + -- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) ) + -- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) ) + if not string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not find %s %s in string %s', + isPattern and 'pattern' or 'substring', sub, str) + end +end + +function M.assertStrIContains( str, sub, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if not string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not find (case insensitively) substring %s in string %s', + sub, str) + end +end + +function M.assertNotStrContains( str, sub, isPattern, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Found the not expected %s %s in string %s', + isPattern and 'pattern' or 'substring', sub, str) + end +end + +function M.assertNotStrIContains( str, sub, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Found (case insensitively) the not expected substring %s in string %s', + sub, str) + end +end + +function M.assertStrMatches( str, pattern, start, final, extra_msg_or_nil ) + -- Verify a full match for the string + if not strMatch( str, pattern, start, final ) then + pattern, str = prettystrPairs(pattern, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not match pattern %s with string %s', + pattern, str) + end +end + +local function _assertErrorMsgEquals( stripFileAndLine, expectedMsg, func, ... ) + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error: '..M.prettystr(expectedMsg), nil, 3 ) + end + if type(expectedMsg) == "string" and type(error_msg) ~= "string" then + -- table are converted to string automatically + error_msg = tostring(error_msg) + end + local differ = false + if stripFileAndLine then + if error_msg:gsub("^.+:%d+: ", "") ~= expectedMsg then + differ = true + end + else + if error_msg ~= expectedMsg then + local tr = type(error_msg) + local te = type(expectedMsg) + if te == 'table' then + if tr ~= 'table' then + differ = true + else + local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg) + if not ok then + differ = true + end + end + else + differ = true + end + end + end + + if differ then + error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg) + fail_fmt(3, nil, 'Error message expected: %s\nError message received: %s\n', + expectedMsg, error_msg) + end +end + +function M.assertErrorMsgEquals( expectedMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + _assertErrorMsgEquals(false, expectedMsg, func, ...) +end + +function M.assertErrorMsgContentEquals(expectedMsg, func, ...) + _assertErrorMsgEquals(true, expectedMsg, func, ...) +end + +function M.assertErrorMsgContains( partialMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error containing: '..prettystr(partialMsg), nil, 2 ) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not string.find( error_msg, partialMsg, nil, true ) then + error_msg, partialMsg = prettystrPairs(error_msg, partialMsg) + fail_fmt(2, nil, 'Error message does not contain: %s\nError message received: %s\n', + partialMsg, error_msg) + end +end + +function M.assertErrorMsgMatches( expectedMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error matching: "'..expectedMsg..'"', nil, 2 ) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not strMatch( error_msg, expectedMsg ) then + expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg) + fail_fmt(2, nil, 'Error message does not match pattern: %s\nError message received: %s\n', + expectedMsg, error_msg) + end +end + +------------------------------------------------------------------ +-- Type assertions +------------------------------------------------------------------ + +function M.assertEvalToTrue(value, extra_msg_or_nil) + if not value then + failure("expected: a value evaluating to true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertEvalToFalse(value, extra_msg_or_nil) + if value then + failure("expected: false or nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsTrue(value, extra_msg_or_nil) + if value ~= true then + failure("expected: true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsTrue(value, extra_msg_or_nil) + if value == true then + failure("expected: not true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsFalse(value, extra_msg_or_nil) + if value ~= false then + failure("expected: false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsFalse(value, extra_msg_or_nil) + if value == false then + failure("expected: not false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsNil(value, extra_msg_or_nil) + if value ~= nil then + failure("expected: nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNil(value, extra_msg_or_nil) + if value == nil then + failure("expected: not nil, actual: nil", extra_msg_or_nil, 2) + end +end + +--[[ +Add type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type matches the +expected string (derived from the function name): +M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx" +]] +for _, funcName in ipairs( + {'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean', + 'assertIsFunction', 'assertIsUserdata', 'assertIsThread'} +) do + local typeExpected = funcName:match("^assertIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeExpected = typeExpected and typeExpected:lower() + or error("bad function name '"..funcName.."' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) ~= typeExpected then + if type(value) == 'nil' then + fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: nil', + typeExpected, type(value), prettystrPairs(value)) + else + fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: type %s, value %s', + typeExpected, type(value), prettystrPairs(value)) + end + end + end +end + +--[[ +Add shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility) +M.isXxx(value) -> returns true if type(value) conforms to "xxx" +]] +for _, typeExpected in ipairs( + {'Number', 'String', 'Table', 'Boolean', + 'Function', 'Userdata', 'Thread', 'Nil' } +) do + local typeExpectedLower = typeExpected:lower() + local isType = function(value) + return (type(value) == typeExpectedLower) + end + M['is'..typeExpected] = isType + M['is_'..typeExpectedLower] = isType +end + +--[[ +Add non-type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type differs from the +expected string (derived from the function name): +M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx" +]] +for _, funcName in ipairs( + {'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean', + 'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread'} +) do + local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeUnexpected = typeUnexpected and typeUnexpected:lower() + or error("bad function name '"..funcName.."' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) == typeUnexpected then + fail_fmt(2, extra_msg_or_nil, 'expected: not a %s type, actual: value %s', + typeUnexpected, prettystrPairs(value)) + end + end +end + +function M.assertIs(actual, expected, extra_msg_or_nil) + if actual ~= expected then + if not M.ORDER_ACTUAL_EXPECTED then + actual, expected = expected, actual + end + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + expected, actual = prettystrPairs(expected, actual, '\n', '') + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt(2, extra_msg_or_nil, 'expected and actual object should not be different\nExpected: %s\nReceived: %s', + expected, actual) + end +end + +function M.assertNotIs(actual, expected, extra_msg_or_nil) + if actual == expected then + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + local s_expected + if not M.ORDER_ACTUAL_EXPECTED then + s_expected = prettystrPairs(actual) + else + s_expected = prettystrPairs(expected) + end + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt(2, extra_msg_or_nil, 'expected and actual object should be different: %s', s_expected ) + end +end + + +------------------------------------------------------------------ +-- Scientific assertions +------------------------------------------------------------------ + + +function M.assertIsNaN(value, extra_msg_or_nil) + if type(value) ~= "number" or value == value then + failure("expected: NaN, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNaN(value, extra_msg_or_nil) + if type(value) == "number" and value ~= value then + failure("expected: not NaN, actual: NaN", extra_msg_or_nil, 2) + end +end + +function M.assertIsInf(value, extra_msg_or_nil) + if type(value) ~= "number" or math.abs(value) ~= math.huge then + failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= math.huge then + failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsMinusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= -math.huge then + failure("expected: -#Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsPlusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == math.huge then + failure("expected: not #Inf, actual: #Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == -math.huge then + failure("expected: not -#Inf, actual: -#Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsInf(value, extra_msg_or_nil) + if type(value) == "number" and math.abs(value) == math.huge then + failure("expected: not infinity, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusZero(value, extra_msg_or_nil) + if type(value) ~= 'number' or value ~= 0 then + failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + else if (1/value == -math.huge) then + -- more precise error diagnosis + failure("expected: +0.0, actual: -0.0", extra_msg_or_nil, 2) + else if (1/value ~= math.huge) then + -- strange, case should have already been covered + failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertIsMinusZero(value, extra_msg_or_nil) + if type(value) ~= 'number' or value ~= 0 then + failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + else if (1/value == math.huge) then + -- more precise error diagnosis + failure("expected: -0.0, actual: +0.0", extra_msg_or_nil, 2) + else if (1/value ~= -math.huge) then + -- strange, case should have already been covered + failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertNotIsPlusZero(value, extra_msg_or_nil) + if type(value) == 'number' and (1/value == math.huge) then + failure("expected: not +0.0, actual: +0.0", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusZero(value, extra_msg_or_nil) + if type(value) == 'number' and (1/value == -math.huge) then + failure("expected: not -0.0, actual: -0.0", extra_msg_or_nil, 2) + end +end + +function M.assertTableContains(t, expected, extra_msg_or_nil) + -- checks that table t contains the expected element + if table_findkeyof(t, expected) == nil then + t, expected = prettystrPairs(t, expected) + fail_fmt(2, extra_msg_or_nil, 'Table %s does NOT contain the expected element %s', + t, expected) + end +end + +function M.assertNotTableContains(t, expected, extra_msg_or_nil) + -- checks that table t doesn't contain the expected element + local k = table_findkeyof(t, expected) + if k ~= nil then + t, expected = prettystrPairs(t, expected) + fail_fmt(2, extra_msg_or_nil, 'Table %s DOES contain the unwanted element %s (at key %s)', + t, expected, prettystr(k)) + end +end + +---------------------------------------------------------------- +-- Compatibility layer +---------------------------------------------------------------- + +-- for compatibility with LuaUnit v2.x +function M.wrapFunctions() + -- In LuaUnit version <= 2.1 , this function was necessary to include + -- a test function inside the global test suite. Nowadays, the functions + -- are simply run directly as part of the test discovery process. + -- so just do nothing ! + io.stderr:write[[Use of WrapFunctions() is no longer needed. +Just prefix your test function names with "test" or "Test" and they +will be picked up and run by LuaUnit. +]] +end + +local list_of_funcs = { + -- { official function name , alias } + + -- general assertions + { 'assertEquals' , 'assert_equals' }, + { 'assertItemsEquals' , 'assert_items_equals' }, + { 'assertNotEquals' , 'assert_not_equals' }, + { 'assertAlmostEquals' , 'assert_almost_equals' }, + { 'assertNotAlmostEquals' , 'assert_not_almost_equals' }, + { 'assertEvalToTrue' , 'assert_eval_to_true' }, + { 'assertEvalToFalse' , 'assert_eval_to_false' }, + { 'assertStrContains' , 'assert_str_contains' }, + { 'assertStrIContains' , 'assert_str_icontains' }, + { 'assertNotStrContains' , 'assert_not_str_contains' }, + { 'assertNotStrIContains' , 'assert_not_str_icontains' }, + { 'assertStrMatches' , 'assert_str_matches' }, + { 'assertError' , 'assert_error' }, + { 'assertErrorMsgEquals' , 'assert_error_msg_equals' }, + { 'assertErrorMsgContains' , 'assert_error_msg_contains' }, + { 'assertErrorMsgMatches' , 'assert_error_msg_matches' }, + { 'assertErrorMsgContentEquals', 'assert_error_msg_content_equals' }, + { 'assertIs' , 'assert_is' }, + { 'assertNotIs' , 'assert_not_is' }, + { 'assertTableContains' , 'assert_table_contains' }, + { 'assertNotTableContains' , 'assert_not_table_contains' }, + { 'wrapFunctions' , 'WrapFunctions' }, + { 'wrapFunctions' , 'wrap_functions' }, + + -- type assertions: assertIsXXX -> assert_is_xxx + { 'assertIsNumber' , 'assert_is_number' }, + { 'assertIsString' , 'assert_is_string' }, + { 'assertIsTable' , 'assert_is_table' }, + { 'assertIsBoolean' , 'assert_is_boolean' }, + { 'assertIsNil' , 'assert_is_nil' }, + { 'assertIsTrue' , 'assert_is_true' }, + { 'assertIsFalse' , 'assert_is_false' }, + { 'assertIsNaN' , 'assert_is_nan' }, + { 'assertIsInf' , 'assert_is_inf' }, + { 'assertIsPlusInf' , 'assert_is_plus_inf' }, + { 'assertIsMinusInf' , 'assert_is_minus_inf' }, + { 'assertIsPlusZero' , 'assert_is_plus_zero' }, + { 'assertIsMinusZero' , 'assert_is_minus_zero' }, + { 'assertIsFunction' , 'assert_is_function' }, + { 'assertIsThread' , 'assert_is_thread' }, + { 'assertIsUserdata' , 'assert_is_userdata' }, + + -- type assertions: assertIsXXX -> assertXxx + { 'assertIsNumber' , 'assertNumber' }, + { 'assertIsString' , 'assertString' }, + { 'assertIsTable' , 'assertTable' }, + { 'assertIsBoolean' , 'assertBoolean' }, + { 'assertIsNil' , 'assertNil' }, + { 'assertIsTrue' , 'assertTrue' }, + { 'assertIsFalse' , 'assertFalse' }, + { 'assertIsNaN' , 'assertNaN' }, + { 'assertIsInf' , 'assertInf' }, + { 'assertIsPlusInf' , 'assertPlusInf' }, + { 'assertIsMinusInf' , 'assertMinusInf' }, + { 'assertIsPlusZero' , 'assertPlusZero' }, + { 'assertIsMinusZero' , 'assertMinusZero'}, + { 'assertIsFunction' , 'assertFunction' }, + { 'assertIsThread' , 'assertThread' }, + { 'assertIsUserdata' , 'assertUserdata' }, + + -- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat) + { 'assertIsNumber' , 'assert_number' }, + { 'assertIsString' , 'assert_string' }, + { 'assertIsTable' , 'assert_table' }, + { 'assertIsBoolean' , 'assert_boolean' }, + { 'assertIsNil' , 'assert_nil' }, + { 'assertIsTrue' , 'assert_true' }, + { 'assertIsFalse' , 'assert_false' }, + { 'assertIsNaN' , 'assert_nan' }, + { 'assertIsInf' , 'assert_inf' }, + { 'assertIsPlusInf' , 'assert_plus_inf' }, + { 'assertIsMinusInf' , 'assert_minus_inf' }, + { 'assertIsPlusZero' , 'assert_plus_zero' }, + { 'assertIsMinusZero' , 'assert_minus_zero' }, + { 'assertIsFunction' , 'assert_function' }, + { 'assertIsThread' , 'assert_thread' }, + { 'assertIsUserdata' , 'assert_userdata' }, + + -- type assertions: assertNotIsXXX -> assert_not_is_xxx + { 'assertNotIsNumber' , 'assert_not_is_number' }, + { 'assertNotIsString' , 'assert_not_is_string' }, + { 'assertNotIsTable' , 'assert_not_is_table' }, + { 'assertNotIsBoolean' , 'assert_not_is_boolean' }, + { 'assertNotIsNil' , 'assert_not_is_nil' }, + { 'assertNotIsTrue' , 'assert_not_is_true' }, + { 'assertNotIsFalse' , 'assert_not_is_false' }, + { 'assertNotIsNaN' , 'assert_not_is_nan' }, + { 'assertNotIsInf' , 'assert_not_is_inf' }, + { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, + { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, + { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, + { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, + { 'assertNotIsFunction' , 'assert_not_is_function' }, + { 'assertNotIsThread' , 'assert_not_is_thread' }, + { 'assertNotIsUserdata' , 'assert_not_is_userdata' }, + + -- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat) + { 'assertNotIsNumber' , 'assertNotNumber' }, + { 'assertNotIsString' , 'assertNotString' }, + { 'assertNotIsTable' , 'assertNotTable' }, + { 'assertNotIsBoolean' , 'assertNotBoolean' }, + { 'assertNotIsNil' , 'assertNotNil' }, + { 'assertNotIsTrue' , 'assertNotTrue' }, + { 'assertNotIsFalse' , 'assertNotFalse' }, + { 'assertNotIsNaN' , 'assertNotNaN' }, + { 'assertNotIsInf' , 'assertNotInf' }, + { 'assertNotIsPlusInf' , 'assertNotPlusInf' }, + { 'assertNotIsMinusInf' , 'assertNotMinusInf' }, + { 'assertNotIsPlusZero' , 'assertNotPlusZero' }, + { 'assertNotIsMinusZero' , 'assertNotMinusZero' }, + { 'assertNotIsFunction' , 'assertNotFunction' }, + { 'assertNotIsThread' , 'assertNotThread' }, + { 'assertNotIsUserdata' , 'assertNotUserdata' }, + + -- type assertions: assertNotIsXXX -> assert_not_xxx + { 'assertNotIsNumber' , 'assert_not_number' }, + { 'assertNotIsString' , 'assert_not_string' }, + { 'assertNotIsTable' , 'assert_not_table' }, + { 'assertNotIsBoolean' , 'assert_not_boolean' }, + { 'assertNotIsNil' , 'assert_not_nil' }, + { 'assertNotIsTrue' , 'assert_not_true' }, + { 'assertNotIsFalse' , 'assert_not_false' }, + { 'assertNotIsNaN' , 'assert_not_nan' }, + { 'assertNotIsInf' , 'assert_not_inf' }, + { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, + { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, + { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, + { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, + { 'assertNotIsFunction' , 'assert_not_function' }, + { 'assertNotIsThread' , 'assert_not_thread' }, + { 'assertNotIsUserdata' , 'assert_not_userdata' }, + + -- all assertions with Coroutine duplicate Thread assertions + { 'assertIsThread' , 'assertIsCoroutine' }, + { 'assertIsThread' , 'assertCoroutine' }, + { 'assertIsThread' , 'assert_is_coroutine' }, + { 'assertIsThread' , 'assert_coroutine' }, + { 'assertNotIsThread' , 'assertNotIsCoroutine' }, + { 'assertNotIsThread' , 'assertNotCoroutine' }, + { 'assertNotIsThread' , 'assert_not_is_coroutine' }, + { 'assertNotIsThread' , 'assert_not_coroutine' }, +} + +-- Create all aliases in M +for _,v in ipairs( list_of_funcs ) do + local funcname, alias = v[1], v[2] + M[alias] = M[funcname] + + if EXPORT_ASSERT_TO_GLOBALS then + _G[funcname] = M[funcname] + _G[alias] = M[funcname] + end +end + +---------------------------------------------------------------- +-- +-- Outputters +-- +---------------------------------------------------------------- + +-- A common "base" class for outputters +-- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html + +local genericOutput = { __class__ = 'genericOutput' } -- class +local genericOutput_MT = { __index = genericOutput } -- metatable +M.genericOutput = genericOutput -- publish, so that custom classes may derive from it + +function genericOutput.new(runner, default_verbosity) + -- runner is the "parent" object controlling the output, usually a LuaUnit instance + local t = { runner = runner } + if runner then + t.result = runner.result + t.verbosity = runner.verbosity or default_verbosity + t.fname = runner.fname + else + t.verbosity = default_verbosity + end + return setmetatable( t, genericOutput_MT) +end + +-- abstract ("empty") methods +function genericOutput:startSuite() + -- Called once, when the suite is started +end + +function genericOutput:startClass(className) + -- Called each time a new test class is started +end + +function genericOutput:startTest(testName) + -- called each time a new test is started, right before the setUp() + -- the current test status node is already created and available in: self.result.currentNode +end + +function genericOutput:updateStatus(node) + -- called with status failed or error as soon as the error/failure is encountered + -- this method is NOT called for a successful test because a test is marked as successful by default + -- and does not need to be updated +end + +function genericOutput:endTest(node) + -- called when the test is finished, after the tearDown() method +end + +function genericOutput:endClass() + -- called when executing the class is finished, before moving on to the next class of at the end of the test execution +end + +function genericOutput:endSuite() + -- called at the end of the test suite execution +end + + +---------------------------------------------------------------- +-- class TapOutput +---------------------------------------------------------------- + +local TapOutput = genericOutput.new() -- derived class +local TapOutput_MT = { __index = TapOutput } -- metatable +TapOutput.__class__ = 'TapOutput' + + -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html + + function TapOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + return setmetatable( t, TapOutput_MT) + end + function TapOutput:startSuite() + print("1.."..self.result.selectedCount) + print('# Started on '..self.result.startDate) + end + function TapOutput:startClass(className) + if className ~= '[TestFunctions]' then + print('# Starting class: '..className) + end + end + + function TapOutput:updateStatus( node ) + if node:isSkipped() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t# SKIP ", node.msg, "\n" ) + return + end + + io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + if self.verbosity > M.VERBOSITY_LOW then + print( prefixString( '# ', node.msg ) ) + end + if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then + print( prefixString( '# ', node.stackTrace ) ) + end + end + + function TapOutput:endTest( node ) + if node:isSuccess() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + end + end + + function TapOutput:endSuite() + print( '# '..M.LuaUnit.statusLine( self.result ) ) + return self.result.notSuccessCount + end + + +-- class TapOutput end + +---------------------------------------------------------------- +-- class JUnitOutput +---------------------------------------------------------------- + +-- See directory junitxml for more information about the junit format +local JUnitOutput = genericOutput.new() -- derived class +local JUnitOutput_MT = { __index = JUnitOutput } -- metatable +JUnitOutput.__class__ = 'JUnitOutput' + + function JUnitOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + t.testList = {} + return setmetatable( t, JUnitOutput_MT ) + end + + function JUnitOutput:startSuite() + -- open xml file early to deal with errors + if self.fname == nil then + error('With Junit, an output filename must be supplied with --name!') + end + if string.sub(self.fname,-4) ~= '.xml' then + self.fname = self.fname..'.xml' + end + self.fd = io.open(self.fname, "w") + if self.fd == nil then + error("Could not open file for writing: "..self.fname) + end + + print('# XML output to '..self.fname) + print('# Started on '..self.result.startDate) + end + function JUnitOutput:startClass(className) + if className ~= '[TestFunctions]' then + print('# Starting class: '..className) + end + end + function JUnitOutput:startTest(testName) + print('# Starting test: '..testName) + end + + function JUnitOutput:updateStatus( node ) + if node:isFailure() then + print( '# Failure: ' .. prefixString( '# ', node.msg ):sub(4, nil) ) + -- print('# ' .. node.stackTrace) + elseif node:isError() then + print( '# Error: ' .. prefixString( '# ' , node.msg ):sub(4, nil) ) + -- print('# ' .. node.stackTrace) + end + end + + function JUnitOutput:endSuite() + print( '# '..M.LuaUnit.statusLine(self.result)) + + -- XML file writing + self.fd:write('\n') + self.fd:write('\n') + self.fd:write(string.format( + ' \n', + self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount, self.result.skippedCount )) + self.fd:write(" \n") + self.fd:write(string.format(' \n', _VERSION ) ) + self.fd:write(string.format(' \n', M.VERSION) ) + -- XXX please include system name and version if possible + self.fd:write(" \n") + + for i,node in ipairs(self.result.allTests) do + self.fd:write(string.format(' \n', + node.className, node.testName, node.duration ) ) + if node:isNotSuccess() then + self.fd:write(node:statusXML()) + end + self.fd:write(' \n') + end + + -- Next two lines are needed to validate junit ANT xsd, but really not useful in general: + self.fd:write(' \n') + self.fd:write(' \n') + + self.fd:write(' \n') + self.fd:write('\n') + self.fd:close() + return self.result.notSuccessCount + end + + +-- class TapOutput end + +---------------------------------------------------------------- +-- class TextOutput +---------------------------------------------------------------- + +--[[ Example of other unit-tests suite text output +-- Python Non verbose: +For each test: . or F or E +If some failed tests: + ============== + ERROR / FAILURE: TestName (testfile.testclass) + --------- + Stack trace +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) +-- Python Verbose: +testname (filename.classname) ... ok +testname (filename.classname) ... FAIL +testname (filename.classname) ... ERROR +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) +-- Ruby: +Started + . + Finished in 0.002695 seconds. + 1 tests, 2 assertions, 0 failures, 0 errors +-- Ruby: +>> ruby tc_simple_number2.rb +Loaded suite tc_simple_number2 +Started +F.. +Finished in 0.038617 seconds. + 1) Failure: +test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]: +Adding doesn't work. +<3> expected but was +<4>. +3 tests, 4 assertions, 1 failures, 0 errors +-- Java Junit +.......F. +Time: 0,003 +There was 1 failure: +1) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError + at junit.samples.VectorTest.testCapacity(VectorTest.java:87) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) +FAILURES!!! +Tests run: 8, Failures: 1, Errors: 0 +-- Maven +# mvn test +------------------------------------------------------- + T E S T S +------------------------------------------------------- +Running math.AdditionTest +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: +0.03 sec <<< FAILURE! +Results : +Failed tests: + testLireSymbole(math.AdditionTest) +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 +-- LuaUnit +---- non verbose +* display . or F or E when running tests +---- verbose +* display test name + ok/fail +---- +* blank line +* number) ERROR or FAILURE: TestName + Stack trace +* blank line +* number) ERROR or FAILURE: TestName + Stack trace +then -------------- +then "Ran x tests in 0.000s (%d not selected, %d skipped)" +then OK or FAILED (failures=1, error=1) +]] + +local TextOutput = genericOutput.new() -- derived class +local TextOutput_MT = { __index = TextOutput } -- metatable +TextOutput.__class__ = 'TextOutput' + + function TextOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT) + t.errorList = {} + return setmetatable( t, TextOutput_MT ) + end + + function TextOutput:startSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print( 'Started on '.. self.result.startDate ) + end + end + + function TextOutput:startTest(testName) + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write( " ", self.result.currentNode.testName, " ... " ) + end + end + + function TextOutput:endTest( node ) + if node:isSuccess() then + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write("Ok\n") + else + io.stdout:write(".") + io.stdout:flush() + end + else + if self.verbosity > M.VERBOSITY_DEFAULT then + print( node.status ) + print( node.msg ) + --[[ + -- find out when to do this: + if self.verbosity > M.VERBOSITY_DEFAULT then + print( node.stackTrace ) + end + ]] + else + -- write only the first character of status E, F or S + io.stdout:write(string.sub(node.status, 1, 1)) + io.stdout:flush() + end + end + end + + function TextOutput:displayOneFailedTest( index, fail ) + print(index..") "..fail.testName ) + print( fail.msg ) + print( fail.stackTrace ) + print() + end + + function TextOutput:displayErroredTests() + if #self.result.errorTests ~= 0 then + print("Tests with errors:") + print("------------------") + for i, v in ipairs(self.result.errorTests) do + self:displayOneFailedTest(i, v) + end + end + end + + function TextOutput:displayFailedTests() + if #self.result.failedTests ~= 0 then + print("Failed tests:") + print("-------------") + for i, v in ipairs(self.result.failedTests) do + self:displayOneFailedTest(i, v) + end + end + end + + function TextOutput:endSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print("=========================================================") + else + print() + end + self:displayErroredTests() + self:displayFailedTests() + print( M.LuaUnit.statusLine( self.result ) ) + if self.result.notSuccessCount == 0 then + print('OK') + end + end + +-- class TextOutput end + + +---------------------------------------------------------------- +-- class NilOutput +---------------------------------------------------------------- + +local function nopCallable() + --print(42) + return nopCallable +end + +local NilOutput = { __class__ = 'NilOuptut' } -- class +local NilOutput_MT = { __index = nopCallable } -- metatable + +function NilOutput.new(runner) + return setmetatable( { __class__ = 'NilOutput' }, NilOutput_MT ) +end + +---------------------------------------------------------------- +-- +-- class LuaUnit +-- +---------------------------------------------------------------- + +M.LuaUnit = { + outputType = TextOutput, + verbosity = M.VERBOSITY_DEFAULT, + __class__ = 'LuaUnit', + instances = {} +} +local LuaUnit_MT = { __index = M.LuaUnit } + +if EXPORT_ASSERT_TO_GLOBALS then + LuaUnit = M.LuaUnit +end + + function M.LuaUnit.new() + local newInstance = setmetatable( {}, LuaUnit_MT ) + return newInstance + end + + -----------------[[ Utility methods ]]--------------------- + + function M.LuaUnit.asFunction(aObject) + -- return "aObject" if it is a function, and nil otherwise + if 'function' == type(aObject) then + return aObject + end + end + + function M.LuaUnit.splitClassMethod(someName) + --[[ + Return a pair of className, methodName strings for a name in the form + "class.method". If no class part (or separator) is found, will return + nil, someName instead (the latter being unchanged). + This convention thus also replaces the older isClassMethod() test: + You just have to check for a non-nil className (return) value. + ]] + local separator = string.find(someName, '.', 1, true) + if separator then + return someName:sub(1, separator - 1), someName:sub(separator + 1) + end + return nil, someName + end + + function M.LuaUnit.isMethodTestName( s ) + -- return true is the name matches the name of a test method + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == 'test' + end + + function M.LuaUnit.isTestName( s ) + -- return true is the name matches the name of a test + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == 'test' + end + + function M.LuaUnit.collectTests() + -- return a list of all test names in the global namespace + -- that match LuaUnit.isTestName + + local testNames = {} + for k, _ in pairs(_G) do + if type(k) == "string" and M.LuaUnit.isTestName( k ) then + table.insert( testNames , k ) + end + end + table.sort( testNames ) + return testNames + end + + function M.LuaUnit.parseCmdLine( cmdLine ) + -- parse the command line + -- Supported command line parameters: + -- --verbose, -v: increase verbosity + -- --quiet, -q: silence output + -- --error, -e: treat errors as fatal (quit program) + -- --output, -o, + name: select output type + -- --pattern, -p, + pattern: run test matching pattern, may be repeated + -- --exclude, -x, + pattern: run test not matching pattern, may be repeated + -- --shuffle, -s, : shuffle tests before reunning them + -- --name, -n, + fname: name of output file for junit, default to stdout + -- --repeat, -r, + num: number of times to execute each test + -- [testnames, ...]: run selected test names + -- + -- Returns a table with the following fields: + -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE + -- output: nil, 'tap', 'junit', 'text', 'nil' + -- testNames: nil or a list of test names to run + -- exeRepeat: num or 1 + -- pattern: nil or a list of patterns + -- exclude: nil or a list of patterns + + local result, state = {}, nil + local SET_OUTPUT = 1 + local SET_PATTERN = 2 + local SET_EXCLUDE = 3 + local SET_FNAME = 4 + local SET_REPEAT = 5 + + if cmdLine == nil then + return result + end + + local function parseOption( option ) + if option == '--help' or option == '-h' then + result['help'] = true + return + elseif option == '--version' then + result['version'] = true + return + elseif option == '--verbose' or option == '-v' then + result['verbosity'] = M.VERBOSITY_VERBOSE + return + elseif option == '--quiet' or option == '-q' then + result['verbosity'] = M.VERBOSITY_QUIET + return + elseif option == '--error' or option == '-e' then + result['quitOnError'] = true + return + elseif option == '--failure' or option == '-f' then + result['quitOnFailure'] = true + return + elseif option == '--shuffle' or option == '-s' then + result['shuffle'] = true + return + elseif option == '--output' or option == '-o' then + state = SET_OUTPUT + return state + elseif option == '--name' or option == '-n' then + state = SET_FNAME + return state + elseif option == '--repeat' or option == '-r' then + state = SET_REPEAT + return state + elseif option == '--pattern' or option == '-p' then + state = SET_PATTERN + return state + elseif option == '--exclude' or option == '-x' then + state = SET_EXCLUDE + return state + end + error('Unknown option: '..option,3) + end + + local function setArg( cmdArg, state ) + if state == SET_OUTPUT then + result['output'] = cmdArg + return + elseif state == SET_FNAME then + result['fname'] = cmdArg + return + elseif state == SET_REPEAT then + result['exeRepeat'] = tonumber(cmdArg) + or error('Malformed -r argument: '..cmdArg) + return + elseif state == SET_PATTERN then + if result['pattern'] then + table.insert( result['pattern'], cmdArg ) + else + result['pattern'] = { cmdArg } + end + return + elseif state == SET_EXCLUDE then + local notArg = '!'..cmdArg + if result['pattern'] then + table.insert( result['pattern'], notArg ) + else + result['pattern'] = { notArg } + end + return + end + error('Unknown parse state: '.. state) + end + + + for i, cmdArg in ipairs(cmdLine) do + if state ~= nil then + setArg( cmdArg, state, result ) + state = nil + else + if cmdArg:sub(1,1) == '-' then + state = parseOption( cmdArg ) + else + if result['testNames'] then + table.insert( result['testNames'], cmdArg ) + else + result['testNames'] = { cmdArg } + end + end + end + end + + if result['help'] then + M.LuaUnit.help() + end + + if result['version'] then + M.LuaUnit.version() + end + + if state ~= nil then + error('Missing argument after '..cmdLine[ #cmdLine ],2 ) + end + + return result + end + + function M.LuaUnit.help() + print(M.USAGE) + os.exit(0) + end + + function M.LuaUnit.version() + print('LuaUnit v'..M.VERSION..' by Philippe Fremy ') + os.exit(0) + end + +---------------------------------------------------------------- +-- class NodeStatus +---------------------------------------------------------------- + + local NodeStatus = { __class__ = 'NodeStatus' } -- class + local NodeStatus_MT = { __index = NodeStatus } -- metatable + M.NodeStatus = NodeStatus + + -- values of status + NodeStatus.SUCCESS = 'SUCCESS' + NodeStatus.SKIP = 'SKIP' + NodeStatus.FAIL = 'FAIL' + NodeStatus.ERROR = 'ERROR' + + function NodeStatus.new( number, testName, className ) + -- default constructor, test are PASS by default + local t = { number = number, testName = testName, className = className } + setmetatable( t, NodeStatus_MT ) + t:success() + return t + end + + function NodeStatus:success() + self.status = self.SUCCESS + -- useless because lua does this for us, but it helps me remembering the relevant field names + self.msg = nil + self.stackTrace = nil + end + + function NodeStatus:skip(msg) + self.status = self.SKIP + self.msg = msg + self.stackTrace = nil + end + + function NodeStatus:fail(msg, stackTrace) + self.status = self.FAIL + self.msg = msg + self.stackTrace = stackTrace + end + + function NodeStatus:error(msg, stackTrace) + self.status = self.ERROR + self.msg = msg + self.stackTrace = stackTrace + end + + function NodeStatus:isSuccess() + return self.status == NodeStatus.SUCCESS + end + + function NodeStatus:isNotSuccess() + -- Return true if node is either failure or error or skip + return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP) + end + + function NodeStatus:isSkipped() + return self.status == NodeStatus.SKIP + end + + function NodeStatus:isFailure() + return self.status == NodeStatus.FAIL + end + + function NodeStatus:isError() + return self.status == NodeStatus.ERROR + end + + function NodeStatus:statusXML() + if self:isError() then + return table.concat( + {' \n', + ' \n'}) + elseif self:isFailure() then + return table.concat( + {' \n', + ' \n'}) + elseif self:isSkipped() then + return table.concat({' ', xmlEscape(self.msg),'\n' } ) + end + return ' \n' -- (not XSD-compliant! normally shouldn't get here) + end + + --------------[[ Output methods ]]------------------------- + + local function conditional_plural(number, singular) + -- returns a grammatically well-formed string "%d " + local suffix = '' + if number ~= 1 then -- use plural + suffix = (singular:sub(-2) == 'ss') and 'es' or 's' + end + return string.format('%d %s%s', number, singular, suffix) + end + + function M.LuaUnit.statusLine(result) + -- return status line string according to results + local s = { + string.format('Ran %d tests in %0.3f seconds', + result.runCount, result.duration), + conditional_plural(result.successCount, 'success'), + } + if result.notSuccessCount > 0 then + if result.failureCount > 0 then + table.insert(s, conditional_plural(result.failureCount, 'failure')) + end + if result.errorCount > 0 then + table.insert(s, conditional_plural(result.errorCount, 'error')) + end + else + table.insert(s, '0 failures') + end + if result.skippedCount > 0 then + table.insert(s, string.format("%d skipped", result.skippedCount)) + end + if result.nonSelectedCount > 0 then + table.insert(s, string.format("%d non-selected", result.nonSelectedCount)) + end + return table.concat(s, ', ') + end + + function M.LuaUnit:startSuite(selectedCount, nonSelectedCount) + self.result = { + selectedCount = selectedCount, + nonSelectedCount = nonSelectedCount, + successCount = 0, + runCount = 0, + currentTestNumber = 0, + currentClassName = "", + currentNode = nil, + suiteStarted = true, + startTime = os.clock(), + startDate = os.date(os.getenv('LUAUNIT_DATEFMT')), + startIsodate = os.date('%Y-%m-%dT%H:%M:%S'), + patternIncludeFilter = self.patternIncludeFilter, + + -- list of test node status + allTests = {}, + failedTests = {}, + errorTests = {}, + skippedTests = {}, + + failureCount = 0, + errorCount = 0, + notSuccessCount = 0, + skippedCount = 0, + } + + self.outputType = self.outputType or TextOutput + self.output = self.outputType.new(self) + self.output:startSuite() + end + + function M.LuaUnit:startClass( className, classInstance ) + self.result.currentClassName = className + self.output:startClass( className ) + self:setupClass( className, classInstance ) + end + + function M.LuaUnit:startTest( testName ) + self.result.currentTestNumber = self.result.currentTestNumber + 1 + self.result.runCount = self.result.runCount + 1 + self.result.currentNode = NodeStatus.new( + self.result.currentTestNumber, + testName, + self.result.currentClassName + ) + self.result.currentNode.startTime = os.clock() + table.insert( self.result.allTests, self.result.currentNode ) + self.output:startTest( testName ) + end + + function M.LuaUnit:updateStatus( err ) + -- "err" is expected to be a table / result from protectedCall() + if err.status == NodeStatus.SUCCESS then + return + end + + local node = self.result.currentNode + + --[[ As a first approach, we will report only one error or one failure for one test. + However, we can have the case where the test is in failure, and the teardown is in error. + In such case, it's a good idea to report both a failure and an error in the test suite. This is + what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for + example, there could be more (failures + errors) count that tests. What happens to the current node ? + We will do this more intelligent version later. + ]] + + -- if the node is already in failure/error, just don't report the new error (see above) + if node.status ~= NodeStatus.SUCCESS then + return + end + + if err.status == NodeStatus.FAIL then + node:fail( err.msg, err.trace ) + table.insert( self.result.failedTests, node ) + elseif err.status == NodeStatus.ERROR then + node:error( err.msg, err.trace ) + table.insert( self.result.errorTests, node ) + elseif err.status == NodeStatus.SKIP then + node:skip( err.msg ) + table.insert( self.result.skippedTests, node ) + else + error('No such status: ' .. prettystr(err.status)) + end + + self.output:updateStatus( node ) + end + + function M.LuaUnit:endTest() + local node = self.result.currentNode + -- print( 'endTest() '..prettystr(node)) + -- print( 'endTest() '..prettystr(node:isNotSuccess())) + node.duration = os.clock() - node.startTime + node.startTime = nil + self.output:endTest( node ) + + if node:isSuccess() then + self.result.successCount = self.result.successCount + 1 + elseif node:isError() then + if self.quitOnError or self.quitOnFailure then + -- Runtime error - abort test execution as requested by + -- "--error" option. This is done by setting a special + -- flag that gets handled in internalRunSuiteByInstances(). + print("\nERROR during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isFailure() then + if self.quitOnFailure then + -- Failure - abort test execution as requested by + -- "--failure" option. This is done by setting a special + -- flag that gets handled in internalRunSuiteByInstances(). + print("\nFailure during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isSkipped() then + self.result.runCount = self.result.runCount - 1 + else + error('No such node status: ' .. prettystr(node.status)) + end + self.result.currentNode = nil + end + + function M.LuaUnit:endClass() + self:teardownClass( self.lastClassName, self.lastClassInstance ) + self.output:endClass() + end + + function M.LuaUnit:endSuite() + if self.result.suiteStarted == false then + error('LuaUnit:endSuite() -- suite was already ended' ) + end + self.result.duration = os.clock()-self.result.startTime + self.result.suiteStarted = false + + -- Expose test counts for outputter's endSuite(). This could be managed + -- internally instead by using the length of the lists of failed tests + -- but unit tests rely on these fields being present. + self.result.failureCount = #self.result.failedTests + self.result.errorCount = #self.result.errorTests + self.result.notSuccessCount = self.result.failureCount + self.result.errorCount + self.result.skippedCount = #self.result.skippedTests + + self.output:endSuite() + end + + function M.LuaUnit:setOutputType(outputType, fname) + -- Configures LuaUnit runner output + -- outputType is one of: NIL, TAP, JUNIT, TEXT + -- when outputType is junit, the additional argument fname is used to set the name of junit output file + -- for other formats, fname is ignored + if outputType:upper() == "NIL" then + self.outputType = NilOutput + return + end + if outputType:upper() == "TAP" then + self.outputType = TapOutput + return + end + if outputType:upper() == "JUNIT" then + self.outputType = JUnitOutput + if fname then + self.fname = fname + end + return + end + if outputType:upper() == "TEXT" then + self.outputType = TextOutput + return + end + error( 'No such format: '..outputType,2) + end + + --------------[[ Runner ]]----------------- + + function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName) + -- if classInstance is nil, this is just a function call + -- else, it's method of a class being called. + + local function err_handler(e) + -- transform error into a table, adding the traceback information + return { + status = NodeStatus.ERROR, + msg = e, + trace = string.sub(debug.traceback("", 1), 2) + } + end + + local ok, err + if classInstance then + -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround + ok, err = xpcall( function () methodInstance(classInstance) end, err_handler ) + else + ok, err = xpcall( function () methodInstance() end, err_handler ) + end + if ok then + return {status = NodeStatus.SUCCESS} + end + -- print('ok="'..prettystr(ok)..'" err="'..prettystr(err)..'"') + + local iter_msg + iter_msg = self.exeRepeat and 'iteration '..self.currentCount + + err.msg, err.status = M.adjust_err_msg_with_iter( err.msg, iter_msg ) + + if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then + err.trace = nil + return err + end + + -- reformat / improve the stack trace + if prettyFuncName then -- we do have the real method name + err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '"..prettyFuncName.."'") + end + if STRIP_LUAUNIT_FROM_STACKTRACE then + err.trace = stripLuaunitTrace2(err.trace, err.msg) + end + + return err -- return the error "object" (table) + end + + + function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance) + -- When executing a test function, className and classInstance must be nil + -- When executing a class method, all parameters must be set + + if type(methodInstance) ~= 'function' then + self:unregisterSuite() + error( tostring(methodName)..' must be a function, not '..type(methodInstance)) + end + + local prettyFuncName + if className == nil then + className = '[TestFunctions]' + prettyFuncName = methodName + else + prettyFuncName = className..'.'..methodName + end + + if self.lastClassName ~= className then + if self.lastClassName ~= nil then + self:endClass() + end + self:startClass( className, classInstance ) + self.lastClassName = className + self.lastClassInstance = classInstance + end + + self:startTest(prettyFuncName) + + local node = self.result.currentNode + for iter_n = 1, self.exeRepeat or 1 do + if node:isNotSuccess() then + break + end + self.currentCount = iter_n + + -- run setUp first (if any) + if classInstance then + local func = self.asFunction( classInstance.setUp ) or + self.asFunction( classInstance.Setup ) or + self.asFunction( classInstance.setup ) or + self.asFunction( classInstance.SetUp ) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className..'.setUp')) + end + end + + -- run testMethod() + if node:isSuccess() then + self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName)) + end + + -- lastly, run tearDown (if any) + if classInstance then + local func = self.asFunction( classInstance.tearDown ) or + self.asFunction( classInstance.TearDown ) or + self.asFunction( classInstance.teardown ) or + self.asFunction( classInstance.Teardown ) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className..'.tearDown')) + end + end + end + + self:endTest() + end + + function M.LuaUnit.expandOneClass( result, className, classInstance ) + --[[ + Input: a list of { name, instance }, a class name, a class instance + Ouptut: modify result to add all test method instance in the form: + { className.methodName, classInstance } + ]] + for methodName, methodInstance in sortedPairs(classInstance) do + if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName( methodName ) then + table.insert( result, { className..'.'..methodName, classInstance } ) + end + end + end + + function M.LuaUnit.expandClasses( listOfNameAndInst ) + --[[ + -- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance} + -- functions and methods remain untouched + Input: a list of { name, instance } + Output: + * { function name, function instance } : do nothing + * { class.method name, class instance }: do nothing + * { class name, class instance } : add all method names in the form of (className.methodName, classInstance) + ]] + local result = {} + + for i,v in ipairs( listOfNameAndInst ) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + table.insert( result, { name, instance } ) + else + if type(instance) ~= 'table' then + error( 'Instance must be a table or a function, not a '..type(instance)..' with value '..prettystr(instance)) + end + local className, methodName = M.LuaUnit.splitClassMethod( name ) + if className then + local methodInstance = instance[methodName] + if methodInstance == nil then + error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) + end + table.insert( result, { name, instance } ) + else + M.LuaUnit.expandOneClass( result, name, instance ) + end + end + end + + return result + end + + function M.LuaUnit.applyPatternFilter( patternIncFilter, listOfNameAndInst ) + local included, excluded = {}, {} + for i, v in ipairs( listOfNameAndInst ) do + -- local name, instance = v[1], v[2] + if patternFilter( patternIncFilter, v[1] ) then + table.insert( included, v ) + else + table.insert( excluded, v ) + end + end + return included, excluded + end + + local function getKeyInListWithGlobalFallback( key, listOfNameAndInst ) + local result = nil + for i,v in ipairs( listOfNameAndInst ) do + if(listOfNameAndInst[i][1] == key) then + result = listOfNameAndInst[i][2] + break + end + end + if(not M.LuaUnit.asFunction( result ) ) then + result = _G[key] + end + return result + end + + function M.LuaUnit:setupSuite( listOfNameAndInst ) + local setupSuite = getKeyInListWithGlobalFallback("setupSuite", listOfNameAndInst) + if self.asFunction( setupSuite ) then + self:updateStatus( self:protectedCall( nil, setupSuite, 'setupSuite' ) ) + end + end + + function M.LuaUnit:teardownSuite(listOfNameAndInst) + local teardownSuite = getKeyInListWithGlobalFallback("teardownSuite", listOfNameAndInst) + if self.asFunction( teardownSuite ) then + self:updateStatus( self:protectedCall( nil, teardownSuite, 'teardownSuite') ) + end + end + + function M.LuaUnit:setupClass( className, instance ) + if type( instance ) == 'table' and self.asFunction( instance.setupClass ) then + self:updateStatus( self:protectedCall( instance, instance.setupClass, className..'.setupClass' ) ) + end + end + + function M.LuaUnit:teardownClass( className, instance ) + if type( instance ) == 'table' and self.asFunction( instance.teardownClass ) then + self:updateStatus( self:protectedCall( instance, instance.teardownClass, className..'.teardownClass' ) ) + end + end + + function M.LuaUnit:internalRunSuiteByInstances( listOfNameAndInst ) + --[[ Run an explicit list of tests. Each item of the list must be one of: + * { function name, function instance } + * { class name, class instance } + * { class.method name, class instance } + This function is internal to LuaUnit. The official API to perform this action is runSuiteByInstances() + ]] + + local expandedList = self.expandClasses( listOfNameAndInst ) + if self.shuffle then + randomizeTable( expandedList ) + end + local filteredList, filteredOutList = self.applyPatternFilter( + self.patternIncludeFilter, expandedList ) + + self:startSuite( #filteredList, #filteredOutList ) + self:setupSuite( listOfNameAndInst ) + + for i,v in ipairs( filteredList ) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + self:execOneFunction( nil, name, nil, instance ) + else + -- expandClasses() should have already taken care of sanitizing the input + assert( type(instance) == 'table' ) + local className, methodName = M.LuaUnit.splitClassMethod( name ) + assert( className ~= nil ) + local methodInstance = instance[methodName] + assert(methodInstance ~= nil) + self:execOneFunction( className, methodName, instance, methodInstance ) + end + if self.result.aborted then + break -- "--error" or "--failure" option triggered + end + end + + if self.lastClassName ~= nil then + self:endClass() + end + + self:teardownSuite( listOfNameAndInst ) + self:endSuite() + + if self.result.aborted then + print("LuaUnit ABORTED (as requested by --error or --failure option)") + self:unregisterSuite() + os.exit(-2) + end + end + + function M.LuaUnit:internalRunSuiteByNames( listOfName ) + --[[ Run LuaUnit with a list of generic names, coming either from command-line or from global + namespace analysis. Convert the list into a list of (name, valid instances (table or function)) + and calls internalRunSuiteByInstances. + ]] + + local instanceName, instance + local listOfNameAndInst = {} + + for i,name in ipairs( listOfName ) do + local className, methodName = M.LuaUnit.splitClassMethod( name ) + if className then + instanceName = className + instance = _G[instanceName] + + if instance == nil then + self:unregisterSuite() + error( "No such name in global space: "..instanceName ) + end + + if type(instance) ~= 'table' then + self:unregisterSuite() + error( 'Instance of '..instanceName..' must be a table, not '..type(instance)) + end + + local methodInstance = instance[methodName] + if methodInstance == nil then + self:unregisterSuite() + error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) + end + + else + -- for functions and classes + instanceName = name + instance = _G[instanceName] + end + + if instance == nil then + self:unregisterSuite() + error( "No such name in global space: "..instanceName ) + end + + if (type(instance) ~= 'table' and type(instance) ~= 'function') then + self:unregisterSuite() + error( 'Name must match a function or a table: '..instanceName ) + end + + table.insert( listOfNameAndInst, { name, instance } ) + end + + self:internalRunSuiteByInstances( listOfNameAndInst ) + end + + function M.LuaUnit.run(...) + -- Run some specific test classes. + -- If no arguments are passed, run the class names specified on the + -- command line. If no class name is specified on the command line + -- run all classes whose name starts with 'Test' + -- + -- If arguments are passed, they must be strings of the class names + -- that you want to run or generic command line arguments (-o, -p, -v, ...) + local runner = M.LuaUnit.new() + return runner:runSuite(...) + end + + function M.LuaUnit:registerSuite() + -- register the current instance into our global array of instances + -- print('-> Register suite') + M.LuaUnit.instances[ #M.LuaUnit.instances+1 ] = self + end + + function M.unregisterCurrentSuite() + -- force unregister the last registered suite + table.remove(M.LuaUnit.instances, #M.LuaUnit.instances) + end + + function M.LuaUnit:unregisterSuite() + -- print('<- Unregister suite') + -- remove our current instqances from the global array of instances + local instanceIdx = nil + for i, instance in ipairs(M.LuaUnit.instances) do + if instance == self then + instanceIdx = i + break + end + end + + if instanceIdx ~= nil then + table.remove(M.LuaUnit.instances, instanceIdx) + -- print('Unregister done') + end + + end + + function M.LuaUnit:initFromArguments( ... ) + --[[Parses all arguments from either command-line or direct call and set internal + flags of LuaUnit runner according to it. + Return the list of names which were possibly passed on the command-line or as arguments + ]] + local args = {...} + if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then + -- run was called with the syntax M.LuaUnit:runSuite() + -- we support both M.LuaUnit.run() and M.LuaUnit:run() + -- strip out the first argument self to make it a command-line argument list + table.remove(args,1) + end + + if #args == 0 then + args = cmdline_argv + end + + local options = pcall_or_abort( M.LuaUnit.parseCmdLine, args ) + + -- We expect these option fields to be either `nil` or contain + -- valid values, so it's safe to always copy them directly. + self.verbosity = options.verbosity + self.quitOnError = options.quitOnError + self.quitOnFailure = options.quitOnFailure + + self.exeRepeat = options.exeRepeat + self.patternIncludeFilter = options.pattern + self.shuffle = options.shuffle + + options.output = options.output or os.getenv('LUAUNIT_OUTPUT') + options.fname = options.fname or os.getenv('LUAUNIT_JUNIT_FNAME') + + if options.output then + if options.output:lower() == 'junit' and options.fname == nil then + print('With junit output, a filename must be supplied with -n or --name') + os.exit(-1) + end + pcall_or_abort(self.setOutputType, self, options.output, options.fname) + end + + return options.testNames + end + + function M.LuaUnit:runSuite( ... ) + testNames = self:initFromArguments(...) + self:registerSuite() + self:internalRunSuiteByNames( testNames or M.LuaUnit.collectTests() ) + self:unregisterSuite() + return self.result.notSuccessCount + end + + function M.LuaUnit:runSuiteByInstances( listOfNameAndInst, commandLineArguments ) + --[[ + Run all test functions or tables provided as input. + Input: a list of { name, instance } + instance can either be a function or a table containing test functions starting with the prefix "test" + return the number of failures and errors, 0 meaning success + ]] + -- parse the command-line arguments + testNames = self:initFromArguments( commandLineArguments ) + self:registerSuite() + self:internalRunSuiteByInstances( listOfNameAndInst ) + self:unregisterSuite() + return self.result.notSuccessCount + end + + + +-- class LuaUnit + +-- For compatbility with LuaUnit v2 +M.run = M.LuaUnit.run +M.Run = M.LuaUnit.run + +function M:setVerbosity( verbosity ) + -- set the verbosity value (as integer) + M.LuaUnit.verbosity = verbosity +end +M.set_verbosity = M.setVerbosity +M.SetVerbosity = M.setVerbosity + + +return M \ No newline at end of file diff --git a/tests/Lua/runtests.lua b/tests/Lua/runtests.lua index febb5c20a5..d055ab15fc 100644 --- a/tests/Lua/runtests.lua +++ b/tests/Lua/runtests.lua @@ -1,9 +1,9 @@ luaunit = require('luaunit') -TestMod = {} -function TestMod.testHello() - assertEquals(1, 1) -end +-- TestMod = {} +-- function TestMod.testHello() +-- assertEquals(1, 1) +-- end TestArithmetic = require('TestArithmetic') TestArray = require('TestArray') From b9a18335dde7b989edff5029cab63236b1778d6d Mon Sep 17 00:00:00 2001 From: Alex Swan <1506553+alexswan10k@users.noreply.github.com> Date: Fri, 10 Sep 2021 18:18:10 +0100 Subject: [PATCH 21/41] progress --- .vscode/launch.json | 11 ++ src/Fable.Transforms/Lua/Fable2Lua.fs | 12 +- src/Fable.Transforms/Lua/Lua.fs | 3 +- src/Fable.Transforms/Lua/LuaPrinter.fs | 14 +- src/fable-library-lua/fable/Util.lua | 212 +++++++++++++------------ tests/Lua/TestControlFlow.fs | 18 ++- 6 files changed, 159 insertions(+), 111 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 7e7abe28bd..7c3da83c0c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,6 +15,17 @@ "stopAtEntry": true, "console": "internalConsole" }, + { + "name": "BuildLuaTest", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/Fable.Cli/bin/Debug/net5.0/fable.dll", + "args": ["watch", "--cwd", "tests/Lua", "--exclude", "Fable.Core", "--outDir", "build/tests/Lua", "--lang", "Lua", "--fableLib", "build/tests/Lua/fable-lib"], + "cwd": "${workspaceFolder}", + "stopAtEntry": true, + "console": "internalConsole" + }, { "name": "Attach to Node", "port": 9229, diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index cd19df032d..d4ebcc23c5 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -169,6 +169,8 @@ module Transforms = | Fable.AnonymousRecordType(_, _) -> GetObjMethod(transformExpr expr, fieldName) | _ -> transformExpr expr + | Fable.Expr.Delegate _ -> + transformExpr expr |> Brackets | _ -> transformExpr expr FunctionCall(lhs, List.map transformExpr callInfo.Args) | Fable.Expr.Import (info, t, r) -> @@ -208,7 +210,7 @@ module Transforms = asSingleExprIifeTr com exprs | Fable.Expr.Let (ident, value, body) -> let statements = [ - Assignment([ident.Name], transformExpr value) + Assignment([ident.Name], transformExpr value, true) transformExpr body |> Return ] Helpers.maybeIife statements @@ -228,7 +230,7 @@ module Transforms = if idents.Length = boundValues.Length then let statements = [ for (ident, value) in List.zip idents boundValues do - yield Assignment([ident.Name], transformExpr value) + yield Assignment([ident.Name], transformExpr value, false) yield transformExpr target |> Return ] statements @@ -282,7 +284,7 @@ module Transforms = Function([], [ transformExpr body |> Return ]) - ])) + ]), true) let finalizer = finalizer |> Option.map transformExpr let catch = catch |> Option.map (fun (ident, expr) -> ident.Name, transformExpr expr) IfThenElse(Helpers.ident "status", [ @@ -301,10 +303,10 @@ module Transforms = let transformDeclarations (com: LuaCompiler) = function | Fable.ModuleDeclaration m -> - Assignment(["moduleDecTest"], Expr.Const (ConstString "moduledectest")) + Assignment(["moduleDecTest"], Expr.Const (ConstString "moduledectest"), false) | Fable.MemberDeclaration m -> if m.Args.Length = 0 then - Assignment([m.Name], transformExpr com m.Body) + Assignment([m.Name], transformExpr com m.Body, true) else let unwrapSelfExStatements = diff --git a/src/Fable.Transforms/Lua/Lua.fs b/src/Fable.Transforms/Lua/Lua.fs index f7046f9dd9..79935772f7 100644 --- a/src/Fable.Transforms/Lua/Lua.fs +++ b/src/Fable.Transforms/Lua/Lua.fs @@ -42,6 +42,7 @@ type Expr = | SetValue of Expr * value: Expr | SetExpr of Expr * Expr * value: Expr | FunctionCall of f: Expr * args: Expr list + | Brackets of Expr | AnonymousFunc of args: string list * body: Statement list | Unknown of string | Macro of string * args: Expr list @@ -52,7 +53,7 @@ type Expr = | NewArr of values: Expr list type Statement = - | Assignment of names: string list * Expr + | Assignment of names: string list * Expr * isLocal: bool | FunctionDeclaration of name: string * args: string list * body: Statement list * exportToMod: bool | Return of Expr | Do of Expr diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index fe24264fd4..e17ef546af 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -201,6 +201,10 @@ module Output = writeln ctx "" writei ctx "}" | NoOp -> () + | Brackets expr -> + write ctx "(" + writeExpr ctx expr + write ctx ")" | Unknown x -> writeCommented ctx "unknown" x | x -> sprintf "%A" x |> writeCommented ctx "todo" @@ -213,9 +217,11 @@ module Output = writeExpr ctx item and writeStatement ctx = function - | Assignment(names, expr) -> + | Assignment(names, expr, isLocal) -> let names = names |> Helper.separateWithCommas - writei ctx names + writei ctx "" + if isLocal then write ctx "local " + write ctx names write ctx " = " writeExpr ctx expr writeln ctx "" @@ -297,7 +303,7 @@ module Output = write ctx "return mod" //debugging writeln ctx "" - //writeln ctx "--[[" - //sprintf "%s" file.ASTDebug |> write ctx + // writeln ctx "--[[" + // sprintf "%s" file.ASTDebug |> write ctx //sprintf "%A" file.Statements |> write ctx //writeln ctx " --]]" \ No newline at end of file diff --git a/src/fable-library-lua/fable/Util.lua b/src/fable-library-lua/fable/Util.lua index 655f1f85d3..ca5ff0521a 100644 --- a/src/fable-library-lua/fable/Util.lua +++ b/src/fable-library-lua/fable/Util.lua @@ -26,6 +26,13 @@ -- end -- return mod +function TableConcat(t1,t2) + for i=1,#t2 do + t1[#t1+1] = t2[i] + end + return t1 +end + -- https://stackoverflow.com/questions/5977654/how-do-i-use-the-bitwise-operator-xor-in-lua local function BitXOR(a,b)--Bitwise xor local p,c=1,0 @@ -1459,106 +1466,111 @@ function ____exports.checkArity(self, arity, f) end)) or f end function ____exports.partialApply(self, arity, f, args) - if f == nil then - return nil - elseif f[CURRIED_KEY] ~= nil then - f = f[CURRIED_KEY] - do - local i = 0 - while i < #args do - f = f(nil, args[i + 1]) - i = i + 1 - end - end - return f - else - local ____switch209 = arity - if ____switch209 == 1 then - goto ____switch209_case_0 - elseif ____switch209 == 2 then - goto ____switch209_case_1 - elseif ____switch209 == 3 then - goto ____switch209_case_2 - elseif ____switch209 == 4 then - goto ____switch209_case_3 - elseif ____switch209 == 5 then - goto ____switch209_case_4 - elseif ____switch209 == 6 then - goto ____switch209_case_5 - elseif ____switch209 == 7 then - goto ____switch209_case_6 - elseif ____switch209 == 8 then - goto ____switch209_case_7 - end - goto ____switch209_case_default - ::____switch209_case_0:: - do - return function(____, a1) return f:apply( - nil, - __TS__ArrayConcat(args, {a1}) - ) end - end - ::____switch209_case_1:: - do - return function(____, a1) return function(____, a2) return f:apply( - nil, - __TS__ArrayConcat(args, {a1, a2}) - ) end end - end - ::____switch209_case_2:: - do - return function(____, a1) return function(____, a2) return function(____, a3) return f:apply( - nil, - __TS__ArrayConcat(args, {a1, a2, a3}) - ) end end end - end - ::____switch209_case_3:: - do - return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return f:apply( - nil, - __TS__ArrayConcat(args, {a1, a2, a3, a4}) - ) end end end end - end - ::____switch209_case_4:: - do - return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return f:apply( - nil, - __TS__ArrayConcat(args, {a1, a2, a3, a4, a5}) - ) end end end end end - end - ::____switch209_case_5:: - do - return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return function(____, a6) return f:apply( - nil, - __TS__ArrayConcat(args, {a1, a2, a3, a4, a5, a6}) - ) end end end end end end - end - ::____switch209_case_6:: - do - return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return function(____, a6) return function(____, a7) return f:apply( - nil, - __TS__ArrayConcat(args, {a1, a2, a3, a4, a5, a6, a7}) - ) end end end end end end end - end - ::____switch209_case_7:: - do - return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return function(____, a6) return function(____, a7) return function(____, a8) return f:apply( - nil, - __TS__ArrayConcat(args, {a1, a2, a3, a4, a5, a6, a7, a8}) - ) end end end end end end end end - end - ::____switch209_case_default:: - do - error( - __TS__New( - Error, - "Partially applying to more than 8-arity is not supported: " .. tostring(arity) - ), - 0 - ) - end - ::____switch209_end:: - end + if arity == 1 then + return function (a) return f(table.unpack(__TS__ArrayConcat({ table.unpack(args) }, a))) end + elseif arity == 2 then + return function (a, b) return f(table.unpack(__TS__ArrayConcat({ table.unpack(args) }, a, b))) end + end + -- if f == nil then + -- return nil + -- elseif type(f) == 'table' and f[CURRIED_KEY] ~= nil then + -- f = f[CURRIED_KEY] + -- do + -- local i = 0 + -- while i < #args do + -- f = f(nil, args[i + 1]) + -- i = i + 1 + -- end + -- end + -- return f + -- else + -- local ____switch209 = arity + -- if ____switch209 == 1 then + -- goto ____switch209_case_0 + -- elseif ____switch209 == 2 then + -- goto ____switch209_case_1 + -- elseif ____switch209 == 3 then + -- goto ____switch209_case_2 + -- elseif ____switch209 == 4 then + -- goto ____switch209_case_3 + -- elseif ____switch209 == 5 then + -- goto ____switch209_case_4 + -- elseif ____switch209 == 6 then + -- goto ____switch209_case_5 + -- elseif ____switch209 == 7 then + -- goto ____switch209_case_6 + -- elseif ____switch209 == 8 then + -- goto ____switch209_case_7 + -- end + -- goto ____switch209_case_default + -- ::____switch209_case_0:: + -- do + -- return function(____, a1) return f:apply( + -- nil, + -- __TS__ArrayConcat(args, {a1}) + -- ) end + -- end + -- ::____switch209_case_1:: + -- do + -- return function(____, a1) return function(____, a2) return f:apply( + -- nil, + -- __TS__ArrayConcat(args, {a1, a2}) + -- ) end end + -- end + -- ::____switch209_case_2:: + -- do + -- return function(____, a1) return function(____, a2) return function(____, a3) return f:apply( + -- nil, + -- __TS__ArrayConcat(args, {a1, a2, a3}) + -- ) end end end + -- end + -- ::____switch209_case_3:: + -- do + -- return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return f:apply( + -- nil, + -- __TS__ArrayConcat(args, {a1, a2, a3, a4}) + -- ) end end end end + -- end + -- ::____switch209_case_4:: + -- do + -- return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return f:apply( + -- nil, + -- __TS__ArrayConcat(args, {a1, a2, a3, a4, a5}) + -- ) end end end end end + -- end + -- ::____switch209_case_5:: + -- do + -- return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return function(____, a6) return f:apply( + -- nil, + -- __TS__ArrayConcat(args, {a1, a2, a3, a4, a5, a6}) + -- ) end end end end end end + -- end + -- ::____switch209_case_6:: + -- do + -- return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return function(____, a6) return function(____, a7) return f:apply( + -- nil, + -- __TS__ArrayConcat(args, {a1, a2, a3, a4, a5, a6, a7}) + -- ) end end end end end end end + -- end + -- ::____switch209_case_7:: + -- do + -- return function(____, a1) return function(____, a2) return function(____, a3) return function(____, a4) return function(____, a5) return function(____, a6) return function(____, a7) return function(____, a8) return f:apply( + -- nil, + -- __TS__ArrayConcat(args, {a1, a2, a3, a4, a5, a6, a7, a8}) + -- ) end end end end end end end end + -- end + -- ::____switch209_case_default:: + -- do + -- error( + -- __TS__New( + -- Error, + -- "Partially applying to more than 8-arity is not supported: " .. tostring(arity) + -- ), + -- 0 + -- ) + -- end + -- ::____switch209_end:: + -- end end function ____exports.mapCurriedArgs(self, fn, mappings) local function mapArg(self, fn, arg, mappings, idx) diff --git a/tests/Lua/TestControlFlow.fs b/tests/Lua/TestControlFlow.fs index e3dfa46d36..3713b33899 100644 --- a/tests/Lua/TestControlFlow.fs +++ b/tests/Lua/TestControlFlow.fs @@ -67,4 +67,20 @@ let testExThrow() = 3 with ex -> 4 - a |> equal 4 \ No newline at end of file + a |> equal 4 + + +[] +let testSimpleFnParam() = + let add a b = a + b + let fn addFn a b = addFn a b + fn (add) 3 2 |> equal 5 + +[] +let testPartialApply() = + let add a b = a + b + let fn addFn a b = addFn a b + fn (add) 3 2 |> equal 5 + let fnAdd4 = fn (add) 4 + fnAdd4 5 |> equal 9 + fnAdd4 2 |> equal 6 \ No newline at end of file From 092698393470a797f9f414b9293591bca314a700 Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Wed, 7 Dec 2022 20:42:58 -0500 Subject: [PATCH 22/41] tidying --- build.fsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.fsx b/build.fsx index 581dac418e..405cf51b0b 100644 --- a/build.fsx +++ b/build.fsx @@ -216,7 +216,8 @@ let buildLibraryPy() = // Copy python related files from projectDir to buildDir copyFiles libraryDir "*" buildDirPy copyFiles projectDir "*.py" (buildDirPy "fable_library") - // Fix issues with Fable .fsproj not supporting links + + // Fix issues with Fable .fsproj not supporting links copyDirNonRecursive (buildDirPy "fable_library/fable-library") (buildDirPy "fable_library") removeDirRecursive (buildDirPy "fable_library/fable-library") From f873b04069a2943358ffb1cf80b0603afb76e04a Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Wed, 7 Dec 2022 20:50:16 -0500 Subject: [PATCH 23/41] updated package-lock to align with main --- package-lock.json | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 57bc008fe8..003d51b574 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,4 +1,6 @@ { + "name": "Fable", + "lockfileVersion": 2, "requires": true, "packages": { "": { @@ -2774,6 +2776,14 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2784,14 +2794,6 @@ "strip-ansi": "^6.0.1" } }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", From e50fa35c150580532d0f20753ead0cefdf7e1925 Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Wed, 7 Dec 2022 20:54:06 -0500 Subject: [PATCH 24/41] updated launch.json to use .net 6 --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 4e7eed60f3..976c08bd5b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/src/Fable.Cli/bin/Debug/net5.0/fable.dll", + "program": "${workspaceFolder}/src/Fable.Cli/bin/Debug/net6.0/fable.dll", "args": ["watch", "--cwd", "tests/Lua", "--exclude", "Fable.Core", "--outDir", "build/tests/Lua", "--lang", "Lua", "--fableLib", "build/tests/Lua/fable-lib"], "cwd": "${workspaceFolder}", "stopAtEntry": true, From 9a14ff466184ae88b314039e26aaeac7b5880596 Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Wed, 7 Dec 2022 23:44:12 -0500 Subject: [PATCH 25/41] lua spike, getting there --- src/Fable.Cli/Pipeline.fs | 31 +++-- src/Fable.Transforms/FSharp2Fable.Util.fs | 158 ++++++++++++---------- src/Fable.Transforms/Lua/Fable2Lua.fs | 45 +++--- src/Fable.Transforms/Lua/Lua.fs | 13 +- src/Fable.Transforms/Replacements.Api.fs | 2 +- 5 files changed, 141 insertions(+), 108 deletions(-) diff --git a/src/Fable.Cli/Pipeline.fs b/src/Fable.Cli/Pipeline.fs index cf92817cd8..17f5685643 100644 --- a/src/Fable.Cli/Pipeline.fs +++ b/src/Fable.Cli/Pipeline.fs @@ -344,18 +344,33 @@ module Rust = module Lua = open Fable.Transforms - let compileFile (com: Compiler) (cliArgs: CliArgs) dedupTargetDir (outPath: string) = async { + type LuaWriter(com: Compiler, cliArgs: CliArgs, pathResolver, targetPath: string) = + let sourcePath = com.CurrentFile + let fileExt = cliArgs.CompilerOptions.FileExtension + let stream = new IO.StreamWriter(targetPath) + interface Printer.Writer with + member _.Write(str) = + stream.WriteAsync(str) |> Async.AwaitTask + member _.MakeImportPath(path) = + let projDir = IO.Path.GetDirectoryName(cliArgs.ProjectFile) + let path = Imports.getImportPath pathResolver sourcePath targetPath projDir cliArgs.OutDir path + if path.EndsWith(".fs") then Path.ChangeExtension(path, fileExt) else path + member _.AddSourceMapping _ = () + member _.AddLog(msg, severity, ?range) = + com.AddLog(msg, severity, ?range=range, fileName=com.CurrentFile) + member _.Dispose() = stream.Dispose() + + let compileFile (com: Compiler) (cliArgs: CliArgs) pathResolver isSilent (outPath: string) = async { let program = FSharp2Fable.Compiler.transformFile com |> FableTransforms.transformFile com |> Fable2Lua.transformFile com - use w = new IO.StreamWriter(outPath) - let ctx = LuaPrinter.Output.Writer.create w - LuaPrinter.Output.writeFile ctx program - //use writer = new LuaWriter(com, cliArgs, dedupTargetDir, outPath) - //do! (writer :> LuaPrinter.Writer).Write(sprintf "AST: %A" fable.Declarations) - //do! run writer program + // + if not (isSilent) then + use writer = new LuaWriter(com, cliArgs, pathResolver, outPath) + do! LuaPrinter.run writer crate + } @@ -366,4 +381,4 @@ let compileFile (com: Compiler) (cliArgs: CliArgs) pathResolver isSilent (outPat | Php -> Php.compileFile com cliArgs pathResolver isSilent outPath | Dart -> Dart.compileFile com cliArgs pathResolver isSilent outPath | Rust -> Rust.compileFile com cliArgs pathResolver isSilent outPath - | Lua -> Lua.compileFile com cliArgs dedupTargetDir outPath + | Lua -> Lua.compileFile com cliArgs pathResolver isSilent outPath diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index e617d91c37..ab7d693b9f 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -310,8 +310,10 @@ type FsEnt(maybeAbbrevEnt: FSharpEntity) = Path.normalizePath asmPath |> Fable.AssemblyPath | None -> FsEnt.SourcePath ent |> Fable.SourcePath - { FullName = FsEnt.FullName ent - Path = path } + { + FullName = FsEnt.FullName ent + Path = path + } interface Fable.Entity with member _.Ref = FsEnt.Ref ent @@ -377,36 +379,38 @@ type FsEnt(maybeAbbrevEnt: FSharpEntity) = type Scope = (FSharpMemberOrFunctionOrValue option * Fable.Ident * Fable.Expr option) list type Context = - { Scope: Scope - ScopeInlineValues: (FSharpMemberOrFunctionOrValue * FSharpExpr) list - UsedNamesInRootScope: Set - UsedNamesInDeclarationScope: HashSet - CapturedBindings: HashSet - GenericArgs: Map - EnclosingMember: FSharpMemberOrFunctionOrValue option - PrecompilingInlineFunction: FSharpMemberOrFunctionOrValue option - CaughtException: Fable.Ident option - BoundConstructorThis: Fable.Ident option - BoundMemberThis: Fable.Ident option - InlinePath: Log.InlinePath list - CaptureBaseConsCall: (FSharpEntity * (Fable.Expr -> unit)) option - Witnesses: Fable.Witness list + { + Scope: Scope + ScopeInlineValues: (FSharpMemberOrFunctionOrValue * FSharpExpr) list + UsedNamesInRootScope: Set + UsedNamesInDeclarationScope: HashSet + CapturedBindings: HashSet + GenericArgs: Map + EnclosingMember: FSharpMemberOrFunctionOrValue option + PrecompilingInlineFunction: FSharpMemberOrFunctionOrValue option + CaughtException: Fable.Ident option + BoundConstructorThis: Fable.Ident option + BoundMemberThis: Fable.Ident option + InlinePath: Log.InlinePath list + CaptureBaseConsCall: (FSharpEntity * (Fable.Expr -> unit)) option + Witnesses: Fable.Witness list } static member Create(?usedRootNames) = - { Scope = [] - ScopeInlineValues = [] - UsedNamesInRootScope = defaultArg usedRootNames Set.empty - UsedNamesInDeclarationScope = Unchecked.defaultof<_> - CapturedBindings = Unchecked.defaultof<_> - GenericArgs = Map.empty - EnclosingMember = None - PrecompilingInlineFunction = None - CaughtException = None - BoundConstructorThis = None - BoundMemberThis = None - InlinePath = [] - CaptureBaseConsCall = None - Witnesses = [] + { + Scope = [] + ScopeInlineValues = [] + UsedNamesInRootScope = defaultArg usedRootNames Set.empty + UsedNamesInDeclarationScope = Unchecked.defaultof<_> + CapturedBindings = Unchecked.defaultof<_> + GenericArgs = Map.empty + EnclosingMember = None + PrecompilingInlineFunction = None + CaughtException = None + BoundConstructorThis = None + BoundMemberThis = None + InlinePath = [] + CaptureBaseConsCall = None + Witnesses = [] } type IFableCompiler = @@ -645,9 +649,11 @@ module Helpers = else memb.Accessibility.IsPublic let makeRange (r: Range) = - { start = { line = r.StartLine; column = r.StartColumn } - ``end``= { line = r.EndLine; column = r.EndColumn } - identifierName = None } + { + start = { line = r.StartLine; column = r.StartColumn } + ``end``= { line = r.EndLine; column = r.EndColumn } + identifierName = None + } let makeRangeFrom (fsExpr: FSharpExpr) = Some (makeRange fsExpr.Range) @@ -681,7 +687,7 @@ module Helpers = // Mutable public values must be called as functions in JS (see #986) let isModuleValueCompiledAsFunction (com: Compiler) (memb: FSharpMemberOrFunctionOrValue) = match com.Options.Language with - | Python | JavaScript | TypeScript -> memb.IsMutable && isNotPrivate memb + | Python | JavaScript | TypeScript | Lua -> memb.IsMutable && isNotPrivate memb | Rust | Php | Dart -> false let isModuleValueForCalls com (declaringEntity: FSharpEntity) (memb: FSharpMemberOrFunctionOrValue) = @@ -705,7 +711,7 @@ module Helpers = if interfaceFullname = fullname2 then true else e.DeclaredInterfaces - |> Seq.exists (testInterfaceHierarchy interfaceFullname) + |> Seq.exists (testInterfaceHierarchy interfaceFullname) | _ -> false let isOptionalParam (p: FSharpParameter) = @@ -1097,14 +1103,18 @@ module TypeHelpers = let private makeRuntimeTypeWithMeasure (genArgs: IList) fullName = let genArgs = [getMeasureFullName genArgs |> Fable.Measure] let r: Fable.EntityRef = - { FullName = fullName - Path = Fable.CoreAssemblyName "System.Runtime" } + { + FullName = fullName + Path = Fable.CoreAssemblyName "System.Runtime" + } Fable.DeclaredType(r, genArgs) let private makeFSharpCoreType fullName = let r: Fable.EntityRef = - { FullName = fullName - Path = Fable.CoreAssemblyName "FSharp.Core" } + { + FullName = fullName + Path = Fable.CoreAssemblyName "FSharp.Core" + } Fable.DeclaredType(r, []) let makeTypeFromDef withConstraints ctxTypeArgs (genArgs: IList) (tdef: FSharpEntity) = @@ -1488,13 +1498,13 @@ module TypeHelpers = fieldNames |> Array.tryFindIndex ((=) m.DisplayName) |> function - | None -> + | None -> if expectedTypes |> List.forall (function | Fable.Option _ -> true | _ -> false) then None // Optional fields can be missing else formatMissingFieldError m.DisplayName expectedTypes |> Some - | Some i -> + | Some i -> let expr = List.item i argExprs let ty = expr.Type if ty |> fitsInto (Allow.TheUsual ||| Allow.AnyIntoErased) expectedTypes then @@ -1557,8 +1567,8 @@ module TypeHelpers = // sort errors by their appearance in code |> List.sortBy fst |> function - | [] -> Ok () - | errors -> Error errors + | [] -> Ok () + | errors -> Error errors | _ -> Ok () // TODO: Error instead if we cannot check the interface? @@ -1596,13 +1606,15 @@ module Identifiers = ctx.UsedNamesInDeclarationScope.Add(sanitizedName) |> ignore - { Name = sanitizedName - Type = makeType ctx.GenericArgs fsRef.FullType - IsThisArgument = fsRef.IsMemberThisValue - IsCompilerGenerated = fsRef.IsCompilerGenerated - IsMutable = isMutable - Range = { makeRange fsRef.DeclarationLocation - with identifierName = Some fsRef.DisplayName } |> Some } + { + Name = sanitizedName + Type = makeType ctx.GenericArgs fsRef.FullType + IsThisArgument = fsRef.IsMemberThisValue + IsCompilerGenerated = fsRef.IsCompilerGenerated + IsMutable = isMutable + Range = { makeRange fsRef.DeclarationLocation + with identifierName = Some fsRef.DisplayName } |> Some + } let putIdentInScope com ctx (fsRef: FSharpMemberOrFunctionOrValue) value: Context*Fable.Ident = let ident = makeIdentFrom com ctx fsRef @@ -1929,11 +1941,11 @@ module Util = // We cannot retrieve compiler generated members from the entity | Some ent when not memb.IsCompilerGenerated -> let nonCurriedArgTypes = - if memb.CurriedParameterGroups.Count = 1 then - memb.CurriedParameterGroups[0] - |> Seq.mapToList (fun p -> makeType Map.empty p.Type) - |> Some - else None + if memb.CurriedParameterGroups.Count = 1 then + memb.CurriedParameterGroups[0] + |> Seq.mapToList (fun p -> makeType Map.empty p.Type) + |> Some + else None Fable.MemberRef(FsEnt.Ref(ent), { CompiledName = memb.CompiledName IsInstance = memb.IsInstanceMember @@ -1942,9 +1954,9 @@ module Util = | ent -> let entRef = ent |> Option.map FsEnt.Ref let argTypes = - memb.CurriedParameterGroups - |> Seq.concat - |> Seq.mapToList (fun p -> makeType Map.empty p.Type) + memb.CurriedParameterGroups + |> Seq.concat + |> Seq.mapToList (fun p -> makeType Map.empty p.Type) let returnType = makeType Map.empty memb.ReturnParameter.Type Fable.GeneratedMember.Function(memb.CompiledName, argTypes, returnType, isInstance=memb.IsInstanceMember, hasSpread=hasParamArray memb, ?entRef=entRef) @@ -2084,16 +2096,18 @@ module Util = match entity with | Some ent when isReplacementCandidateFrom ent -> let info: Fable.ReplaceCallInfo = - { SignatureArgTypes = callInfo.SignatureArgTypes - DeclaringEntityFullName = ent.FullName - HasSpread = hasParamArray memb - IsModuleValue = isModuleValueForCalls com ent memb - IsInterface = ent.IsInterface - CompiledName = memb.CompiledName - OverloadSuffix = - if ent.IsFSharpModule then "" - else getOverloadSuffixFrom ent memb - GenericArgs = callInfo.GenericArgs } + { + SignatureArgTypes = callInfo.SignatureArgTypes + DeclaringEntityFullName = ent.FullName + HasSpread = hasParamArray memb + IsModuleValue = isModuleValueForCalls com ent memb + IsInterface = ent.IsInterface + CompiledName = memb.CompiledName + OverloadSuffix = + if ent.IsFSharpModule then "" + else getOverloadSuffixFrom ent memb + GenericArgs = callInfo.GenericArgs + } match ctx.PrecompilingInlineFunction with | Some _ -> // Deal with reraise so we don't need to save caught exception every time @@ -2143,9 +2157,11 @@ module Util = | Atts.emitProperty -> "$0." + macro + "{{=$1}}" | _ -> macro let emitInfo: Fable.EmitInfo = - { Macro = macro - IsStatement = isStatement - CallInfo = callInfo } + { + Macro = macro + IsStatement = isStatement + CallInfo = callInfo + } Fable.Emit(emitInfo, typ, r) |> Some | _ -> None) diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index d4ebcc23c5..f3a76fb84c 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -13,8 +13,6 @@ open Fable.Compilers.Lua open Fable.Naming open Fable.Core -// type ILuaCompiler = -// inherit Compiler // type LuaCompiler(com: Fable.Compiler) = // interface ILuaCompiler // with @@ -58,7 +56,7 @@ module Transforms = NewObj(equality::pairs) else sprintf "Names and values do not match %A %A" names values |> Unknown let transformValueKind (com: LuaCompiler) = function - | Fable.NumberConstant(v,_,_) -> + | Fable.NumberConstant(:? float as v,kind,_) -> Const(ConstNumber v) | Fable.StringConstant(s) -> Const(ConstString s) @@ -77,7 +75,7 @@ module Transforms = let values = values |> List.map (transformExpr com) Helpers.tryNewObj names values else sprintf "unknown ety %A %A %A %A" values ref args entity |> Unknown - | Fable.NewAnonymousRecord(values, names, _) -> + | Fable.NewAnonymousRecord(values, names, _, _) -> let transformedValues = values |> List.map (transformExpr com) Helpers.tryNewObj (Array.toList names) transformedValues | Fable.NewUnion(values, tag, _, _) -> @@ -89,8 +87,10 @@ module Transforms = // let fields = values |> List.mapi(fun i x -> sprintf "p_%i" i, transformExpr com x) // NewObj(fields) NewArr(values |> List.map (transformExpr com)) - | Fable.NewArray(values, t) -> - NewArr(values |> List.map (transformExpr com)) + | Fable.NewArray(kind, t, _) -> + match kind with + | Fable.ArrayValues values -> NewArr(values |> List.map (transformExpr com)) + | _ -> NewArr([]) | Fable.Null _ -> Const(ConstNull) | x -> sprintf "unknown %A" x |> ConstString |> Const @@ -98,9 +98,10 @@ module Transforms = let transformExpr = transformExpr com function | Fable.OperationKind.Binary(BinaryModulus, left, right) -> - GetField(Helpers.ident "math", "fmod") |> Helpers.fcall [transformExpr left; transformExpr right] + GetField(Helpers.ident "math", "fmod") |> Helpers.fcall [transformExpr left; transformExpr right] | Fable.OperationKind.Binary (op, left, right) -> - let op = match op with + let op = + match op with | BinaryMultiply -> Multiply | BinaryDivide -> Divide | BinaryEqual -> Equals @@ -125,8 +126,7 @@ module Transforms = let asSingleExprIife (exprs: Expr list): Expr= //function match exprs with | [] -> NoOp - | [h] -> - h + | [h] -> h | exprs -> let statements = Helpers.transformStatements @@ -157,17 +157,16 @@ module Transforms = let transformExpr (com: LuaCompiler) expr= let transformExpr = transformExpr com let transformOp = transformOp com - match expr with | Fable.Expr.Value(value, _) -> transformValueKind com value | Fable.Expr.Call(expr, callInfo, t, r) -> let lhs = match expr with - | Fable.Expr.Get(expr, Fable.GetKind.FieldGet(fieldName, isMut), t, _) -> + | Fable.Expr.Get(expr, Fable.GetKind.FieldGet info, t, _) -> match t with | Fable.DeclaredType(_, _) - | Fable.AnonymousRecordType(_, _) -> - GetObjMethod(transformExpr expr, fieldName) + | Fable.AnonymousRecordType(_, _, _) -> + GetObjMethod(transformExpr expr, info.Name) | _ -> transformExpr expr | Fable.Expr.Delegate _ -> transformExpr expr |> Brackets @@ -190,12 +189,12 @@ module Transforms = | s -> GetObjMethod(rcall, s) | Fable.Expr.IdentExpr(i) when i.Name <> "" -> Ident {Namespace = None; Name = i.Name } - | Fable.Expr.Operation (kind, _, _) -> + | Fable.Expr.Operation (kind, _, _, _) -> transformOp kind - | Fable.Expr.Get(expr, Fable.GetKind.FieldGet(fieldName, isMut), t, _) -> - GetField(transformExpr expr, fieldName) - | Fable.Expr.Get(expr, Fable.GetKind.UnionField(caseIdx, fieldIdx), _, _) -> - GetField(transformExpr expr, sprintf "p_%i" fieldIdx) + | Fable.Expr.Get(expr, Fable.GetKind.FieldGet info, t, _) -> + GetField(transformExpr expr, info.Name) + | Fable.Expr.Get(expr, Fable.GetKind.UnionField info , _, _) -> + GetField(transformExpr expr, sprintf "p_%i" info.FieldIndex) | Fable.Expr.Get(expr, Fable.GetKind.ExprGet(e), _, _) -> GetAtIndex(transformExpr expr, transformExpr e) | Fable.Expr.Get(expr, Fable.GetKind.TupleIndex(i), _, _) -> @@ -266,7 +265,7 @@ module Transforms = FunctionCall(Helpers.ident "error", [errorExpr]) | Fable.Extended(Fable.ExtendedSet.Curry(expr, d), _) -> transformExpr expr |> sprintf "todo curry %A" |> Unknown - | Fable.Delegate(idents, body, _) -> + | Fable.Delegate(idents, body, _, _) -> Function(idents |> List.map(fun i -> i.Name), [transformExpr body |> Return |> flattenReturnIifes]) //can be flattened | Fable.ForLoop(ident, start, limit, body, isUp, _) -> Helpers.maybeIife [ @@ -274,7 +273,7 @@ module Transforms = ] | Fable.TypeCast(expr, t) -> transformExpr expr //typecasts are meaningless - | Fable.WhileLoop(guard, body, label, range) -> + | Fable.WhileLoop(guard, body, label) -> Helpers.maybeIife [ WhileLoop(transformExpr guard, [transformExpr body |> Do]) ] @@ -308,13 +307,13 @@ module Transforms = if m.Args.Length = 0 then Assignment([m.Name], transformExpr com m.Body, true) else - + let info = com.Com.GetMember(m.MemberRef) let unwrapSelfExStatements = match transformExpr com m.Body |> Return |> flattenReturnIifes with | Return (FunctionCall(AnonymousFunc([], statements), [])) -> statements | s -> [s] - FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), unwrapSelfExStatements, m.Info.IsPublic) + FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), unwrapSelfExStatements, info.IsPublic) | Fable.ClassDeclaration(d) -> com.AddClassDecl d //todo - build prototype members out diff --git a/src/Fable.Transforms/Lua/Lua.fs b/src/Fable.Transforms/Lua/Lua.fs index 79935772f7..637e57be28 100644 --- a/src/Fable.Transforms/Lua/Lua.fs +++ b/src/Fable.Transforms/Lua/Lua.fs @@ -9,8 +9,9 @@ type Const = | ConstNull type LuaIdentity = - { Namespace: string option - Name: string + { + Namespace: string option + Name: string } type UnaryOp = @@ -63,6 +64,8 @@ type Statement = | IfThenElse of guard: Expr * thenSt: Statement list * elseSt: Statement list type File = - { Filename: string - Statements: (Statement) list - ASTDebug: string } \ No newline at end of file + { + Filename: string + Statements: (Statement) list + ASTDebug: string + } \ No newline at end of file diff --git a/src/Fable.Transforms/Replacements.Api.fs b/src/Fable.Transforms/Replacements.Api.fs index 5d6325fe29..749066c658 100644 --- a/src/Fable.Transforms/Replacements.Api.fs +++ b/src/Fable.Transforms/Replacements.Api.fs @@ -86,7 +86,7 @@ let createMutablePublicValue (com: ICompiler) value = match com.Options.Language with | Python -> Py.Replacements.createAtom com value | JavaScript | TypeScript -> JS.Replacements.createAtom com value - | Rust | Php | Dart -> value + | Rust | Php | Dart | Lua -> value let getRefCell (com: ICompiler) r typ (expr: Expr) = match com.Options.Language with From c7f2313de51405645e3e7bdc4c7e7d032fd988e8 Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Thu, 8 Dec 2022 08:26:12 -0500 Subject: [PATCH 26/41] only warnings now --- src/Fable.Cli/Pipeline.fs | 6 +++--- src/Fable.Transforms/Lua/LuaPrinter.fs | 22 ++++++++++++++++++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/Fable.Cli/Pipeline.fs b/src/Fable.Cli/Pipeline.fs index 17f5685643..f2b83da6c0 100644 --- a/src/Fable.Cli/Pipeline.fs +++ b/src/Fable.Cli/Pipeline.fs @@ -361,15 +361,15 @@ module Lua = member _.Dispose() = stream.Dispose() let compileFile (com: Compiler) (cliArgs: CliArgs) pathResolver isSilent (outPath: string) = async { - let program = + let lua = FSharp2Fable.Compiler.transformFile com |> FableTransforms.transformFile com |> Fable2Lua.transformFile com // - if not (isSilent) then + if not (isSilent || LuaPrinter.isEmpty lua) then use writer = new LuaWriter(com, cliArgs, pathResolver, outPath) - do! LuaPrinter.run writer crate + do! LuaPrinter.run writer lua } diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index e17ef546af..c3b21eadbe 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -7,10 +7,17 @@ open Fable open Fable.AST open Fable.AST.Lua +type System.Text.StringBuilder with + member sb.Write (txt: string) = + sb.Append(txt) |> ignore + member sb.WriteLine (txt: string) = + sb.Append(txt) |> ignore + sb.AppendLine() |> ignore module Output = + type Writer = - { Writer: TextWriter + { Writer: System.Text.StringBuilder Indent: int Precedence: int CurrentNamespace: string option } @@ -306,4 +313,15 @@ module Output = // writeln ctx "--[[" // sprintf "%s" file.ASTDebug |> write ctx //sprintf "%A" file.Statements |> write ctx - //writeln ctx " --]]" \ No newline at end of file + //writeln ctx " --]]" + +let isEmpty (file: File): bool = + false //TODO: determine if printer will not print anything + +let run (writer: Printer.Writer) (lib: File): Async = + async { + let sb = System.Text.StringBuilder() + let ctx = Output.Writer.create sb + Output.writeFile ctx lib + do! writer.Write(sb.ToString()) + } \ No newline at end of file From 6c0e1205566eb905f72df7148c10ea19ce50c21c Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Thu, 8 Dec 2022 08:30:17 -0500 Subject: [PATCH 27/41] build succeeds compiling lua fails --- src/Fable.Cli/Entry.fs | 2 ++ src/Fable.Cli/Util.fs | 1 + src/Fable.Transforms/Lua/Fable2Lua.fs | 6 +++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Fable.Cli/Entry.fs b/src/Fable.Cli/Entry.fs index d9b2dded5a..9dc39e40d0 100644 --- a/src/Fable.Cli/Entry.fs +++ b/src/Fable.Cli/Entry.fs @@ -237,6 +237,7 @@ type Runner = | Python -> "FABLE_COMPILER_PYTHON" | TypeScript -> "FABLE_COMPILER_TYPESCRIPT" | JavaScript -> "FABLE_COMPILER_JAVASCRIPT" + | Lua -> "FABLE_COMPILER_LUA" ] |> List.distinct @@ -364,6 +365,7 @@ let getStatus = function | Dart -> "beta" | TypeScript -> "alpha" | Php -> "experimental" + | Lua -> "experimental" [] let main argv = diff --git a/src/Fable.Cli/Util.fs b/src/Fable.Cli/Util.fs index a5649e3905..730b2e9182 100644 --- a/src/Fable.Cli/Util.fs +++ b/src/Fable.Cli/Util.fs @@ -150,6 +150,7 @@ module File = | Fable.Dart -> ".dart" | Fable.Rust -> ".rs" | Fable.JavaScript -> ".js" + | Fable.Lua -> ".lua" match language, usesOutDir with | Fable.Python, _ -> fileExt // Extension will always be .py for Python diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index f3a76fb84c..1c0f08477e 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -175,11 +175,11 @@ module Transforms = | Fable.Expr.Import (info, t, r) -> let path = match info.Kind, info.Path with - | LibraryImport, Regex "fable-lib\/(\w+).(?:fs|js)" [name] -> + | libImport, Regex "fable-lib\/(\w+).(?:fs|js)" [name] -> "fable-lib/" + name - | LibraryImport, Regex"fable-library-lua\/fable\/fable-library\/(\w+).(?:fs|js)" [name] -> + | _, Regex"fable-library-lua\/fable\/fable-library\/(\w+).(?:fs|js)" [name] -> "fable-lib/fable-library" + name - | LibraryImport, Regex"fable-library-lua\/fable\/(\w+).(?:fs|js)" [name] -> + | _, Regex"fable-library-lua\/fable\/(\w+).(?:fs|js)" [name] -> "fable-lib/" + name | _ -> info.Path.Replace(".fs", "").Replace(".js", "") //todo - make less brittle From 1738ac778829077bdfbf231a02a8a439ffc009f9 Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Thu, 8 Dec 2022 09:19:31 -0500 Subject: [PATCH 28/41] no more warnings --- src/Fable.Cli/ProjectCracker.fs | 2 +- src/Fable.Core/Fable.Core.LuaInterop.fs | 87 +++++++++++++++++++++++ src/Fable.Core/Fable.Core.fsproj | 5 +- src/Fable.Transforms/FSharp2Fable.Util.fs | 2 +- src/Fable.Transforms/Lua/Fable2Lua.fs | 27 ++++--- src/fable-library-lua/fable/Native.fs | 7 +- 6 files changed, 107 insertions(+), 23 deletions(-) create mode 100644 src/Fable.Core/Fable.Core.LuaInterop.fs diff --git a/src/Fable.Cli/ProjectCracker.fs b/src/Fable.Cli/ProjectCracker.fs index bb1ee581f9..d5a9ce560b 100644 --- a/src/Fable.Cli/ProjectCracker.fs +++ b/src/Fable.Cli/ProjectCracker.fs @@ -603,7 +603,7 @@ let getFableLibraryPath (opts: CrackerOptions) = | Dart -> "fable-library-dart", "fable_library" | Rust -> "fable-library-rust", "fable-library-rust" | TypeScript -> "fable-library-ts", "fable-library-ts" - | Lua -> "fable-library-lua", "fable-library" + | Lua -> "fable-library-lua", "fable-library-lua" | _ -> "fable-library", "fable-library" + "." + Literals.VERSION let fableLibrarySource = diff --git a/src/Fable.Core/Fable.Core.LuaInterop.fs b/src/Fable.Core/Fable.Core.LuaInterop.fs new file mode 100644 index 0000000000..012d2a5e4c --- /dev/null +++ b/src/Fable.Core/Fable.Core.LuaInterop.fs @@ -0,0 +1,87 @@ +module Fable.Core.LuaInterop + +open System +open Fable.Core + +/// Has same effect as `unbox` (dynamic casting erased in compiled Python code). +/// The casted type can be defined on the call site: `!!myObj?bar(5): float` +let (!!) x: 'T = nativeOnly + +/// Implicit cast for erased unions (U2, U3...) +let inline (!^) (x:^t1) : ^t2 = ((^t1 or ^t2) : (static member op_ErasedCast : ^t1 -> ^t2) x) + +/// Dynamically access a property of an arbitrary object. +/// `myObj?propA` in Python becomes `myObj.propA` +/// `myObj?(propA)` in Python becomes `myObj[propA]` +let (?) (o: obj) (prop: obj): 'a = nativeOnly + +/// Dynamically assign a value to a property of an arbitrary object. +/// `myObj?propA <- 5` in Python becomes `myObj.propA = 5` +/// `myObj?(propA) <- 5` in Python becomes `myObj[propA] = 5` +let (?<-) (o: obj) (prop: obj) (v: obj): unit = nativeOnly + +/// Works like `ImportAttribute` (same semantics as ES6 imports). +/// You can use "*" or "default" selectors. +let import<'T> (selector: string) (path: string):'T = nativeOnly + +/// F#: let myMember = importMember "myModule" +/// Py: from my_module import my_member +/// Note the import must be immediately assigned to a value in a let binding +let importMember<'T> (path: string):'T = nativeOnly + +/// F#: let myLib = importAll "myLib" +/// Py: from my_lib import * +let importAll<'T> (path: string):'T = nativeOnly + +[] +module Lua = + + type [] ArrayConstructor = + [] + abstract Create: size: int -> 'T[] + [] + abstract isArray: arg: obj -> bool + abstract from: arg: obj -> 'T[] + + [] + let Array: ArrayConstructor = nativeOnly +(* +/// Destructure and apply a tuple to an arbitrary value. +/// E.g. `myFn $ (arg1, arg2)` in Python becomes `myFn(arg1, arg2)` +let ($) (callee: obj) (args: obj): 'a = nativeOnly + +/// Upcast the right operand to obj (and uncurry it if it's a function) and create a key-value tuple. +/// Mostly convenient when used with `createObj`. +/// E.g. `createObj [ "a" ==> 5 ]` in Python becomes `{ a: 5 }` +let (==>) (key: string) (v: obj): string*obj = nativeOnly + +/// Destructure a tuple of arguments and applies to literal Python code as with EmitAttribute. +/// E.g. `emitExpr (arg1, arg2) "$0 + $1"` in Python becomes `arg1 + arg2` +let emitExpr<'T> (args: obj) (pyCode: string): 'T = nativeOnly + +/// Same as emitExpr but intended for Python code that must appear in a statement position +/// E.g. `emitStatement aValue "while($0 < 5) doSomething()"` +let emitStatement<'T> (args: obj) (pyCode: string): 'T = nativeOnly + +/// Create a literal Python object from a collection of key-value tuples. +/// E.g. `createObj [ "a" ==> 5 ]` in Python becomes `{ a: 5 }` +let createObj (fields: #seq): obj = nativeOnly + +/// Create a literal Python object from a collection of union constructors. +/// E.g. `keyValueList CaseRules.LowerFirst [ MyUnion 4 ]` in Python becomes `{ myUnion: 4 }` +let keyValueList (caseRule: CaseRules) (li: 'T seq): obj = nativeOnly + +/// Create an empty Python object: {} +let createEmpty<'T> : 'T = nativeOnly + +[] +let pyTypeof (x: obj): string = nativeOnly + +[] +let pyInstanceof (x: obj) (cons: obj): bool = nativeOnly + + + +/// Imports a file only for its side effects +let importSideEffects (path: string): unit = nativeOnly +*) \ No newline at end of file diff --git a/src/Fable.Core/Fable.Core.fsproj b/src/Fable.Core/Fable.Core.fsproj index bd9fd36d5d..01a46053cb 100644 --- a/src/Fable.Core/Fable.Core.fsproj +++ b/src/Fable.Core/Fable.Core.fsproj @@ -1,4 +1,4 @@ - + true @@ -15,6 +15,7 @@ + @@ -25,4 +26,4 @@ - + \ No newline at end of file diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index ab7d693b9f..b9414a16bf 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -688,7 +688,7 @@ module Helpers = let isModuleValueCompiledAsFunction (com: Compiler) (memb: FSharpMemberOrFunctionOrValue) = match com.Options.Language with | Python | JavaScript | TypeScript | Lua -> memb.IsMutable && isNotPrivate memb - | Rust | Php | Dart -> false + | Rust | Php | Dart -> false let isModuleValueForCalls com (declaringEntity: FSharpEntity) (memb: FSharpMemberOrFunctionOrValue) = declaringEntity.IsFSharpModule diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index 1c0f08477e..bf242a1146 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -102,25 +102,22 @@ module Transforms = | Fable.OperationKind.Binary (op, left, right) -> let op = match op with - | BinaryMultiply -> Multiply - | BinaryDivide -> Divide - | BinaryEqual -> Equals - | BinaryPlus -> Plus - | BinaryMinus -> Minus - | BinaryEqualStrict -> Equals - | BinaryUnequal -> Unequal - | BinaryUnequalStrict -> Unequal - | BinaryLess -> Less - | BinaryGreater -> Greater - | BinaryLessOrEqual -> LessOrEqual - | BinaryGreaterOrEqual -> GreaterOrEqual + | BinaryOperator.BinaryMultiply -> Multiply + | BinaryOperator.BinaryDivide -> Divide + | BinaryOperator.BinaryEqual -> Equals + | BinaryOperator.BinaryPlus -> Plus + | BinaryOperator.BinaryMinus -> Minus + | BinaryOperator.BinaryUnequal -> Unequal + | BinaryOperator.BinaryLess -> Less + | BinaryOperator.BinaryGreater -> Greater + | BinaryOperator.BinaryLessOrEqual -> LessOrEqual + | BinaryOperator.BinaryGreaterOrEqual -> GreaterOrEqual | x -> sprintf "%A" x |> BinaryTodo Binary(op, transformExpr left, transformExpr right ) | Fable.OperationKind.Unary (op, expr) -> match op with - | UnaryNotBitwise -> transformExpr expr //not sure why this is being added - | UnaryNot -> Unary(Not, transformExpr expr) - | UnaryVoid -> NoOp + | UnaryOperator.UnaryNotBitwise -> transformExpr expr //not sure why this is being added + | UnaryOperator.UnaryNot -> Unary(Not, transformExpr expr) | _ -> sprintf "%A %A" op expr |> Unknown | x -> Unknown(sprintf "%A" x) let asSingleExprIife (exprs: Expr list): Expr= //function diff --git a/src/fable-library-lua/fable/Native.fs b/src/fable-library-lua/fable/Native.fs index 0d73a5d911..d48b07c05d 100644 --- a/src/fable-library-lua/fable/Native.fs +++ b/src/fable-library-lua/fable/Native.fs @@ -5,8 +5,7 @@ module Native open System.Collections.Generic open Fable.Core -open Fable.Core.PyInterop -open Fable.Import +open Fable.Core.LuaInterop [] type Cons<'T> = @@ -25,10 +24,10 @@ module Helpers = let allocateArrayFromCons (cons: Cons<'T>) (len: int) : 'T [] = if isNull cons then - PY.Constructors.Array.Create(len) + Lua.Array.Create(len) else cons.Allocate(len) - let inline isDynamicArrayImpl arr = PY.Constructors.Array.isArray arr + let inline isDynamicArrayImpl arr = Lua.Array.isArray arr // let inline typedArraySetImpl (target: obj) (source: obj) (offset: int): unit = // !!target?set(source, offset) From 568dd26ff444fbb2a90d620e6a4c67416dbfd68e Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Thu, 8 Dec 2022 10:13:36 -0500 Subject: [PATCH 29/41] might delete later, but it builds well --- build.fsx | 4 +- src/Fable.Core/Fable.Core.LuaInterop.fs | 58 ++++++++++++------------- src/fable-library-lua/fable/Native.fs | 2 +- tests/Lua/Fable.Tests.Lua.fsproj | 4 +- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/build.fsx b/build.fsx index 405cf51b0b..b1da42a8a7 100644 --- a/build.fsx +++ b/build.fsx @@ -238,7 +238,7 @@ let buildLibraryLua() = // Copy *.lua from projectDir to buildDir copyDirRecursive libraryDir buildDirLua - runInDir buildDirLua ("lua52 -v") + runInDir buildDirLua ("lua -v") //runInDir buildDirLua ("lua ./setup.lua develop") @@ -584,7 +584,7 @@ let testLua() = copyFile (projectDir "luaunit.lua") (buildDir "luaunit.lua") copyFile (projectDir "runtests.lua") (buildDir "runtests.lua") - runInDir buildDir "lua52 runtests.lua" + runInDir buildDir "lua runtests.lua" type RustTestMode = | SingleThreaded | MultiThreaded diff --git a/src/Fable.Core/Fable.Core.LuaInterop.fs b/src/Fable.Core/Fable.Core.LuaInterop.fs index 012d2a5e4c..7aace70a21 100644 --- a/src/Fable.Core/Fable.Core.LuaInterop.fs +++ b/src/Fable.Core/Fable.Core.LuaInterop.fs @@ -3,35 +3,35 @@ module Fable.Core.LuaInterop open System open Fable.Core -/// Has same effect as `unbox` (dynamic casting erased in compiled Python code). -/// The casted type can be defined on the call site: `!!myObj?bar(5): float` -let (!!) x: 'T = nativeOnly - -/// Implicit cast for erased unions (U2, U3...) -let inline (!^) (x:^t1) : ^t2 = ((^t1 or ^t2) : (static member op_ErasedCast : ^t1 -> ^t2) x) - -/// Dynamically access a property of an arbitrary object. -/// `myObj?propA` in Python becomes `myObj.propA` -/// `myObj?(propA)` in Python becomes `myObj[propA]` -let (?) (o: obj) (prop: obj): 'a = nativeOnly - -/// Dynamically assign a value to a property of an arbitrary object. -/// `myObj?propA <- 5` in Python becomes `myObj.propA = 5` -/// `myObj?(propA) <- 5` in Python becomes `myObj[propA] = 5` -let (?<-) (o: obj) (prop: obj) (v: obj): unit = nativeOnly - -/// Works like `ImportAttribute` (same semantics as ES6 imports). -/// You can use "*" or "default" selectors. -let import<'T> (selector: string) (path: string):'T = nativeOnly - -/// F#: let myMember = importMember "myModule" -/// Py: from my_module import my_member -/// Note the import must be immediately assigned to a value in a let binding -let importMember<'T> (path: string):'T = nativeOnly - -/// F#: let myLib = importAll "myLib" -/// Py: from my_lib import * -let importAll<'T> (path: string):'T = nativeOnly +// /// Has same effect as `unbox` (dynamic casting erased in compiled Lua code). +// /// The casted type can be defined on the call site: `!!myObj?bar(5): float` +// let (!!) x: 'T = nativeOnly + +// /// Implicit cast for erased unions (U2, U3...) +// let inline (!^) (x:^t1) : ^t2 = ((^t1 or ^t2) : (static member op_ErasedCast : ^t1 -> ^t2) x) + +// /// Dynamically access a property of an arbitrary object. +// /// `myObj?propA` in Lua becomes `myObj.propA` +// /// `myObj?(propA)` in Lua becomes `myObj[propA]` +// let (?) (o: obj) (prop: obj): 'a = nativeOnly + +// /// Dynamically assign a value to a property of an arbitrary object. +// /// `myObj?propA <- 5` in Lua becomes `myObj.propA = 5` +// /// `myObj?(propA) <- 5` in Lua becomes `myObj[propA] = 5` +// let (?<-) (o: obj) (prop: obj) (v: obj): unit = nativeOnly + +// /// Works like `ImportAttribute` (same semantics as ES6 imports). +// /// You can use "*" or "default" selectors. +// let import<'T> (selector: string) (path: string):'T = nativeOnly + +// /// F#: let myMember = importMember "myModule" +// /// Py: from my_module import my_member +// /// Note the import must be immediately assigned to a value in a let binding +// let importMember<'T> (path: string):'T = nativeOnly + +// /// F#: let myLib = importAll "myLib" +// /// Py: from my_lib import * +// let importAll<'T> (path: string):'T = nativeOnly [] module Lua = diff --git a/src/fable-library-lua/fable/Native.fs b/src/fable-library-lua/fable/Native.fs index d48b07c05d..dd36daca7f 100644 --- a/src/fable-library-lua/fable/Native.fs +++ b/src/fable-library-lua/fable/Native.fs @@ -5,8 +5,8 @@ module Native open System.Collections.Generic open Fable.Core +open Fable.Core.JsInterop open Fable.Core.LuaInterop - [] type Cons<'T> = [] diff --git a/tests/Lua/Fable.Tests.Lua.fsproj b/tests/Lua/Fable.Tests.Lua.fsproj index 2c8cce6b21..6daee57fcc 100644 --- a/tests/Lua/Fable.Tests.Lua.fsproj +++ b/tests/Lua/Fable.Tests.Lua.fsproj @@ -1,13 +1,13 @@ - net5.0 + net6.0 false false true preview - + From 340bf6381687ffcad66c471462c8a4f01dd133fa Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Thu, 8 Dec 2022 22:20:44 -0500 Subject: [PATCH 30/41] objects, initial attempt --- src/Fable.Transforms/Lua/Compiler.fs | 5 +- src/Fable.Transforms/Lua/Fable2Lua.fs | 137 +++++++++++++++++++++++-- src/Fable.Transforms/Lua/Lua.fs | 3 +- src/Fable.Transforms/Lua/LuaPrinter.fs | 3 +- 4 files changed, 134 insertions(+), 14 deletions(-) diff --git a/src/Fable.Transforms/Lua/Compiler.fs b/src/Fable.Transforms/Lua/Compiler.fs index c7b44da7ea..f4bbcc8f00 100644 --- a/src/Fable.Transforms/Lua/Compiler.fs +++ b/src/Fable.Transforms/Lua/Compiler.fs @@ -1,5 +1,6 @@ module rec Fable.Compilers.Lua +open Fable open Fable.AST open Fable.AST.Fable @@ -13,4 +14,6 @@ type LuaCompiler(com: Fable.Compiler) = types |> Map.tryFind e member this.DecisionTreeTargets (exprs: (list * Expr) list) = decisionTreeTargets <- exprs - member this.GetDecisionTreeTargets (idx: int) = decisionTreeTargets.[idx] \ No newline at end of file + member this.GetDecisionTreeTargets (idx: int) = decisionTreeTargets.[idx] + member this.GetMember(memberRef: Fable.MemberRef): Fable.MemberFunctionOrValue = + com.GetMember(memberRef: Fable.MemberRef) \ No newline at end of file diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index bf242a1146..df9ef41bd8 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -14,9 +14,31 @@ open Fable.Naming open Fable.Core -// type LuaCompiler(com: Fable.Compiler) = -// interface ILuaCompiler // with - //member this.AddType(entref, Type: LuaType) = this.AddType(entref, phpType) +type UsedNames = + { RootScope: HashSet + DeclarationScopes: HashSet + CurrentDeclarationScope: HashSet } +type BoundVars = + { EnclosingScope: HashSet + LocalScope: HashSet + Inceptions: int } + +type ITailCallOpportunity = + abstract Label: string + abstract Args: Fable.Ident list + abstract IsRecursiveRef: Fable.Expr -> bool + +type Context = + { File: Fable.File + UsedNames: UsedNames + BoundVars: BoundVars + DecisionTargets: (Fable.Ident list * Fable.Expr) list + HoistVars: Fable.Ident list -> bool + TailCallOpportunity: ITailCallOpportunity option + OptimizeTailCall: unit -> unit + ScopedTypeParams: Set + TypeParamsScope: int } + module Transforms = module Helpers = let transformStatements transformStatements transformReturn exprs = [ @@ -58,6 +80,8 @@ module Transforms = let transformValueKind (com: LuaCompiler) = function | Fable.NumberConstant(:? float as v,kind,_) -> Const(ConstNumber v) + | Fable.NumberConstant(:? int as v,kind,_) -> + Const(ConstInteger v) | Fable.StringConstant(s) -> Const(ConstString s) | Fable.BoolConstant(b) -> @@ -166,7 +190,7 @@ module Transforms = GetObjMethod(transformExpr expr, info.Name) | _ -> transformExpr expr | Fable.Expr.Delegate _ -> - transformExpr expr |> Brackets + transformExpr expr |> Parentheses | _ -> transformExpr expr FunctionCall(lhs, List.map transformExpr callInfo.Args) | Fable.Expr.Import (info, t, r) -> @@ -261,7 +285,7 @@ module Transforms = //transformExpr expr FunctionCall(Helpers.ident "error", [errorExpr]) | Fable.Extended(Fable.ExtendedSet.Curry(expr, d), _) -> - transformExpr expr |> sprintf "todo curry %A" |> Unknown + transformExpr expr |> sprintf "(Fable2Lua:~266) todo curry %A" |> Unknown | Fable.Delegate(idents, body, _, _) -> Function(idents |> List.map(fun i -> i.Name), [transformExpr body |> Return |> flattenReturnIifes]) //can be flattened | Fable.ForLoop(ident, start, limit, body, isUp, _) -> @@ -295,9 +319,54 @@ module Transforms = | _ -> () ]) ] - | x -> Unknown (sprintf "%A" x) + | x -> Unknown (sprintf "(transform fallthrough) %A" x) + + + let transformDeclarations (com: LuaCompiler) ctx decl = + let withCurrentScope (ctx: Context) (usedNames: Set) f = + let ctx = + { ctx with UsedNames = { ctx.UsedNames with CurrentDeclarationScope = HashSet usedNames } } + + let result = f ctx + ctx.UsedNames.DeclarationScopes.UnionWith(ctx.UsedNames.CurrentDeclarationScope) + result + let transformAttachedProperty (com: LuaCompiler) ctx (info: Fable.MemberFunctionOrValue) (memb: Fable.MemberDecl) = + //TODO For some reason this never gets hit + let isStatic = not info.IsInstance + let isGetter = info.IsGetter + + let decorators = + [ if isStatic then + Helpers.ident "staticmethod" + elif isGetter then + Helpers.ident "property" + else + Helpers.ident $"{memb.Name}.setter" ] + + let args, body, returnType = [""], [Do (Unknown "")] , Unknown "" + //getMemberArgsAndBody com ctx (Attached isStatic) false memb.Args memb.Body + + let key = "key" + // memberFromName com ctx memb.Name + // |> nameFromKey com ctx + + // let arguments = + // if isStatic then + // // { args with Args = [""] } + // else + // { args with Args = args}//args.Args } - let transformDeclarations (com: LuaCompiler) = function + Function ( args, body ) + //(key, arguments, body = body, decoratorList = decorators, returns = returnType) + |> List.singleton + let transformAttachedMethod (com: LuaCompiler) ctx (info: Fable.MemberFunctionOrValue) (memb: Fable.MemberDecl) = + [Helpers.ident info.FullName] + //TODO For some reason this never gets hit + + + + + match decl with | Fable.ModuleDeclaration m -> Assignment(["moduleDecTest"], Expr.Const (ConstString "moduledectest"), false) | Fable.MemberDeclaration m -> @@ -313,15 +382,61 @@ module Transforms = FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), unwrapSelfExStatements, info.IsPublic) | Fable.ClassDeclaration(d) -> com.AddClassDecl d - //todo - build prototype members out - //SNoOp - sprintf "ClassDeclaration %A" d |> Unknown |> Do + let ent = d.Entity + let classMembers = + d.AttachedMembers + |> List.collect (fun memb -> + withCurrentScope ctx memb.UsedNames + <| fun ctx -> + let info = + memb.ImplementedSignatureRef + |> Option.map com.GetMember + |> Option.defaultWith (fun () -> com.GetMember(memb.MemberRef)) + + if not memb.IsMangled + && (info.IsGetter || info.IsSetter) then + transformAttachedProperty com ctx info memb + else + transformAttachedMethod com ctx info memb) + match d.Constructor with + | Some cons -> + withCurrentScope ctx cons.UsedNames <| fun ctx -> + Assignment([d.Name], NewArr(classMembers), true) //transformClassWithPrimaryConstructor com ctx ent decl classMembers cons + | None -> + Assignment([d.Name], NewArr(classMembers), true) + | x -> sprintf "%A" x |> Unknown |> Do let transformFile com (file: Fable.File): File = + let declScopes = + let hs = HashSet() + + for decl in file.Declarations do + hs.UnionWith(decl.UsedNames) + + hs + let ctx: Context = + { + File = file + UsedNames = + { RootScope = HashSet file.UsedNamesInRootScope + DeclarationScopes = declScopes + CurrentDeclarationScope = Unchecked.defaultof<_> } + BoundVars = + { EnclosingScope = HashSet() + LocalScope = HashSet() + Inceptions = 0 } + DecisionTargets = [] + HoistVars = fun _ -> false + TailCallOpportunity = None + OptimizeTailCall = fun () -> () + ScopedTypeParams = Set.empty + TypeParamsScope = 0 + } + let comp = LuaCompiler(com) { Filename = "abc" - Statements = file.Declarations |> List.map (Transforms.transformDeclarations comp) + Statements = file.Declarations |> List.map (Transforms.transformDeclarations comp ctx) ASTDebug = sprintf "%A" file.Declarations } \ No newline at end of file diff --git a/src/Fable.Transforms/Lua/Lua.fs b/src/Fable.Transforms/Lua/Lua.fs index 637e57be28..10111c1eed 100644 --- a/src/Fable.Transforms/Lua/Lua.fs +++ b/src/Fable.Transforms/Lua/Lua.fs @@ -4,6 +4,7 @@ namespace rec Fable.AST.Lua type Const = | ConstNumber of float + | ConstInteger of int | ConstString of string | ConstBool of bool | ConstNull @@ -43,7 +44,7 @@ type Expr = | SetValue of Expr * value: Expr | SetExpr of Expr * Expr * value: Expr | FunctionCall of f: Expr * args: Expr list - | Brackets of Expr + | Parentheses of Expr | AnonymousFunc of args: string list * body: Statement list | Unknown of string | Macro of string * args: Expr list diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index c3b21eadbe..83bcebc25e 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -77,6 +77,7 @@ module Output = match c with | ConstString s -> s |> sprintf "'%s'" |> write ctx | ConstNumber n -> n |> sprintf "%f" |> write ctx + | ConstInteger n -> n |> sprintf "%i" |> write ctx | ConstBool b -> b |> sprintf "%b" |> write ctx | ConstNull -> write ctx "nil" | FunctionCall(e, args) -> @@ -208,7 +209,7 @@ module Output = writeln ctx "" writei ctx "}" | NoOp -> () - | Brackets expr -> + | Parentheses expr -> write ctx "(" writeExpr ctx expr write ctx ")" From 5a3ce7574262041ffa2bd21b2e6341b54526d142 Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Sun, 4 Jun 2023 19:16:08 -0400 Subject: [PATCH 31/41] fixing signature for lua --- src/Fable.Cli/Pipeline.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fable.Cli/Pipeline.fs b/src/Fable.Cli/Pipeline.fs index de83295e65..2a76052d77 100644 --- a/src/Fable.Cli/Pipeline.fs +++ b/src/Fable.Cli/Pipeline.fs @@ -357,7 +357,7 @@ module Lua = let projDir = IO.Path.GetDirectoryName(cliArgs.ProjectFile) let path = Imports.getImportPath pathResolver sourcePath targetPath projDir cliArgs.OutDir path if path.EndsWith(".fs") then Path.ChangeExtension(path, fileExt) else path - member _.AddSourceMapping _ = () + member _.AddSourceMapping(_,_,_,_,_,_) = () member _.AddLog(msg, severity, ?range) = com.AddLog(msg, severity, ?range=range, fileName=com.CurrentFile) member _.Dispose() = stream.Dispose() From 2a2385e5f0c493eaf5c077de30353457f853fd02 Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Sun, 6 Aug 2023 23:31:37 -0400 Subject: [PATCH 32/41] Need to rewrite class definitions in terms of lua objects --- src/Fable.Transforms/Lua/Fable2Lua.fs | 37 ++++++++++++++------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index df9ef41bd8..2d126c6f4d 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -78,9 +78,9 @@ module Transforms = NewObj(equality::pairs) else sprintf "Names and values do not match %A %A" names values |> Unknown let transformValueKind (com: LuaCompiler) = function - | Fable.NumberConstant(:? float as v,kind,_) -> + | Fable.NumberConstant(:? float as v,_kind,_) -> Const(ConstNumber v) - | Fable.NumberConstant(:? int as v,kind,_) -> + | Fable.NumberConstant(:? int as v,_kind,_) -> Const(ConstInteger v) | Fable.StringConstant(s) -> Const(ConstString s) @@ -367,7 +367,7 @@ module Transforms = match decl with - | Fable.ModuleDeclaration m -> + | Fable.ModuleDeclaration _m -> Assignment(["moduleDecTest"], Expr.Const (ConstString "moduledectest"), false) | Fable.MemberDeclaration m -> if m.Args.Length = 0 then @@ -382,25 +382,26 @@ module Transforms = FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), unwrapSelfExStatements, info.IsPublic) | Fable.ClassDeclaration(d) -> com.AddClassDecl d - let ent = d.Entity + let _ent = d.Entity + + let transformAttached (memb: Fable.MemberDecl) ctx = + let info = + memb.ImplementedSignatureRef + |> Option.map com.GetMember + |> Option.defaultWith (fun () -> com.GetMember(memb.MemberRef)) + + if not memb.IsMangled + && (info.IsGetter || info.IsSetter) then + transformAttachedProperty com ctx info memb + else + transformAttachedMethod com ctx info memb let classMembers = d.AttachedMembers |> List.collect (fun memb -> - withCurrentScope ctx memb.UsedNames - <| fun ctx -> - let info = - memb.ImplementedSignatureRef - |> Option.map com.GetMember - |> Option.defaultWith (fun () -> com.GetMember(memb.MemberRef)) - - if not memb.IsMangled - && (info.IsGetter || info.IsSetter) then - transformAttachedProperty com ctx info memb - else - transformAttachedMethod com ctx info memb) + withCurrentScope ctx memb.UsedNames <| transformAttached memb) match d.Constructor with | Some cons -> - withCurrentScope ctx cons.UsedNames <| fun ctx -> + withCurrentScope ctx cons.UsedNames <| fun _ctx -> Assignment([d.Name], NewArr(classMembers), true) //transformClassWithPrimaryConstructor com ctx ent decl classMembers cons | None -> Assignment([d.Name], NewArr(classMembers), true) @@ -439,4 +440,4 @@ let transformFile com (file: Fable.File): File = Filename = "abc" Statements = file.Declarations |> List.map (Transforms.transformDeclarations comp ctx) ASTDebug = sprintf "%A" file.Declarations - } \ No newline at end of file + } From 2b6714fe5b853f8716f4884d304c9743ce46d4cb Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Tue, 4 Jun 2024 19:55:59 -0400 Subject: [PATCH 33/41] minor merging things --- src/Fable.Transforms/FSharp2Fable.Util.fs | 1 + src/quicktest/QuickTest.fs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 73e6618bec..66e262cb67 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -393,6 +393,7 @@ type FsEnt(maybeAbbrevEnt: FSharpEntity) = argTypes |> Option.map ( function + | [| Fable.Unit |] -> [||] | argTypes -> argTypes ) diff --git a/src/quicktest/QuickTest.fs b/src/quicktest/QuickTest.fs index c7e3080be6..7bd62ccbf2 100644 --- a/src/quicktest/QuickTest.fs +++ b/src/quicktest/QuickTest.fs @@ -96,6 +96,5 @@ printfn "Running quick tests..." // Write here your unit test, you can later move it // to Fable.Tests project. For example: -// to Fable.Tests project. For example: // testCase "Addition works" <| fun () -> // 2 + 2 |> equal 4 From a43aa919cce847e94a396d4a8b857dd80ee2236a Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Tue, 4 Jun 2024 20:24:28 -0400 Subject: [PATCH 34/41] fixing small errors --- src/Fable.Cli/Entry.fs | 6 +- src/Fable.Transforms/Lua/Fable2Lua.fs | 489 +++++++++++++++----------- 2 files changed, 289 insertions(+), 206 deletions(-) diff --git a/src/Fable.Cli/Entry.fs b/src/Fable.Cli/Entry.fs index b3d1db0b11..0a211bb2c1 100644 --- a/src/Fable.Cli/Entry.fs +++ b/src/Fable.Cli/Entry.fs @@ -186,7 +186,7 @@ let argLanguage (args: CliArgs) = | "ts" | "typescript" -> Ok TypeScript | "lua" - | "Lua" -> Lua + | "Lua" -> Ok Lua | "py" | "python" -> Ok Python | "php" -> Ok Php @@ -322,6 +322,7 @@ type Runner = | Python -> "FABLE_COMPILER_PYTHON" | TypeScript -> "FABLE_COMPILER_TYPESCRIPT" | JavaScript -> "FABLE_COMPILER_JAVASCRIPT" + | Lua -> "FABLE_COMPILER_LUA" ] |> List.distinct @@ -476,7 +477,8 @@ let getLibPkgVersion = | Python | Rust | Dart - | Php -> None + | Php + | Lua -> None let private logPrelude commands language = match commands with diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index 2d126c6f4d..679ddcc10f 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -15,13 +15,18 @@ open Fable.Core type UsedNames = - { RootScope: HashSet - DeclarationScopes: HashSet - CurrentDeclarationScope: HashSet } + { + RootScope: HashSet + DeclarationScopes: HashSet + CurrentDeclarationScope: HashSet + } + type BoundVars = - { EnclosingScope: HashSet - LocalScope: HashSet - Inceptions: int } + { + EnclosingScope: HashSet + LocalScope: HashSet + Inceptions: int + } type ITailCallOpportunity = abstract Label: string @@ -29,83 +34,121 @@ type ITailCallOpportunity = abstract IsRecursiveRef: Fable.Expr -> bool type Context = - { File: Fable.File - UsedNames: UsedNames - BoundVars: BoundVars - DecisionTargets: (Fable.Ident list * Fable.Expr) list - HoistVars: Fable.Ident list -> bool - TailCallOpportunity: ITailCallOpportunity option - OptimizeTailCall: unit -> unit - ScopedTypeParams: Set - TypeParamsScope: int } + { + File: Fable.File + UsedNames: UsedNames + BoundVars: BoundVars + DecisionTargets: (Fable.Ident list * Fable.Expr) list + HoistVars: Fable.Ident list -> bool + TailCallOpportunity: ITailCallOpportunity option + OptimizeTailCall: unit -> unit + ScopedTypeParams: Set + TypeParamsScope: int + } module Transforms = module Helpers = - let transformStatements transformStatements transformReturn exprs = [ + let transformStatements transformStatements transformReturn exprs = + [ match exprs |> List.rev with - | h::t -> + | h :: t -> for x in t |> List.rev do yield transformStatements x + yield transformReturn h | [] -> () ] - let ident name = Ident {Name = name; Namespace = None} - let fcall args expr= FunctionCall(expr, args) - let iife statements = FunctionCall(AnonymousFunc([], statements), []) - let debugLog expr = FunctionCall(Helpers.ident "print", [expr]) |> Do - let libEquality a b= - FunctionCall(GetObjMethod(FunctionCall(Helpers.ident "require", [ConstString "./fable-lib/Util" |> Const]), "equals"), [a; b]) - let maybeIife = function + + let ident name = + Ident + { + Name = name + Namespace = None + } + + let fcall args expr = FunctionCall(expr, args) + + let iife statements = + FunctionCall(AnonymousFunc([], statements), []) + + let debugLog expr = + FunctionCall(Helpers.ident "print", [ expr ]) |> Do + + let libEquality a b = + FunctionCall( + GetObjMethod( + FunctionCall(Helpers.ident "require", [ ConstString "./fable-lib/Util" |> Const ]), + "equals" + ), + [ a; b ] + ) + + let maybeIife = + function | [] -> NoOp - | [Return expr] -> expr + | [ Return expr ] -> expr | statements -> iife statements + let tryNewObj (names: string list) (values: Expr list) = if names.Length = values.Length then let pairs = List.zip names values - let compareExprs = names - |> List.map (fun name -> - libEquality - (GetField(Helpers.ident "self", name)) - (GetField(Helpers.ident "toCompare", name))) - let compareExprAcc = compareExprs |> List.reduce (fun acc item -> Binary(And, acc, item) ) - let equality = "Equals", Function (["self"; "toCompare"], [ - //yield debugLog (ConstString "Calling equality" |> Const) - // debugLog (Helpers.ident "self") - // debugLog (Helpers.ident "toCompare") - //yield! compareExprs |> List.map debugLog - Return compareExprAcc - ]) - NewObj(equality::pairs) - else sprintf "Names and values do not match %A %A" names values |> Unknown - let transformValueKind (com: LuaCompiler) = function - | Fable.NumberConstant(:? float as v,_kind,_) -> - Const(ConstNumber v) - | Fable.NumberConstant(:? int as v,_kind,_) -> - Const(ConstInteger v) - | Fable.StringConstant(s) -> - Const(ConstString s) - | Fable.BoolConstant(b) -> - Const(ConstBool b) - | Fable.UnitConstant -> - Const(ConstNull) - | Fable.CharConstant(c) -> - Const(ConstString (string c)) + + let compareExprs = + names + |> List.map (fun name -> + libEquality (GetField(Helpers.ident "self", name)) (GetField(Helpers.ident "toCompare", name)) + ) + + let compareExprAcc = + compareExprs |> List.reduce (fun acc item -> Binary(And, acc, item)) + + let equality = + "Equals", + Function( + [ "self"; "toCompare" ], + [ + //yield debugLog (ConstString "Calling equality" |> Const) + // debugLog (Helpers.ident "self") + // debugLog (Helpers.ident "toCompare") + //yield! compareExprs |> List.map debugLog + Return compareExprAcc + ] + ) + + NewObj(equality :: pairs) + else + sprintf "Names and values do not match %A %A" names values |> Unknown + + let transformValueKind (com: LuaCompiler) = + function + | Fable.NumberConstant(Fable.AST.Fable.NumberValue.Float64 v, _kind) -> Const(ConstNumber v) + | Fable.NumberConstant(Fable.AST.Fable.NumberValue.Int32 v, _kind) -> Const(ConstInteger v) + | Fable.StringConstant(s) -> Const(ConstString s) + | Fable.BoolConstant(b) -> Const(ConstBool b) + | Fable.UnitConstant -> Const(ConstNull) + | Fable.CharConstant(c) -> Const(ConstString(string c)) // | Fable.EnumConstant(e,ref) -> // convertExpr com e | Fable.NewRecord(values, ref, args) -> let entity = com.Com.GetEntity(ref) + if entity.IsFSharpRecord then - let names = entity.FSharpFields |> List.map(fun f -> f.Name) + let names = entity.FSharpFields |> List.map (fun f -> f.Name) let values = values |> List.map (transformExpr com) Helpers.tryNewObj names values - else sprintf "unknown ety %A %A %A %A" values ref args entity |> Unknown + else + sprintf "unknown ety %A %A %A %A" values ref args entity |> Unknown | Fable.NewAnonymousRecord(values, names, _, _) -> let transformedValues = values |> List.map (transformExpr com) Helpers.tryNewObj (Array.toList names) transformedValues | Fable.NewUnion(values, tag, _, _) -> - let values = values |> List.map(transformExpr com) |> List.mapi(fun i x -> sprintf "p_%i" i, x) - NewObj(("tag", tag |> float |> ConstNumber |> Const)::values) - | Fable.NewOption (value, t, _) -> + let values = + values + |> List.map (transformExpr com) + |> List.mapi (fun i x -> sprintf "p_%i" i, x) + + NewObj(("tag", tag |> float |> ConstNumber |> Const) :: values) + | Fable.NewOption(value, t, _) -> value |> Option.map (transformExpr com) |> Option.defaultValue (Const ConstNull) | Fable.NewTuple(values, isStruct) -> // let fields = values |> List.mapi(fun i x -> sprintf "p_%i" i, transformExpr com x) @@ -115,15 +158,17 @@ module Transforms = match kind with | Fable.ArrayValues values -> NewArr(values |> List.map (transformExpr com)) | _ -> NewArr([]) - | Fable.Null _ -> - Const(ConstNull) + | Fable.Null _ -> Const(ConstNull) | x -> sprintf "unknown %A" x |> ConstString |> Const + let transformOp com = let transformExpr = transformExpr com + function | Fable.OperationKind.Binary(BinaryModulus, left, right) -> - GetField(Helpers.ident "math", "fmod") |> Helpers.fcall [transformExpr left; transformExpr right] - | Fable.OperationKind.Binary (op, left, right) -> + GetField(Helpers.ident "math", "fmod") + |> Helpers.fcall [ transformExpr left; transformExpr right ] + | Fable.OperationKind.Binary(op, left, right) -> let op = match op with | BinaryOperator.BinaryMultiply -> Multiply @@ -137,47 +182,53 @@ module Transforms = | BinaryOperator.BinaryLessOrEqual -> LessOrEqual | BinaryOperator.BinaryGreaterOrEqual -> GreaterOrEqual | x -> sprintf "%A" x |> BinaryTodo - Binary(op, transformExpr left, transformExpr right ) - | Fable.OperationKind.Unary (op, expr) -> + + Binary(op, transformExpr left, transformExpr right) + | Fable.OperationKind.Unary(op, expr) -> match op with | UnaryOperator.UnaryNotBitwise -> transformExpr expr //not sure why this is being added | UnaryOperator.UnaryNot -> Unary(Not, transformExpr expr) | _ -> sprintf "%A %A" op expr |> Unknown | x -> Unknown(sprintf "%A" x) - let asSingleExprIife (exprs: Expr list): Expr= //function + + let asSingleExprIife (exprs: Expr list) : Expr = //function match exprs with | [] -> NoOp - | [h] -> h + | [ h ] -> h | exprs -> - let statements = - Helpers.transformStatements - (Do) - (Return) - exprs + let statements = Helpers.transformStatements (Do) (Return) exprs statements |> Helpers.maybeIife + let flattenReturnIifes e = let rec collectStatementsRec = function - | Return (FunctionCall(AnonymousFunc([], [Return s]), [])) -> - [Return s] - | Return (FunctionCall(AnonymousFunc([], statements), [])) -> //self executing functions only + | Return(FunctionCall(AnonymousFunc([], [ Return s ]), [])) -> [ Return s ] + | Return(FunctionCall(AnonymousFunc([], statements), [])) -> //self executing functions only statements |> List.collect collectStatementsRec - | x -> [x] + | x -> [ x ] + let statements = collectStatementsRec e + match statements with - | [Return s] -> Return s + | [ Return s ] -> Return s | [] -> NoOp |> Do | _ -> FunctionCall(AnonymousFunc([], statements), []) |> Return - let asSingleExprIifeTr com : Fable.Expr list -> Expr = List.map (transformExpr com) >> asSingleExprIife + let asSingleExprIifeTr com : Fable.Expr list -> Expr = + List.map (transformExpr com) >> asSingleExprIife + let (|Regex|_|) pattern input = let m = Regex.Match(input, pattern) - if m.Success then Some(List.tail [ for g in m.Groups -> g.Value ]) - else None - let transformExpr (com: LuaCompiler) expr= + if m.Success then + Some(List.tail [ for g in m.Groups -> g.Value ]) + else + None + + let transformExpr (com: LuaCompiler) expr = let transformExpr = transformExpr com let transformOp = transformOp com + match expr with | Fable.Expr.Value(value, _) -> transformValueKind com value | Fable.Expr.Call(expr, callInfo, t, r) -> @@ -186,53 +237,59 @@ module Transforms = | Fable.Expr.Get(expr, Fable.GetKind.FieldGet info, t, _) -> match t with | Fable.DeclaredType(_, _) - | Fable.AnonymousRecordType(_, _, _) -> - GetObjMethod(transformExpr expr, info.Name) + | Fable.AnonymousRecordType(_, _, _) -> GetObjMethod(transformExpr expr, info.Name) | _ -> transformExpr expr - | Fable.Expr.Delegate _ -> - transformExpr expr |> Parentheses + | Fable.Expr.Delegate _ -> transformExpr expr |> Parentheses | _ -> transformExpr expr + FunctionCall(lhs, List.map transformExpr callInfo.Args) - | Fable.Expr.Import (info, t, r) -> + | Fable.Expr.Import(info, t, r) -> let path = match info.Kind, info.Path with - | libImport, Regex "fable-lib\/(\w+).(?:fs|js)" [name] -> - "fable-lib/" + name - | _, Regex"fable-library-lua\/fable\/fable-library\/(\w+).(?:fs|js)" [name] -> + | libImport, Regex "fable-lib\/(\w+).(?:fs|js)" [ name ] -> "fable-lib/" + name + | _, Regex "fable-library-lua\/fable\/fable-library\/(\w+).(?:fs|js)" [ name ] -> "fable-lib/fable-library" + name - | _, Regex"fable-library-lua\/fable\/(\w+).(?:fs|js)" [name] -> - "fable-lib/" + name - | _ -> - info.Path.Replace(".fs", "").Replace(".js", "") //todo - make less brittle - let rcall = FunctionCall(Ident { Namespace=None; Name= "require" }, [Const (ConstString path)]) + | _, Regex "fable-library-lua\/fable\/(\w+).(?:fs|js)" [ name ] -> "fable-lib/" + name + | _ -> info.Path.Replace(".fs", "").Replace(".js", "") //todo - make less brittle + + let rcall = + FunctionCall( + Ident + { + Namespace = None + Name = "require" + }, + [ Const(ConstString path) ] + ) + match info.Selector with | "" -> rcall | s -> GetObjMethod(rcall, s) | Fable.Expr.IdentExpr(i) when i.Name <> "" -> - Ident {Namespace = None; Name = i.Name } - | Fable.Expr.Operation (kind, _, _, _) -> - transformOp kind - | Fable.Expr.Get(expr, Fable.GetKind.FieldGet info, t, _) -> - GetField(transformExpr expr, info.Name) - | Fable.Expr.Get(expr, Fable.GetKind.UnionField info , _, _) -> + Ident + { + Namespace = None + Name = i.Name + } + | Fable.Expr.Operation(kind, _, _, _) -> transformOp kind + | Fable.Expr.Get(expr, Fable.GetKind.FieldGet info, t, _) -> GetField(transformExpr expr, info.Name) + | Fable.Expr.Get(expr, Fable.GetKind.UnionField info, _, _) -> GetField(transformExpr expr, sprintf "p_%i" info.FieldIndex) - | Fable.Expr.Get(expr, Fable.GetKind.ExprGet(e), _, _) -> - GetAtIndex(transformExpr expr, transformExpr e) + | Fable.Expr.Get(expr, Fable.GetKind.ExprGet(e), _, _) -> GetAtIndex(transformExpr expr, transformExpr e) | Fable.Expr.Get(expr, Fable.GetKind.TupleIndex(i), _, _) -> - GetAtIndex(transformExpr expr, Const (ConstNumber (float i))) - | Fable.Expr.Get(expr, Fable.GetKind.OptionValue, _, _) -> - transformExpr expr //todo null check, throw if null? - | Fable.Expr.Set(expr, Fable.SetKind.ValueSet, t, value, _) -> - SetValue(transformExpr expr, transformExpr value) + GetAtIndex(transformExpr expr, Const(ConstNumber(float i))) + | Fable.Expr.Get(expr, Fable.GetKind.OptionValue, _, _) -> transformExpr expr //todo null check, throw if null? + | Fable.Expr.Set(expr, Fable.SetKind.ValueSet, t, value, _) -> SetValue(transformExpr expr, transformExpr value) | Fable.Expr.Set(expr, Fable.SetKind.ExprSet(e), t, value, _) -> SetExpr(transformExpr expr, transformExpr e, transformExpr value) - | Fable.Expr.Sequential exprs -> - asSingleExprIifeTr com exprs - | Fable.Expr.Let (ident, value, body) -> - let statements = [ - Assignment([ident.Name], transformExpr value, true) - transformExpr body |> Return - ] + | Fable.Expr.Sequential exprs -> asSingleExprIifeTr com exprs + | Fable.Expr.Let(ident, value, body) -> + let statements = + [ + Assignment([ ident.Name ], transformExpr value, true) + transformExpr body |> Return + ] + Helpers.maybeIife statements | Fable.Expr.Emit(m, _, _) -> // let argsExprs = m.CallInfo.Args |> List.map transformExpr @@ -246,28 +303,32 @@ module Transforms = com.DecisionTreeTargets(lst) transformExpr expr | Fable.Expr.DecisionTreeSuccess(i, boundValues, _) -> - let idents,target = com.GetDecisionTreeTargets(i) + let idents, target = com.GetDecisionTreeTargets(i) + if idents.Length = boundValues.Length then let statements = - [ for (ident, value) in List.zip idents boundValues do - yield Assignment([ident.Name], transformExpr value, false) + [ + for (ident, value) in List.zip idents boundValues do + yield Assignment([ ident.Name ], transformExpr value, false) yield transformExpr target |> Return - ] - statements - |> Helpers.maybeIife - else sprintf "not equal lengths %A %A" idents boundValues |> Unknown - | Fable.Expr.Lambda(arg, body, name) -> - Function([arg.Name], [transformExpr body |> Return]) + ] + + statements |> Helpers.maybeIife + else + sprintf "not equal lengths %A %A" idents boundValues |> Unknown + | Fable.Expr.Lambda(arg, body, name) -> Function([ arg.Name ], [ transformExpr body |> Return ]) | Fable.Expr.CurriedApply(applied, args, _, _) -> FunctionCall(transformExpr applied, args |> List.map transformExpr) - | Fable.Expr.IfThenElse (guardExpr, thenExpr, elseExpr, _) -> + | Fable.Expr.IfThenElse(guardExpr, thenExpr, elseExpr, _) -> Ternary(transformExpr guardExpr, transformExpr thenExpr, transformExpr elseExpr) | Fable.Test(expr, kind, b) -> match kind with - | Fable.UnionCaseTest i-> - Binary(Equals, GetField(transformExpr expr, "tag") , Const (ConstNumber (float i))) + | Fable.UnionCaseTest i -> Binary(Equals, GetField(transformExpr expr, "tag"), Const(ConstNumber(float i))) | Fable.OptionTest isSome -> - if isSome then Binary(Unequal, Const ConstNull, transformExpr expr) else Binary(Equals, Const ConstNull, transformExpr expr) + if isSome then + Binary(Unequal, Const ConstNull, transformExpr expr) + else + Binary(Equals, Const ConstNull, transformExpr expr) | Fable.TestKind.TypeTest t -> // match t with // | Fable.DeclaredType (ent, genArgs) -> @@ -276,50 +337,53 @@ module Transforms = // | Fable.Transforms.Types.array // | _ -> // | _ -> () - Binary(Equals, GetField(transformExpr expr, "type"), Const (t.ToString() |> ConstString)) - | _ -> - Unknown(sprintf "test %A %A" expr kind) + Binary(Equals, GetField(transformExpr expr, "type"), Const(t.ToString() |> ConstString)) + | _ -> Unknown(sprintf "test %A %A" expr kind) | Fable.Extended(Fable.ExtendedSet.Throw(expr, _), t) -> - let errorExpr = - Const (ConstString "There was an error, todo") - //transformExpr expr - FunctionCall(Helpers.ident "error", [errorExpr]) + let errorExpr = Const(ConstString "There was an error, todo") + //transformExpr expr + FunctionCall(Helpers.ident "error", [ errorExpr ]) | Fable.Extended(Fable.ExtendedSet.Curry(expr, d), _) -> transformExpr expr |> sprintf "(Fable2Lua:~266) todo curry %A" |> Unknown | Fable.Delegate(idents, body, _, _) -> - Function(idents |> List.map(fun i -> i.Name), [transformExpr body |> Return |> flattenReturnIifes]) //can be flattened + Function(idents |> List.map (fun i -> i.Name), [ transformExpr body |> Return |> flattenReturnIifes ]) //can be flattened | Fable.ForLoop(ident, start, limit, body, isUp, _) -> - Helpers.maybeIife [ - ForLoop(ident.Name, transformExpr start, transformExpr limit, [transformExpr body |> Do]) + Helpers.maybeIife + [ + ForLoop(ident.Name, transformExpr start, transformExpr limit, [ transformExpr body |> Do ]) ] - | Fable.TypeCast(expr, t) -> - transformExpr expr //typecasts are meaningless + | Fable.TypeCast(expr, t) -> transformExpr expr //typecasts are meaningless | Fable.WhileLoop(guard, body, label) -> - Helpers.maybeIife [ - WhileLoop(transformExpr guard, [transformExpr body |> Do]) - ] + Helpers.maybeIife [ WhileLoop(transformExpr guard, [ transformExpr body |> Do ]) ] | Fable.TryCatch(body, catch, finalizer, _) -> - Helpers.maybeIife [ - Assignment(["status"; "resOrErr"], FunctionCall(Helpers.ident "pcall", [ - Function([], [ - transformExpr body |> Return - ]) - ]), true) - let finalizer = finalizer |> Option.map transformExpr - let catch = catch |> Option.map (fun (ident, expr) -> ident.Name, transformExpr expr) - IfThenElse(Helpers.ident "status", [ - match finalizer with - | Some finalizer -> yield Do finalizer - | None -> () - yield Helpers.ident "resOrErr" |> Return - ], [ - match catch with - | Some(ident, expr) -> - yield expr |> Return - | _ -> () - ]) - ] - | x -> Unknown (sprintf "(transform fallthrough) %A" x) + Helpers.maybeIife + [ + Assignment( + [ "status"; "resOrErr" ], + FunctionCall(Helpers.ident "pcall", [ Function([], [ transformExpr body |> Return ]) ]), + true + ) + let finalizer = finalizer |> Option.map transformExpr + + let catch = + catch |> Option.map (fun (ident, expr) -> ident.Name, transformExpr expr) + + IfThenElse( + Helpers.ident "status", + [ + match finalizer with + | Some finalizer -> yield Do finalizer + | None -> () + yield Helpers.ident "resOrErr" |> Return + ], + [ + match catch with + | Some(ident, expr) -> yield expr |> Return + | _ -> () + ] + ) + ] + | x -> Unknown(sprintf "(transform fallthrough) %A" x) let transformDeclarations (com: LuaCompiler) ctx decl = @@ -330,25 +394,33 @@ module Transforms = let result = f ctx ctx.UsedNames.DeclarationScopes.UnionWith(ctx.UsedNames.CurrentDeclarationScope) result - let transformAttachedProperty (com: LuaCompiler) ctx (info: Fable.MemberFunctionOrValue) (memb: Fable.MemberDecl) = + + let transformAttachedProperty + (com: LuaCompiler) + ctx + (info: Fable.MemberFunctionOrValue) + (memb: Fable.MemberDecl) + = //TODO For some reason this never gets hit let isStatic = not info.IsInstance let isGetter = info.IsGetter let decorators = - [ if isStatic then - Helpers.ident "staticmethod" - elif isGetter then - Helpers.ident "property" - else - Helpers.ident $"{memb.Name}.setter" ] + [ + if isStatic then + Helpers.ident "staticmethod" + elif isGetter then + Helpers.ident "property" + else + Helpers.ident $"{memb.Name}.setter" + ] - let args, body, returnType = [""], [Do (Unknown "")] , Unknown "" - //getMemberArgsAndBody com ctx (Attached isStatic) false memb.Args memb.Body + let args, body, returnType = [ "" ], [ Do(Unknown "") ], Unknown "" + //getMemberArgsAndBody com ctx (Attached isStatic) false memb.Args memb.Body let key = "key" - // memberFromName com ctx memb.Name - // |> nameFromKey com ctx + // memberFromName com ctx memb.Name + // |> nameFromKey com ctx // let arguments = // if isStatic then @@ -356,59 +428,62 @@ module Transforms = // else // { args with Args = args}//args.Args } - Function ( args, body ) + Function(args, body) //(key, arguments, body = body, decoratorList = decorators, returns = returnType) |> List.singleton - let transformAttachedMethod (com: LuaCompiler) ctx (info: Fable.MemberFunctionOrValue) (memb: Fable.MemberDecl) = - [Helpers.ident info.FullName] - //TODO For some reason this never gets hit - + let transformAttachedMethod + (com: LuaCompiler) + ctx + (info: Fable.MemberFunctionOrValue) + (memb: Fable.MemberDecl) + = + [ Helpers.ident info.FullName ] + //TODO For some reason this never gets hit match decl with - | Fable.ModuleDeclaration _m -> - Assignment(["moduleDecTest"], Expr.Const (ConstString "moduledectest"), false) + | Fable.ModuleDeclaration _m -> Assignment([ "moduleDecTest" ], Expr.Const(ConstString "moduledectest"), false) | Fable.MemberDeclaration m -> if m.Args.Length = 0 then - Assignment([m.Name], transformExpr com m.Body, true) + Assignment([ m.Name ], transformExpr com m.Body, true) else let info = com.Com.GetMember(m.MemberRef) + let unwrapSelfExStatements = match transformExpr com m.Body |> Return |> flattenReturnIifes with - | Return (FunctionCall(AnonymousFunc([], statements), [])) -> - statements - | s -> [s] - FunctionDeclaration(m.Name, m.Args |> List.map(fun a -> a.Name), unwrapSelfExStatements, info.IsPublic) + | Return(FunctionCall(AnonymousFunc([], statements), [])) -> statements + | s -> [ s ] + + FunctionDeclaration(m.Name, m.Args |> List.map (fun a -> a.Name), unwrapSelfExStatements, info.IsPublic) | Fable.ClassDeclaration(d) -> com.AddClassDecl d let _ent = d.Entity - - let transformAttached (memb: Fable.MemberDecl) ctx = + + let transformAttached (memb: Fable.MemberDecl) ctx = let info = memb.ImplementedSignatureRef |> Option.map com.GetMember |> Option.defaultWith (fun () -> com.GetMember(memb.MemberRef)) - if not memb.IsMangled - && (info.IsGetter || info.IsSetter) then + if not memb.IsMangled && (info.IsGetter || info.IsSetter) then transformAttachedProperty com ctx info memb else transformAttachedMethod com ctx info memb + let classMembers = d.AttachedMembers - |> List.collect (fun memb -> - withCurrentScope ctx memb.UsedNames <| transformAttached memb) + |> List.collect (fun memb -> withCurrentScope ctx memb.UsedNames <| transformAttached memb) + match d.Constructor with - | Some cons -> - withCurrentScope ctx cons.UsedNames <| fun _ctx -> - Assignment([d.Name], NewArr(classMembers), true) //transformClassWithPrimaryConstructor com ctx ent decl classMembers cons - | None -> - Assignment([d.Name], NewArr(classMembers), true) + | Some cons -> + withCurrentScope ctx cons.UsedNames + <| fun _ctx -> Assignment([ d.Name ], NewArr(classMembers), true) //transformClassWithPrimaryConstructor com ctx ent decl classMembers cons + | None -> Assignment([ d.Name ], NewArr(classMembers), true) | x -> sprintf "%A" x |> Unknown |> Do -let transformFile com (file: Fable.File): File = +let transformFile com (file: Fable.File) : File = let declScopes = let hs = HashSet() @@ -416,17 +491,22 @@ let transformFile com (file: Fable.File): File = hs.UnionWith(decl.UsedNames) hs + let ctx: Context = { File = file UsedNames = - { RootScope = HashSet file.UsedNamesInRootScope - DeclarationScopes = declScopes - CurrentDeclarationScope = Unchecked.defaultof<_> } + { + RootScope = HashSet file.UsedNamesInRootScope + DeclarationScopes = declScopes + CurrentDeclarationScope = Unchecked.defaultof<_> + } BoundVars = - { EnclosingScope = HashSet() - LocalScope = HashSet() - Inceptions = 0 } + { + EnclosingScope = HashSet() + LocalScope = HashSet() + Inceptions = 0 + } DecisionTargets = [] HoistVars = fun _ -> false TailCallOpportunity = None @@ -436,8 +516,9 @@ let transformFile com (file: Fable.File): File = } let comp = LuaCompiler(com) + { Filename = "abc" - Statements = file.Declarations |> List.map (Transforms.transformDeclarations comp ctx) + Statements = file.Declarations |> List.map (Transforms.transformDeclarations comp ctx) ASTDebug = sprintf "%A" file.Declarations } From d3ae15a055c0cc0e1ce35ec4264008b271b7e54e Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Tue, 4 Jun 2024 20:30:12 -0400 Subject: [PATCH 35/41] fixed formatting --- build_old.fsx | 59 +++++----- src/Fable.Core/Fable.Core.LuaInterop.fs | 7 +- src/Fable.Transforms/Lua/Compiler.fs | 16 ++- src/Fable.Transforms/Lua/Lua.fs | 5 +- src/Fable.Transforms/Lua/LuaPrinter.fs | 138 +++++++++++++++--------- src/fable-library-lua/fable/Native.fs | 65 +++++------ src/fable-library-lua/fable/Timer.fs | 10 +- 7 files changed, 176 insertions(+), 124 deletions(-) diff --git a/build_old.fsx b/build_old.fsx index 4f7b083086..4d56babdee 100644 --- a/build_old.fsx +++ b/build_old.fsx @@ -1,5 +1,5 @@ #load "src/Fable.PublishUtils/PublishUtils.fs" - +//This is old, please use build project via build.cmd or build.sh open System open System.Text.RegularExpressions open PublishUtils @@ -282,31 +282,36 @@ let buildLibraryPy () = removeDirRecursive (buildDirPy "fable_library/fable-library") -let buildLibraryLua() = +let buildLibraryLua () = let libraryDir = "src/fable-library-lua" let projectDir = libraryDir + "/fable" let buildDirLua = "build/fable-library-lua" - cleanDirs [buildDirLua] + cleanDirs [ buildDirLua ] - runFableWithArgs projectDir [ - "--outDir " + buildDirLua "fable" - "--fableLib " + buildDirLua "fable" - "--lang Lua" - "--exclude Fable.Core" - "--define FABLE_LIBRARY" - ] + runFableWithArgs + projectDir + [ + "--outDir " + buildDirLua "fable" + "--fableLib " + buildDirLua "fable" + "--lang Lua" + "--exclude Fable.Core" + "--define FABLE_LIBRARY" + ] // Copy *.lua from projectDir to buildDir copyDirRecursive libraryDir buildDirLua runInDir buildDirLua ("lua -v") - //runInDir buildDirLua ("lua ./setup.lua develop") -let buildLuaLibraryIfNotExists() = +//runInDir buildDirLua ("lua ./setup.lua develop") +let buildLuaLibraryIfNotExists () = let baseDir = __SOURCE_DIRECTORY__ + if not (pathExists (baseDir "build/fable-library-lua")) then - buildLibraryLua() + buildLibraryLua () + let buildLibraryPyIfNotExists () = let baseDir = __SOURCE_DIRECTORY__ + if not (pathExists (baseDir "build/fable-library-py")) then buildLibraryPy () @@ -680,25 +685,29 @@ let testPython () = // Testing in Windows // runInDir buildDir "python -m pytest -x" -let testLua() = - buildLuaLibraryIfNotExists() // NOTE: fable-library-py needs to be built separately. +let testLua () = + buildLuaLibraryIfNotExists () // NOTE: fable-library-py needs to be built separately. let projectDir = "tests/Lua" let buildDir = "build/tests/Lua" - cleanDirs [buildDir] + cleanDirs [ buildDir ] copyDirRecursive ("build" "fable-library-lua" "fable") (buildDir "fable-lib") runInDir projectDir "dotnet test" - runFableWithArgs projectDir [ - "--outDir " + buildDir - "--exclude Fable.Core" - "--lang Lua" - "--fableLib " + buildDir "fable-lib" //("fable-library-lua" "fable") //cannot use relative paths in lua. Copy to subfolder? - ] + + runFableWithArgs + projectDir + [ + "--outDir " + buildDir + "--exclude Fable.Core" + "--lang Lua" + "--fableLib " + buildDir "fable-lib" //("fable-library-lua" "fable") //cannot use relative paths in lua. Copy to subfolder? + ] copyFile (projectDir "luaunit.lua") (buildDir "luaunit.lua") copyFile (projectDir "runtests.lua") (buildDir "runtests.lua") runInDir buildDir "lua runtests.lua" + type RustTestMode = | NoStd | Default @@ -1011,7 +1020,7 @@ match BUILD_ARGS_LOWER with | "test-rust-threaded" :: _ -> testRust Threaded | "test-dart" :: _ -> testDart (false) | "watch-test-dart" :: _ -> testDart (true) -| "test-lua"::_ -> testLua() +| "test-lua" :: _ -> testLua () | "quicktest" :: _ -> @@ -1070,8 +1079,8 @@ match BUILD_ARGS_LOWER with | ("fable-library-rust" | "library-rust") :: _ -> buildLibraryRust () | ("fable-library-dart" | "library-dart") :: _ -> let clean = hasFlag "--no-clean" |> not - buildLibraryDart(clean) -| ("fable-library-lua" | "library-lua")::_ -> buildLibraryLua() + buildLibraryDart (clean) +| ("fable-library-lua" | "library-lua") :: _ -> buildLibraryLua () | ("fable-compiler-js" | "compiler-js") :: _ -> let minify = hasFlag "--no-minify" |> not diff --git a/src/Fable.Core/Fable.Core.LuaInterop.fs b/src/Fable.Core/Fable.Core.LuaInterop.fs index 7aace70a21..44b6822126 100644 --- a/src/Fable.Core/Fable.Core.LuaInterop.fs +++ b/src/Fable.Core/Fable.Core.LuaInterop.fs @@ -36,11 +36,14 @@ open Fable.Core [] module Lua = - type [] ArrayConstructor = + [] + type ArrayConstructor = [] abstract Create: size: int -> 'T[] + [] abstract isArray: arg: obj -> bool + abstract from: arg: obj -> 'T[] [] @@ -84,4 +87,4 @@ let pyInstanceof (x: obj) (cons: obj): bool = nativeOnly /// Imports a file only for its side effects let importSideEffects (path: string): unit = nativeOnly -*) \ No newline at end of file +*) diff --git a/src/Fable.Transforms/Lua/Compiler.fs b/src/Fable.Transforms/Lua/Compiler.fs index f4bbcc8f00..597476dcae 100644 --- a/src/Fable.Transforms/Lua/Compiler.fs +++ b/src/Fable.Transforms/Lua/Compiler.fs @@ -8,12 +8,10 @@ type LuaCompiler(com: Fable.Compiler) = let mutable types = Map.empty let mutable decisionTreeTargets = [] member this.Com = com - member this.AddClassDecl (c: ClassDecl) = - types <- types |> Map.add c.Entity c - member this.GetByRef (e: EntityRef) = - types |> Map.tryFind e - member this.DecisionTreeTargets (exprs: (list * Expr) list) = - decisionTreeTargets <- exprs - member this.GetDecisionTreeTargets (idx: int) = decisionTreeTargets.[idx] - member this.GetMember(memberRef: Fable.MemberRef): Fable.MemberFunctionOrValue = - com.GetMember(memberRef: Fable.MemberRef) \ No newline at end of file + member this.AddClassDecl(c: ClassDecl) = types <- types |> Map.add c.Entity c + member this.GetByRef(e: EntityRef) = types |> Map.tryFind e + member this.DecisionTreeTargets(exprs: (list * Expr) list) = decisionTreeTargets <- exprs + member this.GetDecisionTreeTargets(idx: int) = decisionTreeTargets.[idx] + + member this.GetMember(memberRef: Fable.MemberRef) : Fable.MemberFunctionOrValue = + com.GetMember(memberRef: Fable.MemberRef) diff --git a/src/Fable.Transforms/Lua/Lua.fs b/src/Fable.Transforms/Lua/Lua.fs index 10111c1eed..24242d3193 100644 --- a/src/Fable.Transforms/Lua/Lua.fs +++ b/src/Fable.Transforms/Lua/Lua.fs @@ -18,6 +18,7 @@ type LuaIdentity = type UnaryOp = | Not | NotBitwise + type BinaryOp = | Equals | Unequal @@ -60,7 +61,7 @@ type Statement = | Return of Expr | Do of Expr | SNoOp - | ForLoop of string * start: Expr* limit: Expr* body: Statement list + | ForLoop of string * start: Expr * limit: Expr * body: Statement list | WhileLoop of guard: Expr * body: Statement list | IfThenElse of guard: Expr * thenSt: Statement list * elseSt: Statement list @@ -69,4 +70,4 @@ type File = Filename: string Statements: (Statement) list ASTDebug: string - } \ No newline at end of file + } diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index 83bcebc25e..31ba72e646 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -8,52 +8,61 @@ open Fable.AST open Fable.AST.Lua type System.Text.StringBuilder with - member sb.Write (txt: string) = - sb.Append(txt) |> ignore - member sb.WriteLine (txt: string) = + + member sb.Write(txt: string) = sb.Append(txt) |> ignore + + member sb.WriteLine(txt: string) = sb.Append(txt) |> ignore sb.AppendLine() |> ignore module Output = type Writer = - { Writer: System.Text.StringBuilder - Indent: int - Precedence: int - CurrentNamespace: string option } + { + Writer: System.Text.StringBuilder + Indent: int + Precedence: int + CurrentNamespace: string option + } module Helper = - let separateWithCommas = function + let separateWithCommas = + function | [] -> "" - | [x] -> x + | [ x ] -> x | lst -> lst |> List.reduce (fun acc item -> acc + " ," + item) - let indent ctx = - { ctx with Indent = ctx.Indent + 1} + let indent ctx = { ctx with Indent = ctx.Indent + 1 } module Writer = let create w = - { Writer = w; Indent = 0; Precedence = Int32.MaxValue; CurrentNamespace = None } + { + Writer = w + Indent = 0 + Precedence = Int32.MaxValue + CurrentNamespace = None + } let writeIndent ctx = for _ in 1 .. ctx.Indent do ctx.Writer.Write(" ") - let write ctx txt = - ctx.Writer.Write(txt: string) + let write ctx txt = ctx.Writer.Write(txt: string) let writei ctx txt = writeIndent ctx write ctx txt - let writeln ctx txt = - ctx.Writer.WriteLine(txt: string) + let writeln ctx txt = ctx.Writer.WriteLine(txt: string) + let writeCommented ctx help txt = writeln ctx "--[[" write ctx help writeln ctx txt writeln ctx " --]]" - let writeOp ctx = function + + let writeOp ctx = + function | Multiply -> write ctx "*" | Equals -> write ctx "==" | Unequal -> write ctx "~=" @@ -67,12 +76,15 @@ module Output = | And -> write ctx "and" | Or -> write ctx "or" | BinaryTodo x -> writeCommented ctx "binary todo" x - let sprintExprSimple = function + + let sprintExprSimple = + function | Ident i -> i.Name | _ -> "" - let rec writeExpr ctx = function - | Ident i -> - write ctx i.Name + + let rec writeExpr ctx = + function + | Ident i -> write ctx i.Name | Const c -> match c with | ConstString s -> s |> sprintf "'%s'" |> write ctx @@ -92,13 +104,15 @@ module Output = write ctx ")" writeln ctx "" let ctxI = indent ctx + for b in body do writeStatement ctxI b + writei ctx "end)" | Unary(Not, expr) -> write ctx "not " writeExpr ctx expr - | Binary (op, left, right) -> + | Binary(op, left, right) -> writeExpr ctx left write ctx " " writeOp ctx op @@ -116,7 +130,7 @@ module Output = writeExpr ctx expr write ctx "[" //hack alert - lua indexers are 1-based and not 0-based, so we need to "add1". Probably correct soln here is to simplify ast after +1 if possible - let add1 = Binary(BinaryOp.Plus, Const (ConstNumber 1.0), idx) + let add1 = Binary(BinaryOp.Plus, Const(ConstNumber 1.0), idx) writeExpr ctx add1 write ctx "]" | SetValue(expr, value) -> @@ -143,7 +157,7 @@ module Output = //writei ctx "or " writeExpr ctxI elseExpr write ctx ")" - | Macro (macro, args) -> + | Macro(macro, args) -> // let subbedMacro = // (s, args |> List.mapi(fun i x -> i.ToString(), sprintExprSimple x)) @@ -152,27 +166,31 @@ module Output = let regex = System.Text.RegularExpressions.Regex("\$(?\d)(?\.\.\.)?") let matches = regex.Matches(macro) let mutable pos = 0 + for m in matches do let n = int m.Groups.["n"].Value - write ctx (macro.Substring(pos,m.Index-pos)) + write ctx (macro.Substring(pos, m.Index - pos)) + if m.Groups.["s"].Success then if n < args.Length then match args.[n] with | NewArr items -> - let mutable first = true - for value in items do - if first then - first <- false - else - write ctx ", " - writeExpr ctx value - | _ -> - writeExpr ctx args.[n] + let mutable first = true + + for value in items do + if first then + first <- false + else + write ctx ", " + + writeExpr ctx value + | _ -> writeExpr ctx args.[n] elif n < args.Length then writeExpr ctx args.[n] pos <- m.Index + m.Length + write ctx (macro.Substring(pos)) | Function(args, body) -> write ctx "function " @@ -187,10 +205,12 @@ module Output = write ctx "{" let ctxI = indent ctx writeln ctxI "" + for idx, (name, expr) in args |> List.mapi (fun i x -> i, x) do writei ctxI name write ctxI " = " writeExpr ctxI expr + if idx < args.Length - 1 then writeln ctxI "," //writeExprs ctxI args @@ -200,9 +220,11 @@ module Output = write ctx "{" let ctxI = indent ctx writeln ctxI "" + for idx, expr in args |> List.mapi (fun i x -> i, x) do writei ctxI "" writeExpr ctxI expr + if idx < args.Length - 1 then writeln ctxI "," //writeExprs ctxI args @@ -213,22 +235,28 @@ module Output = write ctx "(" writeExpr ctx expr write ctx ")" - | Unknown x -> - writeCommented ctx "unknown" x + | Unknown x -> writeCommented ctx "unknown" x | x -> sprintf "%A" x |> writeCommented ctx "todo" - and writeExprs ctx = function + + and writeExprs ctx = + function | [] -> () - | h::t -> + | h :: t -> writeExpr ctx h + for item in t do write ctx ", " writeExpr ctx item - and writeStatement ctx = function + and writeStatement ctx = + function | Assignment(names, expr, isLocal) -> let names = names |> Helper.separateWithCommas writei ctx "" - if isLocal then write ctx "local " + + if isLocal then + write ctx "local " + write ctx names write ctx " = " writeExpr ctx expr @@ -244,6 +272,7 @@ module Output = writeln ctxI "" body |> List.iter (writeStatement ctxI) writeln ctx "end" + if exportToMod then writei ctx "mod." write ctx name @@ -260,7 +289,7 @@ module Output = writei ctx "" writeExpr ctx expr writeln ctx "" - | ForLoop (name, start, limit, body) -> + | ForLoop(name, start, limit, body) -> writei ctx "for " write ctx name write ctx "=" @@ -269,20 +298,24 @@ module Output = writeExpr ctx limit write ctx " do" let ctxI = indent ctx + for statement in body do writeln ctxI "" writeStatement ctxI statement + writeln ctx "" writei ctx "end" writeln ctx "" - | WhileLoop (guard, body) -> + | WhileLoop(guard, body) -> writei ctx "while " writeExpr ctx guard write ctx " do" let ctxI = indent ctx + for statement in body do writeln ctxI "" writeStatement ctxI statement + writeln ctx "" writei ctx "end" writeln ctx "" @@ -291,14 +324,18 @@ module Output = writeExpr ctx guard write ctx " then" let ctxI = indent ctx + for statement in thenSt do writeln ctxI "" writeStatement ctxI statement + writeln ctx "" writei ctx "else" + for statement in elseSt do writeln ctxI "" writeStatement ctxI statement + writeln ctx "" writei ctx "end" writeln ctx "" @@ -306,23 +343,24 @@ module Output = let writeFile ctx (file: File) = writeln ctx "mod = {}" + for s in file.Statements do writeStatement ctx s + write ctx "return mod" //debugging writeln ctx "" - // writeln ctx "--[[" - // sprintf "%s" file.ASTDebug |> write ctx - //sprintf "%A" file.Statements |> write ctx - //writeln ctx " --]]" +// writeln ctx "--[[" +// sprintf "%s" file.ASTDebug |> write ctx +//sprintf "%A" file.Statements |> write ctx +//writeln ctx " --]]" -let isEmpty (file: File): bool = - false //TODO: determine if printer will not print anything +let isEmpty (file: File) : bool = false //TODO: determine if printer will not print anything -let run (writer: Printer.Writer) (lib: File): Async = +let run (writer: Printer.Writer) (lib: File) : Async = async { let sb = System.Text.StringBuilder() let ctx = Output.Writer.create sb Output.writeFile ctx lib do! writer.Write(sb.ToString()) - } \ No newline at end of file + } diff --git a/src/fable-library-lua/fable/Native.fs b/src/fable-library-lua/fable/Native.fs index dd36daca7f..3b49a5cb82 100644 --- a/src/fable-library-lua/fable/Native.fs +++ b/src/fable-library-lua/fable/Native.fs @@ -7,26 +7,28 @@ open System.Collections.Generic open Fable.Core open Fable.Core.JsInterop open Fable.Core.LuaInterop + [] type Cons<'T> = [] - abstract Allocate : len: int -> 'T [] + abstract Allocate: len: int -> 'T[] module Helpers = [] - let arrayFrom (xs: 'T seq) : 'T [] = nativeOnly + let arrayFrom (xs: 'T seq) : 'T[] = nativeOnly [] - let allocateArray (len: int) : 'T [] = nativeOnly + let allocateArray (len: int) : 'T[] = nativeOnly [] - let allocateArrayFrom (xs: 'T []) (len: int) : 'T [] = nativeOnly + let allocateArrayFrom (xs: 'T[]) (len: int) : 'T[] = nativeOnly - let allocateArrayFromCons (cons: Cons<'T>) (len: int) : 'T [] = + let allocateArrayFromCons (cons: Cons<'T>) (len: int) : 'T[] = if isNull cons then Lua.Array.Create(len) else cons.Allocate(len) + let inline isDynamicArrayImpl arr = Lua.Array.isArray arr // let inline typedArraySetImpl (target: obj) (source: obj) (offset: int): unit = @@ -39,71 +41,72 @@ module Helpers = return t1 end)($0, $1) """)>] - let concatImpl (array1: 'T []) (arrays: 'T [] seq) : 'T [] = nativeOnly + let concatImpl (array1: 'T[]) (arrays: 'T[] seq) : 'T[] = nativeOnly - let fillImpl (array: 'T []) (value: 'T) (start: int) (count: int) : 'T [] = + let fillImpl (array: 'T[]) (value: 'T) (start: int) (count: int) : 'T[] = for i = 0 to count - 1 do - array.[i+start] <- value + array.[i + start] <- value + array [] - let foldImpl (folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T []) : 'State = nativeOnly + let foldImpl (folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T[]) : 'State = nativeOnly - let inline foldIndexedImpl (folder: 'State -> 'T -> int -> 'State) (state: 'State) (array: 'T []) : 'State = + let inline foldIndexedImpl (folder: 'State -> 'T -> int -> 'State) (state: 'State) (array: 'T[]) : 'State = !! array?reduce (System.Func<'State, 'T, int, 'State>(folder), state) - let inline foldBackImpl (folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T []) : 'State = + let inline foldBackImpl (folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T[]) : 'State = !! array?reduceRight (System.Func<'State, 'T, 'State>(folder), state) - let inline foldBackIndexedImpl (folder: 'State -> 'T -> int -> 'State) (state: 'State) (array: 'T []) : 'State = + let inline foldBackIndexedImpl (folder: 'State -> 'T -> int -> 'State) (state: 'State) (array: 'T[]) : 'State = !! array?reduceRight (System.Func<'State, 'T, int, 'State>(folder), state) // Typed arrays not supported, only dynamic ones do - let inline pushImpl (array: 'T []) (item: 'T) : int = !! array?append (item) + let inline pushImpl (array: 'T[]) (item: 'T) : int = !! array?append (item) // Typed arrays not supported, only dynamic ones do - let inline insertImpl (array: 'T []) (index: int) (item: 'T) : 'T [] = !! array?splice (index, 0, item) + let inline insertImpl (array: 'T[]) (index: int) (item: 'T) : 'T[] = !! array?splice (index, 0, item) // Typed arrays not supported, only dynamic ones do - let inline spliceImpl (array: 'T []) (start: int) (deleteCount: int) : 'T [] = !! array?splice (start, deleteCount) + let inline spliceImpl (array: 'T[]) (start: int) (deleteCount: int) : 'T[] = !! array?splice (start, deleteCount) [] - let reverseImpl (array: 'T []) : 'T [] = nativeOnly + let reverseImpl (array: 'T[]) : 'T[] = nativeOnly [] - let copyImpl (array: 'T []) : 'T [] = nativeOnly + let copyImpl (array: 'T[]) : 'T[] = nativeOnly [] - let skipImpl (array: 'T []) (count: int) : 'T [] = nativeOnly + let skipImpl (array: 'T[]) (count: int) : 'T[] = nativeOnly [] - let subArrayImpl (array: 'T []) (start: int) (count: int) : 'T [] = nativeOnly + let subArrayImpl (array: 'T[]) (start: int) (count: int) : 'T[] = nativeOnly - let inline indexOfImpl (array: 'T []) (item: 'T) (start: int) : int = !! array?indexOf (item, start) + let inline indexOfImpl (array: 'T[]) (item: 'T) (start: int) : int = !! array?indexOf (item, start) - let inline findImpl (predicate: 'T -> bool) (array: 'T []) : 'T option = !! array?find (predicate) + let inline findImpl (predicate: 'T -> bool) (array: 'T[]) : 'T option = !! array?find (predicate) - let inline findIndexImpl (predicate: 'T -> bool) (array: 'T []) : int = !! array?findIndex (predicate) + let inline findIndexImpl (predicate: 'T -> bool) (array: 'T[]) : int = !! array?findIndex (predicate) - let inline collectImpl (mapping: 'T -> 'U []) (array: 'T []) : 'U [] = !! array?flatMap (mapping) + let inline collectImpl (mapping: 'T -> 'U[]) (array: 'T[]) : 'U[] = !! array?flatMap (mapping) - let inline containsImpl (predicate: 'T -> bool) (array: 'T []) : bool = !! array?filter (predicate) + let inline containsImpl (predicate: 'T -> bool) (array: 'T[]) : bool = !! array?filter (predicate) - let inline existsImpl (predicate: 'T -> bool) (array: 'T []) : bool = !! array?some (predicate) + let inline existsImpl (predicate: 'T -> bool) (array: 'T[]) : bool = !! array?some (predicate) - let inline forAllImpl (predicate: 'T -> bool) (array: 'T []) : bool = !! array?every (predicate) + let inline forAllImpl (predicate: 'T -> bool) (array: 'T[]) : bool = !! array?every (predicate) - let inline filterImpl (predicate: 'T -> bool) (array: 'T []) : 'T [] = !! array?filter (predicate) + let inline filterImpl (predicate: 'T -> bool) (array: 'T[]) : 'T[] = !! array?filter (predicate) [] - let reduceImpl (reduction: 'T -> 'T -> 'T) (array: 'T []) : 'T = nativeOnly + let reduceImpl (reduction: 'T -> 'T -> 'T) (array: 'T[]) : 'T = nativeOnly - let inline reduceBackImpl (reduction: 'T -> 'T -> 'T) (array: 'T []) : 'T = !! array?reduceRight (reduction) + let inline reduceBackImpl (reduction: 'T -> 'T -> 'T) (array: 'T[]) : 'T = !! array?reduceRight (reduction) // Inlining in combination with dynamic application may cause problems with uncurrying // Using Emit keeps the argument signature. Note: Python cannot take an argument here. [] - let sortInPlaceWithImpl (comparer: 'T -> 'T -> int) (array: 'T []) : unit = nativeOnly //!!array?sort(comparer) + let sortInPlaceWithImpl (comparer: 'T -> 'T -> int) (array: 'T[]) : unit = nativeOnly //!!array?sort(comparer) [] - let copyToTypedArray (src: 'T []) (srci: int) (trg: 'T []) (trgi: int) (cnt: int) : unit = nativeOnly + let copyToTypedArray (src: 'T[]) (srci: int) (trg: 'T[]) (trgi: int) (cnt: int) : unit = nativeOnly diff --git a/src/fable-library-lua/fable/Timer.fs b/src/fable-library-lua/fable/Timer.fs index 51fb7fe1ba..87f27724d2 100644 --- a/src/fable-library-lua/fable/Timer.fs +++ b/src/fable-library-lua/fable/Timer.fs @@ -7,20 +7,20 @@ open Fable.Core /// Thread and as such also functions as an example of creating custom /// threads. type ITimer = - abstract daemon : bool with get, set + abstract daemon: bool with get, set /// Start the thread’s activity. - abstract start : unit -> unit + abstract start: unit -> unit /// Stop the timer, and cancel the execution of the timer’s action. /// This will only work if the timer is still in its waiting stage. - abstract cancel : unit -> unit + abstract cancel: unit -> unit /// Create a timer that will run function with arguments args and /// keyword arguments kwargs, after interval seconds have passed. If /// args is None (the default) then an empty list will be used. If /// kwargs is None (the default) then an empty dict will be used. [] - abstract Create : float * (unit -> unit) -> ITimer + abstract Create: float * (unit -> unit) -> ITimer [] -let Timer : ITimer = nativeOnly +let Timer: ITimer = nativeOnly From a951493a8364f5909d37b3361357e83b00633a9a Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Tue, 4 Jun 2024 20:50:54 -0400 Subject: [PATCH 36/41] fixed some ionide analyzer warns --- src/Fable.Transforms/Lua/Compiler.fs | 2 +- src/Fable.Transforms/Lua/Fable2Lua.fs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Fable.Transforms/Lua/Compiler.fs b/src/Fable.Transforms/Lua/Compiler.fs index 597476dcae..4bdb5fb7eb 100644 --- a/src/Fable.Transforms/Lua/Compiler.fs +++ b/src/Fable.Transforms/Lua/Compiler.fs @@ -10,7 +10,7 @@ type LuaCompiler(com: Fable.Compiler) = member this.Com = com member this.AddClassDecl(c: ClassDecl) = types <- types |> Map.add c.Entity c member this.GetByRef(e: EntityRef) = types |> Map.tryFind e - member this.DecisionTreeTargets(exprs: (list * Expr) list) = decisionTreeTargets <- exprs + member this.DecisionTreeTargets(exprs: (Fable.Ident list * Expr) list) = decisionTreeTargets <- exprs member this.GetDecisionTreeTargets(idx: int) = decisionTreeTargets.[idx] member this.GetMember(memberRef: Fable.MemberRef) : Fable.MemberFunctionOrValue = diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index 679ddcc10f..805130b711 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -126,7 +126,7 @@ module Transforms = | Fable.StringConstant(s) -> Const(ConstString s) | Fable.BoolConstant(b) -> Const(ConstBool b) | Fable.UnitConstant -> Const(ConstNull) - | Fable.CharConstant(c) -> Const(ConstString(string c)) + | Fable.CharConstant(c: char) -> Const(ConstString(string c)) // | Fable.EnumConstant(e,ref) -> // convertExpr com e | Fable.NewRecord(values, ref, args) -> @@ -262,9 +262,10 @@ module Transforms = [ Const(ConstString path) ] ) - match info.Selector with - | "" -> rcall - | s -> GetObjMethod(rcall, s) + if String.IsNullOrEmpty info.Selector then + rcall + else + GetObjMethod(rcall, info.Selector) | Fable.Expr.IdentExpr(i) when i.Name <> "" -> Ident { @@ -412,7 +413,7 @@ module Transforms = elif isGetter then Helpers.ident "property" else - Helpers.ident $"{memb.Name}.setter" + Helpers.ident $"%s{memb.Name}.setter" ] let args, body, returnType = [ "" ], [ Do(Unknown "") ], Unknown "" From 057518bbee734cd51aec63da0ddc11b6cfef9c7d Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Tue, 4 Jun 2024 21:49:33 -0400 Subject: [PATCH 37/41] updated build project to include lua --- src/Fable.Build/Fable.Build.fsproj | 3 + src/Fable.Build/FableLibrary/Lua.fs | 18 + src/Fable.Build/Main.fs | 6 + src/Fable.Build/Quicktest/Lua.fs | 15 + src/Fable.Build/Test/Lua.fs | 63 + src/fable-library-lua/Array.fs | 1226 +++++++++++++++++ src/fable-library-lua/Choice.fs | 85 ++ .../{fable => }/Fable.Library.fsproj | 8 +- src/fable-library-lua/Global.fs | 34 + src/fable-library-lua/{fable => }/Native.fs | 0 src/fable-library-lua/{fable => }/Timer.fs | 0 src/fable-library-lua/{fable => }/Util.lua | 0 12 files changed, 1454 insertions(+), 4 deletions(-) create mode 100644 src/Fable.Build/FableLibrary/Lua.fs create mode 100644 src/Fable.Build/Quicktest/Lua.fs create mode 100644 src/Fable.Build/Test/Lua.fs create mode 100644 src/fable-library-lua/Array.fs create mode 100644 src/fable-library-lua/Choice.fs rename src/fable-library-lua/{fable => }/Fable.Library.fsproj (87%) create mode 100644 src/fable-library-lua/Global.fs rename src/fable-library-lua/{fable => }/Native.fs (100%) rename src/fable-library-lua/{fable => }/Timer.fs (100%) rename src/fable-library-lua/{fable => }/Util.lua (100%) diff --git a/src/Fable.Build/Fable.Build.fsproj b/src/Fable.Build/Fable.Build.fsproj index 21aedbefd0..52ce78143a 100644 --- a/src/Fable.Build/Fable.Build.fsproj +++ b/src/Fable.Build/Fable.Build.fsproj @@ -16,6 +16,7 @@ + @@ -27,6 +28,7 @@ + @@ -36,6 +38,7 @@ + diff --git a/src/Fable.Build/FableLibrary/Lua.fs b/src/Fable.Build/FableLibrary/Lua.fs new file mode 100644 index 0000000000..4873c426a3 --- /dev/null +++ b/src/Fable.Build/FableLibrary/Lua.fs @@ -0,0 +1,18 @@ +namespace Build.FableLibrary + +open System.IO +open Fake.IO + +type BuildFableLibraryLua() = + inherit + BuildFableLibrary( + "lua", + Path.Combine("src", "fable-library-lua"), + Path.Combine("src", "fable-library-lua"), + Path.Combine("temp", "fable-library-lua"), + Path.Combine("temp", "fable-library-lua"), + Path.Combine(".", "temp", "fable-library-lua") + ) + + override this.CopyStage() = + Directory.GetFiles(this.LibraryDir, "*") |> Shell.copyFiles this.BuildDir diff --git a/src/Fable.Build/Main.fs b/src/Fable.Build/Main.fs index db0ae5a746..19874145ee 100644 --- a/src/Fable.Build/Main.fs +++ b/src/Fable.Build/Main.fs @@ -19,6 +19,7 @@ Available commands: --python Build fable-library for Python --dart Build fable-library for Dart --rust Build fable-library for Rust + --lua Build fable-library for Lua quicktest Watch for changes and re-run the quicktest This is useful to work on a feature in an isolated @@ -30,6 +31,7 @@ Available commands: python Run for Python dart Run for Dart rust Run for Rust + lua Run for Lua Options: --skip-fable-library Skip building fable-library if folder already exists @@ -41,6 +43,7 @@ Available commands: python Run the tests for Python dart Run the tests for Dart rust Run the tests for Rust + lua Run the tests for Lua integration Run the integration test suite standalone Tests the standalone version of Fable (Fable running on top of Node.js) @@ -119,6 +122,7 @@ let main argv = | "--python" :: _ -> BuildFableLibraryPython().Run() | "--dart" :: _ -> BuildFableLibraryDart().Run() | "--rust" :: _ -> BuildFableLibraryRust().Run() + | "--lua" :: _ -> BuildFableLibraryLua().Run() | _ -> printHelp () | "test" :: args -> match args with @@ -132,6 +136,7 @@ let main argv = // This test is using quicktest project for now, // because it can't compile (yet?) the Main JavaScript tests | "compiler-js" :: _ -> Test.CompilerJs.handle args + | "lua" :: args -> Test.Lua.handle args | _ -> printHelp () | "quicktest" :: args -> match args with @@ -140,6 +145,7 @@ let main argv = | "python" :: _ -> Quicktest.Python.handle args | "dart" :: _ -> Quicktest.Dart.handle args | "rust" :: _ -> Quicktest.Rust.handle args + | "lua" :: _ -> Quicktest.Lua.handle args | _ -> printHelp () | "standalone" :: args -> Standalone.handle args | "compiler-js" :: args -> CompilerJs.handle args diff --git a/src/Fable.Build/Quicktest/Lua.fs b/src/Fable.Build/Quicktest/Lua.fs new file mode 100644 index 0000000000..949e7f9d1d --- /dev/null +++ b/src/Fable.Build/Quicktest/Lua.fs @@ -0,0 +1,15 @@ +module Build.Quicktest.Lua + +open Build.FableLibrary +open Build.Quicktest.Core + +let handle (args: string list) = + genericQuicktest + { + Language = "lua" + FableLibBuilder = BuildFableLibraryLua() + ProjectDir = "src/quicktest-lua" + Extension = ".lua" + RunMode = RunScript + } + args diff --git a/src/Fable.Build/Test/Lua.fs b/src/Fable.Build/Test/Lua.fs new file mode 100644 index 0000000000..d0d2a5821c --- /dev/null +++ b/src/Fable.Build/Test/Lua.fs @@ -0,0 +1,63 @@ +module Build.Test.Lua + +open Build.FableLibrary +open System.IO +open Build.Utils +open BlackFox.CommandLine +open SimpleExec +open Fake.IO + +let private buildDir = Path.Resolve("temp", "tests", "Lua") +let private testsFolder = Path.Resolve("tests", "Lua") +let private testsFsprojFolder = Path.Resolve("tests", "Lua") + +let handle (args: string list) = + let skipFableLibrary = args |> List.contains "--skip-fable-library" + let isWatch = args |> List.contains "--watch" + let noDotnet = args |> List.contains "--no-dotnet" + + BuildFableLibraryLua().Run(skipFableLibrary) + + Directory.clean buildDir + + Directory.GetFiles(testsFolder, "*") |> Seq.iter (Shell.copyFile buildDir) + + Directory.GetFiles(testsFolder, "*.lua") |> Seq.iter (Shell.copyFile buildDir) + + let testCmd = $"lua test {buildDir}/main.lua" + + let fableArgs = + CmdLine.concat + [ + CmdLine.empty + |> CmdLine.appendRaw testsFsprojFolder + |> CmdLine.appendPrefix "--outDir" (buildDir "src") + |> CmdLine.appendPrefix "--lang" "lua" + |> CmdLine.appendPrefix "--exclude" "Fable.Core" + |> CmdLine.appendRaw "--noCache" + + if isWatch then + CmdLine.empty + |> CmdLine.appendRaw "--watch" + |> CmdLine.appendRaw "--runWatch" + |> CmdLine.appendRaw testCmd + else + CmdLine.empty |> CmdLine.appendRaw "--run" |> CmdLine.appendRaw testCmd + ] + + if isWatch then + Async.Parallel + [ + if not noDotnet then + Command.RunAsync("dotnet", "watch test -c Release", workingDirectory = testsFsprojFolder) + |> Async.AwaitTask + + Command.WatchFableAsync(fableArgs, workingDirectory = buildDir) + |> Async.AwaitTask + ] + |> Async.RunSynchronously + |> ignore + else + Command.Run("dotnet", "test -c Release", workingDirectory = testsFsprojFolder) + + Command.Fable(fableArgs, workingDirectory = buildDir) diff --git a/src/fable-library-lua/Array.fs b/src/fable-library-lua/Array.fs new file mode 100644 index 0000000000..d01cfafbce --- /dev/null +++ b/src/fable-library-lua/Array.fs @@ -0,0 +1,1226 @@ +module ArrayModule + +// Disables warn:1204 raised by use of LanguagePrimitives.ErrorStrings.* +#nowarn "1204" + +open System.Collections.Generic +open Fable.Core +open Fable.Core.JsInterop + +open Native +open Native.Helpers + +let private indexNotFound () = + failwith "An index satisfying the predicate was not found in the collection." + +let private differentLengths () = failwith "Arrays had different lengths" + +// Pay attention when benchmarking to append and filter functions below +// if implementing via native JS array .concat() and .filter() do not fall behind due to js-native transitions. + +// Don't use native JS Array.prototype.concat as it doesn't work with typed arrays +let append (array1: 'T[]) (array2: 'T[]) ([] cons: Cons<'T>) : 'T[] = + let len1 = array1.Length + let len2 = array2.Length + let newArray = allocateArrayFromCons cons (len1 + len2) + + for i = 0 to len1 - 1 do + newArray.[i] <- array1.[i] + + for i = 0 to len2 - 1 do + newArray.[i + len1] <- array2.[i] + + newArray + +let filter (predicate: 'T -> bool) (array: 'T[]) = filterImpl predicate array + +// intentionally returns target instead of unit +let fill (target: 'T[]) (targetIndex: int) (count: int) (value: 'T) : 'T[] = fillImpl target value targetIndex count + +let getSubArray (array: 'T[]) (start: int) (count: int) : 'T[] = subArrayImpl array start count + +let last (array: 'T[]) = + if array.Length = 0 then + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString + + array.[array.Length - 1] + +let tryLast (array: 'T[]) = + if array.Length = 0 then + None + else + Some array.[array.Length - 1] + +let mapIndexed (f: int -> 'T -> 'U) (source: 'T[]) ([] cons: Cons<'U>) : 'U[] = + let len = source.Length + let target = allocateArrayFromCons cons len + + for i = 0 to (len - 1) do + target.[i] <- f i source.[i] + + target + +let map (f: 'T -> 'U) (source: 'T[]) ([] cons: Cons<'U>) : 'U[] = + let len = source.Length + let target = allocateArrayFromCons cons len + + for i = 0 to (len - 1) do + target.[i] <- f source.[i] + + target + +let mapIndexed2 + (f: int -> 'T1 -> 'T2 -> 'U) + (source1: 'T1[]) + (source2: 'T2[]) + ([] cons: Cons<'U>) + : 'U[] + = + if source1.Length <> source2.Length then + failwith "Arrays had different lengths" + + let result = allocateArrayFromCons cons source1.Length + + for i = 0 to source1.Length - 1 do + result.[i] <- f i source1.[i] source2.[i] + + result + +let map2 (f: 'T1 -> 'T2 -> 'U) (source1: 'T1[]) (source2: 'T2[]) ([] cons: Cons<'U>) : 'U[] = + if source1.Length <> source2.Length then + failwith "Arrays had different lengths" + + let result = allocateArrayFromCons cons source1.Length + + for i = 0 to source1.Length - 1 do + result.[i] <- f source1.[i] source2.[i] + + result + +let mapIndexed3 + (f: int -> 'T1 -> 'T2 -> 'T3 -> 'U) + (source1: 'T1[]) + (source2: 'T2[]) + (source3: 'T3[]) + ([] cons: Cons<'U>) + : 'U[] + = + if source1.Length <> source2.Length || source2.Length <> source3.Length then + failwith "Arrays had different lengths" + + let result = allocateArrayFromCons cons source1.Length + + for i = 0 to source1.Length - 1 do + result.[i] <- f i source1.[i] source2.[i] source3.[i] + + result + +let map3 + (f: 'T1 -> 'T2 -> 'T3 -> 'U) + (source1: 'T1[]) + (source2: 'T2[]) + (source3: 'T3[]) + ([] cons: Cons<'U>) + : 'U[] + = + if source1.Length <> source2.Length || source2.Length <> source3.Length then + failwith "Arrays had different lengths" + + let result = allocateArrayFromCons cons source1.Length + + for i = 0 to source1.Length - 1 do + result.[i] <- f source1.[i] source2.[i] source3.[i] + + result + +let mapFold<'T, 'State, 'Result> + (mapping: 'State -> 'T -> 'Result * 'State) + state + (array: 'T[]) + ([] cons: Cons<'Result>) + = + match array.Length with + | 0 -> [||], state + | len -> + let mutable acc = state + let res = allocateArrayFromCons cons len + + for i = 0 to array.Length - 1 do + let h, s = mapping acc array.[i] + res.[i] <- h + acc <- s + + res, acc + +let mapFoldBack<'T, 'State, 'Result> + (mapping: 'T -> 'State -> 'Result * 'State) + (array: 'T[]) + state + ([] cons: Cons<'Result>) + = + match array.Length with + | 0 -> [||], state + | len -> + let mutable acc = state + let res = allocateArrayFromCons cons len + + for i = array.Length - 1 downto 0 do + let h, s = mapping array.[i] acc + res.[i] <- h + acc <- s + + res, acc + +let indexed (source: 'T[]) = + let len = source.Length + let target = allocateArray len + + for i = 0 to (len - 1) do + target.[i] <- i, source.[i] + + target + +let truncate (count: int) (array: 'T[]) : 'T[] = + let count = max 0 count + subArrayImpl array 0 count + +let concat (arrays: 'T[] seq) ([] cons: Cons<'T>) : 'T[] = + let arrays = + if isDynamicArrayImpl arrays then + arrays :?> 'T[][] // avoid extra copy + else + arrayFrom arrays + + match arrays.Length with + | 0 -> allocateArrayFromCons cons 0 + | 1 -> arrays.[0] + | _ -> + let mutable totalIdx = 0 + let mutable totalLength = 0 + + for arr in arrays do + totalLength <- totalLength + arr.Length + + let result = allocateArrayFromCons cons totalLength + + for arr in arrays do + for j = 0 to (arr.Length - 1) do + result.[totalIdx] <- arr.[j] + totalIdx <- totalIdx + 1 + + result + +let collect (mapping: 'T -> 'U[]) (array: 'T[]) ([] cons: Cons<'U>) : 'U[] = + let mapped = map mapping array Unchecked.defaultof<_> + concat mapped cons +// collectImpl mapping array // flatMap not widely available yet + +let where predicate (array: _[]) = filterImpl predicate array + +let indexOf<'T> + (array: 'T[]) + (item: 'T) + (start: int option) + (count: int option) + ([] eq: IEqualityComparer<'T>) + = + let start = defaultArg start 0 + + let end' = + count |> Option.map (fun c -> start + c) |> Option.defaultValue array.Length + + let rec loop i = + if i >= end' then + -1 + else if eq.Equals(item, array.[i]) then + i + else + loop (i + 1) + + loop start + +let contains<'T> (value: 'T) (array: 'T[]) ([] eq: IEqualityComparer<'T>) = + indexOf array value None None eq >= 0 + +let empty cons = allocateArrayFromCons cons 0 + +let singleton value ([] cons: Cons<'T>) = + let ar = allocateArrayFromCons cons 1 + ar.[0] <- value + ar + +let initialize count initializer ([] cons: Cons<'T>) = + if count < 0 then + invalidArg "count" LanguagePrimitives.ErrorStrings.InputMustBeNonNegativeString + + let result = allocateArrayFromCons cons count + + for i = 0 to count - 1 do + result.[i] <- initializer i + + result + +let pairwise (array: 'T[]) = + if array.Length < 2 then + [||] + else + let count = array.Length - 1 + let result = allocateArray count + + for i = 0 to count - 1 do + result.[i] <- array.[i], array.[i + 1] + + result + +let replicate count initial ([] cons: Cons<'T>) = + // Shorthand version: = initialize count (fun _ -> initial) + if count < 0 then + invalidArg "count" LanguagePrimitives.ErrorStrings.InputMustBeNonNegativeString + + let result: 'T array = allocateArrayFromCons cons count + + for i = 0 to result.Length - 1 do + result.[i] <- initial + + result + +let copy (array: 'T[]) = + // if isTypedArrayImpl array then + // let res = allocateArrayFrom array array.Length + // for i = 0 to array.Length-1 do + // res.[i] <- array.[i] + // res + // else + copyImpl array + +let copyTo (source: 'T[]) (sourceIndex: int) (target: 'T[]) (targetIndex: int) (count: int) = + // TODO: Check array lengths + System.Array.Copy(source, sourceIndex, target, targetIndex, count) + +let reverse (array: 'T[]) = + // if isTypedArrayImpl array then + // let res = allocateArrayFrom array array.Length + // let mutable j = array.Length-1 + // for i = 0 to array.Length-1 do + // res.[j] <- array.[i] + // j <- j - 1 + // res + // else + copyImpl array |> reverseImpl + +let scan<'T, 'State> folder (state: 'State) (array: 'T[]) ([] cons: Cons<'State>) = + let res = allocateArrayFromCons cons (array.Length + 1) + res.[0] <- state + + for i = 0 to array.Length - 1 do + res.[i + 1] <- folder res.[i] array.[i] + + res + +let scanBack<'T, 'State> folder (array: 'T[]) (state: 'State) ([] cons: Cons<'State>) = + let res = allocateArrayFromCons cons (array.Length + 1) + res.[array.Length] <- state + + for i = array.Length - 1 downto 0 do + res.[i] <- folder array.[i] res.[i + 1] + + res + +let skip count (array: 'T[]) ([] cons: Cons<'T>) = + if count > array.Length then + invalidArg "count" "count is greater than array length" + + if count = array.Length then + allocateArrayFromCons cons 0 + else + let count = + if count < 0 then + 0 + else + count + + skipImpl array count + +let skipWhile predicate (array: 'T[]) ([] cons: Cons<'T>) = + let mutable count = 0 + + while count < array.Length && predicate array.[count] do + count <- count + 1 + + if count = array.Length then + allocateArrayFromCons cons 0 + else + skipImpl array count + +let take count (array: 'T[]) ([] cons: Cons<'T>) = + if count < 0 then + invalidArg "count" LanguagePrimitives.ErrorStrings.InputMustBeNonNegativeString + + if count > array.Length then + invalidArg "count" "count is greater than array length" + + if count = 0 then + allocateArrayFromCons cons 0 + else + subArrayImpl array 0 count + +let takeWhile predicate (array: 'T[]) ([] cons: Cons<'T>) = + let mutable count = 0 + + while count < array.Length && predicate array.[count] do + count <- count + 1 + + if count = 0 then + allocateArrayFromCons cons 0 + else + subArrayImpl array 0 count + +let addInPlace (x: 'T) (array: 'T[]) = + // if isTypedArrayImpl array then invalidArg "array" "Typed arrays not supported" + pushImpl array x |> ignore + +let addRangeInPlace (range: seq<'T>) (array: 'T[]) = + // if isTypedArrayImpl array then invalidArg "array" "Typed arrays not supported" + for x in range do + addInPlace x array + +let insertRangeInPlace index (range: seq<'T>) (array: 'T[]) = + // if isTypedArrayImpl array then invalidArg "array" "Typed arrays not supported" + let mutable i = index + + for x in range do + insertImpl array i x |> ignore + i <- i + 1 + +let removeInPlace (item: 'T) (array: 'T[]) ([] eq: IEqualityComparer<'T>) = + let i = indexOf array item None None eq + + if i > -1 then + spliceImpl array i 1 |> ignore + true + else + false + +let removeAllInPlace predicate (array: 'T[]) = + let rec countRemoveAll count = + let i = findIndexImpl predicate array + + if i > -1 then + spliceImpl array i 1 |> ignore + countRemoveAll count + 1 + else + count + + countRemoveAll 0 + +let partition (f: 'T -> bool) (source: 'T[]) ([] cons: Cons<'T>) = + let len = source.Length + let res1 = allocateArrayFromCons cons len + let res2 = allocateArrayFromCons cons len + let mutable iTrue = 0 + let mutable iFalse = 0 + + for i = 0 to len - 1 do + if f source.[i] then + res1.[iTrue] <- source.[i] + iTrue <- iTrue + 1 + else + res2.[iFalse] <- source.[i] + iFalse <- iFalse + 1 + + res1 |> truncate iTrue, res2 |> truncate iFalse + +let find (predicate: 'T -> bool) (array: 'T[]) : 'T = + match findImpl predicate array with + | Some res -> res + | None -> indexNotFound () + +let tryFind (predicate: 'T -> bool) (array: 'T[]) : 'T option = findImpl predicate array + +let findIndex (predicate: 'T -> bool) (array: 'T[]) : int = + match findIndexImpl predicate array with + | index when index > -1 -> index + | _ -> + indexNotFound () + -1 + +let tryFindIndex (predicate: 'T -> bool) (array: 'T[]) : int option = + match findIndexImpl predicate array with + | index when index > -1 -> Some index + | _ -> None + +let pick chooser (array: _[]) = + let rec loop i = + if i >= array.Length then + indexNotFound () + else + match chooser array.[i] with + | None -> loop (i + 1) + | Some res -> res + + loop 0 + +let tryPick chooser (array: _[]) = + let rec loop i = + if i >= array.Length then + None + else + match chooser array.[i] with + | None -> loop (i + 1) + | res -> res + + loop 0 + +let findBack predicate (array: _[]) = + let rec loop i = + if i < 0 then + indexNotFound () + elif predicate array.[i] then + array.[i] + else + loop (i - 1) + + loop (array.Length - 1) + +let tryFindBack predicate (array: _[]) = + let rec loop i = + if i < 0 then + None + elif predicate array.[i] then + Some array.[i] + else + loop (i - 1) + + loop (array.Length - 1) + +let findLastIndex predicate (array: _[]) = + let rec loop i = + if i < 0 then + -1 + elif predicate array.[i] then + i + else + loop (i - 1) + + loop (array.Length - 1) + +let findIndexBack predicate (array: _[]) = + let rec loop i = + if i < 0 then + indexNotFound () + -1 + elif predicate array.[i] then + i + else + loop (i - 1) + + loop (array.Length - 1) + +let tryFindIndexBack predicate (array: _[]) = + let rec loop i = + if i < 0 then + None + elif predicate array.[i] then + Some i + else + loop (i - 1) + + loop (array.Length - 1) + +let choose (chooser: 'T -> 'U option) (array: 'T[]) ([] cons: Cons<'U>) = + let res: 'U[] = [||] + + for i = 0 to array.Length - 1 do + match chooser array.[i] with + | None -> () + | Some y -> pushImpl res y |> ignore + + match box cons with + | null -> res // avoid extra copy + | _ -> map id res cons + +let foldIndexed<'T, 'State> folder (state: 'State) (array: 'T[]) = + // if isTypedArrayImpl array then + // let mutable acc = state + // for i = 0 to array.Length - 1 do + // acc <- folder i acc array.[i] + // acc + // else + foldIndexedImpl (fun acc x i -> folder i acc x) state array + +let fold<'T, 'State> folder (state: 'State) (array: 'T[]) = + // if isTypedArrayImpl array then + // let mutable acc = state + // for i = 0 to array.Length - 1 do + // acc <- folder acc array.[i] + // acc + // else + foldImpl (fun acc x -> folder acc x) state array + +let iterate action (array: 'T[]) = + for i = 0 to array.Length - 1 do + action array.[i] + +let iterateIndexed action (array: 'T[]) = + for i = 0 to array.Length - 1 do + action i array.[i] + +let iterate2 action (array1: 'T1[]) (array2: 'T2[]) = + if array1.Length <> array2.Length then + differentLengths () + + for i = 0 to array1.Length - 1 do + action array1.[i] array2.[i] + +let iterateIndexed2 action (array1: 'T1[]) (array2: 'T2[]) = + if array1.Length <> array2.Length then + differentLengths () + + for i = 0 to array1.Length - 1 do + action i array1.[i] array2.[i] + +let isEmpty (array: 'T[]) = array.Length = 0 + +let forAll predicate (array: 'T[]) = + // if isTypedArrayImpl array then + // let mutable i = 0 + // let mutable result = true + // while i < array.Length && result do + // result <- predicate array.[i] + // i <- i + 1 + // result + // else + forAllImpl predicate array + +let permute f (array: 'T[]) = + let size = array.Length + let res = copyImpl array + let checkFlags = allocateArray size + + iterateIndexed + (fun i x -> + let j = f i + + if j < 0 || j >= size then + invalidOp "Not a valid permutation" + + res.[j] <- x + checkFlags.[j] <- 1 + ) + array + + let isValid = checkFlags |> forAllImpl ((=) 1) + + if not isValid then + invalidOp "Not a valid permutation" + + res + +let setSlice (target: 'T[]) (lower: int option) (upper: int option) (source: 'T[]) = + let lower = defaultArg lower 0 + let upper = defaultArg upper -1 + + let length = + (if upper >= 0 then + upper + else + target.Length - 1) + - lower + // can't cast to TypedArray, so can't use TypedArray-specific methods + // if isTypedArrayImpl target && source.Length <= length then + // typedArraySetImpl target source lower + // else + for i = 0 to length do + target.[i + lower] <- source.[i] + +let sortInPlaceBy (projection: 'a -> 'b) (xs: 'a[]) ([] comparer: IComparer<'b>) : unit = + sortInPlaceWithImpl (fun x y -> comparer.Compare(projection x, projection y)) xs + +let sortInPlace (xs: 'T[]) ([] comparer: IComparer<'T>) = + sortInPlaceWithImpl (fun x y -> comparer.Compare(x, y)) xs + +let inline internal sortInPlaceWith (comparer: 'T -> 'T -> int) (xs: 'T[]) = + sortInPlaceWithImpl comparer xs + xs + +let sort (xs: 'T[]) ([] comparer: IComparer<'T>) : 'T[] = + sortInPlaceWith (fun x y -> comparer.Compare(x, y)) (copyImpl xs) + +let sortBy (projection: 'a -> 'b) (xs: 'a[]) ([] comparer: IComparer<'b>) : 'a[] = + sortInPlaceWith (fun x y -> comparer.Compare(projection x, projection y)) (copyImpl xs) + +let sortDescending (xs: 'T[]) ([] comparer: IComparer<'T>) : 'T[] = + sortInPlaceWith (fun x y -> comparer.Compare(x, y) * -1) (copyImpl xs) + +let sortByDescending (projection: 'a -> 'b) (xs: 'a[]) ([] comparer: IComparer<'b>) : 'a[] = + sortInPlaceWith (fun x y -> comparer.Compare(projection x, projection y) * -1) (copyImpl xs) + +let sortWith (comparer: 'T -> 'T -> int) (xs: 'T[]) : 'T[] = sortInPlaceWith comparer (copyImpl xs) + +let allPairs (xs: 'T1[]) (ys: 'T2[]) : ('T1 * 'T2)[] = + let len1 = xs.Length + let len2 = ys.Length + let res = allocateArray (len1 * len2) + + for i = 0 to xs.Length - 1 do + for j = 0 to ys.Length - 1 do + res.[i * len2 + j] <- (xs.[i], ys.[j]) + + res + +let unfold<'T, 'State> (generator: 'State -> ('T * 'State) option) (state: 'State) : 'T[] = + let res: 'T[] = [||] + + let rec loop state = + match generator state with + | None -> () + | Some(x, s) -> + pushImpl res x |> ignore + loop s + + loop state + res + +// TODO: We should pass Cons<'T> here (and unzip3) but 'a and 'b may differ +let unzip (array: _[]) = + let len = array.Length + let res1 = allocateArray len + let res2 = allocateArray len + + iterateIndexed + (fun i (item1, item2) -> + res1.[i] <- item1 + res2.[i] <- item2 + ) + array + + res1, res2 + +let unzip3 (array: _[]) = + let len = array.Length + let res1 = allocateArray len + let res2 = allocateArray len + let res3 = allocateArray len + + iterateIndexed + (fun i (item1, item2, item3) -> + res1.[i] <- item1 + res2.[i] <- item2 + res3.[i] <- item3 + ) + array + + res1, res2, res3 + +let zip (array1: 'T[]) (array2: 'U[]) = + // Shorthand version: map2 (fun x y -> x, y) array1 array2 + if array1.Length <> array2.Length then + differentLengths () + + let result = allocateArray array1.Length + + for i = 0 to array1.Length - 1 do + result.[i] <- array1.[i], array2.[i] + + result + +let zip3 (array1: 'T[]) (array2: 'U[]) (array3: 'V[]) = + // Shorthand version: map3 (fun x y z -> x, y, z) array1 array2 array3 + if array1.Length <> array2.Length || array2.Length <> array3.Length then + differentLengths () + + let result = allocateArray array1.Length + + for i = 0 to array1.Length - 1 do + result.[i] <- array1.[i], array2.[i], array3.[i] + + result + +let chunkBySize (chunkSize: int) (array: 'T[]) : 'T[][] = + if chunkSize < 1 then + invalidArg "size" "The input must be positive." + + if array.Length = 0 then + [| [||] |] + else + let result: 'T[][] = [||] + // add each chunk to the result + for x = 0 to int (System.Math.Ceiling(float (array.Length) / float (chunkSize))) - 1 do + let start = x * chunkSize + let slice = subArrayImpl array start chunkSize + pushImpl result slice |> ignore + + result + +let splitAt (index: int) (array: 'T[]) : 'T[] * 'T[] = + if index < 0 || index > array.Length then + invalidArg "index" SR.indexOutOfBounds + + subArrayImpl array 0 index, skipImpl array index + +// Note that, though it's not consistent with `compare` operator, +// Array.compareWith doesn't compare first the length, see #2961 +let compareWith (comparer: 'T -> 'T -> int) (source1: 'T[]) (source2: 'T[]) = + if isNull source1 then + if isNull source2 then + 0 + else + -1 + elif isNull source2 then + 1 + else + let len1 = source1.Length + let len2 = source2.Length + + let len = + if len1 < len2 then + len1 + else + len2 + + let mutable i = 0 + let mutable res = 0 + + while res = 0 && i < len do + res <- comparer source1.[i] source2.[i] + i <- i + 1 + + if res <> 0 then + res + elif len1 > len2 then + 1 + elif len1 < len2 then + -1 + else + 0 + +let compareTo (comparer: 'T -> 'T -> int) (source1: 'T[]) (source2: 'T[]) = + if isNull source1 then + if isNull source2 then + 0 + else + -1 + elif isNull source2 then + 1 + else + let len1 = source1.Length + let len2 = source2.Length + + if len1 > len2 then + 1 + elif len1 < len2 then + -1 + else + let mutable i = 0 + let mutable res = 0 + + while res = 0 && i < len1 do + res <- comparer source1.[i] source2.[i] + i <- i + 1 + + res + +let equalsWith (equals: 'T -> 'T -> bool) (array1: 'T[]) (array2: 'T[]) = + if isNull array1 then + if isNull array2 then + true + else + false + elif isNull array2 then + false + else + let mutable i = 0 + let mutable result = true + let length1 = array1.Length + let length2 = array2.Length + + if length1 > length2 then + false + elif length1 < length2 then + false + else + while i < length1 && result do + result <- equals array1.[i] array2.[i] + i <- i + 1 + + result + +let exactlyOne (array: 'T[]) = + if array.Length = 1 then + array.[0] + elif array.Length = 0 then + invalidArg "array" LanguagePrimitives.ErrorStrings.InputSequenceEmptyString + else + invalidArg "array" "Input array too long" + +let tryExactlyOne (array: 'T[]) = + if array.Length = 1 then + Some(array.[0]) + else + None + +let head (array: 'T[]) = + if array.Length = 0 then + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString + else + array.[0] + +let tryHead (array: 'T[]) = + if array.Length = 0 then + None + else + Some array.[0] + +let tail (array: 'T[]) = + if array.Length = 0 then + invalidArg "array" "Not enough elements" + + skipImpl array 1 + +let item index (array: _[]) = array.[index] + +let tryItem index (array: 'T[]) = + if index < 0 || index >= array.Length then + None + else + Some array.[index] + +let foldBackIndexed<'T, 'State> folder (array: 'T[]) (state: 'State) = + // if isTypedArrayImpl array then + // let mutable acc = state + // let size = array.Length + // for i = 1 to size do + // acc <- folder (i-1) array.[size - i] acc + // acc + // else + foldBackIndexedImpl (fun acc x i -> folder i x acc) state array + +let foldBack<'T, 'State> folder (array: 'T[]) (state: 'State) = + // if isTypedArrayImpl array then + // foldBackIndexed (fun _ x acc -> folder x acc) array state + // else + foldBackImpl (fun acc x -> folder x acc) state array + +let foldIndexed2 folder state (array1: _[]) (array2: _[]) = + let mutable acc = state + + if array1.Length <> array2.Length then + failwith "Arrays have different lengths" + + for i = 0 to array1.Length - 1 do + acc <- folder i acc array1.[i] array2.[i] + + acc + +let fold2<'T1, 'T2, 'State> folder (state: 'State) (array1: 'T1[]) (array2: 'T2[]) = + foldIndexed2 (fun _ acc x y -> folder acc x y) state array1 array2 + +let foldBackIndexed2<'T1, 'T2, 'State> folder (array1: 'T1[]) (array2: 'T2[]) (state: 'State) = + let mutable acc = state + + if array1.Length <> array2.Length then + differentLengths () + + let size = array1.Length + + for i = 1 to size do + acc <- folder (i - 1) array1.[size - i] array2.[size - i] acc + + acc + +let foldBack2<'T1, 'T2, 'State> f (array1: 'T1[]) (array2: 'T2[]) (state: 'State) = + foldBackIndexed2 (fun _ x y acc -> f x y acc) array1 array2 state + +let reduce reduction (array: 'T[]) = + if array.Length = 0 then + invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + // if isTypedArrayImpl array then + // foldIndexed (fun i acc x -> if i = 0 then x else reduction acc x) Unchecked.defaultof<_> array + // else + reduceImpl reduction array + +let reduceBack reduction (array: 'T[]) = + if array.Length = 0 then + invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + // if isTypedArrayImpl array then + // foldBackIndexed (fun i x acc -> if i = 0 then x else reduction acc x) array Unchecked.defaultof<_> + // else + reduceBackImpl reduction array + +let forAll2 predicate array1 array2 = + fold2 (fun acc x y -> acc && predicate x y) true array1 array2 + +let rec existsOffset predicate (array: 'T[]) index = + if index = array.Length then + false + else + predicate array.[index] || existsOffset predicate array (index + 1) + +let exists predicate array = existsOffset predicate array 0 + +let rec existsOffset2 predicate (array1: _[]) (array2: _[]) index = + if index = array1.Length then + false + else + predicate array1.[index] array2.[index] + || existsOffset2 predicate array1 array2 (index + 1) + +let rec exists2 predicate (array1: _[]) (array2: _[]) = + if array1.Length <> array2.Length then + differentLengths () + + existsOffset2 predicate array1 array2 0 + +let sum (array: 'T[]) ([] adder: IGenericAdder<'T>) : 'T = + let mutable acc = adder.GetZero() + + for i = 0 to array.Length - 1 do + acc <- adder.Add(acc, array.[i]) + + acc + +let sumBy (projection: 'T -> 'T2) (array: 'T[]) ([] adder: IGenericAdder<'T2>) : 'T2 = + let mutable acc = adder.GetZero() + + for i = 0 to array.Length - 1 do + acc <- adder.Add(acc, projection array.[i]) + + acc + +let maxBy (projection: 'a -> 'b) (xs: 'a[]) ([] comparer: IComparer<'b>) : 'a = + reduce + (fun x y -> + if comparer.Compare(projection y, projection x) > 0 then + y + else + x + ) + xs + +let max (xs: 'a[]) ([] comparer: IComparer<'a>) : 'a = + reduce + (fun x y -> + if comparer.Compare(y, x) > 0 then + y + else + x + ) + xs + +let minBy (projection: 'a -> 'b) (xs: 'a[]) ([] comparer: IComparer<'b>) : 'a = + reduce + (fun x y -> + if comparer.Compare(projection y, projection x) > 0 then + x + else + y + ) + xs + +let min (xs: 'a[]) ([] comparer: IComparer<'a>) : 'a = + reduce + (fun x y -> + if comparer.Compare(y, x) > 0 then + x + else + y + ) + xs + +let average (array: 'T[]) ([] averager: IGenericAverager<'T>) : 'T = + if array.Length = 0 then + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString + + let mutable total = averager.GetZero() + + for i = 0 to array.Length - 1 do + total <- averager.Add(total, array.[i]) + + averager.DivideByInt(total, array.Length) + +let averageBy (projection: 'T -> 'T2) (array: 'T[]) ([] averager: IGenericAverager<'T2>) : 'T2 = + if array.Length = 0 then + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString + + let mutable total = averager.GetZero() + + for i = 0 to array.Length - 1 do + total <- averager.Add(total, projection array.[i]) + + averager.DivideByInt(total, array.Length) + +// let toList (source: 'T[]) = List.ofArray (see Replacements) + +let windowed (windowSize: int) (source: 'T[]) : 'T[][] = + if windowSize <= 0 then + failwith "windowSize must be positive" + + let res = + FSharp.Core.Operators.max 0 (source.Length - windowSize + 1) |> allocateArray + + for i = windowSize to source.Length do + res.[i - windowSize] <- source.[i - windowSize .. i - 1] + + res + +let splitInto (chunks: int) (array: 'T[]) : 'T[][] = + if chunks < 1 then + invalidArg "chunks" "The input must be positive." + + if array.Length = 0 then + [| [||] |] + else + let result: 'T[][] = [||] + let chunks = FSharp.Core.Operators.min chunks array.Length + let minChunkSize = array.Length / chunks + let chunksWithExtraItem = array.Length % chunks + + for i = 0 to chunks - 1 do + let chunkSize = + if i < chunksWithExtraItem then + minChunkSize + 1 + else + minChunkSize + + let start = i * minChunkSize + (FSharp.Core.Operators.min chunksWithExtraItem i) + let slice = subArrayImpl array start chunkSize + pushImpl result slice |> ignore + + result + +let transpose (arrays: 'T[] seq) ([] cons: Cons<'T>) : 'T[][] = + let arrays = + if isDynamicArrayImpl arrays then + arrays :?> 'T[][] // avoid extra copy + else + arrayFrom arrays + + let len = arrays.Length + + match len with + | 0 -> allocateArray 0 + | _ -> + let firstArray = arrays.[0] + let lenInner = firstArray.Length + + if arrays |> forAll (fun a -> a.Length = lenInner) |> not then + differentLengths () + + let result: 'T[][] = allocateArray lenInner + + for i in 0 .. lenInner - 1 do + result.[i] <- allocateArrayFromCons cons len + + for j in 0 .. len - 1 do + result.[i].[j] <- arrays.[j].[i] + + result + +let insertAt (index: int) (y: 'T) (xs: 'T[]) ([] cons: Cons<'T>) : 'T[] = + let len = xs.Length + + if index < 0 || index > len then + invalidArg "index" SR.indexOutOfBounds + + let target = allocateArrayFromCons cons (len + 1) + + for i = 0 to (index - 1) do + target.[i] <- xs.[i] + + target.[index] <- y + + for i = index to (len - 1) do + target.[i + 1] <- xs.[i] + + target + +let insertManyAt (index: int) (ys: seq<'T>) (xs: 'T[]) ([] cons: Cons<'T>) : 'T[] = + let len = xs.Length + + if index < 0 || index > len then + invalidArg "index" SR.indexOutOfBounds + + let ys = arrayFrom ys + let len2 = ys.Length + let target = allocateArrayFromCons cons (len + len2) + + for i = 0 to (index - 1) do + target.[i] <- xs.[i] + + for i = 0 to (len2 - 1) do + target.[index + i] <- ys.[i] + + for i = index to (len - 1) do + target.[i + len2] <- xs.[i] + + target + +let removeAt (index: int) (xs: 'T[]) : 'T[] = + if index < 0 || index >= xs.Length then + invalidArg "index" SR.indexOutOfBounds + + let mutable i = -1 + + xs + |> filter (fun _ -> + i <- i + 1 + i <> index + ) + +let removeManyAt (index: int) (count: int) (xs: 'T[]) : 'T[] = + let mutable i = -1 + // incomplete -1, in-progress 0, complete 1 + let mutable status = -1 + + let ys = + xs + |> filter (fun _ -> + i <- i + 1 + + if i = index then + status <- 0 + false + elif i > index then + if i < index + count then + false + else + status <- 1 + true + else + true + ) + + let status = + if status = 0 && i + 1 = index + count then + 1 + else + status + + if status < 1 then + // F# always says the wrong parameter is index but the problem may be count + let arg = + if status < 0 then + "index" + else + "count" + + invalidArg arg SR.indexOutOfBounds + + ys + +let updateAt (index: int) (y: 'T) (xs: 'T[]) ([] cons: Cons<'T>) : 'T[] = + let len = xs.Length + + if index < 0 || index >= len then + invalidArg "index" SR.indexOutOfBounds + + let target = allocateArrayFromCons cons len + + for i = 0 to (len - 1) do + target.[i] <- + if i = index then + y + else + xs.[i] + + target diff --git a/src/fable-library-lua/Choice.fs b/src/fable-library-lua/Choice.fs new file mode 100644 index 0000000000..4d6ae677c0 --- /dev/null +++ b/src/fable-library-lua/Choice.fs @@ -0,0 +1,85 @@ +namespace FSharp.Core + +[] +type Result<'T, 'TError> = + | Ok of ResultValue: 'T + | Error of ErrorValue: 'TError + +module Result = + [] + let map (mapping: 'a -> 'b) (result: Result<'a, 'c>) : Result<'b, 'c> = + match result with + | Error e -> Error e + | Ok x -> Ok(mapping x) + + [] + let mapError (mapping: 'a -> 'b) (result: Result<'c, 'a>) : Result<'c, 'b> = + match result with + | Error e -> Error(mapping e) + | Ok x -> Ok x + + [] + let bind (binder: 'a -> Result<'b, 'c>) (result: Result<'a, 'c>) : Result<'b, 'c> = + match result with + | Error e -> Error e + | Ok x -> binder x + +[] +type Choice<'T1, 'T2> = + | Choice1Of2 of 'T1 + | Choice2Of2 of 'T2 + +[] +type Choice<'T1, 'T2, 'T3> = + | Choice1Of3 of 'T1 + | Choice2Of3 of 'T2 + | Choice3Of3 of 'T3 + +[] +type Choice<'T1, 'T2, 'T3, 'T4> = + | Choice1Of4 of 'T1 + | Choice2Of4 of 'T2 + | Choice3Of4 of 'T3 + | Choice4Of4 of 'T4 + +[] +type Choice<'T1, 'T2, 'T3, 'T4, 'T5> = + | Choice1Of5 of 'T1 + | Choice2Of5 of 'T2 + | Choice3Of5 of 'T3 + | Choice4Of5 of 'T4 + | Choice5Of5 of 'T5 + +[] +type Choice<'T1, 'T2, 'T3, 'T4, 'T5, 'T6> = + | Choice1Of6 of 'T1 + | Choice2Of6 of 'T2 + | Choice3Of6 of 'T3 + | Choice4Of6 of 'T4 + | Choice5Of6 of 'T5 + | Choice6Of6 of 'T6 + +[] +type Choice<'T1, 'T2, 'T3, 'T4, 'T5, 'T6, 'T7> = + | Choice1Of7 of 'T1 + | Choice2Of7 of 'T2 + | Choice3Of7 of 'T3 + | Choice4Of7 of 'T4 + | Choice5Of7 of 'T5 + | Choice6Of7 of 'T6 + | Choice7Of7 of 'T7 + +module Choice = + let makeChoice1Of2 (x: 'T1) : Choice<'T1, 'a> = Choice1Of2 x + + let makeChoice2Of2 (x: 'T2) : Choice<'a, 'T2> = Choice2Of2 x + + let tryValueIfChoice1Of2 (x: Choice<'T1, 'T2>) : Option<'T1> = + match x with + | Choice1Of2 x -> Some x + | _ -> None + + let tryValueIfChoice2Of2 (x: Choice<'T1, 'T2>) : Option<'T2> = + match x with + | Choice2Of2 x -> Some x + | _ -> None diff --git a/src/fable-library-lua/fable/Fable.Library.fsproj b/src/fable-library-lua/Fable.Library.fsproj similarity index 87% rename from src/fable-library-lua/fable/Fable.Library.fsproj rename to src/fable-library-lua/Fable.Library.fsproj index 7899f66221..1f84e7125f 100644 --- a/src/fable-library-lua/fable/Fable.Library.fsproj +++ b/src/fable-library-lua/Fable.Library.fsproj @@ -8,7 +8,7 @@ - + @@ -23,8 +23,8 @@ - - + + @@ -36,7 +36,7 @@ - + diff --git a/src/fable-library-lua/Global.fs b/src/fable-library-lua/Global.fs new file mode 100644 index 0000000000..acffeabc1c --- /dev/null +++ b/src/fable-library-lua/Global.fs @@ -0,0 +1,34 @@ +namespace Fable.Core + +type IGenericAdder<'T> = + abstract GetZero: unit -> 'T + abstract Add: 'T * 'T -> 'T + +type IGenericAverager<'T> = + abstract GetZero: unit -> 'T + abstract Add: 'T * 'T -> 'T + abstract DivideByInt: 'T * int -> 'T + +type Symbol_wellknown = + abstract ``Symbol.toStringTag``: string + +type IJsonSerializable = + abstract toJSON: unit -> obj + +namespace global + +[] +module SR = + let indexOutOfBounds = + "The index was outside the range of elements in the collection." + + let inputWasEmpty = "Collection was empty." + let inputMustBeNonNegative = "The input must be non-negative." + let inputSequenceEmpty = "The input sequence was empty." + let inputSequenceTooLong = "The input sequence contains more than one element." + + let keyNotFoundAlt = + "An index satisfying the predicate was not found in the collection." + + let differentLengths = "The collections had different lengths." + let notEnoughElements = "The input sequence has an insufficient number of elements." diff --git a/src/fable-library-lua/fable/Native.fs b/src/fable-library-lua/Native.fs similarity index 100% rename from src/fable-library-lua/fable/Native.fs rename to src/fable-library-lua/Native.fs diff --git a/src/fable-library-lua/fable/Timer.fs b/src/fable-library-lua/Timer.fs similarity index 100% rename from src/fable-library-lua/fable/Timer.fs rename to src/fable-library-lua/Timer.fs diff --git a/src/fable-library-lua/fable/Util.lua b/src/fable-library-lua/Util.lua similarity index 100% rename from src/fable-library-lua/fable/Util.lua rename to src/fable-library-lua/Util.lua From 704c92864d382cba1abb3b65746dbcd01a0ff40b Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Tue, 4 Jun 2024 21:51:19 -0400 Subject: [PATCH 38/41] added reminder to fix quicktest --- src/Fable.Build/Quicktest/Lua.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fable.Build/Quicktest/Lua.fs b/src/Fable.Build/Quicktest/Lua.fs index 949e7f9d1d..171abe509d 100644 --- a/src/Fable.Build/Quicktest/Lua.fs +++ b/src/Fable.Build/Quicktest/Lua.fs @@ -2,7 +2,7 @@ module Build.Quicktest.Lua open Build.FableLibrary open Build.Quicktest.Core - +//TODO: Quicktest still needs to be actually written. let handle (args: string list) = genericQuicktest { From a52c1c4a5ac5b44fc360252d65b6d837f03f1310 Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Thu, 6 Jun 2024 21:03:54 -0400 Subject: [PATCH 39/41] tidying making it more similar to Fable.Expr, ensuring completeness --- src/Fable.Transforms/Lua/Fable2Lua.fs | 163 ++++++++++++--------- src/Fable.Transforms/Lua/LuaPrinter.fs | 7 +- src/fable-library-lua/Fable.Library.fsproj | 2 +- tests/Lua/Fable.Tests.Lua.fsproj | 9 +- 4 files changed, 104 insertions(+), 77 deletions(-) diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index 805130b711..a48dc6a451 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -68,6 +68,7 @@ module Transforms = let fcall args expr = FunctionCall(expr, args) + ///lua's immediately invoked function expressions let iife statements = FunctionCall(AnonymousFunc([], statements), []) @@ -191,6 +192,7 @@ module Transforms = | _ -> sprintf "%A %A" op expr |> Unknown | x -> Unknown(sprintf "%A" x) + ///lua's immediately invoked function expressions let asSingleExprIife (exprs: Expr list) : Expr = //function match exprs with | [] -> NoOp @@ -214,6 +216,7 @@ module Transforms = | [] -> NoOp |> Do | _ -> FunctionCall(AnonymousFunc([], statements), []) |> Return + ///lua's immediately invoked function expressions let asSingleExprIifeTr com : Fable.Expr list -> Expr = List.map (transformExpr com) >> asSingleExprIife @@ -230,20 +233,52 @@ module Transforms = let transformOp = transformOp com match expr with - | Fable.Expr.Value(value, _) -> transformValueKind com value - | Fable.Expr.Call(expr, callInfo, t, r) -> + | Fable.IdentExpr i when i.Name = "" -> Unknown "ident" + | Fable.IdentExpr i -> + Ident + { + Namespace = None + Name = i.Name + } + | Fable.Value(value, _) -> transformValueKind com value + | Fable.Lambda(arg, body, name) -> Function([ arg.Name ], [ transformExpr body |> Return ]) + | Fable.Delegate(idents, body, _, _) -> + Function(idents |> List.map (fun i -> i.Name), [ transformExpr body |> Return |> flattenReturnIifes ]) //can be flattened + | Fable.ObjectExpr(_members, typ, _baseCall) -> Unknown $"Obj %A{typ}" + | Fable.TypeCast(expr, t) -> transformExpr expr //typecasts are meaningless + | Fable.Test(expr, kind, b) -> + match kind with + | Fable.UnionCaseTest i -> Binary(Equals, GetField(transformExpr expr, "tag"), Const(ConstNumber(float i))) + | Fable.OptionTest isSome -> + if isSome then + Binary(Unequal, Const ConstNull, transformExpr expr) + else + Binary(Equals, Const ConstNull, transformExpr expr) + | Fable.TestKind.TypeTest t -> + // match t with + // | Fable.DeclaredType (ent, genArgs) -> + // match ent.FullName with + // | Fable.Transforms.Types.ienumerable -> //isArrayLike + // | Fable.Transforms.Types.array + // | _ -> + // | _ -> () + Binary(Equals, GetField(transformExpr expr, "type"), Const(t.ToString() |> ConstString)) + | _ -> Unknown(sprintf "test %A %A" expr kind) + | Fable.Call(expr, callInfo, t, r) -> let lhs = match expr with - | Fable.Expr.Get(expr, Fable.GetKind.FieldGet info, t, _) -> + | Fable.Get(expr, Fable.GetKind.FieldGet info, t, _) -> match t with | Fable.DeclaredType(_, _) | Fable.AnonymousRecordType(_, _, _) -> GetObjMethod(transformExpr expr, info.Name) | _ -> transformExpr expr - | Fable.Expr.Delegate _ -> transformExpr expr |> Parentheses + | Fable.Delegate _ -> transformExpr expr |> Parentheses | _ -> transformExpr expr FunctionCall(lhs, List.map transformExpr callInfo.Args) - | Fable.Expr.Import(info, t, r) -> + | Fable.CurriedApply(applied, args, _, _) -> FunctionCall(transformExpr applied, args |> List.map transformExpr) + | Fable.Operation(kind, _, _, _) -> transformOp kind + | Fable.Import(info, t, r) -> let path = match info.Kind, info.Path with | libImport, Regex "fable-lib\/(\w+).(?:fs|js)" [ name ] -> "fable-lib/" + name @@ -266,44 +301,22 @@ module Transforms = rcall else GetObjMethod(rcall, info.Selector) - | Fable.Expr.IdentExpr(i) when i.Name <> "" -> - Ident - { - Namespace = None - Name = i.Name - } - | Fable.Expr.Operation(kind, _, _, _) -> transformOp kind - | Fable.Expr.Get(expr, Fable.GetKind.FieldGet info, t, _) -> GetField(transformExpr expr, info.Name) - | Fable.Expr.Get(expr, Fable.GetKind.UnionField info, _, _) -> - GetField(transformExpr expr, sprintf "p_%i" info.FieldIndex) - | Fable.Expr.Get(expr, Fable.GetKind.ExprGet(e), _, _) -> GetAtIndex(transformExpr expr, transformExpr e) - | Fable.Expr.Get(expr, Fable.GetKind.TupleIndex(i), _, _) -> - GetAtIndex(transformExpr expr, Const(ConstNumber(float i))) - | Fable.Expr.Get(expr, Fable.GetKind.OptionValue, _, _) -> transformExpr expr //todo null check, throw if null? - | Fable.Expr.Set(expr, Fable.SetKind.ValueSet, t, value, _) -> SetValue(transformExpr expr, transformExpr value) - | Fable.Expr.Set(expr, Fable.SetKind.ExprSet(e), t, value, _) -> - SetExpr(transformExpr expr, transformExpr e, transformExpr value) - | Fable.Expr.Sequential exprs -> asSingleExprIifeTr com exprs - | Fable.Expr.Let(ident, value, body) -> - let statements = - [ - Assignment([ ident.Name ], transformExpr value, true) - transformExpr body |> Return - ] - Helpers.maybeIife statements - | Fable.Expr.Emit(m, _, _) -> + | Fable.Emit(m, _, _) -> // let argsExprs = m.CallInfo.Args |> List.map transformExpr // let macroExpr = Macro(m.Macro, argsExprs) // let exprs = // argsExprs // @ [macroExpr] // asSingleExprIife exprs - Macro(m.Macro, m.CallInfo.Args |> List.map transformExpr) - | Fable.Expr.DecisionTree(expr, lst) -> + if m.IsStatement then + Macro(m.Macro, m.CallInfo.Args |> List.map transformExpr) + else + Unknown $"Emit %A{m.Macro}" + | Fable.DecisionTree(expr, lst) -> com.DecisionTreeTargets(lst) transformExpr expr - | Fable.Expr.DecisionTreeSuccess(i, boundValues, _) -> + | Fable.DecisionTreeSuccess(i, boundValues, _) -> let idents, target = com.GetDecisionTreeTargets(i) if idents.Length = boundValues.Length then @@ -317,45 +330,42 @@ module Transforms = statements |> Helpers.maybeIife else sprintf "not equal lengths %A %A" idents boundValues |> Unknown - | Fable.Expr.Lambda(arg, body, name) -> Function([ arg.Name ], [ transformExpr body |> Return ]) - | Fable.Expr.CurriedApply(applied, args, _, _) -> - FunctionCall(transformExpr applied, args |> List.map transformExpr) - | Fable.Expr.IfThenElse(guardExpr, thenExpr, elseExpr, _) -> - Ternary(transformExpr guardExpr, transformExpr thenExpr, transformExpr elseExpr) - | Fable.Test(expr, kind, b) -> + | Fable.Let(ident, value, body) -> + let statements = + [ + Assignment([ ident.Name ], transformExpr value, true) + transformExpr body |> Return + ] + + Helpers.maybeIife statements + | Fable.LetRec(ls, m) -> + match ls with + | [] -> Unknown "let rec" + | [ (i, e) ] -> Unknown $"let rec %A{i.Name}" + | (i, e) :: ls -> Unknown $"let rec %A{i.Name}" + | Fable.Get(expr, kind, t, _) -> match kind with - | Fable.UnionCaseTest i -> Binary(Equals, GetField(transformExpr expr, "tag"), Const(ConstNumber(float i))) - | Fable.OptionTest isSome -> - if isSome then - Binary(Unequal, Const ConstNull, transformExpr expr) - else - Binary(Equals, Const ConstNull, transformExpr expr) - | Fable.TestKind.TypeTest t -> - // match t with - // | Fable.DeclaredType (ent, genArgs) -> - // match ent.FullName with - // | Fable.Transforms.Types.ienumerable -> //isArrayLike - // | Fable.Transforms.Types.array - // | _ -> - // | _ -> () - Binary(Equals, GetField(transformExpr expr, "type"), Const(t.ToString() |> ConstString)) - | _ -> Unknown(sprintf "test %A %A" expr kind) - | Fable.Extended(Fable.ExtendedSet.Throw(expr, _), t) -> - let errorExpr = Const(ConstString "There was an error, todo") - //transformExpr expr - FunctionCall(Helpers.ident "error", [ errorExpr ]) - | Fable.Extended(Fable.ExtendedSet.Curry(expr, d), _) -> - transformExpr expr |> sprintf "(Fable2Lua:~266) todo curry %A" |> Unknown - | Fable.Delegate(idents, body, _, _) -> - Function(idents |> List.map (fun i -> i.Name), [ transformExpr body |> Return |> flattenReturnIifes ]) //can be flattened - | Fable.ForLoop(ident, start, limit, body, isUp, _) -> + | Fable.GetKind.FieldGet info -> GetField(transformExpr expr, info.Name) + | Fable.GetKind.UnionField info -> GetField(transformExpr expr, sprintf "p_%i" info.FieldIndex) + | Fable.GetKind.ExprGet e -> GetAtIndex(transformExpr expr, transformExpr e) + | Fable.GetKind.TupleIndex i -> GetAtIndex(transformExpr expr, Const(ConstNumber(float i))) + | Fable.GetKind.OptionValue -> transformExpr expr //todo null check, throw if null? + | Fable.ListHead -> Unknown "list Head" + | Fable.ListTail -> Unknown "list Tail" + | Fable.UnionTag -> Unknown "Union Tag" + | Fable.Set(expr, kind, t, value, _) -> + match kind with + | Fable.SetKind.ValueSet -> SetValue(transformExpr expr, transformExpr value) + | Fable.SetKind.ExprSet e -> SetExpr(transformExpr expr, transformExpr e, transformExpr value) + | Fable.SetKind.FieldSet name -> Unknown $"FieldSet %s{name} of type %A{t}" + | Fable.Sequential exprs -> asSingleExprIifeTr com exprs + | Fable.WhileLoop(guard, body, _label) -> + Helpers.maybeIife [ WhileLoop(transformExpr guard, [ transformExpr body |> Do ]) ] + | Fable.ForLoop(ident, start, limit, body, _isUp, _) -> Helpers.maybeIife [ ForLoop(ident.Name, transformExpr start, transformExpr limit, [ transformExpr body |> Do ]) ] - | Fable.TypeCast(expr, t) -> transformExpr expr //typecasts are meaningless - | Fable.WhileLoop(guard, body, label) -> - Helpers.maybeIife [ WhileLoop(transformExpr guard, [ transformExpr body |> Do ]) ] | Fable.TryCatch(body, catch, finalizer, _) -> Helpers.maybeIife [ @@ -384,7 +394,22 @@ module Transforms = ] ) ] - | x -> Unknown(sprintf "(transform fallthrough) %A" x) + | Fable.IfThenElse(guardExpr, thenExpr, elseExpr, _) -> + Ternary(transformExpr guardExpr, transformExpr thenExpr, transformExpr elseExpr) + | Fable.Unresolved(kind, _typ, _range) -> + match kind with + | Fable.UnresolvedExpr.UnresolvedTraitCall _ -> Unknown "Unresolved Trait" + | Fable.UnresolvedExpr.UnresolvedReplaceCall _ -> Unknown "Unresolved Replace" + | Fable.UnresolvedExpr.UnresolvedInlineCall _ -> Unknown "Unresolved Inline" + | Fable.Extended(kind, t) -> + match kind with + | Fable.ExtendedSet.Throw(expr, typ) -> + let errorExpr = Const(ConstString "There was an error, todo") + FunctionCall(Helpers.ident "error", [ errorExpr ]) + | Fable.ExtendedSet.Debugger -> Unknown "Debugger" + | Fable.ExtendedSet.Curry(e, a) -> + //transformExpr expr |> sprintf "(Fable2Lua:~266) todo curry %A" |> Unknown + Unknown $"Curry (arity: %i{a})" //in rare cases currying may need to happen at runtime let transformDeclarations (com: LuaCompiler) ctx decl = diff --git a/src/Fable.Transforms/Lua/LuaPrinter.fs b/src/Fable.Transforms/Lua/LuaPrinter.fs index 31ba72e646..be0ab3da25 100644 --- a/src/Fable.Transforms/Lua/LuaPrinter.fs +++ b/src/Fable.Transforms/Lua/LuaPrinter.fs @@ -112,6 +112,9 @@ module Output = | Unary(Not, expr) -> write ctx "not " writeExpr ctx expr + | Unary(NotBitwise, expr) -> + write ctx "~" + writeExpr ctx expr | Binary(op, left, right) -> writeExpr ctx left write ctx " " @@ -235,8 +238,8 @@ module Output = write ctx "(" writeExpr ctx expr write ctx ")" - | Unknown x -> writeCommented ctx "unknown" x - | x -> sprintf "%A" x |> writeCommented ctx "todo" + + | Unknown x -> writeCommented ctx "todo: unknown" x and writeExprs ctx = function diff --git a/src/fable-library-lua/Fable.Library.fsproj b/src/fable-library-lua/Fable.Library.fsproj index 1f84e7125f..0858de5d60 100644 --- a/src/fable-library-lua/Fable.Library.fsproj +++ b/src/fable-library-lua/Fable.Library.fsproj @@ -1,7 +1,7 @@  - netstandard2.0 + net8.0 $(DefineConstants);FABLE_COMPILER $(DefineConstants);FX_NO_BIGINT diff --git a/tests/Lua/Fable.Tests.Lua.fsproj b/tests/Lua/Fable.Tests.Lua.fsproj index 6daee57fcc..8997a7ed14 100644 --- a/tests/Lua/Fable.Tests.Lua.fsproj +++ b/tests/Lua/Fable.Tests.Lua.fsproj @@ -1,16 +1,15 @@ - net6.0 + net8.0 false false true preview - - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all From 673fa7ceaa53110378e876d5276b1812391ce9e2 Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Thu, 6 Jun 2024 21:12:26 -0400 Subject: [PATCH 40/41] didn't need to emit unknown for macros --- src/Fable.Transforms/Lua/Fable2Lua.fs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Fable.Transforms/Lua/Fable2Lua.fs b/src/Fable.Transforms/Lua/Fable2Lua.fs index a48dc6a451..8fe92266cd 100644 --- a/src/Fable.Transforms/Lua/Fable2Lua.fs +++ b/src/Fable.Transforms/Lua/Fable2Lua.fs @@ -309,10 +309,7 @@ module Transforms = // argsExprs // @ [macroExpr] // asSingleExprIife exprs - if m.IsStatement then - Macro(m.Macro, m.CallInfo.Args |> List.map transformExpr) - else - Unknown $"Emit %A{m.Macro}" + Macro(m.Macro, m.CallInfo.Args |> List.map transformExpr) | Fable.DecisionTree(expr, lst) -> com.DecisionTreeTargets(lst) transformExpr expr From e07d3d1ded5ed786104712e706bf050a4ea9b847 Mon Sep 17 00:00:00 2001 From: Alan Ball Date: Thu, 6 Jun 2024 22:09:22 -0400 Subject: [PATCH 41/41] making a few more things work --- src/Fable.Core/Fable.Core.LuaInterop.fs | 2 +- src/fable-library-lua/Native.fs | 10 ++++---- src/fable-library-lua/Util.lua | 33 +++++++++++++++++++++++++ tests/Lua/TestControlFlow.fs | 6 ++--- tests/Lua/TestUnionType.fs | 10 ++++---- 5 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/Fable.Core/Fable.Core.LuaInterop.fs b/src/Fable.Core/Fable.Core.LuaInterop.fs index 44b6822126..27c542a49b 100644 --- a/src/Fable.Core/Fable.Core.LuaInterop.fs +++ b/src/Fable.Core/Fable.Core.LuaInterop.fs @@ -38,7 +38,7 @@ module Lua = [] type ArrayConstructor = - [] + [] abstract Create: size: int -> 'T[] [] diff --git a/src/fable-library-lua/Native.fs b/src/fable-library-lua/Native.fs index 3b49a5cb82..dcfcb141a8 100644 --- a/src/fable-library-lua/Native.fs +++ b/src/fable-library-lua/Native.fs @@ -70,16 +70,16 @@ module Helpers = // Typed arrays not supported, only dynamic ones do let inline spliceImpl (array: 'T[]) (start: int) (deleteCount: int) : 'T[] = !! array?splice (start, deleteCount) - [] + [] let reverseImpl (array: 'T[]) : 'T[] = nativeOnly - [] + [] let copyImpl (array: 'T[]) : 'T[] = nativeOnly - [] + [] let skipImpl (array: 'T[]) (count: int) : 'T[] = nativeOnly - - [] + //__TS__ArraySplice + [] let subArrayImpl (array: 'T[]) (start: int) (count: int) : 'T[] = nativeOnly let inline indexOfImpl (array: 'T[]) (item: 'T) (start: int) : int = !! array?indexOf (item, start) diff --git a/src/fable-library-lua/Util.lua b/src/fable-library-lua/Util.lua index ca5ff0521a..6f238be622 100644 --- a/src/fable-library-lua/Util.lua +++ b/src/fable-library-lua/Util.lua @@ -33,6 +33,39 @@ function TableConcat(t1,t2) return t1 end +function table.create(len) + local a = {} + for i = 1, len do + a[i] = 0 + end + return a +end + +function table.slice(tbl, first, last) + local sliced = {} + + for i = first or 1, last or #tbl do + sliced[#sliced+1] = tbl[i] + end + + return sliced +end + +function table.reverse(tbl) + local reved = {} + for i = #tbl, 1, -1 do + reved[#reved+1] = tbl[i] + end + return reved +end + +function table.shallow_copy(t) + local t2 = {} + for k,v in pairs(t) do + t2[k] = v + end + return t2 +end -- https://stackoverflow.com/questions/5977654/how-do-i-use-the-bitwise-operator-xor-in-lua local function BitXOR(a,b)--Bitwise xor local p,c=1,0 diff --git a/tests/Lua/TestControlFlow.fs b/tests/Lua/TestControlFlow.fs index 3713b33899..c3154f0ef4 100644 --- a/tests/Lua/TestControlFlow.fs +++ b/tests/Lua/TestControlFlow.fs @@ -74,13 +74,13 @@ let testExThrow() = let testSimpleFnParam() = let add a b = a + b let fn addFn a b = addFn a b - fn (add) 3 2 |> equal 5 + fn add 3 2 |> equal 5 [] let testPartialApply() = let add a b = a + b let fn addFn a b = addFn a b - fn (add) 3 2 |> equal 5 - let fnAdd4 = fn (add) 4 + fn add 3 2 |> equal 5 + let fnAdd4 = fn add 4 fnAdd4 5 |> equal 9 fnAdd4 2 |> equal 6 \ No newline at end of file diff --git a/tests/Lua/TestUnionType.fs b/tests/Lua/TestUnionType.fs index 9b4be18fc1..ddcd1406fb 100644 --- a/tests/Lua/TestUnionType.fs +++ b/tests/Lua/TestUnionType.fs @@ -2,17 +2,17 @@ module Fable.Tests.UnionTypes open Util.Testing -type Gender = Male | Female +type Shape = Square | Circle [] let testMakeUnion () = - let r = Male - r |> equal Male + let r = Square + r |> equal Square [] let testMakeUnion2 () = - let r = Female - r |> equal Female + let r = Circle + r |> equal Circle type Stuff = | A of string * int