Browse Source

Initial work on `core:text/template`

gingerBill 3 năm trước cách đây
mục cha
commit
11b08b4d86

+ 670 - 0
core/text/template/execute.odin

@@ -0,0 +1,670 @@
+package text_template
+
+import "core:io"
+import "core:reflect"
+import "core:strings"
+import "core:mem"
+import "core:mem/virtual"
+import "core:fmt"
+
+import parse "parse"
+
+Template :: struct {
+	tree: ^parse.Tree,
+
+	using config: Config,
+}
+
+Config :: struct {
+	flags:       Flags,
+	allocator:   mem.Allocator,
+	left_delim:  string,
+	right_delim: string,
+}
+
+Flags :: distinct bit_set[Flag; u64]
+Flag :: enum u64 {
+	Emit_Comments,
+}
+
+Parse_Error :: parse.Error
+Execution_Error :: enum {
+	Accumulated_Errors,
+
+	Invalid_Node,
+	Invalid_Command,
+	Invalid_Value,
+
+	Undeclared_Variable,
+	Undefined_Function,
+
+	Invalid_Break,
+	Invalid_Continue,
+
+	Invalid_Argument_Count,
+	Invalid_Argument_Type,
+	Out_Of_Bounds_Access,
+}
+Error :: union {
+	io.Error,
+	Execution_Error,
+}
+
+create_from_string :: proc(input: string, config: Maybe(Config) = nil) -> (t: ^Template, err: Parse_Error) {
+	cfg, _ := config.?
+	if cfg.allocator.procedure == nil {
+		cfg.allocator = context.allocator
+	}
+
+	t = new(Template, cfg.allocator)
+	t.config = cfg
+	t.tree = nil
+	t.tree, err = parse.parse(input, t.left_delim, t.right_delim, .Emit_Comments in t.flags, t.allocator)
+	return
+}
+
+must :: proc(t: ^Template, err: Parse_Error) -> ^Template {
+	assert(t != nil)
+	assert(err == nil)
+	return t
+}
+
+destroy :: proc(t: ^Template) {
+	allocator := t.allocator
+	parse.destroy_tree(t.tree)
+	free(t, allocator)
+}
+
+execute :: proc(t: ^Template, w: io.Writer, data: any) -> Error {
+	s := State{
+		tmpl = t,
+		w    = w,
+	}
+	context.allocator = virtual.arena_allocator(&s.arena)
+	defer free_all(context.allocator)
+
+	err := walk(&s, data,t.tree.root)
+	if err == nil && s.error_count > 0 {
+		err = .Accumulated_Errors
+	}
+	return err
+}
+
+
+State :: struct {
+	arena: virtual.Growing_Arena,
+
+	tmpl:  ^Template,
+	w:     io.Writer,
+	at:    ^parse.Node,
+	vars:  [dynamic]Variable,
+	depth: int,
+	error_count: int,
+}
+Variable :: struct {
+	name:  string,
+	value: any,
+}
+
+walk :: proc(s: ^State, dot: any, node: ^parse.Node) -> Error {
+	s.at = node
+
+	switch n in node.variant {
+	case ^parse.Node_Comment:
+		// ignore
+	case ^parse.Node_Text:
+		io.write_string(s.w, n.text) or_return
+		return nil
+	case ^parse.Node_List:
+		for elem in n.nodes {
+			walk(s, dot, elem) or_return
+		}
+		return nil
+	case ^parse.Node_Action:
+		val := eval_pipeline(s, dot, n.pipe) or_return
+		if len(n.pipe.decl) == 0 {
+			print_value(s, n, val)
+		}
+		return nil
+	case ^parse.Node_Pipeline:
+		print_value(s, n, eval_pipeline(s, dot, n) or_return)
+		return nil
+
+	case ^parse.Node_Import:
+		panic("TODO Node_Import")
+
+	case ^parse.Node_If:
+		return walk_if_or_with(s, .If, dot, n.pipe, n.list, n.else_list)
+	case ^parse.Node_With:
+		return walk_if_or_with(s, .With, dot, n.pipe, n.list, n.else_list)
+	case ^parse.Node_For:
+		return walk_for(s, dot, n)
+
+	case ^parse.Node_Break:
+		return .Invalid_Break
+	case ^parse.Node_Continue:
+		return .Invalid_Continue
+
+	case ^parse.Node_Else,
+	     ^parse.Node_End,
+	     ^parse.Node_Nil,
+	     ^parse.Node_Bool,
+	     ^parse.Node_Number,
+	     ^parse.Node_String,
+	     ^parse.Node_Variable,
+	     ^parse.Node_Identifier,
+	     ^parse.Node_Operator,
+	     ^parse.Node_Dot,
+	     ^parse.Node_Field:
+		return .Invalid_Node
+
+	case ^parse.Node_Command:
+		return .Invalid_Node
+	case ^parse.Node_Chain:
+		return .Invalid_Node
+	}
+
+	return .Invalid_Node
+}
+
+mark_vars :: proc(s: ^State) -> int {
+	return len(s.vars)
+}
+pop_vars :: proc(s: ^State, n: int) {
+	resize(&s.vars, n)
+}
+
+@(private, deferred_in_out=pop_vars)
+SCOPE :: proc(s: ^State) -> int {
+	return mark_vars(s)
+}
+walk_if_or_with :: proc(s: ^State, kind: enum{If, With}, dot: any,
+                        pipe: ^parse.Node_Pipeline,
+                        list: ^parse.Node_List,
+                        else_list: ^parse.Node_List) -> Error {
+	SCOPE(s)
+
+	val := eval_pipeline(s, dot, pipe) or_return
+	truth, ok := is_true(val)
+	if !ok {
+		error(s, "if/with cannot use %v", val)
+	}
+	if truth {
+		if kind == .With {
+			return walk(s, val, list)
+		} else {
+			return walk(s, dot, list)
+		}
+	} else if else_list != nil {
+		return walk(s, dot, else_list)
+	}
+	return nil
+}
+
+walk_for :: proc(s: ^State, dot: any, f: ^parse.Node_For) -> Error {
+	s.at = f
+	SCOPE(s)
+
+	val, _ := indirect(eval_pipeline(s, dot, f.pipe) or_return)
+
+	mark := mark_vars(s)
+
+	the_body :: proc(s: ^State, f: ^parse.Node_For, elem, index: any, mark: int) -> Error {
+		if len(f.pipe.decl) > 0 {
+			set_top_var(s, 1, index) or_return
+		}
+		if len(f.pipe.decl) > 1 {
+			set_top_var(s, 2, elem) or_return
+		}
+		defer pop_vars(s, mark)
+		return walk(s, elem, f.list)
+	}
+
+	original_id := val.id
+	ti := reflect.type_info_base(type_info_of(val.id))
+	val.id = ti.id
+	#partial switch info in ti.variant {
+	case reflect.Type_Info_Array,
+	     reflect.Type_Info_Slice,
+	     reflect.Type_Info_Dynamic_Array:
+		n := reflect.length(val)
+		if n == 0 {
+			break
+		}
+		for i in 0..<n {
+			the_body(s, f, reflect.index(val, i), i, mark) or_return
+		}
+		return nil
+	case reflect.Type_Info_Map:
+		if reflect.length(val) == 0 {
+			break
+		}
+
+		gs := reflect.type_info_base(info.generated_struct).variant.(reflect.Type_Info_Struct)
+		ed := reflect.type_info_base(gs.types[1]).variant.(reflect.Type_Info_Dynamic_Array)
+		entry_type := ed.elem.variant.(reflect.Type_Info_Struct)
+		key_offset :=  entry_type.offsets[2]
+		value_offset :=  entry_type.offsets[3]
+		entry_size := uintptr(ed.elem_size)
+		key_type := entry_type.types[2]
+		value_type := entry_type.types[3]
+
+		rm := (^mem.Raw_Map)(val.data)
+
+		data := uintptr(rm.entries.data)
+		for i in 0..<rm.entries.len {
+			key := any{rawptr(data + key_offset), key_type.id}
+			value := any{rawptr(data + value_offset), value_type.id}
+
+			the_body(s, f, key, value, mark) or_return
+
+			data += entry_size
+		}
+		return nil
+
+		// TODO
+	case:
+		error(s, "for cannot iterate over %v", original_id)
+	}
+
+	if f.else_list != nil {
+		return walk(s, dot, f.else_list)
+	}
+	return nil
+}
+
+indirect :: proc(val: any) -> (v: any, is_nil: bool) {
+	v = val
+	for v != nil {
+		ti := reflect.type_info_base(type_info_of(v.id))
+		info, ok := ti.variant.(reflect.Type_Info_Pointer)
+		if !ok {
+			break
+		}
+
+		ptr := (^rawptr)(v.data)^
+		if ptr == nil {
+			return v, true
+		}
+		v = any{ptr, info.elem.id}
+	}
+	return v, false
+}
+
+
+error :: proc(s: ^State, format: string, args: ..any) {
+	s.error_count += 1
+
+	assert(s.at != nil)
+
+	// NOTE(bill): the line and column are recalculated
+	// each time here because errors are usually an early
+	// out for this execution system
+
+	pos := int(s.at.pos)
+	text := s.tmpl.tree.input[:pos]
+	col := strings.last_index(text, "\n")
+	if col < 0 {
+		col = pos
+	} else {
+		col += 1
+		col = pos - col
+	}
+
+	line := 1 + strings.count(text, "\n")
+
+	name := s.tmpl.tree.name
+	if name == "" {
+		name = "<input>"
+	}
+	fmt.eprintf("%s:%d:%d: ", name, line, col)
+	fmt.eprintf(format, ..args)
+	fmt.eprintln()
+}
+
+is_true :: proc(val: any) -> (truth, ok: bool) {
+	check_trivial :: proc(v: any) -> (bool, bool) {
+		data := reflect.as_bytes(v)
+		for v in data {
+			if v != 0 {
+				return true, true
+			}
+		}
+		return false, true
+	}
+
+	if val == nil {
+		return false, true
+	}
+
+	ti := reflect.type_info_base(type_info_of(val.id))
+	switch v in ti.variant {
+	case reflect.Type_Info_Named:
+		unreachable()
+	case reflect.Type_Info_Integer,
+	     reflect.Type_Info_Rune,
+	     reflect.Type_Info_Float,
+	     reflect.Type_Info_Complex,
+	     reflect.Type_Info_Quaternion,
+	     reflect.Type_Info_Boolean,
+	     reflect.Type_Info_Bit_Set,
+	     reflect.Type_Info_Enum:
+	     return check_trivial(val)
+
+	case reflect.Type_Info_String:
+		if v.is_cstring {
+			cstr := (^cstring)(val.data)^
+			if cstr == nil {
+				return false, true
+			}
+			return ([^]u8)(cstr)[0] != 0, true
+		}
+		str := (^string)(val.data)^
+		return len(str) > 0, true
+
+	case reflect.Type_Info_Any:
+		return false, false
+	case reflect.Type_Info_Type_Id:
+		return (^typeid)(val.data)^ != nil, true
+	case reflect.Type_Info_Pointer,
+	     reflect.Type_Info_Multi_Pointer:
+		return (^rawptr)(val.data)^ != nil, true
+
+	case reflect.Type_Info_Procedure:
+		return
+
+	case reflect.Type_Info_Array:
+		return v.count > 0, true
+	case reflect.Type_Info_Enumerated_Array:
+		return v.count > 0, true
+	case reflect.Type_Info_Dynamic_Array:
+		a := (^mem.Raw_Dynamic_Array)(val.data)
+		return a.len > 0, true
+	case reflect.Type_Info_Slice:
+		a := (^mem.Raw_Slice)(val.data)
+		return a.len > 0, true
+	case reflect.Type_Info_Tuple:
+		return
+	case reflect.Type_Info_Struct:
+		// All structs are always non nil
+		return true, true
+	case reflect.Type_Info_Union:
+	     return reflect.union_variant_typeid(val) != nil, true
+	case reflect.Type_Info_Map:
+		m := (^mem.Raw_Map)(val.data)
+		return m.entries.len > 0, true
+	case reflect.Type_Info_Simd_Vector:
+		return v.count > 0, true
+	case reflect.Type_Info_Relative_Pointer:
+		return check_trivial(val)
+	case reflect.Type_Info_Relative_Slice:
+		return check_trivial(val)
+	case reflect.Type_Info_Matrix:
+		return check_trivial(val)
+	}
+	return
+}
+
+
+eval_pipeline :: proc(s: ^State, dot: any, pipe: ^parse.Node_Pipeline) -> (value: any, err: Error) {
+	if pipe == nil {
+		return
+	}
+	s.at = pipe
+	value = nil
+	for cmd in pipe.cmds {
+		value = eval_command(s, dot, cmd, value) or_return
+	}
+	for var in pipe.decl {
+		if pipe.is_assign {
+			set_var(s, var.name, value) or_return
+		} else {
+			push_var(s, var.name, value)
+		}
+	}
+	return
+}
+
+set_var :: proc(s: ^State, name: string, value: any) -> Error {
+	for i := mark_vars(s)-1; i >= 0; i -= 1 {
+		if s.vars[i].name == name {
+			s.vars[i].value = value
+			return nil
+		}
+	}
+	return .Undeclared_Variable
+}
+
+set_top_var :: proc(s: ^State, n: int, value: any) -> Error {
+	if len(s.vars) > 0 {
+		s.vars[len(s.vars)-n].value = value
+		return nil
+	}
+	return .Undeclared_Variable
+}
+
+push_var :: proc(s: ^State, name: string, value: any) {
+	append(&s.vars, Variable{name, value})
+}
+get_var :: proc(s: ^State, name: string) -> (value: any, err: Error) {
+	for i := mark_vars(s)-1; i >= 0; i -= 1 {
+		if s.vars[i].name == name {
+			return s.vars[i].value, nil
+		}
+	}
+	error(s, "undeclared variable $%s", name)
+	return nil, .Undeclared_Variable
+
+}
+
+eval_command :: proc(s: ^State, dot: any, cmd: ^parse.Node_Command, final: any) -> (value: any, err: Error) {
+	first_word := cmd.args[0]
+	#partial switch n in first_word.variant {
+	case ^parse.Node_Field:
+		s.at = n
+		return eval_fields(s, dot, n.idents)
+	case ^parse.Node_Chain:
+		return eval_chain(s, dot, n)
+	case ^parse.Node_Identifier:
+		return eval_function(s, dot, n.ident, cmd, final)
+	case ^parse.Node_Operator:
+		return eval_function(s, dot, n.value, cmd, final)
+	case ^parse.Node_Pipeline:
+		return eval_pipeline(s, dot, n)
+	case ^parse.Node_Variable:
+		s.at = n
+		return get_var(s, n.name)
+	}
+	s.at = first_word
+	#partial switch n in first_word.variant {
+	case ^parse.Node_Bool:
+		return new_any(n.ok), nil
+	case ^parse.Node_Dot:
+		return dot, nil
+	case ^parse.Node_Nil:
+		return nil, nil
+	case ^parse.Node_Number:
+		if i, ok := n.i.?; ok {
+			return new_any(i), nil
+		}
+		if u, ok := n.u.?; ok {
+			return new_any(u), nil
+		}
+		if f, ok := n.f.?; ok {
+			return new_any(f), nil
+		}
+		return eval_function(s, dot, n.text, cmd, final)
+	case ^parse.Node_String:
+		return new_any(n.text), nil
+	}
+
+	error(s, "cannot evaluate command %v", first_word.variant)
+	return nil, .Invalid_Command
+}
+
+eval_chain :: proc(s: ^State, dot: any, chain: ^parse.Node_Chain) -> (value: any, err: Error) {
+	s.at = chain
+	return eval_fields(s, eval_arg(s, dot, chain.node) or_return, chain.fields[:])
+}
+
+eval_fields :: proc(s: ^State, dot: any, idents: []string) -> (value: any, err: Error) {
+	value = dot
+	for ident in idents {
+		value = eval_field(s, value, ident) or_return
+	}
+	return
+}
+
+eval_field :: proc(s: ^State, dot: any, ident: string) -> (value: any, err: Error) {
+	if dot == nil {
+		return nil, nil
+	}
+
+	ti := reflect.type_info_base(type_info_of(dot.id))
+	#partial switch info in ti.variant {
+	case reflect.Type_Info_Struct:
+		value = reflect.struct_field_value_by_name(dot, ident, true)
+		if value != nil {
+			return
+		}
+	case reflect.Type_Info_Pointer:
+		if dot.data != nil {
+			deref := (^rawptr)(dot.data)^
+			return eval_field(s, {deref, info.elem.id}, ident)
+		}
+	case reflect.Type_Info_Map:
+		key_type := reflect.type_info_base(info.key)
+		switch key_type.id {
+		case typeid_of(string), typeid_of(cstring):
+			gs := reflect.type_info_base(info.generated_struct).variant.(reflect.Type_Info_Struct)
+			ed := reflect.type_info_base(gs.types[1]).variant.(reflect.Type_Info_Dynamic_Array)
+			entry_type := ed.elem.variant.(reflect.Type_Info_Struct)
+			key_offset :=  entry_type.offsets[2]
+			value_offset :=  entry_type.offsets[3]
+			entry_size := uintptr(ed.elem_size)
+
+			rm := (^mem.Raw_Map)(dot.data)
+
+			data := uintptr(rm.entries.data)
+			for i in 0..<rm.entries.len {
+				key: string
+				switch key_type.id {
+				case typeid_of(string):
+					key = (^string)(data + key_offset)^
+				case typeid_of(cstring):
+					key = string((^cstring)(data + key_offset)^)
+				}
+
+				if key == ident {
+					ptr := rawptr(data + value_offset)
+					return any{ptr, entry_type.types[3].id}, nil
+				}
+
+				data += entry_size
+			}
+			return nil, nil
+		}
+	}
+
+
+	error(s, "cannot evaluate field %s in type %v", ident, dot.id)
+	return nil, .Invalid_Value
+}
+
+eval_function :: proc(s: ^State, dot: any, name: string, cmd: ^parse.Node_Command, final: any) -> (value: any, err: Error) {
+	cmd_args := cmd.args[1:]
+
+	switch name {
+	case "+", "-":
+		if len(cmd_args) < 1 {
+			error(s, "%q expects at least 1 argument", name)
+			return nil, .Invalid_Argument_Count
+		}
+		// TODO
+
+		return nil, nil
+	case "*":
+		if len(cmd_args) < 1 {
+			error(s, "%q expects at least 2 arguments, got %d", name, len(cmd_args))
+			return nil, .Invalid_Argument_Count
+		}
+		// TODO
+		return nil, nil
+	}
+
+	function, ok := builtin_funcs[name]
+	if !ok {
+		error(s, "%q is not a defined function", name)
+		err = .Undefined_Function
+		return
+	}
+
+	if function == nil {
+		switch name {
+		case "and":
+			// TODO
+		case "or":
+			// TODO
+		case:
+			panic("unhandled built-in procedure")
+		}
+	}
+
+	n := len(cmd_args)
+	if final != nil {
+		n += 1
+	}
+	args_to_call := make([dynamic]any, 0, n)
+	for arg in cmd_args {
+		append(&args_to_call, eval_arg(s, dot, arg) or_return)
+	}
+	if final != nil {
+		append(&args_to_call, final)
+	}
+
+	return function(args_to_call[:])
+}
+
+eval_arg :: proc(s: ^State, dot: any, arg: ^parse.Node) -> (value: any, err: Error) {
+	s.at = arg
+	#partial switch n in arg.variant {
+	case ^parse.Node_Dot:
+		return dot, nil
+	case ^parse.Node_Nil:
+		return nil, nil
+	case ^parse.Node_Bool:
+		return new_any(n.ok), nil
+	case ^parse.Node_Number:
+		if i, ok := n.i.?; ok {
+			return new_any(i), nil
+		}
+		if u, ok := n.u.?; ok {
+			return new_any(u), nil
+		}
+		if f, ok := n.f.?; ok {
+			return new_any(f), nil
+		}
+	case ^parse.Node_String:
+		return new_any(n.text), nil
+
+	case ^parse.Node_Field:
+		return eval_fields(s, dot, n.idents)
+	case ^parse.Node_Variable:
+		return get_var(s, n.name)
+	case ^parse.Node_Pipeline:
+		return eval_pipeline(s, dot, n)
+	case ^parse.Node_Chain:
+		return eval_chain(s, dot, n)
+	}
+	return nil, .Invalid_Node
+}
+
+
+print_value :: proc(s: ^State, n: ^parse.Node, val: any) {
+	s.at = n
+	if val == nil {
+		io.write_string(s.w, "nil")
+	} else {
+		fmt.wprint(s.w, val)
+	}
+}

