hairy 0.2.1

Compiled text templates (not unlike Mustache and Handlebars), with support for expressions and custom functions inside such expressions.
Documentation
//! The `hairy` crate provides text templates, not unlike mustache and handlebars (and the earlier ctemplate original), but a bit different, focussing on error handling.
//! Scoping is different: variables must specify explicitly which scope they use (by using `a.b` syntax). This is to avoid implicit behaviour when variables are (accidentally) overwritten.
//! To catch errors early on, optionally types can be added, which are checked compile time.
//! All errors are treated as hard errors and are reported (including a stack trace).
//! So missing values and typing errors are not silently ignored.
//! Also, the templates support evaluation: `{{=expression}}`.
//! The supported expressions are from the [`expry`] crate, which allows executing expression on binary encoded JSON-like values (with support for defining custom functions).
//! Auto-escaping is applied to the output to avoid security issues (such as letting user input be exected as javascript).
//!
//! # Syntax
//!
//! In short the features and syntax:
//! - Variables can be declared at top of an input file with `type key: expry` pairs (JSON is a valid expry).
//!   `type` can be either `inline` (value is replaced during compilation of the template) or `default`
//!   (which is a default value that is added to the evaluation time value if the field does not exists).
//!   The first non-parsable line signals the end of the variables.
//! - Values can be outputted with `{{=value}}` (supports expressions, see below). Escape mode can
//!   be specified with `{{=value:escape_mode}}`. Values are processed by a user-definable escaper
//!   that can take the escape mode into account. Normally only strings and numbers are displayed,
//!   and the `null` value is considered a nop (without generating an error). Other values are considered
//!   an error, except when the `sjs` (script-js) or `js` escape modes are used.
//! - Boolean conditionals with `{{if expr}}contents{{end}}`. Contents is displayed if value evaluates
//!   to `true`. Keywords `else` and `elseif` are also supported:
//!   `{{if expr1}}a{{elseif expr2}}b{{else}}c{{endif}}`. If `expr` is `true` the then branch is outputted,
//!   if `null` or `false` the else branch is taken, otherwise an error is generated.
//! - Let conditionals with `{{if let x = expr}}a{{end}}`. If `expr` is non `null` contents
//!   is displayed, otherwise (on non error) the else branch is displayed. `else` and `elseif` are
//!   supported.
//! - Iterators with `{{for variable in name}}content{{endfor}}`, which can be used to iterate over
//!   (arrays of) any type. The contents of the array is available in the loop body under key `variable`.
//!   If `variable` is of the form `(i,v)`, then the current index number is stored in the variable `i`.
//! - Template definition with `{{define name}}content{{enddefine}}`. Templates can have optional default
//!   values (in an object) with `{{define name defaults object}}content{{enddefine}}`. Defaults are
//!   resolved at template compile time, using the global context given to the compile function;
//! - Template instantiation with `{{call name}}` or `{{call name with value}}`. `name` can be an
//!   expression. If the name starts with a `*`, name can be an expression that resolves to a string
//!   (which is treated as a template name). If the name starts with `**`, name should be an expression
//!   that resolves to a binary form template code (as produced by the compile functions). If the `with`
//!   syntax is used, only the data specified in `value` is passed along (so the current context is not
//!   automatically transfered/merged, to do that use for value `{...this, key: value}`). This is done to
//!   avoid errors.
//! - Error handling: missing fields are always considered an error.
//!   Errors can be suppressed with the `expr ??? alternative` try syntax (on error in `expr`,
//!   `alternative` will be executed). Shorthand for `expr ??? null` is `expr ???`, which can be used in
//!   loops with `{{for i in someField???}}`, which ignores errors if the field is not found. Same for
//!   conditionals.
//!
//! Some other template systems have the ability to iterate without variables, in effect making 
//! the object fields available in the current context. Although this is at first appealing,
//! this makes these templates error-prone. Types can differ, depending on the presence of fields
//! in the object iterated over (which can change for each iteration). One of the design goals of hairy
//! is error handling. To fall back to the value in the global context on error such as field not
//! found, the following expression can be used: `{{iteration_variable.foo???foo}}`. This makes the
//! whole expression explicit in its intent.
//!
//! Note that if extra variables are introduced with a loop or a `if let`, they do not overwrite
//! the field with the same name in the `this` object. The original value can be retrieved by using
//! `this.name` if `name` was given another value.
//!
//! # Escaping
//!
//! Normally, all strings that are dynamically outputted (using an evaluating `{{=..}}`
//! statement) are automatically 'escaped' using the escaper as given to an argument to the
//! `hairy_eval` function. However, often you will want different escape modes depending on the
//! input context. For example in HTML, output may not normally contain `<` or `>`, but output
//! inside attributes in HTML tags are normally escaped like URLs. Although users can specify for
//! every output the escape mode by appending a `:html` or `:url` escaping mode to an expression,
//! this is error-prone. Auto escaping is therefore a safer alternative, by automatically deciding
//! the escape mode from the input. The general advise is to escape defensively. `hairy` functions
//! support an argument that is used to look up the escaping mode for a certain position in the input.
//!
//! # Easy interface
//!
//! The 'easy' interface if you just want to quickly use an HTML template, with auto escaping of the
//! input, and returning a nicely formatted error that can be presented to the user. To evaluate, use
//! [`hairy_eval_html`], so the proper escaper is used.
//!
//! Note that although [`hairy_compile_html`] and [`hairy_eval_html`] are easier to use, they are
//! somewhat slower. For top performance please use the [`hairy_compile`] and the [`hairy_eval`]
//! functions.
//!
//! # Example using the simple interface
//!
//! ```
//! use hairy::*;
//! use expry::*;
//! use expry_macros::*;
//! use std::io::Write;
//! let template = r#"foobar = {{=this.foovar .. this.barvar}}"#;
//! let mut options = HairyOptions::new();
//! let value = value!({
//!   "foovar": "foo",
//!   "barvar": "bar",
//! }).encode_to_vec();
//! options.set_named_dynamic_values(&[("this",value.to_ref())]);
//! let result = hairy_compile_html(template, "test.tpl", &(&options).try_into().unwrap());
//! match result {
//!   Ok(parsed) => {
//!     match hairy_eval_html(parsed.to_ref(), &(&options).try_into().unwrap()) {
//!       Ok(output) => { std::io::stdout().write_all(&output); },
//!       Err(err) => { eprintln!("{}", err); },
//!     }
//!   },
//!   Err(mut err) => {
//!     eprintln!("{}", err);
//!     panic!(); // to check this example
//!   }
//! }
//! ```
//!
//! # Enclosing other template files
//!
//! The value can contain other template compiled with `hairy_compile_html`. So if value `body`
//! contains the bytecode of such a template, that template can be invoked from the 'main' template
//! by using the template expression `{{call ((body))()}}`.
//!
//! ```
//! use hairy::*;
//! use expry::*;
//! use expry_macros::*;
//! use std::io::Write;
//! let main_template = r#"<html><title>{{=value.title}}</title><body>{{call ((value.body))({value.title,value.foobarvar})}}</body></html>"#;
//! let mut options = HairyCompileOptions::new();
//! options.set_dynamic_value_name_and_types(&[("value",expry_type!("{title:string,body:string,foobarvar:string}"))]);
//! let main = hairy_compile_html(main_template, "main.tpl", &options).unwrap();
//! let child_template = r#"<p>title of this page = {{=this.title}}</p><p>foobar = {{=this.foobarvar}}"#;
//! options.set_dynamic_value_name_and_types(&[("this", expry_type!("{title:string,foobarvar:string}"))]);
//! let child = hairy_compile_html(child_template, "child.tpl", &options).unwrap();
//! let value = value!({
//!   "body": child,
//!   "foobarvar": "foobar",
//!   "title": "my title",
//! }).encode_to_vec();
//! let mut options = HairyEvalOptions::new();
//! options.values = vec![value.to_ref()];
//! match hairy_eval_html(main.to_ref(), &options) {
//!   Ok(output) => { std::io::stderr().write_all(&output).unwrap(); },
//!   Err(mut err) => {
//!     eprintln!("{}", err);
//!     panic!(); // to check this example
//!   },
//! }
//! ```
//!
#![allow(dead_code)]

extern crate alloc;
use alloc::format;
use alloc::vec;
use alloc::vec::Vec;
use alloc::string::String;

use core::fmt;
use core::fmt::Write;

pub use expry::{BytecodeVec,BytecodeRef,EncodedValueVec,EncodedValueRef,NoCustomFuncs};
use expry::*;

const MAX_TEMPLATE_RECURSION: usize = 64;
const MAX_TEMPLATE_ARGS: u64 = 32;

/// The errors that can occur during Hairy template parsing. It can either be a lexer error (so a part of
/// the input can not be translated to a token), or a parser error (a different token expected).
/// As Hairy templates can contain expressions, these expressions can also trigger errors. These
/// can either be in the parsing of the expressions, or during optimizing (which causes
/// EvalErrors). Other errors are reporting using `Other`.
#[derive(Copy, Clone)]
pub enum HairyParserError<'a> {
    Parser(&'a str),
    Expr(CompileErrorDescription<'a>),
    Type(&'a str),
    Eval(EvalError<'a>),
    Other(&'a str),
}

#[derive(Clone, Debug)]
pub struct HairyCompileError<'a> {
    expr: &'a str,
    error: HairyParserError<'a>,
    start: usize, // counted from the back of the input string
    end: usize, // counted from the back of the input string
    extra: Option<(usize, usize)>,
    line_context: Option<LineContext>,
    extra_line_no: u32,
}
impl<'a> HairyCompileError<'a> {
    fn get_line_context<'s>(&'s mut self) -> &'s LineContext {
        if self.line_context.is_none() {
            self.line_context = Some(LineContext::new(self.expr));
        }
        self.line_context.as_ref().unwrap()
    }
}

impl<'a> core::fmt::Display for HairyCompileError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} [{}-{}]", self.error, self.start, self.end)
    }
}

impl<'a> From<EncodingError> for HairyParserError<'a> {
    fn from(_: EncodingError) -> Self {
        HairyParserError::Other("internal encoding error")
    }
}
impl<'a> fmt::Debug for HairyParserError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self)
    }
}
impl<'a> core::fmt::Display for HairyParserError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            HairyParserError::Expr(err) => write!(f, "{} (during parsing of the expression)", err),
            HairyParserError::Parser(s) => write!(f, "{}", s),
            HairyParserError::Other(s) => write!(f, "{}", s),
            HairyParserError::Eval(s) => write!(f, "{}", s),
            HairyParserError::Type(s) => write!(f, "{}", s),
        }
    }
}

