Your best resource for TeX is the TeXBook, particularly chapter 20 ("Definitions (also called Macros)") and Appendix D ("Dirty Tricks"). Appendix B provides a list of almost all macros defined at startup.

The reference for plain TeX at tug.org is also a good resource.

TeX-autogolfer is a tool to help in golfing TeX. It's a combo of a simple minifier (remove whitespace, etc.) and a meta-macro system; for example you can write \usegolf{rebind\advance}, and it creates (effectively) \let\A\advance and replaces all of your uses of \advance with \A.

Basics

Differences from Plain TeX

The TeX language on the site is very similar to Knuth's plain TeX, with a few small modifications:

Spaces

Space characters can be annoying to manage. Important spacing rules:

Math operations

Math can be performed in counters. Define a counter with \newcount, and print the value with \the.

\newcount\x
\x=15  % equivalent to the following line
\x15   % (the equals sign is optional)
\the\x % prints 15 as two tokens, "1" then "5"

Counters are 32-bit signed integers. Operations that overflow cause the program to instantly halt with no output.

\x\y            % x = y
\advance\x5     % x += 5
\advance\x by\y % equivalent to the following line
\advance\x\y    % x += y (the "by" text is optional)
\advance\x-\y   % x += -y
\multiply\x\y   % x *= y
\divide\x\y     % x /= y (floor divide)

There is no modulo operator. Modulo can be calculated (if you must) using the definition x % y = x - x / y * y, where x/y is floor division

% d = x - x % y, x = x % y (requires an extra counter)
\d\x\divide\d\y\multiply\d\y\advance\x-\d

Or modulo can be computed by repeated subtraction, assuming x and y are positive.

% x = x % y (slow)
\loop\ifnum\x>\y\advance\x-\y\repeat

ASCII conversions

Convert a char code to corresponding character with \char: \char97 gives the token a.

Convert a character to char code with a backtick: \number`a gives the two tokens 97, and \newcount\x\x=`a assigns 97 as the value of the counter \x.

Looping over a string

The following code loops over all tokens until ;, replacing all - tokens with (DASH)

\def\f#1{\ifx#1;%
    \let\n\relax%
\else%
    \let\n\f%
    \ifx#1-(DASH)%
    \else #1%
    \fi%
\fi\n}

\f 0-185186-70-;

Improvement: an extra stop token

The following code loops over all tokens until ,;, replacing all - tokens with (DASH)

\def\f#1#2;{\ifx#1,\else
    \ifx#1-(DASH)%
    \else #1%
    \fi%
    \f#2;
\fi}

\f 0-185186-70-,;

Tweak: check for empty argument

The following code loops over all tokens until ;, replacing all - tokens with (DASH). It assumes there are no .s in the input.

\def\f#1#2;{%
    \ifx#1-(DASH)%
    \else #1%
    \fi%
\if.#2.\else%
    \f#2;%
\fi}
\f 0-185186-70-;

Looping over two strings

The following code prints "Equal!" if two input token lists are equal.

\def\streq#1#2;#3#4;{
  \if#1,
    \if#3,
      % strings are equal, done
      Equal!
    \else
      % left string is shorter than right string
    \fi
  \else
    \if#3,
      % right string is shorter than left string
    \else
      %#1 and #3 are both not ','
      \if#1#3
        \streq#2;#4;
      \fi
    \fi
  \fi
}

\streq ABC,;DEF,; % (empty output)

\streq ABC,;ABC,; % outputs "Equal!"

Golfing

One-byte macro ending

If a macro you define takes an argument longer than one token, you might be tempted to use curly braces. However, \def can allow for a single end character, saving one byte per usage.

\def\f#1{something #1 something}
\f{123}\f{456}
% compare
\def\f#1;{something #1 something}
\f123;\f456;

\let

Commonly-used macros can be aliased with \let

\newcount\a\newcount\b\newcount\c
% compare
\let\N\newcount\N\a\N\b\N\c

At the top of a long solution, you might see a block of \lets like \let\N\newcount\let\I\ifnum\let\A\advance

Tilde is active

