Browse Source

Merge branch 'pu-98'

Close #98.
Andreas Stenius 11 years ago
parent
commit
baef038ff9

+ 2 - 1
.travis.yml

@@ -1,6 +1,7 @@
 language: erlang
 language: erlang
 otp_release:
 otp_release:
+#  - R16B03 (not yet available on travis)
   - R16B02
   - R16B02
   - R15B03
   - R15B03
-  - R14B04
+#  - R14B04 (seems lists:concat/1 is broken on R14B04..)
 script: "make test"
 script: "make test"

+ 36 - 34
README.markdown

@@ -27,7 +27,7 @@ in this directory.
 Template compilation
 Template compilation
 --------------------
 --------------------
 
 
-Four ways:
+Usage:
 
 
     erlydtl:compile("/path/to/template.dtl", my_module_name)
     erlydtl:compile("/path/to/template.dtl", my_module_name)
 
 
@@ -37,9 +37,21 @@ Four ways:
 
 
     erlydtl:compile(<<"<html>{{ foo }}</html>">>, my_module_name, Options)
     erlydtl:compile(<<"<html>{{ foo }}</html>">>, my_module_name, Options)
 
 
+Result:
+
+    ok %% existing compiled template is up to date.
+    
+    {ok, Module}
+    {ok, Module, Warnings}
+    {ok, Module, Binary}
+    {ok, Module, Binary, Warnings}
+    
+    error
+    {error, Errors, Warnings}
+    
 Options is a proplist possibly containing:
 Options is a proplist possibly containing:
 
 
-* `outdir` - Directory to store generated .beam files. If not
+* `out_dir` - Directory to store generated .beam files. If not
   specified, no .beam files will be created.
   specified, no .beam files will be created.
 
 
 * `doc_root` - Included template paths will be relative to this
 * `doc_root` - Included template paths will be relative to this
@@ -83,12 +95,6 @@ Options is a proplist possibly containing:
   and returns a binary with the file contents. Defaults to `{file,
   and returns a binary with the file contents. Defaults to `{file,
   read_file}`. Useful for reading templates from a network resource.
   read_file}`. Useful for reading templates from a network resource.
 
 
-* `compiler_options` - Proplist with extra options passed directly to
-  `compiler:forms/2`. This option can be supplied multiple times. Note
-  that the most common options can be given directly with the list of
-  options to the erlydtl compiler (see Erlang Compiler options,
-  below).
-
 * `force_recompile` - Recompile the module even if the source's
 * `force_recompile` - Recompile the module even if the source's
   checksum has not changed. Useful for debugging.
   checksum has not changed. Useful for debugging.
 
 
@@ -111,8 +117,6 @@ Options is a proplist possibly containing:
 * `binary_strings` - Whether to compile strings as binary terms
 * `binary_strings` - Whether to compile strings as binary terms
   (rather than lists). Defaults to `true`.
   (rather than lists). Defaults to `true`.
 
 
-* `verbose` - Enable verbose printing of compilation results.
-
 * `record_info` - List of records to look for when rendering the
 * `record_info` - List of records to look for when rendering the
   template. Each record info is a tuple with the fields of the record:
   template. Each record info is a tuple with the fields of the record:
 
 
@@ -129,30 +133,28 @@ Options is a proplist possibly containing:
   similar use, except that this option does NOT affect whether or not
   similar use, except that this option does NOT affect whether or not
   a .beam file is saved.
   a .beam file is saved.
 
 