type Expr<'a> = &'a str;
type Text<'a> = &'a str;
type Name<'a> = &'a str;
type Bytes<'a> = &'a [u8];

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
enum HairyToken<'a> {
    EndOfInput(),
    Text(Text<'a>),
    Loop(Expr<'a>, usize, Name<'a>, Name<'a>), // expr, offset of expr variable, index variable, contents variable
    Conditional(Option<Name<'a>>, Expr<'a>, usize), // optional name for 'if let x = ' construct, expr
    ElseConditional(Option<Name<'a>>, Expr<'a>, usize), // optional name for 'elseif let x = ' construct, expr
    Else(),
    BlockEnd(Name<'a>),
    Eval(Expr<'a>, usize, Bytes<'a>), // .1: offset of expression (for error messages)
    CallTemplate(Name<'a>, bool, usize, &'a [(Expr<'a>, usize)]), // .1: offset of name/expression (for error messages)
    TemplateDef(Name<'a>, &'a [(Name<'a>, Text<'a>, usize)]),
    Arguments(&'a [(Name<'a>, Text<'a>, usize)]),
    Inline(Name<'a>, Text<'a>, usize),
}

/// This spanner will try to recognize the content of a Hairy template (`{{..}}`). It counts the number of
/// nested `{` and `}`. It does have support for recognizing embedded strings, so `{{foo != "}"}}` will work. Raw strings or the like are not supported, instead you can use `{foo != r#" \" "#}`. Stops at first `}` encountered that is not started inside the expression.
pub struct TemplateTagContentSpanner {
    prev_was_escape: bool,
    string: bool,
    nested: u32,
}

impl Spanner for TemplateTagContentSpanner {
    fn next(&mut self, b: char) -> bool {
        if !self.prev_was_escape && !self.string && b == '{' {
            self.nested += 1;
        }
        if !self.prev_was_escape && !self.string && b == '}' {
            if self.nested == 0 {
                return false;
            }
            self.nested -= 1;
        }
        if !self.prev_was_escape && b == '"' {
            self.string = !self.string;
        }
        self.prev_was_escape = !self.prev_was_escape && b == '\\';
        true
    }
    fn valid(&mut self, _len: usize) -> bool {
        self.nested == 0 && !self.string && !self.prev_was_escape
    }
}
impl TemplateTagContentSpanner {
    pub fn new() -> Self {
        Self {
            prev_was_escape: false,
            string: false,
            nested: 0,
        }
    }
}

impl Default for TemplateTagContentSpanner {
    fn default() -> Self {
        Self::new()
    }
}

/// Recognizes expressions by counting the `{`, `}`, `[`, `]`, `(`, `)`, and `"`. Does not support advanced
/// string literals such as raw strings (e.g. `r##" " "##`). Stops at first `]` or `}` or `)` encountered
/// that is not started inside the expression.
pub struct ExprySpanner<'a> {
    prev_was_escape: bool,
    string: bool,
    nested: u32,
    stop: &'a [char],
}

impl<'a> Spanner for ExprySpanner<'a> {
    fn next(&mut self, b: char) -> bool {
        if !self.prev_was_escape && !self.string && (b == '{' || b == '[' || b == '(') {
            self.nested += 1;
        }
        if self.nested == 0 && !self.prev_was_escape && !self.string && self.stop.contains(&b) {
            return false;
        }
        if !self.prev_was_escape && !self.string && (b == '}' || b == ']' || b == ')') {
            if self.nested == 0 {
                return false;
            }
            self.nested -= 1;
        }
        if !self.prev_was_escape && b == '"' {
            self.string = !self.string;
        }
        self.prev_was_escape = !self.prev_was_escape && b == '\\';
        true
    }

    fn valid(&mut self, len: usize) -> bool {
        len > 0 && self.nested == 0 && !self.string && !self.prev_was_escape
    }
}

impl<'a> ExprySpanner<'a> {
    pub fn new(stop: &'a [char]) -> Self {
        Self {
            prev_was_escape: false,
            string: false,
            nested: 0,
            stop,
        }
    }
}

impl Default for ExprySpanner<'static> {
    fn default() -> Self {
        Self::new(&[])
    }
}

fn find_subsequence<T>(haystack: &[T], needle: &[T]) -> Option<usize> where for<'a> &'a [T]: PartialEq {
    haystack.windows(needle.len()).position(|window| window == needle)
}

fn hairy_tokenize<'a,'b,'c,'e>(
    reader: &mut &'b str,
    context: &mut ParserContext<'a,'b,'c,'e>,
) -> Result<(HairyToken<'b>,usize), (HairyParserError<'b>,usize,usize)> {
    const EXPECT_HAIRY_CLOSE : HairyParserError = HairyParserError::Parser("expecting closing `}}` after a non empty expression");
    fn read_enters(reader: &mut &str) -> usize {
        let mut retval = 0;
        if reader.accept("\r") {
            retval += 1;
        }
        if reader.accept("\n") {
            retval += 1;
        }
        retval
    }
    while reader.accept("{{!") {
        let mut embedded_spanner = TemplateTagContentSpanner::new();
        embedded_spanner.span(reader);
        if !reader.accept("}}") {
            return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
        }
        read_enters(reader);
    }
    let remaining = reader.len();
    if reader.is_empty() {
        return Ok((HairyToken::EndOfInput(), remaining));
    }
    let variable_matcher = |c: char| c == '$' || c == '_' || c == '\\' || c.is_ascii_alphanumeric();
    //let err_pos = *pos;
    if reader.accept("{{for ") {
        let mut embedded_spanner = TemplateTagContentSpanner::new();
        let expr = embedded_spanner.span(reader).map_or("", |x| x);
        if expr.is_empty() || !reader.accept("}}") {
            return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
        }
        let content_end = reader.len() + 2;
        let content_end_offset = 2 + read_enters(reader);
        const IN_LEN : usize = 4;
        if let Some((variable, expr)) = expr.split_once(" in ") {
            let mut variable = variable.trim_start();
            let mut index_variable = "";
            let mut content_variable = "";
            if strparse(variable).accept('(').span(is_space, 0).span(variable_matcher, 1).matched(&mut index_variable).span(is_space, 0).accept(",").span(is_space, 0).span(variable_matcher, 1).matched(&mut content_variable).span(is_space, 0).accept(')').valid(&mut variable) {
                return Ok((HairyToken::Loop(expr, content_end_offset, index_variable, content_variable), remaining));
            }
            if variable.trim().contains(|x| !variable_matcher(x)) {
                const EXPECT_HAIRY_VARIABLE : HairyParserError = HairyParserError::Parser("variable name may only consists of alphanumeric chars, and `$` and `_`");
                return Err((EXPECT_HAIRY_VARIABLE, variable.len() + IN_LEN + expr.len() + content_end, IN_LEN + expr.len() + content_end));
            }
            return Ok((HairyToken::Loop(expr, content_end_offset, "", variable), remaining));
        } else {
            const EXPECT_HAIRY_VARIABLE : HairyParserError = HairyParserError::Parser("expected variable name (indicated with `{{for variable in expr}}`)");
            return Err((EXPECT_HAIRY_VARIABLE, expr.len() + content_end, content_end));
        }
    }
    let elseif = reader.accept("{{elseif ");
    if elseif || reader.accept("{{if ") {
        let mut embedded_spanner = TemplateTagContentSpanner::new();
        let expr = embedded_spanner.span(reader).map_or("", |x| x);
        if expr.is_empty() || !reader.accept("}}") {
            return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
        }
        let content_end = reader.len() + 2;
        let content_end_offset = 2 + read_enters(reader);
        if let Some((_, expr)) = expr.split_once("let ") {
            const ASSIGNMENT_LEN : usize = 4;
            if let Some((variable, expr)) = expr.split_once(" = ") {
                if variable.trim().contains(|x| !variable_matcher(x)) {
                    const EXPECT_HAIRY_VARIABLE : HairyParserError = HairyParserError::Parser("variable name may only consists of alphanumeric chars, and `$` and `_`");
                    return Err((EXPECT_HAIRY_VARIABLE, variable.len() + ASSIGNMENT_LEN + expr.len() + content_end, ASSIGNMENT_LEN + expr.len() + content_end));
                }
                if elseif {
                    return Ok((HairyToken::ElseConditional(Some(variable), expr, content_end_offset),remaining));
                } else {
                    return Ok((HairyToken::Conditional(Some(variable), expr, content_end_offset),remaining));
                }
            } else {
                const EXPECT_HAIRY_VARIABLE : HairyParserError = HairyParserError::Parser("expected variable name (indicated with `{{if let variable = expr}}`)");
                return Err((EXPECT_HAIRY_VARIABLE, expr.len() + content_end, content_end));
            }
        } else if elseif {
            return Ok((HairyToken::ElseConditional(None, expr, content_end_offset),remaining));
        } else {
            return Ok((HairyToken::Conditional(None, expr, content_end_offset),remaining));
        }
    }
    if reader.accept("{{else}}") {
        read_enters(reader);
        return Ok((HairyToken::Else(),remaining));
    }
    if reader.accept("{{end") {
        let mut embedded_spanner = TemplateTagContentSpanner::new();
        let expr = embedded_spanner.span(reader).map_or("", |x| x);
        if !reader.accept("}}") {
            return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
        }
        read_enters(reader);
        return Ok((HairyToken::BlockEnd(expr),remaining));
    }
    if reader.accept("{{inline ") {
        let mut embedded_spanner = TemplateTagContentSpanner::new();
        let expr = embedded_spanner.span(reader).map_or("", |x| x);
        if expr.is_empty() || !reader.accept("}}") {
            return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
        }
        let content_end = reader.len() + 2;
        let content_end_offset = 2 + read_enters(reader);
        let check = |name: &str, extra_len: usize| -> Result<(),(HairyParserError, usize, usize)> {
            let matcher = |c: char| c == '$' || c == '_' || c == '\\' || c.is_ascii_alphanumeric();
            if name.trim().contains(|x| !matcher(x)) {
                const HAIRY_ERROR : HairyParserError = HairyParserError::Parser("template name may only consists of alphanumeric chars, and `$` and `_`");
                return Err((HAIRY_ERROR, name.len() + extra_len + content_end, extra_len + content_end));
            }
            Ok(())
        };
        if let Some((name, expr)) = expr.split_once('=') {
            check(name, 1 + expr.len())?;
            let name = name.trim();
            return Ok((HairyToken::Inline(name, expr, expr.len() + content_end_offset),remaining));
        } else {
            return Err((HairyParserError::Parser("expecting = after variable name"), expr.len() + content_end, content_end));
        }
    }
    if reader.accept("{{arguments") {
        let mut embedded_spanner = TemplateTagContentSpanner::new();
        let mut expr = embedded_spanner.span(reader).map_or("", |x| x);
        if expr.is_empty() || !reader.accept("}}") {
            return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
        }
        let content_end = reader.len() + 2;
        let content_end_offset = 2 + read_enters(reader);
        let mut args = ScopedArrayBuilder::new(context.allocator);
        expr = expr.trim_start();
        while let Some(variable) = expr.span_fn(&mut |c| c == '_' || c.is_ascii_alphanumeric()) {
            if !expr.accept(":") {
                // FIXME: in strict mode, require type, so return: return Err((HairyParserError::Parser("expecting ':' after label name"), expr.len() + content_end, content_end));
                args.push((variable.trim(), "*", expr.len() + content_end_offset));
                if !expr.accept(",") {
                    break;
                }
                expr = expr.trim_start();
                continue;
            } else {
                let mut embedded_spanner = ExprySpanner::new(&[',',')']);
                if let Some(type_spec) = embedded_spanner.span(&mut expr) {
                    args.push((variable.trim(), type_spec, expr.len() + content_end_offset));
                    if !expr.accept(",") {
                        break;
                    }
                    expr = expr.trim_start();
                } else {
                    return Err((HairyParserError::Parser("expecting value after label"), expr.len() + content_end, content_end));
                }
            }
        }
        let args = args.build();
        return Ok((HairyToken::Arguments(args),remaining));
    }
    if reader.accept("{{define ") {
        let mut embedded_spanner = TemplateTagContentSpanner::new();
        let expr = embedded_spanner.span(reader).map_or("", |x| x);
        if expr.is_empty() || !reader.accept("}}") {
            return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
        }
        let content_end = reader.len() + 2;
        let content_end_offset = 2 + read_enters(reader);
        let check = |name: &str, extra_len: usize| -> Result<(),(HairyParserError, usize, usize)> {
            let matcher = |c: char| c == '$' || c == '_' || c == '\\' || c.is_ascii_alphanumeric();
            if name.trim().contains(|x| !matcher(x)) {
                const HAIRY_ERROR : HairyParserError = HairyParserError::Parser("template name may only consists of alphanumeric chars, and `$` and `_`");
                return Err((HAIRY_ERROR, name.len() + extra_len + content_end, extra_len + content_end));
            }
            Ok(())
        };
        if let Some((name, mut expr)) = expr.split_once('(') {
            check(name, 1 + expr.len())?;
            let mut args = ScopedArrayBuilder::new(context.allocator);
            expr = expr.trim_start();
            while let Some(variable) = expr.span_fn(&mut |c| c == '_' || c.is_ascii_alphanumeric()) {
                if !expr.accept(":") {
                    // FIXME: in strict mode, require type, so return: return Err((HairyParserError::Parser("expecting ':' after label name"), expr.len() + content_end, content_end));
                    args.push((variable.trim(), "*", expr.len() + content_end_offset));
                    if !expr.accept(",") {
                        break;
                    }
                    expr = expr.trim_start();
                    continue;
                } else {
                    let mut embedded_spanner = ExprySpanner::new(&[',',')']);
                    if let Some(type_spec) = embedded_spanner.span(&mut expr) {
                        args.push((variable.trim(), type_spec, expr.len() + content_end_offset));
                        if !expr.accept(",") {
                            break;
                        }
                        expr = expr.trim_start();
                    } else {
                        return Err((HairyParserError::Parser("expecting value after label"), expr.len() + content_end, content_end));
                    }
                }
            }
            if !expr.accept(")") {
                return Err((HairyParserError::Parser("expecting ')' after arguments"), expr.len() + content_end, content_end));
            }
            let args = args.build();
            return Ok((HairyToken::TemplateDef(name.trim(), args),remaining));
        }
        check(expr, 0)?;
        return Ok((HairyToken::TemplateDef(expr.trim(), &[]),remaining));
    }
    if reader.accept("{{call ") {
        let mut embedded_spanner = TemplateTagContentSpanner::new();
        let mut expr = embedded_spanner.span(reader).map_or("", |x| x);
        if expr.is_empty() || !reader.accept("}}") {
            return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
        }
        let content_end = reader.len() + 2;
        let content_end_offset = 2 + read_enters(reader);
        let dynamic = expr.accept("(");
        let mut matcher = |c: char| c == '$' || c == '_' || c == '\\' || c.is_ascii_alphanumeric();
        if let Some(name) = if dynamic { ExprySpanner::default().span(&mut expr) } else { expr.span_fn(&mut matcher) } {
            if dynamic && !expr.accept(")") {
                return Err((HairyParserError::Parser("expecting ')' to close dynamic expression for name"), expr.len() + content_end, content_end));
            }
            if !expr.accept("(") {
                return Err((HairyParserError::Parser("expecting '(' for arguments"), expr.len() + content_end, content_end));
            }
            let offset = 1 + expr.len() + content_end_offset;
            let mut args = ScopedArrayBuilder::new(context.allocator);
            expr = expr.trim_start();
            while let Some(expression) = ExprySpanner::new(&[',',')']).span(&mut expr) {
                args.push((expression.trim_start(), expr.len() + content_end_offset));
                if !expr.accept(",") {
                    break;
                }
                expr = expr.trim_start();
            }
            if !expr.accept(")") {
                return Err((HairyParserError::Parser("expecting ')' after arguments"), expr.len() + content_end, content_end));
            }
            let args = args.build();
            return Ok((HairyToken::CallTemplate(name.trim(), dynamic, offset + usize::from(dynamic), args),remaining));
        }
        let matcher = |c: char| c == '$' || c == '_' || c == '\\' || c.is_ascii_alphanumeric();
        if expr.trim().contains(|x| !matcher(x)) {
            const HAIRY_ERROR : HairyParserError = HairyParserError::Parser("template name may only consists of alphanumeric chars, and `$` and `_` (and start with `*` or `**`)");
            return Err((HAIRY_ERROR, expr.len() + content_end, content_end));
        }
        return Ok((HairyToken::CallTemplate(expr, false, content_end_offset, &[]), remaining));
    }
    if reader.accept("{{=") {
        let mut embedded_spanner = TemplateTagContentSpanner::new();
        let expr = embedded_spanner.span(reader).map_or("", |x| x);
        if expr.is_empty() || !reader.accept("}}") {
            return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
        }
        let content_end_offset = 2;
        const ESCAPE_MODE_LEN : usize = 1;
        if let Some((expr, escape_mode)) = expr.rsplit_once(':') {
            if escape_mode.chars().all(|c| c.is_ascii_alphanumeric()) {
                return Ok((HairyToken::Eval(expr, escape_mode.len()+ESCAPE_MODE_LEN+content_end_offset, escape_mode.as_bytes()),remaining));
            }
        }
        return Ok((HairyToken::Eval(expr, content_end_offset, b""),remaining));
    }
    if reader.accept("{{") {
        const EXPECT_HAIRY_COMMAND : HairyParserError = HairyParserError::Parser("expected a command after an hairy opening tag: if, else, end, for, call, define, arguments, inline, =");
        return Err((EXPECT_HAIRY_COMMAND, remaining, reader.len()));
    }

    let mut retval = ScopedStringBuilder::new(context.allocator);
    loop {
        // searching with reader.find("{{") is slower (?!?)
        let mut start = 0;
        let mut found;
        loop {
            found = reader[start..].find('{').map(|x| x+start);
            if let Some(pos) = found {
                if pos >= reader.len()-1 || &reader[pos..pos+2] != "{{" {
                    start = pos+1;
                    continue;
                }
            }
            break;
        }
        match found {
            None => {
                let extra = &reader[..];
                if !extra.is_empty() {
                    retval.write_str(extra).ok();
                }
                *reader = &reader[0..0];
                return Ok((HairyToken::Text(retval.build()),remaining));
            },
            Some(n) => {
                let extra = &reader[0..n];
                *reader = &reader[n..];
                if !extra.ends_with('\\') {
                    if !extra.is_empty() {
                        retval.write_str(extra).ok();
                    }
                    return Ok((HairyToken::Text(retval.build()),remaining));
                }
                let extra = &extra[..extra.len()-1];
                if !extra.is_empty() {
                    retval.write_str(extra).ok();
                }
                let reader_saved = *reader;
                *reader = &reader[2..]; // skip "{{"
                let mut embedded_spanner = ExprySpanner::default();
                if let Some(expr) = embedded_spanner.span(reader) {
                    let expr = &reader_saved[0..expr.len()+4]; // include the "{{" before and the "}}" after
                    retval.write_str(expr).ok();
                }
                if !reader.accept("}}") {
                    return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
                }
            },
        }
    }
}

#[allow(clippy::upper_case_acronyms)]
type AST<'a> = Vec<HairyCommand<'a>>;
type LineNo = u32;
type ColumnNo = u32;

type SourceContext = (LineNo, ColumnNo);

#[derive(PartialEq, Eq, Clone, Copy)]
enum ResolveTemplate<'a> {
    Static(Name<'a>),
    DynamicName(BytecodeRef<'a>),
    DynamicBody(BytecodeRef<'a>),
}

#[derive(PartialEq, Clone)]
enum HairyCommand<'a> {
    Text(Bytes<'a>),
    Loop(BytecodeRef<'a>, Name<'a>, Name<'a>, AST<'a>, SourceContext),
    Conditional(Vec<(Option<Name<'a>>, BytecodeRef<'a>, AST<'a>, SourceContext)>, AST<'a>),
    Eval(BytecodeRef<'a>, Bytes<'a>, SourceContext), // name is the escape mode
    CallTemplate(ResolveTemplate<'a>, Vec<(BytecodeRef<'a>, ExpryType)>, SourceContext),
}

impl<'a> fmt::Debug for ResolveTemplate<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ResolveTemplate::Static(name) => write!(f, "{}", name),
            ResolveTemplate::DynamicName(_) => write!(f, "<dynamic-name>"),
            ResolveTemplate::DynamicBody(_) => write!(f, "<dynamic-body>"),
        }
    }
}

impl<'a> fmt::Debug for HairyCommand<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            HairyCommand::Text(s) => write!(f, "\"{}\"", String::from_utf8_lossy(s)),
            HairyCommand::Eval(expr, mode, _) => write!(f, "{{{{{:?}:{:?}}}}}", expr, mode),
            HairyCommand::Conditional(conditions, else_body) => {
                for (i,(variable, condition, body, _source_context)) in conditions.iter().enumerate() {
                    if let Some(variable) = variable {
                        write!(f, "{{{{{} let {} = {:?}}}}}", if i == 0 { "if" } else { "elseif" }, variable, condition)?;
                    } else {
                        write!(f, "{{{{{} {:?}}}}}", if i == 0 { "if" } else { "elseif" }, condition)?;
                    }
                    write!(f, "{:?}", body)?;
                }
                write!(f, "{{{{else}}}}")?;
                write!(f, "{:?}", else_body)?;
                write!(f, "{{{{end}}}}")
            },
            HairyCommand::Loop(tag, index_variable, content_variable, body, _) => {
                write!(f, "{{{{for ({:?},{:?}) in {:?}}}}}", index_variable, content_variable, tag)?;
                write!(f, "{:?}", body)?;
                write!(f, "{{{{end}}}}")
            },
            HairyCommand::CallTemplate(name, defaults, _) => {
                write!(f, "{{{{{:?} <- {:?}}}}}", name, defaults)
            },
        }
    }
}
struct Define<'b> {
    name: &'b str,
    filename: &'b str,
    args: Vec<ExpryType>,
    body: AST<'b>,
    subtemplates: Vec<Define<'b>>,
}

struct CompiledDefine<'b> {
    name: &'b [u8],
    filename: &'b [u8],
    args_count: u64,
    lazy_arg_types: &'b [u8], // Vec<(&'b str, ExpryType)>,
    body: BytecodeRef<'b>,
    subtemplates: BytecodeRef<'b>,
}

struct ParserContext<'a,'b,'c,'e> where 'c: 'b, 'e: 'b {
    line_context: LineContext,
    allocator: &'a mut MemoryScope<'c>,
    local_defines: Vec<Define<'b>>,
    outside_defines: Option<&'a std::collections::BTreeMap<Vec<u8>,Vec<ExpryType>>>,
    all_defines_known: bool,
    inlines: DecodedObject<'b>,
    value_names: Vec<(&'b str, ExpryType)>,
    value_count: Option<usize>,
    filename: &'b str,
    custom_types: &'a std::collections::BTreeMap<Key<'a>, (Vec<ExpryType>, ExpryType)>,
    static_custom: &'a dyn CustomFuncs<'e>,
    input_len: usize,
    escaping: Option<&'a [(usize, &'b [u8])]>,
    extra_line_no: u32,
}
type HairyParserState<'a,'b,'c,'e> = ParserState<'b,HairyToken<'b>, HairyParserError<'b>, ParserContext<'a,'b,'c,'e>>;
type HairyParserResult<'b,T> = ParserResult<T, HairyParserError<'b>>;

