feat: Warns the user if they exceed 16 context frames.

This commit is contained in:
Spencer Brower
2026-07-28 15:41:44 -04:00
parent f1c7d02bc1
commit 6d7b88a8d6
5 changed files with 240 additions and 152 deletions
+182 -137
View File
@@ -3,16 +3,26 @@ package mustache
import "base:runtime"
import "core:fmt"
import "core:log"
import "core:reflect"
import "core:strings"
// A hypothetical maximum context depth. Trying to pass more than this many items
// to render(tpl, data), or nesting templates further than this depth would be
// an error.
// May be enforced in a later version (for performance)
MAX_CONTEXT_DEPTH :: #config(MAX_CONTEXT_DEPTH, 16)
// Context_Stack is the growable stack of data frames walked top-to-bottom by
// resolve_name. Frames are pushed on section descent and popped on exit; the
// root data (or each element of a root []any) forms the base frames.
Context_Stack :: [dynamic]any
// ---------------------------------------------------------------------------
// Error types
// ---------------------------------------------------------------------------
Error_Kind :: enum {
Syntax, // parse-time: malformed template
Data, // render-time: template fine, data wrong (e.g. filter misuse)
Data, // render-time: template fine, data wrong (e.g. filter misuse)
}
Error_Body :: struct {
@@ -22,14 +32,18 @@ Error_Body :: struct {
}
// Error is nil when no error occurred.
Error :: union { Error_Body }
Error :: union {
Error_Body,
}
// body unwraps the Error_Body from a non-nil Error.
// Precondition: err != nil.
body :: proc(err: Error) -> Error_Body {
switch e in err {
case Error_Body: return e
case: return {}
case Error_Body:
return e
case:
return {}
}
}
@@ -143,11 +157,9 @@ render :: proc(
err: Error,
) {
builder: strings.Builder
strings.builder_init(&builder, allocator)
defer strings.builder_destroy(&builder)
strings.builder_init(&builder, context.temp_allocator)
ctx := make([dynamic]any, 0, 4, allocator)
defer delete(ctx)
ctx := make(Context_Stack, 0, 4, context.temp_allocator)
// If data is a []any, expand into individual context frames.
// Otherwise, push as a single frame.
@@ -287,14 +299,14 @@ parse_section :: proc(
case .Section_Close:
if strings.contains(tok.value, "|") {
return Error_Body {
msg = fmt.tprintf(
"pipe expression not allowed in close tag '{{{{/%s}}}}' — use the bare key",
tok.value,
),
pos = tok.pos,
kind = .Syntax,
}
return Error_Body {
msg = fmt.tprintf(
"pipe expression not allowed in close tag '{{{{/%s}}}}' — use the bare key",
tok.value,
),
pos = tok.pos,
kind = .Syntax,
}
}
if end_tag != "" && tok.value == end_tag {
pos^ += 1
@@ -331,12 +343,7 @@ parse_section :: proc(
idx := len(nodes)
append(
nodes,
Node {
kind = .Parent,
key = tok.value,
indent = tok.indent,
pos = tok.pos,
},
Node{kind = .Parent, key = tok.value, indent = tok.indent, pos = tok.pos},
)
parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return
nodes[idx].children = nodes[idx + 1:len(nodes)]
@@ -344,15 +351,7 @@ parse_section :: proc(
case .Block_Open:
pos^ += 1
idx := len(nodes)
append(
nodes,
Node {
kind = .Block,
key = tok.value,
indent = tok.indent,
pos = tok.pos,
},
)
append(nodes, Node{kind = .Block, key = tok.value, indent = tok.indent, pos = tok.pos})
parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return
nodes[idx].children = nodes[idx + 1:len(nodes)]
}
@@ -499,21 +498,29 @@ remove_line_indent :: proc(s: string, indent: string, allocator := context.alloc
render_template :: proc(
pt: Template,
ctx: ^[dynamic]any,
ctx: ^Context_Stack,
partials: map[string]Template,
b: ^strings.Builder,
blocks: map[string]Block_Override,
indent: string,
) -> Error {
if len(indent) > 0 {
state := Indent_State{indent = indent, at_line_start = false}
state := Indent_State {
indent = indent,
at_line_start = false,
}
strings.write_string(b, indent) // first line always gets indent
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, &state)
}
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, nil)
}
write_indented :: proc(b: ^strings.Builder, indent: string, content: string, at_line_start: ^bool) {
write_indented :: proc(
b: ^strings.Builder,
indent: string,
content: string,
at_line_start: ^bool,
) {
if len(indent) == 0 || len(content) == 0 {
strings.write_string(b, content)
return
@@ -544,7 +551,7 @@ write_indented :: proc(b: ^strings.Builder, indent: string, content: string, at_
render_nodes :: proc(
current: Template,
nodes: []Node,
ctx: ^[dynamic]any,
ctx: ^Context_Stack,
partials: map[string]Template,
b: ^strings.Builder,
blocks: map[string]Block_Override = nil,
@@ -588,16 +595,16 @@ render_nodes :: proc(
if perr == nil {
temp: strings.Builder
strings.builder_init(&temp, context.temp_allocator)
render_nodes(
sub_tpl,
sub_tpl.nodes[:],
ctx,
partials,
&temp,
blocks,
nil,
) or_return
write_value(b, strings.to_string(temp), escape = true)
render_nodes(
sub_tpl,
sub_tpl.nodes[:],
ctx,
partials,
&temp,
blocks,
nil,
) or_return
write_value(b, strings.to_string(temp), escape = true)
}
} else {
write_value(b, val, escape = true)
@@ -630,16 +637,16 @@ render_nodes :: proc(
if perr == nil {
temp: strings.Builder
strings.builder_init(&temp, context.temp_allocator)
render_nodes(
sub_tpl,
sub_tpl.nodes[:],
ctx,
partials,
&temp,
blocks,
nil,
) or_return
write_value(b, strings.to_string(temp), escape = false)
render_nodes(
sub_tpl,
sub_tpl.nodes[:],
ctx,
partials,
&temp,
blocks,
nil,
) or_return
write_value(b, strings.to_string(temp), escape = false)
}
} else {
write_value(b, val, escape = false)
@@ -666,23 +673,36 @@ render_nodes :: proc(
context.temp_allocator,
)
if perr == nil {
render_nodes(
sub_tpl,
sub_tpl.nodes[:],
ctx,
partials,
b,
blocks,
nil,
) or_return
render_nodes(
sub_tpl,
sub_tpl.nodes[:],
ctx,
partials,
b,
blocks,
nil,
) or_return
}
} else if is_truthy(val) {
children := node.children
elem_info, count, data := list_info(val)
if elem_info != nil {
for j in 0 ..< count {
elem := extract_list_element(elem_info, data, j)
append(ctx, elem)
} else if is_truthy(val) {
children := node.children
elem_info, count, data := list_info(val)
if elem_info != nil {
for j in 0 ..< count {
elem := extract_list_element(elem_info, data, j)
context_push(ctx, elem, current, node)
defer pop(ctx)
render_nodes(
current,
children,
ctx,
partials,
b,
blocks,
indent_state,
) or_return
}
} else {
context_push(ctx, val, current, node)
defer pop(ctx)
render_nodes(
current,
@@ -694,13 +714,8 @@ render_nodes :: proc(
indent_state,
) or_return
}
} else {
append(ctx, val)
defer pop(ctx)
render_nodes(current, children, ctx, partials, b, blocks, indent_state) or_return
}
}
i += 1 + len(node.children)
i += 1 + len(node.children)
case .Inverted:
val := resolve_name(node.key, ctx[:])
@@ -714,10 +729,18 @@ render_nodes :: proc(
}
val = transformed
}
if !is_truthy(val) {
render_nodes(current, node.children, ctx, partials, b, blocks, indent_state) or_return
}
i += 1 + len(node.children)
if !is_truthy(val) {
render_nodes(
current,
node.children,
ctx,
partials,
b,
blocks,
indent_state,
) or_return
}
i += 1 + len(node.children)
case .Partial:
name := node.key
@@ -736,64 +759,64 @@ render_nodes :: proc(
}
i += 1
case .Block:
content_nodes: []Node
content_blocks := blocks
render_current := current
case .Block:
content_nodes: []Node
content_blocks := blocks
render_current := current
found_override := false
if blocks != nil {
if o, ok := blocks[node.key]; ok {
content_nodes = o.nodes
found_override = true
render_current = o.source
found_override := false
if blocks != nil {
if o, ok := blocks[node.key]; ok {
content_nodes = o.nodes
found_override = true
render_current = o.source
}
}
}
if !found_override {
content_nodes = node.children
}
if len(node.indent) > 0 {
temp: strings.Builder
strings.builder_init(&temp, context.temp_allocator)
render_nodes(
render_current,
content_nodes,
ctx,
partials,
&temp,
content_blocks,
nil,
) or_return
at_ls := true
write_indented(b, node.indent, strings.to_string(temp), &at_ls)
} else {
render_nodes(
render_current,
content_nodes,
ctx,
partials,
b,
content_blocks,
indent_state,
) or_return
}
i += 1 + len(node.children)
case .Parent:
parent_children := node.children
merged := merge_block_overrides(parent_children, blocks, current)
pt, found := partials[node.key]
if !found {
warn_missing_partial(current, partials, node, node.key)
} else {
warn_unmatched_block_overrides(current, pt, parent_children)
render_template(pt, ctx, partials, b, merged, node.indent) or_return
if indent_state != nil {
indent_state.at_line_start = false
if !found_override {
content_nodes = node.children
}
}
i += 1 + len(node.children)
if len(node.indent) > 0 {
temp: strings.Builder
strings.builder_init(&temp, context.temp_allocator)
render_nodes(
render_current,
content_nodes,
ctx,
partials,
&temp,
content_blocks,
nil,
) or_return
at_ls := true
write_indented(b, node.indent, strings.to_string(temp), &at_ls)
} else {
render_nodes(
render_current,
content_nodes,
ctx,
partials,
b,
content_blocks,
indent_state,
) or_return
}
i += 1 + len(node.children)
case .Parent:
parent_children := node.children
merged := merge_block_overrides(parent_children, blocks, current)
pt, found := partials[node.key]
if !found {
warn_missing_partial(current, partials, node, node.key)
} else {
warn_unmatched_block_overrides(current, pt, parent_children)
render_template(pt, ctx, partials, b, merged, node.indent) or_return
if indent_state != nil {
indent_state.at_line_start = false
}
}
i += 1 + len(node.children)
}
}
return nil
@@ -937,3 +960,25 @@ warn_unmatched_block_overrides :: proc(
}
}
context_push :: proc(ctx: ^Context_Stack, val: any, current: Template, node: Node) {
append(ctx, val)
if len(ctx^) == MAX_CONTEXT_DEPTH + 1 {
warn_context_depth(current, node)
}
}
// warn_context_depth emits a diagnostic warning pointing at the section tag
// whose push carried the context stack past MAX_CONTEXT_DEPTH.
warn_context_depth :: proc(current: Template, node: Node) {
msg := fmt.tprintf(
"context stack depth exceeded %d (possible recursive section/partial)",
MAX_CONTEXT_DEPTH,
)
path := current.path
if path == "" {
path = "<input>"
}
diag := format_error(path, current.source, node.pos, msg, "", colorize = should_colorize())
log.warnf("%s", diag)
}