├── .gitignore
├── CHANGELOG.md
├── README.md
├── app
└── Main.hs
├── flake.lock
├── flake.nix
├── psel.cabal
├── src
└── PsEl
│ ├── CommandBuild.hs
│ ├── CommandRun.hs
│ ├── ForeignTemplate.hs
│ ├── PselEl.hs
│ ├── SExp.hs
│ ├── SExpConstructor.hs
│ ├── SExpDisplay.hs
│ ├── SExpOptimize.hs
│ ├── SExpPattern.hs
│ ├── SExpRaw.hs
│ ├── SExpTraverse.hs
│ └── Transpile.hs
├── test-bare.purs
├── .gitignore
├── packages.dhall
├── spago.dhall
└── test
│ ├── Main.el
│ ├── Main.js
│ ├── Main.purs
│ ├── PsEl
│ ├── Record.purs
│ └── TCO.purs
│ ├── Utils.el
│ ├── Utils.js
│ └── Utils.purs
└── test-lib.purs
├── .gitignore
├── spago.dhall
└── test
├── Main.el
└── Main.purs
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist-newstyle/
2 | /result
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Revision history for psel
2 |
3 | ## 0.1.0.0 -- YYYY-mm-dd
4 |
5 | * First version. Released on an unsuspecting world.
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # psel: Elisp(Emacs Lisp) backend for PureScript
2 |
3 | WIP
4 |
5 | ## Motivation
6 |
7 | Elisp is powerful and flexible, but when it comes to complex logic code, some might prefer strongly typed language.
8 | Since it is possible to call PS functions from elisp, it is possible to write partially in PS.
9 | Also, you can reuse PureScript libraries as long as they do not depend on JS/browser specific libraries.
10 |
11 | ## Installation
12 |
13 | Currently, there is no release yet. To install `psel` command, you can build from source or use nix to install.
14 |
15 | ### Build from source
16 |
17 | Requires `cabal-install` and `GHC` 8.10.7.
18 |
19 | git clone git@github.com:psel-org/psel.git
20 | cd psel
21 | cabal install
22 |
23 | ### Install using Nix(flake.nix)
24 |
25 | Requires `nix` >= 2.4.
26 |
27 | nix profile install github:psel-org/psel
28 |
29 | ## Usage
30 |
31 | Psel is intended to use through Spago. Set `backend = "psel"` in your `spago.dhall` file.
32 | There is no package-set release yet. You can use WIP package-set `https://raw.githubusercontent.com/psel-org/package-sets/main/src/el-0.14.5-20211116/packages.dhall` for now.
33 |
34 | ```dhall
35 | { name = "your-project-name"
36 | , dependencies = [ "prelude" ]
37 | , packages = https://raw.githubusercontent.com/psel-org/package-sets/main/src/el-0.14.5-20211116/packages.dhall
38 | , backend = "psel"
39 | , sources = [ "src/**/*.purs", "test/**/*.purs" ]
40 | }
41 | ```
42 |
43 | `spago build` will output all .el files under `output.el` directory. To require from emacs, add this path to `load-path` variable.
44 |
45 | ## Module and Top-level bindings
46 |
47 | `Data.Foo` module will be transpiled to `Data.Foo.el`.
48 | Top-level binding `fooBar` in `Data.Foo` module will be transpiled to `(defvar Data.Foo.fooBar ...)`.
49 |
50 | ## Type
51 |
52 | Purescript | Elisp
53 | -----------|------
54 | Int | Integer
55 | Double | Double
56 | String | String
57 | Array | Vector
58 | Char | Integer(elisp doen't have char type)
59 | Boolean | True -> `t`, False -> `nil` (elisp doesn't have boolean type)
60 | Records | alist (e.g. `(('foo . 1) ('bar . "a"))`)
61 | Unit | nil
62 | Data types | Vector with constructor tag symbol in first slot and arguments in the remaining slots. Constructor with no argument will be represented by tag symbol only.
e.g. `Just 42` -> `['Just 42]`
e.g. `Nothing` -> `'Nothing`
63 | Tuple | `Tuple a b` -> `(cons a b)`
64 | List | `Cons 1 (Cons 2 Nil))` -> `(list 1 2)`
65 |
66 | ## Optimization
67 |
68 | ### TCO(Tail-Call Optimization)
69 |
70 | Currently, TCO is only applied to certain forms of self-recursion functions.
71 | TCO will convert these self-resursive calls to `while` s-exp expression.
72 |
73 | ### MagicDo
74 |
75 | Implemented only for `Effect` monad.
76 | `ST` monad remains unoptimized. Also `whileE` and `forE` combinator of `Effect` monad is also untouched.
77 |
78 | ### Uncurried types
79 |
80 | Not done yet.
81 |
82 | `Data.Function.Uncurried(funcitons package)` and `Effect.Uncurried(effects pacakge)`
83 |
84 | ## TODO
85 |
86 | * [x] Support FFI
87 | * [x] Prelude fork
88 | * [x] Make an github orgiznation
89 | * [x] Make psel command spago friendly
90 | * [x] Create package-set
91 | * [x] Implement TCO
92 | * [x] Implement MagicDo
93 | * [ ] Support core libraries([WIP](https://github.com/psel-org/package-sets))
94 | * [ ] Minimal example and add Usage document
95 | * [ ] purescript-emacs
96 | * [ ] Support contrib libraries
97 | * [x] Write some tests
98 | * [x] Add flake.nix
99 | * [ ] Setup CI
100 |
101 | ## References
102 |
103 | Learned how to write a CoreFn-type PureScript backend from [purenix](https://github.com/purenix-org/purenix) and [purerl](https://github.com/purerl/purerl).
104 |
--------------------------------------------------------------------------------
/app/Main.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE DeriveGeneric #-}
2 | {-# LANGUAGE DuplicateRecordFields #-}
3 | {-# LANGUAGE NamedFieldPuns #-}
4 | {-# LANGUAGE OverloadedStrings #-}
5 |
6 | module Main where
7 |
8 | import Options.Generic (ParseRecord (parseRecord), getRecord, lispCaseModifiers, parseRecordWithModifiers)
9 | import PsEl.CommandBuild qualified as CommandBuild
10 | import PsEl.CommandRun qualified as CommandRun
11 | import RIO
12 | import System.Environment (getArgs)
13 |
14 | data Option = Option
15 | { generateMissingFfi :: Bool
16 | , run :: Maybe Text
17 | }
18 | deriving (Generic, Show)
19 |
20 | instance ParseRecord Option where
21 | parseRecord = parseRecordWithModifiers lispCaseModifiers
22 |
23 | -- spagoは一つのコマンドで複数回バックエンドコマンドを起動する
24 | -- 例えばspago test はビルドを意味する無引数で実行した後,
25 | -- --run Test.Main.main コマンドでテストを実行する。
26 | -- ["--run","Test.Main.main"]
27 | main :: IO ()
28 | main = do
29 | Option{generateMissingFfi, run} <- getRecord "psel"
30 | runSimpleApp $ do
31 | case run of
32 | Just mainFunc -> do
33 | CommandRun.run mainFunc
34 | Nothing -> do
35 | let config =
36 | CommandBuild.defaultConfig
37 | { CommandBuild.generateMissingFfi = generateMissingFfi
38 | }
39 | liftIO $ CommandBuild.build config
40 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "locked": {
5 | "lastModified": 1638122382,
6 | "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=",
7 | "owner": "numtide",
8 | "repo": "flake-utils",
9 | "rev": "74f7e4319258e287b0f9cb95426c9853b282730b",
10 | "type": "github"
11 | },
12 | "original": {
13 | "owner": "numtide",
14 | "repo": "flake-utils",
15 | "type": "github"
16 | }
17 | },
18 | "nixpkgs": {
19 | "locked": {
20 | "lastModified": 1639320937,
21 | "narHash": "sha256-T/phSkh/rsnRpihiiUSa3QcoOGE9kKgFjA2tcZ2dm58=",
22 | "owner": "NixOS",
23 | "repo": "nixpkgs",
24 | "rev": "b243326a0280807950e554821532929a07dbfd54",
25 | "type": "github"
26 | },
27 | "original": {
28 | "owner": "NixOS",
29 | "repo": "nixpkgs",
30 | "type": "github"
31 | }
32 | },
33 | "root": {
34 | "inputs": {
35 | "flake-utils": "flake-utils",
36 | "nixpkgs": "nixpkgs"
37 | }
38 | }
39 | },
40 | "root": "root",
41 | "version": 7
42 | }
43 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2021 Serokell
2 | #
3 | # SPDX-License-Identifier: CC0-1.0
4 |
5 | {
6 | description = "psel";
7 |
8 | inputs = {
9 | nixpkgs.url = "github:NixOS/nixpkgs";
10 | flake-utils.url = "github:numtide/flake-utils";
11 | };
12 |
13 | outputs = { self, nixpkgs, flake-utils }:
14 | flake-utils.lib.eachDefaultSystem (system:
15 | let
16 | pkgs = nixpkgs.legacyPackages.${system};
17 |
18 | haskellPackages = pkgs.haskell.packages.ghc8107;
19 |
20 | jailbreakUnbreak = pkg:
21 | pkgs.haskell.lib.doJailbreak (pkg.overrideAttrs (_: { meta = { }; }));
22 |
23 | packageName = "psel";
24 | in {
25 | packages.${packageName} =
26 | haskellPackages.callCabal2nix packageName self rec {
27 | # Dependency overrides go here
28 | };
29 |
30 | defaultPackage = self.packages.${system}.${packageName};
31 |
32 | devShell = pkgs.mkShell {
33 | buildInputs = [];
34 | inputsFrom = builtins.attrValues self.packages.${system};
35 | };
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/psel.cabal:
--------------------------------------------------------------------------------
1 | cabal-version: 2.4
2 | name: psel
3 | version: 0.1.0.0
4 |
5 | -- A short (one-line) description of the package.
6 | -- synopsis:
7 |
8 | -- A longer description of the package.
9 | -- description:
10 |
11 | -- A URL where users can report bugs.
12 | -- bug-reports:
13 |
14 | -- The license under which the package is released.
15 | -- license:
16 | author: kamoii
17 | maintainer: kamoii
18 |
19 | -- A copyright notice.
20 | -- copyright:
21 | -- category:
22 | extra-source-files:
23 | CHANGELOG.md
24 | README.md
25 |
26 | library
27 | exposed-modules:
28 | PsEl.CommandBuild
29 | PsEl.CommandRun
30 | PsEl.ForeignTemplate
31 | PsEl.PselEl
32 | PsEl.SExp
33 | PsEl.SExpConstructor
34 | PsEl.SExpDisplay
35 | PsEl.SExpOptimize
36 | PsEl.SExpPattern
37 | PsEl.SExpRaw
38 | PsEl.SExpTraverse
39 | PsEl.Transpile
40 |
41 | -- Modules included in this library but not exported.
42 | other-modules:
43 |
44 | -- LANGUAGE extensions used by modules in this package.
45 | -- other-extensions:
46 | build-depends:
47 | , aeson
48 | , base ^>=4.14.3.0
49 | , generic-lens
50 | , lens
51 | , neat-interpolation
52 | , purescript ^>=0.14.4
53 | , purescript-cst
54 | , recursion-schemes
55 | , reflection
56 | , regex
57 | , rio
58 |
59 | hs-source-dirs: src
60 | default-language: Haskell2010
61 | default-extensions:
62 | NoImplicitPrelude
63 | ImportQualifiedPost
64 |
65 | executable psel
66 | main-is: Main.hs
67 |
68 | -- Modules included in this executable, other than Main.
69 | -- other-modules:
70 |
71 | -- LANGUAGE extensions used by modules in this package.
72 | -- other-extensions:
73 | build-depends:
74 | , base ^>=4.14.3.0
75 | , optparse-applicative
76 | , optparse-generic
77 | , psel
78 | , rio
79 |
80 | hs-source-dirs: app
81 | default-language: Haskell2010
82 | default-extensions:
83 | NoImplicitPrelude
84 | ImportQualifiedPost
85 |
--------------------------------------------------------------------------------
/src/PsEl/CommandBuild.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE DuplicateRecordFields #-}
2 | {-# LANGUAGE NamedFieldPuns #-}
3 | {-# LANGUAGE OverloadedStrings #-}
4 | {-# LANGUAGE NoImplicitPrelude #-}
5 |
6 | module PsEl.CommandBuild (
7 | build,
8 | Config (..),
9 | defaultConfig,
10 | ) where
11 |
12 | import Data.Aeson qualified as Aeson
13 | import Data.Aeson.Types (parseEither)
14 | import Language.PureScript (ModuleName (ModuleName))
15 | import Language.PureScript.CoreFn qualified as P
16 | import Language.PureScript.CoreFn.FromJSON (moduleFromJSON)
17 | import PsEl.ForeignTemplate (foreignTemplate)
18 | import PsEl.PselEl (pselEl)
19 | import PsEl.SExp (Feature (..), Symbol, featureFileName)
20 | import PsEl.SExpOptimize (optimize)
21 | import PsEl.SExpDisplay (displayFeature, displayString)
22 | import PsEl.Transpile (ffiFeatureSuffix, pselFeature, transpile)
23 | import RIO
24 | import RIO.Directory qualified as Dir
25 | import RIO.FilePath ((>))
26 | import RIO.FilePath qualified as FP
27 | import RIO.List (intersperse, sort)
28 | import RIO.Text (justifyLeft, pack, unpack)
29 | import RIO.Text qualified as T
30 | import System.Exit qualified as Sys
31 | import System.IO (hPutStrLn, putStrLn)
32 |
33 | build :: Config -> IO ()
34 | build Config{generateMissingFfi} = do
35 | let workdir = "."
36 | let moduleRoot = workdir > "output"
37 | let elispRoot = workdir > "output.el"
38 | whenM (Dir.doesDirectoryExist elispRoot) $ Dir.removeDirectoryRecursive elispRoot
39 | Dir.createDirectory elispRoot
40 | writeFileUtf8 (elispRoot > featureFileName pselFeature) (pselEl pselFeature)
41 | moduleDirs <- filter (/= "cache-db.json") <$> Dir.listDirectory moduleRoot
42 | warngings' <- forM moduleDirs $ \rel -> do
43 | let coreFnPath = moduleRoot > rel > "corefn.json"
44 | value <- Aeson.eitherDecodeFileStrict coreFnPath >>= either Sys.die pure
45 | (_version, module') <- either Sys.die pure $ parseEither moduleFromJSON value
46 | handleModule elispRoot module'
47 | let warnings = mconcat warngings'
48 | printWarnings warnings
49 | when generateMissingFfi
50 | . doGenerateMissingForeignFiles
51 | . snd
52 | $ partitionEithers warnings
53 |
54 | data Config = Config
55 | { generateMissingFfi :: Bool
56 | }
57 |
58 | defaultConfig :: Config
59 | defaultConfig =
60 | Config
61 | { generateMissingFfi = False
62 | }
63 |
64 | -- (1)
65 | -- ForeignFileが(provide)を行なっている場合警告を出す必要があるかも。
66 | -- また必要なFFI定義を
67 | handleModule :: FilePath -> P.Module P.Ann -> IO [Warning]
68 | handleModule elispRoot module'@P.Module{P.moduleName, P.modulePath} = do
69 | let feature = optimize $ transpile module'
70 | let Feature{name, requireFFI} = feature
71 | let targetPath = elispRoot > featureFileName name
72 | let foreignSourcePath = FP.replaceExtension modulePath "el"
73 | writeFileUtf8Builder targetPath (displayFeature feature)
74 | hasForeignSource <- Dir.doesFileExist foreignSourcePath
75 | case (hasForeignSource, requireFFI) of
76 | (True, Just (ffiName, _foreignSymbols)) -> do
77 | let foreignTargetPath = elispRoot > featureFileName ffiName
78 | Dir.copyFile foreignSourcePath foreignTargetPath -- (1)
79 | pure []
80 | (True, Nothing) ->
81 | pure [Left UnneededFFIFileWarning{moduleName, modulePath}]
82 | (False, Just (ffiName, foreignSymbols)) -> do
83 | pure
84 | [ Right
85 | MissingFFIFileWarning
86 | { moduleName
87 | , modulePath
88 | , foreignSourcePath
89 | , foreignSymbols
90 | }
91 | ]
92 | (False, Nothing) ->
93 | pure []
94 |
95 | -- PSコンパイラはpackage-awareではない。そのため当然corefnにはモジュールがどのパッ
96 | -- ケージのどのバージョンのものかの情報は含まれていない。ただspago使っている場合,
97 | -- PSのソースコードのパスでパッケージ名は推測できる。
98 | --
99 | -- 例えばspagoでpreludeをコンパイルした場合,Data.Eqモジュールは次のmodulePathを持つ。
100 | --
101 | -- e.g. "modulePath":".spago/prelude/master/src/Data/Eq.purs"
102 | --
103 | guessPackageByModulePath :: FilePath -> Maybe Text
104 | guessPackageByModulePath path = do
105 | path' <- T.stripPrefix ".spago/" (pack path)
106 | let pkg = T.takeWhile (/= '/') path'
107 | guard $ not (T.null pkg)
108 | pure pkg
109 |
110 | -- 依存ではなく現在のパッケージの
111 | guessIsCurrentPackageByModulePath :: FilePath -> Bool
112 | guessIsCurrentPackageByModulePath path =
113 | let path' = pack path
114 | in T.isPrefixOf "src/" path' || T.isPrefixOf "test/" path'
115 |
116 | printWarnings :: [Warning] -> IO ()
117 | printWarnings warnings = do
118 | let (unneeds, missings) = partitionEithers warnings
119 | unless (null unneeds) $ putStderrLn $ displayUnneedWarngins unneeds
120 | unless (null missings) $ putStderrLn $ displayMissingWarngins missings
121 |
122 | displayUnneedWarngins :: [UnneededFFIFileWarning] -> Utf8Builder
123 | displayUnneedWarngins [] =
124 | mempty
125 | displayUnneedWarngins warnings =
126 | mconcat
127 | . intersperse "\n"
128 | $ mconcat [header, [""], modules, [""]]
129 | where
130 | header =
131 | [ "!!! WARNING !!!"
132 | , "These modules contains FFI file but does not use any FFI."
133 | , "These FFI files are ignored, so no worry, but this could be smell of a bug."
134 | ]
135 |
136 | modules = map display' warnings
137 |
138 | display' UnneededFFIFileWarning{moduleName = ModuleName mn} =
139 | display mn
140 |
141 | displayMissingWarngins :: [MissingFFIFileWarning] -> Utf8Builder
142 | displayMissingWarngins [] =
143 | mempty
144 | displayMissingWarngins warnings =
145 | mconcat
146 | . intersperse "\n"
147 | $ mconcat [header, [""], modules, [""]]
148 | where
149 | header =
150 | [ "!!! WARNING !!!"
151 | , "These modules uses FFI but missing corresponding FFI file."
152 | , "If you require these module it will fail try requrieing its FFI file."
153 | ]
154 |
155 | modules =
156 | map display $ sort $ map displayText' warnings
157 |
158 | displayText' MissingFFIFileWarning{moduleName = ModuleName mn, modulePath} =
159 | displayColumns
160 | 24
161 | [ "Package: " <> fromMaybe "--" (guessPackageByModulePath modulePath)
162 | , "Module: " <> mn
163 | ]
164 |
165 | --
166 | doGenerateMissingForeignFiles :: [MissingFFIFileWarning] -> IO ()
167 | doGenerateMissingForeignFiles ws =
168 | go $
169 | filter
170 | ( \MissingFFIFileWarning{modulePath} ->
171 | guessIsCurrentPackageByModulePath modulePath
172 | )
173 | ws
174 | where
175 | go [] =
176 | putStdoutLn "No missing foreign file."
177 | go ws = do
178 | putStdoutLn "Generating missing foreign files.\n"
179 | forM_ ws $ \MissingFFIFileWarning{foreignSourcePath, foreignSymbols} -> do
180 | let template = foreignTemplate foreignSymbols
181 | writeFileUtf8Builder foreignSourcePath template
182 | putStdoutLn $ display (pack foreignSourcePath)
183 | putStdoutLn "\nNote: You don't need to provide feature(e.g. (provide 'Data.Eq)) in a foreign file."
184 | putStdoutLn "Foreign files will be copied with a diffirent file name."
185 |
186 | putStderrLn :: Utf8Builder -> IO ()
187 | putStderrLn ub = hPutBuilder stderr . getUtf8Builder $ ub <> "\n"
188 |
189 | putStdoutLn :: Utf8Builder -> IO ()
190 | putStdoutLn ub = hPutBuilder stdout . getUtf8Builder $ ub <> "\n"
191 |
192 | displayColumns :: Int -> [Text] -> Text
193 | displayColumns _ [] = mempty
194 | displayColumns width vs =
195 | mconcat $ mapBut1 (justifyLeft width ' ' . (<> ",")) vs
196 | where
197 | mapBut1 :: (a -> a) -> [a] -> [a]
198 | mapBut1 f [] = []
199 | mapBut1 f [x] = [x]
200 | mapBut1 f (x : xs) = f x : mapBut1 f xs
201 |
202 | type Warning =
203 | Either UnneededFFIFileWarning MissingFFIFileWarning
204 |
205 | data UnneededFFIFileWarning = UnneededFFIFileWarning
206 | { moduleName :: ModuleName
207 | , modulePath :: FilePath
208 | }
209 |
210 | data MissingFFIFileWarning = MissingFFIFileWarning
211 | { moduleName :: ModuleName
212 | , modulePath :: FilePath
213 | , foreignSourcePath :: FilePath
214 | , foreignSymbols :: [Symbol]
215 | }
216 |
--------------------------------------------------------------------------------
/src/PsEl/CommandRun.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE FlexibleInstances #-}
2 | {-# LANGUAGE OverloadedStrings #-}
3 |
4 | module PsEl.CommandRun (run) where
5 |
6 | import Data.Coerce (Coercible, coerce)
7 | import Language.PureScript.CST (ParserState (ParserState), lex, parseQualIdentP, runParser)
8 | import Language.PureScript.CST.Types (Ident (getIdent), QualifiedName (QualifiedName, qualModule, qualName))
9 | import Language.PureScript.Names (Ident (Ident))
10 | import PsEl.SExp (funcall0, symbol)
11 | import PsEl.SExpConstructor qualified as C
12 | import PsEl.SExpDisplay (displaySExp)
13 | import PsEl.Transpile qualified as Trp
14 | import RIO
15 | import RIO.Lens (_1, _2)
16 | import RIO.Process (HasProcessContext (processContextL), ProcessContext, mkDefaultProcessContext, proc, runProcess_)
17 | import RIO.Text (unpack)
18 |
19 | -- parseQualIdentP :: Parser (QualifiedName Ident) を
20 | -- e.g. mainFunc = Test.Main.main
21 | -- emacsに渡す引数は次のようになる。
22 | --
23 | -- emacs --batch --directory output.el --eval "(require 'Test.Main)" --eval "(funcall Test.Main.main)"
24 | --
25 | -- デバッグしたい場合は --funcall toggle-debug-on-error を --evalの前に入れれば良い
26 | run :: HasLogFunc env => Text -> RIO env ()
27 | run mainFunc = do
28 | pc <- mkDefaultProcessContext
29 | env <- ask
30 | runRIO (pc, env) $ do
31 | case snd (parseMainFunc mainFunc) of
32 | Right QualifiedName{qualModule = Just mn, qualName = ident} -> do
33 | let featureName = Trp.featureName mn
34 | let mainFuncSym = symbol $ Trp.globalVar mn (Ident (getIdent ident))
35 | let args =
36 | mconcat
37 | [ ["--batch"]
38 | , ["--directory", "output.el"]
39 | , ["--eval", showSExp $ C.require featureName]
40 | , ["--eval", showSExp $ funcall0 mainFuncSym]
41 | ]
42 | proc "emacs" args runProcess_
43 | Right _ ->
44 | logError "Unexpected"
45 | Left ne ->
46 | logError "Unexpected"
47 | where
48 | parseMainFunc t =
49 | runParser (ParserState (lex t) [] []) parseQualIdentP
50 |
51 | showSExp =
52 | unpack . utf8BuilderToText . displaySExp
53 |
54 | -- newtype Ctx e = Ctx (ProcessContext, e)
55 | -- orphan intance にはならない???
56 |
57 | instance HasProcessContext (ProcessContext, e) where
58 | processContextL = _1
59 |
60 | instance HasLogFunc e => HasLogFunc (a, e) where
61 | logFuncL = _2 . logFuncL
62 |
--------------------------------------------------------------------------------
/src/PsEl/ForeignTemplate.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 |
3 | module PsEl.ForeignTemplate where
4 |
5 | import PsEl.SExp (DefVar (DefVar, definition, name), SExp (SExp), Symbol, symbol)
6 | import PsEl.SExpDisplay qualified as Display
7 | import RIO
8 | import RIO.List (intersperse)
9 |
10 | -- 必要なforeign識別子からFFIファイルのテンプレートを作成。
11 | foreignTemplate :: [Symbol] -> Utf8Builder
12 | foreignTemplate symbols =
13 | mconcat . intersperse "\n\n" $
14 | mconcat
15 | [ headLines
16 | , defVarLines
17 | ]
18 | where
19 | headLines =
20 | [ ";; -*- lexical-binding: t; -*-"
21 | ]
22 |
23 | defVarLines =
24 | map Display.displayDefVar templateDefVars
25 |
26 | templateDefVars =
27 | map (\s -> DefVar{name = s, definition = placehold}) symbols
28 |
29 | -- わざと束縛されていないシンボルを使うことでrequire時に実装漏れで例外投げるように
30 | placehold =
31 | symbol "foreign-not-implemeneted"
32 |
--------------------------------------------------------------------------------
/src/PsEl/PselEl.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE QuasiQuotes #-}
2 |
3 | module PsEl.PselEl where
4 |
5 | import NeatInterpolation (trimming)
6 | import PsEl.SExp (FeatureName (FeatureName))
7 | import PsEl.SExpDisplay (displaySymbol)
8 | import RIO
9 |
10 | -- 生成するelで足りない関数を定義。全てのモジュールelよりrequireされる必要あり。
11 | -- emacs標準で同等機能が提供されている場合は乗り換える。
12 | -- psel/alist-set はオブジェクト更新時に使う。
13 | --
14 | -- ???複数の output.el がload-path上にある場合どうする?
15 | pselEl :: FeatureName -> Text
16 | pselEl (FeatureName sym) =
17 | let feature = utf8BuilderToText $ displaySymbol sym
18 | in [trimming|
19 | ;; psel.el -*- lexical-binding: t; -*-
20 |
21 | (require 'seq)
22 |
23 | ;; Exception
24 |
25 | (defun psel/unrecoverable-error (&rest args)
26 | (signal 'psel/unrecoverable-error args))
27 |
28 | (defun psel/assert-error (msg)
29 | (signal 'psel/assert-error (list msg)))
30 |
31 | ;; Alist
32 |
33 | (defun psel/alist-get (field alist)
34 | (let ((c (assq field alist)))
35 | (if (null c)
36 | (psel/unrecoverable-error)
37 | (cdr c))))
38 |
39 | (defun psel/alist-set (field val alist)
40 | "Update the first cons with car eq to field in a immutable way."
41 | (cond ((null alist)
42 | (psel/unrecoverable-error))
43 | ((eq (caar alist) field)
44 | (cons (cons field val) (cdr alist)))
45 | (t
46 | (cons (car alist) (psel/alist-set field val (cdr alist))))))
47 |
48 | (defun psel/alist-set-or-insert (field val alist)
49 | "Update the first appearnce or insert a new field in an immutable way."
50 | (cond ((null alist)
51 | (cons (cons field val) nil))
52 | ((eq (caar alist) field)
53 | (cons (cons field val) (cdr alist)))
54 | (t
55 | (cons (car alist) (psel/alist-set-or-insert field val (cdr alist))))))
56 |
57 | (defun psel/alist-delete (field alist)
58 | "Delete the first cons with car eq to field in a immutable way."
59 | (cond ((null alist)
60 | (psel/unrecoverable-error))
61 | ((eq (caar alist) field)
62 | (cdr alist))
63 | (t
64 | (cons (car alist) (psel/alist-delete field (cdr alist))))))
65 |
66 | (defun psel/alist-equal (a b)
67 | "Alist equality. Key order doesn't matter."
68 | (let ((keys (mapcar 'car a)))
69 | (and (eq (length keys) (length b))
70 | (equal a (mapcar (lambda (key) (assq key b)) keys)))))
71 |
72 | ;; funcall
73 |
74 | (defmacro psel/funcall (f &rest args)
75 | "funcall 1-arg function"
76 | (seq-reduce (lambda (b a) `(funcall ,b ,a)) args f))
77 |
78 | (provide '$feature)
79 | |]
80 |
--------------------------------------------------------------------------------
/src/PsEl/SExp.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE DeriveGeneric #-}
2 | {-# LANGUAGE DeriveTraversable #-}
3 | {-# LANGUAGE DuplicateRecordFields #-}
4 | {-# LANGUAGE GeneralizedNewtypeDeriving #-}
5 | {-# LANGUAGE OverloadedStrings #-}
6 | {-# LANGUAGE TypeFamilies #-}
7 |
8 | module PsEl.SExp where
9 |
10 | import Data.Functor.Foldable qualified as RS
11 | import RIO
12 | import RIO.Lens (_1, _2)
13 | import RIO.NonEmpty qualified as NonEmpty
14 | import RIO.Text (unpack)
15 |
16 | -- Fix Point
17 | newtype SExp = SExp {unSExpr :: SExpF SExp}
18 | deriving (Generic)
19 |
20 | type instance RS.Base SExp = SExpF
21 | instance RS.Recursive SExp where
22 | project = unSExpr
23 |
24 | -- Expression Functor
25 | -- 制御構造(let*,letrec,cond,pcase)は List でも表現できるが,
26 | -- SExpに対して最適化をかける際に解析する必要があるため別途コンストラクタで表現。
27 | -- Listは関数呼出しもしくはリテラル時のみ使う。
28 | --
29 | -- S式とCoreFnの間の中間言語を目指す?
30 | -- 一般的なS式までトランスパイル時に落してしまうと情報も落ちてしまって最適化がしづらくなる。
31 | -- 例えば Quote/Backquote/Comm。自由変数の参照を取得する場合,quote中のシンボルは無視する必要がある。
32 | -- またbackquoteがネストされた場合,そのコンテキストも考慮する必要がある。
33 | -- Quote/Backqoute自体は限定的に使われているので,より情報を残した形で
34 | -- SExpFから除くというのが正しい気もするが
35 | -- Quote もSymbolにしか適用されていないので Quote e よりは QuotedSymbol Symbol として方が
36 | --
37 | -- ただ懸念としてはあまり上げすぎるとCoreFnと同じレベルになってしまう。
38 | --
39 | -- Lambda0/FunCall0はMagicDo実装のために導入
40 | data SExpF e
41 | = Integer Integer
42 | | Double Double
43 | | String Text
44 | | Character Char
45 | | Symbol Symbol
46 | | MkAlist [(Symbol, e)]
47 | | Progn [e]
48 | | If e e e
49 | | Cond [(e, e)]
50 | | Let LetType [(Symbol, e)] e
51 | | Pcase [e] [PcaseAlt e]
52 | | -- | 1引数関数
53 | Lambda1 Symbol e
54 | | -- | 1引数関数の呼出し
55 | -- e.g. FunCall1 f a -> (funcall f a)
56 | FunCall1 e e
57 | | -- | 任意引数関数の呼出し
58 | -- Data.Function.UncurriedのFn2などの呼出しを最適化するに利用する。
59 | -- FunCall{0,1}も包含しているが,基本上記の最適化以外では利用しない。
60 | -- e.g. FunCallN f [a0, .., an] -> (funcall f a0 a1 ... an)
61 | FunCallN e [e]
62 | | -- | 任意引数のネイティブ関数の呼出し
63 | -- e.g. FunCallNative "foo" [a, b] -> (foo a b)
64 | FunCallNative Symbol [e]
65 | | -- | e.g. 'foo
66 | QuotedSymbol Symbol
67 | | Lambda0 e
68 | | FunCall0 e
69 | deriving (Functor, Foldable, Traversable, Generic)
70 |
71 | data LetType
72 | = LetStar
73 | | LetRec
74 | deriving (Eq, Ord)
75 |
76 | data PcaseAlt e = PcaseAlt
77 | { patterns :: [PPattern e]
78 | , guard :: Maybe e
79 | , code :: e
80 | }
81 | deriving (Functor, Foldable, Traversable)
82 |
83 | -- pcase pattern
84 | -- 相互再帰の recursion scheme は面倒なのでこちらは普通に定義。
85 | -- guard用のconstructerを用意していないのは, guardを使うのは最も外側のみのため。
86 | -- コンストラクタを減らしたほうが解析しやずいはず。
87 | data PPattern e
88 | = PAny
89 | | PInteger Integer
90 | | PString Text
91 | | PCharacter Char
92 | | -- | 定数の意味でのシンボル(束縛せず)
93 | PSymbol Symbol
94 | | PBind Symbol
95 | | PBackquotedList [PPattern e]
96 | | PBackquotedVector [PPattern e]
97 | | PBackquotedCons (PPattern e) (PPattern e)
98 | | PAnd [PPattern e]
99 | | PPred e
100 | | -- | Boolは特別扱い(最適化でif文に変更しうるので)
101 | -- 本来は以下のようにしていた
102 | -- list [symbol "pred", bool (symbol "null") (symbol "identity") b]
103 | PPredBool Bool
104 | | PApp e (PPattern e)
105 | deriving (Functor, Foldable, Traversable, Generic)
106 |
107 | -- Unsafe prefixは任意のテキストが妥当なelispシンボルにならないことを示している。
108 | newtype Symbol = UnsafeSymbol {symbolText :: Text}
109 | deriving (Eq, Ord, IsString)
110 |
111 | integer :: Integer -> SExp
112 | integer = SExp . Integer
113 |
114 | double :: Double -> SExp
115 | double = SExp . Double
116 |
117 | string :: Text -> SExp
118 | string = SExp . String
119 |
120 | character :: Char -> SExp
121 | character = SExp . Character
122 |
123 | symbol :: Symbol -> SExp
124 | symbol = SExp . Symbol
125 |
126 | progn :: [SExp] -> SExp
127 | progn = SExp . Progn
128 |
129 | quotedSymbol :: Symbol -> SExp
130 | quotedSymbol = SExp . QuotedSymbol
131 |
132 | -- association list(e.g. `((foo . ,v) (bar . 2))
133 | -- don't need comma
134 | alist :: [(Symbol, SExp)] -> SExp
135 | alist = SExp . MkAlist
136 |
137 | if' :: SExp -> SExp -> SExp -> SExp
138 | if' c t e = SExp $ If c t e
139 |
140 | cond :: [(SExp, SExp)] -> SExp
141 | cond = SExp . Cond
142 |
143 | letStar :: [(Symbol, SExp)] -> SExp -> SExp
144 | letStar bindings body = SExp $ Let LetStar bindings body
145 |
146 | letRec :: [(Symbol, SExp)] -> SExp -> SExp
147 | letRec bindings body = SExp $ Let LetRec bindings body
148 |
149 | pcase :: [SExp] -> [PcaseAlt SExp] -> SExp
150 | pcase exps cases = SExp $ Pcase exps cases
151 |
152 | lambda1 :: Symbol -> SExp -> SExp
153 | lambda1 arg body = SExp $ Lambda1 arg body
154 |
155 | -- e.g. (lambda (a) (lambda (b) ...))
156 | lambda1Fold :: NonEmpty Symbol -> SExp -> SExp
157 | lambda1Fold args body =
158 | let a0 :| as = NonEmpty.reverse args
159 | in foldl' (flip lambda1) (lambda1 a0 body) as
160 |
161 | funcall1 :: SExp -> SExp -> SExp
162 | funcall1 f arg = SExp $ FunCall1 f arg
163 |
164 | funcallN :: SExp -> [SExp] -> SExp
165 | funcallN f args = SExp $ FunCallN f args
166 |
167 | lambda0 :: SExp -> SExp
168 | lambda0 = SExp . Lambda0
169 |
170 | funcall0 :: SExp -> SExp
171 | funcall0 = SExp . FunCall0
172 |
173 | funcallNative :: Symbol -> [SExp] -> SExp
174 | funcallNative f args = SExp $ FunCallNative f args
175 |
176 | --
177 | data DefVar = DefVar
178 | { name :: Symbol
179 | , definition :: SExp
180 | }
181 |
182 | newtype FeatureName = FeatureName Symbol
183 |
184 | -- Feature (Emacs requirable file)
185 | data Feature = Feature
186 | { name :: FeatureName
187 | , requires :: [FeatureName]
188 | , requireFFI :: Maybe (FeatureName, [Symbol])
189 | , defVars :: [DefVar]
190 | }
191 |
192 | featureFileName :: FeatureName -> String
193 | featureFileName (FeatureName (UnsafeSymbol s)) =
194 | unpack s <> ".el"
195 |
--------------------------------------------------------------------------------
/src/PsEl/SExpConstructor.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 |
3 | module PsEl.SExpConstructor where
4 |
5 | import PsEl.SExp
6 | import RIO
7 |
8 | nil :: SExp
9 | nil = symbol "nil"
10 |
11 | t :: SExp
12 | t = symbol "t"
13 |
14 | require :: FeatureName -> SExp
15 | require (FeatureName name) =
16 | funcallNative "require" [quotedSymbol name]
17 |
18 | provide :: FeatureName -> SExp
19 | provide (FeatureName name) =
20 | funcallNative "provide" [quotedSymbol name]
21 |
22 |
--------------------------------------------------------------------------------
/src/PsEl/SExpDisplay.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE DuplicateRecordFields #-}
2 | {-# LANGUAGE LambdaCase #-}
3 | {-# LANGUAGE NamedFieldPuns #-}
4 | {-# LANGUAGE OverloadedStrings #-}
5 | {-# LANGUAGE QuasiQuotes #-}
6 |
7 | module PsEl.SExpDisplay where
8 |
9 | import Data.Functor.Foldable (cata, para)
10 | import Data.List (intersperse)
11 | import PsEl.SExp
12 | import PsEl.SExpConstructor qualified as C
13 | import PsEl.SExpRaw qualified as Raw
14 | import RIO hiding (bracket)
15 | import RIO.Lens (_1, _2)
16 | import RIO.Text.Partial qualified as T
17 | import Text.RE.TDFA.Text qualified as RE
18 |
19 | displayFeature :: Feature -> Utf8Builder
20 | displayFeature Feature{name, requires, requireFFI, defVars} =
21 | mconcat . intersperse "\n" $
22 | mconcat
23 | [ [headLine]
24 | , requireLines
25 | , loadFFILine
26 | , defVarLines
27 | , [provideLine]
28 | ]
29 | where
30 | headLine =
31 | ";; -*- lexical-binding: t; -*-"
32 |
33 | requireLines =
34 | map (displaySExp . C.require) requires
35 |
36 | -- FFIファイルのファイル名と最終的なファイル名が異なるため,FFIファイルには
37 | -- provide は書かず, load する。FFIファイルは対応するelモジュールファイルか
38 | -- らしかloadされず,elモジュールファイルのほうはrequireされるので複数回load
39 | -- されることはない。loadはロード時にメッセージを出すが第3引数にtを渡すこと
40 | -- で抑制できる。
41 | --
42 | -- require側で provideするよることも考えたが, requireされた側にprovideがなかっ
43 | -- た場合emacsが例外を投げる。
44 | --
45 | -- 逆にFFIファイル内ではprovideを書いてはいけない(チェックするべき)。例えば
46 | -- Data/Eq.el という FFIファイルは Dat.Eq._FOREIGN_.el というファイルにコピー
47 | -- される。
48 | loadFFILine = case requireFFI of
49 | Just (FeatureName (UnsafeSymbol name), _) ->
50 | [displaySExp $ funcallNative "load" [string name, C.nil, C.t]]
51 | Nothing -> []
52 |
53 | defVarLines =
54 | map displayDefVar defVars
55 |
56 | provideLine =
57 | displaySExp $ C.provide name
58 |
59 | displayDefVar :: DefVar -> Utf8Builder
60 | displayDefVar DefVar{name, definition} =
61 | displaySExp $ funcallNative "defvar" [symbol name, definition]
62 |
63 | displaySExp :: SExp -> Utf8Builder
64 | displaySExp = cata displaySExpFRaw . convSExp
65 |
66 | convSExp :: SExp -> Raw.SExp
67 | convSExp = cata conv
68 | where
69 | conv :: SExpF Raw.SExp -> Raw.SExp
70 | conv (Integer i) = Raw.integer i
71 | conv (Double d) = Raw.double d
72 | conv (String s) = Raw.string s
73 | conv (Character c) = Raw.character c
74 | conv (Symbol sym) = Raw.symbol sym
75 | conv (MkAlist xs) = convMkAlist xs
76 | conv (Progn es) = convProgn es
77 | conv (If e et ee) = Raw.list [Raw.symbol "if", e, et, ee]
78 | conv (Cond alts) = Raw.list $ Raw.symbol "cond" : map (\(p, e) -> Raw.list (p : eraseProgn e)) alts
79 | conv (Let letType binds body) = convLetish letType binds body
80 | conv (Pcase exprs cases) = convPcase exprs cases
81 | conv (Lambda1 arg body) = Raw.list $ [Raw.symbol "lambda", Raw.list [Raw.symbol arg]] <> eraseProgn body
82 | conv (Lambda0 body) = Raw.list $ [Raw.symbol "lambda", Raw.list []] <> eraseProgn body
83 | conv (FunCall1 f arg) = Raw.list [Raw.symbol "funcall", f, arg]
84 | conv (FunCall0 f) = Raw.list [Raw.symbol "funcall", f]
85 | conv (FunCallN f args) = Raw.list $ [Raw.symbol "funcall", f] <> args
86 | conv (FunCallNative sym args) = Raw.list $ Raw.symbol sym : args
87 | conv (QuotedSymbol qs) = Raw.quote $ Raw.symbol qs
88 |
89 | -- erase uneeded progns
90 | eraseProgn :: Raw.SExp -> [Raw.SExp]
91 | eraseProgn (Raw.SExp (Raw.Progn es)) = es
92 | eraseProgn e = [e]
93 |
94 | -- remvoe nested progns
95 | convProgn :: [Raw.SExp] -> Raw.SExp
96 | convProgn = Raw.progn . foldr go []
97 | where
98 | go :: Raw.SExp -> [Raw.SExp] -> [Raw.SExp]
99 | go (Raw.SExp (Raw.Progn es)) = (es <>)
100 | go e = (e :)
101 |
102 | convMkAlist :: [(Symbol, Raw.SExp)] -> Raw.SExp
103 | convMkAlist =
104 | Raw.backquote
105 | . Raw.list
106 | . map (uncurry Raw.cons . over _2 comma' . over _1 Raw.symbol)
107 | where
108 | comma' s
109 | | Raw.isLiteral s = s
110 | | otherwise = Raw.comma s
111 |
112 | -- Let系
113 | convLetish :: LetType -> [(Symbol, Raw.SExp)] -> Raw.SExp -> Raw.SExp
114 | convLetish letType binds body =
115 | Raw.list $
116 | [ Raw.symbol name
117 | , Raw.list (map (\(s, e) -> Raw.list [Raw.symbol s, e]) binds)
118 | ]
119 | <> eraseProgn body
120 | where
121 | name = case letType of
122 | LetStar -> "let*"
123 | LetRec -> "letrec"
124 |
125 | -- displayPcase :: [Utf8Builder] -> [PcaseAlt Utf8Builder] -> Utf8Builder
126 | -- displayPcase exprs cases = _
127 |
128 | convPcase :: [Raw.SExp] -> [PcaseAlt Raw.SExp] -> Raw.SExp
129 | convPcase exprs cases =
130 | Raw.list $ [Raw.symbol "pcase", unifyExprs exprs] <> map caseAlt cases
131 | where
132 | unifyExprs = \case
133 | [] -> error "Empty pcase exprs"
134 | [s] -> s
135 | ss -> Raw.list (Raw.symbol "list" : ss)
136 |
137 | unifyPatterns = \case
138 | [] -> error "Empty pcase exprs"
139 | [p] -> p
140 | ps -> PBackquotedList ps
141 |
142 | caseAlt PcaseAlt{patterns, guard, code} =
143 | Raw.list $
144 | [ pattern' (unifyPatterns patterns) & maybe id addGuard guard
145 | ]
146 | <> eraseProgn code
147 | where
148 | addGuard guard sexp =
149 | Raw.list [Raw.symbol "and", sexp, Raw.list [Raw.symbol "guard", guard]]
150 |
151 | pattern' :: PPattern Raw.SExp -> Raw.SExp
152 | pattern' PAny =
153 | Raw.symbol "_"
154 | pattern' (PInteger i) =
155 | Raw.integer i
156 | pattern' (PString s) =
157 | Raw.string s
158 | pattern' (PCharacter c) =
159 | Raw.character c
160 | pattern' (PSymbol sym) =
161 | Raw.quote $ Raw.symbol sym
162 | pattern' (PBind sym) =
163 | Raw.symbol sym
164 | pattern' (PBackquotedList ps) =
165 | Raw.backquote . Raw.list $ map (commaMaybe . pattern') ps
166 | pattern' (PBackquotedVector ps) =
167 | Raw.backquote . Raw.vector $ map (commaMaybe . pattern') ps
168 | pattern' (PBackquotedCons car cdr) =
169 | Raw.backquote $ Raw.cons (commaMaybe (pattern' car)) (commaMaybe (pattern' cdr))
170 | pattern' (PAnd ps) =
171 | Raw.list $ Raw.symbol "and" : map pattern' ps
172 | pattern' (PPred pred) =
173 | Raw.list [Raw.symbol "pred", pred]
174 | pattern' (PPredBool b) =
175 | Raw.list [Raw.symbol "pred", bool (Raw.symbol "null") (Raw.symbol "identity") b]
176 | pattern' (PApp s p) =
177 | Raw.list [Raw.symbol "app", s, pattern' p]
178 |
179 | -- バッククオート中の入れ子になっている不要な (`)や(') を取り除く
180 | -- e.g. `[ ... ,`[...] -> `[ ... [...]
181 | commaMaybe :: Raw.SExp -> Raw.SExp
182 | commaMaybe (Raw.SExp (Raw.Backquote se)) =
183 | se
184 | commaMaybe (Raw.SExp (Raw.Quote se)) =
185 | se
186 | commaMaybe s
187 | | Raw.isLiteral s = s
188 | | otherwise = Raw.comma s
189 |
190 | displaySExpFRaw :: Raw.SExpF Utf8Builder -> Utf8Builder
191 | displaySExpFRaw (Raw.Integer i) = display i
192 | displaySExpFRaw (Raw.Double d) = display d
193 | displaySExpFRaw (Raw.String t) = displayString t
194 | displaySExpFRaw (Raw.Character c) = "?" <> display c
195 | displaySExpFRaw (Raw.Symbol sym) = displaySymbol sym
196 | displaySExpFRaw (Raw.Cons car cdr) = paren [car <> " . " <> cdr]
197 | displaySExpFRaw (Raw.Progn es) = paren $ "progn" : es
198 | displaySExpFRaw (Raw.List xs) = paren xs
199 | displaySExpFRaw (Raw.Vector xs) = bracket xs
200 | displaySExpFRaw (Raw.Quote s) = "'" <> s
201 | displaySExpFRaw (Raw.Backquote s) = "`" <> s
202 | displaySExpFRaw (Raw.Comma s) = "," <> s
203 |
204 | -- 取り敢えず雑なquotingで
205 | displayString :: Text -> Utf8Builder
206 | displayString t = "\"" <> display (quote t) <> "\""
207 | where
208 | quote = T.replace "\"" "\\\""
209 |
210 | -- elispでは
211 | -- https://www.gnu.org/software/emacs/manual/html_node/elisp/Symbol-Type.html
212 | -- まずエスケープが必要な文字に関しては \ を前置。
213 | -- 文字レベルでエスケープが不要としても全体として数値として解釈されてしまうものは先頭を\でエスケープする。
214 | displaySymbol :: Symbol -> Utf8Builder
215 | displaySymbol (UnsafeSymbol s) = display $ escape s
216 | where
217 | -- 取り敢えず[0-9a-zA-Z-+=*/_~!@$%^&:<>{}_] 以外は \ でエスケープすることにする。
218 | -- ? もエスケープは不要なのだが ?a など文字リテラルと
219 | -- ただし文字リテラル(?a)や数値として解釈されるもの(-1.5)などは先頭に \ を付ける必要がある
220 | escape =
221 | (RE.*=~/ [RE.ed|[^[:alnum:].+=*/_~!@$%^&:<>{}_-]///\${0}|])
222 | isNumberLiteral =
223 | RE.matched . (RE.?=~ [RE.re|^[+-]?[[:digit:]]*(\.[[:digit:]]+)?$|])
224 | unambiguousify t =
225 | bool id ("\\" <>) $ isNumberLiteral t
226 |
227 | paren :: [Utf8Builder] -> Utf8Builder
228 | paren xs = "(" <> mconcat (intersperse " " xs) <> ")"
229 |
230 | bracket :: [Utf8Builder] -> Utf8Builder
231 | bracket xs = "[" <> mconcat (intersperse " " xs) <> "]"
232 |
--------------------------------------------------------------------------------
/src/PsEl/SExpOptimize.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE DataKinds #-}
2 | {-# LANGUAGE DeriveFunctor #-}
3 | {-# LANGUAGE DuplicateRecordFields #-}
4 | {-# LANGUAGE GeneralizedNewtypeDeriving #-}
5 | {-# LANGUAGE NamedFieldPuns #-}
6 | {-# LANGUAGE OverloadedStrings #-}
7 | {-# LANGUAGE TupleSections #-}
8 | {-# LANGUAGE TypeApplications #-}
9 |
10 | module PsEl.SExpOptimize where
11 |
12 | import Control.Lens (itoListOf)
13 | import Data.Functor.Foldable (Recursive (para), cata, cataA)
14 | import Data.Generics.Sum (_Ctor)
15 | import Data.Generics.Wrapped (_Unwrapped)
16 | import Language.PureScript qualified as PS
17 | import PsEl.SExp
18 | import PsEl.SExpPattern qualified as P
19 | import PsEl.SExpTraverse (Index (..), freeVars)
20 | import PsEl.Transpile (globalVar, localUnusedVar)
21 | import RIO
22 | import RIO.Lens (_1)
23 | import RIO.Map qualified as Map
24 | import RIO.State (State, evalState, get, put)
25 |
26 | newtype OptimizeM a = OptimizeM {runOptimize :: State Int a}
27 | deriving (Functor, Applicative, Monad)
28 |
29 | -- PSが $ prefixの変数を使わない,という前提
30 | mkUniqVar :: Text -> OptimizeM Symbol
31 | mkUniqVar base = OptimizeM $ do
32 | i <- get
33 | put $ i + 1
34 | pure $ UnsafeSymbol $ "$" <> base <> "-" <> textDisplay i
35 |
36 | optimize :: Feature -> Feature
37 | optimize f@Feature{defVars} =
38 | f
39 | { defVars =
40 | map
41 | (\dv -> dv{definition = optimizeDefVar dv})
42 | defVars
43 | }
44 |
45 | optimizeDefVar :: DefVar -> SExp
46 | optimizeDefVar DefVar{name, definition} = flip evalState 0 . runOptimize $ do
47 | pure (applyRewrite definition)
48 | >>= applyTopLevelTCO name
49 | >>= applyTCO
50 | where
51 | applyRewrite :: SExp -> SExp
52 | applyRewrite =
53 | cata
54 | ( magicDo
55 | . directlyCallFns
56 | . removeDataFunctions
57 | . removeRebindOnlyPcase
58 | . replacePcaseToIf
59 | . SExp
60 | )
61 |
62 | applyTopLevelTCO :: Symbol -> SExp -> OptimizeM SExp
63 | applyTopLevelTCO name sexp =
64 | fromMaybe (pure sexp) $ applyTCOToName name sexp
65 |
66 | -- top-downに適用する必要あり
67 | applyTCO :: SExp -> OptimizeM SExp
68 | applyTCO = para applyTCO'
69 |
70 | -- only letrec names are possibily self-resursive
71 | applyTCO' :: SExpF (SExp, OptimizeM SExp) -> OptimizeM SExp
72 | applyTCO' (Let LetRec binds body) = do
73 | binds' <- for binds $ \(name, (orig, sexp)) ->
74 | (name,) <$> case applyTCOToName name orig of
75 | Nothing -> sexp
76 | Just om -> applyTCO =<< om
77 | body' <- snd body
78 | pure . SExp $ Let LetRec binds' body'
79 | applyTCO' sexp =
80 | SExp <$> mapM snd sexp
81 |
82 | applyTCOToName :: Symbol -> SExp -> Maybe (OptimizeM SExp)
83 | applyTCOToName name sexp =
84 | case toLambdas sexp of
85 | ([], _) ->
86 | Nothing
87 | lambdas ->
88 | fmap (fmap fromLambdas) (selfRecursiveTCO name lambdas)
89 |
90 | toLambdas :: SExp -> ([Symbol], SExp)
91 | toLambdas (SExp (Lambda1 sym sexp)) = over _1 (sym :) $ toLambdas sexp
92 | toLambdas sexp = ([], sexp)
93 |
94 | fromLambdas :: ([Symbol], SExp) -> SExp
95 | fromLambdas (args, body) = foldr lambda1 body args
96 |
97 | -- PSでif文を使うとcorefnではcaseに変換される。素直にそのままコード生成すると例
98 | -- えばのようになる。
99 | --
100 | -- (pcase b ((pred identity) 1) (_ 0))
101 | --
102 | -- 上記ケースは (if b 1 0) に変換できる。ただpaseマクロが次まで展開してくれる。
103 | --
104 | -- (if (identity b) (let nil 1) (let nil 0))
105 | --
106 | -- そのため速度的な改善はそれほどでもない。
107 | replacePcaseToIf :: SExp -> SExp
108 | replacePcaseToIf
109 | ( P.Pcase
110 | [e]
111 | [PcaseAlt [PPredBool b] Nothing thenE, PcaseAlt [PAny] Nothing elseE]
112 | ) = if' (bool (funcallNative "not" [e]) e b) thenE elseE
113 | replacePcaseToIf s = s
114 |
115 | -- 変数のbindingのみ行なってる単一Altのpcase式の除去。
116 | -- 例えば:
117 | --
118 | -- selfRec1 :: Int -> Int -> Int
119 | -- selfRec1 i to
120 | -- | eqInt i to = 1
121 | -- | true = selfRec1 (succInt i) to
122 | --
123 | -- はそのままコード変換すると次のようになる。
124 | --
125 | -- (defvar Test.PsEl.TCO.selfRec1
126 | -- (lambda (i)
127 | -- (lambda (to)
128 | -- (pcase (list i to)
129 | -- (`(,i1 ,to1)
130 | -- (cond ((funcall (funcall Test.Util....
131 | --
132 | -- pcaseは単に変数を再束縛しているだけであり,変数名を置き換えをすれば除去可能である。
133 | -- PSレベルで行なえる変換に思えるが,CoreFnデータ型ににcond(ifの連結)に相当するものがないからかな?
134 | removeRebindOnlyPcase :: SExp -> SExp
135 | removeRebindOnlyPcase (P.Pcase exps [PcaseAlt pats Nothing code])
136 | | Just expsSyms <- traverse (^? _Unwrapped . _Ctor @"Symbol") exps
137 | , Just patsSyms <- traverse (^? _Ctor @"PBind") pats =
138 | code
139 | & over freeVars (symbolsRewrite (zip patsSyms expsSyms))
140 | removeRebindOnlyPcase s = s
141 |
142 | -- 不要な ($), (&) の除去
143 | -- ほぼ意味がなく結合優先度や適用順序を調整するために使われる。
144 | -- prelude の Data.Functinモジュールの apply($) 及び applyFlipped(&)である。
145 | --
146 | -- apply :: forall a b. (a -> b) -> a -> b
147 | -- apply f x = f x
148 | --
149 | -- (funcall apply b>) -> b>
150 | --
151 | -- applyFlipped :: forall a b. a -> (a -> b) -> b
152 | -- applyFlipped x f = f x
153 | --
154 | -- (funcall (funcall applyFlipped ) b>) -> (funcall b> )
155 | --
156 | -- ??? ModuleNameやIdentはLanguage.PureScript.Constants以下で定義されている?
157 | removeDataFunctions :: SExp -> SExp
158 | removeDataFunctions (P.FunCall1 (P.Symbol f) a)
159 | | f == globalVar moduleDataFunction (PS.Ident "apply") = a
160 | removeDataFunctions (P.FunCall1 (P.FunCall1 (P.Symbol f) a) ab)
161 | | f == globalVar moduleDataFunction (PS.Ident "applyFlipped") = funcall1 ab a
162 | removeDataFunctions s = s
163 |
164 | moduleDataFunction :: PS.ModuleName
165 | moduleDataFunction = PS.ModuleName "Data.Function"
166 |
167 | -- MagicDo
168 | --
169 | -- 参考までに pskt での実装方法
170 | -- https://github.com/csicar/pskt/blob/aa0d5df52e9579abd38061c6ab891489ebf295c4/src/CodeGen/MagicDo.hs#L45
171 | -- fuse0 で (funcall0 (lambda0 ..) を除去しているので(多分)bottomupに適用しても問題ないはず。
172 | --
173 | -- bind :: forall a b. m a -> (a -> m b) -> m b
174 | --
175 | -- (funcall (funcall (funcall Control.Bind.bind Effect.bindEffect) ) (lambda (arg) ))
176 | --
177 | -- if arg is unused, then:
178 | --
179 | -- (lambda ()
180 | -- (funcall )
181 | -- (funcall ))
182 | --
183 | -- or else:
184 | --
185 | -- (lambda ()
186 | -- (let* ((arg (funcall )))
187 | -- (funcall )))
188 | --
189 | -- 捨てられる場合
190 | --
191 | -- discard :: forall f b. Bind f => f a -> (a -> f b) -> f b
192 | --
193 | -- (funcall (funcall (funcall (funcall Control.Bind.discard Control.Bind.discardUnit) Effect.bindEffect) ) (lambda (arg) ))
194 | -- ->
195 | -- (lambda ()
196 | -- (funcall )
197 | -- (funcall ))
198 | --
199 | -- pure
200 | --
201 | -- pure :: forall a. a -> f a
202 | --
203 | -- (funcall (funcall Control.Applicative.pure Effect.applicativeEffect) )
204 | -- ->
205 | -- (lambda () v)
206 | --
207 | -- NOTE: psktと同様 while や untilもやるべきかな。
208 | -- あともう少しパターンマッチにしやすいよう Symbolの型変えるか?
209 | magicDo :: SExp -> SExp
210 | magicDo (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.Symbol bind) (P.Symbol bindEffect)) ma) (P.Lambda1 arg mb))
211 | | bind == identBind && bindEffect == identBindEffect =
212 | if arg == localUnusedVar
213 | then lambda0 $ progn [fuse0 (funcall0 ma), fuse0 (funcall0 mb)]
214 | else lambda0 $ letStar [(arg, fuse0 (funcall0 ma))] (fuse0 (funcall0 mb))
215 | magicDo (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.Symbol discard) (P.Symbol discardUnit)) (P.Symbol bindEffect)) ma) (P.Lambda1 _ mb))
216 | | discard == identDiscard && discardUnit == identDiscardUnit && bindEffect == identBindEffect =
217 | lambda0 $ progn [fuse0 (funcall0 ma), fuse0 (funcall0 mb)]
218 | magicDo (P.FunCall1 (P.FunCall1 (P.Symbol pure) (P.Symbol applicativeEffect)) v)
219 | | pure == identPure && applicativeEffect == identApplicativeEffect =
220 | lambda0 v
221 | magicDo s = s
222 |
223 | -- NOTE: (lambda0 (funcall0 ..)) もなくせるかと思うが多分そのようなケースは発生しない
224 | -- NOTE: 良く考えたら上記のケースは (lambda0 (funcall 0 (let ((..)) x))) のケースが
225 | -- あるから多分fuseは駄目な気がする
226 | -- magicDo が cata で bottomup に適用されているので再帰除去は不要。
227 | fuse0 :: SExp -> SExp
228 | fuse0 (P.FunCall0 (P.Lambda0 a)) = a
229 | fuse0 s = s
230 |
231 | identBind = globalVar moduleControlBind (PS.Ident "bind")
232 | identPure = globalVar moduleControlApplicative (PS.Ident "pure")
233 | identDiscard = globalVar moduleControlBind (PS.Ident "discard")
234 | identDiscardUnit = globalVar moduleControlBind (PS.Ident "discardUnit")
235 | identBindEffect = globalVar moduleEffect (PS.Ident "bindEffect")
236 | identApplicativeEffect = globalVar moduleEffect (PS.Ident "applicativeEffect")
237 | moduleEffect = PS.ModuleName "Effect"
238 | moduleControlApplicative = PS.ModuleName "Control.Applicative"
239 | moduleControlBind = PS.ModuleName "Control.Bind"
240 |
241 | -- 引数が全部与えられていた場合, FnN{0,..,10} を直接呼ぶ
242 |
243 | directlyCallFns :: SExp -> SExp
244 | directlyCallFns (P.FunCall1 (P.Symbol runFn0) f)
245 | | runFn0 == identRunFn 0 = funcallN f []
246 | directlyCallFns (P.FunCall1 (P.FunCall1 (P.Symbol runFn1) f) a0)
247 | | runFn1 == identRunFn 1 = funcallN f [a0]
248 | directlyCallFns (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.Symbol runFn2) f) a0) a1)
249 | | runFn2 == identRunFn 2 = funcallN f [a0, a1]
250 | directlyCallFns (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.Symbol runFn3) f) a0) a1) a2)
251 | | runFn3 == identRunFn 3 = funcallN f [a0, a1, a2]
252 | directlyCallFns (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.Symbol runFn4) f) a0) a1) a2) a3)
253 | | runFn4 == identRunFn 4 = funcallN f [a0, a1, a2, a3]
254 | directlyCallFns (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.Symbol runFn5) f) a0) a1) a2) a3) a4)
255 | | runFn5 == identRunFn 5 = funcallN f [a0, a1, a2, a3, a4]
256 | directlyCallFns (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.Symbol runFn6) f) a0) a1) a2) a3) a4) a5)
257 | | runFn6 == identRunFn 6 = funcallN f [a0, a1, a2, a3, a4, a5]
258 | directlyCallFns (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.Symbol runFn7) f) a0) a1) a2) a3) a4) a5) a6)
259 | | runFn7 == identRunFn 7 = funcallN f [a0, a1, a2, a3, a4, a5, a6]
260 | directlyCallFns (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.Symbol runFn8) f) a0) a1) a2) a3) a4) a5) a6) a7)
261 | | runFn8 == identRunFn 8 = funcallN f [a0, a1, a2, a3, a4, a5, a6, a7]
262 | directlyCallFns (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.Symbol runFn9) f) a0) a1) a2) a3) a4) a5) a6) a7) a8)
263 | | runFn9 == identRunFn 9 = funcallN f [a0, a1, a2, a3, a4, a5, a6, a7, a8]
264 | directlyCallFns (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.FunCall1 (P.Symbol runFn10) f) a0) a1) a2) a3) a4) a5) a6) a7) a8) a9)
265 | | runFn10 == identRunFn 10 = funcallN f [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9]
266 | directlyCallFns s = s
267 |
268 | identRunFn :: Int -> Symbol
269 | identRunFn i = globalVar moduleFunctionUncurried (PS.Ident $ "runFn" <> textDisplay i)
270 | moduleFunctionUncurried = PS.ModuleName "Data.Function.Uncurried"
271 |
272 | -- 自己再帰関数の最適化
273 | -- ただし最適化可能なケース全てに於いて最適化を実行できるわけではない。
274 | -- 差し当り自明なケースのみ最適化を実行する。
275 | -- トップダウンに適用する必要あり。whileループに変換すると
276 | -- 変換後のコードでは末尾呼出し検出がtrue/negativeになってしまう。
277 | --
278 | -- sym 現在最適化対象の識別子
279 | -- args は少なくとも一つ以上持つとする
280 | -- body は関数の中身
281 | --
282 | selfRecursiveTCO :: Symbol -> ([Symbol], SExp) -> Maybe (OptimizeM ([Symbol], SExp))
283 | selfRecursiveTCO sym (args, body) = do
284 | let argLen = length args
285 | let calls = filter ((== sym) . snd) $ itoListOf freeVars body
286 | if not (null calls) && all (isTC argLen . fst) calls
287 | then Just $ (args,) <$> performTCO
288 | else Nothing
289 | where
290 | -- 末尾呼出しかを判定。
291 | -- 必要な1引数呼出し回数が第一引数で渡される。
292 | -- 第二引数の[Index]は逆順であることに注意(内から外の順)。
293 | --
294 | -- (a)
295 | -- 呼出し途中にある識別子に束縛された場合。現在は単に末尾呼出しされていないとしているが,
296 | -- その識別子が(元bodyから見て)末尾呼出しされている場合は最適化可能なケースが存在する。
297 | -- 実際JSバックエンドはそこまで最適化している。
298 | --
299 | -- (b)
300 | -- 引数位置にあったとしても最適化可能なケースは存在するが解析が難しいため現在はしない。
301 | --
302 | -- (c)
303 | -- 本来必要な引数の数を超えて呼び出されるケースは存在する。
304 | -- 例えば foo :: Int -> Int -> Int という型でも実装が foo i = if ... (\j -> i + j)
305 | -- のような形であった場合, 元の args引数の長さは1になる。
306 | --
307 | -- ?? Lambda0/FunCall0 は直Falseにしなくても救済措置があるかも。
308 | -- ただ実際に有り得るかは疑問。
309 | isTC :: Int -> [Index] -> Bool
310 | isTC i [] = i == 0
311 | isTC i (ix : ixs) = case ix of
312 | ILambda1 -> isTC (i + 1) ixs
313 | ILambda0 -> False
314 | IBind _ -> False -- (a)
315 | IArg -> False -- (b)
316 | ICond -> False
317 | ITail -> isTC i ixs
318 | IFunCall1
319 | | i > 0 -> isTC (i - 1) ixs
320 | | otherwise -> False -- (c)
321 | IFunCall0 -> False
322 | IFunCallN -> False
323 |
324 | -- 例えば次の関数が最適化対象とする。
325 | --
326 | -- (lambda (i)
327 | -- (lambda (j)
328 | -- <..body..>))
329 | --
330 | -- 次のようにwhile文を使ったループに変更する。
331 | -- 生成変数($ prefix)は一意性を持つよう被らないsuffixを付ける。
332 | -- <..body'..> は <..body..>内の再帰呼出しを $loop-fn に置き換え,
333 | -- lambda引数をローカルの物に置き換えたもの。
334 | -- 現状再帰呼出し関数をshadowすれば置き換えは不要だが,
335 | -- 将来的に部分的に置き換えの必要が発生した場合も考えて全て置き換えておく。
336 | -- lambda引数をローカルに置き換える(下の例だと $i, $j)のは,
337 | -- 関数ば部分適用された場合,lambda引数を直でsetqすると呼出し間でその
338 | -- 変更が共有されてしまうため。
339 | --
340 | -- (lambda (i)
341 | -- (lambda (j)
342 | -- (let* (($i i)
343 | -- ($j j)
344 | -- ($continue (make-symbol "")) ;; uniq symbol
345 | -- ($result $do-loop)
346 | -- ($loop-fn (lambda (_i)
347 | -- (lambda (_j)
348 | -- (setq $i _i $j _j)
349 | -- $continue))))
350 | -- (while (eq $result $continue)
351 | -- (setq $result <..body'..>))
352 | -- $result)))
353 | --
354 | performTCO :: OptimizeM SExp
355 | performTCO = do
356 | varContinue <- mkUniqVar "continue"
357 | varResult <- mkUniqVar "result"
358 | varLoopFn <- mkUniqVar $ symbolText sym
359 | argsLocal <- traverse (mkUniqVar . symbolText) args
360 | argsTmp <- traverse (mkUniqVar . symbolText) args
361 | let bindLocalVars =
362 | zipWith (curry (fmap symbol)) argsLocal args
363 | let bindSpecialVars =
364 | [ (varContinue, funcallNative "make-symbol" [string ""])
365 | , (varResult, symbol varContinue)
366 | ,
367 | ( varLoopFn
368 | , foldr
369 | lambda1
370 | ( progn2
371 | (funcallNative "setq" (zipWith (\a a' -> [symbol a, symbol a']) argsLocal argsTmp & mconcat))
372 | (symbol varContinue)
373 | )
374 | argsTmp
375 | )
376 | ]
377 | let body' =
378 | body & over freeVars (symbolsRewrite ((sym, varLoopFn) : zip args argsLocal))
379 | pure $
380 | letStar
381 | (bindLocalVars <> bindSpecialVars)
382 | ( progn2
383 | ( funcallNative
384 | "while"
385 | [ funcallNative "eq" [symbol varResult, symbol varContinue]
386 | , funcallNative "setq" [symbol varResult, body']
387 | ]
388 | )
389 | (symbol varResult)
390 | )
391 | where
392 | progn2 e0 e1 = progn [e0, e1]
393 |
394 | symbolsRewrite :: [(Symbol, Symbol)] -> Symbol -> Symbol
395 | symbolsRewrite symPairs =
396 | let symMap = Map.fromList symPairs
397 | in \sym -> fromMaybe sym (Map.lookup sym symMap)
398 |
--------------------------------------------------------------------------------
/src/PsEl/SExpPattern.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE PatternSynonyms #-}
2 |
3 | module PsEl.SExpPattern where
4 |
5 | import PsEl.SExp as S
6 |
7 | pattern Integer i = SExp (S.Integer i)
8 | pattern Symbol s = SExp (S.Symbol s)
9 | pattern Lambda1 s e = SExp (S.Lambda1 s e)
10 | pattern Lambda0 e = SExp (S.Lambda0 e)
11 | pattern FunCall1 f a = SExp (S.FunCall1 f a)
12 | pattern FunCall0 f = SExp (S.FunCall0 f)
13 | pattern Pcase es alts = SExp (S.Pcase es alts)
14 |
--------------------------------------------------------------------------------
/src/PsEl/SExpRaw.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE DeriveTraversable #-}
2 | {-# LANGUAGE TypeFamilies #-}
3 |
4 | module PsEl.SExpRaw where
5 |
6 | import Data.Functor.Foldable qualified as RS
7 | import PsEl.SExp (Symbol)
8 | import RIO
9 |
10 | newtype SExp = SExp {unSExpr :: SExpF SExp}
11 |
12 | type instance RS.Base SExp = SExpF
13 | instance RS.Recursive SExp where
14 | project = unSExpr
15 |
16 | data SExpF e
17 | = Integer Integer
18 | | Double Double
19 | | String Text
20 | | Character Char
21 | | Symbol Symbol
22 | | Cons e e
23 | | Progn [e]
24 | | List [e]
25 | | Vector [e]
26 | | Quote e
27 | | Backquote e
28 | | Comma e
29 | deriving (Functor, Foldable, Traversable)
30 |
31 | integer :: Integer -> SExp
32 | integer = SExp . Integer
33 |
34 | double :: Double -> SExp
35 | double = SExp . Double
36 |
37 | string :: Text -> SExp
38 | string = SExp . String
39 |
40 | character :: Char -> SExp
41 | character = SExp . Character
42 |
43 | symbol :: Symbol -> SExp
44 | symbol = SExp . Symbol
45 |
46 | vector :: [SExp] -> SExp
47 | vector = SExp . Vector
48 |
49 | -- e.g. (car . cdr)
50 | cons :: SExp -> SExp -> SExp
51 | cons car cdr = SExp $ Cons car cdr
52 |
53 | progn :: [SExp] -> SExp
54 | progn = SExp . Progn
55 |
56 | list :: [SExp] -> SExp
57 | list = SExp . List
58 |
59 | quote :: SExp -> SExp
60 | quote = SExp . Quote
61 |
62 | backquote :: SExp -> SExp
63 | backquote = SExp . Backquote
64 |
65 | comma :: SExp -> SExp
66 | comma = SExp . Comma
67 |
68 | -- 文字列や数値などのリテラル表記かを判定。
69 | -- vector(e.g. [1 2 3])もリテラルであることに注意。
70 | isLiteral :: SExp -> Bool
71 | isLiteral (SExp s') = case s' of
72 | Integer _ -> True
73 | Double _ -> True
74 | String _ -> True
75 | Character _ -> True
76 | Vector _ -> True
77 | _ -> False
78 |
--------------------------------------------------------------------------------
/src/PsEl/SExpTraverse.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE ApplicativeDo #-}
2 | {-# LANGUAGE FlexibleContexts #-}
3 | {-# LANGUAGE ScopedTypeVariables #-}
4 | {-# LANGUAGE TupleSections #-}
5 |
6 | module PsEl.SExpTraverse where
7 |
8 | import Control.Lens (Indexable, IndexedTraversal', indexed)
9 | import Data.Functor.Foldable (cata)
10 | import Data.List (mapAccumL)
11 | import PsEl.SExp
12 | import RIO
13 | import RIO.Lens (_2)
14 | import RIO.NonEmpty qualified as NE
15 | import RIO.Set qualified as Set
16 |
17 | -- indexはシンプルなself-recursiveなTCOを最適化するために必要な情報のみ含む。
18 | -- その意味ではIArgとICondを分ける必要はないかな..
19 | data Index
20 | = ILambda1
21 | | ILambda0
22 | | IBind Symbol
23 | | -- 引数部
24 | -- progn の最後の式以外もargと見做す
25 | -- e.g. (pcase ...), (alist ), (funcall x )
26 | IArg
27 | | -- 条件部
28 | -- e.g. (if ..), (cond ( ..) ( ...))
29 | ICond
30 | | -- 長さ一のボディ
31 | ITail
32 | | -- funcall1(1引数呼出し)の対象
33 | IFunCall1
34 | | IFunCall0
35 | | IFunCallN
36 |
37 | -- | `Taversal' SExp Symbol` for free variables
38 | freeVars ::
39 | forall f p.
40 | (Applicative f, Indexable [Index] p) =>
41 | p Symbol (f Symbol) ->
42 | SExp ->
43 | f SExp
44 | freeVars p s = cata go s [] mempty
45 | where
46 | go ::
47 | SExpF ([Index] -> Set Symbol -> f SExp) ->
48 | [Index] ->
49 | Set Symbol ->
50 | f SExp
51 | -- 自由変数のみfを適用する
52 | go (Symbol sym) ix vars
53 | | not (Set.member sym vars) = SExp . Symbol <$> indexed p ix sym
54 | -- 束縛(lambda)
55 | go (Lambda1 sym body) ix vars = do
56 | body' <- body (ILambda1 : ix) (Set.insert sym vars)
57 | pure $ SExp $ Lambda1 sym body'
58 | go (Lambda0 body) ix vars = do
59 | body' <- body (ILambda0 : ix) vars
60 | pure $ SExp $ Lambda0 body'
61 |
62 | -- 束縛(let*)
63 | go (Let LetStar binds body) ix vars = do
64 | let f vars (sym, val) = (Set.insert sym vars, (sym,) <$> val (IBind sym : ix) vars)
65 | let (vars', binds') = mapAccumL f vars binds
66 | binds'' <- sequenceA binds'
67 | body' <- body (ITail : ix) vars'
68 | pure $ SExp $ Let LetStar binds'' body'
69 |
70 | -- 束縛(letrec)
71 | go (Let LetRec binds body) ix vars = do
72 | let vars' = foldr (Set.insert . fst) vars binds
73 | let f (sym, val) = (sym,) <$> val (IBind sym : ix) vars'
74 | binds' <- traverse f binds
75 | body' <- body (ITail : ix) vars
76 | pure $ SExp $ Let LetRec binds' body'
77 |
78 | -- 束縛(pcase)
79 | go (Pcase exps alts) ix vars = do
80 | exps' <- traverse (\e -> e (IArg : ix) vars) exps
81 | alts' <- traverse (pcaseAlt ix vars) alts
82 | pure $ SExp $ Pcase exps' alts'
83 |
84 | -- 関数呼出し(funcall)
85 | go (FunCall1 f arg) ix vars = do
86 | f' <- f (IFunCall1 : ix) vars
87 | arg' <- arg (IArg : ix) vars
88 | pure $ SExp $ FunCall1 f' arg'
89 | go (FunCall0 f) ix vars = do
90 | f' <- f (IFunCall0 : ix) vars
91 | pure $ SExp $ FunCall0 f'
92 | go (FunCallN f args) ix vars = do
93 | f' <- f (IFunCallN : ix) vars
94 | args' <- traverse (\arg -> arg (IArg : ix) vars) args
95 | pure $ SExp $ FunCallN f' args'
96 |
97 | -- 関数呼出し(ネイティブ)
98 | go (FunCallNative sym args) ix vars = do
99 | args' <- traverse (\a -> a (IArg : ix) vars) args
100 | pure $ SExp $ FunCallNative sym args'
101 |
102 | -- if
103 | go (If condE thenE elseE) ix vars = do
104 | condE' <- condE (ICond : ix) vars
105 | thenE' <- thenE (ITail : ix) vars
106 | elseE' <- elseE (ITail : ix) vars
107 | pure $ SExp $ If condE' thenE' elseE'
108 |
109 | -- cond
110 | go (Cond alts) ix vars = do
111 | let f (condE, bodyE) = do
112 | condE' <- condE (ICond : ix) vars
113 | bodyE' <- bodyE (ITail : ix) vars
114 | pure (condE', bodyE')
115 | alts' <- traverse f alts
116 | pure $ SExp $ Cond alts'
117 |
118 | -- mkalist
119 | go (MkAlist xs) ix vars = do
120 | xs' <- traverse (\(f, e) -> (f,) <$> e (IArg : ix) vars) xs
121 | pure $ SExp $ MkAlist xs'
122 |
123 | -- progn
124 | go (Progn xs) ix vars = do
125 | let len = length xs
126 | let addIndex i = if i == len then ITail else IArg
127 | xs' <- traverse (\(i, e) -> e (addIndex i : ix) vars) $ zip [1 ..] xs
128 | pure $ SExp $ Progn xs'
129 |
130 | -- その他(再帰しないもの)
131 | go s ix vars =
132 | SExp <$> sequenceA (error "unreachable" <$> s)
133 |
134 | pcaseAlt ::
135 | [Index] ->
136 | Set Symbol ->
137 | PcaseAlt ([Index] -> Set Symbol -> f SExp) ->
138 | f (PcaseAlt SExp)
139 | pcaseAlt ix vars (PcaseAlt patterns guard code) =
140 | let (vars', patterns') = mapAccumL ppattern vars $ map (fmap ($ (ICond : ix))) patterns
141 | in PcaseAlt
142 | <$> sequenceA patterns'
143 | <*> traverse (\e -> e (ICond : ix) vars') guard
144 | <*> code (ITail : ix) vars'
145 |
146 | -- 相互再帰のrecursion schemeを避けるため ppatternは手動での再帰関数。
147 | -- 構造系では先にくる要素で束縛が存在しうることに注意。
148 | -- PPatternの型パラメータ(Set Symbol -> f SExp)が必要なのは pred と appのみ。
149 | ppattern ::
150 | Set Symbol ->
151 | PPattern (Set Symbol -> f SExp) ->
152 | (Set Symbol, f (PPattern SExp))
153 | ppattern vars PAny =
154 | (vars, pure PAny)
155 | ppattern vars (PInteger i) =
156 | (vars, pure (PInteger i))
157 | ppattern vars (PString s) =
158 | (vars, pure (PString s))
159 | ppattern vars (PCharacter c) =
160 | (vars, pure (PCharacter c))
161 | ppattern vars (PSymbol c) =
162 | (vars, pure (PSymbol c))
163 | ppattern vars (PBind sym) =
164 | (Set.insert sym vars, pure (PBind sym))
165 | ppattern vars (PBackquotedList es) =
166 | let (vars', es') = mapAccumL ppattern vars es
167 | in (vars', PBackquotedList <$> sequenceA es')
168 | ppattern vars (PBackquotedVector es) =
169 | let (vars', es') = mapAccumL ppattern vars es
170 | in (vars', PBackquotedVector <$> sequenceA es')
171 | ppattern vars0 (PBackquotedCons car cdr) =
172 | let (vars1, car') = ppattern vars0 car
173 | (vars2, cdr') = ppattern vars1 cdr
174 | in (vars2, PBackquotedCons <$> car' <*> cdr')
175 | ppattern vars (PAnd pps) =
176 | let (vars', pps') = mapAccumL ppattern vars pps
177 | in (vars', PAnd <$> sequenceA pps')
178 | ppattern vars (PPred pred) =
179 | (vars, PPred <$> pred vars)
180 | ppattern vars (PPredBool b) =
181 | (vars, pure (PPredBool b))
182 | ppattern vars (PApp e pp) =
183 | let (vars', pp') = ppattern vars pp
184 | in (vars', PApp <$> e vars <*> pp')
185 |
--------------------------------------------------------------------------------
/src/PsEl/Transpile.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE DataKinds #-}
2 | {-# LANGUAGE DuplicateRecordFields #-}
3 | {-# LANGUAGE FlexibleContexts #-}
4 | {-# LANGUAGE LambdaCase #-}
5 | {-# LANGUAGE NamedFieldPuns #-}
6 | {-# LANGUAGE OverloadedStrings #-}
7 | {-# LANGUAGE TupleSections #-}
8 |
9 | module PsEl.Transpile where
10 |
11 | import Data.Reflection (Given (given), give)
12 | import Language.PureScript (Ident (..), ModuleName (ModuleName), ProperName (ProperName), ProperNameType (ConstructorName, TypeName))
13 | import Language.PureScript qualified as P hiding (ProperName)
14 | import Language.PureScript.Constants.Prim qualified as C
15 | import Language.PureScript.CoreFn
16 | import Language.PureScript.Errors (SourceSpan)
17 | import Language.PureScript.Names (ProperName (runProperName), Qualified (Qualified), mkQualified)
18 | import Language.PureScript.PSString (PSString (toUTF16CodeUnits))
19 | import Language.PureScript.PSString qualified as PS
20 | import PsEl.SExp hiding (Let)
21 | import PsEl.SExpConstructor qualified as C
22 | import RIO
23 | import RIO.Lens
24 | import RIO.NonEmpty qualified as NonEmpty
25 | import RIO.NonEmpty.Partial qualified as NonEmptyPartial
26 | import RIO.Set qualified as Set
27 | import RIO.Text qualified as T
28 | import RIO.Text.Partial qualified as Partial
29 |
30 | transpile :: Module Ann -> Feature
31 | transpile
32 | Module
33 | { moduleSourceSpan
34 | , moduleComments
35 | , moduleName
36 | , modulePath
37 | , moduleImports
38 | , moduleExports
39 | , moduleForeign
40 | , moduleDecls
41 | } = Feature{name, requires, requireFFI, defVars}
42 | where
43 | name =
44 | featureName moduleName
45 |
46 | -- 理由は分からないが, moduleImportsには自モジュールも含まれている。
47 | -- それをrequireしてしまうと再帰ruquireのエラーになるため除外する。
48 | requires =
49 | pselFeature : map featureName (filter (not . ignoreModule) (map snd moduleImports))
50 |
51 | ignoreModule mn =
52 | mn == moduleName || isPrimModule mn
53 |
54 | defVars =
55 | mconcat $ map (decl moduleName) moduleDecls
56 |
57 | requireFFI =
58 | if null moduleForeign
59 | then Nothing
60 | else Just (ffiFeatureName moduleName, map (globalVar moduleName) moduleForeign)
61 |
62 | isPrimModule mn@(ModuleName t) =
63 | mn == C.Prim || T.isPrefixOf "Prim." t
64 |
65 | -- 全ての生成モジュールに必要になるヘルパーライブラリ
66 | pselFeature :: FeatureName
67 | pselFeature = FeatureName "psel"
68 |
69 | -- モジュール名はそのまま Feqatureとする
70 | -- キャメルケース,ドット区切りはelispの規約に沿っていないが,
71 | -- PSからの生成ファイルということ
72 | featureName :: ModuleName -> FeatureName
73 | featureName (ModuleName t) = FeatureName $ mkSymbol t
74 |
75 | -- PSのモジュール名では _ は多分使わないので大丈夫かな?
76 | ffiFeatureName :: ModuleName -> FeatureName
77 | ffiFeatureName (ModuleName s) = FeatureName $ mkSymbol $ s <> ffiFeatureSuffix
78 |
79 | ffiFeatureSuffix :: Text
80 | ffiFeatureSuffix = "._FOREIGN_"
81 |
82 | -- elispではシンボルは任意の文字列を
83 | -- ただしS式として表示する際適切なエスケープが必要(例えば空白や ' は \ でのエスケープが必要)。
84 | -- 参照 https://www.gnu.org/software/emacs/manual/html_node/elisp/Symbol-Type.html
85 | mkSymbol :: Text -> Symbol
86 | mkSymbol = UnsafeSymbol
87 |
88 | -- マクロや組込関数(built-ins, special-formも含む)の名前衝突も値スロットだけ使う分には考える必要はない。
89 | -- シンタックス上のキーワードではなく特別な関数が関数スロットに設定されている。
90 | -- 以下のように値スロットに束縛したところで問題ない。
91 | --
92 | -- (let ((defun 1) ;; defun is macro
93 | -- (let 2)) ;; let is special-form
94 | -- (+ defun let)) ;; => 3
95 | --
96 | -- ただ定数(特別なシンボルで,キーワードを除けば t, nilのみ)は束縛するとエラーが出る。
97 | -- https://www.gnu.org/software/emacs/manual/html_node/elisp/Constant-Variables.html
98 | -- Purescriptの識別子には使えない ^ 文字をケツにつける。
99 | localVar :: Ident -> Symbol
100 | localVar ident =
101 | mkSymbol $
102 | if t `elem` constants
103 | then t <> "^"
104 | else t
105 | where
106 | t = _identToText ident
107 | constants = ["t", "nil"]
108 |
109 | -- bindで結果を使わない場合,$__unusedというローカル変数が割当てられる。
110 | -- 最適化の際に識別できるよう識別子として提供する。
111 | -- 本来であれば型によって表現されるべきである。
112 | -- 関連issueが作成されていた記憶がある。
113 | localUnusedVar :: Symbol
114 | localUnusedVar = mkSymbol "$__unused"
115 |
116 | -- グローバル変数の衝突に関しては,PSモジュールのprefix(e.g. Foo.Bar.foo)を使うので衝突は基本起こらない。
117 | globalVar :: ModuleName -> Ident -> Symbol
118 | globalVar (ModuleName mn) ident =
119 | mkSymbol $ mn <> "." <> _identToText ident
120 |
121 | _identToText :: Ident -> Text
122 | _identToText (Ident t) = t
123 | _identToText (GenIdent mvar i) = fromMaybe "__instance" mvar <> textDisplay i
124 | _identToText UnusedIdent = error "impossible"
125 |
126 | -- | Bind
127 |
128 | -- top-level binding で Rec な binding はありえるが(相互再帰の場合),
129 | -- 定義順は関係ないので flatten すればいいだけ。
130 | decl :: ModuleName -> Bind Ann -> [DefVar]
131 | decl mn bind = map (uncurry decl') binds
132 | where
133 | decl' ident e =
134 | DefVar
135 | { name = globalVar mn ident
136 | , definition = give mn (expr e)
137 | }
138 | binds =
139 | case bind of
140 | NonRec _ ident expr -> [(ident, expr)]
141 | Rec bs -> map (over _1 snd) bs
142 |
143 | -- 現在処理中のモジュール名を返す。
144 | -- モジュール別の特殊対応が必要な場合に使う。
145 | -- 引数で渡す・readerモナド導入はかなり面倒なので reflectionライブラリを利用する。
146 | -- https://hackage.haskell.org/package/reflection-2.1.6/docs/Data-Reflection.html
147 | currentModuleName :: Given ModuleName => ModuleName
148 | currentModuleName = given
149 |
150 | -- | Expr
151 | expr :: Given ModuleName => Expr Ann -> SExp
152 | expr (Literal _ lit) = literal lit
153 | expr (Constructor _ tname cname ids) = constructor tname cname ids
154 | expr (Accessor _ ps e) = objectAccess ps (expr e)
155 | expr (ObjectUpdate _ e xs) = objectUpdate (map (over _2 expr) xs) (expr e)
156 | expr (Abs _ id e) = lambda1 (localVar id) (expr e)
157 | expr (App _ e0 e1) = funcall1 (expr e0) (expr e1)
158 | expr (Var _ qident) = var qident
159 | expr (Case _ es cas) = case' (map expr es) cas
160 | expr (Let _ binds e) = let' binds (expr e)
161 |
162 | -- nil 及び t は特別な定数でありは束縛やsetqはできない。
163 | -- vectorのリテラル表記 [a b] は a や b を評価しないことに注意。
164 | -- そのためvectorのリテラル表記ではなく vector関数を使う必要がある
165 | --
166 | -- A vector, like a string or a number, is considered a constant for evaluation:
167 | -- the result of evaluating it is the same vector. This does not evaluate or
168 | -- even examine the elements of the vector.
169 | --
170 | literal :: Given ModuleName => Literal (Expr Ann) -> SExp
171 | literal (NumericLiteral (Left i)) = integer i
172 | literal (NumericLiteral (Right d)) = double d
173 | literal (StringLiteral ps) = string $ psstring ps
174 | literal (CharLiteral c) = character c
175 | literal (BooleanLiteral b) = bool C.nil C.t b
176 | literal (ArrayLiteral exs) = funcallNative "vector" (map expr exs)
177 | literal (ObjectLiteral xs) = objectLiteral $ map (over _2 expr) xs
178 |
179 | -- 型チェックの都合上 Prim.undefinedという未定義の参照が入ることがある。
180 | -- (実装に問題がなければ)参照されることはないので適当な未定義の参照に置き換える。
181 | -- -> 違うっぽい。参照はされるが使われることはない,かな。なので 'ps-prim-undefined に。
182 | var :: Qualified Ident -> SExp
183 | var v@(Qualified mn id)
184 | | v == primUndefined = quotedSymbol "ps-prim-undefined"
185 | | otherwise = symbol (maybe localVar globalVar mn id)
186 | where
187 | primUndefined = mkQualified (Ident C.undefined) C.Prim
188 |
189 | -- Rec(相互参照と自己参照など) と NonRec があるので注意が必要。
190 | -- PSのletは順序関係なし(順序によってshadowingは変化しない)
191 | -- 全部 letrec で束縛してしまうのが多分正解かな?
192 | -- ただ殆どのケースで let* (頑張れば let)で十分なのに letrec は微妙か?
193 | -- NonRec のみなら let*,一つでも Rec があれば letrec でいいかな。
194 | let' :: Given ModuleName => [Bind Ann] -> SExp -> SExp
195 | let' binds = letC bindS
196 | where
197 | ext :: Bind a -> [((Ident, Expr a), Bool)]
198 | ext = \case
199 | NonRec _ ident expr -> [((ident, expr), False)]
200 | Rec bs -> map ((,True) . over _1 snd) bs
201 |
202 | binds' =
203 | mconcat $ map ext binds
204 |
205 | letC =
206 | if any snd binds'
207 | then letRec
208 | else letStar
209 |
210 | bindS =
211 | map (bimap localVar expr . fst) binds'
212 |
213 | -- pcaseマクロを利用する
214 | --
215 | -- 対象がリストなのはカンマ区切りで複数対象を指定できるので(e.g. case a, b of)
216 | -- 各CaseAlternativeは同じ数だけのbinderが必要。
217 | -- 複数指定の場合はリストに包んでpcaseに適用させる。(e.g. (pcase (list a b) ..))
218 | case' :: Given ModuleName => [SExp] -> [CaseAlternative Ann] -> SExp
219 | case' ss cas = pcase ss cases
220 | where
221 | -- マッチング節が一つしかなくガード節を使っている場合は cond が利用できる。
222 | cases :: [PcaseAlt SExp]
223 | cases = case cas of
224 | [] ->
225 | []
226 | [CaseAlternative bs (Left xs)] ->
227 | [ PcaseAlt
228 | { patterns = map binder bs
229 | , guard = Nothing
230 | , code = cond (map (bimap expr expr) xs)
231 | }
232 | ]
233 | cs ->
234 | concatMap caseAlt cs
235 |
236 | -- ガード毎に別のマッチングにする必要がある。
237 | -- あるマッチング節のいずれのガード節でも該当しない場合次のマッチング節に移る必要があるが,
238 | -- マッチング節一つでcondで分岐した場合,移ることができないため。そのためにリストを返している。
239 | -- 同じbinderなのにガード節毎にbinderが重複する形になるが仕方なし。
240 | caseAlt :: CaseAlternative Ann -> [PcaseAlt SExp]
241 | caseAlt (CaseAlternative bs e) = do
242 | (guard', ex) <- case e of
243 | Left xs -> do
244 | (guard, ex) <- xs
245 | pure (Just guard, ex)
246 | Right ex ->
247 | pure (Nothing, ex)
248 | pure $
249 | PcaseAlt
250 | { patterns = map binder bs
251 | , guard = expr <$> guard'
252 | , code = expr ex
253 | }
254 |
255 | -- (1)
256 | -- newtypeのマッチングの場合はConstructorBindersが呼ばれる。
257 | -- 当然newtypeなので下の値がそのまま入っているサブbinderは一つのはず。
258 | -- Annのメタ情報を見る必要がある。
259 | --
260 | -- (2)
261 | -- QualifiedのmnはMaybe ModuleNameである。
262 | -- どのようなケースでNothingとなるのか?
263 | binder :: Binder Ann -> PPattern SExp
264 | binder (NullBinder _) =
265 | PAny
266 | binder (LiteralBinder _ lit) =
267 | literalBinder lit
268 | binder (VarBinder _ id) =
269 | PBind $ localVar id
270 | binder (ConstructorBinder (_, _, _, Just IsNewtype) _ _ [b]) =
271 | binder b -- (1)
272 | binder (ConstructorBinder (_, _, _, Just IsNewtype) _ _ _bs) =
273 | error "Unexpected binding"
274 | binder (ConstructorBinder _ (Qualified (Just mn) tname) (Qualified _ cname) bs) =
275 | constructorBinder mn tname cname (map binder bs)
276 | binder (ConstructorBinder _ (Qualified Nothing tname) (Qualified _ cname) bs) =
277 | error "Unexpected binding" -- (2)
278 | binder (NamedBinder _ id b) =
279 | PAnd [PBind (localVar id), binder b]
280 |
281 | -- boolean binder と object binder がやっかい。
282 | -- boolean binder だからといって単に t, nil というシンボル使っても意味がない
283 | -- (pred ..) を使って nil か t(nil以外)を判別する必要がある。
284 | -- floatのbinderも使えないので pred + = を使う必要がある。
285 | literalBinder :: Literal (Binder Ann) -> PPattern SExp
286 | literalBinder (NumericLiteral (Left i)) =
287 | PInteger i
288 | literalBinder (NumericLiteral (Right d)) =
289 | PPred (lambda1 "v" (funcallNative "=" [symbol "v", double d]))
290 | literalBinder (StringLiteral ps) =
291 | PString $ psstring ps
292 | literalBinder (CharLiteral c) =
293 | PCharacter c
294 | literalBinder (BooleanLiteral b) =
295 | PPredBool b
296 | literalBinder (ArrayLiteral bs) =
297 | PBackquotedVector $ map binder bs
298 | literalBinder (ObjectLiteral xs) =
299 | objectLiteralBinder $ map (over _2 binder) xs
300 |
301 | -- | DataType
302 |
303 | -- データ型は先頭にタグの入ったベクターで表現する。
304 | -- ただし引数を持たないコンストラクタはシンボルのみで表現する。
305 | --
306 | -- e.g. Foo 1 2 -> ['Foo 1 2]
307 | -- e.g. Nothing -> 'Nothing
308 | --
309 | -- consturctorはデータ型の定義箇所で呼ばれる。
310 | -- (newtype のコンスラクタははidentity関数にbindされる)。
311 | -- 例えば Data.List.Types には List型が次のように定義されている。
312 | --
313 | -- data List a = Nil | Cons a (List a)
314 | --
315 | -- NilとConsコンストラクに対してそれぞれconstructor関数が呼ばれ,
316 | -- 次のコンストラクタ関数が定義される。
317 | --
318 | -- (defvar Data.List.Types.Nil (vector 'Nil))
319 | -- (defvar Data.List.Types.Cons (lambda (value0) (lambda (value1) (vector 'Cons value0 value1))))
320 | --
321 | -- ただList型に関しては特別に elisp に於けるリストで表現する。
322 | -- リストはlispに於いて普遍的に使われる構造でありリストを表現に持つデータ型により
323 | -- Elisp/PureScript間のデータの遣り取りが自然に書けるようになる。
324 | --
325 | -- Tupleも同様の理由で特別な表現を持たせる。Tuple a b は (cons a b) で表現する。
326 | -- ListのConsコンストラクタと混同しないように。
327 | --
328 | constructor ::
329 | Given ModuleName =>
330 | ProperName 'TypeName ->
331 | ProperName 'ConstructorName ->
332 | [Ident] ->
333 | SExp
334 | constructor (ProperName "List") cname ids
335 | | ModuleName "Data.List.Types" <- currentModuleName =
336 | case (cname, ids) of
337 | (ProperName "Nil", []) -> C.nil
338 | (ProperName "Cons", [_, _]) -> constructorNative "cons" ids
339 | _ -> error "Unexpected List constcutor"
340 | constructor (ProperName "Tuple") cname ids
341 | | ModuleName "Data.Tuple" <- currentModuleName =
342 | case (cname, ids) of
343 | (ProperName "Tuple", [_, _]) -> constructorNative "cons" ids
344 | _ -> error "Unexpected Tuple constcutor"
345 | constructor tname cname ids =
346 | case NonEmpty.nonEmpty (map localVar ids) of
347 | Just args ->
348 | let vals = map symbol $ NonEmpty.toList args
349 | in lambda1Fold args
350 | . funcallNative "vector"
351 | $ quotedSymbol (constructorTag cname) : vals
352 | Nothing ->
353 | quotedSymbol (constructorTag cname)
354 |
355 | -- 特別扱いのデータ型のみ使う
356 | constructorNative :: Symbol -> [Ident] -> SExp
357 | constructorNative name ids =
358 | lambda1Fold args (funcallNative name vals)
359 | where
360 | args = localVar <$> NonEmptyPartial.fromList ids
361 | vals = map symbol $ NonEmpty.toList args
362 |
363 | -- e.g. `[Foo ,e0 ,e1]
364 | constructorBinder ::
365 | ModuleName ->
366 | ProperName 'TypeName ->
367 | ProperName 'ConstructorName ->
368 | [PPattern SExp] ->
369 | PPattern SExp
370 | constructorBinder (ModuleName "Data.List.Types") (ProperName "List") cname binds =
371 | case (cname, binds) of
372 | (ProperName "Nil", []) -> PBackquotedList []
373 | (ProperName "Cons", [car, cdr]) -> PBackquotedCons car cdr
374 | _ -> error "Unexpected List binder"
375 | constructorBinder (ModuleName "Data.Tuple") (ProperName "Tuple") cname binds =
376 | case (cname, binds) of
377 | (ProperName "Tuple", [car, cdr]) -> PBackquotedCons car cdr
378 | _ -> error "Unexpected Tuple binder"
379 | constructorBinder _ _ cname [] =
380 | PSymbol (constructorTag cname)
381 | constructorBinder _ _ cname binds =
382 | PBackquotedVector $ PSymbol (constructorTag cname) : binds
383 |
384 | constructorTag :: ProperName 'ConstructorName -> Symbol
385 | constructorTag = mkSymbol . runProperName
386 |
387 | -- | Object
388 |
389 | -- Assocation List
390 | objectLiteral :: [(PSString, SExp)] -> SExp
391 | objectLiteral xs = alist $ map (over _1 objectField) xs
392 |
393 | -- pcaseに使われるbinder
394 | -- 複数ある場合は and で連結する必要がある。
395 | -- alistを使っての構造分解はできない。順序が異なるし,部分的にマッチングも有りえるため。
396 | -- (app (lambda (v) (alist-get ' v)) PATTERN) を利用する
397 | -- lambda1 で名前vが導入されているが,シャドウする危険はない。
398 | objectLiteralBinder :: [(PSString, PPattern SExp)] -> PPattern SExp
399 | objectLiteralBinder = \case
400 | [] -> PAny
401 | [(ps, bind')] -> bind (objectField ps, bind')
402 | bs -> PAnd $ map (bind . over _1 objectField) bs
403 | where
404 | bind (field, bind') =
405 | PApp
406 | ( lambda1
407 | "v"
408 | ( funcallNative
409 | "psel/alist-get"
410 | [ quotedSymbol field
411 | , symbol "v"
412 | ]
413 | )
414 | )
415 | bind'
416 |
417 | -- e.g. (cdr (assq 'foo obj))
418 | objectAccess :: PSString -> SExp -> SExp
419 | objectAccess fname obj =
420 | funcallNative
421 | "psel/alist-get"
422 | [ quotedSymbol (objectField fname)
423 | , obj
424 | ]
425 |
426 | -- 標準で非破壊的にalistを設定するための関数が提供されていない。
427 | -- (copy-alist + setf + alist-get で出来なくはないがややこい)。
428 | -- psel.el からimmutableにalist を更新するpset/alist-set関数を提供する。
429 | -- e.g. (psel/alist-set 'foo 1 (psel/alist-set 'bar "a" obj))
430 | --
431 | -- またレコードの更新構文を使ったとしても必ずしもObjectUpdateにコンパイルされるわけではない。
432 | -- (恐らく)レコードが小さければレコードリテラル+フィールド参照に置き換えられる。
433 | objectUpdate :: [(PSString, SExp)] -> SExp -> SExp
434 | objectUpdate updates obj = foldl' alistSet obj updates
435 | where
436 | alistSet obj (fname, s) =
437 | funcallNative
438 | "psel/alist-set"
439 | [ quotedSymbol (objectField fname)
440 | , s
441 | ]
442 |
443 | -- PSのフィールド名をそのままSymbolにして使う
444 | objectField :: PSString -> Symbol
445 | objectField = mkSymbol . psstring
446 |
447 | -- | PSString
448 |
449 | -- 参照
450 | -- https://hackage.haskell.org/package/purescript-0.13.8/docs/Language-PureScript-PSString.html#t:PSString
451 | -- https://github.com/purescript/purescript/issues/2434
452 | --
453 | -- 何やら厄介な話。
454 | -- Strings in PureScript are sequences of UTF-16 code units,
455 | -- which do not necessarily represent UTF-16 encoded text.
456 | --
457 | -- lone surrogatesが含まれる場合が厄介らしい。
458 | -- 恐らく殆んど使われることはないと思うので docodeStringで Nothingの場合はエラーを投げる
459 | -- https://hackage.haskell.org/package/purescript-0.13.8/docs/Language-PureScript-PSString.html#v:decodeString
460 | --
461 | psstring :: PSString -> Text
462 | psstring ps = case PS.decodeString ps of
463 | Just t -> t
464 | Nothing -> error "Unexpected one surragates in string literal"
465 |
--------------------------------------------------------------------------------
/test-bare.purs/.gitignore:
--------------------------------------------------------------------------------
1 | /bower_components/
2 | /node_modules/
3 | /.pulp-cache/
4 | /output/
5 | /output.*/
6 | /generated-docs/
7 | /.psc-package/
8 | /.psc*
9 | /.purs*
10 | /.psa*
11 | /.spago
12 |
--------------------------------------------------------------------------------
/test-bare.purs/packages.dhall:
--------------------------------------------------------------------------------
1 | {-
2 | Welcome to your new Dhall package-set!
3 |
4 | Below are instructions for how to edit this file for most use
5 | cases, so that you don't need to know Dhall to use it.
6 |
7 | ## Use Cases
8 |
9 | Most will want to do one or both of these options:
10 | 1. Override/Patch a package's dependency
11 | 2. Add a package not already in the default package set
12 |
13 | This file will continue to work whether you use one or both options.
14 | Instructions for each option are explained below.
15 |
16 | ### Overriding/Patching a package
17 |
18 | Purpose:
19 | - Change a package's dependency to a newer/older release than the
20 | default package set's release
21 | - Use your own modified version of some dependency that may
22 | include new API, changed API, removed API by
23 | using your custom git repo of the library rather than
24 | the package set's repo
25 |
26 | Syntax:
27 | where `entityName` is one of the following:
28 | - dependencies
29 | - repo
30 | - version
31 | -------------------------------
32 | let upstream = --
33 | in upstream
34 | with packageName.entityName = "new value"
35 | -------------------------------
36 |
37 | Example:
38 | -------------------------------
39 | let upstream = --
40 | in upstream
41 | with halogen.version = "master"
42 | with halogen.repo = "https://example.com/path/to/git/repo.git"
43 |
44 | with halogen-vdom.version = "v4.0.0"
45 | with halogen-vdom.dependencies = [ "extra-dependency" ] # halogen-vdom.dependencies
46 | -------------------------------
47 |
48 | ### Additions
49 |
50 | Purpose:
51 | - Add packages that aren't already included in the default package set
52 |
53 | Syntax:
54 | where `` is:
55 | - a tag (i.e. "v4.0.0")
56 | - a branch (i.e. "master")
57 | - commit hash (i.e. "701f3e44aafb1a6459281714858fadf2c4c2a977")
58 | -------------------------------
59 | let upstream = --
60 | in upstream
61 | with new-package-name =
62 | { dependencies =
63 | [ "dependency1"
64 | , "dependency2"
65 | ]
66 | , repo =
67 | "https://example.com/path/to/git/repo.git"
68 | , version =
69 | ""
70 | }
71 | -------------------------------
72 |
73 | Example:
74 | -------------------------------
75 | let upstream = --
76 | in upstream
77 | with benchotron =
78 | { dependencies =
79 | [ "arrays"
80 | , "exists"
81 | , "profunctor"
82 | , "strings"
83 | , "quickcheck"
84 | , "lcg"
85 | , "transformers"
86 | , "foldable-traversable"
87 | , "exceptions"
88 | , "node-fs"
89 | , "node-buffer"
90 | , "node-readline"
91 | , "datetime"
92 | , "now"
93 | ]
94 | , repo =
95 | "https://github.com/hdgarrood/purescript-benchotron.git"
96 | , version =
97 | "v7.0.0"
98 | }
99 | -------------------------------
100 | -}
101 | let upstream = {=} in upstream
102 |
--------------------------------------------------------------------------------
/test-bare.purs/spago.dhall:
--------------------------------------------------------------------------------
1 | {-
2 | Welcome to a Spago project!
3 | You can edit this file as you like.
4 |
5 | Need help? See the following resources:
6 | - Spago documentation: https://github.com/purescript/spago
7 | - Dhall language tour: https://docs.dhall-lang.org/tutorials/Language-Tour.html
8 |
9 | When creating a new Spago project, you can use
10 | `spago init --no-comments` or `spago init -C`
11 | to generate this file without the comments in this block.
12 | -}
13 | { name = "psel test"
14 | , dependencies = [] : List Text
15 | , packages = ./packages.dhall
16 | , backend = "psel"
17 | , sources = [ "test/**/*.purs" ]
18 | }
19 |
--------------------------------------------------------------------------------
/test-bare.purs/test/Main.el:
--------------------------------------------------------------------------------
1 | ;; -*- lexical-binding: t; -*-
2 |
3 | (defvar Test.Main.mkMainLike
4 | (lambda (f)
5 | (lambda ()
6 | (funcall f nil))))
7 |
--------------------------------------------------------------------------------
/test-bare.purs/test/Main.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | exports.mkMainLike = function(f) {
4 | return function() {
5 | return f();
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/test-bare.purs/test/Main.purs:
--------------------------------------------------------------------------------
1 | module Test.Main where
2 |
3 | import Test.PsEl.TCO
4 | import Test.PsEl.Record
5 | import Test.Utils
6 |
7 | -- Pre-prelude world. We don't have == nor Effect yet.
8 | -- Elisp特有の事情だが、レコードの比較は単純なequalでできないため別関数に分けている。
9 | -- spagoからのmain関数起動は無引数の呼出しを想定しているが、
10 | -- Effectモジュールが使えるまでFFIで関数を無引数関数で包む必要がある。
11 | foreign import data MainLike :: Type
12 | foreign import mkMainLike :: forall a. ({} -> a) -> MainLike
13 |
14 | main :: MainLike
15 | main = mkMainLike main'
16 |
17 | -- we don't have Effect nor Binding(do block) yet.
18 | main' :: {} -> Array (Array Boolean)
19 | main' _ =
20 | [ testLet {}
21 | , testCond {}
22 | , testCase {}
23 | , testTypeClass {}
24 | , testTCO {}
25 | , testRecord {}
26 | ]
27 |
28 | testCond :: {} -> Array Boolean
29 | testCond _ =
30 | [ assertEqual "if(1)" (f true) 1
31 | , assertEqual "if(2)" (f false) 0
32 | , assertEqual "guard(1)" (g true) "b"
33 | , assertEqual "guard(2)" (g false) "o"
34 | ]
35 | where
36 | f b = if b then 1 else 0
37 | g b
38 | | b = "b"
39 | | true = "o"
40 |
41 | testLet :: {} -> Array Boolean
42 | testLet _ =
43 | [ let a = 1
44 | b = [a, a]
45 | in assertEqual "let(1)" b [1,1]
46 | , let a [] = b [1]
47 | a [x] = b [x, x]
48 | a xs = xs
49 | b [] = a []
50 | b [x] = a [x]
51 | b [x, _] = a [x, x, x]
52 | b _ = []
53 | in assertEqual "letrec(1)" (b []) [1,1,1]
54 | ]
55 |
56 | testCase :: {} -> Array Boolean
57 | testCase _ =
58 | [ assertEqual "case int(1)" (caseInt 1) "a"
59 | , assertEqual "case int(2)" (caseInt 2) "b"
60 | , assertEqual "case int(3)" (caseInt 3) "c"
61 | , assertEqual "case number(1)" (caseNumber 1.1) "a"
62 | , assertEqual "case number(2)" (caseNumber 2.2) "b"
63 | , assertEqual "case number(3)" (caseNumber 1.11) "c"
64 | , assertEqual "case boolean(1)" (caseBoolean true) "true"
65 | , assertEqual "case boolean(2)" (caseBoolean false) "false"
66 | , assertEqual "case record(1)" (caseRecord { a: 1, b: "foo" }) "a"
67 | , assertEqual "case record(2)" (caseRecord { a: 2, b: "foo" }) "foo"
68 | , assertEqual "case record(3)" (caseRecord { a: 3, b: "foo" }) "c"
69 | , assertEqual "case array(1)" (caseArray []) "empty"
70 | , assertEqual "case array(2)" (caseArray ["a"]) "a"
71 | , assertEqual "case array(3)" (caseArray ["a","b","c"]) "b"
72 | , assertEqual "case array(4)" (caseArray ["a","b","b"]) "other"
73 | , assertEqual "case datatype(1)" (caseDataType Zero) "zero"
74 | , assertEqual "case datatype(2)" (caseDataType (One "a")) "a"
75 | , assertEqual "case datatype(3)" (caseDataType (Two "1" 1)) "1"
76 | , assertEqual "case datatype(4)" (caseDataType (Two "1" 2)) "other"
77 | , assertEqual "case newtype(1)" (caseNewType (NT "a")) "a!"
78 | , assertEqual "case newtype(2)" (caseNewType (NT "b")) "b"
79 | , assertEqual "case as(1)" (caseAs [[1,9],[3,4]]) [1,9]
80 | , assertEqual "case as(2)" (caseAs [[1,2],[3,9]]) [3,9]
81 | , assertEqual "case multiple(1)" (caseMultiple 1 "a") "1a"
82 | , assertEqual "case multiple(2)" (caseMultiple 1 "c") "c"
83 | , assertEqual "case multiple(3)" (caseMultiple 2 "a") "2"
84 | , assertEqual "case multiple(4)" (caseMultiple 1 "b") "b"
85 | , assertEqual "case multiple(5)" (caseMultiple 1 "c") "c"
86 | , assertEqual "case complex(1)" (caseComplex { a: [] }) []
87 | , assertEqual "case complex(2)" (caseComplex { a: [{a1: "a", a2: { a3: "b"}}] }) ["a"]
88 | , assertEqual "case complex(3)" (caseComplex { a: [{a1: "a", a2: { a3: "b"}}, {a1: "c", a2: { a3: "d"}}] }) ["a", "d"]
89 | , assertEqual "case guard(1)" (caseGuard { a: true, b: true }) "a"
90 | , assertEqual "case guard(2)" (caseGuard { a: true, b: false }) "a"
91 | , assertEqual "case guard(3)" (caseGuard { a: false, b: true }) "b"
92 | , assertEqual "case guard(4)" (caseGuard { a: false, b: false }) "c"
93 | ]
94 |
95 | caseInt :: Int -> String
96 | caseInt = case _ of
97 | 1 -> "a"
98 | 2 -> "b"
99 | _ -> "c"
100 |
101 | caseNumber :: Number -> String
102 | caseNumber = case _ of
103 | 1.1 -> "a"
104 | 2.2 -> "b"
105 | _ -> "c"
106 |
107 | caseBoolean :: Boolean -> String
108 | caseBoolean = case _ of
109 | true -> "true"
110 | false -> "false"
111 |
112 | caseRecord :: {a :: Int, b :: String} -> String
113 | caseRecord = case _ of
114 | { a: 1 } -> "a"
115 | { b: v, a: 2 } -> v
116 | _ -> "c"
117 |
118 | caseArray :: Array String -> String
119 | caseArray = case _ of
120 | [] -> "empty"
121 | [v] -> v
122 | ["a", v, "c"] -> v
123 | _ -> "other"
124 |
125 | data DT
126 | = Zero
127 | | One String
128 | | Two String Int
129 |
130 | caseDataType :: DT -> String
131 | caseDataType = case _ of
132 | Zero -> "zero"
133 | One s -> s
134 | Two s 1 -> s
135 | Two _ _ -> "other"
136 |
137 | newtype NT = NT String
138 |
139 | caseNewType :: NT -> String
140 | caseNewType = case _ of
141 | NT "a" -> "a!"
142 | NT s -> s
143 |
144 | caseAs :: Array (Array Int) -> Array Int
145 | caseAs = case _ of
146 | [t@[1, _], [3, 4]] -> t
147 | [[1, 2], v@[_, 9]] -> v
148 | _ -> []
149 |
150 | caseMultiple :: Int -> String -> String
151 | caseMultiple = case _,_ of
152 | 1, "a" -> "1a"
153 | 2, _ -> "2"
154 | _, "b" -> "b"
155 | _, _ -> "c"
156 |
157 | caseComplex :: { a :: Array { a1 :: String, a2 :: { a3 :: String }}} -> Array String
158 | caseComplex = case _ of
159 | { a: [{ a1: "a"}] } -> ["a"]
160 | { a: [{ a1: v1, a2: _}, { a1: _, a2: { a3: v2}} ] } -> [v1, v2]
161 | _ -> []
162 |
163 | caseGuard :: { a :: Boolean, b :: Boolean } -> String
164 | caseGuard = case _ of
165 | o
166 | | o.a -> "a"
167 | | o.b -> "b"
168 | o -> "c"
169 |
170 | testTypeClass :: {} -> Array Boolean
171 | testTypeClass _ =
172 | [ assertEqual "type class(1)" (m1 1) "int"
173 | , assertEqual "type class(2)" (m2 1) 1
174 | , assertEqual "type class(3)" (m1 "b") "b"
175 | , assertEqual "type class(4)" (m2 "b") 0
176 | ]
177 |
178 | class TC a where
179 | m1 :: a -> String
180 | m2 :: a -> Int
181 |
182 | instance tcInt :: TC Int where
183 | m1 _ = "int"
184 | m2 a = a
185 |
186 | instance tcString :: TC String where
187 | m1 s = s
188 | m2 _ = 0
189 |
--------------------------------------------------------------------------------
/test-bare.purs/test/PsEl/Record.purs:
--------------------------------------------------------------------------------
1 | module Test.PsEl.Record where
2 |
3 | import Test.Utils
4 |
5 | testRecord :: {} -> Array Boolean
6 | testRecord _ =
7 | [ assertEqual "record access(1)" rec1.a 1
8 | , assertEqual "record access(2)" rec1.a' 2
9 | , assertEqual "record access(3)" rec1." a+-*/{}()'\"\\ " 3
10 | , assertEqual "record access(4)" rec1."123" 4
11 | , assertEqualRecord "record update(1)" (rec2 { a = 2 }) { a:2, b:2 }
12 | , assertEqualRecord "record update(2)" (rec2 { b = 1 }) { a:1, b:1 }
13 | ]
14 | where
15 | -- レコード任意の文字列をフォールド名として持てる
16 | rec1 =
17 | { a: 1
18 | , a': 2
19 | , " a+-*/{}()'\"\\ ": 3
20 | , "123": 4
21 | }
22 |
23 | rec2 = { a: 1, b: 2 }
24 |
--------------------------------------------------------------------------------
/test-bare.purs/test/PsEl/TCO.purs:
--------------------------------------------------------------------------------
1 | module Test.PsEl.TCO where
2 |
3 | import Test.Utils
4 |
5 | testTCO :: {} -> Array Boolean
6 | testTCO _ =
7 | [ assertEqual "self recursion(1-1) 10" (selfRec1 0 10) 1
8 | , assertEqual "self recursion(1-2) 10000" (selfRec1 0 10000) 1
9 | , assertEqual "self recursion(2-1) 10" ((selfRec2 0 10) {}) 1
10 | , assertEqual "self recursion(2-2) 10000" ((selfRec2 0 10000) {}) 1
11 | , assertEqual "self recursion(3) 20" (selfRec3 0 20) 2
12 | , assertEqual "self recursion(4-1) 10" (selfRec4 0 10) 1
13 | -- , assertEqual "self recursion(4-2) 10000" (selfRec4 0 10000) 1
14 | , assertEqual "self recursion(5-1) 10" (selfRec5 0 10) 1
15 | -- , assertEqual "self recursion(5-2) 10000" (selfRec5 0 10000) 1
16 | , assertEqual "self recursion(6-1) 10" (selfRec6 0 10) 1
17 | -- , assertEqual "self recursion(6-2) 10000" (selfRec6 0 10000) 1
18 | , assertEqual "self recursion(7-1) 10" (selfRec7 0 10) 1
19 | , assertEqual "self recursion(7-2) 10000" (selfRec7 0 10000) 1
20 | , assertEqual "self recursion(8-1) 10" (selfRec8 10) 1
21 | , assertEqual "self recursion(8-2) 10" (selfRec8 10000) 1
22 | -- to check partial applied i is not changed
23 | , assertEqual "self recursion(8-3) 10" (selfRec8 10) 1
24 | ]
25 |
26 | selfRec1 :: Int -> Int -> Int
27 | selfRec1 i to
28 | | eqInt i to = 1
29 | | true = selfRec1 (succInt i) to
30 |
31 | selfRec2 :: Int -> Int -> {} -> Int
32 | selfRec2 i to
33 | | eqInt i to = \_ -> 1
34 | | true = selfRec2 (succInt i) to
35 |
36 | -- 末尾呼出しと非末尾呼出しが混在している場合、結構ややこい
37 | -- JS backend doesn't apply TCO for this function.
38 | -- 多分非末尾呼出しが混在しているからかな。末尾呼出しの方が取れほど呼ばれるかは不明だからか。
39 | selfRec3 :: Int -> Int -> Int
40 | selfRec3 i to
41 | | eqInt i to = 1
42 | | eqInt i 10 = succInt (selfRec3 (succInt i) to) -- non-recursive call
43 | | true = selfRec3 (succInt i) to -- recursive call
44 |
45 | -- lambda
46 | selfRec4 :: Int -> Int -> Int
47 | selfRec4 i to = go i
48 | where
49 | go i
50 | | eqInt i to = 1
51 | | true = selfRec4 (succInt i) to
52 |
53 | -- 再帰関数のローカル変数も再帰関数の場合。
54 | -- top-downに再帰関数をloop化した場合でも正常に動作する必要あり。
55 | -- これJS正しいのか???怪しいところ
56 | selfRec5 :: Int -> Int -> Int
57 | selfRec5 i to
58 | | eqInt i to = 1
59 | | eqInt i 10 = go i
60 | where
61 | go i
62 | | eqInt i 20 = go (succInt i)
63 | | true = selfRec5 i to
64 | | true = selfRec5 (succInt i) to
65 |
66 | -- JS backend doesn't optimize this case, though it should be able.
67 | -- Rare case.
68 | selfRec6 :: Int -> Int -> Int
69 | selfRec6 i to
70 | | eqInt i to = 1
71 | | true =
72 | let f = selfRec6 (succInt i)
73 | in f to
74 |
75 | -- ローカルな自己再帰関数
76 | selfRec7 :: Int -> Int -> Int
77 | selfRec7 i to = go i to
78 | where
79 | go i to
80 | | eqInt i to = 1
81 | | true = go (succInt i) to
82 |
83 | -- ローカルな自己再帰関数(部分適用版)
84 | selfRec8 :: Int -> Int
85 | selfRec8 = go 0
86 | where
87 | go i to
88 | | eqInt i to = 1
89 | | true = go (succInt i) to
90 |
91 |
92 | -- 型クラス使って辞書受け取るバージョンも
93 |
--------------------------------------------------------------------------------
/test-bare.purs/test/Utils.el:
--------------------------------------------------------------------------------
1 | ;; -*- lexical-binding: t; -*-
2 |
3 | (defvar Test.Utils.assertEqual
4 | (lambda (label)
5 | (lambda (a)
6 | (lambda (b)
7 | (message label)
8 | (if (equal a b)
9 | t
10 | (psel/assert-error label))))))
11 |
12 | (defvar Test.Utils.assertEqualRecord
13 | (lambda (label)
14 | (lambda (a)
15 | (lambda (b)
16 | (message label)
17 | (if (psel/alist-equal a b)
18 | t
19 | (psel/assert-error label))))))
20 |
21 | (defvar Test.Utils.eqInt
22 | (lambda (a)
23 | (lambda (b)
24 | (= a b))))
25 |
26 | (defvar Test.Utils.succInt
27 | (lambda (i)
28 | (1+ i)))
29 |
--------------------------------------------------------------------------------
/test-bare.purs/test/Utils.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | exports.assertEqual = function(label) {
4 | return function(a) {
5 | return function(b) {
6 | console.log(label);
7 | // 取り敢えず実装
8 | return true;
9 | // if (a == b) {
10 | // return true;
11 | // } else {
12 | // throw new Error(label);
13 | // }
14 | };
15 | };
16 | };
17 |
18 | exports.assertEqualRecord = function(label) {
19 | return function(a) {
20 | return function(b) {
21 | console.log(label);
22 | // 取り敢えず実装
23 | return true;
24 | };
25 | };
26 | };
27 |
28 | exports.eqInt = function(a) {
29 | return function(b) {
30 | return a == b;
31 | };
32 | };
33 |
34 | exports.succInt = function(a) {
35 | return a + 1;
36 | };
37 |
--------------------------------------------------------------------------------
/test-bare.purs/test/Utils.purs:
--------------------------------------------------------------------------------
1 | module Test.Utils where
2 |
3 | foreign import assertEqual :: forall a. String -> a -> a -> Boolean
4 | foreign import assertEqualRecord :: forall r. String -> { | r } -> { | r } -> Boolean
5 |
6 | -- for tests
7 | foreign import eqInt :: Int -> Int -> Boolean
8 | foreign import succInt :: Int -> Int
9 |
--------------------------------------------------------------------------------
/test-lib.purs/.gitignore:
--------------------------------------------------------------------------------
1 | /bower_components/
2 | /node_modules/
3 | /.pulp-cache/
4 | /output/
5 | /output.*/
6 | /generated-docs/
7 | /.psc-package/
8 | /.psc*
9 | /.purs*
10 | /.psa*
11 | /.spago
12 |
--------------------------------------------------------------------------------
/test-lib.purs/spago.dhall:
--------------------------------------------------------------------------------
1 | { name = "test-lib"
2 | , dependencies =
3 | [ "prelude"
4 | , "effect"
5 | , "console"
6 | , "assert"
7 | , "refs"
8 | , "lists"
9 | , "functions"
10 | , "tuples"
11 | ]
12 | , packages =
13 | https://raw.githubusercontent.com/psel-org/package-sets/main/src/el-0.14.5-20211116/packages.dhall
14 | , backend = "psel"
15 | , sources = [ "test/**/*.purs" ]
16 | }
17 |
--------------------------------------------------------------------------------
/test-lib.purs/test/Main.el:
--------------------------------------------------------------------------------
1 | ;; -*- lexical-binding: t; -*-
2 |
3 | (defvar Test.Main.nativeList
4 | '(1 2 3))
5 |
6 | (defvar Test.Main.add2
7 | (lambda (a b)
8 | (+ a b)))
9 |
--------------------------------------------------------------------------------
/test-lib.purs/test/Main.purs:
--------------------------------------------------------------------------------
1 | module Test.Main where
2 |
3 | import Prelude
4 | import Effect
5 | import Effect.Ref as Ref
6 | import Effect.Console (log)
7 | import Test.Assert (assertEqual)
8 | import Data.List
9 | import Data.Tuple as Tuple
10 | import Data.Function.Uncurried
11 |
12 | main :: Effect Unit
13 | main = do
14 | testApply
15 | testEffect
16 | testList
17 | testTuple
18 | testFunctionUncurried
19 |
20 | testApply :: Effect Unit
21 | testApply = do
22 | log "apply(1)"
23 | assertEqual
24 | { actual: (+) 1 $ 1
25 | , expected: 2
26 | }
27 | log "apply(2)"
28 | assertEqual
29 | { actual: (+) 1 $ (+) 1 $ 1
30 | , expected: 3
31 | }
32 | log "applyFlipped(1)"
33 | assertEqual
34 | { actual: 1 # (+) 1
35 | , expected: 2
36 | }
37 | log "applyFlipped(2)"
38 | assertEqual
39 | { actual: 1 # (+) 1 # (+) 1
40 | , expected: 3
41 | }
42 |
43 | testEffect :: Effect Unit
44 | testEffect = do
45 | log "effect(1)"
46 | v <- testEffect1
47 | assertEqual { actual: v, expected: 4 }
48 |
49 | -- disgarding effects
50 | testEffect1 :: Effect Int
51 | testEffect1 = do
52 | ref <- Ref.new 1
53 | succVoid ref
54 | _ <- succ ref
55 | succVoid ref
56 | val <- Ref.read ref
57 | pure val
58 | where
59 | succ ref = Ref.modify (_ + 1) ref
60 | succVoid ref = void $ Ref.modify (_ + 1) ref
61 |
62 | foreign import nativeList :: List Int
63 |
64 | testList :: Effect Unit
65 | testList = do
66 | let l = (1..3)
67 | log "list(1)"
68 | assertEqual { actual: nativeList, expected: l }
69 | log "list(2)"
70 | assertEqual { actual: f nativeList, expected: 6 }
71 | assertEqual { actual: f l, expected: 6 }
72 | where
73 | f (Cons a (Cons b (Cons c Nil))) = a + b + c
74 | f _ = 0
75 |
76 | testTuple :: Effect Unit
77 | testTuple = do
78 | log "tuple(1)"
79 | assertEqual { actual: Tuple.fst a, expected: 1 }
80 | log "tuple(2)"
81 | assertEqual { actual: Tuple.snd a, expected: "a" }
82 | where
83 | a = Tuple.Tuple 1 "a"
84 |
85 | foreign import add2 :: Fn2 Int Int Int
86 |
87 | add2Wrapper :: Int -> Int -> Int
88 | add2Wrapper a b = runFn2 add2 a b
89 |
90 | testFunctionUncurried :: Effect Unit
91 | testFunctionUncurried = do
92 | log "function uncurried(1)"
93 | assertEqual { actual: runFn2 add2 1 2, expected: 3 }
94 | log "function uncurried(2)"
95 | let t = runFn2 add2 1
96 | assertEqual { actual: t 3, expected: 4 }
97 | assertEqual { actual: t 4, expected: 5 }
98 | log "function uncurried(3)"
99 | assertEqual { actual: add2Wrapper 1 2, expected: 3 }
100 |
--------------------------------------------------------------------------------