fn hairy_expr<'b,'c,'e>(parser: &'_ mut HairyParserState<'_,'b,'c,'e>) -> HairyParserResult<'b,AST<'b>> where 'c: 'b, 'e: 'b {
    let mut retval: AST = Vec::new();
    parser.repeat(|parser: &mut _| {
        let to_source_context = |pos, parser: &mut HairyParserState| { let (a,b,_,_) = parser.context().line_context.remaining_to_line_info(pos as u32); (a+parser.context().extra_line_no,b) };
        match parser.get()? {
            (HairyToken::Text(text), _) => {
                // trim empty lines, so templates can use some formatting (
                let text = text.trim_matches('\n');
                retval.push(HairyCommand::Text(text.as_bytes()));
                Ok(true)
            },
            (HairyToken::Eval(expr, expr_offset, mut escape_mode), info) => {
                let context = parser.context();
                let (bytecode,needs_dynamic_eval,return_type,warnings) = expry_compile_expr_typed(expr, Some(&context.inlines), None, &context.value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+expr_offset, e.error_end()+expr_offset), HairyParserError::Expr(e.error())))?;
                if !warnings.is_empty() {
                    let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
                    return Err(parser.error_other(&info, hairy_error));
                }
                // FIXME: with type checks this is not needed, but can be useful for users not using the type check.
                if escape_mode.is_empty() {
                    match return_type {
                        ExpryType::Any => {},
                        ExpryType::Null => {},
                        ExpryType::Int => {},
                        ExpryType::Float => {},
                        ExpryType::Double => {},
                        ExpryType::String => {},
                        ExpryType::Nullable(x) if matches!(*x, ExpryType::Any | ExpryType::Null | ExpryType::Int | ExpryType::Float | ExpryType::Double | ExpryType::String) => {},
                        _ => {
                            let msg = write!(parser.context().allocator, "only numbers and strings can be outputted (in regular escape modes), not {:?}", return_type);
                            return Err(parser.error_other(&info, HairyParserError::Type(msg)));
                        },
                    }
                }

                let source_context = to_source_context(info.start_to_end_of_input(), parser);
                if escape_mode.is_empty() {
                    if let Some(escaping) = parser.context().escaping {
                        escape_mode = lookup_escape_mode(escaping, parser.context().input_len - info.start_to_end_of_input());
                    }
                }
                // if static string value replace with HairyToken::Text
                if !needs_dynamic_eval && escape_mode == b"none" {
                    match expry_eval(bytecode, &mut Vec::new(), parser.context().allocator) {
                        Ok(DecodedValue::String(v)) => {
                            retval.push(HairyCommand::Text(v));
                            return Ok(true);
                        },
                        other => {
                            eprintln!("{:?}", other);
                        },
                    }
                }
                retval.push(HairyCommand::Eval(bytecode, escape_mode, source_context));
                Ok(true)
            },
            (HairyToken::CallTemplate(name, dynamic, offset_name, args), info) => {
                let mut args_bytecode = Vec::new();
                for (expr, offset_expr) in args {
                    let context = parser.context();
                    let (bytecode, _, return_type, warnings) = expry_compile_expr_typed(expr, Some(&context.inlines), None, &context.value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+offset_expr, e.error_end()+offset_expr), HairyParserError::Expr(e.error())))?;
                    if !warnings.is_empty() {
                        let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
                        return Err(parser.error_other(&info, hairy_error));
                    }
                    args_bytecode.push((bytecode, return_type));
                }
                let source_context = to_source_context(info.start_to_end_of_input(), parser);
                let name = name.trim_start_matches(is_space);
                if dynamic && name.starts_with('(') {
                    let context = parser.context();
                    let (name_bytecode,_needs_dynamic_eval,type_of_body,warnings) = expry_compile_expr_typed(name, Some(&context.inlines), None, &context.value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+offset_name, e.error_end()+offset_name), HairyParserError::Expr(e.error())))?;
                    if !warnings.is_empty() {
                        let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
                        return Err(parser.error_other(&info, hairy_error));
                    }
                    if !type_of_body.used_as(&ExpryType::Nullable(Box::new(ExpryType::String))) {
                        let msg = write!(parser.context().allocator, "expression in '()' should yield a string?, not a {}", type_of_body);
                        return Err(parser.error_other(&info, HairyParserError::Type(msg)));
                    }
                    retval.push(HairyCommand::CallTemplate(ResolveTemplate::DynamicBody(name_bytecode), args_bytecode, source_context));
                } else if dynamic {
                    let context = parser.context();
                    let (name_bytecode,_needs_dynamic_eval,type_of_name,warnings) = expry_compile_expr_typed(name, Some(&context.inlines), None, &context.value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+offset_name, e.error_end()+offset_name), HairyParserError::Expr(e.error())))?;
                    if !warnings.is_empty() {
                        let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
                        return Err(parser.error_other(&info, hairy_error));
                    }
                    if !type_of_name.used_as(&ExpryType::Nullable(Box::new(ExpryType::String))) {
                        let msg = write!(parser.context().allocator, "expression in '()' should yield a string?, not a {}", type_of_name);
                        return Err(parser.error_other(&info, HairyParserError::Type(msg)));
                    }
                    retval.push(HairyCommand::CallTemplate(ResolveTemplate::DynamicName(name_bytecode), args_bytecode, source_context));
                } else {
                    // check if the template exists, and has the correct type for arguments
                    if let Some(Define{args,..}) = parser.context().local_defines.iter().find(|x| x.name == name) {
                        let args_count = args.len();
                        if args_count != args_bytecode.len() {
                            let msg = write!(parser.context().allocator, "invocation of {} expected {} arguments, not {}", name, args_count, args_bytecode.len());
                            return Err(parser.error_other(&info, HairyParserError::Type(msg)));
                        }
                        for (i,(expected_type,(_,actual_type))) in (*args).iter().zip(args_bytecode.iter()).enumerate() {
                            if !actual_type.used_as(expected_type) {
                                let expected_type = (*expected_type).clone();
                                let msg = write!(parser.context().allocator, "argument {} to invocation of {} should be {}, but is instead {}", i, name, expected_type, actual_type);
                                return Err(parser.error_other(&info, HairyParserError::Type(msg)));
                            }
                        }
                    } else if let Some(args) = parser.context().outside_defines.and_then(|x| x.get(name.as_bytes())) {
                        let args_count = args.len();
                        if args_count != args_bytecode.len() {
                            let msg = write!(parser.context().allocator, "invocation of {} expected {} arguments, not {}", name, args_count, args_bytecode.len());
                            return Err(parser.error_other(&info, HairyParserError::Type(msg)));
                        }
                        for (i,(expected_type,(_,actual_type))) in args.iter().zip(args_bytecode.iter()).enumerate() {
                            if !actual_type.used_as(expected_type) {
                                let msg = write!(parser.context().allocator, "argument {} to invocation of {} should be {}, but is instead {}", i, name, expected_type, actual_type);
                                return Err(parser.error_other(&info, HairyParserError::Type(msg)));
                            }
                        }
                    } else if parser.context().all_defines_known {
                        let msg = write!(parser.context().allocator, "declaration of define '{}' could not be found", name);
                        return Err(parser.error_other(&info, HairyParserError::Type(msg)));
                    }
                    retval.push(HairyCommand::CallTemplate(ResolveTemplate::Static(name), args_bytecode, source_context));
                }
                Ok(true)
            },
            (HairyToken::Conditional(variable, expr, offset_expr), info) => {
                let value_names = parser.context().value_names.clone();
                let context = parser.context();
                let (expr_bytecode,_needs_dynamic_eval,return_type,warnings) = expry_compile_expr_typed(expr, Some(&context.inlines), None, &value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+offset_expr, e.error_end()+offset_expr), HairyParserError::Expr(e.error())))?;
                if !warnings.is_empty() {
                    let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
                    return Err(parser.error_other(&info, hairy_error));
                }

                let source_context = to_source_context(info.start_to_end_of_input(), parser);
                let body;
                if let Some(variable) = variable {
                    let return_type = match return_type {
                        ExpryType::Nullable(x) => *x,
                        x => x,
                    };
                    parser.context().value_names.push((variable, return_type));
                    body = hairy_expr(parser);
                    parser.context().value_names.pop();
                } else {
                    match return_type {
                        ExpryType::Any => {},
                        ExpryType::Bool => {},
                        ExpryType::Nullable(x) if matches!(*x, ExpryType::Bool) => {},
                        _ => {
                            let err_msg = write!(parser.context().allocator, "expr should return a bool, not a {} (or use the `if let name = ...` construct)", return_type);
                            return Err(parser.error_other(&info.bound(offset_expr, offset_expr), HairyParserError::Parser(err_msg)))
                        },
                    }
                    body = hairy_expr(parser);
                }
                let body = body?;

                let mut conditions = vec![(variable, expr_bytecode, body, source_context)];

                loop {
                    match parser.get()? {
                        (HairyToken::ElseConditional(variable, expr, offset_expr), info) => {
                            let context = parser.context();
                            let (expr_bytecode,_needs_dynamic_eval,type_of_bytecode,warnings) = expry_compile_expr_typed(expr, Some(&context.inlines), None, &value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+offset_expr, e.error_end()+offset_expr), HairyParserError::Expr(e.error())))?;
                            if !warnings.is_empty() {
                                let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
                                return Err(parser.error_other(&info, hairy_error));
                            }

                            let source_context = to_source_context(info.start_to_end_of_input(), parser);
                            let body;
                            if let Some(variable) = variable {
                                parser.context().value_names.push((variable, type_of_bytecode));
                                body = hairy_expr(parser);
                                parser.context().value_names.pop();
                            } else {
                                body = hairy_expr(parser);
                            }
                            let body = body?;
                            conditions.push((variable, expr_bytecode, body, source_context));
                        },
                        (HairyToken::Else(), _info) => {
                            let else_body = hairy_expr(parser)?;
                            match parser.get()? {
                                (HairyToken::BlockEnd(end_tag), info) => {
                                    if !end_tag.is_empty() && "if" != end_tag.trim() {
                                        let err_msg = write!(parser.context().allocator, "start (if) and end ({}) tag should be the same", end_tag.trim());
                                        return Err(parser.error_other(&info, HairyParserError::Parser(err_msg)))
                                    }
                                    retval.push(HairyCommand::Conditional(conditions, else_body));
                                    return Ok(true)
                                },
                                (token, info) => return Err(parser.error_token(token, info, |_| {
                                    HairyParserError::Parser("expected 'end' of conditional")
                                })),
                            }
                        },
                        (HairyToken::BlockEnd(end_tag), info) => {
                            if !end_tag.is_empty() && "if" != end_tag.trim() {
                                let err_msg = write!(parser.context().allocator, "start (if) and end ({}) tag should be the same", end_tag.trim());
                                return Err(parser.error_other(&info, HairyParserError::Parser(err_msg)))
                            }
                            retval.push(HairyCommand::Conditional(conditions, Vec::new()));
                            return Ok(true)
                        },
                        (token, info) => return Err(parser.error_token(token, info, |_| {
                            HairyParserError::Parser("expected 'elseif', 'else', or 'end' of conditional")
                        })),
                    }
                }
            },
            (HairyToken::Loop(expr, expr_offset, index_variable, content_variable), info) => {
                let value_names = parser.context().value_names.clone();
                let context = parser.context();
                let (expr_bytecode,_needs_dynamic_eval,type_of_bytecode,warnings) = expry_compile_expr_typed(expr, Some(&context.inlines), None, &value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+expr_offset, e.error_end()+expr_offset), HairyParserError::Expr(e.error())))?;
                if !warnings.is_empty() {
                    let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
                    return Err(parser.error_other(&info, hairy_error));
                }
                let type_of_bytecode = match type_of_bytecode {
                    ExpryType::Nullable(x) if matches!(*x, ExpryType::Array(_) | ExpryType::Any) => {
                        if let ExpryType::Array(x) = *x {
                            *x
                        } else {
                            *x
                        }
                    },
                    ExpryType::Array(x) => *x,
                    ExpryType::Any => type_of_bytecode,
                    t => {
                        let err_msg = write!(parser.context().allocator, "expression should return a non-empty array, not a {}", t);
                        return Err(parser.error_other(&info, HairyParserError::Type(err_msg)))
                    },
                };
                let source_context = to_source_context(info.start_to_end_of_input(), parser);
                parser.context().value_names.push((index_variable, ExpryType::Int));
                parser.context().value_names.push((content_variable, type_of_bytecode));
                let body = hairy_expr(parser);
                parser.context().value_names.pop();
                parser.context().value_names.pop();
                let body = body?;
                match parser.get()? {
                    (HairyToken::BlockEnd(end_tag), info) => {
                        if !end_tag.is_empty() && "for" != end_tag.trim() {
                            let err_msg = write!(parser.context().allocator, "start (for) and end ({}) tag should be the same", end_tag.trim());
                            return Err(parser.error_other(&info, HairyParserError::Parser(err_msg)))
                        }
                        retval.push(HairyCommand::Loop(expr_bytecode, index_variable, content_variable, body, source_context));
                        Ok(true)
                    }
                    (token, info) => Err(parser.error_token(token, info, |_| {
                        HairyParserError::Parser("expected 'end' of loop")
                    })),
                }
            },
            (HairyToken::TemplateDef(name, args), info) => {
                let mut compiled_args = Vec::new(); //ScopedArrayBuilder::new(parser.context().allocator);
                for (label, type_spec, offset) in args {
                    let type_spec = expry_parse_type(type_spec, parser.context().allocator).map_err(|CompileError{error,start,end, ..}| parser.error_other(&info.bound(start+offset, end+offset), HairyParserError::Expr(error)))?;
                    compiled_args.push((*label, type_spec));
                }
                //let compiled_args = compiled_args.build();

                //eprintln!("template define {} with args {:?}", name, args);
                core::mem::swap(&mut compiled_args, &mut parser.context().value_names);
                let body = hairy_expr(parser);
                core::mem::swap(&mut compiled_args, &mut parser.context().value_names);
                let body = body?;

                match parser.get()? {
                    (HairyToken::BlockEnd(end_tag), info) => {
                        if !end_tag.is_empty() && "define" != end_tag {
                            let err_msg = write!(parser.context().allocator, "start (define) and end ({}) tag should be the same", end_tag);
                            return Err(parser.error_other(&info, HairyParserError::Parser(err_msg)))
                        }
                        let filename = parser.context().filename;
                        // check that we don't override a template
                        if parser.context().local_defines.iter().any(|x| x.name == name) {
                            let err_msg = write!(parser.context().allocator, "template '{}' already defined", name);
                            return Err(parser.error_other(&info, HairyParserError::Parser(err_msg)));
                        }
                        parser.context().local_defines.push(Define{name, filename, args: compiled_args.iter().map(|(_,x)| x.clone()).collect(), body, subtemplates: Vec::new()});
                        Ok(true)
                    }
                    (token, info) => Err(parser.error_token(token, info, |_| {
                        HairyParserError::Parser("expected 'end' of template definition")
                    })),
                }
            },
            (HairyToken::Arguments(args), info) => {
                let mut compiled_args = Vec::new(); //ScopedArrayBuilder::new(parser.context().allocator);
                for (label, type_spec, offset) in args {
                    let type_spec = expry_parse_type(type_spec, parser.context().allocator).map_err(|CompileError{error,start,end, ..}| parser.error_other(&info.bound(start+offset, end+offset), HairyParserError::Expr(error)))?;
                    compiled_args.push((*label, type_spec));
                }
                if let Some(count) = parser.context().value_count {
                    if count != args.len() {
                        let err = write!(parser.context().allocator, "different number of arguments in both code and template itself: {} and {}", count, args.len());
                        return Err(parser.error_other(&info, HairyParserError::Parser(err)));
                    }
                    let mut value_names = core::mem::take(&mut parser.context().value_names);
                    for (i,(k, v)) in value_names.iter_mut().enumerate() {
                        if let Some((specified_name,specified)) = compiled_args.get(i) {
                            if !k.is_empty() && k != specified_name {
                                let err = write!(parser.context().allocator, "argument {} are named both in code and in the template with different names: {} and {}", i, *k, *specified_name);
                                return Err(parser.error_other(&info, HairyParserError::Parser(err)));
                            }
                            *k = specified_name;
                            if v.used_as(specified) {
                                //eprintln!("using {} instead of {} for {}", specified, v, k);
                                if !matches!(specified, ExpryType::Any) {
                                    *v = specified.clone();
                                }
                            } else {
                                let err = write!(parser.context().allocator, "argument {} named '{}' have conflicting types in code and in the template itself: {} can not be used as {}", i, k, v, specified);
                                return Err(parser.error_other(&info, HairyParserError::Parser(err)));
                            }
                        } else {
                            return Err(parser.error_other(&info, HairyParserError::Parser("expected a different number of arguments")));
                        }
                    }
                    parser.context().value_names = value_names;
                } else if parser.context().value_names.is_empty() {
                    parser.context().value_names.extend_from_slice(&compiled_args);
                } else {
                    return Err(parser.error_other(&info, HairyParserError::Parser("setting 'arguments' can only happen outside defines")));
                }
                Ok(true)
            },
            (HairyToken::Inline(name, expr, offset_expr), info) => {
                let context = parser.context();
                let (bytecode, _dynamic, _return_type,warnings) = expry_compile_expr_typed(expr, Some(&context.inlines), None, &[], context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+offset_expr, e.error_end()+offset_expr), HairyParserError::Expr(e.error())))?;
                if !warnings.is_empty() {
                    let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
                    return Err(parser.error_other(&info, hairy_error));
                }
                let context = parser.context();
                let value = expry_eval_func(bytecode, &mut Vec::new(), context.allocator, context.static_custom).map_err(|err| parser.error_other(&info, HairyParserError::Eval(err)))?;
                parser.context().inlines.insert(key_str(name), value);
                Ok(true)
            },
            (token, info) => Err(parser.error_token(token, info, |_| HairyParserError::Parser(""))),
        }
    })?;
    Ok(retval)
}
fn expry_type_warnings_to_hairy_eval_error<'b,'c>(warnings: &[ExpryTypeWarning], allocator: &mut MemoryScope<'c>) -> HairyEvalError<'b> where 'c: 'b {
    let mut buffer = ScopedStringBuilder::new(allocator);
    write!(&mut buffer, "possibly non existant field used without error handling: ").unwrap();
    for w in warnings {
        match w {
            ExpryTypeWarning::PossibleUnsetField(name) => { write!(&mut buffer, "{}, ", String::from_utf8_lossy(name)).unwrap(); },
        }
    }
    HairyEvalError::Error(buffer.build())
}

fn expry_type_warnings_to_hairy_parser_error<'b,'c>(warnings: &[ExpryTypeWarning], allocator: &mut MemoryScope<'c>) -> HairyParserError<'b> where 'c: 'b {
    let mut buffer = ScopedStringBuilder::new(allocator);
    write!(&mut buffer, "possibly non existant field used without error handling: ").unwrap();
    for w in warnings {
        match w {
            ExpryTypeWarning::PossibleUnsetField(name) => { write!(&mut buffer, "{}, ", String::from_utf8_lossy(name)).unwrap(); },
        }
    }
    HairyParserError::Type(buffer.build())
}
fn hairy_top_level<'b,'c,'e>(parser: &'_ mut HairyParserState<'_,'b,'c,'e>) -> HairyParserResult<'b,AST<'b>> where 'c: 'b, 'e: 'b {
    let retval = hairy_expr(parser)?;
    parser.accept(HairyToken::EndOfInput(), None, || HairyParserError::Parser("expected end of input"))?;
    Ok(retval)
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum EscapeMode {
    Normal,
    InsideTag,
    InsideAttributeSingleQuote,
    InsideAttributeDoubleQuote,
    InsideScript,
    InsideStyle,
    InsideComment,
}
fn mode_to_bytes(mode: EscapeMode, tag: &str, attribute: &str) -> &'static [u8] {
    // see https://wonko.com/post/html-escaping/ for explanation
    match mode {
        EscapeMode::Normal => b"", // "html" is the default, so we don't need to store it
        EscapeMode::InsideTag => b"",
        EscapeMode::InsideAttributeDoubleQuote | EscapeMode::InsideAttributeSingleQuote if is_url(tag, attribute) => b"url",
        EscapeMode::InsideAttributeDoubleQuote | EscapeMode::InsideAttributeSingleQuote => b"+",
        EscapeMode::InsideScript => b"sjs", // script js
        EscapeMode::InsideStyle => b"",
        EscapeMode::InsideComment => b"",
    }
}

fn is_url(tag: &str, attribute: &str) -> bool {
    if attribute == "srcset" && tag == "source" {
        return true;
    }
    if attribute == "object" && tag == "data" {
        return true;
    }
    if attribute == "action" && tag == "form" {
        return true;
    }
    if attribute == "href" && (tag == "a" || tag == "area" || tag == "base" || tag == "link") {
        true
    } else {
        attribute == "src" && (tag == "audio" || tag == "embed" || tag == "iframe" || tag == "img" || tag == "input" || tag == "script" || tag == "source" || tag == "track" || tag == "video")
    }
}
fn parse_html<'a,'b, 'c>(input: &'b str, extra_line_no: u32, scope: &mut MemoryScope<'c>) -> Result<&'a [(usize,&'static [u8])], HairyCompileError<'b>> where 'c: 'a {
    let mut retval = ScopedArrayBuilder::new(scope);
    let mut inside = 0;
    let mut inside_string = false;

    let mut mode = EscapeMode::Normal;
    let mut tag = "";
    let mut attribute = ""; // can also contain the tag name
    let mut last_mode = mode;
    let mut last_mode_bytes = mode_to_bytes(mode, tag, attribute);
    retval.push((0, mode_to_bytes(mode, tag, attribute)));
    for (i,c) in input.char_indices() {
        // skip any chars inside hairy templates, because otherwise {{foo <- args}} triggers the
        // HTML open.
        if inside_string {
            inside_string = c != '"';
            continue;
        }
        match c {
            '{' => inside += 1,
            '}' => inside -= 1,
            '"' if inside > 0 => inside_string = true,
            _ => {},
        }
        if inside > 0 {
            continue;
        }
        match mode {
            EscapeMode::Normal => {
                if c == '<' {
                    if input[i..].starts_with("<!--") {
                        mode = EscapeMode::InsideComment;
                        tag = "!--";
                    } else if let Some((prefix, _)) = input[i+1..].split_once(|x: char| !x.is_ascii_alphanumeric() && x != '/') {
                        if !prefix.is_empty() {
                            mode = EscapeMode::InsideTag;
                            tag = prefix;
                        }
                    }
                    attribute = "";
                }
            },
            EscapeMode::InsideTag => {
                match c {
                    '=' => {
                        if !input[i..].starts_with("=\"") && !input[i..].starts_with("=\'") {
                            return Err(HairyCompileError{expr: input, error: HairyParserError::Parser("for auto escaping to work, attributes of HTML should have quotes around the values"), start: input.len()-i, end: input.len()-i, extra: None, line_context: None, extra_line_no});

                        }
                    }
                    '"' => {
                        mode = EscapeMode::InsideAttributeDoubleQuote;
                    },
                    '\'' => {
                        mode = EscapeMode::InsideAttributeSingleQuote;
                    },
                    '>' => {
                        mode = if tag == "script" { EscapeMode::InsideScript } else if tag == "style" { EscapeMode::InsideStyle } else { EscapeMode::Normal };
                    },
                    '<' => {
                        return Err(HairyCompileError{expr: input, error: HairyParserError::Parser("unexpected character encounted inside tag, likely to break auto escaping."), start: input.len()-i, end: input.len()-i, extra: None, line_context: None, extra_line_no, });
                    }
                    c if attribute.is_empty() && c.is_ascii_alphanumeric() => {
                        if let Some((prefix,_)) = input[i..].split_once(|x: char| !x.is_ascii_alphanumeric() && x != '_') {
                            attribute = prefix;
                        }
                    },
                    ' ' => {
                        attribute = "";
                    },
                    _ => {
                    },
                };
            },
            EscapeMode::InsideAttributeDoubleQuote => {
                if c == '"' {
                    mode = EscapeMode::InsideTag;
                    attribute = "";
                } else if c == '<' || c == '>' {
                    return Err(HairyCompileError{expr: input, error: HairyParserError::Parser("unexpected character encounted inside attribute, likely to break auto escaping."), start: input.len()-i, end: input.len()-i, extra: None, line_context: None, extra_line_no, });
                }
            },
            EscapeMode::InsideAttributeSingleQuote => {
                if c == '\'' {
                    mode = EscapeMode::InsideTag;
                    attribute = "";
                } else if c == '<' || c == '>' {
                    return Err(HairyCompileError{expr: input, error: HairyParserError::Parser("unexpected character encounted inside attribute, likely to break auto escaping."), start: input.len()-i, end: input.len()-i, extra: None, line_context: None, extra_line_no, });
                }
            },
            EscapeMode::InsideScript => {
                if c == '<' {
                    if input[i..].starts_with("</script>") {
                        mode = EscapeMode::InsideTag;
                        tag = "/script";
                    } else {
                        // valid inside script tag:
                        // document.innerHTML = '<button>Open in App</button>';
                    }
                }
            },
            EscapeMode::InsideStyle => {
                if c == '<' {
                    if input[i..].starts_with("</style>") {
                        mode = EscapeMode::InsideTag;
                        tag = "/style";
                    } else {
                        return Err(HairyCompileError{expr: input, error: HairyParserError::Parser("expected end of style tag"), start: input.len()-i, end: input.len()-i, extra: None, line_context: None, extra_line_no, });
                    }
                }
            },
            EscapeMode::InsideComment => {
                if input[i..].starts_with("-->") {
                    mode = EscapeMode::Normal;
                }
            }
        }
        if mode != last_mode {
            if mode_to_bytes(mode, tag, attribute) != last_mode_bytes {
                last_mode_bytes = mode_to_bytes(mode, tag, attribute);
                retval.push((i+1, last_mode_bytes));
            }
            last_mode = mode;
        }
    }
    if mode != EscapeMode::Normal {
        // FIXME: remember when the last tag/attribute was opened, that is probably a more useful error location for this error
        let message = match mode {
            EscapeMode::Normal => "",
            EscapeMode::InsideTag => "expected to end with correctly closed tag, now 'inside tag' mode",
            EscapeMode::InsideAttributeDoubleQuote => "expected to end with correctly closed tag, now 'inside attribute with double quotes' mode",
            EscapeMode::InsideAttributeSingleQuote => "expected to end with correctly closed tag, now 'inside attribute with single quotes' mode",
            EscapeMode::InsideScript => "expected to end with correctly closed tag, now 'inside script' mode",
            EscapeMode::InsideStyle => "expected to end with correctly closed tag, now 'inside style' mode",
            EscapeMode::InsideComment => "expected to end with correctly closed HTML comment, now 'inside HTML comment' mode",
        };
        return Err(HairyCompileError{expr: input, error: HairyParserError::Parser(message), start: 0, end: 0, extra: None, line_context: None, extra_line_no, });
    }
    Ok(retval.build())
}

fn lookup_escape_mode<'a>(escaping: &'_ [(usize, &'a [u8])], pos: usize) -> &'a [u8] {
    let pp = escaping.partition_point(|x| x.0 <= pos);
    if pp > 0 {
        return escaping[pp-1].1;
    }
    b""
}

#[derive(Clone)]
pub struct HairyOptions<'b,'c,'d> {
    pub custom: &'c dyn CustomFuncs<'d>,
    dynamic_values: Vec<(&'b str, EncodedValueRef<'b>)>,
    pub escaper: &'c dyn Escaper,
    pub given_templates: Option<&'c [RawReader<'b>]>,
    pub inlines: DecodedObject<'b>,
    pub extra_line_no: u32,
}

impl<'b,'c,'d> HairyOptions<'b,'c,'d> {
    pub fn new() -> Self {
        Self {
            custom: &NoCustomFuncs{},
            dynamic_values: Vec::new(),
            escaper: &DefaultEscaper{ default_escape_mode: b"html" },
            given_templates: None,
            inlines: DecodedObject::new(),
            extra_line_no: 0,
        }
    }
    pub fn custom<T: CustomFuncs<'d>>(&mut self, custom: &'c T) {
        self.custom = custom;
    }
    pub fn set_named_dynamic_values(&mut self, args: &[(&'b str, EncodedValueRef<'b>)]) {
        self.dynamic_values.clear();
        self.dynamic_values.extend_from_slice(args);
    }
    pub fn set_dynamic_values(&mut self, args: &[EncodedValueRef<'b>]) {
        self.dynamic_values.clear();
        let placeholder : &'b str = "";
        self.dynamic_values.extend(args.iter().map(|x| (placeholder, *x)));
    }
}

impl<'b,'c,'d> Default for HairyOptions<'b,'c,'d> {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Clone)]
pub struct HairyCompileOptions<'b,'c,'d> {
    // typing of custom functions
    pub custom_types: std::collections::BTreeMap<Key<'static>,(Vec<ExpryType>,ExpryType)>,
    // dynamic arguments
    dynamic_values_types: Vec<(&'b str, ExpryType)>,
    dynamic_values_count: Option<usize>,
    // functions that can be evaluated compile time
    pub static_custom: &'c dyn CustomFuncs<'d>,
    // fields that can be inserted statically
    pub inlines: DecodedObject<'b>,
    // typing information of hairy defines
    pub defines: Option<std::collections::BTreeMap<Vec<u8>,Vec<ExpryType>>>,
    pub override_name: Option<&'b str>,
    pub extra_line_no: u32,
}

impl<'b,'c,'d> HairyCompileOptions<'b,'c,'d> {
    pub fn new() -> Self {
        Self {
            custom_types: std::collections::BTreeMap::new(),
            static_custom: &NoCustomFuncs{},
            inlines: DecodedObject::new(),
            defines: None,
            override_name: None,
            dynamic_values_types: Vec::new(),
            dynamic_values_count: None,
            extra_line_no: 0,
        }
    }
    pub fn static_custom<T: CustomFuncs<'d>>(&mut self, custom: &'c T) {
        self.static_custom = custom;
    }
    pub fn set_dynamic_value_count(&mut self, args: usize) {
        self.dynamic_values_types.resize(args, ("", ExpryType::Any));
        self.dynamic_values_count = Some(args);
    }
    pub fn set_dynamic_value_name_and_types(&mut self, args: &[(&'b str, ExpryType)]) {
        self.dynamic_values_types.clear();
        self.dynamic_values_types.extend_from_slice(args);
        self.dynamic_values_count = Some(args.len());
    }
    pub fn set_dynamic_value_types(&mut self, args: &[ExpryType]) {
        self.dynamic_values_types.clear();
        self.dynamic_values_types.extend(args.iter().map(|x| ("", x.clone())));
        self.dynamic_values_count = Some(args.len());
    }
    pub fn set_dynamic_value_names(&mut self, args: &[&'b str]) {
        self.dynamic_values_types.clear();
        self.dynamic_values_types.extend(args.iter().map(|x| (*x, ExpryType::Any)));
        self.dynamic_values_count = Some(args.len());
    }
}

impl<'b,'c,'d> Default for HairyCompileOptions<'b,'c,'d> {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Clone)]
pub struct HairyEvalOptions<'b,'c,'d> {
    pub custom: &'c dyn CustomFuncs<'d>,
    pub escaper: &'c dyn Escaper,
    pub values: Vec<EncodedValueRef<'b>>,
    pub given_templates: Option<&'c [RawReader<'b>]>, // last is checked first
}

impl<'b,'c,'d> HairyEvalOptions<'b,'c,'d> {
    pub fn new() -> Self {
        Self {
            custom: &NoCustomFuncs{},
            escaper: &DefaultEscaper{ default_escape_mode: b"html" },
            values: Vec::new(),
            given_templates: None,
        }
    }
    pub fn custom<T: CustomFuncs<'d>>(&mut self, custom: &'c T) {
        self.custom = custom;
    }
}

