Liquor 2.0 language is developed with several goals in mind.
This specification is primarily targeted at language implementors.
Liquor 2.0 language is a templating language for text-based content, e.g. HTML pages. As Liquor is a templating language, it is useless without extension with domain-specific features from a host environment; it is similar to Lua in this aspect.
Liquor is meant to be statically compiled to another language for efficiency, typically to the one the host environment is executed in. It also provides sandbox restrictions, which allow Liquor code to invoke certain methods on the host objects, but only ones explicitly marked as scriptable.
Liquor is a statically scoped, weakly and dynamically typed imperative language with lazily evaluated expressions. As it is essentially a domain-specific language for string concatenation, it has an unusual syntax where code is embedded in a text stream, and a final result of executing a Liquor program is always a string. All language constructs are similarly centered around string manipulation.
Liquor has four basic elements: blocks, tags, interpolations and expressions. All four of these elements can be executed and return a value. Blocks, tags and interpolations always return a string value.
Liquor does not have non-local control flow constructs by itself, such as exceptions and function definitions. This was done intentionally in order to simplify the language.
Liquor has distinct compile-time and runtime error checking. There are no fatal runtime errors, i.e. a Liquor program always evaluates to some value.
Liquor has the following basic types: Null, Boolean, Integer, String, Tuple and External. A value of every type except External can be created from within a Liquor program. Values of type External can only be returned by the host environment.
All Liquor values are immutable; once created, a value cannot change.
There is exactly one value of type Null, and it is called null.
There are exactly two values of type Boolean, and they are called true and false.
The only values considered “falseful” in a conditional context are null and false. Every other value, including Integer 0 (zero), is considered “truthful”.
Type Integer denotes an integer value of unspecified size. Implementation may impose additional restrictions on the representable range of Integer type.
Type String denotes a sequence of Unicode codepoints. Note that codepoints are not the same as characters or graphemes; there may exist an implementation-specific way of handling composite characters. See also the relevant Unicode FAQ entry.
Type Tuple denotes a heteromorphic sequence of values.
Type External denotes an object belonging to the host environment.
Liquor supports exactly one implicit type conversion. In any context where a String value is expected, an Integer value can be provided. The Integer value will then be converted to a corresponding decimal ASCII representation without any leading zeroes.
Liquor features expressions, which can be used to perform computations with values. This section does not define a normative grammar; the full grammar is provided in section Grammar.
Order of evaluation of Liquor expressions is not defined. As every value is immutable, the value of the entire expression should not depend upon the order of evaluation. Implementation-provided functions must not access or mutate global state; implementation-provided tags may access or mutate global state, but this is highly discouraged.
All Liquor types except External can be specified as literals in expressions.
Identifiers null, true and false evaluate to the corresponding values.
Numeric literals evaluate to a corresponding Integer value, and always use base 10. Numeric literals can be specified with any amount of leading zeroes. There are no negative numeric literals.
String literals evaluate to a corresponding String value. String literals can be equivalently specified with single or double quotes. Strings support escaping with backslash, and there are exactly two escape sequences: one inserts a literal backslash, and the other one inserts a literal quote. More specifically, single quoted string supports escape sequences \\
and \'
, and double quoted string supports escape sequences \\
and \"
. A single backslash followed by any character not specified above is translated to a literal backslash.
Tuple literals evaluate to a corresponding Tuple value. Tuple literals are surrounded by square brackets and delimited with commas; that is, [ 1, 2, 3 ]
is a tuple literal containing three integer values, one, two and three, in that exact order.
Liquor supports unary and binary infix operators in expressions. All operators are left-associative.
Liquor operators are listed in order of precedence, from highest to lowest, by the following table:
[]
, .
, ()
, .()
-
, !
*
, /
, %
+
, binary -
==
, !=
, <
, <=
, >
, >=
&&
||
The following operators are infix and binary: *
, /
, %
, +
, -
, ==
, !=
, <
, <=
, >
, >=
, &&
, ||
.
The following operators are infix and unary: -
, !
.
The operators []
, .
, ()
, .()
are not infix and are provided in this table only to define precedence rules.
Arithmetic operators are *
(multiplication), /
(division), %
(modulo), +
(plus) and -
(minus; binary and unary).
All arithmetic operators except +
, whether unary or binary, require every argument to be of type Integer. If this is not the case, a runtime error condition (type error) is signaled.
Operator +
requires both arguments to be of the same type, and only accepts arguments of type Integer, String or Tuple. If any of the conditions is not satisfied, a runtime error condition (type error) is signaled. For arguments of type String or Tuple, the +
operator evaluates to the concatenation of left and right arguments in that order.
If the result of an arithmetic operation, except operator +
with non-Integer arguments, exceeds the range an implementation can represent, the behavior is implementation-defined.
Boolean operators are !
(not; unary), &&
(and) and ||
(or).
All boolean operators, whether unary or binary, convert each argument to type Boolean prior to evaluation. The rules of conversion are:
All boolean operators return a value of type Boolean. Binary boolean operators do not provide any guarantees on order or sequence of evaluation. However, a correct implementation which does not feature functions with side effects will not suffer from this behavior.
Comparison operators are ==
(equals), !=
(not equals), <
(less), <=
(less or equal), >
(greater) and >=
(greater or equal).
Operators ==
and !=
compare values by equality, not identity. Thus, the expression [ 1, 2 ] == [ 1, 2 ]
evluates to true. These operators never signal an error condition or implicitly convert types.
Operators <
, <=
, >
and >=
require both arguments to be of type Integer. If this is not the case, a runtime error condition (type error) is signaled. Otherwise, a corresponding value of type Boolean is returned.
Indexing operator is []
.
Indexing operator requires its left-hand side argument to be of type Tuple or External, and right-hand side argument to be of type Integer. If this is not the case, a runtime error condition (type error) is signaled.
If the left-hand side argument is of type External, the behavior is implementation-defined. A runtime error condition (external error) is signaled if the particular external value does not support indexing.
Indexing operator of form t[n]
evaluates to n-th value from tuple t with zero-based indexing. If n
is negative, then n+1-th element from the end of tuple is returned. For example, t[-1]
will evaluate to the last element of the tuple t.
If the requested element does not exist in the tuple, the indexing operator evaluates to null.
Identifiers can be bound to functions prior to compilation. Identifiers null, true and false cannot be bound to a function.
Functions are defined in an implementation-specific way. Functions can have zero to one unnamed formal parameters and any amount of named formal parameters. If an unnamed formal parameter is accepted, it is mandatory. Named formal parameters can be either mandatory or optional. Absence of a mandatory formal parameter will result in a compile-time error (argument error). Named formal parameter order is irrelevant.
Function calls have mandatory parentheses, and arguments are whitespace-delimited.
If a function call includes two named parameters with the same name, a compile-time error (syntax error) is raised.
If a hypothetical function substr has one unnamed formal parameter and two optional named formal parameters from and length, then all of the following expressions are syntactically valid and will not result in a compile-time error: substr("foobar")
, substr("foobar" from: 1)
, substr("foobar" from: 1 length:(5 - 2))
. The following expression, however, is syntactically valid but will result in a compile-time error: substr(from: 1)
.
Access operators are .
and .()
.
The .
form is syntactic sugar for .()
form without any arguments. That is, e.f
is completely equivalent to e.f()
.
Access operator requires its left-hand side argument to be of type External. If this is not the case, a runtime error condition (type error) is signaled.
Access operator of form e.f(arg kw: value)
evaluates to the result of calling method f of external object e with the corresponding arguments. Argument syntax is the same as for function calls.
This evaluation is done in an implementation-defined way. Access operator can evaluate to any type.
If the requested method does not exist in the external object or cannot successfully evaluate, a runtime error condition (external error) is signaled. Errors in the called method must not interrupt execution of the calling Liquor program.
Every identifier except null, true and false which is not bound to a function name is available to be bound as a variable name. Such identifier would evaluate to a value of the variable.
Variable definition and scoping will be further discussed in section Tags.
Referencing an undefined variable will result in a compile-time error (name error).
Filter expressions are a syntactic sugar for chaining method calls.
Filter expressions consist of a linear chain of function calls where n-th function’s return value is passed to n+1-th function’s unnamed parameter. Named parameters may be specified without parentheses within a corresponding chain element.
All functions used in a filter expression should accept an unnamed parameter. If this is not the case, a compile-time error (argument error) is raised. Semantics of mandatory and optional named parameters are the same as for regular function calls.
In essence, e | f a: 1 | g
is equivalent to g(f(e() a: 1))
.
A block is a chunk of plaintext with tags and interpolations embedded into it. Every Liquor program has at least one block: the toplevel one.
A block consisting only of plaintext would return its literal value upon execution. Thus, the famous Hello World program would be as follows:
Hello World!
This program would evaluate to a string Hello World!
.
A block can have other elements embedded into it. When such a block is executed, these elements are executed in lexical order and are replaced with the value returned by the element.
An interpolation is a syntactic construct of form {{ expr }}
which can be embedded in a block. The expression expr
should evaluate to a value of type String or Null; an implicit conversion might take place. If this is not the case, a runtime error condition (type error) is signaled.
If expr evaluates to a String, the interpolation returns it. Otherwise, the interpolation returns an empty string.
An example of using an interpolation would be:
The sum of two and three is: {{ 2 + 3 }}
This program would evaluate to a string The sum of two and three is: 5
.
A tag is a syntactic construct of form {% tag expr kw: arg do: %} ... {% end tag %}
. A tag has a syntax similar to a function call, but it can receive blocks of code as argument values and lazily evaluate passed expressions and blocks of code.
Tags have full control upon parameter evaluation. Tags can require arguments to be of a certain lexical form, e.g. a for
tag could require its unnamed formal parameter to be a lexical identifier.
To pass a block of code to a tag, the closing tag delimiter should immediately follow a parameter name. Everything from the closing tag delimiter to the matching opening tag delimiter should be parsed as a block and passed as a value of the corresponding parameter. After the matching opening tag delimiter, the parameter list is continued.
If a tag t
does not include any embedded blocks, it ends after a first matching closing tag delimiter. Otherwise, the tag ends after a first matching construct of the form {% end t %}
.
Unlike functions, tags can receive multiple named parameters with the same name. Named parameters of tags are a syntactic tool and should be thoroughly verified by the implementation. Specifying incorrect names or order of named parameters may result in a compile-time error (syntax error).
All of the following are examples of syntactically valid tags:
{% yield %}
{% if var > 10 do: %}
Var is greater than 10.
{% end if %}
{% for i in: [ 1, 2, 3 ] do: %}
Value: {{ i }}
{% end for %}
{% if length(params.test) == 1 then: %}
Test has length 1.
{% elsif: length(params.test) == 2 then: %}
Test has length 2.
{% else: %}
Test has unidentified length.
{% end if %}
{% capture "buffer" do: %}
This text will be printed twice.
{% end capture %}
{% yield from: "buffer" %}
{% yield from: "buffer" %}
The following Extended Backus-Naur Form grammar is normative. The native character set of Liquor is Unicode, and every character literal specified is an explicit codepoint.
Statement a to b
is equivalent to codepoint set which includes every codepoint from a to b inclusive. Statement a except b
means that both a and b are tokens which consist of exactly one codepoint, and every character satisfying a and not satisfying in b is accepted. Statement lookahead a
means that the current token should only be produced if the codepoint immediately following it satisfies a.
Strictly speaking, this grammar lies within GLR domain, but if, as it is usually the case, an implementation has separate lexer and parser, a LALR(1) parser could be used. This will be further explained in section Blocks.
Operator precedence table is provided in section Operators.
Inside a Tag or Interpolation body any Whitespace is used to separate adjacent tokens, but is otherwise ignored. The cases where naïvely removing Whitespace would cause ambiguity can be determined by watching for lookahead
clauses.
The Tag, TagFirstContinuation and EndTag production rules deviate from canonical LR(1) grammar structure. To parse these rules correctly, a LALR(1) parser should maintain a stack of tag identifiers and correctly decide on ambiguous reduction of rules Identifier and EndTag.
When the parser follows the second reduction for rule TagFirstContinuation, it should push the corresponding Tag Identifier on the top of the tag stack.
When the parser is about to decide whether it should reduce the sequence satisfying Identifier to EndTag or leave it as is, it should only reduce the sequence to EndTag if the Identifier part of the EndTag rule equals the value at the top of the tag stack. If this is the case, the topmost value is popped from the tag stack.
Liquor compiling process consists of three distinct parts: parsing, scope resolution and translation. Each stage includes exhaustive error checking; additionally, translation and scope resolution are heavily dependent on the defined tags and their behavior.
To ease development process, an implementation generally should not stop compilation after encountering an error. As an exception to the general rule, implementation must stop parsing and abandon any intermediate result after encountering a syntax error. Rationale to this behavior is that with Liquor’s interleaved structure, successful error recovery after parsing errors is unlikely.
Every error must carry precise location information: in particular, an error location must feature line, start column and end column.
The following algorithm can be used to calculate precise location information for every Unicode character in the source code:
This algorithm, unlike the rest of Liquor, is specified in terms of characters and not codepoints. This means that an implementation must recognize surrogate pairs and compose them into one character.
Syntax error will be signaled upon encountering any of the following conditions:
Syntax errors must include source location information and point to the exact token which caused the error.
Argument error will be signaled upon encountering any of the following conditions:
Argument errors must include source location information and point either to the exact parameter which caused the error, or to the argument list in case of a missing parameter.
Name error will be signaled upon encountering any of the following conditions:
Name errors must include source location information and point to the exact token which caused the error.
Tags control every aspect of scope construction and resolution.
Basically, tags can perform three scope-related actions: declare a variable, assign a variable and create a nested scope.
Declaring a variable binds the identifier to a value. To declare a variable, the identifier should not be bound in the current scope. If this is not the case, a compile-time error (name error) is raised. If the identifier is bound in an outer scope, it will be rebound in the current scope. Such a binding ceases to exist when the current scope is left.
Assigning a variable, similarly to accessing, requires the variable to be declared in any of the scopes. Assigning a variable changes its value in the innermost scope.
Creating a nested scope allows for shadowing of the variables. Tags must only execute contents of the passed blocks in a nested scope. Passed expressions are always executed in the tag’s scope. A tag must ensure that every scope it created will be left before the tag will finish executing.
An implementation should have a way to inject variables into the outermost scope.
Evaluation of Liquor programs follows lexical order for blocks, and is undefined for expressions. As all expressions are pure, this does not result in ambiguity.
A Liquor program always evaluates to a string. Liquor recognizes the value of and attempts to produce sensible output even for partially invalid programs; to keep the codebase manageable, the runtime environment must report all runtime errors to the programmer.
A type error arises when a value of certain type(s) is expected in a context, but a value of a different type is provided. In this case, the runtime records the error and substitutes the value for a zero value, respectively for every type:
An external error arises when an unknown external method is called, or there is a problem evaluating the external method. In this case, the runtime records the error and returns null instead.
The dummy external is an external object which performs no operation when any method is called on it and returns null.
Implementations must implement every builtin tag and function mentioned in this section. Implementations may implement any additional tags, but must not alter behavior of the described ones.
Tag declare has one valid syntactic form:
{% declare var = expr %}
Declare binds the name var to the result of executing expr in the current scope. If var is already bound in current scope, declare mutates the binding. If var is already bound in an outer scope, declare creates a new binding in the current scope.
The declare tag itself evaluates to an empty string.
Tag assign has one valid syntactic form:
{% assign var = expr %}
Assign binds the name var to the result of executing expr in the current scope. If var is already bound, assign mutates the binding.
The assign tag itself evaluates to an empty string.
Tag for has two valid syntactic forms:
{% for var in: list do: %}
code
{% end for %}
{% for var from: lower-limit to: upper-limit do: %}
code
{% end for %}
In the for..in form, this tag invokes code with var bound to each element of list sequentally. If list is not a Tuple, a runtime error condition (type error) is signaled.
In the for..from..to form, this tag invokes code with var bound to each integer between lower-limit and upper-limit, inclusive. If lower-limit or upper-limit is not an Integer, a runtime error condition (type error) is signaled.
Both forms of the tag also bind a special var_loop variable to the [for loop external]{#for-external}.
The for tag evaluates to the concatenation of the values its code has evaluated to.
The for loop external is an External that allows to query the current state of the loop. It provides the following parameterless methods:
length - index - 1
.index == 0
.index == length - 1
.Tag if has one valid syntactic form:
{% if cond-1 then: %}
code-1
[{% elsif: cond-2 then: %}
code-2] ...
[{% else: %}
code-else]
{% end if %}
This tag can optionally have any amount of elsif clauses and only one else clause.
The if tag sequentally evaluates each passed condition cond-1, cond-2, … until a truthful value is computed. Then, it executes the corresponding code. If none of the conditions evaluate to a truthful value, the tag executes code-else if it exists.
The if tag returns the result of evaluating the corresponding code block, or an empty string if none of the blocks were executed.
Tag unless has one valid syntactic form:
{% unless cond then: %}
code
{% end unless %}
The unless tag evaluates cond. Unless it yields a truthful, code is also evaluated.
The unless tag returns the result of evaluating code, or an empty string.
Tag capture has one valid syntactic form:
{% capture var = %}
code
{% end capture %}
The capture tag evaluates code and binds the name var to the result. If var is already bound, capture mutates the binding.
The capture tag returns an empty string.
Tag content_for has one valid syntactic form:
{% content_for "handle" capture: %}
code
{% end content_for %}
The content_for tag accepts a String handle as an immediate value. It evaluates code and assigns the result to the handle handle, which must be stored in an implementation-specific way.
The content_for tag returns an empty string.
See also notes on Layout implementation.
Tag yield has three valid syntactic forms:
{% yield %}
In this form, the yield tag evaluates to the content of inner template.
{% yield "handle" %}
{% yield "handle" if_none: %}
code
{% end yield %}
The yield tag accepts a String handle as an immediate value. If a string with handle handle was captured previously with {% content_for %}, then yield returns that string. If there is no captured string with that handle, yield either returns the result of evaluating if_none block if it exists, or an empty string.
See also notes on Layout implementation.
Tag include has one valid syntactic form:
{% include "partial_name" %}
The include tag accepts a String partial_name as an immediate value. It lexically includes the code of partial template partial_name in a newly created scope.
The include tag must not allow infinite recursion to happen. If such a condition is encountered, a compile-time error (syntax error) is signaled.
See also notes on Layout implementation.
Liquor offers a number of builtin functions. Their formal parameters are described using a shorthand notation:
fn_name(unnamed-arg-type kwarg1: kwarg1-type [kwarg2: kwarg2-type, kwarg2-alt-type])
In this case, function fn_name has an unnamed parameter accepting value of type unnamed-arg-type, a mandatory keyword parameter kwarg1 accepting values of type kwarg1-type, and an optional keyword parameter kwarg2 accepting values of either type kwarg2-type or kwarg2-alt-type.
is_empty(Any)
Returns true iff the unnamed argument is one of null, "”, [].
size(String, Tuple)
Returns the length of the unnamed argument as an integer.
size(String format: String)
Parses the unnamed argument as time in ISO8601 format, and reformats it using an implementation-defined strftime alike function.
to_number(String, Integer)
If the unnamed argument is an Integer, returns it. If it is a string, parses it as a decimal number, possibly with leading minus sign.
is_even(Integer)
Returns true if the unnamed argument is even, false otherwise.
is_odd(Integer)
Returns true if the unnamed argument is odd, false otherwise.
downcase(String)
Returns the unnamed argument, converted to lowercase, using the Unicode case folding.
upcase(String)
Returns the unnamed argument, converted to uppercase, using the Unicode case folding.
capitalize(String)
Returns the unnamed argument with its first character converted to uppercase, using the Unicode case folding.
starts_with(String pattern: String)
Returns true if the unnamed argument starts with pattern, false otherwise. No normalization is performed.
strip_newlines(String)
Returns the unnamed argument without any U+000A characters.
join(Tuple with: String)
Returns the concatenation of elements of the unnamed argument (which all must be Strings) interpsersed with the value of with.
split(String by: String)
Returns a tuple of fragments of the unnamed argument, extracted between occurences of by. If the unnamed argument is "”, returns [].
replace(String pattern: String replacement: String)
Returns the unnamed argument with all occurences of pattern replaced with replacement.
replace_first(String pattern: String replacement: String)
Returns the unnamed argument with the first occurence of pattern replaced with replacement.
remove(String pattern: String)
Returns the unnamed argument with all occurences of pattern removed.
remove_first(String pattern: String)
Returns the unnamed argument with the first occurences of pattern removed.
newline_to_br(String)
Returns the unnamed argument with <br>
inserted before every U+000A character.
url_escape(String)
Returns the unnamed argument, processed using the application/x-www-form-urlencoded encoding algorithm.
html_escape(String)
Returns the unnamed argument with &
, <
, >
, '
, "
and /
escaped to the correpsonding HTML entities.
html_escape(String)
h(String)
Like html_escape, but does not affect &
that is a part of an HTML entity.
strip_html(String)
Returns the unnamed argument with all HTML tags and comments removed.
decode_html_entities(String)
Returns the unnamed argument with all HTML entities replaced by the corresponding Unicode character.
compact(Tuple)
Returns the unnamed argument without null elements.
reverse(Tuple)
Returns the unnamed argument, reversed.
reverse(Tuple)
Returns the unnamed argument with only first instance of non-unique elements left.
min(Tuple [by: String])
Returns the minimal element of the unnamed argument. The ordering between values of different types is implementation-defined. Including an External in the unnamed argument may lead to a runtime error (type error).
If by is passed, the unnamed argument must consist only of External values. In this case, the ordering is performed by calling the method specified by by.
max(Tuple [by: String])
See the min function.
in_groups_of(Tuple size: Integer [fill_with: String, Boolean])
Returns the unnamed argument, split into tuples of size elements. If fill_with is passed, appends the value of fill_with to the last tuple, so that it is size elements big.
in_groups(Tuple count: Integer [fill_with: String, Boolean])
Returns the unnamed argument, split into count equally-sized (except the last one) tuples. If fill_with is passed, appends the value of fill_with to the last tuple, so that it is as big as the others.
includes(Tuple, External element: Any)
Returns true if the unnamed argument contains element, false otherwise. Not all externals support this operation; if an unsupported external is passed, runtime error condition (type error) is signaled.
index_of(Tuple, External element: Any)
Returns the index of element in the unnamed argument or null if it does not contain element. Not all externals support this operation; if an unsupported external is passed, runtime error condition (type error) is signaled.
truncate(String [length: Integer omission: String])
Returns the unnamed argument, truncated to length (50 by default) characters and with omission (...
by default) appended.
truncate_words(String [length: Integer omission: String])
Returns the unnamed argument, truncated to length (15 by default) words and with omission (...
by default) appended.
html_truncate(String [length: Integer omission: String])
Returns the unnamed argument, truncated to length (50 by default) characters inside HTML text node and with omission (...
by default) appended to the last HTML text node.
html_truncate_words(String [length: Integer omission: String])
Returns the unnamed argument, truncated to length (15 by default) words inside HTML text node and with omission (...
by default) appended to the last HTML text node.
In order to support the content_for and yield tags, the runtime maintains a dictionary associating the handles provided to content_for with the content. This dictionary is shared between all layout(s) and the innermost template; the templates are evaluated from the innermost to outermost.