unclecode/crawl4ai · error · ValueError

Circular include {p}

Error message

Circular include {p}

What it means

ValueError from the C4A-Script compiler's _parse_with_includes pass: an INCLUDE command resolves (relative to the compile root) to a file already present in the current include chain ('seen' set), which means the include graph is cyclic. Compilation stops immediately because inlining would recurse forever.

Source

Thrown at crawl4ai/script/c4ai_script.py:350

        # Handle list input by joining with newlines
        if isinstance(text, list):
            text = '\n'.join(text)
        
        ir = self._parse_with_includes(text)
        ir = self._collect_procs(ir)
        ir = self._inline_calls(ir)
        ir = self._apply_set_vars(ir)
        return [self._emit_js(c) for c in ir if isinstance(c,Cmd) and c.op!="NOP"]

    # passes
    def _parse_with_includes(self,txt,seen=None):
        seen=seen or set()
        cmds=ASTBuilder().transform(self.parser.parse(txt))
        out=[]
        for c in cmds:
            if isinstance(c,Cmd) and c.op=="INCLUDE":
                p=(self.root/c.args[0]).resolve()
                if p in seen: raise ValueError(f"Circular include {p}")
                seen.add(p); out+=self._parse_with_includes(p.read_text(),seen)
            else: out.append(c)
        return out

    def _collect_procs(self,ir):
        out=[]
        for i in ir:
            if isinstance(i,Proc): self.procs[i.name]=i
            else: out.append(i)
        return out

    def _inline_calls(self,ir):
        out=[]
        for c in ir:
            if isinstance(c,Cmd) and c.op=="CALL":
                if c.args[0] not in self.procs:
                    raise ValueError(f"Unknown procedure {c.args[0]!r}")
                out+=self._inline_calls(self.procs[c.args[0]].body)

View on GitHub (pinned to 7e80152142)

Solutions

  1. Break the cycle: move shared commands into a third file both scripts include, with no back-edges.
  2. Remove self-includes — a file never needs to include itself.
  3. Check for accidental identical includes of the same file twice in one chain; hoist the duplicate include to the top-level script.
  4. Map the include graph quickly (grep INCLUDE lines) to find the loop before editing.

Example fix

# before
# a.c4a:  INCLUDE "b.c4a"
# b.c4a:  INCLUDE "a.c4a"   → ValueError: Circular include .../a.c4a

# after
# common.c4a: CLICK "#login"; WAIT_FOR navigation
# a.c4a:     INCLUDE "common.c4a"
# b.c4a:     INCLUDE "common.c4a"
Defensive patterns

Strategy: validation

Validate before calling

def check_includes_acyclic(entry: Path, root: Path) -> bool:
    import re
    def walk(p, seen):
        if p in seen:
            return False
        seen = seen | {p}
        for m in re.finditer(r'INCLUDE\s+"([^"]+)"', p.read_text()):
            if not walk((root / m.group(1)).resolve(), seen):
                return False
        return True
    return walk(entry.resolve(), set())

Try / catch

try:
    js = compile_string(script, root=root)
except ValueError as e:
    if 'Circular include' in str(e):
        raise ValueError(f"Fix include cycle: {e}") from e

Prevention

When it happens

Trigger: Script A includes B and B includes A (directly or through a longer chain), or a script includes itself. The check is per-chain: the resolved path appearing anywhere in the active 'seen' set raises.

Common situations: Shared snippet files that include each other's helpers, refactoring where two partial scripts were made mutually inclusive, a file including itself as a default-header habit, or symlinks/paths that resolve to the same file via different spellings.

Related errors


AI-assisted analysis of unclecode/crawl4ai@7e80152142 (2026-08-14). Data as JSON: /api/errors/4f46f7c38af61702. Report an issue: GitHub.