\foo{x} → \bar{x} → \foo{x} → ∞
Expected: hit depth limit, emit warning, output either x or the unexpanded \foo{x}
Actual: / \foo */ — silently converted to a comment, lost the argument x entirely
Thanks for the stress test! You are absolutely right, and I really appreciate you catching this edge case.
I've traced the issue in the codebase:
1. The recursion depth limit *is* triggering correctly (preventing an infinite loop).
2. However, when it bails out, it returns the partially expanded macro call (e.g., `\foo{x}`).
3. Since the macro definition was removed during the preprocessing step, the parser then sees `\foo` as an unknown command and converts it into an error comment, accidentally discarding the argument `{x}` in the process.
*The intended behavior* should definitely be to preserve the content (the `x`) even if the macro logic fails. I will fix the fallback logic to ensure it fails gracefully without eating the arguments.
Thanks again for the sharp eye! These kinds of checks are super helpful.
Great question! I love Pandoc and use it often, but as a "universal" converter, it sometimes misses the nuances of specific pairs.
Tylax is designed specifically for the LaTeX $\leftrightarrow$ Typst workflow. By focusing on just this 1-on-1 pair, we can offer:
Better Math & Macros: A built-in macro expander handles custom commands (\newcommand) and complex nested math that general parsers often struggle with.
Cleaner Code: The output is designed to be idiomatic and human-readable (e.g., using native Typst functions), not just "compilable."
WASM Support: Being written in Rust means it runs instantly in the browser, making it easy to embed in web apps without a backend.
Pandoc is the Swiss Army knife; we're trying to be a specialized tool just for this specific transition.
That makes sense, especially the issue with macros. As many people have pointed out, since TeX is not just markup but an actual programming language, its output can not be determined, in the general case, without running the source through the TeX interpreter. Of course, the same is true of Typst.
You are absolutely spot on. Both systems are Turing-complete, so a perfect conversion without a full runtime execution is theoretically impossible for the general case.
That's exactly the trade-off Tylax makes: we aren't trying to be a full TeX engine (which would be overkill and slow). Instead, we aim to cover the "99% use case" of academic and technical writing—where macros are mostly used for shorthand, notation aliases, or simple formatting, rather than complex computation.
Our "limited macro expander" is the middle ground: it's dumb enough to be fast and safe (no infinite loops), but smart enough to handle the \newcommand shortcuts that riddle almost every paper. It's about being pragmatically useful rather than theoretically perfect
\newcommand{\foo}[1]{\bar{#1}} \renewcommand{\bar}[1]{\foo{#1}} % mutual recursion \foo{x} \def\x{\y}\def\y{z}\x % chained expansion
+>
#set page(paper: "a4") #set heading(numbering: "1.") #set math.equation(numbering: "(1)") /* \foo / / \y /
\foo{x} → \bar{x} → \foo{x} → ∞ Expected: hit depth limit, emit warning, output either x or the unexpanded \foo{x} Actual: / \foo */ — silently converted to a comment, lost the argument x entirely
I've traced the issue in the codebase: 1. The recursion depth limit *is* triggering correctly (preventing an infinite loop). 2. However, when it bails out, it returns the partially expanded macro call (e.g., `\foo{x}`). 3. Since the macro definition was removed during the preprocessing step, the parser then sees `\foo` as an unknown command and converts it into an error comment, accidentally discarding the argument `{x}` in the process.
*The intended behavior* should definitely be to preserve the content (the `x`) even if the macro logic fails. I will fix the fallback logic to ensure it fails gracefully without eating the arguments.
Thanks again for the sharp eye! These kinds of checks are super helpful.