Repository: wende/elmchemy
Branch: master
Commit: 21650416e634
Files: 70
Total size: 215.4 KB
Directory structure:
gitextract_du00lrkm/
├── .gitattributes
├── .gitignore
├── .gitmodules
├── .npmrc
├── .tool-versions
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── ISSUE_TEMPLATE
├── LICENSE
├── Main.elm
├── Makefile
├── README.md
├── book.json
├── bump.sh
├── elchemy
├── elchemy_ex/
│ ├── .gitignore
│ ├── README.md
│ ├── config/
│ │ └── config.exs
│ ├── elm/
│ │ └── Hello.elm
│ ├── elm-package.json
│ ├── mix.exs
│ └── test/
│ ├── elchemy_ex_test.exs
│ └── test_helper.exs
├── elchemy_node.js
├── elm-package.json
├── elmchemy
├── lib/
│ └── mix/
│ ├── mix.exs
│ └── tasks/
│ └── compile.elchemy.ex
├── mix.exs
├── package.json
├── roadmap/
│ ├── BASIC_TYPES.md
│ ├── COMMENTS.md
│ ├── FLAGS.md
│ ├── FUNCTIONS.md
│ ├── INLINING.md
│ ├── INSTALLATION.md
│ ├── INTEROP.md
│ ├── MODULES.md
│ ├── README.md
│ ├── SIDE_EFFECTS.md
│ ├── STRUCTURES.md
│ ├── SUMMARY.md
│ ├── SYNTAX.md
│ ├── TESTING.md
│ ├── TROUBLESHOOTING.md
│ ├── TYPES.md
│ └── TYPE_ALIASES.md
├── src/
│ └── Elchemy/
│ ├── Alias.elm
│ ├── Ast.elm
│ ├── Compiler.elm
│ ├── Context.elm
│ ├── Expression.elm
│ ├── Ffi.elm
│ ├── Function.elm
│ ├── Helpers.elm
│ ├── Meta.elm
│ ├── Operator.elm
│ ├── Selector.elm
│ ├── Statement.elm
│ ├── Type.elm
│ └── Variable.elm
├── templates/
│ ├── Hello.elm
│ ├── elchemy.exs
│ ├── elchemy_test.exs
│ └── elm-package.json
└── tests/
├── .gitignore
├── Main.elm
├── Tests.elm
├── UnitTests.elm
└── elm-package.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
stable/* linguist-vendored
example/* linguist-vendored
================================================
FILE: .gitignore
================================================
.DS_Store
elm-stuff/
node_modules/
elchemy.js
output.ex
example/elm.js
.#*
.elchemy*
*.orig
elixir-stuff/
elchemy_ex/lib/*
*un~
elchemy-*.ez
_build/
docs/
example/
stable/
_book/
.elixir_ls/
.vscode/
test_project/
================================================
FILE: .gitmodules
================================================
[submodule "elchemy-core"]
path = elchemy-core
url = https://github.com/wende/elchemy-core.git
================================================
FILE: .npmrc
================================================
tag-version-prefix = ""
================================================
FILE: .tool-versions
================================================
elm 0.18.0
elixir 1.7.4
================================================
FILE: .travis.yml
================================================
language: elixir
elixir:
- 1.5.0
- 1.6.4
- 1.7.0
- 1.7.1
- 1.7.3
otp_release:
- 20.0
- 21.0
matrix:
exclude:
# Elixir 1.5.0 doesn't work with OTP 21
- elixir: 1.5.0
otp_release: 21.0
# Elixir 1.6.4 doesn't work with OTP 21
- elixir: 1.6.4
otp_release: 21.0
os:
- linux
cache:
directories:
- test/elm-stuff/build-artifacts
- sysconfcpus
before_install:
- if [ ${TRAVIS_OS_NAME} == "osx" ];
then brew update; brew install nvm; mkdir ~/.nvm; export NVM_DIR=~/.nvm; source $(brew --prefix nvm)/nvm.sh;
fi
- echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
- | # epic build time improvement - see https://github.com/elm-lang/elm-compiler/issues/1473#issuecomment-245704142
if [ ! -d sysconfcpus/bin ];
then
git clone https://github.com/obmarg/libsysconfcpus.git;
cd libsysconfcpus;
./configure --prefix=$TRAVIS_BUILD_DIR/sysconfcpus;
make && make install;
cd ..;
fi
install:
- nvm install 6.11.2
- nvm use 6.11.2
- node --version
- npm --version
- npm install -g elm@0.18.0
- mv $(npm config get prefix)/bin/elm-make $(npm config get prefix)/bin/elm-make-old
- printf '%s\n\n' '#!/bin/bash' 'echo "Running elm-make with sysconfcpus -n 2"' '$TRAVIS_BUILD_DIR/sysconfcpus/bin/sysconfcpus -n 2 elm-make-old "$@"' > $(npm config get prefix)/bin/elm-make
- chmod +x $(npm config get prefix)/bin/elm-make
- npm install
- elm package install --yes
- cd elchemy-core/
- mix local.rebar --force # for Elixir 1.3.0 and up
- mix local.hex --force
- mix deps.get
- cd ../
- make compile
- make compile-std
after_failure:
- find elchemy-core/lib | xargs cat
script:
- make test-all
notifications:
email: false
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at krzysztof.wende@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: ISSUE_TEMPLATE
================================================
### Check first
If anything doesn't work, try
```
npm install -g elchemy
elchemy clean
elchemy init
mix test
```
To experiment with the latest Elchemy version of the parser online go to
https://wende.github.io/elchemy/stable/
### Template:
Add:
## Example:
```elm
```
->
```elixir
```
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2017 Krzysztof Wende
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Main.elm
================================================
port module Main exposing (main)
import Html exposing (..)
import Html
import Elchemy.Compiler as Compiler
import Markdown
type Msg
= Replace String
| String
view : String -> Html Msg
view model =
Markdown.toHtml [] <| "```elixir\n" ++ (Compiler.tree model) ++ "\n```"
init : String -> ( String, Cmd Msg )
init v =
( v, Cmd.none )
main : Program String String Msg
main =
Html.programWithFlags
{ init = init
, update = update
, view = view
, subscriptions =
(\_ ->
Sub.batch
[ updateInput Replace
]
)
}
update : Msg -> String -> ( String, Cmd Msg )
update action model =
case action of
Replace m ->
( m, Cmd.none )
String ->
( "", Cmd.none )
port updateInput : (String -> msg) -> Sub msg
================================================
FILE: Makefile
================================================
dev:
pkill -f http-server &
echo "Make sure to install http-server with npm i -g http-server"
elm-make Main.elm --output=example/elm.js --debug
(http-server ./example/ -p 8081 -c-1 &) && open "http://127.0.0.1:8081"
release:
elm-make Main.elm --output=example/elm.js
mkdir -p docs/stable
cp -r example/ docs/stable/
compile:
elm-make Main.elm --yes --output compiled.js
sed 's/var Elm = {}/&; \
require(".\/elchemy_node.js").execute(_user$$project$$Elchemy_Compiler$$tree, _user$$project$$Elchemy_Compiler$$fullTree, _user$$project$$Elchemy_Compiler$$treeAndCommons)/' compiled.js > elchemy.js
rm compiled.js
compile-watch:
find . -name "*.elm" | grep -v "elm-stuff" | grep -v .# | entr make compile
test:
./node_modules/.bin/elm-test
test-all:
make test
make test-std
make compile-elixir # Change to compile-elixir-and-test when let..in fixed completely
make test-project
test-project:
rm -rf test_project
mix new test_project
cd test_project ; \
(yes | ../elchemy init) ;\
../elchemy compile elm lib ;\
cp -r ../elchemy-core/lib lib/elm-deps ;\
cp -r ../elchemy-core/elm lib/elm-deps-elixir-files ;\
mix test
test-std:
cd elchemy-core/ && mix test
compile-std:
cd elchemy-core && rm -rf .elchemy
make compile-std-incremental
compile-std-incremental:
make compile
cd elchemy-core && ../elchemy compile elm lib
compile-std-watch:
find elchemy-core -name "*.elm" | grep -v ".#" | grep -v "elm-stuff" | entr make compile-std
compile-std-tests-watch:
find elchemy-core \( -name "*.elm" -or -name '*.ex*' \) | grep -v "elchemy.ex" | grep -v ".#" | grep -v "elm-stuff" | entr bash -c "make compile && make compile-std && make test-std"
compile-incremental-std-tests-watch:
find elchemy-core \( -name "*.elm" -or -name '*.ex*' \) | grep -v "elchemy.ex" | grep -v ".#" | grep -v "elm-stuff" | entr bash -c "make compile && make compile-std-incremental && make test-std"
tests-watch:
find . -name "*.elm" | grep -v ".#" | grep -v "elm-stuff" | entr ./node_modules/.bin/elm-test
install-sysconf:
git clone "https://github.com/obmarg/libsysconfcpus.git"
cd libsysconfcpus && ./configure && make && make install
cd .. && rm -rf libsysconfcpus
compile-elixir:
make compile
rm -rf elchemy_ex/elm-deps
rm -rf elchemy_ex/.elchemy
cd elchemy_ex && ../elchemy compile ../src lib
compile-elixir-and-run:
make compile-elixir
cd elchemy_ex && mix compile
compile-elixir-and-test:
make compile-elixir
cd elchemy_ex && mix test
build-docs:
cd ../elchemy-page && git checkout master && git pull && elm install && yarn && yarn build
rm -rf docs/*
cp -r ../elchemy-page/dist/* docs/
================================================
FILE: README.md
================================================
#### Quick install
```shell
npm install -g elchemy
```
# What is it?
Elchemy lets you write simple, fast and quality type safe code while leveraging both the Elm's safety and Elixir's ecosystem
### In case of any questions about the project feel free to submit them in Issues with Q&A label
# Features
- **Type inference:** Powerful type inference means you rarely have to annotate types. Everything gets checked for you by the compiler
- **Easy and type-safe interop**: You can call Elixir/Erlang without any extra boiler-plate. All the calls you make are checked in terms of type-safety as thoroughly as possible based on Elixir's typespecs.
- **All the best of Elm and Elixir**: Elchemy inherits what's best in Elm - type safety, inference and extreme expressiveness, but also what's best in Elixir - Doc-tests, tooling and obviously the entire BEAM platform.
- **Nearly no runtime errors** - Elchemy's type system **eliminates almost all runtime errors**. With a shrinking set of edge cases, your entire Elchemy codebase is safe. Elixir parts of the codebase are the only ones to be a suspect in cases of runtime errors happening.
- **Beautiful and fully readable output** - The produced code is idiomatic, performant and can be easily read and analyzed without taking a single look at the original source.
# Maturity of the project
- Parser - **99%** of Elm's syntax (see [elm-ast](https://github.com/Bogdanp/elm-ast/issues))
- Compiler - **90%** (Sophisticated incremental compilation.
No support for Windows yet though ([#287](https://github.com/wende/elchemy/issues/287)) and also big reliance on unix tools ([#288](https://github.com/wende/elchemy/issues/288))
- Elchemy-core - **95%** ( Everything covered except side effects and JSON Decoders)
- Interop with Elixir - **90%** - Purity tests ([#162](https://github.com/wende/elchemy/issues/162)) and handling of macro-heavy libraries ([#276](https://github.com/wende/elchemy/issues/276)) to go
- Ideology - **70%** - We've got a pretty solid idea of where Elchemy is going
- Documentation - **80%** - There are two tutorials and a complete Gitbook documentation. Few entrance level tutorials though, this project tries to change it.
- Elchemy-effects - **20%** - You can't and shouldn't write anything with side-effects in Elchemy yet. We're working on finding the best solution for effects that would fit both Elm's and Elixir's community (see [#297](https://github.com/wende/elchemy/issues/297) for more info)
- Elchemy-core for Erlang VM - **5%** - Everything for os related tasks like filesystem, OTP goodies etc are yet to be done
- Elchemy type checker - **20%** - Self-hosted elchemy type inference algorithm, written by @wende and inspired by @Bogdanp.
# Usage
### Prerequisites
- [elixir@1.4.0-1.6.x](https://elixir-lang.org/install.html)
- [node@5+](https://nodejs.org/en/)
- [elm-lang@0.18.0](https://guide.elm-lang.org/install.html) (`npm install -g elm@0.18.0`)
- [elm-github-install@0.1.2](https://github.com/gdotdesign/elm-github-install) - Compiler will install it automatically for you, if you don't have it yet.
### Installation in an existing Elixir project
Install `elchemy` globally with:
```shell
npm install -g elchemy
```
Then, in the root directory of your project do:
```shell
elchemy init
```
And follow the instructions.
`elchemy` will find all `*.elm` files specified in `elchemy_path` and compile it into corresponding `*.ex` files in `lib` directory.
You can override output directory specifying `elixirc_paths`.
### Installation as a standalone
```shell
npm install -g elchemy
```
Usage
```
elchemy compile source_dir output_dir
```
### Recommended editors setup
- [Atom](https://atom.io/) with [elixir-language](https://atom.io/packages/language-elixir) and [atom-elixir](https://github.com/msaraiva/atom-elixir) or [elixir-ide](https://atom.io/packages/ide-elixir) for Elixir; and [language-elm](https://atom.io/packages/language-elm) + [elmjutsu](https://atom.io/packages/elmjutsu) for Elchemy.
- [Visual Studio Code](https://code.visualstudio.com/) with [vscode-elixir](https://marketplace.visualstudio.com/items?itemName=mjmcloug.vscode-elixir), [vscode-elixir-ls](https://github.com/JakeBecker/vscode-elixir-ls) and [vscode-elm](https://github.com/elm-tooling/elm-language-client-vscode)
### Build from source
```
git clone https://github.com/wende/elchemy.git
cd elchemy
make compile
./elchemy compile source_dir output_dir
```
and
```
make dev
```
In order to launch and test the web demo.
## Troubleshooting
If something doesn't work, try
```
npm install -g elchemy
elchemy clean
elchemy init
mix test
```
first
# FAQ
## Why *would* I want to use that?
- You like types
- But even more you prefer compile-time errors over run-time error
- You prefer `add b c = b + c` over `defp add(a, b), do: b + c`
- You like curry
- You think failing fast is cool, but not as cool as not failing at all
## Why *wouldn't* I want to use that?
- Your project relies on die-hard battle tested libraries, and you despise any versions starting with 0
- You're afraid that when you learn what Monad is your mustache will grow, and eyesight weaken
## Can I use it in already existing Elixir project?
You can, but nice and dandy compile tools are still on their way
## Will my employer notice I'm having an affair with Elchemy?
The output files of Elchemy treat the code readability as a first class citizen. The code is meant to be properly indented, the comments aren't omitted, and the code is optimized as hard as it can ( f.i case clauses reduce to function overloads)
## When will Elchemy become 1.0.0?
Once it's done, so as it is supposed to be in the so called semantic versioning. :innocent:
## Can I contribute?
Definitely. Yes. Please do. :two_hearts:
## How are types represented?
You're a nosy one, aren't you? :smile: Elchemy represents all type constructors as snake cased atoms, and all type applications as tuples. Which means that `MyType 42 "Forty two" Error` in Elchemy equals to `{:my_type, 42, "Forty Two", :error}` in Elixir.
## Can I use already existing Elm libraries with Elchemy?
As long as they don't use any Native modules, Ports or Elm runtime they can be safely imported and used
## Can I use already existing Elixir libraries with Elchemy?
Yes. You can do an `ffi` call to any function in any module. Whether it's Elixir module, Erlang module, or even a macro you can include it in your code. Ffi calls are a treated specially in Elchemy, and they get generated test to analyze the types based on @specs, so that you don't compromise type safety for using Elixir code.
In order to increase readability it's advised not to use `ffi` calls if not necessary and always document and doctest them.
## But what about out of function macros? Like tests and `use Module`?
Unfortunately you can't write any macros with `do..end` blocks yet. You can write any out of function code using an elixir inline code with:
```elm
{- ex
*code_here*
-}
```
But it is a last resort solution and shouldn't ever be abused.
## Can I define an Elixir macro in Elchemy?
So you want to write an Elm-like code, that will manipulate Elixir code, which generates an Elixir code that manipulates Elixir code? How about no?
## Do I need to have Elm installed to compile my `.elm` files with Elchemy?
Elchemy uses Elm to typecheck your program. It is possible to use it without Elm on your machine, while it's not advised.
# Contributor credits:
- Tomasz Cichociński - [@baransu](https://github.com/baransu)
- Colin Bankier - [@colinbankier](https://github.com/colinbankier)
- Nathaniel Knight - [@neganp](https://github.com/neganp)
# Inspiration:
- [Elm](https://github.com/elm/compiler) by Evan Czaplicki - [@evancz](https://github.com/evancz)
- [Elixir](https://github.com/elixir-lang/elixir) by José Valim - [@josevalim](https://github.com/josevalim)
- [Elm-AST](https://github.com/bogdanp/elm-ast) by Bogdan Popa - [@bogdanp](https://github.com/bogdanp)
# Contributing Guide
- Everyone is welcome to contribute :hugs:
- Refer to https://bogdanp.github.io/elm-ast/example/ to have better understanding of parsed tokens.
- Refer to https://wende.github.io/elchemy/stable/ to know the latest development version of the parser
- For project management we use ZenHub. You can see the Kanban board, card estimates and all the reports by installing a browser extension here: [Opera/Chrome](https://chrome.google.com/webstore/detail/zenhub-for-github/ogcgkffhplmphkaahpmffcafajaocjbd), [Firefox](zenhub.com)
## Targeted values:
- Fully readable and indented elixir code generated from compilation
- Seamless and stress less interop with existing Elixir code, preferably with magically working type safety
- Full integration with entire elm syntax for editors and compilers magic
================================================
FILE: book.json
================================================
{
"root" : "./roadmap"
}
================================================
FILE: bump.sh
================================================
#!/bin/bash
set -e
if [ -z "$1" ]; then
echo 'usage ./bump.sh [ | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git]'
exit 0
fi
if git diff-index --quiet HEAD --; then
CHANGELOG=`git changelog -x --tag VER`
npm version $1
SEMVER='[0-9][0-9]*\.[0-9][0-9]*\.[0-9]*-*[0-9]*'
VER=`npm ls | grep -o elchemy@$SEMVER | grep -o $SEMVER`
CHANGELOG=${CHANGELOG/VER/$VER}
echo "$CHANGELOG"
make compile-std
cd elchemy-core
CORE_BRANCH=`git branch | grep \* | cut -d ' ' -f2`
sed -i "" "s/$SEMVER/$VER/g" mix.exs
git pull origin $CORE_BRANCH
git commit -am "Release $VER"
git tag $VER
if ! [[ $* == *-n* ]]; then
git push origin $CORE_BRANCH $VER
fi
cd ..
ELCHEMY_BRANCH=`git branch | grep \* | cut -d ' ' -f2`
sed -i "" "s/$SEMVER/$VER/g" mix.exs
sed -i "" "s/elchemy-core\": \"0.0.0 <= v < $SEMVER/elchemy-core\": \"0.0.0 <= v < $VER/g" ./templates/elm-package.json
rm -f elchemy-*.ez
mix archive.build
mix archive.install "elchemy-$VER.ez" --force
git pull origin $ELCHEMY_BRANCH
sed -i "" "s/$SEMVER/$VER/g" src/Elchemy/Compiler.elm
make compile
make build-docs
make release
git add docs/ -f
sed -i "" "s/version=\"$SEMVER\"/version=\"$VER\"/g" ./elchemy
sed -i "" "s/name\": \"elchemy\"/name\": \"elmchemy\"/g" package.json
if ! [[ $* == *-n* ]]; then
npm publish
fi
sed -i "" "s/name\": \"elmchemy\"/name\": \"elchemy\"/g" package.json
if ! [[ $* == *-n* ]]; then
npm publish
fi
git tag -d "$VER"
git commit -am "$CHANGELOG"
git tag $VER
if ! [[ $* == *-n* ]]; then
git push origin $ELCHEMY_BRANCH $VER
hub release create -p -a "elchemy-$VER.ez" $VER
fi
else
echo "Git directory must be clean"
exit 1
fi
================================================
FILE: elchemy
================================================
#!/bin/bash
version="0.8.8"
if ! $(elm -v 2> /dev/null | grep 0.18 > /dev/null ); then
echo "Elchemy requires elm 0.18. Install it and make sure it's available system-wide."
echo "For 0.19 support visit https://github.com/wende/elchemy/issues/348"
exit 1
fi
set -e
VERBOSE=false
if [[ $* == *--verbose* ]]; then
VERBOSE=true
fi
function create_file {
local file=$1
if [[ ${file} == *"elm-stuff/packages"* ]]; then
file=${file/elm-stuff\/packages/elm-deps}
fi
mkdir -p `dirname $file`
echo "" > $file
echo "$file"
}
SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
SOURCE="$(readlink "$SOURCE")"
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done
SOURCE_DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
case "$1" in
version)
echo "Elchemy $version"
;;
clean)
rm -rf ./elm-deps
rm -rf ./elm-stuff
rm -rf ~/.elm-install/github.com/
find . | grep "\.elchemy.ex" | xargs rm -f
rm -rf .elchemy
;;
new)
mix new $2
cd $2
$SOURCE_DIR/elchemy init
;;
init)
if [ -a ./mix.exs ]
then
cp $SOURCE_DIR/templates/elchemy.exs ./.elchemy.exs
mix archive.install "https://github.com/wende/elchemy/releases/download/$version/elchemy-$version.ez"
mkdir -p elm
cp $SOURCE_DIR/templates/elm-package.json ./
cp $SOURCE_DIR/templates/Hello.elm ./elm/
if [ -d ./test ]; then
cp $SOURCE_DIR/templates/elchemy_test.exs ./test/
fi
printf "Elchemy $version initialised. Make sure to add:\n\n\t|> elem(Code.eval_file(\".elchemy.exs\"), 0).init\n\nto your mix.exs file as the last line of the project() function.\nThis pipes the project keyword list to the elchemy init function to configure some additional values.\n\nThen run mix test to check if everything went fine\n"
printf "\nelm-deps" >> .gitignore
printf "\nelm-stuff" >> .gitignore
printf "\node_modules" >> .gitignore
printf "\.elchemy" >> .gitignore
else
printf "ERROR: No elixir project found. Make sure to run init in a project"
fi
;;
compile)
# Create ./.elchemy if doesn't exist
mkdir -p ".elchemy"
MTIME="$(cat ".elchemy/mtime" 2> /dev/null || echo "1995-04-10 23:35:02")"
echo "" > .elchemy/output
if [ ! -d ./elm-deps ] || [[ ! $(find ./elm-package.json -newermt "$MTIME") == "" ]]; then
if ! hash elm-github-install 2>/dev/null; then
echo "No elm-github-install found. Installing..."
npm i -g elm-github-install@1.6.1
fi
echo "-- Downloading Elchemy deps --"
elm-install
fi
# Copy all elixir files that are inside packages
echo "-- Copying Elixir native files --"
for f in `{ find -L elm-stuff/packages -name "*.ex*" | grep -v "\.elchemy\.ex" ;}`
do
if [ $VERBOSE = true ]; then
echo "FOUND $f"
fi
file="${file/^elm\//lib\//}"
file=$(create_file $f)
if [ $VERBOSE = true ]; then
echo "TO $file"
fi
cp $f $file
done
i=0
echo "-- Compiling Elm files --"
# Find all elm files inside packages and compile them
for f in `{ find $2 -name "*.elm" -newermt "$MTIME" | grep -v "elm-stuff" | grep -v "#." ; find -L elm-stuff/packages -name "*.elm" -newermt "$MTIME" | grep -v "/tests/" | grep -v "/example/" ;}`
do
if [[ ${f} == *"elm-lang"* ]] || [[ ${f} == *"Elchemy.elm"* ]]; then
continue
fi
echo "----------"
echo "Type Checking $f"
echo ">>>>$f" >> .elchemy/output
# We don't need to typecheck deps again
if [[ ${f} != *"elm-stuff"* ]] && ! [[ $* == *--unsafe* ]]; then
(echo n | elm-make $f --output .elchemy/output_tmp.js) || { echo 'Type Check failed' ; exit 1; }
rm .elchemy/output_tmp.js
fi
i=$((i+1))
echo "#$i"
cat $f >> .elchemy/output
done
echo "-- Linking files --"
node --max_old_space_size=8192 $SOURCE_DIR/elchemy.js .elchemy/output .elchemy/elixir_output .elchemy/cache.json
current_file=""
while IFS= read -r line; do
if [[ $line =~ ">>>>" ]]; then
current_file="${line/\/\///}"
current_file="${current_file/>>>>/}"
echo "Linking: $current_file"
current_file="${current_file/$2\//$3/}"
current_file="${current_file%%.elm}.elchemy.ex"
current_file=$(echo ${current_file} | perl -pe 's/([a-z0-9])([A-Z])/$1_\L$2/g')
current_file=$(create_file $current_file)
echo "To: $current_file"
else
if [ "$current_file" != "" ]; then
printf '%s\n' "$line" >> "$current_file"
fi
fi
done < .elchemy/elixir_output
#rm .elchemy/elixir_output
#rm .elchemy/output
echo $(date +"%Y-%m-%d %H:%M:%S") > .elchemy/mtime
;;
*)
echo $"Usage: $0 []"
cat <
Start a new project
init
Add Elchemy to an existing project
compile [INPUT_DIR] [OUTPUT_DIR] [--unsafe]
Compile Elchemy source code
clean
Remove temporary files
version
Print Elchmey's version number number and exit
EOF
exit 1
esac
================================================
FILE: elchemy_ex/.gitignore
================================================
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
elm-deps
elm-stuff
================================================
FILE: elchemy_ex/README.md
================================================
# ElchemyEx
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `elchemy_ex` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:elchemy_ex, "~> 0.1.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/elchemy_ex](https://hexdocs.pm/elchemy_ex).
================================================
FILE: elchemy_ex/config/config.exs
================================================
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.
# You can configure your application as:
#
# config :elchemy_ex, key: :value
#
# and access this configuration in your application as:
#
# Application.get_env(:elchemy_ex, :key)
#
# You can also configure a 3rd-party app:
#
# config :logger, level: :info
#
# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env}.exs"
================================================
FILE: elchemy_ex/elm/Hello.elm
================================================
module Hello exposing (..)
{-| Prints "world!"
hello == "world!"
-}
hello : String
hello =
"world!"
================================================
FILE: elchemy_ex/elm-package.json
================================================
{
"version": "1.0.0",
"summary": "helpful summary of your project, less than 80 characters",
"repository": "https://github.com/user/project.git",
"license": "BSD3",
"source-directories": [
"../src"
],
"exposed-modules": [
"Compiler"
],
"dependencies": {
"wende/elchemy-core" : "0.0.0 <= v <= 1.0.0",
"elm-lang/core": "5.1.1 <= v < 6.0.0",
"elm-lang/html": "2.0.0 <= v < 3.0.0",
"evancz/elm-markdown": "3.0.2 <= v < 4.0.0",
"Bogdanp/elm-ast": "8.0.9 <= v < 9.0.0",
"elm-community/list-extra": "6.0.0 <= v < 7.0.0"
},
"dependency-sources": {
"wende/elchemy-core" : "../elchemy-core/"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}
================================================
FILE: elchemy_ex/mix.exs
================================================
defmodule ElchemyEx.Mixfile do
use Mix.Project
def project do
[
app: :elchemy_ex,
version: "0.1.0",
elixir: "~> 1.4",
# Commented out until release
# compilers: [:elchemy, :yecc, :leex, :erlang, :elixir, :app],
elixirc_paths: ["lib", "elm-deps"],
elchemy_path: "../src",
start_permanent: Mix.env == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
]
end
end
================================================
FILE: elchemy_ex/test/elchemy_ex_test.exs
================================================
defmodule ElchemyExTest do
use ExUnit.Case
test "Parsing works" do
assert Ast.parse("module A exposing (..)\na = 1") != []
end
end
================================================
FILE: elchemy_ex/test/test_helper.exs
================================================
ExUnit.start()
================================================
FILE: elchemy_node.js
================================================
var path = require("path")
var fs = require("fs");
var SOURCE_DIR = process.argv[2]
var TARGET_DIR = process.argv[3]
var CACHE_PATH = process.argv[4]
function execute(tree, fullTree, treeAndCommons) {
var cacheExists = CACHE_PATH && fs.existsSync(CACHE_PATH)
// If cache is provided and cache doesn't exist the compiler doesn't expect cache, and will produce cache after compilation
if(CACHE_PATH && !cacheExists) {
var compiledSource = fs.readFileSync(SOURCE_DIR).toString();
var compiledOutput = treeAndCommons(compiledSource);
var compiledCode = compiledOutput._0
var compiledCache = JSON.stringify(compiledOutput._1)
fs.writeFileSync(TARGET_DIR, compiledCode);
fs.writeFileSync(CACHE_PATH, compiledCache);
}
// If cache ISN'T provided the compiler DOESN'T expect cache.
else if(!CACHE_PATH){
var compiledSource = fs.readFileSync(SOURCE_DIR).toString();
var compiledCode = tree(compiledSource);
fs.writeFileSync(TARGET_DIR, compiledCode);
}
// If found it will use the cached ExContext.commons and compile target using it
else {
var cachedCommons = JSON.parse(fs.readFileSync(CACHE_PATH).toString())
var compiledSource = fs.readFileSync(SOURCE_DIR).toString();
var compiledOutput = fullTree(cachedCommons)(compiledSource);
var compiledCode = compiledOutput._0
fs.writeFileSync(TARGET_DIR, compiledCode);
}
}
module.exports = {
execute: execute
}
================================================
FILE: elm-package.json
================================================
{
"version": "1.0.0",
"summary": "helpful summary of your project, less than 80 characters",
"repository": "https://github.com/user/project.git",
"license": "BSD3",
"source-directories": [
".",
"./src"
],
"exposed-modules": [
"Elchemy.Compiler"
],
"dependencies": {
"elm-lang/core": "5.1.1 <= v < 6.0.0",
"elm-lang/html": "2.0.0 <= v < 3.0.0",
"evancz/elm-markdown": "3.0.2 <= v < 4.0.0",
"Bogdanp/elm-ast": "8.0.0 <= v < 10.0.0",
"elm-community/list-extra": "6.0.0 <= v < 7.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}
================================================
FILE: elmchemy
================================================
#!/bin/bash
echo "!!! DEPRECATION NOTICE !!!"
echo "elmchemy name is deprecated. Use elchemy (without an m) instead"
echo "!!! DEPRECATION NOTICE !!!"
SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
SOURCE="$(readlink "$SOURCE")"
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done
SOURCE_DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
$SOURCE_DIR/elchemy "$@"
================================================
FILE: lib/mix/mix.exs
================================================
defmodule Elchemy.Mixfile do
use Mix.Project
def project do
[app: :elchemy,
name: "Elchemy Compiler",
description: "Mix compiler wrapper around Elchemy project",
version: "0.4.25",
elixir: "~> 1.4",
description: "",
package: package(),
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
elixirc_paths: ["elm", "lib"],
elchemy_path: "elm",
deps: deps()]
end
defp package do
# These are the default files included in the package
[
name: :elchemy,
files: ["lib", "priv", "mix.exs", "README*", "readme*", "LICENSE*", "license*"],
maintainers: ["Krzysztof Wende"],
licenses: ["Apache 2.0"],
links: %{"GitHub" => "https://github.com/wende/elchemy"}
]
end
# Configuration for the OTP application
#
# Type "mix help compile.app" for more information
def application do
# Specify extra applications you'll use from Erlang/Elixir
[extra_applications: [:logger]]
end
# Dependencies can be Hex packages:
#
# {:my_dep, "~> 0.4.25"}
#
# Or git/path repositories:
#
# {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.4.25"}
#
# Type "mix help deps" for more examples and options
defp deps do
[]
end
end
================================================
FILE: lib/mix/tasks/compile.elchemy.ex
================================================
defmodule Mix.Tasks.Compile.Elchemy do
use Mix.Task
def run(_args) do
project = Mix.Project.config
src = project[:elchemy_path]
dests = project[:elixirc_paths] || ["lib"]
elchemy_executable = project[:elchemy_executable] || "elchemy"
version = System.version
# Crash if elchemy not found globally
unless 0 == Mix.shell.cmd("which #{elchemy_executable}") do
Mix.raise "Elchemy not found under #{elchemy_executable}. You might need to run `npm install elchemy -g`"
end
# Crash if elchemy not found globally
unless dests, do: IO.warn "No 'elixirc_paths' setting found"
if src && dests do
[dest | _] = dests
unless 0 == Mix.shell.cmd("#{elchemy_executable} compile #{src} #{dest}") do
Mix.raise "Elchemy failed the compilation with an error\n"
end
end
# Force project to be reloaded and deps compiled after elm-deps created.
IO.puts "-- Recompiling dependencies for elchemy --"
if project = Mix.Project.pop() do
%{name: name, file: file} = project
Mix.Project.push(name, file)
end
Mix.Task.run "deps.get"
Mix.Task.run "deps.compile"
IO.puts "-- Elchemy compilation complete --\n"
if Regex.match?(~r/1.7.[0-9]+$/, version), do: IO.warn "Elchemy's functionality is limited in Elixit 1.7. To support the full experience please use different Elixir version"
end
end
================================================
FILE: mix.exs
================================================
defmodule Elchemy.Mixfile do
use Mix.Project
def project do
[app: :elchemy,
name: "Elchemy Compiler",
description: "Mix compiler wrapper around Elchemy project",
version: "0.8.8",
elixir: "~> 1.4",
description: "",
package: package(),
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
elixirc_paths: ["elm", "lib", "elm-deps"],
elchemy_path: "elm",
deps: deps()]
end
defp package do
# These are the default files included in the package
[
name: :elchemy,
files: ["lib", "priv", "mix.exs", "README*", "readme*", "LICENSE*", "license*"],
maintainers: ["Krzysztof Wende"],
licenses: ["Apache 2.0"],
links: %{"GitHub" => "https://github.com/wende/elchemy"}
]
end
# Configuration for the OTP application
#
# Type "mix help compile.app" for more information
def application do
# Specify extra applications you'll use from Erlang/Elixir
[extra_applications: [:logger]]
end
defp deps do
[]
end
end
================================================
FILE: package.json
================================================
{
"name": "elchemy",
"version": "0.8.8",
"description": "Write Elixir code using Elm-inspired syntax (elm-make compatible)",
"directories": {
"example": "example",
"test": "tests"
},
"files": [
"elchemy",
"elchemy.js",
"elchemy_node.js",
"templates/*"
],
"bin": {
"elchemy": "./elchemy"
},
"scripts": {
"test": "elm-test",
"precommit": "lint-staged"
},
"repository": {
"type": "git",
"url": "git+https://github.com/wende/elchemy.git"
},
"bugs": {
"url": "https://github.com/wende/elchemy/issues"
},
"homepage": "https://github.com/wende/elchemy#readme",
"devDependencies": {
"elm-format": "^0.6.1-alpha",
"elm-test": "0.18.0",
"husky": "^0.13.3",
"lint-staged": "^3.4.1"
},
"lint-staged": {
"*.elm": [
"node_modules/.bin/elm-format --yes",
"git add"
]
}
}
================================================
FILE: roadmap/BASIC_TYPES.md
================================================
# Basic types
Because Elm and Elixir share a lot of common basic types, there is no need to redefine all of them for Elchemy. For simplicity and interoperability some of the standard types translate directly to each other.
Here is a table of all standard types used in the Elchemy environment and their Elixir equivalents:
| Elchemy | Elixir |
| --- | --- |
| `a` | `any()`
| `comparable` | `term()`
| `Int` | `integer()`
| `Float` | `number()`
| `number` | `number()`
| `Bool` | `boolean()`
| `Char` | `integer()`
| `String` | `String.t()`
| `List x` | `list()`
| `(1, 2)` | `{1, 2}`
| `Maybe Int` | `{integer()}` | `nil`
| `Just x` | `{x}`
| `Nothing` | `nil`
| `Result x y` | `{:ok, y}` | `{:error, x}`
| `Ok x` | `{:ok, x}`
| `Err x` | `{:error, x}`
| `{x = 1, y = 1}` | `%{x: 1, y: 1}` |
| `Dict String Int` | `%{key(String.t()) => integer())}` |
================================================
FILE: roadmap/COMMENTS.md
================================================
# Comments
In Elchemy there is several types of comments.
### Inline comments
```elm
-- My comment
```
Which are always ignored by the compiler and won't be included in the compiled output file
### Block comment
Second type of a comment is a block comment
``` elm
{- block comment -}
```
Which are included in the output file, but can only be used as a top level construct
### Doc comments
Another one and probably most important is a doc-comment.
``` elm
{-| Doc comment -}
```
Doc-comments follow these rules:
1. **First doc comment in a file always compiles to a `@moduledoc`**
2. Doc comments before types compile to `@typedoc`
3. Doc comments before functions compile to `@doc`
4. A doc comment with 4 space indentation and containing a `==` comparison in it will be compiled to a **one line** [doctest](https://elixir-lang.org/getting-started/mix-otp/docs-tests-and-with.html#doctests)
Doctest example:
```elm
{-| My function adds two values
myFunction 1 2 == 3
-}
```
Would end up being
```elixir
@doc """
My function adds two values
iex> my_function().(1).(2)
3
"""
```
================================================
FILE: roadmap/FLAGS.md
================================================
# Flags
Elchemy compiler for purposes of experimenting and stretching the boundaries accepts flags.
#### DO NOT ATTEMPT TO DO THAT UNLESS YOU'RE SURE IT'S THE ONLY WAY
To pass a flag to a compiler there is a special comment syntax
```
{- flag flagname:+argument flagname2:+argument2 }
```
So far there is 4 flag types:
#### `notype:+TypeName`
Omits putting the `@type` into the compiled output code
Used when you need type checking inside Elchemy ecosystem, without forwarding the definition into the output code.
Example:
```elm
{- flag notype:+MyHiddenType -}
type MyHiddenType = Hidden a
```
---
#### `nodef:+functionName`
Omits entire function definition from the code output. The function `@spec` will still be produced.
Example:
```elm
{- flag nodef:+myHiddenFunction -}
myHiddenFunction = 1
```
---
#### `nospec:+functionName`
Omits function spec from the code output
Example:
```elm
{- flag nospec:+myHiddenFunction -}
myHiddenFunction : Int
```
---
#### `noverify:+functionName`
Omits function verify macro from the code output. Usable only when using for functions defined as FFI
Example:
```elm
{- flag nospec:+myHiddenFunction -}
myHiddenFunction : Int
```
================================================
FILE: roadmap/FUNCTIONS.md
================================================
# Function definition and currying
### Function definition
To define a function that gets exported you **need** to declare a type for it with
``` elm
functionName : ArgType -> ArgType2 -> ReturnType
```
And a function body underneath
``` elm
functionName argOne argTwo = body
```
This will output following definition:
``` elixir
curry function_name/2
@spec function_name(arg_type, arg_type2) :: return_type
def function_name(arg_one, arg_two) do
body
end
```
Following rules apply:
1. A function will use `def` or `defp` based on `exposing` clause at the top of the module
2. The name of the function as well as its spec will always be snake_cased version of the camelCase name
3. `curry function/arity` is a construct that makes the function redefined in a form that takes 0 arguments, and returns X times curried function (2 times in this example). Which means that from elixir our function can be called both as: `function_name(arg_one, arg_two)` or `function_name().(arg_one).(arg_two)` and it won't have any different effect
4. `@spec` clause will always resolve types provided, to a most readable and still understandable by the elixir compiler form
#### Curried definition
Because of the curried nature of Elm function definitions we can just make our function return functions
For example instead of writing
``` elm
addTwo : Int -> Int
addTwo a = 2 + a
```
We could just write
``` elm
addTwo : Int -> Int
addTwo = (+) 2
```
In which case Elchemy will recognize a curried return and still provide you with a 1 and 0 arity functions, not only the 0 one.
And output of such a definition would look like:
``` elixir
curry add_two/1
@spec add_two(integer) :: integer
def add_two(x1) do
(&+/0).(2).(x1)
end
```
Which basically means that Elchemy derives function arity from the type, rathar then function body
================================================
FILE: roadmap/INLINING.md
================================================
# Inlining Elixir Code
#### DO NOT ATTEMPT TO DO THAT UNLESS YOU'RE SURE IT'S THE ONLY WAY
In Elchemy it's possible to inline Elixir code using
```elm
{- ex
## Code
def any_function() do
1
end
-}
```
================================================
FILE: roadmap/INSTALLATION.md
================================================
# Installation
You can install Elchemy using [npm](https://www.npmjs.com/) with
```
npm install -g elchemy
```
To integrate Elchemy with your project you need to execute:
```
elchemy init
```
Inside your elixir project directory.
If you don't have a project created, you need to first create it. It's advised to use [Mix](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html#our-first-project) for that.
Assuming the simplest example project called `my_project` the standard path would be:
```
mix new my_project
cd my_project
elchemy init
```
Then open your `mix.exs` file inside project root directory. And add:
```elixir
|> elem(Code.eval_file(".elchemy.exs"), 0).init
```
At the end of your `project/0` function definition. Like so:
(As of OTP 21.0 and above you must also add `@compile :tuple_calls` at the top of the Mix module. It is caused by tuple calls support being removed from newer versions of Erlang)
Before:
```elixir
defmodule MyProject.Mixfile do
use Mix.Project
def project do
[
app: :my_project,
version: "0.1.0",
elixir: "~> 1.5",
start_permanent: Mix.env == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
...
```
After:
```elixir
defmodule MyProject.Mixfile do
use Mix.Project
def project do
[
app: :my_project,
version: "0.1.0",
elixir: "~> 1.5",
start_permanent: Mix.env == :prod,
deps: deps()
] |> elem(Code.eval_file(".elchemy.exs"), 0).init
end
# Run "mix help compile.app" to learn about applications.
def application do
...
```
================================================
FILE: roadmap/INTEROP.md
================================================
# Interop
## Calling Elchemy
To call generated code from Elchemy you don't need anything special.
Each function exposed from a module can be called either in it's regular or curried form.
Keep in mind that in case of functions expecting a [higher-order function\[s\]](https://en.wikipedia.org/wiki/Higher-order_function) in its parameters Elchemy will also do it's magic to make sure it's compatible both ways.
For instance if your function looks like that:
```elm
applyFunction2 : (a -> b -> c) -> a -> b -> c
applyFunction2 f p1 p2 = f p1 p2
```
You can call it Elixir way:
```elixir
apply_function2(fn a, b -> a + b end)
```
As well as Elchemy (curried) way:
```elixir
apply_function2(fn a -> fn b -> a + b end end)
```
And it will work as fine
---
## Calling Elixir
To call elixir from Elchemy you need to define a foreign function interface.
To do that you can use `ffi` [special syntax (?)](SPECIAL_SYNTAX.md)
`ffi` requires the function to have a very specific format, which is:
1. You need to make sure the type signature is adequate to the corresponding typespec of a function
2. There should be no explicitly stated parameters (Defined as `f = ...` not `f a b c = ...`)
3. The **only** expression inside the function should be an ffi call in a format of:
`ffi "Module" "function"`
A good example of an ffi call would be
```elm
upcase : String -> String
upcase =
ffi "String" "upcase"
```
A generated code of that statement would be
``` elixir
@spec upcase(String.t) :: String.t
curry upcase/1
verify as: String.upcase/1
def upcase(a1), do: String.upcase(a1)
```
Where `verify, as:` is a type safety generator about which you can read more in [testing](TESTING.md) section.
================================================
FILE: roadmap/MODULES.md
================================================
# Module definition and imports
To define a module in Elchemy you need to use a
``` elm
module ModuleName exposing (..)
```
Which would directly translate to `defmodule` block where functions/types mentioned in the `exposing` clause will automatically use `def` or `defp`
## Imports
There are two types of imports in Elchemy.
### Without exposing
One is a import without exposed functions like
``` elm
import SomeModule
```
Which would directly translate to
``` elixir
alias SomeModule
```
Because it doesn't import any of the exposed contents, only makes sure
that the module is in our namespace.
### With exposing
``` elm
import SomeModule exposing (funA, TypeA, funB)
```
Which outputs
``` elixir
import SomeModule, only: [{:fun_a, 0}, {:fun_b, 0}]
```
Which would put `SomeModule` into our namespace and also allow us to use
`fun_a` and `fun_b` without explicitly adding a module name before them.
================================================
FILE: roadmap/README.md
================================================
# Elchemy
This page lists all of the ideas and solutions behind Elchemy to share the
ideology as well as existing and incoming solutions to problems this project faced
or eventually will have to face
# Table of contents
## Basics
- [Basic Types](BASIC_TYPES.md)
- [Defining Types](TYPES.md)
- [Defining Type Aliases](TYPE_ALIASES.md)
- [Structures](STRUCTURES.md)
- [Defining Modules](MODULES.md)
- [Defining Functions](FUNCTIONS.md)
- [Comments](COMMENTS.md)
- [Interop (Foreign Function Interface)](INTEROP.md)
## Advanced
- [Side Effects / TEA](SIDE_EFFECTS.md)
- [Unit Testing](TESTING.md)
- [Compiler flags](FLAGS.md)
- [Inlining Elixir](INLINING.md)
## Tooling
- [Installation](./INSTALLATION.md)
- [Troubleshooting](TROUBLESHOOTING.md)
# Introduction
Elchemy is a set of tools and frameworks, designed to provide a language and an environment
as close to [Elm programming language](http://elm-lang.org) as possible, to build server applications
in a DSL-like manner for Erlang VM platform, with a readable and efficient Elixir code as an output.
## Features
Elchemy inherits many values from its parents: Elm and Elixir
### Elm
- ML like syntax maximizing expressiveness with additional readability and simplicity constraints
- Static typing with type inference
- Beautiful compilation errors
- Tagged union types and type aliases with type parameters (aka generic types)
- All functions are curried by default
- [No typeclasses](http://www.haskellforall.com/2012/05/scrap-your-type-classes.html)
### Erlang/Elixir
- Documentation as a first class citizen
- Doc tests
- Battle-tested distribution system that just works
### Additional
- Foreign function calls type safety
- Foreign function calls purity checks
- Dependency system based on GitHub
- Compile time code optimizations
================================================
FILE: roadmap/SIDE_EFFECTS.md
================================================
# Side Effects
Side Effects are yet to come. You can read in detail about the plans for implementing them here [#297](https://github.com/wende/elchemy/issues/297)
================================================
FILE: roadmap/STRUCTURES.md
================================================
# Structs
Elchemy represents all the structs as maps, so a struct defined like
``` elm
human : { name : String
, age : Int
}
```
Is an equivalent of
``` elixir
@spec human :: %{name: String.t(), age: integer()}
```
Also type aliases denoting structs can be instantiated like functions
``` elm
type alias Human =
{ name : String
, age : Int
}
```
``` elm
Human "Krzysztof" 22
```
## Struct polymorphism
What's more structs can describe a map that has at least specified elements using an update syntax.
``` elm
type alias Employee x =
{ x
| salary : Int
}
```
Which means any struct that has a field `salary` of type integer.
That way we can define our pseudo-inheritance and polymorphism for more advanced structures.
``` elm
type alias Human =
Employee
{ name : String
, age : Int }
human : Human
```
Would resolve to
``` elixir
@spec human :: %{
salary: integer(),
name: String.t,
age: integer()
}
```
But be advised that using this "polymorphic" approach strips us from the ability to use type aliases as constructors.
================================================
FILE: roadmap/SUMMARY.md
================================================
# Summary
### Documentation
* [Table Of Contents](./README.md)
* [Installation](./INSTALLATION.md)
* Basics
* [Syntax overview](./SYNTAX.md)
* [Basic Types](./BASIC_TYPES.md)
* [Defining Types](./TYPES.md)
* [Defining Type Aliases](./TYPE_ALIASES.md)
* [Defining Structures](./STRUCTURES.md)
* [Defining Modules](./MODULES.md)
* [Defining Functions](./FUNCTIONS.md)
* [Comments](./COMMENTS.md)
* [Interop](./INTEROP.md)
* Advanced
* [Side Effects / TEA](./SIDE_EFFECTS.md)
* [Unit Testing](./TESTING.md)
* [Compiler Flags](./FLAGS.md)
* [Inlining Elixir](./INLINING.md)
* Tooling
* [Troubleshooting](./TROUBLESHOOTING.md)
================================================
FILE: roadmap/SYNTAX.md
================================================
Elchemy is a functional reactive programming language that compiles to (server-side) Elixir (Erlang VM).
Elchemy is statically typed, which means that the compiler catches most errors immediately and provides clear and understandable error messages.
```elm
-- Single line comments start with two dashes.
{- Multiline comments can be enclosed in a block like this.
{- They can be nested. -}
-}
{-- The Basics --}
-- Arithmetic
1 + 1 -- 2
8 - 1 -- 7
10 * 2 -- 20
-- Every number literal without a decimal point can be either an Int or a Float.
33 / 2 -- 16.5 with floating point division
33 // 2 -- 16 with integer division
-- Exponents
5 ^ 2 -- 25
-- Booleans
not True -- False
not False -- True
1 == 1 -- True
1 /= 1 -- False
1 < 10 -- True
-- Strings and characters
"This is a string because it uses double quotes."
'a' -- characters in single quotes
-- Strings can be appended.
"Hello " ++ "world!" -- "Hello world!"
{-- Lists, Tuples, and Records --}
-- Every element in a list must have the same type.
["the", "quick", "brown", "fox"]
[1, 2, 3, 4, 5]
-- The second example can also be written with two dots.
List.range 1 5
-- Append lists just like strings.
List.range 1 5 ++ List.range 6 10 == List.range 1 10 -- True
-- To add one item, use "cons".
0 :: List.range 1 5 -- [0, 1, 2, 3, 4, 5]
-- The head and tail of a list are returned as a Maybe. Instead of checking
-- every value to see if it's null, you deal with missing values explicitly.
List.head (List.range 1 5) -- Just 1
List.tail (List.range 1 5) -- Just [2, 3, 4, 5]
List.head [] -- Nothing
-- List.functionName means the function lives in the List module.
-- Every element in a tuple can be a different type, but a tuple has a
-- fixed length.
("elchemy", 42)
-- Access the elements of a pair with the first and second functions.
-- (This is a shortcut; we'll come to the "real way" in a bit.)
Tuple.first ("elchemy", 42) -- "elchemy"
Tuple.second ("elchemy", 42) -- 42
-- The empty tuple, or "unit", is sometimes used as a placeholder.
-- It is the only value of its type, also called "Unit".
()
-- Records are like tuples but the fields have names. The order of fields
-- doesn't matter. Notice that record values use equals signs, not colons.
{ x = 3, y = 7 }
-- Access a field with a dot and the field name.
{ x = 3, y = 7 }.x -- 3
-- Or with an accessor function, which is a dot and the field name on its own.
.y { x = 3, y = 7 } -- 7
-- Update the fields of a record. (It must have the fields already.)
{ person |
name = "George" }
-- Update multiple fields at once, using the current values.
{ particle |
position = particle.position + particle.velocity,
velocity = particle.velocity + particle.acceleration }
{-- Control Flow --}
-- If statements always have an else, and the branches must be the same type.
if powerLevel > 9000 then
"WHOA!"
else
"meh"
-- If statements can be chained.
if n < 0 then
"n is negative"
else if n > 0 then
"n is positive"
else
"n is zero"
-- Use case statements to pattern match on different possibilities.
case aList of
[] -> "matches the empty list"
[x]-> "matches a list of exactly one item, " ++ toString x
x::xs -> "matches a list of at least one item whose head is " ++ toString x
-- Pattern matches go in order. If we put [x] last, it would never match because
-- x::xs also matches (xs would be the empty list). Matches do not "fall through".
-- The compiler will alert you to missing or extra cases.
-- Pattern match on a Maybe.
case List.head aList of
Just x -> "The head is " ++ toString x
Nothing -> "The list was empty."
{-- Functions --}
-- Elchemy's syntax for functions is very minimal, relying mostly on whitespace
-- rather than parentheses and curly brackets. There is no "return" keyword.
-- Define a function with its name, arguments, an equals sign, and the body.
multiply a b =
a * b
-- Apply (call) a function by passing it arguments (no commas necessary).
multiply 7 6 -- 42
-- Partially apply a function by passing only some of its arguments.
-- Then give that function a new name.
double =
multiply 2
-- Constants are similar, except there are no arguments.
answer =
42
-- Pass functions as arguments to other functions.
List.map double (List.range 1 4) -- [2, 4, 6, 8]
-- Or write an anonymous function.
List.map (\a -> a * 2) (List.range 1 4) -- [2, 4, 6, 8]
-- You can also write operators as functions wrapping them in parens
List.map ((+) 2) (List.range 1 4) -- [3, 4, 5, 6]
-- You can pattern match in function definitions when there's only one case.
-- This function takes one tuple rather than two arguments.
-- This is the way you'll usually unpack/extract values from tuples.
area (width, height) =
width * height
area (6, 7) -- 42
-- Use curly brackets to pattern match record field names.
-- Use let to define intermediate values.
volume {width, height, depth} =
let
area = width * height
in
area * depth
volume { width = 3, height = 2, depth = 7 } -- 42
-- Functions can be recursive.
fib n =
if n < 2 then
1
else
fib (n - 1) + fib (n - 2)
List.map fib (List.range 0 8) -- [1, 1, 2, 3, 5, 8, 13, 21, 34]
-- Another recursive function (use List.length in real code).
listLength aList =
case aList of
[] -> 0
x::xs -> 1 + listLength xs
-- Function calls happen before any infix operator. Parens indicate precedence.
cos (degrees 30) ^ 2 + sin (degrees 30) ^ 2 -- 1
-- First degrees is applied to 30, then the result is passed to the trig
-- functions, which is then squared, and the addition happens last.
{-- Types and Type Annotations --}
-- The compiler will infer the type of every value in your program.
-- Types are always uppercase. Read x : T as "x has type T".
-- Some common types
5 : Int
6.7 : Float
"hello" : String
True : Bool
-- Functions have types too. Read -> as "goes to". Think of the rightmost type
-- as the type of the return value, and the others as arguments.
not : Bool -> Bool
round : Float -> Int
-- When you define a value, it's good practice to write its type above it.
-- The annotation is a form of documentation, which is verified by the compiler.
double : Int -> Int
double x = x * 2
-- Function arguments are passed in parentheses.
-- Lowercase types are type variables: they can be any type, as long as each
-- call is consistent.
List.map : (a -> b) -> List a -> List b
-- "List dot map has type a-goes-to-b, goes to list of a, goes to list of b."
-- There are three special lowercase types: number, comparable, and appendable.
-- Numbers allow you to use arithmetic on Ints and Floats.
-- Comparable allows you to order numbers and strings, like a < b.
-- Appendable things can be combined with a ++ b.
{-- Type Aliases and Union Types --}
-- When you write a record or tuple, its type already exists.
-- (Notice that record types use colon and record values use equals.)
origin : { x : Float, y : Float, z : Float }
origin =
{ x = 0, y = 0, z = 0 }
-- You can give existing types a nice name with a type alias.
type alias Point3D =
{ x : Float, y : Float, z : Float }
-- If you alias a record, you can use the name as a constructor function.
otherOrigin : Point3D
otherOrigin =
Point3D 0 0 0
-- But it's still the same type, so you can equate them.
origin == otherOrigin -- True
-- By contrast, defining a union type creates a type that didn't exist before.
-- A union type is so called because it can be one of many possibilities.
-- Each of the possibilities is represented as a "tag".
type Direction =
North | South | East | West
-- Tags can carry other values of known type. This can work recursively.
type IntTree =
Leaf | Node Int IntTree IntTree
-- "Leaf" and "Node" are the tags. Everything following a tag is a type.
-- Tags can be used as values or functions.
root : IntTree
root =
Node 7 Leaf Leaf
-- Union types (and type aliases) can use type variables.
type Tree a =
Leaf | Node a (Tree a) (Tree a)
-- "The type tree-of-a is a leaf, or a node of a, tree-of-a, and tree-of-a."
-- Pattern match union tags. The uppercase tags will be matched exactly. The
-- lowercase variables will match anything. Underscore also matches anything,
-- but signifies that you aren't using it.
leftmostElement : Tree a -> Maybe a
leftmostElement tree =
case tree of
Leaf -> Nothing
Node x Leaf _ -> Just x
Node _ subtree _ -> leftmostElement subtree
-- That's pretty much it for the language itself. Now let's see how to organize
-- and run your code.
{-- Modules and Imports --}
-- The core libraries are organized into modules, as are any third-party
-- libraries you may use. For large projects, you can define your own modules.
-- Put this at the top of the file. If omitted, you're in Main.
module Name
-- By default, everything is exported. You can specify exports explicitly.
module Name exposing (MyType, myValue)
-- Import a type and all it's Tags
import Name exposing (MyType(..))
-- One common pattern is to export a union type but not its tags. This is known
-- as an "opaque type", and is frequently used in libraries.
-- Import code from other modules to use it in this one.
-- Places Dict in scope, so you can call Dict.insert.
import Dict
-- Imports the Dict module and the Dict type, so your annotations don't have to
-- say Dict.Dict. You can still use Dict.insert.
import Dict exposing (Dict)
{-- Command Line Tools --}
-- Install in a project
$ elchemy init
```
================================================
FILE: roadmap/TESTING.md
================================================
# Testing
So far there is no unit testing tool for Elchemy.
To test your code use Elixir's `ExUnit` or see document [COMMENTS.md](COMMENTS.md) for using Doctests.
================================================
FILE: roadmap/TROUBLESHOOTING.md
================================================
# Troubleshooting
Generally grand amount of problems can be solved with a simple command:
```
elchemy clean
```
It cleans all of the caches, temporary files, dependencies and code file outputs out of the current project.
If
```
elchemy clean
elchemy init
mix compile
```
still yields compilation errors feel free to report an issue.
For the sake of being able to reproduce please provide:
- Elchemy version (`elchemy version`)
- Elixir version (`elixir -v`)
- Erlang version (first line in `erl`)
- Elm version (`elm -v`)
- Operating system (`uname -msr`)
It's also helpful to include:
- Result of `tree -L 2` inside your project folder
- Code of the file failing
================================================
FILE: roadmap/TYPES.md
================================================
# Union types
Elchemy (exactly like Elm) uses [Tagged union types](https://en.wikipedia.org/wiki/Tagged_union)
What it means is basically you can define a type by adding a tag to
an already existing value and the meaning of this tag is to inform about the
context of that value.
For instance:
``` elm
type Shape = Dot Int | Line Int Int | Triangle Int Int Int
```
What's important is that Dot, Line and Triangle are just tags, so they can't be used as a type name (in function signatures for example)
The only purpose of these is to pattern match on them in constructs like case..of, let..in or in arguments
Elchemy represents tagged unions as tuples with a first element being an atom with snake_cased tag name, or - in case of just tags - as a single atom value.
For example our previously defined type would translate to:
``` elixir
@type shape :: { :dot, integer() } |
{ :line, integer(), integer() } |
{ :triangle, integer(), integer(), integer() }
```
But a type like this:
``` elm
type Size = XS | S | M | L | XL
```
Would translate to:
``` elixir
@type size :: :xs | :s | :m | :l | :xl
```
### Type Parameters
Types can also take type parameters like:
``` elm
type Maybe x = Just x | Nothing
```
All type parameters will resolve to any() by Elchemy.
### Types as constructors
A Type can be insinstantiated using a tag and values.
For example to instantiate `Just Int` we would write `Just 10`.
If you don't provide all of the parameters, Elchemy will recognize it and
translate it into a curried function, so that 'Just' instead turning into;
``` elixir
:just
```
turns into:
``` elixir
fn x1 -> {:just, x1} end
```
================================================
FILE: roadmap/TYPE_ALIASES.md
================================================
# Type Aliases
In Elchemy Type Aliases are completely virtual constructs that never make it out of the compiler.
However whenever a type alias is used throughout your code Elchemy will expand the alias and substitute with the right replacement.
For instance if we write
``` elm
type alias MyList = List Int
a : MyList
a = [1, 2, 3]
```
The Elixir output would be
``` elixir
@spec a :: list(integer())
def a(), do: [1, 2, 3]
```
With correct type resolution
## Type aliases as constructors
If a type alias represents a structure like
``` elm
type alias Human = { name : String, age : Int }
```
You can use the name of an alias as a function to quickly instatiate a struct
. For instance:
``` elm
Human "Krzysztof" 22
```
================================================
FILE: src/Elchemy/Alias.elm
================================================
module Elchemy.Alias exposing (registerAliases, replaceTypeAliases, resolveTypeBody)
import Ast.Statement exposing (Statement(..), Type(..))
import Dict exposing (Dict)
import Elchemy.Ast as Ast
import Elchemy.Context as Context
exposing
( Alias
, AliasType
, Context
, TypeBody(..)
, wrongArityAlias
)
import Elchemy.Helpers as Helpers exposing ((=>), lastAndRest)
registerAliases : Context -> List Statement -> Context
registerAliases c list =
List.foldl registerAlias c list
registerAlias : Statement -> Context -> Context
registerAlias s c =
case s of
TypeAliasDeclaration tc t ->
registerTypeAlias c tc t
TypeDeclaration tc types ->
registerUnionType c tc types
FunctionTypeDeclaration name t ->
registerFunctionDefinition c name t
_ ->
c
resolveTypeBody : Context -> TypeBody -> List Type -> Type
resolveTypeBody c typeBody givenArgs =
case typeBody of
SimpleType t ->
t
ArgumentedType name expectedArgs return ->
let
arity =
List.length givenArgs
expected =
List.length expectedArgs
in
if arity == expected then
resolveTypes c expectedArgs givenArgs return
else
wrongArityAlias c expected givenArgs name
registerTypeAlias : Context -> Type -> Type -> Context
registerTypeAlias c tc t =
case tc of
TypeConstructor [ name ] arguments ->
let
arity =
List.length arguments
typeBody =
ArgumentedType name arguments t
ali =
Alias c.mod arity Context.TypeAlias t typeBody []
in
Context.addAlias c.mod name ali c
ts ->
Context.crash c <| "Wrong type alias declaration " ++ toString ts
registerUnionType : Context -> Type -> List Type -> Context
registerUnionType c tc types =
case tc of
TypeConstructor [ name ] arguments ->
let
typeVar =
TypeVariable <| "@" ++ name
arity =
List.length arguments
( names, newC ) =
registerTypes types name c
ali =
Alias c.mod arity Context.Type typeVar (SimpleType typeVar) names
in
Context.addAlias c.mod name ali newC
ts ->
Context.crash c <| "Wrong type declaration " ++ toString ts
registerFunctionDefinition : Context -> String -> Type -> Context
registerFunctionDefinition c name t =
let
arity =
replaceTypeAliases c t
|> Helpers.typeApplicationToList
|> List.length
in
Context.addFunctionDefinition c name (Context.FunctionDefinition (arity - 1) t)
registerTypes : List Type -> String -> Context -> ( List String, Context )
registerTypes types parentAlias c =
let
addType t ( names, context ) =
case t of
TypeConstructor [ name ] args ->
(name :: names)
=> Context.addType c.mod parentAlias name (List.length args) context
any ->
Context.crash c "Type can only start with a tag"
in
List.foldl addType ( [], c ) types
{-| Function taking a type and replacing all aliases it points to with their dealiased version
-}
replaceTypeAliases : Context -> Type -> Type
replaceTypeAliases c t =
let
mapOrFunUpdate mod default typeName args =
Context.getAlias mod typeName c
|> Helpers.filterMaybe (.aliasType >> (==) Context.TypeAlias)
|> Maybe.map (\{ typeBody } -> resolveTypeBody c typeBody args)
|> Maybe.andThen
(\body ->
case body of
TypeRecordConstructor _ _ ->
Just body
TypeApplication _ _ ->
Just body
_ ->
Nothing
)
|> Maybe.withDefault default
typeConstructorReplace default fullType args =
Helpers.moduleAccess c.mod fullType
|> (\( mod, typeName ) -> mapOrFunUpdate mod default typeName args)
replaceAlias t =
case t of
TypeConstructor fullType args ->
typeConstructorReplace t fullType args
t ->
t
in
Ast.walkTypeOutwards replaceAlias t
resolveTypes : Context -> List Type -> List Type -> Type -> Type
resolveTypes c expected given return =
let
expectedName n =
case n of
TypeVariable name ->
name
other ->
Context.crash c <|
"type can only take variables. "
++ toString other
++ "is incorrect"
paramsWithResolution =
List.map2 (,) (List.map expectedName expected) given
|> List.foldl (uncurry Dict.insert) Dict.empty
replace t =
case t of
(TypeVariable name) as default ->
Dict.get name paramsWithResolution
|> Maybe.withDefault default
t ->
t
in
Ast.walkTypeOutwards replace return
{-| Resolve an alias from local context
-}
localAlias : String -> Context -> Maybe Alias
localAlias name context =
Context.getAlias context.mod name context
================================================
FILE: src/Elchemy/Ast.elm
================================================
module Elchemy.Ast exposing (foldExpression, walkExpressionInwards, walkExpressionOutwards, walkTypeInwards, walkTypeOutwards)
{-| Contains helper functions to manage Elm Expression and Statement.Type ASTs
-}
import Ast.Expression exposing (Expression(..))
import Ast.Statement exposing (Type(..))
{-| Walks a tree of Ast.Statement.Type starting from the bottom branches and goes to the top using a replacer function
-}
walkTypeOutwards : (Type -> Type) -> Type -> Type
walkTypeOutwards f t =
f <| walkType (walkTypeOutwards f) t
{-| Walks a tree of Ast.Statement.Type starting from the top and goes down the branches using a replacer function
-}
walkTypeInwards : (Type -> Type) -> Type -> Type
walkTypeInwards f t =
f t |> walkType (walkTypeInwards f)
walkType : (Type -> Type) -> Type -> Type
walkType walkFunction t =
case t of
(TypeVariable name) as x ->
x
TypeConstructor modulePathAndName args ->
TypeConstructor modulePathAndName (List.map walkFunction args)
TypeRecordConstructor name args ->
TypeRecordConstructor (walkFunction name) <|
List.map (Tuple.mapSecond walkFunction) args
TypeTuple args ->
TypeTuple <| List.map walkFunction args
TypeRecord args ->
TypeRecord <| List.map (Tuple.mapSecond walkFunction) args
TypeApplication l r ->
TypeApplication (walkFunction l) (walkFunction r)
{-| Walks a tree of Ast.Expression.Expression starting from the bottom branches and goes to the top using a replacer function
-}
walkExpressionOutwards : (Expression -> Expression) -> Expression -> Expression
walkExpressionOutwards f t =
f <| walkExpression (walkExpressionOutwards f) t
{-| Walks a tree of Ast.Expression.Expression starting from the top and goes down the branches using a replacer function
-}
walkExpressionInwards : (Expression -> Expression) -> Expression -> Expression
walkExpressionInwards f t =
f t |> walkExpression (walkExpressionInwards f)
walkExpression : (Expression -> Expression) -> Expression -> Expression
walkExpression f t =
case f t of
(Variable _) as x ->
x
(Character _) as c ->
c
(String _) as s ->
s
(Integer _) as i ->
i
(Float _) as f ->
f
List exps ->
List (List.map (walkExpression f) exps)
Tuple exps ->
Tuple (List.map (walkExpression f) exps)
Access mod field ->
Access (walkExpression f mod) field
(AccessFunction field) as af ->
af
Record fields ->
Record <| List.map (Tuple.mapSecond <| walkExpression f) fields
RecordUpdate name fields ->
RecordUpdate name <| List.map (Tuple.mapSecond <| walkExpression f) fields
If check true false ->
If (walkExpression f check) (walkExpression f true) (walkExpression f false)
Let assignments body ->
Let (List.map (Tuple.mapSecond <| walkExpression f) assignments) (walkExpression f body)
Case target branches ->
Case (walkExpression f target) (List.map (\( l, r ) -> ( walkExpression f l, walkExpression f r )) branches)
Lambda args body ->
Lambda (List.map (walkExpression f) args) (walkExpression f body)
Application left right ->
Application (walkExpression f left) (walkExpression f right)
BinOp op left right ->
BinOp (walkExpression f op) (walkExpression f left) (walkExpression f right)
{-| Walks a tree of Ast.Expression.Expression starting from the top and goes down the branches using a folder function
-}
foldExpression : (Expression -> acc -> acc) -> acc -> Expression -> acc
foldExpression f acc t =
let
rec =
flip <| foldExpression f
in
f t <|
case t of
Variable _ ->
acc
Character _ ->
acc
String _ ->
acc
Integer _ ->
acc
Float _ ->
acc
List exps ->
List.foldl rec acc exps
Tuple exps ->
List.foldl rec acc exps
Access mod field ->
rec mod acc
AccessFunction field ->
acc
Record fields ->
List.foldl (Tuple.second >> rec) acc fields
RecordUpdate name fields ->
List.foldl (Tuple.second >> rec) acc fields
If check true false ->
acc |> rec check |> rec true |> rec false
Let assignments body ->
List.foldl (\( a, b ) lAcc -> rec a lAcc |> rec b) acc assignments |> rec body
Case target branches ->
acc |> rec target |> (\nAcc -> List.foldl (\( a, b ) lAcc -> rec a lAcc |> rec b) nAcc branches)
Lambda args body ->
List.foldl rec acc args |> rec body
Application left right ->
acc |> rec left |> rec right
BinOp op left right ->
acc |> rec op |> rec left |> rec right
================================================
FILE: src/Elchemy/Compiler.elm
================================================
module Elchemy.Compiler exposing (version, tree)
{-| Module responsible for compiling Elm code to Elixir
@docs version, tree
-}
import Ast
import Ast.Statement exposing (Statement)
import Dict exposing (Dict)
import Elchemy.Alias as Alias
import Elchemy.Context as Context exposing (Context)
import Elchemy.Helpers as Helpers exposing (ind, toSnakeCase)
import Elchemy.Meta as Meta
import Elchemy.Statement as Statement
import Regex exposing (HowMany(..), Regex, regex)
{-| Returns current version
-}
version : String
version =
"0.8.8"
glueStart : String
glueStart =
ind 0 ++ "use Elchemy" ++ "\n"
glueEnd : String
glueEnd =
"\n"
++ String.trim
"""
end
"""
++ "\n"
getName : String -> ( String, String )
getName file =
case String.split "\n" file of
n :: rest ->
( n, String.join "\n" rest )
[] ->
( "", "" )
{-| Transforms a code in Elm to code in Elixir
-}
tree : String -> String
tree =
treeAndCommons >> Tuple.first
{-| Transforms a code in Elm to code in Elixir and returns commons
-}
treeAndCommons : String -> ( String, Context.Commons )
treeAndCommons m =
fullTree Context.emptyCommons m
{-| Transforms a code in Elm with cache from previous run to code in Elixir and cache
-}
fullTree : Context.Commons -> String -> ( String, Context.Commons )
fullTree cachedCommons m =
-- If only no blank characters
if Regex.contains (Regex.regex "^\\s*$") m then
( "", cachedCommons )
else if not <| String.contains (">>" ++ ">>") m then
m
|> parse "NoName.elm"
|> getContext
|> (\( c, a ) ->
case c of
Nothing ->
Debug.crash "Failed getting context"
Just c ->
( getCode c a, c.commons )
)
else
let
multiple =
String.split (">>" ++ ">>") m
count =
Debug.log "Number of files" (List.length multiple)
files =
multiple
|> List.map getName
|> List.indexedMap (,)
|> List.map
(\( i, ( name, code ) ) ->
let
_ =
flip Debug.log name <|
"Parsing "
++ toString (count - i)
++ "/"
++ toString count
++ " # "
in
( name, parse name code )
)
wContexts =
files
|> List.map (\( name, ast ) -> ( name, getContext ast ))
|> List.filterMap
(\a ->
case a of
( _, ( Nothing, _ ) ) ->
Nothing
( name, ( Just c, ast ) ) ->
Just ( name, c, ast )
)
commons =
wContexts
|> List.map (\( name, ctx, ast ) -> ctx.commons)
|> (::) cachedCommons
|> getCommonImports
|> (\modules -> { modules = modules })
wTrueContexts =
wContexts
|> List.map (\( name, c, ast ) -> ( name, { c | commons = commons }, ast ))
compileWithIndex ( i, ( name, c, ast ) ) =
let
_ =
flip Debug.log name <|
"Compiling "
++ toString (count - i)
++ "/"
++ toString count
++ " # "
in
-- "Used to avoid quadruple > becuase it's a meta string"
">>" ++ ">>" ++ name ++ "\n" ++ getCode c ast
in
wTrueContexts
|> List.indexedMap (,)
|> List.map compileWithIndex
|> String.join "\n"
|> flip (,) commons
getCommonImports : List Context.Commons -> Dict String Context.Module
getCommonImports commons =
let
merge aliases acc =
Dict.merge Dict.insert (\k v v2 -> Dict.insert k v2) Dict.insert acc aliases Dict.empty
in
List.foldl (.modules >> merge) Dict.empty commons
getContext : List Statement -> ( Maybe Context, List Statement )
getContext statements =
case statements of
[] ->
( Nothing, [] )
mod :: statements ->
let
base =
Statement.moduleStatement mod
in
( Just (Alias.registerAliases base statements), statements )
aggregateStatements : Statement -> ( Context, String ) -> ( Context, String )
aggregateStatements s ( c, code ) =
let
( newC, newCode ) =
Statement.elixirS c s
in
( newC, code ++ newCode )
getCode : Context -> List Statement -> String
getCode context statements =
let
shadowsBasics =
Context.importBasicsWithoutShadowed context
( newC, code ) =
List.foldl aggregateStatements ( context, "" ) statements
in
("# Compiled using Elchemy v" ++ version)
++ "\n"
++ ("defmodule " ++ context.mod ++ " do")
++ glueStart
++ ind context.indent
++ shadowsBasics
++ code
++ glueEnd
++ Meta.metaDefinition { newC | inMeta = True }
++ "\n\n"
parse : String -> String -> List Statement
parse fileName code =
case Ast.parse (prepare code) of
Ok ( _, _, statements ) ->
statements
Err ( (), { input, position }, [ msg ] ) ->
let
( line, column ) =
getLinePosition position code
in
Debug.crash <|
"]ERR> Parsing error in:\n "
++ fileName
++ ":"
++ toString line
++ ":"
++ toString column
++ "\n"
++ msg
++ "\nat:\n "
++ (input
|> String.lines
|> List.take 30
|> String.join "\n"
)
++ "\n"
err ->
Debug.crash (toString err)
prepare : String -> String
prepare codebase =
codebase |> removeComments
removeComments : String -> String
removeComments =
Regex.replace All (regex " +--.*\\r?\\n") (always "")
>> Regex.replace All (regex "\\s--.*\\r?\\n") (always "")
>> Regex.replace All (regex "\n +\\w+ : .*") (always "")
getLinePosition : Int -> String -> ( Int, Int )
getLinePosition character input =
let
lines =
String.slice 0 character input |> String.lines
line =
List.length lines
column =
List.reverse lines |> List.head |> Maybe.map String.length |> Maybe.withDefault 0
in
( line, column )
================================================
FILE: src/Elchemy/Context.elm
================================================
module Elchemy.Context
exposing
( Alias
, AliasType(..)
, Commons
, Context
, FunctionDefinition
, Module
, Parser
, TypeBody(..)
, addAlias
, addFlag
, addFunctionDefinition
, addModuleAlias
, addType
, areMatchingArity
, changeCurrentModule
, crash
, deindent
, empty
, emptyCommons
, getAlias
, getArity
, getShadowedFunctions
, getType
, hasFlag
, importBasicsWithoutShadowed
, inArgs
, indent
, isPrivate
, listOfImports
, maybeModuleAlias
, mergeTypes
, mergeVariables
, notImplemented
, onlyWithoutFlag
, putIntoModule
, wrongArityAlias
)
import Ast.Expression exposing (Expression)
import Ast.Statement exposing (ExportSet(..), Statement, Type(..))
import Dict exposing (Dict)
import Elchemy.Helpers as Helpers exposing (toSnakeCase)
import Set exposing (Set)
type alias Parser =
Context -> Expression -> String
{-| A structure containing all the essential information about Type Alias
-}
type TypeBody
= SimpleType Type
| ArgumentedType String (List Type) Type
type alias Alias =
{ parentModule : String
, arity : Int
, aliasType : AliasType
, body : Type
, typeBody : TypeBody
, types : List String
}
type alias UnionType =
{ arity : Int
, parentModule : String
, parentAlias : String
}
{-| Type of an Alias which can be either Type or Type Alias. It's important to note
that a Type in here is only a definition of a type like
type A
= TagA
| TagB
Where only A is a `Context.AliasType.Type`, two separate tags are other instances and
belong to Types not Aliases
-}
type AliasType
= Type
| TypeAlias
{-| A flag for a compiler and its value
-}
type alias Flag =
( String, String )
{-| Definition of a function and its correspoint Ast.Type structure
-}
type alias FunctionDefinition =
{ arity : Int, def : Ast.Statement.Type }
{-| Dict holding information about defined modules
-}
type alias Module =
{ aliases : Dict String Alias
, types : Dict String UnionType
, functions : Dict String FunctionDefinition
, exports : ExportSet
}
type alias Commons =
{ modules : Dict String Module
}
{-| Context containing all the necessary information about current place in a file
like what's the name of a module, what aliases, types and variables are currently defined,
what flags were set for the compiler, what functions were defined and if it is in definition mode.
-}
type alias Context =
{ mod : String
, commons : Commons
, exports : ExportSet
, indent : Int
, flags : List Flag
, variables : Set String
, inArgs : Bool
, hasModuleDoc : Bool
, lastDoc : Maybe String
, inTypeDefiniton : Bool
, importedTypes : Dict String String
, aliasedModules : Dict String String
-- Dict functionName (moduleName, arity)
, importedFunctions : Dict String ( String, Int )
, meta : Maybe Expression
, inMeta : Bool
}
{-| Crashes the compiler because the alias was used with wrong arity.
Shouldn't ever happen if run after elm-make
-}
wrongArityAlias : Context -> Int -> List Type -> String -> a
wrongArityAlias c arity list name =
crash c <|
"Expected "
++ toString arity
++ " arguments for "
++ name
++ ". But got "
++ (toString <| List.length list)
{-| Puts something into module
Usage:
putIntoModule "Module" "name" .aliases (x -> { c | aliases = x }) ali c
-}
putIntoModule :
String
-> String
-> (Module -> Dict String a)
-> (Module -> Dict String a -> Module)
-> a
-> Context
-> Context
putIntoModule mod name getter setter thing c =
let
updateMod : Maybe Module -> Maybe Module
updateMod maybeMod =
maybeMod
|> Maybe.map getter
|> Maybe.withDefault Dict.empty
|> Dict.update name (always <| Just thing)
|> setter (maybeMod |> Maybe.withDefault emptyModule)
|> Just
commons =
c.commons
in
{ c | commons = { commons | modules = commons.modules |> Dict.update mod updateMod } }
{-| Adds an alias definition to the context
-}
addAlias : String -> String -> Alias -> Context -> Context
addAlias mod name =
putIntoModule mod name .aliases (\m x -> { m | aliases = x })
{-| Adds a type definition to the context
-}
addType : String -> String -> String -> Int -> Context -> Context
addType mod parentAlias name arity =
let
t =
{ arity = arity, parentModule = mod, parentAlias = parentAlias }
in
putIntoModule mod name .types (\m x -> { m | types = x }) t
{-| Add type definition into context
-}
addFunctionDefinition : Context -> String -> FunctionDefinition -> Context
addFunctionDefinition c name d =
putIntoModule c.mod name .functions (\m x -> { m | functions = x }) d c
{-| Get's either alias or type from context based on `from` accessor
-}
getFromContext :
(Module -> Dict String a)
-> String
-> String
-> Context
-> Maybe a
getFromContext from mod name context =
context
|> .commons
|> .modules
|> Dict.get mod
|> Maybe.map from
|> Maybe.andThen (Dict.get name)
{-| Get's an alias from context based on name of a module and of an alias
Wrapped in Maybe
-}
getAlias : String -> String -> Context -> Maybe Alias
getAlias =
getFromContext .aliases
{-| Get's a type from context based on name of a module and of a type
Wrapped in Maybe
-}
getType : String -> String -> Context -> Maybe UnionType
getType =
getFromContext .types
{-| Gets arity of the function in the module
-}
getArity : Context -> String -> String -> Maybe Int
getArity ctx m fn =
let
local =
ctx.commons.modules
|> Dict.get m
|> Maybe.map .functions
|> Maybe.andThen (Dict.get fn)
|> Maybe.map .arity
imported =
ctx.importedFunctions
|> Dict.get fn
|> Maybe.map Tuple.second
in
Helpers.maybeOr local imported
{-| Checks if function arity stored in context is the same as arguments count
-}
areMatchingArity : Context -> String -> String -> List a -> Bool
areMatchingArity c mod fn args =
List.length args == Maybe.withDefault -1 (getArity c mod fn)
{-| Returns empty context
-}
empty : String -> ExportSet -> Context
empty name exports =
{ mod = name
, exports = exports
, indent = 0
, flags = []
, variables = Set.empty
, inArgs = False
, hasModuleDoc = False
, lastDoc = Nothing
, commons = { modules = Dict.singleton name (Module Dict.empty Dict.empty Dict.empty exports) }
, inTypeDefiniton = False
, importedTypes =
Dict.fromList
[ ( "Order", "Elchemy.XBasics" )
, ( "Result", "Elchemy.XResult" )
]
, importedFunctions = Dict.empty
, aliasedModules = Dict.empty
, meta = Nothing
, inMeta = False
}
{-| Returns empty commons structure
-}
emptyCommons : Commons
emptyCommons =
{ modules = Dict.empty
}
changeCurrentModule : String -> Context -> Context
changeCurrentModule mod c =
{ c | mod = mod }
{-| Returns empty module record
-}
emptyModule : Module
emptyModule =
Module Dict.empty Dict.empty Dict.empty AllExport
{-| Increases current indenation level of a context
-}
indent : Context -> Context
indent c =
{ c | indent = c.indent + 1 }
{-| Decreases current indenation level of a context
-}
deindent : Context -> Context
deindent c =
{ c | indent = c.indent - 1 }
{-| Adds a flag to the compiler of a context
-}
addFlag : Flag -> Context -> Context
addFlag flag c =
{ c | flags = flag :: c.flags }
{-| Puts the code only if the given flag and its given value DOESN'T exist
-}
onlyWithoutFlag : Context -> String -> String -> String -> String
onlyWithoutFlag c key value code =
if hasFlag key value c then
""
else
code
{-| -}
getAllFlags : String -> Context -> List String
getAllFlags key c =
c.flags
|> List.filter (Tuple.first >> (==) key)
|> List.map Tuple.second
{-| True if has a flag with a particular value
-}
hasFlag : String -> String -> Context -> Bool
hasFlag key value c =
c.flags
|> List.any ((==) ( key, value ))
{-| Makes the state to be inside argument declaration,
thanks to that the compiler knows not to treat the declaration of new variables
as a refference to an older values or functions and prevents injection of parens
-}
inArgs : Context -> Context
inArgs c =
{ c | inArgs = True }
{-| Tells you if a function is private or public based on context of a module
-}
isPrivate : Context -> String -> Bool
isPrivate context name =
case context.exports of
SubsetExport exports ->
if List.any ((==) (FunctionExport name)) exports then
False
else
True
AllExport ->
False
other ->
crash context "No such export"
{-| Merges a set of two variables from two different contexts
-}
mergeVariables : Context -> Context -> Context
mergeVariables left right =
{ left | variables = Set.union left.variables right.variables }
{-| Finds all defined functions and all auto imported functions (XBasics) and returns
the commons subset. Return empty list for XBasics
-}
getShadowedFunctions : Context -> List String -> List ( String, FunctionDefinition )
getShadowedFunctions context list =
let
functions =
context.commons.modules
|> Dict.get context.mod
|> Maybe.map .functions
|> Maybe.withDefault Dict.empty
findReserved name =
functions
|> Dict.get name
|> Maybe.map ((,) name >> List.singleton)
|> Maybe.withDefault []
in
if context.mod == "Elchemy.XBasics" then
[]
else
list
|> List.concatMap findReserved
{-| Changes function definitions to a list of qualified imports including 0 and full arity
-}
listOfImports : List ( String, FunctionDefinition ) -> List String
listOfImports shadowed =
let
importTuple ( name, arity ) =
toSnakeCase False name
++ ": 0, "
++ toSnakeCase False name
++ ": "
++ toString arity
in
shadowed
|> List.map (Tuple.mapSecond .arity)
|> List.map importTuple
{-| Get code representation of import XBasics with exclusion of functions defined locally
-}
importBasicsWithoutShadowed : Context -> String
importBasicsWithoutShadowed c =
let
importModule mod list =
if list /= [] then
list
|> String.join ", "
|> (++) ("import " ++ mod ++ ", except: [")
|> flip (++) "]\n"
else
""
shadowedBasics =
getShadowedFunctions c Helpers.reservedBasicFunctions
|> listOfImports
shadowedKernel =
getShadowedFunctions c Helpers.reservedKernelFunctions
|> listOfImports
in
importModule "Elchemy.XBasics" shadowedBasics
++ importModule "Kernel" shadowedKernel
{-| Register a new module alias
import ModuleA as ModuleB
Would delias all ModuleB calls to ModuleA in case of Type and TypeAlias constructors
-}
addModuleAlias : String -> Maybe String -> Context -> Context
addModuleAlias oldName newName c =
newName
|> Maybe.map (\name -> { c | aliasedModules = c.aliasedModules |> Dict.insert name oldName })
|> Maybe.withDefault c
{-| Replace a module name with it's original name it aliases to. Otherwise return the same name
-}
maybeModuleAlias : Context -> String -> String
maybeModuleAlias c s =
c.aliasedModules
|> Dict.get s
|> Maybe.withDefault s
{-| Merges everything that should be imported from given module, based
on given export set value
-}
mergeTypes : ExportSet -> String -> Context -> Context
mergeTypes set mod c =
let
getAll getter mod =
c.commons.modules
|> Dict.get mod
|> Maybe.map getter
|> Maybe.withDefault Dict.empty
getAlias : String -> Dict String Alias
getAlias aliasName =
getAll .aliases mod
|> Dict.filter (\k _ -> k == aliasName)
getTypes : String -> Maybe ExportSet -> Dict String UnionType
getTypes aliasName maybeExportSet =
getAll .types mod
|> Dict.filter (\k { parentAlias } -> parentAlias == aliasName)
putAllLocal getter setter dict c =
Dict.foldl (\key value acc -> putIntoModule c.mod key getter setter value acc) c dict
importOne export c =
case export of
TypeExport aliasName types ->
c
|> putAllLocal .aliases (\m x -> { m | aliases = x }) (getAlias aliasName)
|> putAllLocal .types (\m x -> { m | types = x }) (getTypes aliasName types)
FunctionExport _ ->
c
_ ->
crash c "You can't import subset of subsets"
in
case set of
AllExport ->
c
|> putAllLocal .aliases (\m x -> { m | aliases = x }) (getAll .aliases mod)
|> putAllLocal .types (\m x -> { m | types = x }) (getAll .types mod)
SubsetExport list ->
List.foldl importOne c list
_ ->
crash c "You can't import something that's not a subset"
{-| Throw a nice error with the context involving it
-}
crash : Context -> String -> a
crash c prompt =
Debug.crash <|
"Compilation error:\n\n\t"
++ prompt
++ "\n\nin module: "
++ c.mod
{-| Throw a nice error saying that this feature is not implemented yet
-}
notImplemented : Context -> String -> a -> String
notImplemented c feature value =
" ## ERROR: No "
++ feature
++ " implementation for "
++ toString value
++ " yet"
++ "\n"
|> Debug.crash
================================================
FILE: src/Elchemy/Expression.elm
================================================
module Elchemy.Expression exposing (elixirE)
import Ast.Expression exposing (Expression(..))
import Elchemy.Context as Context
exposing
( Context
, areMatchingArity
, deindent
, inArgs
, indent
, mergeVariables
, onlyWithoutFlag
)
import Elchemy.Operator as Operator
import Elchemy.Selector as Selector
import Elchemy.Type as Type
import Elchemy.Variable as Variable exposing (rememberVariables)
import Elchemy.Helpers as Helpers
exposing
( (=>)
, Operator(..)
, applicationToList
, atomize
, generateArguments
, ind
, isCapitilzed
, lastAndRest
, maybeOr
, modulePath
, operatorType
, toSnakeCase
, translateOperator
)
{-| Encode any given expression
-}
elixirE : Context -> Expression -> String
elixirE c e =
case e of
Variable var ->
elixirVariable c var
-- Primitive types
(Application name arg) as application ->
tupleOrFunction c application
RecordUpdate name keyValuePairs ->
"%{"
++ toSnakeCase True name
++ " | "
++ (List.map (\( a, b ) -> toSnakeCase True a ++ ": " ++ elixirE c b) keyValuePairs
|> String.join ", "
)
++ "}"
-- Primitive operators
Access (Variable modules) right ->
modulePath modules
++ "."
++ String.join "." (List.map (toSnakeCase True) right)
Access left right ->
elixirE c left
++ "."
++ String.join "." right
AccessFunction name ->
"(fn a -> a." ++ toSnakeCase True name ++ " end)"
BinOp (Variable [ op ]) l r ->
Operator.elixirBinop c elixirE op l r
-- Rest
e ->
elixirControlFlow c e
{-| Encode control flow expressions
-}
elixirControlFlow : Context -> Expression -> String
elixirControlFlow c e =
case e of
Case var body ->
caseE c var body
Lambda args body ->
lambda c args body
(If check onTrue ((If _ _ _) as onFalse)) as exp ->
[ "cond do" ]
++ handleIfExp (indent c) exp
++ [ ind c.indent, "end" ]
|> String.join ""
If check onTrue onFalse ->
"if "
++ elixirE c check
++ " do "
++ elixirE c onTrue
++ " else "
++ elixirE c onFalse
++ " end"
Let variables expression ->
variables
|> Variable.organizeLetInVariablesOrder c
|> Variable.groupByCrossDependency
|> (flip List.foldl ( c, "" ) <|
\varGroup ( cAcc, codeAcc ) ->
(case varGroup of
[] ->
cAcc => ""
[ ( var, exp ) ] ->
elixirLetInBranch cAcc ( var, exp )
multiple ->
elixirLetInMutualFunctions cAcc multiple
)
|> (\( c, string ) ->
mergeVariables c cAcc
=> codeAcc
++ string
++ ind c.indent
)
)
|> (\( c, code ) -> code ++ elixirE c expression)
_ ->
elixirPrimitve c e
{-| Encodes a mutual function usage into `let` macro
-}
elixirLetInMutualFunctions : Context -> List ( Expression, Expression ) -> ( Context, String )
elixirLetInMutualFunctions context expressionsList =
let
vars =
List.map Tuple.first expressionsList
names =
expressionsList
|> List.map (Tuple.first >> Variable.extractName c >> toSnakeCase True)
c =
rememberVariables vars context
letBranchToLambda : Context -> ( Expression, Expression ) -> String
letBranchToLambda c ( head, body ) =
case applicationToList head of
[] ->
""
[ single ] ->
elixirE c body
(Variable [ name ]) :: args ->
lambda c args body
_ ->
Context.crash c <| toString head ++ " is not a let in branch"
in
c
=> "{"
++ (names |> String.join ", ")
++ "} = let ["
++ (expressionsList
|> List.map (\(( var, exp ) as v) -> ( Variable.extractName c var, v ))
|> List.map (Tuple.mapSecond <| letBranchToLambda (indent c))
|> List.map (\( name, body ) -> ind (c.indent + 1) ++ toSnakeCase True name ++ ": " ++ body)
|> String.join ","
)
++ ind c.indent
++ "]"
{-| Encodes a branch of let..in expression
let
{a, b} == 2 --< This is a branch
in
10
-}
elixirLetInBranch : Context -> ( Expression, Expression ) -> ( Context, String )
elixirLetInBranch c ( left, exp ) =
let
wrapElixirE c exp =
case exp of
Let _ _ ->
"(" ++ ind (c.indent + 1) ++ elixirE (indent c) exp ++ ind c.indent ++ ")"
_ ->
elixirE c exp
in
case applicationToList left of
[ (Variable [ name ]) as var ] ->
rememberVariables [ var ] c
=> toSnakeCase True name
++ " = "
++ wrapElixirE (c |> rememberVariables [ var ]) exp
((Variable [ name ]) as var) :: args ->
if Helpers.isCapitilzed name then
(c |> rememberVariables args)
=> tupleOrFunction (rememberVariables args c) left
++ " = "
++ wrapElixirE c exp
else
rememberVariables [ var ] c
=> toSnakeCase True name
++ " = rec "
++ toSnakeCase True name
++ ", "
++ lambda (c |> rememberVariables [ var ]) args exp
[ assign ] ->
rememberVariables [ assign ] c
=> elixirE (inArgs c) assign
++ " = "
++ wrapElixirE (rememberVariables [ assign ] c) exp
_ ->
c => ""
{-| Encode primitive value
-}
elixirPrimitve : Context -> Expression -> String
elixirPrimitve c e =
case e of
Integer value ->
toString value
Float value ->
let
name =
toString value
in
if String.contains "." name then
name
else
name ++ ".0"
Character value ->
case value of
' ' ->
"?\\s"
'\n' ->
"?\\n"
'\x0D' ->
"?\\r"
'\t' ->
"?\\t"
'\\' ->
"?\\\\"
'\x00' ->
"?\\0"
other ->
"?" ++ String.fromChar other
String value ->
"\"" ++ value ++ "\""
List vars ->
"["
++ (List.map (elixirE c) vars
|> String.join ", "
)
++ "]"
Tuple vars ->
"{"
++ (List.map (elixirE c) vars
|> String.join ", "
)
++ "}"
Record keyValuePairs ->
"%{"
++ (List.map (\( a, b ) -> toSnakeCase True a ++ ": " ++ elixirE c b) keyValuePairs
|> String.join ", "
)
++ "}"
_ ->
Context.notImplemented c "expression" e
{-| Change if expression body into list of clauses
-}
handleIfExp : Context -> Expression -> List String
handleIfExp c e =
case e of
If check onTrue onFalse ->
[ ind c.indent
, elixirE (indent c) check
, " -> "
, elixirE (indent c) onTrue
]
++ handleIfExp c onFalse
_ ->
[ ind c.indent
, "true -> "
, elixirE (indent c) e
]
{-| Returns if called function is a special macro inline by the compiler
-}
isMacro : Expression -> Bool
isMacro e =
case e of
Application a _ ->
isMacro a
Variable [ x ] ->
List.member x
[ "tryFfi"
, "ffi"
, "lffi"
, "macro"
, "flambda"
, "updateIn"
, "updateIn2"
, "updateIn3"
, "updateIn4"
, "updateIn5"
, "putIn"
, "putIn"
, "putIn2"
, "putIn3"
, "putIn4"
, "putIn5"
, "getIn"
, "getIn2"
, "getIn3"
, "getIn4"
, "getIn5"
]
other ->
False
{-| Flattens Type application into a List of expressions or returns a singleton if it's not a type
-}
flattenApplication : Expression -> List Expression
flattenApplication application =
case application of
Application left right ->
if isMacro application || isTuple application then
flattenApplication left ++ [ right ]
else
[ application ]
other ->
[ other ]
{-| Returns uncurried function application if arguments length is matching definition arity
otherwise returns curried version
-}
functionApplication : Context -> Expression -> Expression -> String
functionApplication c left right =
let
reduceArgs c args separator =
args |> List.map (elixirE c) |> String.join separator
in
case applicationToList (Application left right) of
(Variable [ fn ]) :: args ->
if areMatchingArity c c.mod fn args then
toSnakeCase True fn ++ "(" ++ reduceArgs c args ", " ++ ")"
else if c.inMeta then
Context.crash c "You need to use full "
else
elixirE c left ++ ".(" ++ elixirE c right ++ ")"
(Access (Variable modules) [ fn ]) :: args ->
let
mod =
modulePath modules
fnName =
toSnakeCase True fn
in
if areMatchingArity c mod fn args then
mod ++ "." ++ fnName ++ "(" ++ reduceArgs c args ", " ++ ")"
else
mod ++ "." ++ fnName ++ "().(" ++ reduceArgs c args ").(" ++ ")"
_ ->
elixirE c left ++ ".(" ++ elixirE c right ++ ")"
encodeAccessMacroAndRest : Context -> ( Selector.AccessMacro, List Expression ) -> String
encodeAccessMacroAndRest c ( Selector.AccessMacro t arity selectors, rest ) =
let
encodeSelector (Selector.Access s) =
":" ++ toSnakeCase True s
encodedSelectors =
selectors |> List.map encodeSelector |> String.join ", "
encodedType =
case t of
Selector.Update ->
"update_in_"
Selector.Get ->
"get_in_"
Selector.Put ->
"put_in_"
encodedRest =
case rest of
[] ->
""
list ->
".("
++ (List.map (elixirE c) rest |> String.join ").(")
++ ")"
in
encodedType
++ "(["
++ encodedSelectors
++ "])"
++ encodedRest
{-| Returns code representation of tuple or function depending on definition
-}
tupleOrFunction : Context -> Expression -> String
tupleOrFunction c a =
case flattenApplication a of
-- Not a macro
(Application left right) :: [] ->
functionApplication c left right
-- A macro
(Variable [ "ffi" ]) :: rest ->
Context.crash c "Ffi inside function body is deprecated since Elchemy 0.3"
(Variable [ "macro" ]) :: rest ->
Context.crash c "You can't use `macro` inside a function body"
(Variable [ "tryFfi" ]) :: rest ->
Context.crash c "tryFfi inside function body is deprecated since Elchemy 0.3"
(Variable [ "lffi" ]) :: rest ->
Context.crash c "Lffi inside function body is deprecated since Elchemy 0.3"
(Variable [ "flambda" ]) :: rest ->
Context.crash c "Flambda is deprecated since Elchemy 0.3"
[ Variable [ "Just" ], arg ] ->
"{" ++ elixirE c arg ++ "}"
[ Variable [ "Ok" ], arg ] ->
"{:ok, " ++ elixirE c arg ++ "}"
[ Variable [ "Err" ], arg ] ->
"{:error, " ++ elixirE c arg ++ "}"
[ Variable [ "Do" ], arg ] ->
"quote do " ++ elixirE c arg ++ " end"
-- Regular non-macro application
((Variable list) as call) :: rest ->
Selector.maybeAccessMacro c call rest
|> Maybe.map (encodeAccessMacroAndRest c)
|> Maybe.withDefault
(Helpers.moduleAccess c.mod list
|> (\( mod, last ) ->
aliasFor (Context.changeCurrentModule (Context.maybeModuleAlias c mod) c) last rest
|> Maybe.withDefault
("{"
++ elixirE c (Variable [ last ])
++ ", "
++ (List.map (elixirE c) rest |> String.join ", ")
++ "}"
)
)
)
other ->
Context.crash c ("Shouldn't ever work for" ++ toString other)
{-| Return an alias for type alias or union type if it exists, return Nothing otherwise
-}
aliasFor : Context -> String -> List Expression -> Maybe String
aliasFor c name rest =
maybeOr (typeAliasApplication c name rest) (typeApplication c name rest)
{-| Returns Just only if the passed alias type is a type alias
-}
filterTypeAlias : Context.Alias -> Maybe Context.Alias
filterTypeAlias ({ aliasType } as ali) =
case aliasType of
Context.TypeAlias ->
Just ali
Context.Type ->
Nothing
{-| Returns a type alias application based on current context definitions
-}
typeAliasApplication : Context -> String -> List Expression -> Maybe String
typeAliasApplication c name args =
Context.getAlias c.mod name c
|> Maybe.andThen filterTypeAlias
|> Maybe.andThen (Type.typeAliasConstructor args)
|> Maybe.map (elixirE c)
{-| Returns a type application based on current context definitions
-}
typeApplication : Context -> String -> List Expression -> Maybe String
typeApplication c name args =
Context.getType c.mod name c
|> (Maybe.map <|
\{ arity } ->
let
len =
List.length args
dif =
arity - len
arguments =
generateArguments dif
varArgs =
List.map (List.singleton >> Variable) arguments
in
if arity == 0 then
atomize name
else if dif >= 0 then
(arguments
|> List.map ((++) "fn ")
|> List.map (flip (++) " -> ")
|> String.join ""
)
++ "{"
++ atomize name
++ ", "
++ (List.map (rememberVariables varArgs c |> elixirE) (args ++ varArgs)
|> String.join ", "
)
++ "}"
|> flip (++) (String.repeat dif " end")
else
Context.crash c <|
"Expected "
++ toString arity
++ " arguments for '"
++ name
++ "'. Got: "
++ toString (List.length args)
)
{-| Returns True if an expression is type application or false if it's a regular application
-}
isTuple : Expression -> Bool
isTuple a =
case a of
Application a _ ->
isTuple a
Variable [ "()" ] ->
True
Variable [ name ] ->
isCapitilzed name
Variable list ->
Helpers.moduleAccess "" list
|> (\( _, last ) -> isTuple (Variable [ last ]))
other ->
False
{-| Create 'case' expression by passing a value being "cased on" and list of branches
-}
caseE : Context -> Expression -> List ( Expression, Expression ) -> String
caseE c var body =
"case "
++ elixirE c var
++ " do"
++ String.join "" (List.map (rememberVariables [ var ] c |> caseBranch) body)
++ ind c.indent
++ "end"
{-| Create a single branch of case statement by giving left and right side of the arrow
-}
caseBranch : Context -> ( Expression, Expression ) -> String
caseBranch c ( left, right ) =
(ind (c.indent + 1) ++ elixirE (inArgs c) left)
++ " ->"
++ ind (c.indent + 2)
++ elixirE (c |> indent |> indent |> rememberVariables [ left ]) right
{-| Used to encode a function and create a curried function from a lambda expression
-}
lambda : Context -> List Expression -> Expression -> String
lambda c args body =
case args of
arg :: rest ->
"fn "
++ elixirE (inArgs c) arg
++ " -> "
++ lambda (c |> rememberVariables [ arg ]) rest body
++ " end"
[] ->
elixirE c body
{-| Produce a variable out of it's expression, considering some of the hardcoded values
used for easier interaction with Elixir
-}
elixirVariable : Context -> List String -> String
elixirVariable c var =
case var of
[] ->
""
[ "()" ] ->
"{}"
[ "Nothing" ] ->
"nil"
[ "Just" ] ->
"fn x1 -> {x1} end"
[ "Err" ] ->
"fn x1 -> {:error, x1} end"
[ "Ok" ] ->
"fn x1 -> {:ok, x1} end"
[ "curry" ] ->
"curried()"
[ "uncurry" ] ->
"uncurried()"
list ->
Helpers.moduleAccess c.mod list
|> (\( mod, name ) ->
if isCapitilzed name then
aliasFor (Context.changeCurrentModule mod c) name []
|> Maybe.withDefault (atomize name)
else if String.startsWith "@" name then
String.dropLeft 1 name
|> atomize
else
case operatorType name of
Builtin ->
-- We need a curried version, so kernel won't work
if name == "<|" then
"flip().((&|>/0).())"
else
"(&XBasics." ++ translateOperator name ++ "/0).()"
Custom ->
translateOperator name
None ->
name |> toSnakeCase True |> Variable.varOrNah c
)
================================================
FILE: src/Elchemy/Ffi.elm
================================================
module Elchemy.Ffi exposing (generateFfi)
import Ast.Expression exposing (Expression(..))
import Ast.Statement exposing (Type(TypeConstructor))
import Dict
import Elchemy.Context as Context exposing (Context, Parser, onlyWithoutFlag)
import Elchemy.Function as Function
import Elchemy.Helpers as Helpers
exposing
( applicationToList
, generateArguments
, generateArguments_
, ind
, toSnakeCase
)
import Elchemy.Type as Type
import Elchemy.Variable as Variable exposing (rememberVariables)
{-| Encodes and inlines a foreign function interface macro
-}
generateFfi :
Context
-> Parser
-> String
-> List (List Type)
-> Expression
-> String
generateFfi c elixirE name argTypes e =
let
typeDef =
c.commons.modules
|> Dict.get c.mod
|> Maybe.andThen (.functions >> Dict.get name)
appList =
applicationToList e
uncurryArguments c =
uncurrify c elixirE argTypes
wrapAllInVar =
List.map <| List.singleton >> Variable
in
case ( typeDef, applicationToList e ) of
( Nothing, (Variable [ "ffi" ]) :: _ ) ->
Context.crash c "Ffi requires type definition"
( Nothing, (Variable [ "macro" ]) :: _ ) ->
Context.crash c "Macro requires type definition"
( Just def, [ Variable [ "ffi" ], String mod, String fun ] ) ->
let
arguments =
generateArguments_ "a" def.arity
in
Function.functionCurry c elixirE name def.arity []
++ (onlyWithoutFlag c "noverify" name <|
ind c.indent
++ "verify as: "
++ mod
++ "."
++ fun
++ "/"
++ toString def.arity
)
++ ind c.indent
++ "def"
++ Function.privateOrPublic c name
++ " "
++ toSnakeCase True name
++ "("
++ (arguments |> String.join ", ")
++ ")"
++ ", do: "
++ mod
++ "."
++ fun
++ "("
++ (uncurryArguments (rememberVariables (wrapAllInVar arguments) c) |> String.join ", ")
++ ")"
( Just def, [ Variable [ "macro" ], String mod, String fun ] ) ->
let
arguments =
generateArguments_ "a" def.arity
varArgs =
wrapAllInVar arguments
in
if Type.hasReturnedType (TypeConstructor [ "Macro" ] []) def.def then
"defmacro"
++ Function.privateOrPublic c name
++ " "
++ toSnakeCase True name
++ "("
++ (arguments |> String.join ", ")
++ ")"
++ ", do: "
++ mod
++ "."
++ fun
++ "("
++ (varArgs |> List.map (elixirE (rememberVariables varArgs c)) |> String.join ", ")
++ ")"
else
Context.crash c "Macro calls have to return a Macro type"
( Just def, [ Variable [ "tryFfi" ], String mod, String fun ] ) ->
let
arguments =
generateArguments_ "a" def.arity
in
Function.functionCurry c elixirE name def.arity []
++ ind c.indent
++ "def"
++ Function.privateOrPublic c name
++ " "
++ toSnakeCase True name
++ "("
++ (generateArguments_ "a" def.arity |> String.join ", ")
++ ")"
++ " do "
++ ind (c.indent + 1)
++ "try_catch fn -> "
++ ind (c.indent + 2)
++ mod
++ "."
++ fun
++ "("
++ (uncurryArguments (rememberVariables (wrapAllInVar arguments) c) |> String.join ", ")
++ ")"
++ ind (c.indent + 1)
++ "end"
++ ind c.indent
++ "end"
_ ->
Context.crash c "Wrong ffi definition"
{-| Walk through function definition and uncurry all of the multi argument functions
-}
uncurrify : Context -> Parser -> List (List Type) -> List String
uncurrify c elixirE argTypes =
let
arity =
List.length argTypes - 1
indexes =
List.range 1 arity
in
List.map2 (,) indexes argTypes
|> List.map
(\( i, arg ) ->
case arg of
[] ->
Context.crash c "Impossible"
[ any ] ->
"a" ++ toString i
list ->
let
var =
Variable [ "a" ++ toString i ]
makeFlambda =
Flambda <| List.length list - 1
in
resolveFfi c elixirE (makeFlambda var)
)
type Ffi
= Lffi Expression Expression
| Ffi Expression Expression Expression
| TryFfi Expression Expression Expression
| Flambda Int Expression
| Macro Expression Expression Expression
{-| encodes an ffi based on context and a parser
-}
resolveFfi : Context -> Parser -> Ffi -> String
resolveFfi c elixirE ffi =
let
combineComas args =
args |> List.map (elixirE c) |> String.join ","
in
case ffi of
TryFfi (String mod) (String fun) (Tuple args) ->
"try_catch fn _ -> "
++ mod
++ "."
++ fun
++ "("
++ combineComas args
++ ")"
++ " end"
-- One or many arg fun
TryFfi (String mod) (String fun) any ->
"try_catch fn _ -> "
++ mod
++ "."
++ fun
++ "("
++ elixirE c any
++ ")"
++ " end"
-- Elchemy hack
Ffi (String mod) (String fun) (Tuple args) ->
mod ++ "." ++ fun ++ "(" ++ combineComas args ++ ")"
-- One or many arg fun
Ffi (String mod) (String fun) any ->
mod ++ "." ++ fun ++ "(" ++ elixirE c any ++ ")"
-- Elchemy hack
Macro (String mod) (String fun) (Tuple args) ->
mod ++ "." ++ fun ++ "(" ++ combineComas args ++ ")"
-- One or many arg fun
Macro (String mod) (String fun) any ->
mod ++ "." ++ fun ++ "(" ++ elixirE c any ++ ")"
-- Elchemy hack
Lffi (String fun) (Tuple args) ->
fun ++ "(" ++ combineComas args ++ ")"
-- One arg fun
Lffi (String fun) any ->
fun ++ "(" ++ elixirE c any ++ ")"
Flambda arity fun ->
let
args =
generateArguments arity
in
"fn ("
++ String.join "," args
++ ") -> "
++ elixirE c fun
++ (List.map (\a -> ".(" ++ a ++ ")") args
|> String.join ""
)
++ " end"
_ ->
Context.crash c "Wrong ffi call"
================================================
FILE: src/Elchemy/Function.elm
================================================
module Elchemy.Function
exposing
( functionCurry
, genFunctionDefinition
, genOverloadedFunctionDefinition
, privateOrPublic
)
import Ast.Expression exposing (Expression(..))
import Ast.Statement exposing (Type)
import Dict
import Elchemy.Context as Context exposing (Context, Parser, inArgs, indent)
import Elchemy.Helpers as Helpers
exposing
( Operator(..)
, generateArguments
, ind
, isCustomOperator
, operatorType
, toSnakeCase
, translateOperator
)
import Elchemy.Variable as Variable exposing (rememberVariables)
{-| Encodes a function defintion with all decorations like curry and type spec
-}
genFunctionDefinition :
Context
-> Parser
-> String
-> List Expression
-> Expression
-> String
genFunctionDefinition c elixirE name args body =
let
typeDef =
c.commons.modules
|> Dict.get c.mod
|> Maybe.andThen (.functions >> Dict.get name)
arity =
typeDef |> Maybe.map .arity |> Maybe.withDefault 0
lambdasAt =
getLambdaArgumentIndexes (Maybe.map .def typeDef)
in
if Context.hasFlag "nodef" name c then
functionCurry c elixirE name arity lambdasAt
else
functionCurry c elixirE name arity lambdasAt
++ genElixirFunc c elixirE name args (arity - List.length args) body
++ "\n"
{-| Generates an overloaded function defintion when body is matched on case
-}
genOverloadedFunctionDefinition :
Context
-> Parser
-> String
-> List Expression
-> Expression
-> List ( Expression, Expression )
-> String
genOverloadedFunctionDefinition c elixirE name args body expressions =
let
typeDef =
c.commons.modules
|> Dict.get c.mod
|> Maybe.andThen (.functions >> Dict.get name)
arity =
typeDef |> Maybe.map .arity |> Maybe.withDefault 0
lambdasAt =
getLambdaArgumentIndexes (Maybe.map .def typeDef)
pairAsArgs asArgs =
asArgs
|> List.map2 (flip <| BinOp <| Variable [ "as" ]) args
caseBranch ( left, right ) =
case left of
Tuple matchedArgs ->
genElixirFunc c elixirE name (pairAsArgs matchedArgs) (arity - List.length (pairAsArgs matchedArgs)) right
_ ->
genElixirFunc c elixirE name (pairAsArgs [ left ]) (arity - 1) right
in
if Context.hasFlag "nodef" name c then
functionCurry c elixirE name arity lambdasAt
else
functionCurry c elixirE name arity lambdasAt
++ (expressions
|> List.map caseBranch
|> List.foldr (++) ""
|> flip (++) "\n"
)
{-| Encodes a function defintion based on given params
-}
genElixirFunc :
Context
-> Parser
-> String
-> List Expression
-> Int
-> Expression
-> String
genElixirFunc c elixirE name args missingArgs body =
case ( operatorType name, args ) of
( Builtin, [ l, r ] ) ->
[ ind c.indent
, "def"
, privateOrPublic c name
, " "
, elixirE (c |> rememberVariables [ l ]) l
, " "
, translateOperator name
, " "
, elixirE (rememberVariables [ r ] c) r
, " do"
, ind <| c.indent + 1
, elixirE (indent c |> rememberVariables args) body
, ind c.indent
, "end"
]
|> String.join ""
( Custom, _ ) ->
[ ind c.indent
, "def"
, privateOrPublic c name
, " "
, translateOperator name
, "("
, args
|> List.map (c |> rememberVariables args |> elixirE)
|> flip (++) (generateArguments missingArgs)
|> String.join ", "
, ") do"
, ind <| c.indent + 1
, elixirE (indent c |> rememberVariables args) body
, generateArguments missingArgs
|> List.map (\a -> ".(" ++ a ++ ")")
|> String.join ""
, ind c.indent
, "end"
]
|> String.join ""
( Builtin, _ ) ->
Context.crash c
("operator " ++ name ++ " has to have 2 arguments but has " ++ toString args)
( None, _ ) ->
let
missing =
generateArguments missingArgs
wrapIfMiss s =
if List.length missing > 0 then
s
else
""
missingVarargs =
List.map (List.singleton >> Variable) missing
in
[ ind c.indent
, "def"
, privateOrPublic c name
, " "
, toSnakeCase True name
, "("
, args
++ missingVarargs
|> List.map (c |> inArgs |> elixirE)
|> String.join ", "
, ") do"
, ind <| c.indent + 1
, wrapIfMiss "("
, elixirE (indent c |> rememberVariables (args ++ missingVarargs)) body
, wrapIfMiss ")"
, missing
|> List.map (\a -> ".(" ++ a ++ ")")
|> String.join ""
, ind c.indent
, "end"
]
|> String.join ""
{-| Returns "p" if a function is private. Else returns empty string
-}
privateOrPublic : Context -> String -> String
privateOrPublic context name =
if Context.isPrivate context name then
"p"
else
""
{-| Encodes a curry macro for the function
-}
functionCurry : Context -> Parser -> String -> Int -> List ( Int, Int ) -> String
functionCurry c elixirE name arity lambdasAt =
case ( arity, Context.hasFlag "nocurry" name c ) of
( 0, _ ) ->
""
( _, True ) ->
""
( arity, False ) ->
let
resolvedName =
if isCustomOperator name then
translateOperator name
else
toSnakeCase True name
p =
privateOrPublic c name
lambdas =
lambdasAt
|> List.map (\( a, b ) -> "{" ++ toString a ++ ", " ++ toString b ++ "}")
in
if lambdas == [] || p == "p" then
[ ind c.indent
, "curry"
, " "
, resolvedName
, "/"
, toString arity
]
|> String.join ""
else
[ ind c.indent
, "curry"
, " "
, resolvedName
, "/"
, toString arity
, ", lambdas: ["
, lambdas |> String.join ", "
, "]"
]
|> String.join ""
{-| Gives indexes (starting from 0) of the arguments which
are lambdas with arity bigger than 1
-}
getLambdaArgumentIndexes : Maybe Type -> List ( Int, Int )
getLambdaArgumentIndexes t =
Maybe.map Helpers.typeApplicationToList t
|> Maybe.withDefault []
|> List.map Helpers.typeApplicationToList
|> List.indexedMap (,)
-- -1 since a -> b is not 2 arity
|> List.map (Tuple.mapSecond <| List.length >> (+) -1)
|> List.filter (\( _, r ) -> r > 1)
================================================
FILE: src/Elchemy/Helpers.elm
================================================
module Elchemy.Helpers
exposing
( (=>)
, MaybeUpper(..)
, Operator(..)
, applicationToList
, atomize
, capitalize
, constructApplication
, escape
, filterMaybe
, findInList
, generateArguments
, generateArguments_
, ind
, indAll
, indNoNewline
, isCapitilzed
, isCustomOperator
, isStdModule
, lastAndRest
, listNonEmptyOr
, listToApplication
, maybeOr
, maybeReplaceStd
, moduleAccess
, modulePath
, operatorType
, operators
, ops
, prependAll
, replaceOp
, replaceOp_
, replaceReserved
, reservedBasicFunctions
, reservedKernelFunctions
, reservedWords
, toSnakeCase
, translateOperator
, trimIndentations
, typeApplicationToList
, uncons
, unquoteSplicing
)
import Ast.Expression exposing (Expression(..))
import Ast.Statement exposing (Type(..))
import Char
import Dict exposing (Dict)
import Regex exposing (HowMany(..), Regex(..), regex)
type MaybeUpper
= Upper String
| Lower String
{-| Convert string to snakecase, if the flag is set to true then it won't replace reserved words
-}
toSnakeCase : Bool -> String -> String
toSnakeCase isntAtom s =
let
safe x =
if isntAtom then
replaceReserved x
else
x
in
if String.toUpper s == s then
String.toLower s
else
s
|> Regex.split Regex.All (Regex.regex "(?=[A-Z])")
|> String.join "_"
|> String.toLower
|> safe
capitalize : String -> String
capitalize s =
String.uncons s
|> Maybe.map (Tuple.mapFirst Char.toUpper >> uncurry String.cons)
|> Maybe.withDefault ""
atomize : String -> String
atomize s =
":" ++ toSnakeCase False s
{-| Returns if string start with uppercase
-}
isCapitilzed : String -> Bool
isCapitilzed s =
String.uncons s
|> Maybe.map (Tuple.first >> Char.isUpper)
|> Maybe.withDefault False
indNoNewline : Int -> String
indNoNewline i =
List.repeat ((i + 1) * 2) " " |> String.join ""
ind : Int -> String
ind i =
"\n" ++ indNoNewline i
prependAll : String -> String -> String
prependAll with target =
String.lines target
|> List.map
(\line ->
if String.trim line == "" then
line
else
with ++ line
)
|> String.join "\n"
indAll : Int -> String -> String
indAll i s =
"\n" ++ prependAll (String.dropLeft 1 (ind i)) s
uncons : List a -> ( Maybe a, List a )
uncons list =
case list of
a :: b ->
( Just a, b )
[] ->
( Nothing, [] )
lastAndRest : List a -> ( Maybe a, List a )
lastAndRest list =
list
|> List.reverse
|> uncons
|> Tuple.mapSecond List.reverse
moduleAccess : String -> List String -> ( String, String )
moduleAccess defaultModule stringList =
stringList
|> List.reverse
|> uncons
|> Tuple.mapSecond (List.reverse >> listNonEmptyOr (String.join ".") defaultModule)
|> Tuple.mapFirst (Maybe.withDefault "")
|> (\( a, b ) -> ( b, a ))
listNonEmptyOr : (List a -> b) -> b -> List a -> b
listNonEmptyOr f b aList =
case aList of
[] ->
b
list ->
f list
unquoteSplicing : String -> String
unquoteSplicing =
Regex.replace All (regex "(^\\{|\\}$)") (\_ -> "")
operators : Dict String String
operators =
[ ( "||", "||" )
, ( "&&", "&&" )
, ( "==", "==" )
, ( "/=", "!=" )
, ( "<", "<" )
, ( ">", ">" )
, ( ">=", ">=" )
, ( "<=", "<=" )
, ( "++", "++" )
, ( "+", "+" )
, ( "-", "-" )
, ( "*", "*" )
, ( "/", "/" )
, ( ">>", ">>>" )
, ( "<|", "" )
, ( "<<", "" )
, ( "|>", "|>" )
-- Exception
, ( "%", "rem" )
-- Exception
, ( "//", "div" )
-- Exception
--, ( "rem", "rem" )
-- Exception
, ( "^", "" )
-- Exception
, ( "::", "cons" )
, ( "not", "!" )
, ( ",", "tuple2" )
, ( ",,", "tuple3" )
, ( ",,,", "tuple4" )
, ( ",,,,", "tuple5" )
, ( "as", "=" )
]
|> List.foldl (uncurry Dict.insert) Dict.empty
type Operator
= None
| Builtin
| Custom
isCustomOperator : String -> Bool
isCustomOperator op =
operatorType op == Custom
operatorType : String -> Operator
operatorType name =
let
is_builtin =
operators
|> Dict.keys
|> List.any ((==) name)
is_custom =
Regex.contains (regex "^[+\\-\\/*=.$<>:&|^?%#@~!]+$") name
in
case ( is_builtin, is_custom ) of
( True, _ ) ->
Builtin
( False, True ) ->
Custom
_ ->
None
translateOperator : String -> String
translateOperator op =
case Dict.get op operators of
Just "" ->
Debug.crash <|
op
++ " is not a valid or not implemented yet operator"
Just key ->
key
_ ->
replaceOp op
trimIndentations : String -> String
trimIndentations line =
Regex.replace All (regex "\\s+\\n") (always "\n") line
generateArguments : Int -> List String
generateArguments =
generateArguments_ "x"
generateArguments_ : String -> Int -> List String
generateArguments_ str n =
List.range 1 n
|> List.map toString
|> List.map ((++) str)
escape : String -> String
escape s =
Regex.replace All (regex "\\\\") (always "\\\\") s
ops : List ( Int, Char )
ops =
[ '+', '-', '/', '*', '=', '.', '$', '<', '>', ':', '&', '|', '^', '?', '%', '#', '@', '~', '!' ] |> List.indexedMap (,)
{-| Gives a String representation of module path
-}
modulePath : List String -> String
modulePath list =
let
snakeIfLower a =
if isCapitilzed a then
a
else
toSnakeCase True a
in
list
|> List.map snakeIfLower
|> String.join "."
|> maybeReplaceStd
maybeReplaceStd : String -> String
maybeReplaceStd s =
if isStdModule s then
"Elchemy.X" ++ s
else
s
isStdModule : String -> Bool
isStdModule a =
List.member a
[ "Basics"
, "Bitwise"
, "Char"
, "Date"
, "Debug"
, "Dict"
, "List"
, "String"
, "Maybe"
, "Regex"
, "Result"
, "Set"
, "String"
, "Tuple"
]
reservedWords : List String
reservedWords =
[ "fn"
, "do"
, "end"
, "cond"
, "receive"
, "or"
, "and"
, "quote"
, "unquote"
, "unquote_splicing"
, "module"
, "use"
]
reservedBasicFunctions : List String
reservedBasicFunctions =
[ -- From Elchemy STD
"cons"
, "compare"
, "xor"
, "negate"
, "sqrt"
, "clamp"
, "logBase"
, "e"
, "pi"
, "cos"
, "sin"
, "tan"
, "acos"
, "asin"
, "atan"
, "atan2"
, "round"
, "floor"
, "ceiling"
, "truncate"
, "toFloat"
, "toString"
, "identity"
, "always"
, "flip"
, "tuple2"
, "tuple3"
, "tuple4"
, "tuple5"
, "rec"
]
reservedKernelFunctions : List String
reservedKernelFunctions =
[ -- From Elixir std
"isTuple"
, "abs"
, "apply"
, "binary_part"
, "bit_size"
, "byte_size"
, "div"
, "elem"
, "exit"
, "function_exported?"
, "get_and_update_in"
, "get_in"
, "hd"
, "inspect"
, "is_atom"
, "is_binary"
, "is_bitstring"
, "is_boolean"
, "is_float"
, "is_function"
, "is_integer"
, "is_list"
, "is_map"
, "is_number"
, "is_pid"
, "is_port"
, "is_reference"
, "is_tuple"
, "length"
, "macro_exported?"
, "make_ref"
, "map_size"
, "max"
, "min"
, "node"
, "not"
, "pop_in"
, "put_elem"
, "put_in"
, "rem"
, "round"
, "self"
, "send"
, "spawn"
, "spawn_link"
, "spawn_monitor"
, "struct"
, "struct!"
, "throw"
, "tl"
, "trunc"
, "tuple_size"
, "update_in"
]
replaceOp : String -> String
replaceOp op =
String.toList op
|> List.map replaceOp_
|> String.join ""
|> flip (++) "__"
replaceOp_ : Char -> String
replaceOp_ op =
case
List.filter (\( i, o ) -> op == o) ops
of
( index, _ ) :: _ ->
"op" ++ toString index
_ ->
Debug.crash "Illegal op"
replaceReserved : String -> String
replaceReserved a =
if List.member a reservedWords then
a ++ "__"
else
a
{-| Change application into a list of expressions
-}
applicationToList : Expression -> List Expression
applicationToList application =
case application of
Application left right ->
applicationToList left ++ [ right ]
other ->
[ other ]
{-| Change list of expressions into an application
-}
listToApplication : List Expression -> Expression
listToApplication list =
case list of
[] ->
Debug.crash "Empty list to expression conversion"
[ one ] ->
one
left :: rest ->
Application left (listToApplication rest)
{-| Change type application into a list of expressions
-}
typeApplicationToList : Type -> List Type
typeApplicationToList application =
case application of
TypeApplication left right ->
left :: typeApplicationToList right
other ->
[ other ]
{-| Construct application, rever of applicationToList function
-}
constructApplication : List String -> List Expression
constructApplication list =
case list of
[] ->
Debug.crash "Wrong application"
[ one ] ->
[ Variable [ one ] ]
head :: tail ->
[ List.foldl (\a acc -> Application acc (Variable [ a ])) (Variable [ head ]) tail ]
{-| Nicer syntax for tuples
-}
(=>) : a -> b -> ( a, b )
(=>) =
(,)
infixr 0 =>
{-| Take left maybe, or right maybe if Nothing
-}
maybeOr : Maybe a -> Maybe a -> Maybe a
maybeOr m1 m2 =
case m1 of
Just a ->
m1
Nothing ->
m2
{-| Filter Maybe based on a predicate
-}
filterMaybe : (a -> Bool) -> Maybe a -> Maybe a
filterMaybe f m =
flip Maybe.andThen m <|
\a ->
if f a then
Just a
else
Nothing
{-| Finds a value in a list
-}
findInList : (a -> Bool) -> List a -> Maybe a
findInList f =
flip List.foldl Nothing <|
\a acc ->
if f a then
Just a
else
acc
================================================
FILE: src/Elchemy/Meta.elm
================================================
module Elchemy.Meta exposing (metaDefinition)
{-| Defines a meta module for macro interactions
-}
import Ast.Expression exposing (Expression(..))
import Dict
import Elchemy.Ast as Ast
import Elchemy.Context as Context exposing (Context)
import Elchemy.Expression as Expression
import Elchemy.Helpers as Helpers exposing (ind, modulePath)
type ImportOrRequire
= Import String String Int
| Require String
{-| Defines the meta module for Macro usage
-}
metaDefinition : Context -> String
metaDefinition c =
let
defMeta meta =
"defmodule "
++ c.mod
++ ".Meta do"
++ ind c.indent
++ requiredImports
++ "\n"
++ ind c.indent
++ Expression.elixirE c meta
++ "\nend"
getUsedFunctions =
Ast.walkExpressionOutwards
addMacro t acc =
case t of
Variable [ name ] ->
c.importedFunctions
|> Dict.get name
|> Maybe.map (\( mod, arity ) -> [ Import mod name arity ])
|> Maybe.withDefault []
|> (++) acc
Access (Variable mods) _ ->
Require (modulePath mods) :: acc
_ ->
acc
requiredImports =
c.meta
|> Maybe.map (Ast.foldExpression addMacro [])
|> Maybe.withDefault []
|> List.foldl insertRequirement Dict.empty
|> Dict.toList
|> List.map
(\( k, v ) ->
case v of
[] ->
"require " ++ k
other ->
"import "
++ k
++ ", only: ["
++ (List.map stringify other |> String.join ",")
++ "]"
)
|> String.join (ind c.indent)
stringify ( name, arity ) =
"{:" ++ name ++ ", " ++ toString arity ++ "}"
insertRequirement rOrI dict =
case rOrI of
Require mod ->
dict
|> Dict.update mod (Maybe.withDefault [] >> Just)
Import mod name arity ->
dict
|> Dict.update mod
(Maybe.map ((::) ( name, arity ))
>> Maybe.withDefault [ ( name, arity ) ]
>> Just
)
in
c.meta
|> Maybe.map defMeta
|> Maybe.withDefault ""
================================================
FILE: src/Elchemy/Operator.elm
================================================
module Elchemy.Operator exposing (elixirBinop)
import Ast.Expression exposing (Expression(..))
import Elchemy.Context as Context exposing (Context, Parser)
import Elchemy.Helpers as Helpers exposing (Operator(..), ind, operatorType, translateOperator)
{-| Encode binary operator inlcuding the researved ones
-}
elixirBinop : Context -> Parser -> String -> Expression -> Expression -> String
elixirBinop c elixirE op l r =
case op of
"//" ->
"div(" ++ elixirE c l ++ ", " ++ elixirE c r ++ ")"
"%" ->
"rem(" ++ elixirE c l ++ ", " ++ elixirE c r ++ ")"
"^" ->
":math.pow(" ++ elixirE c l ++ ", " ++ elixirE c r ++ ")"
"::" ->
"["
++ elixirE c l
++ " | "
++ elixirE c r
++ "]"
"<<" ->
elixirBinop c elixirE ">>" r l
"<|" ->
if l == Variable [ "Do" ] then
"quote do " ++ elixirE c r ++ " end"
else
elixirBinop c elixirE "|>" r l
"|>" ->
"("
++ elixirE c l
++ (flattenPipes r
|> List.map (elixirE c)
|> List.map ((++) (ind c.indent ++ "|> ("))
|> List.map (flip (++) ").()")
|> String.join ""
)
++ ")"
"as" ->
elixirE c l
++ " = "
++ elixirE c r
op ->
case operatorType op of
Builtin ->
[ "(", elixirE c l, " ", translateOperator op, " ", elixirE c r, ")" ]
|> String.join ""
Custom ->
translateOperator op
++ "("
++ elixirE c l
++ ", "
++ elixirE c r
++ ")"
None ->
Context.crash c ("Illegal operator " ++ op)
{-| Flattens pipes into a list of expressions
-}
flattenPipes : Expression -> List Expression
flattenPipes e =
case e of
BinOp (Variable [ "|>" ]) l ((BinOp (Variable [ "|>" ]) r _) as n) ->
[ l ] ++ flattenPipes n
BinOp (Variable [ "|>" ]) l r ->
[ l ] ++ [ r ]
other ->
[ other ]
================================================
FILE: src/Elchemy/Selector.elm
================================================
module Elchemy.Selector exposing (AccessMacro(..), AccessMacroType(..), Selector(..), maybeAccessMacro)
import Ast.Expression exposing (Expression(AccessFunction, Application, Variable))
import Char
import Elchemy.Context as Context exposing (Context)
import Elchemy.Helpers as Helpers
import List.Extra
import Regex
type Selector
= Access String
type AccessMacroType
= Get
| Put
| Update
type AccessMacro
= AccessMacro AccessMacroType Int (List Selector)
getSelector : Context -> Expression -> Selector
getSelector c expression =
case expression of
AccessFunction name ->
Access (Helpers.toSnakeCase True name)
_ ->
Context.crash c "The only allowed selectors are: .field"
maybeAccessMacro : Context -> Expression -> List Expression -> Maybe ( AccessMacro, List Expression )
maybeAccessMacro c call args =
let
accessMacroArgs arity args =
case compare (List.length args) arity of
LT ->
Context.crash c <|
"Access macros [updateIn/getIn/putIn] cannot be partially applied. Expecting "
++ toString arity
++ " selector arguments."
EQ ->
( List.map (getSelector c) args, [] )
GT ->
List.Extra.splitAt arity args
|> Tuple.mapFirst (List.map <| getSelector c)
in
case ( call, args ) of
( Variable [ name ], args ) ->
accessMacroType name
|> Maybe.map
(\( t, arity ) ->
let
( selectors, rest ) =
accessMacroArgs arity args
in
( AccessMacro t arity selectors, rest )
)
_ ->
Nothing
accessMacroType : String -> Maybe ( AccessMacroType, Int )
accessMacroType string =
let
getArity =
String.filter Char.isDigit
>> String.toInt
>> Result.withDefault 1
getType x =
[ ( "updateIn\\d?", Update )
, ( "putIn\\d?", Put )
, ( "getIn\\d?", Get )
]
|> List.foldl
(\( match, res ) acc ->
case acc of
Nothing ->
if Regex.contains (Regex.regex match) x then
Just res
else
Nothing
res ->
res
)
Nothing
in
getType string
|> Maybe.map (\t -> ( t, getArity string ))
================================================
FILE: src/Elchemy/Statement.elm
================================================
module Elchemy.Statement exposing (elixirS, moduleStatement)
import Ast
import Ast.BinOp exposing (operators)
import Ast.Expression exposing (Expression(..))
import Ast.Statement exposing (ExportSet(..), Statement(..), Type(..))
import Dict exposing (Dict)
import Elchemy.Alias as Alias
import Elchemy.Context as Context exposing (Context, deindent, indent, onlyWithoutFlag)
import Elchemy.Expression as Expression
import Elchemy.Ffi as Ffi
import Elchemy.Function as Function
import Elchemy.Helpers as Helpers
exposing
( (=>)
, Operator(..)
, filterMaybe
, ind
, indAll
, indNoNewline
, isCustomOperator
, modulePath
, operatorType
, prependAll
, toSnakeCase
, translateOperator
, typeApplicationToList
)
import Elchemy.Type as Type
import Regex exposing (HowMany(..), Regex, regex)
type ElchemyComment
= Doc String
| Ex String
| Normal String
| Flag String
type DocType
= Fundoc
| Typedoc
| ModuleDoc
{-| Make sure first statement is a module declaration
-}
moduleStatement : Statement -> Context
moduleStatement s =
case s of
ModuleDeclaration path exports ->
Context.empty (modulePath path) exports
other ->
Debug.crash "First statement must be module declaration"
typeDefinition : Context -> String -> List Type -> List Type -> Bool -> ( Context, String )
typeDefinition c name args types isUnion =
let
( newC, code ) =
c.lastDoc
|> Maybe.map (elixirDoc c Typedoc name)
|> Maybe.withDefault ( c, "" )
getVariableName t =
case t of
TypeVariable name ->
name
_ ->
Context.crash c (toString t ++ " is not a type variable")
arguments =
if args == [] then
""
else
"("
++ (List.map getVariableName args |> String.join ", ")
++ ")"
mapType =
(if isUnion then
Type.uniontype { c | inTypeDefiniton = True }
else
Type.elixirT False { c | inTypeDefiniton = True }
)
<< Alias.replaceTypeAliases c
in
(,) newC <|
onlyWithoutFlag c "notype" name <|
code
++ ind c.indent
++ "@type "
++ toSnakeCase True name
++ arguments
++ " :: "
++ (List.map mapType types |> String.join " | ")
++ "\n"
{-| Encode any statement
-}
elixirS : Context -> Statement -> ( Context, String )
elixirS c s =
case s of
InfixDeclaration _ _ _ ->
( c, "" )
TypeDeclaration (TypeConstructor [ name ] args) types ->
typeDefinition c name args types True
TypeAliasDeclaration (TypeConstructor [ name ] args) t ->
typeDefinition c name args [ t ] False
FunctionTypeDeclaration "meta" t ->
if t == TypeConstructor [ "List" ] [ TypeConstructor [ "Macro" ] [] ] then
( c, "" )
else
Context.crash c "Function `meta` is reserved and its type has to be of List Macro"
FunctionDeclaration "meta" [] body ->
if not <| definitionExists "meta" c then
Context.crash c "Function `meta` requires type definition of List Macro"
else
( { c | meta = Just body }, "" )
FunctionTypeDeclaration name typedef ->
( c, "" )
FunctionDeclaration name args body ->
let
definition =
c.commons.modules
|> Dict.get c.mod
|> Maybe.andThen (.functions >> Dict.get name >> Maybe.map .def)
|> Maybe.map (Alias.replaceTypeAliases c)
( newC, code ) =
c.lastDoc
|> Maybe.map (elixirDoc c Fundoc name)
|> Maybe.withDefault ( c, "" )
spec =
onlyWithoutFlag newC "nodef" name code
++ (case operatorType name of
Builtin ->
-- TODO implement operator specs
""
Custom ->
definition
|> Maybe.map
(\def ->
onlyWithoutFlag newC "nospec" name <|
ind newC.indent
++ "@spec "
++ translateOperator name
++ Type.typespec newC def
)
|> Maybe.withDefault ""
None ->
definition
|> Maybe.map
(\def ->
onlyWithoutFlag newC "nospec" name <|
ind newC.indent
++ "@spec "
++ toSnakeCase True name
++ Type.typespec newC def
)
|> Maybe.withDefault ""
)
genFfi =
Ffi.generateFfi c Expression.elixirE name <|
(c.commons.modules
|> Dict.get c.mod
|> Maybe.andThen (.functions >> Dict.get name)
|> Maybe.map (.def >> typeApplicationToList)
|> Maybe.withDefault []
|> List.map typeApplicationToList
)
isPrivate =
Context.isPrivate c name
isTuple t =
case t of
Tuple _ ->
True
_ ->
False
in
newC
=> (case body of
(Application (Application (Variable [ "ffi" ]) _) _) as app ->
spec
++ ind (c.indent + 1)
++ genFfi app
(Application (Application (Variable [ "tryFfi" ]) _) _) as app ->
spec
++ ind (c.indent + 1)
++ genFfi app
(Application (Application (Variable [ "macro" ]) _) _) as app ->
ind c.indent
++ genFfi app
++ "\n"
-- Case ((Variable [ _ ]) as var) expressions ->
-- if [ var ] == args then
-- Function.genOverloadedFunctionDefinition c Expression.elixirE name args body expressions
-- else
-- Function.genFunctionDefinition c Expression.elixirE name args body
--
-- Case (Tuple vars) expressions ->
-- if vars == args && List.all (Tuple.first >> isTuple) expressions then
-- Function.genOverloadedFunctionDefinition c Expression.elixirE name args body expressions
-- else
-- Function.genFunctionDefinition c Expression.elixirE name args body
_ ->
spec
++ Function.genFunctionDefinition c Expression.elixirE name args body
)
Comment content ->
elixirComment c content
-- That's not a real import. In elixir it's called alias
ImportStatement path aliasedAs Nothing ->
(c |> Context.addModuleAlias (modulePath path) aliasedAs)
=> ind c.indent
++ "alias "
++ modulePath path
++ aliasAs aliasedAs
ImportStatement path aliasedAs (Just ((SubsetExport exports) as subset)) ->
let
mod =
modulePath path
imports =
List.map exportSetToList exports
|> List.foldr (++) []
excepts =
c.commons.modules
|> Dict.get c.mod
|> Maybe.map (.functions >> Dict.keys >> duplicates imports)
|> Maybe.withDefault []
only =
if imports == [] then
[]
else
[ "only: ["
++ String.join ", " (elixirExportList c mod imports)
++ "]"
]
except =
if excepts == [] then
[]
else
[ "except: ["
++ String.join ", " (elixirExportList c mod excepts)
++ "]"
]
importOrAlias =
if imports == [] && excepts == [] then
"alias "
else
"import "
newC =
c
|> Context.addModuleAlias mod aliasedAs
|> insertImports mod subset
|> Context.mergeTypes subset (modulePath path)
in
newC
=> ind newC.indent
++ importOrAlias
++ ([ [ modulePath path ], only, except ]
|> List.foldr (++) []
|> String.join ", "
)
++ aliasAs aliasedAs
-- Suppresses the compiler warning
ImportStatement [ "Elchemy" ] Nothing (Just AllExport) ->
( c, "" )
ImportStatement modPath aliasedAs (Just AllExport) ->
let
mod =
modulePath modPath
exports =
c.commons.modules
|> Dict.get mod
|> Maybe.map (.functions >> Dict.keys)
|> Maybe.withDefault []
excepts =
c.commons.modules
|> Dict.get c.mod
|> Maybe.map (.functions >> Dict.keys >> duplicates exports)
|> Maybe.withDefault []
except =
if excepts == [] then
[]
else
[ "except: ["
++ String.join ", " (elixirExportList c mod excepts)
++ "]"
]
newC =
c
|> Context.addModuleAlias mod aliasedAs
|> insertImports mod AllExport
|> Context.mergeTypes AllExport mod
in
newC
=> ind c.indent
++ "import "
++ ([ [ mod ], except ]
|> List.foldr (++) []
|> String.join ", "
)
++ aliasAs aliasedAs
s ->
(,) c <|
Context.notImplemented c "statement" s
{-| Returns a String "as: ModuleAlias" or empty string if no module alias
-}
aliasAs : Maybe String -> String
aliasAs =
Maybe.map (\newName -> ", as: " ++ newName)
>> Maybe.withDefault ""
{-| Returns True if
-}
definitionExists : String -> Context -> Bool
definitionExists name c =
c.commons.modules
|> Dict.get c.mod
|> Maybe.andThen (.functions >> Dict.get name)
|> (/=) Nothing
{-| Based on ExportSet of the `import` call inserts all of the imported types
and functions into the current context
-}
insertImports : String -> ExportSet -> Context -> Context
insertImports mod subset c =
let
exportNames =
Type.getExportedTypeNames c mod subset
importedFunctions subset =
case subset of
AllExport ->
c.commons.modules
|> Dict.get mod
|> Maybe.map .functions
|> Maybe.map Dict.toList
|> Maybe.withDefault []
|> List.map (\( key, { arity } ) -> ( key, arity ))
SubsetExport list ->
List.concatMap importedFunctions list
FunctionExport name ->
c.commons.modules
|> Dict.get mod
|> Maybe.map .functions
|> Maybe.andThen (Dict.get name)
|> Maybe.map (\{ arity } -> [ ( name, arity ) ])
|> Maybe.withDefault []
TypeExport _ _ ->
[]
in
{ c
| importedTypes = List.foldl (flip Dict.insert mod) c.importedTypes exportNames
, importedFunctions =
importedFunctions subset
|> List.foldl (\( f, arity ) acc -> Dict.insert f ( mod, arity ) acc) c.importedFunctions
}
{-| Verify correct flag format
-}
verifyFlag : Context -> List String -> Maybe ( String, String )
verifyFlag c flag =
case flag of
[ k, v ] ->
Just ( k, v )
[ "" ] ->
Nothing
a ->
Context.crash c <| "Wrong flag format " ++ toString a
{-| Encode elixir comment and return a context with updated last doc
-}
elixirComment : Context -> String -> ( Context, String )
elixirComment c content =
case getCommentType content of
Doc content ->
if c.hasModuleDoc then
{ c | lastDoc = Just content } => ""
else
elixirDoc c ModuleDoc c.mod content
Ex content ->
(,) c <|
(content
|> String.split "\n"
|> List.map (Regex.replace All (regex "^ ") (always ""))
-- |> List.map String.trim
|> String.join "\n"
|> indAll c.indent
)
Flag content ->
flip (,) "" <|
(content
|> Regex.split All (regex "\\s+")
|> List.map (String.split ":+")
|> List.filterMap (verifyFlag c)
|> List.foldl Context.addFlag c
)
Normal content ->
(,) c <|
(content
|> prependAll "# "
|> indAll c.indent
)
{-| Enocode a doc and return new context
-}
elixirDoc : Context -> DocType -> String -> String -> ( Context, String )
elixirDoc c doctype name content =
let
prefix =
if not c.hasModuleDoc then
"@moduledoc"
else if doctype == Fundoc then
"@doc"
else
"@typedoc"
in
(,)
{ c
| hasModuleDoc = True
, lastDoc = Nothing
}
<|
ind c.indent
++ prefix
++ " \"\"\"\n "
++ (content
|> String.lines
|> List.map (maybeDoctest c name)
|> List.map Helpers.escape
|> List.map (Regex.replace All (regex "\"\"\"") (always "\\\"\\\"\\\""))
-- |> map trimIndentations
|> String.join (ind c.indent)
-- Drop an unnecessary ammounts of \n's
|> Regex.replace All (regex "\n(\n| ){3,}\n") (always "\n\n")
)
++ ind c.indent
++ "\"\"\""
{-| Get a type of the comment by it's content
-}
getCommentType : String -> ElchemyComment
getCommentType comment =
let
findCommentType regex commentType acc =
case acc of
Normal content ->
if Regex.contains regex content then
commentType <|
Regex.replace (Regex.AtMost 1) regex (always "") content
else
Normal content
other ->
other
in
[ ( "^\\sex\\b", Ex )
, ( "^\\|", Doc )
, ( "^\\sflag\\b", Flag )
]
|> List.map (\( a, b ) -> ( Regex.regex a, b ))
|> List.foldl (uncurry findCommentType) (Normal comment)
{-| Encode all exports from a module
-}
exportSetToList : ExportSet -> List String
exportSetToList exp =
case exp of
TypeExport _ _ ->
[]
FunctionExport name ->
[ name ]
AllExport ->
[]
SubsetExport _ ->
[]
elixirExportList : Context -> String -> List String -> List String
elixirExportList c mod list =
let
defineFor name arity =
"{:'"
++ name
++ "', "
++ toString arity
++ "}"
wrap name =
if isCustomOperator name then
defineFor (translateOperator name) 0
++ ", "
++ defineFor (translateOperator name) 2
else if name == "ffi" then
""
else
defineFor (toSnakeCase True name) 0
++ (c.commons.modules
|> Dict.get mod
|> Maybe.map .functions
|> Maybe.andThen (Dict.get name)
|> Maybe.map .arity
|> filterMaybe ((/=) 0)
|> Maybe.map (defineFor (toSnakeCase True name))
|> Maybe.map ((++) ", ")
|> Maybe.withDefault ""
)
in
List.map wrap list
duplicates : List a -> List a -> List a
duplicates listA listB =
List.filter (flip List.member listB) listA
{-| Replace a function doc with a doctest if in correct format
-}
maybeDoctest : Context -> String -> String -> String
maybeDoctest c forName line =
if String.startsWith (ind (c.indent + 1)) ("\n" ++ line) then
case Ast.parseExpression Ast.BinOp.operators (String.trim line) of
Ok ( _, _, BinOp (Variable [ "==" ]) l r ) ->
let
shadowed =
Context.getShadowedFunctions c Helpers.reservedBasicFunctions
++ Context.getShadowedFunctions c Helpers.reservedKernelFunctions
|> List.filter (Tuple.first >> (==) forName)
importBasics =
if shadowed == [] then
""
else
Context.importBasicsWithoutShadowed c
|> String.trimRight
|> String.split "\n"
|> String.join (ind (c.indent + 2) ++ "iex> ")
|> (++) (indNoNewline 1 ++ "iex> ")
|> flip (++) (ind 0)
in
importBasics
++ indNoNewline (c.indent + 1)
++ "iex> import "
++ c.mod
++ ind (c.indent + 2)
++ "iex> "
++ Expression.elixirE c l
++ ind (c.indent + 2)
++ Expression.elixirE c r
++ "\n"
_ ->
line
else
line
================================================
FILE: src/Elchemy/Type.elm
================================================
module Elchemy.Type exposing (elixirT, getExportedTypeNames, hasReturnedType, typeAliasConstructor, typespec, uniontype)
import Ast.Expression exposing (Expression(..))
import Ast.Statement exposing (ExportSet(..), Type(..))
import Dict
import Elchemy.Alias as Alias
import Elchemy.Context as Context exposing (Context, indent)
import Elchemy.Helpers as Helpers
exposing
( atomize
, filterMaybe
, ind
, lastAndRest
, toSnakeCase
, typeApplicationToList
)
{-| Enocde any elm type
-}
elixirT : Bool -> Context -> Type -> String
elixirT flatten c t =
case t of
TypeTuple [] ->
"no_return"
TypeTuple [ a ] ->
elixirT flatten c a
TypeTuple ((a :: rest) as list) ->
"{"
++ (List.map (elixirT flatten c) list |> String.join ", ")
++ "}"
TypeVariable "number" ->
"number"
(TypeVariable name) as var ->
case String.uncons name of
Just ( '@', name ) ->
toSnakeCase True name
any ->
if c.inTypeDefiniton then
name
else
"any"
TypeConstructor t args ->
let
( mod, last ) =
Helpers.moduleAccess c.mod t
modulePath =
if mod == c.mod then
c.importedTypes
|> Dict.get last
|> Maybe.map (\a -> a ++ ".")
|> Maybe.withDefault ""
else
mod ++ "."
in
modulePath ++ elixirType flatten c last args
TypeRecord fields ->
"%{"
++ ind (c.indent + 1)
++ (fields
|> List.map (\( k, v ) -> toSnakeCase False k ++ ": " ++ elixirT flatten (indent c) v)
|> String.join ("," ++ ind (c.indent + 1))
)
++ ind c.indent
++ "}"
(TypeRecordConstructor _ _) as tr ->
"%{"
++ ind (c.indent + 1)
++ (typeRecordFields (indent c) flatten tr
|> String.join (", " ++ ind (c.indent + 1))
)
++ ind c.indent
++ "}"
TypeApplication l r ->
if flatten then
typeApplicationToList r
|> lastAndRest
|> (\( last, rest ) ->
"("
++ ((l :: rest)
|> List.map (elixirT flatten (indent c))
|> String.join ", "
)
++ " -> "
++ (last
|> Maybe.map (elixirT flatten c)
|> Maybe.withDefault ""
)
++ ")"
)
else
"("
++ elixirT flatten c l
++ " -> "
++ elixirT flatten c r
++ ")"
{-| alias for elixirT with flatting of type application
-}
elixirTFlat : Context -> Type -> String
elixirTFlat =
elixirT True
{-| alias for elixirT without flatting of type application
-}
elixirTNoFlat : Context -> Type -> String
elixirTNoFlat =
elixirT False
{-| Return fieilds of type record as a list of string key value pairs
-}
typeRecordFields : Context -> Bool -> Type -> List String
typeRecordFields c flatten t =
let
keyValuePair ( k, v ) =
k ++ ": " ++ elixirT flatten c v
in
case t of
TypeRecordConstructor (TypeConstructor [ name ] args) fields ->
let
inherited =
Context.getAlias c.mod name c
|> Maybe.map (\{ typeBody } -> Alias.resolveTypeBody c typeBody args)
|> Maybe.map (typeRecordFields c flatten)
in
List.map keyValuePair fields
++ Maybe.withDefault [ "" ] inherited
TypeRecordConstructor (TypeRecord inherited) fields ->
List.map keyValuePair <| fields ++ inherited
TypeRecordConstructor (TypeVariable _) fields ->
List.map keyValuePair fields
TypeRecordConstructor (TypeTuple [ a ]) fields ->
typeRecordFields c flatten (TypeRecordConstructor a fields)
TypeRecordConstructor ((TypeRecordConstructor _ _) as tr) fields ->
List.map keyValuePair fields
++ typeRecordFields c flatten tr
(TypeRecord fields) as tr ->
List.map keyValuePair fields
any ->
Context.crash c ("Wrong type record constructor " ++ toString any)
{-| Translate and encode Elm type to Elixir type
-}
elixirType : Bool -> Context -> String -> List Type -> String
elixirType flatten c name args =
case ( name, args ) of
( "String", [] ) ->
"String.t"
( "Char", [] ) ->
"integer"
( "Bool", [] ) ->
"boolean"
( "Int", [] ) ->
"integer"
( "Pid", [] ) ->
"pid"
( "Float", [] ) ->
"float"
( "List", [ t ] ) ->
"list(" ++ elixirT flatten c t ++ ")"
-- ( "Dict", [ key, val ] ) ->
-- "%{}"
( "Maybe", [ t ] ) ->
"{" ++ elixirT flatten c t ++ "} | nil"
( "Nothing", [] ) ->
"nil"
( "Just", [ t ] ) ->
elixirT flatten c t
( "Err", [ t ] ) ->
"{:error, " ++ elixirT flatten c t ++ "}"
( "Ok", [ t ] ) ->
if t == TypeTuple [] then
"ok"
else
"{:ok," ++ elixirT flatten c t ++ "}"
( t, [] ) ->
toSnakeCase True t
-- aliasOr c t [] (atomize t)
( t, list ) ->
toSnakeCase True t
++ "("
++ (List.map (elixirT flatten c) list |> String.join ", ")
++ ")"
-- aliasOr c t list <|
-- "{"
-- ++ atomize t
-- ++ ", "
-- ++ (List.map (elixirT flatten c) list |> String.join ", ")
-- ++ "}"
{-| Gets all types from a subset export
-}
getExportedTypeNames : Context -> String -> ExportSet -> List String
getExportedTypeNames c mod subset =
case subset of
SubsetExport list ->
List.concatMap (getExportedTypeNames c mod) list
TypeExport name _ ->
[ name ]
AllExport ->
c.commons.modules
|> Dict.get mod
|> Maybe.map (\mod -> (mod.aliases |> Dict.keys) ++ (mod.types |> Dict.keys))
|> Maybe.withDefault []
FunctionExport _ ->
[]
{-| Enocde a typespec with 0 arity
-}
typespec0 : Context -> Type -> String
typespec0 c t =
"() :: " ++ elixirTNoFlat c t
{-| Encode a typespec
-}
typespec : Context -> Type -> String
typespec c t =
case t |> typeApplicationToList |> lastAndRest of
( Just last, args ) ->
"("
++ (List.map (elixirTNoFlat c) args
|> String.join ", "
)
++ ") :: "
++ elixirTNoFlat c last
( Nothing, _ ) ->
Context.crash c "impossible"
{-| Encode a union type
-}
uniontype : Context -> Type -> String
uniontype c t =
case t of
TypeConstructor [ name ] [] ->
atomize name
TypeConstructor [ name ] list ->
"{"
++ atomize name
++ ", "
++ (List.map (elixirTNoFlat c) list |> String.join ", ")
++ "}"
other ->
Context.crash c ("I am looking for union type constructor. But got " ++ toString other)
{-| Change a constructor of a type alias into an expression after resolving it from contextual alias
-}
typeAliasConstructor : List Expression -> Context.Alias -> Maybe Expression
typeAliasConstructor args ({ parentModule, aliasType, arity, body, typeBody } as ali) =
case ( aliasType, body ) of
( Context.Type, _ ) ->
Nothing
( _, TypeConstructor [ name ] _ ) ->
Nothing
( _, TypeRecord kvs ) ->
let
params =
List.length kvs
|> (+) (0 - List.length args)
|> List.range 1
|> List.map (toString >> (++) "arg")
|> List.map (List.singleton >> Variable)
varargs =
kvs
|> List.map2 (flip (,)) (args ++ params)
|> List.map (Tuple.mapFirst Tuple.first)
in
Record varargs
|> Lambda params
|> Just
-- Error in AST. Single TypeTuple are just paren app
( _, TypeTuple [ app ] ) ->
typeAliasConstructor args { ali | typeBody = Context.SimpleType app }
( _, TypeTuple kvs ) ->
let
args =
List.length kvs
|> List.range 1
|> List.map (toString >> (++) "arg")
|> List.map (List.singleton >> Variable)
in
Just (Lambda args (Tuple args))
( _, TypeVariable name ) ->
Just (Variable [ name ])
other ->
Nothing
{-| Apply alias, orelse return the provided default value
-}
aliasOr : Context -> String -> List Type -> String -> String
aliasOr c name args default =
Context.getAlias c.mod name c
|> (Maybe.map <|
\{ parentModule, typeBody, aliasType } ->
if parentModule == c.mod then
elixirTNoFlat c (Alias.resolveTypeBody c typeBody args)
else
case aliasType of
Context.Type ->
parentModule ++ "." ++ elixirTNoFlat c (Alias.resolveTypeBody c typeBody args)
Context.TypeAlias ->
Alias.resolveTypeBody c typeBody args
|> elixirTNoFlat { c | mod = parentModule }
)
|> Maybe.withDefault default
hasReturnedType : Type -> Type -> Bool
hasReturnedType returned t =
case List.reverse (typeApplicationToList t) of
[] ->
False
t :: _ ->
t == returned
================================================
FILE: src/Elchemy/Variable.elm
================================================
module Elchemy.Variable
exposing
( extractName
, groupByCrossDependency
, organizeLetInVariablesOrder
, rememberVariables
, varOrNah
)
import Ast.Expression exposing (..)
import Elchemy.Context as Context exposing (Context, inArgs)
import Elchemy.Helpers as Helpers exposing (toSnakeCase)
import List.Extra
import Set
{-| Put variables into context so they are treated like variables in future
-}
rememberVariables : List Expression -> Context -> Context
rememberVariables list c =
let
addToContext var context =
{ context
| variables = Set.insert (toSnakeCase True var) context.variables
}
in
list
|> List.map extractVariablesUsed
|> List.foldr (++) []
|> List.foldl addToContext c
{-| Check if a string is a variable or no, based on remembered variables
-}
varOrNah : Context -> String -> String
varOrNah c var =
if Set.member var c.variables || c.inArgs then
var
else if c.inMeta then
c.mod ++ "." ++ var ++ "()"
else
var ++ "()"
{-| Extract variables from an expression
-}
extractVariablesUsed : Expression -> List String
extractVariablesUsed exp =
let
many vars =
vars
|> List.map extractVariablesUsed
|> List.foldr (++) []
one var =
[ var ]
none =
[]
in
case exp of
Record vars ->
vars
|> List.map Tuple.second
|> many
Tuple vars ->
many vars
Variable [ name ] ->
one name
List vars ->
many vars
Application left right ->
many [ left, right ]
BinOp (Variable [ "::" ]) x xs ->
many [ x, xs ]
BinOp (Variable [ "as" ]) ((Variable _) as v1) ((Variable _) as v2) ->
many [ v1, v2 ]
BinOp (Variable [ "as" ]) l ((Variable [ _ ]) as r) ->
many [ l, r ]
BinOp _ l r ->
many [ l, r ]
-- Assignments
Case head branches ->
List.concatMap (uncurry rightWithoutLeft) branches
|> withoutVars (extractVariablesUsed head)
Let definitions return ->
List.concatMap (uncurry rightWithoutLeft) definitions
Lambda head body ->
extractVariablesUsed body
|> withoutVars (List.concatMap extractVariablesUsed head)
_ ->
none
{-| Organize let in variables in order based on how they use each other
Example:
a = b
b = 1
Would become:
b = 1
a = b
-}
organizeLetInVariablesOrder : Context -> List ( Expression, Expression ) -> List ( Expression, Expression )
organizeLetInVariablesOrder c expressionList =
case bubbleSelect (\a b -> not <| isIn a b) expressionList of
Ok list ->
list
Err list ->
let
_ =
Context.crash c <|
"Couldn't find a solution to "
++ toString (list |> List.map Tuple.first)
in
[]
{-| Returns a name of a variable, or a name of a function being applied
-}
extractName : Context -> Expression -> String
extractName c expression =
case Helpers.applicationToList expression of
[ Variable [ name ] ] ->
name
[ single ] ->
Context.crash c (toString single ++ " is not a variable")
multi ->
List.head multi
|> Maybe.map (extractName c)
|> Maybe.withDefault ""
{-| Returns a list of names of a variable, or a name of a function being applied
a = 1 --> ["a"]
f a b c = 1 --> ["f"]
(a, b, c) = 1 --> ["a", "b", "c"]
-}
extractNamesAssigned : Expression -> List String
extractNamesAssigned expression =
case Helpers.applicationToList expression of
[ Variable [ name ] ] ->
[ name ]
[ single ] ->
extractVariablesUsed single
multi ->
List.head multi
|> Maybe.map extractNamesAssigned
|> Maybe.withDefault []
{-| Extracts only the arguments of an expression (if the expression is a function)
-}
extractArguments : Expression -> List String
extractArguments expression =
case Helpers.applicationToList expression of
[ single ] ->
[]
multi ->
List.tail multi
|> Maybe.map (List.concatMap extractNamesAssigned)
|> Maybe.withDefault []
{-| Returns true if a name of right argument is mentioned in a body of left variable
-}
isIn : ( Expression, Expression ) -> ( Expression, Expression ) -> Bool
isIn ( leftHead, _ ) ( rightHead, rightDef ) =
let
anyMembers members list =
List.any (flip List.member list) members
in
withoutVars (extractArguments rightHead) (extractVariablesUsed rightDef)
|> anyMembers (extractNamesAssigned leftHead)
{-| Returns a list of variables in right, without these passed as first argument}
-}
withoutVars : List String -> List String -> List String
withoutVars vars right =
List.filter (not << flip List.member vars) right
{-| Returns a list of variables used in right expression, without variables defined in
left expression
-}
rightWithoutLeft : Expression -> Expression -> List String
rightWithoutLeft left right =
withoutVars (extractVariablesUsed left) (extractVariablesUsed right)
{-| Groups functions that mutually call each other in lists
-}
groupByCrossDependency : List ( Expression, Expression ) -> List (List ( Expression, Expression ))
groupByCrossDependency expressionsList =
expressionsList
|> List.Extra.groupWhile (\l r -> isIn l r && isIn r l)
{-| Selects a correct order of elements in a list based on a dependency algorithm
if dependency requirement (f) is met for all of the other elements it is inserted
otherwise it looks for next element that fits the requirement.
Function returns first combination found, satifying the predicate.
If no function was found it returns Nothing
-}
bubbleSelect : (a -> a -> Bool) -> List a -> Result (List a) (List a)
bubbleSelect f list =
let
findIndex discarded list =
List.Extra.break (\a -> List.all (f a) discarded) list
findNext discarded list acc =
case list of
[] ->
if discarded == [] then
Ok <| List.reverse acc
else
Err discarded
-- Trick to allow mutual recursion
-- case findIndex discarded acc of
-- ( l, r ) ->
-- Ok <| l ++ (List.reverse discarded) ++ r
current :: tail ->
let
newlist =
discarded ++ tail
in
if List.all (flip f current) newlist then
findNext [] newlist (current :: acc)
else
findNext (current :: discarded) tail acc
in
findNext [] list []
================================================
FILE: templates/Hello.elm
================================================
module Hello exposing (..)
{-| Example module, It says hello if you ask it nicely
-}
{-| Prints "world!"
hello == "world!"
-}
hello : String
hello =
"world!"
================================================
FILE: templates/elchemy.exs
================================================
defmodule ElchemyInit do
@deps_directory_depth 3
def init(project) do
if !project || !project[:deps] do
IO.warn """
The project structure is invalid. Make sure that
|> elem(Code.eval_file(".elchemy.exs"), 0).init
Line was put __after__ the closing bracked `]`
"""
else
project
|> put_in([:compilers], [:elchemy | (project[:compilers] || Mix.compilers())])
|> put_in([:elchemy_path], "elm")
|> put_in([:deps], project[:deps] ++ elm_deps())
end
end
def elm_deps() do
if File.exists?("elm-deps") do
find!("elm-deps", @deps_directory_depth)
|> check_mix_file()
|> Enum.map(fn {app_name, path} ->
{app_name, path: path, override: true}
end)
else
[]
end
end
def check_mix_file(paths) do
Enum.map paths, fn path ->
mix_file = Path.join(path, "mix.exs")
if File.exists?(mix_file) do
{parse_app_name(mix_file), path}
else
{app_name, version} = app_info_from_path(path)
create_mix_file(mix_file, app_name, version)
{app_name, path}
end
end
end
def create_mix_file(mix_file, app_name, version) do
module_name = app_name
|> Atom.to_string
|> String.split("_")
|> Enum.map(&String.capitalize/1)
|> Enum.join("")
content =
"""
defmodule #{module_name}.Mixfile do
use Mix.Project
def project do
[app: #{inspect app_name},
version: "#{version}",
elixir: "~> 1.4",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
elixirc_paths: ["src"],
deps: deps()]
end
def application do
[extra_applications: [:logger]]
end
defp deps, do: [{:elchemy, override: false}]
end
"""
IO.puts "Creating mix file #{mix_file}"
File.write!(mix_file, content)
end
def app_info_from_path(path) do
[_, _, repo_name, version] = path |> Path.split
app_name = repo_name
|> String.replace(~r"[-]", "_")
|> String.replace(~r"[^a-zA-Z0-9_]", "")
|> String.to_atom
{app_name, version}
end
def find!(dir, depth), do: find!([], dir, depth)
def find!(dirs, dir, 0), do: [dir | dirs]
def find!(dirs, dir, depth) do
dir
|> File.ls!
|> Enum.map(&Path.join(dir, &1))
|> Enum.filter(&File.dir?/1)
|> Enum.reduce(dirs, &find!(&2, &1, depth - 1))
end
def parse_app_name(mix_file) do
contents = File.read!(mix_file)
{app_name, _} = ~r"app:(.*?)," |> Regex.run(contents, capture: :all_but_first)
|> List.first |> String.trim |> Code.eval_string
app_name
end
end
ElchemyInit
================================================
FILE: templates/elchemy_test.exs
================================================
defmodule ElchemyTest do
use ExUnit.Case
use Elchemy
doctest Hello
test "Hello" do
assert Hello.hello() == "world!"
end
end
================================================
FILE: templates/elm-package.json
================================================
{
"version": "1.0.0",
"summary": "helpful summary of your project, less than 80 characters",
"repository": "https://github.com/user/project.git",
"license": "BSD3",
"source-directories": [
"./elm"
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "5.1.1 <= v < 6.0.0",
"wende/elchemy-core": "0.0.0 <= v < 0.8.8"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}
================================================
FILE: tests/.gitignore
================================================
/elm-stuff/
================================================
FILE: tests/Main.elm
================================================
port module Main exposing (..)
import Test
import Tests
import UnitTests
import Test.Runner.Node exposing (run, TestProgram)
import Json.Encode exposing (Value)
main : TestProgram
main =
run emit (Test.describe "Elchemy" [ Tests.all, UnitTests.all ])
port emit : ( String, Value ) -> Cmd msg
================================================
FILE: tests/Tests.elm
================================================
module Tests exposing (accessMacros, all, binOps, caseOfs, doctests, fileImports, functions, has, hasFull, letIns, lists, records, specs, tuples, typeAliases, typeConstructors, types)
import Elchemy.Compiler as Compiler
import Expect
import Regex exposing (..)
import String
import Test exposing (..)
(|++) : String -> String -> String
(|++) l r =
l ++ "\n" ++ r
hasFull expected s =
let
result =
Compiler.tree s
in
String.contains (String.trim expected) result
|> Expect.true ("Code:\n" ++ result ++ "\n\ndoes not contain:\n" ++ expected)
has : String -> String -> Expect.Expectation
has expected s =
let
result =
Compiler.tree ("module MyModule exposing (nothing) \n{-| Moduledoc -}" ++ s)
|> Regex.replace All (regex "\\n( )+") (always "")
|> Regex.replace All (regex "( )+") (always " ")
in
String.contains (String.trim expected) result
|> Expect.true ("Code:\n" ++ result ++ "\n\ndoes not contain:\n" ++ expected)
tuples : Test
tuples =
describe "Tuples"
[ test "Tuples w spaces" <|
\() ->
"tuple = (1, 2)" |> has "{1, 2}"
, test "Tuples w/o spaces" <|
\() ->
"tuple = ( 1, 2 )" |> has "{1, 2}"
, test "Nested tuples" <|
\() ->
"tuple = (1, (2, 3))" |> has "{1, {2, 3}}"
]
lists : Test
lists =
describe "Lists"
[ test "Lists w spaces" <|
\() ->
"list = [ 1, 2 ]" |> has "[1, 2]"
, test "Lists w/o spaces" <|
\() ->
"list = [1, 2]" |> has "[1, 2]"
, test "Nested Lists" <|
\() ->
"list = [ 1, [2, 3] ]" |> has "[1, [2, 3]]"
, test "Other nested list" <|
\() ->
"list = [[1, 2], 3]" |> has "[[1, 2], 3]"
, test "Cons operator" <|
\() ->
"list = 1 :: 2 :: [3]" |> has "[1 | [2 | [3]]]"
]
functions : Test
functions =
let
testModules =
"""
>>>> b.elm
module B exposing (..)
import A exposing (..)
testFull : Int
testFull = A.fun 1 2
testCurried : Int
testCurried = A.fun 1
join : List String -> String
join a = String.join " " a
tested : Float
tested = importedFun 10 10.0
>>>> a.elm
module A exposing (fun, importedFun)
fun : Int -> Int -> Int
fun a b = 1
importedFun : Int -> Float -> Float
importedFun a b = 1
"""
in
describe "Functions"
[ test "Application" <|
\() ->
"app = a b c d" |> has "a().(b()).(c()).(d())"
, test "Uncurried application when all args provided" <|
\() ->
"a : a -> a -> a -> a"
|++ "app = a b c d"
|> has "a(b(), c(), d())"
, test "ffi" <|
\() ->
"upcase : String -> String"
|++ "upcase name = ffi \"String\" \"to_upper\" "
|> has "String.to_upper("
, test "macro" <|
\() ->
"upcase : Macro"
|++ "upcase = macro \"String\" \"to_upper\" "
|> has "String.to_upper("
, test "macro doesn't lambdify arguments" <|
\() ->
"upcase : (Int -> Int) -> Macro"
|++ "upcase = macro \"String\" \"to_upper\" "
|> has "String.to_upper(a1)"
, test "Function names are snakecased" <|
\() ->
"camelCase = 1" |> has "camel_case()"
, test "Function calls are snakecased" <|
\() ->
"a = camelCase 1" |> has "camel_case().(1)"
, test "Uncurried function calls are snakecased" <|
\() ->
"fooBar : a -> a -> a"
|++ "app = fooBar 1 2"
|> has "foo_bar(1, 2)"
, test "Can call function recursively" <|
\() ->
"a = let f a = f (a - 1) in f"
|> has "f = rec f, fn a ->"
, test "Correct curried application from modules" <|
\() -> testModules |> hasFull "A.fun().(1)"
, test "Correct full application from modules" <|
\() -> testModules |> hasFull "A.fun(1, 2)"
, test "Correct curried application for undefined module" <|
\() -> testModules |> hasFull "Elchemy.XString.join().(\" \").(a)"
, test "Correct curried application for imported functions" <|
\() -> testModules |> hasFull "imported_fun(10, 10.0)"
]
binOps : Test
binOps =
describe "Binary Operators"
[ test "Simple ops" <|
\() ->
"add = a + b" |> has "a() + b()"
, test "Ops as lambda" <|
\() ->
"add = (+)" |> has "(&XBasics.+/0).()"
, test "Ops as lambda with param" <|
\() ->
"add = ((+) 2)" |> has "(&XBasics.+/0).().(2)"
, test "Complex ops as lambda " <|
\() ->
"add = map (+) list" |> has "map().((&XBasics.+/0).()).(list())"
]
specs : Test
specs =
describe "Specs"
[ test "Typespecs with dependant types" <|
\() ->
"sum : (List Int) -> Int"
|++ "sum = 0"
|> has "@spec sum(list(integer)) :: integer"
, test "Typespecs with functions" <|
\() ->
"map : (List a) -> (a -> a) -> (List a)"
|++ "map = 0"
|> has "map(list(any), (any -> any)) :: list(any)"
, test "Typespecs with functions #2" <|
\() ->
"map : (a -> a) -> (b -> b) -> (List a)"
|++ "map = 0"
|> has "map((any -> any), (any -> any)) :: list(any)"
, test "Typespecs with multiple arg functions" <|
\() ->
"map : (List a) -> (a -> a -> b) -> (List a)"
|++ "map = 0"
|> has "map(list(any), (any -> (any -> any))) :: list(any) "
, test "Typespecs names are snakecased" <|
\() ->
"mapMap : a"
|++ "mapMap = 0"
|> has "@spec map_map"
, test "Records in typespecs" <|
\() ->
"record : { a : Int, b : String}"
|++ "record = 0"
|> has "@spec record() :: %{a: integer,b: String.t}"
, test "Remote typespecs" <|
\() ->
"f : Remote.Module.Type -> String.T"
|++ "f = 0"
|> has "f(Remote.Module.type) :: String.t"
]
records : Test
records =
describe "Records"
[ test "Records work" <|
\() ->
"a = { a = 1 }" |> has "%{a: 1}"
, test "Complex records work" <|
\() ->
"a = { a = 1, b = 2, c = (a b)}" |> has "%{a: 1, b: 2, c: a().(b())}"
, test "Updating records work" <|
\() ->
"addToA r = {r | a = (r.a + 5), b = 2} " |> has "%{r | a: (r.a + 5), b: 2}"
]
types : Test
types =
describe "types"
[ test "Types" <|
\() ->
"type AType = BType | CType" |> has "@type a_type :: :b_type | :c_type"
, test "TypeRecord" <|
\() ->
"type alias A = {a : Int, b: Int, c: Int}"
|++ "a = A 1 2 3"
|> has "%{a: 1, b: 2, c: 3}"
, test "Type alias application" <|
\() ->
"type alias A = {a : Int, b : Int}"
|++ "a = A 10"
|> has "fn arg1 -> %{a: 10, b: arg1} end"
, test "Types work when applied incompletely" <|
\() ->
"type Focus = A Int Int | B Int Int Int"
|++ "a = B 1 1"
|> has "fn x1 -> {:b, 1, 1, x1} end"
, test "TypeTuple" <|
\() ->
"type alias A = (Int, Int, Int)"
|++ "a = A 1 2 "
|> has "{arg1, arg2, arg3}"
, test "Types ignore typealiases" <|
\() ->
"type alias AnyAlias = Lol"
|++ "type AnyType = AnyAlias | AnyType"
|> has "@type any_type :: :any_alias | :any_type"
, test "Types can wrap records" <|
\() ->
"type Lens big small = Lens { get : big -> small }"
|> has "@type lens(big, small) :: {:lens, %{get: (big -> small)}}"
, test "Types args don't polute type application" <|
\() ->
"type Focus big small = Focus { get : big -> small }"
|++ "a = Focus { get = get, update = update }"
|> has "{:focus, %{get: get(), update: update()}}"
]
typeConstructors : Test
typeConstructors =
describe "Type Constructors"
[ test "Type application" <|
\() ->
"a = Type a b c" |> has "{:type, a(), b(), c()}"
, test "Type in tuple" <|
\() ->
"a = (Type, a, b, c)" |> has "{:type, a(), b(), c()}"
, test "Remote types" <|
\() ->
"a = Remote.Type a b c" |> has "{:type, a(), b(), c()}"
, test "Remote types in tuples" <|
\() ->
"a = (Remote.Type, a, b, c)" |> has "{:type, a(), b(), c()}"
]
doctests : Test
doctests =
describe "Doctests"
[ test "Doctests" <|
\() ->
"{-| A equals 1. It just does\n"
++ " a == 1\n"
++ "-}\n"
++ "a : Int\n"
++ "a = 1\n"
|> has "iex> a\n"
]
typeAliases : Test
typeAliases =
describe "Type aliases in specs"
[ test "TypeAlias substitution" <|
\() ->
"type alias MyType a = List a"
|++ "test : MyType Int"
|++ "test = 0"
|> has "@spec test() :: my_type(integer"
, test "Type substitution" <|
\() ->
"type MyType = Wende | NieWende"
|++ "test : MyType"
|++ "test = 0"
|> has "@spec test() :: my_type"
, test "TypeAlias argument substitution" <|
\() ->
"type alias MyType a = List a"
|++ "test : MyType Int"
|++ "test = 0"
|> has "@spec test() :: my_type(integer)"
, test "TypeAlias argument substitution between types" <|
\() ->
"type alias AnyKey val = (a, val)"
|++ "type alias Val a = AnyKey a"
|++ "test : Val Int"
|++ "test = 0"
|> has "@spec test() :: val(integer)"
, test "TypeAlias no argument substitution in Type" <|
\() ->
"type alias MyList a = List a"
|++ "type Val a = AnyKey (MyList a)"
|++ "test : Val Int"
|++ "test = 0"
|> has "@spec test() :: val"
-- Polymorhpism
, test "Polymorhpic record alias" <|
\() ->
"type Wende = Wende"
|++ "type alias Wendable a = { a | wendify : (a -> Wende)}"
|++ "type alias Man = Wendable { gender: Bool }"
|++ "a : Man -> String "
|++ "a = 0"
|> has "@type man :: %{wendify: (%{gender: boolean} -> wende), gender: boolean"
, test "Multi polymorhpic record alias" <|
\() ->
"type Wende = Wende"
|++ "type alias Namable a = { a | name : String }"
|++ "type alias Agable a = { a | age: Int }"
|++ "type alias Man = Namable (Agable { gender : String })"
|++ "a : Man -> String "
|++ "a = 0"
|> has "@type man :: %{name: String.t, age: integer, gender: String.t}"
, test "Interface as type" <|
\() ->
"type alias Namable a = { a | name : String }"
|++ "getName : Namable a -> String"
|++ "getName = 0"
|> has "@spec get_name(%{name: String.t}) :: String.t"
]
fileImports =
describe "Imports"
[ test "Same alias names in two files" <|
\() ->
"""
>>>> FileA.elm
module A exposing (..)
type alias A = Int
>>>> FileB.elm
module B exposing (..)
type alias B = Float
"""
-- If it compiles it's already good
|> hasFull ""
, test "Imported alias from another file" <|
\() ->
"""
>>>> FileA.elm
module A exposing (..)
type alias MyAlias = Int
>>>> FileB.elm
module B exposing (..)
import A exposing (..)
a : MyAlias
a = 1
"""
|> hasFull "@spec a() :: A.my_alias"
, test "Imported type from another file" <|
\() ->
"""
>>>> FileA.elm
module A exposing (..)
type MyType = TypeA Int | TypeB Int
a : MyType
a = TypeA
>>>> FileB.elm
module B exposing (..)
import A exposing (..)
a : MyType
a = TypeB
"""
|> hasFull "fn x1 -> {:type_b, x1} end"
, test "Named type from another aliased module" <|
\() ->
"""
>>>> Foo.elm
module Foo exposing (Baz)
type alias Baz = {a: Int, b: Int}
>>>> Bar.elm
module Bar exposing (..)
import Foo
a : Foo.Baz
a = Foo.Baz 10 20
"""
|> hasFull "%{a: 10, b: 20}"
, test "Named type from another aliased module with as" <|
\() ->
"""
>>>> Foo.elm
module Foo.Fighters exposing (Baz)
type alias Baz = {a: Int, b: Int}
>>>> Bar.elm
module Bar exposing (..)
import Foo.Fighters as Fighters exposing(a)
a : Fighters.Baz
a = Fighters.Baz 10 20
"""
|> hasFull "%{a: 10, b: 20}"
, test "Imported specific type from another file" <|
\() ->
"""
>>>> FileA.elm
module A exposing (..)
type MyType = TypeA Int | TypeB Int
a : MyType
a = TypeA
>>>> FileB.elm
module B exposing (..)
import A exposing (MyType(TypeB))
a : MyType
a = TypeA
"""
|> hasFull ":type_a"
, test "Imported all union types from another file" <|
\() ->
"""
>>>> FileA.elm
module A exposing (..)
type MyType = TypeA Int | TypeB Int
a : MyType
a = TypeA
>>>> FileB.elm
module B exposing (..)
import A exposing (MyType(..))
a : MyType
a = (TypeA, TypeB)
"""
|> hasFull ":type_a"
, test "Doesn't import what imports imported" <|
\() ->
"""
>>>> A.elm
module A exposing (..)
type Invisible = Invi Int
>>>> B.elm
module B exposing (..)
import A exposing (..)
>>>> C.elm
module C exposing (..)
import A exposing (..)
>>>> B.elm
module D exposing (..)
import B exposing (..)
import C exposing (..)
a : Invisible
a = 1
"""
|> hasFull "invisible"
, test "Qualified imports work too" <|
\() ->
"""
>>>> a.elm
module A exposing (A)
type As a = Tag a
>>>> b.elm
module B exposing (..)
import A exposing (As)
a : As a
a = Tag
"""
|> hasFull "fn x1 -> {:tag, x1} end"
, test "Conflicted imports are excepts" <|
\() ->
"""
>>>> a.elm
module Something.A exposing (a)
a : a -> Int
a _ = 1
>>>> b.elm
module Something.B exposing (..)
import Something.A exposing (..)
a : a -> Int
a _ = 10
"""
|> hasFull "import Something.A, except: [{:'a', 0}, {:'a', 1}]"
]
letIns : Test
letIns =
describe "Let in constructs"
[ test "Allows to reffer variables in reversed order" <|
\() -> """
test = let
a = b
b = 2
in a
""" |> has "b = 2a = b"
, test "More advanced order" <|
\() -> """
test = let
a = b
b = 2
c = a
d = a + c
in d
""" |> has "b = 2a = bc = ad = (a + c)"
, test "Functions work too" <|
\() -> """
test = let
a = \\() -> b
b = 10
in d
""" |> has "b = 10a = fn {} -> b end"
, test "Union types aren't functions" <|
\() -> """
test = let
A a = A 1
b = 10
in d
""" |> has "{:a, a} = {:a, 1}b = 10"
, test "Sugared functions work too (with arguments)" <|
\() -> """
test = let
a x = x + b
b = 10
x = 1
in x
""" |> has "b = 10a = rec a, fn x -> (x + b) endx = 1xend"
, test "Multiple sugared functions work too" <|
\() -> """
test = let
a x = x + b
b a = a
x = 1
in x
""" |> has "b = rec b, fn a -> a enda = rec a, fn x -> (x + b) endx = 1xend"
, test "Doesn't mind shadowing" <|
\() -> """
test = let
a = \\b -> b
b = 10
in d
""" |> has "a = fn b -> b endb = 10"
, test "Doesn't mind destructuring" <|
\() -> """
test = let
newX = x + 1
(x, y) = (1, 2)
newY = y + 1
in newX
""" |> has "{x, y} = {1, 2}new_x = (x + 1)new_y = (y + 1)"
-- , test "Solves simple mutual recursion" <|
-- \() -> """
-- test =
-- let
-- fx a = fy + 1
-- fy b = fx + 1
-- in fx 10
-- """ |> has "{fx, fy} = let [fx: fn a -> "
-- , test "Solves mutual recursion" <|
-- \() -> """
-- test =
-- let
-- fx x = case x of
-- 0 -> 0
-- x -> fy x - 1
-- end
-- fy x = case x of
-- 0 -> 0
-- x -> fx x - 1
-- end
-- in fx 10
-- """ |> has "{fx, fy} = let [fx: fn x -> "
-- , test "Solves mutual relation" <|
-- \() -> """
-- test =
-- let
-- x = y + 1
-- y = x + 1
-- in x
-- """ |> has "{x, y} = let [x: (y + 1)"
]
caseOfs =
describe "Case ofs"
[ test "Simple case of" <|
\() ->
"""
test =
case x of
1 -> 1
2 -> 2
""" |> has "case x() do1 ->12 ->2end"
, test "Nested case of" <|
\() ->
"""
test =
case x of
1 -> 1
2 -> case y of
0 -> 0
3 -> 3
""" |> has "case x() do1 ->12 ->case y() do0 ->0end3 ->3end"
]
accessMacros : Test
accessMacros =
describe "Access macros compile properly"
[ test "Update"
(\() ->
"test = updateIn .a (\\a -> a + 1)" |> has "update_in_([:a]).(fn a -> (a + 1) end)"
)
, test "Update5"
(\() ->
"test = updateIn5 .a .b .c .d .e (\\a -> a + 1) v" |> has "update_in_([:a, :b, :c, :d, :e]).(fn a -> (a + 1) end).(v())"
)
, test "Get"
(\() ->
"test = getIn3 .something .something .darkSide True v" |> has "get_in_([:something, :something, :dark_side]).(:true).(v())"
)
, test "Put"
(\() ->
"test = putIn4 .a .b .c .d 10 v" |> has "put_in_([:a, :b, :c, :d]).(10).(v())"
)
]
all : Test
all =
describe "All"
[ tuples
, lists
, functions
, binOps
-- Disabled util specs are working correctly
, specs
, typeAliases
, types
, records
, typeConstructors
, doctests
, fileImports
, letIns
, caseOfs
, accessMacros
]
================================================
FILE: tests/UnitTests.elm
================================================
module UnitTests exposing (..)
import Test exposing (..)
import Expect
all : Test
all =
describe "Test"
[ test "Test truth" <|
\() ->
Expect.equal "a" "a"
]
================================================
FILE: tests/elm-package.json
================================================
{
"version": "1.0.0",
"summary": "Sample Elm Test",
"repository": "https://github.com/user/project.git",
"license": "BSD-3-Clause",
"source-directories": [
".",
"../src"
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "5.0.0 <= v < 6.0.0",
"elm-community/elm-test": "3.0.0 <= v < 4.0.0",
"rtfeldman/node-test-runner": "3.0.0 <= v < 4.0.0",
"Bogdanp/elm-ast": "8.0.0 <= v < 10.0.0",
"elm-community/list-extra": "6.0.0 <= v < 7.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}