Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c345dab9c1 | ||
|
|
2d46eda619 | ||
|
|
beb9b1b3ef | ||
|
|
cb512a7f17 | ||
|
|
e4fdeb6b79 | ||
|
|
6b22171018 | ||
|
|
f0112289e6 | ||
|
|
72e18a0a0c | ||
|
|
e2be3f652e | ||
|
|
490bd32322 | ||
|
|
1bfcba133a | ||
|
|
c3d024c16c | ||
|
|
1530a761d5 | ||
|
|
5197b6ece6 |
+11
-11
@@ -1,11 +1,11 @@
|
||||
/erl_crash.dump
|
||||
/src/*_lexer.erl
|
||||
/src/*_parser.erl
|
||||
/test/ebin
|
||||
/test/tmp
|
||||
/test/elixir/tmp
|
||||
/ebin/*
|
||||
/exbin/*
|
||||
/src/*rl.old
|
||||
/.eunit/*
|
||||
/deps/*
|
||||
.formatter.exs
|
||||
/_build/
|
||||
/cover/
|
||||
/deps/
|
||||
/doc/
|
||||
/.fetch
|
||||
erl_crash.dump
|
||||
*.ez
|
||||
n8n_openai_adapter-*.tar
|
||||
/tmp/
|
||||
/result
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
language: erlang
|
||||
script: "make compile docs test"
|
||||
notifications:
|
||||
irc: "irc.freenode.org#elixir-lang"
|
||||
recipients:
|
||||
- jose.valim@plataformatec.com.br
|
||||
otp_release:
|
||||
- R15B
|
||||
@@ -1,3 +0,0 @@
|
||||
# v0.5.0 (2012-05-24)
|
||||
|
||||
* First official release
|
||||
@@ -1,10 +0,0 @@
|
||||
LEGAL NOTICE INFORMATION
|
||||
------------------------
|
||||
|
||||
All the files in this distribution are covered under either the Elixir's
|
||||
license (see the file LICENSE) except some files mentioned below that contains
|
||||
sections that are under Erlang's License (EPL):
|
||||
|
||||
src/elixir_glob.erl
|
||||
src/eex_lexer.erl (generated by build scripts)
|
||||
src/elixir_parser.erl (generated by build scripts)
|
||||
@@ -1,13 +0,0 @@
|
||||
Copyright 2012 Plataformatec.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,48 +0,0 @@
|
||||
REBAR:=$(shell which rebar || echo ./rebar)
|
||||
ERLC=erlc -I include
|
||||
ERL=erl -I include -noshell -pa ebin
|
||||
|
||||
.PHONY: ebin docs
|
||||
.NOTPARALLEL: compile
|
||||
|
||||
compile: ebin ebin/__MAIN__ ebin/elixir.app
|
||||
|
||||
ebin/elixir.app:
|
||||
@ $(REBAR) compile
|
||||
|
||||
ebin:
|
||||
@ $(REBAR) compile
|
||||
|
||||
ebin/__MAIN__: lib/*.ex lib/*/*.ex
|
||||
@ rm -rf ebin/__MAIN__
|
||||
$(ERL) -s elixir_compiler core -s erlang halt
|
||||
@ rm -rf ebin/elixir.app
|
||||
|
||||
clean:
|
||||
@ $(REBAR) clean
|
||||
|
||||
docs: compile
|
||||
@ bin/elixirc "lib/**/*.ex" --ignore-module-conflict --docs -o for_docs
|
||||
@ rm -rf ebin/__MAIN__
|
||||
@ mv for_docs/__MAIN__ ebin/__MAIN__
|
||||
@ rm -rf for_docs
|
||||
|
||||
release_docs: docs
|
||||
bin/elixir ../exdoc/bin/exdoc
|
||||
rm -rf ../elixir-lang.github.com/docs
|
||||
mv output ../elixir-lang.github.com/docs
|
||||
|
||||
test: test_erlang test_elixir
|
||||
|
||||
test_erlang: compile
|
||||
@ echo "==> erlang (eunit)"
|
||||
@ mkdir -p test/ebin
|
||||
@ # Compile test files
|
||||
@ $(ERLC) -o test/ebin test/erlang/*.erl
|
||||
@ # Look and execute each file
|
||||
@ time $(ERL) -pa test/ebin -s test_helper test -s erlang halt
|
||||
@ echo
|
||||
|
||||
test_elixir: compile
|
||||
@ echo "==> elixir (exunit)"
|
||||
@ time bin/elixir -r "test/elixir/test_helper.exs" -pr "test/elixir/**/*_test.exs"
|
||||
@@ -1,57 +1,94 @@
|
||||

|
||||
=========
|
||||
[](http://travis-ci.org/elixir-lang/elixir)
|
||||
# n8n-openai-adapter
|
||||
|
||||
Elixir is a programming language built on top of the Erlang VM. As Erlang, it is a functional language built to support distributed, fault-tolerant, non-stop applications with hot code swapping.
|
||||
An OpenAI-compatible HTTP adapter that exposes self-hosted **n8n chat agents**
|
||||
behind a standard `/v1/chat/completions` API, so any OpenAI client (Cursor,
|
||||
LibreChat, the `openai` SDK, a custom app) can talk to your n8n agents as if
|
||||
they were OpenAI models.
|
||||
|
||||
Elixir is also dynamic typed but, differently from Erlang, it is also homoiconic, allowing meta-programming via macros. Elixir also supports polymorphism via protocols (similar to Clojure's), dynamic records and provides a reference mechanism.
|
||||
n8n itself does **not** ship an inbound OpenAI-compatible endpoint (its "AI
|
||||
Gateway" is an outbound proxy to n8n Cloud). This small Elixir service is the
|
||||
bridge: one `/v1/chat/completions` endpoint, routed to whichever n8n agent you
|
||||
name in the `model` field.
|
||||
|
||||
Finally, Elixir and Erlang share the same bytecode and data types. This means you can invoke Erlang code from Elixir (and vice-versa) without any conversion or performance hit. This allows a developer to mix the expressiveness of Elixir with the robustness and performance of Erlang.
|
||||
## How it works
|
||||
|
||||
# Usage
|
||||
```
|
||||
Your OpenAI client
|
||||
POST /v1/chat/completions {"model":"scholar-agent","thread_id":"abc","messages":[...]}
|
||||
|
|
||||
v
|
||||
n8n-openai-adapter (Plug + Bandit)
|
||||
- authorize (Bearer <ADAPTER_API_KEY>)
|
||||
- look up "scholar-agent" -> n8n chat webhook URL (AgentRegistry GenServer)
|
||||
- take the last user message
|
||||
- forward to the n8n webhook {sessionId: thread_id, action: sendMessage, chatInput}
|
||||
|
|
||||
v
|
||||
n8n agent (its MCP tools, memory, etc. run as usual)
|
||||
|
|
||||
v
|
||||
returns OpenAI-shaped {"choices":[{"message":{"role":"assistant","content":...}}]}
|
||||
```
|
||||
|
||||
You can run and learn about Elixir in the [Getting Started guide][1].
|
||||
Multiple agents = multiple `model` names, each mapped to a different n8n webhook
|
||||
in the `AGENTS` env var.
|
||||
|
||||
But if you just want to try it out, clone this repository to your machine, compile and test it:
|
||||
## Configuration (env vars)
|
||||
|
||||
$ git clone https://github.com/elixir-lang/elixir.git
|
||||
$ cd elixir
|
||||
$ make test
|
||||
| Var | Required | Purpose |
|
||||
|------------------|----------|---------------------------------------------------------------------|
|
||||
| `ADAPTER_API_KEY`| yes | Bearer key that OpenAI clients send. |
|
||||
| `ADMIN_API_KEY` | yes | Bearer key for the admin API / web admin page. |
|
||||
| `AGENTS_FILE` | no | Path to the JSON store (default `/var/lib/n8n-openai/agents.json`). |
|
||||
| `PORT` | no | HTTP port (default `8000`). |
|
||||
| `CHAT_WEBHOOK_BASIC` | no | `"user:password"` if your n8n Chat Trigger is Basic-auth protected. |
|
||||
|
||||
If tests pass, you are ready to move on to the [Getting Started guide][1] or to try Interactive Elixir by running: `bin/iex` in your terminal.
|
||||
Agents are **not** configured via env — they're managed at runtime through the
|
||||
web admin page / admin API and persisted to `AGENTS_FILE`. The store starts
|
||||
empty; add agents after boot.
|
||||
|
||||
However, if tests fail, it is likely you have an outdated Erlang version (Elixir requires Erlang R15B or later). You can check your Erlang version by calling `erl` in the command line. You will see some information as follow:
|
||||
## Admin API (manage agents at runtime)
|
||||
|
||||
Erlang R15B (erts-5.8.4) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:0] [hipe] [kernel-poll:false]
|
||||
Agents are persisted to `AGENTS_FILE` and can be added/removed without a
|
||||
redeploy, using the `ADMIN_API_KEY`:
|
||||
|
||||
If you have the correct version and tests still fail, feel free to [open an issue][2].
|
||||
```bash
|
||||
# list
|
||||
curl -H "Authorization: Bearer $ADMIN_API_KEY" https://openai.bueso.eu/admin/agents
|
||||
|
||||
# Contributing & Roadmap
|
||||
# add / update an agent
|
||||
curl -X POST -H "Authorization: Bearer $ADMIN_API_KEY" -H "Content-Type: application/json" \
|
||||
-d '{"model":"media-agent","webhook":"https://n8n.bueso.eu/webhook/<id>/chat"}' \
|
||||
https://openai.bueso.eu/admin/agents
|
||||
|
||||
If you want to contribute, the code is organized as follows:
|
||||
# remove
|
||||
curl -X DELETE -H "Authorization: Bearer $ADMIN_API_KEY" \
|
||||
https://openai.bueso.eu/admin/agents/media-agent
|
||||
```
|
||||
|
||||
* `include`, `src` - Both directories contain part of the source code written in Erlang. `yecc` is used as the parser;
|
||||
The store is authoritative and persists across restarts; no env config needed.
|
||||
|
||||
* `lib` - Contains Elixir's STDLIB, written in Elixir;
|
||||
## Building & running
|
||||
|
||||
* `test/elixir` - Tests for Elixir's STDLIB, written in Elixir. For this purpose, Elixir ships with a small unit test library called `ExUnit`;
|
||||
```bash
|
||||
mix deps.get
|
||||
mix compile
|
||||
ADAPTER_API_KEY=secret AGENTS='{"scholar-agent":"https://n8n.bueso.eu/webhook/<id>/chat"}' \
|
||||
PORT=8000 mix run --no-halt
|
||||
```
|
||||
|
||||
* `test/erlang` - Contains tests for Elixir, written in Erlang. Usually, just internal stuff is tested here. The preferred way to test is in Elixir itself.
|
||||
## Testing
|
||||
|
||||
We usually keep a list of features and bugs [in the issue tracker][2].
|
||||
```bash
|
||||
MIX_ENV=test mix test
|
||||
```
|
||||
|
||||
# Important links
|
||||
## Nix
|
||||
|
||||
* #elixir-lang on freenode IRC
|
||||
* [Getting Started Guide][1]
|
||||
* [Mailing list](http://groups.google.com/group/elixir-lang-core)
|
||||
* [Issue tracker][2]
|
||||
* [Textmate Bundle for Elixir](https://github.com/elixir-lang/elixir-tmbundle)
|
||||
* [Vim Elixir](https://github.com/elixir-lang/vim-elixir)
|
||||
The repo ships a `flake.nix` exporting `overlays.default` and a `packages.default`
|
||||
(the packaged BEAM release), so it can be consumed as a flake input from your
|
||||
NixOS config just like any other flake — e.g.:
|
||||
|
||||
[1]: http://elixir-lang.org/getting_started/1.html
|
||||
[2]: https://github.com/elixir-lang/elixir/issues
|
||||
|
||||
# License
|
||||
|
||||
Copyright (c) 2012 Plataformatec. See LICENSE file.
|
||||
```nix
|
||||
inputs.n8n-openai-adapter.url = "git+https://gitea.bueso.eu/<owner>/n8n-openai-adapter";
|
||||
```
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
## Release process
|
||||
|
||||
This document simply outlines the release process:
|
||||
|
||||
1) Run `make clean test` to ensure all tests pass from scratch
|
||||
|
||||
2) Remove .dev extension from current versions
|
||||
|
||||
3) Ensure CHANGELOG is updated and tag release version with timestamp in it
|
||||
|
||||
4) Commit changes above and tag new version on Git
|
||||
|
||||
5) After release, bump versions and add .dev back
|
||||
|
||||
## Places where version is mentioned
|
||||
|
||||
* src/elixir.app.src
|
||||
* lib/system.ex
|
||||
* CHANGELOG
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
#!/bin/sh
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: `basename $0` [options] [.exs file] [data]
|
||||
|
||||
-v Prints version and exit
|
||||
-e \"command\" Evaluates the given command (*)
|
||||
-r \"file\" Requires the given files/patterns (*)
|
||||
-pr \"file\" Requires the given files/patterns in parallel (*)
|
||||
-pa \"path\" Prepend the given path to Erlang code path (*)
|
||||
-pz \"path\" Append the given path to Erlang code path (*)
|
||||
--no-halt Do not halt the Erlang VM after execution
|
||||
|
||||
** Options marked with (*) can be given more than once;
|
||||
|
||||
** Options given after the .exs file or -- are passed down to the executed code;
|
||||
|
||||
** Options can be passed to the erlang runtime using ELIXIR_ERL_OPTS." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readlink_f () {
|
||||
cd `dirname $1` > /dev/null
|
||||
local filename=`basename $1`
|
||||
if [ -h "$filename" ]; then
|
||||
readlink_f `readlink $filename`
|
||||
else
|
||||
echo "`pwd -P`/$filename"
|
||||
fi
|
||||
}
|
||||
|
||||
SELF=`readlink_f $0`
|
||||
SCRIPT_PATH=`dirname $SELF`
|
||||
erl -pa $SCRIPT_PATH/../ebin -noshell $ELIXIR_ERL_OPTS -s elixir start_cli -extra "$@"
|
||||
@@ -1,24 +0,0 @@
|
||||
@echo off
|
||||
if "%*" == "" (
|
||||
goto documentation
|
||||
) else (
|
||||
goto run
|
||||
)
|
||||
:documentation
|
||||
echo Usage: %~nx0 [options] [.exs file] [data]
|
||||
echo.
|
||||
echo -v Prints version and exit
|
||||
echo -e command Evaluates the given command (*)
|
||||
echo -r command Requires the given file/pattern (*)
|
||||
echo -pr command Requires the given file/pattern in parallel (*)
|
||||
echo -pa path Prepend the given path to Erlang code path (*)
|
||||
echo -pz path Append the given path to Erlang code path (*)
|
||||
echo --no-halt Do not halt the Erlang VM after execution
|
||||
echo.
|
||||
echo ** Options marked with (*) can be given more than once;
|
||||
echo.
|
||||
echo ** Options given after the .exs file or -- are passed down to the executed code;
|
||||
echo.
|
||||
echo ** Options can be passed to the erlang runtime using ELIXIR_ERL_OPTS.
|
||||
:run
|
||||
erl -pa %~dp0\..\ebin -noshell %ELIXIR_ERL_OPTS% -s elixir start_cli -extra %*
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
#!/bin/sh
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: `basename $0` [switches] [.ex files]
|
||||
|
||||
-v Prints version and exit
|
||||
-o The directory to output compiled files
|
||||
-pa \"path\" Prepend the given path to Erlang code path (*)
|
||||
-pz \"path\" Append the given path to Erlang code path (*)
|
||||
--docs Attach documentation to compiled modules
|
||||
--debug-info Attach debug info to compiled modules
|
||||
--ignore-module-conflict
|
||||
|
||||
** Options marked with (*) can be given more than once;
|
||||
|
||||
** Options given after -- are passed down to the executed code;
|
||||
|
||||
** Options can be passed to the erlang runtime using ELIXIR_ERL_OPTS." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readlink_f () {
|
||||
cd `dirname $1` > /dev/null
|
||||
local filename=`basename $1`
|
||||
if [ -h "$filename" ]; then
|
||||
readlink_f `readlink $filename`
|
||||
else
|
||||
echo "`pwd -P`/$filename"
|
||||
fi
|
||||
}
|
||||
|
||||
SELF=`readlink_f $0`
|
||||
SCRIPT_PATH=`dirname $SELF`
|
||||
$SCRIPT_PATH/elixir +compile "$@"
|
||||
@@ -1,24 +0,0 @@
|
||||
@echo off
|
||||
if "%*" == "" (
|
||||
goto documentation
|
||||
) else (
|
||||
goto run
|
||||
)
|
||||
:documentation
|
||||
echo Usage: %~nx0 [switches] [.ex files]
|
||||
echo.
|
||||
echo -v Prints version and exit
|
||||
echo -o The directory to output compiled files
|
||||
echo -pa path Prepend the given path to Erlang code path (*)
|
||||
echo -pz path Append the given path to Erlang code path (*)
|
||||
echo --docs Attach documentation to compiled modules
|
||||
echo --debug-info Attach debug info to compiled modules
|
||||
echo --ignore-module-conflict
|
||||
echo.
|
||||
echo ** Options marked with (*) can be given more than once;
|
||||
echo.
|
||||
echo ** Options given after -- are passed down to the executed code;
|
||||
echo.
|
||||
echo ** Options can be passed to the erlang runtime using ELIXIR_ERL_OPTS." >&2
|
||||
:run
|
||||
call %~dp0\elixir.bat +compile %*
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/sh
|
||||
readlink_f () {
|
||||
cd `dirname $1` > /dev/null
|
||||
local filename=`basename $1`
|
||||
if [ -h "$filename" ]; then
|
||||
readlink_f `readlink $filename`
|
||||
else
|
||||
echo "`pwd -P`/$filename"
|
||||
fi
|
||||
}
|
||||
|
||||
SELF=`readlink_f $0`
|
||||
SCRIPT_PATH=`dirname $SELF`
|
||||
erl -pa $SCRIPT_PATH/../ebin -noinput $ELIXIR_ERL_OPTS -s elixir start_cli -extra --no-halt -e "Elixir.IEx.start" "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
@echo off
|
||||
erl -pa %~dp0\..\ebin -noshell %ELIXIR_ERL_OPTS% -s elixir start_cli -extra --no-halt -e "Elixir.IEx.start" %*
|
||||
@@ -0,0 +1,7 @@
|
||||
import Config
|
||||
|
||||
import_config "#{config_env()}.exs"
|
||||
|
||||
if config_env() == :test do
|
||||
config :logger, level: :warning
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
import Config
|
||||
|
||||
# Dev: no special config — all runtime settings come from env vars.
|
||||
@@ -0,0 +1,5 @@
|
||||
import Config
|
||||
|
||||
# Production: no hardcoded values here. All runtime config (PORT, AGENTS,
|
||||
# ADAPTER_API_KEY, CHAT_WEBHOOK_BASIC) comes from the systemd EnvironmentFile
|
||||
# in the NixOS service module.
|
||||
@@ -0,0 +1,7 @@
|
||||
import Config
|
||||
|
||||
# Test environment: the app starts with an empty agent store (no AGENTS env
|
||||
# seeding — agents are managed via the admin API). ADAPTER_API_KEY /
|
||||
# ADMIN_API_KEY are set in test/test_helper.exs. AGENTS_FILE must be set HERE
|
||||
# (config loads before the app boots) to a writable tmp path.
|
||||
System.put_env("AGENTS_FILE", Path.join(System.tmp_dir!(), "n8n-openai-test-agents.json"))
|
||||
Generated
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1788881743,
|
||||
"narHash": "sha256-2V9GZGvPfrNzxFozhI9dcqV+c3QdA8YZrvAAzqEB+dI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d6524aaca2ff07876657ae2b323f24be4874944b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
description = "OpenAI-compatible adapter exposing n8n chat agents behind /v1/chat/completions";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ self, nixpkgs, ... }:
|
||||
let
|
||||
supportedSystems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
];
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
in
|
||||
{
|
||||
packages = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
beamPackages = pkgs.beamPackages;
|
||||
in
|
||||
{
|
||||
default = beamPackages.mixRelease {
|
||||
pname = "n8n-openai-adapter";
|
||||
version = "0.1.0";
|
||||
src = self;
|
||||
mixFodDeps = beamPackages.fetchMixDeps {
|
||||
pname = "n8n-openai-adapter";
|
||||
version = "0.1.0";
|
||||
src = self;
|
||||
hash = "sha256-sdAhpZUeF33V9xjEa/z/aTmCllfetMjO/1XyfJfUNao=";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
overlays.default = final: prev: {
|
||||
n8n-openai-adapter = self.packages.${final.stdenv.system}.default;
|
||||
};
|
||||
|
||||
# Proper NixOS module: consume with
|
||||
# imports = [ inputs.n8n-openai-adapter.nixosModules.default ];
|
||||
# services.n8n-openai-adapter = { enable = true; domain = "..."; port = 8134; };
|
||||
nixosModules.default = import ./nixos-module.nix;
|
||||
};
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
-define(ELIXIR_WRAP_CALL(Line, Module, Function, Args),
|
||||
{ call, Line,
|
||||
{ remote, Line,
|
||||
{ record_field, 1, { atom, 1, '' }, { atom, Line, Module } },
|
||||
{ atom, Line, Function } },
|
||||
Args
|
||||
}).
|
||||
|
||||
-define(ELIXIR_ATOM_CONCAT(Atoms), list_to_atom(lists:concat(Atoms))).
|
||||
-define(ELIXIR_MACRO(Macro), list_to_atom(lists:concat(['MACRO-',Macro]))).
|
||||
|
||||
-record(elixir_scope, {
|
||||
assign=false, %% when true, new variables can be defined in that subtree
|
||||
guard=false, %% when true, we are inside a guard
|
||||
noref=false, %% when true, don't resolve references
|
||||
noname=false, %% when true, don't add new names (used by try)
|
||||
check_clauses=true, %% when true, check def clauses ordering
|
||||
super=false, %% when true, it means super was invoked
|
||||
name_args=false, %% when true, it means arguments should be named
|
||||
macro=[], %% a stack with macros nesting
|
||||
function=[], %% the current function
|
||||
recur=[], %% the current loop function to be recurred
|
||||
module=[], %% the current module
|
||||
vars=dict:new(), %% a dict of defined variables and their alias
|
||||
temp_vars=dict:new(), %% a dict of all variables defined in a particular assign
|
||||
clause_vars=dict:new(), %% a dict of all variables defined in a particular clause
|
||||
quote_vars=dict:new(), %% a dict of all quoted variables
|
||||
counter=0, %% a counter for the variables defined
|
||||
filename="nofile", %% the current scope filename
|
||||
local=[], %% the scope to evaluate local functions against
|
||||
refer=[], %% an orddict with references by new -> old names
|
||||
requires=elixir_dispatch:default_requires(), %% a set with modules required
|
||||
macros=elixir_dispatch:default_macros(), %% a list with macros imported by module
|
||||
functions=elixir_dispatch:default_functions(), %% a list with functions imported by module
|
||||
scheduled=[]}). %% scheduled modules to be loaded
|
||||
|
||||
-record(elixir_quote, {
|
||||
line=0,
|
||||
marker=quoted,
|
||||
unquote=true
|
||||
}).
|
||||
-178
@@ -1,178 +0,0 @@
|
||||
import Elixir.Builtin, except: [access: 2]
|
||||
|
||||
defprotocol Access do
|
||||
@moduledoc """
|
||||
The Access protocol is the underlying protocol invoked
|
||||
when the brackets syntax is used. For instance, `foo[bar]`
|
||||
is translated to `access foo, bar` which, by default,
|
||||
invokes `Access.access` protocol.
|
||||
|
||||
This protocol is implemented by default for most builtin
|
||||
types, like tuples, atoms, functions, etc.
|
||||
"""
|
||||
|
||||
@only [List, BitString, Record, Tuple, Atom, Function]
|
||||
|
||||
@doc """
|
||||
Receives the element being accessed and the access item.
|
||||
"""
|
||||
def access(element, qualifier)
|
||||
end
|
||||
|
||||
defimpl Access, for: Tuple do
|
||||
@doc """
|
||||
Access the tuple via an integer. Negative indexes
|
||||
performs an inverted lookup, for example, -1 can be
|
||||
used to retrieve the last item in the tuple. Returns
|
||||
nil if an out of bounds access occurs.
|
||||
|
||||
## Examples
|
||||
|
||||
tuple = { :a, :b, :c }
|
||||
tuple[-1] #=> :c
|
||||
|
||||
"""
|
||||
def access(tuple, integer) when is_integer(integer) and integer > 0 and integer <= tuple_size(tuple) do
|
||||
:erlang.element(integer, tuple)
|
||||
end
|
||||
|
||||
def access(tuple, integer) when is_integer(integer) and integer < 0 do
|
||||
size = tuple_size(tuple)
|
||||
position = integer + size + 1
|
||||
if position > size or position < 1,
|
||||
do: nil, else: :erlang.element(position, tuple)
|
||||
end
|
||||
|
||||
def access(_tuple, integer) when is_integer(integer) do
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Access, for: List do
|
||||
@doc """
|
||||
Access the list via a predicate.
|
||||
|
||||
If a regular expression, it returns a list with the
|
||||
matched contents.
|
||||
|
||||
If an atom, assumes the list is a keywords list and
|
||||
access the key in the keywords equals to the given
|
||||
atom.
|
||||
|
||||
Notice this protocol does not implement an integer
|
||||
lookup. This is intentional since doing an index
|
||||
based access on lists is usually undesired.
|
||||
|
||||
## Examples
|
||||
|
||||
list = 'sample'
|
||||
list[%r/a/] #=> 'a'
|
||||
|
||||
keywords = [a: 1, b: 2]
|
||||
keywords[:a] #=> 1
|
||||
|
||||
"""
|
||||
|
||||
## Atom
|
||||
|
||||
def access(list, atom) when is_atom(atom) do
|
||||
atom_access(list, atom)
|
||||
end
|
||||
|
||||
## Regex
|
||||
|
||||
def access(list, re) when is_regex(re) do
|
||||
case Erlang.re.run(list, Regex.re_pattern(re), [{ :capture, :first, :list }]) do
|
||||
:nomatch -> nil
|
||||
{ :match, [result] } -> result
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp atom_access([{k, _}|_], key) when key < k, do: nil
|
||||
defp atom_access([{k, _}|d], key) when key > k, do: atom_access(d, key)
|
||||
defp atom_access([{_k, value}|_], _key), do: value
|
||||
defp atom_access([], _), do: nil
|
||||
end
|
||||
|
||||
defimpl Access, for: BitString do
|
||||
@doc """
|
||||
Access the binary via a predicate.
|
||||
|
||||
If a regular expression, it returns a binary with the
|
||||
matched contents.
|
||||
|
||||
## Examples
|
||||
|
||||
binary = "abc"
|
||||
Binary.access binary, %r(a) #=> "a"
|
||||
|
||||
"""
|
||||
|
||||
## Regex
|
||||
|
||||
def access(binary, re) when is_binary(binary) and is_regex(re) do
|
||||
case Erlang.re.run(binary, Regex.re_pattern(re), [{ :capture, :first, :binary }]) do
|
||||
:nomatch -> nil
|
||||
{ :match, [result] } -> result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Access, for: Atom do
|
||||
@doc """
|
||||
Access the atom via keywords. Different from other
|
||||
implementations, the Access protocol for atoms is
|
||||
special cased during compilation time to provide
|
||||
faster read and write access for records.
|
||||
|
||||
This pattern can also be used in guards, which is
|
||||
a very useful way to extract information from a
|
||||
record.
|
||||
|
||||
## Examples
|
||||
|
||||
def increment(State[counter: counter] = state) do
|
||||
state.counter(counter + 1)
|
||||
end
|
||||
|
||||
In the example above, we use the Access protocol on atoms
|
||||
to match the counter field in the record State. Considering
|
||||
the record definition is as follows:
|
||||
|
||||
defrecord State, counter: 0, other: nil
|
||||
|
||||
The clause above is translated to:
|
||||
|
||||
def increment({ State, counter, _ } = state) do
|
||||
state.counter(counter + 1)
|
||||
end
|
||||
|
||||
Which is a very convenient way to pattern match and have
|
||||
faster read times. The same pattern can be used to create
|
||||
a new record:
|
||||
|
||||
def new_state(counter) do
|
||||
State[counter: counter]
|
||||
end
|
||||
|
||||
All fields not specified on creation defaults to their
|
||||
default value.
|
||||
"""
|
||||
def access(atom, keywords) when is_list(keywords) do
|
||||
atom.new(keywords)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Access, for: Function do
|
||||
@doc """
|
||||
The Access protocol for functions simply invokes
|
||||
the function passing the item as argument. This
|
||||
is useful because it allows a function to be
|
||||
passed as argument in places a dict would also fit.
|
||||
"""
|
||||
def access(function, item) do
|
||||
function.(item)
|
||||
end
|
||||
end
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
defmodule Binary do
|
||||
@moduledoc """
|
||||
Functions for working with binaries.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Simply invokes the Access protocol for the given binary.
|
||||
Check `Access.BitString` for more information.
|
||||
"""
|
||||
def access(binary, access) when is_binary(binary) do
|
||||
Access.BitString.access(binary, access)
|
||||
end
|
||||
|
||||
@doc %B"""
|
||||
Receives a char list and escapes all special chars (like \n)
|
||||
and interpolation markers. A last argument is given and wraps
|
||||
the whole char list given.
|
||||
|
||||
## Examples
|
||||
|
||||
Binary.escape "foo", ?'
|
||||
#=> "'foo'"
|
||||
|
||||
"""
|
||||
def escape(other, char) do
|
||||
<<char>> <> do_escape(other, char)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check if a binary is printable considering it is encoded
|
||||
as UTF-8. Returns true if so, false otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
Binary.printable?("abc") #=> true
|
||||
|
||||
"""
|
||||
|
||||
# Allow basic ascii chars
|
||||
def printable?(<<c, t|:binary>>) when c >= ?\s and c <= ?~ do
|
||||
printable?(t)
|
||||
end
|
||||
|
||||
# From 16#A0 to 16#BF
|
||||
def printable?(<<194, c, t|:binary>>) when c >= 160 and c <= 191 do
|
||||
printable?(t)
|
||||
end
|
||||
|
||||
# From 16#C0 to 16#7FF
|
||||
def printable?(<<m, o1, t|:binary>>) when m >= 195 and m <= 223 and o1 >= 128 and o1 < 192 do
|
||||
printable?(t)
|
||||
end
|
||||
|
||||
# From 16#800 to 16#CFFF
|
||||
def printable?(<<m, o1, o2, t|:binary>>) when m >= 224 and m <= 236 and
|
||||
o1 >= 128 and o1 < 192 and o2 >= 128 and o2 < 192 do
|
||||
printable?(t)
|
||||
end
|
||||
|
||||
# From 16#D000 to 16#D7FF
|
||||
def printable?(<<237, o1, o2, t|:binary>>) when
|
||||
o1 >= 128 and o1 < 160 and o2 >= 128 and o2 < 192 do
|
||||
printable?(t)
|
||||
end
|
||||
|
||||
# Reject 16#FFFF and 16#FFFE
|
||||
def printable?(<<239, 191, o>>) when o == 190 or o == 191 do
|
||||
false
|
||||
end
|
||||
|
||||
# From 16#E000 to 16#EFFF
|
||||
def printable?(<<m, o1, o2, t|:binary>>) when (m == 238 or m == 239) and
|
||||
o1 >= 128 and o1 < 192 and o2 >= 128 and o2 < 192 do
|
||||
printable?(t)
|
||||
end
|
||||
|
||||
# From 16#F000 to 16#FFFD
|
||||
def printable?(<<239, o1, o2, t|:binary>>) when
|
||||
o1 >= 128 and o1 < 192 and o2 >= 128 and o2 < 192 do
|
||||
printable?(t)
|
||||
end
|
||||
|
||||
# From 16#10000 to 16#3FFFF
|
||||
def printable?(<<240, o1, o2, o3, t|:binary>>) when
|
||||
o1 >= 144 and o1 < 192 and o2 >= 128 and o2 < 192 and o3 >= 128 and o3 < 192 do
|
||||
printable?(t)
|
||||
end
|
||||
|
||||
# Reject 16#110000 onwards
|
||||
def printable?(<<244, o1, _, _, _|:binary>>) when o1 >= 144 do
|
||||
false
|
||||
end
|
||||
|
||||
# From 16#4000 to 16#10FFFF
|
||||
def printable?(<<m, o1, o2, o3, t|:binary>>) when m >= 241 and m <= 244 and
|
||||
o1 >= 128 and o1 < 192 and o2 >= 128 and o2 < 192 and o3 >= 128 and o3 < 192 do
|
||||
printable?(t)
|
||||
end
|
||||
|
||||
def printable?(<<?\n, t|:binary>>), do: printable?(t)
|
||||
def printable?(<<?\r, t|:binary>>), do: printable?(t)
|
||||
def printable?(<<?\t, t|:binary>>), do: printable?(t)
|
||||
def printable?(<<?\v, t|:binary>>), do: printable?(t)
|
||||
def printable?(<<?\b, t|:binary>>), do: printable?(t)
|
||||
def printable?(<<?\f, t|:binary>>), do: printable?(t)
|
||||
def printable?(<<?\e, t|:binary>>), do: printable?(t)
|
||||
|
||||
def printable?(<<>>), do: true
|
||||
def printable?(_), do: false
|
||||
|
||||
@doc %B"""
|
||||
Unescape the given chars. The unescaping is driven by the same
|
||||
rules as single- and double-quoted strings. Check `unescape/2`
|
||||
for information on how to customize the escaping map.
|
||||
|
||||
In this setup, Elixir will escape the following: `\b`, `\d`,
|
||||
`\e`, `\f`, `\n`, `\r`, `\s`, `\t` and `\v`. Octals are also
|
||||
escaped according to the latin1 set they represent.
|
||||
|
||||
## Examples
|
||||
|
||||
Binary.unescape "example\\n"
|
||||
#=> "example\n"
|
||||
|
||||
In the example above, we pass a string with `\n` escaped
|
||||
and we return a version with it unescaped.
|
||||
"""
|
||||
def unescape(chars) do
|
||||
Erlang.elixir_interpolation.unescape_chars(chars)
|
||||
end
|
||||
|
||||
@doc %B"""
|
||||
Unescape the given chars according to the map given.
|
||||
Check `unescape/1` if you want to use the same map as Elixir
|
||||
single- and double-quoted strings.
|
||||
|
||||
## Map
|
||||
|
||||
The map must be a function. The function receives an integer
|
||||
representing the number of the characters it wants to unescape.
|
||||
Here is the default mapping function implemented by Elixir:
|
||||
|
||||
def unescape_map(?b), do: ?\b
|
||||
def unescape_map(?d), do: ?\d
|
||||
def unescape_map(?e), do: ?\e
|
||||
def unescape_map(?f), do: ?\f
|
||||
def unescape_map(?n), do: ?\n
|
||||
def unescape_map(?r), do: ?\r
|
||||
def unescape_map(?s), do: ?\s
|
||||
def unescape_map(?t), do: ?\t
|
||||
def unescape_map(?v), do: ?\v
|
||||
def unescape_map(e), do: e
|
||||
|
||||
If the `unescape_map` function returns false. The char is
|
||||
not escaped and `\` is kept in the char list.
|
||||
|
||||
## Examples
|
||||
|
||||
Using the unescape_map defined above is easy:
|
||||
|
||||
Binary.unescape "example\\n", unescape_map(&1)
|
||||
|
||||
"""
|
||||
def unescape(chars, map) do
|
||||
Erlang.elixir_interpolation.unescape_chars(chars, map)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unescape the given tokens according to the default map.
|
||||
Check `unescape/1` and `unescape/2` for more information
|
||||
about unescaping. Only tokens that are char lists are
|
||||
unescaped, all others are ignored. This method is useful
|
||||
when implementing your own sigils. Check the implementation
|
||||
of `Elixir::Builtin.__b__` for examples.
|
||||
"""
|
||||
def unescape_tokens(tokens) do
|
||||
Erlang.elixir_interpolation.unescape_tokens(tokens)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unescape the given tokens according to the given map.
|
||||
Check `unescape_tokens/1` and `unescaped/2` for more information.
|
||||
"""
|
||||
def unescape_tokens(tokens, map) do
|
||||
Erlang.elixir_interpolation.unescape_tokens(tokens, map)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp do_escape(<<char, t|:binary>>, char) do
|
||||
<<?\\, char, do_escape(t, char)|:binary>>
|
||||
end
|
||||
|
||||
defp do_escape(<<h, t|:binary>>, char) when
|
||||
h == ?# or h == ?\b or
|
||||
h == ?\d or h == ?\e or
|
||||
h == ?\f or h == ?\n or
|
||||
h == ?\r or h == ?\\ or
|
||||
h == ?\t or h == ?\v do
|
||||
<<?\\, escape_map(h), do_escape(t, char)|:binary>>
|
||||
end
|
||||
|
||||
defp do_escape(<<h, t|:binary>>, char) do
|
||||
<<h, do_escape(t,char)|:binary>>
|
||||
end
|
||||
|
||||
defp do_escape(<<>>, char) do
|
||||
<<char>>
|
||||
end
|
||||
|
||||
defp escape_map(?#), do: ?#
|
||||
defp escape_map(?\b), do: ?b
|
||||
defp escape_map(?\d), do: ?d
|
||||
defp escape_map(?\e), do: ?e
|
||||
defp escape_map(?\f), do: ?f
|
||||
defp escape_map(?\n), do: ?n
|
||||
defp escape_map(?\r), do: ?r
|
||||
defp escape_map(?\\), do: ?\\
|
||||
defp escape_map(?\t), do: ?t
|
||||
defp escape_map(?\v), do: ?v
|
||||
end
|
||||
@@ -1,74 +0,0 @@
|
||||
import Elixir.Builtin, except: [to_binary: 1]
|
||||
|
||||
defprotocol Binary.Chars do
|
||||
@moduledoc %B"""
|
||||
The Binary.Chars protocol is responsible for
|
||||
converting a structure to a Binary (only if applicable).
|
||||
The only function required to be implemented is
|
||||
`to_binary` which does the conversion.
|
||||
|
||||
The `to_binary` function automatically imported
|
||||
by Elixir.Builtin invokes this protocol. String
|
||||
interpolation also invokes to_binary in its
|
||||
arguments. For example, `"foo#{bar}"` is the same
|
||||
as `"foo" <> to_binary(bar)`.
|
||||
"""
|
||||
|
||||
@only [BitString, List, Number, Atom, Record]
|
||||
|
||||
def to_binary(thing)
|
||||
end
|
||||
|
||||
defimpl Binary.Chars, for: Atom do
|
||||
@doc """
|
||||
Convert the atom literally to a binary, except
|
||||
`nil` which is converted to an empty string.
|
||||
"""
|
||||
def to_binary(nil) do
|
||||
""
|
||||
end
|
||||
|
||||
def to_binary(atom) do
|
||||
atom_to_binary(atom, :utf8)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Binary.Chars, for: BitString do
|
||||
@doc """
|
||||
Simply returns the binary itself.
|
||||
"""
|
||||
def to_binary(thing) when is_binary(thing) do
|
||||
thing
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Binary.Chars, for: List do
|
||||
@doc """
|
||||
Consider the list is an iolist and converts it
|
||||
to a binary. This allows a list of binaries, or
|
||||
a charlist, or a mix of both, to be converted
|
||||
successfully.
|
||||
|
||||
## Examples
|
||||
|
||||
to_binary 'foo' #=> "foo"
|
||||
to_binary ["foo", 'bar'] #=> "foobar"
|
||||
|
||||
"""
|
||||
def to_binary(thing) do
|
||||
iolist_to_binary(thing)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Binary.Chars, for: Number do
|
||||
@doc """
|
||||
Simply converts the number (integer or a float) to a binary.
|
||||
"""
|
||||
def to_binary(thing) when is_integer(thing) do
|
||||
list_to_binary integer_to_list(thing)
|
||||
end
|
||||
|
||||
def to_binary(thing) do
|
||||
list_to_binary float_to_list(thing)
|
||||
end
|
||||
end
|
||||
@@ -1,257 +0,0 @@
|
||||
import Elixir.Builtin, except: [inspect: 1]
|
||||
|
||||
defprotocol Binary.Inspect do
|
||||
@moduledoc """
|
||||
The `Binary.Inspect` protocol is responsible for
|
||||
converting any structure to a Binary for textual
|
||||
representation. All basic data structures (tuple,
|
||||
list, function, pid, etc) implement the inspect
|
||||
protocol. Other structures are adviced to implement
|
||||
the protocol in order to provide pretty printing.
|
||||
"""
|
||||
|
||||
@only [BitString, List, Record, Tuple, Atom, Number, Any]
|
||||
|
||||
def inspect(thing)
|
||||
end
|
||||
|
||||
defimpl Binary.Inspect, for: Atom do
|
||||
@doc """
|
||||
Represents the atom as an Elixir term.
|
||||
The atoms false, true and nil are simply
|
||||
quoted. Modules are properly represented
|
||||
as modules using the dot notation.
|
||||
|
||||
## Examples
|
||||
|
||||
inspect(:foo) #=> ":foo"
|
||||
inspect(nil) #=> "nil"
|
||||
inspect(Foo.Bar) #=> "Foo.Bar"
|
||||
|
||||
"""
|
||||
def inspect(false), do: "false"
|
||||
def inspect(true), do: "true"
|
||||
def inspect(nil), do: "nil"
|
||||
def inspect(:""), do: ":\"\""
|
||||
|
||||
def inspect(atom) do
|
||||
binary = atom_to_binary(atom)
|
||||
|
||||
cond do
|
||||
valid_identifier?(binary) == <<>> ->
|
||||
":" <> binary
|
||||
valid_ref_identifier?(binary) == <<>> ->
|
||||
"__MAIN__." <> rest = binary
|
||||
rest
|
||||
true ->
|
||||
":" <> Binary.escape(binary, ?")
|
||||
end
|
||||
end
|
||||
|
||||
# Detect if atom is a module reference (__MAIN__.Foo.Bar.Baz)
|
||||
|
||||
defp valid_ref_identifier?("__MAIN__" <> rest) do
|
||||
valid_ref_piece?(rest)
|
||||
end
|
||||
|
||||
defp valid_ref_identifier?(rest) do
|
||||
rest
|
||||
end
|
||||
|
||||
defp valid_ref_piece?(<<?., h, t|:binary>>) when h >= ?A and h <= ?Z do
|
||||
valid_ref_piece? valid_identifier?(t)
|
||||
end
|
||||
|
||||
defp valid_ref_piece?(other), do: other
|
||||
|
||||
# Detect if atom is :letter_or_underscore
|
||||
|
||||
defp valid_identifier?(<<h, t|:binary>>) \
|
||||
when h >= ?a and h <= ?z \
|
||||
when h >= ?A and h <= ?Z \
|
||||
when h == ?_ do
|
||||
valid_identifier? t
|
||||
end
|
||||
|
||||
defp valid_identifier?(other), do: other
|
||||
end
|
||||
|
||||
defimpl Binary.Inspect, for: BitString do
|
||||
@doc %B"""
|
||||
Represents the string as itself escaping
|
||||
all necessary characters.
|
||||
|
||||
## Examples
|
||||
|
||||
inspect("bar") #=> "bar"
|
||||
inspect("f\"oo") #=> "f\"oo"
|
||||
|
||||
"""
|
||||
def inspect(thing) when is_binary(thing) do
|
||||
if Binary.printable?(thing) do
|
||||
Binary.escape(thing, ?")
|
||||
else
|
||||
as_bitstring(thing)
|
||||
end
|
||||
end
|
||||
|
||||
def inspect(thing) do
|
||||
as_bitstring(thing)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp as_bitstring(thing) do
|
||||
erlang = Erlang.io_lib.format('~p', [thing])
|
||||
list_to_binary List.reverse(replace(erlang, []))
|
||||
end
|
||||
|
||||
defp replace([?:|t], acc), do: replace(t, [?||acc])
|
||||
defp replace([h|t], acc) when is_list(h), do: replace(t, replace(h, acc))
|
||||
defp replace([h|t], acc), do: replace(t, [h|acc])
|
||||
defp replace([], acc), do: acc
|
||||
end
|
||||
|
||||
defimpl Binary.Inspect, for: List do
|
||||
|
||||
@doc %B"""
|
||||
Represents a list checking if it can be printed or not.
|
||||
If so, a single-quoted representation is returned,
|
||||
otherwise the brackets syntax is used.
|
||||
|
||||
## Examples
|
||||
|
||||
inspect('bar') #=> 'bar'
|
||||
inspect([0|'bar']) #=> "[0,98,97,114]"
|
||||
inspect([:foo,:bar]) #=> "[:foo, :bar]"
|
||||
|
||||
"""
|
||||
|
||||
def inspect([]), do: "[]"
|
||||
|
||||
def inspect(thing) do
|
||||
if Erlang.io_lib.printable_list(thing) do
|
||||
Binary.escape(list_to_binary(thing), ?')
|
||||
else
|
||||
container_join(thing, "[", "]")
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
def container_join([h], acc, last) do
|
||||
acc <> Binary.Inspect.inspect(h) <> last
|
||||
end
|
||||
|
||||
def container_join([h|t], acc, last) when is_list(t) do
|
||||
acc = acc <> Binary.Inspect.inspect(h) <> ","
|
||||
container_join(t, acc, last)
|
||||
end
|
||||
|
||||
def container_join([h|t], acc, last) do
|
||||
acc <> Binary.Inspect.inspect(h) <> "|" <> Binary.Inspect.inspect(t) <> last
|
||||
end
|
||||
|
||||
def container_join([], acc, last) do
|
||||
acc <> last
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Binary.Inspect, for: Tuple do
|
||||
@doc """
|
||||
Inspect tuples. If the tuple represents a record,
|
||||
it shows it nicely formatted using the access syntax.
|
||||
|
||||
## Examples
|
||||
|
||||
inspect({1,2,3}) #=> "{1,2,3}"
|
||||
inspect(ArgumentError.new) #=> ArgumentError[message: "argument error"]
|
||||
|
||||
"""
|
||||
def inspect({}), do: "{}"
|
||||
|
||||
def inspect(exception) when is_exception(exception) do
|
||||
[name,_|tail] = tuple_to_list(exception)
|
||||
[_|fields] = lc { field, _ } in name.__record__(:fields), do: field
|
||||
Binary.Inspect.Atom.inspect(name) <> records_join(fields, tail, "[", "]")
|
||||
end
|
||||
|
||||
def inspect(thing) do
|
||||
list = tuple_to_list(thing)
|
||||
[name|tail] = list
|
||||
|
||||
if is_record?(name) do
|
||||
fields = lc { field, _ } in name.__record__(:fields), do: field
|
||||
Binary.Inspect.Atom.inspect(name) <> records_join(fields, tail, "[", "]")
|
||||
else
|
||||
Binary.Inspect.List.container_join(list, "{", "}")
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp is_record?(name) do
|
||||
is_atom(name) and match?("__MAIN__." <> _, atom_to_binary(name, :utf8)) and
|
||||
:erlang.function_exported(name, :__record__, 1)
|
||||
end
|
||||
|
||||
defp records_join([f], [v], acc, last) do
|
||||
acc <> atom_to_binary(f, :utf8) <> ": " <> Binary.Inspect.inspect(v) <> last
|
||||
end
|
||||
|
||||
defp records_join([fh|ft], [vh|vt], acc, last) do
|
||||
acc = acc <> atom_to_binary(fh, :utf8) <> ": " <> Binary.Inspect.inspect(vh) <> ", "
|
||||
records_join(ft, vt, acc, last)
|
||||
end
|
||||
|
||||
defp records_join([], [], acc, last) do
|
||||
acc <> last
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Binary.Inspect, for: Number do
|
||||
@doc """
|
||||
Represents the number as a binary.
|
||||
|
||||
## Examples
|
||||
|
||||
inspect(1) #=> "1"
|
||||
|
||||
"""
|
||||
def inspect(thing) when is_integer(thing) do
|
||||
list_to_binary integer_to_list(thing)
|
||||
end
|
||||
|
||||
def inspect(thing) do
|
||||
list_to_binary float_to_list(thing)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Binary.Inspect, for: Regex do
|
||||
@doc %B"""
|
||||
Represents the Regex using the `%r""` syntax.
|
||||
|
||||
## Examples
|
||||
|
||||
inspect(%r/foo/m) #=> "%r\"foo\"m"
|
||||
|
||||
"""
|
||||
def inspect(thing) do
|
||||
"%r" <> Binary.Inspect.inspect(Regex.source(thing)) <> Regex.opts(thing)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Binary.Inspect, for: Any do
|
||||
@doc """
|
||||
For all other terms not implemented, we use the default
|
||||
Erlang representation.
|
||||
|
||||
## Examples
|
||||
|
||||
inspect Process.self #=> "<0.35.0>"
|
||||
|
||||
"""
|
||||
def inspect(thing) do
|
||||
iolist_to_binary Erlang.io_lib.format('~p', [thing])
|
||||
end
|
||||
end
|
||||
@@ -1,48 +0,0 @@
|
||||
defmodule Bitwise do
|
||||
@moduledoc """
|
||||
This module provide macros for bitwise operators
|
||||
provided by Erlang. These macros can be used in guards.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Bitwise not.
|
||||
"""
|
||||
defmacro bnot(expr) do
|
||||
quote do: __op__ :bnot, unquote(expr)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise and.
|
||||
"""
|
||||
defmacro band(left, right) do
|
||||
quote do: __op__ :band, unquote(left), unquote(right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise or.
|
||||
"""
|
||||
defmacro bor(left, right) do
|
||||
quote do: __op__ :bor, unquote(left), unquote(right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise xor.
|
||||
"""
|
||||
defmacro bxor(left, right) do
|
||||
quote do: __op__ :bxor, unquote(left), unquote(right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic bitshift left.
|
||||
"""
|
||||
defmacro bsl(left, right) do
|
||||
quote do: __op__ :bsl, unquote(left), unquote(right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic bitshift right.
|
||||
"""
|
||||
defmacro bsr(left, right) do
|
||||
quote do: __op__ :bsr, unquote(left), unquote(right)
|
||||
end
|
||||
end
|
||||
-185
@@ -1,185 +0,0 @@
|
||||
defmodule Code do
|
||||
@moduledoc """
|
||||
The Code module is responsible to manage code compilation,
|
||||
code evaluation and code loading.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns all the loaded files.
|
||||
"""
|
||||
def loaded_files do
|
||||
server_call :loaded
|
||||
end
|
||||
|
||||
@doc """
|
||||
Appends a path to Erlang VM code path.
|
||||
"""
|
||||
def append_path(path) do
|
||||
Erlang.code.add_pathz(to_char_list(path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prepends a path to Erlang VM code path.
|
||||
"""
|
||||
def prepend_path(path) do
|
||||
Erlang.code.add_patha(to_char_list(path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evalutes the contents given by string. The second argument is the binding
|
||||
(which should be a Keyword) followed by a keyword list of options. The
|
||||
options can be:
|
||||
|
||||
* `:file` - the file to be considered in the evaluation
|
||||
* `:line` - the line the script starts
|
||||
* `:delegate_locals_to` - delegate local calls to the given module,
|
||||
otherwise functions are evaluated inside Erlang's default scope.
|
||||
|
||||
## Examples
|
||||
|
||||
Code.eval "a + b", [a: 1, b: 2], file: __FILE__, line: __LINE__
|
||||
#=> { 3, [ {:a, 1}, {:b, 2} ] }
|
||||
|
||||
"""
|
||||
def eval(string, binding // [], opts // []) do
|
||||
{ value, binding, _scope } =
|
||||
Erlang.elixir.eval :unicode.characters_to_list(string), binding, opts
|
||||
{ value, binding }
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evalutes the quoted contents.
|
||||
|
||||
## Options
|
||||
|
||||
This function accepts a list of options. The supported
|
||||
options are:
|
||||
|
||||
* `:file` - The filename to be used in stacktraces
|
||||
or by the __FILE__ macro in case there is an error.
|
||||
|
||||
* `:line` - The line to be used when expanding __LINE__
|
||||
macros and expressions inside the quote.
|
||||
|
||||
## Examples
|
||||
|
||||
contents = quote hygiene: false, do: a + b
|
||||
Code.eval_quoted contents, [a: 1, b: 2], file: __FILE__, line: __LINE__
|
||||
#=> { 3, [ {:a, 1}, {:b, 2} ] }
|
||||
|
||||
"""
|
||||
def eval_quoted(quoted, binding // [], opts // []) do
|
||||
{ value, binding, _scope } =
|
||||
Erlang.elixir.eval_quoted [quoted], binding, opts
|
||||
{ value, binding }
|
||||
end
|
||||
|
||||
@doc """
|
||||
Loads the given `file`. Accepts `relative_to` as an argument to tell
|
||||
where the file is located. If the file was already required/loaded,
|
||||
loads it again. It returns the full path of the loaded file.
|
||||
|
||||
When loading a file, you may skip passing .exs as extension as Elixir
|
||||
automatically adds it for you.
|
||||
"""
|
||||
def load_file(file, relative_to // nil) do
|
||||
file = find_file(file, relative_to)
|
||||
server_call { :loaded, file }
|
||||
Erlang.elixir_compiler.file to_char_list(file)
|
||||
file
|
||||
end
|
||||
|
||||
@doc """
|
||||
Requires the given `file`. Accepts `relative_to` as an argument to tell
|
||||
where the file is located. If the file was already required/loaded,
|
||||
returns nil, otherwise the full path of the loaded file.
|
||||
|
||||
When requiring a file, you may skip passing .exs as extension as
|
||||
Elixir automatically adds it for you.
|
||||
"""
|
||||
def require_file(file, relative_to // nil) do
|
||||
file = find_file(file, relative_to)
|
||||
|
||||
case server_call({ :loaded, file }) do
|
||||
:ok ->
|
||||
Erlang.elixir_compiler.file to_char_list(file)
|
||||
file
|
||||
:duplicated -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Loads the compilation options from the code server.
|
||||
Check compiler_options/1 for more information.
|
||||
"""
|
||||
def compiler_options do
|
||||
server_call :compiler_options
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets compilation options. Those options are global
|
||||
since they are stored by Elixir's Code Server.
|
||||
|
||||
Available options are:
|
||||
|
||||
* docs - when true, retain documentation in the compiled module;
|
||||
* debug_info - when true, retain debug information in the compiled module.
|
||||
Notice debug information can be used to reconstruct the source code;
|
||||
* ignore_module_conflict - when true, override modules that were already defined;
|
||||
|
||||
"""
|
||||
def compiler_options(opts) do
|
||||
server_call { :compiler_options, opts }
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the given string and returns a list of tuples where
|
||||
the first element is the module name and the second one is its
|
||||
binary.
|
||||
|
||||
For compiling many files at once, check `Elixir.ParallelCompiler`.
|
||||
"""
|
||||
def compile_string(string, file // 'nofile') do
|
||||
Erlang.elixir_compiler.string :unicode.characters_to_list(string), to_char_list(file)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensure if the given module is loaded. If the module is already loaded,
|
||||
it works as no-op. If the module was not loaded yet, it tries to load it.
|
||||
|
||||
If it succeeds loading the module anyhow, it returns `{ :module, module }`.
|
||||
If not, returns `{ :error, reason }` with the error reason.
|
||||
"""
|
||||
def ensure_loaded(module) when is_atom(module) do
|
||||
Erlang.code.ensure_loaded(module)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
# Finds the file given the relative_to path.
|
||||
# If the file is found, returns its path in binary, fails otherwise.
|
||||
defp find_file(file, relative_to) do
|
||||
file = to_binary(file)
|
||||
|
||||
file = if relative_to do
|
||||
File.expand_path(file, relative_to)
|
||||
else
|
||||
File.expand_path(file)
|
||||
end
|
||||
|
||||
if File.regular?(file) do
|
||||
file
|
||||
else
|
||||
prefix = "#{file}.exs"
|
||||
if File.regular?(prefix) do
|
||||
prefix
|
||||
else
|
||||
raise ArgumentError, message: "could not load #{file}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp server_call(args) do
|
||||
Erlang.gen_server.call(:elixir_code_server, args)
|
||||
end
|
||||
end
|
||||
-152
@@ -1,152 +0,0 @@
|
||||
defprotocol Dict do
|
||||
@only [Record]
|
||||
|
||||
@moduledoc """
|
||||
This module provides the Dict protocol
|
||||
with the goal of being a common API
|
||||
to work with dictionaries.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns a list containing all dict's keys.
|
||||
The keys are not guaranteed to be sorted, unless
|
||||
the underlying dict implementation defines so.
|
||||
|
||||
## Examples
|
||||
|
||||
Dict.keys [a: 1, b: 2] #=> [:a,:b]
|
||||
|
||||
"""
|
||||
def keys(dict)
|
||||
|
||||
@doc """
|
||||
Returns a list containing all dict's values.
|
||||
|
||||
## Examples
|
||||
|
||||
Dict.values [a: 1, b: 2] #=> [1,2]
|
||||
|
||||
"""
|
||||
def values(dict)
|
||||
|
||||
@doc """
|
||||
Returns the number of elements in `dict`.
|
||||
|
||||
## Examples
|
||||
|
||||
Dict.size [a: 1, b: 2] #=> 2
|
||||
|
||||
"""
|
||||
def size(dict)
|
||||
|
||||
@doc """
|
||||
Returns whether the given key exists in the given dict.
|
||||
|
||||
## Examples
|
||||
|
||||
Dict.has_key?([a:, 1], :a) #=> true
|
||||
Dict.has_key?([a:, 1], :b) #=> false
|
||||
|
||||
"""
|
||||
def has_key?(dict, key)
|
||||
|
||||
@doc """
|
||||
Returns the value associated with `key` in `dict`. If `dict` does not
|
||||
contain `key`, returns `default` (or nil if not provided).
|
||||
|
||||
## Examples
|
||||
|
||||
Dict.get [a: 1], :a #=> 1
|
||||
Dict.get [a: 1], :b #=> nil
|
||||
Dict.get [a: 1], :b, 3 #=> 3
|
||||
|
||||
"""
|
||||
def get(dict, key)
|
||||
def get(dict, key, default)
|
||||
|
||||
@doc """
|
||||
Stores the given `value` under `key` in `dict`.
|
||||
If `dict` already has `key`, the stored value is replaced by the new one.
|
||||
|
||||
## Examples
|
||||
|
||||
Dict.put [a: 1, b: 2], :a, 3
|
||||
#=> [a: 3, b: 2]
|
||||
|
||||
"""
|
||||
def put(dict, key, val)
|
||||
|
||||
@doc """
|
||||
Removes the entry stored under the given key from `dict`.
|
||||
If `dict` does not contain `key`, returns the dictionary unchanged.
|
||||
|
||||
## Examples
|
||||
|
||||
Dict.delete [a: 1, b: 2], :a #=> [b: 2]
|
||||
Dict.delete [b: 2], :a #=> [b: 2]
|
||||
|
||||
"""
|
||||
def delete(dict, key)
|
||||
|
||||
@doc """
|
||||
Merges two dicts into one. If the dicts have duplicated entries, the one
|
||||
given as second argument wins.
|
||||
|
||||
## Examples
|
||||
|
||||
Dict.merge [a: 1, b: 2], [a: 3, d: 4]
|
||||
#=> [a:3, b:2, d: 4]
|
||||
|
||||
"""
|
||||
def merge(dict1, dict2)
|
||||
|
||||
@doc """
|
||||
Merges two dicts into one. If the dicts have duplicated entries, the given
|
||||
function is invoked to solve conflicts.
|
||||
|
||||
## Examples
|
||||
|
||||
Dict.merge [a: 1, b: 2], [a: 3, d: 4], fn _k, v1, v2 ->
|
||||
v1 + v2
|
||||
end
|
||||
#=> [a: 4, b: 2, d: 4]
|
||||
|
||||
"""
|
||||
def merge(dict1, dict2, fun)
|
||||
|
||||
@doc """
|
||||
Update a value in `dict` by calling `fun` on the value to get a new
|
||||
value. An exception is generated if `key` is not present in the dict.
|
||||
|
||||
## Examples
|
||||
|
||||
Dict.update [a: 1, b: 2], :a, fn val -> -val end
|
||||
#=> [a: -1, b: 2]
|
||||
|
||||
"""
|
||||
def update(dict, key, fun)
|
||||
|
||||
@doc """
|
||||
Update a value in `dict` by calling `fun` on the value to get a new value. If
|
||||
`key` is not present in `dict` then `initial` will be stored as the first
|
||||
value.
|
||||
|
||||
## Examples
|
||||
|
||||
Dict.update [a: 1, b: 2], :c, 3, fn val -> -val end
|
||||
#=> [a: 1, b: 2, c: 3]
|
||||
|
||||
"""
|
||||
def update(dict, key, initial, fun)
|
||||
|
||||
@doc """
|
||||
Returns an empty dict of the same type as `dict`.
|
||||
"""
|
||||
def empty(dict)
|
||||
|
||||
@doc """
|
||||
Returns a list of key-value pairs stored in `dict`.
|
||||
No particular order is enforced.
|
||||
"""
|
||||
def to_list(dict)
|
||||
end
|
||||
@@ -1,45 +0,0 @@
|
||||
defmodule Dict.Common do
|
||||
@moduledoc false
|
||||
|
||||
defmacro __using__(_module, ref) do
|
||||
quote do
|
||||
@doc """
|
||||
Creates a new empty dict.
|
||||
"""
|
||||
def new do
|
||||
unquote(ref).empty(nil)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new dict from a list of pairs.
|
||||
|
||||
## Examples
|
||||
|
||||
#{unquote(inspect(__MODULE__))}.new [{:b,1},{:a,2}]
|
||||
#=> [a: 1, b: 2]
|
||||
|
||||
"""
|
||||
def new(pairs) do
|
||||
Enum.reduce pairs, new, fn { k, v }, dict ->
|
||||
unquote(ref).put(dict, k, v)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new dict from a list of elements with the
|
||||
help of the transformation function.
|
||||
|
||||
## Examples
|
||||
|
||||
#{unquote(inspect(__MODULE__))}.new ["a", "b"], fn x -> {x, x} end
|
||||
#=> ["a": "a", "b": "b"]
|
||||
"""
|
||||
def new(list, transform) when is_function(transform) do
|
||||
Enum.reduce list, new(), fn i, dict ->
|
||||
{ k, v } = transform.(i)
|
||||
unquote(ref).put(dict, k, v)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-214
@@ -1,214 +0,0 @@
|
||||
defexception EEx.SyntaxError, message: nil
|
||||
|
||||
defmodule EEx do
|
||||
@moduledoc %B"""
|
||||
EEx stands for Embedded Elixir. It allows you to embed
|
||||
Elixir code inside a string in a robust way:
|
||||
|
||||
EEx.eval_string "foo <%= bar %>", [bar: "baz"]
|
||||
#=> "foo baz"
|
||||
|
||||
## API
|
||||
|
||||
This module provides 3 main APIs for you to use:
|
||||
|
||||
1) Evaluate a string (`eval_string`) or a file (`eval_file`)
|
||||
directly. This is the simplest API to use but also the
|
||||
slowest, since the code is evaluated and not compiled before;
|
||||
|
||||
2) Define a function from a string (`function_from_string`)
|
||||
or a file (`function_from_file`). This allows you to embed
|
||||
the template as a function inside a module which will then
|
||||
be compiled. This is the preferred API if you have access
|
||||
to the template at compilation time;
|
||||
|
||||
3) Compile a string (`compile_string`) or a file (`compile_file`)
|
||||
into Elixir syntax tree. This is the API used by both functions
|
||||
above and is available to you if you want to provide your own
|
||||
ways of handling the compiled template.
|
||||
|
||||
## Engine
|
||||
|
||||
EEx has the concept of engines which allows you to modify or
|
||||
transform the code extracted from the given string or file.
|
||||
|
||||
By default, `EEx` uses the `EEx.SmartEngine` that provides some
|
||||
conveniences on top of the simple `EEx.Engine`.
|
||||
|
||||
### Tags
|
||||
|
||||
`EEx.SmartEngine` supports the following tags:
|
||||
|
||||
<% Elixir expression - inline with output %>
|
||||
<& Elxir matching expression - not printed &>
|
||||
<%= Elixir expression - replace with result %>
|
||||
|
||||
All expressions that output something to the template
|
||||
*must* use the equals sign (`=`). Since everything in
|
||||
Elixir is a macro, there are no exceptions for this rule.
|
||||
For example, while some template languages would special-
|
||||
case `if` clauses, they are treated the same in EEx and
|
||||
also requires `=` in order to have their result printed:
|
||||
|
||||
<%= if true do %>
|
||||
It is obviously true
|
||||
<% else %>
|
||||
This will never appear
|
||||
<% end %>
|
||||
|
||||
The `<& ... &>` expression is only used in matching clauses.
|
||||
For example, the `cond` macro would be written as:
|
||||
|
||||
<%= cond do %>
|
||||
<& false -> &> Never printed
|
||||
<& true -> &> always printed
|
||||
<% end %>
|
||||
|
||||
Notice that different engines may have different rules
|
||||
for each tag. Other tags may be added in future versions.
|
||||
|
||||
### Macros
|
||||
|
||||
`EEx.SmartEngine` also adds two macros to your template.
|
||||
The first one is the `for` macro, which allows you to easily
|
||||
loop a variable:
|
||||
|
||||
EEx.eval_string "<%= for x in [1,2,3] do %><%= x %>\n<% end %>", []
|
||||
#=> "1\n2\n3\n"
|
||||
|
||||
It also adds defines a macro named `@` that allows easy access:
|
||||
|
||||
EEx.eval_string "<%= @foo %>", assigns: [foo: 1]
|
||||
#=> 1
|
||||
|
||||
In other words, <%= @foo %> is simply translated to:
|
||||
|
||||
<%= Keyword.get assigns, :foo %>
|
||||
|
||||
The assigns extension is useful when the number of variables
|
||||
required by the template is not specified at compilation time.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Generates a function definition from the string.
|
||||
The kind (`:def` or `:defp`) must be given, the
|
||||
function name, its arguments and the compilation options.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Sample do
|
||||
require EEx
|
||||
EEx.function_from_string :def, :sample, "<%= a + b %>", [:a, :b]
|
||||
end
|
||||
|
||||
Sample.sample(1, 2) #=> "3"
|
||||
|
||||
"""
|
||||
defmacro function_from_string(kind, name, source, args // [], options // []) do
|
||||
quote do
|
||||
info = Keyword.merge unquote(options), [file: __FILE__, line: __LINE__]
|
||||
EEx.function_from_quoted(__MODULE__, unquote(kind), unquote(name),
|
||||
unquote(args), EEx.compile_string(unquote(source), info), info)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a function definition from the file contents.
|
||||
The kind (`:def` or `:defp`) must be given, the
|
||||
function name, its arguments and the compilation options.
|
||||
|
||||
This function is useful in case you have templates but
|
||||
you want to precompile inside a module for speed.
|
||||
|
||||
## Examples
|
||||
|
||||
# sample.eex
|
||||
<%= a + b %>
|
||||
|
||||
# sample.ex
|
||||
defmodule Sample do
|
||||
require EEx
|
||||
EEx.function_from_file :def, :sample, "sample.eex", [:a, :b]
|
||||
end
|
||||
|
||||
# iex
|
||||
Sample.sample(1, 2) #=> "3"
|
||||
|
||||
"""
|
||||
defmacro function_from_file(kind, name, filename, args // [], options // []) do
|
||||
quote do
|
||||
file = unquote(filename)
|
||||
info = Keyword.merge unquote(options), [file: file, line: 1]
|
||||
|
||||
@file file
|
||||
EEx.function_from_quoted(__MODULE__, unquote(kind), unquote(name),
|
||||
unquote(args), EEx.compile_file(file, info), info)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a string `source` and generate a quoted expression
|
||||
that can be evaluated by Elixir or compiled to a function.
|
||||
"""
|
||||
def compile_string(source, options // []) do
|
||||
EEx.Compiler.compile(source, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a `filename` and generate a quoted expression
|
||||
that can be evaluated by Elixir or compiled to a function.
|
||||
"""
|
||||
def compile_file(filename, options // []) do
|
||||
options = Keyword.merge options, [file: filename, line: 1]
|
||||
compile_string(File.read!(filename), options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a string `source` and evaluate the values using the `bindings`.
|
||||
|
||||
## Examples
|
||||
|
||||
EEx.eval_string "foo <%= bar %>", [bar: "baz"]
|
||||
#=> "foo baz"
|
||||
|
||||
"""
|
||||
def eval_string(source, bindings // [], options // []) do
|
||||
compiled = compile_string(source, options)
|
||||
do_eval(compiled, bindings, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a `filename` and evaluate the values using the `bindings`.
|
||||
|
||||
## Examples
|
||||
|
||||
# sample.ex
|
||||
foo <%= bar %>
|
||||
|
||||
# iex
|
||||
EEx.eval_file "sample.ex", [bar: "baz"]
|
||||
#=> "foo baz"
|
||||
|
||||
"""
|
||||
def eval_file(filename, bindings // [], options // []) do
|
||||
options = Keyword.put options, :file, filename
|
||||
compiled = compile_file(filename, options)
|
||||
do_eval(compiled, bindings, options)
|
||||
end
|
||||
|
||||
### Helpers
|
||||
|
||||
@doc false
|
||||
def function_from_quoted(module, kind, name, args, source, info) do
|
||||
args = Enum.map args, fn arg -> { arg, 0, nil } end
|
||||
quote = quote do
|
||||
unquote(kind).(unquote(name).(unquote_splicing(args)), do: unquote(source))
|
||||
end
|
||||
Module.eval_quoted module, quote, [], info
|
||||
end
|
||||
|
||||
defp do_eval(compiled, bindings, options) do
|
||||
{ result, _ } = Code.eval_quoted(compiled, bindings, options)
|
||||
result
|
||||
end
|
||||
end
|
||||
@@ -1,117 +0,0 @@
|
||||
defrecord EEx.State, engine: EEx.SmartEngine, dict: [], file: 'nofile', line: 1, start_line: 1
|
||||
|
||||
defmodule EEx.Compiler do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
This is the compilation entry point. It glues the tokenizer
|
||||
and the engine together by handling the tokens and invoking
|
||||
the engine every time a full expression or text is received.
|
||||
"""
|
||||
def compile(source, options) do
|
||||
line = Keyword.get(options, :line, 1)
|
||||
tokens = EEx.Tokenizer.tokenize(source, line)
|
||||
state = EEx.State.new(options)
|
||||
generate_buffer(tokens, "", [], state)
|
||||
end
|
||||
|
||||
# Generates the buffers by handling each expression from the tokenizer
|
||||
|
||||
defp generate_buffer([{ :text, _line, chars }|t], buffer, scope, state) do
|
||||
buffer = state.engine.handle_text(buffer, chars)
|
||||
generate_buffer(t, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer([{ :expr, line, mark, chars }|t], buffer, scope, state) do
|
||||
expr = maybe_block Erlang.elixir_translator.forms(chars, line, state.file)
|
||||
buffer = state.engine.handle_expr(buffer, mark, expr)
|
||||
generate_buffer(t, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer([{ :start_expr, line, mark, chars }|t], buffer, scope, state) do
|
||||
{ contents, t } = generate_buffer(t, "", [chars|scope], state.dict([]).line(line).start_line(line))
|
||||
buffer = state.engine.handle_expr(buffer, mark, contents)
|
||||
generate_buffer(t, buffer, scope, state.dict([]))
|
||||
end
|
||||
|
||||
defp generate_buffer([{ :middle_expr, line, _, chars }|t], buffer, [current|scope], state) do
|
||||
{ wrapped, state } = wrap_expr(current, line, buffer, chars, state)
|
||||
generate_buffer(t, "", [wrapped|scope], state.line(line))
|
||||
end
|
||||
|
||||
defp generate_buffer([{ :end_expr, line, _, chars }|t], buffer, [current|_], state) do
|
||||
{ wrapped, state } = wrap_expr(current, line, buffer, chars, state)
|
||||
tuples = maybe_block Erlang.elixir_translator.forms(wrapped, state.start_line, state.file)
|
||||
buffer = insert_quotes(tuples, state.dict)
|
||||
{ buffer, t }
|
||||
end
|
||||
|
||||
defp generate_buffer([{ :end_expr, line, _, chars }|_], _buffer, [], _state) do
|
||||
raise EEx.SyntaxError, message: "unexpected token: #{inspect chars} at line #{inspect line}"
|
||||
end
|
||||
|
||||
defp generate_buffer([], buffer, [], _state) do
|
||||
buffer
|
||||
end
|
||||
|
||||
defp generate_buffer([], _buffer, _scope, _state) do
|
||||
raise EEx.SyntaxError, message: "unexpected end of string. expecting a closing <% end %>."
|
||||
end
|
||||
|
||||
# Creates a placeholder and wrap it inside the expression block
|
||||
|
||||
defp wrap_expr(current, line, buffer, chars, state) do
|
||||
new_lines = List.duplicate(?\n, line - state.line)
|
||||
|
||||
if state.dict == [] and is_empty?(buffer) do
|
||||
{ current ++ new_lines ++ chars, state }
|
||||
else
|
||||
key = length(state.dict)
|
||||
placeholder = '__EEX__(' ++ integer_to_list(key) ++ ');'
|
||||
{ current ++ placeholder ++ new_lines ++ chars, state.prepend_dict([{key, buffer}]) }
|
||||
end
|
||||
end
|
||||
|
||||
# Check if the syntax node represents an empty string
|
||||
|
||||
defp is_empty?(bin) when is_binary(bin) do
|
||||
bc(<<c>> in bin when not c in [?\s,?\t,?\r,?\n], do: <<c>>) == ""
|
||||
end
|
||||
|
||||
defp is_empty?({ :<>, _, [left, right] }) do
|
||||
is_empty?(left) and is_empty?(right)
|
||||
end
|
||||
|
||||
defp is_empty?(_) do
|
||||
false
|
||||
end
|
||||
|
||||
# Block wrapping
|
||||
|
||||
defp maybe_block([]), do: nil
|
||||
defp maybe_block([h]), do: h
|
||||
defp maybe_block(other), do: { :__block__, 0, other }
|
||||
|
||||
# Changes placeholder to real expression
|
||||
|
||||
defp insert_quotes({ :__EEX__, _, [key] }, dict) do
|
||||
{ ^key, value } = List.keyfind dict, key, 1
|
||||
value
|
||||
end
|
||||
|
||||
defp insert_quotes({ left, line, right }, dict) do
|
||||
{ insert_quotes(left, dict), line, insert_quotes(right, dict) }
|
||||
end
|
||||
|
||||
defp insert_quotes({ left, right }, dict) do
|
||||
{ insert_quotes(left, dict), insert_quotes(right, dict) }
|
||||
end
|
||||
|
||||
defp insert_quotes(list, dict) when is_list(list) do
|
||||
Enum.map list, insert_quotes(&1, dict)
|
||||
end
|
||||
|
||||
defp insert_quotes(other, _dict) do
|
||||
other
|
||||
end
|
||||
end
|
||||
@@ -1,57 +0,0 @@
|
||||
defmodule EEx.Engine do
|
||||
@moduledoc %B"""
|
||||
This is the basic EEx engine that ships with Elixir.
|
||||
An engine needs to implement two functions:
|
||||
|
||||
* `handle_text(buffer, text)` - it receives the buffer,
|
||||
the text and must return a new quoted expression;
|
||||
|
||||
* `handle_expr(buffer, marker, expr)` - it receives the buffer,
|
||||
the marker, the expr and must return a new quoted expression;
|
||||
|
||||
The marker is what follows exactly after `<%`. For example,
|
||||
`<% foo %>` has an empty marker, but `<%= foo %>` has `'='`
|
||||
as marker. The allowed markers so far are:
|
||||
|
||||
* `''`
|
||||
* `'='`
|
||||
|
||||
Read `handle_expr/3` below for more information about the markers
|
||||
implemented by default by this engine.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
The default implementation simply concatenates text to the buffer.
|
||||
"""
|
||||
def handle_text(buffer, text) do
|
||||
quote do: unquote(buffer) <> unquote(text)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Implements expressions according to the markers.
|
||||
|
||||
<% Elixir expression - inline with output %>
|
||||
<%= Elixir expression - replace with result %>
|
||||
|
||||
All other markers are not implemented by this engine.
|
||||
"""
|
||||
def handle_expr(buffer, '=', expr) do
|
||||
quote do
|
||||
tmp_1 = unquote(buffer)
|
||||
tmp_2 = to_binary(unquote(expr))
|
||||
tmp_1 <> tmp_2
|
||||
end
|
||||
end
|
||||
|
||||
def handle_expr(buffer, '', expr) do
|
||||
quote do
|
||||
tmp = unquote(buffer)
|
||||
unquote(expr)
|
||||
tmp
|
||||
end
|
||||
end
|
||||
|
||||
def behaviour_info(:callbacks) do
|
||||
[handle_text: 2, handle_expr: 3]
|
||||
end
|
||||
end
|
||||
@@ -1,132 +0,0 @@
|
||||
defmodule EEx.TransformerEngine do
|
||||
@moduledoc """
|
||||
An abstract engine that is meant to be used and
|
||||
built upon in other modules. This engine implements
|
||||
the `EEx.Engine` behavior and provides a `transform`
|
||||
overridable directive that allows a developer to
|
||||
customize the expression returned by the engine.
|
||||
|
||||
Check `EEx.AssignsEngine`, `EEx.ForEngine` and
|
||||
`EEx.SmartEngine` for examples of using this module.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_, _) do
|
||||
quote do
|
||||
@behavior EEx.Engine
|
||||
|
||||
def handle_text(buffer, text) do
|
||||
EEx.Engine.handle_text(buffer, text)
|
||||
end
|
||||
|
||||
def handle_expr(buffer, mark, expr) do
|
||||
EEx.Engine.handle_expr(buffer, mark, transform(expr))
|
||||
end
|
||||
|
||||
defp transform({ a, b, c }) do
|
||||
{ transform(a), b, transform(c) }
|
||||
end
|
||||
|
||||
defp transform({ a, b }) do
|
||||
{ transform(a), transform(b) }
|
||||
end
|
||||
|
||||
defp transform(list) when is_list(list) do
|
||||
lc i in list, do: transform(i)
|
||||
end
|
||||
|
||||
defp transform(other) do
|
||||
other
|
||||
end
|
||||
|
||||
defoverridable [transform: 1, handle_expr: 3, handle_text: 2]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule EEx.AssignsEngine do
|
||||
@moduledoc """
|
||||
An abstract engine that, when used with the
|
||||
`TransformerEngine`, allows a developer to access
|
||||
assigns using `@` as syntax.
|
||||
|
||||
This engine is included by default on the SmartEngine.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyEngine do
|
||||
use EEx.TransformerEngine
|
||||
use EEx.AssignsEngine
|
||||
end
|
||||
|
||||
EEx.eval_string("<%= @foo %>", assigns: [foo: 1])
|
||||
#=> 1
|
||||
|
||||
In the example above, we can access the value `foo` under
|
||||
the binding `assigns` using `@foo`. This is useful when
|
||||
a template, after compiled, may receive different assigns
|
||||
and the developer don't want to recompile it for each
|
||||
variable set.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_, _) do
|
||||
quote [unquote: false] do
|
||||
defp transform({ :@, line, [{ name, _, atom }] }) when is_atom(name) and is_atom(atom) do
|
||||
quote(do: Keyword.get var!(assigns), unquote(name))
|
||||
end
|
||||
|
||||
defp transform(_), do: super
|
||||
defoverridable [transform: 1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule EEx.ForEngine do
|
||||
@moduledoc %B"""
|
||||
An abstract engine that, when used with the
|
||||
`TransformerEngine`, allows a developer to easily loop
|
||||
using `for`.
|
||||
|
||||
This engine is included by default on the SmartEngine.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyEngine do
|
||||
use EEx.TransformerEngine
|
||||
use EEx.ForEngine
|
||||
end
|
||||
|
||||
EEx.eval_string("<%= for x in [1,2,3] do %><%= x %>\n<% end %>", assigns: [foo: 1])
|
||||
#=> "1\n2\n3\n"
|
||||
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_, _) do
|
||||
quote [unquote: false] do
|
||||
defp transform({ :for, line, [{ :in, _, [var, collection] }, opts] }) do
|
||||
quote do
|
||||
Enum.map_join(unquote(collection), "", fn(unquote(var), unquote(opts)))
|
||||
end
|
||||
end
|
||||
|
||||
defp transform(_), do: super
|
||||
defoverridable [transform: 1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule EEx.SmartEngine do
|
||||
use EEx.TransformerEngine
|
||||
use EEx.AssignsEngine
|
||||
use EEx.ForEngine
|
||||
|
||||
@moduledoc """
|
||||
An engine meant for end-user usage that includes both
|
||||
`AssignsEngine` and `ForEngine` conveniences. Therefore,
|
||||
a developer can easily access assigns via `@` and loop
|
||||
using `for`. Read `EEx.AssignsEngine` and `EEx.ForEngine`
|
||||
for examples.
|
||||
"""
|
||||
end
|
||||
@@ -1,144 +0,0 @@
|
||||
defmodule EEx.Tokenizer do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Tokenizes the given char list or binary.
|
||||
It returns 4 different types of tokens as result:
|
||||
|
||||
* { :text, line, contents }
|
||||
* { :expr, line, marker, contents }
|
||||
* { :start_expr, line, marker, contents }
|
||||
* { :end_expr, line, marker, contents }
|
||||
|
||||
"""
|
||||
def tokenize(bin, line) when is_binary(bin) do
|
||||
tokenize(binary_to_list(bin), line)
|
||||
end
|
||||
|
||||
def tokenize(list, line) do
|
||||
List.reverse(tokenize(list, line, line, [], []))
|
||||
end
|
||||
|
||||
defp tokenize([?<,h|t], current_line, line, buffer, acc) when h in [?&, ?%] do
|
||||
{ marker, t } = retrieve_marker(h, t)
|
||||
{ expr, new_line, rest } = tokenize_expr h, t, line, []
|
||||
|
||||
token = token_name(h, expr)
|
||||
expr = List.reverse(expr)
|
||||
|
||||
# If it isn't a start or end token, it may be a middle token.
|
||||
if token == :expr do
|
||||
token = middle_expr_token_name(expr)
|
||||
end
|
||||
|
||||
acc = tokenize_text(current_line, buffer, acc)
|
||||
tokenize rest, new_line, new_line, [], [ { token, line, marker, expr } | acc]
|
||||
end
|
||||
|
||||
defp tokenize('\n' ++ t, current_line, line, buffer, acc) do
|
||||
tokenize t, current_line, line + 1, [?\n|buffer], acc
|
||||
end
|
||||
|
||||
defp tokenize([h|t], current_line, line, buffer, acc) do
|
||||
tokenize t, current_line, line, [h|buffer], acc
|
||||
end
|
||||
|
||||
defp tokenize([], current_line, _line, buffer, acc) do
|
||||
tokenize_text(current_line, buffer, acc)
|
||||
end
|
||||
|
||||
# Retrieve marker for <%, <& is ignored
|
||||
|
||||
defp retrieve_marker(?%, '=' ++ t) do
|
||||
{ '=', t }
|
||||
end
|
||||
|
||||
defp retrieve_marker(_, t) do
|
||||
{ '', t }
|
||||
end
|
||||
|
||||
# Tokenize an expression until we find %> or &>
|
||||
|
||||
defp tokenize_expr(char, [char,?>|t], line, buffer) do
|
||||
{ buffer, line, t }
|
||||
end
|
||||
|
||||
defp tokenize_expr(char, '\n' ++ t, line, buffer) do
|
||||
tokenize_expr char, t, line + 1, [?\n|buffer]
|
||||
end
|
||||
|
||||
defp tokenize_expr(char, [h|t], line, buffer) do
|
||||
tokenize_expr char, t, line, [h|buffer]
|
||||
end
|
||||
|
||||
# Raise an error if the expected token is not found
|
||||
|
||||
defp tokenize_expr(char, [], _line, _buffer) do
|
||||
raise EEx.SyntaxError, message: "missing token: " <> <<char, ?>>>
|
||||
end
|
||||
|
||||
# Receive an expression content and check
|
||||
# if it is a start, middle or an end token.
|
||||
#
|
||||
# Start tokens finish with `do` and '->'
|
||||
# Middle tokens are marked as <& or keywords
|
||||
# End tokens contain only the end word
|
||||
|
||||
defp token_name(?&, _), do: :middle_expr
|
||||
defp token_name(?%, rest), do: token_name(rest)
|
||||
|
||||
defp token_name([h|t]) when h in [?\s, ?\t] do
|
||||
token_name(t)
|
||||
end
|
||||
|
||||
defp token_name('od' ++ [h|_]) when h in [?\s, ?\t, ?)] do
|
||||
:start_expr
|
||||
end
|
||||
|
||||
defp token_name('>-' ++ _) do
|
||||
:start_expr
|
||||
end
|
||||
|
||||
defp token_name('dne' ++ t) do
|
||||
if only_spaces?(t), do: :end_expr, else: :expr
|
||||
end
|
||||
|
||||
defp token_name(_) do
|
||||
:expr
|
||||
end
|
||||
|
||||
# Receive an expression contents and see if it matches
|
||||
# a keyword block syntax, like else.
|
||||
|
||||
defp middle_expr_token_name([h|t]) when h in [?\s, ?\t] do
|
||||
middle_expr_token_name(t)
|
||||
end
|
||||
|
||||
defp middle_expr_token_name([h|_] = list) when h >= ?a and h <= ?z do
|
||||
if valid_middle_identifier?(list), do: :middle_expr, else: :expr
|
||||
end
|
||||
|
||||
defp middle_expr_token_name(_) do
|
||||
:expr
|
||||
end
|
||||
|
||||
defp valid_middle_identifier?('else' ++ rest), do: only_spaces?(rest)
|
||||
defp valid_middle_identifier?('after' ++ rest), do: only_spaces?(rest)
|
||||
defp valid_middle_identifier?('catch' ++ rest), do: only_spaces?(rest)
|
||||
defp valid_middle_identifier?('rescue' ++ rest), do: only_spaces?(rest)
|
||||
defp valid_middle_identifier?(_), do: false
|
||||
|
||||
defp only_spaces?([h|t]) when h in [?\s, ?\t], do: only_spaces?(t)
|
||||
defp only_spaces?(other), do: other == []
|
||||
|
||||
# Tokenize the buffered text by appending
|
||||
# it to the given accumulator.
|
||||
|
||||
defp tokenize_text(_line, [], acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp tokenize_text(line, buffer, acc) do
|
||||
[{ :text, line, list_to_binary(List.reverse(buffer)) } | acc]
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,249 +0,0 @@
|
||||
defrecord Elixir.CLI.Config, commands: [], close: [],
|
||||
output: '.', compile: [], halt: true, compiler_options: []
|
||||
|
||||
defmodule Elixir.CLI do
|
||||
@moduledoc false
|
||||
|
||||
import Exception, only: [format_stacktrace: 1]
|
||||
|
||||
# Invoked directly from erlang boot process. It parses all argv
|
||||
# options and execute them in the order they are specified.
|
||||
def process_argv(options) do
|
||||
{ config, argv } = process_options(options, Elixir.CLI.Config.new)
|
||||
|
||||
argv = lc arg in argv, do: list_to_binary(arg)
|
||||
Erlang.gen_server.call(:elixir_code_server, { :argv, argv })
|
||||
|
||||
all_commands = List.reverse(config.commands) ++ List.reverse(config.close)
|
||||
|
||||
try do
|
||||
Enum.map all_commands, process_command(&1, config)
|
||||
if config.halt do
|
||||
at_exit(0)
|
||||
halt(0)
|
||||
end
|
||||
rescue
|
||||
exception ->
|
||||
at_exit(1)
|
||||
stacktrace = System.stacktrace
|
||||
IO.puts :standard_error, "** (#{inspect exception.__record__(:name)}) #{exception.message}"
|
||||
print_stacktrace(stacktrace)
|
||||
halt(1)
|
||||
catch
|
||||
:exit, reason when is_integer(reason) ->
|
||||
at_exit(reason)
|
||||
halt(reason)
|
||||
:exit, :normal ->
|
||||
at_exit(0)
|
||||
halt(0)
|
||||
kind, reason ->
|
||||
at_exit(1)
|
||||
stacktrace = System.stacktrace
|
||||
IO.puts :standard_error, "** (#{kind}) #{inspect(reason)}"
|
||||
print_stacktrace(stacktrace)
|
||||
halt(1)
|
||||
end
|
||||
end
|
||||
|
||||
## Private
|
||||
|
||||
defp at_exit(status) do
|
||||
hooks = Erlang.gen_server.call(:elixir_code_server, :at_exit)
|
||||
lc hook in hooks do
|
||||
try do
|
||||
hook.(status)
|
||||
rescue
|
||||
exception ->
|
||||
IO.puts :standard_error, "** (#{inspect exception.__record__(:name)}) #{exception.message}"
|
||||
print_stacktrace(System.stacktrace)
|
||||
catch
|
||||
kind, reason ->
|
||||
IO.puts :standard_error, "** #{kind} #{inspect(reason)}"
|
||||
print_stacktrace(System.stacktrace)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp invalid_option(option) do
|
||||
IO.puts(:standard_error, "Unknown option #{list_to_binary(option)}")
|
||||
halt(1)
|
||||
end
|
||||
|
||||
defp shared_option?(list, config, callback) do
|
||||
case process_shared(list, config) do
|
||||
{ [h|t], _ } when h == hd(list) ->
|
||||
invalid_option h
|
||||
{ new_list, new_config } ->
|
||||
callback.(new_list, new_config)
|
||||
end
|
||||
end
|
||||
|
||||
defp print_stacktrace(stacktrace) do
|
||||
Enum.each stacktrace, fn s -> IO.puts :standard_error, " #{format_stacktrace(s)}" end
|
||||
end
|
||||
|
||||
# Process shared options
|
||||
|
||||
defp process_shared(['-v'|t], config) do
|
||||
IO.puts "Elixir #{System.version}"
|
||||
process_shared t, config
|
||||
end
|
||||
|
||||
defp process_shared(['-e',h|t], config) do
|
||||
process_shared t, config.prepend_commands [{:eval,h}]
|
||||
end
|
||||
|
||||
defp process_shared(['-pa',h|t], config) do
|
||||
Enum.each File.wildcard(File.expand_path(h)), Code.prepend_path(&1)
|
||||
process_shared t, config
|
||||
end
|
||||
|
||||
defp process_shared(['-pz',h|t], config) do
|
||||
Enum.each File.wildcard(File.expand_path(h)), Code.append_path(&1)
|
||||
process_shared t, config
|
||||
end
|
||||
|
||||
defp process_shared(['-r',h|t], config) do
|
||||
config = Enum.reduce File.wildcard(h), config, fn path, config ->
|
||||
config.prepend_commands [{:require, path}]
|
||||
end
|
||||
process_shared t, config
|
||||
end
|
||||
|
||||
defp process_shared(['-pr',h|t], config) do
|
||||
process_shared t, config.prepend_commands [{:parallel_require, h}]
|
||||
end
|
||||
|
||||
defp process_shared(list, config) do
|
||||
{ list, config }
|
||||
end
|
||||
|
||||
# Process init options
|
||||
|
||||
def process_options(['--'|t], config) do
|
||||
{ config, t }
|
||||
end
|
||||
|
||||
def process_options(['--no-halt'|t], config) do
|
||||
process_options t, config.halt(false)
|
||||
end
|
||||
|
||||
def process_options(['+compile'|t], config) do
|
||||
process_compiler t, config
|
||||
end
|
||||
|
||||
def process_options([h|t] = list, config) do
|
||||
case h do
|
||||
'-' ++ _ ->
|
||||
shared_option? list, config, process_options(&1, &2)
|
||||
_ ->
|
||||
{ config.prepend_commands([{:require, h}]), t }
|
||||
end
|
||||
end
|
||||
|
||||
def process_options([], config) do
|
||||
{ config, [] }
|
||||
end
|
||||
|
||||
# Process compiler options
|
||||
|
||||
defp process_compiler(['--'|t], config) do
|
||||
{ config, t }
|
||||
end
|
||||
|
||||
defp process_compiler(['-o',h|t], config) do
|
||||
process_compiler t, config.output(h)
|
||||
end
|
||||
|
||||
defp process_compiler(['--docs'|t], config) do
|
||||
process_compiler t, config.merge_compiler_options(docs: true)
|
||||
end
|
||||
|
||||
defp process_compiler(['--debug-info'|t], config) do
|
||||
process_compiler t, config.merge_compiler_options(debug_info: true)
|
||||
end
|
||||
|
||||
# This option is used internally so we can compile
|
||||
# Elixir with Elixir without raising module conflicts
|
||||
defp process_compiler(['--ignore-module-conflict'|t], config) do
|
||||
process_compiler t, config.merge_compiler_options(ignore_module_conflict: true)
|
||||
end
|
||||
|
||||
defp process_compiler([h|t] = list, config) do
|
||||
case h do
|
||||
'-' ++ _ ->
|
||||
shared_option? list, config, process_compiler(&1, &2)
|
||||
_ ->
|
||||
pattern = if File.dir?(h), do: '#{h}/**/*.ex', else: h
|
||||
process_compiler t, config.prepend_compile [pattern]
|
||||
end
|
||||
end
|
||||
|
||||
defp process_compiler([], config) do
|
||||
{ config.prepend_commands([{:compile, config.compile}]), [] }
|
||||
end
|
||||
|
||||
# Process commands
|
||||
|
||||
defp process_command({:eval, expr}, _config) do
|
||||
Erlang.elixir.eval(expr, [])
|
||||
end
|
||||
|
||||
defp process_command({:require, file}, _config) do
|
||||
Code.require_file(file)
|
||||
end
|
||||
|
||||
defp process_command({:parallel_require, pattern}, _config) do
|
||||
files = File.wildcard(pattern)
|
||||
files = List.uniq(files)
|
||||
files = Enum.filter files, File.regular?(&1)
|
||||
spawn_requires(files, [])
|
||||
end
|
||||
|
||||
defp process_command({:compile, patterns}, config) do
|
||||
Erlang.file.make_dir(config.output)
|
||||
|
||||
files = Enum.map patterns, File.wildcard(&1)
|
||||
files = List.uniq(List.concat(files))
|
||||
files = Enum.filter files, File.regular?(&1)
|
||||
|
||||
Code.compiler_options(config.compiler_options)
|
||||
Elixir.ParallelCompiler.files_to_path(files, config.output,
|
||||
fn file -> IO.puts "Compiled #{file}" end)
|
||||
end
|
||||
|
||||
# Responsible for spawning requires in parallel
|
||||
# For now, we spawn at maximum four process at the same time
|
||||
|
||||
defp spawn_requires([], []), do: :done
|
||||
defp spawn_requires([], waiting), do: wait_for_messages([], waiting)
|
||||
|
||||
defp spawn_requires(files, waiting) when length(waiting) >= 4 do
|
||||
wait_for_messages(files, waiting)
|
||||
end
|
||||
|
||||
defp spawn_requires([h|t], waiting) do
|
||||
parent = Process.self
|
||||
|
||||
child = spawn_link fn ->
|
||||
try do
|
||||
Code.require_file(h)
|
||||
parent <- { :required, Process.self }
|
||||
catch
|
||||
kind, reason ->
|
||||
parent <- { :failure, Process.self, kind, reason, System.stacktrace }
|
||||
end
|
||||
end
|
||||
|
||||
spawn_requires(t, [child|waiting])
|
||||
end
|
||||
|
||||
defp wait_for_messages(files, waiting) do
|
||||
receive do
|
||||
{ :required, child } ->
|
||||
spawn_requires(files, List.delete(waiting, child))
|
||||
{ :failure, _child, kind, reason, stacktrace } ->
|
||||
Erlang.erlang.raise(kind, reason, stacktrace)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,27 +0,0 @@
|
||||
# Implement error_handler pattern for Erlang
|
||||
# which is integrated with Elixir.ParallelCompiler
|
||||
defmodule Elixir.ErrorHandler do
|
||||
@moduledoc false
|
||||
|
||||
def undefined_function(module, fun, args) do
|
||||
ensure_loaded(module)
|
||||
Erlang.error_handler.undefined_function(module, fun, args)
|
||||
end
|
||||
|
||||
def undefined_lambda(module, fun, args) do
|
||||
ensure_loaded(module)
|
||||
Erlang.error_handler.undefined_lambda(module, fun, args)
|
||||
end
|
||||
|
||||
defp ensure_loaded(module) do
|
||||
case Code.ensure_loaded(module) do
|
||||
{ :module, _ } -> []
|
||||
{ :error, _ } ->
|
||||
parent = Process.get(:elixir_parent_compiler)
|
||||
parent <- { :waiting, Process.self, module }
|
||||
receive do
|
||||
{ :release, ^parent } -> :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,106 +0,0 @@
|
||||
defmodule Elixir.IEx.UnicodeIO do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Implements the get IO API used by IEx. It receives the
|
||||
code cache, the instructions counter and needs to
|
||||
return a list with the new characters inserted.
|
||||
"""
|
||||
def get(cache, _count) do
|
||||
prompt = case cache do
|
||||
[] -> "iex> "
|
||||
_ -> "...> "
|
||||
end
|
||||
:unicode.characters_to_list(Erlang.io.get_line(prompt))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Implements the put IO API used by IEx. It receives the
|
||||
result and prints it.
|
||||
"""
|
||||
def put(result) do
|
||||
IO.inspect result
|
||||
end
|
||||
|
||||
@doc """
|
||||
Implements the error IO API used by IEx. It prints error
|
||||
messages.
|
||||
"""
|
||||
def error(result) do
|
||||
IO.puts :standard_error, result
|
||||
end
|
||||
end
|
||||
|
||||
defrecord Elixir.IEx.Config, io: nil, binding: nil, cache: '', counter: 0, scope: nil
|
||||
|
||||
defmodule Elixir.IEx do
|
||||
@moduledoc false
|
||||
|
||||
import Exception, only: [format_stacktrace: 1]
|
||||
|
||||
def start(binding // [], io // Elixir.IEx.UnicodeIO) do
|
||||
config = boot_config(binding, io)
|
||||
function = fn -> do_loop(config) end
|
||||
Erlang.user_drv.start([:"tty_sl -c -e", {:erlang, :spawn, [function]}])
|
||||
end
|
||||
|
||||
def simple_start(binding // [], io // Elixir.IEx.UnicodeIO) do
|
||||
config = boot_config(binding, io)
|
||||
do_loop(config)
|
||||
end
|
||||
|
||||
def c(files, path // ".") do
|
||||
tuples = Elixir.ParallelCompiler.files_to_path List.wrap(files), path
|
||||
Enum.map tuples, elem(&1, 1)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp boot_config(binding, io) do
|
||||
IO.puts "Interactive Elixir (#{System.version}) - press Ctrl+C to exit"
|
||||
|
||||
scope = Erlang.elixir.scope_for_eval(
|
||||
file: 'iex',
|
||||
delegate_locals_to: __MODULE__
|
||||
)
|
||||
|
||||
Elixir.IEx.Config.new(io: io, binding: binding, scope: scope)
|
||||
end
|
||||
|
||||
defp do_loop(config) do
|
||||
io = config.io
|
||||
|
||||
config = config.increment_counter
|
||||
counter = config.counter
|
||||
cache = config.cache
|
||||
code = cache ++ io.get(cache, counter)
|
||||
|
||||
new_config =
|
||||
try do
|
||||
{ result, new_binding, scope } =
|
||||
Erlang.elixir.eval(code, config.binding, counter, config.scope)
|
||||
io.put result
|
||||
config.binding(new_binding).cache('').scope(scope)
|
||||
rescue
|
||||
TokenMissingError ->
|
||||
config.cache(code)
|
||||
exception ->
|
||||
stacktrace = System.stacktrace
|
||||
io.error "** (#{inspect exception.__record__(:name)}) #{exception.message}"
|
||||
print_stacktrace io, stacktrace
|
||||
config.cache('')
|
||||
catch
|
||||
kind, error ->
|
||||
stacktrace = System.stacktrace
|
||||
io.error "** (#{kind}) #{inspect(error)}"
|
||||
print_stacktrace io, stacktrace
|
||||
config.cache('')
|
||||
end
|
||||
|
||||
do_loop(new_config)
|
||||
end
|
||||
|
||||
defp print_stacktrace(io, stacktrace) do
|
||||
Enum.each stacktrace, fn s -> io.error " #{format_stacktrace(s)}" end
|
||||
end
|
||||
end
|
||||
@@ -1,120 +0,0 @@
|
||||
defmodule Elixir.ParallelCompiler do
|
||||
refer Erlang.orddict, as: Orddict
|
||||
|
||||
@moduledoc """
|
||||
A module responsible for compiling files in parallel.
|
||||
"""
|
||||
|
||||
defmacrop default_callback, do: quote(do: fn x -> x end)
|
||||
|
||||
@doc """
|
||||
Compiles the given files.
|
||||
|
||||
Those files are compiled in parallel and can automatically
|
||||
detect dependencies between them. Once a dependency is found,
|
||||
the current file stops being compiled until the dependency is
|
||||
resolved.
|
||||
|
||||
A callback that receives every time a file is compiled
|
||||
with the module names and binaries defined inside it can
|
||||
be optionally given as argument.
|
||||
"""
|
||||
def files(files, callback // default_callback) do
|
||||
files_to_path(files, nil, callback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the given files to the given path.
|
||||
Read files/2 for more information.
|
||||
"""
|
||||
def files_to_path(files, path, callback // default_callback) do
|
||||
Code.ensure_loaded(Elixir.ErrorHandler)
|
||||
files = Enum.map(files, to_char_list(&1))
|
||||
path = if path, do: to_char_list(path)
|
||||
spawn_compilers(files, path, callback, [], [], [])
|
||||
end
|
||||
|
||||
# We already have 4 currently running, don't spawn new ones
|
||||
defp spawn_compilers(files, output, callback, waiting, queued, result) when
|
||||
length(queued) - length(waiting) >= 4 do
|
||||
wait_for_messages(files, output, callback, waiting, queued, result)
|
||||
end
|
||||
|
||||
# Spawn a compiler for each file in the list until we reach the limit
|
||||
defp spawn_compilers([h|t], output, callback, waiting, queued, result) do
|
||||
parent = Process.self()
|
||||
|
||||
child = spawn_link fn ->
|
||||
Process.put(:elixir_parent_compiler, parent)
|
||||
Process.flag(:error_handler, Elixir.ErrorHandler)
|
||||
|
||||
try do
|
||||
if output do
|
||||
Erlang.elixir_compiler.file_to_path(h, output)
|
||||
else
|
||||
Erlang.elixir_compiler.file(h)
|
||||
end
|
||||
parent <- { :compiled, Process.self(), h }
|
||||
catch
|
||||
kind, reason ->
|
||||
parent <- { :failure, Process.self(), kind, reason, System.stacktrace }
|
||||
end
|
||||
end
|
||||
|
||||
spawn_compilers(t, output, callback, waiting, [{child,h}|queued], result)
|
||||
end
|
||||
|
||||
# No more files, nothing waiting, queue is empty, we are done
|
||||
defp spawn_compilers([], _output, _callback, [], [], result), do: result
|
||||
|
||||
# Queued x, waiting for x: POSSIBLE ERROR! Release processes so we get the failures
|
||||
defp spawn_compilers([], output, callback, waiting, queued, result) when length(waiting) == length(queued) do
|
||||
Enum.each queued, fn { child, _ } -> child <- { :release, Process.self() } end
|
||||
wait_for_messages([], output, callback, waiting, queued, result)
|
||||
end
|
||||
|
||||
# No more files, but queue and waiting are not full or do not match
|
||||
defp spawn_compilers([], output, callback, waiting, queued, result) do
|
||||
wait_for_messages([], output, callback, waiting, queued, result)
|
||||
end
|
||||
|
||||
# Wait for messages from child processes
|
||||
defp wait_for_messages(files, output, callback, waiting, queued, result) do
|
||||
receive do
|
||||
{ :compiled, child, file } ->
|
||||
callback.(list_to_binary(file))
|
||||
new_queued = List.keydelete(queued, child, 1)
|
||||
# Sometimes we may have spurious entries in the waiting
|
||||
# list because someone invoked try/rescue UndefinedFunctionError
|
||||
new_waiting = List.keydelete(waiting, child, 1)
|
||||
spawn_compilers(files, output, callback, new_waiting, new_queued, result)
|
||||
{ :module_available, child, module, binary } ->
|
||||
new_waiting = release_waiting_processes(module, waiting)
|
||||
new_result = [{module, binary}|result]
|
||||
wait_for_messages(files, output, callback, new_waiting, queued, new_result)
|
||||
{ :waiting, child, on } ->
|
||||
new_waiting = Orddict.store(child, on, waiting)
|
||||
spawn_compilers(files, output, callback, new_waiting, queued, result)
|
||||
{ :failure, child, kind, reason, stacktrace } ->
|
||||
extra = if match?({^child, module}, List.keyfind(waiting, child, 1)) do
|
||||
" (undefined module #{inspect module})"
|
||||
end
|
||||
|
||||
{^child, file} = List.keyfind(queued, child, 1)
|
||||
IO.puts "== Compilation error on file #{list_to_binary(file)}#{extra} =="
|
||||
Erlang.erlang.raise(kind, reason, stacktrace)
|
||||
end
|
||||
end
|
||||
|
||||
# Release waiting processes that are waiting for the given module
|
||||
defp release_waiting_processes(module, waiting) do
|
||||
Enum.filter waiting, fn { child, waiting_module } ->
|
||||
if waiting_module == module do
|
||||
child <- { :release, Process.self() }
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,496 +0,0 @@
|
||||
defmodule Elixir.SpecialForms do
|
||||
@moduledoc """
|
||||
In this module we define Elixir special forms. Those are called
|
||||
special forms because they cannot be overridden by the developer
|
||||
and sometimes have lexical scope (like refer, require, import, etc).
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Defines a new tuple.
|
||||
|
||||
## Examples
|
||||
|
||||
:{}.(1,2,3)
|
||||
{ 1, 2, 3 }
|
||||
"""
|
||||
defmacro :{}.(args)
|
||||
|
||||
@doc """
|
||||
Defines a new list.
|
||||
|
||||
## Examples
|
||||
|
||||
:[].(1,2,3)
|
||||
[ 1, 2, 3 ]
|
||||
"""
|
||||
defmacro :[].(args)
|
||||
|
||||
@doc """
|
||||
Defines a new bitstring.
|
||||
|
||||
## Examples
|
||||
|
||||
:<<>>.(1,2,3)
|
||||
<< 1, 2, 3 >>
|
||||
"""
|
||||
defmacro :<<>>.(args)
|
||||
|
||||
@doc """
|
||||
`refer` is used to setup aliases between modules.
|
||||
|
||||
## Examples
|
||||
|
||||
`refer` can be used to setup an alias for any module:
|
||||
|
||||
defmodule Math do
|
||||
refer MyKeyword, as: Keyword
|
||||
end
|
||||
|
||||
In the example above, we have set up `MyOrdict` to be referenced
|
||||
as `Keyword`. So now, any reference to `Keyword` will be
|
||||
automatically replaced by `MyKeyword`.
|
||||
|
||||
In case one wants to access the original `Keyword`, it can be done
|
||||
by accessing __MAIN__:
|
||||
|
||||
Keyword.values #=> uses MyKeyword.values
|
||||
__MAIN__.Keyword.values #=> uses Keyword.values
|
||||
|
||||
Notice that calling `refer` without the `as:` option automatically
|
||||
sets an alias based on the last part of the module. For example:
|
||||
|
||||
refer Foo.Bar.Baz
|
||||
|
||||
Is the same as:
|
||||
|
||||
refer Foo.Bar.Baz, as: Baz
|
||||
|
||||
## Lexical scope
|
||||
|
||||
`import`, `require` and `refer` are called directives and all
|
||||
have lexical scope. This means you can set up aliases inside
|
||||
specific functions and it won't affect the overall scope.
|
||||
"""
|
||||
defmacro refer(module, opts)
|
||||
|
||||
@doc """
|
||||
`require` is used to require the presence of external
|
||||
modules so macros can be invoked.
|
||||
|
||||
## Examples
|
||||
|
||||
Notice that usually modules should not be required before usage,
|
||||
the only exception is if you want to use the macros from a module.
|
||||
In such cases, you need to explicitly require them.
|
||||
|
||||
Let's suppose you created your own `if` implementation in the module
|
||||
`MyMacros`. If you want to invoke it, you need to first explicitly
|
||||
require the `MyMacros`:
|
||||
|
||||
defmodule Math do
|
||||
require MyMacros
|
||||
MyMacros.if do_something, it_works
|
||||
end
|
||||
|
||||
An attempt to call a macro that was not loaded will raise an error.
|
||||
|
||||
## Refer shortcut
|
||||
|
||||
`require` also accepts `as:` as an option so it automatically sets
|
||||
up an alias. Please check `refer` for more information.
|
||||
|
||||
"""
|
||||
defmacro require(module, opts)
|
||||
|
||||
@doc """
|
||||
`import` allows one to easily access functions or macros from
|
||||
others modules without using the qualified name.
|
||||
|
||||
## Examples
|
||||
|
||||
If you want to use the `values` function from `Keyword` several times
|
||||
in your module and you don't want to always type `Keyword.values`,
|
||||
you can simply import it:
|
||||
|
||||
defmodule Math do
|
||||
import Keyword, only: [values: 1]
|
||||
|
||||
def some_function do
|
||||
# call values(orddict)
|
||||
end
|
||||
end
|
||||
|
||||
In this case, we are importing only the function `values` (with arity 1)
|
||||
from `Keyword`. Although `only` is optional, its usage is recommended.
|
||||
`except` could also be given as an option. If no option is given, all
|
||||
functions and macros are imported.
|
||||
|
||||
In case you want to import only functions or macros, you can pass a
|
||||
first argument selecting the scope:
|
||||
|
||||
import :macros, MyMacros
|
||||
|
||||
And you can then use `only` or `except` to filter the macros being
|
||||
included.
|
||||
|
||||
## Lexical scope
|
||||
|
||||
It is important to notice that `import` is lexical. This means you
|
||||
can import specific macros inside specific functions:
|
||||
|
||||
defmodule Math do
|
||||
def some_function do
|
||||
# 1) Disable `if/2` from Elixir.Builtin
|
||||
import Elixir.Builtin, except: [if: 2]
|
||||
|
||||
# 2) Require the new `if` macro from MyMacros
|
||||
import MyMacros
|
||||
|
||||
# 3) Use the new macro
|
||||
if do_something, it_works
|
||||
end
|
||||
end
|
||||
|
||||
In the example above, we imported macros from `MyMacros`, replacing
|
||||
the original `if/2` implementation by our own during that
|
||||
specific function. All other functions in that module will still
|
||||
be able to use the original one.
|
||||
|
||||
## Refer/Require shortcut
|
||||
|
||||
All imported modules are also required by default. `import`
|
||||
also accepts `as:` as an option so it automatically sets up
|
||||
an alias. Please check `refer` for more information.
|
||||
"""
|
||||
defmacro import(module, opts)
|
||||
|
||||
@doc """
|
||||
Returns the current module name as an atom or nil otherwise.
|
||||
"""
|
||||
defmacro __MODULE__
|
||||
|
||||
@doc """
|
||||
Returns the current file name as a binary.
|
||||
"""
|
||||
defmacro __FILE__
|
||||
|
||||
@doc """
|
||||
Returns the current line number as an integer.
|
||||
"""
|
||||
defmacro __LINE__
|
||||
|
||||
@doc """
|
||||
Returns the current function as a tuple,
|
||||
where the first element is the name as an atom
|
||||
and the second is the arity as an integer.
|
||||
"""
|
||||
defmacro __FUNCTION__
|
||||
|
||||
@doc """
|
||||
Allows you to get the representation of any expression.
|
||||
|
||||
## Examples
|
||||
|
||||
quote do: sum(1, 2, 3)
|
||||
#=> { :sum, 0, [1, 2, 3] }
|
||||
|
||||
## Homoiconicity
|
||||
|
||||
Elixir is an homoiconic language. Any Elixir program can be
|
||||
represented using its own data structures. The building block
|
||||
of Elixir homoiconicity is a tuple with three elements, for example:
|
||||
|
||||
{ :sum, 1, [1, 2, 3] }
|
||||
|
||||
The tuple above represents a function call to sum passing 1, 2 and
|
||||
3 as arguments. The tuple elements are:
|
||||
|
||||
* The first element of the tuple is always an atom or
|
||||
another tuple in the same representation;
|
||||
* The second element of the tuple is always an integer
|
||||
representing the line number;
|
||||
* The third element of the tuple are the arguments for the
|
||||
function call. The third argument may be an atom, meaning
|
||||
that it may be a variable.
|
||||
|
||||
## Macro literals
|
||||
|
||||
Besides the tuple described above, Elixir has a few literals that
|
||||
when quoted return themselves. They are:
|
||||
|
||||
:sum #=> Atoms
|
||||
1 #=> Integers
|
||||
2.0 #=> Floats
|
||||
[1,2] #=> Lists
|
||||
"binaries" #=> Binaries
|
||||
{key, value} #=> Tuple with two elements
|
||||
|
||||
## Hygiene
|
||||
|
||||
Elixir macros are hygienic regarding to variables. This means
|
||||
a variable defined in a macro cannot affect the scope where
|
||||
the macro is included. Consider the following example:
|
||||
|
||||
defmodule Hygiene do
|
||||
defmacro no_interference do
|
||||
quote do: a = 1
|
||||
end
|
||||
end
|
||||
|
||||
require Hygiene
|
||||
|
||||
a = 10
|
||||
Hygiene.no_interference
|
||||
a #=> 10
|
||||
|
||||
In the example above, `a` returns 10 even if the macro
|
||||
is apparently setting it to 1 because the variables defined
|
||||
in the macro does not affect the context the macro is
|
||||
executed. If you want to set or get a variable, you can do
|
||||
it with the help of the `var!` macro:
|
||||
|
||||
defmodule NoHygiene do
|
||||
defmacro interference do
|
||||
quote do: var!(a) = 1
|
||||
end
|
||||
end
|
||||
|
||||
require NoHygiene
|
||||
|
||||
a = 10
|
||||
NoHygiene.interference
|
||||
a #=> 11
|
||||
|
||||
Notice that references are not hygienic in Elixir unless
|
||||
you explicitly access it via __MAIN__ to the reference name.
|
||||
|
||||
quote do
|
||||
__MAIN__.Foo #=> Access the root Foo
|
||||
Foo #=> Access the Foo reference in the current
|
||||
module (if any is set), then fallback to root
|
||||
end
|
||||
|
||||
## Options
|
||||
|
||||
`quote` also accepts some options as arguments. For example,
|
||||
hygiene can be turned off via `hygiene: false` which is useful
|
||||
when one is generating a code that should be inserted into
|
||||
some function.
|
||||
"""
|
||||
defmacro quote(opts, do: contents)
|
||||
|
||||
@doc """
|
||||
Unquotes the given expression from inside a macro.
|
||||
|
||||
## Examples
|
||||
|
||||
Imagine the situation you have a variable `name` and
|
||||
you want to inject it inside some quote. The first attempt
|
||||
would be:
|
||||
|
||||
value = 13
|
||||
quote do: sum(1, value, 3)
|
||||
|
||||
Which would then return:
|
||||
|
||||
{ :sum, 0, [1, { :value, 0, quoted }, 3] }
|
||||
|
||||
Which is not the expected result. For this, we use unquote:
|
||||
|
||||
value = 13
|
||||
quote do: sum(1, unquote(value), 3)
|
||||
#=> { :sum, 0, [1, 13, 3] }
|
||||
|
||||
"""
|
||||
defmacro unquote(expr)
|
||||
|
||||
@doc """
|
||||
Unquotes the given list expanding its arguments. Similar
|
||||
to unquote.
|
||||
|
||||
## Examples
|
||||
|
||||
values = [2,3,4]
|
||||
quote do: sum(1, unquote_splicing(values), 5)
|
||||
#=> { :sum, 0, [1, 2, 3, 4, 5] }
|
||||
|
||||
"""
|
||||
defmacro unquote_splicing(expr)
|
||||
|
||||
@doc """
|
||||
Returns an anonymous function based on the given arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
sum = fn(x, y) -> x + y end
|
||||
sum.(1, 2) #=> 3
|
||||
|
||||
Notice that a function needs to be invoked using the dot between
|
||||
the function and the arguments.
|
||||
|
||||
A function could also be defined using the `end` syntax, although
|
||||
it is recommend to use it only with the stab operator in order to
|
||||
avoid ambiguity. For example, consider this case:
|
||||
|
||||
Enum.map [1,2,3], fn x ->
|
||||
x * 2
|
||||
end
|
||||
|
||||
The example works fine because `->` binds to the closest function call,
|
||||
which is `fn`, but if we replace it by `do/end`, it will fail:
|
||||
|
||||
Enum.map [1,2,3], fn(x) do
|
||||
x * 2
|
||||
end
|
||||
|
||||
The reason it fails is because do/end always bind to the farthest
|
||||
function call.
|
||||
|
||||
## Function with multiple clauses
|
||||
|
||||
One may define a function which expects different clauses as long
|
||||
as all clauses expects the same number of arguments:
|
||||
|
||||
fun = fn do
|
||||
x, y when y < 0 ->
|
||||
x - y
|
||||
x, y ->
|
||||
x + y
|
||||
end
|
||||
|
||||
fun.(10, -10) #=> 20
|
||||
fun.(10, 10) #=> 20
|
||||
|
||||
"""
|
||||
defmacro fn(args)
|
||||
|
||||
@doc """
|
||||
Handle annonymous recursive loops.
|
||||
|
||||
## Examples
|
||||
|
||||
list = [1,2,3]
|
||||
|
||||
loop list, [] do
|
||||
[h|t], acc ->
|
||||
recur t, [h*2|acc]
|
||||
[], acc ->
|
||||
acc
|
||||
end
|
||||
#=> [6,4,2]
|
||||
|
||||
Notice that all match clauses expects the same ammount
|
||||
of arguments. Guards can also be given.
|
||||
|
||||
Recursion happens by calling recur with the same number
|
||||
of arguments of each match clause. `recur` does not guarantee
|
||||
that it will be tail recursive.
|
||||
"""
|
||||
defmacro loop(args)
|
||||
|
||||
@doc """
|
||||
A function that forces the current loop to recur. See `loop/1`
|
||||
for more information.
|
||||
"""
|
||||
defmacro recur(args)
|
||||
|
||||
@doc """
|
||||
List comprehensions allow you to quickly build a list from another list:
|
||||
|
||||
lc n in [1,2,3,4], do: n * 2
|
||||
#=> [2,4,6,8]
|
||||
|
||||
A comprehension accepts many generators and also filters. Filters must be given after the when clause:
|
||||
|
||||
# A comprehension with a generator and a filter
|
||||
lc n in [1,2,3,4,5,6] when rem(n, 2) == 0, do: n
|
||||
#=> [2,4,6]
|
||||
|
||||
# A comprehension with two generators
|
||||
lc x in [1,2], y in [2,3], do: x*y
|
||||
#=> [2,3,4,6]
|
||||
|
||||
Elixir provides generators for both lists and bitstrings:
|
||||
|
||||
# A list generator:
|
||||
lc n in [1,2,3,4], do: n * 2
|
||||
#=> [2,4,6,8]
|
||||
|
||||
# A bit string generator:
|
||||
lc <<n>> in <<1,2,3,4>>, do: n * 2
|
||||
#=> [2,4,6,8]
|
||||
|
||||
Bit string generators are quite useful when you need to organize bit string streams:
|
||||
|
||||
iex> pixels = <<213,45,132,64,76,32,76,0,0,234,32,15>>
|
||||
iex> lc <<r:8,g:8,b:8>> in pixels, do: {r,g,b}
|
||||
[{213,45,132},{64,76,32},{76,0,0},{234,32,15}]
|
||||
|
||||
Elixir does its best to hide the differences between list and bit string generators.
|
||||
However, there is a special case due to Erlang limitation where we need to explicitly
|
||||
tell Erlang that a list is being given as argument:
|
||||
|
||||
# This will fail because when Elixir sees that the left side
|
||||
# of the in expression is a bit string, it expects the right side
|
||||
# to be a bit string as well:
|
||||
lc <<n>> in [<<1>>,<<2>>,<<3>>], do: n*2
|
||||
#=> ** (ErlangError) erlang error {:bad_generator,[<<1>>,<<2>>,<<3>>]}
|
||||
|
||||
# You need to be explicit and use inlist:
|
||||
lc inlist(<<n>>, [<<1>>,<<2>>,<<3>>]), do: n*2
|
||||
#=> [2,4,6]
|
||||
|
||||
# For consistency, inbin is also available:
|
||||
lc inbin(<<n>>, <<1,2,3>>), do: n*2
|
||||
#=> [2,4,6]
|
||||
|
||||
Notice that although comprehensions uses `when` to specify filters, filters are not
|
||||
guards and therefore accept any expression (they are not limited as guards).
|
||||
"""
|
||||
defmacro lc(args)
|
||||
|
||||
@doc """
|
||||
Defines a bit comprehension. It follows the same syntax as
|
||||
a list comprehension but expects each element returned to
|
||||
be a bitstring. For example, here is how to remove all
|
||||
spaces from a string:
|
||||
|
||||
bc <<c>> in " hello world " when c != ?\s, do: <<c>>
|
||||
"helloworld"
|
||||
|
||||
"""
|
||||
defmacro bc(args)
|
||||
|
||||
@doc """
|
||||
Keeps one of the given expressions depending in the context
|
||||
of evaluation is a guard or not. This is useful when creating
|
||||
macro that should work both inside and outside guards but
|
||||
still hold some characteristics.
|
||||
|
||||
## Example
|
||||
|
||||
A good example is the `is_exception/1` macro defined in Elixir:
|
||||
|
||||
defmacro is_exception(thing) do
|
||||
quote do
|
||||
quote do
|
||||
is_tuple(unquote(thing)) and elem(unquote(thing), 2) == :__exception__
|
||||
else
|
||||
result = unquote(thing)
|
||||
is_tuple(result) and elem(result, 2) == :__exception__
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Notice that if inside a guard, we unquote the same element twice.
|
||||
This will cause the same element to be evaluted twice, but this is
|
||||
fine for guards since we cannot assign variables in guards and
|
||||
we cannot call expressions inside guards. However, when outside
|
||||
of a guard, evaluating the arguments twice can be harmful and
|
||||
unexpected, for this reason, we save the result in a variable.
|
||||
|
||||
In the example above, `in_guard` is allowing us to customize
|
||||
the same macro to work inside and outside guards.
|
||||
"""
|
||||
defmacro in_guard(do: do_block, else: else_block)
|
||||
end
|
||||
-901
@@ -1,901 +0,0 @@
|
||||
defprotocol Enum.Iterator do
|
||||
@moduledoc """
|
||||
This is the protocol used by the `Enum` module.
|
||||
Usually, when you invoke a function in the module `Enum`,
|
||||
the first argument passed to `Enum` is a collection which
|
||||
is forwarded to this protocol in order to retrieve information
|
||||
on how to iterate the collection. That said, when:
|
||||
|
||||
Enum.map [1,2,3], &1 * 2
|
||||
|
||||
Is invoked, it invokes Enum.Iterator.iterator([1,2,3])
|
||||
which returns all the information required by Enum.
|
||||
Read each function documentation below for more information.
|
||||
"""
|
||||
|
||||
@only [List, Record]
|
||||
|
||||
@doc """
|
||||
Iteration in Elixir happens with the help of a iterator
|
||||
function. Every time this function is called, it must
|
||||
return a tuple with two elements. The first element
|
||||
is the next item and the second can be any Elixir term
|
||||
which the function is going to receive as argument the
|
||||
next time it is invoked.
|
||||
|
||||
When there are no more items to be iterated, the function
|
||||
must return the atom `:stop`.
|
||||
|
||||
In order to retrieve this iterator function, Elixir invokes
|
||||
`Enum.Iterator.iterator(collection)` which should return a
|
||||
tuple with two elements: the first element is the iterator
|
||||
function and the second is the first step of iteration.
|
||||
|
||||
As an example, here is the implementation of iterator for lists:
|
||||
|
||||
def iterator(list), do: { iterate(&1), iterate(list) }
|
||||
defp iterate([h|t]), do: { h, t }
|
||||
defp iterate([]), do: :stop
|
||||
|
||||
"""
|
||||
def iterator(collection)
|
||||
end
|
||||
|
||||
defprotocol Enum.OrdIterator do
|
||||
@moduledoc """
|
||||
This protocol is invoked by some functions in Enum which
|
||||
requires an ordered collection to function correctly. For
|
||||
instance, `Enum.split_with/2`, `Enum.take_while` all rely
|
||||
on this protocol.
|
||||
|
||||
An ordered collection does not mean the items are ordered
|
||||
according to the Elixir ordering but simply that any two
|
||||
distinct instances of the same collection with exactly
|
||||
the same items always yield the same order when iterated.
|
||||
"""
|
||||
|
||||
@only [List, Record]
|
||||
|
||||
@doc """
|
||||
Must return a tuple under the same conditions as
|
||||
`Enum.Iterator.iterator`.
|
||||
"""
|
||||
def iterator(collection)
|
||||
|
||||
@doc """
|
||||
On each step, the iterator function returned by `iterator/1`
|
||||
returns a tuple with two elements. This function receives
|
||||
those two elements and must return a list back.
|
||||
|
||||
This is used in order to quicky return a list from any point
|
||||
during iteration. For example, consider the function `Enum.drop`.
|
||||
`Enum.drop collection, 3` should drop 3 items and return a list
|
||||
back. While we could loop over the remaining items to get a list
|
||||
back, this function is invoked allowing us to get a result
|
||||
back without a need to loop the remaining items.
|
||||
"""
|
||||
def to_list(current, next)
|
||||
end
|
||||
|
||||
defmodule Enum do
|
||||
require Enum.Iterator, as: I
|
||||
require Enum.OrdIterator, as: O
|
||||
|
||||
@moduledoc """
|
||||
Provides a set of algorithms that enumerate over collections according to the
|
||||
`Enum.Iterator` protocol. Most of the functions in this module have two
|
||||
flavours. If a given collection implements the mentioned protocol (like
|
||||
list, for instance), you can do
|
||||
|
||||
Enum.map [1,2,3], fn(x) -> x * 2 end
|
||||
|
||||
Depending on the type of the collection, the user-provided function will
|
||||
accept a certain type of argument. For dicts, the argument is always a
|
||||
`{ key, value }` tuple.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Invokes the given `fun` for each item in the `collection` and returns true if
|
||||
each invocation returns true as well, otherwise it shirt-circuits and returns
|
||||
false.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.all? [2,4,6], fn(x) -> rem(x, 2) == 0 end
|
||||
#=> true
|
||||
|
||||
Enum.all? [2,3,4], fn(x) -> rem(x, 2) == 0 end
|
||||
#=> false
|
||||
|
||||
If no function is given, it defaults to checking if
|
||||
all items in the collection evaluate to true.
|
||||
|
||||
Enum.all? [1,2,3] #=> true
|
||||
Enum.all? [1,nil,3] #=> false
|
||||
|
||||
"""
|
||||
def all?(collection, fun // fn(x) -> x end) do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
do_all?(pointer, iterator, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invokes the given `fun` for each item in the `collection` and returns true if
|
||||
at least one invocation returns true. Returns false otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.any? [2,4,6], fn(x) -> rem(x, 2) == 1 end
|
||||
#=> false
|
||||
|
||||
Enum.any? [2,3,4], fn(x) -> rem(x, 2) == 1 end
|
||||
#=> true
|
||||
|
||||
If no function is given, it defaults to checking if
|
||||
at least one item in the collection evaluates to true.
|
||||
|
||||
Enum.any? [false,false,false] #=> false
|
||||
Enum.any? [false,true,false] #=> true
|
||||
|
||||
"""
|
||||
def any?(collection, fun // fn(x) -> x end) do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
do_any?(pointer, iterator, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Drops the first `count` items from the collection. Expects an ordered
|
||||
collection.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.drop [1,2,3], 2 #=> [3]
|
||||
Enum.drop [1,2,3], 10 #=> []
|
||||
Enum.drop [1,2,3], 0 #=> [1,2,3]
|
||||
|
||||
"""
|
||||
def drop(collection, count) do
|
||||
elem split(collection, count), 2
|
||||
end
|
||||
|
||||
@doc """
|
||||
Drops items at the beginning of `collection` while `fun` returns true.
|
||||
Expects an ordered collection.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.drop_while [1,2,3,4,5], fn(x) -> x < 3 end
|
||||
#=> [3,4,5]
|
||||
"""
|
||||
def drop_while(collection, fun) do
|
||||
{ iterator, pointer } = O.iterator(collection)
|
||||
module = O.__impl_for__!(collection)
|
||||
do_drop_while(pointer, iterator, fun, module)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invokes the given `fun` for each item in the `collection`.
|
||||
Returns the `collection` itself.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.each ['some', 'example'], fn(x) -> IO.puts x end
|
||||
|
||||
"""
|
||||
def each(collection, fun) do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
do_each(pointer, iterator, fun)
|
||||
collection
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the collection is empty, otherwise false.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.empty? [] #=> true
|
||||
Enum.empty? [1,2,3] #=> false
|
||||
|
||||
"""
|
||||
def empty?(collection) when is_list(collection) do
|
||||
collection == []
|
||||
end
|
||||
|
||||
def empty?(collection) do
|
||||
{ _iterator, pointer } = I.iterator(collection)
|
||||
pointer === :stop
|
||||
end
|
||||
|
||||
@doc """
|
||||
Filters the collection, i.e. returns only those elements
|
||||
for which `fun` returns true.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.filter [1, 2, 3], fn(x) -> rem(x, 2) == 0 end
|
||||
#=> [2]
|
||||
|
||||
"""
|
||||
def filter(collection, fun) do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
do_filter(pointer, iterator, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Filters the collection and maps its values in one pass.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.filter_map [1, 2, 3], fn(x) -> rem(x, 2) == 0 end, &1 * 2
|
||||
#=> [4]
|
||||
|
||||
"""
|
||||
def filter_map(collection, filter, mapper) do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
do_filter_map(pointer, iterator, filter, mapper)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the first item for which `fun` returns a truthy value. If no such
|
||||
item is found, returns `ifnone`.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.find [2,4,6], fn(x) -> rem(x, 2) == 1 end
|
||||
#=> nil
|
||||
|
||||
Enum.find [2,4,6], 0, fn(x) -> rem(x, 2) == 1 end
|
||||
#=> 0
|
||||
|
||||
Enum.find [2,3,4], fn(x) -> rem(x, 2) == 1 end
|
||||
#=> 3
|
||||
|
||||
"""
|
||||
def find(collection, ifnone // nil, fun) do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
do_find(pointer, iterator, ifnone, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Similar to find, but returns the value of the function
|
||||
invocation instead of the element itself.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.find_value [2,4,6], fn(x) -> rem(x, 2) == 1 end
|
||||
#=> nil
|
||||
|
||||
Enum.find_value [2,4,6], 0, fn(x) -> rem(x, 2) == 1 end
|
||||
#=> 0
|
||||
|
||||
Enum.find_value [2,3,4], fn(x) -> rem(x, 2) == 1 end
|
||||
#=> true
|
||||
|
||||
"""
|
||||
def find_value(collection, ifnone // nil, fun) do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
do_find_value(pointer, iterator, ifnone, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Joins the given `collection` according to `joiner`.
|
||||
Joiner can be either a binary or a list and the
|
||||
result will be of the same type as joiner. If
|
||||
joiner is not passed at all, it defaults to an
|
||||
empty binary.
|
||||
|
||||
All items in the collection must be convertible
|
||||
to binary, otherwise an error is raised.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.join([1,2,3]) #=> "123"
|
||||
Enum.join([1,2,3], " = ") #=> "1 = 2 = 3"
|
||||
Enum.join([1,2,3], ' = ') #=> '1 = 2 = 3'
|
||||
|
||||
"""
|
||||
def join(collection, joiner // "") do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
join(iterator, pointer, joiner)
|
||||
end
|
||||
|
||||
defp join(iterator, collection, joiner) when is_list(joiner) do
|
||||
binary_to_list join(iterator, collection, list_to_binary(joiner))
|
||||
end
|
||||
|
||||
defp join(iterator, pointer, joiner) do
|
||||
do_join(pointer, iterator, joiner, nil)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a new collection, where each item is the result
|
||||
of invoking `fun` on each corresponding item of `collection`.
|
||||
For dicts, the function accepts a key-value tuple.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.map [1, 2, 3], fn(x) -> x * 2 end
|
||||
#=> [2, 4, 6]
|
||||
|
||||
Enum.map [a: 1, b: 2], fn({k, v}) -> { k, -v } end
|
||||
#=> [a: -1, b: -2]
|
||||
|
||||
"""
|
||||
def map(collection, fun) when is_list(collection) do
|
||||
lc item in collection, do: fun.(item)
|
||||
end
|
||||
|
||||
def map(collection, fun) do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
do_map(pointer, iterator, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Maps and joins the given `collection` in one pass.
|
||||
Joiner can be either a binary or a list and the
|
||||
result will be of the same type as joiner. If
|
||||
joiner is not passed at all, it defaults to an
|
||||
empty binary.
|
||||
|
||||
All items in the collection must be convertible
|
||||
to binary, otherwise an error is raised.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.map_join([1,2,3], &1 * 2) #=> "246"
|
||||
Enum.map_join([1,2,3], &1 * 2, " = ") #=> "2 = 4 = 6"
|
||||
Enum.map_join([1,2,3], &1 * 2, ' = ') #=> '2 = 4 = 6'
|
||||
|
||||
"""
|
||||
def map_join(collection, joiner // "", mapper) do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
map_join(iterator, pointer, mapper, joiner)
|
||||
end
|
||||
|
||||
defp map_join(iterator, collection, mapper, joiner) when is_list(joiner) do
|
||||
binary_to_list map_join(iterator, collection, mapper, list_to_binary(joiner))
|
||||
end
|
||||
|
||||
defp map_join(iterator, pointer, mapper, joiner) do
|
||||
do_map_join(pointer, iterator, mapper, joiner, nil)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invokes the given `fun` for each item in the `collection`
|
||||
while also keeping an accumulator. Returns a tuple where
|
||||
the first element is the mapped collection and the second
|
||||
one is the final accumulator.
|
||||
|
||||
For dicts, the first tuple element has to be a { key, value }
|
||||
tuple itself.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.map_reduce [1, 2, 3], 0, fn(x, acc) -> { x * 2, x + acc } end
|
||||
#=> { [2, 4, 6], 6 }
|
||||
|
||||
"""
|
||||
def map_reduce(collection, acc, f) when is_list(collection) do
|
||||
:lists.mapfoldl(f, acc, collection)
|
||||
end
|
||||
|
||||
def map_reduce(collection, acc, fun) do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
do_map_reduce(pointer, iterator, [], acc, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Partitions `collection` into two where the first one contains elements
|
||||
for which `fun` returns a truthy value, and the second one -- for which `fun`
|
||||
returns false or nil.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.partition [1, 2, 3], fn(x) -> rem(x, 2) == 0 end
|
||||
#=> { [2], [1,3] }
|
||||
|
||||
"""
|
||||
def partition(collection, fun) do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
do_partition(pointer, iterator, fun, [], [])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invokes `fun` for each element in the collection passing the accumulator
|
||||
`acc` and the element as arguments. The return value is stored in `acc`.
|
||||
Returns the accumulator.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.reduce [1, 2, 3], 0, fn(x, acc) -> x + acc end
|
||||
#=> 6
|
||||
|
||||
"""
|
||||
def reduce(collection, acc, fun) when is_list(collection) do
|
||||
:lists.foldl(fun, acc, collection)
|
||||
end
|
||||
|
||||
def reduce(collection, acc, fun) do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
do_reduce(pointer, iterator, acc, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sorts the collection according to the quick sort algorithm.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.qsort [3,2,1] #=> [1,2,3]
|
||||
|
||||
"""
|
||||
def qsort(collection) when is_list(collection) do
|
||||
do_list_qsort(collection, [])
|
||||
end
|
||||
|
||||
def qsort(collection) do
|
||||
{ iterator, pointer } = I.iterator(collection)
|
||||
do_qsort(pointer, iterator, [])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Splits the enumerable into two collections, leaving `count` elements in the
|
||||
first one. Expects an ordered collection.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.split [1,2,3], 2 #=> { [1,2], [3] }
|
||||
Enum.split [1,2,3], 10 #=> { [1,2,3], [] }
|
||||
Enum.split [1,2,3], 0 #=> { [], [1,2,3] }
|
||||
|
||||
"""
|
||||
def split(collection, count) when count >= 0 do
|
||||
{ iterator, pointer } = O.iterator(collection)
|
||||
module = O.__impl_for__!(collection)
|
||||
do_split(pointer, iterator, count, [], module)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Splits `collection` at the first element, for which `fun` returns true.
|
||||
Expects an ordered collection.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.split_with [1,2,3,4], fn x -> x == 2 end
|
||||
#=> { [1], [2, 3, 4] }
|
||||
"""
|
||||
def split_with(collection, fun) do
|
||||
{ iterator, pointer } = O.iterator(collection)
|
||||
module = O.__impl_for__!(collection)
|
||||
do_split_with(pointer, iterator, fun, [], module)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Takes the first `count` items from the collection. Expects an ordered
|
||||
collection.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.take [1,2,3], 2 #=> [1,2]
|
||||
Enum.take [1,2,3], 10 #=> [1,2,3]
|
||||
Enum.take [1,2,3], 0 #=> []
|
||||
|
||||
"""
|
||||
def take(collection, count) do
|
||||
elem split(collection, count), 1
|
||||
end
|
||||
|
||||
@doc """
|
||||
Takes the items at the beginning of `collection` while `fun` returns true.
|
||||
Expects an ordered collection.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.take_while [1,2,3], fn(x) -> x < 3 end
|
||||
#=> [1, 2]
|
||||
|
||||
"""
|
||||
def take_while(collection, fun // fn(x) -> x end) do
|
||||
{ iterator, pointer } = O.iterator(collection)
|
||||
do_take_while(pointer, iterator, fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Iterates the given function n times, passing values from 1
|
||||
to n.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.times 3, fn(x) -> IO.puts x end
|
||||
1
|
||||
2
|
||||
3
|
||||
|
||||
"""
|
||||
def times(times, function) when times >= 0 do
|
||||
case is_function(function, 0) do
|
||||
true ->
|
||||
do_times_0(times, 1, function)
|
||||
_ ->
|
||||
do_times_1(times, 1, function)
|
||||
end
|
||||
times
|
||||
end
|
||||
|
||||
@doc """
|
||||
Iterates the given function n times, passing values from 1
|
||||
to n. Also has an accumulator similar to reduce to store the
|
||||
value between computations.
|
||||
|
||||
## Examples
|
||||
|
||||
Enum.times 5, 0, fn(x, acc) -> acc + x end
|
||||
#=> 15
|
||||
|
||||
"""
|
||||
def times(times, acc, function) when times >= 0 do
|
||||
do_times_2(times, 1, function, acc)
|
||||
end
|
||||
|
||||
## Implementations
|
||||
|
||||
## all?
|
||||
|
||||
defp do_all?({ h, next }, iterator, fun) do
|
||||
case fun.(h) do
|
||||
x in [false, nil] ->
|
||||
false
|
||||
_ ->
|
||||
do_all?(iterator.(next), iterator, fun)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_all?(:stop, _, _) do
|
||||
true
|
||||
end
|
||||
|
||||
## any?
|
||||
|
||||
defp do_any?({ h, next }, iterator, fun) do
|
||||
case fun.(h) do
|
||||
x in [false, nil] ->
|
||||
do_any?(iterator.(next), iterator, fun)
|
||||
_ ->
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
defp do_any?(:stop, _, _) do
|
||||
false
|
||||
end
|
||||
|
||||
## drop_while
|
||||
|
||||
defp do_drop_while({ h, next }, iterator, fun, module) do
|
||||
case fun.(h) do
|
||||
x in [false, nil] ->
|
||||
module.to_list(h, next)
|
||||
_ ->
|
||||
do_drop_while(iterator.(next), iterator, fun, module)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_drop_while(:stop, _, _, _) do
|
||||
[]
|
||||
end
|
||||
|
||||
## find
|
||||
|
||||
defp do_find({ h, next }, iterator, ifnone, fun) do
|
||||
case fun.(h) do
|
||||
x in [false, nil] ->
|
||||
do_find(iterator.(next), iterator, ifnone, fun)
|
||||
_ ->
|
||||
h
|
||||
end
|
||||
end
|
||||
|
||||
defp do_find(:stop, _, ifnone, _) do
|
||||
ifnone
|
||||
end
|
||||
|
||||
## find_value
|
||||
|
||||
defp do_find_value({ h, next }, iterator, ifnone, fun) do
|
||||
case fun.(h) do
|
||||
x in [false, nil] ->
|
||||
do_find_value(iterator.(next), iterator, ifnone, fun)
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
defp do_find_value(:stop, _, ifnone, _) do
|
||||
ifnone
|
||||
end
|
||||
|
||||
## each
|
||||
|
||||
defp do_each({ h, next }, iterator, fun) do
|
||||
fun.(h)
|
||||
do_each(iterator.(next), iterator, fun)
|
||||
end
|
||||
|
||||
defp do_each(:stop, _, _) do
|
||||
[]
|
||||
end
|
||||
|
||||
## filter
|
||||
|
||||
defp do_filter({ h, next }, iterator, fun) do
|
||||
case fun.(h) do
|
||||
x in [false, nil] ->
|
||||
do_filter(iterator.(next), iterator, fun)
|
||||
_ ->
|
||||
[h|do_filter(iterator.(next), iterator, fun)]
|
||||
end
|
||||
end
|
||||
|
||||
defp do_filter(:stop, _, _) do
|
||||
[]
|
||||
end
|
||||
|
||||
## filter_map
|
||||
|
||||
defp do_filter_map({ h, next }, iterator, filter, mapper) do
|
||||
case filter.(h) do
|
||||
x in [false, nil] ->
|
||||
do_filter_map(iterator.(next), iterator, filter, mapper)
|
||||
_ ->
|
||||
[mapper.(h)|do_filter_map(iterator.(next), iterator, filter, mapper)]
|
||||
end
|
||||
end
|
||||
|
||||
defp do_filter_map(:stop, _, _, _) do
|
||||
[]
|
||||
end
|
||||
|
||||
## reduce
|
||||
|
||||
defp do_reduce({ h, next }, iterator, acc, fun) do
|
||||
do_reduce(iterator.(next), iterator, fun.(h, acc), fun)
|
||||
end
|
||||
|
||||
defp do_reduce(:stop, _, acc, _) do
|
||||
acc
|
||||
end
|
||||
|
||||
## split_with
|
||||
|
||||
defp do_split_with({ h, next }, iterator, fun, acc, module) do
|
||||
case fun.(h) do
|
||||
x in [false, nil] ->
|
||||
{ List.reverse(acc), module.to_list(h, next) }
|
||||
_ ->
|
||||
do_split_with(iterator.(next), iterator, fun, [h|acc], module)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_split_with(:stop, _, _, acc, _module) do
|
||||
{ List.reverse(acc), [] }
|
||||
end
|
||||
|
||||
## join
|
||||
|
||||
# The first item is simply stringified unless ...
|
||||
defp do_join({ h, next }, iterator, joiner, nil) do
|
||||
do_join(iterator.(next), iterator, joiner, to_binary(h))
|
||||
end
|
||||
|
||||
# The first item is :stop, then we return an empty string;
|
||||
defp do_join(:stop, _, _joiner, nil) do
|
||||
""
|
||||
end
|
||||
|
||||
# All other items are concatenated to acc, by first adding the joiner;
|
||||
defp do_join({ h, next }, iterator, joiner, acc) do
|
||||
acc = << acc | :binary, joiner | :binary, to_binary(h) | :binary >>
|
||||
do_join(iterator.(next), iterator, joiner, acc)
|
||||
end
|
||||
|
||||
# Until we have to stop iteration, then we return acc.
|
||||
defp do_join(:stop, _, _joiner, acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
## map join
|
||||
|
||||
# The first item is simply stringified unless ...
|
||||
defp do_map_join({ h, next }, iterator, mapper, joiner, nil) do
|
||||
do_map_join(iterator.(next), iterator, mapper, joiner, to_binary(mapper.(h)))
|
||||
end
|
||||
|
||||
# The first item is :stop, then we return an empty string;
|
||||
defp do_map_join(:stop, _, _mapper, _joiner, nil) do
|
||||
""
|
||||
end
|
||||
|
||||
# All other items are concatenated to acc, by first adding the joiner;
|
||||
defp do_map_join({ h, next }, iterator, mapper, joiner, acc) do
|
||||
acc = << acc | :binary, joiner | :binary, to_binary(mapper.(h)) | :binary >>
|
||||
do_map_join(iterator.(next), iterator, mapper, joiner, acc)
|
||||
end
|
||||
|
||||
# Until we have to stop iteration, then we return acc.
|
||||
defp do_map_join(:stop, _, _mapper, _joiner, acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
## map
|
||||
|
||||
defp do_map({ h, next }, iterator, fun) do
|
||||
[fun.(h)|do_map(iterator.(next), iterator, fun)]
|
||||
end
|
||||
|
||||
defp do_map(:stop, _, _) do
|
||||
[]
|
||||
end
|
||||
|
||||
## map_reduce
|
||||
|
||||
defp do_map_reduce({ h, next }, iterator, list_acc, acc, f) do
|
||||
{ result, acc } = f.(h, acc)
|
||||
do_map_reduce(iterator.(next), iterator, [result|list_acc], acc, f)
|
||||
end
|
||||
|
||||
defp do_map_reduce(:stop, _, list_acc, acc, _f) do
|
||||
{ List.reverse(list_acc), acc }
|
||||
end
|
||||
|
||||
## partition
|
||||
|
||||
defp do_partition({ h, next }, iterator, fun, acc1, acc2) do
|
||||
case fun.(h) do
|
||||
x in [false, nil] ->
|
||||
do_partition(iterator.(next), iterator, fun, acc1, [h|acc2])
|
||||
_ ->
|
||||
do_partition(iterator.(next), iterator, fun, [h|acc1], acc2)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_partition(:stop, _, _, acc1, acc2) do
|
||||
{ List.reverse(acc1), List.reverse(acc2) }
|
||||
end
|
||||
|
||||
## qsort (lists)
|
||||
|
||||
defp do_list_qsort([], acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp do_list_qsort([h|t], acc) do
|
||||
do_list_qsort_part(h, t, {[], [h], []}, acc)
|
||||
end
|
||||
|
||||
defp do_list_qsort_part(_, [], { l, e, g }, acc) do
|
||||
do_list_qsort(l, e ++ do_list_qsort(g, acc))
|
||||
end
|
||||
|
||||
defp do_list_qsort_part(x, [h|t], { l, e, g }, acc) do
|
||||
cond do
|
||||
h < x ->
|
||||
do_list_qsort_part(x, t, { [h|l], e, g }, acc)
|
||||
h > x ->
|
||||
do_list_qsort_part(x, t, { l, e, [h|g] }, acc)
|
||||
true ->
|
||||
do_list_qsort_part(x, t, { l, [h|e], g }, acc)
|
||||
end
|
||||
end
|
||||
|
||||
## qsort (iterator)
|
||||
|
||||
defp do_qsort({ h, next }, iterator, acc) do
|
||||
do_qsort_part(h, iterator.(next), iterator, {[], [h], []}, acc)
|
||||
end
|
||||
|
||||
defp do_qsort(:stop, _iterator, acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp do_qsort_part(_, :stop, _iterator, { l, e, g }, acc) do
|
||||
do_list_qsort(l, e ++ do_list_qsort(g, acc))
|
||||
end
|
||||
|
||||
defp do_qsort_part(x, { h, next }, iterator, { l, e, g }, acc) do
|
||||
cond do
|
||||
h < x ->
|
||||
do_qsort_part(x, iterator.(next), iterator, { [h|l], e, g }, acc)
|
||||
h > x ->
|
||||
do_qsort_part(x, iterator.(next), iterator, { l, e, [h|g] }, acc)
|
||||
true ->
|
||||
do_qsort_part(x, iterator.(next), iterator, { l, [h|e], g }, acc)
|
||||
end
|
||||
end
|
||||
|
||||
## split
|
||||
|
||||
defp do_split({ h, next }, iterator, counter, acc, module) when counter > 0 do
|
||||
do_split(iterator.(next), iterator, counter - 1, [h|acc], module)
|
||||
end
|
||||
|
||||
defp do_split({ h, next }, _iterator, 0, acc, module) do
|
||||
{ List.reverse(acc), module.to_list(h, next) }
|
||||
end
|
||||
|
||||
defp do_split(:stop, _, _, acc, _module) do
|
||||
{ List.reverse(acc), [] }
|
||||
end
|
||||
|
||||
## take_while
|
||||
|
||||
defp do_take_while({ h, next }, iterator, fun) do
|
||||
case fun.(h) do
|
||||
x in [false, nil] ->
|
||||
[]
|
||||
_ ->
|
||||
[h|do_take_while(iterator.(next), iterator, fun)]
|
||||
end
|
||||
end
|
||||
|
||||
defp do_take_while(:stop, _, _) do
|
||||
[]
|
||||
end
|
||||
|
||||
## times
|
||||
|
||||
defp do_times_0(limit, counter, _function) when counter > limit do
|
||||
end
|
||||
|
||||
defp do_times_0(limit, counter, function) do
|
||||
function.()
|
||||
do_times_0(limit, 1 + counter, function)
|
||||
end
|
||||
|
||||
defp do_times_1(limit, counter, _function) when counter > limit do
|
||||
end
|
||||
|
||||
defp do_times_1(limit, counter, function) do
|
||||
function.(counter)
|
||||
do_times_1(limit, 1 + counter, function)
|
||||
end
|
||||
|
||||
defp do_times_2(limit, counter, _function, acc) when counter > limit do
|
||||
acc
|
||||
end
|
||||
|
||||
defp do_times_2(limit, counter, function, acc) do
|
||||
new_acc = function.(counter, acc)
|
||||
do_times_2(limit, 1 + counter, function, new_acc)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Enum.Iterator, for: List do
|
||||
def iterator(list), do: { iterate(&1), iterate(list) }
|
||||
defp iterate([h|t]), do: { h, t }
|
||||
defp iterate([]), do: :stop # The :stop atom is the end of the iteration.
|
||||
end
|
||||
|
||||
defimpl Enum.OrdIterator, for: List do
|
||||
def iterator(list) do
|
||||
Enum.Iterator.List.iterator(list)
|
||||
end
|
||||
|
||||
def to_list(h, next), do: [h|next]
|
||||
end
|
||||
|
||||
defimpl Enum.Iterator, for: HashDict.Record do
|
||||
def iterator(dict), do: Enum.Iterator.List.iterator(to_list(dict))
|
||||
defp to_list(dict), do: Dict.HashDict.Record.to_list(dict)
|
||||
end
|
||||
|
||||
defimpl Enum.Iterator, for: Orddict.Record do
|
||||
def iterator(dict), do: Enum.Iterator.List.iterator(to_list(dict))
|
||||
defp to_list(dict), do: Dict.Orddict.Record.to_list(dict)
|
||||
end
|
||||
|
||||
defimpl Enum.OrdIterator, for: Orddict.Record do
|
||||
def iterator(dict) do
|
||||
Enum.Iterator.Orddict.Record.iterator(dict)
|
||||
end
|
||||
|
||||
def to_list(h, next), do: [h|next]
|
||||
end
|
||||
@@ -1,74 +0,0 @@
|
||||
defmodule ExUnit do
|
||||
@moduledoc """
|
||||
Basic unit test structure for Elixir.
|
||||
|
||||
## Example
|
||||
|
||||
A basic setup for ExUnit is shown below:
|
||||
|
||||
# File: assertion_test.exs
|
||||
|
||||
# 1) Start ExUnit. You can pass some options as argument (list below)
|
||||
ExUnit.start
|
||||
|
||||
# 2) Next we create a new TestCase and use ExUnit.Case
|
||||
defmodule AssertionTest do
|
||||
use ExUnit.Case
|
||||
|
||||
# 3) A test is a method which name finishes with _test
|
||||
def test_always_pass
|
||||
assert true
|
||||
end
|
||||
end
|
||||
|
||||
To run the test above, all you need to to is to run the file
|
||||
using elixir from command line. Assuming you named your file
|
||||
assertion_test.exs, you can run it as:
|
||||
|
||||
bin/elixir assertion_test.exs
|
||||
|
||||
## Assertions
|
||||
|
||||
Check ExUnit.Assertions for assertions documentation.
|
||||
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Start ExUnit. Required to be invoked before loading
|
||||
any file that uses ExUnit.Case. Check `configure/1`
|
||||
to see the supported options.
|
||||
"""
|
||||
def start(options // []) do
|
||||
ExUnit.Server.start_link
|
||||
configure(options)
|
||||
System.at_exit fn status ->
|
||||
if status == 0, do: ExUnit.run
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Configure ExUnit.
|
||||
|
||||
## Options
|
||||
|
||||
ExUnit supports the following options given to start:
|
||||
|
||||
* `:formatter` - The formatter that will print results
|
||||
* `:max_cases` - Maximum number of cases to run in parallel
|
||||
|
||||
"""
|
||||
def configure(options) do
|
||||
ExUnit.Server.merge_options(options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
API used to run the tests. A developer does not
|
||||
need to call it directly.
|
||||
"""
|
||||
def run do
|
||||
config = ExUnit.Runner.Config.new ExUnit.Server.options
|
||||
config = config.formatter(config.formatter.start)
|
||||
failures = ExUnit.Runner.start config
|
||||
if failures > 0, do: halt(1), else: halt(0)
|
||||
end
|
||||
end
|
||||
@@ -1,407 +0,0 @@
|
||||
defexception ExUnit.AssertionError, message: "assertion failed"
|
||||
|
||||
defmodule ExUnit.Assertions do
|
||||
@moduledoc """
|
||||
This module contains a set of assertions functions that are
|
||||
imported by default into your test cases.
|
||||
|
||||
In general, a developer will want to use the general
|
||||
`assert` macro in tests. The macro tries to be smart
|
||||
and provide good reporting whenever there is a failure.
|
||||
For example, `assert some_fun() == 10` will fail (assuming
|
||||
`some_fun()` returns 13):
|
||||
|
||||
Expected 10 to be equal to 13
|
||||
|
||||
This module also provides other small convenient functions
|
||||
like `assert_match`, `assert_member` and `assert_raise` to
|
||||
easily handle other common cases as, respectively, asserting
|
||||
if two terms match, asserting if an item belongs to a list or
|
||||
if a function raises an exception.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Asserts the `expected` value is true.
|
||||
|
||||
`assert` in general tries to be smart and provide a good
|
||||
reporting whenever there is a failure. For example,
|
||||
`assert 10 > 15` is going to fail with a message:
|
||||
|
||||
Expected 10 to be more than 15
|
||||
|
||||
## Examples
|
||||
|
||||
assert true
|
||||
|
||||
"""
|
||||
defmacro assert(expected) do
|
||||
translate_assertion(expected)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the `expected` value is true.
|
||||
If it fails, raises the given message.
|
||||
|
||||
## Examples
|
||||
|
||||
assert false, "it will never be true"
|
||||
|
||||
"""
|
||||
def assert(expected, message) when is_binary(message) do
|
||||
unless expected do
|
||||
raise ExUnit.AssertionError, message: message
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
## START HELPERS
|
||||
|
||||
defmacrop negation?(op) do
|
||||
quote do: (var!(op) == :! or var!(op) == :not)
|
||||
end
|
||||
|
||||
defp translate_assertion({ :==, _, [left, right] }) do
|
||||
{ expected, actual } = guess_expected_and_actual(left, right)
|
||||
assert_operator :==, expected, actual, "equal to (==)"
|
||||
end
|
||||
|
||||
defp translate_assertion({ :<, _, [left, right] }) do
|
||||
assert_operator :<, left, right, "less than"
|
||||
end
|
||||
|
||||
defp translate_assertion({ :>, _, [left, right] }) do
|
||||
assert_operator :>, left, right, "more than"
|
||||
end
|
||||
|
||||
defp translate_assertion({ :<=, _, [left, right] }) do
|
||||
assert_operator :<=, left, right, "less than or equal to"
|
||||
end
|
||||
|
||||
defp translate_assertion({ :>=, _, [left, right] }) do
|
||||
assert_operator :>=, left, right, "more than or equal to"
|
||||
end
|
||||
|
||||
defp translate_assertion({ :===, _, [left, right] }) do
|
||||
{ expected, actual } = guess_expected_and_actual(left, right)
|
||||
assert_operator :===, expected, actual, "equal to (===)"
|
||||
end
|
||||
|
||||
defp translate_assertion({ :!==, _, [left, right] }) do
|
||||
{ expected, actual } = guess_expected_and_actual(left, right)
|
||||
assert_operator :!==, expected, actual, "not equal to (!==)"
|
||||
end
|
||||
|
||||
defp translate_assertion({ :!=, _, [left, right] }) do
|
||||
{ expected, actual } = guess_expected_and_actual(left, right)
|
||||
assert_operator :!=, expected, actual, "not equal to (!=)"
|
||||
end
|
||||
|
||||
defp translate_assertion({ :access, _, [container, base] }) do
|
||||
quote do
|
||||
container = unquote(container)
|
||||
base = unquote(base)
|
||||
assert(container[base], "Expected #{inspect base} to access #{inspect container}")
|
||||
end
|
||||
end
|
||||
|
||||
defp translate_assertion({ op, _, [{ :access, _, [container, base] }] }) when negation?(op) do
|
||||
quote do
|
||||
container = unquote(container)
|
||||
base = unquote(base)
|
||||
assert(!container[base], "Expected #{inspect base} to not access #{inspect container}")
|
||||
end
|
||||
end
|
||||
|
||||
defp translate_assertion(expected) do
|
||||
quote do
|
||||
value = unquote(expected)
|
||||
assert value, "Expected #{inspect value} to be true"
|
||||
end
|
||||
end
|
||||
|
||||
defp guess_expected_and_actual(left, right) do
|
||||
case right do
|
||||
{ fun, i, _ } when is_integer(i) and (fun != :<<>> or fun != :{}) ->
|
||||
{ left, right }
|
||||
_ ->
|
||||
{ right, left }
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_operator(operator, expected, actual, text) do
|
||||
quote do
|
||||
left = unquote(expected)
|
||||
right = unquote(actual)
|
||||
assert unquote(operator).(left, right),
|
||||
"Expected #{inspect left} to be #{unquote(text)} #{inspect right}"
|
||||
end
|
||||
end
|
||||
|
||||
## END HELPERS
|
||||
|
||||
@doc """
|
||||
Asserts the `expected` value matches `received`. This relies
|
||||
on Elixir's pattern match instead of simply comparing terms.
|
||||
|
||||
## Examples
|
||||
|
||||
assert_match { 1, _, 3 }, { 1, 2, 3 }
|
||||
|
||||
"""
|
||||
defmacro assert_match(expected, received) do
|
||||
quote do
|
||||
try do
|
||||
unquote(expected) = unquote(received)
|
||||
true
|
||||
rescue
|
||||
x in [MatchError] ->
|
||||
raise ExUnit.AssertionError, message: x.message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the value is a member of the given enumerable.
|
||||
Used to check if an item belongs to a list.
|
||||
|
||||
## Examples
|
||||
|
||||
assert_member "foo", ["foo", "bar"]
|
||||
|
||||
"""
|
||||
def assert_member(base, container, message // nil) do
|
||||
message = message || "Expected #{inspect container} to include #{inspect base}"
|
||||
assert(Enum.find(container, &1 == base), message)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the `exception` is raised during `function` execution with the expected message.
|
||||
|
||||
## Examples
|
||||
|
||||
assert_raise ArithmeticError, "bad argument in arithmetic expression", fn ->
|
||||
1 + "test"
|
||||
end
|
||||
"""
|
||||
def assert_raise(exception, expected_message, function) do
|
||||
error = assert_raise(exception, function)
|
||||
assert error.message == expected_message
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the `exception` is raised during `function` execution.
|
||||
|
||||
## Examples
|
||||
|
||||
assert_raise ArithmeticError, fn ->
|
||||
1 + "test"
|
||||
end
|
||||
|
||||
"""
|
||||
def assert_raise(exception, function) do
|
||||
try do
|
||||
function.()
|
||||
flunk "Expected #{inspect exception} exception but nothing was raised"
|
||||
rescue
|
||||
error in [exception] -> error
|
||||
error ->
|
||||
name = error.__record__(:name)
|
||||
|
||||
if name == ExUnit.AssertionError do
|
||||
raise(error)
|
||||
else
|
||||
flunk "Expected exception #{inspect exception}, got #{inspect name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the `enum` collection is empty.
|
||||
|
||||
## Examples
|
||||
|
||||
assert_empty []
|
||||
assert_empty [1, 2]
|
||||
|
||||
"""
|
||||
def assert_empty(enum, message // nil) do
|
||||
message = message || "Expected #{inspect enum} to be empty"
|
||||
assert Enum.empty?(enum), message
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the `value` is nil.
|
||||
|
||||
"""
|
||||
def assert_nil(value, message // nil) do
|
||||
message = message || "Expected #{inspect value} to be nil"
|
||||
assert value == nil, message
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the `expected` and `received` are within `delta`.
|
||||
|
||||
## Examples
|
||||
|
||||
assert_in_delta 1.1, 1.5, 0.2
|
||||
assert_in_delta 10, 15, 4
|
||||
|
||||
"""
|
||||
def assert_in_delta(expected, received, delta, message // nil) do
|
||||
diff = abs(expected - received)
|
||||
message = message ||
|
||||
"Expected |#{inspect expected} - #{inspect received}| (#{inspect diff}) to be < #{inspect delta}"
|
||||
assert diff < delta, message
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the throw `expected` during `function` execution.
|
||||
|
||||
## Examples
|
||||
|
||||
assert_throw 1, fn ->
|
||||
throw 1
|
||||
end
|
||||
|
||||
"""
|
||||
def assert_throw(expected, function) do
|
||||
assert_catch(:throw, expected, function)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the exit `expected` during `function` execution.
|
||||
|
||||
## Examples
|
||||
|
||||
assert_exit 1, fn ->
|
||||
exit 1
|
||||
end
|
||||
|
||||
"""
|
||||
def assert_exit(expected, function) do
|
||||
assert_catch(:exit, expected, function)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the error `expected` during `function` execution.
|
||||
|
||||
## Examples
|
||||
|
||||
assert_error :function_clause, fn ->
|
||||
List.flatten(1)
|
||||
end
|
||||
|
||||
"""
|
||||
def assert_error(expected, function) do
|
||||
assert_catch(:error, expected, function)
|
||||
end
|
||||
|
||||
defp assert_catch(expected_type, expected_value, function) do
|
||||
try do
|
||||
function.()
|
||||
flunk "Expected #{expected_type} #{inspect expected_value}, got nothing"
|
||||
catch
|
||||
^expected_type, ^expected_value ->
|
||||
expected_value
|
||||
^expected_type, actual_value ->
|
||||
flunk "Expected #{expected_type} #{inspect expected_value}, got #{inspect actual_value}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the `not_expected` value is false.
|
||||
|
||||
## Examples
|
||||
|
||||
refute false
|
||||
|
||||
"""
|
||||
def refute(not_expected, message // nil) do
|
||||
message = message || "Expected #{inspect not_expected} to be false"
|
||||
not assert(!not_expected, message)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Assets the `expected` value does not match `received`. This uses
|
||||
Elixir's pattern matching instead of simply comparing terms.
|
||||
|
||||
## Examples
|
||||
|
||||
refute_match { 1, _, 3 }, { 1, 2, 3 }
|
||||
|
||||
"""
|
||||
defmacro refute_match(expected, received) do
|
||||
quote do
|
||||
try do
|
||||
unquote(expected) = unquote(received)
|
||||
flunk "Unexpected right side #{inspect unquote(received)} match"
|
||||
rescue
|
||||
x in [MatchError] -> true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the `enum` collection is not empty.
|
||||
|
||||
## Examples
|
||||
|
||||
refute_empty []
|
||||
refute_empty [1, 2]
|
||||
|
||||
"""
|
||||
def refute_empty(enum, message // nil) do
|
||||
message = message || "Expected #{inspect enum} to not be empty"
|
||||
refute Enum.empty?(enum), message
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the `value` is not nil.
|
||||
"""
|
||||
def refute_nil(value, message // nil) do
|
||||
message = message || "Expected #{inspect value} to not be nil"
|
||||
refute value == nil, message
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the `expected` and `received` are not within `delta`.
|
||||
|
||||
## Examples
|
||||
|
||||
refute_in_delta 1.1, 1.2, 0.2
|
||||
refute_in_delta 10, 11, 2
|
||||
|
||||
"""
|
||||
def refute_in_delta(expected, received, delta, message // nil) do
|
||||
diff = abs(expected - received)
|
||||
message = message ||
|
||||
"Expected |#{inspect expected} - #{inspect received}| (#{inspect diff}) to not be < #{inspect delta}"
|
||||
refute diff < delta, message
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts the value is not a member of the given enumerable.
|
||||
Used to check if an item belongs to a list.
|
||||
|
||||
## Examples
|
||||
|
||||
refute_member "baz", ["foo", "bar"]
|
||||
|
||||
"""
|
||||
def refute_member(base, container, message // nil) do
|
||||
message = message || "Expected #{inspect container} to not include #{inspect base}"
|
||||
refute(Enum.find(container, &1 == base), message)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fails with a message.
|
||||
|
||||
## Examples
|
||||
|
||||
flunk "This should raise an error"
|
||||
|
||||
"""
|
||||
def flunk(message // "Epic Fail!") do
|
||||
assert false, message
|
||||
end
|
||||
end
|
||||
@@ -1,90 +0,0 @@
|
||||
defmodule ExUnit.Case do
|
||||
@moduledoc """
|
||||
This module is meant to be used in other modules
|
||||
as a way to configure and prepare them for testing.
|
||||
|
||||
When used, it allows the following options:
|
||||
|
||||
* :sync - by default, ExUnit run test cases in parallel
|
||||
to each other. If this test case needs to run in sync,
|
||||
set sync to true.
|
||||
|
||||
## Callbacks
|
||||
|
||||
This module defines two callbacks. `setup_all` and `teardown_all`
|
||||
which are executed before and after all tests respectively.
|
||||
Those callbacks needs to return :ok, otherwise we assume
|
||||
tests should not be run.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule AssertionTest do
|
||||
use ExUnit.Case
|
||||
|
||||
def test_always_pass
|
||||
assert true
|
||||
end
|
||||
end
|
||||
|
||||
"""
|
||||
|
||||
@doc false
|
||||
defmacro __using__(module, opts // []) do
|
||||
if Keyword.get(opts, :sync, false) do
|
||||
ExUnit.Server.add_sync_case(module)
|
||||
else
|
||||
ExUnit.Server.add_case(module)
|
||||
end
|
||||
|
||||
quote do
|
||||
import ExUnit.Assertions
|
||||
import ExUnit.Case
|
||||
|
||||
def setup_all, do: :ok
|
||||
def teardown_all, do: :ok
|
||||
|
||||
defoverridable [setup_all: 0, teardown_all: 0]
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Provides a convenient macro that allows a test to be
|
||||
defined with a string. This macro automatically inserts
|
||||
the atom :ok as the last line of the test. That said,
|
||||
a passing test always returns :ok, but, more important,
|
||||
it forces Elixir to not tail call optimize the test and
|
||||
therefore avoiding hiding lines from the backtrace.
|
||||
|
||||
## Examples
|
||||
|
||||
test "true is equal to true" do
|
||||
assert true == true
|
||||
end
|
||||
|
||||
"""
|
||||
defmacro test(message, contents) do
|
||||
contents =
|
||||
case contents do
|
||||
[do: block] ->
|
||||
quote do
|
||||
unquote(contents)
|
||||
:ok
|
||||
end
|
||||
_ ->
|
||||
quote do
|
||||
try(unquote(contents))
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
quote do
|
||||
message = unquote(message)
|
||||
message = if is_binary(message) do
|
||||
:"test #{message}"
|
||||
else
|
||||
:"test_#{message}"
|
||||
end
|
||||
def message, [], [], do: unquote(contents)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,63 +0,0 @@
|
||||
defmodule ExUnit.Formatter do
|
||||
# TODO: Eventually we need to come up with a
|
||||
# public API for the formatter to allow customization
|
||||
@moduledoc false
|
||||
|
||||
use GenServer.Behavior
|
||||
|
||||
defrecord Config, counter: 0, failures: []
|
||||
|
||||
import Exception, only: [format_stacktrace: 1]
|
||||
|
||||
def start do
|
||||
{ :ok, pid } = Erlang.gen_server.start_link(__MODULE__, [], [])
|
||||
pid
|
||||
end
|
||||
|
||||
def init(_args) do
|
||||
{ :ok, Config.new }
|
||||
end
|
||||
|
||||
def handle_call({:each, _test_case, _test, nil }, _from, config) do
|
||||
IO.print "."
|
||||
{ :reply, :ok, config.increment_counter }
|
||||
end
|
||||
|
||||
def handle_call({:each, test_case, test, failure }, _from, config) do
|
||||
IO.print "F"
|
||||
{ :reply, :ok, config.increment_counter.
|
||||
prepend_failures([{test_case, test, failure}]) }
|
||||
end
|
||||
|
||||
def handle_call({:each_case, _test_case}, _from, config) do
|
||||
{ :reply, :ok, config }
|
||||
end
|
||||
|
||||
def handle_call(:finish, _from, config) do
|
||||
IO.print "\n\n"
|
||||
Enum.reduce List.reverse(config.failures), 1, print_failure(&1, &2)
|
||||
failures_count = length(config.failures)
|
||||
IO.puts "#{config.counter} tests, #{failures_count} failures."
|
||||
{ :reply, failures_count, config }
|
||||
end
|
||||
|
||||
defp print_failure({test_case, test, { kind, reason, stacktrace }}, acc) do
|
||||
IO.puts "#{acc}) #{test} (#{inspect test_case})"
|
||||
IO.puts " ** #{format_catch(kind, reason)}\n stacktrace:"
|
||||
Enum.each filter_stacktrace(stacktrace), fn(s) -> IO.puts " #{format_stacktrace(s)}" end
|
||||
IO.print "\n"
|
||||
acc + 1
|
||||
end
|
||||
|
||||
defp format_catch(:error, exception) do
|
||||
"(#{inspect exception.__record__(:name)}) #{exception.message}"
|
||||
end
|
||||
|
||||
defp format_catch(kind, reason) do
|
||||
"(#{kind}) #{inspect(reason)}"
|
||||
end
|
||||
|
||||
defp filter_stacktrace([{ ExUnit.Assertions, _, _, _ }|t]), do: filter_stacktrace(t)
|
||||
defp filter_stacktrace([h|t]), do: [h|filter_stacktrace(t)]
|
||||
defp filter_stacktrace([]), do: []
|
||||
end
|
||||
@@ -1,124 +0,0 @@
|
||||
defrecord ExUnit.Runner.Config, formatter: ExUnit.Formatter, cases: [], max_cases: 4, taken_cases: 0, sync_cases: []
|
||||
|
||||
defmodule ExUnit.Runner do
|
||||
@moduledoc false
|
||||
|
||||
# The runner entry point. At first, it will simply spawn async cases
|
||||
# and expect messages as the cases finish. When all async cases are
|
||||
# spawned and finished, we start running the sync cases. When sync
|
||||
# cases finish, tell the formatter we finished and exit.
|
||||
def start(config) do
|
||||
if config.cases == [] do
|
||||
cond do
|
||||
config.taken_cases > 0 ->
|
||||
do_loop config
|
||||
config.sync_cases == [] ->
|
||||
call_formatter config, :finish
|
||||
true ->
|
||||
do_loop spawn_sync_cases(config)
|
||||
end
|
||||
else
|
||||
do_loop spawn_async_cases(config)
|
||||
end
|
||||
end
|
||||
|
||||
# Loop expecting messages from the spawned cases. Whenever a test
|
||||
# case has finished executing, decrease the taken cases counter and
|
||||
# attempt to spawn new ones.
|
||||
defp do_loop(config) do
|
||||
receive do
|
||||
{ pid, :each, { test_case, test, final } } ->
|
||||
call_formatter config, { :each, test_case, test, final }
|
||||
do_loop config
|
||||
{ pid, :each_case, test_case } ->
|
||||
call_formatter config, { :each_case, test_case }
|
||||
start config.increment_taken_cases(-1)
|
||||
end
|
||||
end
|
||||
|
||||
# Spawn the maximum possible of cases according to the max_cases value.
|
||||
defp spawn_async_cases(config) do
|
||||
case config.cases do
|
||||
[test_case|t] ->
|
||||
if config.taken_cases < config.max_cases do
|
||||
spawn_case test_case
|
||||
spawn_async_cases config.increment_taken_cases.cases(t)
|
||||
else
|
||||
config
|
||||
end
|
||||
[] ->
|
||||
config
|
||||
end
|
||||
end
|
||||
|
||||
# After all cases were run, it is time to run the asynchronous ones.
|
||||
def spawn_sync_cases(config) do
|
||||
[test_case|t] = config.sync_cases
|
||||
spawn_case test_case
|
||||
config.sync_cases(t)
|
||||
end
|
||||
|
||||
# Spawn each test case in a new process.
|
||||
defp spawn_case(test_case) do
|
||||
pid = Process.self
|
||||
spawn_link fn -> run_tests(pid, test_case) end
|
||||
end
|
||||
|
||||
defp run_tests(pid, test_case) do
|
||||
try do
|
||||
tests = tests_for(test_case)
|
||||
test_case.setup_all
|
||||
Enum.each tests, run_test(pid, test_case, &1)
|
||||
test_case.teardown_all
|
||||
after
|
||||
pid <- { Process.self, :each_case, test_case }
|
||||
end
|
||||
end
|
||||
|
||||
defp run_test(pid, test_case, test) do
|
||||
final = try do
|
||||
partial = try do
|
||||
apply test_case, test, []
|
||||
nil
|
||||
rescue
|
||||
error1 ->
|
||||
{ :error, error1, System.stacktrace }
|
||||
catch
|
||||
kind1, error1 ->
|
||||
{ kind1, error1, System.stacktrace }
|
||||
end
|
||||
|
||||
partial
|
||||
rescue
|
||||
error2 ->
|
||||
{ :error, error2, System.stacktrace }
|
||||
catch
|
||||
kind2, error2 ->
|
||||
{ kind2, error2, System.stacktrace }
|
||||
end
|
||||
|
||||
pid <- { Process.self, :each, { test_case, test, final } }
|
||||
end
|
||||
|
||||
defp call_formatter(config, message) do
|
||||
Erlang.gen_server.call(config.formatter, message)
|
||||
end
|
||||
|
||||
# Retrieves test functions from the module.
|
||||
defp tests_for(mod) do
|
||||
exports = mod.__info__(:functions)
|
||||
tests_for exports, []
|
||||
end
|
||||
|
||||
defp tests_for([{function,0}|t], acc) do
|
||||
list = atom_to_list(function)
|
||||
if match?('test_' ++ _, list) || match?('test ' ++ _, list) do
|
||||
tests_for t, [function|acc]
|
||||
else
|
||||
tests_for t, acc
|
||||
end
|
||||
end
|
||||
|
||||
defp tests_for([_|t], acc), do: tests_for t, acc
|
||||
defp tests_for([], acc), do: List.reverse(acc)
|
||||
end
|
||||
@@ -1,65 +0,0 @@
|
||||
defrecord ExUnit.Server.Config, options: [], cases: [], sync_cases: []
|
||||
|
||||
defmodule ExUnit.Server do
|
||||
@moduledoc false
|
||||
|
||||
use GenServer.Behavior
|
||||
|
||||
def start_link do
|
||||
{ :ok, _ } = Erlang.gen_server.start_link({:local, :exunit_server}, __MODULE__, [], [])
|
||||
end
|
||||
|
||||
def add_case(name) do
|
||||
check fn -> Erlang.gen_server.call(:exunit_server, { :add_case, name }) end
|
||||
end
|
||||
|
||||
def add_sync_case(name) do
|
||||
check fn -> Erlang.gen_server.call(:exunit_server, { :add_sync_case, name }) end
|
||||
end
|
||||
|
||||
def merge_options(options) do
|
||||
check fn -> Erlang.gen_server.call(:exunit_server, { :merge_options, options }) end
|
||||
end
|
||||
|
||||
def options do
|
||||
Erlang.gen_server.call(:exunit_server, :options)
|
||||
end
|
||||
|
||||
## Callbacks
|
||||
|
||||
def init(_args) do
|
||||
{ :ok, ExUnit.Server.Config.new }
|
||||
end
|
||||
|
||||
def handle_call({:add_case, name}, _from, config) do
|
||||
{ :reply, :ok, config.prepend_cases [name] }
|
||||
end
|
||||
|
||||
def handle_call({:add_sync_case, name}, _from, config) do
|
||||
{ :reply, :ok, config.prepend_sync_cases [name] }
|
||||
end
|
||||
|
||||
def handle_call({:merge_options, options}, _from, config) do
|
||||
{ :reply, :ok, config.merge_options(options) }
|
||||
end
|
||||
|
||||
def handle_call(:options, _from, config) do
|
||||
options = Keyword.merge config.options,
|
||||
cases: List.reverse(config.cases),
|
||||
sync_cases: List.reverse(config.sync_cases)
|
||||
{ :reply, options, config }
|
||||
end
|
||||
|
||||
def handle_call(request, from, config) do
|
||||
super(request, from, config)
|
||||
end
|
||||
|
||||
defp check(function) do
|
||||
try do
|
||||
function.()
|
||||
catch
|
||||
:exit, { :noproc, _ } ->
|
||||
exit "ExUnit.Server is not running. Are you sure you used exunit from command line?"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,197 +0,0 @@
|
||||
defmodule Exception do
|
||||
@moduledoc """
|
||||
Several convenience functions to work and pretty print
|
||||
exceptions and backtraces.
|
||||
"""
|
||||
|
||||
# Normalize an exception converting Erlang exceptions
|
||||
# to Elixir style exceptions. This is meant to be used
|
||||
# internally.
|
||||
@doc false
|
||||
def normalize(exception) when is_exception(exception) do
|
||||
exception
|
||||
end
|
||||
|
||||
def normalize(:badarg) do
|
||||
ArgumentError.new
|
||||
end
|
||||
|
||||
def normalize(:badarith) do
|
||||
ArithmeticError.new
|
||||
end
|
||||
|
||||
def normalize(:system_limit) do
|
||||
SystemLimitError.new
|
||||
end
|
||||
|
||||
def normalize({ :badarity, { fun, args } }) do
|
||||
BadArityError.new(function: fun, args: args)
|
||||
end
|
||||
|
||||
def normalize({ :badfun, actual }) do
|
||||
BadFunctionError.new(actual: actual)
|
||||
end
|
||||
|
||||
def normalize({ :badmatch, actual }) do
|
||||
MatchError.new(actual: actual)
|
||||
end
|
||||
|
||||
def normalize({ :case_clause, actual }) do
|
||||
CaseClauseError.new(actual: actual)
|
||||
end
|
||||
|
||||
def normalize(:undef) do
|
||||
UndefinedFunctionError.new from_stacktrace(System.stacktrace)
|
||||
end
|
||||
|
||||
def normalize(:function_clause) do
|
||||
FunctionClauseError.new from_stacktrace(System.stacktrace)
|
||||
end
|
||||
|
||||
def normalize({ :badarg, payload }) do
|
||||
ArgumentError.new message: "argument error: #{inspect(payload)}"
|
||||
end
|
||||
|
||||
def normalize(other) do
|
||||
ErlangError.new original: other
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a module, fun and arity and returns a string
|
||||
representing such invocation. Arity may also be a list
|
||||
of arguments. It follows the same syntax as in stacktraces.
|
||||
"""
|
||||
def format_module_fun_arity(module, fun, arity) do
|
||||
<< ?:, fun | :binary >> = inspect(fun)
|
||||
|
||||
if is_list(arity) do
|
||||
inspected = lc x in arity, do: inspect(x)
|
||||
"#{inspect module}.#{fun}(#{Enum.join(inspected, ", ")})"
|
||||
else
|
||||
"#{inspect module}.#{fun}/#{arity}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats each line in the stacktrace.
|
||||
"""
|
||||
def format_stacktrace({module, fun, arity, file_line}) do
|
||||
"#{format_file_line(file_line)}#{format_module_fun_arity(module, fun, arity)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats file and line information present in stacktraces.
|
||||
Expect them to be given in a keywords list.
|
||||
"""
|
||||
def format_file_line(file_line) do
|
||||
format_file_line(Keyword.get(file_line, :file), Keyword.get(file_line, :line))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats the given file and line.
|
||||
"""
|
||||
def format_file_line(file, line) do
|
||||
if file do
|
||||
file = to_binary(file)
|
||||
if line && line != 0 do
|
||||
"#{file}:#{line}: "
|
||||
else
|
||||
"#{file}: "
|
||||
end
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp from_stacktrace([{ module, function, arity, _ }|_]) do
|
||||
[module: module, function: function, arity: arity]
|
||||
end
|
||||
|
||||
defp from_stacktrace(_), do: []
|
||||
end
|
||||
|
||||
defexception RuntimeError, message: "runtime error"
|
||||
defexception ArgumentError, message: "argument error"
|
||||
defexception ArithmeticError, message: "bad argument in arithmetic expression"
|
||||
defexception SystemLimitError, message: "a system limit has been reached"
|
||||
|
||||
defexception SyntaxError, [file: nil, line: nil, description: "syntax error"] do
|
||||
def message(exception) do
|
||||
"#{Exception.format_file_line(exception.file, exception.line)}#{exception.description}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception TokenMissingError, [file: nil, line: nil, description: "expression is incomplete"] do
|
||||
def message(exception) do
|
||||
"#{Exception.format_file_line(exception.file, exception.line)}#{exception.description}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception CompileError, [file: nil, line: nil, description: "compile error"] do
|
||||
def message(exception) do
|
||||
"#{Exception.format_file_line(exception.file, exception.line)}#{exception.description}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception BadFunctionError, [actual: nil] do
|
||||
def message(exception) do
|
||||
"bad function: #{inspect(exception.actual)}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception MatchError, [actual: nil] do
|
||||
def message(exception) do
|
||||
"no match of right hand side value: #{inspect(exception.actual)}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception CaseClauseError, [actual: nil] do
|
||||
def message(exception) do
|
||||
"no case clause matching: #{inspect(exception.actual)}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception BadArityError, [function: nil, args: nil] do
|
||||
def message(exception) do
|
||||
"bad arity error: #{inspect(exception.function)} called with #{inspect(exception.args)}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception UndefinedFunctionError, [module: nil, function: nil, arity: nil] do
|
||||
def message(exception) do
|
||||
if exception.function do
|
||||
formatted = Exception.format_module_fun_arity exception.module, exception.function, to_arity(exception.arity)
|
||||
"undefined function: #{formatted}"
|
||||
else
|
||||
"undefined function"
|
||||
end
|
||||
end
|
||||
|
||||
defp to_arity(arity) when is_integer(arity), do: arity
|
||||
defp to_arity(list) when is_list(list), do: length(list)
|
||||
end
|
||||
|
||||
defexception FunctionClauseError, [module: nil, function: nil, arity: nil] do
|
||||
def message(exception) do
|
||||
if exception.function do
|
||||
formatted = Exception.format_module_fun_arity exception.module, exception.function, exception.arity
|
||||
"no function clause matching: #{formatted}"
|
||||
else
|
||||
"no function clause matches"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defexception Protocol.UndefinedError, [protocol: nil, structure: nil] do
|
||||
def message(exception) do
|
||||
"protocol #{inspect exception.protocol} not implemented for #{inspect exception.structure}"
|
||||
end
|
||||
end
|
||||
|
||||
defexception ErlangError, [original: nil] do
|
||||
def message(exception) do
|
||||
"erlang error: #{inspect(exception.original)}"
|
||||
end
|
||||
end
|
||||
-381
@@ -1,381 +0,0 @@
|
||||
defmodule File do
|
||||
require Erlang.file, as: F
|
||||
require Erlang.filename, as: FN
|
||||
require Erlang.filelib, as: FL
|
||||
|
||||
defrecord Info, Record.extract(:file_info, from_lib: "kernel/include/file.hrl"), moduledoc: """
|
||||
A record responsible to hold file information. Its fields are:
|
||||
|
||||
* `size` - Size of file in bytes.
|
||||
* `type` - `:device`, `:directory`, `:regular`, `:other`. The type of the file.
|
||||
* `access` - `:read`, `:write`, `:read_write`, `:none`. The current system access to
|
||||
the file.
|
||||
* `atime` - The last time the file was read.
|
||||
* `mtime` - The last time the file was written.
|
||||
* `ctime` - The interpretation of this time field depends on the operating
|
||||
system. On Unix, it is the last time the file or the inode was
|
||||
changed. In Windows, it is the create time.
|
||||
* `mode` - The file permissions.
|
||||
* `links` - The number of links to this file. This is always 1 for file
|
||||
systems which have no concept of links.
|
||||
* `major_device` - Identifies the file system where the file is located.
|
||||
In windows, the number indicates a drive as follows:
|
||||
0 means A:, 1 means B:, and so on.
|
||||
* `minor_device` - Only valid for character devices on Unix. In all other
|
||||
cases, this field is zero.
|
||||
* `inode` - Gives the inode number. On non-Unix file systems, this field
|
||||
will be zero.
|
||||
* `uid` - Indicates the owner of the file.
|
||||
* `gid` - Gives the group that the owner of the file belongs to. Will be
|
||||
zero for non-Unix file systems.
|
||||
|
||||
The time type returned in `atime`, `mtime`, and `ctime` is dependent on the
|
||||
time type set in options. `{:time, type}` where type can be `:local`,
|
||||
`:universal`, or `:posix`. Default is `:local`.
|
||||
"""
|
||||
|
||||
defexception Error, [reason: nil, action: "", path: nil] do
|
||||
def message(exception) do
|
||||
formatted = list_to_binary(F.format_error(exception.reason))
|
||||
"could not #{exception.action} #{exception.path}: #{formatted}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Expands the path by returning its absolute name and expanding
|
||||
any `.` and `..` characters.
|
||||
|
||||
## Examples
|
||||
|
||||
File.expand_path("/foo/bar/../bar") == "/foo/bar"
|
||||
|
||||
"""
|
||||
def expand_path(path) do
|
||||
normalize FN.absname(path)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Expands the path to the relative location and expanding
|
||||
any `.` and `..` characters. If the path is already an
|
||||
absolute path, the relative location is ignored.
|
||||
|
||||
## Examples
|
||||
|
||||
File.expand_path("foo/bar/../bar", "/baz") == "/baz/foo/bar"
|
||||
File.expand_path("/foo/bar/../bar", "/baz") == "/foo/bar"
|
||||
|
||||
"""
|
||||
def expand_path(path, relative_to) do
|
||||
normalize FN.absname(FN.absname(path, relative_to))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the path is a regular file.
|
||||
|
||||
## Examples
|
||||
|
||||
File.regular? __FILE__ #=> true
|
||||
"""
|
||||
def regular?(path) do
|
||||
FL.is_regular(path)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the path is a directory.
|
||||
"""
|
||||
def dir?(path) do
|
||||
FL.is_dir(path)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the given argument exists.
|
||||
It can be regular file, directory, socket,
|
||||
symbolic link, named pipe or device file.
|
||||
|
||||
## Examples
|
||||
|
||||
File.exists?("test/")
|
||||
#=> true
|
||||
|
||||
File.exists?("missing.txt")
|
||||
#=> false
|
||||
|
||||
File.exists?("/dev/null")
|
||||
#=> true
|
||||
|
||||
"""
|
||||
def exists?(path) do
|
||||
match?({ :ok, _ }, F.read_file_info(path))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the last component of the path or the path
|
||||
itself if it does not contain any directory separators.
|
||||
|
||||
## Examples
|
||||
|
||||
File.basename("foo")
|
||||
#=> "foo"
|
||||
|
||||
File.basename("foo/bar")
|
||||
#=> "bar"
|
||||
|
||||
File.basename("/")
|
||||
#=> ""
|
||||
|
||||
"""
|
||||
def basename(path) do
|
||||
FN.basename(path)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the last component of `path` with the `extension`
|
||||
stripped. This function should be used to remove a specific
|
||||
extension which might, or might not, be there.
|
||||
|
||||
## Examples
|
||||
|
||||
File.basename("~/foo/bar.ex", ".ex")
|
||||
#=> "bar"
|
||||
File.basename("~/foo/bar.exs", ".ex")
|
||||
#=> "bar.exs"
|
||||
File.basename("~/foo/bar.old.ex", ".ex")
|
||||
#=> "bar.old"
|
||||
|
||||
"""
|
||||
def basename(path, extension) do
|
||||
FN.basename(path, extension)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return the `directory` component of `path`.
|
||||
|
||||
## Examples
|
||||
|
||||
File.dirname("/foo/bar.ex")
|
||||
#=> "foo"
|
||||
|
||||
"""
|
||||
def dirname(path) do
|
||||
FN.dirname(path)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return the `extension` of the last component of `path`.
|
||||
|
||||
## Examples
|
||||
|
||||
File.extname("foo.erl")
|
||||
#=> ".erl"
|
||||
File.extname("~/foo/bar")
|
||||
#=> ""
|
||||
|
||||
"""
|
||||
def extname(path) do
|
||||
FN.extension(path)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the `path` with the `extension` stripped.
|
||||
|
||||
## Examples
|
||||
|
||||
File.rootname("/foo/bar")
|
||||
#=> "/foo/bar"
|
||||
File.rootname("/foo/bar.ex")
|
||||
#=> "/foo/bar"
|
||||
|
||||
"""
|
||||
def rootname(path) do
|
||||
FN.rootname(path)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the `path` with the `extension` stripped. This function should be used to
|
||||
remove a specific extension which might, or might not, be there.
|
||||
|
||||
## Examples
|
||||
|
||||
File.rootname("/foo/bar.erl", ".erl")
|
||||
#=> "/foo/bar"
|
||||
File.rootname("/foo/bar.erl", ".ex")
|
||||
#=> "/foo/bar.erl"
|
||||
|
||||
"""
|
||||
def rootname(path, extension) do
|
||||
FN.rootname(path, extension)
|
||||
end
|
||||
|
||||
|
||||
@doc """
|
||||
Returns a string with one or more paths components joint by the path separator.
|
||||
This function should be used to convert a list of strings in a path.
|
||||
|
||||
## Examples
|
||||
|
||||
File.join(["~", "foo"])
|
||||
#=> "~/foo"
|
||||
File.join(["foo"])
|
||||
#=> "foo"
|
||||
File.join(["/", "foo", "bar"])
|
||||
#=> "/foo/bar"
|
||||
"""
|
||||
def join(paths) do
|
||||
FN.join(paths)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Join two paths.
|
||||
|
||||
## Examples
|
||||
|
||||
File.join("foo", "bar")
|
||||
#=> "foo/bar"
|
||||
|
||||
"""
|
||||
def join(left, right) do
|
||||
FN.join(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `{:ok, binary}`, where `binary` is a binary data object that contains the contents
|
||||
of `filename`, or `{:error, reason}` if an error occurs.
|
||||
|
||||
Typical error reasons:
|
||||
|
||||
* :enoent - The file does not exist.
|
||||
* :eacces - Missing permission for reading the file,
|
||||
or for searching one of the parent directories.
|
||||
* :eisdir - The named file is a directory.
|
||||
* :enotdir - A component of the file name is not a directory.
|
||||
On some platforms, enoent is returned instead.
|
||||
* :enomem - There is not enough memory for the contents of the file.
|
||||
|
||||
You can use `Erlang.file.format_error(reason)` to get a descriptive string of the error.
|
||||
"""
|
||||
def read(path) do
|
||||
F.read_file(path)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns binary with the contents of the given filename or raises
|
||||
File.Error if an error occurs.
|
||||
"""
|
||||
def read!(path) do
|
||||
case read(path) do
|
||||
{ :ok, binary } ->
|
||||
binary
|
||||
{ :error, reason } ->
|
||||
raise File.Error, reason: reason, action: "read file", path: to_binary(path)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with the path splitted by the path separator.
|
||||
If an empty string is given, then it returns the root path.
|
||||
|
||||
## Examples
|
||||
|
||||
File.split("")
|
||||
#=> ["/"]
|
||||
File.split("foo")
|
||||
#=> ["foo"]
|
||||
File.split("/foo/bar")
|
||||
#=> ["/", "foo", "bar"]
|
||||
|
||||
"""
|
||||
def split(path) do
|
||||
FN.split(path)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Traverses files and directories according to the given glob expression.
|
||||
|
||||
The wildcard string looks like an ordinary filename, except that certain
|
||||
"wildcard characters" are interpreted in a special way. The following
|
||||
characters are special:
|
||||
|
||||
* `?` - Matches one character.
|
||||
* `*` - Matches any number of characters up to the end of
|
||||
the filename, the next dot, or the next slash.
|
||||
* `**` - Two adjacent <c>*</c>'s used as a single pattern will
|
||||
match all files and zero or more directories and subdirectories.
|
||||
* `[char1,char2,...]` - Matches any of the characters listed. Two characters
|
||||
separated by a hyphen will match a range of characters.
|
||||
* `{item1,item2,...}` - Matches one of the alternatives.
|
||||
|
||||
Other characters represent themselves. Only filenames that have exactly
|
||||
the same character in the same position will match. Note that matching
|
||||
is case-sensitive; i.e. "a" will not match "A".
|
||||
|
||||
## Examples
|
||||
|
||||
Imagine you have a directory called `projects` with three Elixir projects
|
||||
inside of it: `elixir`, `exdoc` and `dynamo`. You can find all `.beam` files
|
||||
inside their ebin directories all projects as follows:
|
||||
|
||||
File.wildcard("projects/*/ebin/**/*.beam")
|
||||
|
||||
If you want to search for both `.beam` and `.app` files, you could do:
|
||||
|
||||
File.wildcard("projects/*/ebin/**/*.{beam,app}")
|
||||
|
||||
"""
|
||||
def wildcard(glob) when is_binary(glob) do
|
||||
paths = Erlang.elixir_glob.wildcard binary_to_list(glob)
|
||||
Enum.map paths, list_to_binary(&1)
|
||||
end
|
||||
|
||||
def wildcard(path) when is_list(path) do
|
||||
Erlang.elixir_glob.wildcard(path)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns information about a file. If the file exists, it
|
||||
returns a `{ :ok, info }` tuple, where info is as a
|
||||
`File.Info` record. Retuns `{ :error, reason }` with
|
||||
the same reasons as `File.read` if a failure occurs.
|
||||
"""
|
||||
def read_info(path, opts // []) do
|
||||
case :file.read_file_info(path, opts) do
|
||||
{:ok, fileinfo} ->
|
||||
{:ok, Info.new fileinfo}
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Same as `read_info` but returns the `File.Info` directly and
|
||||
throws `File.Error` if an error is returned.
|
||||
"""
|
||||
def read_info!(path, opts // []) do
|
||||
case read_info(path, opts) do
|
||||
{:ok, info} ->
|
||||
info
|
||||
{:error, reason} ->
|
||||
raise File.Error, reason: reason, action: "read file info", path: to_binary(path)
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
# Normalize the given path by removing "..".
|
||||
defp normalize(path), do: normalize(split(path), [])
|
||||
|
||||
defp normalize([top|t], [_|acc]) when top == ".." or top == '..' do
|
||||
normalize t, acc
|
||||
end
|
||||
|
||||
defp normalize([top|t], acc) when top == "." or top == '.' do
|
||||
normalize t, acc
|
||||
end
|
||||
|
||||
defp normalize([h|t], acc) do
|
||||
normalize t, [h|acc]
|
||||
end
|
||||
|
||||
defp normalize([], acc) do
|
||||
join List.reverse(acc)
|
||||
end
|
||||
end
|
||||
@@ -1,48 +0,0 @@
|
||||
defmodule GenServer.Behavior do
|
||||
@doc """
|
||||
By using this module, you get default GenServer callbacks
|
||||
for `handle_call`, `handle_info`, `handle_cast`, `terminate`
|
||||
and `code_change`. `init` still needs to be implemented by the
|
||||
developer. Since these functions are defined as overridable,
|
||||
they can be partially customized and have a global clause
|
||||
that simply invokes `super`. See `ExUnit.Server` for some
|
||||
code examples.
|
||||
|
||||
This module also tags the behavior as :gen_server. For more
|
||||
information on gen_server, please refer to the Erlang
|
||||
documentation:
|
||||
|
||||
http://www.erlang.org/doc/man/gen_server.html
|
||||
http://www.erlang.org/doc/design_principles/gen_server_concepts.html
|
||||
|
||||
"""
|
||||
defmacro __using__(_, _) do
|
||||
quote do
|
||||
@behavior :gen_server
|
||||
|
||||
def handle_call(_request, _from, state) do
|
||||
{ :reply, :undef, state }
|
||||
end
|
||||
|
||||
def handle_info(_msg, state) do
|
||||
{ :noreply, state }
|
||||
end
|
||||
|
||||
def handle_cast(_msg, state) do
|
||||
{ :noreply, state }
|
||||
end
|
||||
|
||||
def terminate(reason, state) do
|
||||
:error_logger.error_report('#{inspect __MODULE__} crashed:\n#{inspect reason}')
|
||||
:error_logger.error_report('#{inspect __MODULE__} snapshot:\n#{inspect state}')
|
||||
:ok
|
||||
end
|
||||
|
||||
def code_change(_old, state, _extra) do
|
||||
{ :ok, state }
|
||||
end
|
||||
|
||||
defoverridable [handle_call: 3, handle_info: 2, handle_cast: 2, terminate: 2, code_change: 3]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,74 +0,0 @@
|
||||
defrecord HashDict.Record, data: nil
|
||||
|
||||
defimpl Dict, for: HashDict.Record do
|
||||
refer HashDict.Record, as: HD
|
||||
|
||||
def keys(HD[data: data]) do
|
||||
:dict.fetch_keys data
|
||||
end
|
||||
|
||||
def values(HD[data: data]) do
|
||||
:dict.fold fn _key, value, acc ->
|
||||
[value|acc]
|
||||
end, [], data
|
||||
end
|
||||
|
||||
def size(HD[data: data]) do
|
||||
:dict.size data
|
||||
end
|
||||
|
||||
def has_key?(HD[data: data], key) do
|
||||
:dict.is_key key, data
|
||||
end
|
||||
|
||||
def get(HD[data: data], key, default // nil) do
|
||||
case :dict.find(key, data) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
:error ->
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def put(HD[data: data], key, value) do
|
||||
HD[data: :dict.store key, value, data]
|
||||
end
|
||||
|
||||
def delete(HD[data: data], key) do
|
||||
HD[data: :dict.erase key, data]
|
||||
end
|
||||
|
||||
def merge(HD[data: d1], HD[data: d2]) do
|
||||
HD[data: :dict.merge fn _k, _v1, v2 -> v2 end, d1, d2]
|
||||
end
|
||||
|
||||
def merge(HD[data: d1], HD[data: d2], fun) do
|
||||
HD[data: :dict.merge fun, d1, d2]
|
||||
end
|
||||
|
||||
def update(HD[data: data], key, fun) do
|
||||
HD[data: :dict.update key, fun, data]
|
||||
end
|
||||
|
||||
def update(HD[data: data], key, initial, fun) do
|
||||
HD[data: :dict.update key, fun, initial, data]
|
||||
end
|
||||
|
||||
def empty(_) do
|
||||
HD[data: :dict.new]
|
||||
end
|
||||
|
||||
def to_list(HD[data: data]) do
|
||||
:dict.to_list data
|
||||
end
|
||||
end
|
||||
|
||||
defmodule HashDict do
|
||||
@moduledoc """
|
||||
This module implements a dictionary based on hashing of the keys.
|
||||
It is a simple wrapper around [Erlang's dict module](http://www.erlang.org/doc/man/dict.html)
|
||||
and exposed via the `Dict` protocol.
|
||||
"""
|
||||
|
||||
use Dict.Common, Dict.HashDict.Record
|
||||
end
|
||||
@@ -1,46 +0,0 @@
|
||||
defmodule IO do
|
||||
@moduledoc """
|
||||
Module responsible for doing IO.
|
||||
It is incomplete now. More functions will be
|
||||
added in upcoming releases.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Prints the given argument to the given device.
|
||||
By default the device is the standard output.
|
||||
The argument is converted to binary before
|
||||
printing.
|
||||
|
||||
It returns `:ok` if it succeeds.
|
||||
|
||||
## Examples
|
||||
|
||||
IO.print :sample
|
||||
#=> "sample"
|
||||
|
||||
IO.print :standard_error, "error"
|
||||
#=> "error"
|
||||
|
||||
"""
|
||||
def print(device // :standard_io, item) do
|
||||
Erlang.io.put_chars device, to_binary(item)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prints the given argument to the device,
|
||||
similarly to print but adds a new line
|
||||
at the end.
|
||||
"""
|
||||
def puts(device // :standard_io, item) do
|
||||
Erlang.io.put_chars device, to_binary(item)
|
||||
Erlang.io.nl(device)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prints the given argument to the device
|
||||
but inspects it before.
|
||||
"""
|
||||
def inspect(device // :standard_io, item) do
|
||||
puts device, Elixir.Builtin.inspect(item)
|
||||
end
|
||||
end
|
||||
-226
@@ -1,226 +0,0 @@
|
||||
defmodule Keyword do
|
||||
@moduledoc """
|
||||
A keyword is a list of tuples where the first element
|
||||
of the tuple is an atom and the second element can be
|
||||
any value. The list is sorted by the first element of
|
||||
each tuple.
|
||||
|
||||
A keyword may have duplicated keys, so it is not strictly
|
||||
a dictionary. However most of the functions in this module
|
||||
allows it to behave exactly as a dictionary. For example,
|
||||
`Keyword.get` will get the first entry matching the given
|
||||
key, regardless if duplicated entries exist. Similarly,
|
||||
`Keyword.put` and `Keyword.delete` ensure all duplicated
|
||||
entries for a given key are removed when invoked.
|
||||
|
||||
This module uses `==` as operator to check if two keys
|
||||
are equal or not.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Creates a Keyword from enum. Differently from `Keyword.new`
|
||||
that behaves as a dict, `Keyword.from_enum` do not remove
|
||||
duplicated entries.
|
||||
"""
|
||||
def from_enum(enum) do
|
||||
Enum.qsort(enum)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an empty keywords list, i.e. an empty list.
|
||||
"""
|
||||
def new do
|
||||
[]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a Keyword from an enumerable. Similarly to dicts,
|
||||
duplicated entries are removed, the latest one prevails.
|
||||
|
||||
## Examples
|
||||
|
||||
Keyword.new [{:b,1},{:a,2}]
|
||||
#=> [a: 2, b: 1]
|
||||
|
||||
"""
|
||||
def new(pairs) do
|
||||
Enum.reduce pairs, [], fn {k, v}, keywords ->
|
||||
put(keywords, k, v)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a Keyword from an enumerable with the
|
||||
help of the transformation function. Duplicated
|
||||
entries are removed, the latest one prevails.
|
||||
|
||||
## Examples
|
||||
|
||||
Keyword.new [:a, :b], fn x -> {x,x} end
|
||||
#=> [a: :a, b: :b]
|
||||
"""
|
||||
def new(pairs, transform) do
|
||||
Enum.reduce pairs, [], fn i, keywords ->
|
||||
{ k, v } = transform.(i)
|
||||
put(keywords, k, v)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the value for specific key.
|
||||
|
||||
If key not exist return default value (nil if no default value)
|
||||
exists.
|
||||
|
||||
If duplicated entries exist, the first one is returned.
|
||||
Use get_values/2 to retrieve all entries.
|
||||
|
||||
## Examples
|
||||
|
||||
Keyword.get [a: 1], :a #=> 1
|
||||
Keyword.get [a: 1], :b #=> nil
|
||||
Keyword.get [a: 1], :b, 3 #=> 3
|
||||
"""
|
||||
def get(keywords, key, default // nil)
|
||||
def get([{k, _}|_], key, default) when key < k, do: default
|
||||
def get([{k, _}|d], key, default) when key > k, do: get(d, key, default)
|
||||
def get([{_, value}|_], _key, _default), do: value
|
||||
def get([], _, default), do: default
|
||||
|
||||
@doc """
|
||||
Gets all values for a specific key.
|
||||
|
||||
## Examples
|
||||
|
||||
Keyword.get_values [a: 1, a: 2], :a
|
||||
#=> [1,2]
|
||||
|
||||
"""
|
||||
def get_values([{k, _}|_], key) when key < k, do: []
|
||||
def get_values([{k, _}|d], key) when key > k, do: get_values(d, key)
|
||||
def get_values([{_, value}|d], key), do: [value|get_values(d, key)]
|
||||
def get_values([], _), do: []
|
||||
|
||||
@doc """
|
||||
Returns all keys from the keywords list. Duplicated
|
||||
keys appear duplicated in the final list of keys.
|
||||
|
||||
## Examples
|
||||
|
||||
Keyword.keys [a: 1, b: 2] #=> [:a,:b]
|
||||
|
||||
"""
|
||||
def keys(keywords) do
|
||||
lc { key, _ } in keywords, do: key
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all values.
|
||||
|
||||
## Examples
|
||||
|
||||
Keyword.values [a: 1, b: 2] #=> [1,2]
|
||||
"""
|
||||
def values(keywords) do
|
||||
lc { _, value } in keywords, do: value
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes all entries in the keywords list for a specific key.
|
||||
If the key does not exist, returns the keywords list unchanged.
|
||||
Use `delete_first` to delete just the first entry in case of
|
||||
duplicated keys.
|
||||
|
||||
## Examples
|
||||
|
||||
Keyword.delete [a: 1, b: 2], :a #=> [b: 2]
|
||||
Keyword.delete [b: 2], :a #=> [b: 2]
|
||||
"""
|
||||
def delete([{k, _}|_] = keywords, key) when key < k, do: keywords
|
||||
def delete([{k, _} = e|tail], key) when key > k, do: [e|delete(tail, key)]
|
||||
def delete([{_, _}|tail], key), do: delete(tail, key)
|
||||
def delete([], _), do: []
|
||||
|
||||
@doc """
|
||||
Sets the given `value` under `key`.
|
||||
|
||||
If a previous value is already stored, all entries are
|
||||
removed and the value is overriden.
|
||||
|
||||
Use `put_other/3` to add a new value for an existing key
|
||||
without removing previous ones.
|
||||
|
||||
## Examples
|
||||
|
||||
Keyword.put [a: 1, b: 2], :a, 3
|
||||
#=> [a: 3, b: 2]
|
||||
"""
|
||||
def put([{k, _} = e|keywords], key, value) when key < k and is_atom(key) do
|
||||
[{key, value},e|keywords]
|
||||
end
|
||||
|
||||
def put([{k, _} = e|keywords], key, value) when key > k do
|
||||
[e|put(keywords, key, value)]
|
||||
end
|
||||
|
||||
def put([{key, _}|keywords], key, value) when is_atom(key) do
|
||||
[{key, value}|delete(keywords, key)]
|
||||
end
|
||||
|
||||
def put([], key, value) when is_atom(key) do
|
||||
[{key, value}]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Merges two keywords lists into one. If they have duplicated
|
||||
entries, the one given as second argument wins.
|
||||
|
||||
## Examples
|
||||
|
||||
Keyword.merge [a: 1, b: 2], [a: 3, d: 4]
|
||||
#=> [a:3, b:2, d: 4]
|
||||
"""
|
||||
def merge(d1, d2) do
|
||||
merge(d1, d2, fn _k, _v1, v2 -> v2 end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Merges two keywords lists into one. If they have duplicated
|
||||
entries, the given function is invoked to solve conflicts.
|
||||
|
||||
## Examples
|
||||
|
||||
Keyword.merge [a: 1, b: 2], [a: 3, d: 4], fn _k, v1, v2 ->
|
||||
v1 + v2
|
||||
end
|
||||
#=> [a:4, b:2, d: 4]
|
||||
"""
|
||||
def merge([{k1, _} = e1|d1], [{k2, _} = e2|d2], fun) when k1 < k2 and is_atom(k1) do
|
||||
[e1|merge(d1, [e2|d2], fun)]
|
||||
end
|
||||
|
||||
def merge([{k1, _} = e1|d1], [{k2, _} = e2|d2], fun) when k1 > k2 and is_atom(k2) do
|
||||
[e2|merge([e1|d1], d2, fun)]
|
||||
end
|
||||
|
||||
def merge([{k1, v1}|d1], [{k1, v2}|d2], fun) do
|
||||
[{k1, fun.(k1, v1, v2)}|merge(d1, d2, fun)]
|
||||
end
|
||||
|
||||
def merge([], d2, _fun), do: d2
|
||||
def merge(d1, [], _fun), do: d1
|
||||
|
||||
@doc """
|
||||
Returns whether a given key exists in the given keywords.
|
||||
|
||||
### Examples
|
||||
Keyword.key?([a: 1], :a)
|
||||
#=> true
|
||||
Keyword.key?([a: 1], :b)
|
||||
#=> false
|
||||
"""
|
||||
def key?([{k, _}|_], key) when key < k, do: false
|
||||
def key?([{k, _}|d], key) when key > k, do: key?(d, key)
|
||||
def key?([{_, _}|_], _key), do: true
|
||||
def key?([], _), do: false
|
||||
end
|
||||
-476
@@ -1,476 +0,0 @@
|
||||
defmodule List do
|
||||
@moduledoc """
|
||||
Implements functions that only make sense for lists
|
||||
and cannot be part of the Enum protocol. In general,
|
||||
favor using the Enum API instead of List.
|
||||
|
||||
A decision was taken to delegate most functions to
|
||||
Erlang's standard lib but following Elixir's convention
|
||||
of receiving the target (in this case, a list) as the
|
||||
first argument.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Simply invokes the Access protocol for the given list.
|
||||
Check `Access.List` for more information.
|
||||
"""
|
||||
def access(list, access) when is_list(list) do
|
||||
Access.List.access(list, access)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Given a list of lists, concatenates the sublists into a single list.
|
||||
|
||||
## Examples
|
||||
|
||||
List.concat [[1,[2],3], [4], [5,6]]
|
||||
#=> [1,[2],3,4,5,6]
|
||||
|
||||
"""
|
||||
def concat(list) when is_list(list) do
|
||||
Erlang.lists.append(list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates the list on the right with the list on the left.
|
||||
|
||||
This function produces the same result the `++` operator. The only difference
|
||||
is a minor optimization: when the first list contains only one element, we
|
||||
simply add it as a head to the second list.
|
||||
|
||||
## Examples
|
||||
|
||||
List.concat [1,2,3], [4,5,6]
|
||||
#=> [1,2,3,4,5,6]
|
||||
|
||||
"""
|
||||
def concat([h], elements) when is_list(elements) do
|
||||
[h|elements]
|
||||
end
|
||||
|
||||
def concat(list, elements) when is_list(list) and is_list(elements) do
|
||||
list ++ elements
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the given item from the list. Returns a list without the item.
|
||||
If the item occurs more than once in the list, just the first occurrence
|
||||
is removed.
|
||||
|
||||
## Examples
|
||||
|
||||
List.delete([1,2,3], 1)
|
||||
#=> [2,3]
|
||||
|
||||
"""
|
||||
def delete(list, item) do
|
||||
Erlang.lists.delete(item, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Flattens the given `list` of nested lists. An optional
|
||||
tail can be given that will be added at the end of
|
||||
the flattened list.
|
||||
|
||||
## Examples
|
||||
|
||||
List.flatten [1,[[2],3]]
|
||||
#=> [1,2,3]
|
||||
|
||||
List.flatten [1,[[2],3]], [4,5]
|
||||
#=> [1,2,3,4,5]
|
||||
|
||||
"""
|
||||
def flatten(list) do
|
||||
Erlang.lists.flatten(list)
|
||||
end
|
||||
|
||||
def flatten(list, tail) do
|
||||
Erlang.lists.flatten(list, tail)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Folds (reduces) the given list to the left with
|
||||
a function. Requires an accumulator.
|
||||
|
||||
## Examples
|
||||
|
||||
List.foldl [5,5], 10, fn x, acc -> x + acc end
|
||||
#=> 20
|
||||
|
||||
List.foldl [1,2,3,4], 0, fn x, acc -> x - acc end
|
||||
#=> 2
|
||||
|
||||
"""
|
||||
def foldl(list, acc, function) when is_list(list) and is_function(function) do
|
||||
Erlang.lists.foldl(function, acc, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Folds (reduces) the given list to the right with
|
||||
a function. Requires an accumulator.
|
||||
|
||||
## Examples
|
||||
|
||||
List.foldr [1,2,3,4], 0, fn x, acc -> x - acc end
|
||||
#=> -2
|
||||
|
||||
"""
|
||||
def foldr(list, acc, function) when is_list(list) and is_function(function) do
|
||||
Erlang.lists.foldr(function, acc, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reverses the given list. This function simply delegates
|
||||
to `lists:reverse` which is implemented in C for performance.
|
||||
|
||||
## Examples
|
||||
|
||||
List.reverse [1,2,3]
|
||||
#=> [3,2,1]
|
||||
|
||||
"""
|
||||
defdelegate [reverse: 1], to: Erlang.lists
|
||||
|
||||
@doc """
|
||||
Returns the last element in `list` or nil if the `list` is empty.
|
||||
|
||||
## Examples
|
||||
|
||||
List.last []
|
||||
#=> nil
|
||||
List.last [1]
|
||||
#=> 1
|
||||
List.last [1, 2, 3]
|
||||
#=> 3
|
||||
|
||||
"""
|
||||
def last([]), do: nil
|
||||
|
||||
defdelegate [last: 1], to: Erlang.lists
|
||||
|
||||
@doc """
|
||||
Checks if the given `term` is included in the list.
|
||||
This function simply delegates to `lists:member`
|
||||
which is implemented in C for performance.
|
||||
|
||||
## Examples
|
||||
|
||||
List.member? [1,2,3], 1
|
||||
#=> true
|
||||
|
||||
List.member? [1,2,3], 0
|
||||
#=> false
|
||||
|
||||
"""
|
||||
def member?(list, term) do
|
||||
Erlang.lists.member(term, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and returns the first tuple
|
||||
where the item at position `posistion` matches with the
|
||||
given `item`.
|
||||
|
||||
## Examples
|
||||
|
||||
List.keyfind([a: 1, b: 2], :a, 1)
|
||||
#=> { :a, 1 }
|
||||
|
||||
List.keyfind([a: 1, b: 2], 2, 2)
|
||||
#=> { :b, 2 }
|
||||
|
||||
List.keyfind([a: 1, b: 2], :c, 1)
|
||||
#=> nil
|
||||
|
||||
"""
|
||||
def keyfind(list, item, position, default // nil) do
|
||||
Erlang.lists.keyfind(item, position, list) || default
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and returns true if there is
|
||||
a tuple where the item at position `posistion` matches
|
||||
with the given `item`.
|
||||
|
||||
## Examples
|
||||
|
||||
List.keymember?([a: 1, b: 2], :a, 1)
|
||||
#=> true
|
||||
|
||||
List.keymember?([a: 1, b: 2], 2, 2)
|
||||
#=> true
|
||||
|
||||
List.keymember?([a: 1, b: 2], :c, 1)
|
||||
#=> false
|
||||
|
||||
"""
|
||||
def keymember?(list, item, position) do
|
||||
Erlang.lists.keymember(item, position, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and deletes the first tuple
|
||||
where the item at position `posistion` matches with the
|
||||
given `item`. Returns the new tuple.
|
||||
|
||||
## Examples
|
||||
|
||||
List.keydelete([a: 1, b: 2], :a, 1)
|
||||
#=> [{ :b, 2 }]
|
||||
|
||||
List.keydelete([a: 1, b: 2], 2, 2)
|
||||
#=> [{ :a, 1 }]
|
||||
|
||||
List.keydelete([a: 1, b: 2], :c, 1)
|
||||
#=> [{ :a, 1 }, { :b, 2 }]
|
||||
|
||||
"""
|
||||
def keydelete(list, item, position) do
|
||||
Erlang.lists.keydelete(item, position, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of integers in the given range (both ends included when
|
||||
possible). An optional step can be provided as well (defaults to 1).
|
||||
|
||||
If first > last and no step is provided, the numbers will be in descending
|
||||
order.
|
||||
|
||||
## Examples
|
||||
|
||||
List.range 1, 3 #=> [1,2,3]
|
||||
List.range 1, 8, 2 #=> [1,3,5,7]
|
||||
List.range 1, 0 #=> []
|
||||
List.range 3, 1 #=> [3,2,1]
|
||||
List.range 5, 1, -2 #=> [5, 3, 1]
|
||||
|
||||
"""
|
||||
def range(first, last, step // nil)
|
||||
|
||||
def range(first, last, step) when is_integer(first) and is_integer(last) and first <= last do
|
||||
step = case step do
|
||||
nil ->
|
||||
Erlang.lists.seq(first, last, 1)
|
||||
x when x < 0 ->
|
||||
[]
|
||||
_ ->
|
||||
Erlang.lists.seq(first, last, step)
|
||||
end
|
||||
end
|
||||
|
||||
def range(first, last, step) when is_integer(first) and is_integer(last) and first > last do
|
||||
step = case step do
|
||||
nil ->
|
||||
Erlang.lists.seq(first, last, -1)
|
||||
x when x > 0 ->
|
||||
[]
|
||||
_ ->
|
||||
Erlang.lists.seq(first, last, step)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sorts the list by comparing each term. For an alternative
|
||||
sorting algorithm, check `Enum.qsort`.
|
||||
|
||||
## Examples
|
||||
|
||||
List.sort [3, 4, 2, 1, 7]
|
||||
#=> [1, 2, 3, 4, 7]
|
||||
|
||||
"""
|
||||
def sort(list) do
|
||||
:lists.sort list
|
||||
end
|
||||
|
||||
|
||||
@doc """
|
||||
Sorts the list according to an ordering function. fun(a, b) should
|
||||
return true if `a` compares less than or equal to `b`, `false` otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
List.sort [3, 4, 2, 1, 7], fn a, b -> b <= a end
|
||||
#=> [7, 4, 3, 2, 1]
|
||||
|
||||
"""
|
||||
def sort(list, fun) do
|
||||
:lists.sort fun, list
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list without duplicated items.
|
||||
|
||||
## Examples
|
||||
|
||||
List.uniq [1,2,3,2,1]
|
||||
#=> [1,2,3]
|
||||
|
||||
"""
|
||||
def uniq(list) when is_list(list) do
|
||||
do_uniq(list, [])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Duplicates the given element n times in a list.
|
||||
|
||||
## Examples
|
||||
|
||||
List.duplicate "hello", 3
|
||||
#=> ["hello","hello","hello"]
|
||||
|
||||
List.duplicate [1,2], 2
|
||||
#=> [[1,2],[1,2]]
|
||||
"""
|
||||
def duplicate(elem, n) do
|
||||
Erlang.lists.duplicate(n, elem)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Looks for a term in a list and returns its position.
|
||||
If term is found in the first position, return 1.
|
||||
If no terms not found in list, the return value is nil.
|
||||
|
||||
### Examples
|
||||
|
||||
List.find_index ['a'], 'b'
|
||||
#=> nil
|
||||
List.find_index ['a'], 'a'
|
||||
#=> 1
|
||||
"""
|
||||
def find_index(list, term) do
|
||||
index = Erlang.string.str(list, [term])
|
||||
case index == 0 do
|
||||
true -> nil
|
||||
false -> index
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Wraps the argument in a list.
|
||||
If the argument is already a list, returns the list.
|
||||
If the argument is nil, returns an empty list.
|
||||
|
||||
## Examples
|
||||
|
||||
List.wrap [1,2,3] #=> [1,2,3]
|
||||
|
||||
"""
|
||||
def wrap(list) when is_list(list) do
|
||||
list
|
||||
end
|
||||
|
||||
def wrap(nil) do
|
||||
[]
|
||||
end
|
||||
|
||||
def wrap(other) do
|
||||
[other]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Zips corresponding elements from two lists (or tuples) into one list of tuples. The
|
||||
number of elements in the resulting list is equal to the length of the
|
||||
shortest list among the given ones.
|
||||
|
||||
## Examples
|
||||
|
||||
List.zip [1, 2, 3], [4, 5, 6]
|
||||
#=> [{1, 4}, {2, 5}, {3, 6}]
|
||||
|
||||
List.zip [1, 2], [4, 5, 6]
|
||||
#=> [{1, 4}, {2, 5}]
|
||||
|
||||
"""
|
||||
def zip(item1, item2) do
|
||||
do_zip(to_list(item1), to_list(item2), [])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Zips corresponding elements from each list in `list_of_lists`.
|
||||
|
||||
## Examples
|
||||
|
||||
List.zip [[1, 2], [3, 4], [5, 6]]
|
||||
#=> [{1, 3, 5}, {2, 4, 6}]
|
||||
|
||||
List.zip [[1, 2], [3], [5, 6]]
|
||||
#=> [{1, 3, 5}]
|
||||
|
||||
"""
|
||||
def zip(list_of_lists) when is_list(list_of_lists) do
|
||||
do_zip(list_of_lists, [])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unzips the given list of lists or tuples into separate lists and returns a
|
||||
list of lists.
|
||||
|
||||
## Examples
|
||||
|
||||
List.unzip [{1, 2}, {3, 4}]
|
||||
#=> [[1, 3], [2, 4]]
|
||||
|
||||
List.unzip [{1, :a, "apple"}, {2, :b, "banana"}, {3, :c}]
|
||||
#=> [[1, 2, 3], [:a, :b, :c]]
|
||||
|
||||
"""
|
||||
def unzip(list) when is_list(list) do
|
||||
:lists.map tuple_to_list(&1), zip(list)
|
||||
end
|
||||
|
||||
## Private
|
||||
|
||||
# uniq
|
||||
|
||||
defp do_uniq([h|t], acc) do
|
||||
case Erlang.lists.member(h, acc) do
|
||||
true ->
|
||||
do_uniq(t, acc)
|
||||
false ->
|
||||
[h|do_uniq(t, [h|acc])]
|
||||
end
|
||||
end
|
||||
|
||||
defp do_uniq([], _acc) do
|
||||
[]
|
||||
end
|
||||
|
||||
# zip
|
||||
|
||||
defp do_zip([h1|t1], [h2|t2], acc) do
|
||||
do_zip t1, t2, [{h1, h2}|acc]
|
||||
end
|
||||
|
||||
defp do_zip(_, _, acc) do
|
||||
reverse acc
|
||||
end
|
||||
|
||||
defp do_zip(list, acc) do
|
||||
converter = fn x, acc -> do_zip_each(to_list(x), acc) end
|
||||
{mlist, heads} = :lists.mapfoldl converter, [], list
|
||||
|
||||
case heads do
|
||||
nil ->
|
||||
:lists.reverse acc
|
||||
_ ->
|
||||
do_zip mlist, [list_to_tuple(:lists.reverse(heads))|acc]
|
||||
end
|
||||
end
|
||||
|
||||
defp do_zip_each(_, nil) do
|
||||
{ nil, nil }
|
||||
end
|
||||
|
||||
defp do_zip_each([h|t], acc) do
|
||||
{ t, [h|acc] }
|
||||
end
|
||||
|
||||
defp do_zip_each([], _) do
|
||||
{ nil, nil }
|
||||
end
|
||||
|
||||
defp to_list(tuple) when is_tuple(tuple), do: tuple_to_list(tuple)
|
||||
defp to_list(list) when is_list(list), do: list
|
||||
end
|
||||
@@ -1,32 +0,0 @@
|
||||
defprotocol List.Chars do
|
||||
@moduledoc %B"""
|
||||
The List.Chars protocol is responsible for
|
||||
converting a structure to a list (only if applicable).
|
||||
The only function required to be implemented is
|
||||
`to_char_list` which does the conversion.
|
||||
|
||||
The `to_char_list` function automatically imported
|
||||
by Elixir.Builtin invokes this protocol.
|
||||
"""
|
||||
|
||||
@only [BitString, List, Atom, Number, Record]
|
||||
|
||||
def to_char_list(thing)
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: Atom do
|
||||
def to_char_list(atom), do: atom_to_list(atom)
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: BitString do
|
||||
def to_char_list(bitstring), do: bitstring_to_list(bitstring)
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: List do
|
||||
def to_char_list(list), do: list
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: Number do
|
||||
def to_char_list(integer) when is_integer(integer), do: integer_to_list(integer)
|
||||
def to_char_list(float) when is_float(float), do: float_to_list(float)
|
||||
end
|
||||
@@ -1,32 +0,0 @@
|
||||
defmodule Macro do
|
||||
@moduledoc """
|
||||
This module provides conveniences for working with macros.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Recursively escapes the given value so it can be inserted
|
||||
into a syntax tree. Structures that are valid syntax nodes
|
||||
(like atoms, integers, binaries) are represented by themselves.
|
||||
|
||||
## Examples
|
||||
|
||||
Macro.escape(:foo)
|
||||
#=> :foo
|
||||
|
||||
Macro.escape({ :a, :b, :c })
|
||||
#=> { :{}, 0, [:a, :b, :c] }
|
||||
"""
|
||||
def escape({ left, right }) do
|
||||
{ escape(left), escape(right) }
|
||||
end
|
||||
|
||||
def escape(tuple) when is_tuple(tuple) do
|
||||
{ :{}, 0, escape(tuple_to_list(tuple)) }
|
||||
end
|
||||
|
||||
def escape(list) when is_list(list) do
|
||||
lc item in list, do: escape(item)
|
||||
end
|
||||
|
||||
def escape(other), do: other
|
||||
end
|
||||
-470
@@ -1,470 +0,0 @@
|
||||
# We cannot use to_char_list because it depends on inspect,
|
||||
# which depends on protocol, which depends on this module.
|
||||
import Elixir.Builtin, except: [to_char_list: 1]
|
||||
|
||||
defmodule Module do
|
||||
require Erlang.ets, as: ETS
|
||||
|
||||
@moduledoc """
|
||||
This module provides many functions to deal with modules during
|
||||
compilation time. It allows a developer to dynamically attach
|
||||
documentation, merge data, register attributes and so forth.
|
||||
|
||||
After the module is compiled, using many of the functions in
|
||||
this module will raise errors, since it is out of their purpose
|
||||
to inspect runtime data. Most of the runtime data can be inspected
|
||||
via the `__info__(attr)` function attached to each compiled module.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Evalutes the quotes contents in the given module context.
|
||||
Raises an error if the module was already compiled.
|
||||
|
||||
## Options
|
||||
|
||||
This function accepts a list of options. The supported
|
||||
options are:
|
||||
|
||||
* `:file` - The filename to be used in stacktraces
|
||||
or by the __FILE__ macro in case there is an error.
|
||||
|
||||
* `:line` - The line to be used when expanding __LINE__
|
||||
macros. The stacktrace line information is not affected
|
||||
by this option as the line inside each quoted expression
|
||||
is used instead.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Foo do
|
||||
contents = quote do: (def sum(a, b), do: a + b)
|
||||
Module.eval_quoted __MODULE__, contents, [], file: __FILE__, line: __LINE__
|
||||
end
|
||||
|
||||
Foo.sum(1, 2) #=> 3
|
||||
"""
|
||||
def eval_quoted(module, quoted, binding // [], opts // []) do
|
||||
assert_not_compiled!(:eval_quoted, module)
|
||||
|
||||
{ binding, scope } = Erlang.elixir_module.binding_and_scope_for_eval(opts, module, binding)
|
||||
Erlang.elixir_def.reset_last(module)
|
||||
|
||||
line = Keyword.get opts, :line, 1
|
||||
{ value, binding, _scope } = Erlang.elixir.eval_quoted([quoted], binding, line, scope)
|
||||
{ value, binding }
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates the list of arguments and returns the module name.
|
||||
It handles char lists, binaries and atoms.
|
||||
|
||||
## Examples
|
||||
|
||||
Module.concat [Foo, Bar] #=> Foo.Bar
|
||||
Module.concat [Foo, "Bar"] #=> Foo.Bar
|
||||
Module.concat [Foo, 'Bar'] #=> Foo.Bar
|
||||
|
||||
"""
|
||||
def concat(list) when is_list(list) do
|
||||
Erlang.elixir_ref.concat(list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates two arguments and returns the module name.
|
||||
It handles char lists, binaries and atoms.
|
||||
|
||||
## Examples
|
||||
|
||||
Module.concat Foo, Bar #=> Foo.Bar
|
||||
Module.concat Foo, "Bar" #=> Foo.Bar
|
||||
Module.concat Foo, 'Bar' #=> Foo.Bar
|
||||
|
||||
"""
|
||||
def concat(left, right) do
|
||||
Erlang.elixir_ref.concat([left, right])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates the list arguments and returns the module
|
||||
name only if the module was already referenced.
|
||||
If the module was not referenced yet, fails with ArgumentError.
|
||||
It handles char lists, binaries and atoms.
|
||||
|
||||
## Examples
|
||||
|
||||
Module.safe_concat [Unknown, Module]
|
||||
#=> ArgumentError
|
||||
|
||||
"""
|
||||
def safe_concat(list) when is_list(list) do
|
||||
Erlang.elixir_ref.safe_concat(list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates two arguments and returns the module
|
||||
name only if the module was already referenced.
|
||||
If the module was not referenced yet, fails with ArgumentError.
|
||||
It handles char lists, binaries and atoms.
|
||||
|
||||
## Examples
|
||||
|
||||
Module.safe_concat Unknown, Module
|
||||
#=> ArgumentError
|
||||
|
||||
"""
|
||||
def safe_concat(left, right) do
|
||||
Erlang.elixir_ref.safe_concat([left, right])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the module is compiled or not.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Foo do
|
||||
Module.compiled?(__MODULE__) #=> false
|
||||
end
|
||||
|
||||
Module.compiled?(Foo) #=> true
|
||||
|
||||
"""
|
||||
def compiled?(module) do
|
||||
table = data_table_for(module)
|
||||
table == ETS.info(table, :name)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads the data for the given module. This is used
|
||||
to read data of uncompiled modules. If the module
|
||||
was already compiled, you shoul access the data
|
||||
directly by invoking `__info__(:data)` in that module.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Foo do
|
||||
Module.merge_data __MODULE__, value: 1
|
||||
Module.read_data __MODULE__ #=> [value: 1]
|
||||
end
|
||||
|
||||
"""
|
||||
def read_data(module) do
|
||||
assert_not_compiled!(:read_data, module)
|
||||
ETS.lookup_element(data_table_for(module), :data, 2)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads the data from `module` at the given key `at`.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Foo do
|
||||
Module.merge_data __MODULE__, value: 1
|
||||
Module.read_data __MODULE__, :value #=> 1
|
||||
end
|
||||
|
||||
"""
|
||||
def read_data(module, at) do
|
||||
Keyword.get read_data(module), at
|
||||
end
|
||||
|
||||
@doc """
|
||||
Merge the given data into the module, overriding any
|
||||
previous one.
|
||||
|
||||
If any of the given data is a registered attribute, it is
|
||||
automatically added to the attribute set, instead of marking
|
||||
it as data. See register_attribute/2 and add_attribute/3 for
|
||||
more info.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Foo do
|
||||
Module.merge_data __MODULE__, value: 1
|
||||
end
|
||||
|
||||
Foo.__info__(:data) #=> [value: 1]
|
||||
|
||||
"""
|
||||
def merge_data(module, data) do
|
||||
assert_not_compiled!(:merge_data, module)
|
||||
|
||||
table = data_table_for(module)
|
||||
old = ETS.lookup_element(table, :data, 2)
|
||||
registered = ETS.lookup_element(table, :registered_attributes, 2)
|
||||
data = lc kv in data, do: normalize_data(kv)
|
||||
|
||||
{ attrs, new } = :lists.partition fn {k,_} -> List.member?(registered, k) end, data
|
||||
lc {k,v} in attrs, do: add_attribute(module, k, v)
|
||||
ETS.insert(table, { :data, Keyword.merge(old, new) })
|
||||
end
|
||||
|
||||
@doc """
|
||||
Attaches documentation to a given function. It expects
|
||||
the module the function belongs to, the line (a non negative
|
||||
integer), the kind (def or defmacro), a tuple representing
|
||||
the function and its arity and the documentation, which should
|
||||
be either a binary or a boolean.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyModule do
|
||||
Module.add_doc(__MODULE__, __LINE__ + 1, :def, { :version, 0 }, "Manually added docs")
|
||||
def version, do: 1
|
||||
end
|
||||
|
||||
"""
|
||||
def add_doc(_module, _line, kind, _tuple, nil) when kind in [:defp, :defmacrop] do
|
||||
:ok
|
||||
end
|
||||
|
||||
def add_doc(_module, _line, kind, _tuple, _doc) when kind in [:defp, :defmacrop] do
|
||||
{ :error, :private_doc }
|
||||
end
|
||||
|
||||
def add_doc(module, line, kind, tuple, doc) when
|
||||
is_binary(doc) or is_boolean(doc) or doc == nil do
|
||||
assert_not_compiled!(:add_doc, module)
|
||||
table = docs_table_for(module)
|
||||
|
||||
case { ETS.lookup(table, tuple), doc } do
|
||||
{ [], _ } ->
|
||||
ETS.insert(table, { tuple, line, kind, doc })
|
||||
:ok
|
||||
{ _, nil } ->
|
||||
:ok
|
||||
_ ->
|
||||
{ :error, :existing_doc }
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a function was defined, regardless if it is
|
||||
a macro or a private function. Use function_defined?/3
|
||||
to assert for an specific type.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Example do
|
||||
Module.function_defined? __MODULE__, { :version, 0 } #=> false
|
||||
def version, do: 1
|
||||
Module.function_defined? __MODULE__, { :version, 0 } #=> true
|
||||
end
|
||||
|
||||
"""
|
||||
def function_defined?(module, tuple) when is_tuple(tuple) do
|
||||
assert_not_compiled!(:function_defined?, module)
|
||||
table = function_table_for(module)
|
||||
ETS.lookup(table, tuple) != []
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a function was defined and also for its `kind`.
|
||||
`kind` can be either :def, :defp or :defmacro.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Example do
|
||||
Module.function_defined? __MODULE__, { :version, 0 }, :defp #=> false
|
||||
def version, do: 1
|
||||
Module.function_defined? __MODULE__, { :version, 0 }, :defp #=> false
|
||||
end
|
||||
|
||||
"""
|
||||
def function_defined?(module, tuple, kind) do
|
||||
List.member? defined_functions(module, kind), tuple
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return all functions defined in the given module.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Example do
|
||||
def version, do: 1
|
||||
Module.defined_functions __MODULE__ #=> [{:version,1}]
|
||||
end
|
||||
|
||||
"""
|
||||
def defined_functions(module) do
|
||||
assert_not_compiled!(:defined_functions, module)
|
||||
table = function_table_for(module)
|
||||
lc { tuple, _, _, _, _, _, _, _ } in ETS.tab2list(table), do: tuple
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all functions defined in te given module according
|
||||
to its kind.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Example do
|
||||
def version, do: 1
|
||||
Module.defined_functions __MODULE__, :def #=> [{:version,1}]
|
||||
Module.defined_functions __MODULE__, :defp #=> []
|
||||
end
|
||||
|
||||
"""
|
||||
def defined_functions(module, kind) do
|
||||
assert_not_compiled!(:defined_functions, module)
|
||||
table = function_table_for(module)
|
||||
lc { tuple, stored_kind, _, _, _, _, _, _ } in ETS.tab2list(table) when stored_kind == kind, do: tuple
|
||||
end
|
||||
|
||||
@doc """
|
||||
Makes the given functions in the given module overridable.
|
||||
An overridable function is lazily defined, allowing a
|
||||
developer to customize it.
|
||||
"""
|
||||
def make_overridable(module, tuples) do
|
||||
assert_not_compiled!(:make_overridable, module)
|
||||
table = function_table_for(module)
|
||||
lc tuple in tuples do
|
||||
case ETS.lookup(table, tuple) do
|
||||
[clause] ->
|
||||
ETS.delete(table, tuple)
|
||||
Erlang.elixir_def_overridable.define(module, tuple, clause)
|
||||
_ ->
|
||||
{ name, arity } = tuple
|
||||
raise "Cannot make function #{name}/#{arity} overridable because it was not defined"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a compilation callback hook that is invoked
|
||||
exactly before the module is compiled.
|
||||
|
||||
This callback is useful, for example, when used with `use`
|
||||
as a mechanism to clean up any internal data in the module
|
||||
before it is compiled.
|
||||
|
||||
## Examples
|
||||
|
||||
Imagine you are creating a module/library that is meant for
|
||||
external usage called `MyLib`. It could be defined as:
|
||||
|
||||
defmodule MyLib do
|
||||
def __using__(target) do
|
||||
Module.merge_data target, some_data: nil
|
||||
Module.add_compile_callback(target, __MODULE__, :__callback__)
|
||||
end
|
||||
|
||||
defmacro __callback__(target) do
|
||||
value = Module.read_data(target, :some_data)
|
||||
quote do: (def my_lib_value, do: unquote(value))
|
||||
end
|
||||
end
|
||||
|
||||
And a module could use `MyLib` with:
|
||||
|
||||
defmodule App do
|
||||
use ModuleTest.ToBeUsed
|
||||
@some_data :new_value
|
||||
end
|
||||
|
||||
In the example above, `MyLib` defines a data on the target.
|
||||
This data can be updated throughout the module definition
|
||||
and therefore, the final value of the data can only be retrieved
|
||||
via the compilation callback.
|
||||
|
||||
In this example, the compilation callback reads the value and
|
||||
compile it to a function.
|
||||
"""
|
||||
def add_compile_callback(module, target, fun // :__compiling__) do
|
||||
assert_not_compiled!(:add_compile_callback, module)
|
||||
new = { target, fun }
|
||||
table = data_table_for(module)
|
||||
old = ETS.lookup_element(table, :compile_callbacks, 2)
|
||||
ETS.insert(table, { :compile_callbacks, [new|old] })
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an Erlang attribute to the given module with the given
|
||||
key and value. The same attribute can be added more than once.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyModule do
|
||||
Module.add_attribute __MODULE__, :custom_threshold_for_lib, 10
|
||||
end
|
||||
|
||||
"""
|
||||
def add_attribute(module, key, value) when is_atom(key) do
|
||||
assert_not_compiled!(:add_attribute, module)
|
||||
table = data_table_for(module)
|
||||
attrs = ETS.lookup_element(table, :attributes, 2)
|
||||
ETS.insert(table, { :attributes, [{key, value}|attrs] })
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes all attributes that matches the given key.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyModule do
|
||||
Module.add_attribute __MODULE__, :custom_threshold_for_lib, 10
|
||||
Module.delete_attribute __MODULE__, :custom_threshold_for_lib
|
||||
end
|
||||
|
||||
"""
|
||||
def delete_attribute(module, key) when is_atom(key) do
|
||||
assert_not_compiled!(:delete_attribute, module)
|
||||
table = data_table_for(module)
|
||||
attrs = ETS.lookup_element(table, :attributes, 2)
|
||||
final = lc {k,v} in attrs when k != key, do: {k,v}
|
||||
ETS.insert(table, { :attributes, final })
|
||||
end
|
||||
|
||||
@doc """
|
||||
Registers an attribute. This allows a developer to use the data API
|
||||
but Elixir will register the data as an attribute automatically.
|
||||
By default, `vsn`, `behavior` and other Erlang attributes are
|
||||
automatically registered.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyModule do
|
||||
Module.register_attribute __MODULE__, :custom_threshold_for_lib
|
||||
@custom_threshold_for_lib 10
|
||||
end
|
||||
|
||||
"""
|
||||
def register_attribute(module, new) do
|
||||
assert_not_compiled!(:register_attribute, module)
|
||||
table = data_table_for(module)
|
||||
old = ETS.lookup_element(table, :registered_attributes, 2)
|
||||
ETS.insert(table, { :registered_attributes, [new|old] })
|
||||
end
|
||||
|
||||
@doc false
|
||||
# Used internally to compile documentation. This function
|
||||
# is private and must be used only internally.
|
||||
def compile_doc(module, line, kind, pair) do
|
||||
doc = read_data(module, :doc)
|
||||
result = add_doc(module, line, kind, pair, doc)
|
||||
merge_data(module, doc: nil)
|
||||
result
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp normalize_data({ :on_load, atom }) when is_atom(atom), do: { :on_load, { atom, 0 } }
|
||||
defp normalize_data(other), do: other
|
||||
|
||||
defp data_table_for(module) do
|
||||
list_to_atom Erlang.lists.concat([:d, module])
|
||||
end
|
||||
|
||||
defp function_table_for(module) do
|
||||
list_to_atom Erlang.lists.concat([:f, module])
|
||||
end
|
||||
|
||||
defp docs_table_for(module) do
|
||||
list_to_atom Erlang.lists.concat([:o, module])
|
||||
end
|
||||
|
||||
defp assert_not_compiled!(fun, module) do
|
||||
compiled?(module) ||
|
||||
raise ArgumentError,
|
||||
message: "could not call #{fun} on module #{inspect module} because it was already compiled"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,124 @@
|
||||
defmodule N8nOpenaiAdapter.AgentRegistry do
|
||||
@moduledoc """
|
||||
GenServer holding the map of `model name -> n8n chat webhook URL`, persisted
|
||||
to a JSON file so agents survive restarts and can be managed at runtime via
|
||||
the admin API (no redeploy needed to add an agent).
|
||||
|
||||
Load order at startup:
|
||||
1. If the agents file exists, load it (authoritative — admin edits persist).
|
||||
2. Else seed from the `AGENTS` env var (JSON map) or the legacy
|
||||
`N8N_WEBHOOK_URL`/`MODEL_NAME` pair, then write the file.
|
||||
|
||||
The file path comes from `AGENTS_FILE` (default `/var/lib/n8n-openai/agents.json`).
|
||||
"""
|
||||
use GenServer
|
||||
|
||||
@default_file "/var/lib/n8n-openai/agents.json"
|
||||
|
||||
# --- Client API ---
|
||||
|
||||
def start_link(initial_agents) do
|
||||
GenServer.start_link(__MODULE__, initial_agents, name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc "Return the webhook URL for a model, or :error if unknown."
|
||||
def webhook_for(model) do
|
||||
GenServer.call(__MODULE__, {:webhook_for, model})
|
||||
end
|
||||
|
||||
@doc "Return all known model names."
|
||||
def model_names do
|
||||
GenServer.call(__MODULE__, :model_names)
|
||||
end
|
||||
|
||||
@doc "Return the full model -> webhook map."
|
||||
def all do
|
||||
GenServer.call(__MODULE__, :all)
|
||||
end
|
||||
|
||||
@doc "Add or update an agent (model -> webhook). Persists to disk."
|
||||
def put(model, webhook) do
|
||||
GenServer.call(__MODULE__, {:put, model, webhook})
|
||||
end
|
||||
|
||||
@doc "Remove an agent by model. Returns :ok or :not_found. Persists to disk."
|
||||
def delete(model) do
|
||||
GenServer.call(__MODULE__, {:delete, model})
|
||||
end
|
||||
|
||||
# --- Server callbacks ---
|
||||
|
||||
@impl true
|
||||
def init(seed) do
|
||||
file = file_path()
|
||||
agents = load_or_seed(file, seed)
|
||||
{:ok, %{agents: agents, file: file}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:webhook_for, model}, _from, state) do
|
||||
{:reply, Map.get(state.agents, model, :error), state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:model_names, _from, state) do
|
||||
{:reply, Map.keys(state.agents), state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:all, _from, state) do
|
||||
{:reply, state.agents, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:put, model, webhook}, _from, state) do
|
||||
agents = Map.put(state.agents, model, webhook)
|
||||
persist(state.file, agents)
|
||||
{:reply, :ok, %{state | agents: agents}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:delete, model}, _from, state) do
|
||||
case Map.pop(state.agents, model) do
|
||||
{nil, _} ->
|
||||
{:reply, :not_found, state}
|
||||
|
||||
{_old, agents} ->
|
||||
persist(state.file, agents)
|
||||
{:reply, :ok, %{state | agents: agents}}
|
||||
end
|
||||
end
|
||||
|
||||
# --- Helpers ---
|
||||
|
||||
defp file_path, do: System.get_env("AGENTS_FILE", @default_file)
|
||||
|
||||
defp load_or_seed(file, seed) do
|
||||
case File.read(file) do
|
||||
{:ok, contents} ->
|
||||
case Jason.decode(contents) do
|
||||
{:ok, map} when is_map(map) -> map
|
||||
_ -> seed_and_write(file, seed)
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
seed_and_write(file, seed)
|
||||
end
|
||||
end
|
||||
|
||||
defp seed_and_write(file, seed) do
|
||||
agents = normalize_seed(seed)
|
||||
persist(file, agents)
|
||||
agents
|
||||
end
|
||||
|
||||
defp normalize_seed(seed) when is_map(seed), do: seed
|
||||
|
||||
defp normalize_seed(_), do: %{}
|
||||
|
||||
defp persist(file, agents) do
|
||||
dir = Path.dirname(file)
|
||||
File.mkdir_p!(dir)
|
||||
File.write!(file, Jason.encode!(agents, pretty: true))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
defmodule N8nOpenaiAdapter.Application do
|
||||
@moduledoc """
|
||||
OTP application entrypoint. Starts the AgentRegistry (model -> n8n webhook
|
||||
mapping, loaded from config) and the Bandit HTTP server behind the Plug router.
|
||||
"""
|
||||
use Application
|
||||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
{N8nOpenaiAdapter.AgentRegistry, configured_agents()},
|
||||
{Bandit, plug: N8nOpenaiAdapter.Router, scheme: :http, port: port()}
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: N8nOpenaiAdapter.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
|
||||
defp port, do: String.to_integer(System.get_env("PORT", "8000"))
|
||||
|
||||
# Agents are managed at runtime via the admin API and persisted to AGENTS_FILE.
|
||||
# We start with an empty store (no AGENTS env seeding) — the web admin is the
|
||||
# single source of truth for which agents are exposed.
|
||||
defp configured_agents, do: %{}
|
||||
end
|
||||
@@ -0,0 +1,468 @@
|
||||
defmodule N8nOpenaiAdapter.Router do
|
||||
@moduledoc """
|
||||
OpenAI-compatible HTTP surface for n8n chat agents.
|
||||
|
||||
GET /v1/models -> lists configured agents (OpenAI shape)
|
||||
POST /v1/chat/completions -> {model, messages} -> forwards last user msg
|
||||
to the agent's n8n webhook, returns an
|
||||
OpenAI chat.completion response.
|
||||
|
||||
Auth: clients send `Authorization: Bearer <ADAPTER_API_KEY>`. The webhook
|
||||
call to n8n can carry its own basic auth via `CHAT_WEBHOOK_BASIC="user:pass"`.
|
||||
|
||||
Plug order matters: `match` -> JSON parser (consumes body) -> `dispatch`.
|
||||
Parsed JSON lands in `conn.body_params`, so handlers read body_params, not
|
||||
a manual body read.
|
||||
"""
|
||||
use Plug.Router
|
||||
|
||||
alias N8nOpenaiAdapter.AgentRegistry
|
||||
|
||||
plug(:match)
|
||||
plug(Plug.Parsers, parsers: [:json], json_decoder: Jason)
|
||||
plug(:dispatch)
|
||||
|
||||
# --- Helpers ---
|
||||
|
||||
# Ensure the request carries a valid ADAPTER_API_KEY bearer token.
|
||||
defp authorize!(conn) do
|
||||
expected = System.get_env("ADAPTER_API_KEY")
|
||||
|
||||
case get_req_header(conn, "authorization") do
|
||||
[auth] ->
|
||||
token =
|
||||
case Regex.run(~r/^Bearer\s+(.+)$/i, auth) do
|
||||
[_, t] -> t
|
||||
_ -> auth
|
||||
end
|
||||
|
||||
if expected && Plug.Crypto.secure_compare(token, expected) do
|
||||
conn
|
||||
else
|
||||
{:halt, send_resp(conn, 401, Jason.encode!(%{error: %{message: "Invalid API key"}}))}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:halt,
|
||||
send_resp(conn, 401, Jason.encode!(%{error: %{message: "Missing Authorization header"}}))}
|
||||
end
|
||||
end
|
||||
|
||||
# Admin endpoints use a SEPARATE ADMIN_API_KEY so you can grant agent
|
||||
# management without exposing the OpenAI-facing key.
|
||||
defp authorize_admin!(conn) do
|
||||
expected = System.get_env("ADMIN_API_KEY")
|
||||
|
||||
case get_req_header(conn, "authorization") do
|
||||
[auth] ->
|
||||
token =
|
||||
case Regex.run(~r/^Bearer\s+(.+)$/i, auth) do
|
||||
[_, t] -> t
|
||||
_ -> auth
|
||||
end
|
||||
|
||||
if expected && Plug.Crypto.secure_compare(token, expected) do
|
||||
conn
|
||||
else
|
||||
{:halt, send_resp(conn, 401, Jason.encode!(%{error: %{message: "Invalid admin key"}}))}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:halt,
|
||||
send_resp(conn, 401, Jason.encode!(%{error: %{message: "Missing Authorization header"}}))}
|
||||
end
|
||||
end
|
||||
|
||||
defp json(conn, status, body) do
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(status, Jason.encode!(body))
|
||||
end
|
||||
|
||||
defp error(conn, status, message) do
|
||||
json(conn, status, %{error: %{message: message}})
|
||||
end
|
||||
|
||||
# Pull the last user message content out of an OpenAI messages array.
|
||||
defp last_user_text(messages) do
|
||||
messages
|
||||
|> Enum.reverse()
|
||||
|> Enum.find_value(fn
|
||||
%{"role" => "user", "content" => c} when is_binary(c) -> c
|
||||
_ -> nil
|
||||
end)
|
||||
end
|
||||
|
||||
# Forward a message to the n8n chat webhook and return the assistant reply.
|
||||
defp call_n8n(webhook, session_id, chat_input) do
|
||||
body = %{sessionId: session_id, action: "sendMessage", chatInput: chat_input}
|
||||
|
||||
headers =
|
||||
case System.get_env("CHAT_WEBHOOK_BASIC") do
|
||||
nil ->
|
||||
[{"content-type", "application/json"}]
|
||||
|
||||
basic ->
|
||||
[
|
||||
{"content-type", "application/json"},
|
||||
{"authorization", "Basic " <> Base.encode64(basic)}
|
||||
]
|
||||
end
|
||||
|
||||
case Req.post(webhook, json: body, headers: headers, receive_timeout: 120_000) do
|
||||
{:ok, %{status: 200, body: %{"output" => output}}} -> {:ok, output}
|
||||
{:ok, %{status: s}} -> {:error, "n8n webhook returned HTTP #{s}"}
|
||||
{:error, e} -> {:error, "n8n webhook error: #{Exception.message(e)}"}
|
||||
end
|
||||
end
|
||||
|
||||
# --- Routes ---
|
||||
|
||||
get "/v1/models" do
|
||||
case authorize!(conn) do
|
||||
{:halt, conn} ->
|
||||
conn
|
||||
|
||||
conn ->
|
||||
models =
|
||||
Enum.map(AgentRegistry.model_names(), fn name ->
|
||||
%{id: name, object: "model", created: 0, owned_by: "n8n"}
|
||||
end)
|
||||
|
||||
json(conn, 200, %{object: "list", data: models})
|
||||
end
|
||||
end
|
||||
|
||||
post "/v1/chat/completions" do
|
||||
case authorize!(conn) do
|
||||
{:halt, conn} ->
|
||||
conn
|
||||
|
||||
conn ->
|
||||
handle_chat(conn)
|
||||
end
|
||||
end
|
||||
|
||||
# --- Admin API (separate ADMIN_API_KEY) ---
|
||||
|
||||
# The admin page itself is served WITHOUT the Bearer auth gate — it's a static
|
||||
# form. The user enters their ADMIN_API_KEY in the page, and that key drives
|
||||
# the auth'd /admin/agents CRUD calls. Serving the page openly is harmless (it
|
||||
# exposes no data; the agent list is only fetched with a valid key).
|
||||
get "/admin" do
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, admin_page())
|
||||
end
|
||||
|
||||
get "/admin/agents" do
|
||||
case authorize_admin!(conn) do
|
||||
{:halt, conn} ->
|
||||
conn
|
||||
|
||||
conn ->
|
||||
json(conn, 200, %{agents: AgentRegistry.all()})
|
||||
end
|
||||
end
|
||||
|
||||
post "/admin/agents" do
|
||||
case authorize_admin!(conn) do
|
||||
{:halt, conn} ->
|
||||
conn
|
||||
|
||||
conn ->
|
||||
data = conn.body_params
|
||||
model = Map.get(data, "model")
|
||||
webhook = Map.get(data, "webhook")
|
||||
|
||||
cond do
|
||||
not is_binary(model) or model == "" ->
|
||||
error(conn, 400, "Bad request: missing or invalid \"model\"")
|
||||
|
||||
not is_binary(webhook) or webhook == "" ->
|
||||
error(conn, 400, "Bad request: missing or invalid \"webhook\"")
|
||||
|
||||
true ->
|
||||
AgentRegistry.put(model, webhook)
|
||||
json(conn, 200, %{ok: true, model: model, webhook: webhook})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
delete "/admin/agents/:model" do
|
||||
case authorize_admin!(conn) do
|
||||
{:halt, conn} ->
|
||||
conn
|
||||
|
||||
conn ->
|
||||
case AgentRegistry.delete(conn.params["model"]) do
|
||||
:ok -> json(conn, 200, %{ok: true})
|
||||
:not_found -> error(conn, 404, "Unknown model")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
match _ do
|
||||
case authorize!(conn) do
|
||||
{:halt, conn} ->
|
||||
conn
|
||||
|
||||
conn ->
|
||||
json(conn, 404, %{error: %{message: "Not found"}})
|
||||
end
|
||||
end
|
||||
|
||||
# Temporary diagnostics: log the raw chat request (stream flag + headers) so
|
||||
# we can see exactly what SwiftChat sends. Remove after diagnosis.
|
||||
defp log_request(conn, data) do
|
||||
stream = Map.get(data, "stream")
|
||||
accept = get_req_header(conn, "accept") |> List.first()
|
||||
content_type = get_req_header(conn, "content-type") |> List.first()
|
||||
model = Map.get(data, "model")
|
||||
|
||||
IO.puts(
|
||||
"CHAT_REQ model=#{inspect(model)} stream=#{inspect(stream)} " <>
|
||||
"accept=#{inspect(accept)} content_type=#{inspect(content_type)} " <>
|
||||
"body=#{Jason.encode!(data) |> String.slice(0, 500)}"
|
||||
)
|
||||
end
|
||||
|
||||
defp handle_chat(conn) do
|
||||
data = conn.body_params
|
||||
# Temporary diagnostics: log the raw chat request so we can see exactly what
|
||||
# SwiftChat sends (stream flag, headers). Remove after diagnosis.
|
||||
log_request(conn, data)
|
||||
|
||||
model = Map.get(data, "model")
|
||||
messages = Map.get(data, "messages", [])
|
||||
stream? = Map.get(data, "stream", false)
|
||||
thread_id = Map.get(data, "thread_id")
|
||||
session_id = if is_binary(thread_id) and thread_id != "", do: thread_id, else: "default"
|
||||
|
||||
cond do
|
||||
not is_binary(model) or model == "" ->
|
||||
error(conn, 400, "Bad request: missing or invalid \"model\"")
|
||||
|
||||
not is_list(messages) ->
|
||||
error(conn, 400, "Bad request: expected \"messages\" array")
|
||||
|
||||
true ->
|
||||
case last_user_text(messages) do
|
||||
nil ->
|
||||
error(conn, 400, "No user message in request")
|
||||
|
||||
chat_input ->
|
||||
case AgentRegistry.webhook_for(model) do
|
||||
:error ->
|
||||
error(conn, 400, "Unknown model: #{model}")
|
||||
|
||||
webhook ->
|
||||
case call_n8n(webhook, session_id, chat_input) do
|
||||
{:ok, output} ->
|
||||
if stream? do
|
||||
stream_completion(conn, model, output)
|
||||
else
|
||||
json(conn, 200, completion_body(model, output))
|
||||
end
|
||||
|
||||
{:error, msg} ->
|
||||
if stream? do
|
||||
stream_error(conn, msg)
|
||||
else
|
||||
error(conn, 502, msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Build a single, non-streaming chat.completion body.
|
||||
defp completion_body(model, output) do
|
||||
%{
|
||||
id: "chatcmpl-#{Base.encode16(:crypto.strong_rand_bytes(12), case: :lower)}",
|
||||
object: "chat.completion",
|
||||
created: System.system_time(:second),
|
||||
model: model,
|
||||
choices: [
|
||||
%{
|
||||
index: 0,
|
||||
message: %{role: "assistant", content: output},
|
||||
finish_reason: "stop"
|
||||
}
|
||||
],
|
||||
usage: %{prompt_tokens: 0, completion_tokens: 0, total_tokens: 0}
|
||||
}
|
||||
end
|
||||
|
||||
# Emit an OpenAI-compatible SSE stream. The n8n reply arrives whole, so we
|
||||
# deliver it as a single delta chunk (role chunk first, then content) and
|
||||
# terminate with `data: [DONE]`. OpenAI SDKs that request `stream: true`
|
||||
# consume this shape; without it they render an empty response.
|
||||
defp stream_completion(conn, model, output) do
|
||||
id = "chatcmpl-#{Base.encode16(:crypto.strong_rand_bytes(12), case: :lower)}"
|
||||
created = System.system_time(:second)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_resp_content_type("text/event-stream")
|
||||
|> put_resp_header("cache-control", "no-cache")
|
||||
|> send_chunked(200)
|
||||
|
||||
chunks = [
|
||||
%{
|
||||
id: id,
|
||||
object: "chat.completion.chunk",
|
||||
created: created,
|
||||
model: model,
|
||||
choices: [%{index: 0, delta: %{role: "assistant"}, finish_reason: nil}]
|
||||
},
|
||||
%{
|
||||
id: id,
|
||||
object: "chat.completion.chunk",
|
||||
created: created,
|
||||
model: model,
|
||||
choices: [%{index: 0, delta: %{content: output}, finish_reason: nil}]
|
||||
},
|
||||
%{
|
||||
id: id,
|
||||
object: "chat.completion.chunk",
|
||||
created: created,
|
||||
model: model,
|
||||
choices: [%{index: 0, delta: %{}, finish_reason: "stop"}]
|
||||
}
|
||||
]
|
||||
|
||||
{:ok, conn} =
|
||||
Enum.reduce_while(chunks, {:ok, conn}, fn chunk, {:ok, conn} ->
|
||||
case chunk(conn, "data: #{Jason.encode!(chunk)}\n\n") do
|
||||
{:ok, conn} -> {:cont, {:ok, conn}}
|
||||
{:error, _} -> {:halt, {:ok, conn}}
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, conn} = chunk(conn, "data: [DONE]\n\n")
|
||||
conn
|
||||
end
|
||||
|
||||
# Emit an SSE-formatted error so a streaming client sees the failure.
|
||||
defp stream_error(conn, msg) do
|
||||
conn =
|
||||
conn
|
||||
|> put_resp_content_type("text/event-stream")
|
||||
|> put_resp_header("cache-control", "no-cache")
|
||||
|> send_chunked(502)
|
||||
|
||||
payload = %{error: %{message: msg}}
|
||||
{:ok, conn} = chunk(conn, "data: #{Jason.encode!(payload)}\n\n")
|
||||
{:ok, conn} = chunk(conn, "data: [DONE]\n\n")
|
||||
conn
|
||||
end
|
||||
|
||||
# Self-contained admin page: lists agents and lets you add/remove them via
|
||||
# the admin API. Uses the ADMIN_API_KEY (entered in the page) as the Bearer
|
||||
# token for the /admin/agents calls.
|
||||
defp admin_page do
|
||||
"""
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>n8n OpenAI Adapter — Agents</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; }
|
||||
h1 { font-size: 1.4rem; }
|
||||
input[type=text], input[type=password] { width: 100%; padding: .5rem; margin: .25rem 0 .75rem; box-sizing: border-box; }
|
||||
button { padding: .5rem 1rem; cursor: pointer; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
|
||||
th, td { text-align: left; padding: .5rem; border-bottom: 1px solid #ddd; }
|
||||
.msg { margin-top: 1rem; padding: .5rem; border-radius: 4px; }
|
||||
.ok { background: #e6f4ea; color: #1e7e34; }
|
||||
.err { background: #fdecea; color: #c62828; }
|
||||
.del { color: #c62828; background: none; border: none; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>n8n OpenAI Adapter — Agents</h1>
|
||||
<p>Manage which n8n chat agents are exposed as OpenAI models.</p>
|
||||
|
||||
<label for="key">Admin API key</label>
|
||||
<input type="password" id="key" placeholder="ADMIN_API_KEY" autocomplete="off">
|
||||
<button onclick="loadAgents()">Load agents</button>
|
||||
|
||||
<h2>Add / update agent</h2>
|
||||
<label for="model">Model name</label>
|
||||
<input type="text" id="model" placeholder="e.g. scholar-agent">
|
||||
<label for="webhook">n8n chat webhook URL</label>
|
||||
<input type="text" id="webhook" placeholder="https://n8n.bueso.eu/webhook/<id>/chat">
|
||||
<button onclick="saveAgent()">Save agent</button>
|
||||
|
||||
<div id="msg"></div>
|
||||
|
||||
<h2>Configured agents</h2>
|
||||
<table id="agents"><thead><tr><th>Model</th><th>Webhook</th><th></th></tr></thead><tbody></tbody></table>
|
||||
|
||||
<script>
|
||||
const key = () => document.getElementById('key').value.trim();
|
||||
const msg = (text, ok) => {
|
||||
const el = document.getElementById('msg');
|
||||
el.className = 'msg ' + (ok ? 'ok' : 'err');
|
||||
el.textContent = text;
|
||||
};
|
||||
const auth = () => ({ 'Authorization': 'Bearer ' + key(), 'Content-Type': 'application/json' });
|
||||
|
||||
async function loadAgents() {
|
||||
if (!key()) return msg('Enter the admin API key first', false);
|
||||
try {
|
||||
const r = await fetch('/admin/agents', { headers: auth() });
|
||||
if (!r.ok) return msg('Failed to load agents: HTTP ' + r.status, false);
|
||||
const data = await r.json();
|
||||
const tbody = document.querySelector('#agents tbody');
|
||||
tbody.innerHTML = '';
|
||||
for (const [model, webhook] of Object.entries(data.agents)) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = '<td>' + model + '</td><td>' + webhook + '</td>' +
|
||||
'<td><button class="del" onclick="deleteAgent(\\'' + model + '\\')">Delete</button></td>';
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
msg('Loaded ' + Object.keys(data.agents).length + ' agent(s)', true);
|
||||
} catch (e) { msg('Error: ' + e, false); }
|
||||
}
|
||||
|
||||
async function saveAgent() {
|
||||
const model = document.getElementById('model').value.trim();
|
||||
const webhook = document.getElementById('webhook').value.trim();
|
||||
if (!key()) return msg('Enter the admin API key first', false);
|
||||
if (!model || !webhook) return msg('Model and webhook are required', false);
|
||||
try {
|
||||
const r = await fetch('/admin/agents', {
|
||||
method: 'POST', headers: auth(),
|
||||
body: JSON.stringify({ model, webhook })
|
||||
});
|
||||
if (!r.ok) return msg('Failed to save agent: HTTP ' + r.status, false);
|
||||
document.getElementById('model').value = '';
|
||||
document.getElementById('webhook').value = '';
|
||||
msg('Saved agent "' + model + '"', true);
|
||||
loadAgents();
|
||||
} catch (e) { msg('Error: ' + e, false); }
|
||||
}
|
||||
|
||||
async function deleteAgent(model) {
|
||||
if (!key()) return msg('Enter the admin API key first', false);
|
||||
if (!confirm('Delete agent "' + model + '"?')) return;
|
||||
try {
|
||||
const r = await fetch('/admin/agents/' + encodeURIComponent(model), {
|
||||
method: 'DELETE', headers: auth()
|
||||
});
|
||||
if (!r.ok) return msg('Failed to delete: HTTP ' + r.status, false);
|
||||
msg('Deleted agent "' + model + '"', true);
|
||||
loadAgents();
|
||||
} catch (e) { msg('Error: ' + e, false); }
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
end
|
||||
end
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
defmodule Node do
|
||||
@moduledoc """
|
||||
Functions related to Erlang nodes.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns true if the local node is alive; that is, if the node can be
|
||||
part of a distributed system. Otherwise, it returns false.
|
||||
"""
|
||||
def alive? do
|
||||
:erlang.is_alive()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of all visible nodes in the system, excluding
|
||||
the local node. Same as list(visible).
|
||||
"""
|
||||
def list do
|
||||
:erlang.nodes()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of nodes according to argument given. The result
|
||||
returned when the argument is a list, is the list of nodes
|
||||
satisfying the disjunction(s) of the list elements.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#nodes-1 for more info.
|
||||
"""
|
||||
def list(args) do
|
||||
:erlang.nodes(args)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Monitors the status of the node. If flag is true, monitoring is
|
||||
turned on. If flag is false, monitoring is turned off.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#monitor_node-2 for more info.
|
||||
"""
|
||||
def monitor(node, flag) do
|
||||
:erlang.monitor_node(node, flag)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Behaves as monitor_node/2 except that it allows an extra
|
||||
option to be given, namely :allow_passive_connect.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#monitor_node-3 for more info.
|
||||
"""
|
||||
def monitor(node, flag, options) do
|
||||
:erlang.monitor_node(node, flag, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Forces the disconnection of a node. This will appear to the `node` as if
|
||||
the local node has crashed. This BIF is mainly used in the Erlang network
|
||||
authentication protocols. Returns true if disconnection succeeds, otherwise
|
||||
false. If the local node is not alive, the function returns ignored.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#disconnect_node-1 for more info.
|
||||
"""
|
||||
def disconnect(node) do
|
||||
:erlang.disconnect_node(node)
|
||||
end
|
||||
end
|
||||
@@ -1,68 +0,0 @@
|
||||
defmodule OptionParser.Simple do
|
||||
@doc """
|
||||
Parses the argv and returns one tuple with parsed options
|
||||
and the arguments.
|
||||
|
||||
## Example
|
||||
|
||||
OptionParser.Simple.parse(["--debug"])
|
||||
#=> { [debug: true], [] }
|
||||
|
||||
OptionParser.Simple.parse(["--source", "lib"])
|
||||
#=> { [source: "lib"], [] }
|
||||
|
||||
OptionParser.Simple.parse(["--source", "lib", "test/enum_test.exs"])
|
||||
#=> { [source: "lib"], ["test/enum_test.exs"] }
|
||||
|
||||
A set of aliases can be given as second argument:
|
||||
|
||||
OptionParser.Simple.parse(["-d"], [d: :debug])
|
||||
#=> { [debug: true], [] }
|
||||
|
||||
"""
|
||||
def parse(options, aliases // []) when is_list(options) and is_list(aliases) do
|
||||
parse(options, aliases, [], [])
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp parse([<<?-, option|:binary>>, h|t], aliases, dict, args) do
|
||||
option = normalize_option(option, aliases)
|
||||
|
||||
case h do
|
||||
<<?-, _|:binary>> ->
|
||||
dict = Keyword.put dict, option, true
|
||||
parse([h|t], aliases, dict, args)
|
||||
_ ->
|
||||
dict = key_value(option, h, dict)
|
||||
parse(t, aliases, dict, args)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse([<<?-, option|:binary>>], aliases, dict, args) do
|
||||
option = normalize_option(option, aliases)
|
||||
dict = Keyword.put dict, option, true
|
||||
{ dict, args }
|
||||
end
|
||||
|
||||
defp parse(value, _, dict, args) do
|
||||
{ dict, List.concat(args, value) }
|
||||
end
|
||||
|
||||
defp key_value(key, boolean, dict) when boolean in ["false", "true"] do
|
||||
Keyword.put dict, key, binary_to_atom(boolean)
|
||||
end
|
||||
|
||||
defp key_value(key, value, dict) do
|
||||
Keyword.put dict, key, value
|
||||
end
|
||||
|
||||
defp normalize_option(<<?-, option|:binary>>, aliases) do
|
||||
normalize_option(option, aliases)
|
||||
end
|
||||
|
||||
defp normalize_option(option, aliases) do
|
||||
option = binary_to_atom(option)
|
||||
aliases[option] || option
|
||||
end
|
||||
end
|
||||
@@ -1,72 +0,0 @@
|
||||
defrecord Orddict.Record, data: nil
|
||||
|
||||
defimpl Dict, for: Orddict.Record do
|
||||
refer Orddict.Record, as: O
|
||||
|
||||
def keys(O[data: data]) do
|
||||
lc { k, _ } in data, do: k
|
||||
end
|
||||
|
||||
def values(O[data: data]) do
|
||||
lc { _, v } in data, do: v
|
||||
end
|
||||
|
||||
def size(O[data: data]) do
|
||||
length(data)
|
||||
end
|
||||
|
||||
def has_key?(O[data: data], key) do
|
||||
:orddict.is_key key, data
|
||||
end
|
||||
|
||||
def get(O[data: data], key, default // nil) do
|
||||
case :orddict.find(key, data) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
:error ->
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def put(O[data: data], key, value) do
|
||||
O[data: :orddict.store key, value, data]
|
||||
end
|
||||
|
||||
def delete(O[data: data], key) do
|
||||
O[data: :orddict.erase key, data]
|
||||
end
|
||||
|
||||
def merge(O[data: d1], O[data: d2]) do
|
||||
O[data: :orddict.merge fn _k, _v1, v2 -> v2 end, d1, d2]
|
||||
end
|
||||
|
||||
def merge(O[data: d1], O[data: d2], fun) do
|
||||
O[data: :orddict.merge fun, d1, d2]
|
||||
end
|
||||
|
||||
def update(O[data: data], key, fun) do
|
||||
O[data: :orddict.update key, fun, data]
|
||||
end
|
||||
|
||||
def update(O[data: data], key, initial, fun) do
|
||||
O[data: :orddict.update key, fun, initial, data]
|
||||
end
|
||||
|
||||
def empty(_) do
|
||||
O[data: []]
|
||||
end
|
||||
|
||||
def to_list(O[data: data]) do
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Orddict do
|
||||
@moduledoc """
|
||||
This module implements a dictionary based that stores items
|
||||
as a list of tuples. It is a simple wrapper around
|
||||
[Erlang's orddict module](http://www.erlang.org/doc/man/orddict.html)
|
||||
and exposed via the `Dict` protocol.
|
||||
"""
|
||||
use Dict.Common, Dict.Orddict.Record
|
||||
end
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
defmodule Port do
|
||||
@moduledoc """
|
||||
Functions related to Erlang ports.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#open_port-2.
|
||||
"""
|
||||
def open(name, settings) do
|
||||
:erlang.open_port(name, settings)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_close-1.
|
||||
"""
|
||||
def close(port) do
|
||||
:erlang.port_close(port)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_command-2.
|
||||
"""
|
||||
def command(port, data, options // []) do
|
||||
:erlang.port_command(port, data, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_connect-2.
|
||||
"""
|
||||
def connect(port, pid) do
|
||||
:erlang.port_connect(port, pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_control-3.
|
||||
"""
|
||||
def control(port, operation, data) do
|
||||
:erlang.port_control(port, operation, data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_call-3.
|
||||
"""
|
||||
def call(port, operation, data) do
|
||||
:erlang.port_call(port, operation, data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_info-1.
|
||||
"""
|
||||
def info(port) do
|
||||
:erlang.port_info(port)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#port_info-2.
|
||||
"""
|
||||
def info(port, item) do
|
||||
:erlang.port_info(port, item)
|
||||
end
|
||||
|
||||
@doc """
|
||||
See http://www.erlang.org/doc/man/erlang.html#ports-0.
|
||||
"""
|
||||
def list do
|
||||
:erlang.ports
|
||||
end
|
||||
end
|
||||
-319
@@ -1,319 +0,0 @@
|
||||
defmodule Process do
|
||||
@moduledoc """
|
||||
This module provides convenience functions around processes and
|
||||
the process dictionary. In Erlang, most of these functions are
|
||||
auto-imported, but in Elixir they are grouped in a module for
|
||||
convenience. Notice that these functions, different from Erlang's,
|
||||
always return nil instead of undefined. You can use their Erlang
|
||||
version if you want the undefined value.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns true if the process exists and is alive, that is,
|
||||
is not exiting and has not exited. Otherwise, returns false.
|
||||
|
||||
`pid` must refer to a process at the local node.
|
||||
"""
|
||||
def alive?(pid) do
|
||||
:erlang.is_process_alive(pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the current process.
|
||||
"""
|
||||
def self do
|
||||
:erlang.self()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all key-values in the dictionary
|
||||
with no specific ordering (i.e. they are
|
||||
not a keywords list).
|
||||
"""
|
||||
def get do
|
||||
:erlang.get()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value for the given key.
|
||||
"""
|
||||
def get(key, default // nil) do
|
||||
case :erlang.get(key) do
|
||||
:undefined ->
|
||||
default
|
||||
value ->
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all keys that have the given `value`.
|
||||
"""
|
||||
defdelegate [get_keys: 1], to: :erlang
|
||||
|
||||
@doc """
|
||||
Stores the given key-value in the process dictionary.
|
||||
"""
|
||||
def put(key, value) do
|
||||
nillify :erlang.put(key, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes all items in the dictionary.
|
||||
"""
|
||||
def delete() do
|
||||
:erlang.erase()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the given key from the dictionary.
|
||||
"""
|
||||
def delete(key) do
|
||||
nillify :erlang.erase(key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends an exit signal with the given reason to the pid.
|
||||
|
||||
The following behavior apply if reason is any term except `:normal` or `:kill`:
|
||||
|
||||
1) If pid is not trapping exits, pid itself will exist with the given reason;
|
||||
|
||||
2) If pid is trapping exits, the exit signal is transformed into a message
|
||||
{'EXIT', from, reason} and delivered to the message queue of pid;
|
||||
|
||||
3) If reason is the atom `:normal`, pid will not exit. If it is trapping exits,
|
||||
the exit signal is transformed into a message {'EXIT', from, :normal} and
|
||||
delivered to its message queue;
|
||||
|
||||
4) If reason is the atom `:kill`, that is if `exit(pid, :kill)` is called, an
|
||||
untrappable exit signal is sent to pid which will unconditionally exit with
|
||||
exit reason `:killed`.
|
||||
|
||||
## Examples
|
||||
|
||||
Process.exit(other, :kil)
|
||||
|
||||
"""
|
||||
defdelegate [exit: 2], to: :erlang
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of `fun`.
|
||||
Otherwise works like spawn/3.
|
||||
"""
|
||||
defdelegate [spawn: 1], to: :erlang
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of `fun`
|
||||
on `node`. If `node` does not exist, a useless pid is returned.
|
||||
Otherwise works like spawn/3.
|
||||
"""
|
||||
defdelegate [spawn: 2], to: :erlang
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of
|
||||
`module.function(args)`. The new process created will be placed in the system
|
||||
scheduler queue and be run some time later.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#spawn-3 for more info.
|
||||
"""
|
||||
defdelegate [spawn: 3], to: :erlang
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of
|
||||
`module.function(args)` on `node`. If `node` does not exists, a useless
|
||||
pid is returned. Otherwise works like spawn/3.
|
||||
"""
|
||||
defdelegate [spawn: 4], to: :erlang
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of `fun`.
|
||||
A link is created between the calling process and the new
|
||||
process, atomically. Otherwise works like spawn/3.
|
||||
"""
|
||||
defdelegate [spawn_link: 1], to: :erlang
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of `fun`
|
||||
on `node`. A link is created between the calling process and the
|
||||
new process, atomically. If `node` does not exist, a useless pid is returned
|
||||
(and due to the link, an exit signal with exit reason :noconnection will be
|
||||
received). Otherwise works like spawn/3.
|
||||
"""
|
||||
defdelegate [spawn_link: 2], to: :erlang
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of
|
||||
`module.function(args)`. A link is created between the calling process
|
||||
and the new process, atomically. Otherwise works like spawn/3.
|
||||
"""
|
||||
defdelegate [spawn_link: 3], to: :erlang
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of
|
||||
`module.function(args)` on `node`. A link is created between the calling
|
||||
process and the new process, atomically. If `node` does not exist, a useless
|
||||
pid is returned (and due to the link, an exit signal with exit reason
|
||||
:noconnection will be received). Otherwise works like spawn/3.
|
||||
"""
|
||||
defdelegate [spawn_link: 4], to: :erlang
|
||||
|
||||
@doc """
|
||||
Returns the pid of a new process started by the application of `fun`
|
||||
and reference for a monitor created to the new process.
|
||||
Otherwise works like spawn/3.
|
||||
"""
|
||||
defdelegate [spawn_monitor: 1], to: :erlang
|
||||
|
||||
@doc """
|
||||
A new process is started by the application of `module.function(args)`
|
||||
and the process is monitored at the same time. Returns the pid and a
|
||||
reference for the monitor. Otherwise works like spawn/3.
|
||||
"""
|
||||
defdelegate [spawn_monitor: 3], to: :erlang
|
||||
|
||||
@doc """
|
||||
The same as `spawn/1` but accepts extra options as arguments.
|
||||
Please read http://www.erlang.org/doc/man/erlang.html#spawn_opt-4 for
|
||||
documentation of the options.
|
||||
"""
|
||||
defdelegate [spawn_opt: 2], to: :erlang
|
||||
|
||||
@doc """
|
||||
The same as `spawn/2` but accepts extra options as arguments.
|
||||
Please read http://www.erlang.org/doc/man/erlang.html#spawn_opt-4 for
|
||||
documentation of the options.
|
||||
"""
|
||||
defdelegate [spawn_opt: 3], to: :erlang
|
||||
|
||||
@doc """
|
||||
The same as `spawn/3` but accepts extra options as arguments.
|
||||
Please read http://www.erlang.org/doc/man/erlang.html#spawn_opt-4 for
|
||||
documentation of the options.
|
||||
"""
|
||||
defdelegate [spawn_opt: 4], to: :erlang
|
||||
|
||||
@doc """
|
||||
The same as `spawn/4` but accepts extra options as arguments.
|
||||
Please read http://www.erlang.org/doc/man/erlang.html#spawn_opt-4 for
|
||||
documentation of the options.
|
||||
"""
|
||||
defdelegate [spawn_opt: 5], to: :erlang
|
||||
|
||||
@doc """
|
||||
The calling process starts monitoring the item given.
|
||||
It returns the monitor reference.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#monitor-2 for more info.
|
||||
"""
|
||||
def monitor(item) do
|
||||
:erlang.monitor(:process, item)
|
||||
end
|
||||
|
||||
@doc """
|
||||
If monitor_ref is a reference which the calling process
|
||||
obtained by calling monitor/1, this monitoring is turned off.
|
||||
If the monitoring is already turned off, nothing happens.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#demonitor-2 for more info.
|
||||
"""
|
||||
def demonitor(monitor_ref, options // []) do
|
||||
:erlang.demonitor(monitor_ref, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of process identifiers corresponding to all the
|
||||
processes currently existing on the local node.
|
||||
|
||||
Note that a process that is exiting, exists but is not alive, i.e.,
|
||||
alive?/1 will return false for a process that is exiting,
|
||||
but its process identifier will be part of the result returned.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#processes-0 for more info.
|
||||
"""
|
||||
def list do
|
||||
:erlang.processes()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a link between the calling process and another process
|
||||
(or port) `pid`, if there is not such a link already.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#link-1 for more info.
|
||||
"""
|
||||
defdelegate [link: 1], to: :erlang
|
||||
|
||||
@doc """
|
||||
Removes the link, if there is one, between the calling process and
|
||||
the process or port referred to by `pid`. Returns true and does not
|
||||
fail, even if there is no link or `id` does not exist
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#unlink-1 for more info.
|
||||
"""
|
||||
defdelegate [unlink: 1], to: :erlang
|
||||
|
||||
@doc """
|
||||
Associates the name with a pid or a port identifier. name, which must
|
||||
be an atom, can be used instead of the pid / port identifier in the
|
||||
send operator (name <- message).
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#register-2 for more info.
|
||||
"""
|
||||
defdelegate [register: 2], to: :erlang
|
||||
|
||||
@doc """
|
||||
Removes the registered name, associated with a pid or a port identifier.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#unregister-1 for more info.
|
||||
"""
|
||||
defdelegate [unregister: 1], to: :erlang
|
||||
|
||||
@doc """
|
||||
Returns the pid or port identifier with the registered name.
|
||||
Returns undefined if the name is not registered.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#whereis-1 for more info.
|
||||
"""
|
||||
defdelegate [whereis: 1], to: :erlang
|
||||
|
||||
@doc """
|
||||
Returns a list of names which have been registered using register/2.
|
||||
"""
|
||||
def registered do
|
||||
:erlang.registered()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets certain flags for the process which calls this function.
|
||||
Returns the old value of the flag.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#process_flag-2 for more info.
|
||||
"""
|
||||
def flag(flag, value) do
|
||||
:erlang.process_flag(flag, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets certain flags for the process Pid, in the same manner as flag/2.
|
||||
Returns the old value of the flag. The allowed values for Flag are
|
||||
only a subset of those allowed in flag/2, namely: save_calls.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#process_flag-3 for more info.
|
||||
"""
|
||||
def flag(pid, flag, value) do
|
||||
:erlang.process_flag(pid, flag, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns information about the process identified by pid
|
||||
or undefined if the process is not alive.
|
||||
|
||||
See http://www.erlang.org/doc/man/erlang.html#process_info-2 for more info.
|
||||
"""
|
||||
def info(pid, spec) do
|
||||
:erlang.process_info(pid, spec)
|
||||
end
|
||||
|
||||
defp nillify(:undefined), do: nil
|
||||
defp nillify(other), do: other
|
||||
end
|
||||
-310
@@ -1,310 +0,0 @@
|
||||
defmodule Protocol do
|
||||
@moduledoc false
|
||||
|
||||
# We need to use Erlang.lists because Enum is not available yet
|
||||
require Erlang.lists, as: L
|
||||
|
||||
@doc """
|
||||
Handle `defprotocol`. It will define a function for each
|
||||
protocol plus two extra functions:
|
||||
|
||||
* `__protocol__/1` - returns the protocol name when :name is given,
|
||||
and a keywords list with the protocol functions
|
||||
when :functions is given;
|
||||
|
||||
* `__impl_for__/1` - receives one argument and returns a module
|
||||
that implements the protocol for the given
|
||||
data type. If no implementation matches, returns nil;
|
||||
|
||||
* `__impl_for__!/1` - same as above but raises an error if an implementation is not found
|
||||
"""
|
||||
def defprotocol(name, [do: block]) do
|
||||
quote do
|
||||
defmodule unquote(name) do
|
||||
# Remove "harmful" macros
|
||||
# We don't want to allow function definition inside protocols
|
||||
import Elixir.Builtin, except: [
|
||||
defmacro: 1, defmacro: 2, defmacro: 4,
|
||||
defp: 1, defp: 2, defp: 4,
|
||||
def: 1, def: 2, def: 4
|
||||
]
|
||||
|
||||
# Import the new dsl that holds the new def
|
||||
import Protocol.DSL
|
||||
|
||||
# Set up a clear slate to store defined functions
|
||||
@functions []
|
||||
|
||||
# Invoke the user given block
|
||||
unquote(block)
|
||||
|
||||
# Define callbacks and meta information
|
||||
{ conversions, fallback } = Protocol.conversions_for(__MODULE__, @only, @except)
|
||||
Protocol.impl_for(__MODULE__, conversions)
|
||||
Protocol.meta(__MODULE__, @functions, fallback)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Implement the given protocol for the given module.
|
||||
It also defines a `__impl__` function which
|
||||
returns the protocol being implemented.
|
||||
"""
|
||||
def defimpl(protocol, [do: block, for: for]) do
|
||||
quote do
|
||||
protocol = unquote(protocol)
|
||||
for = unquote(for)
|
||||
name = Module.concat(protocol, for)
|
||||
|
||||
Protocol.assert_protocol(protocol)
|
||||
|
||||
defmodule name do
|
||||
def __impl__, do: unquote(protocol)
|
||||
unquote(block)
|
||||
end
|
||||
|
||||
Protocol.assert_impl(name, protocol)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check if the given module is a protocol. Raises an error
|
||||
if not loaded or not a protocol.
|
||||
"""
|
||||
def assert_protocol(module) do
|
||||
try do
|
||||
module.__info__(:data)
|
||||
rescue
|
||||
UndefinedFunctionError ->
|
||||
raise ArgumentError, message: "#{module} is not loaded"
|
||||
end
|
||||
|
||||
try do
|
||||
module.__protocol__(:name)
|
||||
rescue
|
||||
UndefinedFunctionError ->
|
||||
raise ArgumentError, message: "#{module} is not a protocol"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check if the given `impl` is a valid impl for `protocol`.
|
||||
Raises an error if not.
|
||||
|
||||
"""
|
||||
def assert_impl(impl, protocol) do
|
||||
remaining = protocol.__protocol__(:functions) -- impl.__info__(:functions)
|
||||
|
||||
if remaining != [] do
|
||||
pp = Enum.map_join remaining, ", ", fn {x,y} -> "#{x}/#{y}" end
|
||||
raise ArgumentError,
|
||||
message: "#{inspect impl} did not implement #{inspect protocol}, missing: #{pp}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines meta information about the protocol and internal callbacks.
|
||||
"""
|
||||
def meta(module, functions, fallback) do
|
||||
contents = quote do
|
||||
def __protocol__(:name), do: __MODULE__
|
||||
def __protocol__(:functions), do: unquote(:lists.sort(functions))
|
||||
|
||||
def __impl_for__(arg) do
|
||||
case __raw_impl__(arg) do
|
||||
__MODULE__.Record ->
|
||||
target = Module.concat(__MODULE__, :erlang.element(1, arg))
|
||||
try do
|
||||
target.__impl__
|
||||
target
|
||||
rescue
|
||||
UndefinedFunctionError -> __fallback__
|
||||
end
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
def __impl_for__!(arg) do
|
||||
if module = __impl_for__(arg) do
|
||||
module
|
||||
else
|
||||
raise Protocol.UndefinedError, protocol: __MODULE__, structure: arg
|
||||
end
|
||||
end
|
||||
|
||||
defp __fallback__, do: unquote(fallback)
|
||||
end
|
||||
|
||||
Module.eval_quoted module, contents, [], file: __FILE__, line: __LINE__
|
||||
end
|
||||
|
||||
@doc """
|
||||
Implements the function that detects the protocol and returns
|
||||
the module to dispatch to. Returns module.Record for records
|
||||
which should be properly handled by the dispatching function.
|
||||
"""
|
||||
def impl_for(module, conversions) do
|
||||
contents = lc kind in conversions, do: each_impl_for(kind, conversions)
|
||||
|
||||
# If we don't implement all protocols and any is not in the
|
||||
# list, we need to add a final clause that returns nil.
|
||||
if !L.member({ Any, :is_any }, conversions) && length(conversions) < 10 do
|
||||
contents = contents ++ [quote do
|
||||
defp __raw_impl__(_) do
|
||||
nil
|
||||
end
|
||||
end]
|
||||
end
|
||||
|
||||
Module.eval_quoted module, contents, [], file: __FILE__, line: __LINE__
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the default conversions according to the given
|
||||
only/except options.
|
||||
"""
|
||||
def conversions_for(module, only, except) do
|
||||
kinds = all_types
|
||||
|
||||
conversions =
|
||||
if only do
|
||||
L.map(fn i -> L.keyfind(i, 1, kinds) end, only)
|
||||
else
|
||||
except = except || [Any]
|
||||
L.foldl(fn i, list -> L.keydelete(i, 1, list) end, kinds, except)
|
||||
end
|
||||
|
||||
fallback = cond do
|
||||
L.keyfind(Tuple, 1, conversions) ->
|
||||
Module.concat module, Tuple
|
||||
L.keyfind(Any, 1, conversions) ->
|
||||
Module.concat module, Any
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
|
||||
{ conversions, fallback }
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp all_types do
|
||||
[
|
||||
{ Record, :is_record },
|
||||
{ Tuple, :is_tuple },
|
||||
{ Atom, :is_atom },
|
||||
{ List, :is_list },
|
||||
{ BitString, :is_bitstring },
|
||||
{ Number, :is_number },
|
||||
{ Function, :is_function },
|
||||
{ PID, :is_pid },
|
||||
{ Port, :is_port },
|
||||
{ Reference, :is_reference },
|
||||
{ Any, :is_any }
|
||||
]
|
||||
end
|
||||
|
||||
# Returns a quoted expression that allow to checks
|
||||
# if a variable named first is built in or not.
|
||||
defp is_builtin?([{h,_}]) do
|
||||
quote do
|
||||
first == unquote(h)
|
||||
end
|
||||
end
|
||||
|
||||
defp is_builtin?([{h,_}|t]) do
|
||||
quote do
|
||||
first == unquote(h) or unquote(is_builtin?(t))
|
||||
end
|
||||
end
|
||||
|
||||
# Specially handle tuples as they can also be record.
|
||||
# If this is the case, module.Record will be returned.
|
||||
defp each_impl_for({ _, :is_record }, conversions) do
|
||||
quote do
|
||||
defp __raw_impl__(arg) when is_tuple(arg) and is_atom(:erlang.element(1, arg)) do
|
||||
first = :erlang.element(1, arg)
|
||||
case unquote(is_builtin?(conversions)) do
|
||||
true -> __MODULE__.Tuple
|
||||
false ->
|
||||
case atom_to_list(first) do
|
||||
'__MAIN__' ++ _ -> __MODULE__.Record
|
||||
_ -> __MODULE__.Tuple
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Special case any as we don't need to generate a guard.
|
||||
defp each_impl_for({ _, :is_any }, _) do
|
||||
quote do
|
||||
defp __raw_impl__(_) do
|
||||
__MODULE__.Any
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Generate all others protocols.
|
||||
defp each_impl_for({ kind, fun }, _) do
|
||||
quote do
|
||||
defp __raw_impl__(arg) when unquote(fun).(arg) do
|
||||
Module.concat __MODULE__, unquote(kind)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Protocol.DSL do
|
||||
@moduledoc false
|
||||
|
||||
defmacro def(expression) do
|
||||
{ name, arity } =
|
||||
case expression do
|
||||
{ _, _, args } when args == [] or is_atom(args) ->
|
||||
raise ArgumentError, message: "protocol functions expect at least one argument"
|
||||
{ name, _, args } when is_atom(name) and is_list(args) ->
|
||||
{ name, length(args) }
|
||||
_ ->
|
||||
raise ArgumentError, message: "invalid args for defprotocol"
|
||||
end
|
||||
|
||||
# Generate arguments according the arity. The arguments
|
||||
# are named xa, xb and so forth. We cannot use string
|
||||
# interpolation to generate the arguments because of compile
|
||||
# dependencies, so we use the <<>> instead.
|
||||
args = lc i in :lists.seq(1, arity) do
|
||||
{ binary_to_atom(<<?x, i + 64>>), 0, :quoted }
|
||||
end
|
||||
|
||||
quote do
|
||||
# Append new function to the list
|
||||
@functions [unquote({name, arity})|@functions]
|
||||
|
||||
Elixir.Builtin.def unquote(name).(unquote_splicing(args)) do
|
||||
args = [unquote_splicing(args)]
|
||||
case __raw_impl__(xA) do
|
||||
__MODULE__.Record ->
|
||||
try do
|
||||
target = Module.concat(__MODULE__, :erlang.element(1, xA))
|
||||
apply target, unquote(name), args
|
||||
rescue
|
||||
UndefinedFunctionError ->
|
||||
case __fallback__ do
|
||||
nil ->
|
||||
raise Protocol.UndefinedError, protocol: __MODULE__, structure: xA
|
||||
other ->
|
||||
apply other, unquote(name), args
|
||||
end
|
||||
end
|
||||
nil ->
|
||||
raise Protocol.UndefinedError, protocol: __MODULE__, structure: xA
|
||||
other ->
|
||||
apply other, unquote(name), args
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-300
@@ -1,300 +0,0 @@
|
||||
defmodule Record do
|
||||
@moduledoc """
|
||||
Functions to define and interact with Erlang records
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Extract record information from an Erlang file and
|
||||
return the fields as a list of tuples.
|
||||
|
||||
## Examples
|
||||
|
||||
defrecord FileInfo, Record.extract(:file_info, from_lib: "kernel/include/file.hrl")
|
||||
|
||||
"""
|
||||
def extract(name, opts) do
|
||||
Record.Extractor.retrieve(name, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Main entry point for records definition.
|
||||
This is invoked directly by `Elixir.Builtin.defrecord`.
|
||||
Returns the quoted expression of a module given by name.
|
||||
"""
|
||||
def defrecord(name, values, opts) do
|
||||
moduledoc = Keyword.get(opts, :moduledoc, false)
|
||||
block = Keyword.get(opts, :do)
|
||||
definition = Keyword.get(opts, :definition, Record.Definition)
|
||||
|
||||
quote do
|
||||
defmodule unquote(name) do
|
||||
@moduledoc unquote(moduledoc)
|
||||
Record.define_functions(__MODULE__, unquote(values), unquote(definition))
|
||||
unquote(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
# Private endpoint that defines the functions for the Record.
|
||||
def define_functions(module, values, definition) do
|
||||
# Escape the values so they are valid syntax nodes
|
||||
values = Macro.escape(values)
|
||||
|
||||
contents = [
|
||||
reflection(module, values),
|
||||
getters_and_setters(values, 1, [], definition),
|
||||
initializers(values)
|
||||
]
|
||||
|
||||
Module.eval_quoted module, contents, [], file: __FILE__, line: __LINE__
|
||||
end
|
||||
|
||||
# Define __record__/1 and __record__/2 as reflection functions
|
||||
# that returns the record names and fields.
|
||||
#
|
||||
# Note that fields are *not* keywords. They are in the same
|
||||
# order as given as parameter and reflects the order of the
|
||||
# fields in the tuple.
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# defrecord FileInfo, atime: nil, mtime: nil
|
||||
#
|
||||
# FileInfo.__record__(:name) #=> FileInfo
|
||||
# FileInfo.__record__(:fields) #=> [atime: nil, mtime: nil]
|
||||
#
|
||||
defp reflection(name, values) do
|
||||
quote do
|
||||
def __record__(kind), do: __record__(kind, nil)
|
||||
def __record__(:name, _), do: unquote(name)
|
||||
def __record__(:fields, _), do: unquote(values)
|
||||
end
|
||||
end
|
||||
|
||||
# Define initializers methods. For a declaration like:
|
||||
#
|
||||
# defrecord FileInfo, atime: nil, mtime: nil
|
||||
#
|
||||
# It will define three methods:
|
||||
#
|
||||
# def new() do
|
||||
# new([])
|
||||
# end
|
||||
#
|
||||
# def new([]) do
|
||||
# { FileInfo, nil, nil }
|
||||
# end
|
||||
#
|
||||
# def new(opts) do
|
||||
# { FileInfo, Keyword.get(opts, :atime), Keyword.get(opts, :mtime) }
|
||||
# end
|
||||
#
|
||||
defp initializers(values) do
|
||||
defaults = Enum.map values, elem(&1, 2)
|
||||
|
||||
# For each value, define a piece of code that will receive
|
||||
# an ordered dict of options (opts) and it will try to fetch
|
||||
# the given key from the ordered dict, falling back to the
|
||||
# default value if one does not exist.
|
||||
selective = Enum.map values, fn {k,v} ->
|
||||
quote do: Keyword.get(opts, unquote(k), unquote(v))
|
||||
end
|
||||
|
||||
quote do
|
||||
def new(), do: new([])
|
||||
def new([]), do: { __MODULE__, unquote_splicing(defaults) }
|
||||
def new(opts) when is_list(opts), do: { __MODULE__, unquote_splicing(selective) }
|
||||
def new(tuple) when is_tuple(tuple), do: setelem(tuple, 1, __MODULE__)
|
||||
end
|
||||
end
|
||||
|
||||
# Implement getters and setters for each attribute.
|
||||
# For a declaration like:
|
||||
#
|
||||
# defrecord FileInfo, atime: nil, mtime: nil
|
||||
#
|
||||
# It will define four methods:
|
||||
#
|
||||
# def :atime.(record) do
|
||||
# elem(record, 2)
|
||||
# end
|
||||
#
|
||||
# def :atime.(record, value) do
|
||||
# setelem(record, 2, value)
|
||||
# end
|
||||
#
|
||||
# def :mtime.(record) do
|
||||
# elem(record, 3)
|
||||
# end
|
||||
#
|
||||
# def :mtime.(record, value) do
|
||||
# setelem(record, value, 3)
|
||||
# end
|
||||
#
|
||||
# `element` and `setelement` will simply get and set values
|
||||
# from the record tuple. Notice that `:atime.(record)` is just
|
||||
# a dynamic way to say `atime(record)`. We need to use this
|
||||
# syntax as `unquote(key)(record)` wouldn't be valid (as Elixir
|
||||
# allows you to parenthesis just on specific cases as `foo()`
|
||||
# and `foo.bar()`)
|
||||
defp getters_and_setters([{ key, default }|t], i, acc, definition) do
|
||||
i = i + 1
|
||||
functions = definition.functions_for(key, default, i)
|
||||
getters_and_setters(t, i, [functions | acc], definition)
|
||||
end
|
||||
|
||||
defp getters_and_setters([], _i, acc, _), do: acc
|
||||
end
|
||||
|
||||
defmodule Record.Extractor do
|
||||
@moduledoc false
|
||||
|
||||
|
||||
# Retrieve a record definition from an Erlang file using
|
||||
# the same lookup as the *include* attribute from Erlang modules.
|
||||
def retrieve(name, from: string) do
|
||||
file = to_char_list(string)
|
||||
|
||||
case Erlang.code.where_is_file(file) do
|
||||
:non_existing -> realfile = file
|
||||
realfile -> nil
|
||||
end
|
||||
|
||||
retrieve_record(name, realfile)
|
||||
end
|
||||
|
||||
# Retrieve a record definition from an Erlang file using
|
||||
# the same lookup as the *include_lib* attribute from Erlang modules.
|
||||
def retrieve(name, from_lib: file) do
|
||||
[app|path] = Erlang.filename.split(to_char_list(file))
|
||||
case Erlang.code.lib_dir(to_char_list(app)) do
|
||||
{ :error, _ } ->
|
||||
raise ArgumentError, "Lib file #{to_binary(file)} could not be found"
|
||||
libpath ->
|
||||
retrieve_record name, Erlang.filename.join([libpath|path])
|
||||
end
|
||||
end
|
||||
|
||||
# Retrieve the record with the given name from the given file
|
||||
defp retrieve_record(name, file) do
|
||||
records = retrieve_from_file(file)
|
||||
if record = List.keyfind(records, name, 1) do
|
||||
parse_record(record)
|
||||
else
|
||||
raise ArgumentError, "No record #{name} found at #{to_binary(file)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Parse the given file and retrieve all existent records.
|
||||
defp retrieve_from_file(file) do
|
||||
lc { :attribute, _, :record, record } in read_file(file), do: record
|
||||
end
|
||||
|
||||
# Read a file and return its abstract syntax form that also
|
||||
# includes record and other preprocessor modules. This is done
|
||||
# by using Erlang's epp_dodger.
|
||||
defp read_file(file) do
|
||||
case Erlang.epp_dodger.quick_parse_file(file) do
|
||||
{ :ok, form } ->
|
||||
form
|
||||
other ->
|
||||
raise "Error parsing file #{to_binary(file)}, got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Parse a tuple with name and fields and returns a
|
||||
# list of second order tuples where the first element
|
||||
# is the field and the second is its default value.
|
||||
defp parse_record({ _name, fields }) do
|
||||
cons = List.foldr fields, { nil, 0 }, fn f, acc ->
|
||||
{ :cons, 0, parse_field(f), acc }
|
||||
end
|
||||
{ :value, list, _ } = Erlang.erl_eval.expr(cons, [])
|
||||
list
|
||||
end
|
||||
|
||||
defp parse_field({ :typed_record_field, record_field, _type }) do
|
||||
parse_field(record_field)
|
||||
end
|
||||
|
||||
defp parse_field({ :record_field, _, key }) do
|
||||
{ :tuple, 0, [key, {:atom, 0, :nil}] }
|
||||
end
|
||||
|
||||
defp parse_field({ :record_field, _, key, value }) do
|
||||
{ :tuple, 0, [key, value] }
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Record.Definition do
|
||||
@moduledoc false
|
||||
|
||||
# Main entry point. It defines both default functions
|
||||
# via `default_for` and extensions via `extension_for`.
|
||||
def functions_for(key, default, i) do
|
||||
[
|
||||
default_for(key, default, i),
|
||||
extension_for(key, default, i)
|
||||
]
|
||||
end
|
||||
|
||||
# Skip the __exception__ for defexception.
|
||||
def default_for(:__exception__, _default, _i) do
|
||||
nil
|
||||
end
|
||||
|
||||
# Define the default functions for each field.
|
||||
def default_for(key, _default, i) do
|
||||
bin_update = "update_" <> atom_to_binary(key)
|
||||
update = binary_to_atom(bin_update)
|
||||
|
||||
quote do
|
||||
def unquote(key).(record) do
|
||||
:erlang.element(unquote(i), record)
|
||||
end
|
||||
|
||||
def unquote(key).(value, record) do
|
||||
:erlang.setelement(unquote(i), record, value)
|
||||
end
|
||||
|
||||
def unquote(update).(function, record) do
|
||||
current = :erlang.element(unquote(i), record)
|
||||
:erlang.setelement(unquote(i), record, function.(current))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Define extensions based on the default type.
|
||||
def extension_for(key, default, i) when is_list(default) do
|
||||
bin_key = atom_to_binary(key)
|
||||
prepend = :"prepend_#{bin_key}"
|
||||
merge = :"merge_#{bin_key}"
|
||||
|
||||
quote do
|
||||
def unquote(prepend).(value, record) do
|
||||
current = :erlang.element(unquote(i), record)
|
||||
:erlang.setelement(unquote(i), record, value ++ current)
|
||||
end
|
||||
|
||||
def unquote(merge).(value, record) do
|
||||
current = :erlang.element(unquote(i), record)
|
||||
:erlang.setelement(unquote(i), record, Keyword.merge(current, value))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def extension_for(key, default, i) when is_number(default) do
|
||||
bin_key = atom_to_binary(key)
|
||||
increment = :"increment_#{bin_key}"
|
||||
|
||||
quote do
|
||||
def unquote(increment).(value // 1, record) do
|
||||
current = :erlang.element(unquote(i), record)
|
||||
:erlang.setelement(unquote(i), record, current + value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def extension_for(_, _, _), do: nil
|
||||
end
|
||||
-238
@@ -1,238 +0,0 @@
|
||||
defmodule Regex do
|
||||
@moduledoc %B"""
|
||||
Regular expressions for Elixir built on top of the re module
|
||||
in the Erlang Standard Library. More information can be found
|
||||
on re documentation: http://www.erlang.org/doc/man/re.html
|
||||
|
||||
Regular expressions in Elixir can be created using Regex.compile
|
||||
or using the special form with `%r`:
|
||||
|
||||
# A simple regular expressions that matches foo anywhere in the string
|
||||
%r/foo/
|
||||
|
||||
# A regular expression with case insensitive options and handle unicode chars
|
||||
%r/foo/iu
|
||||
|
||||
The re module provides several options, some of them are not
|
||||
available in Elixir while others are enabled by default. The
|
||||
ones enabled by default are:
|
||||
|
||||
* multiline - the given string is always considered to be multiline, so
|
||||
`^` and `$` marks the beginning and end of each line. You need to use
|
||||
`\A` and `\z` to match the end or beginning of the string
|
||||
|
||||
The available options, followed by their shortcut in parenthesis, are:
|
||||
|
||||
* unicode (u) - used when you want to match against specific unicode characters
|
||||
* caseless (i) - add case insensitivity
|
||||
* dotall (m) - causes dot to match newlines and also set newline to anycrlf.
|
||||
The new line setting can be overwritten by setting `(*CR)` or `(*LF)` or
|
||||
`(*CRLF)` or `(*ANY)` according to re documentation
|
||||
* extended (x) - whitespace characters are ignored except when escaped and
|
||||
allow `#` to delimit comments
|
||||
* firstline (f) - forces the unanchored pattern to match before or at the first
|
||||
newline, though the matched text may continue over the newline
|
||||
* ungreedy (r) - invert the "greediness" of the regexp
|
||||
|
||||
The options not available are:
|
||||
|
||||
* anchored - not available, use `^` or `\A` instead
|
||||
* dollar_endonly - not available, use `\z` instead
|
||||
* no_auto_capture - not available, use `?:` instead
|
||||
* newline - not available, use `(*CR)` or `(*LF)` or `(*CRLF)` or `(*ANYCRLF)`
|
||||
or `(*ANY)` at the beginning of the regexp according to the re documentation
|
||||
|
||||
Most of the functions in this module accept either a binary or a char list
|
||||
as string subject. The result is based on the argument (a binary will return
|
||||
a binary, a char list will return a char list).
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Compile the regular expression according to the given options.
|
||||
The result returned is a record named :re_pattern and its
|
||||
length can be modified in future releases.
|
||||
|
||||
Check the module documentation for more information
|
||||
about the options supported by compile.
|
||||
"""
|
||||
def compile(source, options // "") do
|
||||
source = to_binary(source)
|
||||
options = to_binary(options)
|
||||
re_opts = [:multiline|translate_options(options)]
|
||||
{ :ok, compiled } = Erlang.re.compile(source, re_opts)
|
||||
{ Regex, compiled, source, options }
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a boolean if there was a match or not.
|
||||
|
||||
## Examples
|
||||
|
||||
Regex.match? %r/foo/, "foo" #=> true
|
||||
Regex.match? %r/foo/, "bar" #=> false
|
||||
|
||||
"""
|
||||
def match?({ Regex, compiled, _, _ }, string) do
|
||||
:nomatch != Erlang.re.run(string, compiled)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs the regular expression against the given string.
|
||||
It returns a list with all matches or nil if no match ocurred.
|
||||
|
||||
## Examples
|
||||
|
||||
Regex.run %r/c(d)/, "abcd" #=> ["cd", "d"]
|
||||
Regex.run %r/e/, "abcd" #=> nil
|
||||
|
||||
"""
|
||||
def run({ Regex, compiled, _, _ }, string) do
|
||||
case Erlang.re.run(string, compiled, [{ :capture, :all, return_for(string) }]) do
|
||||
:nomatch ->
|
||||
nil
|
||||
{ :match, results } ->
|
||||
results
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with the match indexes in the given string.
|
||||
The matches are tuples where the first element is the index
|
||||
(zero indexed) the match happened and the second is the length
|
||||
of the match.
|
||||
|
||||
## Examples
|
||||
|
||||
Regex.run %r/c(d)/, "abcd" #=> [{2,2},{3,1}]
|
||||
Regex.run %r/e/, "abcd" #=> nil
|
||||
|
||||
"""
|
||||
def indexes({ Regex, compiled, _, _ }, string) do
|
||||
case Erlang.re.run(string, compiled, [{ :capture, :all, :index }, { :offset, 0 }]) do
|
||||
:nomatch ->
|
||||
nil
|
||||
{ :match, results } ->
|
||||
results
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the underlying re_pattern in the regular expression.
|
||||
"""
|
||||
def re_pattern({ Regex, compiled, _, _ }) do
|
||||
compiled
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the regex source as binary.
|
||||
|
||||
## Examples
|
||||
|
||||
Regex.source %r(foo) #=> "foo"
|
||||
|
||||
"""
|
||||
def source({ Regex, _, source, _ }) do
|
||||
source
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the regex options as a list.
|
||||
|
||||
## Examples
|
||||
|
||||
Regex.opts %r(foo)m #=> 'm'
|
||||
|
||||
"""
|
||||
def opts({ Regex, _, _, opts }) do
|
||||
opts
|
||||
end
|
||||
|
||||
@doc """
|
||||
Same as run, but scans the target several times collecting all matches of
|
||||
the regular expression. A list is returned with each match. If the item in
|
||||
the list is a binary, it means there were no captures. If the item is another
|
||||
list, each element in this secondary list is a capture.
|
||||
|
||||
## Examples
|
||||
|
||||
Regex.scan %r/c(d|e)/, "abcd abce" #=> [["d"], ["e"]]
|
||||
Regex.scan %r/c(?:d|e)/, "abcd abce" #=> ["cd", "ce"]
|
||||
Regex.scan %r/e/, "abcd" #=> []
|
||||
|
||||
"""
|
||||
def scan({ Regex, compiled, _, _ }, string) do
|
||||
options = [{ :capture, :all, return_for(string) }, :global, { :offset, 0 }]
|
||||
case Erlang.re.run(string, compiled, options) do
|
||||
:nomatch -> []
|
||||
{ :match, results } ->
|
||||
lc result in results do
|
||||
case result do
|
||||
[t] -> t
|
||||
[h|t] -> t
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Split the given target in the number of parts specified. If no ammount
|
||||
of parts is given, it defaults to :infinity.
|
||||
"""
|
||||
def split({ Regex, compiled, _, _ }, string, parts // :infinity) do
|
||||
options = [{ :return, return_for(string) }, :trim, { :parts, parts }]
|
||||
Erlang.re.split(string, compiled, options)
|
||||
end
|
||||
|
||||
@doc %B"""
|
||||
Receives a string and a replacement and returns a string where the
|
||||
first match of the regular expressions is replaced by replacement.
|
||||
Inside the replacement, you can either give "&" to access the whole
|
||||
regular expression or \N, where N is in integer to access an specific
|
||||
matching parens.
|
||||
|
||||
## Examples
|
||||
|
||||
Regex.replace(%r/d/, "abc", "d") #=> "abc"
|
||||
Regex.replace(%r/b/, "abc", "d") #=> "adc"
|
||||
Regex.replace(%r/b/, "abc", "[&]") #=> "a[b]c"
|
||||
Regex.replace(%r/b/, "abc", "[\\&]") #=> "a[&]c"
|
||||
Regex.replace(%r/(b)/, "abc", "[\\1]") #=> "a[b]c"
|
||||
|
||||
"""
|
||||
def replace({ Regex, compiled, _, _ }, string, replacement) do
|
||||
Erlang.re.replace(string, compiled, replacement, [{ :return, return_for(string) }])
|
||||
end
|
||||
|
||||
@doc """
|
||||
The same as replace, but replaces all parts where the regular
|
||||
expressions matches in the string. Please read `replace/3` for
|
||||
documentation and examples.
|
||||
"""
|
||||
def replace_all({ Regex, compiled, _, _ }, string, replacement) do
|
||||
Erlang.re.replace(string, compiled, replacement, [{ :return, return_for(string) }, :global])
|
||||
end
|
||||
|
||||
# Helpers
|
||||
|
||||
@doc false
|
||||
# Unescape map function used by Binary.unescape.
|
||||
def unescape_map(?f), do: ?\f
|
||||
def unescape_map(?n), do: ?\n
|
||||
def unescape_map(?r), do: ?\r
|
||||
def unescape_map(?t), do: ?\t
|
||||
def unescape_map(?v), do: ?\v
|
||||
def unescape_map(_), do: false
|
||||
|
||||
# Private Helpers
|
||||
|
||||
defp return_for(element) when is_binary(element), do: :binary
|
||||
defp return_for(element) when is_list(element), do: :list
|
||||
|
||||
defp translate_options(<<?u, t|:binary>>), do: [:unicode|translate_options(t)]
|
||||
defp translate_options(<<?i, t|:binary>>), do: [:caseless|translate_options(t)]
|
||||
defp translate_options(<<?x, t|:binary>>), do: [:extended|translate_options(t)]
|
||||
defp translate_options(<<?f, t|:binary>>), do: [:firstline|translate_options(t)]
|
||||
defp translate_options(<<?r, t|:binary>>), do: [:ungreedy|translate_options(t)]
|
||||
defp translate_options(<<?m, t|:binary>>), do: [:dotall,{:newline,:anycrlf}|translate_options(t)]
|
||||
defp translate_options(<<>>), do: []
|
||||
end
|
||||
-169
@@ -1,169 +0,0 @@
|
||||
# Since Elixir does not (yet) support local macros,
|
||||
# we need to wrap the step that extracts git information
|
||||
# in this module.
|
||||
defmodule System.GitCompiler do
|
||||
@moduledoc false
|
||||
|
||||
defmacro generate do
|
||||
quote do
|
||||
@doc """
|
||||
Returns a tuple { Elixir version, commit sha-1, build date }.
|
||||
|
||||
The format of the return value may change in a future release. Please
|
||||
make sure your code doesn't depend on it.
|
||||
"""
|
||||
def build_info do
|
||||
{ System.version,
|
||||
unquote(get_head_sha),
|
||||
unquote(get_date) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Tries to run `git rev-parse HEAD`. In case of success returns the
|
||||
# commit sha, otherwise returns an empty string.
|
||||
defp get_head_sha do
|
||||
# The following failures are possible:
|
||||
#
|
||||
# 1) there is no `git` command
|
||||
# 2) pwd is not a git repository
|
||||
#
|
||||
command = 'git rev-parse HEAD'
|
||||
opts = [:stream, :exit_status, :use_stdio,
|
||||
:stderr_to_stdout, :in, :eof]
|
||||
port = :erlang.open_port {:spawn, command}, opts
|
||||
|
||||
output = read_port port
|
||||
case output do
|
||||
{ 0, data } ->
|
||||
Regex.replace_all %r/\n/, to_binary(data), ""
|
||||
_ ->
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
defp read_port(port, data // []) do
|
||||
receive do
|
||||
{^port, {:data, new_data}} ->
|
||||
read_port port, [new_data|data]
|
||||
{^port, :eof} ->
|
||||
:erlang.port_close port
|
||||
receive do
|
||||
{^port, {:exit_status, exit_status}} ->
|
||||
{exit_status, List.reverse data}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_date do
|
||||
list_to_binary :httpd_util.rfc1123_date
|
||||
end
|
||||
end
|
||||
|
||||
defmodule System do
|
||||
@moduledoc """
|
||||
The System module provides access to some variables used or
|
||||
maintained by the VM and to functions that interact strongly
|
||||
with the VM or the host system.
|
||||
"""
|
||||
|
||||
require System.GitCompiler
|
||||
System.GitCompiler.generate
|
||||
|
||||
@doc """
|
||||
Returns Elixir's version as binary.
|
||||
"""
|
||||
def version, do: "0.5.0"
|
||||
|
||||
@doc """
|
||||
Returns the list of command-line arguments passed to the program.
|
||||
"""
|
||||
def argv do
|
||||
Erlang.gen_server.call(:elixir_code_server, :argv)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Registers a function that will be invoked
|
||||
at the end of program execution. Useful for
|
||||
invoking a hook on scripted mode.
|
||||
|
||||
The function must expect the exit status code
|
||||
as argument.
|
||||
"""
|
||||
def at_exit(fun) when is_function(fun, 1) do
|
||||
server_call { :at_exit, fun }
|
||||
end
|
||||
|
||||
@doc """
|
||||
Executes `command` in a command shell of the target OS,
|
||||
captures the standard output of the command and returns
|
||||
the result as a binary.
|
||||
"""
|
||||
def cmd(command) do
|
||||
list_to_binary :os.cmd(to_char_list(command))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of all environment variables. Each environment variable is
|
||||
given as a single string of the format "VarName=Value", where VarName is the
|
||||
name of the variable and Value its value.
|
||||
"""
|
||||
def get_env do
|
||||
Enum.map :os.getenv, list_to_binary &1
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value of the environment variable
|
||||
`varname` as a binary, or nil if the environment
|
||||
variable is undefined.
|
||||
"""
|
||||
def get_env(varname) do
|
||||
case :os.getenv(to_char_list(varname)) do
|
||||
false -> nil
|
||||
other -> list_to_binary(other)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the process identifier of the current Erlang emulator
|
||||
in the format most commonly used by the operating system environment.
|
||||
|
||||
See http://www.erlang.org/doc/man/os.html#getpid-0 for more info.
|
||||
"""
|
||||
def get_pid, do: list_to_binary(:os.getpid)
|
||||
|
||||
@doc """
|
||||
Sets a new `value` for the environment variable `varname`.
|
||||
"""
|
||||
def put_env(varname, value) do
|
||||
:os.putenv to_char_list(varname), to_char_list(value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets a new value for each environment variable corresponding
|
||||
to each key in `dict`.
|
||||
"""
|
||||
def put_env(dict) do
|
||||
Enum.each dict, fn {key, val} -> put_env key, val end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get the stacktrace.
|
||||
"""
|
||||
def stacktrace do
|
||||
filter_stacktrace Erlang.erlang.get_stacktrace
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
# Filter stacktrace by removing internal BOOTSTRAP calls.
|
||||
defp filter_stacktrace([{ Elixir.Builtin, :raise, _, _ }|t]), do: filter_stacktrace(t)
|
||||
defp filter_stacktrace([{ _mod, :BOOTSTRAP, _, info }|t]),
|
||||
do: filter_stacktrace([{ Elixir.Builtin, :defmodule, 2, info }|t])
|
||||
defp filter_stacktrace([h|t]), do: [h|filter_stacktrace(t)]
|
||||
defp filter_stacktrace([]), do: []
|
||||
|
||||
defp server_call(args) do
|
||||
Erlang.gen_server.call(:elixir_code_server, args)
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
defmodule Tuple do
|
||||
@doc """
|
||||
Simply invokes the Access protocol for the given tuple.
|
||||
Check `Access.Tuple` for more information.
|
||||
"""
|
||||
def access(tuple, access) when is_tuple(tuple) do
|
||||
Access.Tuple.access(tuple, access)
|
||||
end
|
||||
end
|
||||
-189
@@ -1,189 +0,0 @@
|
||||
defmodule URI do
|
||||
@on_load :preload_parsers
|
||||
|
||||
defrecord Info, [scheme: nil, path: nil, query: nil,
|
||||
fragment: nil, authority: nil,
|
||||
userinfo: nil, host: nil, port: nil,
|
||||
specifics: nil]
|
||||
|
||||
import Bitwise
|
||||
|
||||
@moduledoc """
|
||||
Utilities for working with and creating URIs.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Takes an enumerable (containing a sequence of two-item tuples)
|
||||
and returns a string of k=v&k2=v2... where keys and values are
|
||||
URL encoded as per encode. Keys and values can be any term
|
||||
that implements the Binary.Chars protocol (i.e. can be converted
|
||||
to binary).
|
||||
"""
|
||||
def encode_query(l), do: Enum.join(Enum.map(l, pair(&1)), "&")
|
||||
|
||||
@doc """
|
||||
Given a query string of the form "key1=value1&key=value2...", produces an
|
||||
orddict with one entry for each key-value pair. Each key and value will be a
|
||||
binary. It also does percent-unescaping of both keys and values.
|
||||
|
||||
Returns nil if the query string is malformed.
|
||||
"""
|
||||
def decode_query(q, dict // Orddict.new) do
|
||||
if Regex.match?(%r/^\s*$/, q) do
|
||||
dict
|
||||
else
|
||||
parts = Regex.split %r/&/, to_binary(q)
|
||||
impl = Dict.__impl_for__!(dict)
|
||||
|
||||
try do
|
||||
List.foldl parts, dict, fn kvstr, acc ->
|
||||
case Regex.split(%r/=/, kvstr) do
|
||||
[ key, value ] when key != "" ->
|
||||
impl.put acc, decode(key), decode(value)
|
||||
_ ->
|
||||
throw :malformed_query_string
|
||||
end
|
||||
end
|
||||
catch
|
||||
:malformed_query_string -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp pair({k, v}) do
|
||||
encode(to_binary(k)) <> "=" <> encode(to_binary(v))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Percent (URL) encodes a URI.
|
||||
"""
|
||||
def encode(s), do: bc <<c>> in s, do: <<percent(c)|:binary>>
|
||||
|
||||
defp percent(32), do: <<?+>>
|
||||
defp percent(?-), do: <<?->>
|
||||
defp percent(?_), do: <<?_>>
|
||||
defp percent(?.), do: <<?.>>
|
||||
|
||||
defp percent(c) when
|
||||
c >= ?0 and c <= ?9 when
|
||||
c >= ?a and c <= ?z when
|
||||
c >= ?A and c <= ?Z do
|
||||
<<c>>
|
||||
end
|
||||
|
||||
defp percent(c), do: escape_byte(c)
|
||||
|
||||
defp escape_byte(c), do: "%" <> hex(c)
|
||||
|
||||
defp hex(n) when n <= 9, do: <<n + ?0>>
|
||||
defp hex(n) when n > 15 do
|
||||
hex(bsr(n, 4)) <> hex(band(n, 15))
|
||||
end
|
||||
defp hex(n), do: <<n + ?A - 10>>
|
||||
|
||||
@doc """
|
||||
Unpercent (URL) decodes a URI.
|
||||
"""
|
||||
def decode(<<?%, hex1, hex2, tail |:binary >>) do
|
||||
<< bsl(hex2dec(hex1), 4) + hex2dec(hex2) >> <> decode(tail)
|
||||
end
|
||||
|
||||
def decode(<<head, tail |:binary >>) do
|
||||
<<check_plus(head)>> <> decode(tail)
|
||||
end
|
||||
|
||||
def decode(<<>>), do: <<>>
|
||||
|
||||
defp hex2dec(n) when n >= ?A and n <= ?F, do: n - ?A + 10
|
||||
defp hex2dec(n) when n >= ?0 and n <= ?9, do: n - ?0
|
||||
|
||||
defp check_plus(?+), do: 32
|
||||
defp check_plus(c), do: c
|
||||
|
||||
@doc """
|
||||
Parses a URI into components.
|
||||
|
||||
URIs have portions that are handled specially for the
|
||||
particular scheme of the URI. For example, http and https
|
||||
have different default ports. Sometimes the parsing
|
||||
of portions themselves are different. This parser
|
||||
is extensible via behavior modules. If you have a
|
||||
module named URI.MYSCHEME with a function called
|
||||
'parse' that takes a single argument, the generically
|
||||
parsed URI, that function will be called when this
|
||||
parse function is passed a URI of that scheme. This
|
||||
allows you to build on top of what the URI library
|
||||
currently offers. You also need to define default_port
|
||||
which takes 0 arguments and returns the default port
|
||||
for that particular scheme. Take a look at URI.HTTPS for an
|
||||
example of one of these extension modules.
|
||||
"""
|
||||
def parse(s) do
|
||||
# From http://tools.ietf.org/html/rfc3986#appendix-B
|
||||
regex = %r/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/
|
||||
parts = nillify(Regex.run(regex, s))
|
||||
|
||||
destructure [_, _, scheme, _, authority, path, _, query, _, fragment], parts
|
||||
{ userinfo, host, port } = split_authority(authority)
|
||||
|
||||
info = URI.Info[
|
||||
scheme: scheme, path: path, query: query,
|
||||
fragment: fragment, authority: authority,
|
||||
userinfo: userinfo, host: host, port: port
|
||||
]
|
||||
|
||||
scheme_specific(scheme, info)
|
||||
end
|
||||
|
||||
defp scheme_specific(scheme, info) do
|
||||
if scheme do
|
||||
module =
|
||||
try do
|
||||
Module.safe_concat(URI, :string.to_upper(binary_to_list(scheme)))
|
||||
rescue
|
||||
ArgumentError -> nil
|
||||
end
|
||||
|
||||
if module && match?({:module,^module}, Code.ensure_loaded(module)) do
|
||||
module.parse(default_port(info, module))
|
||||
else
|
||||
info
|
||||
end
|
||||
else
|
||||
info
|
||||
end
|
||||
end
|
||||
|
||||
defp default_port(info, module) do
|
||||
if info.port, do: info, else: info.port(module.default_port)
|
||||
end
|
||||
|
||||
# Split an authority into its userinfo, host and port parts.
|
||||
defp split_authority(s) do
|
||||
s = s || ""
|
||||
components = Regex.run %r/(^(.*)@)?([^:]*)(:(\d*))?/, s
|
||||
destructure [_, _, userinfo, host, _, port], nillify(components)
|
||||
port = if port, do: list_to_integer(binary_to_list(port))
|
||||
{ userinfo, host, port }
|
||||
end
|
||||
|
||||
# Regex.run returns empty strings sometimes. We want
|
||||
# to replace those with nil for consistency.
|
||||
defp nillify(l) do
|
||||
lc s in l do
|
||||
if size(s) > 0 do
|
||||
s
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Reference parsers so the parse/1 doesn't fail
|
||||
# on safe_concat.
|
||||
defp preload_parsers do
|
||||
parsers = [URI.FTP, URI.HTTP, URI.HTTPS, URI.LDAP, URI.SFTP, URI.TFTP]
|
||||
Enum.each parsers, Code.ensure_loaded(&1)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
defmodule URI.FTP do
|
||||
@behavior URI.Parser
|
||||
def default_port(), do: 21
|
||||
def parse(info), do: info
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
defmodule URI.HTTP do
|
||||
@behavior URI.Parser
|
||||
def default_port(), do: 80
|
||||
def parse(info), do: info
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
defmodule URI.HTTPS do
|
||||
@behavior URI.Parser
|
||||
def default_port(), do: 443
|
||||
def parse(info), do: info
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
defmodule URI.LDAP do
|
||||
@behavior URI.Parser
|
||||
def default_port(), do: 389
|
||||
|
||||
# TODO: LDAP specific parsing.
|
||||
def parse(info), do: info
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
defmodule URI.Parser do
|
||||
@moduledoc """
|
||||
Defines the behavior for each URI.Parser.
|
||||
Check URI.HTTP for a possible implementation.
|
||||
"""
|
||||
|
||||
def behaviour_info(:callbacks) do
|
||||
[parse: 1,
|
||||
default_port: 0]
|
||||
end
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
defmodule URI.SFTP do
|
||||
@behavior URI.Parser
|
||||
def default_port(), do: 22
|
||||
def parse(info), do: info
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
defmodule URI.TFTP do
|
||||
@behavior URI.Parser
|
||||
def default_port(), do: 69
|
||||
def parse(info), do: info
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
defmodule N8nOpenaiAdapter.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :n8n_openai_adapter,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.16",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger, :crypto],
|
||||
mod: {N8nOpenaiAdapter.Application, []}
|
||||
]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:bandit, "~> 1.5"},
|
||||
{:plug, "~> 1.16"},
|
||||
{:jason, "~> 1.4"},
|
||||
{:req, "~> 0.5"}
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
%{
|
||||
"bandit": {:hex, :bandit, "1.12.5", "af205a8e550f304caae09a97d29fd3c79a7f337526ea7cd772d2ff11d2f7c800", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "c5684ca062fa407cac115aec3256383f3e2ec9fdced7904d59cf5a7bb7ed6181"},
|
||||
"finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"},
|
||||
"hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"},
|
||||
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mint": {:hex, :mint, "1.10.0", "85af3353bfc504f5bdfe494bd92b8490f87a306dc659ee1ad0af435107e898dc", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "8b16fb72aaa7531d206a1f05e4cc85509ba531ccec7a17a22736c9c95cbb24d1"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"},
|
||||
"req": {:hex, :req, "0.7.4", "23e9ffec17de032a46a4b15ed65c09793893bf4a7c680f4bbf6227fce6bdf74d", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "4b192d63253e8dcc6221ef992ea9ebef7d3555166e8423aa5b553e86bc3c69a2"},
|
||||
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
# NixOS module for the n8n OpenAI-compatible adapter.
|
||||
#
|
||||
# Consume from config-nixos:
|
||||
# imports = [ inputs.n8n-openai-adapter.nixosModules.default ];
|
||||
# services.n8n-openai-adapter = {
|
||||
# enable = true;
|
||||
# domain = "openai.bueso.eu";
|
||||
# port = 8134;
|
||||
# };
|
||||
#
|
||||
# The module defines the systemd service, the service user, and the sops secret
|
||||
# (`n8n_openai_env` carrying ADAPTER_API_KEY + AGENTS). The nginx vhost is NOT
|
||||
# defined here — add it in the consuming config (config-nixos has its own
|
||||
# nginx.nix with ACME/SSL). This keeps the flake self-contained for the service
|
||||
# while the site exposure stays a deployment concern.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.n8n-openai-adapter;
|
||||
in
|
||||
{
|
||||
options.services.n8n-openai-adapter = {
|
||||
enable = lib.mkEnableOption "n8n OpenAI-compatible adapter";
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.n8n-openai-adapter;
|
||||
description = "The n8n-openai-adapter package (from the overlay).";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 8134;
|
||||
description = "Port the adapter HTTP server listens on (nginx proxies to it).";
|
||||
};
|
||||
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Domain the adapter is served under (used for the nginx vhost in the consuming config).";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
sops.secrets.n8n_openai_env = {
|
||||
owner = "n8n-openai";
|
||||
group = "n8n-openai";
|
||||
mode = "0440";
|
||||
};
|
||||
|
||||
systemd.services.n8n-openai-adapter = {
|
||||
description = "OpenAI-compatible adapter for n8n chat agents";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network-online.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
serviceConfig = {
|
||||
ExecStart = "${cfg.package}/bin/n8n_openai_adapter start";
|
||||
EnvironmentFile = "/run/secrets/n8n_openai_env";
|
||||
Environment = [
|
||||
"PORT=${toString cfg.port}"
|
||||
# Agents are managed at runtime via the admin API and persisted here.
|
||||
"AGENTS_FILE=/var/lib/n8n-openai/agents.json"
|
||||
# Elixir release needs a cookie; the release's start script reads
|
||||
# releases/COOKIE which isn't baked in, so set it explicitly.
|
||||
"RELEASE_COOKIE=n8n-openai-adapter"
|
||||
];
|
||||
Restart = "always";
|
||||
RestartSec = 5;
|
||||
User = "n8n-openai";
|
||||
Group = "n8n-openai";
|
||||
StateDirectory = "n8n-openai";
|
||||
};
|
||||
};
|
||||
|
||||
users.users.n8n-openai = {
|
||||
isSystemUser = true;
|
||||
group = "n8n-openai";
|
||||
home = "/var/lib/n8n-openai";
|
||||
createHome = true;
|
||||
};
|
||||
users.groups.n8n-openai = { };
|
||||
};
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{erl_opts, [
|
||||
warn_unused_vars,
|
||||
warn_export_all,
|
||||
warn_shadow_vars,
|
||||
warn_unused_import,
|
||||
warn_unused_function,
|
||||
warn_bif_clash,
|
||||
warn_unused_record,
|
||||
warn_deprecated_function,
|
||||
warn_obsolete_guard,
|
||||
strict_validation,
|
||||
warn_exported_vars,
|
||||
%% warn_export_vars,
|
||||
%% warn_missing_spec,
|
||||
%% warn_untyped_record,
|
||||
%% warnings_as_errors,
|
||||
no_debug_info
|
||||
]}.
|
||||
|
||||
{yrl_opts, [
|
||||
{report, true},
|
||||
{verbose, false}
|
||||
]}.
|
||||
@@ -1,10 +0,0 @@
|
||||
{application, elixir,
|
||||
[{description, "elixir"},
|
||||
{vsn, "0.5.0"},
|
||||
{modules, [
|
||||
elixir
|
||||
]},
|
||||
{registered,[elixir_code_server]},
|
||||
{applications, [kernel,stdlib]},
|
||||
{mod, {elixir,[]}}
|
||||
]}.
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
-module(elixir).
|
||||
-behaviour(application).
|
||||
-export([start_cli/0, start_app/0,
|
||||
scope_for_eval/1, eval/2, eval/3, eval/4,
|
||||
eval_quoted/2, eval_quoted/3, eval_quoted/4,
|
||||
eval_forms/3]).
|
||||
-include("elixir.hrl").
|
||||
|
||||
% OTP APPLICATION API
|
||||
|
||||
-export([start/2, stop/1, config_change/3]).
|
||||
|
||||
start(_Type, _Args) ->
|
||||
%% Set the shell to unicode so printing inside files work
|
||||
io:setopts([{encoding,unicode}]),
|
||||
elixir_sup:start_link([]).
|
||||
|
||||
stop(_S) ->
|
||||
ok.
|
||||
|
||||
config_change(_Changed, _New, _Remove) ->
|
||||
ok.
|
||||
|
||||
%% ELIXIR ENTRY POINTS
|
||||
|
||||
% Start the Elixir app. This is the proper way to boot Elixir from
|
||||
% inside an Erlang process.
|
||||
|
||||
start_app() ->
|
||||
case lists:keyfind(?MODULE, 1, application:loaded_applications()) of
|
||||
false -> application:start(?MODULE);
|
||||
_ -> ok
|
||||
end.
|
||||
|
||||
% Boot and process given options. Invoked by Elixir's script.
|
||||
|
||||
start_cli() ->
|
||||
start_app(),
|
||||
'__MAIN__.Elixir.CLI':process_argv(init:get_plain_arguments()).
|
||||
|
||||
%% EVAL HOOKS
|
||||
|
||||
scope_for_eval(Opts) ->
|
||||
Filename = case orddict:find(file, Opts) of
|
||||
{ ok, F } -> to_char_list(F);
|
||||
error -> "nofile"
|
||||
end,
|
||||
|
||||
Local = case orddict:find(delegate_locals_to, Opts) of
|
||||
{ ok, L } -> L;
|
||||
error -> []
|
||||
end,
|
||||
|
||||
#elixir_scope{filename=Filename,local=Local}.
|
||||
|
||||
%% String evaluation
|
||||
|
||||
eval(String, Binding) -> eval(String, Binding, []).
|
||||
|
||||
eval(String, Binding, Opts) ->
|
||||
case orddict:find(line, Opts) of
|
||||
{ ok, Line } -> [];
|
||||
error -> Line = 1
|
||||
end,
|
||||
eval(String, Binding, Line, scope_for_eval(Opts)).
|
||||
|
||||
eval(String, Binding, Line, #elixir_scope{filename=Filename} = S) ->
|
||||
Forms = elixir_translator:forms(String, Line, Filename),
|
||||
eval_forms(Forms, Binding, S).
|
||||
|
||||
%% Quoted evaluation
|
||||
|
||||
eval_quoted(Tree, Binding) -> eval_quoted(Tree, Binding, []).
|
||||
|
||||
eval_quoted(Tree, Binding, Opts) ->
|
||||
case orddict:find(line, Opts) of
|
||||
{ ok, Line } -> [];
|
||||
error -> Line = 1
|
||||
end,
|
||||
eval_quoted(Tree, Binding, Line, scope_for_eval(Opts)).
|
||||
|
||||
eval_quoted(Tree, Binding, Line, #elixir_scope{} = S) ->
|
||||
eval_forms(elixir_quote:linify(Line, Tree), Binding, S).
|
||||
|
||||
%% Handle forms evaluation internally, it is an
|
||||
%% internal API not meant for external usage.
|
||||
|
||||
eval_forms(Tree, Binding, RawScope) ->
|
||||
Scope = RawScope#elixir_scope{vars=binding_dict(Binding)},
|
||||
{ ParseTree, NewScope } = elixir_translator:translate(Tree, Scope),
|
||||
case ParseTree of
|
||||
[] -> { nil, Binding, NewScope };
|
||||
_ ->
|
||||
{value, Value, NewBinding} = erl_eval:exprs(ParseTree, normalize_binding(Binding)),
|
||||
{Value, final_binding(NewBinding, NewScope#elixir_scope.vars), NewScope }
|
||||
end.
|
||||
|
||||
%% INTERNAL HELPERS
|
||||
|
||||
to_char_list(Bin) when is_binary(Bin) -> binary_to_list(Bin);
|
||||
to_char_list(List) when is_list(List) -> List.
|
||||
|
||||
binding_dict(List) -> binding_dict(List, dict:new()).
|
||||
binding_dict([{H,_}|T], Dict) -> binding_dict(T, dict:store(H, H, Dict));
|
||||
binding_dict([], Dict) -> Dict.
|
||||
|
||||
final_binding(Binding, Vars) -> final_binding(Binding, [], Binding, Vars).
|
||||
final_binding([{Var,_}|T], Acc, Binding, Vars) ->
|
||||
case atom_to_list(Var) of
|
||||
"_EX" ++ _ -> final_binding(T, Acc, Binding, Vars);
|
||||
_ ->
|
||||
RealName = dict:fetch(Var, Vars),
|
||||
RealValue = proplists:get_value(RealName, Binding, nil),
|
||||
final_binding(T, [{Var, RealValue}|Acc], Binding, Vars)
|
||||
end;
|
||||
|
||||
final_binding([], Acc, _Binding, _Vars) -> lists:reverse(Acc).
|
||||
|
||||
normalize_binding(Binding) ->
|
||||
Keyword = orddict:from_list(Binding),
|
||||
case orddict:find('_EXMODULE', Keyword) of
|
||||
{ ok, _ } -> Keyword;
|
||||
_ -> orddict:store('_EXMODULE', nil, Keyword)
|
||||
end.
|
||||
@@ -1,241 +0,0 @@
|
||||
%% Handle code related to rocket args, guard and -> matching
|
||||
%% for case, fn, receive and friends. try is handled in elixir_try.
|
||||
-module(elixir_clauses).
|
||||
-export([
|
||||
assigns/3, assigns_block/5, assigns_block/6, extract_last_guards/1,
|
||||
get_pairs/4, get_pairs/5, match/3, extract_args/1, extract_guards/1]).
|
||||
-import(elixir_variables, [umergec/2]).
|
||||
-include("elixir.hrl").
|
||||
|
||||
%% Get pairs from a clause.
|
||||
|
||||
get_pairs(Line, Key, Clauses, S) ->
|
||||
get_pairs(Line, Key, Clauses, S, false).
|
||||
|
||||
get_pairs(Line, Key, Clauses, S, AllowNil) ->
|
||||
case orddict:find(Key, Clauses) of
|
||||
{ ok, { '->', _, Pairs } } ->
|
||||
[{ Key, Left, Right } || { Left, Right } <- Pairs];
|
||||
{ ok, nil } when AllowNil ->
|
||||
[];
|
||||
{ ok, _ } ->
|
||||
elixir_errors:syntax_error(Line, S#elixir_scope.filename, "expected pairs with -> for key ~s", [Key]);
|
||||
_ ->
|
||||
[]
|
||||
end.
|
||||
|
||||
% Function for translating assigns.
|
||||
|
||||
assigns(Fun, Args, #elixir_scope{assign=false} = S) ->
|
||||
{ Result, NewS } = assigns(Fun, Args, S#elixir_scope{assign=true, temp_vars=dict:new()}),
|
||||
{ Result, NewS#elixir_scope{assign=false} };
|
||||
|
||||
assigns(Fun, Args, S) -> Fun(Args, S).
|
||||
|
||||
%% Function for translating a block that is preceeded by an
|
||||
%% assignment and optional guards. This is used by def* and fn.
|
||||
|
||||
assigns_block(Line, Fun, BareArgs, Exprs, S) ->
|
||||
{ Args, Guards } = extract_guards(BareArgs),
|
||||
assigns_block(Line, Fun, Args, Exprs, Guards, S).
|
||||
|
||||
assigns_block(Line, Fun, Args, Exprs, Guards, S) ->
|
||||
{ TArgs, SA } = elixir_clauses:assigns(Fun, Args, S),
|
||||
{ TExprs, SE } = elixir_translator:translate(Exprs, SA),
|
||||
|
||||
FArgs = listify(TArgs),
|
||||
SG = SA#elixir_scope{guard=true},
|
||||
FGuards = [translate_guard(Line, Guard, SG) || Guard <- Guards],
|
||||
|
||||
% Uncompact expressions from the block.
|
||||
case TExprs of
|
||||
[{ block, _, FExprs }] -> [];
|
||||
_ -> FExprs = TExprs
|
||||
end,
|
||||
|
||||
{ { clause, Line, FArgs, FGuards, FExprs }, SE }.
|
||||
|
||||
% Translate/Extract guards from the given expression.
|
||||
|
||||
translate_guard(Line, Guard, S) ->
|
||||
[element(1, elixir_translator:translate_each(elixir_quote:linify(Line, Guard), S))].
|
||||
|
||||
extract_guards({ 'when', _, [Left, Right] }) ->
|
||||
Clauses = extract_or_clauses(Right, []),
|
||||
case Left of
|
||||
{ 'in', Line, [Var, _] } ->
|
||||
{ Var, [{ 'and', Line, [Left, Clause] } || Clause <- Clauses] };
|
||||
_ ->
|
||||
{ Left, Clauses }
|
||||
end;
|
||||
|
||||
extract_guards({ 'in', _, [Var, _] } = Expr) -> { Var, [Expr] };
|
||||
extract_guards(Else) -> { Else, [] }.
|
||||
|
||||
extract_or_clauses({ 'when', _, [Left, Right] }, Acc) -> extract_or_clauses(Right, [Left|Acc]);
|
||||
extract_or_clauses(Term, Acc) -> [Term|Acc].
|
||||
|
||||
% Extract name and args from the given expression.
|
||||
|
||||
extract_args({ { '.', _, [Name] }, _, Args }) when is_atom(Name), is_list(Args) -> { Name, Args };
|
||||
extract_args({ Name, _, Args }) when is_atom(Name), is_atom(Args) -> { Name, [] };
|
||||
extract_args({ Name, _, Args }) when is_atom(Name), is_list(Args) -> { Name, Args }.
|
||||
|
||||
% Extract guards when it is in the last element of the args
|
||||
|
||||
extract_last_guards([]) -> { [], [] };
|
||||
extract_last_guards(Args) ->
|
||||
{ Left, Right } = elixir_tree_helpers:split_last(Args),
|
||||
{ Bare, Guards } = extract_guards(Right),
|
||||
{ Left ++ [Bare], Guards }.
|
||||
|
||||
% Function for translating macros with match style like case and receive.
|
||||
|
||||
match(Line, DecoupledClauses, RawS) ->
|
||||
S = RawS#elixir_scope{clause_vars=dict:new()},
|
||||
|
||||
case DecoupledClauses of
|
||||
[DecoupledClause] ->
|
||||
{ TDecoupledClause, TS } = each_clause(Line, DecoupledClause, S),
|
||||
{ [TDecoupledClause], TS };
|
||||
_ ->
|
||||
% Transform tree just passing the variables counter forward
|
||||
% and storing variables defined inside each clause.
|
||||
Transformer = fun(X, {Acc, CV}) ->
|
||||
{ TX, TAcc } = each_clause(Line, X, Acc),
|
||||
{ TX, { umergec(S, TAcc), [TAcc#elixir_scope.clause_vars|CV] } }
|
||||
end,
|
||||
|
||||
{ TClauses, { TS, RawCV } } = lists:mapfoldl(Transformer, {S, []}, DecoupledClauses),
|
||||
|
||||
% Now get all the variables defined inside each clause
|
||||
CV = lists:reverse(RawCV),
|
||||
NewVars = lists:umerge([lists:sort(dict:fetch_keys(X)) || X <- CV]),
|
||||
|
||||
case NewVars of
|
||||
[] -> { TClauses, TS };
|
||||
_ ->
|
||||
% Create a new scope that contains a list of all variables
|
||||
% defined inside all the clauses. It returns this new scope and
|
||||
% a list of tuples where the first element is the variable name,
|
||||
% the second one is the new pointer to the variable and the third
|
||||
% is the old pointer.
|
||||
{ FinalVars, FS } = lists:mapfoldl(fun normalize_vars/2, TS, NewVars),
|
||||
|
||||
% Defines a tuple that will be used as left side of the match operator
|
||||
LeftVars = [{var, Line, NewValue} || {_, NewValue,_} <- FinalVars],
|
||||
{ StorageVar, SS } = elixir_variables:build_erl(Line, FS),
|
||||
|
||||
% Expand all clauses by adding a match operation at the end that assigns
|
||||
% variables missing in one clause to the others.
|
||||
Expander = fun(Clause, Counter) ->
|
||||
ClauseVars = lists:nth(Counter, CV),
|
||||
RightVars = [normalize_clause_var(Var, OldValue, ClauseVars) || {Var, _, OldValue} <- FinalVars],
|
||||
|
||||
AssignExpr = generate_match(Line, LeftVars, RightVars),
|
||||
ClauseExprs = element(5, Clause),
|
||||
[Final|RawClauseExprs] = lists:reverse(ClauseExprs),
|
||||
|
||||
% If the last sentence has a match clause, we need to assign its value
|
||||
% in the variable list. If not, we insert the variable list before the
|
||||
% final clause in order to keep it tail call optimized.
|
||||
FinalClauseExprs = case has_match_tuple(Final) of
|
||||
true ->
|
||||
case Final of
|
||||
{ match, _, { var, _, UserVarName } = UserVar, _ } when UserVarName /= '_' ->
|
||||
[UserVar,AssignExpr,Final|RawClauseExprs];
|
||||
_ ->
|
||||
StorageExpr = { match, Line, StorageVar, Final },
|
||||
[StorageVar,AssignExpr,StorageExpr|RawClauseExprs]
|
||||
end;
|
||||
false ->
|
||||
[Final,AssignExpr|RawClauseExprs]
|
||||
end,
|
||||
|
||||
FinalClause = setelement(5, Clause, lists:reverse(FinalClauseExprs)),
|
||||
{ FinalClause, Counter + 1 }
|
||||
end,
|
||||
|
||||
{ FClauses, _ } = lists:mapfoldl(Expander, 1, TClauses),
|
||||
{ FClauses, SS }
|
||||
end
|
||||
end.
|
||||
|
||||
% Handle each key/value clause pair and translate them accordingly.
|
||||
|
||||
each_clause(Line, { do, [Condition], Expr }, S) ->
|
||||
assigns_block(Line, fun elixir_translator:translate_each/2, Condition, [Expr], S);
|
||||
|
||||
each_clause(Line, { 'after', [Condition], Expr }, S) ->
|
||||
{ TCondition, SC } = elixir_translator:translate_each(Condition, S),
|
||||
{ TBody, SB } = elixir_translator:translate([Expr], SC),
|
||||
{ { clause, Line, [TCondition], [], TBody }, SB };
|
||||
|
||||
each_clause(Line, { Key, [_|_], _ }, S) when Key == do; Key == 'after' ->
|
||||
elixir_errors:syntax_error(Line, S#elixir_scope.filename, "too many arguments given for ~s", [Key]);
|
||||
|
||||
each_clause(Line, { Key, _, _ }, S) ->
|
||||
elixir_errors:syntax_error(Line, S#elixir_scope.filename, "invalid key ~s", [Key]).
|
||||
|
||||
% Check if the given expression is a match tuple.
|
||||
% This is a small optimization to allow us to change
|
||||
% existing assignments instead of creating new ones every time.
|
||||
|
||||
has_match_tuple({'receive', _, _, _, _}) ->
|
||||
true;
|
||||
|
||||
has_match_tuple({'receive', _, _}) ->
|
||||
true;
|
||||
|
||||
has_match_tuple({'case', _, _, _}) ->
|
||||
true;
|
||||
|
||||
has_match_tuple({match, _, _, _}) ->
|
||||
true;
|
||||
|
||||
has_match_tuple(H) when is_tuple(H) ->
|
||||
has_match_tuple(tuple_to_list(H));
|
||||
|
||||
has_match_tuple(H) when is_list(H) ->
|
||||
lists:any(fun has_match_tuple/1, H);
|
||||
|
||||
has_match_tuple(_) -> false.
|
||||
|
||||
% Normalize the given var checking its existence in the scope var dictionary.
|
||||
|
||||
normalize_vars(Var, #elixir_scope{vars=Vars, clause_vars=ClauseVars} = S) ->
|
||||
{ { _, _, NewValue }, NS } = elixir_variables:build_erl(0, S),
|
||||
|
||||
FS = NS#elixir_scope{
|
||||
vars=dict:store(Var, NewValue, Vars),
|
||||
clause_vars=dict:store(Var, NewValue, ClauseVars)
|
||||
},
|
||||
|
||||
Expr = case dict:find(Var, Vars) of
|
||||
{ ok, OldValue } -> { var, 0, OldValue };
|
||||
error -> { atom, 0, nil }
|
||||
end,
|
||||
|
||||
{ { Var, NewValue, Expr }, FS }.
|
||||
|
||||
% Normalize a var by checking if it was defined in the clause.
|
||||
% If so, use it, otherwise use from main scope.
|
||||
|
||||
normalize_clause_var(Var, OldValue, ClauseVars) ->
|
||||
case dict:find(Var, ClauseVars) of
|
||||
{ ok, ClauseValue } -> { var, 0, ClauseValue };
|
||||
error -> OldValue
|
||||
end.
|
||||
|
||||
%% generate_match
|
||||
|
||||
generate_match(Line, [Left], [Right]) ->
|
||||
{ match, Line, Left, Right };
|
||||
|
||||
generate_match(Line, LeftVars, RightVars) ->
|
||||
{ match, Line, { tuple, Line, LeftVars }, { tuple, Line, RightVars } }.
|
||||
|
||||
%% Listify
|
||||
|
||||
listify(Expr) when not is_list(Expr) -> [Expr];
|
||||
listify(Expr) -> Expr.
|
||||
@@ -1,64 +0,0 @@
|
||||
-module(elixir_code_server).
|
||||
-export([start_link/0, init/1, handle_call/3, handle_cast/2,
|
||||
handle_info/2, terminate/2, code_change/3]).
|
||||
-behavior(gen_server).
|
||||
-record(elixir_code_server, {
|
||||
argv=[],
|
||||
loaded=[],
|
||||
at_exit=[],
|
||||
compiler_options=[]
|
||||
}).
|
||||
|
||||
start_link() ->
|
||||
{ ok, _ } = gen_server:start_link({local, elixir_code_server}, ?MODULE, [], []).
|
||||
|
||||
init(_args) ->
|
||||
process_flag(trap_exit, true),
|
||||
{ ok, #elixir_code_server{} }.
|
||||
|
||||
handle_call({loaded, Path}, _From, Config) ->
|
||||
Current = Config#elixir_code_server.loaded,
|
||||
case lists:member(Path, Current) of
|
||||
true ->
|
||||
{ reply, duplicated, Config#elixir_code_server{loaded=Current} };
|
||||
false ->
|
||||
{ reply, ok, Config#elixir_code_server{loaded=[Path|Current]} }
|
||||
end;
|
||||
|
||||
handle_call({at_exit, AtExit}, _From, Config) ->
|
||||
{ reply, ok, Config#elixir_code_server{at_exit=[AtExit|Config#elixir_code_server.at_exit]} };
|
||||
|
||||
handle_call({argv, Argv}, _From, Config) ->
|
||||
{ reply, ok, Config#elixir_code_server{argv=Argv} };
|
||||
|
||||
handle_call({compiler_options, Options}, _From, Config) ->
|
||||
{ reply, ok, Config#elixir_code_server{compiler_options=Options} };
|
||||
|
||||
handle_call(loaded, _From, Config) ->
|
||||
{ reply, Config#elixir_code_server.loaded, Config };
|
||||
|
||||
handle_call(at_exit, _From, Config) ->
|
||||
{ reply, Config#elixir_code_server.at_exit, Config };
|
||||
|
||||
handle_call(argv, _From, Config) ->
|
||||
{ reply, Config#elixir_code_server.argv, Config };
|
||||
|
||||
handle_call(compiler_options, _From, Config) ->
|
||||
{ reply, Config#elixir_code_server.compiler_options, Config };
|
||||
|
||||
handle_call(_Request, _From, Config) ->
|
||||
{ reply, undef, Config }.
|
||||
|
||||
handle_cast(_Request, Config) ->
|
||||
{ noreply, Config }.
|
||||
|
||||
handle_info(_Request, Config) ->
|
||||
{ noreply, Config }.
|
||||
|
||||
terminate(Reason, Config) ->
|
||||
io:format("[FATAL] ~p crashed:\n~p~n", [?MODULE, Reason]),
|
||||
io:format("[FATAL] ~p snapshot:\n~p~n", [?MODULE, Config]),
|
||||
ok.
|
||||
|
||||
code_change(_Old, Config, _Extra) ->
|
||||
{ ok, Config }.
|
||||
@@ -1,233 +0,0 @@
|
||||
-module(elixir_compiler).
|
||||
-export([get_opts/0, get_opt/1, get_opt/2, string/2, file/1, file_to_path/2]).
|
||||
-export([core/0, module/3, eval_forms/4]).
|
||||
-include("elixir.hrl").
|
||||
|
||||
%% Public API
|
||||
|
||||
%% Get compilation options.
|
||||
|
||||
get_opt(Key) -> get_opt(Key, get_opts()).
|
||||
|
||||
get_opt(Key, Dict) ->
|
||||
case orddict:find(Key, Dict) of
|
||||
{ ok, Value } -> Value;
|
||||
error -> false
|
||||
end.
|
||||
|
||||
get_opts() ->
|
||||
gen_server:call(elixir_code_server, compiler_options).
|
||||
|
||||
%% Compiles the given string.
|
||||
|
||||
string(Contents, Filename) ->
|
||||
Previous = get(elixir_compiled),
|
||||
|
||||
try
|
||||
put(elixir_compiled, []),
|
||||
Forms = elixir_translator:forms(Contents, 1, Filename),
|
||||
eval_forms(Forms, 1, Filename, #elixir_scope{filename=Filename}),
|
||||
lists:reverse(get(elixir_compiled))
|
||||
after
|
||||
put(elixir_compiled, Previous)
|
||||
end.
|
||||
|
||||
%% Compile a file, return a tuple of module names and binaries.
|
||||
|
||||
file(Relative) ->
|
||||
Filename = filename:absname(Relative),
|
||||
case file:read_file(Filename) of
|
||||
{ok, Bin} ->
|
||||
string(unicode:characters_to_list(Bin), Filename);
|
||||
Error ->
|
||||
erlang:error(Error)
|
||||
end.
|
||||
|
||||
%% Compiles a file to the given path (directory).
|
||||
|
||||
file_to_path(File, Path) ->
|
||||
Lists = file(File),
|
||||
[binary_to_path(X, Path) || X <- Lists],
|
||||
Lists.
|
||||
|
||||
%% Evaluates the contents/forms by compiling them to an Erlang module.
|
||||
|
||||
eval_forms(Forms, Line, Module, #elixir_scope{module=[]} = S) ->
|
||||
eval_forms(Forms, Line, Module, nil, S);
|
||||
|
||||
eval_forms(Forms, Line, Module, #elixir_scope{module=Value} = S) ->
|
||||
eval_forms(Forms, Line, Module, Value, S).
|
||||
|
||||
eval_forms(Forms, Line, RawModule, Value, S) ->
|
||||
case (Value == nil) and allows_fast_compilation(Forms) of
|
||||
true -> eval_compilation(Forms, Line, S);
|
||||
false -> code_loading_compilation(Forms, Line, RawModule, Value, S)
|
||||
end.
|
||||
|
||||
eval_compilation(Forms, _Line, S) ->
|
||||
{ Result, _Binding, FS } = elixir:eval_forms(Forms, [{'_EXMODULE',nil}], S),
|
||||
{ Result, FS }.
|
||||
|
||||
code_loading_compilation(Forms, Line, RawModule, Value, S) ->
|
||||
Module = escape_module(RawModule),
|
||||
{ Exprs, FS } = elixir_translator:translate(Forms, S),
|
||||
ModuleForm = module_form(Exprs, Line, S#elixir_scope.filename, Module),
|
||||
{ module(ModuleForm, S, fun(Mod, _) ->
|
||||
Res = Mod:'BOOTSTRAP'(Value),
|
||||
code:purge(Module),
|
||||
code:delete(Module),
|
||||
Res
|
||||
end), FS }.
|
||||
|
||||
%% Internal API
|
||||
|
||||
%% Compile the module by forms based on the scope information
|
||||
%% executes the callback in case of success. This automatically
|
||||
%% handles errors and warnings. Used by this module and elixir_module.
|
||||
module(Forms, S, Callback) ->
|
||||
Options = case get_opt(debug_info) of
|
||||
true -> [debug_info];
|
||||
_ -> []
|
||||
end,
|
||||
module(Forms, S#elixir_scope.filename, Options, Callback).
|
||||
|
||||
module(Forms, Filename, Options, Callback) ->
|
||||
case compile:forms([no_auto_import()|Forms], [return,{source,Filename}|Options]) of
|
||||
{ok, ModuleName, Binary, Warnings} ->
|
||||
format_warnings(Filename, Warnings),
|
||||
code:load_binary(ModuleName, Filename, Binary),
|
||||
Callback(ModuleName, Binary);
|
||||
{error, Errors, Warnings} ->
|
||||
format_warnings(Filename, Warnings),
|
||||
format_errors(Filename, Errors)
|
||||
end.
|
||||
|
||||
%% Compile core files for bootstrap.
|
||||
%% Invoked from the Makefile.
|
||||
|
||||
core() ->
|
||||
elixir:start_app(),
|
||||
gen_server:call(elixir_code_server, { compiler_options, [{internal,true}] }),
|
||||
[core_file(File) || File <- core_main()],
|
||||
AllLists = [filelib:wildcard(Wildcard) || Wildcard <- core_list()],
|
||||
Files = lists:append(AllLists) -- core_main(),
|
||||
[core_file(File) || File <- '__MAIN__.List':uniq(Files)].
|
||||
|
||||
%% HELPERS
|
||||
|
||||
no_auto_import() ->
|
||||
{ attribute, 0, compile, {
|
||||
no_auto_import, erlang:module_info(exports) } }.
|
||||
|
||||
module_form(Exprs, Line, Filename, Module) ->
|
||||
Args = [{ var, Line, '_EXMODULE'}],
|
||||
|
||||
[
|
||||
{ attribute, Line, file, { Filename, 1 } },
|
||||
{ attribute, Line, module, Module },
|
||||
{ attribute, Line, export, [{ 'BOOTSTRAP',1 }] },
|
||||
{ function, Line, 'BOOTSTRAP', length(Args), [
|
||||
{ clause, Line, Args, [], Exprs }
|
||||
] }
|
||||
].
|
||||
|
||||
%% Fast compilation is available?
|
||||
|
||||
allows_fast_compilation([{defmodule,_,_}|T]) -> allows_fast_compilation(T);
|
||||
allows_fast_compilation([]) -> true;
|
||||
allows_fast_compilation(_) -> false.
|
||||
|
||||
%% Escape the module name, removing slashes, dots,
|
||||
%% so it can be loaded by Erlang.
|
||||
|
||||
escape_module(Module) when is_atom(Module) ->
|
||||
escape_module(atom_to_list(Module));
|
||||
|
||||
escape_module(Module) when is_list(Module) ->
|
||||
list_to_atom(escape_each(Module)).
|
||||
|
||||
escape_each([H|T]) when H >= $A, H =< $Z; H >= $a, H =< $z; H >= $0, H =< $9; H == $- ->
|
||||
[H|escape_each(T)];
|
||||
|
||||
escape_each([_|T]) ->
|
||||
[$_|escape_each(T)];
|
||||
|
||||
escape_each([]) -> [].
|
||||
|
||||
%% Receives a module Binary and outputs it in the given path.
|
||||
|
||||
binary_to_path({ModuleName, Binary}, CompilePath) ->
|
||||
Path = make_dir(CompilePath, atom_to_list(ModuleName), []),
|
||||
ok = file:write_file(Path, Binary),
|
||||
Path.
|
||||
|
||||
%% Loops through a module name creating the directories
|
||||
%% in the destination. Returns the final filename with .beam.
|
||||
|
||||
make_dir(Current, [$.|T], Buffer) ->
|
||||
NewCurrent = filename:join(Current, lists:reverse(Buffer)),
|
||||
case file:make_dir(NewCurrent) of
|
||||
{ error, eexist } -> [];
|
||||
ok -> []
|
||||
end,
|
||||
make_dir(NewCurrent, T, []);
|
||||
|
||||
make_dir(Current, [H|T], Buffer) ->
|
||||
make_dir(Current, T, [H|Buffer]);
|
||||
|
||||
make_dir(Current, [], Buffer) ->
|
||||
filename:join(Current, lists:reverse(Buffer) ++ ".beam").
|
||||
|
||||
%% CORE FILES COMPILATION
|
||||
|
||||
core_file(File) ->
|
||||
try
|
||||
Lists = file(File),
|
||||
[binary_to_path(X, "ebin") || X <- Lists],
|
||||
io:format("Compiled ~s~n", [File])
|
||||
catch
|
||||
Kind:Reason ->
|
||||
io:format("~p: ~p~nstacktrace: ~p~n", [Kind, Reason, erlang:get_stacktrace()]),
|
||||
exit(1)
|
||||
end.
|
||||
|
||||
core_list() ->
|
||||
[
|
||||
"lib/uri/parser.ex",
|
||||
"lib/elixir/formatter.ex",
|
||||
"lib/dict.ex",
|
||||
"lib/*/*.ex",
|
||||
"lib/*.ex"
|
||||
].
|
||||
|
||||
core_main() ->
|
||||
[
|
||||
"lib/elixir/builtin.ex",
|
||||
"lib/module.ex",
|
||||
"lib/keyword.ex",
|
||||
"lib/list.ex",
|
||||
"lib/protocol.ex",
|
||||
"lib/enum.ex",
|
||||
"lib/macro.ex",
|
||||
"lib/record.ex",
|
||||
"lib/exception.ex",
|
||||
"lib/binary/inspect.ex",
|
||||
"lib/binary/chars.ex",
|
||||
"lib/list/chars.ex",
|
||||
"lib/gen_server/behavior.ex"
|
||||
].
|
||||
|
||||
%% ERROR HANDLING
|
||||
|
||||
format_errors(_Filename, []) ->
|
||||
exit({nocompile, "compilation failed but no error was raised"});
|
||||
|
||||
format_errors(Filename, Errors) ->
|
||||
lists:foreach(fun ({_, Each}) ->
|
||||
lists:foreach(fun (Error) -> elixir_errors:handle_file_error(Filename, Error) end, Each)
|
||||
end, Errors).
|
||||
|
||||
format_warnings(Filename, Warnings) ->
|
||||
lists:foreach(fun ({_, Each}) ->
|
||||
lists:foreach(fun (Warning) -> elixir_errors:handle_file_warning(Filename, Warning) end, Each)
|
||||
end, Warnings).
|
||||
@@ -1,290 +0,0 @@
|
||||
% Holds the logic responsible for functions definition (def(p) and defmacro(p)).
|
||||
-module(elixir_def).
|
||||
-export([table/1,
|
||||
build_table/1,
|
||||
delete_table/1,
|
||||
reset_last/1,
|
||||
wrap_definition/7,
|
||||
store_definition/8,
|
||||
store_each/8,
|
||||
unwrap_stored_definitions/1,
|
||||
format_error/1]).
|
||||
-include("elixir.hrl").
|
||||
|
||||
%% Table management functions. Called internally.
|
||||
|
||||
table(Module) -> ?ELIXIR_ATOM_CONCAT([f, Module]).
|
||||
|
||||
build_table(Module) ->
|
||||
FunctionTable = table(Module),
|
||||
ets:new(FunctionTable, [set, named_table, public]),
|
||||
ets:insert(FunctionTable, { last, [] }),
|
||||
FunctionTable.
|
||||
|
||||
delete_table(Module) ->
|
||||
ets:delete(table(Module)).
|
||||
|
||||
%% Reset the last item. Useful when evaling code.
|
||||
reset_last(Module) ->
|
||||
ets:insert(table(Module), { last, [] }).
|
||||
|
||||
%% Wraps the function into a call to store_definition once the function
|
||||
%% definition is read. The function is compiled into a meta tree to ensure
|
||||
%% we will receive the full function.
|
||||
%%
|
||||
%% We need to wrap functions instead of eagerly defining them to ensure
|
||||
%% functions inside branches won't propagate, for example:
|
||||
%%
|
||||
%% if false do
|
||||
%% def bar, do: 1
|
||||
%% else
|
||||
%% def bar, do: 2
|
||||
%% end
|
||||
%%
|
||||
%% If we just analyzed the compiled structure (i.e. the function availables
|
||||
%% before evaluating the function body), we would see both definitions.
|
||||
wrap_definition(Kind, Line, Name, Args, Guards, Expr, S) ->
|
||||
MetaS = elixir_variables:serialize_scope(S),
|
||||
|
||||
Invoke = [
|
||||
{atom, Line, Kind},
|
||||
{integer, Line, Line},
|
||||
{var, Line, '_EXMODULE'},
|
||||
Name,
|
||||
Args,
|
||||
Guards,
|
||||
Expr,
|
||||
MetaS
|
||||
],
|
||||
|
||||
?ELIXIR_WRAP_CALL(Line, ?MODULE, store_definition, Invoke).
|
||||
|
||||
% Invoked by the wrap definition with the function abstract tree.
|
||||
% Each function is then added to the function table.
|
||||
|
||||
store_definition(Kind, Line, nil, _Name, _Args, _Guards, _Expr, RawS) ->
|
||||
S = elixir_variables:deserialize_scope(RawS),
|
||||
elixir_errors:syntax_error(Line, S#elixir_scope.filename, "cannot define function outside module, invalid scope for ~s", [Kind]);
|
||||
|
||||
store_definition(Kind, Line, Module, Name, Args, Guards, RawExpr, RawS) ->
|
||||
Arity = length(Args),
|
||||
|
||||
DS = elixir_variables:deserialize_scope(RawS),
|
||||
S = DS#elixir_scope{function={Name,Arity}, module=Module},
|
||||
|
||||
case RawExpr of
|
||||
skip_definition -> Expr = nil;
|
||||
[{ do, Expr }] -> [];
|
||||
_ -> Expr = { 'try', Line, [RawExpr] }
|
||||
end,
|
||||
|
||||
{ Function, Defaults, TS } = translate_definition(Kind, Line, Name, Args, Guards, Expr, S),
|
||||
|
||||
Filename = TS#elixir_scope.filename,
|
||||
FunctionTable = table(Module),
|
||||
|
||||
CO = elixir_compiler:get_opts(),
|
||||
compile_docs(Kind, Line, Module, Name, Arity, TS, CO),
|
||||
|
||||
Location = retrieve_file(Module, CO),
|
||||
Stack = S#elixir_scope.macro,
|
||||
|
||||
%% Store function
|
||||
case RawExpr of
|
||||
skip_definition -> [];
|
||||
_ ->
|
||||
compile_super(Module, TS),
|
||||
CheckClauses = S#elixir_scope.check_clauses,
|
||||
store_each(CheckClauses, Kind, Filename, Location,
|
||||
Stack, FunctionTable, length(Defaults), Function)
|
||||
end,
|
||||
|
||||
%% Store defaults
|
||||
[store_each(false, Kind, Filename, Location, Stack, FunctionTable, 0,
|
||||
function_for_clause(Name, Default)) || Default <- Defaults],
|
||||
|
||||
{ Name, Arity }.
|
||||
|
||||
%% Compile the documentation related to the module.
|
||||
|
||||
compile_super(Module, #elixir_scope{function=Function, super=true}) ->
|
||||
elixir_def_overridable:store(Module, Function, true);
|
||||
|
||||
compile_super(_Module, _S) -> [].
|
||||
|
||||
compile_docs(Kind, Line, Module, Name, Arity, S, CO) ->
|
||||
case elixir_compiler:get_opt(internal, CO) of
|
||||
true -> [];
|
||||
_ ->
|
||||
case '__MAIN__.Module':compile_doc(Module, Line, Kind, { Name, Arity }) of
|
||||
{ error, Message } -> elixir_errors:handle_file_warning(S#elixir_scope.filename,
|
||||
{ Line, ?MODULE, { Message, { Name, Arity } } });
|
||||
_ -> []
|
||||
end
|
||||
end.
|
||||
|
||||
retrieve_file(Module, CO) ->
|
||||
case elixir_compiler:get_opt(internal, CO) of
|
||||
true -> [];
|
||||
_ ->
|
||||
case '__MAIN__.Module':read_data(Module, file) of
|
||||
nil -> [];
|
||||
Else ->
|
||||
'__MAIN__.Module':merge_data(Module, [{file,nil}]),
|
||||
if
|
||||
is_binary(Else) -> { Else, 1 };
|
||||
is_tuple(Else) -> Else
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
%% Translate the given call and expression given
|
||||
%% and then store it in memory.
|
||||
|
||||
translate_definition(Kind, Line, Name, RawArgs, RawGuards, Expr, S) ->
|
||||
{ Args, Guards } = lists:mapfoldl(fun
|
||||
({ 'in', _, [Left, _] } = X, Acc) ->
|
||||
{ Left, add_to_guards(Line, X, Acc) };
|
||||
(X, Acc) ->
|
||||
{ X, Acc }
|
||||
end, RawGuards, RawArgs),
|
||||
|
||||
Arity = length(Args),
|
||||
{ Unpacked, Defaults } = elixir_def_defaults:unpack(Kind, Name, Args, S),
|
||||
|
||||
{ TClause, TS } = elixir_clauses:assigns_block(Line,
|
||||
fun elixir_translator:translate/2, Unpacked, [Expr], Guards, S),
|
||||
|
||||
FClause = case TS#elixir_scope.name_args of
|
||||
true ->
|
||||
FArgs = elixir_def_overridable:assign_args(Line, element(3, TClause), TS),
|
||||
setelement(3, TClause, FArgs);
|
||||
false -> TClause
|
||||
end,
|
||||
|
||||
Function = { function, Line, Name, Arity, [FClause] },
|
||||
{ Function, Defaults, TS }.
|
||||
|
||||
% Unwrap the functions stored in the functions table.
|
||||
% It returns a list of all functions to be exported, plus the macros,
|
||||
% and the body of all functions.
|
||||
unwrap_stored_definitions(Module) ->
|
||||
Table = table(Module),
|
||||
ets:delete(Table, last),
|
||||
unwrap_stored_definition(ets:tab2list(Table), [], [], [], [], [], {[],[]}).
|
||||
|
||||
unwrap_stored_definition([Fun|T], Exports, Private, Def, Defmacro, Defmacrop, Functions) when element(2, Fun) == def ->
|
||||
Tuple = element(1, Fun),
|
||||
unwrap_stored_definition(
|
||||
T, [Tuple|Exports], Private, [Tuple|Def], Defmacro, Defmacrop,
|
||||
function_for_stored_definition(Fun, Functions)
|
||||
);
|
||||
|
||||
unwrap_stored_definition([Fun|T], Exports, Private, Def, Defmacro, Defmacrop, Functions) when element(2, Fun) == defmacro ->
|
||||
{ Name, Arity } = Tuple = element(1, Fun),
|
||||
Macro = { ?ELIXIR_MACRO(Name), Arity },
|
||||
|
||||
unwrap_stored_definition(
|
||||
T, [Macro|Exports], Private, Def, [Tuple|Defmacro], Defmacrop,
|
||||
function_for_stored_definition(setelement(1, Fun, Macro), Functions)
|
||||
);
|
||||
|
||||
unwrap_stored_definition([Fun|T], Exports, Private, Def, Defmacro, Defmacrop, Functions) when element(2, Fun) == defp ->
|
||||
unwrap_stored_definition(
|
||||
T, Exports, [element(1, Fun)|Private], Def, Defmacro, Defmacrop,
|
||||
function_for_stored_definition(Fun, Functions)
|
||||
);
|
||||
|
||||
unwrap_stored_definition([Fun|T], Exports, Private, Def, Defmacro, Defmacrop, Functions) when element(2, Fun) == defmacrop ->
|
||||
unwrap_stored_definition(
|
||||
T, Exports, [element(1, Fun)|Private], Def, Defmacro,
|
||||
[{ element(1, Fun), element(3, Fun) }|Defmacrop], Functions
|
||||
);
|
||||
|
||||
unwrap_stored_definition([], Exports, Private, Def, Defmacro, Defmacrop, {Functions,Tail}) ->
|
||||
{ Exports, Private, Def, Defmacro, Defmacrop, lists:reverse(Tail ++ Functions) }.
|
||||
|
||||
%% Helpers
|
||||
|
||||
function_for_stored_definition({{Name, Arity}, _, Line, _, [], _, _, Clauses}, {Functions,Tail}) ->
|
||||
{
|
||||
[{ function, Line, Name, Arity, lists:reverse(Clauses) }|Functions],
|
||||
Tail
|
||||
};
|
||||
|
||||
function_for_stored_definition({{Name, Arity}, _, Line, _, Location, _, _, Clauses}, {Functions,Tail}) ->
|
||||
{
|
||||
Functions,
|
||||
[
|
||||
{ function, Line, Name, Arity, lists:reverse(Clauses) },
|
||||
{ attribute, Line, file, Location } | Tail
|
||||
]
|
||||
}.
|
||||
|
||||
function_for_clause(Name, { clause, Line, Args, _Guards, _Exprs } = Clause) ->
|
||||
{ function, Line, Name, length(Args), [Clause] }.
|
||||
|
||||
%% Store each definition in the table.
|
||||
%% This function also checks and emit warnings in case
|
||||
%% the kind, of the visibility of the function changes.
|
||||
|
||||
store_each(Check, Kind, Filename, Location, Stack, Table, Defaults, {function, Line, Name, Arity, Clauses}) ->
|
||||
case ets:lookup(Table, {Name, Arity}) of
|
||||
[{{Name, Arity}, StoredKind, _, _, StoredLocation, StoredStack, StoredDefaults, StoredClauses}] ->
|
||||
FinalLocation = StoredLocation,
|
||||
FinalDefaults = Defaults + StoredDefaults,
|
||||
FinalClauses = Clauses ++ StoredClauses,
|
||||
check_valid_kind(Line, Filename, Name, Arity, Kind, StoredKind),
|
||||
check_valid_defaults(Line, Filename, Name, Arity, Defaults),
|
||||
Check andalso (Stack == StoredStack) andalso check_valid_clause(Line, Filename, Name, Arity, Table);
|
||||
[] ->
|
||||
FinalLocation = Location,
|
||||
FinalDefaults = Defaults,
|
||||
FinalClauses = Clauses,
|
||||
Check andalso ets:insert(Table, { last, { Name, Arity } })
|
||||
end,
|
||||
ets:insert(Table, {{Name, Arity}, Kind, Line, Filename, FinalLocation, Stack, FinalDefaults, FinalClauses}).
|
||||
|
||||
%% Validations
|
||||
|
||||
check_valid_kind(_Line, _Filename, _Name, _Arity, Kind, Kind) -> [];
|
||||
check_valid_kind(Line, Filename, Name, Arity, Kind, StoredKind) ->
|
||||
elixir_errors:form_error(Line, Filename, ?MODULE,
|
||||
{ changed_kind, { Name, Arity, StoredKind, Kind } }).
|
||||
|
||||
check_valid_clause(Line, Filename, Name, Arity, Table) ->
|
||||
case ets:lookup_element(Table, last, 2) of
|
||||
{Name,Arity} -> [];
|
||||
[] -> [];
|
||||
{ElseName, ElseArity} ->
|
||||
elixir_errors:handle_file_warning(Filename, { Line, ?MODULE,
|
||||
{ changed_clause, { { Name, Arity }, { ElseName, ElseArity } } } })
|
||||
end.
|
||||
|
||||
check_valid_defaults(_Line, _Filename, _Name, _Arity, 0) -> [];
|
||||
check_valid_defaults(Line, Filename, Name, Arity, _) ->
|
||||
elixir_errors:handle_file_warning(Filename, { Line, ?MODULE, { clauses_with_docs, { Name, Arity } } }).
|
||||
|
||||
%% Helpers
|
||||
|
||||
add_to_guards(_Line, Expr, []) ->
|
||||
[Expr];
|
||||
add_to_guards(Line, Expr, Clauses) ->
|
||||
[{ 'and', Line, [Expr, Clause] } || Clause <- Clauses].
|
||||
|
||||
%% Format errors
|
||||
|
||||
format_error({clauses_with_docs,{Name,Arity}}) ->
|
||||
io_lib:format("function ~s/~B has default values and multiple clauses, it is recommended to use a separate clause for declaring defalts", [Name, Arity]);
|
||||
|
||||
format_error({private_doc,{Name,Arity}}) ->
|
||||
io_lib:format("function ~s/~B is private, @doc's are always discarded for private functions", [Name, Arity]);
|
||||
|
||||
format_error({existing_doc,{Name,Arity}}) ->
|
||||
io_lib:format("@doc's for function ~s/~B have been given more than once, the first version is being kept", [Name, Arity]);
|
||||
|
||||
format_error({changed_clause,{{Name,Arity},{ElseName,ElseArity}}}) ->
|
||||
io_lib:format("function ~s/~B does not match previous clause ~s/~B", [Name, Arity, ElseName, ElseArity]);
|
||||
|
||||
format_error({changed_kind,{Name,Arity,Previous,Current}}) ->
|
||||
io_lib:format("~s ~s/~B already defined as ~s", [Current, Name, Arity, Previous]).
|
||||
@@ -1,59 +0,0 @@
|
||||
% Handle default clauses for function definitions.
|
||||
-module(elixir_def_defaults).
|
||||
-export([unpack/4]).
|
||||
-include("elixir.hrl").
|
||||
|
||||
% Unpack default args from the given clause. Invoked by elixir_translate.
|
||||
unpack(Kind, Name, Args, S) ->
|
||||
unpack_each(Kind, Name, Args, [], [], S).
|
||||
|
||||
%% Helpers
|
||||
|
||||
%% Unpack default from given args.
|
||||
%% Returns the given arguments without their default
|
||||
%% clauses and a list of clauses for the default calls.
|
||||
unpack_each(Kind, Name, [{'//', Line, [Expr, _]}|T] = List, Acc, Clauses, S) ->
|
||||
Base = build_match(Acc, Line, []),
|
||||
{ Args, Invoke } = extract_defaults(List, [], []),
|
||||
|
||||
SM = S#elixir_scope{counter=length(Base)},
|
||||
{ DefArgs, SA } = elixir_clauses:assigns(fun elixir_translator:translate/2, Base ++ Args, SM),
|
||||
{ InvokeArgs, _ } = elixir_translator:translate_args(Base ++ Invoke, SA),
|
||||
|
||||
Call = { call, Line,
|
||||
{ atom, Line, name_for_kind(Kind, Name) },
|
||||
InvokeArgs
|
||||
},
|
||||
|
||||
Clause = { clause, Line, DefArgs, [], [Call] },
|
||||
unpack_each(Kind, Name, T, [Expr|Acc], [Clause|Clauses], S);
|
||||
|
||||
unpack_each(Kind, Name, [H|T], Acc, Clauses, S) ->
|
||||
unpack_each(Kind, Name, T, [H|Acc], Clauses, S);
|
||||
|
||||
unpack_each(_Kind, _Name, [], Acc, Clauses, _S) ->
|
||||
{ lists:reverse(Acc), lists:reverse(Clauses) }.
|
||||
|
||||
% Extract default values from args following the current default clause.
|
||||
|
||||
extract_defaults([{'//', _, [_Expr, Default]}|T], NewArgs, NewInvoke) ->
|
||||
extract_defaults(T, NewArgs, [Default|NewInvoke]);
|
||||
|
||||
extract_defaults([H|T], NewArgs, NewInvoke) ->
|
||||
extract_defaults(T, [H|NewArgs], [H|NewInvoke]);
|
||||
|
||||
extract_defaults([], NewArgs, NewInvoke) ->
|
||||
{ lists:reverse(NewArgs), lists:reverse(NewInvoke) }.
|
||||
|
||||
% Build matches for all the previous argument until the current default clause.
|
||||
|
||||
build_match([], _Line, Acc) -> Acc;
|
||||
|
||||
build_match([_|T], Line, Acc) ->
|
||||
Var = { ?ELIXIR_ATOM_CONCAT(["_EX", length(T)]), Line, nil },
|
||||
build_match(T, Line, [Var|Acc]).
|
||||
|
||||
% Given the invoked function name based on the kind
|
||||
|
||||
name_for_kind(Kind, Name) when Kind == defmacro; Kind == defmacrop -> ?ELIXIR_MACRO(Name);
|
||||
name_for_kind(_Kind, Name) -> Name.
|
||||
@@ -1,70 +0,0 @@
|
||||
%% Module responsible for local invocation of macros and functions.
|
||||
-module(elixir_def_local).
|
||||
-export([
|
||||
macro_for/3,
|
||||
function_for/3,
|
||||
format_error/1,
|
||||
check_unused_local_macros/3
|
||||
]).
|
||||
-include("elixir.hrl").
|
||||
|
||||
%% Reading
|
||||
|
||||
macro_for(_Tuple, _All, #elixir_scope{module=[]}) -> false;
|
||||
|
||||
macro_for(Tuple, All, #elixir_scope{module=Module}) ->
|
||||
case ets:lookup(elixir_def:table(Module), Tuple) of
|
||||
[{Tuple, Kind, Line, _, _, _, _, Clauses}] when Kind == defmacro; All, Kind == defmacrop ->
|
||||
RewrittenClauses = [rewrite_clause(Clause, Module) || Clause <- Clauses],
|
||||
Fun = { 'fun', Line, {clauses, lists:reverse(RewrittenClauses)} },
|
||||
{ value, Result, _Binding } = erl_eval:exprs([Fun], []),
|
||||
Result;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
function_for(Module, Name, Arity) ->
|
||||
Tuple = { Name, Arity },
|
||||
case ets:lookup(elixir_def:table(Module), Tuple) of
|
||||
[{Tuple, _, Line, _, _, _, _, Clauses}] ->
|
||||
% elixir_def_local:record(Line, Tuple, false, Module),
|
||||
RewrittenClauses = [rewrite_clause(Clause, Module) || Clause <- Clauses],
|
||||
Fun = { 'fun', Line, {clauses, lists:reverse(RewrittenClauses)} },
|
||||
{ value, Result, _Binding } = erl_eval:exprs([Fun], []),
|
||||
Result;
|
||||
_ ->
|
||||
[_|T] = erlang:get_stacktrace(),
|
||||
erlang:raise(error, undef, [{Module,Name,Arity,[]}|T])
|
||||
end.
|
||||
|
||||
%% Helpers
|
||||
%% TODO: Consider caching functions in a table for performance.
|
||||
|
||||
rewrite_clause({ call, Line, { atom, Line, RawName }, Args }, Module) ->
|
||||
Remote = { remote, Line,
|
||||
{ atom, Line, ?MODULE },
|
||||
{ atom, Line, function_for }
|
||||
},
|
||||
Arity = { integer, Line, length(Args) },
|
||||
Name = { atom, Line, rewrite_name(atom_to_list(RawName), RawName) },
|
||||
FunCall = { call, Line, Remote, [{ atom, Line, Module }, Name, Arity] },
|
||||
{ call, Line, FunCall, Args };
|
||||
|
||||
rewrite_clause(Tuple, Module) when is_tuple(Tuple) ->
|
||||
list_to_tuple(rewrite_clause(tuple_to_list(Tuple), Module));
|
||||
|
||||
rewrite_clause(List, Module) when is_list(List) ->
|
||||
[rewrite_clause(Item, Module) || Item <- List];
|
||||
|
||||
rewrite_clause(Else, _) -> Else.
|
||||
|
||||
rewrite_name("MACRO-" ++ Rest, _) -> list_to_atom(Rest);
|
||||
rewrite_name(_, Name) -> Name.
|
||||
|
||||
%% Error handling
|
||||
|
||||
check_unused_local_macros(Filename, Recorded, PMacros) ->
|
||||
[elixir_errors:handle_file_warning(Filename,
|
||||
{ Line, ?MODULE, { unused_macro, Fun } }) || { Fun, Line } <- PMacros, not lists:member(Fun, Recorded)].
|
||||
|
||||
format_error({unused_macro,{Name, Arity}}) ->
|
||||
io_lib:format("macro ~s/~B is unused", [Name, Arity]).
|
||||
@@ -1,101 +0,0 @@
|
||||
% Holds the logic responsible for defining overridable functions and handling super.
|
||||
-module(elixir_def_overridable).
|
||||
-export([define/3, store_pending/1, is_defined/2, ensure_defined/4,
|
||||
assign_args/3, retrieve_args/3, name/2, store/3, format_error/1]).
|
||||
-include("elixir.hrl").
|
||||
|
||||
overridable(Module) ->
|
||||
ets:lookup_element(elixir_module:data_table(Module), overridable, 2).
|
||||
|
||||
overridable(Module, Value) ->
|
||||
ets:insert(elixir_module:data_table(Module), { overridable, Value }).
|
||||
|
||||
%% Add new function to the overridable dictionary.
|
||||
|
||||
define(Module, Tuple, Args) ->
|
||||
Old = overridable(Module),
|
||||
New = [{ Tuple, { 1, [Args] } }],
|
||||
Abstract = orddict:merge(fun(_K, { Count, V1 }, _V2) -> { Count + 1, [Args|V1] } end, Old, New),
|
||||
overridable(Module, Abstract).
|
||||
|
||||
%% Check if an overridable function is defined.
|
||||
|
||||
is_defined(Module, Tuple) ->
|
||||
Overridable = overridable(Module),
|
||||
case orddict:find(Tuple, Overridable) of
|
||||
{ ok, { _, [_|_] } } -> true;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
ensure_defined(Line, Module, Tuple, S) ->
|
||||
case elixir_def_overridable:is_defined(Module, Tuple) of
|
||||
true -> [];
|
||||
_ -> elixir_errors:form_error(Line, S#elixir_scope.filename, ?MODULE, { no_super, Module, Tuple })
|
||||
end.
|
||||
|
||||
%% Retrieve args defined for the given arity.
|
||||
|
||||
retrieve_args(Line, Arity, S) ->
|
||||
{
|
||||
[ { var, Line, super_arg(X) } || X <- lists:seq(1, Arity) ],
|
||||
S#elixir_scope{name_args=true}
|
||||
}.
|
||||
|
||||
%% Assign pseudo variables to the given vars.
|
||||
|
||||
assign_args(Line, Args, S) ->
|
||||
{ FArgs, _ } = lists:mapfoldl(fun(X, Acc) -> assign_args(Line, X, Acc, S) end, 1, Args),
|
||||
FArgs.
|
||||
|
||||
assign_args(Line, X, Acc, _) ->
|
||||
Match = { match, Line, X, { var, Line, super_arg(Acc) } },
|
||||
{ Match, Acc + 1 }.
|
||||
|
||||
super_arg(Counter) ->
|
||||
?ELIXIR_ATOM_CONCAT(['_EXS', Counter]).
|
||||
|
||||
%% Gets the name based on the function and stored overridables
|
||||
|
||||
name(Module, Function) ->
|
||||
name(Module, Function, overridable(Module)).
|
||||
|
||||
name(_Module, { Name, _ } = Function, Overridable) ->
|
||||
{ Count, _ } = orddict:fetch(Function, Overridable),
|
||||
?ELIXIR_ATOM_CONCAT(["OVERRIDABLE-", Count, "-", Name]).
|
||||
|
||||
%% Store
|
||||
|
||||
store(Module, Function, GenerateName) ->
|
||||
Overridable = overridable(Module),
|
||||
{ Count, [H|T] } = orddict:fetch(Function, Overridable),
|
||||
overridable(Module, orddict:store(Function, { Count, T }, Overridable)),
|
||||
|
||||
{ { Name, Arity }, Kind, Line, Filename, Location, Stack, Defaults, Clauses } = H,
|
||||
|
||||
{ FinalKind, FinalName } = case GenerateName of
|
||||
true -> { defp, name(Module, Function, Overridable) };
|
||||
false -> { Kind, Name }
|
||||
end,
|
||||
|
||||
Def = { function, Line, FinalName, Arity, Clauses },
|
||||
elixir_def:store_each(false, FinalKind, Filename, Location,
|
||||
Stack, elixir_def:table(Module), Defaults, Def).
|
||||
|
||||
%% Store pending declarations that were not manually made concrete.
|
||||
|
||||
store_pending(Module) ->
|
||||
[store(Module, X, false) || { X, { _, [_|_] } } <- overridable(Module),
|
||||
not '__MAIN__.Module':'function_defined?'(Module, X)].
|
||||
|
||||
%% Error handling
|
||||
|
||||
format_error({ no_super, Module, { Name, Arity } }) ->
|
||||
Bins = [ format_fa(X) || { X, { _, [_|_] } } <- overridable(Module)],
|
||||
Joined = '__MAIN__.Enum':join(Bins, <<", ">>),
|
||||
io_lib:format("no super defined for ~s/~B in module ~p. Overridable functions available are: ~s",
|
||||
[Name, Arity, elixir_errors:inspect(Module), Joined]).
|
||||
|
||||
format_fa({ Name, Arity }) ->
|
||||
A = atom_to_binary(Name, utf8),
|
||||
B = list_to_binary(integer_to_list(Arity)),
|
||||
<< A/binary, $/, B/binary >>.
|
||||
@@ -1,346 +0,0 @@
|
||||
%% Helpers related to dispatching to imports and references.
|
||||
%% This module access the information stored on the scope
|
||||
%% by elixir_import and therefore assumes it is normalized (ordsets)
|
||||
-module(elixir_dispatch).
|
||||
-export([default_macros/0, default_functions/0, default_requires/0,
|
||||
dispatch_require/6, dispatch_imports/5,
|
||||
require_function/5, import_function/4,
|
||||
format_error/1]).
|
||||
-include("elixir.hrl").
|
||||
-import(ordsets, [is_element/2]).
|
||||
-define(BUILTIN, '__MAIN__.Elixir.Builtin').
|
||||
|
||||
default_functions() ->
|
||||
[ { ?BUILTIN, ordsets:union(in_elixir_functions(), in_erlang_functions()) } ].
|
||||
default_macros() ->
|
||||
[ { ?BUILTIN, ordsets:union(in_elixir_macros(), in_erlang_macros()) } ].
|
||||
default_requires() ->
|
||||
[ ?BUILTIN ].
|
||||
|
||||
%% Function retrieval
|
||||
|
||||
import_function(Line, Name, Arity, S) ->
|
||||
Tuple = { Name, Arity },
|
||||
case find_dispatch(Tuple, S#elixir_scope.functions) of
|
||||
false ->
|
||||
case find_dispatch(Tuple, S#elixir_scope.macros) of
|
||||
false -> { { 'fun', Line, { function, Name, Arity } }, S };
|
||||
_ -> false
|
||||
end;
|
||||
Receiver ->
|
||||
elixir_import:record(import, Tuple, Receiver, S),
|
||||
remote_function(Line, Receiver, Name, Arity, S)
|
||||
end.
|
||||
|
||||
require_function(Line, Receiver, Name, Arity, S) ->
|
||||
Tuple = { Name, Arity },
|
||||
|
||||
case is_element(Tuple, get_optional_macros(Receiver)) of
|
||||
true -> false;
|
||||
false -> remote_function(Line, Receiver, Name, Arity, S)
|
||||
end.
|
||||
|
||||
%% Dispatch based on scope's imports
|
||||
|
||||
dispatch_imports(Line, Name, Args, S, Callback) ->
|
||||
Arity = length(Args),
|
||||
Tuple = { Name, Arity },
|
||||
case find_dispatch(Tuple, S#elixir_scope.functions) of
|
||||
false ->
|
||||
case find_dispatch(Tuple, S#elixir_scope.macros) of
|
||||
false ->
|
||||
Fun = (S#elixir_scope.function /= Tuple) andalso elixir_def_local:macro_for(Tuple, true, S),
|
||||
case Fun of
|
||||
false -> Callback();
|
||||
_ ->
|
||||
Receiver = S#elixir_scope.module,
|
||||
elixir_import:record(import, Tuple, Receiver, S),
|
||||
dispatch_macro_fun(Line, Fun, Receiver, Name, Arity, Args, S)
|
||||
end;
|
||||
?BUILTIN ->
|
||||
elixir_import:record(import, Tuple, ?BUILTIN, S),
|
||||
dispatch_builtin_macro(Line, Tuple, Args, S);
|
||||
Receiver ->
|
||||
elixir_import:record(import, Tuple, Receiver, S),
|
||||
dispatch_macro(Line, Receiver, Name, Arity, Args, S)
|
||||
end;
|
||||
Receiver ->
|
||||
elixir_import:record(import, Tuple, Receiver, S),
|
||||
Endpoint = case (Receiver == ?BUILTIN) andalso is_element(Tuple, in_erlang_functions()) of
|
||||
true -> erlang;
|
||||
false -> Receiver
|
||||
end,
|
||||
elixir_translator:translate_each({ { '.', Line, [Endpoint, Name] }, Line, Args }, S)
|
||||
end.
|
||||
|
||||
%% Dispatch based on scope's require
|
||||
|
||||
dispatch_require(Line, ?BUILTIN, Name, Args, S, Callback) ->
|
||||
Arity = length(Args),
|
||||
Tuple = {Name, Arity},
|
||||
|
||||
case is_element(Tuple, in_erlang_functions()) of
|
||||
true ->
|
||||
elixir_translator:translate_each({ { '.', Line, [erlang, Name] }, Line, Args }, S);
|
||||
false ->
|
||||
case dispatch_builtin_macro(Line, Tuple, Args, S) of
|
||||
false -> Callback();
|
||||
Else -> Else
|
||||
end
|
||||
end;
|
||||
|
||||
dispatch_require(Line, Receiver, Name, Args, S, Callback) ->
|
||||
Arity = length(Args),
|
||||
Tuple = {Name, Arity},
|
||||
|
||||
Fun = (S#elixir_scope.module == Receiver) andalso (S#elixir_scope.function /= Tuple) andalso
|
||||
elixir_def_local:macro_for(Tuple, false, S),
|
||||
|
||||
case Fun of
|
||||
false ->
|
||||
case is_element(Tuple, get_optional_macros(Receiver)) of
|
||||
true -> dispatch_macro(Line, Receiver, Name, Arity, Args, S);
|
||||
false -> Callback()
|
||||
end;
|
||||
_ ->
|
||||
elixir_import:record(import, Tuple, Receiver, S),
|
||||
dispatch_macro_fun(Line, Fun, Receiver, Name, Arity, Args, S)
|
||||
end.
|
||||
|
||||
%% HELPERS
|
||||
|
||||
dispatch_builtin_macro(Line, { Name, Arity } = Tuple, Args, S) ->
|
||||
case is_element(Tuple, in_erlang_macros()) of
|
||||
true -> elixir_macros:translate_macro({ Name, Line, Args }, S);
|
||||
false ->
|
||||
case is_element(Tuple, in_elixir_macros()) of
|
||||
true -> dispatch_macro(Line, ?BUILTIN, Name, Arity, Args, S);
|
||||
false -> false
|
||||
end
|
||||
end.
|
||||
|
||||
dispatch_macro(Line, Receiver, Name, Arity, Args, S) ->
|
||||
Macro = ?ELIXIR_MACRO(Name),
|
||||
dispatch_macro_fun(Line, fun Receiver:Macro/Arity, Receiver, Name, Arity, Args, S).
|
||||
|
||||
dispatch_macro_fun(Line, Fun, Receiver, Name, Arity, Args, S) ->
|
||||
ensure_required(Line, Receiver, Name, Arity, S),
|
||||
Tree = try
|
||||
apply(Fun, Args)
|
||||
catch
|
||||
Kind:Reason ->
|
||||
Info = { Receiver, Name, length(Args), [{ file, S#elixir_scope.filename }, { line, Line }] },
|
||||
erlang:raise(Kind, Reason, insert_before_dispatch_macro(Info, erlang:get_stacktrace()))
|
||||
end,
|
||||
NewS = S#elixir_scope{macro=[{Line,Receiver,Name,Arity}|S#elixir_scope.macro]},
|
||||
{ TTree, TS } = elixir_translator:translate_each(elixir_quote:linify(Line, Tree), NewS),
|
||||
{ TTree, TS#elixir_scope{macro=S#elixir_scope.macro} }.
|
||||
|
||||
find_dispatch(Tuple, [{ Name, Values }|T]) ->
|
||||
case is_element(Tuple, Values) of
|
||||
true -> Name;
|
||||
false -> find_dispatch(Tuple, T)
|
||||
end;
|
||||
|
||||
find_dispatch(_Tuple, []) -> false.
|
||||
|
||||
%% Insert call site into backtrace right after dispatch macro
|
||||
|
||||
insert_before_dispatch_macro(Info, [{ elixir_dispatch, dispatch_macro_fun, _, _ }|_] = T) ->
|
||||
[Info|T];
|
||||
|
||||
insert_before_dispatch_macro(Info, [H|T]) ->
|
||||
[H|insert_before_dispatch_macro(Info, T)];
|
||||
|
||||
insert_before_dispatch_macro(_, []) ->
|
||||
[].
|
||||
|
||||
%% ERROR HANDLING
|
||||
|
||||
ensure_required(_Line, Receiver, _Name, _Arity, #elixir_scope{module=Receiver}) -> ok;
|
||||
ensure_required(Line, Receiver, Name, Arity, S) ->
|
||||
Requires = S#elixir_scope.requires,
|
||||
case is_element(Receiver, Requires) of
|
||||
true -> ok;
|
||||
false ->
|
||||
Tuple = { unrequired_module, { Receiver, Name, Arity, Requires } },
|
||||
elixir_errors:form_error(Line, S#elixir_scope.filename, ?MODULE, Tuple)
|
||||
end.
|
||||
|
||||
format_error({ unrequired_module,{Receiver, Name, Arity, Required }}) ->
|
||||
io_lib:format("tried to invoke macro ~s.~s/~B but module was not required. Required: ~p",
|
||||
[elixir_errors:inspect(Receiver), Name, Arity, [elixir_errors:inspect(R) || R <- Required]]).
|
||||
|
||||
%% INTROSPECTION
|
||||
|
||||
remote_function(Line, Receiver, Name, Arity, S) ->
|
||||
Final =
|
||||
case Receiver == ?BUILTIN andalso is_element({ Name, Arity }, in_erlang_functions()) of
|
||||
true -> erlang;
|
||||
false -> Receiver
|
||||
end,
|
||||
|
||||
{ { 'fun', Line, { function,
|
||||
{ atom, Line, Final },
|
||||
{ atom, Line, Name },
|
||||
{ integer, Line, Arity}
|
||||
} }, S }.
|
||||
|
||||
%% Do not try to get macros from Erlang. Speeds up compilation a bit.
|
||||
get_optional_macros(erlang) -> [];
|
||||
|
||||
get_optional_macros(Receiver) ->
|
||||
case code:ensure_loaded(Receiver) of
|
||||
{ module, Receiver } ->
|
||||
try
|
||||
Receiver:'__info__'(macros)
|
||||
catch
|
||||
error:undef -> []
|
||||
end;
|
||||
{ error, _ } -> []
|
||||
end.
|
||||
|
||||
%% Functions imported from Elixit.Builtin module. Sorted on compilation.
|
||||
|
||||
in_elixir_functions() ->
|
||||
try
|
||||
?BUILTIN:'__info__'(functions) -- [{'__info__',1}]
|
||||
catch
|
||||
error:undef -> []
|
||||
end.
|
||||
|
||||
%% Macros imported from Elixit.Builtin module. Sorted on compilation.
|
||||
|
||||
in_elixir_macros() ->
|
||||
try
|
||||
?BUILTIN:'__info__'(macros)
|
||||
catch
|
||||
error:undef -> []
|
||||
end.
|
||||
|
||||
%% Functions imported from Erlang module. MUST BE SORTED.
|
||||
in_erlang_functions() ->
|
||||
[
|
||||
{ abs, 1 },
|
||||
{ atom_to_binary, 2 },
|
||||
{ atom_to_list, 1 },
|
||||
% Those are allowed in guard clauses, so we need to bring them back.
|
||||
% { binary_part, 2 },
|
||||
% { binary_part, 3 },
|
||||
{ binary_to_atom, 2 },
|
||||
{ binary_to_existing_atom, 2 },
|
||||
{ binary_to_list, 1 },
|
||||
{ binary_to_list, 3 },
|
||||
{ binary_to_term, 1 },
|
||||
{ binary_to_term, 2 },
|
||||
{ bit_size, 1 },
|
||||
{ bitstring_to_list, 1 },
|
||||
{ byte_size, 1 },
|
||||
% { date, 0 },
|
||||
{ exit, 1 },
|
||||
{ float, 1 },
|
||||
{ float_to_list, 1 },
|
||||
{ halt, 0 },
|
||||
{ halt, 1 },
|
||||
{ halt, 2 },
|
||||
{ hd, 1 },
|
||||
{ integer_to_list, 1 },
|
||||
{ integer_to_list, 2 },
|
||||
{ iolist_size, 1 },
|
||||
{ iolist_to_binary, 1 },
|
||||
{ is_atom, 1 },
|
||||
{ is_binary, 1 },
|
||||
{ is_bitstring, 1 },
|
||||
{ is_boolean, 1 },
|
||||
{ is_float, 1 },
|
||||
{ is_function, 1 },
|
||||
{ is_function, 2 },
|
||||
{ is_integer, 1 },
|
||||
{ is_list, 1 },
|
||||
{ is_number, 1 },
|
||||
{ is_pid, 1 },
|
||||
{ is_port, 1 },
|
||||
{ is_reference, 1 },
|
||||
{ is_tuple, 1 },
|
||||
{ length, 1 },
|
||||
{ list_to_atom, 1 },
|
||||
{ list_to_binary, 1 },
|
||||
{ list_to_bitstring, 1 },
|
||||
{ list_to_existing_atom, 1 },
|
||||
{ list_to_float, 1 },
|
||||
{ list_to_integer, 1 },
|
||||
{ list_to_integer, 2 },
|
||||
{ list_to_pid, 1 },
|
||||
{ list_to_tuple, 1 },
|
||||
{ make_ref, 0 },
|
||||
{ max, 2 },
|
||||
{ min, 2 },
|
||||
{ node, 0 },
|
||||
{ node, 1 },
|
||||
% { now, 0 },
|
||||
{ pid_to_list, 1 },
|
||||
{ round, 1 },
|
||||
{ size, 1 },
|
||||
{ spawn, 1 },
|
||||
{ spawn, 3 },
|
||||
{ spawn_link, 1 },
|
||||
{ spawn_link, 3 },
|
||||
% { split_binary, 2 },
|
||||
{ term_to_binary, 1 },
|
||||
{ term_to_binary, 2 },
|
||||
{ throw, 1 },
|
||||
% { time, 0 },
|
||||
{ tl, 1 },
|
||||
{ trunc, 1 },
|
||||
{ tuple_size, 1 },
|
||||
{ tuple_to_list, 1 }
|
||||
].
|
||||
|
||||
%% Macros implemented in Erlang. MUST BE SORTED.
|
||||
in_erlang_macros() ->
|
||||
[
|
||||
{'!=',2},
|
||||
{'!==',2},
|
||||
{'*',2},
|
||||
{'+',1},
|
||||
{'+',2},
|
||||
{'++',2},
|
||||
{'-',1},
|
||||
{'-',2},
|
||||
{'--',2},
|
||||
{'/',2},
|
||||
{'<',2},
|
||||
{'<-',2},
|
||||
{'<=',2},
|
||||
{'==',2},
|
||||
{'===',2},
|
||||
{'>',2},
|
||||
{'>=',2},
|
||||
{'@',1},
|
||||
{access,2},
|
||||
{'and',2},
|
||||
{apply,2},
|
||||
{apply,3},
|
||||
{'case',2},
|
||||
{def,1},
|
||||
{def,2},
|
||||
{def,4},
|
||||
{defmacro,1},
|
||||
{defmacro,2},
|
||||
{defmacro,4},
|
||||
{defmacrop,1},
|
||||
{defmacrop,2},
|
||||
{defmacrop,4},
|
||||
{defmodule,2},
|
||||
{defp,1},
|
||||
{defp,2},
|
||||
{defp,4},
|
||||
{'not',1},
|
||||
{'or',2},
|
||||
{'receive',1},
|
||||
{'try',1},
|
||||
{use,1},
|
||||
{use,2},
|
||||
{'var!',1},
|
||||
{'xor',2}
|
||||
].
|
||||
@@ -1,97 +0,0 @@
|
||||
% A bunch of helpers to help to deal with errors in Elixir source code.
|
||||
% This is not exposed in the Elixir language.
|
||||
-module(elixir_errors).
|
||||
-export([syntax_error/3, syntax_error/4, inspect/1,
|
||||
form_error/4, parse_error/4, assert_module_scope/3,
|
||||
assert_no_function_scope/3, assert_function_scope/3,
|
||||
handle_file_warning/2, handle_file_error/2]).
|
||||
-include("elixir.hrl").
|
||||
|
||||
%% Handle inspecting for exceptions
|
||||
|
||||
inspect(Atom) when is_atom(Atom) ->
|
||||
case atom_to_list(Atom) of
|
||||
"__MAIN__." ++ Rest -> list_to_atom(Rest);
|
||||
_ -> Atom
|
||||
end;
|
||||
|
||||
inspect(Other) -> Other.
|
||||
|
||||
%% Raised during macros translation.
|
||||
|
||||
syntax_error(Line, Filename, Message) when is_list(Message) ->
|
||||
syntax_error(Line, Filename, iolist_to_binary(Message));
|
||||
|
||||
syntax_error(Line, Filename, Message) when is_binary(Message) ->
|
||||
raise(Line, Filename, '__MAIN__.SyntaxError', Message).
|
||||
|
||||
syntax_error(Line, Filename, Format, Args) ->
|
||||
Message = io_lib:format(Format, Args),
|
||||
raise(Line, Filename, '__MAIN__.SyntaxError', iolist_to_binary(Message)).
|
||||
|
||||
%% Raised on tokenizing/parsing
|
||||
|
||||
parse_error(Line, Filename, _Error, []) ->
|
||||
raise(Line, Filename, '__MAIN__.TokenMissingError', <<"syntax error: expression is incomplete">>);
|
||||
|
||||
parse_error(Line, Filename, Error, Token) ->
|
||||
BinError = if
|
||||
is_atom(Error) -> atom_to_binary(Error, utf8);
|
||||
true -> iolist_to_binary(Error)
|
||||
end,
|
||||
|
||||
BinToken = case Token of
|
||||
[] -> <<>>;
|
||||
_ -> iolist_to_binary(Token)
|
||||
end,
|
||||
|
||||
Message = <<BinError / binary, BinToken / binary >>,
|
||||
raise(Line, Filename, '__MAIN__.SyntaxError', Message).
|
||||
|
||||
%% Raised during compilation
|
||||
|
||||
form_error(Line, Filename, Module, Desc) ->
|
||||
Message = iolist_to_binary(format_error(Module, Desc)),
|
||||
raise(Line, Filename, '__MAIN__.CompileError', Message).
|
||||
|
||||
%% Handle warnings and errors (called during module compilation)
|
||||
|
||||
handle_file_warning(_Filename, {_Line,sys_core_fold,Ignore}) when
|
||||
Ignore == nomatch_clause_type; Ignore == useless_building ->
|
||||
[];
|
||||
|
||||
handle_file_warning(Filename, {Line,Module,Desc}) ->
|
||||
Message = format_error(Module, Desc),
|
||||
io:format(file_format(Line, Filename, Message) ++ "\n").
|
||||
|
||||
handle_file_error(Filename, {Line,Module,Desc}) ->
|
||||
form_error(Line, Filename, Module, Desc).
|
||||
|
||||
%% Assertions
|
||||
|
||||
assert_no_function_scope(_Line, _Kind, #elixir_scope{function=[]}) -> [];
|
||||
assert_no_function_scope(Line, Kind, S) ->
|
||||
syntax_error(Line, S#elixir_scope.filename, "cannot invoke ~s inside a function", [Kind]).
|
||||
|
||||
assert_module_scope(Line, Kind, #elixir_scope{module=[],filename=Filename}) ->
|
||||
syntax_error(Line, Filename, "cannot invoke ~s outside module", [Kind]);
|
||||
assert_module_scope(_Line, _Kind, #elixir_scope{module=Module}) -> Module.
|
||||
|
||||
assert_function_scope(Line, Kind, #elixir_scope{function=[],filename=Filename}) ->
|
||||
syntax_error(Line, Filename, "cannot invoke ~s outside function", [Kind]);
|
||||
assert_function_scope(_Line, _Kind, #elixir_scope{function=Function}) -> Function.
|
||||
|
||||
%% Helpers
|
||||
|
||||
raise(Line, Filename, Kind, Message) ->
|
||||
Stacktrace = erlang:get_stacktrace(),
|
||||
erlang:raise(error, { Kind, '__exception__', Message, iolist_to_binary(Filename), Line }, Stacktrace).
|
||||
|
||||
file_format(Line, Filename, Message) ->
|
||||
lists:flatten(io_lib:format("~ts:~w: ~ts", [Filename, Line, Message])).
|
||||
|
||||
format_error([], Desc) ->
|
||||
io_lib:format("~p", [Desc]);
|
||||
|
||||
format_error(Module, Desc) ->
|
||||
Module:format_error(Desc).
|
||||
@@ -1,284 +0,0 @@
|
||||
%% Modified version of filelib:wildcard that can handle "**"
|
||||
%% and automatically skips directories starting with "." by default.
|
||||
|
||||
%% %CopyrightBegin%
|
||||
%%
|
||||
%% Copyright Ericsson AB 1997-2010. All Rights Reserved.
|
||||
%%
|
||||
%% The contents of this file are subject to the Erlang Public License,
|
||||
%% Version 1.1, (the "License"); you may not use this file except in
|
||||
%% compliance with the License. You should have received a copy of the
|
||||
%% Erlang Public License along with this software. If not, it can be
|
||||
%% retrieved online at http://www.erlang.org/.
|
||||
%%
|
||||
%% Software distributed under the License is distributed on an "AS IS"
|
||||
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
|
||||
%% the License for the specific language governing rights and limitations
|
||||
%% under the License.
|
||||
%%
|
||||
%% %CopyrightEnd%
|
||||
|
||||
-module(elixir_glob).
|
||||
-compile({no_auto_import,[error/1]}).
|
||||
-export([wildcard/1]).
|
||||
-include_lib("kernel/include/file.hrl").
|
||||
|
||||
-define(HANDLE_ERROR(Expr),
|
||||
try
|
||||
Expr
|
||||
catch
|
||||
error:{badpattern,_}=UnUsUalVaRiAbLeNaMe ->
|
||||
%% Get the stack backtrace correct.
|
||||
erlang:error(UnUsUalVaRiAbLeNaMe)
|
||||
end).
|
||||
|
||||
wildcard(Pattern) when is_list(Pattern) ->
|
||||
?HANDLE_ERROR(do_wildcard(Pattern, file)).
|
||||
|
||||
do_wildcard(Pattern, Mod) when is_list(Pattern) ->
|
||||
do_wildcard_comp(do_compile_wildcard(Pattern), Mod).
|
||||
|
||||
do_wildcard_comp({compiled_wildcard,{exists,File}}, Mod) ->
|
||||
case eval_read_file_info(File, Mod) of
|
||||
{ok,_} -> [File];
|
||||
_ -> []
|
||||
end;
|
||||
do_wildcard_comp({compiled_wildcard,[Base|Rest]}, Mod) ->
|
||||
do_wildcard_1([Base], Rest, Mod).
|
||||
|
||||
%%%
|
||||
%%% Pattern matching using a compiled wildcard.
|
||||
%%%
|
||||
|
||||
do_wildcard_1(Files, Pattern, Mod) ->
|
||||
do_wildcard_2(Files, Pattern, [], Mod).
|
||||
|
||||
do_wildcard_2([File|Rest], Pattern, Result, Mod) ->
|
||||
do_wildcard_2(Rest, Pattern, do_wildcard_3(File, Pattern, Result, Mod), Mod);
|
||||
do_wildcard_2([], _, Result, _Mod) ->
|
||||
Result.
|
||||
|
||||
do_wildcard_3(Base, [[double_star]|Rest], Result, Mod) ->
|
||||
lists:sort(do_double_star(current, [Base], Rest, Result, Mod, true));
|
||||
do_wildcard_3(Base, [Pattern|Rest], Result, Mod) ->
|
||||
case do_list_dir(Base, Mod) of
|
||||
{ok, Files0} ->
|
||||
Files = lists:sort(Files0),
|
||||
Matches = wildcard_4(Pattern, Files, Base, []),
|
||||
do_wildcard_2(Matches, Rest, Result, Mod);
|
||||
_ ->
|
||||
Result
|
||||
end;
|
||||
do_wildcard_3(Base, [], Result, _Mod) ->
|
||||
[Base|Result].
|
||||
|
||||
wildcard_4(Pattern, [File|Rest], Base, Result) when is_binary(File) ->
|
||||
case wildcard_5(Pattern, binary_to_list(File)) of
|
||||
true ->
|
||||
wildcard_4(Pattern, Rest, Base, [join(Base, File)|Result]);
|
||||
false ->
|
||||
wildcard_4(Pattern, Rest, Base, Result)
|
||||
end;
|
||||
wildcard_4(Pattern, [File|Rest], Base, Result) ->
|
||||
case wildcard_5(Pattern, File) of
|
||||
true ->
|
||||
wildcard_4(Pattern, Rest, Base, [join(Base, File)|Result]);
|
||||
false ->
|
||||
wildcard_4(Pattern, Rest, Base, Result)
|
||||
end;
|
||||
wildcard_4(_Patt, [], _Base, Result) ->
|
||||
Result.
|
||||
|
||||
wildcard_5([question|Rest1], [_|Rest2]) ->
|
||||
wildcard_5(Rest1, Rest2);
|
||||
wildcard_5([accept], _) ->
|
||||
true;
|
||||
wildcard_5([double_star], _) ->
|
||||
true;
|
||||
wildcard_5([star|Rest], File) ->
|
||||
do_star(Rest, File);
|
||||
wildcard_5([{one_of, Ordset}|Rest], [C|File]) ->
|
||||
case ordsets:is_element(C, Ordset) of
|
||||
true -> wildcard_5(Rest, File);
|
||||
false -> false
|
||||
end;
|
||||
wildcard_5([{alt, Alts}], File) ->
|
||||
do_alt(Alts, File);
|
||||
wildcard_5([C|Rest1], [C|Rest2]) when is_integer(C) ->
|
||||
wildcard_5(Rest1, Rest2);
|
||||
wildcard_5([X|_], [Y|_]) when is_integer(X), is_integer(Y) ->
|
||||
false;
|
||||
wildcard_5([], []) ->
|
||||
true;
|
||||
wildcard_5([], [_|_]) ->
|
||||
false;
|
||||
wildcard_5([_|_], []) ->
|
||||
false.
|
||||
|
||||
do_double_star(Base, [H|T], Rest, Result, Mod, Root) ->
|
||||
Full = join(Base, H),
|
||||
Result1 = case do_list_dir(Full, Mod) of
|
||||
{ok, Files} ->
|
||||
do_double_star(Full, Files, Rest, Result, Mod, false);
|
||||
_ -> Result
|
||||
end,
|
||||
Result2 = case Root andalso Rest == [] of
|
||||
true -> Result1;
|
||||
false -> do_wildcard_3(Full, Rest, Result1, Mod)
|
||||
end,
|
||||
do_double_star(Base, T, Rest, Result2, Mod, Root);
|
||||
do_double_star(_Base, [], _Rest, Result, _Mod, _Root) ->
|
||||
Result.
|
||||
|
||||
do_star(Pattern, [X|Rest]) ->
|
||||
case wildcard_5(Pattern, [X|Rest]) of
|
||||
true -> true;
|
||||
false -> do_star(Pattern, Rest)
|
||||
end;
|
||||
do_star(Pattern, []) ->
|
||||
wildcard_5(Pattern, []).
|
||||
|
||||
do_alt([Alt|Rest], File) ->
|
||||
case wildcard_5(Alt, File) of
|
||||
true -> true;
|
||||
false -> do_alt(Rest, File)
|
||||
end;
|
||||
do_alt([], _File) ->
|
||||
false.
|
||||
|
||||
do_list_dir(current, Mod) -> eval_list_dir(".", Mod);
|
||||
do_list_dir(Dir, Mod) -> eval_list_dir(Dir, Mod).
|
||||
|
||||
join(current, File) -> File;
|
||||
join(Base, File) -> filename:join(Base, File).
|
||||
|
||||
|
||||
%%% Compiling a wildcard.
|
||||
|
||||
do_compile_wildcard(Pattern) ->
|
||||
{compiled_wildcard,compile_wildcard_1(Pattern)}.
|
||||
|
||||
compile_wildcard_1(Pattern) ->
|
||||
[Root|Rest] = filename:split(Pattern),
|
||||
case filename:pathtype(Root) of
|
||||
relative ->
|
||||
compile_wildcard_2([Root|Rest], current);
|
||||
_ ->
|
||||
compile_wildcard_2(Rest, [Root])
|
||||
end.
|
||||
|
||||
compile_wildcard_2([Part|Rest], Root) ->
|
||||
case compile_part(Part) of
|
||||
Part ->
|
||||
compile_wildcard_2(Rest, join(Root, Part));
|
||||
Pattern ->
|
||||
compile_wildcard_3(Rest, [Pattern,Root])
|
||||
end;
|
||||
compile_wildcard_2([], Root) -> {exists,Root}.
|
||||
|
||||
compile_wildcard_3([Part|Rest], Result) ->
|
||||
compile_wildcard_3(Rest, [compile_part(Part)|Result]);
|
||||
compile_wildcard_3([], Result) ->
|
||||
lists:reverse(Result).
|
||||
|
||||
compile_part(Part) ->
|
||||
compile_part(Part, false, []).
|
||||
|
||||
compile_part_to_sep(Part) ->
|
||||
compile_part(Part, true, []).
|
||||
|
||||
compile_part([], true, _) ->
|
||||
error(missing_delimiter);
|
||||
compile_part([$,|Rest], true, Result) ->
|
||||
{ok, $,, lists:reverse(Result), Rest};
|
||||
compile_part([$}|Rest], true, Result) ->
|
||||
{ok, $}, lists:reverse(Result), Rest};
|
||||
compile_part([$?|Rest], Upto, Result) ->
|
||||
compile_part(Rest, Upto, [question|Result]);
|
||||
compile_part([$*,$*], Upto, Result) ->
|
||||
compile_part([], Upto, [double_star|Result]);
|
||||
compile_part([$*,$*|Rest], Upto, Result) ->
|
||||
compile_part(Rest, Upto, [star|Result]);
|
||||
compile_part([$*], Upto, Result) ->
|
||||
compile_part([], Upto, [accept|Result]);
|
||||
compile_part([$*|Rest], Upto, Result) ->
|
||||
compile_part(Rest, Upto, [star|Result]);
|
||||
compile_part([$[|Rest], Upto, Result) ->
|
||||
case compile_charset(Rest, ordsets:new()) of
|
||||
{ok, Charset, Rest1} ->
|
||||
compile_part(Rest1, Upto, [Charset|Result]);
|
||||
error ->
|
||||
compile_part(Rest, Upto, [$[|Result])
|
||||
end;
|
||||
compile_part([${|Rest], Upto, Result) ->
|
||||
case compile_alt(Rest) of
|
||||
{ok, Alt} ->
|
||||
lists:reverse(Result, [Alt]);
|
||||
error ->
|
||||
compile_part(Rest, Upto, [${|Result])
|
||||
end;
|
||||
compile_part([X|Rest], Upto, Result) ->
|
||||
compile_part(Rest, Upto, [X|Result]);
|
||||
compile_part([], _Upto, Result) ->
|
||||
lists:reverse(Result).
|
||||
|
||||
compile_charset([$]|Rest], Ordset) ->
|
||||
compile_charset1(Rest, ordsets:add_element($], Ordset));
|
||||
compile_charset([$-|Rest], Ordset) ->
|
||||
compile_charset1(Rest, ordsets:add_element($-, Ordset));
|
||||
compile_charset([], _Ordset) ->
|
||||
error;
|
||||
compile_charset(List, Ordset) ->
|
||||
compile_charset1(List, Ordset).
|
||||
|
||||
compile_charset1([Lower, $-, Upper|Rest], Ordset) when Lower =< Upper ->
|
||||
compile_charset1(Rest, compile_range(Lower, Upper, Ordset));
|
||||
compile_charset1([$]|Rest], Ordset) ->
|
||||
{ok, {one_of, Ordset}, Rest};
|
||||
compile_charset1([X|Rest], Ordset) ->
|
||||
compile_charset1(Rest, ordsets:add_element(X, Ordset));
|
||||
compile_charset1([], _Ordset) ->
|
||||
error.
|
||||
|
||||
compile_range(Lower, Current, Ordset) when Lower =< Current ->
|
||||
compile_range(Lower, Current-1, ordsets:add_element(Current, Ordset));
|
||||
compile_range(_, _, Ordset) ->
|
||||
Ordset.
|
||||
|
||||
compile_alt(Pattern) ->
|
||||
compile_alt(Pattern, []).
|
||||
|
||||
compile_alt(Pattern, Result) ->
|
||||
case compile_part_to_sep(Pattern) of
|
||||
{ok, $,, AltPattern, Rest} ->
|
||||
compile_alt(Rest, [AltPattern|Result]);
|
||||
{ok, $}, AltPattern, Rest} ->
|
||||
NewResult = [AltPattern|Result],
|
||||
RestPattern = compile_part(Rest),
|
||||
{ok, {alt, [Alt++RestPattern || Alt <- NewResult]}};
|
||||
Pattern ->
|
||||
error
|
||||
end.
|
||||
|
||||
error(Reason) ->
|
||||
erlang:error({badpattern,Reason}).
|
||||
|
||||
eval_read_file_info(File, file) ->
|
||||
file:read_file_info(File);
|
||||
eval_read_file_info(File, erl_prim_loader) ->
|
||||
case erl_prim_loader:read_file_info(File) of
|
||||
error -> {error, erl_prim_loader};
|
||||
Res-> Res
|
||||
end;
|
||||
eval_read_file_info(File, Mod) ->
|
||||
Mod:read_file_info(File).
|
||||
|
||||
eval_list_dir(Dir, file) ->
|
||||
file:list_dir(Dir);
|
||||
eval_list_dir(Dir, erl_prim_loader) ->
|
||||
case erl_prim_loader:list_dir(Dir) of
|
||||
error -> {error, erl_prim_loader};
|
||||
Res-> Res
|
||||
end;
|
||||
eval_list_dir(Dir, Mod) ->
|
||||
Mod:list_dir(Dir).
|
||||
@@ -1,276 +0,0 @@
|
||||
%% Module responsible for handling imports and conflicts.
|
||||
%% For imports dispatch, please check elixir_dispatch.
|
||||
-module(elixir_import).
|
||||
-export([import/5, recorded_locals/1, format_error/1,
|
||||
ensure_no_import_conflict/4, ensure_no_local_conflict/4,
|
||||
build_table/1, delete_table/1, record/4]).
|
||||
-include("elixir.hrl").
|
||||
|
||||
table(Module) -> ?ELIXIR_ATOM_CONCAT([i, Module]).
|
||||
|
||||
build_table(Module) ->
|
||||
ets:new(table(Module), [set, named_table, public]).
|
||||
|
||||
delete_table(Module) ->
|
||||
ets:delete(table(Module)).
|
||||
|
||||
record(_Kind, _Tuple, _Receiver, #elixir_scope{module=[]}) ->
|
||||
[];
|
||||
|
||||
record(import, Tuple, Receiver, #elixir_scope{module=Module}) ->
|
||||
ets:insert(table(Module), { Tuple, Receiver }).
|
||||
|
||||
recorded_locals(Module) ->
|
||||
Table = table(Module),
|
||||
Match = { '$1', Module },
|
||||
Result = ets:match(Table, Match),
|
||||
ets:match_delete(Table, Match),
|
||||
lists:append(Result).
|
||||
|
||||
%% Update the scope to consider the imports for reference
|
||||
%% based on the given options and selector.
|
||||
|
||||
import(Line, Ref, Opts, Selector, S) ->
|
||||
SF = case (Selector == all) or (Selector == functions) of
|
||||
false -> S;
|
||||
true ->
|
||||
FunctionsFun = fun() -> get_functions(Ref) end,
|
||||
Functions = calculate(Line, Ref, Opts,
|
||||
S#elixir_scope.functions, FunctionsFun, S),
|
||||
S#elixir_scope{functions=Functions}
|
||||
end,
|
||||
|
||||
SM = case (Selector == all) or (Selector == macros) of
|
||||
false -> SF;
|
||||
true ->
|
||||
MacrosFun = fun() ->
|
||||
case Selector of
|
||||
all -> get_optional_macros(Ref);
|
||||
_ -> get_macros(Line, Ref, SF)
|
||||
end
|
||||
end,
|
||||
Macros = calculate(Line, Ref, Opts,
|
||||
SF#elixir_scope.macros, MacrosFun, SF),
|
||||
SF#elixir_scope{macros=Macros}
|
||||
end,
|
||||
|
||||
SM.
|
||||
|
||||
%% IMPORT FUNCTION RELATED HELPERS
|
||||
|
||||
%% Calculates the imports based on only and except
|
||||
|
||||
calculate(Line, Key, Opts, Old, AvailableFun, S) ->
|
||||
Filename = S#elixir_scope.filename,
|
||||
All = keydelete(Key, Old),
|
||||
|
||||
New = case orddict:find(only, Opts) of
|
||||
{ ok, Only } ->
|
||||
case Only -- get_exports(Key) of
|
||||
[{Name,Arity}|_] ->
|
||||
Tuple = { invalid_import, { Key, Name, Arity } },
|
||||
elixir_errors:form_error(Line, Filename, ?MODULE, Tuple);
|
||||
_ -> intersection(Only, AvailableFun())
|
||||
end;
|
||||
error ->
|
||||
case orddict:find(except, Opts) of
|
||||
{ ok, Except } ->
|
||||
case keyfind(Key, Old) of
|
||||
false -> AvailableFun() -- Except;
|
||||
{Key,ToRemove} -> ToRemove -- Except
|
||||
end;
|
||||
error -> AvailableFun()
|
||||
end
|
||||
end,
|
||||
|
||||
%% Normalize the data before storing it
|
||||
Final = ordsets:from_list(New -- internal_funs()),
|
||||
|
||||
case Final of
|
||||
[] -> All;
|
||||
_ ->
|
||||
ensure_no_conflicts(Line, Filename, Final, keydelete(Key, S#elixir_scope.macros)),
|
||||
ensure_no_conflicts(Line, Filename, Final, keydelete(Key, S#elixir_scope.functions)),
|
||||
ensure_no_in_erlang_macro_conflict(Line, Filename, Key, Final, internal_conflict),
|
||||
[{ Key, Final }|All]
|
||||
end.
|
||||
|
||||
%% Retrieve functions and macros from modules
|
||||
|
||||
get_exports(Module) ->
|
||||
try
|
||||
Module:'__info__'(functions) ++ Module:'__info__'(macros)
|
||||
catch
|
||||
error:undef -> Module:module_info(exports)
|
||||
end.
|
||||
|
||||
get_functions(Module) ->
|
||||
try
|
||||
Module:'__info__'(functions)
|
||||
catch
|
||||
error:undef -> Module:module_info(exports)
|
||||
end.
|
||||
|
||||
get_macros(Line, Module, S) ->
|
||||
try
|
||||
Module:'__info__'(macros)
|
||||
catch
|
||||
error:undef ->
|
||||
Tuple = { no_macros, Module },
|
||||
elixir_errors:form_error(Line, S#elixir_scope.filename, ?MODULE, Tuple)
|
||||
end.
|
||||
|
||||
get_optional_macros(Module) ->
|
||||
case code:ensure_loaded(Module) of
|
||||
{ module, Module } ->
|
||||
try
|
||||
Module:'__info__'(macros)
|
||||
catch
|
||||
error:undef -> []
|
||||
end;
|
||||
{ error, _ } -> []
|
||||
end.
|
||||
|
||||
%% VALIDATION HELPERS
|
||||
|
||||
%% Check if any of the locals defined conflicts with an invoked
|
||||
%% Elixir "implemented in Erlang" macro. Checking if a local
|
||||
%% conflicts with an import is automatically done by Erlang.
|
||||
|
||||
ensure_no_local_conflict(Line, Filename, Module, AllDefined) ->
|
||||
ensure_no_in_erlang_macro_conflict(Line, Filename, Module, AllDefined, local_conflict).
|
||||
|
||||
%% Find conlicts in the given list of functions with
|
||||
%% the recorded set of imports.
|
||||
|
||||
ensure_no_import_conflict(Line, Filename, Module, AllDefined) ->
|
||||
Table = table(Module),
|
||||
Matches = [X || X <- AllDefined, ets:member(Table, X)],
|
||||
|
||||
case Matches of
|
||||
[{Name,Arity}|_] ->
|
||||
Key = ets:lookup_element(Table, {Name, Arity }, 2),
|
||||
Tuple = { import_conflict, { Key, Name, Arity } },
|
||||
elixir_errors:form_error(Line, Filename, ?MODULE, Tuple);
|
||||
[] ->
|
||||
ok
|
||||
end.
|
||||
|
||||
%% Ensure the given functions don't clash with any
|
||||
%% of Elixir non overridable macros.
|
||||
|
||||
ensure_no_in_erlang_macro_conflict(Line, Filename, Key, [{Name,Arity}|T], Reason) ->
|
||||
Values = lists:filter(fun({X,Y}) ->
|
||||
(Name == X) andalso ((Y == '*') orelse (Y == Arity))
|
||||
end, non_overridable_macros()),
|
||||
|
||||
case Values /= [] of
|
||||
true ->
|
||||
Tuple = { Reason, { Key, Name, Arity } },
|
||||
elixir_errors:form_error(Line, Filename, ?MODULE, Tuple);
|
||||
false -> ensure_no_in_erlang_macro_conflict(Line, Filename, Key, T, Reason)
|
||||
end;
|
||||
|
||||
ensure_no_in_erlang_macro_conflict(_Line, _Filename, _Key, [], _) -> ok.
|
||||
|
||||
%% Find conlicts in the given list of functions with the set of imports.
|
||||
%% Used internally to ensure a newly imported fun or macro does not
|
||||
%% conflict with an already imported set.
|
||||
|
||||
ensure_no_conflicts(Line, Filename, Functions, [{Key,Value}|T]) ->
|
||||
Filtered = lists:filter(fun(X) -> lists:member(X, Functions) end, Value),
|
||||
case Filtered of
|
||||
[{Name,Arity}|_] ->
|
||||
Tuple = { already_imported, { Key, Name, Arity } },
|
||||
elixir_errors:form_error(Line, Filename, ?MODULE, Tuple);
|
||||
[] ->
|
||||
ensure_no_conflicts(Line, Filename, Functions, T)
|
||||
end;
|
||||
|
||||
ensure_no_conflicts(_Line, _Filename, _Functions, _S) -> ok.
|
||||
|
||||
%% ERROR HANDLING
|
||||
|
||||
format_error({already_imported,{Receiver, Name, Arity}}) ->
|
||||
io_lib:format("function ~s/~B already imported from ~s", [Name, Arity, elixir_errors:inspect(Receiver)]);
|
||||
|
||||
format_error({invalid_import,{Receiver, Name, Arity}}) ->
|
||||
io_lib:format("cannot import ~s.~s/~B because it doesn't exist",
|
||||
[elixir_errors:inspect(Receiver), Name, Arity]);
|
||||
|
||||
format_error({import_conflict,{Receiver, Name, Arity}}) ->
|
||||
io_lib:format("imported ~s.~s/~B conflicts with local function",
|
||||
[elixir_errors:inspect(Receiver), Name, Arity]);
|
||||
|
||||
format_error({local_conflict,{_, Name, Arity}}) ->
|
||||
io_lib:format("cannot define local ~s/~B because it conflicts with Elixir internal macros", [Name, Arity]);
|
||||
|
||||
format_error({internal_conflict,{Receiver, Name, Arity}}) ->
|
||||
io_lib:format("cannot import ~s.~s/~B because it conflicts with Elixir internal macros",
|
||||
[elixir_errors:inspect(Receiver), Name, Arity]);
|
||||
|
||||
format_error({ no_macros, Module }) ->
|
||||
io_lib:format("could not load macros from module ~s", [elixir_errors:inspect(Module)]).
|
||||
|
||||
%% LIST HELPERS
|
||||
|
||||
keyfind(Key, List) ->
|
||||
lists:keyfind(Key, 1, List).
|
||||
|
||||
keydelete(Key, List) ->
|
||||
lists:keydelete(Key, 1, List).
|
||||
|
||||
intersection([H|T], All) ->
|
||||
case lists:member(H, All) of
|
||||
true -> [H|intersection(T, All)];
|
||||
false -> intersection(T, All)
|
||||
end;
|
||||
|
||||
intersection([], _All) -> [].
|
||||
|
||||
%% INTROSPECTION
|
||||
|
||||
%% Internal funs that are never imported etc.
|
||||
|
||||
internal_funs() ->
|
||||
[
|
||||
{ module_info, 0 },
|
||||
{ module_info, 1 },
|
||||
{ '__info__', 1 },
|
||||
{ '__using__', 2 }
|
||||
].
|
||||
|
||||
%% Macros implemented in Erlang that are not overridable.
|
||||
|
||||
non_overridable_macros() ->
|
||||
[
|
||||
{'^',1},
|
||||
{'=',2},
|
||||
{'__op__',2},
|
||||
{'__op__',3},
|
||||
{'__block__','*'},
|
||||
{'->','2'},
|
||||
{'<<>>','*'},
|
||||
{'{}','*'},
|
||||
{'[]','*'},
|
||||
{'require',1},
|
||||
{'require',2},
|
||||
{'import',1},
|
||||
{'import',2},
|
||||
{'import',3},
|
||||
{'__MODULE__',0},
|
||||
{'__FILE__',0},
|
||||
{'__LINE__',0},
|
||||
{'__FUNCTION__',0},
|
||||
{'__ref__',1},
|
||||
{'quote',1},
|
||||
{'quote',2},
|
||||
{'unquote',1},
|
||||
{'unquote_splicing',1},
|
||||
{'fn','*'},
|
||||
{'loop','*'},
|
||||
{'recur','*'},
|
||||
{'super','*'},
|
||||
{'bc','*'},
|
||||
{'lc','*'}
|
||||
].
|
||||
@@ -1,149 +0,0 @@
|
||||
% Handle string and string-like interpolations.
|
||||
-module(elixir_interpolation).
|
||||
-export([extract/4, unescape_chars/1, unescape_chars/2,
|
||||
unescape_tokens/1, unescape_tokens/2, unescape_map/1]).
|
||||
-define(is_octal(S), S >= $0 andalso S =< $7).
|
||||
-include("elixir.hrl").
|
||||
|
||||
%% Extract string interpolations
|
||||
|
||||
extract(Line, Interpol, String, Last) ->
|
||||
extract(Line, Interpol, String, [], [], [], Last).
|
||||
|
||||
extract(Line, _Interpol, [], Buffer, [], Output, []) ->
|
||||
finish_extraction(Line, Buffer, Output, []);
|
||||
|
||||
extract(Line, _Interpol, [], _Buffer, [], _Output, Last) ->
|
||||
{ error, { Line, io_lib:format("missing string terminator, expected ~ts", [[Last]]), [] } };
|
||||
|
||||
extract(Line, _Interpol, [Last|Remaining], Buffer, [], Output, Last) ->
|
||||
finish_extraction(Line, Buffer, Output, Remaining);
|
||||
|
||||
extract(Line, _Interpol, [Last], _Buffer, Search, _Output, Last) ->
|
||||
{ error, { Line, io_lib:format("unexpected end of string, expected ~ts", [[hd(Search)]]), [Last] } };
|
||||
|
||||
extract(Line, Interpol, [$\n|Rest], Buffer, Search, Output, Last) ->
|
||||
extract(Line+1, Interpol, Rest, [$\n|Buffer], Search, Output, Last);
|
||||
|
||||
extract(Line, Interpol, [$\\, $#, ${|Rest], Buffer, [], Output, Last) ->
|
||||
extract(Line, Interpol, Rest, [${,$#|Buffer], [], Output, Last);
|
||||
|
||||
extract(Line, Interpol, [$\\,Char|Rest], Buffer, [], Output, Last) ->
|
||||
extract(Line, Interpol, Rest, [Char,$\\|Buffer], [], Output, Last);
|
||||
|
||||
extract(Line, true, [$#, ${|Rest], Buffer, [], Output, Last) ->
|
||||
NewOutput = build_interpol(s, Line, Buffer, Output),
|
||||
extract(Line, true, Rest, [], [$}], NewOutput, Last);
|
||||
|
||||
extract(Line, true, [$}|Rest], Buffer, [$}], Output, Last) ->
|
||||
NewOutput = build_interpol(i, Line, Buffer, Output),
|
||||
extract(Line, true, Rest, [], [], NewOutput, Last);
|
||||
|
||||
%% Check for available separators "", {}, [] and () inside interpolation
|
||||
|
||||
extract(Line, Interpol, [C|Rest], Buffer, [C|Search], Output, Last) when C == $); C == $]; C == $}; C == $"; C == $' ->
|
||||
extract(Line, Interpol, Rest, [C|Buffer], Search, Output, Last);
|
||||
|
||||
extract(Line, Interpol, [C|Rest], Buffer, [_|_] = Search, Output, Last) when C == $"; C == $' ->
|
||||
extract(Line, Interpol, Rest, [C|Buffer], [C|Search], Output, Last);
|
||||
|
||||
extract(Line, Interpol, [${|Rest], Buffer, [_|_] = Search, Output, Last) ->
|
||||
extract(Line, Interpol, Rest, [${|Buffer], [$}|Search], Output, Last);
|
||||
|
||||
extract(Line, Interpol, [$[|Rest], Buffer, [_|_] = Search, Output, Last) ->
|
||||
extract(Line, Interpol, Rest, [$[|Buffer], [$]|Search], Output, Last);
|
||||
|
||||
extract(Line, Interpol, [$(|Rest], Buffer, [_|_] = Search, Output, Last) ->
|
||||
extract(Line, Interpol, Rest, [$(|Buffer], [$)|Search], Output, Last);
|
||||
|
||||
%% Else
|
||||
|
||||
extract(Line, Interpol, [Char|Rest], Buffer, Search, Output, Last) ->
|
||||
extract(Line, Interpol, Rest, [Char|Buffer], Search, Output, Last).
|
||||
|
||||
%% Unescape a series of tokens as returned by extract.
|
||||
|
||||
unescape_tokens(Tokens) ->
|
||||
unescape_tokens(Tokens, fun unescape_map/1).
|
||||
|
||||
unescape_tokens(Tokens, Map) ->
|
||||
[unescape_token(Token, Map) || Token <- Tokens].
|
||||
|
||||
unescape_token(Token, Map) when is_binary(Token) -> unescape_chars(Token, Map);
|
||||
unescape_token(Other, _Map) -> Other.
|
||||
|
||||
% Unescape chars. For instance, "\" "n" (two chars) needs to be converted to "\n" (one char).
|
||||
|
||||
unescape_chars(String) -> unescape_chars(String, fun unescape_map/1).
|
||||
|
||||
-define(to_octal(List),
|
||||
<<(list_to_integer(List, 8))/integer, (unescape_chars(Rest, Map))/binary>>
|
||||
).
|
||||
|
||||
unescape_chars(<<$\\,A,B,C,Rest/binary>>, Map) when ?is_octal(A), ?is_octal(B), ?is_octal(C) ->
|
||||
?to_octal([A,B,C]);
|
||||
|
||||
unescape_chars(<<$\\,A,B,Rest/binary>>, Map) when ?is_octal(A), ?is_octal(B) ->
|
||||
?to_octal([A,B]);
|
||||
|
||||
unescape_chars(<<$\\,A,Rest/binary>>, Map) when ?is_octal(A) ->
|
||||
?to_octal([A]);
|
||||
|
||||
unescape_chars(<<$\\,Escaped,Rest/binary>>, Map) ->
|
||||
case Map(Escaped) of
|
||||
false -> <<$\\,Escaped,(unescape_chars(Rest, Map))/binary>>;
|
||||
Other -> <<Other,(unescape_chars(Rest, Map))/binary>>
|
||||
end;
|
||||
|
||||
unescape_chars(<<Char, Rest/binary>>, Map) ->
|
||||
<<Char, (unescape_chars(Rest, Map))/binary>>;
|
||||
|
||||
unescape_chars(<<>>, _Map) -> <<>>.
|
||||
|
||||
% Unescape Helpers
|
||||
|
||||
unescape_map($b) -> $\b;
|
||||
unescape_map($d) -> $\d;
|
||||
unescape_map($e) -> $\e;
|
||||
unescape_map($f) -> $\f;
|
||||
unescape_map($n) -> $\n;
|
||||
unescape_map($r) -> $\r;
|
||||
unescape_map($s) -> $\s;
|
||||
unescape_map($t) -> $\t;
|
||||
unescape_map($v) -> $\v;
|
||||
unescape_map(E) -> E.
|
||||
|
||||
% Extract Helpers
|
||||
|
||||
finish_extraction(Line, Buffer, Output, Remaining) ->
|
||||
case build_interpol(s, Line, Buffer, Output) of
|
||||
[] -> Final = [<<>>];
|
||||
Final -> []
|
||||
end,
|
||||
{ Line, lists:reverse(Final), Remaining }.
|
||||
|
||||
build_interpol(_Kind, _Line, [], Output) ->
|
||||
Output;
|
||||
|
||||
build_interpol(s, _Line, Buffer, Output) ->
|
||||
[unicode:characters_to_binary(lists:reverse(Buffer))|Output];
|
||||
|
||||
build_interpol(i, Line, Buffer, Output) ->
|
||||
[wrap_interpol(Line, forms(lists:reverse(Buffer), Line))| Output].
|
||||
|
||||
wrap_interpol(_Line, Form) when is_binary(Form) ->
|
||||
Form;
|
||||
|
||||
wrap_interpol(Line, Form) ->
|
||||
{ '|', Line, [{ { '.', Line, ['__MAIN__.Binary.Chars', to_binary] }, Line, [Form]}, binary]}.
|
||||
|
||||
forms(String, StartLine) ->
|
||||
case elixir_tokenizer:tokenize(String, StartLine) of
|
||||
{ok, Tokens} ->
|
||||
case elixir_parser:parse(Tokens) of
|
||||
{ok, [Forms]} when not is_list(Forms) -> Forms;
|
||||
{ok, Forms} -> { '__block__', StartLine, Forms };
|
||||
{error, {Line, _, [Error, Token]}} -> throw({ interpolation_error, { Line, Error, Token } })
|
||||
end;
|
||||
{error, {Line, Error, Token}} -> throw({ interpolation_error, { Line, Error, Token } })
|
||||
end.
|
||||
@@ -1,266 +0,0 @@
|
||||
%% Those macros behave like they belong to Elixir.Builtin,
|
||||
%% but do not since they need to be implemented in Erlang.
|
||||
-module(elixir_macros).
|
||||
-export([translate_macro/2]).
|
||||
-import(elixir_translator, [translate_each/2, translate/2, translate_args/2, translate_apply/7]).
|
||||
-import(elixir_variables, [umergec/2]).
|
||||
-import(elixir_errors, [syntax_error/3, syntax_error/4, assert_no_function_scope/3, assert_module_scope/3]).
|
||||
-include("elixir.hrl").
|
||||
-define(FUNS(), Kind == def; Kind == defp; Kind == defmacro; Kind == defmacrop).
|
||||
|
||||
%% Operators
|
||||
|
||||
translate_macro({ '+', _Line, [Expr] }, S) when is_number(Expr) ->
|
||||
translate_each(Expr, S);
|
||||
|
||||
translate_macro({ '-', _Line, [Expr] }, S) when is_number(Expr) ->
|
||||
translate_each(-1 * Expr, S);
|
||||
|
||||
translate_macro({ Op, Line, Exprs }, S) when is_list(Exprs),
|
||||
Op == '+'; Op == '-'; Op == '*'; Op == '/'; Op == '<-';
|
||||
Op == '++'; Op == '--'; Op == 'not'; Op == 'and';
|
||||
Op == 'or'; Op == 'xor'; Op == '<'; Op == '>';
|
||||
Op == '<='; Op == '>='; Op == '=='; Op == '!=';
|
||||
Op == '==='; Op == '!==' ->
|
||||
translate_each({ '__op__', Line, [Op|Exprs] }, S);
|
||||
|
||||
%% @
|
||||
|
||||
translate_macro({'@', Line, [{ Name, _, Args }]}, S) ->
|
||||
assert_module_scope(Line, '@', S),
|
||||
assert_no_function_scope(Line, '@', S),
|
||||
case is_reserved_data(Name) andalso elixir_compiler:get_opt(internal) of
|
||||
true ->
|
||||
{ { nil, Line }, S };
|
||||
_ ->
|
||||
case Args of
|
||||
[Arg] ->
|
||||
translate_each({
|
||||
{ '.', Line, ['__MAIN__.Module', merge_data] },
|
||||
Line,
|
||||
[ { '__MODULE__', Line, false }, [{ Name, Arg }] ]
|
||||
}, S);
|
||||
_ when is_atom(Args) or (Args == []) ->
|
||||
translate_each({
|
||||
{ '.', Line, ['__MAIN__.Module', read_data] },
|
||||
Line,
|
||||
[ { '__MODULE__', Line, false }, Name ]
|
||||
}, S);
|
||||
_ ->
|
||||
syntax_error(Line, S#elixir_scope.filename, "expected 0 or 1 argument for @~s, got: ~p", [Name, length(Args)])
|
||||
end
|
||||
end;
|
||||
|
||||
%% Case
|
||||
|
||||
translate_macro({'case', Line, [Expr, KV]}, S) ->
|
||||
Clauses = elixir_clauses:get_pairs(Line, do, KV, S),
|
||||
{ TExpr, NS } = translate_each(Expr, S),
|
||||
{ TClauses, TS } = elixir_clauses:match(Line, Clauses, NS),
|
||||
{ { 'case', Line, TExpr, TClauses }, TS };
|
||||
|
||||
%% Try
|
||||
|
||||
translate_macro({'try', Line, [Clauses]}, RawS) ->
|
||||
S = RawS#elixir_scope{noname=true},
|
||||
|
||||
Do = proplists:get_value('do', Clauses, []),
|
||||
{ TDo, SB } = translate([Do], S),
|
||||
|
||||
Catch = [Tuple || { X, _ } = Tuple <- Clauses, X == 'rescue' orelse X == 'catch'],
|
||||
{ TCatch, SC } = elixir_try:clauses(Line, Catch, umergec(S, SB)),
|
||||
|
||||
{ TAfter, SA } = case orddict:find('after', Clauses) of
|
||||
{ ok, After } -> translate([After], umergec(S, SC));
|
||||
error -> { [], SC }
|
||||
end,
|
||||
|
||||
{ { 'try', Line, unpack(TDo), [], TCatch, unpack(TAfter) }, umergec(RawS, SA) };
|
||||
|
||||
%% Receive
|
||||
|
||||
translate_macro({'receive', Line, [KV] }, S) ->
|
||||
Do = elixir_clauses:get_pairs(Line, do, KV, S, true),
|
||||
|
||||
case orddict:is_key('after', KV) of
|
||||
true ->
|
||||
After = elixir_clauses:get_pairs(Line, 'after', KV, S),
|
||||
{ TClauses, SC } = elixir_clauses:match(Line, Do ++ After, S),
|
||||
{ FClauses, TAfter } = elixir_tree_helpers:split_last(TClauses),
|
||||
{ _, _, [FExpr], _, FAfter } = TAfter,
|
||||
{ { 'receive', Line, FClauses, FExpr, FAfter }, SC };
|
||||
false ->
|
||||
{ TClauses, SC } = elixir_clauses:match(Line, Do, S),
|
||||
{ { 'receive', Line, TClauses }, SC }
|
||||
end;
|
||||
|
||||
%% Definitions
|
||||
|
||||
translate_macro({defmodule, Line, [Ref, KV]}, S) ->
|
||||
{ TRef, _ } = translate_each(Ref, S),
|
||||
|
||||
Block = case orddict:find(do, KV) of
|
||||
{ ok, DoValue } -> DoValue;
|
||||
error -> syntax_error(Line, S#elixir_scope.filename, "expected do: argument in defmodule")
|
||||
end,
|
||||
|
||||
{ FRef, FS } = case TRef of
|
||||
{ atom, _, Module } ->
|
||||
NewModule = module_ref(Ref, Module, S#elixir_scope.module),
|
||||
|
||||
RS = case Module == NewModule of
|
||||
true -> S;
|
||||
false ->
|
||||
element(2, translate_each({ refer, Line, [NewModule, [{as,Module}]] }, S))
|
||||
end,
|
||||
|
||||
{
|
||||
{ atom, Line, NewModule },
|
||||
RS#elixir_scope{scheduled=[NewModule|S#elixir_scope.scheduled]}
|
||||
};
|
||||
_ ->
|
||||
{ TRef, S }
|
||||
end,
|
||||
|
||||
{ elixir_module:translate(Line, FRef, Block, S), FS };
|
||||
|
||||
translate_macro({Kind, Line, [Call]}, S) when ?FUNS() ->
|
||||
translate_macro({Kind, Line, [Call, skip_definition]}, S);
|
||||
|
||||
translate_macro({Kind, Line, [Call, Expr]}, S) when ?FUNS() ->
|
||||
assert_module_scope(Line, Kind, S),
|
||||
assert_no_function_scope(Line, Kind, S),
|
||||
{ TCall, Guards } = elixir_clauses:extract_guards(Call),
|
||||
{ Name, Args } = elixir_clauses:extract_args(TCall),
|
||||
TName = elixir_tree_helpers:abstract_syntax(Name),
|
||||
TArgs = elixir_tree_helpers:abstract_syntax(Args),
|
||||
TGuards = elixir_tree_helpers:abstract_syntax(Guards),
|
||||
TExpr = elixir_tree_helpers:abstract_syntax(Expr),
|
||||
{ elixir_def:wrap_definition(Kind, Line, TName, TArgs, TGuards, TExpr, S), S };
|
||||
|
||||
translate_macro({Kind, Line, [Name, Args, Guards, Expr]}, S) when ?FUNS() ->
|
||||
assert_module_scope(Line, Kind, S),
|
||||
assert_no_function_scope(Line, Kind, S),
|
||||
{ TName, NS } = translate_each(Name, S),
|
||||
{ TArgs, AS } = translate_each(Args, NS),
|
||||
{ TGuards, TS } = translate_each(Guards, AS),
|
||||
TExpr = elixir_tree_helpers:abstract_syntax(Expr),
|
||||
{ elixir_def:wrap_definition(Kind, Line, TName, TArgs, TGuards, TExpr, TS), TS };
|
||||
|
||||
%% Modules directives
|
||||
|
||||
translate_macro({use, Line, [Raw]}, S) ->
|
||||
translate_macro({use, Line, [Raw, []]}, S);
|
||||
|
||||
translate_macro({use, Line, [Raw, Args]}, S) ->
|
||||
assert_module_scope(Line, use, S),
|
||||
Module = S#elixir_scope.module,
|
||||
{ TRef, SR } = translate_each(Raw, S),
|
||||
|
||||
Ref = case TRef of
|
||||
{ atom, _, RefAtom } -> RefAtom;
|
||||
_ -> syntax_error(Line, S#elixir_scope.filename, "invalid args for use, expected a reference as argument")
|
||||
end,
|
||||
|
||||
elixir_ref:ensure_loaded(Line, Ref, SR),
|
||||
|
||||
Call = { '__block__', Line, [
|
||||
{ require, Line, [Ref] },
|
||||
{ { '.', Line, [Ref, '__using__'] }, Line, [Module, Args] }
|
||||
] },
|
||||
|
||||
translate_each(Call, S);
|
||||
|
||||
%% Access
|
||||
|
||||
translate_macro({ access, Line, [Element, Keyword] }, S) ->
|
||||
case translate_each(Element, S) of
|
||||
{ { atom, _, Atom }, _ } -> Atom;
|
||||
_ -> Atom = false
|
||||
end,
|
||||
|
||||
case { S#elixir_scope.assign, Atom } of
|
||||
{ false, false } ->
|
||||
Fallback = { { '.', Line, ['__MAIN__.Access', access] }, Line, [Element, Keyword] },
|
||||
translate_each(Fallback, S);
|
||||
{ true, false } ->
|
||||
syntax_error(Line, S#elixir_scope.filename, "invalid usage of access protocol in signature");
|
||||
{ Assign, _ } ->
|
||||
case is_orddict(Keyword) of
|
||||
true -> [];
|
||||
false ->
|
||||
Message0 = "expected contents inside brackets to be a Keyword",
|
||||
syntax_error(Line, S#elixir_scope.filename, Message0)
|
||||
end,
|
||||
|
||||
elixir_ref:ensure_loaded(Line, Atom, S),
|
||||
|
||||
try Atom:'__record__'(fields) of
|
||||
Fields ->
|
||||
{ Match, Remaining } = lists:mapfoldl(fun({Field, Default}, KeywordEach) ->
|
||||
{ case orddict:find(Field, KeywordEach) of
|
||||
{ ok, Value } -> Value;
|
||||
error ->
|
||||
case Assign of
|
||||
true -> { '_', Line, nil };
|
||||
false -> '__MAIN__.Macro':escape(Default)
|
||||
end
|
||||
end, orddict:erase(Field, KeywordEach) }
|
||||
end, Keyword, Fields),
|
||||
|
||||
case Remaining of
|
||||
[] -> translate_each({ '{}', Line, [Atom|Match] }, S);
|
||||
_ ->
|
||||
Keys = [Key || {Key,_} <- Remaining],
|
||||
Message1 = "record ~s does not have some of the given keys: ~p",
|
||||
syntax_error(Line, S#elixir_scope.filename, Message1, [elixir_errors:inspect(Atom), Keys])
|
||||
end
|
||||
catch
|
||||
error:undef ->
|
||||
Message2 = "cannot use module ~s in access protocol because it doesn't represent a record",
|
||||
syntax_error(Line, S#elixir_scope.filename, Message2, [elixir_errors:inspect(Atom)])
|
||||
end
|
||||
end;
|
||||
|
||||
%% Apply - Optimize apply by checking what doesn't need to be dispatched dynamically
|
||||
|
||||
translate_macro({ apply, Line, [Left, Right, Args] }, S) when is_list(Args) ->
|
||||
{ TLeft, SL } = translate_each(Left, S),
|
||||
{ TRight, SR } = translate_each(Right, umergec(S, SL)),
|
||||
translate_apply(Line, TLeft, TRight, Args, S, SL, SR);
|
||||
|
||||
translate_macro({ apply, Line, Args }, S) ->
|
||||
{ TArgs, NS } = translate_args(Args, S),
|
||||
{ ?ELIXIR_WRAP_CALL(Line, erlang, apply, TArgs), NS };
|
||||
|
||||
%% Handle forced variables
|
||||
|
||||
translate_macro({ 'var!', _, [{Name, Line, Atom}] }, S) when is_atom(Name), is_atom(Atom) ->
|
||||
elixir_variables:translate_each(Line, Name, S);
|
||||
|
||||
translate_macro({ 'var!', Line, [_] }, S) ->
|
||||
syntax_error(Line, S#elixir_scope.filename, "invalid args for var!").
|
||||
|
||||
%% HELPERS
|
||||
|
||||
module_ref(_Raw, Module, []) ->
|
||||
Module;
|
||||
|
||||
module_ref({{ '.', _, [{ '__MAIN__', _, Atom }, _]}, _, _}, Module, _Nesting) when is_atom(Atom) ->
|
||||
Module;
|
||||
|
||||
module_ref(_, Module, Nesting) ->
|
||||
elixir_ref:concat([Nesting, Module]).
|
||||
|
||||
is_orddict(Keyword) -> is_list(Keyword) andalso lists:all(fun is_orddict_tuple/1, Keyword).
|
||||
|
||||
is_orddict_tuple({X,_}) when is_atom(X) -> true;
|
||||
is_orddict_tuple(_) -> false.
|
||||
|
||||
is_reserved_data(moduledoc) -> true;
|
||||
is_reserved_data(doc) -> true;
|
||||
is_reserved_data(_) -> false.
|
||||
|
||||
% Unpack a list of expressions from a block.
|
||||
unpack([{ '__block__', _, Exprs }]) -> Exprs;
|
||||
unpack(Exprs) -> Exprs.
|
||||
@@ -1,267 +0,0 @@
|
||||
-module(elixir_module).
|
||||
-export([translate/4, compile/4, data/1, data/2, data_table/1,
|
||||
format_error/1, binding_and_scope_for_eval/3]).
|
||||
-include("elixir.hrl").
|
||||
|
||||
binding_and_scope_for_eval(Opts, Module, Binding) ->
|
||||
binding_and_scope_for_eval(Opts, Module, Binding, elixir:scope_for_eval(Opts)).
|
||||
|
||||
binding_and_scope_for_eval(_Opts, Module, Binding, S) ->
|
||||
{
|
||||
binding_for_eval(Module, Binding),
|
||||
S#elixir_scope{module=Module}
|
||||
}.
|
||||
|
||||
binding_for_eval(Module, Binding) -> [{'_EXMODULE',Module}|Binding].
|
||||
|
||||
data(Module) ->
|
||||
ets:lookup_element(data_table(Module), data, 2).
|
||||
|
||||
data(Module, Value) ->
|
||||
ets:insert(data_table(Module), { data, Value }).
|
||||
|
||||
%% TABLE METHODS
|
||||
|
||||
data_table(Module) ->
|
||||
?ELIXIR_ATOM_CONCAT([d, Module]).
|
||||
|
||||
docs_table(Module) ->
|
||||
?ELIXIR_ATOM_CONCAT([o, Module]).
|
||||
|
||||
%% TRANSFORMATION METHODS
|
||||
|
||||
%% Transformation of args and scope into a compiled erlang call.
|
||||
%% The abstract form for extra arguments may be given and they
|
||||
%% will be passed to the invoked function.
|
||||
|
||||
translate(Line, Ref, Block, S) ->
|
||||
MetaBlock = elixir_tree_helpers:abstract_syntax(Block),
|
||||
MetaS = elixir_variables:serialize_scope(S),
|
||||
|
||||
Args = [{integer, Line, Line}, Ref, MetaBlock, MetaS],
|
||||
?ELIXIR_WRAP_CALL(Line, ?MODULE, compile, Args).
|
||||
|
||||
%% The compilation hook.
|
||||
|
||||
compile(Line, Module, Block, RawS) when is_atom(Module) ->
|
||||
S = elixir_variables:deserialize_scope(RawS),
|
||||
C = elixir_compiler:get_opts(),
|
||||
Filename = S#elixir_scope.filename,
|
||||
|
||||
check_module_availability(Line, Filename, Module, C),
|
||||
build(Module),
|
||||
|
||||
try
|
||||
Result = eval_form(Line, Filename, Module, Block, S),
|
||||
{ Funs, Forms0 } = functions_form(Line, Filename, Module, C),
|
||||
Forms1 = attributes_form(Line, Filename, Module, Forms0),
|
||||
|
||||
elixir_import:ensure_no_local_conflict(Line, Filename, Module, Funs),
|
||||
elixir_import:ensure_no_import_conflict(Line, Filename, Module, Funs),
|
||||
|
||||
Final = [
|
||||
{attribute, Line, file, {Filename,Line}},
|
||||
{attribute, Line, module, Module} | Forms1
|
||||
],
|
||||
|
||||
load_form(Final, S),
|
||||
Result
|
||||
after
|
||||
ets:delete(data_table(Module)),
|
||||
ets:delete(docs_table(Module)),
|
||||
elixir_def:delete_table(Module),
|
||||
elixir_import:delete_table(Module)
|
||||
end;
|
||||
|
||||
compile(Line, Other, _Block, RawS) ->
|
||||
S = elixir_variables:deserialize_scope(RawS),
|
||||
elixir_errors:form_error(Line, S#elixir_scope.filename, ?MODULE, { invalid_module, Other }).
|
||||
|
||||
%% Hook that builds both attribute and functions and set up common hooks.
|
||||
|
||||
build(Module) ->
|
||||
%% Table with meta information about the module.
|
||||
DataTable = data_table(Module),
|
||||
ets:new(DataTable, [set, named_table, public]),
|
||||
ets:insert(DataTable, { data, [] }),
|
||||
ets:insert(DataTable, { attributes, [] }),
|
||||
ets:insert(DataTable, { overridable, [] }),
|
||||
ets:insert(DataTable, { compile_callbacks, [] }),
|
||||
ets:insert(DataTable, { registered_attributes, [behavior, behaviour, compile, vsn, on_load] }),
|
||||
|
||||
%% Keep docs in another table since we don't want to pull out
|
||||
%% all the binaries every time a new documentation is stored.
|
||||
DocsTable = docs_table(Module),
|
||||
ets:new(DocsTable, [ordered_set, named_table, public]),
|
||||
|
||||
%% We keep a separated table for function definitions
|
||||
%% and another one for imports. We keep them in different
|
||||
%% tables for organization and speed purpose (since the
|
||||
%% imports table is frequently written to).
|
||||
elixir_def:build_table(Module),
|
||||
elixir_import:build_table(Module).
|
||||
|
||||
%% Receives the module representation and evaluates it.
|
||||
|
||||
eval_form(Line, Filename, Module, Block, RawS) ->
|
||||
Temp = ?ELIXIR_ATOM_CONCAT(["COMPILE-",Module]),
|
||||
{ Binding, S } = binding_and_scope_for_eval([{file,Filename}], Module, [], RawS),
|
||||
{ Value, NewS } = elixir_compiler:eval_forms([Block], Line, Temp, S),
|
||||
elixir_def_overridable:store_pending(Module),
|
||||
{ Callbacks, FinalS } = callbacks_for(Line, compile_callbacks, Module, [Module], NewS),
|
||||
elixir:eval_forms(Callbacks, binding_for_eval(Module, Binding), FinalS#elixir_scope{check_clauses=false}),
|
||||
Value.
|
||||
|
||||
%% Return the form with exports and function declarations.
|
||||
|
||||
functions_form(Line, Filename, Module, C) ->
|
||||
{ Export, Private, Def, Defmacro, Defmacrop, Functions } = elixir_def:unwrap_stored_definitions(Module),
|
||||
|
||||
{ FinalExport, FinalFunctions } =
|
||||
add_info_function(Line, Filename, Module, Export, Functions, Def, Defmacro, C),
|
||||
|
||||
Recorded = elixir_import:recorded_locals(Module),
|
||||
elixir_def_local:check_unused_local_macros(Filename, Recorded, Defmacrop),
|
||||
|
||||
{ FinalExport ++ Private, [
|
||||
{attribute, Line, export, lists:sort(FinalExport)} | FinalFunctions
|
||||
] }.
|
||||
|
||||
%% Add attributes handling to the form
|
||||
|
||||
attributes_form(Line, _Filename, Module, Current) ->
|
||||
Transform = fun(X, Acc) -> [translate_attribute(Line, X)|Acc] end,
|
||||
Attributes = ets:lookup_element(data_table(Module), attributes, 2),
|
||||
lists:foldl(Transform, Current, Attributes).
|
||||
|
||||
%% Loads the form into the code server.
|
||||
|
||||
load_form(Forms, S) ->
|
||||
elixir_compiler:module(Forms, S, fun(ModuleName, Binary) ->
|
||||
case get(elixir_compiled) of
|
||||
Current when is_list(Current) ->
|
||||
put(elixir_compiled, [{ModuleName,Binary}|Current]),
|
||||
case get(elixir_parent_compiler) of
|
||||
undefined -> [];
|
||||
PID -> PID ! { module_available, self(), ModuleName, Binary }
|
||||
end;
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end).
|
||||
|
||||
check_module_availability(Line, Filename, Module, Compiler) ->
|
||||
case elixir_compiler:get_opt(ignore_module_conflict, Compiler) of
|
||||
false ->
|
||||
case code:ensure_loaded(Module) of
|
||||
{ module, _ } -> elixir_errors:form_error(Line, Filename, ?MODULE, { module_defined, Module });
|
||||
{ error, _ } -> []
|
||||
end;
|
||||
true ->
|
||||
[]
|
||||
end.
|
||||
|
||||
% EXTRA FUNCTIONS
|
||||
|
||||
add_info_function(Line, Filename, Module, Export, Functions, Def, Defmacro, C) ->
|
||||
Pair = { '__info__', 1 },
|
||||
case lists:member(Pair, Export) of
|
||||
true -> elixir_errors:form_error(Line, Filename, ?MODULE, {internal_function_overridden, Pair});
|
||||
false ->
|
||||
Docs = elixir_compiler:get_opt(docs, C),
|
||||
Contents = { function, Line, '__info__', 1, [
|
||||
functions_clause(Line, Def),
|
||||
macros_clause(Line, Defmacro),
|
||||
data_clause(Line, Module),
|
||||
docs_clause(Line, Module, Docs),
|
||||
moduledoc_clause(Line, Module, Docs),
|
||||
compile_clause(Line),
|
||||
else_clause(Line)
|
||||
] },
|
||||
{ [Pair|Export], [Contents|Functions] }
|
||||
end.
|
||||
|
||||
functions_clause(Line, Def) ->
|
||||
Sorted = ordsets:from_list([{'__info__',1}|Def]),
|
||||
{ clause, Line, [{ atom, Line, functions }], [], [elixir_tree_helpers:abstract_syntax(Sorted)] }.
|
||||
|
||||
macros_clause(Line, Defmacro) ->
|
||||
Sorted = ordsets:from_list(Defmacro),
|
||||
{ clause, Line, [{ atom, Line, macros }], [], [elixir_tree_helpers:abstract_syntax(Sorted)] }.
|
||||
|
||||
docs_clause(Line, Module, true) ->
|
||||
Docs = ordsets:from_list(ets:tab2list(docs_table(Module))),
|
||||
{ clause, Line, [{ atom, Line, docs }], [], [elixir_tree_helpers:abstract_syntax(Docs)] };
|
||||
|
||||
docs_clause(Line, _Module, _) ->
|
||||
{ clause, Line, [{ atom, Line, docs }], [], [{ atom, Line, nil }] }.
|
||||
|
||||
moduledoc_clause(Line, Module, true) ->
|
||||
Docs = '__MAIN__.Module':read_data(Module, moduledoc),
|
||||
{ clause, Line, [{ atom, Line, moduledoc }], [], [elixir_tree_helpers:abstract_syntax({ Line, Docs })] };
|
||||
|
||||
moduledoc_clause(Line, _Module, _) ->
|
||||
{ clause, Line, [{ atom, Line, moduledoc }], [], [{ atom, Line, nil }] }.
|
||||
|
||||
data_clause(Line, Module) ->
|
||||
DataTable = data_table(Module),
|
||||
Data = ets:lookup_element(DataTable, data, 2),
|
||||
Pruned = translate_data(Data),
|
||||
{ clause, Line, [{ atom, Line, data }], [], [elixir_tree_helpers:abstract_syntax(Pruned)] }.
|
||||
|
||||
compile_clause(Line) ->
|
||||
Info = { call, Line, { atom, Line, module_info }, [{ atom, Line, compile }] },
|
||||
WrappedInfo = ?ELIXIR_WRAP_CALL(Line, '__MAIN__.Keyword', 'from_enum', [Info]),
|
||||
{ clause, Line, [{ atom, Line, compile }], [], [WrappedInfo] }.
|
||||
|
||||
else_clause(Line) ->
|
||||
Info = { call, Line, { atom, Line, module_info }, [{ var, Line, atom }] },
|
||||
{ clause, Line, [{ var, Line, atom }], [], [Info] }.
|
||||
|
||||
% HELPERS
|
||||
|
||||
callbacks_for(Line, Kind, Module, Args, S) ->
|
||||
Table = data_table(Module),
|
||||
Callbacks = ets:lookup_element(Table, Kind, 2),
|
||||
|
||||
{ Exprs, Refers } = lists:mapfoldl(
|
||||
fun (X, Acc) -> each_callback_for(Line, Args, X, Acc) end,
|
||||
S#elixir_scope.refer, Callbacks),
|
||||
|
||||
{ Exprs, S#elixir_scope{refer=Refers} }.
|
||||
|
||||
each_callback_for(Line, Args, {M,F}, Acc) ->
|
||||
Expr = { { '.', Line, [M,F] }, Line, Args },
|
||||
Refer = case orddict:find(M, Acc) of
|
||||
{ ok, _ } -> Acc;
|
||||
_ -> orddict:store(M, M, Acc)
|
||||
end,
|
||||
{ Expr, Refer }.
|
||||
|
||||
% ATTRIBUTES & DATA
|
||||
|
||||
translate_attribute(Line, X) ->
|
||||
{ attribute, Line, element(1, X), element(2, X) }.
|
||||
|
||||
translate_data([{K,V}|T]) when
|
||||
K == doc; K == moduledoc; K == spec; K == type;
|
||||
K == export_type; K == callbacks; K == overridable;
|
||||
V == nil ->
|
||||
translate_data(T);
|
||||
|
||||
translate_data([{K,V}|T]) ->
|
||||
[{K,V}|translate_data(T)];
|
||||
|
||||
translate_data([]) -> [].
|
||||
|
||||
% ERROR HANDLING
|
||||
|
||||
format_error({ internal_function_overridden, { Name, Arity } }) ->
|
||||
io_lib:format("function ~s/~B is internal and should not be overriden", [Name, Arity]);
|
||||
|
||||
format_error({ invalid_module, Module}) ->
|
||||
io_lib:format("invalid module name: ~p", [Module]);
|
||||
|
||||
format_error({ module_defined, Module }) ->
|
||||
io_lib:format("module ~s already defined (please remove already compiled files before recompiling a module)",
|
||||
[elixir_errors:inspect(Module)]).
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user