impl<'b,'c,'d> Default for HairyEvalOptions<'b,'c,'d> {
    fn default() -> Self {
        Self::new()
    }
}
impl<'b,'c,'d> TryFrom<&HairyOptions<'b,'c,'d>> for HairyCompileOptions<'b,'c,'d> {
    type Error = EncodingError;

    fn try_from(v: &HairyOptions<'b,'c,'d>) -> Result<Self, Self::Error> {
        Ok(Self {
            custom_types: v.custom.types(),
            dynamic_values_types: v.dynamic_values.iter().map(|(name,value)| -> Result<_,_> { Ok((*name, expry_to_type_lazy(&expry_decode_lazy(value)?)?))}).fold(Ok(Vec::new()), |acc,v| -> Result<Vec<_>,EncodingError> { let mut acc = acc?; acc.push(v?); Ok(acc) })?,
            dynamic_values_count: Some(v.dynamic_values.len()),
            static_custom: v.custom,
            inlines: v.inlines.clone(),
            defines: v.given_templates.as_ref().map(|x| hairy_templates_to_types(x)).map_or(Ok(None), |v| v.map(Some))?,
            override_name: None,
            extra_line_no: v.extra_line_no,
        })
    }
}
impl<'b,'c,'d> TryFrom<&HairyOptions<'b,'c,'d>> for HairyEvalOptions<'b,'c,'d> {
    type Error = EncodingError;

    fn try_from(v: &HairyOptions<'b,'c,'d>) -> Result<Self, Self::Error> {
        Ok(Self {
            custom: v.custom,
            escaper: v.escaper,
            values: v.dynamic_values.iter().map(|(_,v)| *v).collect(),
            given_templates: v.given_templates,
        })
    }
}

