Releases: 2389-research/Vinculum
Release list
2.1.0 — Math-feature expansion
Vinculum 2.1.0 — math-feature expansion. A wave of new notation, each rendered natively and platform-free: chemistry, commutative diagrams, Young tableaux, function plots, inference rules / proof trees, siunitx units, the physics package (Dirac notation & derivatives), and syntax trees — plus equation numbering, automatic line breaking, Presentation MathML export, and more. Every feature crosses the VDL1 seam, so it renders identically on Apple, Linux, Windows, Android, and the web. No API breaks; fully compatible with 2.0.x.
Closing the coverage gaps identified in the July 2026 gap analysis, one at a time,
each with docs + a specimen figure. Every feature is platform-free (renders
identically on all targets through the VDL1 seam).
Added
- Syntax / parse trees —
\Tree [.S [.NP … ] [.VP … ] ](qtree / tikz-qtree;\qtree
alias). A labelled root centered above its children, joined by straight edges;
[.Label child …]internal nodes, bare tokens are leaves,{…}for multi-token
labels/leaves, and labels are arbitrary math (so expression trees work too). A real
2D tidy-tree layout — subtree widths bottom-up, nodes placed top-down, edges as
stroked paths — so it renders identically on all five platforms throughVDL1with no
new primitive. NewMathNode.syntaxTree; round-trips throughtoLaTeX, speaks its
structure, exports to MathML as nested rows. The tokenizer gainsbracketVerbatimCommands
so\Tree's bracket body is captured with spaces intact. (Gap analysis — linguistics / CS.) - Physics package — Dirac notation, derivatives, brackets — the most-used
physics
macros, a transpiler to LaTeX (Physics) reusing the whole engine. Dirac notation
(\bra,\ket,\braket{ϕ}{ψ}→ ⟨ϕ|ψ⟩,\ketbra,\expval{A}{ψ}→ ⟨ψ|A|ψ⟩,
\mel{ϕ}{A}{ψ}, with aliases\ip/\op/\ev/\matrixel); derivatives (\dd{x}→ dx,
\dv{f}{x}→ df/dx,\dv[n],\pdv{f}{x}→ ∂f/∂x,\pdv{f}{x}{y}mixed); brackets
(\abs,\norm,\comm{A}{B}→ [A,B],\acomm→ {A,B},\order→ O(x)); and vector
operators (\grad,\curl,\laplacian,\Tr/\tr,\rank—\divleft as ÷). Also
fixes a latent bug:\left\|…\right\|now parses to the ‖ norm delimiter instead of
falling back to parens. (Gap analysis — quantum mechanics / vector calculus.) - Units — siunitx
\num/\ang/\si/\unit/\SI/\qty— physical
quantities and units, a transpiler to LaTeX (SIUnitx) reusing the whole engine like
mhchem.\numformats numbers (scientific notation1.5e3→ 1.5×10³, thin-space digit
grouping from five digits),\angrenders angles (45;30;15→ 45°30′15″),\si/\unit
typeset unit bodies (literalkg.m.s^{-1}or macros\kilo\gram\per\second\squared,
with./~thin spaces,/solidus,\per/\squared/\cubed, and SI prefixes/units
with µ/Ω/Å/°C upright), and\SI/\qtycombine a number and a unit. Both siunitx v2 and
v3 spellings; unknown unit macros pass through as real commands. (Gap analysis — metrology.) - Inference rules / proof trees —
\inferrule[label]{premises}{conclusion}
(aliases\infer,\prftree), the mathpartir/natural-deduction notation. Premises
(\\-separated) are centered over a horizontal bar sitting on the math axis; the
conclusion is centered below; the optional bracketed label (accepts[left=Name])
sits at the bar's right at script size. Empty premises render an axiom bar; rules
nest, so full derivation trees build up. NewMathNode.inferenceRule; round-trips
verbatim, speaks as "from …, and …, infer …", exports to MathML as a zero-numerator
<mfrac>. (Gap analysis — proof theory / PL.) - Function plots — a curated pgfplots subset:
\begin{axis}[domain=a:b, samples=n] \addplot{expr}; … \end{axis}(optionally wrapped intikzpicture).
A new arithmetic evaluator (MathExpression:+ - * / ^, unary minus, parens,
x,pi/e,sin cos tan exp ln sqrt abs …) samples each curve; the layout
auto-ranges y and draws framed axes with nice-numbered ticks, gridlines, and the
curves as stroked polylines (broken across discontinuities). NewMathNode.plot;
everything is stroked paths + glyph runs, so it renders on all five platforms via
VDL1. Full TikZ/pgfplots remains out of scope. (Gap analysis Tier 3 — the
flagship plotting feature.) - Young tableaux —
\ydiagram{4,2,1}(empty Young diagram from a partition) and
\ytableaushort{134,25,6}/\young(…)(filled tableau). A grid of bordered square
cells (stroked paths), left-aligned and stacked top-down, usable inline in an
expression. NewMathNode.youngTableau; round-trips verbatim. (Gap analysis Tier 3.) - Chemistry — mhchem
\ce{…}— chemical formulas and equations. Implemented as
a transpiler to LaTeX (MHChem.transpile), reusing the whole engine (no chemistry-
specific layout): auto-subscripts (H2O→ H₂O), groups (Ca(OH)2),^charges
(SO4^2-→ SO₄²⁻), stoichiometric coefficients (2H2), reaction arrows (->,
<-,<->,<=>equilibrium, and conditional->[\Delta]), bonds (=,#),
states ((s) (l) (g) (aq)), and hydrate dots (*). (Gap analysis Tier 2.) - Commutative diagrams —
\begin{CD} … \end{CD}(amscd): objects on a grid
joined by labelled arrows (@>a>b>,@<,@VaVbV,@A,@=/@\|equality
edges,@.). A real 2D-diagram layout — objects in a grid, arrows drawn as stroked
paths (shaft + chevron head), labels at script size — so it renders identically on
all five platforms through theVDL1wire, with no diagram-specific primitive.
NewMathNode.commutativeDiagram; round-trips verbatim. (Gap analysis Tier 2 — the
flagship diagram.) - Equation numbering & cross-references — document-scoped (like macros).
\label{key}records an equation's number;\eqref{key}→(N)and\ref{key}
→Nresolve throughout prose and math (dangling key →(?)).MathText's new
numberEquations:flag auto-numbers display equations that neither\tagnor
\notag— opt-in, so default output is unchanged.\label/\notag/\nonumber
are stripped before rendering;\tagstill places the number. New platform-free
MathNumberingengine (headless-tested). (Gap analysis Tier 1.) - Automatic line breaking —
layout(node, display:, maxWidth:)wraps an
over-wide equation across lines at TeX's break points (after a binary operator or
relation), stacking the lines baseline-aligned. Top-level only (nested subformulas
stay whole); greedy fit within the budget; unbreakable runs left intact. Fully
opt-in: withoutmaxWidththe output is byte-identical to before (zero golden
churn). The long-deferred flagship layout gap. (Gap analysis Tier 1.) - Presentation MathML export —
MathMLExporter.export(node, display:)(and
MathNode.toMathML()) serializes the parse tree to<math>…</math>:<mi>/<mn>/
<mo>by atom class,<mfrac>,<msqrt>/<mroot>,<msub>/<msup>/<msubsup>,
<mmultiscripts>(prescripts),<mtable>,<mover accent="true">,<menclose>.
XML-escaped and always well-formed (verified withXMLParser); unknown input →
<merror>. For accessibility trees, copy-as-MathML, and interop. (Gap analysis
Tier 1 — a second serializer off the tree, no rendering.) - Prescripts & multiscripts —
\prescript{sup}{sub}{base}(mathtools) and
\sideset{_l^l}{_r^r}{base}(amsmath). Scripts at all four corners of a base:
isotopes (\prescript{14}{6}{C}→ ¹⁴₆C), tensors, left indices. Pre- and
post-scripts share one pair of baseline shifts so every corner aligns; the base
keeps its own operator limits. NewMathNode.multiScriptscase; round-trips via
\sideset/\prescript. (Gap analysis Tier 0 #1.) \multicolumn{n}{align}{content}— table cells spanningncolumns with
their ownl/c/ralignment, in every grid environment (headers, titles,
merged blocks). Content overflowing the span grows its last column. New
MathNode.spannedcase; the no-span path is byte-identical, so existing matrices
are unchanged. (Gap analysis Tier 0 #2.)- Harpoon accents &
\utilde—\overrightharpoon/\overleftharpoon(stretchy
harpoons over the base) and\utilde(a wide tilde below the base, stretchy).
NewMathAccentcases with an under-accent layout branch; both round-trip exactly.
(Gap analysis Tier 0 #4.) \DeclareMathOperator&\mathchoice—\DeclareMathOperator{\name}{text}
(and the*limits form) registers a document-scoped operator that renders like
\operatorname{text}, via the existing macro processor.\mathchoice{D}{T}{S}{SS}
selects one of four branches by the current math style (newMathNode.mathChoice).
(Gap analysis Tier 0 #3.)
Fixed
- Spacing commands inside
\text/\mathrm/\operatornamenow resolve instead
of printing literally:\operatorname{arg\,max}renders "arg max" (thin space),
not "arg,max".\,\:\;\quad\qquad\map to the matching space, and
\&\%\_\#\$to their literal character. Embedded$…$math still works.
2.0.0 — Multi-platform
2.0.0 — 2026-07-21
Vinculum goes multi-platform. The layout engine was always platform-free
(Foundation-only); 2.0 turns that into shipping renderers on four new targets by
introducing one portable seam and reusing it everywhere. Apple and Linux are
unchanged and fully compatible — the major bump reflects the scope, not a break.
The seam — VDL1 display-list wire + C ABI
- A fully-resolved scene serializes to a compact binary display list (
VDL1):
fills, rects, and stroked paths with baked geometry and colors — no fonts or
measurement needed downstream. Documented in docs/DISPLAYLIST.md. - A C ABI (
vinculum_render_displaylist/_free/_abi_version/
_set_font_dir,@_cdecl) renders LaTeX straight toVDL1across a language
boundary, honoring the never-half-broken contract (unsupported input → null). - Cross-language wire conformance: Swift, Kotlin, and C# decoders are all gated
in CI against one committed fixture, so no platform can drift the format. - Measured: CoreText (Apple) and FreeType (Linux/Android/Windows) produce
byte-identical layout geometry to three decimals.
Windows — native rendering, two UI controls, NuGet
- Native
VinculumAndroid.dll(the C ABI, cross-linked against vcpkg FreeType),
driven from .NET via[DllImport](VinculumNative) — no shim. - A C#
VDL1decoder + SkiaSharpSceneRenderer(same Skia as the Android
Canvas, so Windows matches Android by construction; headless-testable). VinculumMathViewas a drop-in control for both WPF and WinUI 3
(Latex/DisplayMode/BaseSize/Ink— themeable ink).- NuGet packages
Vinculum.Rendering/.Windows.Wpf/.Windows.WinUI,
self-contained for win-x64: the native engine, its FreeType deps, the Swift
runtime redistributables, and the math fonts all travel with the package —
verified by a consumer run with the toolchain scrubbed fromPATH. - See windows/README.md.
Android
- The C ABI built as a dynamic library and called via JNI-in-APK; a Kotlin
VDL1
decoder +CanvasSceneRenderer. On-device rendering proven. Font loading uses
an injectable directory (Bundle.moduletraps inside an APK). See
docs/ANDROID.md.
WebAssembly
VinculumLayoutcross-compiles towasm32-unknown-wasip1and renders SVG under
wasmtime, gated in CI. Importing FoundationEssentials (no ICU) shrank the module
from ~48 MB to ~9.5 MB.
Foundation
VinculumLayoutnow builds against FoundationEssentials with pure-Swift
replacements for the few absent APIs (PlatformCompat), proven byte-equivalent —
keeping output identical across every platform (the SVG parity gate is the proof).
1.5.0 — \nolimits, amsmath display style, and a parser that stops eating your formula
Correctness release: \nolimits, amsmath display style in aligned blocks, a parser that no longer eats your formula, and three accessibility bugs.
Minor bump rather than a patch: MathNode gains a case (.noLimitsOperator), which is source-breaking for any client that switches exhaustively over it. Most hosts never do — it's a layout-model type — and if you do, it's a transparent wrapper: delegate to its base.
Added
\nolimitsworks. (#20)
In TeX it forces an operator's scripts to the side even in display style (\sum\nolimits_{i=1}^n) — the standard way to keep an operator compact inside a display equation. Vinculum recognized the command, consumed it, and threw the modifier away, so a display\sumstacked regardless.toLaTeX()dropped it too, so a round-trip silently re-stacked.\displaylimitsremains a no-op, which is correct: it restores the current style's default, which an unmodified operator already uses.
Fixed
-
aligned/align/gather/splitcells are display style, per amsmath. (#21)
Every grid environment's cells were laid out at a hardcoded text style, so a\sumor\intinside analignedblock rendered text-size with side-set limits where real amsmath stacks display-size ones.matrix/array/casescorrectly stay text style regardless of surroundings — that half was already right, and the report's "cells should inherit the surrounding style" framing would have broken it. -
A missing
}or]no longer swallows the rest of the formula. (#8)
Every argument reader scanned to its closer with an unbounded loop and no end-of-stream fallback, so one missing delimiter let a reader consume the whole remaining stream as a single argument — which is then discarded, since it's a colour/thickness name.\textcolor{red a+bproduced colourreda+band an empty body, and reportedisFullySupported == true, so the host's fallback never fired anda+bvanished behind a successful-looking blank render. Braces are now balance-checked up front (escape-aware, so\left\{ x \right\}is untouched); brackets are checked per reader, since[0,1)is ordinary math. -
iOS: an equation first rendered off-main lost its VoiceOver text forever. (#6)
The render entry points arenonisolatedprecisely so hosts can pre-render off the main thread, but the accessibility stamp was gated behindThread.isMainThread— and cache entries are immutable, so every later host reused the unlabeled bitmap. In theMathTextdocument path the image's own label is the only per-equation carrier, so VoiceOver read "image". The guard's premise was wrong: UIImage's accessibility setters are not MainActor-isolated. -
VinculumLabelno longer reads raw LaTeX to VoiceOver. (#7)
On unsupported input withdisplayErrorInlineoff, the view drew nothing and reported zero size — yet stayed a live accessibility element labelled with the raw source, so VoiceOver landed on an invisible node spelling out "backslash n o t a command". Accessibility now follows the visible outcome. -
MathViewannounces math as static text, not "image". (#26)
It kept SwiftUI's default.isImagetrait while the AppKit/UIKit sibling declared.staticText— the same equation announced two different ways depending on which view a host picked. -
A truncated size-variant ladder is rejected instead of kept partial. (#36)
On a short read the parser kept the partial ladder rather than discarding the record like every sibling parser does. Variant records are ordered smallest-first, so a truncated font silently lost a glyph's largest sizes — a\left(that stops growing and falls back to scaling. Only affects already-malformed fonts.
Under the hood
- The rasterizers are now fuzzed (#12) — the fuzz corpus only ever drove a mock measurer, so the FreeType/Cairo/CoreText interop, the code most likely to hold a memory-safety bug, only saw clean equations. Both renderers gate on
isFullySupported, so random garbage never reaches them; the new corpus is valid-by-construction but adversarial, and each test asserts it actually reached the rasterizer. - The render/measure caches are stress-tested concurrently (#46) — their comments made specific multi-thread claims that nothing exercised.
- ARCHITECTURE's "how to add a new command" checklist was wrong and is now measured: five compiler-enforced exhaustive switches, not seven, and
glyphTypographyhas adefault:that swallows a missing case silently.
Performance was re-verified in release after two parser changes rather than assumed — parse 23.8 µs, warm cache hit 0.46 µs, headless layout 7.8 µs, ~77% of a cold render is still CoreText. The published numbers hold. See docs/PERFORMANCE.md.
Install
.package(url: "https://github.com/2389-research/Vinculum.git", from: "1.5.0")Full detail: CHANGELOG.md · Docs & gallery
Vinculum 1.4.2
Correctness release: Linux font metrics, two parser data-loss bugs, and FreeType memory/thread safety. No API changes — a pure bug-fix patch.
If you render on Linux with anything but Latin Modern, your output changes (and is now correct).
Fixed
-
Linux: every font now uses its own MATH constants. (#1)
MathTableParserparses a MATH table, but the Linux renderer handed it the whole.otf— whose first bytes are the sfnt tag ('OTTO'), failing the version guard.constantsreturnednil, and the?? .latinModernfallback silently gave Termes, Pagella, STIX Two and Fira Math Latin Modern's metrics — so font choice had no effect on Linux layout, and nothing went red. The table is now extracted with FreeType, mirroring the Apple path'sCGFont.table(for:).
Measured: Fira Math's axis height is 0.28 and STIX Two's 0.258 — both had been rendering as 0.25. Axis height places every fraction bar and minus sign. Latin Modern is unchanged. -
Parser: a stateful switch no longer swallows
\end. (#2)
\bf/\color{…}/\displaystyleas the last token of an environment cell scanned straight through the environment's own\end, absorbing the terminator, the env name, and any trailing content — silently lost. -
Parser: double sub/superscript degrades visibly instead of dropping content. (#9)
a_b_c/x^2^3(TeX's "Double subscript" error) silently discarded the earlier atom and rendereda_c. It now degrades to a visible, source-locatable fallback. Behavior note: that input previously rendereda_c; it now shows a fallback card. That's the fix — it was deleting your content. -
Linux:
FreeTypeFontowns its font bytes. (#4)
FT_New_Memory_Facedoesn't copy — it reads through the pointer for the face's whole lifetime — but the pointer came fromData.withUnsafeBytes, which only guarantees validity inside the closure. Correctness rested onData's undocumented representation. -
Linux:
FreeTypeFont's sharedFT_Faceis locked. (#3)
Its@unchecked Sendablewas a false promise: every accessor callsFT_Load_Glyph, which mutates the face's shared glyph slot. Sharing one font across threads didn't merely race — it crashed (SIGBUS). Now serialized behind anNSLock, mirroringMathFont.ctFontLockon the Apple side.
Under the hood
- Reproducible Linux CI (#32) — the only job that builds the Silica/Cairo/FreeType backend was resolving
branch:masteragainst upstream HEAD every run. It now restores a pinned, CI-scoped lockfile. Default consumers stay Silica-free. - A Linux render-regression net (#11) — the backend's only assertion was "is it a valid PNG?", so a blank or garbled render passed silently. Renders are now diffed against committed ink signatures.
- Doc-drift guard (#31, #47) — CI now fails if
CONTRIBUTING.md's statedswift-tools-versiondiverges fromPackage.swift; the ARCHITECTURE checklist now lists all seven exhaustiveMathNodeswitches (it claimed four).
Performance claims were re-verified in release after the parser changes rather than assumed — the published numbers hold (parse 22.7µs, warm hit 0.50µs, headless layout 7.9µs, ~76% of a cold render is CoreText). See docs/PERFORMANCE.md.
Install
.package(url: "https://github.com/2389-research/Vinculum.git", from: "1.4.2")Full detail: CHANGELOG.md · Docs & gallery
Vinculum 1.4.1
Native LaTeX math typesetting for Apple platforms and Linux — real glyph shapes and TeX metrics read straight from the font's OpenType MATH table, following Knuth's Appendix G. No MathJax, no KaTeX, no WebView.
What's in 1.4.1
The Silica dependency is now opt-in — default consumers are Silica-free. 1.4.0 declared the Silica/Cairo dependency unconditionally, so every consumer (even Apple-only) was forced to resolve the whole Silica/Cairo/PureSwift graph. Fixed with a package trait (LinuxRaster, default OFF): the Silica dependency and its product links are gated behind it, so a default resolve fetches zero external dependencies — verified on both macOS and Linux. Opt in for the native Linux raster backend with traits: ["LinuxRaster"] (or --traits LinuxRaster). No render logic changed.
Built on the 1.3.0 → 1.4.0 Linux rendering line: VinculumRender draws a MathScene to a PNG on Linux via Silica/Cairo + FreeType (FreeTypeFont loads bundled .otf bytes, provides advances, real per-glyph ink extents, and FT_Outline_Decompose outlines). All five bundled MATH fonts render; a 20-equation corpus renders near-identically on both backends.
Install
.package(url: "https://github.com/2389-research/Vinculum.git", from: "1.4.1")Highlights across the 1.x line
- Font-truth layout — all 56
MathConstantsparsed per font, per-glyph italic correction, cut-in kerning, the full display/text/script/scriptscript style lattice, and the complete TeXbook p. 170 spacing table. sstyoptical scripts, delimiter size-variant ladders → glyph assembly, the font's real √ glyph.- Five bundled fonts (Latin Modern, TeX Gyre Termes & Pagella, STIX Two, Fira Math) + any OpenType MATH font via
MathFont(url:). - Spoken math (ClearSpeak-style),
MathNode.toLaTeX()round-trip, source-ranged diagnostics, opt-in hit-testing. - Apple (CoreText) + Linux (Silica/Cairo/FreeType); self-contained SVG anywhere. The layout engine is platform-free.
Full detail: CHANGELOG.md · Docs & live gallery