From e9c475553e5e84dfd1ab9b2aff02d11a79a63069 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 17 Aug 2026 01:51:30 +0200 Subject: [PATCH] tex: the primitives and rules real package code is built on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running the real pgf/TikZ sources turned up seven places where the engine answered differently from TeX. Each was isolated to a few lines, checked against a real TeX (tectonic) before being implemented, and is a rule that package code leans on constantly — the failures they caused looked like anything but their cause. \ifx compares meanings, and a character token has one: its category and its character, the same meaning a control sequence \let to that character has. So \ifx\next/ is true when \next was \let to a slash. Every one-token lookahead tests what it peeked at this way; without it pgfkeys mis-split every key path it was given and then ran the key's value as if it were a command. TeX's alphabetic constant — `, or ` — was missing entirely, so \catcode`\@=11 quietly set the category of character 0 and left @ alone. Reading a category was missing too, so the files that put a category back (\edef\saved{\the\catcode`\@} … \catcode`\@= \saved) restored garbage. \afterassignment holds one token until the next assignment has been carried out, which is how a scanner resumes itself after \let has swallowed a token. An internal dimension coerces to an integer, its value in scaled points, so \number\pgf@x and \ifnum\wd0>0 read it. A box handle from \newbox is a register *number* (TeX allocates it with \chardef), so \box\mybox reaches the right register instead of box 0, where a package's boxed material used to vanish. A global assignment must outlive every open group, so a local assignment made earlier in one of them is no longer restored over it at the closing brace. The idiom that carries a computed value out of a group — {…\global\pgf@x=\pgf@x} — depended on it entirely. Alongside those: a file name is expanded before it is looked up (a package names the file to load through a macro), and the engine's named colours are published in the form a colour-reading package expects, so a drawing package can ask which model and values a colour has rather than reporting the model as unsupported. With these, the whole pgf stack — pgfrcs, pgfkeys, pgfsys, pgfmath, pgfcore and pgf itself — loads from its own sources under \documentclass{article}, and a picture draws: \pgfpathmoveto…\pgfusepath{stroke} now reaches the page as a real stroked path at the right coordinates. Co-Authored-By: Claude Opus 5 (1M context) --- color.go | 2 + colorbridge.go | 68 +++++++++++ engine.go | 138 +++++++++++++++++++-- packages.go | 30 ++++- primitives.go | 44 +++++-- texfaithful_test.go | 291 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 552 insertions(+), 21 deletions(-) create mode 100644 colorbridge.go create mode 100644 texfaithful_test.go diff --git a/color.go b/color.go index 94434af..b56f039 100644 --- a/color.go +++ b/color.go @@ -131,6 +131,7 @@ func (e *Engine) doDefineColor() { e.colors = map[string]uint32{} } e.colors[name] = parseColorSpec(model, spec) + e.publishColor(name, e.colors[name]) } // parseColorSpec turns a color model + spec string into 0xRRGGBB. @@ -188,6 +189,7 @@ func (e *Engine) doColorlet() { e.colors = map[string]uint32{} } e.colors[name] = c + e.publishColor(name, c) } // doPagecolor implements \pagecolor{expr}: fill the page background with the colour diff --git a/colorbridge.go b/colorbridge.go new file mode 100644 index 0000000..52cced6 --- /dev/null +++ b/colorbridge.go @@ -0,0 +1,68 @@ +// Copyright (c) the go-tex/engine authors. +// SPDX-License-Identifier: BSD-3-Clause + +package engine + +import ( + "fmt" + "strings" +) + +// This file publishes the engine's named colours in the form the color/xcolor +// packages store theirs, so a package that asks "what colour is this, exactly?" +// gets an answer. +// +// The engine resolves colour natively (see color.go) rather than loading xcolor, +// which is fine as long as nothing looks *inside* a colour. A drawing package +// does: pgf sets a stroke colour with \colorlet and then reads the result back — +// +// \expandafter\let\expandafter\pgf@temp\csname\string\color@pgfstrokecolor\endcsname +// \expandafter\pgf@setstrokecolor\pgf@temp % → #1#2#3#4#5, #4 = model, #5 = values +// +// — in order to hand the model and its values to its own driver. Without the +// stored form it reads whatever happens to follow and reports the colour model as +// unsupported, so no picture can set a colour at all. +// +// The stored form is xcolor's: the control sequence \color@ expands to five +// arguments, of which the fourth is the model and the fifth its comma-separated +// values. The engine keeps colours as RGB, so the model published is rgb with +// three 0–1 components — the model every driver understands. + +// publishColor stores a colour under the name a colour-reading package expects, +// alongside the engine's own table. It is called wherever a colour gets a name. +func (e *Engine) publishColor(name string, c uint32) { + if name == "" { + return + } + // \string\color@ is the control sequence's *characters*, which is the + // name xcolor builds with \csname: a literal backslash, then "color@". + e.define(`\color@`+name, &meaning{kind: mMacro, body: xcolorValue(c)}, true) +} + +// xcolorValue renders 0xRRGGBB as xcolor's stored five arguments. +func xcolorValue(c uint32) []tok { + r := float64((c>>16)&0xff) / 255 + g := float64((c>>8)&0xff) / 255 + b := float64(c&0xff) / 255 + spec := fmt.Sprintf("%s,%s,%s", colorComponent(r), colorComponent(g), colorComponent(b)) + // The value is a token list, not text: the reader destructures it into five + // arguments, so the groups have to be real begin/end-group tokens and the + // leading \xcolor@ a real control sequence. + out := []tok{csTok("xcolor@")} + for _, group := range []string{"", "", "rgb", spec} { + out = append(out, chTok('{', catBegin)) + for _, r := range group { + out = append(out, chTok(r, catOther)) + } + out = append(out, chTok('}', catEnd)) + } + return out +} + +// colorComponent formats one 0–1 colour component the way a colour specification +// spells it: at most five decimals, with trailing zeros trimmed. +func colorComponent(v float64) string { + s := fmt.Sprintf("%.5f", v) + s = strings.TrimRight(s, "0") + return strings.TrimSuffix(s, ".") +} diff --git a/engine.go b/engine.go index 3242f1c..38be40a 100644 --- a/engine.go +++ b/engine.go @@ -236,10 +236,11 @@ type Engine struct { // (Run resets it to 0; class/package loading splices file bodies into e.base at // e.bpos and scanning advances through them), which makes it a sound progress // signal even while a heavy .cls is loading. - expandDepth int // >0 while an isolated expansion (\edef/\message) is running - progBpos int // e.bpos at the last observed forward progress - noProgSteps int // expansion steps since e.bpos last advanced - tightLimit int // no-progress ceiling (New sets tightLoopSteps; tests may adjust) + afterToken *tok // token saved by \afterassignment, inserted after the next one + expandDepth int // >0 while an isolated expansion (\edef/\message) is running + progBpos int // e.bpos at the last observed forward progress + noProgSteps int // expansion steps since e.bpos last advanced + tightLimit int // no-progress ceiling (New sets tightLoopSteps; tests may adjust) } const ( @@ -515,14 +516,39 @@ func (e *Engine) meaningOf(t tok) *meaning { } func (e *Engine) define(name string, m *meaning, global bool) { - if !global && len(e.groups) > 0 { + if global { + e.forgetSaved(0, 0, name) + } else if len(e.groups) > 0 { e.save = append(e.save, saveItem{kind: 0, name: name, old: e.eq[name]}) } e.eq[name] = m } +// forgetSaved drops the pending restores for one quantity, which is what makes an +// assignment *global*: the value must outlive every group that is open, so a +// local assignment made earlier in one of them must no longer be restored over +// it at the closing brace. Without this, {\x=1 \global\x=2 } would leave \x at +// its value from before the group — and the idiom that carries a result out of a +// group (pgf's \pgf@process does exactly {…\global\pgf@x=\pgf@x}) would return +// nothing. +func (e *Engine) forgetSaved(kind, idx int, name string) { + if len(e.save) == 0 { + return + } + out := e.save[:0] + for _, it := range e.save { + if it.kind == kind && ((kind == 0 && it.name == name) || (kind != 0 && it.idx == idx)) { + continue + } + out = append(out, it) + } + e.save = out +} + func (e *Engine) setCount(i, v int, global bool) { - if !global && len(e.groups) > 0 { + if global { + e.forgetSaved(1, i, "") + } else if len(e.groups) > 0 { e.save = append(e.save, saveItem{kind: 1, idx: i, oldi: e.count[i]}) } e.count[i] = v @@ -532,7 +558,9 @@ func (e *Engine) setDimen(i, v int, global bool) { if i < 0 || i >= 256 { return } - if !global && len(e.groups) > 0 { + if global { + e.forgetSaved(3, i, "") + } else if len(e.groups) > 0 { e.save = append(e.save, saveItem{kind: 3, idx: i, oldd: e.dimen[i]}) } e.dimen[i] = v @@ -542,7 +570,9 @@ func (e *Engine) setSkip(i int, v glueSpec, global bool) { if i < 0 || i >= 256 { return } - if !global && len(e.groups) > 0 { + if global { + e.forgetSaved(4, i, "") + } else if len(e.groups) > 0 { e.save = append(e.save, saveItem{kind: 4, idx: i, oldg: e.skip[i]}) } e.skip[i] = v @@ -1042,6 +1072,11 @@ func (e *Engine) execCS(t tok) bool { e.fail("Undefined control sequence \\" + t.cs) return false } + if m.kind == mPrim && m.name == "afterassignment" { + m.prim(e) + return true + } + defer e.flushAfterAssignment(m) switch m.kind { case mCountRef: e.countRefAssign(m.code, false) // \n= @@ -1169,12 +1204,30 @@ func (e *Engine) scanInt() int { if m.kind == mCharDef { return sign * m.code } + // A box-register handle from \newbox is a register *number*: TeX + // allocates it with \chardef, so \box\mybox, \wd\mybox and + // \setbox\mybox all read it as the integer it stands for. + if m.kind == mBoxRef { + return sign * m.code + } if m.kind == mCountRef { return sign * e.count[m.code] } if m.kind == mPrim && m.name == "count" { return sign * e.count[e.scanInt()] } + // TeX coerces an internal dimension (or glue) to an integer: its + // value in scaled points. \number\pgf@x and \ifnum\wd0>0 both + // rely on it, and a package that computes with lengths uses it + // constantly. + if e.isInternalDimen(t) && m.name != "dimexpr" { + e.back(t) + v, _ := e.scanDimenValue(false) + return sign * v + } + if m.kind == mPrim && m.name == "catcode" { + return sign * int(e.catcode[rune(e.scanInt())]) + } if m.kind == mPrim && m.name == "numexpr" { return sign * e.scanExpr(false) } @@ -1185,6 +1238,13 @@ func (e *Engine) scanInt() int { } } } + // TeX's alphabetic constant: ` or ` is that character's code. It is how a source names a character + // it cannot write as a number — \catcode`\%=14, \lccode`\a=`\A, + // \chardef\bslash=`\\ — so a package that sets any catcode needs it. + if t.is('`', catOther) { + return sign * e.scanCharCode() + } if !t.cs_ && t.ch >= '0' && t.ch <= '9' { n := int(t.ch - '0') for { @@ -1205,6 +1265,68 @@ func (e *Engine) scanInt() int { } } +// assignmentPrims are the primitives that perform an assignment, after which a +// token saved by \afterassignment is inserted (TeX §1269). A register alias +// (\pgf@x=…) assigns too and is handled by kind, not by name. +var assignmentPrims = map[string]bool{ + "def": true, "gdef": true, "edef": true, "xdef": true, "let": true, + "futurelet": true, "global": true, "chardef": true, "countdef": true, + "dimendef": true, "skipdef": true, "toksdef": true, "newcount": true, + "newdimen": true, "newskip": true, "newtoks": true, "newbox": true, + "count": true, "dimen": true, "skip": true, "toks": true, "catcode": true, + "advance": true, "multiply": true, "divide": true, "setbox": true, + "font": true, "hsize": true, "vsize": true, "parindent": true, + "baselineskip": true, "leftskip": true, "rightskip": true, "sfcode": true, + "hskip": true, "vskip": true, "wd": true, "ht": true, "dp": true, + "columnsep": true, "columnseprule": true, +} + +// flushAfterAssignment inserts the token \afterassignment saved, once the +// assignment it was waiting for has been performed. TeX keeps exactly one such +// token, and it is inserted after the assignment, not before — which is what +// lets a macro see the value that was just assigned (pgf uses it to resume a +// scanner right after \let\next= has swallowed a token). +func (e *Engine) flushAfterAssignment(m *meaning) { + if e.afterToken == nil { + return + } + switch m.kind { + case mCountRef, mDimenRef, mSkipRef, mToksRef, mFont: + case mPrim: + if !assignmentPrims[m.name] { + return + } + default: + return + } + t := *e.afterToken + e.afterToken = nil + e.back(t) +} + +// scanCharCode reads the character after a ` : a character token gives its own +// code, and a control sequence gives the code of its single character (TeX reads +// this token unexpanded, so `\a is the letter a even when \a is a macro). A +// multi-letter control sequence is not a character constant and yields zero. +func (e *Engine) scanCharCode() int { + t, ok := e.getNext() + if !ok { + return 0 + } + code := 0 + if t.cs_ { + r := []rune(t.cs) + if len(r) != 1 { + return 0 + } + code = int(r[0]) + } else { + code = int(t.ch) + } + e.skipOneOptSpace() + return code +} + // unitRatio maps a physical unit keyword to TeX's exact (num, denom) ratio to // points (§458 set_conversion). pt/sp are handled specially in scanDimen. var unitRatio = map[string][2]int{ diff --git a/packages.go b/packages.go index f9cb4c6..b1e3d02 100644 --- a/packages.go +++ b/packages.go @@ -475,11 +475,39 @@ func (e *Engine) doIfFileExists() { } } +// readBraceNameX reads a braced file name, expanding it as TeX's file-name +// scanner does: a package names the file to load through a macro (pgf loads its +// driver as \pgfutil@InputIfFileExists{\pgfsysdriver}), so an unexpanded name +// would never resolve. +func (e *Engine) readBraceNameX() string { + e.skipOptSpace() + t, ok := e.getXToken() + if !ok || t.cs_ || t.cat != catBegin { + if ok { + e.back(t) + } + return "" + } + var b []rune + for { + u, ok := e.getXToken() + if !ok || (!u.cs_ && u.cat == catEnd) { + break + } + if u.cs_ { + b = append(b, []rune(u.cs)...) + continue + } + b = append(b, u.ch) + } + return strings.TrimSpace(string(b)) +} + // doInputIfFileExists implements \InputIfFileExists{name}{then}{else}: when name // resolves it splices the file (with the then-code queued to run after it), else it // runs the else-code. func (e *Engine) doInputIfFileExists() { - name := e.readBraceName() + name := e.readBraceNameX() then := e.readBraceToksRaw() els := e.readBraceToksRaw() data, _, ok := e.findTeXFile(name, []string{"", ".tex"}) diff --git a/primitives.go b/primitives.go index 467e467..dc8323b 100644 --- a/primitives.go +++ b/primitives.go @@ -73,6 +73,13 @@ func (e *Engine) loadPrimitives() { e.prim("xdef", func(e *Engine) { e.doDef(true, true) }) e.prim("let", func(e *Engine) { e.doLet(false) }) e.prim("futurelet", func(e *Engine) { e.doFuturelet(false) }) + // \afterassignment saves one token to be inserted once the next assignment + // has been carried out (see flushAfterAssignment). + e.prim("afterassignment", func(e *Engine) { + if t, ok := e.getNext(); ok { + e.afterToken = &t + } + }) e.prim("global", func(e *Engine) { e.doGlobal() }) e.prim("chardef", func(e *Engine) { e.doChardef(false) }) e.prim("countdef", func(e *Engine) { e.doCountdef() }) @@ -698,6 +705,12 @@ func (e *Engine) doThe() { case m.kind == mPrim && m.name == "dimexpr": e.pushString(formatPt(e.scanExpr(true))) return + case m.kind == mPrim && m.name == "catcode": + // \the\catcode`\@ — a file that changes a character's category + // saves the old value this way and restores it when it is done, so + // reading a catcode matters as much as setting one. + e.pushString(strconv.Itoa(int(e.catcode[rune(e.scanInt())]))) + return case m.kind == mPrim && m.name == "count": e.pushString(strconv.Itoa(e.count[e.scanInt()])) return @@ -817,23 +830,30 @@ func (e *Engine) evalIfx() bool { // ifxEqual reports whether two tokens are \ifx-equal: same character+catcode for // plain characters, or equal meanings for control sequences / active chars. func (e *Engine) ifxEqual(a, b tok) bool { - ma, mb := e.meaningOf(a), e.meaningOf(b) - if ma == nil && mb == nil { - // \ifx compares *meanings*, and every undefined control sequence has the - // same one — "undefined" — so two of them are equal whatever they are - // called. That is what makes \ifx\foo\undefined the standard way to ask - // whether \foo exists, an idiom nearly every package is built on. - if a.cs_ && b.cs_ { - return true - } - return tokEq(a, b) - } + ma, mb := e.ifxMeaning(a), e.ifxMeaning(b) if ma == nil || mb == nil { - return false + // Only an undefined control sequence has no meaning at all, and they all + // share the same one — "undefined" — so two of them are equal whatever + // they are called. That is what makes \ifx\foo\undefined the standard way + // to ask whether \foo exists, an idiom nearly every package is built on. + return ma == nil && mb == nil } return meaningEq(ma, mb) } +// ifxMeaning is what \ifx compares: the meaning of a token. A character token +// carries one too — its category and character — which is the same meaning a +// control sequence \let to that character has. So \ifx\next/ is true when \next +// was \let to a slash, the way every one-token-lookahead scanner tests what it +// peeked at (LaTeX's \@ifnextchar family, pgfkeys' path splitting, …). nil means +// undefined, which only a control sequence can be. +func (e *Engine) ifxMeaning(t tok) *meaning { + if !t.cs_ { + return &meaning{kind: mLetChar, ch: t.ch, cat: t.cat} + } + return e.meaningOf(t) +} + func (e *Engine) evalIf() bool { a, _ := e.getXToken() b, _ := e.getXToken() diff --git a/texfaithful_test.go b/texfaithful_test.go new file mode 100644 index 0000000..92b3773 --- /dev/null +++ b/texfaithful_test.go @@ -0,0 +1,291 @@ +package engine + +import ( + "os" + "path/filepath" + "testing" +) + +// The behaviours checked here were each measured against a real TeX (tectonic) +// before being implemented; the expected values are that engine's, not a guess. +// They are the primitives and rules a package written for TeX takes for granted, +// and every one of them was found by running the real pgf/TikZ sources. + +// \ifx compares meanings, and a character token has one: its category and its +// character — the same meaning a control sequence \let to that character has. So +// \ifx\next/ is true when \next was \let to a slash. Every one-token-lookahead +// scanner tests what it peeked at this way (LaTeX's \@ifnextchar family, +// pgfkeys' path splitting, which silently mis-parsed every key without it). +func TestIfxCharacterAgainstLetCharacter(t *testing.T) { + cases := []struct{ src, want string }{ + {`\def\key{/a/b}\def\c{\message{\ifx\p/Y\else N\fi}}\expandafter\futurelet\expandafter\p\expandafter\c\key\relax`, "Y"}, + {`\let\p=/\message{\ifx\p/Y\else N\fi}`, "Y"}, + {`\let\p=x\message{\ifx\p/Y\else N\fi}`, "N"}, + {`\let\p=/\message{\ifx/\p Y\else N\fi}`, "Y"}, // either way round: \p is the slash + {`\message{\ifx//Y\else N\fi}`, "Y"}, // two characters + {`\message{\ifx/xY\else N\fi}`, "N"}, // + {`\def\p{/}\message{\ifx\p/Y\else N\fi}`, "N"}, // a macro is not a character + {`\message{\ifx\undef/Y\else N\fi}`, "N"}, // undefined is not a character + } + for _, c := range cases { + if got := runExpr(t, c.src); got != c.want { + t.Errorf("%s = %q, want %q", c.src, got, c.want) + } + } +} + +// TeX's alphabetic constant: ` or ` is that character's code. Without it a source cannot name the +// characters whose categories it changes — \catcode`\%=14, \lccode`\a=`\A — and +// every such assignment silently addressed character 0 instead. +func TestAlphabeticConstant(t *testing.T) { + cases := []struct{ src, want string }{ + {"\\message{\\number`a}", "97"}, + {"\\message{\\number`\\a}", "97"}, + {"\\message{\\number`A}", "65"}, + {"\\message{\\number`\\\\}", "92"}, // a backslash, as a control symbol + {"\\message{\\number`\\ }", "32"}, // a space + {"\\message{\\number`0}", "48"}, + {"\\count0=`\\A \\message{\\the\\count0}", "65"}, + {"\\message{\\number`\\relax}", "0"}, // not a single character: no constant + } + for _, c := range cases { + if got := runExpr(t, c.src); got != c.want { + t.Errorf("%s = %q, want %q", c.src, got, c.want) + } + } +} + +// A category code can be read as well as set, which is how a file that changes +// one puts it back: \edef\saved{\the\catcode`\@} … \catcode`\@=\saved. pgf's own +// files do exactly this, and restoring from an unreadable value left @ as an +// escape character, breaking every \pgf@… name that followed. +func TestCatcodeIsReadable(t *testing.T) { + got := runExpr(t, "\\catcode`\\@=11 \\edef\\saved{\\the\\catcode`\\@}"+ + "\\catcode`\\@=12 \\message{[\\the\\catcode`\\@]}"+ + "\\catcode`\\@=\\saved \\message{[\\the\\catcode`\\@]}") + if got != "[12] [11]" { + t.Errorf("catcode round trip = %q, want [12] [11]", got) + } + if got := runExpr(t, "\\message{[\\the\\catcode`\\A][\\the\\catcode`\\ ][\\the\\catcode`\\{]}"); got != "[11][10][1]" { + t.Errorf("initial catcodes = %q, want [11][10][1]", got) + } + // It is an internal integer, so it can be compared and computed with. + if got := runExpr(t, "\\message{\\ifnum\\catcode`\\A=11 lettre\\else autre\\fi}"); got != "lettre" { + t.Errorf("\\ifnum over a catcode = %q", got) + } +} + +// \afterassignment saves one token to be inserted once the next assignment has +// been carried out — after it, so the macro sees the value that was just +// assigned. A scanner resumes itself this way (pgf: \afterassignment\resume\let\t=). +func TestAfterassignment(t *testing.T) { + got := runExpr(t, `\def\after{\message{[apres:\the\count0]}}`+ + `\afterassignment\after\count0=42 \message{[fin]}`) + if got != "[apres:42] [fin]" { + t.Errorf("= %q, want [apres:42] [fin]", got) + } + // It waits for an assignment, not for the next token. + got = runExpr(t, `\def\after{\message{[apres]}}\afterassignment\after\relax\count0=1 \message{[fin]}`) + if got != "[apres] [fin]" { + t.Errorf("= %q, want the token after the assignment", got) + } + // Only one token is held: a second \afterassignment replaces the first. + got = runExpr(t, `\def\a{\message{[a]}}\def\b{\message{[b]}}`+ + `\afterassignment\a\afterassignment\b\count0=1 \message{[fin]}`) + if got != "[b] [fin]" { + t.Errorf("= %q, want only the last token", got) + } + // A \def is an assignment too. + got = runExpr(t, `\def\after{\message{[apres]}}\afterassignment\after\def\x{}\message{[fin]}`) + if got != "[apres] [fin]" { + t.Errorf("= %q", got) + } + if _, err := New().Run(`\afterassignment`); err != nil { // truncated input + t.Fatal(err) + } +} + +// An internal dimension coerces to an integer — its value in scaled points — +// wherever a number is wanted. A package that computes with lengths relies on it +// on every value (pgf converts each coordinate with \number\pgf@x). +func TestDimenCoercesToInteger(t *testing.T) { + cases := []struct{ src, want string }{ + {`\newdimen\Z\Z=1pt\message{\number\Z}`, "65536"}, + {`\newdimen\Z\Z=1pt\message{\the\numexpr\Z*2\relax}`, "131072"}, + {`\hsize=1pt\message{\number\hsize}`, "65536"}, + {`\newdimen\Z\Z=2pt\count0=\Z\message{\the\count0}`, "131072"}, + {`\newdimen\Z\Z=1pt\message{\ifnum\Z>0 positif\else nul\fi}`, "positif"}, + {`\newskip\S\S=1pt plus 2pt\message{\number\S}`, "65536"}, // glue → its width + } + for _, c := range cases { + if got := runExpr(t, c.src); got != c.want { + t.Errorf("%s = %q, want %q", c.src, got, c.want) + } + } +} + +// A box register allocated by \newbox is a register *number* (TeX allocates it +// with \chardef), so every box primitive reads the handle as the integer it +// stands for. Without it a package's own boxes were all box 0, and the material +// put in them vanished. +func TestNewboxHandleIsARegisterNumber(t *testing.T) { + e := New() + if _, err := e.Run(`\newbox\mybox\setbox\mybox=\hbox{\kern5pt}` + + `\setbox1=\hbox{\copy\mybox\kern1pt}\message{[\the\wd\mybox][\the\wd1]}`); err != nil { + t.Fatal(err) + } + if got := trimNL(e.out.String()); got != "[5.0pt][6.0pt]" { + t.Errorf("= %q, want [5.0pt][6.0pt]", got) + } + // \box empties the register, as it does for a numbered one. + got := runExpr(t, `\newbox\b\setbox\b=\hbox{\kern5pt}\setbox1=\hbox{\box\b}`+ + `\message{[\the\wd1][\the\wd\b]}`) + if got != "[5.0pt][0.0pt]" { + t.Errorf("= %q, want [5.0pt][0.0pt]", got) + } +} + +// A global assignment must outlive every group that is open, so a local +// assignment made earlier in one of them is no longer restored over it at the +// closing brace. The idiom that carries a computed result out of a group — pgf's +// \pgf@process is exactly {…\global\pgf@x=\pgf@x} — depends on it entirely. +func TestGlobalSurvivesAnEarlierLocalAssignment(t *testing.T) { + cases := []struct{ src, want string }{ + {`\newdimen\Z{\Z=10pt\global\Z=\Z}\message{\the\Z}`, "10.0pt"}, + {`\newdimen\Z{\Z=10pt}\message{\the\Z}`, "0.0pt"}, // still local without \global + {`\newcount\N{\N=1 \global\N=7 }\message{\the\N}`, "7"}, + {`\newskip\S{\S=1pt\global\S=3pt}\message{\the\S}`, "3.0pt"}, + {`{\def\x{a}\gdef\x{b}}\message{\x}`, "b"}, + {`\def\x{a}{\def\x{b}}\message{\x}`, "a"}, // an ordinary group still restores + {`\newdimen\Z{{\Z=10pt\global\Z=\Z}}\message{\the\Z}`, "10.0pt"}, // through two groups + } + for _, c := range cases { + if got := runExpr(t, c.src); got != c.want { + t.Errorf("%s = %q, want %q", c.src, got, c.want) + } + } +} + +// A file name is expanded before it is looked up, since a package names the file +// to load through a macro (pgf loads its driver as +// \pgfutil@InputIfFileExists{\pgfsysdriver}). +func TestInputIfFileExistsExpandsTheName(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "named.tex"), []byte(`\message{[charge]}`), 0o644); err != nil { + t.Fatal(err) + } + t.Setenv("TEXINPUTS", dir) + e := New() + if err := e.LoadLaTeX(); err != nil { + t.Fatal(err) + } + out, err := e.Run(`\def\thefile{named.tex}` + + `\InputIfFileExists{\thefile}{\message{[trouve]}}{\message{[absent]}}`) + if err != nil { + t.Fatal(err) + } + if got := trimNL(out); got != "[trouve] [charge]" { + t.Errorf("= %q, want the file found through the macro and loaded", got) + } + // A name that resolves to nothing still takes the else branch. + e2 := New() + e2.LoadLaTeX() + out2, _ := e2.Run(`\def\thefile{nulle-part.tex}` + + `\InputIfFileExists{\thefile}{\message{[trouve]}}{\message{[absent]}}`) + if got := trimNL(out2); got != "[absent]" { + t.Errorf("= %q, want [absent]", got) + } +} + +// The engine's named colours are published in the form a colour-reading package +// expects, so a drawing package can ask which model and values a colour has +// instead of reporting the model as unsupported. +func TestColorIsPublishedForReaders(t *testing.T) { + e := New() + if err := e.LoadLaTeX(); err != nil { + t.Fatal(err) + } + out, err := e.Run(`\makeatletter\definecolor{mine}{RGB}{255,128,0}` + + `\def\read#1#2#3#4#5{\message{[modele=#4][valeurs=#5]}}` + + `\expandafter\expandafter\expandafter\read\csname\string\color@mine\endcsname`) + if err != nil { + t.Fatal(err) + } + if got := trimNL(out); got != "[modele=rgb][valeurs=1,0.50196,0]" { + t.Errorf("= %q, want the model and its values", got) + } + // \colorlet publishes its result too, which is how a package names a colour + // before reading it back. + e2 := New() + e2.LoadLaTeX() + out2, _ := e2.Run(`\makeatletter\colorlet{copie}{red}` + + `\def\read#1#2#3#4#5{\message{[#4:#5]}}` + + `\expandafter\expandafter\expandafter\read\csname\string\color@copie\endcsname`) + if got := trimNL(out2); got != "[rgb:1,0,0]" { + t.Errorf("\\colorlet = %q, want [rgb:1,0,0]", got) + } +} + +// colorComponent spells a component the way a colour specification does. +func TestColorComponent(t *testing.T) { + for _, c := range []struct { + in float64 + want string + }{{0, "0"}, {1, "1"}, {0.5, "0.5"}, {128.0 / 255, "0.50196"}, {1.0 / 3, "0.33333"}} { + if got := colorComponent(c.in); got != c.want { + t.Errorf("colorComponent(%v) = %q, want %q", c.in, got, c.want) + } + } +} + +// A malformed use of each of these is read as far as it makes sense rather than +// derailing the run. +func TestFaithfulPrimitivesOnMalformedInput(t *testing.T) { + for _, src := range []string{ + "\\count0=`", // the backtick is the very last thing in the input + "\\message{\\number`}", // ` at end of input + "\\definecolor{}{rgb}{1,0,0}", // a colour with no name + "\\InputIfFileExists x{}{}", // no braced name + "\\InputIfFileExists", // nothing at all + "\\InputIfFileExists{\\undefme}{}{}", // a name that expands to nothing useful + } { + if _, err := New().Run(src); err != nil { + t.Errorf("%s: %v", src, err) + } + } +} + +// A colour name with no colour behind it publishes nothing, and an unreadable +// specification still yields a readable (black) value rather than nonsense. +func TestPublishColorEdges(t *testing.T) { + e := New() + e.publishColor("", 0xffffff) // ignored: no name + if e.eq[`\color@`] != nil { + t.Error("a colour with no name was published") + } + e.publishColor("noir", 0) + m := e.eq[`\color@noir`] + if m == nil { + t.Fatal("colour not published") + } + if got := e.toksToString(m.body); got != `\xcolor@{}{}{rgb}{0,0,0}` { + t.Errorf("black = %q", got) + } +} + +// scanCharCode covers each way the character after a ` can end: a character, a +// single-character control sequence, a multi-letter one (which is not a +// character constant), and nothing at all. +func TestScanCharCodeBranches(t *testing.T) { + for _, c := range []struct{ src, want string }{ + {"\\message{\\number`a}", "97"}, + {"\\message{\\number`\\a}", "97"}, + {"\\message{\\number`\\relax}", "0"}, + {"\\message{[\\number`}", "[0"}, // input ends right after the backtick + } { + if got := runExpr(t, c.src); got != c.want { + t.Errorf("%s = %q, want %q", c.src, got, c.want) + } + } +}