/// Converts a Hairy template (in `reader`) to bytecode that can be evaluation later. To help debugging during later evaluation, a
/// `filename` can be added to the bytecode. A MemoryScope is used to reduce the number of
/// allocated strings. The result stays valid as long as the MemoryScope is in the scope (forced by
/// the lifetime system of Rust). The globals and custom functions are used when evaluation the
/// defaults (for template calls and such).
///
/// For an easy to version of this function, see the [`hairy_compile_html`] function (which provide
/// auto escaping automatically).
///
/// ## Escaping
///
/// Normally, all strings that are dynamically outputted (between an evaluating `{{..}}`
/// statement) are automatically 'escaped' using the escaper as given to an argument to the
/// `hairy_eval` function. However, often you will want different escape modes depending on the
/// input context. For example in HTML, output may not normally contain `<` or `>`, but output
/// inside attributes in HTML tags are normally escaped using url-escaping. Although users can specify for
/// every output the escape mode by appending a `:html` or `:url` escaping mode to an expression,
/// this is error prone. Auto escaping is therefore a safer alternative, by automatically dedcing
/// the escape mode from the input. That is where the `escaping` argument comes in to play. That
/// argument is used to lookup the escaping mode for a certain position in the input.
///
/// # Example
///
/// ```
/// use hairy::*;
/// use expry::*;
/// use expry_macros::*;
/// use std::io::Write;
/// let mut allocator = MemoryPool::new();
/// let mut scope = allocator.rewind();
/// let template = r#"foo {{=this.bar}}"#;
/// let value = value!({"bar":"bar"}).encode_to_scope(&mut scope);
/// let mut options = HairyOptions::new();
/// options.set_named_dynamic_values(&[("this",value)]);
/// let result = hairy_compile(template, "test.tpl", &(&options).try_into().unwrap(), &mut scope, None);
/// match result {
///   Ok(parsed) => {
///     let output = hairy_eval(parsed.to_ref(), &(&options).try_into().unwrap(), &mut scope);
///     if let Ok(output) = output {
///       std::io::stdout().write_all(&output.concat());
///       // OR, more efficient (avoiding allocations):
///       for v in output {
///         std::io::stdout().write_all(v);
///       }
///     }
///   },
///   Err(mut err) => {
///     let err = hairy_compile_error_format(&mut err);
///     eprintln!("{}", err.1);
///     panic!(); // to check this example
///   }
/// }
/// ```
pub fn hairy_compile<'a,'b,'c,'d,'e>(reader: &'b str, filename: &'b str, options: &'a HairyCompileOptions<'b,'d,'e>, scope: &mut MemoryScope<'c>, escaping: Option<&'_ [(usize, &'b [u8])]>) -> Result<BytecodeVec,HairyCompileError<'b>> where 'c: 'b, 'b: 'a, 'e: 'b {
    if reader.len() >= u32::MAX as usize {
        return Err(HairyCompileError{expr: reader, error: HairyParserError::Other("input too large to parse (max 4 GiB)"), start: reader.len(), end: 0usize, extra: None, line_context: None, extra_line_no: options.extra_line_no});
    }
    let context = ParserContext {
        allocator: scope,
        line_context: LineContext::new(reader),
        local_defines: Vec::new(),
        outside_defines: options.defines.as_ref(),
        all_defines_known: options.defines.is_some(),
        inlines: options.inlines.clone(),
        value_names: options.dynamic_values_types.iter().map(|(name,t)| (*name, t.clone())).collect(),
        value_count: options.dynamic_values_count,
        filename,
        custom_types: &options.custom_types,
        static_custom: options.static_custom,
        escaping,
        input_len: reader.len(),
        extra_line_no: options.extra_line_no,
    };
    let mut parser = HairyParserState::new_with(reader, hairy_tokenize, context);
    let ast = match parser.parse(hairy_top_level, HairyParserError::Parser("unexpected token"), HairyParserError::Parser("max recursion depth of parser reached")) {
        Ok(ast) => ast,
        Err((e,start,end,extra)) => return Err(HairyCompileError{expr: reader, error: e, start, end, extra, line_context: Some(parser.context.line_context), extra_line_no: options.extra_line_no}),
    };
    debug_assert!(parser.context().value_count.is_none() || parser.context().value_names.len() == options.dynamic_values_types.len()); // arguments can be set with {{arguments ..}}

    // optimize AST: combine 2 successive Text commands into one
    let ast = ast.into_iter().fold(Vec::new(), |mut acc, item| {
        if let Some(last) = acc.last_mut() {
            if let (HairyCommand::Text(a), HairyCommand::Text(b)) = (last, &item) {
                *a = parser.context().allocator.concat_u8(&[a, b]);
                return acc;
            }
        }
        acc.push(item);
        acc
    });

    let context = parser.consume();

    (|| {
        let name : &str = options.override_name.unwrap_or(filename);
        let define = Define{name, filename, args: context.value_names.iter().map(|(_,y)| y.clone()).collect(), body: ast, subtemplates: context.local_defines };
        let mut length_collector = RawWriterLength::new();
        template_to_binary(&define, &mut length_collector).unwrap_infallible();
        let total_length = length_collector.length();

        let mut bytecode: Vec<u8> = vec![0; total_length];
        let mut writer = RawWriter::with(&mut bytecode);
        template_to_binary(&define, &mut writer)?;
        debug_assert_eq!(0, writer.left());
        Ok(BytecodeVec(bytecode))
    })().map_err(|x| HairyCompileError{expr: reader, error: x, start:0, end:0, extra: None, line_context: Some(context.line_context), extra_line_no: options.extra_line_no})
}

pub fn hairy_extract_subtemplates(template_bytecode: BytecodeRef) -> Result<&[u8],HairyError> {
    let define = decode_hairy(template_bytecode.get())?;
    //eprintln!("extracting subtemplates from '{}': {} bytes", String::from_utf8_lossy(define.filename), define.subtemplates.len());
    Ok(define.subtemplates.get())
}

#[derive(Clone)]
struct CheckTypeContext<'a,'b> {
    custom_types: &'a std::collections::BTreeMap<Key<'a>, (Vec<ExpryType>, ExpryType)>,
    templates: Vec<RawReader<'b>>,
    template_name: &'b [u8],
    values: Vec<&'b ExpryType>,
    escaper: Option<&'a dyn Escaper>,
}
fn check_types<'a,'b,'c,'e>(expression: &mut RawReader<'b>, context: &'_ mut CheckTypeContext<'a,'b>, allocator: &'_ mut MemoryScope<'c>, depth: usize) -> Result<(),HairyError<'e>> where 'b: 'a, 'c: 'e {
    if depth == 0 {
        return Err(HairyEvalError::EvalTooLong().into());
    }
    'next: while !expression.is_empty() {
        let opcode = expression.read_u8().map_err(|_| HairyEvalError::Bytecode("Expected more bytes in expression"))?;
        if opcode == HairyBytecode::Text as u8 {
            let err = |_| HairyEvalError::Bytecode("corrupted TEXT");
            let text_length = expression.read_var_u64().map_err(err)?;
            let _text = expression.read_bytes(text_length as usize).map_err(err)?;
            continue;
        }
        if opcode == HairyBytecode::Evaluate as u8 {
            let err = |_| HairyEvalError::Bytecode("corrupted EXPR");
            let line_no = expression.read_var_u64().map_err(err)?;
            let column_no = expression.read_var_u64().map_err(err)?;
            let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
            let escape_mode = expression.read_var_string().map_err(err)?;
            let (value_type, warnings) = expry_type_from_bytecode(expr, &context.values, context.custom_types, allocator).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Eval, allocator.copy_u8(context.template_name), line_no, column_no))?;
            if !warnings.is_empty() {
                let hairy_error = expry_type_warnings_to_hairy_eval_error(&warnings, allocator);
                return Err(HairyError::wrap(hairy_error, HairyLocationType::Eval, allocator.copy_u8(context.template_name), line_no, column_no));
            }
            // check type of object is allowable by the escaper
            if let Some(escaper) = context.escaper {
                if !escaper.check_type(&value_type, escape_mode) {
                    return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "unsupported type by specified escaper: {} at {}:{}", value_type, line_no, column_no)), HairyLocationType::Eval, allocator.copy_u8(context.template_name), line_no, column_no));
                }
            }
            continue;
        }
        if opcode == HairyBytecode::ConditionalBool as u8 || opcode == HairyBytecode::ConditionalLet as u8 {
            let err = |_| HairyEvalError::Bytecode("corrupted CONDITIONAL");
            let line_no = expression.read_var_u64().map_err(err)?;
            let column_no = expression.read_var_u64().map_err(err)?;
            let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
            let body = expression.read_var_string().map_err(err)?;
            let else_body = expression.read_var_string().map_err(err)?;
            if opcode == HairyBytecode::ConditionalLet as u8 {
                let return_type = expry_type_from_bytecode(expr, &context.values, context.custom_types, allocator);
                let (return_type, warnings) = return_type.map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Eval, allocator.copy_u8(context.template_name), line_no, column_no))?;
                if !warnings.is_empty() {
                    let hairy_error = expry_type_warnings_to_hairy_eval_error(&warnings, allocator);
                    return Err(HairyError::wrap(hairy_error, HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no));
                }
                let return_type = match return_type {
                    ExpryType::Nullable(x) => *x,
                    x => x,
                };
                if !else_body.is_empty() {
                    check_types(&mut RawReader::with(else_body), context, allocator, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no))?;
                }
                if !matches!(return_type, ExpryType::Null) {
                    let mut context = context.clone();
                    context.values.push(&return_type);
                    let retval = check_types(&mut RawReader::with(body), &mut context, allocator, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no));
                    context.values.pop();
                    retval?;
                }
                continue;
            }

            let (retval, warnings) = expry_type_from_bytecode(expr, &context.values, context.custom_types, allocator).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{}", err)),HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no))?;
            if !warnings.is_empty() {
                let hairy_error = expry_type_warnings_to_hairy_eval_error(&warnings, allocator);
                return Err(HairyError::wrap(hairy_error, HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no));
            }
            'handle: {
                if matches!(retval, ExpryType::Bool | ExpryType::Null | ExpryType::Nullable(_)) {
                    if let ExpryType::Nullable(inner) = &retval {
                        if !matches!(**inner, ExpryType::Bool) {
                            break 'handle;
                        }
                    }
                    check_types(&mut RawReader::with(body), context, allocator, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no))?;
                    if !else_body.is_empty() {
                        check_types(&mut RawReader::with(else_body), context, allocator, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no))?;
                    }
                    continue 'next;
                }
            }
            return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "expression in conditional should result in a bool or null, not {retval}")), HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no));
        }
        if opcode == HairyBytecode::Loop as u8 {
            let err = |_| HairyEvalError::Bytecode("corrupted LOOP");
            let line_no = expression.read_var_u64().map_err(err)?;
            let column_no = expression.read_var_u64().map_err(err)?;
            let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
            let body = expression.read_var_string().map_err(err)?;
            let (mut return_type, warnings) = expry_type_from_bytecode(expr, &context.values, context.custom_types, allocator).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")),HairyLocationType::Loop, allocator.copy_u8(context.template_name), line_no, column_no))?;
            if !warnings.is_empty() {
                let hairy_error = expry_type_warnings_to_hairy_eval_error(&warnings, allocator);
                return Err(HairyError::wrap(hairy_error, HairyLocationType::Loop, allocator.copy_u8(context.template_name), line_no, column_no));
            }
            if let ExpryType::Nullable(iteration_type) = return_type {
                return_type = *iteration_type;
            }
            // FIXME: handle StaticArray + EmptyArray?
            if let ExpryType::Array(iteration_type) = return_type {
                let mut context = context.clone();
                context.values.push(&ExpryType::Int);
                context.values.push(&iteration_type);
                let retval = check_types(&mut RawReader::with(body), &mut context, allocator, depth-1).map_err(|err| err.add_location(HairyLocationType::Loop, allocator.copy_u8(context.template_name), line_no, column_no));
                retval?;
                continue;
            }
            // added so that `expr???` indicates ignore on error
            if ExpryType::Null == return_type {
                continue;
            }
            return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "expression in loop should result in an array (with any value) or null, not {return_type}")), HairyLocationType::Loop, allocator.copy_u8(context.template_name), line_no, column_no));
        }
        if opcode == HairyBytecode::CallDynamicName as u8 || opcode == HairyBytecode::CallDynamicBody as u8 || opcode == HairyBytecode::CallStatic as u8 {
            let err = |_| HairyEvalError::Bytecode("corrupted LOOP");
            let line_no = expression.read_var_u64().map_err(err)?;
            let column_no = expression.read_var_u64().map_err(err)?;
            let name = expression.read_var_string().map_err(err)?;
            let args_count = expression.read_var_u64().map_err(err)?;
            let templates = context.templates.clone();
            let result : CompiledDefine;
            // dynamic templates (both name and body) can only work if extra types are allowed: `hairy_bytecode(string, arg1, arg2)` in expry.
            // In this code we can retrieve that type, check that `hairy_bytecode` matches,
            // know that the actual type is a string, and check if all the arguments matches in
            // type. We still can not check the contents of the bytecode. So this can work
            // as long as all extra types does not contain the any type (because then the contents
            // of the template is needed to perform additional checks)..
            if opcode == HairyBytecode::CallDynamicName as u8 || opcode == HairyBytecode::CallDynamicBody as u8 {
                // FIXME: add strict mode and disallow this
                return Ok(());
            } else if opcode == HairyBytecode::CallStatic as u8 {
                result = match resolve_template(name, &context.templates).map_err(|_| HairyEvalError::Error(write!(allocator, "template definitions problems: '{}' not found", String::from_utf8_lossy(name))))? {
                    None => {
                        return Err(HairyError::wrap(HairyEvalError::TemplateNotFound(allocator.copy_u8(name)),HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no));
                    },
                    Some(values) => values,
                };
                //eprintln!("call static template '{}' (of '{}'), with template defaults of {} bytes, and body of {} bytes", String::from_utf8_lossy(name), String::from_utf8_lossy(template_name), template_defaults.0.len(), template_bytecode.0.len());
            } else {
                return Err(HairyError::wrap(HairyEvalError::Error("unrecognized loop opcode in template bytecode"),HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no));
            }
            if result.args_count != args_count || args_count > MAX_TEMPLATE_ARGS {
                return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "call of template '{}' with {} arguments instead of expected {} arguments", String::from_utf8_lossy(result.name), args_count, result.args_count)), HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no));
            }
            let mut values = Vec::new();
            let mut arg_type_reader = RawReader::with(result.lazy_arg_types);
            let mut changed = false;
            for i in 0..args_count {
                let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
                let (actual_arg_type, warnings) = expry_type_from_bytecode(expr, &context.values, context.custom_types, allocator).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no))?;
                if !warnings.is_empty() {
                    let hairy_error = expry_type_warnings_to_hairy_eval_error(&warnings, allocator);
                    return Err(HairyError::wrap(hairy_error, HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no));
                }
                let mut arg_type = expry_parse_type(&String::from_utf8_lossy(arg_type_reader.read_var_string()?), allocator).map_err(|_| HairyError::wrap(HairyEvalError::Error(write!(allocator, "error in bytecode of arguments of define '{}' in file '{}'", String::from_utf8_lossy(result.name), String::from_utf8_lossy(result.filename))), HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no))?;
                //eprintln!("checking type of {}:{}: {} vs {}", String::from_utf8_lossy(result.name), i, arg_type, actual_arg_type);
                if actual_arg_type.used_as(&arg_type) {
                    // Call check should only trigger if an Any type is replaced by something more
                    // specific, as if the types are the same it has already been checked compile
                    // time. It should not trigger for objects/arrays that can be used as another object,
                    // because that will not result in more specific types (so no extra errors can
                    // be detected). We currently do not have a way to check for the latter, so we
                    // assume that it is the case for any compound type.
                    if arg_type == ExpryType::Any  || matches!(arg_type, ExpryType::Object(_) | ExpryType::UniformObject(_) | ExpryType::Array(_) | ExpryType::StaticArray(_) ) {
                        changed = true;
                    }
                    arg_type = actual_arg_type;
                } else {
                    return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "call of template '{}' with wrong type for argument {}: expected {}, but {} can not be used as that", String::from_utf8_lossy(result.name), i, arg_type, actual_arg_type)), HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no));
                }
                values.push(arg_type);
            }
            // recursive calls are deemed ok if the types are ok
            if context.template_name != result.name && changed {
                let mut context2 = CheckTypeContext { templates, template_name: result.name, custom_types: context.custom_types, values: values.iter().collect(), escaper: context.escaper };
                check_types(&mut RawReader::with(result.body.get()), &mut context2, allocator, depth-1).map_err(|err| err.add_location(HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no))?;
                //eprintln!("end call template {} with {} bytes", String::from_utf8_lossy(template_name), template_bytecode.0.len());
            }
            continue;
        }
        return Err(HairyEvalError::Bytecode(write!(allocator, "unrecognized opcode {opcode}")).into());
    }
    Ok(())
}
    
pub fn hairy_check_types<'a,'b,'c>(template_bytecode: BytecodeRef<'b>, dynamic_value_names: Vec<&ExpryType>, given_templates: Option<&'a [RawReader<'b>]>, custom_types: &'a std::collections::BTreeMap<Key<'a>, (Vec<ExpryType>, ExpryType)>, escaper: Option<&'a dyn Escaper>, allocator: &mut MemoryScope<'c>) -> Result<(),HairyError<'b>> where 'c: 'b, 'b: 'a {
    let CompiledDefine{name, body, subtemplates, args_count, ..} = decode_hairy(template_bytecode.get())?;
    
    if dynamic_value_names.len() != args_count as usize {
        return Err(HairyEvalError::Error(write!(allocator, "template called with wrong number of arguments (values), {} instead of {}", dynamic_value_names.len(), args_count)).into());
    }

    let mut templates = Vec::with_capacity(given_templates.as_ref().map(|x| x.len()).unwrap_or_default() + 1);
    if let Some(given_templates) = &given_templates {
        templates.extend_from_slice(given_templates);
    }
    templates.push(RawReader::with(&subtemplates));

    let mut context = CheckTypeContext { templates, template_name: name, custom_types, values: dynamic_value_names, escaper };
    let mut bytecode_reader = RawReader::with(body.get());
    check_types(&mut bytecode_reader, &mut context, allocator, MAX_TEMPLATE_RECURSION)?;
    Ok(())
}

