Changes for version 0.11 - 2026-10-26

  • Groups/includes. The configuration restored at #end_group no longer brings back a stale opened_includes: when a group is opened before #include and ended inside the file, the file stays listed (before, it vanished from the list); when it is opened inside a file and ended after it, the closed file is no longer listed (before, it stayed there to the end of the run).
  • Includes. Incompatible: a file that is still being read may be included again (the check by file name, E_CYCLIC_INCLUDE, is removed: it forbade inclusions that end, e.g. behind a guard, after a change of a configuration key that the file reads, or read with other callbacks active, and missed a key or a section that includes itself). Instead, the new setting Fab/max_include_depth (default 200) limits nested include__text() calls of all kinds; one more halts with E_INCLUDE_TOO_DEEP, naming the file open most often.
  • Tests. New usage cases t/54_m4.t (m4) and t/55_cpp.t (a C preprocessor, agnostic of C syntax), written as callbacks; their expected outputs are those of GNU m4 1.4.19 and GNU cpp 13.3. TODO parts: putting text in front of the rest of the input, and output without the chunk preprocessor.
  • POD. Two new MISS entries in TODO: input__prepend() and out__text().
  • Control flow. The control API is implemented (it was [Planned]): control__start(), control__set_skipping(), control__end(), control__set_pointer(), control__define_invalidation_pointer(), control__jump_to(), and the new control__skipping() and control__loop_counter(). Every input stream (a process_string() call) has its own control stack; groups do not restore it; Fab/control__loop_indices lists the indices of its loop-like frames (those with a label). In a skipped part the chunk preprocessor is not called, and the default parser executes no directive. The interleaver and the directive parser get two more arguments: the skipping state and the replay flag. Errors: E_MISMATCHED_CONTROL_END, E_DANGLING_CONTROL_END, E_UNCLOSED_CONTROL_BLOCK, E_POINTER_NOT_FOUND, E_BLOCK_LABEL_NOT_FOUND, and the new E_CONTROL_OUTSIDE_DIRECTIVE. Differences from the plan: every included text (not only a file) has its own stack; the stack is kept outside the configuration; a jump without a label goes to the innermost loop-like frame (not the top frame); a jump to the start of a frame makes the directive there re-enter it: the first control__start() with the frame's type and label continues it instead of pushing a new frame, and control__reentering() names it; a pointer at the end of a directive is recorded after the directive parser returns; frames above a frame searching for a pointer skip on entry; a frame is loop-like when a label is given; control__skipping(), control__reentering() and control__loop_counter() are new.
  • Control flow. New setting Fab/control__loop_limits (a list, default [1e300]): entry D is the most jumps back in one instance of a loop at loop depth D (counting the loops of including inputs too); the last entry counts for all greater depths. One more halts with E_LOOP_LIMIT.
  • Tests. New t/19-control-flow.t (a parser with #if/#elif/#else/#endif, #while, #repeat, #break, #continue, labels); in t/55_cpp.t the variant using the control API is no longer TODO.
  • Tests. t/55_cpp.t: the engine leaves a macro name unexpanded ("painted") only while that macro's own replacement is being read, as C11 6.10.3.4 and GCC do (before, every token an expansion produced kept the name, so calls delayed by EMPTY() and forced by EVAL were never made). New tests: the examples of the Habr article "Проклятый огонь, или магия препроцессора C" (haqreu, 2024; a decrement, a branch, a test for zero, a delayed recursive call, a countdown), each with the steps of expansion that make its output.
  • Control flow. control__jump_to() with a label that no open block has searches forward for a block with this label that starts directly inside the current block (or at the top level of the input), as the plan's E_BLOCK_LABEL_NOT_FOUND implied; it halts with E_BLOCK_LABEL_NOT_FOUND only if the enclosing block (or the input) ends first. (0.11-pre4 halted at once.) New blocks started while a search is on skip on entry.
  • Tests. t/55_cpp.t: the engine supports __VA_OPT__ (C23). New tests: examples of P. Fultz ("Is the C preprocessor Turing complete?", 2012, and the Cloak wiki: DEFER/OBSTRUCT, NOT/BOOL/IF/WHEN, IS_PAREN, REPEAT, WHILE, comparing names by painted tokens, FOREVER counting the scans) and of h4x0r.org ("Recursive macros in C, demystified", 2025: counting arguments with MAP). t/19-control-flow.t: #grid, one directive starting two loops, and control__reentering().
  • Tests. t/55_cpp.t: the examples of J. Heathcote, "C Pre-Processor Magic" (jhnet): IF_ELSE, BOOL, deferred calls, recursion, MAP with HAS_ARGS.
  • Tests. t/55_cpp.t: the Habr article's bonus (X1(0) doubling 64 times) at depth 8 through the cpp engine, and as 64 sections with promises (190 entries; assembling one level gives 2^8 zeros).

Modules

A powerful, general-purpose document expansion framework (currently Caching is unimplemented {???})