Include Cycle Detection¶
Overview¶
Implementations MUST detect and reject circular include chains.
The Threat¶
a.beancount: include "b.beancount"
b.beancount: include "c.beancount"
c.beancount: include "a.beancount" ; Cycle!
Without detection, this causes: - Infinite loops - Stack overflow - Memory exhaustion
Requirements¶
Detection¶
Implementations MUST:
- Track all files in the current include chain
- Detect when a file is included that's already in the chain
- Report the cycle with the full chain
Handling¶
When a cycle is detected:
- Stop processing the cyclic include
- Report an error with the cycle path
- Continue processing other includes (if error recovery enabled)
Implementation¶
Using a Set¶
def load_file(path: Path, include_chain: set[Path] = None):
if include_chain is None:
include_chain = set()
resolved = path.resolve()
# Check for cycle
if resolved in include_chain:
raise CycleError(include_chain, resolved)
# Add to chain
include_chain.add(resolved)
try:
content = resolved.read_text()
for include in parse_includes(content):
load_file(include, include_chain.copy())
finally:
include_chain.remove(resolved)
Using a Stack¶
def load_file(path: Path, include_stack: list[Path] = None):
if include_stack is None:
include_stack = []
resolved = path.resolve()
# Check for cycle
if resolved in include_stack:
cycle_start = include_stack.index(resolved)
cycle = include_stack[cycle_start:] + [resolved]
raise CycleError(cycle)
include_stack.append(resolved)
try:
content = resolved.read_text()
for include in parse_includes(content):
load_file(include, include_stack)
finally:
include_stack.pop()
Error Messages¶
error: Include cycle detected
--> c.beancount:3:1
|
3 | include "a.beancount"
| ^^^^^^^^^^^^^^^^^^^^^ creates cycle
|
= cycle: a.beancount → b.beancount → c.beancount → a.beancount
Edge Cases¶
Self-Inclusion¶
; file.beancount
include "file.beancount" ; Direct self-cycle
MUST be detected as a cycle of length 1.
Diamond Includes (Not a Cycle)¶
main.beancount includes a.beancount and b.beancount
a.beancount includes common.beancount
b.beancount includes common.beancount
This is NOT a cycle. common.beancount should be loaded once.
Implementation Note¶
Track by canonical path (resolved, normalized) to detect cycles regardless of how the path is written:
include "accounts/../accounts.beancount" ; Same as accounts.beancount
include "./accounts.beancount" ; Same file
Test Vectors¶
Must Detect¶
- Direct self-inclusion
- Two-file cycle (A→B→A)
- Three-file cycle (A→B→C→A)
- Cycle via different path spellings
Must Allow¶
- Diamond pattern (shared include)
- Same file included from multiple places (once)