pub fn hairy_compile_error_format(e: &mut HairyCompileError) -> (u32, String) {
    let mut retval = String::new();
    let (expr, start, end, extra_line_no, error, extra) = (e.expr, e.start, e.end, e.extra_line_no, e.error, e.extra);
    let line_context = e.get_line_context();
    let (line_no, prefix, error_msg) = line_context.format_error_context(expr, start, end, extra_line_no).map_or((0u32, String::new(), String::new()), |x| x);
    if line_no > 0 {
        write!(retval, "{}{}error at line {}:{} {}\n{}", prefix, TERM_BRIGHT_RED, line_no, TERM_RESET, error, error_msg).ok();
    } else {
        write!(retval, "unknown error, error during hairy_compile_error_format").ok();
    }
    if let Some((start, end)) = extra {
        let (line_no, prefix, error_msg) = line_context.format_error_context(expr, start, end, extra_line_no).map_or((0u32, String::new(), String::new()), |x| x);
        if line_no > 0 {
            write!(retval, "{}{}related at line {}:{} expected it here (or earlier)\n{}", prefix, TERM_DIM_CYAN, line_no, TERM_RESET, error_msg).ok();
        }
    }
    (line_no, retval)
}

/// The 'easy' interface if you just want to quickly use a HTML template, with auto escaping of the
/// input, and returning a nicely formatted error that can be presented to the user. For more
/// options, see the [`hairy_compile`] function.
///
/// To evaluate, use [`hairy_eval_html`], so the proper escaper is used.
///
/// Note that although `hairy_compile_html` and `hairy_eval_html` are easier to use, they are
/// somewhat slower. For top performance please use other functions.
///
/// The `inline_overrides` argument overrides the inline key-value pairs that can be specified on the top
/// of a template. These keys can contain alphanumeric characters, `_`, and `$`. The values should be
/// valid expry expressions (JSON is a valid expry). These values are compiled using the
/// `inline_overrides` as value, so the inlines and defaults can depend on a (sub)value of `compiled` (so derived inlines and defaults can be generated).
///
/// # Example
///
/// ```
/// use hairy::*;
/// use expry::*;
/// use expry_macros::*;
/// use std::io::Write;
/// let template = r#"foobar = {{=this.foovar .. this.barvar}}"#;
/// let mut options = HairyOptions::new();
/// let value = value!({
///   "foovar": "foo",
///   "barvar": "bar",
/// }).encode_to_vec();
/// options.set_named_dynamic_values(&[("this",value.to_ref())]);
/// let result = hairy_compile_html(template, "test.tpl", &(&options).try_into().unwrap());
/// match result {
///   Ok(parsed) => {
///     match hairy_eval_html(parsed.to_ref(), &(&options).try_into().unwrap()) {
///       Ok(output) => { std::io::stdout().write_all(&output); },
///       Err(err) => { eprintln!("{}", err); },
///     }
///   },
///   Err(err) => {
///     eprintln!("{}", err);
///     panic!(); // to check this example
///   }
/// }
/// ```
pub fn hairy_compile_html<'b>(reader: &'b str, filename: &'b str, options: &HairyCompileOptions) -> Result<BytecodeVec,String> {
    let mut allocator = MemoryPool::new();
    let mut scope = allocator.rewind();
    hairy_compile_html_scope(reader, filename, options, &mut scope)
}

pub fn hairy_compile_html_scope<'b>(reader: &'b str, filename: &'b str, options: &HairyCompileOptions, scope: &mut MemoryScope) -> Result<BytecodeVec,String> {
    let escaping = parse_html(reader, options.extra_line_no, scope).map_err(|mut err| hairy_compile_error_format(&mut err).1)?;
    hairy_compile(reader, filename, options, scope, Some(escaping)).map_err(|mut err| hairy_compile_error_format(&mut err).1)
}

fn template_to_binary<E, Out: RawOutput<E>>(template: &Define, writer: &mut Out) -> Result<(), E> {
    let Define{name, filename, args, body, subtemplates} = template;
    writer.write_bytes(HAIRY_MAGIC)?;
    writer.write_var_bytes(name.as_bytes())?;
    writer.write_var_bytes(filename.as_bytes())?;
    writer.write_var_u64(args.len() as u64)?;
    // args
    write_with_header(writer, Out::write_var_u64, |writer| {
        for type_spec in args {
            // FIXME: probably slow to generate it twice (one for the length, one for the contents. Switching to binary representation helps.
            let type_spec = format!("{type_spec}");
            writer.write_var_bytes(type_spec.as_bytes())?;
        }
        Ok(())
    })?;
    // body
    write_with_header(writer, Out::write_var_u64, |writer| {
        ast_to_binary(body, writer)
    })?;
    // subtemplates
    write_with_header(writer, Out::write_var_u64, |writer| {
        templates_to_binary(subtemplates, writer)
    })
}

fn templates_to_binary<E, Out: RawOutput<E>>(templates: &[Define], writer: &mut Out) -> Result<(), E> {
    for d in templates {
        template_to_binary(d, writer)?;
    }
    Ok(())
}

fn templates_to_binary_size(templates: &[Define]) -> usize {
    let mut length_collector = RawWriterLength::new();
    templates_to_binary(templates, &mut length_collector).unwrap_infallible();
    length_collector.length()
}

fn ast_to_binary_size(ast: &[HairyCommand]) -> usize {
    let mut length_collector = RawWriterLength::new();
    ast_to_binary(ast, &mut length_collector).unwrap_infallible();
    length_collector.length()
}

enum HairyBytecode {
    Return = 0,
    Text = 1,
    Evaluate = 2,
    Loop = 4,
    ConditionalBool = 5,
    ConditionalLet = 6,
    CallStatic = 7,
    CallDynamicName = 8,
    CallDynamicBody = 9,
}

fn ast_to_binary<E, Out: RawOutput<E>>(ast: &[HairyCommand], writer: &mut Out) -> Result<(), E> {
    for cmd in ast {
        match cmd {
            HairyCommand::Text(text) => {
                writer.write_u8(HairyBytecode::Text as u8)?;
                writer.write_var_bytes(text)?;
            },
            HairyCommand::Conditional(conditions, else_body) => {
                generate_conditions(conditions, else_body, writer)?;
            },
            HairyCommand::Loop(expr, _, _, body, source_context) => {
                writer.write_u8(HairyBytecode::Loop as u8)?;
                writer.write_var_u64(source_context.0 as u64)?;
                writer.write_var_u64(source_context.1 as u64)?;
                writer.write_var_bytes(expr.get())?;
                write_with_header(writer, Out::write_var_u64, |writer| ast_to_binary(body, writer))?;
            },
            HairyCommand::Eval(expr, escape_mode, source_context) => {
                writer.write_u8(HairyBytecode::Evaluate as u8)?;
                writer.write_var_u64(source_context.0 as u64)?;
                writer.write_var_u64(source_context.1 as u64)?;
                writer.write_var_bytes(expr.get())?;
                writer.write_var_bytes(escape_mode)?;
            },
            HairyCommand::CallTemplate(name, args, source_context) => {
                match name {
                    ResolveTemplate::Static(_) => writer.write_u8(HairyBytecode::CallStatic as u8)?,
                    ResolveTemplate::DynamicName(_) => writer.write_u8(HairyBytecode::CallDynamicName as u8)?,
                    ResolveTemplate::DynamicBody(_) => writer.write_u8(HairyBytecode::CallDynamicBody as u8)?,
                }
                writer.write_var_u64(source_context.0 as u64)?;
                writer.write_var_u64(source_context.1 as u64)?;
                match name {
                    ResolveTemplate::Static(name) => writer.write_var_bytes(name.as_bytes())?,
                    ResolveTemplate::DynamicName(expr) => writer.write_var_bytes(expr.get())?,
                    ResolveTemplate::DynamicBody(expr) => writer.write_var_bytes(expr.get())?,
                }
                writer.write_var_u64(args.len() as u64)?;
                for (e,_return_type) in args {
                    writer.write_var_bytes(e.get())?;
                }
            },
        }
    }
    Ok(())
}

fn generate_conditions<E, Out: RawOutput<E>>(conditions: &[(Option<Name>, BytecodeRef, Vec<HairyCommand>, SourceContext)], else_body: &[HairyCommand], writer: &mut Out) -> Result<(), E> {
    if conditions.is_empty() {
        ast_to_binary(else_body, writer)
    } else {
        let (variable, expr, body, source_context) = &conditions[0];
        if variable.is_some() {
            writer.write_u8(HairyBytecode::ConditionalLet as u8)?;
        } else {
            writer.write_u8(HairyBytecode::ConditionalBool as u8)?;
        }
        writer.write_var_u64(source_context.0 as u64)?;
        writer.write_var_u64(source_context.1 as u64)?;
        writer.write_var_bytes(expr.get())?;
        write_with_header(writer, Out::write_var_u64,
                          |writer| ast_to_binary(body, writer))?;
        write_with_header(writer, Out::write_var_u64,
                          |writer| generate_conditions(&conditions[1..], else_body, writer))
    }
}

#[derive(Debug)]
pub enum HairyEvalError<'a> {
    Bytecode(&'a str),
    Evaluate(&'a str),
    Error(&'a str),
    EvalTooLong(), // takes too long, probably recusion depth hit
    TemplateNotFound(&'a [u8]),
}
impl<'a> core::fmt::Display for HairyEvalError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            HairyEvalError::Bytecode(msg) => write!(f, "{msg} (bytecode)"),
            HairyEvalError::Evaluate(msg) => write!(f, "{msg} (eval)"),
            HairyEvalError::Error(msg) => write!(f, "{msg} (error)"),
            HairyEvalError::EvalTooLong() => write!(f, "eval too long"),
            HairyEvalError::TemplateNotFound(msg) => write!(f, "template '{}' not found", String::from_utf8_lossy(msg)),
        }
    }
}

impl<'a> From<EncodingError> for HairyEvalError<'a> {
    fn from(_: EncodingError) -> Self {
        HairyEvalError::Error("hairy template bytecode error")
    }
}
struct Context<'a,'b,'d> {
    custom: &'a dyn CustomFuncs<'d>,
    templates: &'a [RawReader<'b>],
    template_name: &'b [u8],
    escaper: &'a dyn Escaper,
    values: Vec<EncodedValueRef<'b>>,
}

pub trait Escaper {
    #[allow(clippy::ptr_arg)]
    fn append_to_output<'b,'c,'d>(&self, value: DecodedValue<'b>, escape_mode: &'b [u8], allocator: &mut MemoryScope<'c>, output: &mut Vec<&'d [u8]>) -> Result<(),HairyEvalError<'d>> where 'c: 'd, 'b: 'd;
    fn check_type(&self, value_type: &ExpryType, escape_mode: &[u8]) -> bool;
}

/// Default escaper. Defaults to escaping in `html` mode. Currently supports `html`, `url`, and `none`
/// (throws error on other escape modes).
pub struct DefaultEscaper<'e> {
    default_escape_mode: &'e [u8],
}

impl<'e> Default for DefaultEscaper<'e> {
    fn default() -> Self { Self { default_escape_mode: b"html" } }
}

impl<'e> Escaper for DefaultEscaper<'e> {
    fn append_to_output<'b,'c,'d>(&self, value: DecodedValue<'b>, escape_mode: &'b [u8], allocator: &mut MemoryScope<'c>, output: &mut Vec<&'d [u8]>) -> Result<(),HairyEvalError<'d>> where 'c: 'd, 'b: 'd {
        let escape_mode = if escape_mode.is_empty() {
            self.default_escape_mode
        } else {
            escape_mode
        };
        if escape_mode == b"sjs" || escape_mode == b"js" {
            if !value.is_valid_json() {
                return Err(HairyEvalError::Error("UTF-8 problem in js mode: maybe non-text is outputted in a key or a string?"));
            }
            let mut json = write!(allocator, "{value}").as_bytes();
            if escape_mode == b"sjs" {
                json = allocator.copy_with_replacement(json, html_escape_outside_attribute_u8);
            }
            output.push(json);
            return Ok(());
        }
        match value {
            DecodedValue::Null => Ok(()),
            DecodedValue::Int(i) => {
                output.push(write!(allocator, "{i}").as_bytes());
                Ok(())
            },
            DecodedValue::Float(f) => {
                // The output is improved for humans. It forces to convert 0.99999998f to 1.0
                let mut out = match escape_mode {
                    b"unrounded" => write!(allocator, "{f}"),
                    b"f0" => write!(allocator, "{f:.0}"),
                    b"f1" => write!(allocator, "{f:.1}"),
                    b"f2" => write!(allocator, "{f:.2}"),
                    b"f3" => write!(allocator, "{f:.3}"),
                    b"f4" => write!(allocator, "{f:.4}"),
                    b"f5" => write!(allocator, "{f:.5}"),
                    b"f6" => write!(allocator, "{f:.6}"),
                    _ => write!(allocator, "{f:.6}"),
                };
                if escape_mode == self.default_escape_mode {
                    // strip of zero's of the end
                    out = &out[0..out.rfind(|x| x != '0').map_or(out.len(), |x| if out.as_bytes()[x] == b'.' { x+2 } else { x+1 })];
                }
                output.push(out.as_bytes());
                Ok(())
            },
            DecodedValue::Double(f) => {
                // The output is improved for humans. It forces to convert 0.999999999999996 to 1.0
                let mut out = match escape_mode {
                    b"unrounded" => write!(allocator, "{f}"),
                    b"f0" => write!(allocator, "{f:.0}"),
                    b"f1" => write!(allocator, "{f:.1}"),
                    b"f2" => write!(allocator, "{f:.2}"),
                    b"f3" => write!(allocator, "{f:.3}"),
                    b"f4" => write!(allocator, "{f:.4}"),
                    b"f5" => write!(allocator, "{f:.5}"),
                    b"f6" => write!(allocator, "{f:.6}"),
                    _ => write!(allocator, "{f:.14}"),
                };
                if escape_mode == self.default_escape_mode {
                    // strip of zero's of the end
                    out = &out[0..out.rfind(|x| x != '0').map_or(out.len(), |x| if out.as_bytes()[x] == b'.' { x+2 } else { x+1 })];
                }
                output.push(out.as_bytes());
                Ok(())
            },
            DecodedValue::String(s) => {
                if escape_mode == b"html" {
                    output.push(allocator.copy_with_replacement(s, html_escape_outside_attribute_u8));
                } else if escape_mode == b"+" {
                    output.push(allocator.copy_with_replacement(s, html_escape_inside_attribute_u8));
                } else if escape_mode == b"url" {
                    output.push(allocator.copy_with_dynamic_replacement(s, url_escape_u8));
                } else if escape_mode == b"none" {
                    output.push(s);
                } else {
                    return Err(HairyEvalError::Evaluate(write!(allocator, "unsupported escape mode: {}", String::from_utf8_lossy(escape_mode))));
                }
                Ok(())
            },
            _ => Err(HairyEvalError::Evaluate("only numbers and strings can be outputted (in regular escape modes)")),
        }
    }

    fn check_type(&self, mut value_type: &ExpryType, escape_mode: &[u8]) -> bool {
        let escape_mode = if escape_mode.is_empty() {
            self.default_escape_mode
        } else {
            escape_mode
        };
        if escape_mode == b"sjs" || escape_mode == b"js" {
            return true;
        }
        if let ExpryType::Nullable(inner) = value_type {
            value_type = &**inner;
        }
        match value_type {
            ExpryType::Any => true, // strict mode should disallow this
            ExpryType::Null => true,
            ExpryType::Int => true,
            ExpryType::Float => true,
            ExpryType::Double=> true,
            ExpryType::String => true,
            _ => false,
        }
    }
}

fn resolve_template<'b>(name: &'_ [u8], templates: &'_ [RawReader<'b>]) -> Result<Option<CompiledDefine<'b>>,HairyError<'b>> {
    //eprintln!("resolve template '{}' in {} definitions", String::from_utf8_lossy(name), templates.len());
    for reader in templates.iter().rev() {
        let mut reader : RawReader = *reader;
        //eprintln!("new defintions of {} bytes", reader.len());
        while !reader.is_empty() {
            let define = decode_hairy_reader(&mut reader)?;
            //eprintln!("template '{}'", String::from_utf8_lossy(define.name));
            if name == define.name {
                return Ok(Some(define));
            }
        }
    }
    Ok(None)
}

pub fn hairy_templates_to_types(templates: &[RawReader<'_>]) -> Result<std::collections::BTreeMap<Vec<u8>, Vec<ExpryType>>,EncodingError> {
    let mut allocator = MemoryPool::new();
    let mut scope = allocator.rewind();
    let mut retval = std::collections::BTreeMap::new();
    for reader in templates.iter().rev() {
        let mut reader : RawReader = *reader;
        while !reader.is_empty() {
            let define = decode_hairy(reader.read_var_string()?).map_err(|_| EncodingError{ line_nr: line!() })?;
            if !retval.contains_key(define.name) {
                let mut subreader = RawReader::with(define.lazy_arg_types);
                let mut args = Vec::new();
                for _ in 0..define.args_count {
                    let type_spec = subreader.read_var_string()?;
                    // FIXME: replace representation of types with binary type representation, avoids utf8 validation
                    let type_spec = core::str::from_utf8(type_spec).map_err(|_| EncodingError{ line_nr: line!() })?;
                    let result = expry_parse_type(type_spec, &mut scope).map_err(|_| EncodingError{ line_nr: line!() })?;
                    args.push(result);
                }
                let _bytecode = BytecodeRef(subreader.read_var_string()?);
                debug_assert_eq!(0, subreader.len());
                retval.insert(define.name.to_vec(), args);
            }
        }
    }
    Ok(retval)
}

/// Used for stack traces, to make distinction between call sites.
#[derive(Debug)]
pub enum HairyLocationType {
    Conditional,
    Loop,
    Call,
    Eval,
}

/// Used for stack traces, contains the details of a call site.
#[derive(Debug)]
pub struct HairyStackEntry<'a> {
  location_type: HairyLocationType,
  filename: &'a [u8],
  line_no: u64,
  column_no: u64,
}

