Browse Source

Fix #61: add compile options default_vars and constants

vars is deprecated (aliased to default_vars).

The `forloop` variable is now marked as a reserved internal variable name,
and can't be used default/constant variable.
Andreas Stenius 11 years ago
parent
commit
88d3fd414e

+ 4 - 0
NEWS.md

@@ -6,3 +6,7 @@ Standards](http://www.gnu.org/prep/standards/html_node/NEWS-File.html#NEWS-File)
 
 ## master (upcoming release)
 
+* Fix compile time variables and constants (#61)
+
+* The `vars` compile time option has been deprecated in favor of
+  `default_vars`.

+ 34 - 3
README.markdown

@@ -109,6 +109,10 @@ Options is a proplist possibly containing:
   `compiler:forms/2`. This can prove useful when using extensions to
   add extra defines etc when compiling the generated code.
 
+* `constants` - Replace template variables with a constant value when
+  compiling the template. This can _not_ be overridden when rendering
+  the template. See also `default_vars`.
+
 * `custom_filters_modules` **deprecated** - A list of modules to be
   used for handling custom filters. The modules will be searched in
   order and take precedence over the built-in filters. Each custom
@@ -165,6 +169,11 @@ Options is a proplist possibly containing:
   by name (when there is a name to module mapping also provided in the
   `libraries` option) or by module.
 
+* `default_vars` - Provide default values for variables. Any value
+  from the render variables takes precedence. Notice: in case the
+  value is a `fun/0`, it will be called at compile time. See also
+  `constants`.
+
 * `doc_root` - Included template paths will be relative to this
   directory; defaults to the compiled template's directory.
 
@@ -228,9 +237,10 @@ Options is a proplist possibly containing:
   `tuples_0_based`. See also `lists_0_based`.
 
 
-* `vars` - Variables (and their values) to evaluate at compile-time
-  rather than render-time. (Currently not strictly true, see
-  [#61](https://github.com/erlydtl/erlydtl/issues/61))
+* `vars` **deprecated** - Use `default_vars` instead. Variables (and
+  their values) to evaluate at compile-time rather than
+  render-time.
+
 
 * `verbose` - Enable verbose printing of compilation progress. Add
   several for even more verbose (e.g. debug) output.
@@ -364,6 +374,27 @@ can be used for determining which variable bindings need to be passed
 to the `render/3` function.
 
 
+### default_variables/0
+
+```erlang
+my_compiled_template:default_variables() -> [Variable::atom()]
+```
+
+Like `variables/0`, but for any variable which have a default value
+provided at compile time.
+
+
+### constants/0
+
+```erlang
+my_compiled_template:constants() -> [Variable::atom()]
+```
+
+Like `default_variables/0`, but these template variables has been
+replaced with a fixed value at compile time and can not be changed
+when rendering the template.
+
+
 Custom tags and filters
 -----------------------
 

+ 4 - 0
include/erlydtl_ext.hrl

@@ -14,6 +14,7 @@
           doc_root = "", 
           parse_trail = [],
           vars = [],
+          const = [],
           record_info = [],
           filters = [],
           tags = [],
@@ -37,12 +38,15 @@
           tuples_0_based = false
          }).
 
+%% ALL fields of ast_info{} must be lists (see erlydtl_compiler_utils:merge_info/2)
 -record(ast_info, {
           dependencies = [],
           translatable_strings = [],
           translated_blocks= [],
           custom_tags = [],
           var_names = [],
+          def_names = [],
+          const_names = [],
           pre_render_asts = []}).
 
 -record(treewalker, {

+ 189 - 181
src/erlydtl_beam_compiler.erl

@@ -102,6 +102,8 @@ format_error({bad_tag, Name, {Mod, Fun}, Arity}) ->
     io_lib:format("Invalid tag '~p' (~p:~p/~p)", [Name, Mod, Fun, Arity]);
 format_error({load_code, Error}) ->
     io_lib:format("Failed to load BEAM code: ~p", [Error]);
+format_error({reserved_variable, ReservedName}) ->
+    io_lib:format("Variable '~s' is reserved for internal use.", [ReservedName]);
 format_error(Error) ->
     erlydtl_compiler:format_error(Error).
 
@@ -479,14 +481,24 @@ forms({BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum,
     TranslatableStrings = MergedInfo#ast_info.translatable_strings,
     TranslatedBlocks = MergedInfo#ast_info.translated_blocks,
     Variables = lists:usort(MergedInfo#ast_info.var_names),
-
+    DefaultVariables = lists:usort(MergedInfo#ast_info.def_names),
+    Constants = lists:usort(MergedInfo#ast_info.const_names),
     FinalBodyAst = options_match_ast(TreeWalker) ++ stringify(BodyAst, Context),
 
     erl_syntax:revert_forms(
       erl_syntax:form_list(
         ?Q(["-module('@Module@').",
             "-export([render/0, render/1, render/2, source/0, dependencies/0,",
-            "         translatable_strings/0, translated_blocks/0, variables/0]).",
+            "         translatable_strings/0, translated_blocks/0, variables/0,",
+            "         default_variables/0, constants/0]).",
+            "source() -> {_@File@, _@CheckSum@}.",
+            "dependencies() -> _@Dependencies@.",
+            "variables() -> _@Variables@.",
+            "default_variables() -> _@DefaultVariables@.",
+            "constants() -> _@Constants@.",
+            "translatable_strings() -> _@TranslatableStrings@.",
+            "translated_blocks() -> _@TranslatedBlocks@.",
+            "'@_CustomTagsFunctionAst'() -> _.",
             "render() -> render([], []).",
             "render(Variables) -> render(Variables, []).",
             "render(Variables, RenderOptions) ->",
@@ -495,13 +507,7 @@ forms({BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum,
             "  catch",
             "    Err -> {error, Err}",
             "  end.",
-            "source() -> {_@File@, _@CheckSum@}.",
-            "dependencies() -> _@Dependencies@.",
-            "translatable_strings() -> _@TranslatableStrings@.",
-            "translated_blocks() -> _@TranslatedBlocks@.",
-            "variables() -> _@Variables@.",
-            "render_internal(_Variables, RenderOptions) -> _@FinalBodyAst.",
-            "'@_CustomTagsFunctionAst'() -> _."
+            "render_internal(_Variables, RenderOptions) -> _@FinalBodyAst."
            ])
        )).
 
@@ -560,169 +566,145 @@ body_ast(DjangoParseTree, TreeWalker) ->
 
 body_ast(DjangoParseTree, BodyScope, TreeWalker) ->
     {ScopeId, TreeWalkerScope} = begin_scope(BodyScope, TreeWalker),
-    {AstInfoList, TreeWalker1} =
-        lists:mapfoldl(
-          fun ({'autoescape', {identifier, _, OnOrOff}, Contents}, TW) ->
-                  {Info, BodyTW} = body_ast(Contents, push_auto_escape(OnOrOff, TW)),
-                  {Info, pop_auto_escape(BodyTW)};
-              ({'block', {identifier, Pos, Name}, Contents}, #treewalker{ context=Context }=TW) ->
-                  {Block, BlockScope} =
-                      case dict:find(Name, Context#dtl_context.block_dict) of
-                          {ok, ChildBlock} ->
-                              {{ContentsAst, _ContentsInfo}, _ContentsTW} = body_ast(Contents, TW),
-                              {ChildBlock,
-                               create_scope(
-                                 [{block, ?Q("[{super, _@ContentsAst}]")}],
-                                 Pos, TW)
-                              };
-                          _ ->
-                              {Contents, empty_scope()}
-                      end,
-                  body_ast(Block, BlockScope, TW);
-              ({'blocktrans', Args, Contents}, TW) ->
-                  blocktrans_ast(Args, Contents, TW);
-              ({'call', {identifier, _, Name}}, TW) ->
-                  call_ast(Name, TW);
-              ({'call', {identifier, _, Name}, With}, TW) ->
-                  call_with_ast(Name, With, TW);
-              ({'comment', _Contents}, TW) ->
-                  empty_ast(TW);
-              ({'cycle', Names}, TW) ->
-                  cycle_ast(Names, TW);
-              ({'cycle_compat', Names}, TW) ->
-                  cycle_compat_ast(Names, TW);
-              ({'date', 'now', {string_literal, _Pos, FormatString}}, TW) ->
-                  now_ast(FormatString, TW);
-              ({'filter', FilterList, Contents}, TW) ->
-                  filter_tag_ast(FilterList, Contents, TW);
-              ({'firstof', Vars}, TW) ->
-                  firstof_ast(Vars, TW);
-              ({'for', {'in', IteratorList, Variable, Reversed}, Contents}, TW) ->
-                  {EmptyAstInfo, TW1} = empty_ast(TW),
-                  for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
-              ({'for', {'in', IteratorList, Variable, Reversed}, Contents, EmptyPartContents}, TW) ->
-                  {EmptyAstInfo, TW1} = body_ast(EmptyPartContents, TW),
-                  for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
-              ({'if', Expression, Contents, Elif}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  {ElifAstInfo, TW2} = body_ast(Elif, TW1),
-                  ifelse_ast(Expression, IfAstInfo, ElifAstInfo, TW2);
-              ({'if', Expression, Contents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
-                  ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifchanged', '$undefined', Contents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
-                  ifchanged_contents_ast(Contents, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifchanged', Values, Contents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
-                  ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifchangedelse', '$undefined', IfContents, ElseContents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
-                  ifchanged_contents_ast(IfContents, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifchangedelse', Values, IfContents, ElseContents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
-                  ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifelse', Expression, IfContents, ElseContents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
-                  ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifequal', [Arg1, Arg2], Contents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
-                  ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  {ElseAstInfo, TW2} = body_ast(ElseContents,TW1),
-                  ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifnotequal', [Arg1, Arg2], Contents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
-                  ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
-                  ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
-              ({'include', {string_literal, _, File}, Args}, #treewalker{ context=Context }=TW) ->
-                  include_ast(unescape_string_literal(File), Args, Context#dtl_context.local_scopes, TW);
-              ({'include_only', {string_literal, _, File}, Args}, TW) ->
-                  {Info, IncTW} = include_ast(unescape_string_literal(File), Args, [], TW),
-                  {Info, restore_scope(TW, IncTW)};
-              ({'load_libs', Libs}, TW) ->
-                  load_libs_ast(Libs, TW);
-              ({'load_from_lib', What, Lib}, TW) ->
-                  load_from_lib_ast(What, Lib, TW);
-              ({'regroup', {ListVariable, Grouper, {identifier, _, NewVariable}}}, TW) ->
-                  regroup_ast(ListVariable, Grouper, NewVariable, TW);
-              ('end_regroup', TW) ->
-                  {{end_scope, #ast_info{}}, TW};
-              ({'spaceless', Contents}, TW) ->
-                  spaceless_ast(Contents, TW);
-              ({'ssi', Arg}, TW) ->
-                  ssi_ast(Arg, TW);
-              ({'ssi_parsed', {string_literal, _, FileName}}, #treewalker{ context=Context }=TW) ->
-                  include_ast(unescape_string_literal(FileName), [], Context#dtl_context.local_scopes, TW);
-              ({'string', _Pos, String}, TW) ->
-                  string_ast(String, TW);
-              ({'tag', Name, Args}, TW) ->
-                  tag_ast(Name, Args, TW);
-              ({'templatetag', {_, _, TagName}}, TW) ->
-                  templatetag_ast(TagName, TW);
-              ({'trans', Value}, TW) ->
-                  translated_ast(Value, TW);
-              ({'widthratio', Numerator, Denominator, Scale}, TW) ->
-                  widthratio_ast(Numerator, Denominator, Scale, TW);
-              ({'with', Args, Contents}, TW) ->
-                  with_ast(Args, Contents, TW);
-              ({'scope_as', {identifier, _, Name}, Contents}, TW) ->
-                  scope_as(Name, Contents, TW);
-              ({'extension', Tag}, TW) ->
-                  extension_ast(Tag, TW);
-              ({'extends', _}, TW) ->
-                  empty_ast(?ERR(unexpected_extends_tag, TW));
-              (ValueToken, TW) ->
-                  format(value_ast(ValueToken, true, true, TW))
-          end,
-          TreeWalkerScope,
-          DjangoParseTree),
+    BodyFun =
+        fun ({'autoescape', {identifier, _, OnOrOff}, Contents}, TW) ->
+                {Info, BodyTW} = body_ast(Contents, push_auto_escape(OnOrOff, TW)),
+                {Info, pop_auto_escape(BodyTW)};
+            ({'block', {identifier, Pos, Name}, Contents}, #treewalker{ context=Context }=TW) ->
+                {Block, BlockScope} =
+                    case dict:find(Name, Context#dtl_context.block_dict) of
+                        {ok, ChildBlock} ->
+                            {{ContentsAst, _ContentsInfo}, _ContentsTW} = body_ast(Contents, TW),
+                            {ChildBlock,
+                             create_scope(
+                               [{block, ?Q("[{super, _@ContentsAst}]")}],
+                               Pos, TW)
+                            };
+                        _ ->
+                            {Contents, empty_scope()}
+                    end,
+                body_ast(Block, BlockScope, TW);
+            ({'blocktrans', Args, Contents}, TW) ->
+                blocktrans_ast(Args, Contents, TW);
+            ({'call', {identifier, _, Name}}, TW) ->
+                call_ast(Name, TW);
+            ({'call', {identifier, _, Name}, With}, TW) ->
+                call_with_ast(Name, With, TW);
+            ({'comment', _Contents}, TW) ->
+                empty_ast(TW);
+            ({'cycle', Names}, TW) ->
+                cycle_ast(Names, TW);
+            ({'cycle_compat', Names}, TW) ->
+                cycle_compat_ast(Names, TW);
+            ({'date', 'now', {string_literal, _Pos, FormatString}}, TW) ->
+                now_ast(FormatString, TW);
+            ({'filter', FilterList, Contents}, TW) ->
+                filter_tag_ast(FilterList, Contents, TW);
+            ({'firstof', Vars}, TW) ->
+                firstof_ast(Vars, TW);
+            ({'for', {'in', IteratorList, Variable, Reversed}, Contents}, TW) ->
+                {EmptyAstInfo, TW1} = empty_ast(TW),
+                for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
+            ({'for', {'in', IteratorList, Variable, Reversed}, Contents, EmptyPartContents}, TW) ->
+                {EmptyAstInfo, TW1} = body_ast(EmptyPartContents, TW),
+                for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
+            ({'if', Expression, Contents, Elif}, TW) ->
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
+                {ElifAstInfo, TW2} = body_ast(Elif, TW1),
+                ifelse_ast(Expression, IfAstInfo, ElifAstInfo, TW2);
+            ({'if', Expression, Contents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
+                {ElseAstInfo, TW2} = empty_ast(TW1),
+                ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifchanged', '$undefined', Contents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
+                {ElseAstInfo, TW2} = empty_ast(TW1),
+                ifchanged_contents_ast(Contents, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifchanged', Values, Contents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
+                {ElseAstInfo, TW2} = empty_ast(TW1),
+                ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifchangedelse', '$undefined', IfContents, ElseContents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
+                {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
+                ifchanged_contents_ast(IfContents, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifchangedelse', Values, IfContents, ElseContents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
+                {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
+                ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifelse', Expression, IfContents, ElseContents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
+                {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
+                ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifequal', [Arg1, Arg2], Contents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
+                {ElseAstInfo, TW2} = empty_ast(TW1),
+                ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
+                {ElseAstInfo, TW2} = body_ast(ElseContents,TW1),
+                ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifnotequal', [Arg1, Arg2], Contents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
+                {ElseAstInfo, TW2} = empty_ast(TW1),
+                ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
+                {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
+                ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
+            ({'include', {string_literal, _, File}, Args}, #treewalker{ context=Context }=TW) ->
+                include_ast(unescape_string_literal(File), Args, Context#dtl_context.local_scopes, TW);
+            ({'include_only', {string_literal, _, File}, Args}, TW) ->
+                {Info, IncTW} = include_ast(unescape_string_literal(File), Args, [], TW),
+                {Info, restore_scope(TW, IncTW)};
+            ({'load_libs', Libs}, TW) ->
+                load_libs_ast(Libs, TW);
+            ({'load_from_lib', What, Lib}, TW) ->
+                load_from_lib_ast(What, Lib, TW);
+            ({'regroup', {ListVariable, Grouper, {identifier, _, NewVariable}}}, TW) ->
+                regroup_ast(ListVariable, Grouper, NewVariable, TW);
+            ('end_regroup', TW) ->
+                {{end_scope, #ast_info{}}, TW};
+            ({'spaceless', Contents}, TW) ->
+                spaceless_ast(Contents, TW);
+            ({'ssi', Arg}, TW) ->
+                ssi_ast(Arg, TW);
+            ({'ssi_parsed', {string_literal, _, FileName}}, #treewalker{ context=Context }=TW) ->
+                include_ast(unescape_string_literal(FileName), [], Context#dtl_context.local_scopes, TW);
+            ({'string', _Pos, String}, TW) ->
+                string_ast(String, TW);
+            ({'tag', Name, Args}, TW) ->
+                tag_ast(Name, Args, TW);
+            ({'templatetag', {_, _, TagName}}, TW) ->
+                templatetag_ast(TagName, TW);
+            ({'trans', Value}, TW) ->
+                translated_ast(Value, TW);
+            ({'widthratio', Numerator, Denominator, Scale}, TW) ->
+                widthratio_ast(Numerator, Denominator, Scale, TW);
+            ({'with', Args, Contents}, TW) ->
+                with_ast(Args, Contents, TW);
+            ({'scope_as', {identifier, _, Name}, Contents}, TW) ->
+                scope_as(Name, Contents, TW);
+            ({'extension', Tag}, TW) ->
+                extension_ast(Tag, TW);
+            ({'extends', _}, TW) ->
+                empty_ast(?ERR(unexpected_extends_tag, TW));
+            (ValueToken, TW) ->
+                format(value_ast(ValueToken, true, true, TW))
+        end,
+
+    {AstInfoList, TreeWalker1} = lists:mapfoldl(BodyFun, TreeWalkerScope, DjangoParseTree),
 
-    Vars = TreeWalker1#treewalker.context#dtl_context.vars,
-    {AstList, {Info, TreeWalker2}} =
+    {AstList, Info} =
         lists:mapfoldl(
-          fun ({Ast, Info}, {InfoAcc, TreeWalkerAcc}) ->
-                  PresetVars = lists:foldl(
-                                 fun (X, Acc) ->
-                                         case proplists:lookup(X, Vars) of
-                                             none -> Acc;
-                                             Val -> [Val|Acc]
-                                         end
-                                 end,
-                                 [],
-                                 Info#ast_info.var_names),
-                  if length(PresetVars) == 0 ->
-                          {Ast, {merge_info(Info, InfoAcc), TreeWalkerAcc}};
-                     true ->
-                          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(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{}, TreeWalker1},
-          AstInfoList),
+          fun ({Ast, Info}, InfoAcc) ->
+                  {Ast, merge_info(Info, InfoAcc)}
+          end, #ast_info{}, AstInfoList),
 
-    {Ast, TreeWalker3} = end_scope(
+    {Ast, TreeWalker2} = end_scope(
                            fun ([ScopeVars|ScopeBody]) -> [?Q("begin _@ScopeVars, [_@ScopeBody] end")] end,
-                           ScopeId, AstList, TreeWalker2),
-    {{erl_syntax:list(Ast), Info}, TreeWalker3}.
+                           ScopeId, AstList, TreeWalker1),
+    {{erl_syntax:list(Ast), Info}, TreeWalker2}.
 
 
 value_ast(ValueToken, AsString, EmptyIfUndefined, TreeWalker) ->
@@ -1099,8 +1081,7 @@ resolve_variable_ast1({attribute, {{_, Pos, Attr}, Variable}}, {Runtime, Finder}
     FileName = get_current_file(TreeWalker1),
     {{?Q(["'@Runtime@':'@Finder@'(",
           "  _@Attr@, _@VarAst,",
-          "  [",
-          "   {lists_0_based, _@Lists0Based@},",
+          "  [{lists_0_based, _@Lists0Based@},",
           "   {tuples_0_based, _@Tuples0Based@},",
           "   {render_options, RenderOptions},",
           "   {record_info, _RecordInfo},",
@@ -1112,20 +1093,47 @@ resolve_variable_ast1({attribute, {{_, Pos, Attr}, Variable}}, {Runtime, Finder}
 
 resolve_variable_ast1({variable, {identifier, Pos, VarName}}, {Runtime, Finder}, TreeWalker) ->
     Ast = case resolve_variable(VarName, TreeWalker) of
-              undefined ->
+              {_, undefined} ->
                   FileName = get_current_file(TreeWalker),
                   {?Q(["'@Runtime@':'@Finder@'(",
                        "  _@VarName@, _Variables,",
                        "  [{filename, _@FileName@},",
                        "   {pos, _@Pos@},",
                        "   {record_info, _RecordInfo},",
-                       "   {render_options, RenderOptions}])"]),
+                       "   {render_options, RenderOptions}])"
+                      ]),
                    #ast_info{ var_names=[VarName] }};
-              Val ->
+              {default_vars, Val} ->
+                  FileName = get_current_file(TreeWalker),
+                  {?Q(["'@Runtime@':fetch_value(",
+                       "  _@VarName@, _Variables,",
+                       "  [{filename, _@FileName@},",
+                       "   {pos, _@Pos@},",
+                       "   {record_info, _RecordInfo},",
+                       "   {render_options, RenderOptions}],",
+                       "   _@val)"
+                      ],
+                      [{val, merl:term(erlydtl_filters:format_number(Val))}]),
+                   #ast_info{ var_names=[VarName], def_names=[VarName] }};
+              {constant, Val} ->
+                  {merl:term(erlydtl_filters:format_number(Val)),
+                   #ast_info{ const_names=[VarName] }};
+              {scope, Val} ->
                   {Val, #ast_info{}}
           end,
     {Ast, TreeWalker}.
 
+resolve_reserved_variable(ReservedName, TreeWalker) ->
+    resolve_reserved_variable(ReservedName, merl:term(undefined), TreeWalker).
+
+resolve_reserved_variable(ReservedName, Default, TreeWalker) ->
+    case resolve_variable(ReservedName, Default, TreeWalker) of
+        {Src, Value} when Src =:= scope; Value =:= Default ->
+            {Value, TreeWalker};
+        _ ->
+            {Default, ?ERR({reserved_variable, ReservedName}, TreeWalker)}
+    end.
+
 format({{Ast, Info}, TreeWalker}) ->
     auto_escape({{format_number_ast(Ast), Info}, TreeWalker}).
 
@@ -1262,7 +1270,7 @@ for_loop_ast(IteratorList, LoopValue, IsReversed, Contents,
 
     LoopValueAst0 = to_list_ast(LoopValueAst, merl:term(IsReversed), TreeWalker2),
 
-    ParentLoop = resolve_variable('forloop', erl_syntax:atom(undefined), TreeWalker2),
+    {ParentLoop, TreeWalker3} = resolve_reserved_variable('forloop', TreeWalker2),
 
     %% call for loop
     {{?Q(["case erlydtl_runtime:forloop(",
@@ -1285,7 +1293,7 @@ for_loop_ast(IteratorList, LoopValue, IsReversed, Contents,
                               ?Q("() when true -> {_@Vars}")
                       end}]),
       merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo)},
-     TreeWalker2}.
+     TreeWalker3}.
 
 ifchanged_values_ast(Values, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, TreeWalker) ->
     Info = merge_info(IfContentsInfo, ElseContentsInfo),
@@ -1327,10 +1335,10 @@ cycle_ast(Names, #treewalker{ context=Context }=TreeWalker) ->
                 (_, VarNamesAcc) ->
                     {[], VarNamesAcc}
             end, [], Names),
-    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@forloop)",
-        [{forloop, resolve_variable('forloop', TreeWalker)}]),
+    {ForLoop, TreeWalker1} = resolve_reserved_variable('forloop', TreeWalker),
+    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@ForLoop)"),
       #ast_info{ var_names = VarNames }},
-     TreeWalker}.
+     TreeWalker1}.
 
 %% Older Django templates treat cycle with comma-delimited elements as strings
 cycle_compat_ast(Names, #treewalker{ context=Context }=TreeWalker) ->
@@ -1338,10 +1346,10 @@ cycle_compat_ast(Names, #treewalker{ context=Context }=TreeWalker) ->
                    fun ({identifier, _, X}) ->
                            string_ast(X, Context)
                    end, Names),
-    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@forloop)",
-        [{forloop, resolve_variable('forloop', TreeWalker)}]),
+    {ForLoop, TreeWalker1} = resolve_reserved_variable('forloop', TreeWalker),
+    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@ForLoop)"),
       #ast_info{}},
-     TreeWalker}.
+     TreeWalker1}.
 
 now_ast(FormatString, TreeWalker) ->
     %% Note: we can't use unescape_string_literal here

+ 23 - 7
src/erlydtl_compiler.erl

@@ -92,6 +92,8 @@ format_error({read_file, File, Error}) ->
     io_lib:format(
       "Failed to include file ~s: ~s",
       [File, file:format_error(Error)]);
+format_error({deprecated_option, vars}) ->
+    "Compile option 'vars' has been deprecated. Use 'default_vars' instead.";
 format_error(Error) ->
     erlydtl_compiler_utils:format_error(Error).
 
@@ -103,7 +105,9 @@ format_error(Error) ->
 process_opts(File, Module, Options0) ->
     Options1 = proplists:normalize(
                  update_defaults(Options0),
-                 [{aliases, [{outdir, out_dir}]}
+                 [{aliases,
+                   [{outdir, out_dir},
+                    {vars, default_vars}]}
                  ]),
     Source0 = filename:absname(
                 case File of
@@ -122,11 +126,22 @@ process_opts(File, Module, Options0) ->
     Source = shorten_filename(Source0),
     Options = [{compiler_options, [{source, Source}]}
                |compiler_opts(Options1, [])],
-    case File of
-        {dir, _} ->
-            init_context([], Source, Module, Options);
-        _ ->
-            init_context([Source], filename:dirname(Source), Module, Options)
+    Context =
+        case File of
+            {dir, _} ->
+                init_context([], Source, Module, Options);
+            _ ->
+                init_context([Source], filename:dirname(Source), Module, Options)
+        end,
+
+    %% check original options here now that we have a context to
+    %% process any warnings/errors generated.
+    check_opts(Options0, Context).
+
+check_opts(Options, Context) ->
+    case proplists:get_value(vars, Options) of
+        undefined -> Context;
+        _ -> ?WARN({deprecated_option, vars}, Context)
     end.
 
 compiler_opts([CompilerOption|Os], Acc)
@@ -241,7 +256,8 @@ init_context(ParseTrail, DefDir, Module, Options) ->
                                filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
            trans_fun = proplists:get_value(blocktrans_fun, Options, Ctx#dtl_context.trans_fun),
            trans_locales = TransLocales,
-           vars = proplists:get_value(vars, Options, Ctx#dtl_context.vars),
+           vars = proplists:get_value(default_vars, Options, Ctx#dtl_context.vars),
+           const = proplists:get_value(constants, Options, Ctx#dtl_context.const),
            reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
            compiler_options = proplists:append_values(compiler_options, Options),
            binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings),

+ 26 - 32
src/erlydtl_compiler_utils.erl

@@ -187,38 +187,25 @@ call_extension(#dtl_context{ extension_module=Mod }, Fun, Args)
             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)}.
+merge_info(Info1, Info2) when is_record(Info1, ast_info), is_record(Info2, ast_info) ->
+    merge_info1(record_info(size, ast_info), Info1, Info2, #ast_info{}).
 
 resolve_variable(VarName, TreeWalker) ->
     resolve_variable(VarName, undefined, TreeWalker).
 
 resolve_variable(VarName, Default, #treewalker{ context=Context }) ->
-    resolve_variable1(Context#dtl_context.local_scopes, VarName, Default).
+    case resolve_variable1(Context#dtl_context.local_scopes, VarName) of
+        undefined ->
+            case proplists:get_value(VarName, Context#dtl_context.const) of
+                undefined ->
+                    case proplists:get_value(VarName, Context#dtl_context.vars) of
+                        undefined -> {default, Default};
+                        Value -> {default_vars, Value}
+                    end;
+                Value -> {constant, Value}
+            end;
+        Value -> {scope, Value}
+    end.
 
 push_scope(Scope, #treewalker{ context=Context }=TreeWalker) ->
     TreeWalker#treewalker{ context=push_scope(Scope, Context) };
@@ -397,14 +384,21 @@ pos_info(Line) when is_integer(Line) ->
 pos_info({Line, Col}) when is_integer(Line), is_integer(Col) ->
     io_lib:format("~b:~b ", [Line, Col]).
 
-resolve_variable1([], _VarName, Default) -> Default;
-resolve_variable1([Scope|Scopes], VarName, Default) ->
-    case proplists:get_value(VarName, get_scope(Scope), Default) of
-        Default ->
-            resolve_variable1(Scopes, VarName, Default);
+resolve_variable1([], _VarName) -> undefined;
+resolve_variable1([Scope|Scopes], VarName) ->
+    case proplists:get_value(VarName, get_scope(Scope)) of
+        undefined ->
+            resolve_variable1(Scopes, VarName);
         Value -> Value
     end.
 
+merge_info1(1, _, _, Info) -> Info;
+merge_info1(FieldIdx, Info1, Info2, Info) ->
+    Value = lists:merge(
+              lists:sort(element(FieldIdx, Info1)),
+              lists:sort(element(FieldIdx, Info2))),
+    merge_info1(FieldIdx - 1, Info1, Info2, setelement(FieldIdx, Info, Value)).
+
 get_scope({_Id, Scope, _Values}) -> Scope;
 get_scope(Scope) -> Scope.
 

+ 4 - 1
src/erlydtl_runtime.erl

@@ -98,8 +98,11 @@ find_value(Key, Tuple) when is_tuple(Tuple) ->
     end.
 
 fetch_value(Key, Data, Options) ->
+    fetch_value(Key, Data, Options, []).
+
+fetch_value(Key, Data, Options, Default) ->
     case find_value(Key, Data, Options) of
-        undefined -> [];
+        undefined -> Default;
         Val -> Val
     end.
 

+ 9 - 2
test/erlydtl_eunit_testrunner.erl

@@ -47,10 +47,17 @@ run_test(T) ->
         error_ok -> ok
     end.
 
+compile_opts(#test{ compile_vars = undefined, compile_opts = Opts }) ->
+    Opts;
+compile_opts(#test{ compile_vars = Vars, compile_opts = Opts }) ->
+    [{default_vars, Vars}|Opts].
+
 run_compile(T) ->
     case erlydtl:compile(
-           T#test.source, T#test.module,
-           [{vars, T#test.compile_vars}|T#test.compile_opts]) of
+           T#test.source,
+           T#test.module,
+           compile_opts(T))
+    of
         {ok, M, W} ->
             ?assertEqual(T#test.module, M),
             ?assertEqual(T#test.warnings, W);

+ 59 - 1
test/erlydtl_test_defs.erl

@@ -1479,6 +1479,62 @@ all_test_defs() ->
         <<"ytrewQ">>
        }
       ]},
+     {"compile time default vars/constants",
+      begin
+          Tpl = <<"Test {{ var1 }}:{{ var2 }}.">>,
+          Txt = <<"Test 123:abc.">>,
+          Fun = fun (F) ->
+                        fun (#test{ module=M }) ->
+                                M:F()
+                        end
+                end,
+          [{"default vars",
+            Tpl, [], [],
+            [{default_vars, [{var1, 123}, {var2, abc}]}], Txt},
+           {"default vars (using fun)",
+            Tpl, [], [],
+            [{default_vars, [{var1, 123}, {var2, fun () -> abc end}]}], Txt},
+           {"override default vars",
+            Tpl, [{var2, abc}], [],
+            [{default_vars, [{var1, 123}, {var2, 456}]}], Txt},
+           {"constants",
+            Tpl, [], [],
+            [{constants, [{var1, 123}, {var2, abc}]}], Txt},
+           {"constants (using fun)",
+            Tpl, [], [],
+            [{constants, [{var1, 123}, {var2, fun () -> abc end}]}], Txt},
+           {"constants non-overridable",
+            Tpl, [{var1, ohno}, {var2, noway}], [],
+            [{constants, [{var1, 123}, {var2, "abc"}]}], Txt}
+           |[#test{ title = T,
+                    source = Tpl,
+                    compile_vars = undefined,
+                    compile_opts = CO ++ (#test{})#test.compile_opts,
+                    renderer = Fun(F),
+                    output = O
+                  }
+             || {T, F, O, CO} <-
+                    [{"variables/0",
+                      variables, [var1, var2], []},
+                     {"variables/0 w. defaults",
+                      variables, [var1, var2], [{default_vars, [{var1, aaa}]}]},
+                     {"variables/0 w. constants",
+                      variables, [var2], [{constants, [{var1, bbb}]}]},
+                     {"default_variables/0",
+                      default_variables, [], []},
+                     {"default_variables/0 w. defaults",
+                      default_variables, [var1], [debug_compiler, {default_vars, [{var1, aaa}]}]},
+                     {"default_variables/0 w. constants",
+                      default_variables, [], [{constants, [{var1, bbb}]}]},
+                     {"constants/0",
+                      constants, [], []},
+                     {"constants/0 w. defaults",
+                      constants, [], [{default_vars, [{var1, aaa}]}]},
+                     {"constants/0 w. constants",
+                      constants, [var1], [{constants, [{var1, bbb}]}]}
+                    ]
+            ]]
+      end},
      {"functional",
       [functional_test(F)
        %% order is important.
@@ -1507,7 +1563,8 @@ all_test_defs() ->
                    renderer = fun(#test{ module=M, render_vars=V, render_opts=O }) ->
                                       M:render(base1, V, O)
                               end
-                  }]
+                  }
+               ]
       ]}
     ].
 
@@ -1530,6 +1587,7 @@ def_to_test(Group, {Name, DTL, Vars, RenderOpts, CompilerOpts, Output, Warnings}
        source = {template, DTL},
        render_vars = Vars,
        render_opts = RenderOpts,
+       compile_vars = undefined,
        compile_opts = CompilerOpts ++ (#test{})#test.compile_opts,
        output = Output,
        warnings = Warnings