Tilde (~) is an active character (\catcode`\~=\active), so you can use it in lieu of one control sequence.

\let\N\newcount\N\a\N\b\N\c
% compare
\let~\newcount~\a~\b~\c

Repeated macro vs loop

Repeating a macro several times is often shorter than a loop.

\newcount~\loop\ifnum10>~\the~,\advance~1\repeat
% compare (uses an extra variable)
\newcount\i\def~{\the\i\advance\i1,}~~~~~~~~~~

Newlines

The conventional way to make a new paragraph is the \par command. Two or more consecutive newlines are converted to a single \par and are the most common way to get a newline.

Form feed (ASCII character 12 = 0x0C) is equivalent to \par, but it has one restriction: It is \outer, so it is disallowed in many contexts, such as the replacement text of \def definitions (unless the definition is declared as \long\def) and inside \loop...\repeat constructs. Form feed can be typed in the code.golf editor by typing "0c" while holding down the Alt key.

A few other commands are loosely equivalent to \par and useful over a double-newline in niche situations. Try playing around with these to get a feel for when they're useful: \vfil, \eject, \char10, \endgraf. In particular, \eject forces a page break, so it flushes the output, so you can terminate by an error such as stack overflow.

Relevant lines from plain.tex
\catcode`\^^L=\active \outer\def^^L{\par} % ascii form-feed is "\outer\par"
\let\endgraf=\par
\def\break{\penalty-\@M}
\def\eject{\par\break}

\vfil and \char are primitive commands.

The following prints 0 through 9 on their own lines by two approaches which are the same length. The two approaches are a recursive approach using double-newline and a \loop...\repeat approach using \vfil.

\newcount\i\loop\ifnum10>\i\the\i\vfil\advance\i1\repeat
% compare
\newcount\i\def\f{\ifnum10>\i\the\i\advance\i1

\f\fi}\f

Counters

TeX has a fixed set of 256 counters, accessible in a few different ways. Usually you would use newcount to be able to reference a previously unused by a name:

\newcount\i   \the\i  % 0, newcounts are initially 0
\advance\i 3  \the\i  % 3

If you just need one or two counters, you can instead make use of predefined counters:

\day3 \the\day \the\fam % 3 0

If you use lots of counters, alias \count to an active character and address counters by their index (0-255):

\let~\count
\the~0               % 1, if you use a counter that is already in use by TeX, it might have a non-zero value
\advance~0 3 \the~0  % 4

% this way also allows using counters as small arrays:
\the~~0  % index in \count0

Reference tables

Count registers

Table
Register number Initial value Incrementer More
0 1 \eject Page numbering.
1 to 9 0 Page number.
10 25 \newcount Count allocation.
11 15 \newdimen Dimen allocation.
12 17 \newskip Skip allocation.
13 9 \newmuskip Muskip allocation.
14 15 \newbox Box allocation.
15 11 \newtoks Toks allocation.
16 -1 \newread Read file allocation.
17 -1 \newwrite Write file allocation.
18 7 \newfam Math family allocation.
19 0 \newlanguage Language allocation.
20 253 \newinsert (-1) Insert allocation. (count, dimen, skip are related)
21 26 The most recently allocated number.
22 -1 \countdef\m@ne=22 \m@ne=-1 % a handy constant
23 100 Is \interdisplaylinepenalty
24 100 Is \interfootnotelinepenalty
25 to 252 0 Intended useful registers.
253 1000 Is \count\topins (insert)
254 1000 Is \count\footins (insert)
255 92 Idk why 92. Used as scratch? \count@
Methodology
\count0 0
\loop\ifnum\count0<256
 |\ \the\count0\ |\ \the\count\count0\ |%
 \vfil
 \advance\count0 1
\repeat

Lengths

Table
Unit pt + sp sp pt
1in 72pt + 17694sp 4736286sp 72.27pt
1cm 28pt + 29671sp 1864679sp 28.45pt
1cc 12pt + 55057sp 841489sp 12.84pt
1pc 12pt + 0sp 786432sp 12pt
1em 10pt + 0sp 655360sp 10pt
1ex 4pt + 20024sp 282168sp 4.305pt
1mm 2pt + 55395sp 186467sp 2.845pt
1dd 1pt + 4588sp 70124sp 1.070pt
1bp 1pt + 245sp 65781sp 1.004pt
1pt 1pt + 0sp 65536sp 1pt
1sp 0pt + 1sp 1sp 0.000015pt

Note the ex and em dimensions are font-specific, but code.golf only uses the one font \octet.

Methodology
\newdimen\x%
\x1in %
\newcount\pt%
\newcount\sp%
\def\div#1,#2;{%
  \def\iterate{
    \ifdim#1>\x\else%
      \advance\x-#1%
      \advance#2 1%
      \expandafter%
      \iterate%
    \fi%
  }%
  \iterate%
}%
\div 1pt,\pt;
\div 1sp,\sp;
(\the\pt pt + \the\sp sp)

