Browse Source

Restructure compiler code (close #136)

The common infrastructure routines are left in erlydtl_compiler,
while those specific for compiling to .beam files are moved into
erlydtl_beam_compiler and generic stuff is put in erlydtl_compiler_utils.
Andreas Stenius 11 years ago
parent
commit
26b6a793e1

+ 4 - 0
include/erlydtl_ext.hrl

@@ -54,3 +54,7 @@
           pos={1,1},
           state=in_text
         }).
+
+
+-define(ERR(Err, Ctx), erlydtl_compiler_utils:add_error(?MODULE, Err, Ctx)).
+-define(WARN(Warn, Ctx), erlydtl_compiler_utils:add_warning(?MODULE, Warn, Ctx)).

+ 28 - 0
src/erlydtl.erl

@@ -2,7 +2,9 @@
 %%% File:      erlydtl.erl
 %%% @author    Roberto Saccon <rsaccon@gmail.com> [http://rsaccon.com]
 %%% @author    Evan Miller <emmiller@gmail.com>
+%%% @author    Andreas Stenius <kaos@astekk.se>
 %%% @copyright 2008 Roberto Saccon, Evan Miller
+%%% @copyright 2014 Andreas Stenius
 %%% @doc
 %%% Public interface for ErlyDTL
 %%% @end
@@ -10,6 +12,7 @@
 %%% The MIT License
 %%%
 %%% Copyright (c) 2008 Roberto Saccon, Evan Miller
+%%% Copyright (c) 2014 Andreas Stenius
 %%%
 %%% Permission is hereby granted, free of charge, to any person obtaining a copy
 %%% of this software and associated documentation files (the "Software"), to deal
@@ -30,12 +33,18 @@
 %%% THE SOFTWARE.
 %%%
 %%% @since 2007-11-11 by Roberto Saccon, Evan Miller
+%%% @since 2014 by Andreas Stenius
 %%%-------------------------------------------------------------------
 -module(erlydtl).
 -author('rsaccon@gmail.com').
 -author('emmiller@gmail.com').
+-author('Andreas Stenius <kaos@astekk.se>').
 
+
+%% --------------------------------------------------------------------
 %% API
+%% --------------------------------------------------------------------
+
 -export([compile_file/2, compile_file/3]).
 -export([compile_template/2, compile_template/3]).
 -export([compile/2, compile/3]).
@@ -50,6 +59,11 @@
 -type ok_ret() :: {ok, Module::atom()} | {ok, Module::atom(), warnings()}.
 -type err_ret() :: error | {error, errors(), warnings()}.
 
+
+%% --------------------------------------------------------------------
+%% Compile file
+%% --------------------------------------------------------------------
+
 -spec compile_file( list() | binary(), atom() ) -> {ok, Module::atom()} | error.
 compile_file(File, Module) ->
     erlydtl_compiler:compile_file(File, Module, erlydtl_compiler:default_options()).
@@ -58,6 +72,11 @@ compile_file(File, Module) ->
 compile_file(File, Module, Options) ->
     erlydtl_compiler:compile_file(File, Module, Options).
 
+
+%% --------------------------------------------------------------------
+%% Compile template
+%% --------------------------------------------------------------------
+
 -spec compile_template( list() | binary(), atom() ) -> {ok, Module::atom()} | error.
 compile_template(Template, Module) ->
     erlydtl_compiler:compile_template(Template, Module, erlydtl_compiler:default_options()).
@@ -66,6 +85,11 @@ compile_template(Template, Module) ->
 compile_template(Template, Module, Options) ->
     erlydtl_compiler:compile_template(Template, Module, Options).
 
+
+%% --------------------------------------------------------------------
+%% Compile directory
+%% --------------------------------------------------------------------
+
 -spec compile_dir(list() | binary(), atom()) -> {ok, Module::atom()} | error.
 compile_dir(DirectoryPath, Module) ->
     erlydtl_compiler:compile_dir(DirectoryPath, Module, erlydtl_compiler:default_options()).
@@ -75,6 +99,10 @@ compile_dir(DirectoryPath, Module, Options) ->
     erlydtl_compiler:compile_dir(DirectoryPath, Module, Options).
 
 
+%% --------------------------------------------------------------------
+%% Legacy API
+%% --------------------------------------------------------------------
+
 %% keep for backwards compatibility, with a tuple-twist to ease migration / offer alternative path..
 -spec compile(FileOrBinary, atom() ) -> {ok, Module::atom()} | error
                                             when FileOrBinary :: list() | binary() 

+ 1304 - 0
src/erlydtl_beam_compiler.erl

@@ -0,0 +1,1304 @@
+%%%-------------------------------------------------------------------
+%%% File:      erlydtl_beam_compiler.erl
+%%% @author    Roberto Saccon <rsaccon@gmail.com> [http://rsaccon.com]
+%%% @author    Evan Miller <emmiller@gmail.com>
+%%% @author    Andreas Stenius <kaos@astekk.se>
+%%% @copyright 2008 Roberto Saccon, Evan Miller
+%%% @copyright 2014 Andreas Stenius
+%%% @doc
+%%% ErlyDTL template compiler for beam targets.
+%%% @end
+%%%
+%%% The MIT License
+%%%
+%%% Copyright (c) 2007 Roberto Saccon, Evan Miller
+%%% Copyright (c) 2014 Andreas Stenius
+%%%
+%%% Permission is hereby granted, free of charge, to any person obtaining a copy
+%%% of this software and associated documentation files (the "Software"), to deal
+%%% in the Software without restriction, including without limitation the rights
+%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+%%% copies of the Software, and to permit persons to whom the Software is
+%%% furnished to do so, subject to the following conditions:
+%%%
+%%% The above copyright notice and this permission notice shall be included in
+%%% all copies or substantial portions of the Software.
+%%%
+%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+%%% THE SOFTWARE.
+%%%
+%%% @since 2007-12-16 by Roberto Saccon, Evan Miller
+%%% @since 2014 by Andreas Stenius
+%%%-------------------------------------------------------------------
+-module(erlydtl_beam_compiler).
+-author('rsaccon@gmail.com').
+-author('emmiller@gmail.com').
+-author('Andreas Stenius <kaos@astekk.se>').
+
+%% --------------------------------------------------------------------
+%% Definitions
+%% --------------------------------------------------------------------
+
+-export([compile/3, compile_dir/2, format_error/1]).
+
+%% internal use
+-export([
+         is_up_to_date/2,
+
+         format/3,
+         value_ast/5,
+         resolve_scoped_variable_ast/2,
+         resolve_scoped_variable_ast/3,
+         interpret_args/3
+        ]).
+
+-import(erlydtl_compiler, [parse_file/2, do_parse_template/2]).
+
+-import(erlydtl_compiler_utils,
+         [unescape_string_literal/1, full_path/2,
+          print/3, get_current_file/1, add_errors/2, add_warnings/2,
+          merge_info/2, call_extension/3, init_treewalker/1
+         ]).
+
+-include_lib("merl/include/merl.hrl").
+-include("erlydtl_ext.hrl").
+
+
+%% --------------------------------------------------------------------
+%% API
+%% --------------------------------------------------------------------
+
+compile(DjangoParseTree, CheckSum, Context) ->
+    compile_to_binary(DjangoParseTree, CheckSum, Context).
+
+compile_dir(Dir, Context) ->
+    do_compile_dir(Dir, Context).
+
+format_error(no_out_dir) ->
+    "Compiled template not saved (need out_dir option)";
+format_error(unexpected_extends_tag) ->
+    "The extends tag must be at the very top of the template";
+format_error(circular_include) ->
+    "Circular file inclusion!";
+format_error({write_file, Error}) ->
+    io_lib:format(
+      "Failed to write file: ~s",
+      [file:format_error(Error)]);
+format_error(compile_beam) ->
+    "Failed to compile template to .beam file";
+format_error(Error) ->
+    %% may be an error thrown from erlydtl_compiler...
+    erlydtl_compiler:format_error(Error).
+
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
+
+do_compile_dir(Dir, Context) ->
+    %% Find all files in Dir (recursively), matching the regex (no
+    %% files ending in "~").
+    Files = filelib:fold_files(Dir, ".+[^~]$", true, fun(F1,Acc1) -> [F1 | Acc1] end, []),
+    {ParserResults,
+     #dtl_context{ errors=#error_info{ list=ParserErrors } }=Context1}
+        = lists:foldl(
+            fun (File, {ResultAcc, Ctx}) ->
+                    case filename:basename(File) of
+                        "."++_ ->
+                            {ResultAcc, Ctx};
+                        _ ->
+                            FilePath = filename:absname(File),
+                            case filelib:is_dir(FilePath) of
+                                true ->
+                                    {ResultAcc, Ctx};
+                                false ->
+                                    case parse_file(FilePath, Ctx) of
+                                        up_to_date -> {ResultAcc, Ctx};
+                                        {ok, DjangoParseTree, CheckSum} ->
+                                            {[{File, DjangoParseTree, CheckSum}|ResultAcc], Ctx};
+                                        {error, Reason} -> {ResultAcc, ?ERR(Reason, Ctx)}
+                                    end
+                            end
+                    end
+            end,
+            {[], Context},
+            Files),
+    if length(ParserErrors) == 0 ->
+            compile_multiple_to_binary(Dir, ParserResults, Context1);
+       true -> Context1
+    end.
+
+compile_multiple_to_binary(Dir, ParserResults, Context0) ->
+    MatchAst = options_match_ast(Context0),
+    {Functions,
+     {AstInfo, _,
+      #dtl_context{ errors=#error_info{ list=Errors } }=Context1}}
+        = lists:mapfoldl(
+            fun({File, DjangoParseTree, CheckSum}, {AstInfo, TreeWalker, Ctx}) ->
+                    try
+                        FilePath = full_path(File, Ctx#dtl_context.doc_root),
+                        {{BodyAst, BodyInfo}, TreeWalker1} = with_dependency(
+                                                               {FilePath, CheckSum},
+                                                               body_ast(DjangoParseTree, Ctx, TreeWalker)),
+                        FunctionName = filename:rootname(filename:basename(File)),
+                        Function1 = ?Q("_@FunctionName@(_Variables) -> _@FunctionName@(_Variables, [])"),
+                        Function2 = erl_syntax:function(
+                                      erl_syntax:atom(FunctionName),
+                                      [erl_syntax:clause(
+                                         [erl_syntax:variable("_Variables"),
+                                          erl_syntax:variable("RenderOptions")],
+                                         none,
+                                         MatchAst ++ stringify(BodyAst, Ctx))
+                                      ]),
+                        {{FunctionName, Function1, Function2}, {merge_info(AstInfo, BodyInfo), TreeWalker1, Ctx}}
+                    catch
+                        throw:Error ->
+                            {error, {AstInfo, TreeWalker, ?ERR(Error, Ctx)}}
+                    end
+            end,
+            {#ast_info{},
+             init_treewalker(Context0),
+             Context0},
+            ParserResults),
+    if length(Errors) == 0 ->
+            Forms = custom_forms(Dir, Context1#dtl_context.module, Functions, AstInfo),
+            compile_forms(Forms, Context1);
+       true ->
+            Context1
+    end.
+
+compile_to_binary(DjangoParseTree, CheckSum, Context) ->
+    try body_ast(DjangoParseTree, Context, init_treewalker(Context)) of
+        {{BodyAst, BodyInfo}, BodyTreeWalker} ->
+            try custom_tags_ast(BodyInfo#ast_info.custom_tags, Context, BodyTreeWalker) of
+                {{CustomTagsAst, CustomTagsInfo}, _} ->
+                    Forms = forms(
+                              Context#dtl_context.module,
+                              {BodyAst, BodyInfo},
+                              {CustomTagsAst, CustomTagsInfo},
+                              CheckSum,
+                              BodyTreeWalker,
+                              Context),
+                    compile_forms(Forms, Context)
+            catch
+                throw:Error -> ?ERR(Error, Context)
+            end
+    catch
+        throw:Error -> ?ERR(Error, Context)
+    end.
+
+compile_forms(Forms, Context) ->
+    maybe_debug_template(Forms, Context),
+    Options = Context#dtl_context.compiler_options,
+    case compile:forms(Forms, Options) of
+        Compiled when element(1, Compiled) =:= ok ->
+            [ok, Module, Bin|Info] = tuple_to_list(Compiled),
+            lists:foldl(
+              fun (F, C) -> F(Module, Bin, C) end,
+              Context#dtl_context{ bin=Bin },
+              [fun maybe_write/3,
+               fun maybe_load/3,
+               fun (_, _, C) ->
+                       case Info of
+                           [Ws] when length(Ws) > 0 ->
+                               add_warnings(Ws, C);
+                           _ -> C
+                       end
+               end
+              ]);
+        error ->
+            ?ERR(compile_beam, Context);
+        {error, Es, Ws} ->
+            add_warnings(Ws, add_errors(Es, Context))
+    end.
+
+maybe_write(Module, Bin, Context) ->
+    case proplists:get_value(out_dir, Context#dtl_context.all_options) of
+        false -> Context;
+        undefined ->
+            ?WARN(no_out_dir, Context);
+        OutDir ->
+            BeamFile = filename:join([OutDir, [Module, ".beam"]]),
+            print("Template module: ~w -> ~s\n", [Module, BeamFile], Context),
+            case file:write_file(BeamFile, Bin) of
+                ok -> Context;
+                {error, Reason} ->
+                    ?ERR({write_file, Reason}, Context)
+            end
+    end.
+
+maybe_load(Module, Bin, Context) ->
+    case proplists:get_bool(no_load, Context#dtl_context.all_options) of
+        true -> Context;
+        false -> load_code(Module, Bin, Context)
+    end.
+
+load_code(Module, Bin, Context) ->
+    code:purge(Module),
+    case code:load_binary(Module, atom_to_list(Module) ++ ".erl", Bin) of
+        {module, Module} -> Context;
+        Error -> ?WARN({load, Error}, Context)
+    end.
+
+maybe_debug_template(Forms, Context) ->
+    %% undocumented option to debug the compiled template
+    case proplists:get_bool(debug_info, Context#dtl_context.all_options) of
+        false -> nop;
+        true ->
+            Options = Context#dtl_context.compiler_options,
+            print("Compiler options: ~p~n", [Options], Context),
+            try
+                Source = erl_prettypr:format(erl_syntax:form_list(Forms)),
+                File = lists:concat([proplists:get_value(source, Options), ".erl"]),
+                io:format("Saving template source to: ~s.. ~p~n",
+                          [File, file:write_file(File, Source)])
+            catch
+                error:Err ->
+                    io:format("Pretty printing failed: ~p~n"
+                              "Context: ~n~p~n"
+                              "Forms: ~n~p~n",
+                              [Err, Context, Forms])
+            end
+    end.
+
+is_up_to_date(CheckSum, Context) ->
+    Module = Context#dtl_context.module,
+    {M, F} = Context#dtl_context.reader,
+    case catch Module:source() of
+        {_, CheckSum} ->
+            case catch Module:dependencies() of
+                L when is_list(L) ->
+                    RecompileList = lists:foldl(
+                                      fun ({XFile, XCheckSum}, Acc) ->
+                                              case catch M:F(XFile) of
+                                                  {ok, Data} ->
+                                                      case binary_to_list(erlang:md5(Data)) of
+                                                          XCheckSum ->
+                                                              Acc;
+                                                          _ ->
+                                                              [recompile | Acc]
+                                                      end;
+                                                  _ ->
+                                                      [recompile | Acc]
+                                              end
+                                      end, [], L),
+                    case RecompileList of
+                        [] -> true;
+                        _ -> false
+                    end;
+                _ ->
+                    false
+            end;
+        _ ->
+            false
+    end.
+
+
+%%====================================================================
+%% AST functions
+%%====================================================================
+
+custom_tags_ast(CustomTags, Context, TreeWalker) ->
+    %% avoid adding the render_tag/3 fun if it isn't used,
+    %% since we can't add a -compile({nowarn_unused_function, render_tag/3}).
+    %% attribute due to a bug in syntax_tools.
+    case custom_tags_clauses_ast(CustomTags, Context, TreeWalker) of
+        skip ->
+            {{erl_syntax:comment(
+                ["% render_tag/3 is not used in this template."]),
+              #ast_info{}},
+             TreeWalker};
+        {{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} ->
+            {{erl_syntax:function(
+                erl_syntax:atom(render_tag),
+                CustomTagsClauses),
+              CustomTagsInfo},
+             TreeWalker1}
+    end.
+
+custom_tags_clauses_ast([], _Context, _TreeWalker) -> skip;
+custom_tags_clauses_ast(CustomTags, Context, TreeWalker) ->
+    custom_tags_clauses_ast1(CustomTags, [], [], #ast_info{}, Context, TreeWalker).
+
+custom_tags_clauses_ast1([], _ExcludeTags, ClauseAcc, InfoAcc, Context, TreeWalker) ->
+    {{DefaultAst, DefaultInfo}, TreeWalker1} =
+        case call_extension(Context, custom_tag_ast, [Context, TreeWalker]) of
+            undefined ->
+                {{?Q("(_TagName, _, _) -> []"), InfoAcc}, TreeWalker};
+            {{ExtAst, ExtInfo}, ExtTreeWalker} ->
+                Clause = ?Q("(TagName, _Variables, RenderOptions) -> _@tag",
+                            [{tag, options_match_ast(Context, ExtTreeWalker) ++ [ExtAst]}]),
+                {{Clause, merge_info(ExtInfo, InfoAcc)}, ExtTreeWalker}
+        end,
+    {{lists:reverse([DefaultAst|ClauseAcc]), DefaultInfo}, TreeWalker1};
+custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Context, TreeWalker) ->
+    case lists:member(Tag, ExcludeTags) of
+        true ->
+            custom_tags_clauses_ast1(CustomTags, ExcludeTags, ClauseAcc, InfoAcc, Context, TreeWalker);
+        false ->
+            CustomTagFile = full_path(Tag, Context#dtl_context.custom_tags_dir),
+            case filelib:is_file(CustomTagFile) of
+                true ->
+                    case parse_file(CustomTagFile, Context) of
+                        {ok, DjangoParseTree, CheckSum} ->
+                            {{BodyAst, BodyAstInfo}, TreeWalker1} = with_dependency(
+                                                                      {CustomTagFile, CheckSum},
+                                                                      body_ast(DjangoParseTree, Context, TreeWalker)),
+                            MatchAst = options_match_ast(Context, TreeWalker),
+                            Clause = ?Q("(_@Tag@, _Variables, RenderOptions) -> _@MatchAst, _@BodyAst"),
+                            custom_tags_clauses_ast1(
+                              CustomTags, [Tag|ExcludeTags],
+                              [Clause|ClauseAcc], merge_info(BodyAstInfo, InfoAcc),
+                              Context, TreeWalker1);
+                        {error, Reason} ->
+                            throw(Reason)
+                    end;
+                false ->
+                    case call_extension(Context, custom_tag_ast, [Tag, Context, TreeWalker]) of
+                        undefined ->
+                            custom_tags_clauses_ast1(
+                              CustomTags, [Tag | ExcludeTags],
+                              ClauseAcc, InfoAcc, Context, TreeWalker);
+                        {{Ast, Info}, TW} ->
+                            Clause = ?Q("(_@Tag@, _Variables, RenderOptions) -> _@match, _@Ast",
+                                        [{match, options_match_ast(Context, TW)}]),
+                            custom_tags_clauses_ast1(
+                              CustomTags, [Tag | ExcludeTags],
+                              [Clause|ClauseAcc], merge_info(Info, InfoAcc),
+                              Context, TW)
+                    end
+            end
+    end.
+
+dependencies_function(Dependencies) ->
+    ?Q("dependencies() -> _@Dependencies@.").
+
+translatable_strings_function(TranslatableStrings) ->
+    ?Q("translatable_strings() -> _@TranslatableStrings@.").
+
+translated_blocks_function(TranslatedBlocks) ->
+    ?Q("translated_blocks() -> _@TranslatedBlocks@.").
+
+variables_function(Variables) ->
+    ?Q("variables() -> _@vars.",
+       [{vars, merl:term(lists:usort(Variables))}]).
+
+custom_forms(Dir, Module, Functions, AstInfo) ->
+    Exported = [erl_syntax:arity_qualifier(erl_syntax:atom(source_dir), erl_syntax:integer(0)),
+                erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0)),
+                erl_syntax:arity_qualifier(erl_syntax:atom(translatable_strings), erl_syntax:integer(0))
+                | lists:foldl(
+                    fun({FunctionName, _, _}, Acc) ->
+                            [erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(1)),
+                             erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(2))
+                             |Acc]
+                    end, [], Functions)
+               ],
+    ModuleAst = ?Q("-module('@Module@')."),
+    ExportAst = ?Q("-export(['@_Exported'/1])"),
+
+    SourceFunctionAst = ?Q("source_dir() -> _@Dir@."),
+
+    DependenciesFunctionAst = dependencies_function(AstInfo#ast_info.dependencies),
+    TranslatableStringsFunctionAst = translatable_strings_function(AstInfo#ast_info.translatable_strings),
+    FunctionAsts = lists:foldl(fun({_, Function1, Function2}, Acc) -> [Function1, Function2 | Acc] end, [], Functions),
+
+    [erl_syntax:revert(X)
+     || X <- [ModuleAst, ExportAst, SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsFunctionAst
+              | FunctionAsts] ++ AstInfo#ast_info.pre_render_asts
+    ].
+
+stringify(BodyAst, #dtl_context{ binary_strings=BinaryStrings }) ->
+    [?Q("erlydtl_runtime:stringify_final(_@BodyAst, '@BinaryStrings@')")].
+
+forms(Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum, TreeWalker,
+      #dtl_context{ parse_trail=[File|_] }=Context) ->
+    MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
+
+    Render0FunctionAst = ?Q("render() -> render([])."),
+    Render1FunctionAst = ?Q("render(Variables) -> render(Variables, [])."),
+
+    Render2FunctionAst = ?Q(["render(Variables, RenderOptions) ->",
+                             "  try render_internal(Variables, RenderOptions) of",
+                             "    Val -> {ok, Val}",
+                             "  catch",
+                             "    Err -> {error, Err}",
+                             "end."
+                            ]),
+
+    SourceFunctionAst = ?Q("source() -> {_@File@, _@CheckSum@}."),
+
+    DependenciesFunctionAst = dependencies_function(MergedInfo#ast_info.dependencies),
+
+    TranslatableStringsAst = translatable_strings_function(MergedInfo#ast_info.translatable_strings),
+
+    TranslatedBlocksAst = translated_blocks_function(MergedInfo#ast_info.translated_blocks),
+
+    VariablesAst = variables_function(MergedInfo#ast_info.var_names),
+
+    MatchAst = options_match_ast(Context, TreeWalker),
+    BodyAstTmp = MatchAst ++ stringify(BodyAst, Context),
+    RenderInternalFunctionAst = ?Q("render_internal(_Variables, RenderOptions) -> _@BodyAstTmp."),
+
+    ModuleAst  = ?Q("-module('@Module@')."),
+
+    ExportAst = erl_syntax:attribute(
+                  erl_syntax:atom(export),
+                  [erl_syntax:list(
+                     [erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(0)),
+                      erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(1)),
+                      erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(2)),
+                      erl_syntax:arity_qualifier(erl_syntax:atom(source), erl_syntax:integer(0)),
+                      erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0)),
+                      erl_syntax:arity_qualifier(erl_syntax:atom(translatable_strings), erl_syntax:integer(0)),
+                      erl_syntax:arity_qualifier(erl_syntax:atom(translated_blocks), erl_syntax:integer(0)),
+                      erl_syntax:arity_qualifier(erl_syntax:atom(variables), erl_syntax:integer(0))
+                     ])
+                  ]),
+
+    erl_syntax:revert_forms(
+      erl_syntax:form_list(
+        [ModuleAst, ExportAst, Render0FunctionAst, Render1FunctionAst, Render2FunctionAst,
+         SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsAst,
+         TranslatedBlocksAst, VariablesAst, RenderInternalFunctionAst,
+         CustomTagsFunctionAst
+         |BodyInfo#ast_info.pre_render_asts
+        ])).
+
+options_match_ast(Context) -> options_match_ast(Context, undefined).
+options_match_ast(Context, TreeWalker) ->
+    [
+     ?Q("_TranslationFun = proplists:get_value(translation_fun, RenderOptions, none)"),
+     ?Q("_CurrentLocale = proplists:get_value(locale, RenderOptions, none)"),
+     ?Q("_RecordInfo = _@info", [{info, merl:term(Context#dtl_context.record_info)}])
+     | case call_extension(Context, setup_render_ast, [Context, TreeWalker]) of
+           undefined -> [];
+           Ast when is_list(Ast) -> Ast
+       end].
+
+%% child templates should only consist of blocks at the top level
+body_ast([{'extends', {string_literal, _Pos, String}} | ThisParseTree], Context, TreeWalker) ->
+    File = full_path(unescape_string_literal(String), Context#dtl_context.doc_root),
+    case lists:member(File, Context#dtl_context.parse_trail) of
+        true ->
+            throw(circular_include);
+        _ ->
+            case parse_file(File, Context) of
+                {ok, ParentParseTree, CheckSum} ->
+                    BlockDict = lists:foldl(
+                                  fun ({block, {identifier, _, Name}, Contents}, Dict) ->
+                                          dict:store(Name, Contents, Dict);
+                                      (_, Dict) ->
+                                          Dict
+                                  end, dict:new(), ThisParseTree),
+                    with_dependency({File, CheckSum},
+                                    body_ast(ParentParseTree,
+                                             Context#dtl_context{
+                                               block_dict = dict:merge(fun(_Key, _ParentVal, ChildVal) -> ChildVal end,
+                                                                       BlockDict, Context#dtl_context.block_dict),
+                                               parse_trail = [File | Context#dtl_context.parse_trail]},
+                                             TreeWalker));
+                {error, Reason} ->
+                    throw(Reason)
+            end
+    end;
+
+
+body_ast(DjangoParseTree, Context, TreeWalker) ->
+    {AstInfoList, TreeWalker2} = lists:mapfoldl(
+                                   fun
+                                       ({'autoescape', {identifier, _, OnOrOff}, Contents}, TreeWalkerAcc) ->
+                                                       body_ast(Contents, Context#dtl_context{auto_escape = OnOrOff},
+                                                                TreeWalkerAcc);
+                                       ({'block', {identifier, _, Name}, Contents}, TreeWalkerAcc) ->
+                                                       Block = case dict:find(Name, Context#dtl_context.block_dict) of
+                                                                   {ok, ChildBlock} -> ChildBlock;
+                                                                   _ -> Contents
+                                                               end,
+                                                       body_ast(Block, Context, TreeWalkerAcc);
+                                       ({'blocktrans', Args, Contents}, TreeWalkerAcc) ->
+                                                       blocktrans_ast(Args, Contents, Context, TreeWalkerAcc);
+                                       ({'call', {identifier, _, Name}}, TreeWalkerAcc) ->
+                                                       call_ast(Name, TreeWalkerAcc);
+                                       ({'call', {identifier, _, Name}, With}, TreeWalkerAcc) ->
+                                                       call_with_ast(Name, With, Context, TreeWalkerAcc);
+                                       ({'comment', _Contents}, TreeWalkerAcc) ->
+                                                       empty_ast(TreeWalkerAcc);
+                                       ({'cycle', Names}, TreeWalkerAcc) ->
+                                                       cycle_ast(Names, Context, TreeWalkerAcc);
+                                       ({'cycle_compat', Names}, TreeWalkerAcc) ->
+                                                       cycle_compat_ast(Names, Context, TreeWalkerAcc);
+                                       ({'date', 'now', {string_literal, _Pos, FormatString}}, TreeWalkerAcc) ->
+                                                       now_ast(FormatString, Context, TreeWalkerAcc);
+                                       ({'filter', FilterList, Contents}, TreeWalkerAcc) ->
+                                                       filter_tag_ast(FilterList, Contents, Context, TreeWalkerAcc);
+                                       ({'firstof', Vars}, TreeWalkerAcc) ->
+                                                       firstof_ast(Vars, Context, TreeWalkerAcc);
+                                       ({'for', {'in', IteratorList, Variable, Reversed}, Contents}, TreeWalkerAcc) ->
+                                                       {EmptyAstInfo, TreeWalker1} = empty_ast(TreeWalkerAcc),
+                                                       for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, Context, TreeWalker1);
+                                       ({'for', {'in', IteratorList, Variable, Reversed}, Contents, EmptyPartContents}, TreeWalkerAcc) ->
+                                                       {EmptyAstInfo, TreeWalker1} = body_ast(EmptyPartContents, Context, TreeWalkerAcc),
+                                                       for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, Context, TreeWalker1);
+                                       ({'if', Expression, Contents, Elif}, TreeWalkerAcc) ->
+                                                       {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
+                                                       {ElifAstInfo, TreeWalker2} = body_ast(Elif, Context, TreeWalker1),
+                                                       ifelse_ast(Expression, IfAstInfo, ElifAstInfo, Context, TreeWalker2);
+                                       ({'if', Expression, Contents}, TreeWalkerAcc) ->
+                                                       {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
+                                                       {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
+                                                       ifelse_ast(Expression, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+                                       ({'ifchanged', '$undefined', Contents}, TreeWalkerAcc) ->
+                                                       {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
+                                                       {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
+                                                       ifchanged_contents_ast(Contents, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+                                       ({'ifchanged', Values, Contents}, TreeWalkerAcc) ->
+                                                       {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
+                                                       {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
+                                                       ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+                                       ({'ifchangedelse', '$undefined', IfContents, ElseContents}, TreeWalkerAcc) ->
+                                                       {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
+                                                       {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
+                                                       ifchanged_contents_ast(IfContents, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+                                       ({'ifchangedelse', Values, IfContents, ElseContents}, TreeWalkerAcc) ->
+                                                       {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
+                                                       {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
+                                                       ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+                                       ({'ifelse', Expression, IfContents, ElseContents}, TreeWalkerAcc) ->
+                                                       {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
+                                                       {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
+                                                       ifelse_ast(Expression, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+                                       ({'ifequal', [Arg1, Arg2], Contents}, TreeWalkerAcc) ->
+                                                       {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
+                                                       {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
+                                                       ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+                                       ({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, TreeWalkerAcc) ->
+                                                       {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
+                                                       {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context,TreeWalker1),
+                                                       ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+                                       ({'ifnotequal', [Arg1, Arg2], Contents}, TreeWalkerAcc) ->
+                                                       {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
+                                                       {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
+                                                       ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+                                       ({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, TreeWalkerAcc) ->
+                                                       {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
+                                                       {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
+                                                       ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+                                       ({'include', {string_literal, _, File}, Args}, TreeWalkerAcc) ->
+                                                       include_ast(unescape_string_literal(File), Args, Context#dtl_context.local_scopes, Context, TreeWalkerAcc);
+                                       ({'include_only', {string_literal, _, File}, Args}, TreeWalkerAcc) ->
+                                                       include_ast(unescape_string_literal(File), Args, [], Context, TreeWalkerAcc);
+                                       ({'regroup', {ListVariable, Grouper, {identifier, _, NewVariable}}, Contents}, TreeWalkerAcc) ->
+                                                       regroup_ast(ListVariable, Grouper, NewVariable, Contents, Context, TreeWalkerAcc);
+                                       ({'spaceless', Contents}, TreeWalkerAcc) ->
+                                                       spaceless_ast(Contents, Context, TreeWalkerAcc);
+                                       ({'ssi', Arg}, TreeWalkerAcc) ->
+                                                       ssi_ast(Arg, Context, TreeWalkerAcc);
+                                       ({'ssi_parsed', {string_literal, _, FileName}}, TreeWalkerAcc) ->
+                                                       include_ast(unescape_string_literal(FileName), [], Context#dtl_context.local_scopes, Context, TreeWalkerAcc);
+                                       ({'string', _Pos, String}, TreeWalkerAcc) ->
+                                                       string_ast(String, Context, TreeWalkerAcc);
+                                       ({'tag', {identifier, _, Name}, Args}, TreeWalkerAcc) ->
+                                                       tag_ast(Name, Args, Context, TreeWalkerAcc);
+                                       ({'templatetag', {_, _, TagName}}, TreeWalkerAcc) ->
+                                                       templatetag_ast(TagName, Context, TreeWalkerAcc);
+                                       ({'trans', Value}, TreeWalkerAcc) ->
+                                                       translated_ast(Value, Context, TreeWalkerAcc);
+                                       ({'widthratio', Numerator, Denominator, Scale}, TreeWalkerAcc) ->
+                                                       widthratio_ast(Numerator, Denominator, Scale, Context, TreeWalkerAcc);
+                                       ({'with', Args, Contents}, TreeWalkerAcc) ->
+                                                       with_ast(Args, Contents, Context, TreeWalkerAcc);
+                                       ({'extension', Tag}, TreeWalkerAcc) ->
+                                                       extension_ast(Tag, Context, TreeWalkerAcc);
+                                       ({'extends', _}, _TreeWalkerAcc) ->
+                                                       throw(unexpected_extends_tag);
+                                       (ValueToken, TreeWalkerAcc) ->
+                                                       {{ValueAst,ValueInfo},ValueTreeWalker} = value_ast(ValueToken, true, true, Context, TreeWalkerAcc),
+                                                       {{format(ValueAst, Context, ValueTreeWalker),ValueInfo},ValueTreeWalker}
+                                               end, TreeWalker, DjangoParseTree),
+    {AstList, {Info, TreeWalker3}} = lists:mapfoldl(
+                                       fun({Ast, Info}, {InfoAcc, TreeWalkerAcc}) ->
+                                               PresetVars = lists:foldl(fun
+                                                                            (X, Acc) ->
+                                                                               case proplists:lookup(X, Context#dtl_context.vars) of
+                                                                                   none -> Acc;
+                                                                                   Val -> [Val|Acc]
+                                                                               end
+                                                                       end, [], Info#ast_info.var_names),
+                                               case PresetVars of
+                                                   [] ->
+                                                       {Ast, {merge_info(Info, InfoAcc), TreeWalkerAcc}};
+                                                   _ ->
+                                                       Counter = TreeWalkerAcc#treewalker.counter,
+                                                       Name = list_to_atom(lists:concat([pre_render, Counter])),
+                                                       Ast1 = ?Q("'@Name@'(_@PresetVars@, RenderOptions)"),
+                                                       PreRenderAst = ?Q("'@Name@'(_Variables, RenderOptions) -> _@match, _@Ast.",
+                                                                         [{match, options_match_ast(Context, TreeWalkerAcc)}]),
+                                                       PreRenderAsts = Info#ast_info.pre_render_asts,
+                                                       Info1 = Info#ast_info{pre_render_asts = [PreRenderAst | PreRenderAsts]},
+                                                       {Ast1, {merge_info(Info1, InfoAcc), TreeWalkerAcc#treewalker{counter = Counter + 1}}}
+                                               end
+                                       end, {#ast_info{}, TreeWalker2}, AstInfoList),
+    {{erl_syntax:list(AstList), Info}, TreeWalker3}.
+
+
+value_ast(ValueToken, AsString, EmptyIfUndefined, Context, TreeWalker) ->
+    case ValueToken of
+        {'expr', Operator, Value} ->
+            {{ValueAst,InfoValue}, TreeWalker1} = value_ast(Value, false, EmptyIfUndefined, Context, TreeWalker),
+            Op = list_to_atom(Operator),
+            Ast = ?Q("erlydtl_runtime:_@Op@(_@ValueAst)"),
+            {{Ast, InfoValue}, TreeWalker1};
+        {'expr', Operator, Value1, Value2} ->
+            {{Value1Ast,InfoValue1}, TreeWalker1} = value_ast(Value1, false, EmptyIfUndefined, Context, TreeWalker),
+            {{Value2Ast,InfoValue2}, TreeWalker2} = value_ast(Value2, false, EmptyIfUndefined, Context, TreeWalker1),
+            Op = list_to_atom(Operator),
+            Ast = ?Q("erlydtl_runtime:_@Op@(_@Value1Ast, _@Value2Ast)"),
+            {{Ast, merge_info(InfoValue1,InfoValue2)}, TreeWalker2};
+        {'string_literal', _Pos, String} ->
+            string_ast(unescape_string_literal(String), Context, TreeWalker);
+        {'number_literal', _Pos, Number} ->
+            case AsString of
+                true  -> string_ast(Number, Context, TreeWalker);
+                false -> {{erl_syntax:integer(Number), #ast_info{}}, TreeWalker}
+            end;
+        {'apply_filter', Variable, Filter} ->
+            filter_ast(Variable, Filter, Context, TreeWalker);
+        {'attribute', _} = Variable ->
+            resolve_variable_ast(Variable, Context, TreeWalker, EmptyIfUndefined);
+        {'variable', _} = Variable ->
+            resolve_variable_ast(Variable, Context, TreeWalker, EmptyIfUndefined);
+        {extension, Tag} ->
+            extension_ast(Tag, Context, TreeWalker)
+    end.
+
+extension_ast(Tag, Context, TreeWalker) ->
+    case call_extension(Context, compile_ast, [Tag, Context, TreeWalker]) of
+        undefined ->
+            throw({unknown_extension, Tag});
+        Result ->
+            Result
+    end.
+
+
+with_dependencies([], Args) ->
+    Args;
+with_dependencies([Dependency | Rest], Args) ->
+    with_dependencies(Rest, with_dependency(Dependency, Args)).
+
+with_dependency(FilePath, {{Ast, Info}, TreeWalker}) ->
+    {{Ast, Info#ast_info{dependencies = [FilePath | Info#ast_info.dependencies]}}, TreeWalker}.
+
+
+empty_ast(TreeWalker) ->
+    {{erl_syntax:list([]), #ast_info{}}, TreeWalker}.
+
+blocktrans_ast(ArgList, Contents, Context, TreeWalker) ->
+    %% add new scope using 'with' values
+    {NewScope, {ArgInfo, TreeWalker1}} = lists:mapfoldl(fun
+                                                            ({{identifier, _, LocalVarName}, Value}, {AstInfo1, TreeWalker1}) ->
+                                                               {{Ast, Info}, TreeWalker2} = value_ast(Value, false, false, Context, TreeWalker1),
+                                                               {{LocalVarName, Ast}, {merge_info(AstInfo1, Info), TreeWalker2}}
+                                                       end, {#ast_info{}, TreeWalker}, ArgList),
+    NewContext = Context#dtl_context{ local_scopes = [NewScope|Context#dtl_context.local_scopes] },
+    %% key for translation lookup
+    SourceText = lists:flatten(erlydtl_unparser:unparse(Contents)),
+    {{DefaultAst, AstInfo}, TreeWalker2} = body_ast(Contents, NewContext, TreeWalker1),
+    MergedInfo = merge_info(AstInfo, ArgInfo),
+    case Context#dtl_context.trans_fun of
+        none ->
+            %% translate in runtime
+            blocktrans_runtime_ast({DefaultAst, MergedInfo}, TreeWalker2, SourceText, Contents, NewContext);
+        BlockTransFun when is_function(BlockTransFun) ->
+            %% translate in compile-time
+            {FinalAstInfo, FinalTreeWalker, Clauses} = 
+                lists:foldr(
+                  fun(Locale, {AstInfoAcc, ThisTreeWalker, ClauseAcc}) ->
+                          case BlockTransFun(SourceText, Locale) of
+                              default ->
+                                  {AstInfoAcc, ThisTreeWalker, ClauseAcc};
+                              Body ->
+                                  {ok, DjangoParseTree} = do_parse_template(Body, Context),
+                                  {{ThisAst, ThisAstInfo}, TreeWalker3} = body_ast(DjangoParseTree, NewContext, ThisTreeWalker),
+                                  {merge_info(ThisAstInfo, AstInfoAcc), TreeWalker3,
+                                   [?Q("_@Locale@ -> _@ThisAst")|ClauseAcc]}
+                          end
+                  end, {MergedInfo, TreeWalker2, []}, Context#dtl_context.trans_locales),
+            Ast = ?Q("case _CurrentLocale of _@_Clauses -> _; _ -> _@DefaultAst end"),
+            {{Ast, FinalAstInfo#ast_info{ translated_blocks = [SourceText] }}, FinalTreeWalker}
+    end.
+
+blocktrans_runtime_ast({DefaultAst, Info}, Walker, SourceText, Contents, Context) ->
+    %% Contents is flat - only strings and '{{var}}' allowed.
+    %% build sorted list (orddict) of pre-resolved variables to pass to runtime translation function
+    USortedVariables = lists:usort(fun({variable, {identifier, _, A}},
+                                       {variable, {identifier, _, B}}) ->
+                                           A =< B
+                                   end, [Var || {variable, _}=Var <- Contents]),
+    VarBuilder = fun({variable, {identifier, _, Name}}=Var, Walker1) ->
+                         {{Ast2, _InfoIgn}, Walker2}  = resolve_variable_ast(Var, Context, Walker1, false),
+                         {?Q("{_@name, _@Ast2}", [{name, merl:term(atom_to_list(Name))}]),
+                          Walker2}
+                 end,
+    {VarAsts, Walker2} = lists:mapfoldl(VarBuilder, Walker, USortedVariables),
+    VarListAst = erl_syntax:list(VarAsts),
+    BlockTransAst = ?Q(["if _TranslationFun =:= none -> _@DefaultAst;",
+                        "  true -> erlydtl_runtime:translate_block(",
+                        "    _@SourceText@, _TranslationFun, _@VarListAst)",
+                        "end"]),
+    {{BlockTransAst, Info}, Walker2}.
+
+translated_ast({string_literal, _, String}, Context, TreeWalker) ->
+    UnescapedStr = unescape_string_literal(String),
+    case call_extension(Context, translate_ast, [UnescapedStr, Context, TreeWalker]) of
+        undefined ->
+            AstInfo = #ast_info{translatable_strings = [UnescapedStr]},
+            case Context#dtl_context.trans_fun of
+                none -> runtime_trans_ast(erl_syntax:string(UnescapedStr), AstInfo, TreeWalker);
+                _ -> compiletime_trans_ast(UnescapedStr, AstInfo, Context, TreeWalker)
+            end;
+        Translated ->
+            Translated
+    end;
+translated_ast(ValueToken, Context, TreeWalker) ->
+    {{Ast, Info}, TreeWalker1} = value_ast(ValueToken, true, false, Context, TreeWalker),
+    runtime_trans_ast(Ast, Info, TreeWalker1).
+
+runtime_trans_ast(ValueAst, AstInfo, TreeWalker) ->
+    {{?Q("erlydtl_runtime:translate(_@ValueAst, _TranslationFun)"),
+      AstInfo},
+     TreeWalker}.
+
+compiletime_trans_ast(String, AstInfo,
+                      #dtl_context{trans_fun=TFun,
+                                   trans_locales=TLocales}=Context,
+                      TreeWalker) ->
+    ClAst = lists:foldl(
+              fun(Locale, ClausesAcc) ->
+                      [?Q("_@Locale@ -> _@translated",
+                          [{translated, case TFun(String, Locale) of
+                                            default -> string_ast(String, Context);
+                                            Translated -> string_ast(Translated, Context)
+                                        end}])
+                       |ClausesAcc]
+              end,
+              [], TLocales),
+    CaseAst = ?Q(["case _CurrentLocale of",
+                  "  _@_ClAst -> _;",
+                  " _ -> _@string",
+                  "end"],
+                 [{string, string_ast(String, Context)}]),
+    {{CaseAst, AstInfo}, TreeWalker}.
+
+%% Completely unnecessary in ErlyDTL (use {{ "{%" }} etc), but implemented for compatibility.
+templatetag_ast("openblock", Context, TreeWalker) ->
+    string_ast("{%", Context, TreeWalker);
+templatetag_ast("closeblock", Context, TreeWalker) ->
+    string_ast("%}", Context, TreeWalker);
+templatetag_ast("openvariable", Context, TreeWalker) ->
+    string_ast("{{", Context, TreeWalker);
+templatetag_ast("closevariable", Context, TreeWalker) ->
+    string_ast("}}", Context, TreeWalker);
+templatetag_ast("openbrace", Context, TreeWalker) ->
+    string_ast("{", Context, TreeWalker);
+templatetag_ast("closebrace", Context, TreeWalker) ->
+    string_ast("}", Context, TreeWalker);
+templatetag_ast("opencomment", Context, TreeWalker) ->
+    string_ast("{#", Context, TreeWalker);
+templatetag_ast("closecomment", Context, TreeWalker) ->
+    string_ast("#}", Context, TreeWalker).
+
+
+widthratio_ast(Numerator, Denominator, Scale, Context, TreeWalker) ->
+    {{NumAst, NumInfo}, TreeWalker1} = value_ast(Numerator, false, true, Context, TreeWalker),
+    {{DenAst, DenInfo}, TreeWalker2} = value_ast(Denominator, false, true, Context, TreeWalker1),
+    {{ScaleAst, ScaleInfo}, TreeWalker3} = value_ast(Scale, false, true, Context, TreeWalker2),
+    {{format_number_ast(?Q("erlydtl_runtime:widthratio(_@NumAst, _@DenAst, _@ScaleAst)")),
+      merge_info(ScaleInfo, merge_info(NumInfo, DenInfo))},
+     TreeWalker3}.
+
+
+string_ast(Arg, Context) ->
+    merl:term(erlydtl_compiler_utils:to_string(Arg, Context)).
+    
+string_ast(Arg, Context, TreeWalker) ->
+    {{string_ast(Arg, Context), #ast_info{}}, TreeWalker}.
+
+
+include_ast(File, ArgList, Scopes, Context, TreeWalker) ->
+    FilePath = full_path(File, Context#dtl_context.doc_root),
+    case parse_file(FilePath, Context) of
+        {ok, InclusionParseTree, CheckSum} ->
+            {NewScope, {ArgInfo, TreeWalker1}}
+                = lists:mapfoldl(
+                    fun ({{identifier, _, LocalVarName}, Value}, {AstInfo1, TreeWalker1}) ->
+                            {{Ast, Info}, TreeWalker2} = value_ast(Value, false, false, Context, TreeWalker1),
+                            {{LocalVarName, Ast}, {merge_info(AstInfo1, Info), TreeWalker2}}
+                    end, {#ast_info{}, TreeWalker}, ArgList),
+
+            {{BodyAst, BodyInfo}, TreeWalker2} = with_dependency(
+                                                   {FilePath, CheckSum},
+                                                   body_ast(
+                                                     InclusionParseTree,
+                                                     Context#dtl_context{
+                                                       parse_trail = [FilePath | Context#dtl_context.parse_trail],
+                                                       local_scopes = [NewScope|Scopes]
+                                                      }, TreeWalker1)),
+
+            {{BodyAst, merge_info(BodyInfo, ArgInfo)}, TreeWalker2};
+        {error, Reason} -> throw(Reason)
+    end.
+
+%% include at run-time
+ssi_ast(FileName, Context, TreeWalker) ->
+    {{FileAst, Info}, TreeWalker1} = value_ast(FileName, true, true, Context, TreeWalker),
+    {Mod, Fun} = Context#dtl_context.reader,
+    Dir = Context#dtl_context.doc_root,
+    {{?Q("erlydtl_runtime:read_file(_@Mod@, _@Fun@, _@Dir@, _@FileAst)"), Info}, TreeWalker1}.
+
+filter_tag_ast(FilterList, Contents, Context, TreeWalker) ->
+    {{InnerAst, Info}, TreeWalker1} = body_ast(Contents, Context#dtl_context{auto_escape = did}, TreeWalker),
+    {{FilteredAst, FilteredInfo}, TreeWalker2} =
+        lists:foldl(
+          fun ({{identifier, _, Name}, []}, {{AstAcc, InfoAcc}, TreeWalkerAcc})
+                when Name =:= 'escape'; Name =:= 'safe'; Name =:= 'safeseq' ->
+                  {{AstAcc, InfoAcc}, TreeWalkerAcc#treewalker{safe = true}};
+              (Filter, {{AstAcc, InfoAcc}, TreeWalkerAcc}) ->
+                  {{Ast, AstInfo}, TW} = filter_ast1(Filter, AstAcc, Context, TreeWalkerAcc),
+                  {{Ast, merge_info(InfoAcc, AstInfo)}, TW}
+          end,
+          {{?Q("erlang:iolist_to_binary(_@InnerAst)"), Info}, TreeWalker1},
+          FilterList),
+
+    EscapedAst = case search_for_escape_filter(lists:reverse(FilterList), Context) of
+                     on -> ?Q("erlydtl_filters:force_escape(_@FilteredAst)");
+                     _ -> FilteredAst
+                 end,
+    {{EscapedAst, FilteredInfo}, TreeWalker2}.
+
+search_for_escape_filter(FilterList, #dtl_context{auto_escape = on}) ->
+    search_for_safe_filter(FilterList);
+search_for_escape_filter(_, #dtl_context{auto_escape = did}) -> off;
+search_for_escape_filter([{{identifier, _, 'escape'}, []}|Rest], _Context) ->
+    search_for_safe_filter(Rest);
+search_for_escape_filter([_|Rest], Context) ->
+    search_for_escape_filter(Rest, Context);
+search_for_escape_filter([], _Context) -> off.
+
+search_for_safe_filter([{{identifier, _, Name}, []}|_])
+  when Name =:= 'safe'; Name =:= 'safeseq' -> off;
+search_for_safe_filter([_|Rest]) -> search_for_safe_filter(Rest);
+search_for_safe_filter([]) -> on.
+
+filter_ast(Variable, Filter, Context, TreeWalker) ->
+    %% the escape filter is special; it is always applied last, so we have to go digging for it
+
+    %% AutoEscape = 'did' means we (will have) decided whether to escape the current variable,
+    %% so don't do any more escaping
+    {{UnescapedAst, Info}, TreeWalker2} = filter_ast_noescape(
+                                            Variable, Filter,
+                                            Context#dtl_context{auto_escape = did},
+                                            TreeWalker),
+
+    EscapedAst = case search_for_escape_filter(Variable, Filter, Context) of
+                     on -> ?Q("erlydtl_filters:force_escape(_@UnescapedAst)");
+                     _ -> UnescapedAst
+                 end,
+    {{EscapedAst, Info}, TreeWalker2}.
+
+filter_ast_noescape(Variable, {{identifier, _, Name}, []}, Context, TreeWalker)
+  when Name =:= 'escape'; Name =:= 'safe'; Name =:= 'safeseq' ->
+    value_ast(Variable, true, false, Context, TreeWalker#treewalker{safe = true});
+filter_ast_noescape(Variable, Filter, Context, TreeWalker) ->
+    {{ValueAst, Info1}, TreeWalker2} = value_ast(Variable, true, false, Context, TreeWalker),
+    {{VarValue, Info2}, TreeWalker3} = filter_ast1(Filter, ValueAst, Context, TreeWalker2),
+    {{VarValue, merge_info(Info1, Info2)}, TreeWalker3}.
+
+filter_ast1({{identifier, _, Name}, Args}, ValueAst, Context, TreeWalker) ->
+    {{ArgsAst, ArgsInfo}, TreeWalker2} =
+        lists:foldr(
+          fun (Arg, {{AccAst, AccInfo}, AccTreeWalker}) ->
+                  {{ArgAst, ArgInfo}, ArgTreeWalker} = value_ast(Arg, false, false, Context, AccTreeWalker),
+                  {{[ArgAst|AccAst], merge_info(ArgInfo, AccInfo)}, ArgTreeWalker}
+          end,
+          {{[], #ast_info{}}, TreeWalker},
+          Args),
+    FilterAst = filter_ast2(Name, [ValueAst|ArgsAst], Context),
+    {{FilterAst, ArgsInfo}, TreeWalker2}.
+
+filter_ast2(Name, Args, #dtl_context{ filter_modules = [Module|Rest] } = Context) ->
+    case lists:member({Name, length(Args)}, Module:module_info(exports)) of
+        true -> ?Q("'@Module@':'@Name@'(_@Args)");
+        false ->
+            filter_ast2(Name, Args, Context#dtl_context{ filter_modules = Rest })
+    end;
+filter_ast2(Name, Args, _) ->
+    throw({unknown_filter, Name, length(Args)}).
+
+search_for_escape_filter(Variable, Filter, #dtl_context{auto_escape = on}) ->
+    search_for_safe_filter(Variable, Filter);
+search_for_escape_filter(_, _, #dtl_context{auto_escape = did}) ->
+    off;
+search_for_escape_filter(Variable, {{identifier, _, 'escape'}, []} = Filter, _Context) ->
+    search_for_safe_filter(Variable, Filter);
+search_for_escape_filter({apply_filter, Variable, Filter}, _, Context) ->
+    search_for_escape_filter(Variable, Filter, Context);
+search_for_escape_filter(_Variable, _Filter, _Context) ->
+    off.
+
+search_for_safe_filter(_, {{identifier, _, 'safe'}, []}) ->
+    off;
+search_for_safe_filter(_, {{identifier, _, 'safeseq'}, []}) ->
+    off;
+search_for_safe_filter({apply_filter, Variable, Filter}, _) ->
+    search_for_safe_filter(Variable, Filter);
+search_for_safe_filter(_Variable, _Filter) ->
+    on.
+
+finder_function(true) -> {erlydtl_runtime, fetch_value};
+finder_function(false) -> {erlydtl_runtime, find_value}.
+
+finder_function(EmptyIfUndefined, Context) ->
+    case call_extension(Context, finder_function, [EmptyIfUndefined]) of
+        undefined -> finder_function(EmptyIfUndefined);
+        Result -> Result
+    end.
+
+resolve_variable_ast({extension, Tag}, Context, TreeWalker, _) ->
+    extension_ast(Tag, Context, TreeWalker);
+resolve_variable_ast(VarTuple, Context, TreeWalker, EmptyIfUndefined)
+  when is_boolean(EmptyIfUndefined) ->
+    resolve_variable_ast(VarTuple, Context, TreeWalker, finder_function(EmptyIfUndefined, Context));
+resolve_variable_ast(VarTuple, Context, TreeWalker, FinderFunction) ->
+    resolve_variable_ast1(VarTuple, Context, TreeWalker, FinderFunction).
+
+resolve_variable_ast1({attribute, {{_, Pos, Attr}, Variable}}, Context, TreeWalker, FinderFunction) ->
+    {{VarAst, VarInfo}, TreeWalker1} = resolve_variable_ast(Variable, Context, TreeWalker, FinderFunction),
+    FileName = case Context#dtl_context.parse_trail of
+                   [] -> undefined;
+                   [H|_] -> H
+               end,
+    {Runtime, Finder} = FinderFunction,
+    {{?Q(["'@Runtime@':'@Finder@'(",
+          "  _@Attr@, _@VarAst,",
+          "  [{filename, _@FileName@},",
+          "   {pos, _@Pos@},",
+          "   {record_info, _RecordInfo},",
+          "   {render_options, RenderOptions}])"]),
+      VarInfo},
+     TreeWalker1};
+
+resolve_variable_ast1({variable, {identifier, Pos, VarName}}, Context, TreeWalker, FinderFunction) ->
+    VarValue = case resolve_scoped_variable_ast(VarName, Context) of
+                   undefined ->
+                       FileName = case Context#dtl_context.parse_trail of
+                                      [] -> undefined;
+                                      [H|_] -> H
+                                  end,
+                       {Runtime, Finder} = FinderFunction,
+                       ?Q(["'@Runtime@':'@Finder@'(",
+                           "  _@VarName@, _Variables,",
+                           "  [{filename, _@FileName@},",
+                           "   {pos, _@Pos@},",
+                           "   {record_info, _RecordInfo},",
+                           "   {render_options, RenderOptions}])"]);
+                   Val ->
+                       Val
+               end,
+    {{VarValue, #ast_info{ var_names=[VarName] }}, TreeWalker}.
+
+resolve_scoped_variable_ast(VarName, Context) ->
+    resolve_scoped_variable_ast(VarName, Context, undefined).
+
+resolve_scoped_variable_ast(VarName, Context, Default) ->
+    lists:foldl(
+      fun (Scope, Res) ->
+              if Res =:= Default ->
+                      proplists:get_value(VarName, Scope, Default);
+                 true -> Res
+              end
+      end,
+      Default,
+      Context#dtl_context.local_scopes).
+
+format(Ast, Context, TreeWalker) ->
+    auto_escape(format_number_ast(Ast), Context, TreeWalker).
+
+format_number_ast(Ast) ->
+    ?Q("erlydtl_filters:format_number(_@Ast)").
+
+
+auto_escape(Value, _, #treewalker{safe = true}) ->
+    Value;
+auto_escape(Value, #dtl_context{auto_escape = on}, _) ->
+    ?Q("erlydtl_filters:force_escape(_@Value)");
+auto_escape(Value, _, _) ->
+    Value.
+
+firstof_ast(Vars, Context, TreeWalker) ->
+    body_ast([lists:foldr(fun
+                              ({L, _, _}=Var, []) when L=:=string_literal;L=:=number_literal ->
+                                 Var;
+                              ({L, _, _}, _) when L=:=string_literal;L=:=number_literal ->
+                                 erlang:error(errbadliteral);
+                              (Var, []) ->
+                                 {'ifelse', Var, [Var], []};
+                              (Var, Acc) ->
+                                 {'ifelse', Var, [Var], [Acc]} end,
+                          [], Vars)], Context, TreeWalker).
+
+ifelse_ast(Expression, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, Context, TreeWalker) ->
+    Info = merge_info(IfContentsInfo, ElseContentsInfo),
+    {{Ast, ExpressionInfo}, TreeWalker1} = value_ast(Expression, false, false, Context, TreeWalker),
+    {{?Q(["case erlydtl_runtime:is_true(_@Ast) of",
+          "  true -> _@IfContentsAst;",
+          "  _ -> _@ElseContentsAst",
+          "end"]),
+      merge_info(ExpressionInfo, Info)},
+     TreeWalker1}.
+
+with_ast(ArgList, Contents, Context, TreeWalker) ->
+    {ArgAstList, {ArgInfo, TreeWalker1}} =
+        lists:mapfoldl(
+          fun ({{identifier, _, _LocalVarName}, Value}, {AstInfo1, TreeWalker1}) ->
+                  {{Ast, Info}, TreeWalker2} = value_ast(Value, false, false, Context, TreeWalker1),
+                  {Ast, {merge_info(AstInfo1, Info), TreeWalker2}}
+          end, {#ast_info{}, TreeWalker}, ArgList),
+
+    NewScope = lists:map(
+                 fun({{identifier, _, LocalVarName}, _Value}) ->
+                         {LocalVarName, erl_syntax:variable(lists:concat(["Var_", LocalVarName]))}
+                 end, ArgList),
+
+    {{InnerAst, InnerInfo}, TreeWalker2} =
+        body_ast(
+          Contents,
+          Context#dtl_context{local_scopes = [NewScope|Context#dtl_context.local_scopes]},
+          TreeWalker1),
+
+    {{?Q("fun (_@args) -> _@InnerAst end (_@ArgAstList)",
+         [{args, element(2, lists:unzip(NewScope))}]),
+      merge_info(ArgInfo, InnerInfo)},
+     TreeWalker2}.
+
+regroup_ast(ListVariable, GrouperVariable, LocalVarName, Contents, Context, TreeWalker) ->
+    {{ListAst, ListInfo}, TreeWalker1} = value_ast(ListVariable, false, true, Context, TreeWalker),
+    LocalVarAst = erl_syntax:variable(lists:concat(["Var_", LocalVarName])),
+    NewScope = [{LocalVarName, LocalVarAst}],
+
+    {{InnerAst, InnerInfo}, TreeWalker2} = body_ast(Contents,
+                                                    Context#dtl_context{
+                                                      local_scopes = [NewScope|Context#dtl_context.local_scopes]
+                                                     },
+                                                    TreeWalker1),
+
+    {{?Q(["fun (_@LocalVarAst) -> _@InnerAst end",
+         "(erlydtl_runtime:regroup(_@ListAst, _@regroup))"],
+        [{regroup, regroup_filter(GrouperVariable, [])}]),
+      merge_info(ListInfo, InnerInfo)},
+     TreeWalker2}.
+
+regroup_filter({attribute,{{identifier,_,Ident},Next}},Acc) ->
+    regroup_filter(Next,[erl_syntax:atom(Ident)|Acc]);
+regroup_filter({variable,{identifier,_,Var}},Acc) ->
+    erl_syntax:list([erl_syntax:atom(Var)|Acc]).
+
+to_list_ast(Value, IsReversed) ->
+    ?Q("erlydtl_runtime:to_list(_@Value, _@IsReversed)").
+
+to_list_ast(Value, IsReversed, Context, TreeWalker) ->
+    case call_extension(Context, to_list_ast, [Value, IsReversed, Context, TreeWalker]) of
+        undefined -> to_list_ast(Value, IsReversed);
+        Result -> Result
+    end.
+
+for_loop_ast(IteratorList, LoopValue, IsReversed, Contents, {EmptyContentsAst, EmptyContentsInfo}, Context, TreeWalker) ->
+    %% create unique namespace for this instance
+    Level = length(Context#dtl_context.local_scopes),
+    {Row, Col} = element(2, hd(IteratorList)),
+    ForId = lists:concat(["/", Level, "_", Row, ":", Col]),
+
+    Counters = merl:var(lists:concat(["Counters", ForId])),
+    Vars = merl:var(lists:concat(["Vars", ForId])),
+
+    %% setup
+    VarScope = lists:map(
+                 fun({identifier, {R, C}, Iterator}) ->
+                         {Iterator, erl_syntax:variable(
+                                      lists:concat(["Var_", Iterator,
+                                                    "/", Level, "_", R, ":", C
+                                                   ]))}
+                 end, IteratorList),
+    {Iterators, IteratorVars} = lists:unzip(VarScope),
+    IteratorCount = length(IteratorVars),
+
+    {{LoopBodyAst, Info}, TreeWalker1} =
+        body_ast(
+          Contents,
+          Context#dtl_context{
+            local_scopes =
+                [[{'forloop', Counters} | VarScope]
+                 | Context#dtl_context.local_scopes]
+           },
+          TreeWalker),
+
+    {{LoopValueAst, LoopValueInfo}, TreeWalker2} = value_ast(LoopValue, false, true, Context, TreeWalker1),
+
+    LoopValueAst0 = to_list_ast(LoopValueAst, erl_syntax:atom(IsReversed), Context, TreeWalker2),
+
+    ParentLoop = resolve_scoped_variable_ast('forloop', Context, erl_syntax:atom(undefined)),
+
+    %% call for loop
+    {{?Q(["case erlydtl_runtime:forloop(",
+          "  fun (_@Vars, _@Counters) ->",
+          "    {_@IteratorVars} = if is_tuple(_@Vars), size(_@Vars) == _@IteratorCount@ -> _@Vars;",
+          "                          _@___ifclauses -> _",
+          "                       end,",
+          "    {_@LoopBodyAst, erlydtl_runtime:increment_counter_stats(_@Counters)}",
+          "  end,",
+          "  _@LoopValueAst0, _@ParentLoop)",
+          "of",
+          "  empty -> _@EmptyContentsAst;",
+          "  {L, _} -> L",
+          "end"],
+         [{ifclauses, if IteratorCount > 1 ->
+                              ?Q(["() when is_list(_@Vars), length(_@Vars) == _@IteratorCount@ ->",
+                                  "  list_to_tuple(_@Vars);",
+                                  "() when true -> throw({for_loop, _@Iterators@, _@Vars})"]);
+                         true ->
+                              ?Q("() when true -> {_@Vars}")
+                      end}]),
+      merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo)},
+     TreeWalker2}.
+
+ifchanged_values_ast(Values, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, Context, TreeWalker) ->
+    Info = merge_info(IfContentsInfo, ElseContentsInfo),
+    ValueAstFun = fun(Expr, {LTreeWalker, LInfo, Acc}) ->
+                          {{EAst, EInfo}, ETw} = value_ast(Expr, false, true, Context, LTreeWalker),
+                          {ETw, merge_info(LInfo, EInfo),
+                           [?Q("{_@hash, _@EAst}",
+                               [{hash, merl:term(erlang:phash2(Expr))}])
+                            |Acc]}
+                  end,
+    {TreeWalker1, MergedInfo, Changed} = lists:foldl(ValueAstFun, {TreeWalker, Info, []}, Values),
+    {{?Q(["case erlydtl_runtime:ifchanged([_@Changed]) of",
+          "  true -> _@IfContentsAst;",
+          "  _ -> _@ElseContentsAst",
+          "end"]),
+      MergedInfo},
+     TreeWalker1}.
+
+ifchanged_contents_ast(Contents, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, _Context, TreeWalker) ->
+    {{?Q(["case erlydtl_runtime:ifchanged([{_@hash, _@IfContentsAst}]) of",
+          "  true -> _@IfContentsAst;",
+          "  _ -> _@ElseContentsAst",
+          "end"],
+         [{hash, merl:term(erlang:phash2(Contents))}]),
+      merge_info(IfContentsInfo, ElseContentsInfo)},
+     TreeWalker}.
+
+cycle_ast(Names, Context, TreeWalker) ->
+    {NamesTuple, VarNames}
+        = lists:mapfoldl(
+            fun ({string_literal, _, Str}, VarNamesAcc) ->
+                    S = string_ast(unescape_string_literal(Str), Context),
+                    {S, VarNamesAcc};
+                ({variable, _}=Var, VarNamesAcc) ->
+                    {{V, #ast_info{ var_names=[VarName] }}, _} = resolve_variable_ast(Var, Context, TreeWalker, true),
+                    {V, [VarName|VarNamesAcc]};
+                ({number_literal, _, Num}, VarNamesAcc) ->
+                    {format(erl_syntax:integer(Num), Context, TreeWalker), VarNamesAcc};
+                (_, VarNamesAcc) ->
+                    {[], VarNamesAcc}
+            end, [], Names),
+    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@forloop)",
+        [{forloop, resolve_scoped_variable_ast('forloop', Context)}]),
+      #ast_info{ var_names = VarNames }},
+     TreeWalker}.
+
+%% Older Django templates treat cycle with comma-delimited elements as strings
+cycle_compat_ast(Names, Context, TreeWalker) ->
+    NamesTuple = lists:map(
+                   fun ({identifier, _, X}) ->
+                           string_ast(X, Context)
+                   end, Names),
+    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@forloop)",
+        [{forloop, resolve_scoped_variable_ast('forloop', Context)}]),
+      #ast_info{}},
+     TreeWalker}.
+
+now_ast(FormatString, Context, TreeWalker) ->
+    %% Note: we can't use unescape_string_literal here
+    %% because we want to allow escaping in the format string.
+    %% We only want to remove the surrounding escapes,
+    %% i.e. \"foo\" becomes "foo"
+    UnescapeOuter = string:strip(FormatString, both, 34),
+    {{StringAst, Info}, TreeWalker1} = string_ast(UnescapeOuter, Context, TreeWalker),
+    {{?Q("erlydtl_dateformat:format(_@StringAst)"), Info}, TreeWalker1}.
+
+spaceless_ast(Contents, Context, TreeWalker) ->
+    {{Ast, Info}, TreeWalker1} = body_ast(Contents, Context, TreeWalker),
+    {{?Q("erlydtl_runtime:spaceless(_@Ast)"), Info}, TreeWalker1}.
+
+
+%%-------------------------------------------------------------------
+%% Custom tags
+%%-------------------------------------------------------------------
+
+interpret_value({trans, StringLiteral}, Context, TreeWalker) ->
+    translated_ast(StringLiteral, Context, TreeWalker);
+interpret_value(Value, Context, TreeWalker) ->
+    value_ast(Value, false, false, Context, TreeWalker).
+
+interpret_args(Args, Context, TreeWalker) ->
+    lists:foldr(
+      fun ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) ->
+              {{Ast0, AstInfo0}, TreeWalker0} = interpret_value(Value, Context, TreeWalkerAcc),
+              {{[?Q("{_@Key@, _@Ast0}")|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0};
+          (Value, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) ->
+              {{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, Context, TreeWalkerAcc),
+              {{[Ast0|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0}
+      end, {{[], #ast_info{}}, TreeWalker}, Args).
+
+tag_ast(Name, Args, Context, TreeWalker) ->
+    {{InterpretedArgs, AstInfo1}, TreeWalker1} = interpret_args(Args, Context, TreeWalker),
+    {RenderAst, RenderInfo} = custom_tags_modules_ast(Name, InterpretedArgs, Context),
+    {{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker1}.
+
+custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = false }) ->
+    {?Q("render_tag(_@Name@, [_@InterpretedArgs], RenderOptions)"),
+     #ast_info{custom_tags = [Name]}};
+custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = true, module = Module }) ->
+    {?Q("'@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)"),
+     #ast_info{ custom_tags = [Name] }};
+custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [Module|Rest] } = Context) ->
+    try lists:max([I || {N,I} <- Module:module_info(exports), N =:= Name]) of
+        2 ->
+            {?Q("'@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)"), #ast_info{}};
+        1 ->
+            {?Q("'@Module@':'@Name@'([_@InterpretedArgs])"), #ast_info{}};
+        I ->
+            throw({unsupported_custom_tag_fun, {Module, Name, I}})
+    catch _:function_clause ->
+            custom_tags_modules_ast(Name, InterpretedArgs,
+                                    Context#dtl_context{ custom_tags_modules = Rest })
+    end.
+
+call_ast(Module, TreeWalkerAcc) ->
+    call_ast(Module, merl:var("_Variables"), #ast_info{}, TreeWalkerAcc).
+
+call_with_ast(Module, Variable, Context, TreeWalker) ->
+    {{VarAst, VarInfo}, TreeWalker2} = resolve_variable_ast(Variable, Context, TreeWalker, false),
+    call_ast(Module, VarAst, VarInfo, TreeWalker2).
+
+call_ast(Module, Variable, AstInfo, TreeWalker) ->
+    Ast = ?Q(["case '@Module@':render(_@Variable, RenderOptions) of",
+              "  {ok, Rendered} -> Rendered;",
+              "  {error, Reason} -> io_lib:format(\"error: ~p\", [Reason])",
+              "end"]),
+    with_dependencies(Module:dependencies(), {{Ast, AstInfo}, TreeWalker}).

+ 30 - 1417
src/erlydtl_compiler.erl

@@ -48,20 +48,19 @@
          format_error/1, default_options/0]).
 
 %% internal use
--export([
-         parse/1,
-         merge_info/2,
-         format/3,
-         value_ast/5,
-         resolve_scoped_variable_ast/2,
-         resolve_scoped_variable_ast/3,
-         interpret_args/3,
-         unescape_string_literal/1
-        ]).
+-export([parse_file/2, parse_template/2, do_parse_template/2]).
+
+-import(erlydtl_compiler_utils,
+         [print/3, call_extension/3
+         ]).
 
--include_lib("merl/include/merl.hrl").
 -include("erlydtl_ext.hrl").
 
+
+%% --------------------------------------------------------------------
+%% API
+%% --------------------------------------------------------------------
+
 default_options() -> [verbose, report].
 
 compile_template(Template, Module, Options) ->
@@ -74,49 +73,11 @@ compile_file(File, Module, Options) ->
     compile(Context).
 
 compile_dir(Dir, Module, Options) ->
-    Context0 = process_opts({dir, Dir}, Module, Options),
-    %% Find all files in Dir (recursively), matching the regex (no
-    %% files ending in "~").
-    Files = filelib:fold_files(Dir, ".+[^~]$", true, fun(F1,Acc1) -> [F1 | Acc1] end, []),
-    {ParserResults,
-     #dtl_context{ errors=#error_info{ list=ParserErrors } }=Context1}
-        = lists:foldl(
-            fun (File, {ResultAcc, Ctx}) ->
-                    case filename:basename(File) of
-                        "."++_ ->
-                            {ResultAcc, Ctx};
-                        _ ->
-                            FilePath = filename:absname(File),
-                            case filelib:is_dir(FilePath) of
-                                true ->
-                                    {ResultAcc, Ctx};
-                                false ->
-                                    case parse_file(FilePath, Ctx) of
-                                        up_to_date -> {ResultAcc, Ctx};
-                                        {ok, DjangoParseTree, CheckSum} ->
-                                            {[{File, DjangoParseTree, CheckSum}|ResultAcc], Ctx};
-                                        {error, Reason} -> {ResultAcc, add_error(Reason, Ctx)}
-                                    end
-                            end
-                    end
-            end,
-            {[], Context0},
-            Files),
-    Context2 = if length(ParserErrors) == 0 ->
-                       compile_multiple_to_binary(Dir, ParserResults, Context1);
-                  true -> Context1
-               end,
-    collect_result(Context2).
+    Context = process_opts({dir, Dir}, Module, Options),
+    print("Compile directory: ~s~n", [Dir], Context),
+    compile(Context).
 
-parse(Data) ->
-    parse_template(Data, #dtl_context{}).
 
-format_error(no_out_dir) ->
-    "Compiled template not saved (need out_dir option)";
-format_error(unexpected_extends_tag) ->
-    "The extends tag must be at the very top of the template";
-format_error(circular_include) ->
-    "Circular file inclusion!";
 format_error({read_file, Error}) ->
     io_lib:format(
       "Failed to read file: ~s",
@@ -125,12 +86,6 @@ format_error({read_file, File, Error}) ->
     io_lib:format(
       "Failed to include file ~s: ~s",
       [File, file:format_error(Error)]);
-format_error({write_file, Error}) ->
-    io_lib:format(
-      "Failed to write file: ~s",
-      [file:format_error(Error)]);
-format_error(compile_beam) ->
-    "Failed to compile template to .beam file";
 format_error(Other) ->
     io_lib:format("## Error description for ~p not implemented.", [Other]).
 
@@ -263,154 +218,6 @@ collect_result(#dtl_context{ errors=Es, warnings=Ws }) ->
        true -> error
     end.
 
-do_compile(#dtl_context{ bin=undefined, parse_trail=[File|_] }=Context) ->
-    {M, F} = Context#dtl_context.reader,
-    case catch M:F(File) of
-        {ok, Data} when is_binary(Data) ->
-            do_compile(Context#dtl_context{ bin=Data });
-        {error, Reason} ->
-            add_error({read_file, Reason}, Context)
-    end;
-do_compile(#dtl_context{ bin=Template }=Context) ->
-    case parse_template(Template, Context) of
-        up_to_date -> Context;
-        {ok, DjangoParseTree, CheckSum} ->
-            compile_to_binary(DjangoParseTree, CheckSum, Context);
-        {error, Reason} -> add_error(Reason, Context)
-    end.
-
-compile_multiple_to_binary(Dir, ParserResults, Context0) ->
-    MatchAst = options_match_ast(Context0),
-    {Functions,
-     {AstInfo, _,
-      #dtl_context{ errors=#error_info{ list=Errors } }=Context1}}
-        = lists:mapfoldl(
-            fun({File, DjangoParseTree, CheckSum}, {AstInfo, TreeWalker, Ctx}) ->
-                    try
-                        FilePath = full_path(File, Ctx#dtl_context.doc_root),
-                        {{BodyAst, BodyInfo}, TreeWalker1} = with_dependency(
-                                                               {FilePath, CheckSum},
-                                                               body_ast(DjangoParseTree, Ctx, TreeWalker)),
-                        FunctionName = filename:rootname(filename:basename(File)),
-                        Function1 = ?Q("_@FunctionName@(_Variables) -> _@FunctionName@(_Variables, [])"),
-                        Function2 = erl_syntax:function(
-                                      erl_syntax:atom(FunctionName),
-                                      [erl_syntax:clause(
-                                         [erl_syntax:variable("_Variables"),
-                                          erl_syntax:variable("RenderOptions")],
-                                         none,
-                                         MatchAst ++ stringify(BodyAst, Ctx))
-                                      ]),
-                        {{FunctionName, Function1, Function2}, {merge_info(AstInfo, BodyInfo), TreeWalker1, Ctx}}
-                    catch
-                        throw:Error ->
-                            {error, {AstInfo, TreeWalker, add_error(Error, Ctx)}}
-                    end
-            end,
-            {#ast_info{},
-             init_treewalker(Context0),
-             Context0},
-            ParserResults),
-    if length(Errors) == 0 ->
-            Forms = custom_forms(Dir, Context1#dtl_context.module, Functions, AstInfo),
-            compile_forms(Forms, Context1);
-       true ->
-            Context1
-    end.
-
-compile_to_binary(DjangoParseTree, CheckSum, Context) ->
-    try body_ast(DjangoParseTree, Context, init_treewalker(Context)) of
-        {{BodyAst, BodyInfo}, BodyTreeWalker} ->
-            try custom_tags_ast(BodyInfo#ast_info.custom_tags, Context, BodyTreeWalker) of
-                {{CustomTagsAst, CustomTagsInfo}, _} ->
-                    Forms = forms(
-                              Context#dtl_context.module,
-                              {BodyAst, BodyInfo},
-                              {CustomTagsAst, CustomTagsInfo},
-                              CheckSum,
-                              BodyTreeWalker,
-                              Context),
-                    compile_forms(Forms, Context)
-            catch
-                throw:Error -> add_error(Error, Context)
-            end
-    catch
-        throw:Error -> add_error(Error, Context)
-    end.
-
-compile_forms(Forms, Context) ->
-    maybe_debug_template(Forms, Context),
-    Options = Context#dtl_context.compiler_options,
-    case compile:forms(Forms, Options) of
-        Compiled when element(1, Compiled) =:= ok ->
-            [ok, Module, Bin|Info] = tuple_to_list(Compiled),
-            lists:foldl(
-              fun (F, C) -> F(Module, Bin, C) end,
-              Context#dtl_context{ bin=Bin },
-              [fun maybe_write/3,
-               fun maybe_load/3,
-               fun (_, _, C) ->
-                       case Info of
-                           [Ws] when length(Ws) > 0 ->
-                               add_warnings(Ws, C);
-                           _ -> C
-                       end
-               end
-              ]);
-        error ->
-            add_error(compile_beam, Context);
-        {error, Es, Ws} ->
-            add_warnings(Ws, add_errors(Es, Context))
-    end.
-
-maybe_write(Module, Bin, Context) ->
-    case proplists:get_value(out_dir, Context#dtl_context.all_options) of
-        false -> Context;
-        undefined ->
-            add_warning(no_out_dir, Context);
-        OutDir ->
-            BeamFile = filename:join([OutDir, [Module, ".beam"]]),
-            print("Template module: ~w -> ~s\n", [Module, BeamFile], Context),
-            case file:write_file(BeamFile, Bin) of
-                ok -> Context;
-                {error, Reason} ->
-                    add_error({write_file, Reason}, Context)
-            end
-    end.
-
-maybe_load(Module, Bin, Context) ->
-    case proplists:get_bool(no_load, Context#dtl_context.all_options) of
-        true -> Context;
-        false -> load_code(Module, Bin, Context)
-    end.
-
-load_code(Module, Bin, Context) ->
-    code:purge(Module),
-    case code:load_binary(Module, atom_to_list(Module) ++ ".erl", Bin) of
-        {module, Module} -> Context;
-        Error -> add_warning({load, Error}, Context)
-    end.
-
-maybe_debug_template(Forms, Context) ->
-    %% undocumented option to debug the compiled template
-    case proplists:get_bool(debug_info, Context#dtl_context.all_options) of
-        false -> nop;
-        true ->
-            Options = Context#dtl_context.compiler_options,
-            print("Compiler options: ~p~n", [Options], Context),
-            try
-                Source = erl_prettypr:format(erl_syntax:form_list(Forms)),
-                File = lists:concat([proplists:get_value(source, Options), ".erl"]),
-                io:format("Saving template source to: ~s.. ~p~n",
-                          [File, file:write_file(File, Source)])
-            catch
-                error:Err ->
-                    io:format("Pretty printing failed: ~p~n"
-                              "Context: ~n~p~n"
-                              "Forms: ~n~p~n",
-                              [Err, Context, Forms])
-            end
-    end.
 
 init_context(ParseTrail, DefDir, Module, Options) when is_list(Module) ->
     init_context(ParseTrail, DefDir, list_to_atom(Module), Options);
@@ -492,46 +299,10 @@ get_error_info_opts(Class, Options) ->
          {Value, proplists:get_bool(Key, Options)}
      end || Flag <- Flags].
 
-init_treewalker(Context) ->
-    TreeWalker = #treewalker{},
-    case call_extension(Context, init_treewalker, [TreeWalker]) of
-        {ok, TW} when is_record(TW, treewalker) -> TW;
-        undefined -> TreeWalker
-    end.
-
 is_up_to_date(_, #dtl_context{force_recompile = true}) ->
     false;
 is_up_to_date(CheckSum, Context) ->
-    Module = Context#dtl_context.module,
-    {M, F} = Context#dtl_context.reader,
-    case catch Module:source() of
-        {_, CheckSum} ->
-            case catch Module:dependencies() of
-                L when is_list(L) ->
-                    RecompileList = lists:foldl(
-                                      fun ({XFile, XCheckSum}, Acc) ->
-                                              case catch M:F(XFile) of
-                                                  {ok, Data} ->
-                                                      case binary_to_list(erlang:md5(Data)) of
-                                                          XCheckSum ->
-                                                              Acc;
-                                                          _ ->
-                                                              [recompile | Acc]
-                                                      end;
-                                                  _ ->
-                                                      [recompile | Acc]
-                                              end
-                                      end, [], L),
-                    case RecompileList of
-                        [] -> true;
-                        _ -> false
-                    end;
-                _ ->
-                    false
-            end;
-        _ ->
-            false
-    end.
+    erlydtl_beam_compiler:is_up_to_date(CheckSum, Context).
 
 parse_file(File, Context) ->
     {M, F} = Context#dtl_context.reader,
@@ -539,7 +310,7 @@ parse_file(File, Context) ->
         {ok, Data} ->
             parse_template(Data, Context);
         {error, Reason} ->
-            {read_file, File, Reason}
+            {error, {read_file, File, Reason}}
     end.
 
 parse_template(Data, Context) ->
@@ -547,42 +318,17 @@ parse_template(Data, Context) ->
     case is_up_to_date(CheckSum, Context) of
         true -> up_to_date;
         false ->
-            case do_parse(Data, Context) of
+            case do_parse_template(Data, Context) of
                 {ok, Val} -> {ok, Val, CheckSum};
                 Err -> Err
             end
     end.
 
-do_parse(Data, #dtl_context{ scanner_module=Scanner }=Context) ->
+do_parse_template(Data, #dtl_context{ scanner_module=Scanner }=Context) ->
     check_scan(
       apply(Scanner, scan, [Data]),
       Context).
 
-call_extension(#dtl_context{ extension_module=undefined }, _Fun, _Args) ->
-    undefined;
-call_extension(#dtl_context{ extension_module=Mod }, Fun, Args)
-  when is_atom(Mod), is_atom(Fun), is_list(Args) ->
-    M = case code:is_loaded(Mod) of
-            false ->
-                case code:load_file(Mod) of
-                    {module, Mod} ->
-                        Mod;
-                    _ ->
-                        undefined
-                end;
-            _ -> Mod
-        end,
-    if M /= undefined ->
-            case erlang:function_exported(M, Fun, length(Args)) of
-                true ->
-                    apply(M, Fun, Args);
-                false ->
-                    undefined
-            end;
-       true ->
-            undefined
-    end.
-
 check_scan({ok, Tokens}, Context) ->
     Tokens1 = case call_extension(Context, post_scan, [Tokens]) of
                   undefined -> Tokens;
@@ -648,1152 +394,6 @@ reset_token_stream(Ts, [_|S]) ->
 %% we should find the next token in the list of scanned tokens, or something is real fishy
 
 
-custom_tags_ast(CustomTags, Context, TreeWalker) ->
-    %% avoid adding the render_tag/3 fun if it isn't used,
-    %% since we can't add a -compile({nowarn_unused_function, render_tag/3}).
-    %% attribute due to a bug in syntax_tools.
-    case custom_tags_clauses_ast(CustomTags, Context, TreeWalker) of
-        skip ->
-            {{erl_syntax:comment(
-                ["% render_tag/3 is not used in this template."]),
-              #ast_info{}},
-             TreeWalker};
-        {{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} ->
-            {{erl_syntax:function(
-                erl_syntax:atom(render_tag),
-                CustomTagsClauses),
-              CustomTagsInfo},
-             TreeWalker1}
-    end.
-
-custom_tags_clauses_ast([], _Context, _TreeWalker) -> skip;
-custom_tags_clauses_ast(CustomTags, Context, TreeWalker) ->
-    custom_tags_clauses_ast1(CustomTags, [], [], #ast_info{}, Context, TreeWalker).
-
-custom_tags_clauses_ast1([], _ExcludeTags, ClauseAcc, InfoAcc, Context, TreeWalker) ->
-    {{DefaultAst, DefaultInfo}, TreeWalker1} =
-        case call_extension(Context, custom_tag_ast, [Context, TreeWalker]) of
-            undefined ->
-                {{?Q("(_TagName, _, _) -> []"), InfoAcc}, TreeWalker};
-            {{ExtAst, ExtInfo}, ExtTreeWalker} ->
-                Clause = ?Q("(TagName, _Variables, RenderOptions) -> _@tag",
-                            [{tag, options_match_ast(Context, ExtTreeWalker) ++ [ExtAst]}]),
-                {{Clause, merge_info(ExtInfo, InfoAcc)}, ExtTreeWalker}
-        end,
-    {{lists:reverse([DefaultAst|ClauseAcc]), DefaultInfo}, TreeWalker1};
-custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Context, TreeWalker) ->
-    case lists:member(Tag, ExcludeTags) of
-        true ->
-            custom_tags_clauses_ast1(CustomTags, ExcludeTags, ClauseAcc, InfoAcc, Context, TreeWalker);
-        false ->
-            CustomTagFile = full_path(Tag, Context#dtl_context.custom_tags_dir),
-            case filelib:is_file(CustomTagFile) of
-                true ->
-                    case parse_file(CustomTagFile, Context) of
-                        {ok, DjangoParseTree, CheckSum} ->
-                            {{BodyAst, BodyAstInfo}, TreeWalker1} = with_dependency(
-                                                                      {CustomTagFile, CheckSum},
-                                                                      body_ast(DjangoParseTree, Context, TreeWalker)),
-                            MatchAst = options_match_ast(Context, TreeWalker),
-                            Clause = ?Q("(_@Tag@, _Variables, RenderOptions) -> _@MatchAst, _@BodyAst"),
-                            custom_tags_clauses_ast1(
-                              CustomTags, [Tag|ExcludeTags],
-                              [Clause|ClauseAcc], merge_info(BodyAstInfo, InfoAcc),
-                              Context, TreeWalker1);
-                        Error ->
-                            throw(Error)
-                    end;
-                false ->
-                    case call_extension(Context, custom_tag_ast, [Tag, Context, TreeWalker]) of
-                        undefined ->
-                            custom_tags_clauses_ast1(
-                              CustomTags, [Tag | ExcludeTags],
-                              ClauseAcc, InfoAcc, Context, TreeWalker);
-                        {{Ast, Info}, TW} ->
-                            Clause = ?Q("(_@Tag@, _Variables, RenderOptions) -> _@match, _@Ast",
-                                        [{match, options_match_ast(Context, TW)}]),
-                            custom_tags_clauses_ast1(
-                              CustomTags, [Tag | ExcludeTags],
-                              [Clause|ClauseAcc], merge_info(Info, InfoAcc),
-                              Context, TW)
-                    end
-            end
-    end.
-
-dependencies_function(Dependencies) ->
-    ?Q("dependencies() -> _@Dependencies@.").
-
-translatable_strings_function(TranslatableStrings) ->
-    ?Q("translatable_strings() -> _@TranslatableStrings@.").
-
-translated_blocks_function(TranslatedBlocks) ->
-    ?Q("translated_blocks() -> _@TranslatedBlocks@.").
-
-variables_function(Variables) ->
-    ?Q("variables() -> _@vars.",
-       [{vars, merl:term(lists:usort(Variables))}]).
-
-custom_forms(Dir, Module, Functions, AstInfo) ->
-    Exported = [erl_syntax:arity_qualifier(erl_syntax:atom(source_dir), erl_syntax:integer(0)),
-                erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0)),
-                erl_syntax:arity_qualifier(erl_syntax:atom(translatable_strings), erl_syntax:integer(0))
-                | lists:foldl(
-                    fun({FunctionName, _, _}, Acc) ->
-                            [erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(1)),
-                             erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(2))
-                             |Acc]
-                    end, [], Functions)
-               ],
-    ModuleAst = ?Q("-module('@Module@')."),
-    ExportAst = ?Q("-export(['@_Exported'/1])"),
-
-    SourceFunctionAst = ?Q("source_dir() -> _@Dir@."),
-
-    DependenciesFunctionAst = dependencies_function(AstInfo#ast_info.dependencies),
-    TranslatableStringsFunctionAst = translatable_strings_function(AstInfo#ast_info.translatable_strings),
-    FunctionAsts = lists:foldl(fun({_, Function1, Function2}, Acc) -> [Function1, Function2 | Acc] end, [], Functions),
-
-    [erl_syntax:revert(X)
-     || X <- [ModuleAst, ExportAst, SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsFunctionAst
-              | FunctionAsts] ++ AstInfo#ast_info.pre_render_asts
-    ].
-
-stringify(BodyAst, #dtl_context{ binary_strings=BinaryStrings }) ->
-    [?Q("erlydtl_runtime:stringify_final(_@BodyAst, '@BinaryStrings@')")].
-
-forms(Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum, TreeWalker,
-      #dtl_context{ parse_trail=[File|_] }=Context) ->
-    MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
-
-    Render0FunctionAst = ?Q("render() -> render([])."),
-    Render1FunctionAst = ?Q("render(Variables) -> render(Variables, [])."),
-
-    Render2FunctionAst = ?Q(["render(Variables, RenderOptions) ->",
-                             "  try render_internal(Variables, RenderOptions) of",
-                             "    Val -> {ok, Val}",
-                             "  catch",
-                             "    Err -> {error, Err}",
-                             "end."
-                            ]),
-
-    SourceFunctionAst = ?Q("source() -> {_@File@, _@CheckSum@}."),
-
-    DependenciesFunctionAst = dependencies_function(MergedInfo#ast_info.dependencies),
-
-    TranslatableStringsAst = translatable_strings_function(MergedInfo#ast_info.translatable_strings),
-
-    TranslatedBlocksAst = translated_blocks_function(MergedInfo#ast_info.translated_blocks),
-
-    VariablesAst = variables_function(MergedInfo#ast_info.var_names),
-
-    MatchAst = options_match_ast(Context, TreeWalker),
-    BodyAstTmp = MatchAst ++ stringify(BodyAst, Context),
-    RenderInternalFunctionAst = ?Q("render_internal(_Variables, RenderOptions) -> _@BodyAstTmp."),
-
-    ModuleAst  = ?Q("-module('@Module@')."),
-
-    ExportAst = erl_syntax:attribute(
-                  erl_syntax:atom(export),
-                  [erl_syntax:list(
-                     [erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(0)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(1)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(2)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(source), erl_syntax:integer(0)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(translatable_strings), erl_syntax:integer(0)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(translated_blocks), erl_syntax:integer(0)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(variables), erl_syntax:integer(0))
-                     ])
-                  ]),
-
-    erl_syntax:revert_forms(
-      erl_syntax:form_list(
-        [ModuleAst, ExportAst, Render0FunctionAst, Render1FunctionAst, Render2FunctionAst,
-         SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsAst,
-         TranslatedBlocksAst, VariablesAst, RenderInternalFunctionAst,
-         CustomTagsFunctionAst
-         |BodyInfo#ast_info.pre_render_asts
-        ])).
-
-options_match_ast(Context) -> options_match_ast(Context, undefined).
-options_match_ast(Context, TreeWalker) ->
-    [
-     ?Q("_TranslationFun = proplists:get_value(translation_fun, RenderOptions, none)"),
-     ?Q("_CurrentLocale = proplists:get_value(locale, RenderOptions, none)"),
-     ?Q("_RecordInfo = _@info", [{info, merl:term(Context#dtl_context.record_info)}])
-     | case call_extension(Context, setup_render_ast, [Context, TreeWalker]) of
-           undefined -> [];
-           Ast when is_list(Ast) -> Ast
-       end].
-
-%% child templates should only consist of blocks at the top level
-body_ast([{'extends', {string_literal, _Pos, String}} | ThisParseTree], Context, TreeWalker) ->
-    File = full_path(unescape_string_literal(String), Context#dtl_context.doc_root),
-    case lists:member(File, Context#dtl_context.parse_trail) of
-        true ->
-            throw(circular_include);
-        _ ->
-            case parse_file(File, Context) of
-                {ok, ParentParseTree, CheckSum} ->
-                    BlockDict = lists:foldl(
-                                  fun
-                                      ({block, {identifier, _, Name}, Contents}, Dict) ->
-                                                   dict:store(Name, Contents, Dict);
-                                      (_, Dict) ->
-                                                   Dict
-                                           end, dict:new(), ThisParseTree),
-                    with_dependency({File, CheckSum}, body_ast(ParentParseTree, Context#dtl_context{
-                                                                                  block_dict = dict:merge(fun(_Key, _ParentVal, ChildVal) -> ChildVal end,
-                                                                                                          BlockDict, Context#dtl_context.block_dict),
-                                                                                  parse_trail = [File | Context#dtl_context.parse_trail]}, TreeWalker));
-                Err ->
-                    throw(Err)
-            end
-    end;
-
-
-body_ast(DjangoParseTree, Context, TreeWalker) ->
-    {AstInfoList, TreeWalker2} = lists:mapfoldl(
-                                   fun
-                                       ({'autoescape', {identifier, _, OnOrOff}, Contents}, TreeWalkerAcc) ->
-                                                       body_ast(Contents, Context#dtl_context{auto_escape = OnOrOff},
-                                                                TreeWalkerAcc);
-                                       ({'block', {identifier, _, Name}, Contents}, TreeWalkerAcc) ->
-                                                       Block = case dict:find(Name, Context#dtl_context.block_dict) of
-                                                                   {ok, ChildBlock} -> ChildBlock;
-                                                                   _ -> Contents
-                                                               end,
-                                                       body_ast(Block, Context, TreeWalkerAcc);
-                                       ({'blocktrans', Args, Contents}, TreeWalkerAcc) ->
-                                                       blocktrans_ast(Args, Contents, Context, TreeWalkerAcc);
-                                       ({'call', {identifier, _, Name}}, TreeWalkerAcc) ->
-                                                       call_ast(Name, TreeWalkerAcc);
-                                       ({'call', {identifier, _, Name}, With}, TreeWalkerAcc) ->
-                                                       call_with_ast(Name, With, Context, TreeWalkerAcc);
-                                       ({'comment', _Contents}, TreeWalkerAcc) ->
-                                                       empty_ast(TreeWalkerAcc);
-                                       ({'cycle', Names}, TreeWalkerAcc) ->
-                                                       cycle_ast(Names, Context, TreeWalkerAcc);
-                                       ({'cycle_compat', Names}, TreeWalkerAcc) ->
-                                                       cycle_compat_ast(Names, Context, TreeWalkerAcc);
-                                       ({'date', 'now', {string_literal, _Pos, FormatString}}, TreeWalkerAcc) ->
-                                                       now_ast(FormatString, Context, TreeWalkerAcc);
-                                       ({'filter', FilterList, Contents}, TreeWalkerAcc) ->
-                                                       filter_tag_ast(FilterList, Contents, Context, TreeWalkerAcc);
-                                       ({'firstof', Vars}, TreeWalkerAcc) ->
-                                                       firstof_ast(Vars, Context, TreeWalkerAcc);
-                                       ({'for', {'in', IteratorList, Variable, Reversed}, Contents}, TreeWalkerAcc) ->
-                                                       {EmptyAstInfo, TreeWalker1} = empty_ast(TreeWalkerAcc),
-                                                       for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, Context, TreeWalker1);
-                                       ({'for', {'in', IteratorList, Variable, Reversed}, Contents, EmptyPartContents}, TreeWalkerAcc) ->
-                                                       {EmptyAstInfo, TreeWalker1} = body_ast(EmptyPartContents, Context, TreeWalkerAcc),
-                                                       for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, Context, TreeWalker1);
-                                       ({'if', Expression, Contents, Elif}, TreeWalkerAcc) ->
-                                                       {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
-                                                       {ElifAstInfo, TreeWalker2} = body_ast(Elif, Context, TreeWalker1),
-                                                       ifelse_ast(Expression, IfAstInfo, ElifAstInfo, Context, TreeWalker2);
-                                       ({'if', Expression, Contents}, TreeWalkerAcc) ->
-                                                       {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
-                                                       {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
-                                                       ifelse_ast(Expression, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-                                       ({'ifchanged', '$undefined', Contents}, TreeWalkerAcc) ->
-                                                       {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
-                                                       {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
-                                                       ifchanged_contents_ast(Contents, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-                                       ({'ifchanged', Values, Contents}, TreeWalkerAcc) ->
-                                                       {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
-                                                       {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
-                                                       ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-                                       ({'ifchangedelse', '$undefined', IfContents, ElseContents}, TreeWalkerAcc) ->
-                                                       {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
-                                                       {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
-                                                       ifchanged_contents_ast(IfContents, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-                                       ({'ifchangedelse', Values, IfContents, ElseContents}, TreeWalkerAcc) ->
-                                                       {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
-                                                       {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
-                                                       ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-                                       ({'ifelse', Expression, IfContents, ElseContents}, TreeWalkerAcc) ->
-                                                       {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
-                                                       {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
-                                                       ifelse_ast(Expression, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-                                       ({'ifequal', [Arg1, Arg2], Contents}, TreeWalkerAcc) ->
-                                                       {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
-                                                       {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
-                                                       ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-                                       ({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, TreeWalkerAcc) ->
-                                                       {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
-                                                       {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context,TreeWalker1),
-                                                       ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-                                       ({'ifnotequal', [Arg1, Arg2], Contents}, TreeWalkerAcc) ->
-                                                       {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
-                                                       {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
-                                                       ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-                                       ({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, TreeWalkerAcc) ->
-                                                       {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
-                                                       {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
-                                                       ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-                                       ({'include', {string_literal, _, File}, Args}, TreeWalkerAcc) ->
-                                                       include_ast(unescape_string_literal(File), Args, Context#dtl_context.local_scopes, Context, TreeWalkerAcc);
-                                       ({'include_only', {string_literal, _, File}, Args}, TreeWalkerAcc) ->
-                                                       include_ast(unescape_string_literal(File), Args, [], Context, TreeWalkerAcc);
-                                       ({'regroup', {ListVariable, Grouper, {identifier, _, NewVariable}}, Contents}, TreeWalkerAcc) ->
-                                                       regroup_ast(ListVariable, Grouper, NewVariable, Contents, Context, TreeWalkerAcc);
-                                       ({'spaceless', Contents}, TreeWalkerAcc) ->
-                                                       spaceless_ast(Contents, Context, TreeWalkerAcc);
-                                       ({'ssi', Arg}, TreeWalkerAcc) ->
-                                                       ssi_ast(Arg, Context, TreeWalkerAcc);
-                                       ({'ssi_parsed', {string_literal, _, FileName}}, TreeWalkerAcc) ->
-                                                       include_ast(unescape_string_literal(FileName), [], Context#dtl_context.local_scopes, Context, TreeWalkerAcc);
-                                       ({'string', _Pos, String}, TreeWalkerAcc) ->
-                                                       string_ast(String, Context, TreeWalkerAcc);
-                                       ({'tag', {identifier, _, Name}, Args}, TreeWalkerAcc) ->
-                                                       tag_ast(Name, Args, Context, TreeWalkerAcc);
-                                       ({'templatetag', {_, _, TagName}}, TreeWalkerAcc) ->
-                                                       templatetag_ast(TagName, Context, TreeWalkerAcc);
-                                       ({'trans', Value}, TreeWalkerAcc) ->
-                                                       translated_ast(Value, Context, TreeWalkerAcc);
-                                       ({'widthratio', Numerator, Denominator, Scale}, TreeWalkerAcc) ->
-                                                       widthratio_ast(Numerator, Denominator, Scale, Context, TreeWalkerAcc);
-                                       ({'with', Args, Contents}, TreeWalkerAcc) ->
-                                                       with_ast(Args, Contents, Context, TreeWalkerAcc);
-                                       ({'extension', Tag}, TreeWalkerAcc) ->
-                                                       extension_ast(Tag, Context, TreeWalkerAcc);
-                                       ({'extends', _}, _TreeWalkerAcc) ->
-                                                       throw(unexpected_extends_tag);
-                                       (ValueToken, TreeWalkerAcc) ->
-                                                       {{ValueAst,ValueInfo},ValueTreeWalker} = value_ast(ValueToken, true, true, Context, TreeWalkerAcc),
-                                                       {{format(ValueAst, Context, ValueTreeWalker),ValueInfo},ValueTreeWalker}
-                                               end, TreeWalker, DjangoParseTree),
-    {AstList, {Info, TreeWalker3}} = lists:mapfoldl(
-                                       fun({Ast, Info}, {InfoAcc, TreeWalkerAcc}) ->
-                                               PresetVars = lists:foldl(fun
-                                                                            (X, Acc) ->
-                                                                               case proplists:lookup(X, Context#dtl_context.vars) of
-                                                                                   none -> Acc;
-                                                                                   Val -> [Val|Acc]
-                                                                               end
-                                                                       end, [], Info#ast_info.var_names),
-                                               case PresetVars of
-                                                   [] ->
-                                                       {Ast, {merge_info(Info, InfoAcc), TreeWalkerAcc}};
-                                                   _ ->
-                                                       Counter = TreeWalkerAcc#treewalker.counter,
-                                                       Name = list_to_atom(lists:concat([pre_render, Counter])),
-                                                       Ast1 = ?Q("'@Name@'(_@PresetVars@, RenderOptions)"),
-                                                       PreRenderAst = ?Q("'@Name@'(_Variables, RenderOptions) -> _@match, _@Ast.",
-                                                                         [{match, options_match_ast(Context, TreeWalkerAcc)}]),
-                                                       PreRenderAsts = Info#ast_info.pre_render_asts,
-                                                       Info1 = Info#ast_info{pre_render_asts = [PreRenderAst | PreRenderAsts]},
-                                                       {Ast1, {merge_info(Info1, InfoAcc), TreeWalkerAcc#treewalker{counter = Counter + 1}}}
-                                               end
-                                       end, {#ast_info{}, TreeWalker2}, AstInfoList),
-    {{erl_syntax:list(AstList), Info}, TreeWalker3}.
-
-
-value_ast(ValueToken, AsString, EmptyIfUndefined, Context, TreeWalker) ->
-    case ValueToken of
-        {'expr', Operator, Value} ->
-            {{ValueAst,InfoValue}, TreeWalker1} = value_ast(Value, false, EmptyIfUndefined, Context, TreeWalker),
-            Op = list_to_atom(Operator),
-            Ast = ?Q("erlydtl_runtime:_@Op@(_@ValueAst)"),
-            {{Ast, InfoValue}, TreeWalker1};
-        {'expr', Operator, Value1, Value2} ->
-            {{Value1Ast,InfoValue1}, TreeWalker1} = value_ast(Value1, false, EmptyIfUndefined, Context, TreeWalker),
-            {{Value2Ast,InfoValue2}, TreeWalker2} = value_ast(Value2, false, EmptyIfUndefined, Context, TreeWalker1),
-            Op = list_to_atom(Operator),
-            Ast = ?Q("erlydtl_runtime:_@Op@(_@Value1Ast, _@Value2Ast)"),
-            {{Ast, merge_info(InfoValue1,InfoValue2)}, TreeWalker2};
-        {'string_literal', _Pos, String} ->
-            string_ast(unescape_string_literal(String), Context, TreeWalker);
-        {'number_literal', _Pos, Number} ->
-            case AsString of
-                true  -> string_ast(Number, Context, TreeWalker);
-                false -> {{erl_syntax:integer(Number), #ast_info{}}, TreeWalker}
-            end;
-        {'apply_filter', Variable, Filter} ->
-            filter_ast(Variable, Filter, Context, TreeWalker);
-        {'attribute', _} = Variable ->
-            resolve_variable_ast(Variable, Context, TreeWalker, EmptyIfUndefined);
-        {'variable', _} = Variable ->
-            resolve_variable_ast(Variable, Context, TreeWalker, EmptyIfUndefined);
-        {extension, Tag} ->
-            extension_ast(Tag, Context, TreeWalker)
-    end.
-
-extension_ast(Tag, Context, TreeWalker) ->
-    case call_extension(Context, compile_ast, [Tag, Context, TreeWalker]) of
-        undefined ->
-            throw({unknown_extension, Tag});
-        Result ->
-            Result
-    end.
-
-merge_info(Info1, Info2) ->
-    #ast_info{
-       dependencies =
-           lists:merge(
-             lists:sort(Info1#ast_info.dependencies),
-             lists:sort(Info2#ast_info.dependencies)),
-       var_names =
-           lists:merge(
-             lists:sort(Info1#ast_info.var_names),
-             lists:sort(Info2#ast_info.var_names)),
-       translatable_strings =
-           lists:merge(
-             lists:sort(Info1#ast_info.translatable_strings),
-             lists:sort(Info2#ast_info.translatable_strings)),
-       translated_blocks =
-           lists:merge(
-             lists:sort(Info1#ast_info.translated_blocks),
-             lists:sort(Info2#ast_info.translated_blocks)),
-       custom_tags =
-           lists:merge(
-             lists:sort(Info1#ast_info.custom_tags),
-             lists:sort(Info2#ast_info.custom_tags)),
-       pre_render_asts =
-           lists:merge(
-             Info1#ast_info.pre_render_asts,
-             Info2#ast_info.pre_render_asts)}.
-
-
-with_dependencies([], Args) ->
-    Args;
-with_dependencies([Dependency | Rest], Args) ->
-    with_dependencies(Rest, with_dependency(Dependency, Args)).
-
-with_dependency(FilePath, {{Ast, Info}, TreeWalker}) ->
-    {{Ast, Info#ast_info{dependencies = [FilePath | Info#ast_info.dependencies]}}, TreeWalker}.
-
-
-empty_ast(TreeWalker) ->
-    {{erl_syntax:list([]), #ast_info{}}, TreeWalker}.
-
-blocktrans_ast(ArgList, Contents, Context, TreeWalker) ->
-    %% add new scope using 'with' values
-    {NewScope, {ArgInfo, TreeWalker1}} = lists:mapfoldl(fun
-                                                            ({{identifier, _, LocalVarName}, Value}, {AstInfo1, TreeWalker1}) ->
-                                                               {{Ast, Info}, TreeWalker2} = value_ast(Value, false, false, Context, TreeWalker1),
-                                                               {{LocalVarName, Ast}, {merge_info(AstInfo1, Info), TreeWalker2}}
-                                                       end, {#ast_info{}, TreeWalker}, ArgList),
-    NewContext = Context#dtl_context{ local_scopes = [NewScope|Context#dtl_context.local_scopes] },
-    %% key for translation lookup
-    SourceText = lists:flatten(erlydtl_unparser:unparse(Contents)),
-    {{DefaultAst, AstInfo}, TreeWalker2} = body_ast(Contents, NewContext, TreeWalker1),
-    MergedInfo = merge_info(AstInfo, ArgInfo),
-    case Context#dtl_context.trans_fun of
-        none ->
-            %% translate in runtime
-            blocktrans_runtime_ast({DefaultAst, MergedInfo}, TreeWalker2, SourceText, Contents, NewContext);
-        BlockTransFun when is_function(BlockTransFun) ->
-            %% translate in compile-time
-            {FinalAstInfo, FinalTreeWalker, Clauses} = lists:foldr(fun(Locale, {AstInfoAcc, ThisTreeWalker, ClauseAcc}) ->
-                                                                           case BlockTransFun(SourceText, Locale) of
-                                                                               default ->
-                                                                                   {AstInfoAcc, ThisTreeWalker, ClauseAcc};
-                                                                               Body ->
-                                                                                   {ok, DjangoParseTree} = do_parse(Body, Context),
-                                                                                   {{ThisAst, ThisAstInfo}, TreeWalker3} = body_ast(DjangoParseTree, NewContext, ThisTreeWalker),
-                                                                                   {merge_info(ThisAstInfo, AstInfoAcc), TreeWalker3,
-                                                                                    [?Q("_@Locale@ -> _@ThisAst")|ClauseAcc]}
-                                                                           end
-                                                                   end, {MergedInfo, TreeWalker2, []}, Context#dtl_context.trans_locales),
-            Ast = ?Q("case _CurrentLocale of _@_Clauses -> _; _ -> _@DefaultAst end"),
-            {{Ast, FinalAstInfo#ast_info{ translated_blocks = [SourceText] }}, FinalTreeWalker}
-    end.
-
-blocktrans_runtime_ast({DefaultAst, Info}, Walker, SourceText, Contents, Context) ->
-    %% Contents is flat - only strings and '{{var}}' allowed.
-    %% build sorted list (orddict) of pre-resolved variables to pass to runtime translation function
-    USortedVariables = lists:usort(fun({variable, {identifier, _, A}},
-                                       {variable, {identifier, _, B}}) ->
-                                           A =< B
-                                   end, [Var || {variable, _}=Var <- Contents]),
-    VarBuilder = fun({variable, {identifier, _, Name}}=Var, Walker1) ->
-                         {{Ast2, _InfoIgn}, Walker2}  = resolve_variable_ast(Var, Context, Walker1, false),
-                         {?Q("{_@name, _@Ast2}", [{name, merl:term(atom_to_list(Name))}]),
-                          Walker2}
-                 end,
-    {VarAsts, Walker2} = lists:mapfoldl(VarBuilder, Walker, USortedVariables),
-    VarListAst = erl_syntax:list(VarAsts),
-    BlockTransAst = ?Q(["if _TranslationFun =:= none -> _@DefaultAst;",
-                        "  true -> erlydtl_runtime:translate_block(",
-                        "    _@SourceText@, _TranslationFun, _@VarListAst)",
-                        "end"]),
-    {{BlockTransAst, Info}, Walker2}.
-
-translated_ast({string_literal, _, String}, Context, TreeWalker) ->
-    UnescapedStr = unescape_string_literal(String),
-    case call_extension(Context, translate_ast, [UnescapedStr, Context, TreeWalker]) of
-        undefined ->
-            AstInfo = #ast_info{translatable_strings = [UnescapedStr]},
-            case Context#dtl_context.trans_fun of
-                none -> runtime_trans_ast(erl_syntax:string(UnescapedStr), AstInfo, TreeWalker);
-                _ -> compiletime_trans_ast(UnescapedStr, AstInfo, Context, TreeWalker)
-            end;
-        Translated ->
-            Translated
-    end;
-translated_ast(ValueToken, Context, TreeWalker) ->
-    {{Ast, Info}, TreeWalker1} = value_ast(ValueToken, true, false, Context, TreeWalker),
-    runtime_trans_ast(Ast, Info, TreeWalker1).
-
-runtime_trans_ast(ValueAst, AstInfo, TreeWalker) ->
-    {{?Q("erlydtl_runtime:translate(_@ValueAst, _TranslationFun)"),
-      AstInfo},
-     TreeWalker}.
-
-compiletime_trans_ast(String, AstInfo,
-                      #dtl_context{trans_fun=TFun,
-                                   trans_locales=TLocales}=Context,
-                      TreeWalker) ->
-    ClAst = lists:foldl(
-              fun(Locale, ClausesAcc) ->
-                      [?Q("_@Locale@ -> _@translated",
-                          [{translated, case TFun(String, Locale) of
-                                            default -> string_ast(String, Context);
-                                            Translated -> string_ast(Translated, Context)
-                                        end}])
-                       |ClausesAcc]
-              end,
-              [], TLocales),
-    CaseAst = ?Q(["case _CurrentLocale of",
-                  "  _@_ClAst -> _;",
-                  " _ -> _@string",
-                  "end"],
-                 [{string, string_ast(String, Context)}]),
-    {{CaseAst, AstInfo}, TreeWalker}.
-
-%% Completely unnecessary in ErlyDTL (use {{ "{%" }} etc), but implemented for compatibility.
-templatetag_ast("openblock", Context, TreeWalker) ->
-    string_ast("{%", Context, TreeWalker);
-templatetag_ast("closeblock", Context, TreeWalker) ->
-    string_ast("%}", Context, TreeWalker);
-templatetag_ast("openvariable", Context, TreeWalker) ->
-    string_ast("{{", Context, TreeWalker);
-templatetag_ast("closevariable", Context, TreeWalker) ->
-    string_ast("}}", Context, TreeWalker);
-templatetag_ast("openbrace", Context, TreeWalker) ->
-    string_ast("{", Context, TreeWalker);
-templatetag_ast("closebrace", Context, TreeWalker) ->
-    string_ast("}", Context, TreeWalker);
-templatetag_ast("opencomment", Context, TreeWalker) ->
-    string_ast("{#", Context, TreeWalker);
-templatetag_ast("closecomment", Context, TreeWalker) ->
-    string_ast("#}", Context, TreeWalker).
-
-
-widthratio_ast(Numerator, Denominator, Scale, Context, TreeWalker) ->
-    {{NumAst, NumInfo}, TreeWalker1} = value_ast(Numerator, false, true, Context, TreeWalker),
-    {{DenAst, DenInfo}, TreeWalker2} = value_ast(Denominator, false, true, Context, TreeWalker1),
-    {{ScaleAst, ScaleInfo}, TreeWalker3} = value_ast(Scale, false, true, Context, TreeWalker2),
-    {{format_number_ast(?Q("erlydtl_runtime:widthratio(_@NumAst, _@DenAst, _@ScaleAst)")),
-      merge_info(ScaleInfo, merge_info(NumInfo, DenInfo))},
-     TreeWalker3}.
-
-
-string_ast(Arg, #dtl_context{ binary_strings = true }) ->
-    if is_binary(Arg) ->
-            merl:term(Arg);
-       is_list(Arg) ->
-            merl:term(list_to_binary(Arg));
-       is_integer(Arg) ->
-            case erlang:function_exported(erlang, integer_to_binary, 1) of
-                true -> merl:term(erlang:integer_to_binary(Arg));
-                false -> merl:term(list_to_binary(integer_to_list(Arg)))
-            end;
-       is_atom(Arg) ->
-            merl:term(atom_to_binary(Arg, latin1)) %% latin1 so we match atom_to_list/1 behaviour
-    end;
-string_ast(Arg, #dtl_context{ binary_strings = false }) ->
-    if is_list(Arg) ->
-            merl:term(Arg);
-       is_binary(Arg) ->
-            merl:term(binary_to_list(Arg));
-       is_integer(Arg) ->
-            merl:term(integer_to_list(Arg));
-       is_atom(Arg) ->
-            merl:term(atom_to_list(Arg))
-    end.
-
-string_ast(Arg, Context, TreeWalker) ->
-    {{string_ast(Arg, Context), #ast_info{}}, TreeWalker}.
-
-
-include_ast(File, ArgList, Scopes, Context, TreeWalker) ->
-    FilePath = full_path(File, Context#dtl_context.doc_root),
-    case parse_file(FilePath, Context) of
-        {ok, InclusionParseTree, CheckSum} ->
-            {NewScope, {ArgInfo, TreeWalker1}}
-                = lists:mapfoldl(
-                    fun ({{identifier, _, LocalVarName}, Value}, {AstInfo1, TreeWalker1}) ->
-                            {{Ast, Info}, TreeWalker2} = value_ast(Value, false, false, Context, TreeWalker1),
-                            {{LocalVarName, Ast}, {merge_info(AstInfo1, Info), TreeWalker2}}
-                    end, {#ast_info{}, TreeWalker}, ArgList),
-
-            {{BodyAst, BodyInfo}, TreeWalker2} = with_dependency(
-                                                   {FilePath, CheckSum},
-                                                   body_ast(
-                                                     InclusionParseTree,
-                                                     Context#dtl_context{
-                                                       parse_trail = [FilePath | Context#dtl_context.parse_trail],
-                                                       local_scopes = [NewScope|Scopes]
-                                                      }, TreeWalker1)),
-
-            {{BodyAst, merge_info(BodyInfo, ArgInfo)}, TreeWalker2};
-        Err -> throw(Err)
-    end.
-
-%% include at run-time
-ssi_ast(FileName, Context, TreeWalker) ->
-    {{FileAst, Info}, TreeWalker1} = value_ast(FileName, true, true, Context, TreeWalker),
-    {Mod, Fun} = Context#dtl_context.reader,
-    Dir = Context#dtl_context.doc_root,
-    {{?Q("erlydtl_runtime:read_file(_@Mod@, _@Fun@, _@Dir@, _@FileAst)"), Info}, TreeWalker1}.
-
-filter_tag_ast(FilterList, Contents, Context, TreeWalker) ->
-    {{InnerAst, Info}, TreeWalker1} = body_ast(Contents, Context#dtl_context{auto_escape = did}, TreeWalker),
-    {{FilteredAst, FilteredInfo}, TreeWalker2} =
-        lists:foldl(
-          fun ({{identifier, _, Name}, []}, {{AstAcc, InfoAcc}, TreeWalkerAcc})
-                when Name =:= 'escape'; Name =:= 'safe'; Name =:= 'safeseq' ->
-                  {{AstAcc, InfoAcc}, TreeWalkerAcc#treewalker{safe = true}};
-              (Filter, {{AstAcc, InfoAcc}, TreeWalkerAcc}) ->
-                  {{Ast, AstInfo}, TW} = filter_ast1(Filter, AstAcc, Context, TreeWalkerAcc),
-                  {{Ast, merge_info(InfoAcc, AstInfo)}, TW}
-          end,
-          {{?Q("erlang:iolist_to_binary(_@InnerAst)"), Info}, TreeWalker1},
-          FilterList),
-
-    EscapedAst = case search_for_escape_filter(lists:reverse(FilterList), Context) of
-                     on -> ?Q("erlydtl_filters:force_escape(_@FilteredAst)");
-                     _ -> FilteredAst
-                 end,
-    {{EscapedAst, FilteredInfo}, TreeWalker2}.
-
-search_for_escape_filter(FilterList, #dtl_context{auto_escape = on}) ->
-    search_for_safe_filter(FilterList);
-search_for_escape_filter(_, #dtl_context{auto_escape = did}) -> off;
-search_for_escape_filter([{{identifier, _, 'escape'}, []}|Rest], _Context) ->
-    search_for_safe_filter(Rest);
-search_for_escape_filter([_|Rest], Context) ->
-    search_for_escape_filter(Rest, Context);
-search_for_escape_filter([], _Context) -> off.
-
-search_for_safe_filter([{{identifier, _, Name}, []}|_])
-  when Name =:= 'safe'; Name =:= 'safeseq' -> off;
-search_for_safe_filter([_|Rest]) -> search_for_safe_filter(Rest);
-search_for_safe_filter([]) -> on.
-
-filter_ast(Variable, Filter, Context, TreeWalker) ->
-    %% the escape filter is special; it is always applied last, so we have to go digging for it
-
-    %% AutoEscape = 'did' means we (will have) decided whether to escape the current variable,
-    %% so don't do any more escaping
-    {{UnescapedAst, Info}, TreeWalker2} = filter_ast_noescape(
-                                            Variable, Filter,
-                                            Context#dtl_context{auto_escape = did},
-                                            TreeWalker),
-
-    EscapedAst = case search_for_escape_filter(Variable, Filter, Context) of
-                     on -> ?Q("erlydtl_filters:force_escape(_@UnescapedAst)");
-                     _ -> UnescapedAst
-                 end,
-    {{EscapedAst, Info}, TreeWalker2}.
-
-filter_ast_noescape(Variable, {{identifier, _, Name}, []}, Context, TreeWalker)
-  when Name =:= 'escape'; Name =:= 'safe'; Name =:= 'safeseq' ->
-    value_ast(Variable, true, false, Context, TreeWalker#treewalker{safe = true});
-filter_ast_noescape(Variable, Filter, Context, TreeWalker) ->
-    {{ValueAst, Info1}, TreeWalker2} = value_ast(Variable, true, false, Context, TreeWalker),
-    {{VarValue, Info2}, TreeWalker3} = filter_ast1(Filter, ValueAst, Context, TreeWalker2),
-    {{VarValue, merge_info(Info1, Info2)}, TreeWalker3}.
-
-filter_ast1({{identifier, _, Name}, Args}, ValueAst, Context, TreeWalker) ->
-    {{ArgsAst, ArgsInfo}, TreeWalker2} =
-        lists:foldr(
-          fun (Arg, {{AccAst, AccInfo}, AccTreeWalker}) ->
-                  {{ArgAst, ArgInfo}, ArgTreeWalker} = value_ast(Arg, false, false, Context, AccTreeWalker),
-                  {{[ArgAst|AccAst], merge_info(ArgInfo, AccInfo)}, ArgTreeWalker}
-          end,
-          {{[], #ast_info{}}, TreeWalker},
-          Args),
-    FilterAst = filter_ast2(Name, [ValueAst|ArgsAst], Context),
-    {{FilterAst, ArgsInfo}, TreeWalker2}.
-
-filter_ast2(Name, Args, #dtl_context{ filter_modules = [Module|Rest] } = Context) ->
-    case lists:member({Name, length(Args)}, Module:module_info(exports)) of
-        true -> ?Q("'@Module@':'@Name@'(_@Args)");
-        false ->
-            filter_ast2(Name, Args, Context#dtl_context{ filter_modules = Rest })
-    end;
-filter_ast2(Name, Args, _) ->
-    throw({unknown_filter, Name, length(Args)}).
-
-search_for_escape_filter(Variable, Filter, #dtl_context{auto_escape = on}) ->
-    search_for_safe_filter(Variable, Filter);
-search_for_escape_filter(_, _, #dtl_context{auto_escape = did}) ->
-    off;
-search_for_escape_filter(Variable, {{identifier, _, 'escape'}, []} = Filter, _Context) ->
-    search_for_safe_filter(Variable, Filter);
-search_for_escape_filter({apply_filter, Variable, Filter}, _, Context) ->
-    search_for_escape_filter(Variable, Filter, Context);
-search_for_escape_filter(_Variable, _Filter, _Context) ->
-    off.
-
-search_for_safe_filter(_, {{identifier, _, 'safe'}, []}) ->
-    off;
-search_for_safe_filter(_, {{identifier, _, 'safeseq'}, []}) ->
-    off;
-search_for_safe_filter({apply_filter, Variable, Filter}, _) ->
-    search_for_safe_filter(Variable, Filter);
-search_for_safe_filter(_Variable, _Filter) ->
-    on.
-
-finder_function(true) -> {erlydtl_runtime, fetch_value};
-finder_function(false) -> {erlydtl_runtime, find_value}.
-
-finder_function(EmptyIfUndefined, Context) ->
-    case call_extension(Context, finder_function, [EmptyIfUndefined]) of
-        undefined -> finder_function(EmptyIfUndefined);
-        Result -> Result
-    end.
-
-resolve_variable_ast({extension, Tag}, Context, TreeWalker, _) ->
-    extension_ast(Tag, Context, TreeWalker);
-resolve_variable_ast(VarTuple, Context, TreeWalker, EmptyIfUndefined)
-  when is_boolean(EmptyIfUndefined) ->
-    resolve_variable_ast(VarTuple, Context, TreeWalker, finder_function(EmptyIfUndefined, Context));
-resolve_variable_ast(VarTuple, Context, TreeWalker, FinderFunction) ->
-    resolve_variable_ast1(VarTuple, Context, TreeWalker, FinderFunction).
-
-resolve_variable_ast1({attribute, {{_, Pos, Attr}, Variable}}, Context, TreeWalker, FinderFunction) ->
-    {{VarAst, VarInfo}, TreeWalker1} = resolve_variable_ast(Variable, Context, TreeWalker, FinderFunction),
-    FileName = case Context#dtl_context.parse_trail of
-                   [] -> undefined;
-                   [H|_] -> H
-               end,
-    {Runtime, Finder} = FinderFunction,
-    {{?Q(["'@Runtime@':'@Finder@'(",
-          "  _@Attr@, _@VarAst,",
-          "  [{filename, _@FileName@},",
-          "   {pos, _@Pos@},",
-          "   {record_info, _RecordInfo},",
-          "   {render_options, RenderOptions}])"]),
-      VarInfo},
-     TreeWalker1};
-
-resolve_variable_ast1({variable, {identifier, Pos, VarName}}, Context, TreeWalker, FinderFunction) ->
-    VarValue = case resolve_scoped_variable_ast(VarName, Context) of
-                   undefined ->
-                       FileName = case Context#dtl_context.parse_trail of
-                                      [] -> undefined;
-                                      [H|_] -> H
-                                  end,
-                       {Runtime, Finder} = FinderFunction,
-                       ?Q(["'@Runtime@':'@Finder@'(",
-                           "  _@VarName@, _Variables,",
-                           "  [{filename, _@FileName@},",
-                           "   {pos, _@Pos@},",
-                           "   {record_info, _RecordInfo},",
-                           "   {render_options, RenderOptions}])"]);
-                   Val ->
-                       Val
-               end,
-    {{VarValue, #ast_info{ var_names=[VarName] }}, TreeWalker}.
-
-resolve_scoped_variable_ast(VarName, Context) ->
-    resolve_scoped_variable_ast(VarName, Context, undefined).
-
-resolve_scoped_variable_ast(VarName, Context, Default) ->
-    lists:foldl(
-      fun (Scope, Res) ->
-              if Res =:= Default ->
-                      proplists:get_value(VarName, Scope, Default);
-                 true -> Res
-              end
-      end,
-      Default,
-      Context#dtl_context.local_scopes).
-
-format(Ast, Context, TreeWalker) ->
-    auto_escape(format_number_ast(Ast), Context, TreeWalker).
-
-format_number_ast(Ast) ->
-    ?Q("erlydtl_filters:format_number(_@Ast)").
-
-
-auto_escape(Value, _, #treewalker{safe = true}) ->
-    Value;
-auto_escape(Value, #dtl_context{auto_escape = on}, _) ->
-    ?Q("erlydtl_filters:force_escape(_@Value)");
-auto_escape(Value, _, _) ->
-    Value.
-
-firstof_ast(Vars, Context, TreeWalker) ->
-    body_ast([lists:foldr(fun
-                              ({L, _, _}=Var, []) when L=:=string_literal;L=:=number_literal ->
-                                 Var;
-                              ({L, _, _}, _) when L=:=string_literal;L=:=number_literal ->
-                                 erlang:error(errbadliteral);
-                              (Var, []) ->
-                                 {'ifelse', Var, [Var], []};
-                              (Var, Acc) ->
-                                 {'ifelse', Var, [Var], [Acc]} end,
-                          [], Vars)], Context, TreeWalker).
-
-ifelse_ast(Expression, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, Context, TreeWalker) ->
-    Info = merge_info(IfContentsInfo, ElseContentsInfo),
-    {{Ast, ExpressionInfo}, TreeWalker1} = value_ast(Expression, false, false, Context, TreeWalker),
-    {{?Q(["case erlydtl_runtime:is_true(_@Ast) of",
-          "  true -> _@IfContentsAst;",
-          "  _ -> _@ElseContentsAst",
-          "end"]),
-      merge_info(ExpressionInfo, Info)},
-     TreeWalker1}.
-
-with_ast(ArgList, Contents, Context, TreeWalker) ->
-    {ArgAstList, {ArgInfo, TreeWalker1}} =
-        lists:mapfoldl(
-          fun ({{identifier, _, _LocalVarName}, Value}, {AstInfo1, TreeWalker1}) ->
-                  {{Ast, Info}, TreeWalker2} = value_ast(Value, false, false, Context, TreeWalker1),
-                  {Ast, {merge_info(AstInfo1, Info), TreeWalker2}}
-          end, {#ast_info{}, TreeWalker}, ArgList),
-
-    NewScope = lists:map(
-                 fun({{identifier, _, LocalVarName}, _Value}) ->
-                         {LocalVarName, erl_syntax:variable(lists:concat(["Var_", LocalVarName]))}
-                 end, ArgList),
-
-    {{InnerAst, InnerInfo}, TreeWalker2} =
-        body_ast(
-          Contents,
-          Context#dtl_context{local_scopes = [NewScope|Context#dtl_context.local_scopes]},
-          TreeWalker1),
-
-    {{?Q("fun (_@args) -> _@InnerAst end (_@ArgAstList)",
-         [{args, element(2, lists:unzip(NewScope))}]),
-      merge_info(ArgInfo, InnerInfo)},
-     TreeWalker2}.
-
-regroup_ast(ListVariable, GrouperVariable, LocalVarName, Contents, Context, TreeWalker) ->
-    {{ListAst, ListInfo}, TreeWalker1} = value_ast(ListVariable, false, true, Context, TreeWalker),
-    LocalVarAst = erl_syntax:variable(lists:concat(["Var_", LocalVarName])),
-    NewScope = [{LocalVarName, LocalVarAst}],
-
-    {{InnerAst, InnerInfo}, TreeWalker2} = body_ast(Contents,
-                                                    Context#dtl_context{
-                                                      local_scopes = [NewScope|Context#dtl_context.local_scopes]
-                                                     },
-                                                    TreeWalker1),
-
-    {{?Q(["fun (_@LocalVarAst) -> _@InnerAst end",
-         "(erlydtl_runtime:regroup(_@ListAst, _@regroup))"],
-        [{regroup, regroup_filter(GrouperVariable, [])}]),
-      merge_info(ListInfo, InnerInfo)},
-     TreeWalker2}.
-
-regroup_filter({attribute,{{identifier,_,Ident},Next}},Acc) ->
-    regroup_filter(Next,[erl_syntax:atom(Ident)|Acc]);
-regroup_filter({variable,{identifier,_,Var}},Acc) ->
-    erl_syntax:list([erl_syntax:atom(Var)|Acc]).
-
-to_list_ast(Value, IsReversed) ->
-    ?Q("erlydtl_runtime:to_list(_@Value, _@IsReversed)").
-
-to_list_ast(Value, IsReversed, Context, TreeWalker) ->
-    case call_extension(Context, to_list_ast, [Value, IsReversed, Context, TreeWalker]) of
-        undefined -> to_list_ast(Value, IsReversed);
-        Result -> Result
-    end.
-
-for_loop_ast(IteratorList, LoopValue, IsReversed, Contents, {EmptyContentsAst, EmptyContentsInfo}, Context, TreeWalker) ->
-    %% create unique namespace for this instance
-    Level = length(Context#dtl_context.local_scopes),
-    {Row, Col} = element(2, hd(IteratorList)),
-    ForId = lists:concat(["/", Level, "_", Row, ":", Col]),
-
-    Counters = merl:var(lists:concat(["Counters", ForId])),
-    Vars = merl:var(lists:concat(["Vars", ForId])),
-
-    %% setup
-    VarScope = lists:map(
-                 fun({identifier, {R, C}, Iterator}) ->
-                         {Iterator, erl_syntax:variable(
-                                      lists:concat(["Var_", Iterator,
-                                                    "/", Level, "_", R, ":", C
-                                                   ]))}
-                 end, IteratorList),
-    {Iterators, IteratorVars} = lists:unzip(VarScope),
-    IteratorCount = length(IteratorVars),
-
-    {{LoopBodyAst, Info}, TreeWalker1} =
-        body_ast(
-          Contents,
-          Context#dtl_context{
-            local_scopes =
-                [[{'forloop', Counters} | VarScope]
-                 | Context#dtl_context.local_scopes]
-           },
-          TreeWalker),
-
-    {{LoopValueAst, LoopValueInfo}, TreeWalker2} = value_ast(LoopValue, false, true, Context, TreeWalker1),
-
-    LoopValueAst0 = to_list_ast(LoopValueAst, erl_syntax:atom(IsReversed), Context, TreeWalker2),
-
-    ParentLoop = resolve_scoped_variable_ast('forloop', Context, erl_syntax:atom(undefined)),
-
-    %% call for loop
-    {{?Q(["case erlydtl_runtime:forloop(",
-          "  fun (_@Vars, _@Counters) ->",
-          "    {_@IteratorVars} = if is_tuple(_@Vars), size(_@Vars) == _@IteratorCount@ -> _@Vars;",
-          "                          _@___ifclauses -> _",
-          "                       end,",
-          "    {_@LoopBodyAst, erlydtl_runtime:increment_counter_stats(_@Counters)}",
-          "  end,",
-          "  _@LoopValueAst0, _@ParentLoop)",
-          "of",
-          "  empty -> _@EmptyContentsAst;",
-          "  {L, _} -> L",
-          "end"],
-         [{ifclauses, if IteratorCount > 1 ->
-                              ?Q(["() when is_list(_@Vars), length(_@Vars) == _@IteratorCount@ ->",
-                                  "  list_to_tuple(_@Vars);",
-                                  "() when true -> throw({for_loop, _@Iterators@, _@Vars})"]);
-                         true ->
-                              ?Q("() when true -> {_@Vars}")
-                      end}]),
-      merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo)},
-     TreeWalker2}.
-
-ifchanged_values_ast(Values, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, Context, TreeWalker) ->
-    Info = merge_info(IfContentsInfo, ElseContentsInfo),
-    ValueAstFun = fun(Expr, {LTreeWalker, LInfo, Acc}) ->
-                          {{EAst, EInfo}, ETw} = value_ast(Expr, false, true, Context, LTreeWalker),
-                          {ETw, merge_info(LInfo, EInfo),
-                           [?Q("{_@hash, _@EAst}",
-                               [{hash, merl:term(erlang:phash2(Expr))}])
-                            |Acc]}
-                  end,
-    {TreeWalker1, MergedInfo, Changed} = lists:foldl(ValueAstFun, {TreeWalker, Info, []}, Values),
-    {{?Q(["case erlydtl_runtime:ifchanged([_@Changed]) of",
-          "  true -> _@IfContentsAst;",
-          "  _ -> _@ElseContentsAst",
-          "end"]),
-      MergedInfo},
-     TreeWalker1}.
-
-ifchanged_contents_ast(Contents, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, _Context, TreeWalker) ->
-    {{?Q(["case erlydtl_runtime:ifchanged([{_@hash, _@IfContentsAst}]) of",
-          "  true -> _@IfContentsAst;",
-          "  _ -> _@ElseContentsAst",
-          "end"],
-         [{hash, merl:term(erlang:phash2(Contents))}]),
-      merge_info(IfContentsInfo, ElseContentsInfo)},
-     TreeWalker}.
-
-cycle_ast(Names, Context, TreeWalker) ->
-    {NamesTuple, VarNames}
-        = lists:mapfoldl(
-            fun ({string_literal, _, Str}, VarNamesAcc) ->
-                    S = string_ast(unescape_string_literal(Str), Context),
-                    {S, VarNamesAcc};
-                ({variable, _}=Var, VarNamesAcc) ->
-                    {{V, #ast_info{ var_names=[VarName] }}, _} = resolve_variable_ast(Var, Context, TreeWalker, true),
-                    {V, [VarName|VarNamesAcc]};
-                ({number_literal, _, Num}, VarNamesAcc) ->
-                    {format(erl_syntax:integer(Num), Context, TreeWalker), VarNamesAcc};
-                (_, VarNamesAcc) ->
-                    {[], VarNamesAcc}
-            end, [], Names),
-    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@forloop)",
-        [{forloop, resolve_scoped_variable_ast('forloop', Context)}]),
-      #ast_info{ var_names = VarNames }},
-     TreeWalker}.
-
-%% Older Django templates treat cycle with comma-delimited elements as strings
-cycle_compat_ast(Names, Context, TreeWalker) ->
-    NamesTuple = lists:map(
-                   fun ({identifier, _, X}) ->
-                           string_ast(X, Context)
-                   end, Names),
-    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@forloop)",
-        [{forloop, resolve_scoped_variable_ast('forloop', Context)}]),
-      #ast_info{}},
-     TreeWalker}.
-
-now_ast(FormatString, Context, TreeWalker) ->
-    %% Note: we can't use unescape_string_literal here
-    %% because we want to allow escaping in the format string.
-    %% We only want to remove the surrounding escapes,
-    %% i.e. \"foo\" becomes "foo"
-    UnescapeOuter = string:strip(FormatString, both, 34),
-    {{StringAst, Info}, TreeWalker1} = string_ast(UnescapeOuter, Context, TreeWalker),
-    {{?Q("erlydtl_dateformat:format(_@StringAst)"), Info}, TreeWalker1}.
-
-spaceless_ast(Contents, Context, TreeWalker) ->
-    {{Ast, Info}, TreeWalker1} = body_ast(Contents, Context, TreeWalker),
-    {{?Q("erlydtl_runtime:spaceless(_@Ast)"), Info}, TreeWalker1}.
-
-unescape_string_literal(String) ->
-    unescape_string_literal(string:strip(String, both, 34), [], noslash).
-
-unescape_string_literal([], Acc, noslash) ->
-    lists:reverse(Acc);
-unescape_string_literal([$\\ | Rest], Acc, noslash) ->
-    unescape_string_literal(Rest, Acc, slash);
-unescape_string_literal([C | Rest], Acc, noslash) ->
-    unescape_string_literal(Rest, [C | Acc], noslash);
-unescape_string_literal("n" ++ Rest, Acc, slash) ->
-    unescape_string_literal(Rest, [$\n | Acc], noslash);
-unescape_string_literal("r" ++ Rest, Acc, slash) ->
-    unescape_string_literal(Rest, [$\r | Acc], noslash);
-unescape_string_literal("t" ++ Rest, Acc, slash) ->
-    unescape_string_literal(Rest, [$\t | Acc], noslash);
-unescape_string_literal([C | Rest], Acc, slash) ->
-    unescape_string_literal(Rest, [C | Acc], noslash).
-
-
-full_path(File, DocRoot) ->
-    case filename:absname(File) of
-        File -> File;
-        _ -> filename:join([DocRoot, File])
-    end.
-
-%%-------------------------------------------------------------------
-%% Custom tags
-%%-------------------------------------------------------------------
-
-interpret_value({trans, StringLiteral}, Context, TreeWalker) ->
-    translated_ast(StringLiteral, Context, TreeWalker);
-interpret_value(Value, Context, TreeWalker) ->
-    value_ast(Value, false, false, Context, TreeWalker).
-
-interpret_args(Args, Context, TreeWalker) ->
-    lists:foldr(
-      fun ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) ->
-              {{Ast0, AstInfo0}, TreeWalker0} = interpret_value(Value, Context, TreeWalkerAcc),
-              {{[?Q("{_@Key@, _@Ast0}")|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0};
-          (Value, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) ->
-              {{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, Context, TreeWalkerAcc),
-              {{[Ast0|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0}
-      end, {{[], #ast_info{}}, TreeWalker}, Args).
-
-tag_ast(Name, Args, Context, TreeWalker) ->
-    {{InterpretedArgs, AstInfo1}, TreeWalker1} = interpret_args(Args, Context, TreeWalker),
-    {RenderAst, RenderInfo} = custom_tags_modules_ast(Name, InterpretedArgs, Context),
-    {{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker1}.
-
-custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = false }) ->
-    {?Q("render_tag(_@Name@, [_@InterpretedArgs], RenderOptions)"),
-     #ast_info{custom_tags = [Name]}};
-custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = true, module = Module }) ->
-    {?Q("'@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)"),
-     #ast_info{ custom_tags = [Name] }};
-custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [Module|Rest] } = Context) ->
-    try lists:max([I || {N,I} <- Module:module_info(exports), N =:= Name]) of
-        2 ->
-            {?Q("'@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)"), #ast_info{}};
-        1 ->
-            {?Q("'@Module@':'@Name@'([_@InterpretedArgs])"), #ast_info{}};
-        I ->
-            throw({unsupported_custom_tag_fun, {Module, Name, I}})
-    catch _:function_clause ->
-            custom_tags_modules_ast(Name, InterpretedArgs,
-                                    Context#dtl_context{ custom_tags_modules = Rest })
-    end.
-
-call_ast(Module, TreeWalkerAcc) ->
-    call_ast(Module, merl:var("_Variables"), #ast_info{}, TreeWalkerAcc).
-
-call_with_ast(Module, Variable, Context, TreeWalker) ->
-    {{VarAst, VarInfo}, TreeWalker2} = resolve_variable_ast(Variable, Context, TreeWalker, false),
-    call_ast(Module, VarAst, VarInfo, TreeWalker2).
-
-call_ast(Module, Variable, AstInfo, TreeWalker) ->
-    Ast = ?Q(["case '@Module@':render(_@Variable, RenderOptions) of",
-              "  {ok, Rendered} -> Rendered;",
-              "  {error, Reason} -> io_lib:format(\"error: ~p\", [Reason])",
-              "end"]),
-    with_dependencies(Module:dependencies(), {{Ast, AstInfo}, TreeWalker}).
-
-
-print(Fmt, Args, #dtl_context{ verbose = true }) -> io:format(Fmt, Args);
-print(_Fmt, _Args, _Context) -> ok.
-
-get_current_file(#dtl_context{ parse_trail=[File|_] }) -> File;
-get_current_file(#dtl_context{ doc_root=Root }) -> Root.
-
-add_error(Error, #dtl_context{
-                    errors=#error_info{ report=Report, list=Es }=Ei
-                   }=Context) ->
-    Item = get_error_item(
-             Report, "",
-             get_current_file(Context),
-             Error),
-    Context#dtl_context{
-      errors=Ei#error_info{ list=[Item|Es] }
-     }.
-
-add_errors(Errors, Context) ->
-    lists:foldl(
-      fun (E, C) -> add_error(E, C) end,
-      Context, Errors).
-
-add_warning(Warning, #dtl_context{ warnings=warnings_as_errors }=Context) ->
-    add_error(Warning, Context);
-add_warning(Warning, #dtl_context{
-                        warnings=#error_info{ report=Report, list=Ws }=Wi
-                       }=Context) ->
-    Item = get_error_item(
-             Report, "Warning: ",
-             get_current_file(Context),
-             Warning),
-    Context#dtl_context{
-      warnings=Wi#error_info{ list=[Item|Ws] }
-     }.
-
-add_warnings(Warnings, Context) ->
-    lists:foldl(
-      fun (W, C) -> add_warning(W, C) end,
-      Context, Warnings).
-
-get_error_item(Report, Prefix, File, Error) ->
-    case compose_error_desc(Error) of
-        {Pos, Module, ErrorDesc} ->
-            new_error_item(Report, Prefix, File, Pos, Module, ErrorDesc);
-        ErrorItem ->
-            ErrorItem
-    end.
-
-compose_error_desc({Line, ErrorDesc})
-  when is_integer(Line) ->
-    {Line, ?MODULE, ErrorDesc};
-compose_error_desc({{Line, Col}, Module, _}=ErrorDesc)
-  when is_integer(Line), is_integer(Col), is_atom(Module) ->
-    ErrorDesc;
-compose_error_desc({Line, Module, _}=ErrorDesc)
-  when is_integer(Line), is_atom(Module) ->
-    ErrorDesc;
-compose_error_desc({_, InfoList}=ErrorDesc)
-  when is_list(InfoList) -> ErrorDesc;
-compose_error_desc(ErrorDesc) ->
-    {none, ?MODULE, ErrorDesc}.
-
-new_error_item(Report, Prefix, File, Pos, Module, ErrorDesc) ->
-    if Report  ->
-            io:format("~s:~s~s~s~n",
-                      [File, pos_info(Pos), Prefix,
-                       Module:format_error(ErrorDesc)]);
-       true -> nop
-    end,
-    {File, [{Pos, Module, ErrorDesc}]}.
-
-pos_info(none) -> " ";
-pos_info(Line) when is_integer(Line) ->
-    io_lib:format("~b: ", [Line]);
-pos_info({Line, Col}) when is_integer(Line), is_integer(Col) ->
-    io_lib:format("~b:~b ", [Line, Col]).
-
 pack_error_list(Es) ->
     collect_error_info([], Es, []).
 
@@ -1808,3 +408,16 @@ collect_error_info([], Rest, Acc) ->
         [E|Es] ->
             collect_error_info(Es, [], [E|Acc])
     end.
+
+
+do_compile(#dtl_context{ is_compiling_dir=true, parse_trail=[Dir] }=Context) ->
+    erlydtl_beam_compiler:compile_dir(Dir, Context);
+do_compile(#dtl_context{ bin=undefined, parse_trail=[File] }=Context) ->
+    compile_output(parse_file(File, Context), Context);
+do_compile(#dtl_context{ bin=Template }=Context) ->
+    compile_output(parse_template(Template, Context), Context).
+
+compile_output(up_to_date, Context) -> Context;
+compile_output({ok, DjangoParseTree, CheckSum}, Context) ->
+    erlydtl_beam_compiler:compile(DjangoParseTree, CheckSum, Context#dtl_context{ bin=undefined });
+compile_output({error, Reason}, Context) -> ?ERR(Reason, Context).

+ 254 - 0
src/erlydtl_compiler_utils.erl

@@ -0,0 +1,254 @@
+%%%-------------------------------------------------------------------
+%%% File:      erlydtl_compiler_utils.erl
+%%% @author    Roberto Saccon <rsaccon@gmail.com> [http://rsaccon.com]
+%%% @author    Evan Miller <emmiller@gmail.com>
+%%% @author    Andreas Stenius <kaos@astekk.se>
+%%% @copyright 2008 Roberto Saccon, Evan Miller
+%%% @copyright 2014 Andreas Stenius
+%%% @doc
+%%% ErlyDTL template compiler utils.
+%%% @end
+%%%
+%%% The MIT License
+%%%
+%%% Copyright (c) 2007 Roberto Saccon, Evan Miller
+%%% Copyright (c) 2014 Andreas Stenius
+%%%
+%%% Permission is hereby granted, free of charge, to any person obtaining a copy
+%%% of this software and associated documentation files (the "Software"), to deal
+%%% in the Software without restriction, including without limitation the rights
+%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+%%% copies of the Software, and to permit persons to whom the Software is
+%%% furnished to do so, subject to the following conditions:
+%%%
+%%% The above copyright notice and this permission notice shall be included in
+%%% all copies or substantial portions of the Software.
+%%%
+%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+%%% THE SOFTWARE.
+%%%
+%%% @since 2007-12-16 by Roberto Saccon, Evan Miller
+%%% @since 2014 by Andreas Stenius
+%%%-------------------------------------------------------------------
+-module(erlydtl_compiler_utils).
+-author('rsaccon@gmail.com').
+-author('emmiller@gmail.com').
+-author('Andreas Stenius <kaos@astekk.se>').
+
+%% --------------------------------------------------------------------
+%% Definitions
+%% --------------------------------------------------------------------
+
+-export([
+         add_error/3, add_errors/2,
+         add_warning/3, add_warnings/2,
+         call_extension/3,
+         format_error/1,
+         full_path/2,
+         get_current_file/1,
+         init_treewalker/1,
+         merge_info/2,
+         print/3,
+         to_string/2,
+         unescape_string_literal/1
+        ]).
+
+-include("erlydtl_ext.hrl").
+
+
+%% --------------------------------------------------------------------
+%% API
+%% --------------------------------------------------------------------
+
+init_treewalker(Context) ->
+    TreeWalker = #treewalker{},
+    case call_extension(Context, init_treewalker, [TreeWalker]) of
+        {ok, TW} when is_record(TW, treewalker) -> TW;
+        undefined -> TreeWalker
+    end.
+
+to_string(Arg, #dtl_context{ binary_strings = true }) ->
+    to_binary_string(Arg);
+to_string(Arg, #dtl_context{ binary_strings = false }) ->
+    to_list_string(Arg).
+
+unescape_string_literal(String) ->
+    unescape_string_literal(string:strip(String, both, 34), [], noslash).
+
+full_path(File, DocRoot) ->
+    case filename:absname(File) of
+        File -> File;
+        _ -> filename:join([DocRoot, File])
+    end.
+
+print(Fmt, Args, #dtl_context{ verbose = true }) -> io:format(Fmt, Args);
+print(_Fmt, _Args, _Context) -> ok.
+
+get_current_file(#dtl_context{ parse_trail=[File|_] }) -> File;
+get_current_file(#dtl_context{ doc_root=Root }) -> Root.
+
+add_error(Module, Error, #dtl_context{
+                    errors=#error_info{ report=Report, list=Es }=Ei
+                   }=Context) ->
+    Item = get_error_item(
+             Report, "",
+             get_current_file(Context),
+             Error, Module),
+    Context#dtl_context{
+      errors=Ei#error_info{ list=[Item|Es] }
+     }.
+
+add_errors(Errors, Context) ->
+    lists:foldl(
+      fun (E, C) -> add_error(?MODULE, E, C) end,
+      Context, Errors).
+
+add_warning(Module, Warning, #dtl_context{ warnings=warnings_as_errors }=Context) ->
+    add_error(Module, Warning, Context);
+add_warning(Module, Warning, #dtl_context{
+                        warnings=#error_info{ report=Report, list=Ws }=Wi
+                       }=Context) ->
+    Item = get_error_item(
+             Report, "Warning: ",
+             get_current_file(Context),
+             Warning, Module),
+    Context#dtl_context{
+      warnings=Wi#error_info{ list=[Item|Ws] }
+     }.
+
+add_warnings(Warnings, Context) ->
+    lists:foldl(
+      fun (W, C) -> add_warning(?MODULE, W, C) end,
+      Context, Warnings).
+
+call_extension(#dtl_context{ extension_module=undefined }, _Fun, _Args) ->
+    undefined;
+call_extension(#dtl_context{ extension_module=Mod }, Fun, Args)
+  when is_atom(Mod), is_atom(Fun), is_list(Args) ->
+    M = case code:is_loaded(Mod) of
+            false ->
+                case code:load_file(Mod) of
+                    {module, Mod} ->
+                        Mod;
+                    _ ->
+                        undefined
+                end;
+            _ -> Mod
+        end,
+    if M /= undefined ->
+            case erlang:function_exported(M, Fun, length(Args)) of
+                true ->
+                    apply(M, Fun, Args);
+                false ->
+                    undefined
+            end;
+       true ->
+            undefined
+    end.
+
+merge_info(Info1, Info2) ->
+    #ast_info{
+       dependencies =
+           lists:merge(
+             lists:sort(Info1#ast_info.dependencies),
+             lists:sort(Info2#ast_info.dependencies)),
+       var_names =
+           lists:merge(
+             lists:sort(Info1#ast_info.var_names),
+             lists:sort(Info2#ast_info.var_names)),
+       translatable_strings =
+           lists:merge(
+             lists:sort(Info1#ast_info.translatable_strings),
+             lists:sort(Info2#ast_info.translatable_strings)),
+       translated_blocks =
+           lists:merge(
+             lists:sort(Info1#ast_info.translated_blocks),
+             lists:sort(Info2#ast_info.translated_blocks)),
+       custom_tags =
+           lists:merge(
+             lists:sort(Info1#ast_info.custom_tags),
+             lists:sort(Info2#ast_info.custom_tags)),
+       pre_render_asts =
+           lists:merge(
+             Info1#ast_info.pre_render_asts,
+             Info2#ast_info.pre_render_asts)}.
+
+    
+format_error(Other) ->
+    io_lib:format("## Error description for ~p not implemented.", [Other]).
+
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
+
+to_binary_string(Arg) when is_binary(Arg) -> Arg;
+to_binary_string(Arg) when is_list(Arg) -> list_to_binary(Arg);
+to_binary_string(Arg) when is_integer(Arg) ->
+    case erlang:function_exported(erlang, integer_to_binary, 1) of
+        true -> erlang:integer_to_binary(Arg);
+        false -> list_to_binary(integer_to_list(Arg))
+    end;
+to_binary_string(Arg) when is_atom(Arg) -> atom_to_binary(Arg, latin1).
+
+to_list_string(Arg) when is_list(Arg) -> Arg;
+to_list_string(Arg) when is_binary(Arg) -> binary_to_list(Arg);
+to_list_string(Arg) when is_integer(Arg) -> integer_to_list(Arg);
+to_list_string(Arg) when is_atom(Arg) -> atom_to_list(Arg).
+
+unescape_string_literal([], Acc, noslash) ->
+    lists:reverse(Acc);
+unescape_string_literal([$\\ | Rest], Acc, noslash) ->
+    unescape_string_literal(Rest, Acc, slash);
+unescape_string_literal([C | Rest], Acc, noslash) ->
+    unescape_string_literal(Rest, [C | Acc], noslash);
+unescape_string_literal("n" ++ Rest, Acc, slash) ->
+    unescape_string_literal(Rest, [$\n | Acc], noslash);
+unescape_string_literal("r" ++ Rest, Acc, slash) ->
+    unescape_string_literal(Rest, [$\r | Acc], noslash);
+unescape_string_literal("t" ++ Rest, Acc, slash) ->
+    unescape_string_literal(Rest, [$\t | Acc], noslash);
+unescape_string_literal([C | Rest], Acc, slash) ->
+    unescape_string_literal(Rest, [C | Acc], noslash).
+
+get_error_item(Report, Prefix, File, Error, DefaultModule) ->
+    case compose_error_desc(Error, DefaultModule) of
+        {Pos, Module, ErrorDesc} ->
+            new_error_item(Report, Prefix, File, Pos, Module, ErrorDesc);
+        ErrorItem ->
+            ErrorItem
+    end.
+
+compose_error_desc({Line, ErrorDesc}, Module)
+  when is_integer(Line) ->
+    {Line, Module, ErrorDesc};
+compose_error_desc({{Line, Col}, Module, _}=ErrorDesc, _)
+  when is_integer(Line), is_integer(Col), is_atom(Module) ->
+    ErrorDesc;
+compose_error_desc({Line, Module, _}=ErrorDesc, _)
+  when is_integer(Line), is_atom(Module) ->
+    ErrorDesc;
+compose_error_desc({_, InfoList}=ErrorDesc, _)
+  when is_list(InfoList) -> ErrorDesc;
+compose_error_desc(ErrorDesc, Module) ->
+    {none, Module, ErrorDesc}.
+
+new_error_item(Report, Prefix, File, Pos, Module, ErrorDesc) ->
+    if Report  ->
+            io:format("~s:~s~s~s~n",
+                      [File, pos_info(Pos), Prefix,
+                       Module:format_error(ErrorDesc)]);
+       true -> nop
+    end,
+    {File, [{Pos, Module, ErrorDesc}]}.
+
+pos_info(none) -> " ";
+pos_info(Line) when is_integer(Line) ->
+    io_lib:format("~b: ", [Line]);
+pos_info({Line, Col}) when is_integer(Line), is_integer(Col) ->
+    io_lib:format("~b:~b ", [Line, Col]).

+ 3 - 1
src/i18n/blocktrans_extractor.erl

@@ -2,12 +2,14 @@
 
 -export([extract/1]).
 
+-include("include/erlydtl_ext.hrl").
+
 extract(Path) when is_list(Path) ->
     {ok, Contents} = file:read_file(Path),
     extract(Contents);
 
 extract(Contents) when is_binary(Contents) ->
-    case erlydtl_compiler:parse(Contents) of
+    case erlydtl_compiler:do_parse_template(Contents, #dtl_context{}) of
         {ok, ParseTree} ->
             Blocks = process_tree(ParseTree),
             {ok, Blocks};

+ 4 - 2
src/i18n/sources_parser.erl

@@ -7,6 +7,8 @@
 %% Include files
 %%
 
+-include("include/erlydtl_ext.hrl").
+
 -define(bail(Fmt, Args),
         throw(lists:flatten(io_lib:format(Fmt, Args)))).
 
@@ -41,8 +43,8 @@ parse_file(Path) ->
     end.
 
 process_content(Path,Content)->
-    case erlydtl_compiler:parse(Content) of
-        {ok, Data, _} ->
+    case erlydtl_compiler:do_parse_template(Content, #dtl_context{}) of
+        {ok, Data} ->
             {ok, Result} = process_ast(Path, Data),
             Result;
         Error ->

+ 3 - 3
tests/src/erlydtl_extension_test.erl

@@ -23,9 +23,9 @@ parse(State) ->
 
 %% {{ varA or varB }} is equivalent to {% if varA %}{{ varA }}{% else %}{{ varB }}{% endif %}
 compile_ast({value_or, {Value1, Value2}}, Context, TreeWalker) ->
-    {{V1_Ast, V1_Info}, TW1} = erlydtl_compiler:value_ast(Value1, false, false, Context, TreeWalker),
-    {{V2_Ast, V2_Info}, TW2} = erlydtl_compiler:value_ast(Value2, false, false, Context, TW1),
+    {{V1_Ast, V1_Info}, TW1} = erlydtl_beam_compiler:value_ast(Value1, false, false, Context, TreeWalker),
+    {{V2_Ast, V2_Info}, TW2} = erlydtl_beam_compiler:value_ast(Value2, false, false, Context, TW1),
     {{erl_syntax:case_expr(V1_Ast,
                            [erl_syntax:clause([erl_syntax:atom(undefined)], none, [V2_Ast]),
                             erl_syntax:clause([erl_syntax:underscore()], none, [V1_Ast])
-                           ]), erlydtl_compiler:merge_info(V1_Info, V2_Info)}, TW2}.
+                           ]), erlydtl_compiler_utils:merge_info(V1_Info, V2_Info)}, TW2}.

+ 8 - 4
tests/src/erlydtl_functional_tests.erl

@@ -48,7 +48,7 @@ test_list() ->
      "custom_tag2", "custom_tag3", "custom_tag4", "custom_call",
      "include_template", "include_path", "ssi", "extends_path",
      "extends_path2", "trans", "extends2", "extends3",
-     "recursive_block", "extend_recursive_block"
+     "recursive_block", "extend_recursive_block", "missing"
     ].
 
 setup_compile("for_list_preset") ->
@@ -77,12 +77,16 @@ setup_compile("var_preset") ->
     {ok, CompileVars};
 setup_compile("extends2") ->
     File = templates_dir("input/extends2"),
-    Error = {none, erlydtl_compiler, unexpected_extends_tag},
+    Error = {none, erlydtl_beam_compiler, unexpected_extends_tag},
     {{error, [{File, [Error]}], []}, []};
 setup_compile("extends3") ->
     File = templates_dir("input/extends3"),
     Include = templates_dir("input/imaginary"),
-    Error = {none, erlydtl_compiler, {read_file, Include, enoent}},
+    Error = {none, erlydtl_beam_compiler, {read_file, Include, enoent}},
+    {{error, [{File, [Error]}], []}, []};
+setup_compile("missing") ->
+    File = templates_dir("input/missing"),
+    Error = {none, erlydtl_compiler, {read_file, File, enoent}},
     {{error, [{File, [Error]}], []}, []};
 setup_compile(_) ->
     {ok, []}.
@@ -260,7 +264,7 @@ test_compile_render(Name) ->
                        {custom_tags_modules, [erlydtl_custom_tags]}],
             io:format("compiling ... "),
             case erlydtl:compile(File, Module, Options) of
-                {ok, Mod, [{File, [{none,erlydtl_compiler,no_out_dir}]}]} ->
+                {ok, Mod, [{File, [{none,erlydtl_beam_compiler,no_out_dir}]}]} ->
                     if CompileStatus =:= ok -> test_render(Name, Mod);
                        true ->
                             io:format("missing error"),

+ 15 - 12
tests/src/erlydtl_unittests.erl

@@ -284,7 +284,7 @@ tests() ->
                <<"{% for outer in list %}{% for inner in outer %}({{ forloop.parentloop.counter0 }}, {{ forloop.counter0 }})\n{% endfor %}{% endfor %}">>,
                [{'list', [["One", "two"], ["One", "two"]]}], [], [], <<"(0, 0)\n(0, 1)\n(1, 0)\n(1, 1)\n">>,
                %% the warnings we get from the erlang compiler still needs some care..
-               [error_info("erlydtl_running_test", [{0, erl_lint, {unused_var, 'Var_inner/1_1:31'}}, no_out_dir])]},
+               [error_info([{0, erl_lint, {unused_var, 'Var_inner/1_1:31'}}, no_out_dir])]},
               {"If changed",
                <<"{% for x in list %}{% ifchanged %}{{ x }}\n{% endifchanged %}{% endfor %}">>,
                [{'list', ["one", "two", "two", "three", "three", "three"]}], <<"one\ntwo\nthree\n">>},
@@ -1499,21 +1499,24 @@ generate_test_date() ->
 
 
 default_warnings() ->
-    [error_info([no_out_dir])].
+    [error_info([no_out_dir], erlydtl_beam_compiler)].
 
-error_info(File, Ws) ->
-    {File, [error_info(W) || W <- Ws]}.
+error_info(File, Ws, Mod) ->
+    {File, [error_info(W, Mod) || W <- Ws]}.
 
-error_info({Line, ErrorDesc})
+error_info({Line, ErrorDesc}, Mod)
   when is_integer(Line) ->
-  {Line, erlydtl_compiler, ErrorDesc};
-error_info({Line, Module, _}=ErrorDesc)
+  {Line, Mod, ErrorDesc};
+error_info({Line, Module, _}=ErrorDesc, _Mod)
   when is_integer(Line), is_atom(Module) ->
     ErrorDesc;
-error_info({{Line, Col}, Module, _}=ErrorDesc)
+error_info({{Line, Col}, Module, _}=ErrorDesc, _Mod)
   when is_integer(Line), is_integer(Col), is_atom(Module) ->
     ErrorDesc;
-error_info(Ws) when is_list(Ws) ->
-    error_info("erlydtl_running_test", Ws);
-error_info(ErrorDesc) ->
-    {none, erlydtl_compiler, ErrorDesc}.
+error_info(Ws, Mod) when is_list(Ws) ->
+    error_info("erlydtl_running_test", Ws, Mod);
+error_info(ErrorDesc, Mod) ->
+    {none, Mod, ErrorDesc}.
+
+error_info(Ei) ->
+    error_info(Ei, erlydtl_beam_compiler).