This answer is written mostly in the context of a TeX developer.
In my experience, different kinds of bugs requires very different strategy to debug.
Thus, not exactly what OP asks about; nevertheless I try to be comprehensive.
I guess there's a bit of overlap with https://tex.stackexchange.com/a/516657/250119.
The general approach for debugging is
Find out the closest place in the code you wrote that causes the bug.
Determine what is wrong with the code.
Usually the errors are low-level TeX errors caused by you using the command incorrectly.
Part 1. stacktrace
You must know how to determine where the error occur; in other words, get a "stack traceback".
TeX has mechanism for this purpose: What does \errorcontextlines do?.
Unfortunately, it has lots of limitations. See my answer there.
Not-directly-related note.
I'm working on something to improve the situation there.
Either
- some patch to the TeX engine to store where a token originally comes from
(which would be harder because the internal representation of a token in the engine level, and the level in TeX programming is very, very different),
or
- something that requires additional annotation to the code to mark explicit relax-points (or expandable-relax-points), plus something to print a traceback.
For now, while there's no such tool, my method (to determine where the error occur) consist of using \errorcontextlines
to guess as good as possible where the error happens,
then add debug-print statements at the (what I thought to be) relax-points to maximally localize the problematic code.
By relax-point I mean points such that any number of \relax
can be added at that point such that they will be executed (not expanded except possibly the first one),
roughly speaking, they're the statement separation points.
E.g.
⟨\relax point⟩
\int_compare:nNnT ⟨not \relax point⟩ {⟨not \relax point⟩ 12} < {34} {
⟨\relax point⟩
\iow_term:x {⟨not \relax point⟩ 123}
⟨\relax point⟩
}
⟨\relax point⟩
By "add debug-print statements" I mean simply add e.g. \pretty:n {reached here}
at the points you think to be relax points, and see which lines are printed.
\pretty:n {AAA}
\int_compare:nNnT {12} < {34} {
\pretty:n {BBB}
\iow_term:x {123}
\pretty:n {CCC}
}
\pretty:n {DDD}
if the error goes like
> AAA
> BBB
! some error message...
then the error happens between BBB
and CCC
.
If, in the process of adding debug print statements, the error disappears, chances are
- some command gobbles some following argument and it becomes the debug print statement instead of the following statement, or
- some command looks ahead for e.g. optional space, and it terminates without expanding the following command when the pretty-print statement is seen.
(for example, remember to add an explicit space
~
after a command that takes a number e.g. \catcode x=12
, or
a \relax
after a TeX raw \numexpr
etc. Using expl3's wrapper could avoid that, but for things like setting \catcode
or such TeX registers,
or for performance optimization, you'll have to stick with using raw TeX assignment.)
If they're replaced with \relax
the error still not happen, then you could probably fix the issue by adding \relax
there.
Although you'd also want to understand what went wrong at that point.
Just look carefully at the code for the points above.
After finding two consecutive relax-point such that you're sure the error happens between them, and the bug remains unfixed, continue with the following part.
Part 2. find the error
Make sure you read all of the TeXbook at least once.
The engine sometimes behave in counter-intuitive ways, and if you don't know that and "assume" how it behaves you'll be surprised quite a few times.
Example: Why is \unexpanded not respected after an \if? , Why does a newline character still disappear even with other/active catcode set? .
For comparison, Python has a few unexpected behavior as well https://stackoverflow.com/questions/3431676/creating-functions-in-a-loop
nevertheless they're way fewer than that of TeX.
It's true that you won't be able to remember all of them; or sometimes "forget" some while debugging the program, in that case the following methods should help.
Notation: In the following paragraphs I use \pretty:n {⟨token list⟩}
, \prettye:n {⟨token list⟩}
,
\pretty:V \⟨tl var⟩
, \prettyshow:N ⟨control sequence⟩
, etc. for a generic command
to print out the content of a token list/meaning of a variable.
They're taken from my prettytok
package; you can choose to use anything else such as
\show
, \showthe
, \typeout
, \showtokens
, \tl_show:n
, \msg_expandable_error:nn
etc. (as the package author I recommend using my package, see the package documentation for more details.)
It's highly not recommended to step inside the command implementation, either with unravel package, or by printing the X-step-expansion.
Instead just assume it's a black box.
Reason:
- the implementation tends to be very, very complex and usually not the direct cause of the issue
- more importantly, they tend to not have bugs and you want to debug your code, instead of their code.
Nevertheless, if the bug indeed is in the library instead of your code
(see some example bugs of expl3 in https://github.com/latex3/latex3/issues?q=is%3Aissue),
the instruction below should suffice to more-or-less create a MWE for reporting a bug to the library.
If you in addition want to directly find and fix the issue in the library, then
- do not debug by patching the command, that would be somewhat equivalent to debugging a package by looking at its compiled binary code.
Instead...
- download the source dtx file, then modify that to add debugging commands etc..
actually, modifying sty file is not that bad, in the common case of docstrip.
Only comment is removed and @@
get replaced with the package name.
The structure is more or less preserved.
Note that
- remember to make sure that the new sty/dtx file is actually used instead of the old one installed in your system.
For example by temporarily adding \error
to the local copy, compile the TeX file, then ensure that the error occurs.
Also, this is rare, but...
do not use the debugging tool to debug itself.
since, if you modify the base tool, it's not surprising that the debug printer might behave in unexpected ways.
For primitives like \immediate\write16
there is no issue, but otherwise be careful.
Print out the code that is going to be executed.
For example
\cs_new_protected:Npn \function #1 {
\pretty:n {\tl_something:Nnn \tlvar {#1} {...}} % ← add this.
\tl_something:Nnn \tlvar {#1} {...}
}
This may seem redundant, but it might actually helps:
- sometimes TeX tokenization rule might be different from what you expect. E.g.
^^⟨content⟩
will become a different token
- if you type
\pretty:n { \abc ~ }
, only \abc
will be printed out without the explicit space.
- besides, remember that TeX coding is not just "run this code with this variable value", it is self-modifying code.
If the command is "top-level" it tends to not produce any output,
insert \pretty:V \resultvariable
in the next relax-point to wherever it stores the result to.
Otherwise you could e.g. \pretty:x {\tl_something:nnn {...} {...} {...}}
to see the result.
Once you've determined exactly what is passed in the command
(and optionally what the command results in, if it doesn't error out),
read carefully the documentation of the command to see if it's supposed to do that, given that argument.
Chances are, you prepared the argument incorrectly, then the bug is in the code that prepares the argument.
You may also want to try calling that particular command, with that particular argument (*)
in a separate independent document (**)
to see if it errors out the same way.
If it doesn't, the issue is probably in some "global shared state" (e.g. value of escapechar, the definition of some particular macro etc.) so look into that.
It's rather unfortunate that there are many "global states" in TeX, although expl3 commands tend to not have this problem.
(*): if you have difficulty providing the argument because it has nonstandard catcode, I have the precattl
package
(**):
actually this is rather difficult if the argument depends on some other commands being defined. But it's the idea. Also then you may want to print out the value of the argument going to be passed to the argument to see if you typed it in correctly
Sometimes the documentation is not clear (refer to Where do I find out how a command/environment is defined? for where to find the documentation,
but some command's documentation is not that clear.
Experiment around with the command (by making a MWE) to make sure that you understand it correctly.
Most notably are e-TeX commands, where etex_man.pdf
is quite brief.
If you get unexpected behavior involving the following commands, make sure you fully understand what the answer(s) in the linked question explain.
Sometimes expl3 command documentation is (was) not very clear, for example, that \tl_replace_all:Nnn
only operates at top level.
)
Talk about \scantokens
, you should be able to implement an unexpandable \debugscantokens
command that does similar thing to
\scantokens
, but writes the content to a temporary file, so you can more easily inspect the content of the temporary file to see if it is correct.
Although this does not necessarily behave in the same way e.g. even if newlinechar is not 10, a charcode-10 token will still create a physical newline in the generated file, but only for physical file. You should not rely on this behavior regardless.
For debugging expandable commands, or debugging input stream manipulation commands, my package has \prettye:n
and \pretty[e]:w
respectively, although you might choose to reimplement something similar yourself.
\pretty:w
is destructive with respect to catcodes however, so that's a limitation. (somewhat like "can only be used once debug-printer")
Alternative way of debugging expandable commands consist of printing the X-step-expansion of the command.
One way is to use \exp_args:No \exp_args:NNo \pretty:o { ... }
as mentioned above (note that this is not exactly identical to multiple-step expansion, as o-type expansion will convert notexpanded token to normal token; and if some unbalanced brace is generated it will have trouble.
There is the multiexpand
package to simplify how this can be written. So \pretty:o {\romannumeral \multiexpand {5} ...}
, or \expandafter \prettye:w \romannumeral \multiexpand {5} ... \prettystop
for unbalanced-brace case.
For expl3 code, it would help to print out which input is actually passed to the command, if the argument specification is not the base form.
Unless the code uses l3flag's mechanism to keep a state (that is \csname ... \endcsname
defines the control sequence to be \relax
if it were previously undefined),
it will be idempotent. As such if you have
\exp_args:NV \tl_something:oxV \a {\b \c} \d
then you can write
\exp_args:NV \pretty:oxV \a {\b \c} \d
to determine what is actually going to be passed to \tl_something
function.
Comparing that to the "print the X-step-expansion" method mentioned above
this has the advantages
- you don't need to know exactly how many expansion steps the
\exp_args:N*
finishes
- some
\exp_args
are unexpandable (x-type expansion etc.), so you'd need something like \unravel
package;
but then this one, for now, need step-by-step/interact with the console
(to be fair unravel has u⟨text⟩
command that fast-forward to the bare command, but this might overshoot it on typo etc.)
If the issue is with "global state", the logical next step would be to print out the value of the global states right before the command is executed.
How can you know which global states are interesting? Most likely, those that you modify in your code. (you might forget to revert the value to a normal value)
An example of a "global state modification" bug. (my question)
Why do I get `! LaTeX Error: Missing \begin{document}. l.2 gdef @abspage@last{1}` starting from second compilation?
This one, I debugged it by...
first, figure out that the erroneous content happens in the aux file (e.g. with --file-line-error
)
be aware that normally the string to be written to the aux file has the form \gdef \@abspage@last {1}
. (reading LaTeX user manual won't help on this point. You can read source2e, or easier, experiment with compiling a simple working document)
in comparison to that, the \ are missing.
Analysis:
why is it so frustrating and requires knowing so many internal implementation details of (La)TeX?
In other languages e.g. Python/Lua, if you modify some commonly-used global variable built-in e.g. list
or tonumber
,
it's not surprising
if some mysterious error occur in an indeterminate number of lines later, possibly in some library.
(in this respect Lua is worse than Python, as its global namespace is truly global)
However, here, we don't have a facility/primitive to e.g. "get string representation of token list with escapechar=X",
instead we must modify the global variable \escapechar
to achieve the result.
Alternatively -- since escapechar is modified, it's possible to simply print out the value of escapechar at the end of the document and see it's -1, I think.
Another case. I accidentally set \let \FCDtabtomacro \⟨some typo that results in an undefined macro⟩
, then call filecontentsdefmacro
environment and gets "undefined control sequence" with unhelpful traceback.
The error context is not very useful here, but you could
- try printing out the "global state" (in this case it should include definition of
\FCDtabtomacro
), since you modified it before invoking filecontentsdefmacro
.
- read the log file instead of the terminal, and find some way to visibly show the tab character e.g.
:set list
in vim (rather difficult, unless you know exactly what the error is already...)
Conclusion: You can debug it the same way how you debug other programming languages.
Remark: As someone who knows how to use gdb to debug C++, but does not know how to use pdb to debug Python,
I'd say that fast compilation (or, interactive shell) & debug printing is a
not-too-bad substitute for a true debugger.
For instance, instead of printing out the value in the debugger, you can simply add a line \pretty:nV {variable=} \⟨the variable⟩
right before the line it errors out, then read the last value printed out in the output.
Fast compilation/execution is needed, because otherwise you would need to wait a long time before each debugging step,
which is frustrating in every language. (thus, as I mentioned above, if you don't have fast compilation, you need interactive shell or debugger)
Not that I'm saying a debugger is useless; in fact I already wrote one to do a few things that cannot be done otherwise
(currently the debugger can
- inspect the content of the "input stream", distinguishing between tokenized/untokenized content, while not touching the content itself -- this one is not done by
unravel
package or as far as I know any existing way without modifying the compiler, and
- do a one-step execution (standard functionality)
however it needs about 10x more functionality to be anywhere comparably as useful as pretty-print-debugging.)
and I'm pretty comfortable with using pretty-print-debugging
for now so it's not going to happen any time soon.
Miscellaneous notes
Some packages (expl3 for example) has debug mode, try reading the documentation and enable it (if any)
To me, it doesn't seem very useful, however.
Depends on the configuration, it might be tedious to repeatedly stop/start the compiler and/or search for the error message in the log file/verbose terminal output.
Refer to compiling - Reducing the console output of LaTeX - TeX - LaTeX Stack Exchange for ways to make the process easier.
Avoid linebreaks in LaTeX console / log output (or increase columns in terminal) might help a bit regarding viewing the error context, but only if the place you view the error context does not do line wrapping (for me, reading it in the log file is better than reading in the terminal, both because of no line wrapping and that trailing spaces are preserved -- nevertheless make sure that you read all the lines, as some might be indented a lot)
Sometimes, you have your package command producing spurious letters that is being typesetted.
Usually, this is caused by either...
debugging this one is rather difficult, but one way I find reasonably good is to put the whole code inside the preamble,
so LaTeX has the necessarily mechanism (everypar hook) to give an error when something is typeset, so you can quickly locate where the error is.
If everything fails, you can still do binary debugging.
Compare to a Python program with many lines, each line might or might not set the value of a global variable
to something undesired, that would be hard to track down. (with a debugger you can set a memory watchpoint on the variable, but
in TeX there's no debugger for now)
unravel
package
Step-by-step emulation of the TeX engine.
As mentioned, it does not support \directlua
, other XeTeX/LuaTeX-specific things (new font handling etc.),
and catcode-changing commands.
Reference: macros - A LaTeX log analyzer application (visualizing TeX expansion) - TeX - LaTeX Stack Exchange
(despite the title, the answer there announces the package unravel
. TeX log file is just too hard to parse, as all the catcode information is dropped
-- besides, if \escapechar=-1
, then the backslash in output is also dropped completely)
Personally I don't find this much more useful than printf-debugging yet.
Besides the TeX macro \tracingall
, there's also the package trace
which claims to make the output nicer.
See also: debugging - Latex \tracing commands list? - TeX - LaTeX Stack Exchange
Personally I don't have much experience using the trace to debug, except in some particular cases e.g. my answer that explains how to find the line number that runaway argument starts.
This one is from an user perspective.
No error, but weird output
This one is harder to track down than that if there's an actual error, as it's difficult to pinpoint where exactly in the code does the bug happen.
First, read the documentation carefully to be sure that what you want is supported by the package.
In almost all cases, the behavior is documented in the manual. (otherwise it would be a bug.)
How can I know where in the manual should I read?
- Make a minimal example. For example, if the code currently uses component A, B and C, and if you replace B
and C with dummy text the problem persists, then you only need to (re-)read the document of component A.
For an example.
How do you balance two frame boxes in the same page?
As you can tell... in this case the behavior is documented in the documentation, but it's not very practical to read through the whole LaTeX manual to find the behavior.
Another example.
Last time I use a \int_step_inline
inside a tabular environment, and it creates a spurious cell (see Pipe input for table content is working odd for an example.)
This was a difficult one to debug (I end up asking in TeX.SE chat), but I think it could be debugged by...
first, make a MWE. Should not be difficult, and you end up with only a tabular
environment and a \int_step_inline
.
replace the LaTeX command with the TeX primitive.
In particular, tabular
environment with halign
, and int_step_inline
with some loop you implement yourself (normally this would be easier than trying to debug the expl3 implementation, even though expl3 is fully documented)
If the bug disappear when you replace something, you know that the thing you just replaced either has a bug.
Anyway it's not supposed to be used that way, so a fix would be to do something that is guaranteed to work (in this case it would be build the full token list with int_step_inline
before running tabular environment)
Otherwise, it's because the LaTeX tool inherit some quirk from the TeX primitive --
which is the case here, where halign
fully expands the first argument until it sees something unexpandable, and if it's not egroup then it starts a new cell.
A case of package bug.
Square brackets in listings caption
(as far as I can see this is a package bug, as the unusual behavior with []
is not written in the manual)
In this case you should be able to quickly find out where the error is, make a MWE, without delving into the implementation details of the package.
In practice, you'd want to know how to fix the problem as well. (without having to contact the package author and wait for a response)
To come up with a fix is not very easy, but if you know a bit about how LaTeX is usually implemented, the [
is accidentally taken as the delimiter of an optional argument instead of some text to be typeset, you can prepend \relax
to disable that.
Sometimes your program is correct, but it's very slow. Another topic.
Remark:
My opinion is that debugging TeX is harder than debugging Python/C++, but not by much;
and, with better debugging tools, the same programming tasks will take the same effort to debug;
nevertheless, the tasks you do in TeX tends to be more complicated than that in Python (it's uncommon to do typesetting in typical programming languages).
For OP's example on \textsf
/ \enquote
, my opinion is that if Python has similar functionality the error would be similarly misleading
(as, the computer first collect all the text and which font they use; then apply microtype adjustment on it.
The error happens way after the text is first collected, thus the line number is misleading.) --
unless the author intentionally add code to record the stacktrace where the code is collected.
git bisect
...