/// Hairy errors, including a stack trace so debugging is easier.
pub struct HairyError<'a> {
    error: HairyEvalError<'a>,
    stack: Vec<HairyStackEntry<'a>>,
}

impl core::fmt::Display for HairyLocationType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            HairyLocationType::Conditional => write!(f, "cond"),
            HairyLocationType::Loop => write!(f, "loop"),
            HairyLocationType::Call => write!(f, "call"),
            HairyLocationType::Eval => write!(f, "eval"),
        }
    }
}
impl<'a> core::fmt::Display for HairyError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "Hairy error: {}", self.error)?;
        for loc in &self.stack {
            writeln!(f, "  during {} in {} on line {}:{}", loc.location_type, String::from_utf8_lossy(loc.filename), loc.line_no, loc.column_no+1)?;
        }
        Ok(())
    }
}
impl<'a> fmt::Debug for HairyError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{self}")
    }
}

impl<'a> HairyError<'a> {
    fn wrap(error: HairyEvalError<'a>, location_type: HairyLocationType, filename: &'a [u8], line_no: u64, column_no: u64) -> Self { Self { error, stack: vec![HairyStackEntry {
        location_type,
        filename,
        line_no,
        column_no,
    }]} }

    fn add_location(mut self, location: HairyLocationType, filename: &'a [u8], line_no: u64, column_no: u64) -> Self {
        self.stack.push(HairyStackEntry{ location_type: location, filename, line_no, column_no });
        self
    }
}
impl<'a> From<EncodingError> for HairyError<'a> {
    fn from(e: EncodingError) -> Self {
        Self {
            error: e.into(),
            stack: Vec::new(),
        }
    }
}
impl<'a> From<HairyEvalError<'a>> for HairyError<'a> {
    fn from(e: HairyEvalError<'a>) -> Self {
        Self {
            error: e,
            stack: Vec::new(),
        }
    }
}

fn evaluate_to<'a,'b,'c,'d>(expression: &mut RawReader<'b>, context: &'_ mut Context<'a,'b,'d>, allocator: &mut MemoryScope<'c>, output: &mut Vec<&'b [u8]>, depth: usize) -> Result<(),HairyError<'b>> where 'c: 'b, 'b: 'a, 'd: 'b {
    if depth == 0 {
        return Err(HairyEvalError::EvalTooLong().into());
    }
    while !expression.is_empty() {
        let opcode = expression.read_u8().map_err(|_| HairyEvalError::Bytecode("Expected more bytes in expression"))?;
        if opcode == HairyBytecode::Text as u8 {
            let err = |_| HairyEvalError::Bytecode("corrupted TEXT");
            let text_length = expression.read_var_u64().map_err(err)?;
            let text = expression.read_bytes(text_length as usize).map_err(err)?;
            output.push(text);
            continue;
        }
        if opcode == HairyBytecode::Evaluate as u8 {
            let err = |_| HairyEvalError::Bytecode("corrupted EXPR");
            let line_no = expression.read_var_u64().map_err(err)?;
            let column_no = expression.read_var_u64().map_err(err)?;
            let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
            let escape_mode = expression.read_var_string().map_err(err)?;
            let object = expry_eval_func(expr, &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Eval, context.template_name, line_no, column_no))?;
            context.escaper.append_to_output(object, escape_mode, allocator, output).map_err(|err| HairyError::wrap(err, HairyLocationType::Eval, context.template_name, line_no, column_no))?;
            continue;
        }
        if opcode == HairyBytecode::ConditionalBool as u8 || opcode == HairyBytecode::ConditionalLet as u8 {
            let err = |_| HairyEvalError::Bytecode("corrupted CONDITIONAL");
            let line_no = expression.read_var_u64().map_err(err)?;
            let column_no = expression.read_var_u64().map_err(err)?;
            let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
            let body = expression.read_var_string().map_err(err)?;
            let else_body = expression.read_var_string().map_err(err)?;
            if opcode == HairyBytecode::ConditionalLet as u8 {
                let retval = expry_slice_func(expr, &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")),HairyLocationType::Conditional, context.template_name, line_no, column_no))?;
                if LazyDecodedValue::Null == expry_decode_lazy(&retval)? {
                    if !else_body.is_empty() {
                        evaluate_to(&mut RawReader::with(else_body), context, allocator, output, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, context.template_name, line_no, column_no))?;
                    }
                    continue;
                }
                context.values.push(retval);
                let retval = evaluate_to(&mut RawReader::with(body), context, allocator, output, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, context.template_name, line_no, column_no));
                context.values.pop();
                retval?;
                continue;
            }

            let retval = expry_eval_func(expr, &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")),HairyLocationType::Conditional, context.template_name, line_no, column_no))?;
            if let DecodedValue::Bool(true) = retval {
                evaluate_to(&mut RawReader::with(body), context, allocator, output, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, context.template_name, line_no, column_no))?;
                continue;
            }
            // `expr???` indicates ignore on error (which produces a null value), so we
            // continue on null.
            if matches!(retval, DecodedValue::Bool(_) | DecodedValue::Null) {
                if !else_body.is_empty() {
                    evaluate_to(&mut RawReader::with(else_body), context, allocator, output, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, context.template_name, line_no, column_no))?;
                }
                continue;
            }
            return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "expression in conditional should result in a bool or null, not {}", retval.type_string())), HairyLocationType::Conditional, context.template_name, line_no, column_no));
        }
        if opcode == HairyBytecode::Loop as u8 {
            let err = |_| HairyEvalError::Bytecode("corrupted LOOP");
            let line_no = expression.read_var_u64().map_err(err)?;
            let column_no = expression.read_var_u64().map_err(err)?;
            let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
            let body = expression.read_var_string().map_err(err)?;
            let retval = expry_slice_func(expr, &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")),HairyLocationType::Loop, context.template_name, line_no, column_no))?;
            let parsed = expry_decode_lazy(retval.get())?;
            if let LazyDecodedValue::Array(mut array) = parsed {
                let mut index : i64 = 0;
                while !array.is_empty() {
                    // make a state with the current state with one variable replaced.
                    let iteration_value = array.get_raw()?;

                    let index_value = DecodedValue::Int(index).encode_to_scope(allocator);
                    context.values.push(index_value);
                    context.values.push(iteration_value);
                    let retval = evaluate_to(&mut RawReader::with(body), context, allocator, output, depth-1).map_err(|err| err.add_location(HairyLocationType::Loop, context.template_name, line_no, column_no));
                    context.values.pop();
                    context.values.pop();
                    retval?;

                    index += 1;
                }
                continue;
            }
            // added so that `expr???` indicates ignore on error
            if let LazyDecodedValue::Null = parsed {
                continue;
            }
            return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "expression in loop should result in an array (with any value) or null, not {}", parsed.type_string())), HairyLocationType::Loop, context.template_name, line_no, column_no));
        }
        if opcode == HairyBytecode::CallDynamicName as u8 || opcode == HairyBytecode::CallDynamicBody as u8 || opcode == HairyBytecode::CallStatic as u8 {
            let err = |_| HairyEvalError::Bytecode("corrupted LOOP");
            let line_no = expression.read_var_u64().map_err(err)?;
            let column_no = expression.read_var_u64().map_err(err)?;
            let mut name = expression.read_var_string().map_err(err)?;
            let args_count = expression.read_var_u64().map_err(err)?;
            let result;
            if opcode == HairyBytecode::CallDynamicName as u8 {
                let name_value = expry_eval_func(BytecodeRef(name), &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Call, context.template_name, line_no, column_no))?;
                if let DecodedValue::String(name_string) = name_value {
                    name = name_string;
                } else if let DecodedValue::Null = name_value {
                    return Ok(());
                } else {
                    return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "expression in dynamic name template call should resolve to a string (or null), not {}", name_value.type_string())), HairyLocationType::Call, context.template_name, line_no, column_no));
                }
                result = match resolve_template(name, &context.templates).map_err(|_| HairyEvalError::Error(write!(allocator, "template definitions problems: '{}' not found", String::from_utf8_lossy(name))))? {
                    None => {
                        return Err(HairyError::wrap(HairyEvalError::TemplateNotFound(name),HairyLocationType::Call, context.template_name, line_no, column_no));
                    },
                    Some(values) => values,
                };
                //eprintln!("call dynamic template {} with {} bytes", String::from_utf8_lossy(name), template_bytecode.0.len());
            } else if opcode == HairyBytecode::CallDynamicBody as u8 {
                let hairy_template;
                let value = expry_eval_func(BytecodeRef(name), &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Call, context.template_name, line_no, column_no))?;
                if let DecodedValue::String(contents) = value {
                    hairy_template = contents;
                } else if let DecodedValue::Null = value {
                    return Ok(());
                } else {
                    return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "expression in dynamic body template call should resolve to a string (or null), not {}", value.type_string())), HairyLocationType::Call, context.template_name, line_no, column_no));
                }
                result = decode_hairy(hairy_template)?;
                //eprintln!("call dynamic bytecode with {} bytes and {} args", template_bytecode.0.len(), template_arguments);
            } else if opcode == HairyBytecode::CallStatic as u8 {
                result = match resolve_template(name, &context.templates).map_err(|_| HairyEvalError::Error(write!(allocator, "template definitions problems: '{}' not found", String::from_utf8_lossy(name))))? {
                    None => {
                        return Err(HairyError::wrap(HairyEvalError::TemplateNotFound(name),HairyLocationType::Call, context.template_name, line_no, column_no));
                    },
                    Some(values) => values,
                };
                //eprintln!("call static template '{}' (of '{}'), with template defaults of {} bytes, and body of {} bytes", String::from_utf8_lossy(name), String::from_utf8_lossy(template_name), template_defaults.0.len(), template_bytecode.0.len());
            } else {
                return Err(HairyError::wrap(HairyEvalError::Error("unrecognized loop opcode in template bytecode"),HairyLocationType::Call, context.template_name, line_no, column_no));
            }
            if result.args_count != args_count || args_count > MAX_TEMPLATE_ARGS {
                return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "call of template with {} arguments instead of expected {} arguments", args_count, result.args_count)), HairyLocationType::Call, context.template_name, line_no, column_no));
            }
            let mut values = Vec::new();
            for _i in 0..args_count {
                let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
                let retval = expry_slice_func(expr, &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Call, context.template_name, line_no, column_no))?;
                //eprintln!("arg {}: {}", i, expry_decode(retval.get()).unwrap());
                values.push(retval);
            }
            let mut templates = ScopedArrayBuilder::new(allocator);
            templates.extend_from_slice(&context.templates);
            templates.push(RawReader::with(result.subtemplates.get()));
            let templates = templates.build();
            let mut context2 = Context { templates, template_name: result.name, custom: context.custom, escaper: context.escaper, values, };
            evaluate_to(&mut RawReader::with(result.body.get()), &mut context2, allocator, output, depth-1).map_err(|err| err.add_location(HairyLocationType::Call, context.template_name, line_no, column_no))?;
            //eprintln!("end call template {} with {} bytes", String::from_utf8_lossy(template_name), template_bytecode.0.len());
            continue;
        }
        return Err(HairyEvalError::Bytecode(write!(allocator, "unrecognized opcode {opcode}")).into());
    }
    Ok(())
}

/// Evaluate Hairy bytecode to generate output, using a `value` encoded as a [`expry`] object.
pub fn hairy_eval<'a,'b,'c,'d,'e>(template_bytecode: BytecodeRef<'b>, options: &HairyEvalOptions<'b,'d,'e>, allocator: &mut MemoryScope<'c>) -> Result<Vec<&'b [u8]>, HairyError<'b>> where 'c: 'b, 'b: 'a, 'e: 'b {
    let CompiledDefine{name, body, args_count, subtemplates, ..} = decode_hairy(template_bytecode.get())?;
    
    if options.values.len() != args_count as usize {
        return Err(HairyEvalError::Error(write!(allocator, "template called with wrong number of arguments (values), {} instead of {}", options.values.len(), args_count)).into());
    }

    let mut templates = ScopedArrayBuilder::new(allocator);
    if let Some(given_templates) = &options.given_templates {
        templates.extend_from_slice(given_templates);
    }
    templates.push(RawReader::with(subtemplates.get()));
    let templates = templates.build();

    let mut output : Vec<&'b [u8]> = Vec::with_capacity(128);
    let mut context = Context { templates, template_name: name, custom: options.custom, escaper: options.escaper, values: options.values.to_vec() };
    let mut bytecode_reader = RawReader::with(body.get());
    evaluate_to(&mut bytecode_reader, &mut context, allocator, &mut output, MAX_TEMPLATE_RECURSION)?;
    Ok(output)
}

/// The 'easy' interface if you just want to quickly use a HTML template, with auto escaping of the
/// input, and returning a nicely formatted error that can be presented to the user. For more
/// options, see the [`hairy_eval`] function.
///
/// To compile, use [`hairy_compile_html`], so the proper escaper is used.
///
/// Note that although `hairy_compile_html` and `hairy_eval_html` are easier to use, they are
/// somewhat slower. For top performance please use other functions.
pub fn hairy_eval_html(template_bytecode: BytecodeRef<'_>, options: &HairyEvalOptions) -> Result<Vec<u8>, String> {
    let mut allocator = MemoryPool::new();
    let mut scope = allocator.rewind();
    let result = hairy_eval(template_bytecode, options, &mut scope);
    match result {
        Ok(result) => Ok(result.concat()),
        Err(err) => Err(format!("{err}")),
    }
}

const HAIRY_MAGIC : &[u8; 2] = b"hy";

fn decode_hairy(template_bytecode: &[u8]) -> Result<CompiledDefine, HairyError> {
    let mut reader = RawReader::with(template_bytecode);
    decode_hairy_reader(&mut reader)
}

fn decode_hairy_reader<'b>(reader: &mut RawReader<'b>) -> Result<CompiledDefine<'b>, HairyError<'b>> {
    let header = reader.read_bytes(HAIRY_MAGIC.len())?;
    if header != HAIRY_MAGIC {
        return Err(HairyEvalError::Bytecode("expression is missing magic header ('hair')").into());
    }

    let name = reader.read_var_string()?;
    let filename = reader.read_var_string()?;
    let args_count = reader.read_var_u64()?;
    let lazy_args = reader.read_var_string()?;
    let body = BytecodeRef(reader.read_var_string()?);
    let subtemplates = BytecodeRef(reader.read_var_string()?);
    //eprintln!("hairy decoded: {} bytes bytecode, {} bytes subtemplates", body.get().len(), subtemplates.get().len());
    Ok(CompiledDefine{name, filename, args_count, lazy_arg_types: lazy_args, body, subtemplates })
}

#[cfg(test)]
mod hairy {
    use std::io::Write;
    use std::collections::BTreeMap;

    use crate::*;

    pub struct TestCustomFuncs {
    }