+ 91 - 0
core/text/template/function.odin

@@ -0,0 +1,91 @@
+package text_template
+
+import "core:mem"
+import "core:fmt"
+import "core:reflect"
+
+Function :: #type proc(args: []any) -> (value: any, err: Error)
+
+
+@(private)
+new_any :: proc(x: $T) -> any {
+	ptr := new_clone(x)
+	return any{ptr, typeid_of(T)}
+}
+
+builtin_funcs: map[string]Function
+
+@(private, init)
+init_builtin_funcs :: proc() {
+	builtin_funcs["and"] = nil // requires shortcircuiting behaviour so implemented internally
+	builtin_funcs["or"]  = nil // requires shortcircuiting behaviour so implemented internally
+	builtin_funcs["not"] = proc(args: []any) -> (value: any, err: Error) {
+		if len(args) != 1 {
+			err = .Invalid_Argument_Count
+			return
+		}
+		t, _ := is_true(args[0])
+		return new_any(t), nil
+	}
+
+	builtin_funcs["index"] = proc(args: []any) -> (value: any, err: Error) {
+		if len(args) < 2 {
+			err = .Invalid_Argument_Count
+			return
+		}
+
+		arg := args[0]
+		for idx in args[1:] {
+			i, ok := reflect.as_int(idx)
+			if !ok {
+				err = .Invalid_Argument_Type
+				return
+			}
+			if reflect.length(arg) < i {
+				return nil, .Out_Of_Bounds_Access
+			}
+			arg = reflect.index(arg, i)
+		}
+		return arg, nil
+	}
+
+	builtin_funcs["len"] = proc(args: []any) -> (value: any, err: Error) {
+		if len(args) != 1 {
+			err = .Invalid_Argument_Count
+			return
+		}
+
+		n := reflect.length(args[0])
+		return new_any(n), nil
+	}
+
+
+	builtin_funcs["print"] = proc(args: []any) -> (value: any, err: Error) {
+		return new_any(fmt.aprint(..args)), nil
+	}
+	builtin_funcs["println"] = proc(args: []any) -> (value: any, err: Error) {
+		return new_any(fmt.aprintln(..args)), nil
+	}
+	builtin_funcs["printf"] = proc(args: []any) -> (value: any, err: Error) {
+		if len(args) < 1 {
+			err = .Invalid_Argument_Count
+			return
+		}
+		format_any := args[0]
+		format_any.id = reflect.typeid_base(format_any.id)
+		format: string
+		switch v in format_any {
+		case string:
+			format = v
+		case cstring:
+			format = string(v)
+		case:
+			err = .Invalid_Argument_Type
+			return
+		}
+
+		other_args := args[1:]
+		return new_any(fmt.aprintf(format, ..other_args)), nil
+	}
+}
+

