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" }