-*Erlang Compiler options*
-
-As a convenience, the following options are forwarded to
-`compile:forms/2`, along with those from `compiler_options`:
-
-* `return`
-* `return_warnings`
-* `return_errors`
-* `report`
-* `report_warnings`
-* `report_errors`
-* `warnings_as_errors`
-* `verbose`
-
-_Notice_ that the return value from `erlydtl:compile` is affected by
-the options passed to the compiler. See
-[Erlang compiler documentation](http://www.erlang.org/doc/man/compile.html#forms-2)
-for details. _Exception_ to the rule is that we have reverted the
-`forms` statement that `binary` is treated as implicitly set. That is,
-you need to pass the `binary` option to erlydtl in order to get the
-code in the result tuple.
-
-Default compiler options are `[verbose, report_errors]`, which gives
-either a `{ok, Module}` or `error` as return value.
+* `return` - Short form for both `return_warnings` and `return_errors`.
+
+* `return_warnings` - If this flag is set, then an extra field
+  containing warnings is added to the tuple returned on success.
+
+* `return_errors` - If this flag is set, then an error-tuple with two
+  extra fields containing errors and warnings is returned when there
+  are errors.
+
+* `report` - Short form for both `report_warnings` and `report_errors`.
+
+* `report_warnings` - Print warnings as they occur.
+
+* `report_errors` - Print errors as they occur.
+
+* `warnings_as_errors` - Treat warnings as errors.
+
+* `verbose` - Enable verbose printing of compilation results.
+
+* `compiler_options` - Proplist with extra options passed directly to
+  `compiler:forms/2`. This can prove useful when using extensions to
+  add extra defines etc when compiling the generated code.
 
 
 
 
 Helper compilation
 Helper compilation

+ 12 - 3
include/erlydtl_ext.hrl

@@ -1,4 +1,10 @@
 
 
+-record(error_info, {
+          return = false,
+          report = false,
+          list = []
+         }).
+
 -record(dtl_context, {
 -record(dtl_context, {
           local_scopes = [], 
           local_scopes = [], 
           block_dict = dict:new(), 
           block_dict = dict:new(), 
@@ -13,8 +19,8 @@
           custom_tags_dir = [],
           custom_tags_dir = [],
           custom_tags_modules = [],
           custom_tags_modules = [],
           reader = {file, read_file},
           reader = {file, read_file},
-          module = [],
-          compiler_options = [verbose, report_errors],
+          module = undefined,
+          compiler_options = [],
           binary_strings = true,
           binary_strings = true,
           force_recompile = false,
           force_recompile = false,
           locale = none,
           locale = none,
@@ -23,7 +29,10 @@
           extension_module = undefined,
           extension_module = undefined,
           scanner_module = erlydtl_scanner,
           scanner_module = erlydtl_scanner,
           scanned_tokens = [],
           scanned_tokens = [],
-          all_options = []
+          all_options = [],
+          errors = #error_info{},
+          warnings = #error_info{},
+          bin = undefined
          }).
          }).
 
 
 -record(ast_info, {
 -record(ast_info, {

+ 433 - 202
src/erlydtl_compiler.erl

@@ -43,7 +43,8 @@
 %% --------------------------------------------------------------------
 %% --------------------------------------------------------------------
 %% Definitions
 %% Definitions
 %% --------------------------------------------------------------------
 %% --------------------------------------------------------------------
--export([compile/2, compile/3, compile_dir/2, compile_dir/3, parse/1]).
+-export([compile/2, compile/3, compile_dir/2, compile_dir/3,
+         parse/1, format_error/1]).
 
 
 %% exported for use by extension modules
 %% exported for use by extension modules
 -export([
 -export([
@@ -51,6 +52,7 @@
          format/3,
          format/3,
          value_ast/5,
          value_ast/5,
          resolve_scoped_variable_ast/2,
          resolve_scoped_variable_ast/2,
+         resolve_scoped_variable_ast/3,
          interpret_args/3,
          interpret_args/3,
          unescape_string_literal/1
          unescape_string_literal/1
         ]).
         ]).
@@ -58,37 +60,22 @@
 -include("erlydtl_ext.hrl").
 -include("erlydtl_ext.hrl").
 
 
 compile(FileOrBinary, Module) ->
 compile(FileOrBinary, Module) ->
-    compile(FileOrBinary, Module, []).
+    compile(FileOrBinary, Module, [verbose, report_errors]).
 
 
-compile(Binary, Module, Options0) when is_binary(Binary) ->
-    File = "",
-    Options = [{compiler_options, [{source, "/<text>"}]}
-               |process_opts(Options0)],
-    Context = init_dtl_context(File, Module, Options),
-    case parse(Binary, Context) of
-        up_to_date -> ok;
-        {ok, DjangoParseTree, CheckSum} ->
-            compile_to_binary(File, DjangoParseTree, Context, CheckSum);
-        Other -> Other
-    end;
-
-compile(File, Module, Options0) ->
-    Options = process_opts(Options0),
-    Context = init_dtl_context(File, Module, Options),
-    case parse(File, Context) of
-        up_to_date -> ok;
-        {ok, DjangoParseTree, CheckSum} ->
-            compile_to_binary(File, DjangoParseTree, Context, CheckSum);
-        Other -> Other
-    end.
+compile(Binary, Module, Options) when is_binary(Binary) ->
+    Context = process_opts(undefined, Module, Options),
+    compile(Context#dtl_context{ bin = Binary });
 
 
+compile(File, Module, Options) ->
+    Context = process_opts(File, Module, Options),
+    print("Compile template: ~s~n", [File], Context),
+    compile(Context).
 
 
 compile_dir(Dir, Module) ->
 compile_dir(Dir, Module) ->
-    compile_dir(Dir, Module, []).
+    compile_dir(Dir, Module, [verbose, report_errors]).
 
 
-compile_dir(Dir, Module, Options0) ->
-    Options = process_opts(Options0),
-    Context = init_dtl_context_dir(Dir, Module, Options),
+compile_dir(Dir, Module, Options) ->
+    Context = process_opts(Dir, Module, Options),
     %% Find all files in Dir (recursively), matching the regex (no
     %% Find all files in Dir (recursively), matching the regex (no
     %% files ending in "~").
     %% files ending in "~").
     Files = filelib:fold_files(Dir, ".+[^~]$", true, fun(F1,Acc1) -> [F1 | Acc1] end, []),
     Files = filelib:fold_files(Dir, ".+[^~]$", true, fun(F1,Acc1) -> [F1 | Acc1] end, []),
@@ -122,19 +109,57 @@ compile_dir(Dir, Module, Options0) ->
 parse(Data) ->
 parse(Data) ->
     parse(Data, #dtl_context{}).
     parse(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",
+      [file:format_error(Error)]);
+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]).
+
 
 
 %%====================================================================
 %%====================================================================
 %% Internal functions
 %% Internal functions
 %%====================================================================
 %%====================================================================
 
 
-process_opts(Options) ->
+process_opts(File, Module, Options0) ->
     Options1 = proplists:normalize(
     Options1 = proplists:normalize(
-                 update_defaults(Options),
-                 [{aliases, [{out_dir, outdir}]}
+                 update_defaults(Options0),
+                 [{aliases, [{outdir, out_dir}]}
                  ]),
                  ]),
-    process_opts(Options1, []).
+    Source = case File of
+                 undefined ->
+                     filename:join(
+                       [proplists:get_value(out_dir, Options1, ""),
+                        Module]);
+                 {dir, Dir} -> Dir;
+                 _ -> File
+             end,
+    Options = [{compiler_options, [{source, lists:concat([Source, ".erl"])}]}
+               |compiler_opts(Options1, [])],
+    case File of
+        {dir, Dir1} ->
+            init_context([], Dir1, Module, Options);
+        _ ->
+            init_context([Source], filename:dirname(Source), Module, Options)
+    end.
 
 
-process_opts([CompilerOption|Os], Acc)
+compiler_opts([CompilerOption|Os], Acc)
   when
   when
       CompilerOption =:= return;
       CompilerOption =:= return;
       CompilerOption =:= return_warnings;
       CompilerOption =:= return_warnings;
@@ -144,21 +169,15 @@ process_opts([CompilerOption|Os], Acc)
       CompilerOption =:= report_errors;
       CompilerOption =:= report_errors;
       CompilerOption =:= warnings_as_errors;
       CompilerOption =:= warnings_as_errors;
       CompilerOption =:= verbose;
       CompilerOption =:= verbose;
-      element(1, CompilerOption) =:= outdir ->
-    process_opts(Os, [CompilerOption, {compiler_options, [CompilerOption]}|Acc]);
-process_opts([O|Os], Acc) ->
-    process_opts(Os, [O|Acc]);
-process_opts([], Acc) ->
+      CompilerOption =:= debug_info ->
+    compiler_opts(Os, [CompilerOption, {compiler_options, [CompilerOption]}|Acc]);
+compiler_opts([O|Os], Acc) ->
+    compiler_opts(Os, [O|Acc]);
+compiler_opts([], Acc) ->
     lists:reverse(Acc).
     lists:reverse(Acc).
 
 
 update_defaults(Options) ->
 update_defaults(Options) ->
-    Options1 = maybe_add_env_default_opts(Options),
-    case proplists:get_value(compiler_options, Options1) of
-        undefined ->
-            [{compiler_options, (#dtl_context{})#dtl_context.compiler_options}
-             |Options1];
-        _ -> Options1
-    end.
+    maybe_add_env_default_opts(Options).
 
 
 %% shamelessly borrowed from:
 %% shamelessly borrowed from:
 %% https://github.com/erlang/otp/blob/21095e6830f37676dd29c33a590851ba2c76499b/\
 %% https://github.com/erlang/otp/blob/21095e6830f37676dd29c33a590851ba2c76499b/\
@@ -189,6 +208,56 @@ maybe_add_env_default_opts(Options) ->
         _ -> Options ++ env_default_opts()
         _ -> Options ++ env_default_opts()
     end.
     end.
 
 
+compile(Context) ->
+    Context1 = do_compile(Context),
+    collect_result(Context1).
+
+collect_result(#dtl_context{
+                  module=Module, 
+                  errors=#error_info{ list=[] },
+                  warnings=Ws }=Context) ->
+    Info = case Ws of
+               #error_info{ return=true, list=Warnings } ->
+                   [pack_error_list(Warnings)];
+               _ ->
+                   []
+           end,
+    Res = case proplists:get_bool(binary, Context#dtl_context.all_options) of
+              true ->
+                  [ok, Module, Context#dtl_context.bin | Info];
+              false ->
+                  [ok, Module | Info]
+          end,
+    list_to_tuple(Res);
+collect_result(#dtl_context{ errors=Es, warnings=Ws }) ->
+    if Es#error_info.return ->
+            {error,
+             pack_error_list(Es#error_info.list),
+             case Ws of
+                 #error_info{ list=L } ->
+                     pack_error_list(L);
+                 _ ->
+                     []
+             end};
+       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=Binary }=Context) ->
+    case parse(Binary, 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, Context) ->
 compile_multiple_to_binary(Dir, ParserResults, Context) ->
     MatchAst = options_match_ast(Context),
     MatchAst = options_match_ast(Context),
     {Functions, {AstInfo, _}}
     {Functions, {AstInfo, _}}
@@ -224,91 +293,102 @@ compile_multiple_to_binary(Dir, ParserResults, Context) ->
     Forms = custom_forms(Dir, Context#dtl_context.module, Functions, AstInfo),
     Forms = custom_forms(Dir, Context#dtl_context.module, Functions, AstInfo),
     compile_forms(Forms, Context).
     compile_forms(Forms, Context).
 
 
-compile_to_binary(File, DjangoParseTree, Context, CheckSum) ->
+compile_to_binary(DjangoParseTree, CheckSum, Context) ->
     try body_ast(DjangoParseTree, Context, init_treewalker(Context)) of
     try body_ast(DjangoParseTree, Context, init_treewalker(Context)) of
         {{BodyAst, BodyInfo}, BodyTreeWalker} ->
         {{BodyAst, BodyInfo}, BodyTreeWalker} ->
             try custom_tags_ast(BodyInfo#ast_info.custom_tags, Context, BodyTreeWalker) of
             try custom_tags_ast(BodyInfo#ast_info.custom_tags, Context, BodyTreeWalker) of
                 {{CustomTagsAst, CustomTagsInfo}, _} ->
                 {{CustomTagsAst, CustomTagsInfo}, _} ->
-                    Forms = forms(File, Context#dtl_context.module, {BodyAst, BodyInfo},
-                                  {CustomTagsAst, CustomTagsInfo}, CheckSum, Context, BodyTreeWalker),
+                    Forms = forms(
+                              Context#dtl_context.module,
+                              {BodyAst, BodyInfo},
+                              {CustomTagsAst, CustomTagsInfo},
+                              CheckSum,
+                              BodyTreeWalker,
+                              Context),
                     compile_forms(Forms, Context)
                     compile_forms(Forms, Context)
             catch
             catch
-                throw:Error -> Error
+                throw:Error -> add_error(Error, Context)
             end
             end
     catch
     catch
-        throw:Error -> Error
+        throw:Error -> add_error(Error, Context)
     end.
     end.
 
 
 compile_forms(Forms, Context) ->
 compile_forms(Forms, Context) ->
-    dump_forms(Forms, Context),
+    maybe_debug_template(Forms, Context),
     Options = Context#dtl_context.compiler_options,
     Options = Context#dtl_context.compiler_options,
     case compile:forms(Forms, Options) of
     case compile:forms(Forms, Options) of
         Compiled when element(1, Compiled) =:= ok ->
         Compiled when element(1, Compiled) =:= ok ->
             [ok, Module, Bin|Info] = tuple_to_list(Compiled),
             [ok, Module, Bin|Info] = tuple_to_list(Compiled),
             lists:foldl(
             lists:foldl(
-              fun (F, ok) -> F(Module, Bin, Context);
-                  (_, Res) -> Res
-              end,
-              ok,
-              [fun maybe_write_binary/3,
+              fun (F, C) -> F(Module, Bin, C) end,
+              Context#dtl_context{ bin=Bin },
+              [fun maybe_write/3,
                fun maybe_load/3,
                fun maybe_load/3,
-               fun (_, _, _) ->
-                       case proplists:get_bool(binary, Context#dtl_context.all_options) of
-                           true -> Compiled;
-                           false -> list_to_tuple([ok, Module|Info])
+               fun (_, _, C) ->
+                       case Info of
+                           [Ws] when length(Ws) > 0 ->
+                               add_warnings(Ws, C);
+                           _ -> C
                        end
                        end
                end
                end
               ]);
               ]);
-        Err when Err =:= error; element(1, Err) =:= error ->
-            Err
+        error ->
+            add_error(compile_beam, Context);
+        {error, Es, Ws} ->
+            add_warnings(Ws, add_errors(Es, Context))
     end.
     end.
 
 
-maybe_write_binary(Module, Bin, Context) ->
-    case proplists:get_value(outdir, Context#dtl_context.all_options) of
+maybe_write(Module, Bin, Context) ->
+    case proplists:get_value(out_dir, Context#dtl_context.all_options) of
         undefined ->
         undefined ->
-            print("Template module: ~w not saved (no outdir option)\n", [Module], Context);
+            add_warning(no_out_dir, Context);
         OutDir ->
         OutDir ->
-            BeamFile = filename:join([OutDir, atom_to_list(Module) ++ ".beam"]),
+            BeamFile = filename:join([OutDir, [Module, ".beam"]]),
             print("Template module: ~w -> ~s\n", [Module, BeamFile], Context),
             print("Template module: ~w -> ~s\n", [Module, BeamFile], Context),
             case file:write_file(BeamFile, Bin) of
             case file:write_file(BeamFile, Bin) of
-                ok -> ok;
+                ok -> Context;
                 {error, Reason} ->
                 {error, Reason} ->
-                    {error, lists:flatten(
-                              io_lib:format("Beam generation of '~s' failed: ~p",
-                                            [BeamFile, file:format_error(Reason)]))}
+                    add_error({write_file, Reason}, Context)
             end
             end
     end.
     end.
 
 
 maybe_load(Module, Bin, Context) ->
 maybe_load(Module, Bin, Context) ->
     case proplists:get_bool(no_load, Context#dtl_context.all_options) of
     case proplists:get_bool(no_load, Context#dtl_context.all_options) of
-        true -> ok;
-        false -> load_code(Module, Bin)
+        true -> Context;
+        false -> load_code(Module, Bin, Context)
     end.
     end.
 
 
-load_code(Module, Bin) ->
+load_code(Module, Bin, Context) ->
     code:purge(Module),
     code:purge(Module),
     case code:load_binary(Module, atom_to_list(Module) ++ ".erl", Bin) of
     case code:load_binary(Module, atom_to_list(Module) ++ ".erl", Bin) of
-        {module, Module} -> ok;
-        _ -> {error, lists:concat(["code reload failed: ", Module])}
+        {module, Module} -> Context;
+        Error -> add_warning({load, Error}, Context)
     end.
     end.
 
 
-dump_forms(Forms, Context) ->
-    %% undocumented option to debug the compiled Forms
-    case proplists:get_value(debug_compiler, Context#dtl_context.all_options) of
+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 ->
         true ->
-            io:format("template source:~n~s~n",
-                      [try
-                           erl_prettypr:format(erl_syntax:form_list(Forms))
-                       catch
-                           error:Err ->
-                               io_lib:format("Pretty printing failed: ~p~nContext: ~n~p~nForms: ~n~p~n",
-                                             [Err, Context, Forms])
-                       end
-                      ]);
-        _ -> nop
+            Options = Context#dtl_context.compiler_options,
+            print("Compiler options: ~p~n", [Options], Context),
+            try
+                Source = erl_prettypr:format(erl_syntax:form_list(Forms)),
+                File = proplists:get_value(source, Options),
+                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.
     end.
 
 
-init_context(IsCompilingDir, ParseTrail, DefDir, Module, Options) ->
+init_context(ParseTrail, DefDir, Module, Options) when is_list(Module) ->
+    init_context(ParseTrail, DefDir, list_to_atom(Module), Options);
+init_context(ParseTrail, DefDir, Module, Options) ->
     Ctx = #dtl_context{},
     Ctx = #dtl_context{},
     Context = #dtl_context{
     Context = #dtl_context{
                  all_options = Options,
                  all_options = Options,
@@ -331,27 +411,50 @@ init_context(IsCompilingDir, ParseTrail, DefDir, Module, Options) ->
                  force_recompile = proplists:get_bool(force_recompile, Options),
                  force_recompile = proplists:get_bool(force_recompile, Options),
                  locale = proplists:get_value(locale, Options, Ctx#dtl_context.locale),
                  locale = proplists:get_value(locale, Options, Ctx#dtl_context.locale),
                  verbose = proplists:get_value(verbose, Options, Ctx#dtl_context.verbose),
                  verbose = proplists:get_value(verbose, Options, Ctx#dtl_context.verbose),
-                 is_compiling_dir = IsCompilingDir,
+                 is_compiling_dir = ParseTrail == [],
                  extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module),
                  extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module),
                  scanner_module = proplists:get_value(scanner_module, Options, Ctx#dtl_context.scanner_module),
                  scanner_module = proplists:get_value(scanner_module, Options, Ctx#dtl_context.scanner_module),
                  record_info = [{R, lists:zip(I, lists:seq(2, length(I) + 1))}
                  record_info = [{R, lists:zip(I, lists:seq(2, length(I) + 1))}
-                                || {R, I} <- proplists:get_value(record_info, Options, Ctx#dtl_context.record_info)]
+                                || {R, I} <- proplists:get_value(record_info, Options, Ctx#dtl_context.record_info)],
+                 errors = init_error_info(errors, Ctx#dtl_context.errors, Options),
+                 warnings = init_error_info(warnings, Ctx#dtl_context.warnings, Options)
                 },
                 },
     case call_extension(Context, init_context, [Context]) of
     case call_extension(Context, init_context, [Context]) of
         {ok, C} when is_record(C, dtl_context) -> C;
         {ok, C} when is_record(C, dtl_context) -> C;
         undefined -> Context
         undefined -> Context
     end.
     end.
 
 
-init_dtl_context(File, Module, Options) when is_list(Module) ->
-    init_dtl_context(File, list_to_atom(Module), Options);
-init_dtl_context(File, Module, Options) ->
-    init_context(false, [File], filename:dirname(File), Module, Options).
-
-init_dtl_context_dir(Dir, Module, Options) when is_list(Module) ->
-    init_dtl_context_dir(Dir, list_to_atom(Module), Options);
-init_dtl_context_dir(Dir, Module, Options) ->
-    init_context(true, [], Dir, Module, Options).
-
+init_error_info(warnings, Ei, Options) ->
+    case proplists:get_bool(warnings_as_errors, Options) of
+        true -> warnings_as_errors;
+        false ->
+            init_error_info(get_error_info_opts(warnings, Options), Ei)
+    end;
+init_error_info(Class, Ei, Options) ->
+    init_error_info(get_error_info_opts(Class, Options), Ei).
+
+init_error_info([{return, true}|Flags], #error_info{ return = false }=Ei) ->
+    init_error_info(Flags, Ei#error_info{ return = true });
+init_error_info([{report, true}|Flags], #error_info{ report = false }=Ei) ->
+    init_error_info(Flags, Ei#error_info{ report = true });
+init_error_info([_|Flags], Ei) ->
+    init_error_info(Flags, Ei);
+init_error_info([], Ei) -> Ei.
+
+get_error_info_opts(Class, Options) ->
+    Flags = case Class of
+                errors ->
+                    [return, report, {return_errors, return}, {report_errors, report}];
+                warnings ->
+                    [return, report, {return_warnings, return}, {report_warnings, report}]
+            end,
+    [begin
+         {Key, Value} = if is_atom(Flag) -> {Flag, Flag};
+                           true -> Flag
+                        end,
+         {Value, proplists:get_bool(Key, Options)}
+     end || Flag <- Flags].
+    
 init_treewalker(Context) ->
 init_treewalker(Context) ->
     TreeWalker = #treewalker{},
     TreeWalker = #treewalker{},
     case call_extension(Context, init_treewalker, [TreeWalker]) of
     case call_extension(Context, init_treewalker, [TreeWalker]) of
@@ -408,16 +511,9 @@ parse(File, Context) ->
     {M, F} = Context#dtl_context.reader,
     {M, F} = Context#dtl_context.reader,
     case catch M:F(File) of
     case catch M:F(File) of
         {ok, Data} when is_binary(Data) ->
         {ok, Data} when is_binary(Data) ->
-            case parse(Data, Context) of
-                {error, Msg} when is_list(Msg) ->
-                    {error, File ++ ": " ++ Msg};
-                {error, Msg} ->
-                    {error, {File, [Msg]}};
-                Result ->
-                    Result
-            end;
-        _ ->
-            {error, {File, [{0, Context#dtl_context.module, "Failed to read file"}]}}
+            parse(Data, Context);
+        {error, Reason} ->
+            {read_file, File, Reason}
     end.
     end.
 
 
 do_parse(Data, #dtl_context{ scanner_module=Scanner }=Context) ->
 do_parse(Data, #dtl_context{ scanner_module=Scanner }=Context) ->
@@ -516,22 +612,24 @@ reset_token_stream(Ts, [_|S]) ->
 
 
 
 
 custom_tags_ast(CustomTags, Context, TreeWalker) ->
 custom_tags_ast(CustomTags, Context, TreeWalker) ->
-    {{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} = custom_tags_clauses_ast(CustomTags, Context, TreeWalker),
-    %% This doesn't work since erl_syntax:revert/1 chokes on the airity_qualifier in the -compile attribute..
-    %% bug report sent to the erlang-bugs mailing list.
-    %% {{erl_syntax:form_list(
-    %%     [erl_syntax:attribute(
-    %%        erl_syntax:atom(compile),
-    %%        [erl_syntax:tuple(
-    %%           [erl_syntax:atom(nowarn_unused_function),
-    %%            erl_syntax:arity_qualifier(
-    %%              erl_syntax:atom(render_tag),
-    %%              erl_syntax:integer(3))])]),
-    %%      erl_syntax:function(erl_syntax:atom(render_tag), CustomTagsClauses)]),
-    {{erl_syntax:function(erl_syntax:atom(render_tag), CustomTagsClauses),
-      CustomTagsInfo},
-     TreeWalker1}.
+    %% 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_ast(CustomTags, Context, TreeWalker) ->
     custom_tags_clauses_ast1(CustomTags, [], [], #ast_info{}, Context, TreeWalker).
     custom_tags_clauses_ast1(CustomTags, [], [], #ast_info{}, Context, TreeWalker).
 
 
@@ -680,7 +778,8 @@ custom_forms(Dir, Module, Functions, AstInfo) ->
               | FunctionAsts] ++ AstInfo#ast_info.pre_render_asts
               | FunctionAsts] ++ AstInfo#ast_info.pre_render_asts
     ].
     ].
 
 
-forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum, Context, TreeWalker) ->
+forms(Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum, TreeWalker,
+      #dtl_context{ parse_trail=[File|_] }=Context) ->
     MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
     MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
     Render0FunctionAst = erl_syntax:function(
     Render0FunctionAst = erl_syntax:function(
                            erl_syntax:atom(render),
                            erl_syntax:atom(render),
@@ -695,16 +794,16 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}
     Render1FunctionAst = erl_syntax:function(
     Render1FunctionAst = erl_syntax:function(
                            erl_syntax:atom(render),
                            erl_syntax:atom(render),
                            [erl_syntax:clause(
                            [erl_syntax:clause(
-                              [erl_syntax:variable("_Variables")],
+                              [erl_syntax:variable("Variables")],
                               none,
                               none,
                               [erl_syntax:application(
                               [erl_syntax:application(
                                  none, erl_syntax:atom(render),
                                  none, erl_syntax:atom(render),
-                                 [erl_syntax:variable("_Variables"),
+                                 [erl_syntax:variable("Variables"),
                                   erl_syntax:list([])])
                                   erl_syntax:list([])])
                               ])
                               ])
                            ]),
                            ]),
     Function2 = erl_syntax:application(none, erl_syntax:atom(render_internal),
     Function2 = erl_syntax:application(none, erl_syntax:atom(render_internal),
-                                       [erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")]),
+                                       [erl_syntax:variable("Variables"), erl_syntax:variable("RenderOptions")]),
     ClauseOk = erl_syntax:clause([erl_syntax:variable("Val")],
     ClauseOk = erl_syntax:clause([erl_syntax:variable("Val")],
                                  none,
                                  none,
                                  [erl_syntax:tuple([erl_syntax:atom(ok), erl_syntax:variable("Val")])]),
                                  [erl_syntax:tuple([erl_syntax:atom(ok), erl_syntax:variable("Val")])]),
@@ -714,7 +813,7 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}
     Render2FunctionAst = erl_syntax:function(
     Render2FunctionAst = erl_syntax:function(
                            erl_syntax:atom(render),
                            erl_syntax:atom(render),
                            [erl_syntax:clause(
                            [erl_syntax:clause(
-                              [erl_syntax:variable("_Variables"),
+                              [erl_syntax:variable("Variables"),
                                erl_syntax:variable("RenderOptions")],
                                erl_syntax:variable("RenderOptions")],
                               none,
                               none,
                               [erl_syntax:try_expr([Function2], [ClauseOk], [ClauseCatch])])
                               [erl_syntax:try_expr([Function2], [ClauseOk], [ClauseCatch])])
@@ -804,7 +903,7 @@ body_ast([{'extends', {string_literal, _Pos, String}} | ThisParseTree], Context,
     File = full_path(unescape_string_literal(String), Context#dtl_context.doc_root),
     File = full_path(unescape_string_literal(String), Context#dtl_context.doc_root),
     case lists:member(File, Context#dtl_context.parse_trail) of
     case lists:member(File, Context#dtl_context.parse_trail) of
         true ->
         true ->
-            throw({error, "Circular file inclusion!"});
+            throw(circular_include);
         _ ->
         _ ->
             case parse(File, Context) of
             case parse(File, Context) of
                 {ok, ParentParseTree, CheckSum} ->
                 {ok, ParentParseTree, CheckSum} ->
@@ -932,7 +1031,7 @@ body_ast(DjangoParseTree, Context, TreeWalker) ->
                                        ({'extension', Tag}, TreeWalkerAcc) ->
                                        ({'extension', Tag}, TreeWalkerAcc) ->
                                                        extension_ast(Tag, Context, TreeWalkerAcc);
                                                        extension_ast(Tag, Context, TreeWalkerAcc);
                                        ({'extends', _}, _TreeWalkerAcc) ->
                                        ({'extends', _}, _TreeWalkerAcc) ->
-                                                       throw({error, "The extends tag must be at the very top of the template"});
+                                                       throw(unexpected_extends_tag);
                                        (ValueToken, TreeWalkerAcc) ->
                                        (ValueToken, TreeWalkerAcc) ->
                                                        {{ValueAst,ValueInfo},ValueTreeWalker} = value_ast(ValueToken, true, true, Context, TreeWalkerAcc),
                                                        {{ValueAst,ValueInfo},ValueTreeWalker} = value_ast(ValueToken, true, true, Context, TreeWalkerAcc),
                                                        {{format(ValueAst, Context, ValueTreeWalker),ValueInfo},ValueTreeWalker}
                                                        {{format(ValueAst, Context, ValueTreeWalker),ValueInfo},ValueTreeWalker}
@@ -1006,7 +1105,7 @@ value_ast(ValueToken, AsString, EmptyIfUndefined, Context, TreeWalker) ->
 extension_ast(Tag, Context, TreeWalker) ->
 extension_ast(Tag, Context, TreeWalker) ->
     case call_extension(Context, compile_ast, [Tag, Context, TreeWalker]) of
     case call_extension(Context, compile_ast, [Tag, Context, TreeWalker]) of
         undefined ->
         undefined ->
-            throw({error, {unknown_extension, Tag}});
+            throw({unknown_extension, Tag});
         Result ->
         Result ->
             Result
             Result
     end.
     end.
@@ -1274,7 +1373,7 @@ filter_ast2(Name, Args, #dtl_context{ filter_modules = [Module|Rest] } = Context
             filter_ast2(Name, Args, Context#dtl_context{ filter_modules = Rest })
             filter_ast2(Name, Args, Context#dtl_context{ filter_modules = Rest })
     end;
     end;
 filter_ast2(Name, Args, _) ->
 filter_ast2(Name, Args, _) ->
-    throw({error, {unknown_filter, Name, length(Args)}}).
+    throw({unknown_filter, Name, length(Args)}).
 
 
 search_for_escape_filter(Variable, Filter, #dtl_context{auto_escape = on}) ->
 search_for_escape_filter(Variable, Filter, #dtl_context{auto_escape = on}) ->
     search_for_safe_filter(Variable, Filter);
     search_for_safe_filter(Variable, Filter);
@@ -1372,12 +1471,18 @@ resolve_variable_ast1({variable, {identifier, Pos, VarName}}, Context, TreeWalke
     {{VarValue, #ast_info{ var_names=[VarName] }}, TreeWalker}.
     {{VarValue, #ast_info{ var_names=[VarName] }}, TreeWalker}.
 
 
 resolve_scoped_variable_ast(VarName, Context) ->
 resolve_scoped_variable_ast(VarName, Context) ->
-    lists:foldl(fun(Scope, Value) ->
-                        case Value of
-                            undefined -> proplists:get_value(VarName, Scope);
-                            _ -> Value
-                        end
-                end, undefined, Context#dtl_context.local_scopes).
+    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) ->
 format(Ast, Context, TreeWalker) ->
     auto_escape(format_number_ast(Ast), Context, TreeWalker).
     auto_escape(format_number_ast(Ast), Context, TreeWalker).
@@ -1478,59 +1583,109 @@ to_list_ast(Value, IsReversed, Context, TreeWalker) ->
     end.
     end.
 
 
 for_loop_ast(IteratorList, LoopValue, IsReversed, Contents, {EmptyContentsAst, EmptyContentsInfo}, Context, TreeWalker) ->
 for_loop_ast(IteratorList, LoopValue, IsReversed, Contents, {EmptyContentsAst, EmptyContentsInfo}, Context, TreeWalker) ->
-    Vars = lists:map(fun({identifier, _, Iterator}) ->
-                             erl_syntax:variable(lists:concat(["Var_", Iterator]))
-                     end, IteratorList),
-    {{InnerAst, Info}, TreeWalker1} =
+    %% 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 = lists:concat(["Counters", ForId]),
+    Vars = 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(
         body_ast(
           Contents,
           Contents,
           Context#dtl_context{
           Context#dtl_context{
             local_scopes =
             local_scopes =
-                [[{'forloop', erl_syntax:variable("Counters")}
-                  | lists:map(
-                      fun({identifier, _, Iterator}) ->
-                              {Iterator,
-                               erl_syntax:variable(
-                                 lists:concat(["Var_", Iterator]))}
-                      end, IteratorList)
-                 ]| Context#dtl_context.local_scopes]
+                [[{'forloop', erl_syntax:variable(Counters)} | VarScope]
+                 | Context#dtl_context.local_scopes]
            },
            },
           TreeWalker),
           TreeWalker),
-    CounterAst = erl_syntax:application(erl_syntax:atom(erlydtl_runtime),
-                                        erl_syntax:atom(increment_counter_stats), [erl_syntax:variable("Counters")]),
+
+    CounterAst = erl_syntax:application(
+                   erl_syntax:atom(erlydtl_runtime),
+                   erl_syntax:atom(increment_counter_stats),
+                   [erl_syntax:variable(Counters)]),
 
 
     {{LoopValueAst, LoopValueInfo}, TreeWalker2} = value_ast(LoopValue, false, true, Context, TreeWalker1),
     {{LoopValueAst, LoopValueInfo}, TreeWalker2} = value_ast(LoopValue, false, true, Context, TreeWalker1),
 
 
     LoopValueAst0 = to_list_ast(LoopValueAst, erl_syntax:atom(IsReversed), Context, TreeWalker2),
     LoopValueAst0 = to_list_ast(LoopValueAst, erl_syntax:atom(IsReversed), Context, TreeWalker2),
 
 
-    CounterVars0 = case resolve_scoped_variable_ast('forloop', Context) of
-                       undefined ->
-                           erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(init_counter_stats), [LoopValueAst0]);
-                       Value ->
-                           erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(init_counter_stats), [LoopValueAst0, Value])
-                   end,
+    ParentLoop = resolve_scoped_variable_ast('forloop', Context, erl_syntax:atom(undefined)),
+
+    %% call for loop
     {{erl_syntax:case_expr(
     {{erl_syntax:case_expr(
         erl_syntax:application(
         erl_syntax:application(
           erl_syntax:atom('erlydtl_runtime'), erl_syntax:atom('forloop'),
           erl_syntax:atom('erlydtl_runtime'), erl_syntax:atom('forloop'),
-          [erl_syntax:fun_expr([
-                                erl_syntax:clause([erl_syntax:tuple(Vars), erl_syntax:variable("Counters")], none,
-                                                  [erl_syntax:tuple([InnerAst, CounterAst])]),
-                                erl_syntax:clause(case Vars of [H] -> [H, erl_syntax:variable("Counters")];
-                                                      _ -> [erl_syntax:list(Vars), erl_syntax:variable("Counters")] end, none,
-                                                  [erl_syntax:tuple([InnerAst, CounterAst])])
-                               ]),
-           CounterVars0, LoopValueAst0]),
+          [erl_syntax:fun_expr(
+             [erl_syntax:clause(
+                [erl_syntax:variable(Vars),
+                 erl_syntax:variable(Counters)],
+                none,
+                [erl_syntax:match_expr(
+                   erl_syntax:tuple(IteratorVars),
+                   erl_syntax:if_expr(
+                     [erl_syntax:clause(
+                        [], [erl_syntax:application(none, erl_syntax:atom(is_tuple), [erl_syntax:variable(Vars)]),
+                             erl_syntax:infix_expr(
+                               erl_syntax:application(none, erl_syntax:atom(size), [erl_syntax:variable(Vars)]),
+                               erl_syntax:operator('=='),
+                               erl_syntax:integer(IteratorCount))
+                            ],
+                        [erl_syntax:variable(Vars)])
+                      | if IteratorCount > 1 ->
+                                [erl_syntax:clause(
+                                   [], [erl_syntax:application(none, erl_syntax:atom(is_list), [erl_syntax:variable(Vars)]),
+                                        erl_syntax:infix_expr(
+                                          erl_syntax:application(none, erl_syntax:atom(length), [erl_syntax:variable(Vars)]),
+                                          erl_syntax:operator('=='),
+                                          erl_syntax:integer(IteratorCount))
+                                       ],
+                                   [erl_syntax:application(none, erl_syntax:atom(list_to_tuple), [erl_syntax:variable(Vars)])]),
+                                 erl_syntax:clause(
+                                   [], [erl_syntax:atom(true)],
+                                   [erl_syntax:application(
+                                      none, erl_syntax:atom(throw),
+                                      [erl_syntax:tuple(
+                                         [erl_syntax:atom(for_loop),
+                                          erl_syntax:abstract(Iterators),
+                                          erl_syntax:variable(Vars)])
+                                      ])
+                                   ])
+                                ];
+                           true ->
+                                [erl_syntax:clause(
+                                   [], [erl_syntax:atom(true)],
+                                   [erl_syntax:tuple([erl_syntax:variable(Vars)])])
+                                ]
+                        end
+                     ])),
+                 erl_syntax:tuple([LoopBodyAst, CounterAst])
+                ])
+             ]),
+           LoopValueAst0, ParentLoop
+          ]),
+        %% of
         [erl_syntax:clause(
         [erl_syntax:clause(
-           [erl_syntax:tuple([erl_syntax:underscore(),
-                              erl_syntax:list([erl_syntax:tuple([erl_syntax:atom(counter), erl_syntax:integer(1)])],
-                                              erl_syntax:underscore())])],
-           none, [EmptyContentsAst]),
+           [erl_syntax:atom(empty)],
+           none,
+           [EmptyContentsAst]),
          erl_syntax:clause(
          erl_syntax:clause(
            [erl_syntax:tuple([erl_syntax:variable("L"), erl_syntax:underscore()])],
            [erl_syntax:tuple([erl_syntax:variable("L"), erl_syntax:underscore()])],
-           none, [erl_syntax:variable("L")])]
-       ),
-      merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo)
-     }, TreeWalker2}.
+           none, [erl_syntax:variable("L")])
+        ]),
+      merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo)},
+     TreeWalker2}.
 
 
 ifchanged_values_ast(Values, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, Context, TreeWalker) ->
 ifchanged_values_ast(Values, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, Context, TreeWalker) ->
     Info = merge_info(IfContentsInfo, ElseContentsInfo),
     Info = merge_info(IfContentsInfo, ElseContentsInfo),
@@ -1557,38 +1712,43 @@ ifchanged_contents_ast(Contents, {IfContentsAst, IfContentsInfo}, {ElseContentsA
 
 
 
 
 cycle_ast(Names, Context, TreeWalker) ->
 cycle_ast(Names, Context, TreeWalker) ->
-    {NamesTuple, VarNames} = lists:mapfoldl(fun
-                                                ({string_literal, _, Str}, VarNamesAcc) ->
-                                                   {{S, _}, _} = string_ast(unescape_string_literal(Str), Context, TreeWalker),
-                                                   {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),
+    {NamesTuple, VarNames}
+        = lists:mapfoldl(
+            fun ({string_literal, _, Str}, VarNamesAcc) ->
+                    {{S, _}, _} = string_ast(unescape_string_literal(Str), Context, TreeWalker),
+                    {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),
     {{erl_syntax:application(
     {{erl_syntax:application(
         erl_syntax:atom('erlydtl_runtime'), erl_syntax:atom('cycle'),
         erl_syntax:atom('erlydtl_runtime'), erl_syntax:atom('cycle'),
-        [erl_syntax:tuple(NamesTuple), erl_syntax:variable("Counters")]), #ast_info{ var_names = VarNames }}, TreeWalker}.
+        [erl_syntax:tuple(NamesTuple), resolve_scoped_variable_ast('forloop', Context)]),
+      #ast_info{ var_names = VarNames }},
+     TreeWalker}.
 
 
 %% Older Django templates treat cycle with comma-delimited elements as strings
 %% Older Django templates treat cycle with comma-delimited elements as strings
 cycle_compat_ast(Names, Context, TreeWalker) ->
 cycle_compat_ast(Names, Context, TreeWalker) ->
-    NamesTuple = lists:map(fun
-                               ({identifier, _, X}) ->
-                                  {{S, _}, _} = string_ast(X, Context, TreeWalker),
-                                  S
-                          end, Names),
+    NamesTuple = lists:map(
+                   fun ({identifier, _, X}) ->
+                           {{S, _}, _} = string_ast(X, Context, TreeWalker),
+                           S
+                   end, Names),
     {{erl_syntax:application(
     {{erl_syntax:application(
         erl_syntax:atom('erlydtl_runtime'), erl_syntax:atom('cycle'),
         erl_syntax:atom('erlydtl_runtime'), erl_syntax:atom('cycle'),
-        [erl_syntax:tuple(NamesTuple), erl_syntax:variable("Counters")]), #ast_info{}}, TreeWalker}.
+        [erl_syntax:tuple(NamesTuple), resolve_scoped_variable_ast('forloop', Context)]),
+      #ast_info{}},
+     TreeWalker}.
 
 
 now_ast(FormatString, Context, 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"
+    %% 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),
     UnescapeOuter = string:strip(FormatString, both, 34),
     {{StringAst, Info}, TreeWalker1} = string_ast(UnescapeOuter, Context, TreeWalker),
     {{StringAst, Info}, TreeWalker1} = string_ast(UnescapeOuter, Context, TreeWalker),
     {{erl_syntax:application(
     {{erl_syntax:application(
@@ -1677,9 +1837,6 @@ custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules
                                     Context#dtl_context{ custom_tags_modules = Rest })
                                     Context#dtl_context{ custom_tags_modules = Rest })
     end.
     end.
 
 
-print(Fmt, Args, #dtl_context{ verbose = true }) -> io:format(Fmt, Args);
-print(_Fmt, _Args, _Context) -> ok.
-
 call_ast(Module, TreeWalkerAcc) ->
 call_ast(Module, TreeWalkerAcc) ->
     call_ast(Module, erl_syntax:variable("_Variables"), #ast_info{}, TreeWalkerAcc).
     call_ast(Module, erl_syntax:variable("_Variables"), #ast_info{}, TreeWalkerAcc).
 
 
@@ -1708,3 +1865,77 @@ call_ast(Module, Variable, AstInfo, TreeWalker) ->
                  [ErrStrAst]),
                  [ErrStrAst]),
     CallAst = erl_syntax:case_expr(AppAst, [OkAst, ErrorAst]),
     CallAst = erl_syntax:case_expr(AppAst, [OkAst, ErrorAst]),
     with_dependencies(Module:dependencies(), {{CallAst, AstInfo}, TreeWalker}).
     with_dependencies(Module:dependencies(), {{CallAst, AstInfo}, TreeWalker}).
+
+
+print(Fmt, Args, #dtl_context{ verbose = true }) -> io:format(Fmt, Args);
+print(_Fmt, _Args, _Context) -> ok.
+
+
+add_error(Error, #dtl_context{
+                    errors=#error_info{ report=Report, list=Es }=Ei,
+                    parse_trail=[File|_] }=Context) ->
+    Item = get_error_item(Report, "", File, 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,
+                        parse_trail=[File|_] }=Context) ->
+    Item = get_error_item(Report, "Warning: ", File, 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 Error of
+        {Line, ErrorDesc}
+          when is_integer(Line) ->
+            new_error_item(Report, Prefix, File, Line, ?MODULE, ErrorDesc);
+        {Line, Module, ErrorDesc}
+          when is_integer(Line), is_atom(Module) ->
+            new_error_item(Report, Prefix, File, Line, Module, ErrorDesc);
+        {_, InfoList} when is_list(InfoList) -> Error;
+        ErrorDesc ->
+            new_error_item(Report, Prefix, File, none, ?MODULE, ErrorDesc)
+    end.
+    
+new_error_item(Report, Prefix, File, Line, Module, ErrorDesc) ->
+    if Report  ->
+            io:format("~s:~s~s~s~n",
+                      [File, line_info(Line), Prefix,
+                       Module:format_error(ErrorDesc)]);
+       true -> nop
+    end,
+    {File, [{Line, Module, ErrorDesc}]}.
+
+line_info(none) -> " ";
+line_info(Line) when is_integer(Line) ->
+    io_lib:format("~b: ", [Line]).
+
+pack_error_list(Es) ->
+    collect_error_info([], Es, []).
+
+collect_error_info([], [], Acc) ->
+    lists:reverse(Acc);
+collect_error_info([{File, ErrorInfo}|Es], Rest, [{File, FEs}|Acc]) ->
+    collect_error_info(Es, Rest, [{File, ErrorInfo ++ FEs}|Acc]);
+collect_error_info([E|Es], Rest, Acc) ->
+    collect_error_info(Es, [E|Rest], Acc);
+collect_error_info([], Rest, Acc) ->
+    case lists:reverse(Rest) of
+        [E|Es] ->
+            collect_error_info(Es, [], [E|Acc])
+    end.

+ 7 - 5
src/erlydtl_runtime.erl

@@ -238,12 +238,13 @@ init_counter_stats(List) ->
     init_counter_stats(List, undefined).
     init_counter_stats(List, undefined).
 
 
 init_counter_stats(List, Parent) when is_list(List) ->
 init_counter_stats(List, Parent) when is_list(List) ->
+    ListLen = length(List),
     [{counter, 1},
     [{counter, 1},
      {counter0, 0},
      {counter0, 0},
-     {revcounter, length(List)},
-     {revcounter0, length(List) - 1},
+     {revcounter, ListLen},
+     {revcounter0, ListLen - 1},
      {first, true},
      {first, true},
-     {last, length(List) =:= 1},
+     {last, ListLen =:= 1},
      {parentloop, Parent}].
      {parentloop, Parent}].
 
 
 increment_counter_stats([{counter, Counter}, {counter0, Counter0}, {revcounter, RevCounter},
 increment_counter_stats([{counter, Counter}, {counter0, Counter0}, {revcounter, RevCounter},
@@ -255,9 +256,10 @@ increment_counter_stats([{counter, Counter}, {counter0, Counter0}, {revcounter,
      {first, false}, {last, RevCounter0 =:= 1},
      {first, false}, {last, RevCounter0 =:= 1},
      {parentloop, Parent}].
      {parentloop, Parent}].
 
 
-forloop(Fun, Acc0, Values) ->
+forloop(_Fun, [], _Parent) -> empty;
+forloop(Fun, Values, Parent) ->
     push_ifchanged_context(),
     push_ifchanged_context(),
-    Result = lists:mapfoldl(Fun, Acc0, Values),
+    Result = lists:mapfoldl(Fun, init_counter_stats(Values, Parent), Values),
     pop_ifchanged_context(),
     pop_ifchanged_context(),
     Result.
     Result.
 
 

+ 11 - 6
tests/src/erlydtl_functional_tests.erl

@@ -76,11 +76,14 @@ setup_compile("var_preset") ->
     CompileVars = [{preset_var1, "preset-var1"}, {preset_var2, "preset-var2"}],
     CompileVars = [{preset_var1, "preset-var1"}, {preset_var2, "preset-var2"}],
     {ok, CompileVars};
     {ok, CompileVars};
 setup_compile("extends2") ->
 setup_compile("extends2") ->
-    {{error, "The extends tag must be at the very top of the template"}, []};
+    File = templates_dir("input/extends2"),
+    Error = {none, erlydtl_compiler, unexpected_extends_tag},
+    {{error, [{File, [Error]}], []}, []};
 setup_compile("extends3") ->
 setup_compile("extends3") ->
-    File = templates_dir("input/imaginary"),
-    Error = {0, functional_test_extends3, "Failed to read file"},
-    {{error, {File, [Error]}}, []}; %% Huh?! what kind of error message is that!?
+    File = templates_dir("input/extends3"),
+    Include = templates_dir("input/imaginary"),
+    Error = {none, erlydtl_compiler, {read_file, Include, enoent}},
+    {{error, [{File, [Error]}], []}, []};
 setup_compile(_) ->
 setup_compile(_) ->
     {ok, []}.
     {ok, []}.
 
 
@@ -249,17 +252,19 @@ test_compile_render(Name) ->
             Options = [
             Options = [
                        {vars, CompileVars},
                        {vars, CompileVars},
                        force_recompile,
                        force_recompile,
+                       return_errors,
+                       return_warnings,
                        %% debug_compiler,
                        %% debug_compiler,
                        {custom_tags_modules, [erlydtl_custom_tags]}],
                        {custom_tags_modules, [erlydtl_custom_tags]}],
             io:format("compiling ... "),
             io:format("compiling ... "),
             case erlydtl:compile(File, Module, Options) of
             case erlydtl:compile(File, Module, Options) of
-                {ok, Mod} ->
+                {ok, Mod, [{File, [{none,erlydtl_compiler,no_out_dir}]}]} ->
                     if CompileStatus =:= ok -> test_render(Name, Mod);
                     if CompileStatus =:= ok -> test_render(Name, Mod);
                        true ->
                        true ->
                             io:format("missing error"),
                             io:format("missing error"),
                             {error, "compiling should have failed :" ++ File}
                             {error, "compiling should have failed :" ++ File}
                     end;
                     end;
-                {error, _}=Err ->
+                {error, _, _}=Err ->
                     if CompileStatus =:= Err -> io:format("ok");
                     if CompileStatus =:= Err -> io:format("ok");
                        true ->
                        true ->
                             io:format("failed"),
                             io:format("failed"),

+ 46 - 6
tests/src/erlydtl_unittests.erl

@@ -275,7 +275,9 @@ tests() ->
                <<"Al\nAlbert\nJo\nJoseph\n">>},
                <<"Al\nAlbert\nJo\nJoseph\n">>},
               {"Access parent loop counters",
               {"Access parent loop counters",
                <<"{% for outer in list %}{% for inner in outer %}({{ forloop.parentloop.counter0 }}, {{ forloop.counter0 }})\n{% endfor %}{% endfor %}">>,
                <<"{% 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">>},
+               [{'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([no_out_dir]), {"/erlydtl_running_test.erl", [{0, erl_lint, {unused_var, 'Var_inner/1_1:31'}}]}]},
               {"If changed",
               {"If changed",
                <<"{% for x in list %}{% ifchanged %}{{ x }}\n{% endifchanged %}{% endfor %}">>,
                <<"{% for x in list %}{% ifchanged %}{{ x }}\n{% endifchanged %}{% endfor %}">>,
                [{'list', ["one", "two", "two", "three", "three", "three"]}], <<"one\ntwo\nthree\n">>},
                [{'list', ["one", "two", "two", "three", "three", "three"]}], <<"one\ntwo\nthree\n">>},
@@ -1220,7 +1222,7 @@ tests() ->
          [{extension_module, erlydtl_extension_test}], <<"ok">>},
          [{extension_module, erlydtl_extension_test}], <<"ok">>},
         {"proper error message", <<"{{ bar # }}">>, [{bar, "ok"}], [],
         {"proper error message", <<"{{ bar # }}">>, [{bar, "ok"}], [],
          [{extension_module, erlydtl_extension_test}],
          [{extension_module, erlydtl_extension_test}],
-         {error, {1,erlydtl_extension_test,"Unexpected '#' in code at column 8"}}},
+         {error, [error_info([{1,erlydtl_extension_test,"Unexpected '#' in code at column 8"}])], []}},
         %% accept identifiers as expressions (this is a dummy functionality to test the parser extensibility)
         %% accept identifiers as expressions (this is a dummy functionality to test the parser extensibility)
         {"identifiers as expressions", <<"{{ foo.bar or baz }}">>, [{baz, "ok"}], [],
         {"identifiers as expressions", <<"{{ foo.bar or baz }}">>, [{baz, "ok"}], [],
          [{extension_module, erlydtl_extension_test}], <<"ok">>}
          [{extension_module, erlydtl_extension_test}], <<"ok">>}
@@ -1230,9 +1232,20 @@ tests() ->
         <<"{{ r.baz }}">>, [{r, #testrec{ foo="Foo", bar="Bar", baz="Baz" }}], [],
         <<"{{ r.baz }}">>, [{r, #testrec{ foo="Foo", bar="Bar", baz="Baz" }}], [],
         [{record_info, [{testrec, record_info(fields, testrec)}]}],
         [{record_info, [{testrec, record_info(fields, testrec)}]}],
         <<"Baz">>}
         <<"Baz">>}
+      ]},
+     {"error reporting",
+      [{"no out dir warning",
+        <<"foo bar">>, [], [], [], <<"foo bar">>, [error_info([no_out_dir])]},
+       {"warnings as errors",
+        <<"foo bar">>, [], [], [warnings_as_errors], {error, [error_info([no_out_dir])], []}}
       ]}
       ]}
     ].
     ].
 
 
+%% {Name, DTL, Vars, Output}
+%% {Name, DTL, Vars, RenderOpts, Output}
+%% {Name, DTL, Vars, RenderOpts, CompilerOpts, Output}
+%% {Name, DTL, Vars, RenderOpts, CompilerOpts, Output, Warnings}
+
 run_tests() ->
 run_tests() ->
     io:format("Running unit tests..."),
     io:format("Running unit tests..."),
     {Times, Failures} =
     {Times, Failures} =
@@ -1304,6 +1317,7 @@ format_error(Name, Class, Error) ->
 
 
 compile_test(DTL, Opts) ->
 compile_test(DTL, Opts) ->
     Options = [force_recompile,
     Options = [force_recompile,
+               return_errors, return_warnings,
                {custom_filters_modules, [erlydtl_contrib_humanize]}
                {custom_filters_modules, [erlydtl_contrib_humanize]}
                |Opts],
                |Opts],
     timer:tc(erlydtl, compile, [DTL, erlydtl_running_test, Options]).
     timer:tc(erlydtl, compile, [DTL, erlydtl_running_test, Options]).
@@ -1318,12 +1332,14 @@ test_fail(Name, Fmt, Args, T) ->
     {T, {Name, io_lib:format(Fmt, Args)}}.
     {T, {Name, io_lib:format(Fmt, Args)}}.
 
 
 process_unit_test({Name, DTL, Vars, Output}) ->
 process_unit_test({Name, DTL, Vars, Output}) ->
-    process_unit_test({Name, DTL, Vars, [], [], Output});
+    process_unit_test({Name, DTL, Vars, [], [], Output, default_warnings()});
 process_unit_test({Name, DTL, Vars, RenderOpts, Output}) ->
 process_unit_test({Name, DTL, Vars, RenderOpts, Output}) ->
-    process_unit_test({Name, DTL, Vars, RenderOpts, [], Output});
+    process_unit_test({Name, DTL, Vars, RenderOpts, [], Output, default_warnings()});
 process_unit_test({Name, DTL, Vars, RenderOpts, CompilerOpts, Output}) ->
 process_unit_test({Name, DTL, Vars, RenderOpts, CompilerOpts, Output}) ->
+    process_unit_test({Name, DTL, Vars, RenderOpts, CompilerOpts, Output, default_warnings()});
+process_unit_test({Name, DTL, Vars, RenderOpts, CompilerOpts, Output, Warnings}) ->
     case compile_test(DTL, CompilerOpts) of
     case compile_test(DTL, CompilerOpts) of
-        {Tcompile, {ok, _}} ->
+        {Tcompile, {ok, _, Warnings}} ->
             Tc = [{compile, Tcompile}],
             Tc = [{compile, Tcompile}],
             case render_test(Vars, RenderOpts) of
             case render_test(Vars, RenderOpts) of
                 {TrenderL, {ok, IOList}} ->
                 {TrenderL, {ok, IOList}} ->
@@ -1361,7 +1377,7 @@ process_unit_test({Name, DTL, Vars, RenderOpts, CompilerOpts, Output}) ->
                             test_pass([{render_binary, TrenderB}|TrL]);
                             test_pass([{render_binary, TrenderB}|TrL]);
                         {TrenderB, Err} ->
                         {TrenderB, Err} ->
                             test_fail(Name, "Render error (with binary variables): ~p", [Err],
                             test_fail(Name, "Render error (with binary variables): ~p", [Err],
-                                     [{render_binary, TrenderB}|TrL])
+                                      [{render_binary, TrenderB}|TrL])
                     end;
                     end;
                 {TrenderL, Output} ->
                 {TrenderL, Output} ->
                     test_pass([{render_list, TrenderL}|Tc]);
                     test_pass([{render_list, TrenderL}|Tc]);
@@ -1369,6 +1385,12 @@ process_unit_test({Name, DTL, Vars, RenderOpts, CompilerOpts, Output}) ->
                     test_fail(Name, "Render error (with list variables): ~p", [Err],
                     test_fail(Name, "Render error (with list variables): ~p", [Err],
                               [{render_list, TrenderL}|Tc])
                               [{render_list, TrenderL}|Tc])
             end;
             end;
+        {Tcompile, {ok, _, ActualWarnings}} ->
+            test_fail(
+              Name,
+              "Unexpected warnings: ~p~n"
+              "Expected: ~p",
+              [ActualWarnings, Warnings], [{compile, Tcompile}]);
         {Tcompile, Output} -> test_pass([{compile, Tcompile}]);
         {Tcompile, Output} -> test_pass([{compile, Tcompile}]);
         {Tcompile, Err} ->
         {Tcompile, Err} ->
             test_fail(Name, "Compile error: ~p", [Err], [{compile, Tcompile}])
             test_fail(Name, "Compile error: ~p", [Err], [{compile, Tcompile}])
@@ -1407,3 +1429,21 @@ generate_test_date() ->
                     " of ", lists:nth(M, MonthName),
                     " of ", lists:nth(M, MonthName),
                     " ", integer_to_list(Y), "."
                     " ", integer_to_list(Y), "."
                    ]).
                    ]).
+
+
+default_warnings() ->
+    [error_info([no_out_dir])].
+
+error_info(File, Ws) ->
+    {File, [error_info(W) || W <- Ws]}.
+
+error_info({Line, ErrorDesc})
+  when is_integer(Line) ->
+  {Line, erlydtl_compiler, ErrorDesc};
+error_info({Line, Module, ErrorDesc})
+  when is_integer(Line), is_atom(Module) ->
+    {Line, Module, ErrorDesc};
+error_info(Ws) when is_list(Ws) ->
+    error_info("/erlydtl_running_test", Ws);
+error_info(ErrorDesc) ->
+    {none, erlydtl_compiler, ErrorDesc}.