Current section
Files
Jump to
Current section
Files
lib/constants.ex
# Copyright 2014 Josh Burroughs <josh@qhool.com>
# 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.
defmodule Quaff.Constants do
require Record
defmodule CompileError do
defexception message: nil
def exception(opts) do
file = opts[:file] || "<unknown file>"
line = opts[:line] || -1
msg = opts[:message] ||
List.to_string(:io_lib.format( opts[:format], opts[:items] ))
msg = msg <> List.to_string(:io_lib.format( "~n at ~s line ~p", [file,line] ))
%__MODULE__{message: msg}
end
end
defmacro include(header) do
quote do
Quaff.Constants.include(unquote(header),[])
end
end
defmacro include(header, options) do
header =
cond do
:ok == Macro.validate(header) ->
{hd, []} = header |> Code.eval_quoted([], __CALLER__)
hd
true -> header
end
use_constants = options[:constants] || :all
do_export = options[:export] || false
in_module = options[:module] || Macro.expand(quote do __MODULE__ end,__CALLER__)
rel_dir =
cond do
options[:relative_to] && :ok == Macro.validate(options[:relative_to]) ->
{rel_to, []} = options[:relative_to] |> Code.eval_quoted([], __CALLER__)
rel_to
options[:relative_to] -> options[:relative_to]
true -> Macro.expand(quote do __DIR__ end, __CALLER__)
end
inc_dir =
cond do
options[:include] && :ok == Macro.validate(options[:include]) ->
{inc, []} = options[:include] |> Code.eval_quoted([], __CALLER__)
inc
options[:include] -> options[:include]
true -> [Macro.expand(quote do __DIR__ end, __CALLER__)]
end
options = Keyword.put(options, :module, in_module)
options = Keyword.put(options, :relative_to, rel_dir)
options = Keyword.put(options, :include, inc_dir)
const = Enum.map(get_constants(header, options),
fn({c,v}) ->
{normalize_const(c),v}
end)
const = case use_constants do
:all -> const
_ ->
Enum.map(List.wrap(use_constants),
fn(c) ->
c = normalize_const(c)
{c,Keyword.fetch!(const,c)}
end
)
end
attrs =
Enum.map( const,
fn({c,val}) ->
quote do
Module.put_attribute(__MODULE__, unquote(c),
unquote(Macro.escape(val)))
end
end)
funs =
case do_export do
true ->
Enum.map( const,
fn({c,_}) ->
{:ok,ident} =
Code.string_to_quoted(Atom.to_string(c))
quote do
def unquote(ident) do
@unquote(ident)
end
end
end )
_ -> []
end
attrs ++ funs
end
defmacro include_lib(header) do
quote do
Quaff.Constants.include_lib(unquote(header),[])
end
end
defmacro include_lib(header, options) do
opts = options |>
Macro.expand_once(__CALLER__) |>
Keyword.put(:include_lib, true)
quote do
Quaff.Constants.include(unquote(header), unquote(opts))
end
end
defp normalize_const(a) when is_atom(a) do
normalize_const(Atom.to_string(a))
end
defp normalize_const(name) do
c = String.first(name)
normed_str =
case String.upcase(c) do
^c -> "_"<>name
_ -> name
end
String.to_atom(normed_str)
end
def get_constants(header_file) do
get_constants(header_file,[])
end
def get_constants(header_file,options) do
incl_type = case options[:include_lib] do
true -> :macro_include_lib
_ -> :macro_include
end
in_module = options[:module] || caller_module() || __MODULE__
relative_dir = options[:relative_to] || System.cwd
include_dirs = List.flatten([Keyword.get_values(options,:include),
Keyword.get_values(options,:i)])
include_dirs = Enum.map(include_dirs,
fn(d) -> Path.expand(d,relative_dir) end)
{:ok, abs_header, tree} = read_header(header_file, inc_type: incl_type,
relative_to: relative_dir,
include_path: include_dirs,
from_file: "x",
from_line: -1 )
ctx = init_ctx(in_module,abs_header,include_dirs)
defs = find_defns(tree,ctx)
Enum.flat_map(defs,
fn({macro,all_arity}) ->
case Map.get(all_arity,0) do
nil -> []
{[],defn} ->
case parse_constant(defn) do
nil -> []
val -> [{macro,val}]
end
end
end)
end
defp parse_constant(defn) do
case :erl_parse.parse_exprs(defn++[{:dot,0}]) do
{:ok, exprs} ->
case :erl_eval.exprs(exprs,[]) do
{:value, val, _} ->
case has_funs?(val) do
true -> nil
_ -> val
end
_ -> nil
end
_ -> nil
end
end
#funs don't appear to work as module attrs
defp has_funs?(f) when is_function(f) do
true
end
defp has_funs?(ls) when is_list(ls) do
Enum.any?(ls,&has_funs?/1)
end
defp has_funs?(t) when is_tuple(t) do
has_funs?(Tuple.to_list(t))
end
defp has_funs?(_) do
false
end
defp read_header(header_file, inc_type: incl_type,
relative_to: relative_dir,
include_path: include_path,
from_file: from_file,
from_line: from_line ) do
IO.puts("calling resolve_include(#{incl_type}, #{header_file}, #{relative_dir}, #{include_path})")
{:ok,realfile} =
case resolve_include(incl_type,header_file,relative_dir,include_path) do
{:ok,_} = res -> res
{:error,{:not_found,_}} ->
raise( CompileError, format: "Can't locate header ~s~n relative to: ~s~n include path was: ~p",
items: [header_file,relative_dir,include_path],
file: from_file, line: from_line )
end
{:ok,contents} =
case File.read(realfile) do
{:ok,_} = res -> res
{:error,reason} ->
raise( CompileError, format: "Error reading ~s: ~p",
items: [realfile,reason],
file: from_file, line: from_line )
end
contents = String.to_charlist(contents)
{:ok,h_toks,_} = :erl_scan.string(contents,{1,1})
tokens = mark_keywords(h_toks)
{:ok, toks} = :aleppo_parser.parse(tokens)
{:ok, realfile, toks}
end
#this does not work correctly for tail calls
defp caller_module() do
trace =
try do throw(:not_a_problem)
catch :not_a_problem ->
:erlang.get_stacktrace()
end
modules = Enum.map(trace,fn({m,_,_,_}) -> m end)
List.first(Enum.filter(modules,fn(__MODULE__) ->
false
(_) ->
true
end))
end
defp resolve_include(incl_type,file,rel,incl_path) when is_list(file) do
resolve_include(incl_type,List.to_string(file),rel,incl_path)
end
defp resolve_include(:macro_include_lib, ("/"<>_)=abs_file,_,_) do
case File.exists?(abs_file) do
true -> {:ok, abs_file}
_ ->
{:error, {:not_found,abs_file}}
end
end
defp resolve_include(:macro_include_lib, ("./"<>_)=rel_file,rel,_) do
unrelative = Path.expand(rel_file, rel)
case File.exists?(unrelative) do
true -> {:ok, unrelative}
_ ->
{:error, {:not_found,unrelative}}
end
end
defp resolve_include(:macro_include_lib, ("../"<>_)=rel_file,rel,_) do
unrelative = Path.expand(rel_file, rel)
case File.exists?(unrelative) do
true -> {:ok, unrelative}
_ ->
{:error, {:not_found,unrelative}}
end
end
defp resolve_include(:macro_include_lib,file,_,_) do
[app_name|file_path] = :filename.split(String.to_charlist(file))
case :code.lib_dir(List.to_atom(app_name)) do
{:error, _} ->
{:error, {:not_found,file}}
app_lib ->
{:ok,List.to_string(:filename.join([app_lib|file_path]))}
end
end
defp resolve_include(:macro_include,file,rel,incl_path) do
resolve_include(file,rel,incl_path)
end
defp resolve_include("$"<>incl,rel,incl_path) do
[_,var_name,suff] = Regex.run( Regex.compile!("(\w+)(.*)$"), incl )
resolve_include((System.get_env(var_name)||"")<>suff,rel,incl_path)
end
defp resolve_include(("/"<>_)=abs_file,_,_) do
case File.exists?(abs_file) do
true -> {:ok, abs_file}
_ ->
{:error, {:not_found,abs_file}}
end
end
defp resolve_include(file,relative_to,include_path) when is_binary(relative_to) do
unrelative = Path.expand(file,relative_to)
case File.exists?(unrelative) do
true -> {:ok, unrelative}
_ ->
case file do
"./"<>_ -> {:error, {:not_found,file}}
"../"<>_ -> {:error, {:not_found,file}}
_ -> resolve_include(file,include_path)
end
end
end
defp resolve_include(file,[inc|include_path]) do
full = Path.expand(file,inc)
case File.exists?(full) do
true -> {:ok, full}
_ ->
resolve_include(file,include_path)
end
end
defp resolve_include(file,[]) do
#last ditch effort
case :code.where_is_file(String.to_charlist(file)) do
:non_existing -> {:error, {:not_found,file}}
filename -> {:ok, List.to_string(filename)}
end
end
defp expand_nested(tokens,ctx) do
expand_nested(tokens,[],ctx)
end
defp expand_nested([],acc,_) do
Enum.reverse(acc)
end
defp expand_nested([{:macro,{_,loc,:LINE}}|tokens], acc, ctx) do
line = case loc do
{line,_col} -> line
_ -> loc
end
expand_nested(tokens, [{:integer,loc,line}|acc], ctx)
end
defp expand_nested([{:macro,{_,_,name}}|tokens], acc, ctx) do
{[],def_toks} = get_def(ctx,{name,0})
expand_nested( tokens, Enum.reverse(def_toks) ++ acc, ctx)
end
defp expand_nested([{:macro,{_,_,name},args}|tokens], acc, ctx) do
{arg_names,def_toks} = get_def(ctx,{name,length(args)})
arg_mapping = List.zip([arg_names,args])
filled_in =
Enum.flat_map(def_toks,
fn({:var,_,varname}=tok) ->
case Keyword.get(arg_mapping,varname) do
nil -> [tok]
replacement -> replacement
end
(tok) -> [tok]
end)
expand_nested(tokens,Enum.reverse(filled_in)++acc,ctx)
end
defp expand_nested([other|tokens],acc,ctx) do
expand_nested(tokens,[other|acc],ctx)
end
defp find_defns([],ctx) do
defs_list(ctx)
end
defp find_defns([{:eof,_}|tree],ctx) do
find_defns( tree, pop_file(ctx))
end
defp find_defns([{:macro_define,{_,_,name}}|tree],ctx) do
find_defns( tree, put_def(ctx,{name,0},{[],true}) )
end
defp find_defns([{:macro_define,{_,_,name},toks}|tree],ctx) do
expanded = expand_nested(toks,ctx)
find_defns( tree, put_def(ctx,{name,0},{[],expanded}) )
end
defp find_defns([{:macro_define,{_,_,name},args,toks}|tree],ctx) do
expanded = expand_nested(toks,ctx)
arg_names = Enum.map(args,
fn([{:var,_,var_name}]) ->
var_name
end)
find_defns( tree, put_def(ctx,{name,length(args)},{arg_names,expanded}) )
end
defp find_defns([{:macro_undef,{_,_,name}}|tree],ctx) do
find_defns( tree, rm_def(ctx,name) )
end
defp find_defns([{incl_type,{:string,loc,file}}|tree],ctx)
when incl_type in [:macro_include,:macro_include_lib] do
{current_file,dir,includes} = get_paths(ctx)
line = case loc do
{ln,_} -> ln
ln -> ln
end
{:ok,abs_file,subtree} = read_header(file, inc_type: incl_type,
relative_to: dir,
include_path: includes,
from_file: current_file,
from_line: line )
find_defns(subtree++tree,push_file(ctx,abs_file))
end
defp find_defns([{:macro_ifdef,x,ifbody}|tree],ctx) do
find_defns([{:macro_ifdef,x,ifbody,[]}|tree],ctx)
end
defp find_defns([{:macro_ifndef,x,ifbody}|tree],ctx) do
find_defns([{:macro_ifdef,x,[],ifbody}|tree],ctx)
end
defp find_defns([{:macro_ifndef,x,ifbody,elsebody}|tree],ctx) do
find_defns([{:macro_ifdef,x,elsebody,ifbody}|tree],ctx)
end
defp find_defns([{:macro_ifdef,{_,_,name},ifbody,elsebody}|tree],ctx) do
case has_def?(ctx,name) do
true -> find_defns(ifbody++tree,ctx)
false -> find_defns(elsebody++tree,ctx)
end
end
defp find_defns([_other|tree],ctx) do
find_defns(tree,ctx)
end
defp mark_keywords(tokens) do
mark_keywords(tokens,[])
end
defp mark_keywords([{:"-",{_,1}}=dash,{:atom,loc,kw}|tokens],out)
when kw in [:define,:ifdef,:ifndef,:"else",:endif,:undef,:include,:include_lib] do
keyword = String.to_atom(Atom.to_string(kw)<>"_keyword")
mark_keywords(tokens, [{keyword,loc},dash|out])
end
defp mark_keywords([tok|tokens],out) do
mark_keywords(tokens,[tok|out])
end
defp mark_keywords([],out) do
Enum.reverse(out)
end
Record.defrecordp :qc_ctx, [:defs, :files, :relative_dirs, :includes]
## defs dictionary:
defp init_ctx() do
qc_ctx( defs: Map.new(), files: [], relative_dirs: [], includes: [] )
end
defp init_ctx(module,file,incls) do
defs = [{ {:MODULE,0},{[],[{:atom,{1,1},module}]} },
{ {:MODULE_STRING,0},{[],[{:string,{1,1},Atom.to_charlist(module)}]} }]
ctx = qc_ctx(init_ctx(),includes: incls)
put_defs( push_file(ctx, file), defs )
end
defp push_file(qc_ctx( files: files,
relative_dirs: rels,
includes: incls )=ctx,file) do
ctx=put_def(ctx,{:FILE,0},{[],[{:string,{1,1},file}]})
dir = Path.dirname(file)
new_incl = Path.expand("../include",dir)
qc_ctx( ctx, files: [file|files], relative_dirs: [dir|rels], includes: [new_incl|incls] )
end
defp pop_file(qc_ctx( files: [_|files],
relative_dirs: [_|rels],
includes: [_|incls] )=ctx) do
ctx =
case files do
[old_file|_] ->
put_def(ctx,{:FILE,0},{[],[{:string,{1,1},old_file}]})
[] ->
ctx
end
qc_ctx( ctx, files: files, relative_dirs: rels, includes: incls )
end
defp get_paths(qc_ctx( files: [f|_], relative_dirs: [rel|_], includes: incls )) do
{f,rel,incls}
end
defp get_def(qc_ctx( defs: defs ),{name,arity}) do
all_arity = Map.fetch!(defs,name)
Map.fetch!(all_arity,arity)
end
defp defs_list(qc_ctx(defs: defs)) do
Map.to_list(defs)
end
defp put_defs( ctx, defs ) when is_list(defs) do
Enum.reduce(defs,ctx,fn({d,defn},subctx) -> put_def(subctx,d,defn) end)
end
defp put_def( qc_ctx( defs: defs )=ctx,{name,arity},defn) do
all_arity =
case Map.get(defs,name) do
nil -> Map.new()
aa -> aa
end
qc_ctx(ctx, defs: Map.put(defs,name,Map.put(all_arity,arity,defn)))
end
defp has_def?(qc_ctx(defs: defs),name) do
Map.has_key?(defs,name)
end
defp rm_def(qc_ctx(defs: defs)=ctx,name) do
qc_ctx(ctx, defs: Map.delete(defs,name))
end
end