Skip to content

Releases: 2389-research/Vinculum

2.1.0 — Math-feature expansion

Choose a tag to compare

@clintecker clintecker released this 21 Jul 22:37
300817f

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 through VDL1 with no
    new primitive. New MathNode.syntaxTree; round-trips through toLaTeX, speaks its
    structure, exports to MathML as nested rows. The tokenizer gains bracketVerbatimCommands
    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\div left 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. \num formats numbers (scientific notation 1.5e3 → 1.5×10³, thin-space digit
    grouping from five digits), \ang renders angles (45;30;15 → 45°30′15″), \si/\unit
    typeset unit bodies (literal kg.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/\qty combine 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. New MathNode.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 in tikzpicture).
    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). New MathNode.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. New MathNode.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 the VDL1 wire, with no diagram-specific primitive.
    New MathNode.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}
    N resolve throughout prose and math (dangling key → (?)). MathText's new
    numberEquations: flag auto-numbers display equations that neither \tag nor
    \notag — opt-in, so default output is unchanged. \label/\notag/\nonumber
    are stripped before rendering; \tag still places the number. New platform-free
    MathNumbering engine (headless-tested). (Gap analysis Tier 1.)
  • Automatic line breakinglayout(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: without maxWidth the output is byte-identical to before (zero golden
    churn). The long-deferred flagship layout gap. (Gap analysis Tier 1.)
  • Presentation MathML exportMathMLExporter.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 with XMLParser); 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. New MathNode.multiScripts case; round-trips via
    \sideset/\prescript. (Gap analysis Tier 0 #1.)
  • \multicolumn{n}{align}{content} — table cells spanning n columns with
    their own l/c/r alignment, in every grid environment (headers, titles,
    merged blocks). Content overflowing the span grows its last column. New
    MathNode.spanned case; 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).
    New MathAccent cases 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 (new MathNode.mathChoice).
    (Gap analysis Tier 0 #3.)

Fixed

  • Spacing commands inside \text/\mathrm/\operatorname now 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

Choose a tag to compare

@clintecker clintecker released this 21 Jul 04:01
a513c69

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 to VDL1 across 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# VDL1 decoder + SkiaSharp SceneRenderer (same Skia as the Android
    Canvas, so Windows matches Android by construction; headless-testable).
  • VinculumMathView as 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 from PATH.
  • See windows/README.md.

Android

  • The C ABI built as a dynamic library and called via JNI-in-APK; a Kotlin VDL1
    decoder + Canvas SceneRenderer. On-device rendering proven. Font loading uses
    an injectable directory (Bundle.module traps inside an APK). See
    docs/ANDROID.md.

WebAssembly

  • VinculumLayout cross-compiles to wasm32-unknown-wasip1 and renders SVG under
    wasmtime, gated in CI. Importing FoundationEssentials (no ICU) shrank the module
    from ~48 MB to ~9.5 MB.

Foundation

  • VinculumLayout now 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

Choose a tag to compare

@clintecker clintecker released this 17 Jul 04:50
4c06aab

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

  • \nolimits works. (#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 \sum stacked regardless. toLaTeX() dropped it too, so a round-trip silently re-stacked. \displaylimits remains a no-op, which is correct: it restores the current style's default, which an unmodified operator already uses.

Fixed

  • aligned / align / gather / split cells are display style, per amsmath. (#21)
    Every grid environment's cells were laid out at a hardcoded text style, so a \sum or \int inside an aligned block rendered text-size with side-set limits where real amsmath stacks display-size ones. matrix / array / cases correctly 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+b produced colour reda+b and an empty body, and reported isFullySupported == true, so the host's fallback never fired and a+b vanished 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 are nonisolated precisely so hosts can pre-render off the main thread, but the accessibility stamp was gated behind Thread.isMainThread — and cache entries are immutable, so every later host reused the unlabeled bitmap. In the MathText document 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.

  • VinculumLabel no longer reads raw LaTeX to VoiceOver. (#7)
    On unsupported input with displayErrorInline off, 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.

  • MathView announces math as static text, not "image". (#26)
    It kept SwiftUI's default .isImage trait 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 glyphTypography has a default: 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

Choose a tag to compare

@clintecker clintecker released this 17 Jul 02:06

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)
    MathTableParser parses a MATH table, but the Linux renderer handed it the whole .otf — whose first bytes are the sfnt tag ('OTTO'), failing the version guard. constants returned nil, and the ?? .latinModern fallback 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's CGFont.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{…} / \displaystyle as 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 rendered a_c. It now degrades to a visible, source-locatable fallback. Behavior note: that input previously rendered a_c; it now shows a fallback card. That's the fix — it was deleting your content.

  • Linux: FreeTypeFont owns its font bytes. (#4)
    FT_New_Memory_Face doesn't copy — it reads through the pointer for the face's whole lifetime — but the pointer came from Data.withUnsafeBytes, which only guarantees validity inside the closure. Correctness rested on Data's undocumented representation.

  • Linux: FreeTypeFont's shared FT_Face is locked. (#3)
    Its @unchecked Sendable was a false promise: every accessor calls FT_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 an NSLock, mirroring MathFont.ctFontLock on the Apple side.

Under the hood

  • Reproducible Linux CI (#32) — the only job that builds the Silica/Cairo/FreeType backend was resolving branch:master against 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 stated swift-tools-version diverges from Package.swift; the ARCHITECTURE checklist now lists all seven exhaustive MathNode switches (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

Choose a tag to compare

@clintecker clintecker released this 16 Jul 01:22

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 MathConstants parsed 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.
  • ssty optical 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