+ 162 - 0
core/text/template/parse/ast.odin

@@ -0,0 +1,162 @@
+package text_template_parse
+
+import "../scan"
+import "core:strings"
+
+Pos :: scan.Pos
+Token :: scan.Token
+Token_Kind :: scan.Token_Kind
+
+new_node :: proc($T: typeid, pos: Pos = 0, allocator := context.allocator) -> ^T {
+	n := new(T, allocator)
+	n.pos = pos
+	n.variant = n
+	return n
+}
+
+Node :: struct {
+	pos: Pos,
+	variant: union{
+		^Node_Text,
+		^Node_Comment,
+		^Node_Action,
+		^Node_Pipeline,
+		^Node_Chain,
+		^Node_Command,
+		^Node_Import,
+		^Node_Dot,
+		^Node_Field,
+		^Node_Identifier,
+		^Node_Operator,
+		^Node_If,
+		^Node_For,
+		^Node_List,
+		^Node_Nil,
+		^Node_Bool,
+		^Node_Number,
+		^Node_String,
+		^Node_Variable,
+		^Node_With,
+		^Node_Break,
+		^Node_Continue,
+
+		// Dummy nodes
+		^Node_Else,
+		^Node_End,
+	},
+}
+
+Node_Branch :: struct{
+	using base: Node,
+	pipe: ^Node_Pipeline,
+	list: ^Node_List,
+	else_list: ^Node_List,
+}
+
+Node_Text :: struct{
+	using base: Node,
+	text: string,
+}
+Node_Action :: struct{
+	using base: Node,
+	pipe: ^Node_Pipeline,
+}
+Node_Bool :: struct{
+	using base: Node,
+	ok: bool,
+}
+Node_Chain :: struct{
+	using base: Node,
+	node: ^Node,
+	fields: [dynamic]string,
+}
+Node_Command :: struct{
+	using base: Node,
+	args: [dynamic]^Node,
+}
+Node_Dot :: struct{
+	using base: Node,
+}
+Node_Field :: struct{
+	using base: Node,
+	idents: []string,
+}
+Node_Identifier :: struct{
+	using base: Node,
+	ident: string,
+}
+Node_Operator :: struct{
+	using base: Node,
+	value: string,
+}
+
+
+Node_If   :: distinct Node_Branch
+Node_For  :: distinct Node_Branch
+Node_With :: distinct Node_Branch
+
+Node_List :: struct{
+	using base: Node,
+	nodes: [dynamic]^Node,
+}
+Node_Nil :: struct{
+	using base: Node,
+}
+Node_Number :: struct{
+	using base: Node,
+	text: string,
+	i: Maybe(i64),
+	u: Maybe(u64),
+	f: Maybe(f64),
+}
+Node_Pipeline :: struct{
+	using base: Node,
+	is_assign: bool,
+	decl: [dynamic]^Node_Variable,
+	cmds: [dynamic]^Node_Command,
+}
+Node_String :: struct{
+	using base: Node,
+	quoted: string,
+	text: string, // after processing
+}
+Node_Import :: struct{
+	using base: Node,
+	name: string, // unquoted
+	pipe: ^Node_Pipeline,
+}
+Node_Variable :: struct{
+	using base: Node,
+	name: string,
+}
+Node_Comment :: struct{
+	using base: Node,
+	text: string,
+}
+Node_Break :: struct{
+	using base: Node,
+}
+Node_Continue :: struct{
+	using base: Node,
+}
+
+Node_Else :: struct {
+	using base: Node,
+}
+
+Node_End :: struct {
+	using base: Node,
+}
+
+chain_add :: proc(c: ^Node_Chain, field: string) {
+	field := field
+	if len(field) == 0 || field[0] != '.' {
+		panic("not a .field")
+	}
+	field = field[1:]
+	if field == "" {
+		panic("empty field")
+	}
+	append(&c.fields, field)
+}
+

