diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/htt/examples/implementing-types-in-go/index.html b/htt/examples/implementing-types-in-go/index.html new file mode 100644 index 0000000..b99a5fb --- /dev/null +++ b/htt/examples/implementing-types-in-go/index.html @@ -0,0 +1,793 @@ + + +
+ + ++ This example implements some of the AST nodes from the book, Writing an Interpreter in Go, specifically as implemented in this git repository.
+ +
+ You will see nodes generally come in two types, Statement
and Expression
, implementing the empty method of either to mark the type of node.
+ Beyond this, nodes implement String
and TokenLiteral
, and sometimes these are implemented in the same way.
+ We start by creating a template file, ast.htt
and defining the package, implementing the base types and so on. All of this is regular Go code:
% @component main
+package ast;
+
+import (
+ "bytes"
+ "strings"
+
+ "github.com/zanshin/interpreter/token"
+)
+
+type Node interface {
+ TokenLiteral() string
+ String() string
+}
+
+type Statement interface {
+ Node
+ statementNode()
+}
+
+type Expression interface {
+ Node
+ expressionNode()
+}
+% @end
+ package ast;
+
+ import (
+ "bytes"
+ "strings"
+
+ "github.com/zanshin/interpreter/token"
+ )
+
+ type Node interface {
+ TokenLiteral() string
+ String() string
+ }
+
+ type Statement interface {
+ Node
+ statementNode()
+ }
+
+ type Expression interface {
+ Node
+ expressionNode()
+ }
+ + Now let's start rendering nodes.
+ ++ To start, let's define some data describing the nodes we want. When speaking of code generation, the data we are generating code from is often called the model. For larger projects, I would store the model in a separate file. That way, template files could refer to it.
+ +
+ For small, self-contained projects, we can embed the model with the template. When compiling a template file (ast.htt
), HTT will look for a corresponding Lua file (ast.htt.lua
) matching the name of the template file, with the prefix .lua
.
+ Hence, here is the ast.htt.lua
file to accompany our ast.htt
template file:
+
function nodes()
+ return {
+ {
+ is_expr = true,
+ short = "i",
+ name = "Identifier",
+ fields = {
+ { name = "Value", type = "string" }
+ }
+ },
+ {
+ is_expr = true,
+ short = "il",
+ name = "IntegerLiteral",
+ fields = {
+ { name = "Value", type = "int64" }
+ }
+ },
+ }
+end
+
+ The code in ast.htt.lua
is, as the name implies, regular Lua code. The function nodes
returns a list (table) of (associative) tables, each describing a type of AST node. I like to wrap the data in a function to ensure I cannot modify global state and impact other rendering code.
+ To use this data, let's define a component, node
, and just render out the name of the component for now:
% @component node
+{{ctx.name}}
+% @end
+ render_nodes
, which loops over each entry in the model (the nodes
function in ast.htt.lua
) and renders it:
+
+ % @component render_nodes
+% for _, elem in ipairs(nodes()) do
+{{@ node elem }}
+% end
+% @end
+
+ You could add these lines directly to main
. I normally would. But this way I can show you changes to this component in isolation.
+ Finally, we call render_nodes
from our main
component. Since we have no data to pass to render_nodes
, we pass an empty Lua table ({}
).
+
+ Our main component, thus becomes:
+
% @component main
+package ast;
+
+import (
+ "bytes"
+ "strings"
+
+ "github.com/zanshin/interpreter/token"
+)
+
+type Node interface {
+ TokenLiteral() string
+ String() string
+}
+
+type Statement interface {
+ Node
+ statementNode()
+}
+
+type Expression interface {
+ Node
+ expressionNode()
+}
+
+{{@ render_nodes {} }}
+% @end
+ package ast;
+
+ import (
+ "bytes"
+ "strings"
+
+ "github.com/zanshin/interpreter/token"
+ )
+
+ type Node interface {
+ TokenLiteral() string
+ String() string
+ }
+
+ type Statement interface {
+ Node
+ statementNode()
+ }
+
+ type Expression interface {
+ Node
+ expressionNode()
+ }
+
+ Identifier
+ IntegerLiteral
+ % @component node
+type {{ctx.name}} struct {
+ Token token.Token
+ % for _, field in ipairs(ctx.fields) do
+ {{field.name}} {{field.type}}
+ % end
+}
+% @end
+ type Identifier struct {
+ Token token.Token
+ Value string
+ }
+ type IntegerLiteral struct {
+ Token token.Token
+ Value int64
+ }
+ render_nodes
works well:
+ % @component render_nodes
+% for _, elem in ipairs(nodes()) do
+
+{{@ node elem }}
+% end
+% @end
+
+ type Identifier struct {
+ Token token.Token
+ Value string
+ }
+
+ type IntegerLiteral struct {
+ Token token.Token
+ Value int64
+ }
+ String
and TokenLiteral
implementations both return the value of .Token.Literal
.
+ Beyond that, we must implement either the empty function expressionNode()
or statementNode()
, depending on the type of node, as identified by the is_expr
field for the node in our model:
+
+ % @component node
+type {{ctx.name}} struct {
+ Token token.Token
+ % for _, field in ipairs(ctx.fields) do
+ {{field.name}} {{field.type}}
+ % end
+}
+
+% if ctx.is_expr == true then
+func ({{ctx.short}} *{{ctx.name}}) expressionNode() {}
+% else
+func ({{ctx.short}} *{{ctx.name}}) statementNode() {}
+% end
+
+func ({{ctx.short}} *{{ctx.name}}) TokenLiteral() string {
+ return {{ctx.short}}.Token.Literal
+}
+
+func ({{ctx.short}} *{{ctx.name}}) String() string {
+ return {{ctx.short}}.Token.Literal
+}
+% @end
+
+ type Identifier struct {
+ Token token.Token
+ Value string
+ }
+
+ func (i *Identifier) expressionNode() {}
+
+ func (i *Identifier) TokenLiteral() string {
+ return i.Token.Literal
+ }
+
+ func (i *Identifier) String() string {
+ return i.Token.Literal
+ }
+
+ type IntegerLiteral struct {
+ Token token.Token
+ Value int64
+ }
+
+ func (il *IntegerLiteral) expressionNode() {}
+
+ func (il *IntegerLiteral) TokenLiteral() string {
+ return il.Token.Literal
+ }
+
+ func (il *IntegerLiteral) String() string {
+ return il.Token.Literal
+ }
+
+ Some nodes implement a custom String()
method to stringify themselves and their contents.
+ To support this, let us extend the model such that each node can define a string_fn
attribute, pointing to a component which implements the String()
method.
+ Recall that we defined our components in the template ast.htt
and put our accompanying model in ast.htt.lua
, and that it is these two files combined which will
+ To support this, we need a way to reference the components to use for implementing the String()
method. One way to solve this is to send along a reference to the module generated from the template file itself.
+ All templates can use the variable M
to refer to their own modules. So we can just send this along to the nodes()
function which returns the model:
% @component render_nodes
+% for _, elem in ipairs(nodes(M)) do
+
+{{@ node elem }}
+% end
+% @end
+
+ From the model function, we can now refer to components in the template by their name. So m.node
would refer to the node
component defined in ast.htt
function nodes(m)
+ -- `m` is a reference to the template module, now we can reference
+ -- components like `node` as `m.node`.
+end
+
+ With this change, we update the model, expanding the number of nodes we implement. Some, like the ReturnStatement
node, will now use a custom component (here: return_statement_string
) to implement the body of their String()
method.
function nodes(m)
+ return {
+ {
+ is_expr = true,
+ short = "i",
+ name = "Identifier",
+ fields = {
+ { name = "Value", type = "string" }
+ }
+ },
+ {
+ is_expr = false,
+ short = "rs",
+ name = "ReturnStatement",
+ fields = {
+ { name = "ReturnValue", type = "Expression" }
+ },
+ string_fn = m.return_statement_string,
+ },
+ {
+ is_expr = false,
+ short = "es",
+ name = "ExpressionStatement",
+ fields = {
+ { name = "Expression", type = "Expression" }
+ },
+ string_fn = m.expr_statement_string,
+ },
+ {
+ is_expr = true,
+ short = "il",
+ name = "IntegerLiteral",
+ fields = {
+ { name = "Value", type = "int64" }
+ }
+ },
+ {
+ is_expr = true,
+ short = "pe",
+ name = "PrefixExpression",
+ fields = {
+ { name = "Operator", type = "string" },
+ { name = "Right", type = "Expression" },
+ },
+ string_fn = m.prefix_expr_string,
+ }
+ }
+end
+
+
+ The key change in the template is how we implement node
Notice now that we check if ctx.string_fn
is defined, and if so, renders that component:
% @component node
+type {{ctx.name}} struct {
+ Token token.Token
+ % for _, field in ipairs(ctx.fields) do
+ {{field.name}} {{field.type}}
+ % end
+}
+
+% if ctx.is_expr == true then
+func ({{ctx.short}} *{{ctx.name}}) expressionNode() {}
+% else
+func ({{ctx.short}} *{{ctx.name}}) statementNode() {}
+% end
+
+func ({{ctx.short}} *{{ctx.name}}) TokenLiteral() string {
+ return {{ctx.short}}.Token.Literal
+}
+
+func ({{ctx.short}} *{{ctx.name}}) String() string {
+ % if ctx.string_fn then
+ {{@ ctx.string_fn ctx }}
+ % else
+ return {{ctx.short}}.Token.Literal
+ % end
+}
+% @end
+
+ This demonstrates how HTT components can be passed as arguments to other components which can render them. Also notice that since all compontents take exactly one table argument, we can pass all arguments along by passing ctx
.
+ These two facts combine to make components very composable.
+ The final change to the ast.htt
template file is implementing the components for the custom String()
methods.
+ There is nothing to these components aside from noting that since we passed the entire ctx
from the node
component along to these, we still have access to attributes like short
from the model describing the node.
% @component return_statement_string
+var out bytes.Buffer
+
+out.WriteString(rs.Token.Literal + " ")
+if rs.ReturnValue != nil {
+ out.WriteString(rs.ReturnValue.String())
+}
+
+out.WriteString(";")
+
+return out.String()
+% @end
+ % @component expr_statement_string
+if {{ctx.short}}.Expression != nil {
+ return {{ctx.short}}.Expression.String()
+}
+return ""
+% @end
+ % @component prefix_expr_string
+var out bytes.Buffer
+
+out.WriteString("(")
+out.WriteString({{ctx.short}}.Operator)
+out.WriteString({{ctx.short}}.Right.String())
+out.WriteString(")")
+
+return out.String()
+% @end
+
+ type Identifier struct {
+ Token token.Token
+ Value string
+ }
+
+ func (i *Identifier) expressionNode() {}
+
+ func (i *Identifier) TokenLiteral() string {
+ return i.Token.Literal
+ }
+
+ func (i *Identifier) String() string {
+ return i.Token.Literal
+ }
+
+ type ReturnStatement struct {
+ Token token.Token
+ ReturnValue Expression
+ }
+
+ func (rs *ReturnStatement) statementNode() {}
+
+ func (rs *ReturnStatement) TokenLiteral() string {
+ return rs.Token.Literal
+ }
+
+ func (rs *ReturnStatement) String() string {
+ var out bytes.Buffer
+
+ out.WriteString(rs.Token.Literal + " ")
+ if rs.ReturnValue != nil {
+ out.WriteString(rs.ReturnValue.String())
+ }
+
+ out.WriteString(";")
+
+ return out.String()
+ }
+
+ type ExpressionStatement struct {
+ Token token.Token
+ Expression Expression
+ }
+
+ func (es *ExpressionStatement) statementNode() {}
+
+ func (es *ExpressionStatement) TokenLiteral() string {
+ return es.Token.Literal
+ }
+
+ func (es *ExpressionStatement) String() string {
+ if es.Expression != nil {
+ return es.Expression.String()
+ }
+ return ""
+ }
+
+ type IntegerLiteral struct {
+ Token token.Token
+ Value int64
+ }
+
+ func (il *IntegerLiteral) expressionNode() {}
+
+ func (il *IntegerLiteral) TokenLiteral() string {
+ return il.Token.Literal
+ }
+
+ func (il *IntegerLiteral) String() string {
+ return il.Token.Literal
+ }
+
+ type PrefixExpression struct {
+ Token token.Token
+ Operator string
+ Right Expression
+ }
+
+ func (pe *PrefixExpression) expressionNode() {}
+
+ func (pe *PrefixExpression) TokenLiteral() string {
+ return pe.Token.Literal
+ }
+
+ func (pe *PrefixExpression) String() string {
+ var out bytes.Buffer
+
+ out.WriteString("(")
+ out.WriteString(pe.Operator)
+ out.WriteString(pe.Right.String())
+ out.WriteString(")")
+
+ return out.String()
+ }
+
+ I wanted to implement this piece-meal, with you looking on, to get a sense of the process rather than just seeing the finished thing.
+ As a next step, I would refactor the model out into its own file, ast_model.lua
. If we did this, we would not have to pass the model (self) reference (M
) from the template to the nodes()
model function in ast.htt.lua
.
+ To achieve this, we would: +
ast.htt.lua
to ast_model.lua
ast.htt
, import the module by adding % local model = require 'ast_model'
to the top of the fileast.htt
, change the call to nodes(M)
to model.nodes()
ast_model.lua
, import the template module by adding local tpl = require '//ast.htt'
to the top of the file.ast_model.lua
, change all references to the model m.component
to tpl.component
.+ You will make mistakes. Here we will discuss the types of errors you will encounter and how to read the error output.
+ ++ The types of error you will encounter are: +
+ We will revisit the specifics of this error later, but for now, let's cover some general information on reading the error.
+ +resolved script_fpath: '/home/runner/work/htt/htt/docs/examples/debug/run-err-stx.lua'
+resolved htt_root: '/home/runner/work/htt/htt/docs/examples/debug'
+
+Error loading Lua module '//err-stx.htt':
+ err-stx.out.lua:7: 'then' expected near 'end'
+stack traceback:
+ [HTT Library]:524: in function <[HTT Library]:482>
+ [C]: in function 'require'
+ .../runner/work/htt/htt/docs/examples/debug/run-err-stx.lua:1: in main chunk
+ [C]: in function 'xpcall'
+ [HTT Library]:119: in function <[HTT Library]:101>
+
+
+ script_fpath
refers to the script file you used as a starting-point when calling HTT, see script file for details.
+ htt_root
refers to the parent directory of the script file. This is the starting-point which is used when importing additional Lua modules or HTT template files, see HTT root for details.
+ Many lua functions in HTT are provided in addition to the Lua standard API. [HTT Library]:<number>
refers to a line within the ./src/prelude.lua, which holds the definitions for this expanded standard library.
stack traceback:
.
+
+ [[HTT Library]]
), line 432. In the copy of HTT used, this line marks the start of the loader function which HTT installs to intercept calls to require "//<path/to/template>.htt
, which compile HTT templates into lua modules before loading them in as usual.
+ + Compile errors happen when a HTT file cannot be compiled to a Lua module because there is a syntax- or semantic error, which the compiler detected.
+ ++ Here's a trivial example. Content (output) cannot appear outside of a component, so the first line causes a compile error:
+This line causes a compile failure
+% @component one
+% local x = false
+% if x then
+% end
+% @end
+ + These errors should be the easiest to follow. They point out the file, line- and column number. In case you want to know more, it also prints which parse state the compiler was in when encountering the error:
+ +resolved script_fpath: '/home/runner/work/htt/htt/docs/examples/debug/run-err-compile.lua'
+resolved htt_root: '/home/runner/work/htt/htt/docs/examples/debug'
+
+Error compiling HTT template '//err-compile.htt'
+ File: 'err-compile.htt'
+ Line: 1
+ Column: 0
+ Type: illegal_toplevel_content
+ Reason: content must be inside a component
+stack traceback:
+ [HTT Library]:516: in function <[HTT Library]:482>
+ [C]: in function 'require'
+ ...ner/work/htt/htt/docs/examples/debug/run-err-compile.lua:1: in main chunk
+ [C]: in function 'xpcall'
+ [HTT Library]:119: in function <[HTT Library]:101>
+
+
+ The initial script is loaded in a special way. For all other modules, whether written in plain Lua or compiled from HTT templates, Error loading Lua module
means the module contains code which is not valid Lua.
+ In this case, we wrote if x
, not if x then
as is required:
% @component one
+% local x = false
+% if x
+% end
+% @end
+ + And here is the resulting error:
+ +resolved script_fpath: '/home/runner/work/htt/htt/docs/examples/debug/run-err-stx.lua'
+resolved htt_root: '/home/runner/work/htt/htt/docs/examples/debug'
+
+Error loading Lua module '//err-stx.htt':
+ err-stx.out.lua:7: 'then' expected near 'end'
+stack traceback:
+ [HTT Library]:524: in function <[HTT Library]:482>
+ [C]: in function 'require'
+ .../runner/work/htt/htt/docs/examples/debug/run-err-stx.lua:1: in main chunk
+ [C]: in function 'xpcall'
+ [HTT Library]:119: in function <[HTT Library]:101>
+
+
+ The stack trace is uninteresting in this case. We see the issue stems from a require
on line 1 of the start.lua
file, which is the intial script we passed to htt
.
+ However, note the error, Error loading Lua module
, and also note the name of the module, //err-stx.htt
, which is a HTT template. For HTT templates, the module name reflects the path with //
being the start of the path, relative to the HTT root directory.
+ The second thing to note is the actual error from Lua, err-stx.out.lua:7
, the result of compiling err-stx.htt
to Lua is saved in err-stx.out.lua
, and the error in the Lua code is on line 7.
+ In this case, the actual error message from Lua, 'then' expected near 'end'
is easy to follow. Other times, opening the compiled output is needed. Don't worry, the compiled output is written to be easy to follow.
Reading the compiled HTT templates
+
+ Every HTT template file is compiled, and the resulting Lua module is saved on disk. The result of compiling foo.htt
is saved as foo.out.lua
in the same directory.
+ The compiled output should make for easy reading, and sometimes a difficult to understand error becomes obvious when you read the relevant line in the compiled output.
++ Template code can cause errors. If an error happens, you can determine where by examining the stack.
+ +
+ In this example, we have two components, parent
and child
, both defined in //examples/debug/err-exec.htt
. The parent
calls child
which causes a runtime error.
% @component parent
+% local x = false
+% if x then
+% end
+Call child {{@ child {} }}
+% @end
+ % @component child
+% error("I am raising an error")
+% @end
+
+ Note in output that we see the error itself, and before that, we can see the call-stack for components, //examples/debug/err-exec.htt.parent
is the parent
component, and the stack shows how it calls //examples/debug/err-exec.htt.child
(child
).
resolved script_fpath: '/home/runner/work/htt/htt/docs/examples/debug/run-err-exec.lua'
+resolved htt_root: '/home/runner/work/htt/htt/docs/examples/debug'
+
+Error during execution:
+ err-exec.out.lua:5: I am raising an error
+stack traceback:
+ err-exec.out.lua:5: in function '//err-exec.htt.child'
+ [HTT Library]:467: in field 'render'
+ err-exec.out.lua:16: in function '//err-exec.htt.parent'
+ [HTT Library]:467: in local 'render_'
+ [HTT Library]:478: in field 'render'
+ [HTT Loader]:19: in function 'render'
+ ...runner/work/htt/htt/docs/examples/debug/run-err-exec.lua:2: in main chunk
+ [C]: in function 'xpcall'
+ [HTT Library]:119: in function <[HTT Library]:101>
+
+ + This covers how HTT and indeed Lua resolves and imports code and how this relates to module names. This is important both when debugging errors and laying out your projects.
+ +
+ A template is a <file>.htt
file, written in the HTT template syntax. HTT will compile this template to a Lua module, <file>.out.lua
when it is first loaded. A template may also have a companion Code file, <file>.htt.lua
, whose contents is prepended to the compiled module.
+ A module is a Lua concept and is, like a Python module, some unit of code and data. A module is typically a .lua
file which would typically "export" a table of functions and data, defining the module's public API.
+ In the broader Lua ecosystem, modules may be implemented in C or similar by using the C API.
+ A component is a HTT concept. It is a "mini-template", a fragment of templating which can be called and passed around as a function to be rendered inside other components. See more in the Quick Start
+ + +
+ By script file, we refer to the <file>.lua
file which is passed to htt
when running the program and which kicks off the code-generation process.
+ You start the code-generation process by passing a script file to HTT, e.g. htt <file>.lua
.
+ As the script is loaded HTT resolves the path to the script file itself, and treats the directory of that file as the HTT root. All Lua modules and HTT template files are resolved relative to the HTT root directory.
+ When importing modules, Lua uses the string in package.path
to build a list of paths to try, in-order. We can see the package.path
by printing it:
+
$ cat /tmp/test.lua
+ print(package.path)
+ $ htt /tmp/test.lua
+ /tmp/?.lua;/tmp/?/init.lua;
+
+
+ Each path in package.path
is separated by ;
, with ?
being the placeholder which is replaced by the module name (the string argument to require
).
+ If package.path
is /tmp/?.lua;/tmp/?/init.lua;
and we require("foo")
, Lua would:
+
/tmp/foo.lua
/tmp/foo/init.lua
+ If package.path
is /tmp/?.lua;/tmp/?/init.lua;
and we require("foo.bar")
, Lua would:
+
/tmp/foo/bar.lua
/tmp/foo/bar/init.lua
+ HTT extends Lua's regular require
function to also work for importing HTT templates. When requiring templates, three things are different from importing regular Lua modules:
+
+
require
string with //
/
(instead of Lua's .
), also on Windows.htt
file extension
+ Basically, we write what looks like a relative path, using the Unix path-separator (/
) with //
prefixed.
-- look for "foo.htt", relative to HTT root
+ local mod = require("//foo.htt")
+
+ -- look for "bar/foo.htt", relative to HTT root
+ local mod2 = require("//bar/foo.htt")
+
+ + This section is just a minimal recap of the HTT templating syntax so you have the proper vocabulary in place and can visually recognize the various parts of the syntax.
++ To learn how to apply this syntax and write actual templates, see the Quick Start guide.
+ ++ Finally, you may actually want to see a condensed grammar definition. If so, click here
+ ++ The majority of the template will probably be literal text. + Any text which is not triggering any of the syntax rules below is rendered exactly as you typed it in. This is what permits most templates to still be recognizable to people understanding the eventual output.
+ +% --[[ lua code here ]]--
+
+ Lua lines begin with are lines whose first non-whitespace character is %
. The remainder of the line is handled as Lua code and inserted verbatim in the compiled Template's output.
+ Directives are blocks with a defined start- (% @<directive type>
) and end (% @end
).
+ Directives cannot partially overlap, but code
-directives can be nested inside component
-directives.
% @component myComponent
+... content here :)
+% @end
+
+ References to ctx
within the component refers to the Lua table which holds all arguments passed to the component when called.
+ You can write blocks of verbatim Lua code by wrapping it in a @code
directive:
% @code
+-- lua code here
+% @end
+
+ Whenever you see {{ ... }}
, it is a Lua expression.
+ Expressions are evaluated and tostring()
is called on their value and it is this value which is embedded in the output.
+ Calls of the form {{<component> <lua-table-expr>}}
.
+ For example, {{greeting {name = "John"} }}
calls the component greeting
with name
set to John
, accessible from within the component as ctx.name
.
+ Any line which starts with ~>
(after optional indentation).
+ It may sound technical, but it is simple. A line continuation is just that, a continuation of the prior line.
+ + + ++ The following is a definition of the HTT grammar. +
// ...
this is a comment<rule-name> =
is the start of a rule | ...
- is the start of one (of possibly several) definitions of the rule . ...
- means the definition spans multiple lines and this is the next one<rule>*
means 0 or more repetitions of rule
<rule>+
means 1 or more repetitions of rule
<rule>?
means zero or 1 repetitions of rule
'...'
(anything in single-quotes) is a literal valuetop =
+ | lua-line
+ | component
+ | code
+
+ lua-line =
+ | ws* '%' lua nl
+
+ code =
+ | code-open nl
+ . lua nl
+ . code-close nl
+
+ code-open =
+ | ws* '%' ws+ '@code'
+
+ code-close =
+ | ws* '%' ws+ '@end'
+
+ component =
+ | component-open nl
+ . component-line
+ . component-close nl
+
+ component-open =
+ | ws* '%' ws+ '@component' component_name
+
+ component_name =
+ | [a-zA-Z_][a-zA-Z0-9_]*
+
+ component-line =
+ | lua-line
+ | text-line
+ | code
+
+ component-close =
+ | ws* '%' ws+ '@end'
+
+ text-line =
+ | ws* line-continuation? element* nl
+
+ line-continuation =
+ | '~>'
+
+ element =
+ | text
+ | expression
+ | component-call
+
+ // here, 'lua' must be a single-line expression.
+ expression =
+ | '{{' lua '}}'
+
+ // here 'lua-table-expr' is some single-line lua expression
+ // which evaluates to a Lua table.
+ component-call =
+ | '{{@' ws* component_name ws+ lua-table-expr '}}'
+
+ ws =
+ | \r
+ | \t
+ | ' '
+
+ nl =
+ | \r? \n
+
+ + HTT is a code-generator. It is built to help you generate code or configuration files from some high-level description which you either write yourself or get from elsewhere.
+ ++ You could generate repetitive code, such as a slew of AST types in Go, dataclass-like classes in Python, a series of functions for manipulating bits without using bitfields in C and so on. The possibilities are everywhere.
+ ++ But it is not limited to code. This site is generated using HTT. It embeds code examples from actual files, and it runs them, captures their output and embeds that also. It also generates the menu-structure and places output in a folder structure which enables user-friendly URLs.
+ ++ Code generation is also great whenever crossing language- or service/application boundaries. + Think of programs written in different languages exchanging messages with code generated from a Protobuf description. Or of clients, whose code for interacting with a web-service is generated from its Swagger API description.
+ ++ Finally, code generation is also an excellent way to address shortcomings of configuration languages like Ansible, terraform, Nix and so on. + If your DSL files become very long, verbose or difficult to read, perhaps it is time to do some pre-processing in a more capable language, like Lua, and generate simpler DSL files instead.
+ +HTT takes a single Lua script as its argument, from here you can generate as many files as you want by calling the render
function.
Let's create a script, test-htt.lua
:
local tpl = require("//helloworld.htt")
+
+render(tpl.helloWorld, "result.txt")
+ Notice the call to require
. HTT templates are transparently
+ compiled to Lua as they are first imported.
TODO:link to reference on importing
+helloworld.htt
:
+
+ % @component helloWorld
+Hello, John!
+% @end
+ htt test-htt.lua
, then the result.txt
file will look like this:
+
+ Hello, John!
+ helloworld.htt
, and defined a component, helloWorld
, inside ittest-htt.lua
, which uses the render
call to render a component, helloWorld
, to a file: result.txt
.HTT uses plain Lua for all logic inside of templates. Any line which starts with %
is taken to be a line of Lua code injected verbatim into the compiled template.
+ +
What about '% @component'?
+
+ HTT interprets lines where text after %
starts with @
differently. These lines are taken to be directives, of which there are currently 2: @component
and @code
. We will cover this more below.
We can use Lua for loops to repeat a block of output like so:
+ +% for i = 1, 3 do
+hello!
+% end
+ hello!
+ hello!
+ hello!
+ We can use Lua if-statements to decide whether to render a block of output or not:
+ +% if 1 == 2 then
+Math is broken!
+% else
+Phew! Math works
+% end
+ Phew! Math works
+ You can use {{ ... }}
to embed the values of variables or other expressions in the output:
% local name = "Peter"
+Hello, {{name}}!
+2 + 2 is {{2 + 2}}
+ Hello, Peter!
+ 2 + 2 is 4
+ Arguments & components
+See the the "Context" section under "Components" for information on how to pass arguments to a component and how to access them.
+Components are the unit of abstraction. Complex outputs should be built by composing smaller components into larger ones.
+ + To call a component, we use this syntax: {{@ component ctx }}
. component
is any component and ctx
is the context we want to pass on to the component. The context should be a Lua table and it is the way to pass arguments to the component (including other components, if you want!).
Given this component:
+% @component child
+Sincerely hello
+ - Child
+% @end
+ Let's call it from another component.
+ +% @component parent
+let's call the child:
+{{@ child {} }}
+% @end
+ The output becomes:
+let's call the child:
+ Sincerely hello
+ - Child
+ When introducing the syntax to call a component, {{@ component ctx }}
, we touched on the ctx
being a Lua table.
We first define a component which uses the argument name
, if provided. Note that all arguments passed to a component are exposed via the ctx
variable. ctx.name
is simply the Lua way of accessing the attribute name
on the ctx
table.
% @component helloName
+Hello, {{ctx.name or "John Doe"}}!
+% @end
+ Calling the component without no name:
+{{@ helloName {} }}
+ Produces the following:
+Hello, John Doe!
+ Calling the component without no name:
+{{@ helloName {name = "Peter"} }}
+ Produces the following:
+Hello, Peter!
+ % @component child
+Sincerely hello
+ - Child
+% @end
+ % @component middleChild
+--middle child
+ {{@ child {} }}
+% @end
+ % @component callWithIndentation
+{{@ child {} }}
+ {{@ middleChild {} }}
+ {{@ child {} }}
+% @end
+ Rendering this out gives this output:
+ +Sincerely hello
+ - Child
+ --middle child
+ Sincerely hello
+ - Child
+ Sincerely hello
+ - Child
+ How indentation works
+The final indentation of any given line is the sum of:
+This sounds complex, but feels natural. Play around and you will see how indentation generally behaves as you would expect and want.
+There will be times, usually when mixing output and Lua lines, where you want to build up a single line of output across multiple lines in the template.
+ +If a line starts with ~>
(after whitespace), then the line is taken to be a continuation of the preceding line of output.
hello
+~>world!
+hello
+~> world...
+ This renders out to:
+helloworld!
+ hello world...
+ The following example builds up an array where elements are separated by ,
. Another example would be rendering function arguments or parameters.
% local sep = ctx.separator or ","
+[
+% for i, v in ipairs(ctx.lst) do
+% local is_last = (i == #ctx.lst)
+~> "{{v}}"{{not is_last and sep or ""}}
+% end
+~> ]
+ This renders out to:
+[ "one", "two", "three" ]
+ The line printing the separator uses a trick in Lua to simulate a ternary-if:
$test and $if-truthy or $otherwise
.