Compare commits

..
14 Commits
Author SHA1 Message Date
hermes-agent c345dab9c1 Merge pull request 'adapter: support OpenAI streaming (SSE) in /v1/chat/completions' (#1) from feat/adapter-streaming into main 2026-09-12 12:51:34 +02:00
hermes-agent 2d46eda619 adapter: support OpenAI streaming (SSE) in /v1/chat/completions
OpenCode's @ai-sdk/openai-compatible sends stream:true and would render an
empty response because the adapter always returned a single non-streaming
chat.completion JSON body. Now when stream:true, emit OpenAI-compatible SSE
chat.completion.chunk events (role, content, [DONE]) so streaming clients
render text. Non-streaming path unchanged.

Adds a local EchoServer test that exercises the streaming route end-to-end.
2026-09-12 10:50:47 +00:00
hermes-agent beb9b1b3ef Add temporary chat-request logging (diagnose SwiftChat error) 2026-09-11 18:44:45 +00:00
hermes-agent cb512a7f17 admin: serve the page without requiring the Bearer header
A browser opening /admin can't send an Authorization header, so the
admin page was unreachable (401 blank). Serve the HTML form openly — it
exposes no data — and let the in-page ADMIN_API_KEY field drive the
auth'd /admin/agents CRUD calls.
2026-09-11 15:36:15 +00:00
hermes-agent e4fdeb6b79 nixos-module: set RELEASE_COOKIE so the release starts
The Elixir release's start script reads releases/COOKIE which isn't baked
in, so the service crashed on boot (cat: releases/COOKIE: No such file).
Set RELEASE_COOKIE in the systemd Environment to fix startup.
2026-09-10 14:35:28 +00:00
hermes-agent 6b22171018 flake: fill mixFodDeps hash 2026-09-10 07:21:42 +00:00
hermes-agent f0112289e6 flake: add mixFodDeps (fetchMixDeps) for Hex deps 2026-09-10 07:17:56 +00:00
hermes-agent 72e18a0a0c Remove AGENTS env seeding; agents managed only via admin API
The store now starts empty and agents are added/removed exclusively through
the web admin page / admin API, persisted to AGENTS_FILE. No AGENTS env var
needed in the sops secret.
2026-09-10 07:06:26 +00:00
hermes-agent e2be3f652e Add web admin page to manage agents
GET /admin serves a self-contained HTML page (ADMIN_API_KEY protected)
that lists agents and lets you add/update/remove them via the admin API —
no redeploy needed to add an agent.
2026-09-10 06:59:08 +00:00
hermes-agent 490bd32322 Add admin API to manage agents at runtime
- AgentRegistry is now file-backed (AGENTS_FILE, default
  /var/lib/n8n-openai/agents.json): agents persist across restarts and
  can be added/removed without a redeploy.
- New admin endpoints (separate ADMIN_API_KEY):
    GET    /admin/agents
    POST   /admin/agents   {model, webhook}
    DELETE /admin/agents/:model
- AGENTS env only seeds the store on first boot; the file is authoritative.
- NixOS module sets AGENTS_FILE under the writable StateDirectory.
2026-09-10 06:46:05 +00:00
hermes-agent 1bfcba133a Add NixOS module for the adapter service
Export nixosModules.default so the service (systemd unit, service user,
sops secret) is defined in the flake, not re-declared in each host config.
Consume with imports = [ inputs.n8n-openai-adapter.nixosModules.default ]
+ services.n8n-openai-adapter = { enable = true; domain = ...; port = ...; }.
2026-09-10 06:36:44 +00:00
hermes-agent c3d024c16c Apply mix format 2026-09-09 21:46:16 +00:00
hermes-agent 1530a761d5 Add flake.lock pinning nixpkgs 2026-09-09 21:44:55 +00:00
hermes-agent 5197b6ece6 OpenAI-compatible adapter for n8n chat agents (Elixir)
Exposes self-hosted n8n chat agents behind /v1/chat/completions and
/v1/models. Model -> n8n webhook routing via a GenServer registry, so
multiple agents map to multiple models. Plug + Bandit, req for the
n8n webhook call, Bearer auth (ADAPTER_API_KEY). Ships a flake.nix
(beamPackages.mixRelease) so it can be consumed as a NixOS flake input.
2026-09-09 21:43:21 +00:00
178 changed files with 1182 additions and 22243 deletions
+11 -11
View File
@@ -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
-8
View File
@@ -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
-3
View File
@@ -1,3 +0,0 @@
# v0.5.0 (2012-05-24)
* First official release
-10
View File
@@ -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)
-13
View File
@@ -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.
-48
View File
@@ -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"
+73 -36
View File
@@ -1,57 +1,94 @@
![Elixir](https://github.com/elixir-lang/elixir/raw/master/logo.png)
=========
[![Build Status](https://secure.travis-ci.org/elixir-lang/elixir.png?branch=master "Build Status")](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
View File
@@ -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
View File
@@ -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 "$@"
-24
View File
@@ -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
View File
@@ -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 "$@"
-24
View File
@@ -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 %*
-14
View File
@@ -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" "$@"
-2
View File
@@ -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" %*
+7
View File
@@ -0,0 +1,7 @@
import Config
import_config "#{config_env()}.exs"
if config_env() == :test do
config :logger, level: :warning
end
+3
View File
@@ -0,0 +1,3 @@
import Config
# Dev: no special config — all runtime settings come from env vars.
+5
View File
@@ -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.
+7
View File
@@ -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
View File
@@ -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
}
+48
View File
@@ -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;
};
}
-41
View File
@@ -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
View File
@@ -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
View File
@@ -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
-74
View File
@@ -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
-257
View File
@@ -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
-48
View File
@@ -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
View File
@@ -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
View File
@@ -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
-45
View File
@@ -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
View File
@@ -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
-117
View File
@@ -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
-57
View File
@@ -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
-132
View File
@@ -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
-144
View File
@@ -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
-249
View File
@@ -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
-27
View File
@@ -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
-106
View File
@@ -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
-120
View File
@@ -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
-496
View File
@@ -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
View File
@@ -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
-74
View File
@@ -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
-407
View File
@@ -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
-90
View File
@@ -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
-63
View File
@@ -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
-124
View File
@@ -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
-65
View File
@@ -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
-197
View File
@@ -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
View File
@@ -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
-48
View File
@@ -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
-74
View File
@@ -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
-46
View File
@@ -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
View File
@@ -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
View File
@@ -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
-32
View File
@@ -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
-32
View File
@@ -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
View File
@@ -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
+124
View File
@@ -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
+25
View File
@@ -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
+468
View File
@@ -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
View File
@@ -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
-68
View File
@@ -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
-72
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
-9
View File
@@ -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
View File
@@ -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
-5
View File
@@ -1,5 +0,0 @@
defmodule URI.FTP do
@behavior URI.Parser
def default_port(), do: 21
def parse(info), do: info
end
-5
View File
@@ -1,5 +0,0 @@
defmodule URI.HTTP do
@behavior URI.Parser
def default_port(), do: 80
def parse(info), do: info
end
-5
View File
@@ -1,5 +0,0 @@
defmodule URI.HTTPS do
@behavior URI.Parser
def default_port(), do: 443
def parse(info), do: info
end
-7
View File
@@ -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
-11
View File
@@ -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
-5
View File
@@ -1,5 +0,0 @@
defmodule URI.SFTP do
@behavior URI.Parser
def default_port(), do: 22
def parse(info), do: info
end
-5
View File
@@ -1,5 +0,0 @@
defmodule URI.TFTP do
@behavior URI.Parser
def default_port(), do: 69
def parse(info), do: info
end
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

+29
View File
@@ -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
+16
View File
@@ -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"},
}
+86
View File
@@ -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 = { };
};
}
Vendored
BIN
View File
Binary file not shown.
-23
View File
@@ -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}
]}.
-10
View File
@@ -1,10 +0,0 @@
{application, elixir,
[{description, "elixir"},
{vsn, "0.5.0"},
{modules, [
elixir
]},
{registered,[elixir_code_server]},
{applications, [kernel,stdlib]},
{mod, {elixir,[]}}
]}.
-124
View File
@@ -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.
-241
View File
@@ -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.
-64
View File
@@ -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 }.
-233
View File
@@ -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).
-290
View File
@@ -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]).
-59
View File
@@ -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.
-70
View File
@@ -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]).
-101
View File
@@ -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 >>.
-346
View File
@@ -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}
].
-97
View File
@@ -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).
-284
View File
@@ -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).
-276
View File
@@ -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','*'}
].
-149
View File
@@ -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.
-266
View File
@@ -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.
-267
View File
@@ -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