Primitive commands with \the usable

Table
\command \the\command
\lineskip 1.0pt
\baselineskip 12.0pt
\parskip 0.0pt plus 1.0pt
\abovedisplayskip 12.0pt plus 3.0pt minus 9.0pt
\belowdisplayskip 12.0pt plus 3.0pt minus 9.0pt
\abovedisplayshortskip 0.0pt plus 3.0pt
\belowdisplayshortskip 7.0pt plus 3.0pt minus 4.0pt
\leftskip 0.0pt
\rightskip 0.0pt
\topskip 10.0pt
\splittopskip 10.0pt
\tabskip 0.0pt
\spaceskip 0.0pt
\xspaceskip 0.0pt
\parfillskip 0.0pt plus 1.0fil
\thinmuskip 3.0mu
\medmuskip 4.0mu plus 2.0mu minus 4.0mu
\thickmuskip 5.0mu plus 5.0mu
\output
\everypar
\everymath
\everydisplay
\everyhbox
\everyvbox
\everyjob
\everycr
\errhelp
\pretolerance 100
\tolerance 200
\linepenalty 10
\hyphenpenalty 50
\exhyphenpenalty 50
\clubpenalty 150
\widowpenalty 150
\displaywidowpenalty 50
\brokenpenalty 100
\binoppenalty 700
\relpenalty 500
\predisplaypenalty 10000
\postdisplaypenalty 0
\interlinepenalty 0
\doublehyphendemerits 10000
\finalhyphendemerits 5000
\adjdemerits 10000
\mag 1000
\delimiterfactor 901
\looseness 0
\time 1349
\day 7
\month 7
\year 2024
\showboxbreadth 5
\showboxdepth 3
\hbadness 1000
\vbadness 1000
\pausing 0
\tracingonline 0
\tracingmacros 0
\tracingstats 0
\tracingparagraphs 0
\tracingpages 0
\tracingoutput 0
\tracinglostchars 1
\tracingcommands 0
\tracingrestores 0
\uchyph 1
\outputpenalty 0
\maxdeadcycles 25
\hangafter 1
\floatingpenalty 0
\globaldefs 0
\fam 0
\escapechar 92
\defaulthyphenchar 45
\defaultskewchar -1
\endlinechar 13
\newlinechar -1
\language 0
\lefthyphenmin 2
\righthyphenmin 3
\holdinginserts 0
\errorcontextlines 5
\parindent 0.0pt
\mathsurround 0.0pt
\lineskiplimit 0.0pt
\hsize 16000.0pt
\vsize 16000.0pt
\maxdepth 4.0pt
\splitmaxdepth 16383.99998pt
\boxmaxdepth 16383.99998pt
\hfuzz 0.1pt
\vfuzz 0.1pt
\delimitershortfall 5.0pt
\nulldelimiterspace 1.2pt
\scriptspace 0.5pt
\predisplaysize 0.0pt
\displaywidth 0.0pt
\displayindent 0.0pt
\overfullrule 5.0pt
\hangindent 0.0pt
\hoffset 0.0pt
\voffset 0.0pt
\emergencystretch 0.0pt
\font
\parshape 0
\prevgraf 0
\toks0
\count0 2
\dimen0 0.0pt
\skip0 0.0pt
\muskip0 0.0mu
\spacefactor 1000
\deadcycles 0
\insertpenalties 0
\lastpenalty 0
\lastkern 0.0pt
\lastskip 3.33333pt
\inputlineno 173
\badness 0
\pagegoal 16000.0pt
\pagetotal 1438.0pt
\pagestretch 121.0pt
\pagefilstretch 122.0pt
\pagefillstretch 0.0pt
\pagefilllstretch 0.0pt
\pageshrink 0.0pt
\pagedepth 0.0pt
Methodology

Grep through tex.web for calls like primitive("lineskip"). Then run \f\lineskip where \def\f#1{| `\string#1` | \the#1 |\vfil}. If it doesn't give an error, then add the output to the table. If it says missing number, then add a 0 and try again.