+ 528 - 0
core/text/template/parse/parse.odin

@@ -0,0 +1,528 @@
+package text_template_parse
+
+import "core:io"
+import "core:fmt"
+import "core:mem"
+import "core:mem/virtual"
+import "core:strconv"
+import "../scan"
+
+Error :: enum {
+	None,
+
+	Unexpected_Token,
+	Unexpected_EOF,
+
+	Expected_End,
+
+	Invalid_Node,
+
+	Invalid_Character,
+	Invalid_Number,
+	Invalid_String,
+
+	Empty_Command,
+	Missing_Value,
+	Non_Executable_Command,
+	Undefined_Variable,
+	Unexpected_Operand,
+	Invalid_For_Initialization,
+	Too_Many_Declarations,
+}
+Tree :: struct {
+	general_allocator: mem.Allocator,
+
+	arena: virtual.Growing_Arena,
+	name: string,
+
+	tokens: []Token, // general_allocator
+
+	root: ^Node_List,
+	input: string,
+	offset: uint,
+
+	for_loop_depth: uint,
+
+	vars: [dynamic]string,
+}
+
+@(require_results)
+errorf :: proc(t: ^Tree, err: Error, format: string, args: ..any) -> Error {
+	if err != nil {
+		fmt.eprintf(format, ..args)
+		fmt.eprintln()
+	}
+	return err
+}
+
+@(require_results)
+unexpected_token :: proc(t: ^Tree, token: Token) -> Error {
+	return errorf(t, .Unexpected_Token, "unexpected token: %s", token.value)
+}
+
+
+peek :: proc(t: ^Tree, n: uint = 0) -> Token {
+	if t.offset+n < len(t.tokens) {
+		return t.tokens[t.offset+n]
+	}
+	return Token{.EOF, "", Pos(len(t.input)), 0}
+}
+next :: proc(t: ^Tree) -> (token: Token) {
+	if t.offset < len(t.tokens) {
+		token = t.tokens[t.offset]
+		t.offset += 1
+		return
+	}
+	return Token{.EOF, "", Pos(len(t.input)), 0}
+}
+backup :: proc(t: ^Tree, n: uint = 1) {
+	if n > t.offset {
+		t.offset = 0
+	} else {
+		t.offset -= n
+	}
+}
+
+next_non_space :: proc(t: ^Tree) -> (token: Token) {
+	for {
+		token = next(t)
+		if token.kind != .Space {
+			break
+		}
+	}
+	return
+}
+peek_non_space :: proc(t: ^Tree, offset: uint = 0) -> (token: Token) {
+	i := offset
+	for {
+		if t.offset+i < len(t.tokens) {
+			token = t.tokens[t.offset+i]
+		} else {
+			token = Token{.EOF, "", Pos(len(t.input)), 0}
+		}
+		if token.kind != .Space {
+			break
+		}
+		i += 1
+	}
+	return
+}
+peek_after_non_space :: proc(t: ^Tree) -> (token: Token) {
+	return peek_non_space(t, 1)
+}
+
+expect :: proc(t: ^Tree, expected: Token_Kind, ctx: string) -> (token: Token, err: Error) {
+	token = next_non_space(t)
+	if token.kind != expected {
+		err = errorf(t, .Unexpected_Token, "unexpected token, expected %s, got %s", expected, token.value)
+	}
+	return
+}
+
+
+parse :: proc(input: string, left_delim, right_delim: string, emit_comments: bool = false, general_allocator := context.allocator) -> (t: ^Tree, err: Error) {
+	t = new(Tree, general_allocator)
+	t.general_allocator = general_allocator
+	t.vars.allocator = general_allocator
+	t.input = input
+
+
+	s := scan.init(&scan.Scanner{}, t.name, input, left_delim, right_delim, emit_comments)
+	s.tokens.allocator = t.general_allocator
+	scan.run(s)
+	t.tokens = s.tokens[:] // general_allocator
+
+	context.allocator = virtual.arena_allocator(&t.arena)
+
+	t.root = new_node(Node_List)
+	for peek(t).kind != .EOF {
+		if peek(t).kind == .Left_Delim && peek_after_non_space(t).kind == .Declare {
+			// TODO
+			continue
+		}
+		node := text_or_action(t) or_return
+		if node != nil {
+			append(&t.root.nodes, node)
+		} else {
+			break
+		}
+	}
+
+	return
+}
+
+destroy_tree :: proc(t: ^Tree) {
+	if t != nil {
+		virtual.arena_destroy(&t.arena)
+
+		ga := t.general_allocator
+		delete(t.tokens, ga)
+		delete(t.vars)
+		free(t, ga)
+	}
+}
+
+
+text_or_action :: proc(t: ^Tree) -> (node: ^Node, err: Error) {
+	#partial switch token := next_non_space(t); token.kind {
+	case .Text:
+		n := new_node(Node_Text, token.pos)
+		n.text = token.value
+		return n, nil
+	case .Left_Delim:
+		return action(t)
+	case .Comment:
+		n := new_node(Node_Comment, token.pos)
+		n.text = token.value
+		return n, nil
+	case:
+		return nil, unexpected_token(t, token)
+	}
+	return nil, nil
+}
+
+parse_list :: proc(t: ^Tree) -> (list: ^Node_List, next: ^Node, err: Error) {
+	list = new_node(Node_List, peek_non_space(t).pos)
+	for peek_non_space(t).kind != .EOF {
+		node := text_or_action(t) or_return
+		#partial switch n in node.variant {
+		case ^Node_Else:
+			next = n
+			return
+		case ^Node_End:
+			next = n
+			return
+		}
+		append(&list.nodes, node)
+	}
+	err = errorf(t, .Unexpected_EOF, "unexpected EOF")
+	return
+}
+
+parse_control :: proc(t: ^Tree, allow_else_if: bool, ctx: string) -> (pipe: ^Node_Pipeline, list, else_list: ^Node_List, err: Error) {
+	pipe = pipeline(t, ctx, .Right_Delim) or_return
+
+	if ctx == "for" {
+		t.for_loop_depth += 1
+	}
+
+	next_node: ^Node
+	list, next_node = parse_list(t) or_return
+
+	if ctx == "for" {
+		t.for_loop_depth -= 1
+	}
+
+	#partial switch n in next_node.variant {
+	case ^Node_End:
+		// We are done
+
+	case ^Node_Else:
+		if allow_else_if && peek(t).kind == .If {
+			// {{if a}}...{{else if b}}...{{end}}
+			// is translated into
+			// {{if a}}...{{else}}{{if b}}...{{end}}{{end}}
+			next(t)
+			else_list = new_node(Node_List, next_node.pos)
+			append(&else_list.nodes, parse_if(t) or_return)
+			break
+		}
+		else_list, next_node = parse_list(t) or_return
+		if _, ok := next_node.variant.(^Node_End); !ok {
+			errorf(t, .Expected_End, "expected end") or_return
+		}
+	}
+	return
+}
+
+// {{if pipeline}} list {{end}}
+// {{if pipeline}} list {{else}} list {{end}}
+// {{if pipeline}} list {{else if pipeline}} list {{end}}
+parse_if :: proc(t: ^Tree) -> (node: ^Node_If, err: Error) {
+	pipe, list, else_list := parse_control(t, true, "if") or_return
+	node = new_node(Node_If, pipe.pos)
+	node.pipe = pipe
+	node.list = list
+	node.else_list = else_list
+	return
+}
+
+// {{for pipeline}} list {{end}}
+// {{for pipeline}} list {{else}} list {{end}}
+parse_for :: proc(t: ^Tree) -> (node: ^Node_For, err: Error) {
+	pipe, list, else_list := parse_control(t, false, "for") or_return
+	node = new_node(Node_For, pipe.pos)
+	node.pipe = pipe
+	node.list = list
+	node.else_list = else_list
+	return
+}
+
+// {{with pipeline}} list {{end}}
+// {{with pipeline}} list {{else}} list {{end}}
+parse_with :: proc(t: ^Tree) -> (node: ^Node_With, err: Error) {
+	pipe, list, else_list := parse_control(t, false, "with") or_return
+	node = new_node(Node_With, pipe.pos)
+	node.pipe = pipe
+	node.list = list
+	node.else_list = else_list
+	return
+}
+
+
+// {{else}}
+parse_else :: proc(t: ^Tree) -> (node: ^Node_Else, err: Error) {
+	p := peek_non_space(t)
+	if p.kind == .If {
+		node = new_node(Node_Else, p.pos)
+		return
+	}
+	token := expect(t, .Right_Delim, "else") or_return
+	node = new_node(Node_Else, token.pos)
+	return
+}
+// {{end}}
+parse_end :: proc(t: ^Tree) -> (node: ^Node_End, err: Error) {
+	token := expect(t, .Right_Delim, "end") or_return
+	node = new_node(Node_End, token.pos)
+	return
+}
+
+
+
+
+action :: proc(t: ^Tree) -> (^Node, Error) {
+	// TODO actions
+	#partial switch token := next_non_space(t); token.kind {
+	case .If:   return parse_if(t)
+	case .For:  return parse_for(t)
+	case .With: return parse_with(t)
+	case .Else: return parse_else(t)
+	case .End:  return parse_end(t)
+
+	case .Block:
+		return nil, .Invalid_Node
+	case .Break:
+		return nil, .Invalid_Node
+	case .Continue:
+		return nil, .Invalid_Node
+	case .Include:
+		return nil, .Invalid_Node
+	}
+	backup(t)
+
+	return pipeline(t, "command", .Right_Delim)
+}
+
+
+pipeline :: proc(t: ^Tree, ctx: string, end: Token_Kind) -> (pipe: ^Node_Pipeline, err: Error) {
+	pipe = new_node(Node_Pipeline, peek_non_space(t).pos)
+
+	decls: for v := peek_non_space(t); v.kind == .Variable; /**/ {
+		next_non_space(t)
+
+		token_after_variable := peek(t) // could be space
+		next := peek_non_space(t)
+		switch {
+		case next.kind == .Assign, next.kind == .Declare:
+			pipe.is_assign = next.kind == .Assign
+			next_non_space(t)
+			append(&t.vars, v.value)
+			append(&pipe.decl, parse_variable(t, v) or_return)
+
+		case next.kind == .Char && next.value == ",":
+			next_non_space(t)
+			append(&t.vars, v.value)
+			append(&pipe.decl, parse_variable(t, v) or_return)
+			if ctx == "for" && len(pipe.decl) < 2 {
+				#partial switch peek_non_space(t).kind {
+				case .Variable, .Right_Delim, .Right_Paren:
+					v = peek_non_space(t)
+					continue decls
+				}
+				errorf(t, .Invalid_For_Initialization, "for can only initialize variables") or_return
+			}
+			errorf(t, .Too_Many_Declarations, "too many declarations in %s", ctx) or_return
+
+		case token_after_variable.kind == .Space:
+			backup(t, 2)
+		case:
+			backup(t, 1)
+		}
+
+		break decls
+	}
+
+	for {
+		#partial switch tok := next_non_space(t); tok.kind {
+		case end:
+			if len(pipe.cmds) == 0 {
+				errorf(t, .Missing_Value, "missing value for %s", ctx) or_return
+			}
+			for c, i in pipe.cmds[1:] {
+				#partial switch n in c.variant {
+				case ^Node_Bool, ^Node_Dot, ^Node_Nil, ^Node_Number, ^Node_String:
+					errorf(t, .Non_Executable_Command, "non executable command in pipeline stage for %d", i+2) or_return
+				}
+			}
+			return
+		case .Bool, .Char, .Dot, .Field, .Identifier, .Operator, .Number, .Nil, .Raw_String, .String, .Variable, .Left_Paren:
+			backup(t)
+			append(&pipe.cmds, command(t) or_return)
+		case:
+			err = unexpected_token(t, tok)
+			return
+		}
+	}
+}
+
+command :: proc(t: ^Tree) -> (cmd: ^Node_Command, err: Error) {
+	cmd = new_node(Node_Command, peek_non_space(t).pos)
+	loop: for {
+		op := operand(t) or_return
+		if op != nil {
+			append(&cmd.args, op)
+		}
+		#partial switch token := next(t); token.kind {
+		case .Space:
+			continue loop
+		case .Right_Delim, .Right_Paren:
+			backup(t)
+		case .Pipe:
+			break loop
+		case:
+			errorf(t, .Unexpected_Operand, "unexpected operand %s", token.value) or_return
+		}
+		break loop
+	}
+	if len(cmd.args) == 0 {
+		err = errorf(t, .Empty_Command, "empty command")
+	}
+	return
+}
+
+operand :: proc(t: ^Tree) -> (node: ^Node, err: Error) {
+	node = term(t) or_return
+	if node == nil {
+		return
+	}
+	if p := peek(t); p.kind == .Field {
+		chain := new_node(Node_Chain, p.pos)
+		chain.node = node
+		for peek(t).kind == .Field {
+			chain_add(chain, next(t).value)
+		}
+
+		#partial switch n in node.variant {
+		case ^Node_Field:
+			f := new_node(Node_Field, chain.pos)
+			resize(&chain.fields, len(chain.fields)+len(n.idents))
+			copy(chain.fields[len(n.idents):], chain.fields[:])
+			copy(chain.fields[:], n.idents)
+			f.idents = chain.fields[:]
+			node = f
+		case:
+			node = chain
+		}
+
+	}
+	return
+}
+
+// literal (number, string, nil, boolean)
+// function (identifier)
+// operator (function-like thing)
+// .
+// .field
+// $
+// $
+// '(' pipeline ')'
+term :: proc(t: ^Tree) -> (^Node, Error) {
+	#partial switch token := next_non_space(t); token.kind {
+	case .Identifier:
+		n := new_node(Node_Identifier, token.pos)
+		n.ident = token.value
+		return n, nil
+	case .Operator:
+		n := new_node(Node_Operator, token.pos)
+		n.value = token.value
+		return n, nil
+	case .Dot: return new_node(Node_Dot, token.pos), nil
+	case .Nil: return new_node(Node_Nil, token.pos), nil
+	case .Variable:
+		return parse_variable(t, token)
+	case .Field:
+		f := new_node(Node_Field, token.pos)
+		f.idents = make([]string, 1)
+		f.idents[0] = token.value[1:]
+		return f, nil
+	case .Bool:
+		b := new_node(Node_Bool, token.pos)
+		b.ok = token.value == "true"
+		return b, nil
+	case .Char, .Number:
+		return parse_number(t, token)
+	case .String, .Raw_String:
+		text, _, ok := strconv.unquote_string(token.value)
+		if !ok {
+			return nil, errorf(t, .Invalid_String, "invalid string literal: %s", token.value)
+		}
+		n := new_node(Node_String, token.pos)
+		n.quoted = token.value
+		n.text = text
+		return n, nil
+	case .Left_Paren:
+		return pipeline(t, "parenthesized pipeline", .Right_Paren)
+	}
+	backup(t)
+	return nil, nil
+}
+
+
+parse_number :: proc(t: ^Tree, token: Token) -> (^Node_Number, Error) {
+	text := token.value
+	n := new_node(Node_Number, token.pos)
+	n.text = text
+	if token.kind == .Char {
+		r, _, tail, ok := strconv.unquote_char(text[:], text[0])
+		if !ok || tail != "" {
+			return nil, errorf(t, .Invalid_Character, "invalid character literal: %s", text)
+		}
+		n.i = i64(r)
+		n.u = u64(r)
+		n.f = f64(r)
+		return n, nil
+	}
+
+
+	if u, ok := strconv.parse_u64(text); ok {
+		n.u = u
+	}
+	if i, ok := strconv.parse_i64(text); ok {
+		n.i = i
+		if i == 0 {
+			n.u = 0
+		}
+	}
+	if n.u == nil && n.i == nil {
+		if f, ok := strconv.parse_f64(text); ok {
+			n.f = f
+		}
+	}
+	if n.u == nil && n.i == nil && n.f == nil {
+		return nil, errorf(t, .Invalid_Number, "invalid number syntax: %q", text)
+	}
+	return n, nil
+
+}
+
+parse_variable :: proc(t: ^Tree, token: Token) -> (^Node_Variable, Error) {
+	v := new_node(Node_Variable, token.pos)
+	v.name = token.value
+	for var in t.vars {
+		if var == v.name {
+			return v, nil
+		}
+	}
+	return nil, errorf(t, .Undefined_Variable, "undefined variable %q", v.name)
+}