    impl<'a> CustomFuncs<'a> for TestCustomFuncs {
        fn call<'b,'c>(&'_ self, name: &'_ [u8], _args: &'_ [DecodedValue<'b>], scope: &'_ mut MemoryScope<'c>) -> Result<DecodedValue<'b>,&'b str> where 'c: 'b {
            if b"func" == name {
                Ok(DecodedValue::String(scope.copy_u8("dyncustomfoo".as_bytes())))
            } else {
                Err("no custom functions defined except for 'func'")
            }
            //Ok(Binary::String(scope.copy(name.key)))
        }
        fn types(&self) -> BTreeMap<Key<'static>,(Vec<ExpryType>,ExpryType)> {
            BTreeMap::from([
              (key_str("func"), (vec![], ExpryType::String)),
            ])
        }
    }

    #[test]
    fn test_escape() {
        let template = r#"foobar = \{{=this.foovar .. this.barvar}}"#;
        let value = value!({
            "foovar": "foo",
            "barvar": "bar",
        }).encode_to_vec();
        let mut options = HairyOptions::new();
        options.set_named_dynamic_values(&[("this", value.to_ref())]);
        let result = hairy_compile_html(template, "test.tpl", &(&options).try_into().unwrap());
        match result {
            Ok(parsed) => {
                match hairy_eval_html(parsed.to_ref(), &(&options).try_into().unwrap()) {
                    Ok(output) => {
                        assert_eq!(br#"foobar = {{=this.foovar .. this.barvar}}"#, &output[..]);
                    },
                    Err(err) => { eprintln!("{}", err); },
                }
            },
            Err(err) => {
                eprintln!("{}", err);
            }
        }
    }

    #[test]
    fn example_html_interface() {
        let template = r#"foobar = {{=this.foovar .. this.barvar}}"#;
        let value = value!({
            "foovar": "foo",
            "barvar": "bar",
        }).encode_to_vec();
        let mut options = HairyOptions::new();
        options.set_named_dynamic_values(&[("this", value.to_ref())]);
        let result = hairy_compile_html(template, "test.tpl", &(&options).try_into().unwrap());
        match result {
            Ok(parsed) => {
                match hairy_eval_html(parsed.to_ref(), &(&options).try_into().unwrap()) {
                    Ok(output) => {
                        assert_eq!(b"foobar = foobar", &output[..]);
                    },
                    Err(err) => { eprintln!("{}", err); },
                }
            },
            Err(err) => {
                eprintln!("{}", err);
            }
        }
    }

    struct Foo {
        foo: u32,
        bar: bool,
    }


    impl<'a> From<&'a Foo> for DecodedValue<'a> {
        fn from(v: &'a Foo) -> Self {
            value!({
                "foo": v.foo as i64,
                "bar": v.bar,
            })
        }
    }

    #[test]
    fn example_nesting() {
        let main_template = r#"{{inline $sitetitle = "foooooobaaaar"}}
{{inline $arr = [1,2,3]}}
<html><title>{{=$sitetitle}}: {{=this.pagetitle}}</title><body>{{call ((this.bodytemplate))(this, this.body)}}</body></html>"#;
        let mut options = HairyCompileOptions::new();
        options.set_dynamic_value_name_and_types(&[("this", expry_type!("{pagetitle: string, foos: [{foo:int,bar:bool}], foo: {foo:int,bar:bool}, bodytemplate: string, body: {foobarvar: string}}"))]);
        let main = hairy_compile_html(main_template, "main.tpl", &options);
        if let Err(main) = &main {
            eprintln!("{}", main);
        }
        let main = main.unwrap();
        let child_template = r#"<p>title of this page = {{=this.pagetitle}}</p><p>foobar = {{=body.foobarvar}}"#;
        options.set_dynamic_value_name_and_types(&[("this", expry_type!("{pagetitle: string, foos: [{foo:int,bar:bool}], foo: {foo:int,bar:bool}, bodytemplate: string, body: {foobarvar: string}}")), ("body", expry_type!("{foobarvar: string}"))]);
        let child = hairy_compile_html(child_template, "child.tpl", &options);
        if let Err(err) = &child {
            println!("{}", err);
        }
        let child = child.unwrap();
        let foos = vec![
            Foo{foo:1,bar:true},
            Foo{foo:2,bar:false},
        ];
        let value = value!({
          "bodytemplate": child,
          "body": {"foobarvar": "foobar"},
          "pagetitle": "my page",
          "foo": Foo{foo:1,bar:true},
          "foos": foos,
        }).encode_to_vec();
        let mut options = HairyEvalOptions::new();
        options.values = vec![value.to_ref()];
        assert_eq!("<html><title>foooooobaaaar: my page</title><body><p>title of this page = my page</p><p>foobar = foobar</body></html>", String::from_utf8_lossy(&hairy_eval_html(main.to_ref(), &options).unwrap()));
    }

    #[test]
    fn basic() {
        //let hair = r####"foo{{"♥" .. "heart"}}bar -> {{1+2+3}}"####.as_bytes();
        let hairy = r####"
Basic: {{="a"}},{{=1+2+3}}

Normally html is auto escaped (to prevent cross-site-scripting attacks):
{{="<p><b>Bold</b></p>"}}

But if you don't want that, you can add a different escape mode:
{{="<p><b>Bold</b></p>":none}}

Because these templates are aimed at humans, floats get rounded. All these expressions result in the output "1.0". Floats are rounded to 6 digits after the decimal seperator, doubles are rounded tot 14 digits after the decimal seperator. Other behaviour can be specified by using a different escaper or different escape modes.
float rounding:  {{=0.9999998f}}
float rounding:  {{=0.99999998f}}
double rounding: {{=0.999999999999996}}
double rounding: {{=1.0}}
float not rounded:  {{=0.9999998f:unrounded}}

Conditional:
\{{if true}}
Show text
\{{else}}
Hidden
\{{end}}

Output:
{{if true}}
Show text
{{else}}
Hidden
{{end}}

Output:
{{if this.number>0???}}
Not shown, because condition triggers an error
{{else}}
Shown because condition triggers an error which is catched with the `???` operator.
{{endif}}

Loop over array:
\{{for i in this.numbers}}
- \{{=i}}
\{{end}}

Output:
{{for i in this.numbers}}
- {{=i}}
{{end}}

Loop over array with objects, using an index variable (with `\{{for (index,person) in this.persons}}`):
{{for (index,person) in this.persons}}
- {{=index}}={{=person.name}}
{{end}}

Call a template, with a specific context specified after the `&lt;-`. See next example how to define one:
{{call card(false,"Some card", "Explanation over some card.")}}

If the template name is prefixed with one `*`, the template name can be an expression that is evaluated to a template name:
{{call ("CARD".lower())(false, "Some card", "Explanation over some card.")}}

If the template name is prefixed with `**`, the template name can be an expression that is evaluated to template binary code. This is useful to embed templates in one each other. See another example for this.

Define a template. The template can optionally have default values, that are specified after `defaults`. These default values are only evaluated once during compile time (so they do not depend on the context a template is evaluted in).
{{define card(dark:bool,name:string,body:string)}}
<div class="card-{{=dark?"dark":"light"}}">
  <h2>{{=name}}</h2>
  <p>{{=body}}</p>
</div>
{{enddefine}}

        "####;
        //let hair = r####"{{define test}}{{foo}}{{end}}{{test <- {foo:32}}}"####.as_bytes();
        let mut allocator = MemoryPool::new();
        let mut scope = allocator.rewind();
        let maincode = r#"
<html>
    <title extra="{{=this.title}}">{{=this.title}}</title>
    <script>{{=this.title}}</script>
    <script>var foo = {{=this.persons:js}};</script>
    <body>
    {{call ((this.content))(this.numbers,this.persons,this.number)}}
    </body>
</html>"#;
        let mut main = parse_html(maincode, 0, &mut scope);
        if let Err(err) = &mut main {
            println!("{}", hairy_compile_error_format(err).1);
        }
        let mut options = HairyCompileOptions::new();
        options.set_dynamic_value_name_and_types(&[("this", expry_type!("{title:string,persons:[{name:string}],content:string,numbers:[int],number:int}"))]);
        options.custom_types = TestCustomFuncs{}.types();
        let mut main = hairy_compile(maincode, "main.tpl", &options, &mut scope, Some(main.unwrap()));
        if let Err(err) = &mut main {
            println!("{}", hairy_compile_error_format(err).1);
        }
        let main = main.unwrap();
        let mut escaping = parse_html(hairy, 0, &mut scope);
        if let Err(err) = &mut escaping {
            println!("{}", hairy_compile_error_format(err).1);
        }
        let escaping = escaping.unwrap();
        let mut options = HairyOptions::new();
        let value = value!({
            "title": "My title<script>",
            "content": main,
            "numbers": [1,2,3,4],
            "persons": [{"name": "Andrew"},{"name": "Bart"},{"name": "Casper"}],
            "number": 1,
        }).encode_to_scope(&mut scope);
        options.set_named_dynamic_values(&[
            ("this", value),
        ]);
        options.custom(&TestCustomFuncs{});
//&[("numbers", expry_type!("[int]")), ("persons", expry_type!("[{name:string}]"))]
        let mut test = hairy_compile(hairy, "test.tpl", &(&options).try_into().unwrap(), &mut scope, Some(escaping));
        if let Err(err) = &mut test {
            println!("{}", hairy_compile_error_format(err).1);
        }
        let test = test.unwrap();
        if false {
            for _ in 0..16 {
                let before = std::time::Instant::now();
                let count = 4*16384;
                for _ in 0..count {
                    let mut scope = scope.rewind();
                    let output = hairy_eval(main.to_ref(), &(&options).try_into().unwrap(), &mut scope).unwrap();
                    assert!(!output.is_empty());
                }
                let after = std::time::Instant::now();
                let dur = after - before;
                eprintln!("{}x in {} ms", count, dur.as_millis());
            }
        } else {
            let output = hairy_eval(test.to_ref(), &(&options).try_into().unwrap(), &mut scope);
            //let output = HairyEvaluator::new().custom_functions(&mut custom).eval(main.to_ref(), value.to_ref(), &mut scope);
            let output = output.unwrap();
            for c in output {
                print!("{}", std::str::from_utf8(c).unwrap());
            }
            println!();
            //assert!(false);
        }
    }
    use expry_macros::*;
    #[test]
    fn compile_time_expr() {
        let compiled = expry!("2+3");
        if cfg!(feature = "mini") {
            assert_eq!(compiled.get().len(), 13);
        } else {
            assert_eq!(compiled.get().len(), 2+3);
        }
        let mut allocator = MemoryPool::new();
        let mut scope = allocator.rewind();
        let result = expry_eval(compiled, &mut vec![], &mut scope);
        let result = result.unwrap();
        assert_eq!(result, value!(5));
    }

    #[test]
    fn compile_time_type_expr() {
        let result = expry_type!("int?");
        assert_eq!(result, ExpryType::Nullable(Box::new(ExpryType::Int)));
    }

    #[test]
    fn expry_types() {
        assert!(!expry_type!("{foo: string}").used_as(&expry_type!("{foo: string, bar: string}")));
        assert!(expry_type!("{foo: string,bar:string}").used_as(&expry_type!("{foo: string}")));
        assert!(expry_type!("{*: {foo: string, bar: string}}").used_as(&expry_type!("{*: {foo: string}}")));
        assert!(!expry_type!("{*: {parent:string?,title:string,}}").used_as(&expry_type!("{*: {parent?:string,title:string,}}")));
        assert!(expry_type!("{*: {parent:string,title:string,}}").used_as(&expry_type!("{*: {parent?:string,title:string,}}")));
        assert!(!expry_type!("{*: {parent?:string?,title:string,}}").used_as(&expry_type!("{*: {parent:string,title:string,}}")));
    }

    #[test]
    fn embedded() {
        let main_template = r#"<html><title>{{=this.title}}</title><body>{{call ((this.body))(this.title,this.foobarvar)}}</body></html>"#;
        let mut options = HairyCompileOptions::new();
        options.set_dynamic_value_name_and_types(&[("this", expry_type!("{title: string, foobarvar: string, body: string}"))]);
        let main = hairy_compile_html(main_template, "main.tpl", &options);
        if let Err(err) = &main {
            println!("{}", err);
        }
        let main = main.unwrap();
        let child_template = r#"<p>title of this page = {{=title}}</p><p>foobar = {{=foobarvar}}"#;
        options.set_dynamic_value_name_and_types(&[("title", expry_type!("string")),("foobarvar",expry_type!("string"))]);
        let child = hairy_compile_html(child_template, "child.tpl", &options).unwrap();
        let value = value!({
            "body": child,
            "foobarvar": "foobar",
            "title": "my title",
        }).encode_to_vec();
        let mut options = HairyEvalOptions::new();
        options.values = vec![value.to_ref()];
        match hairy_eval_html(main.to_ref(), &options) {
            Ok(output) => { std::io::stdout().write_all(&output).unwrap(); },
            Err(err) => {
                eprintln!("{}", err);
                assert!(false);
            },
        }
    }

    #[test]
    fn inline_script() {
        let template = r#"{{inline $sitetitle = true ? "t" : "f"}}
{{inline foo = 43}}
<html><title>{{=$sitetitle}}: title</title><body><script>
    document.innerHTML = '<button>Open in App</button>';
</script></body></html>
<!-- <foo {{=this.title}}> -->
<{{=this.title}}>
{{=this.foo}}
"#;
        let mut allocator = MemoryPool::new();
        let mut scope = allocator.rewind();
        let parsed = parse_html(template, 0, &mut scope);
        if let Err(msg) = &parsed {
            println!("error: {}", msg);
        }

        let value = value!({"title": "<b>title</b>", "foo": 123}).encode_to_vec();
        let mut options = HairyOptions::new();
        options.set_named_dynamic_values(&[("this", value.to_ref())]);
        let result = hairy_compile_html(template, "test.tpl", &(&options).try_into().unwrap());
        if let Err(msg) = &result {
            println!("error: {}", msg);
        }
        let main = result.unwrap();
        println!("{}", String::from_utf8_lossy(&hairy_eval_html(main.to_ref(), &(&options).try_into().unwrap()).unwrap()));
    }

    #[test]
    fn inline_script2() {
        let template_works = r#"<html><title>title</title><body>
      {{if true}}
    Show text
    {{else}}

    {{end}}
        </body></html>1"#;
        let mut compile_options = HairyCompileOptions::new();
        compile_options.set_dynamic_value_count(0);
        let result = hairy_compile_html(template_works, "test.tpl", &compile_options);
        if let Err(msg) = &result {
            println!("{}", msg);
        }
        result.unwrap();

        let template2 = r#"<html><title>title</title><body>
      {{if true}}
        Show text
        {{else}}
        {{end}}
        </body></html>2"#;
        let result = hairy_compile_html(template2, "test.tpl", &compile_options);
        if let Err(msg) = &result {
            println!("{}", msg);
        }
        result.unwrap();

        let template3 = r#"<html><title>title</title><body>
      {{if true}}
        Show text
       {{end}}
        </body></html>3"#;
        let result = hairy_compile_html(template3, "test.tpl", &compile_options);
        if let Err(msg) = &result {
            println!("{}", msg);
        }
        result.unwrap();
    }

    pub struct TranslateFuncs {
        map: std::collections::HashMap<String,String>,
    }

    impl<'a> CustomFuncs<'a> for TranslateFuncs {
        fn call<'b,'c>(&self, name: &'_ [u8], args: &'_ [DecodedValue<'b>], scope: &'_ mut MemoryScope<'c>) -> Result<DecodedValue<'b>,&'b str> where 'c: 'b {
            if name == b"tr" {
                if let (Some(DecodedValue::String(key)), Some(DecodedValue::String(default))) = (args.get(0), args.get(1)) {
                    let key = core::str::from_utf8(key);
                    if let Ok(key) = key {
                        if let Some(translated) = self.map.get(key) {
                            Ok(DecodedValue::String(scope.copy_u8(translated.as_bytes())))
                        } else {
                            Ok(DecodedValue::String(default))
                        }
                    } else {
                        Err("tr(key, default) with invalid UTF-8 as key")
                    }
                } else {
                    Err("tr(key, default) expected")
                }
            } else {
                Err("no custom functions defined except for 'func'")
            }
        }
        fn types(&self) -> BTreeMap<Key<'static>,(Vec<ExpryType>,ExpryType)> {
            BTreeMap::from([
              (key_str("tr"), (vec![ExpryType::String, ExpryType::String], ExpryType::String)),
            ])
        }
    }

    pub struct FallbackTranslationsFuncs {
    }

    impl<'a> CustomFuncs<'a> for FallbackTranslationsFuncs {
        fn call<'b,'c>(&self, name: &'_ [u8], args: &'_ [DecodedValue<'b>], _scope: &'_ mut MemoryScope<'c>) -> Result<DecodedValue<'b>,&'b str> where 'c: 'b {
            if name == b"tr" {
                if let (_, Some(DecodedValue::String(default))) = (args.get(0), args.get(1)) {
                    Ok(DecodedValue::String(default))
                } else {
                    Err("tr(key, default) expected")
                }
            } else {
                Err("no custom functions defined except for 'tr'")
            }
        }
        fn types(&self) -> BTreeMap<Key<'static>,(Vec<ExpryType>,ExpryType)> {
            BTreeMap::from([
              (key_str("tr"), (vec![ExpryType::String, ExpryType::String], ExpryType::String)),
            ])
        }
    }

    #[test]
    pub fn translations() {
        let mut map = std::collections::HashMap::new();
        map.insert("hello".to_string(), "Hallo.".to_string());
        map.insert("welcome-text".to_string(), "Welkom op onze site, fijn dat je ons virtueel bezoekt.".to_string());
        let template = r#"
<h1>{{=tr("hello", "Hello.")}}</h1>
<p>{{=tr("welcome-text", "Welcome to our site. We are glad that you are here.")}}</p>
"#;
        let mut options = HairyOptions::new();
        let custom = TranslateFuncs { map, };
        options.custom(&custom);
        let result = hairy_compile_html(template, "test.tpl", &(&options).try_into().unwrap());
        if let Err(msg) = &result {
            println!("error: {}", msg);
        }
        let main = result.unwrap();
        let output = hairy_eval_html(main.to_ref(), &(&options).try_into().unwrap());
        if let Err(msg) = &output {
            println!("error: {}", msg);
        }
        let output = output.unwrap();
        //eprintln!("{}", String::from_utf8_lossy(&output));
        assert_eq!(r#"<h1>Hallo.</h1>
<p>Welkom op onze site, fijn dat je ons virtueel bezoekt.</p>"#.as_bytes(), output);

        let mut eval_options = HairyEvalOptions::new();
        eval_options.custom(&FallbackTranslationsFuncs{});
        let output = hairy_eval_html(main.to_ref(), &eval_options);
        if let Err(msg) = &output {
            println!("error: {}", msg);
        }
        let output = output.unwrap();
        //eprintln!("{}", String::from_utf8_lossy(&output));
        assert_eq!(r#"<h1>Hello.</h1>
<p>Welcome to our site. We are glad that you are here.</p>"#.as_bytes(), output);
    }
}