+ 608 - 0
core/text/template/scan/scan.odin

@@ -0,0 +1,608 @@
+package text_template_scan
+
+import "core:fmt"
+import "core:unicode"
+import "core:unicode/utf8"
+import "core:strings"
+
+Pos :: distinct int
+
+Token_Kind :: enum {
+	Error,
+	EOF,
+	
+	Comment,
+	Space,
+	
+	Left_Delim,
+	Right_Delim,	
+	
+	Identifier,	
+	Field,
+	Left_Paren,
+	Right_Paren,
+		
+	Bool,
+	Char,
+	Number,
+	Pipe,
+	Raw_String,
+	String,
+	Text,
+	Variable,
+	Operator,
+
+	Declare, // :=
+	Assign,  // -
+	
+	_Keyword,
+	Dot,
+	Block,
+	Break,
+	Continue,
+	Define,
+	Else,
+	End,
+	For,
+	If,
+	Include,
+	Nil,
+	With,
+}
+
+
+keywords := map[string]Token_Kind {
+	"."        = .Dot,
+	"block"    = .Block,
+	"break"    = .Break,
+	"continue" = .Continue,
+	"define"   = .Define,
+	"else"     = .Else,
+	"end"      = .End,
+	"for"      = .For,
+	"if"       = .If,
+	"include"  = .Include,
+	"nil"      = .Nil,
+	"with"     = .With,
+}
+
+Token :: struct {
+	kind:  Token_Kind,
+	value: string,
+	pos:   Pos,
+	line:  int,
+}
+
+token_to_string :: proc(using tok: Token, allocator := context.temp_allocator) -> string {
+	context.allocator = allocator
+	switch {
+	case kind == .EOF:
+		return fmt.tprint("EOF")
+	case kind == .Error:
+		return fmt.tprint(value)
+	case kind > ._Keyword:
+		return fmt.tprintf("<%s>", value)
+	case len(value) > 10:
+		return fmt.tprintf("%.10q...", value)
+	}
+	return fmt.tprintf("%q", value)
+}
+
+Scanner :: struct {
+	name:          string,
+	input:         string,
+	left_delim:    string,
+	right_delim:   string,
+	pos:           Pos,
+	start:         Pos,
+	width:         Pos,
+	tokens:        [dynamic]Token,
+	paren_depth:   int,
+	line:          int,
+	start_line:    int,
+	emit_comments: bool,
+}
+
+next :: proc(s: ^Scanner) -> rune {
+	if int(s.pos) >= len(s.input) {
+		s.width = 0
+		return utf8.RUNE_EOF
+	}
+	r, w := utf8.decode_rune_in_string(s.input[s.pos:])
+	s.width = Pos(w)
+	s.pos += s.width
+	if r == '\n' {
+		s.line += 1
+	}
+	return r
+}
+
+backup :: proc(s: ^Scanner) {
+	s.pos -= s.width
+	if s.width == 1 && s.input[s.pos] == '\n' {
+		s.line -= 1
+	}
+}
+
+peek :: proc(s: ^Scanner) -> rune {
+	r := next(s)
+	backup(s)
+	return r
+}
+
+emit :: proc(s: ^Scanner, kind: Token_Kind) {
+	append(&s.tokens, Token{
+		kind = kind,
+		pos = s.start,
+		line = s.start_line,
+		value = s.input[s.start:s.pos],
+	})
+	s.start = s.pos
+	s.start_line = s.line
+}
+
+ignore :: proc(s: ^Scanner) {
+	s.line += strings.count(s.input[s.start:s.pos], "\n")
+	s.start = s.pos
+	s.start_line = s.line
+}
+
+accept :: proc(s: ^Scanner, valid: string) -> bool {
+	if strings.contains_rune(valid, next(s)) >= 0 {
+		return true
+	}
+	backup(s)
+	return false
+}
+accept_run :: proc(s: ^Scanner, valid: string) {
+	for strings.contains_rune(valid, next(s)) >= 0 {
+		// Okay
+	}
+	backup(s)
+}
+
+
+// State procedures
+
+DEFAULT_LEFT_DELIM :: "{{"
+DEFAULT_RIGHT_DELIM :: "}}"
+
+LEFT_COMMENT  :: "/*"
+RIGHT_COMMENT :: "*/"
+
+
+init :: proc(s: ^Scanner, name, input: string, left: string = "", right: string = "",
+             emit_comments: bool = false) -> ^Scanner {
+	s.name          = name
+	s.input         = input
+	s.left_delim    = left  if left  != "" else DEFAULT_LEFT_DELIM
+	s.right_delim   = right if right != "" else DEFAULT_RIGHT_DELIM
+	s.emit_comments = emit_comments
+	reset(s)
+	return s
+}
+
+destroy :: proc(s: ^Scanner) {
+	delete(s.tokens)
+	s.tokens = {}
+}
+
+
+reset :: proc(s: ^Scanner) {
+	clear(&s.tokens)
+	s.pos   = 0
+	s.start = 0
+	s.width = 0
+	s.paren_depth = 0
+	s.line        = 1
+	s.start_line  = 1
+}
+
+
+// Finite State Machine Scanning
+
+Scan_State :: enum {
+	None,
+
+	Comment,
+	Space,
+
+	Identifier,
+	Field,
+	Left_Delim,
+	Right_Delim,
+
+	Char,
+	Number,
+	Raw_String,
+	String,
+	Text,
+	Variable,
+
+	Inside_Action,
+}
+
+step :: proc(s: ^Scanner, state: Scan_State) -> Scan_State {
+	scan_error :: proc(s: ^Scanner, value: string) -> Scan_State {
+		append(&s.tokens, Token{
+			kind = .Error,
+			pos = s.start,
+			line = s.start_line,
+			value = value,
+		})
+		return nil
+	}
+	scan_variable_or_field :: proc(s: ^Scanner, kind: Token_Kind) -> Scan_State {
+		if at_terminator(s) {
+			if kind == .Variable {
+				emit(s, kind)
+			} else {
+				emit(s, .Dot)
+			}
+			return .Inside_Action
+		}
+		for {
+			r := next(s)
+			if !is_alpha_numeric(r) {
+				backup(s)
+				break
+			}
+		}
+		if !at_terminator(s) {
+			return scan_error(s, "bad character")
+		}
+		emit(s, kind)
+		return .Inside_Action
+	}
+
+	switch state {
+	case .None:
+		return nil
+
+	case .Comment:
+		s.pos += Pos(len(LEFT_COMMENT))
+		i := strings.index(s.input[s.pos:], RIGHT_COMMENT)
+		if i < 0 {
+			return scan_error(s, "unclosed comment")
+		}
+		s.pos += Pos(i + len(RIGHT_COMMENT))
+		delim, trim_space := at_right_delim(s)
+		if !delim {
+			return scan_error(s, "comment ends before closing delimiter")
+		}
+		if s.emit_comments {
+			emit(s, .Comment)
+		}
+		if trim_space {
+			s.pos += TRIM_MARKER_LEN
+		}
+		s.pos += Pos(len(s.right_delim))
+		if trim_space {
+			s.pos += left_trim_length(s.input[s.pos:])
+		}
+		ignore(s)
+		return .Text
+
+	case .Space:
+		space_count: int
+		for {
+			r := peek(s)
+			if !is_space(r) {
+				break
+			}
+			next(s)
+			space_count += 1
+		}
+
+		if has_right_trim_marker(s.input[s.pos-1:]) && strings.has_prefix(s.input[s.pos-1+TRIM_MARKER_LEN:], s.right_delim) {
+			backup(s)
+			if space_count == 1 {
+				return .Right_Delim
+			}
+		}
+		emit(s, .Space)
+		return .Inside_Action
+
+	case .Identifier:
+		identifier_loop: for {
+			r := next(s)
+			if is_alpha_numeric(r) {
+				// Okay
+			} else {
+				backup(s)
+				word := s.input[s.start:s.pos]
+				if !at_terminator(s) {
+					return scan_error(s, "bad character")
+				}
+
+				if kw := keywords[word]; kw > ._Keyword {
+					emit(s, kw)
+				} else if word == "true" || word == "false" {
+					emit(s, .Bool)
+				} else {
+					emit(s, .Identifier)
+				}
+				break identifier_loop
+			}
+		}
+		return .Inside_Action
+	case .Field:
+		return scan_variable_or_field(s, .Field)
+
+	case .Left_Delim:
+		s.pos += Pos(len(s.left_delim))
+		trim_space := has_left_trim_marker(s.input[s.pos:])
+		after_marker := TRIM_MARKER_LEN if trim_space else 0
+		if strings.has_prefix(s.input[s.pos+after_marker:], LEFT_COMMENT) {
+			s.pos += after_marker
+			ignore(s)
+			return .Comment
+		}
+		emit(s, .Left_Delim)
+		s.pos += after_marker
+		ignore(s)
+		s.paren_depth = 0
+		return .Inside_Action
+
+	case .Right_Delim:
+		trim_space := has_right_trim_marker(s.input[s.pos:])
+		if trim_space {
+			s.pos += TRIM_MARKER_LEN
+			ignore(s)
+		}
+		s.pos += Pos(len(s.right_delim))
+		emit(s, .Right_Delim)
+		if trim_space {
+			s.pos += left_trim_length(s.input[s.pos:])
+			ignore(s)
+		}
+		return .Text
+
+	case .Char:
+		char_loop: for {
+			switch next(s) {
+			case '\\':
+				if r := next(s); r != utf8.RUNE_EOF && r != '\n' {
+					break
+				}
+				fallthrough
+			case utf8.RUNE_EOF, '\n':
+				return scan_error(s, "unterminated character constant")
+			case '\'':
+				break char_loop
+			}
+		}
+		emit(s, .Char)
+		return .Inside_Action
+
+	case .Number:
+		accept(s, "+-")
+		digits := "0123456789_"
+		if accept(s, "0") {
+			switch {
+			case accept(s, "bB"):
+				digits = "01_"
+			case accept(s, "oO"):
+				digits = "01234567_"
+			case accept(s, "xX"):
+				digits = "0123456789ABCDEFabcdef_"
+			}
+		}
+		accept_run(s, digits)
+		if accept(s, ".") {
+			accept_run(s, digits)
+		}
+		if len(digits) == 10+1 && accept(s, "eE") {
+			accept(s, "+-")
+			accept_run(s, digits)
+		}
+		if is_alpha_numeric(peek(s)) {
+			next(s)
+			return scan_error(s, "bad number syntax")
+		}
+		emit(s, .Number)
+		return .Inside_Action
+
+	case .Raw_String:
+		raw_string_loop: for {
+			switch next(s) {
+			case utf8.RUNE_EOF:
+				return scan_error(s, "unterminated raw quoted string")
+			case '`':
+				break raw_string_loop
+			}
+		}
+		emit(s, .Raw_String)
+		return .Inside_Action
+
+	case .String:
+		string_loop: for {
+			switch next(s) {
+			case '\\':
+				if r := next(s); r != utf8.RUNE_EOF && r != '\n' {
+					break
+				}
+				fallthrough
+			case utf8.RUNE_EOF, '\n':
+				return scan_error(s, "unterminated quoted string")
+			case '"':
+				break string_loop
+			}
+		}
+		emit(s, .String)
+		return .Inside_Action
+
+	case .Text:
+		s.width = 0
+		if x := strings.index(s.input[s.pos:], s.left_delim); x >= 0 {
+			ldn := Pos(len(s.left_delim))
+			s.pos += Pos(x)
+			trim_length := Pos(0)
+			if has_left_trim_marker(s.input[s.pos+ldn:]) {
+				trim_length = right_trim_length(s.input[s.start:s.pos])
+			}
+			s.pos -= trim_length
+			if s.pos > s.start {
+				s.line += strings.count(s.input[s.start:s.pos], "\n")
+				emit(s, .Text)
+			}
+			s.pos += trim_length
+			ignore(s)
+			return .Left_Delim
+		}
+		s.pos = Pos(len(s.input))
+		// EOF
+		if s.pos > s.start {
+			s.line += strings.count(s.input[s.start:s.pos], "\n")
+			emit(s, .Text)
+		}
+		emit(s, .EOF)
+
+	case .Variable:
+		if at_terminator(s) {
+			emit(s, .Variable)
+			return .Inside_Action
+		}
+		return scan_variable_or_field(s, .Variable)
+
+	case .Inside_Action:
+		if delim, _ := at_right_delim(s); delim {
+			if s.paren_depth == 0 {
+				return .Right_Delim
+			}
+			return scan_error(s, "unclosed left paren")
+		}
+
+		rp := peek(s)
+		switch r := next(s); {
+		case r == utf8.RUNE_EOF:
+			return scan_error(s, "unclosed action")
+		case is_space(r):
+			backup(s) // Just in case of " -}}"
+			return .Space
+		case r == '.':
+			// Look for a '.field'
+			if s.pos < Pos(len(s.input)) {
+				if r := s.input[s.pos]; r < '0' || r > '9' {
+					return .Field
+				}
+			}
+			// it's a number
+			fallthrough
+
+		case (r == '+' || r == '-') && ('0' <= rp && rp <= '9'):
+			fallthrough
+		case '0' <= r && r <= '9':
+			backup(s)
+			return .Number
+		case r == '+', r == '-', r == '*':
+			emit(s, .Operator)
+		case is_alpha_numeric(r):
+			backup(s)
+			return .Identifier
+		case r == '|':
+			emit(s, .Pipe)
+		case r == '"':
+			return .String
+		case r == '`':
+			return .Raw_String
+		case r == '\'':
+			return .Char
+		case r == '(':
+			emit(s, .Left_Paren)
+			s.paren_depth += 1
+		case r == ')':
+			emit(s, .Right_Paren)
+			s.paren_depth -= 1
+			if s.paren_depth < 0 {
+				return scan_error(s, "unexpected right parenthesis ')'")
+			}
+		case r == '$':
+			return .Variable
+		case r == ':':
+			if next(s) != '=' {
+				return scan_error(s, "expected :=")
+			}
+			emit(s, .Declare)
+		case r == '=':
+			emit(s, .Assign)
+		case r <= unicode.MAX_ASCII && unicode.is_print(r):
+			emit(s, .Char)
+		case:
+			return scan_error(s, "unrecognized character in action")
+		}
+
+		return .Inside_Action
+	}
+	return nil
+}
+
+
+
+run :: proc(s: ^Scanner) {
+	state := Scan_State.Text
+	for state != nil {
+		state = step(s, state)
+	}
+}
+
+@private TRIM_MARKER :: '-'
+@private TRIM_MARKER_LEN :: Pos(2) // includes space
+
+is_space :: proc(r: rune) -> bool {
+	switch r {
+	case ' ', '\t', '\r', '\n':
+		return true
+	}
+	return false
+}
+
+is_alpha_numeric :: proc(r: rune) -> bool {
+	return r == '_' || unicode.is_letter(r) || unicode.is_digit(r)
+}
+
+left_trim_length :: proc(s: string) -> Pos {
+	return Pos(len(s) - len(strings.trim_left_proc(s, is_space)))
+}
+right_trim_length :: proc(s: string) -> Pos {
+	return Pos(len(s) - len(strings.trim_right_proc(s, is_space)))
+}
+
+has_left_trim_marker :: proc(s: string) -> bool {
+	return len(s) >= 2 && s[0] == TRIM_MARKER && is_space(rune(s[1]))
+}
+
+has_right_trim_marker :: proc(s: string) -> bool {
+	return len(s) >= 2 && is_space(rune(s[0])) && s[1] == TRIM_MARKER
+}
+
+at_right_delim :: proc(s: ^Scanner) -> (delim, trim_spaces: bool) {
+	if has_right_trim_marker(s.input[s.pos:]) && strings.has_prefix(s.input[s.pos+TRIM_MARKER_LEN:], s.right_delim) {
+		delim = true
+		trim_spaces = true
+		return
+	}
+	if strings.has_prefix(s.input[s.pos:], s.right_delim) {
+		delim = true
+		trim_spaces = false
+		return
+	}
+	delim = false
+	trim_spaces = false
+	return
+}
+
+at_terminator :: proc(s: ^Scanner) -> bool {
+	r := peek(s)
+	if is_space(r) {
+		return true
+	}
+	
+	switch r {
+	case utf8.RUNE_EOF, '.', ',', '(', ')', '|', ':':
+		return true
+	}
+	
+	rd, _ := utf8.decode_rune_in_string(s.right_delim)
+	return rd == r
+}