Skip to documentation
SLOP

tiny.zen.math.parse

Reference tiny.zen math parse

Defined in math.

API (13)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callsmathparse
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callsprivate sourcelib.zen.src.math.parseexpectInvalidtest sourcelib.zen.src.math.parsetest: parse builds environment rows a...test sourcelib.zen.src.math.parsetest: parse builds scripts around a s...test sourcelib.zen.src.math.parsetest: parse drops a trailing empty en...test sourcelib.zen.src.math.parsetest: parse keeps empty cells and nes...+3 moremath.parseparse
Static calls · unresolved targets: 0 · external targets: 4.

Source: lib/zen/src/math/parse.zig

zig
const std = @import("std");const token = @import("token.zig");const symbol = @import("symbol.zig");const Allocator = std.mem.Allocator;pub const Error = error{ InvalidEquation, OutOfMemory };pub const Diagnostic = struct {    reason: []const u8 = "",    offset: usize = 0,};pub const Ident = struct {    text: []const u8,    upright: bool = false,};pub const Operator = struct {    text: []const u8,    stretchy: bool = false,    rigid: bool = false,    movable_word: bool = false,    prefers_limits: bool = false,};pub const Pair = struct {    first: *Node,    second: *Node,};pub const Radical = struct {    index: ?*Node,    radicand: *Node,};pub const Scripts = struct {    base: *Node,    sub: ?*Node,    sup: ?*Node,    limits: bool,};pub const Accent = struct {    base: *Node,    mark: []const u8,};pub const Table = struct {    rows: []const []Node,    env: symbol.Environment,};pub const Node = union(enum) {    row: []Node,    ident: Ident,    number: []const u8,    operator: Operator,    text: []const u8,    space: []const u8,    frac: Pair,    radical: Radical,    scripts: Scripts,    accent: Accent,    table: Table,};pub const Parsed = struct {    arena: std.heap.ArenaAllocator,    root: Node,    pub fn deinit(self: *Parsed) void {        self.arena.deinit();        self.* = undefined;    }};pub fn parse(backing: Allocator, source: []const u8, diagnostic: ?*Diagnostic) Error!Parsed {    var arena = std.heap.ArenaAllocator.init(backing);    errdefer arena.deinit();    var parser = Parser{        .allocator = arena.allocator(),        .tokens = .{ .source = source },        .diagnostic = diagnostic,    };    const items = try parser.sequence(.end);    if (items.len == 0) return parser.fail("empty equation", 0);    return .{ .arena = arena, .root = .{ .row = items } };}const Terminator = enum { end, close, right, bracket };const Parser = struct {    allocator: Allocator,    tokens: token.Tokenizer,    diagnostic: ?*Diagnostic,    pending: ?token.Token = null,    const CellEnd = enum { column, row, done };    fn fail(self: *Parser, reason: []const u8, offset: usize) error{InvalidEquation} {        if (self.diagnostic) |d| d.* = .{ .reason = reason, .offset = offset };        return error.InvalidEquation;    }    fn advance(self: *Parser) Error!token.Token {        if (self.pending) |tok| {            self.pending = null;            return tok;        }        return self.tokens.next() catch self.fail(self.tokens.reason, self.tokens.start);    }    fn sequence(self: *Parser, terminator: Terminator) Error![]Node {        var items: std.ArrayList(Node) = .empty;        while (true) {            const tok = try self.advance();            switch (tok) {                .end => {                    if (terminator != .end) return self.fail(switch (terminator) {                        .close => "missing '}'",                        .right => "missing '\\right'",                        .bracket => "missing ']'",                        .end => unreachable,                    }, self.tokens.start);                    return try items.toOwnedSlice(self.allocator);                },                .close => {                    if (terminator != .close) return self.fail("unmatched '}'", self.tokens.start);                    return try items.toOwnedSlice(self.allocator);                },                .caret => try self.attachScript(&items, true),                .underscore => try self.attachScript(&items, false),                .prime => try self.attachPrime(&items),                .open => {                    const inner = try self.sequence(.close);                    try items.append(self.allocator, .{ .row = inner });                },                .command => |name| {                    if (std.mem.eql(u8, name, "right")) {                        if (terminator != .right) return self.fail("unmatched '\\right'", self.tokens.start);                        return try items.toOwnedSlice(self.allocator);                    }                    try items.append(self.allocator, try self.command(name));                },                .char => |code| {                    if (terminator == .bracket and code == ']') return try items.toOwnedSlice(self.allocator);                    try items.append(self.allocator, try self.charNode(code, true));                },            }        }    }    fn attachScript(self: *Parser, items: *std.ArrayList(Node), is_sup: bool) Error!void {        const mark = self.tokens.start;        if (items.items.len == 0) {            return self.fail(if (is_sup) "superscript without a base" else "subscript without a base", mark);        }        const arg = try self.heap(try self.argument());        const last = &items.items[items.items.len - 1];        if (last.* == .scripts) {            const scripts = &last.scripts;            if (is_sup) {                if (scripts.sup != null) return self.fail("double superscript", mark);                scripts.sup = arg;            } else {                if (scripts.sub != null) return self.fail("double subscript", mark);                scripts.sub = arg;            }            return;        }        const base = try self.heap(last.*);        last.* = .{ .scripts = .{            .base = base,            .sub = if (is_sup) null else arg,            .sup = if (is_sup) arg else null,            .limits = prefersLimits(base.*),        } };    }    fn attachPrime(self: *Parser, items: *std.ArrayList(Node)) Error!void {        if (items.items.len == 0) return self.fail("prime without a base", self.tokens.start);        const last = &items.items[items.items.len - 1];        const base = try self.heap(last.*);        const mark = try self.heap(Node{ .operator = .{ .text = "′" } });        last.* = .{ .scripts = .{            .base = base,            .sub = null,            .sup = mark,            .limits = false,        } };    }    fn argument(self: *Parser) Error!Node {        const tok = try self.advance();        return switch (tok) {            .open => .{ .row = try self.sequence(.close) },            .command => |name| try self.command(name),            .char => |code| try self.charNode(code, false),            else => self.fail("missing argument", self.tokens.start),        };    }    fn command(self: *Parser, name: []const u8) Error!Node {        if (std.mem.eql(u8, name, "begin")) return try self.environment();        if (std.mem.eql(u8, name, "end")) return self.fail("unmatched '\\end'", self.tokens.start);        if (std.mem.eql(u8, name, "\\")) return self.fail("misplaced '\\\\'", self.tokens.start);        if (std.mem.eql(u8, name, "frac")) {            const numerator = try self.heap(try self.argument());            const denominator = try self.heap(try self.argument());            return .{ .frac = .{ .first = numerator, .second = denominator } };        }        if (std.mem.eql(u8, name, "sqrt")) {            const tok = try self.advance();            if (tok == .char and tok.char == '[') {                const index = try self.heap(Node{ .row = try self.sequence(.bracket) });                const radicand = try self.heap(try self.argument());                return .{ .radical = .{ .index = index, .radicand = radicand } };            }            self.pending = tok;            return .{ .radical = .{ .index = null, .radicand = try self.heap(try self.argument()) } };        }        if (std.mem.eql(u8, name, "text")) {            return .{ .text = try self.allocator.dupe(u8, try self.rawGroup()) };        }        if (std.mem.eql(u8, name, "mathrm") or std.mem.eql(u8, name, "operatorname")) {            return .{ .ident = .{ .text = try self.allocator.dupe(u8, try self.rawGroup()), .upright = true } };        }        if (std.mem.eql(u8, name, "mathit")) {            return .{ .ident = .{ .text = try self.allocator.dupe(u8, try self.rawGroup()), .upright = false } };        }        if (std.mem.eql(u8, name, "mathbb")) return try self.styledIdent(.bb);        if (std.mem.eql(u8, name, "mathcal")) return try self.styledIdent(.cal);        if (std.mem.eql(u8, name, "mathfrak")) return try self.styledIdent(.frak);        if (std.mem.eql(u8, name, "mathbf")) return try self.styledIdent(.bf);        if (std.mem.eql(u8, name, "left")) return try self.leftRight();        if (symbol.commands.get(name)) |entry| {            return switch (entry.kind) {                .identifier => .{ .ident = .{ .text = entry.text, .upright = entry.upright } },                .function => .{ .ident = .{ .text = entry.text, .upright = true } },                .operator => .{ .operator = .{ .text = entry.text } },                .fence => .{ .operator = .{ .text = entry.text, .rigid = true } },                .largeop => .{ .operator = .{ .text = entry.text, .prefers_limits = true } },                .integral => .{ .operator = .{ .text = entry.text } },                .movableop => .{ .operator = .{                    .text = entry.text,                    .movable_word = true,                    .prefers_limits = true,                } },                .space => .{ .space = entry.text },                .accent => .{ .accent = .{                    .base = try self.heap(try self.argument()),                    .mark = entry.text,                } },            };        }        return self.fail("unknown command", self.tokens.start);    }    fn environment(self: *Parser) Error!Node {        const name = try self.rawGroup();        const name_offset = @intFromPtr(name.ptr) - @intFromPtr(self.tokens.source.ptr);        const env = symbol.environments.get(name) orelse return self.fail("unknown environment", name_offset);        var rows: std.ArrayList([]Node) = .empty;        var cells: std.ArrayList(Node) = .empty;        while (true) {            const parsed = try self.cell(name);            try cells.append(self.allocator, .{ .row = parsed.items });            switch (parsed.end) {                .column => continue,                .row => try rows.append(self.allocator, try cells.toOwnedSlice(self.allocator)),                .done => {                    try rows.append(self.allocator, try cells.toOwnedSlice(self.allocator));                    break;                },            }        }        const last = rows.items[rows.items.len - 1];        if (rows.items.len > 1 and last.len == 1 and last[0].row.len == 0) rows.items.len -= 1;        return .{ .table = .{ .rows = try rows.toOwnedSlice(self.allocator), .env = env } };    }    fn cell(self: *Parser, env_name: []const u8) Error!struct { items: []Node, end: CellEnd } {        var items: std.ArrayList(Node) = .empty;        while (true) {            const tok = try self.advance();            switch (tok) {                .end => return self.fail("missing '\\end'", self.tokens.start),                .close => return self.fail("unmatched '}'", self.tokens.start),                .caret => try self.attachScript(&items, true),                .underscore => try self.attachScript(&items, false),                .prime => try self.attachPrime(&items),                .open => {                    const inner = try self.sequence(.close);                    try items.append(self.allocator, .{ .row = inner });                },                .command => |name| {                    if (std.mem.eql(u8, name, "\\")) {                        return .{ .items = try items.toOwnedSlice(self.allocator), .end = .row };                    }                    if (std.mem.eql(u8, name, "end")) {                        const mark = self.tokens.start;                        const found = try self.rawGroup();                        if (!std.mem.eql(u8, found, env_name)) return self.fail("mismatched '\\end'", mark);                        return .{ .items = try items.toOwnedSlice(self.allocator), .end = .done };                    }                    if (std.mem.eql(u8, name, "right")) return self.fail("unmatched '\\right'", self.tokens.start);                    try items.append(self.allocator, try self.command(name));                },                .char => |code| {                    if (code == '&') {                        return .{ .items = try items.toOwnedSlice(self.allocator), .end = .column };                    }                    try items.append(self.allocator, try self.charNode(code, true));                },            }        }    }    fn leftRight(self: *Parser) Error!Node {        const open_text = try self.fenceText();        const inner = try self.sequence(.right);        const close_text = try self.fenceText();        var items: std.ArrayList(Node) = .empty;        if (open_text) |text| {            try items.append(self.allocator, .{ .operator = .{ .text = text, .stretchy = true } });        }        try items.appendSlice(self.allocator, inner);        if (close_text) |text| {            try items.append(self.allocator, .{ .operator = .{ .text = text, .stretchy = true } });        }        return .{ .row = try items.toOwnedSlice(self.allocator) };    }    fn fenceText(self: *Parser) Error!?[]const u8 {        const tok = try self.advance();        switch (tok) {            .char => |code| return switch (code) {                '(' => "(",                ')' => ")",                '[' => "[",                ']' => "]",                '|' => "|",                '/' => "/",                '.' => null,                else => self.fail("invalid delimiter", self.tokens.start),            },            .command => |name| {                if (symbol.commands.get(name)) |entry| {                    if (entry.kind == .fence) return entry.text;                }                return self.fail("invalid delimiter", self.tokens.start);            },            .end => return self.fail("missing delimiter", self.tokens.start),            else => return self.fail("invalid delimiter", self.tokens.start),        }    }    fn styledIdent(self: *Parser, style: symbol.Style) Error!Node {        const raw = try self.rawGroup();        const base = @intFromPtr(raw.ptr) - @intFromPtr(self.tokens.source.ptr);        var buffer: std.ArrayList(u8) = .empty;        for (raw, 0..) |char, index| {            if (char == ' ' or char == '\t') continue;            const code = symbol.styled(style, char) orelse return self.fail("unsupported styled letter", base + index);            var encoded: [4]u8 = undefined;            const length = std.unicode.utf8Encode(code, &encoded) catch return self.fail("unsupported styled letter", base + index);            try buffer.appendSlice(self.allocator, encoded[0..length]);        }        if (buffer.items.len == 0) return self.fail("empty argument", base);        return .{ .ident = .{ .text = try buffer.toOwnedSlice(self.allocator) } };    }    fn rawGroup(self: *Parser) Error![]const u8 {        const source = self.tokens.source;        var index = self.tokens.index;        while (index < source.len and (source[index] == ' ' or source[index] == '\t')) index += 1;        if (index >= source.len or source[index] != '{') return self.fail("expected '{'", index);        index += 1;        const start = index;        var depth: usize = 1;        while (index < source.len) : (index += 1) {            if (source[index] == '{') depth += 1;            if (source[index] == '}') {                depth -= 1;                if (depth == 0) break;            }        }        if (depth != 0) return self.fail("missing '}'", source.len);        self.tokens.index = index + 1;        return source[start..index];    }    fn charNode(self: *Parser, code: u21, run_numbers: bool) Error!Node {        if (code < 0x80) {            const byte: u8 = @intCast(code);            if (std.ascii.isAlphabetic(byte)) {                return .{ .ident = .{ .text = try self.allocator.dupe(u8, &.{byte}) } };            }            if (std.ascii.isDigit(byte)) {                if (run_numbers) return try self.numberNode(byte);                return .{ .number = try self.allocator.dupe(u8, &.{byte}) };            }            return switch (byte) {                '+' => .{ .operator = .{ .text = "+" } },                '-' => .{ .operator = .{ .text = "−" } },                '*' => .{ .operator = .{ .text = "∗" } },                '=' => .{ .operator = .{ .text = "=" } },                '<' => .{ .operator = .{ .text = "<" } },                '>' => .{ .operator = .{ .text = ">" } },                '(' => .{ .operator = .{ .text = "(", .rigid = true } },                ')' => .{ .operator = .{ .text = ")", .rigid = true } },                '[' => .{ .operator = .{ .text = "[", .rigid = true } },                ']' => .{ .operator = .{ .text = "]", .rigid = true } },                '|' => .{ .operator = .{ .text = "|", .rigid = true } },                '/' => .{ .operator = .{ .text = "/", .rigid = true } },                ',' => .{ .operator = .{ .text = "," } },                ';' => .{ .operator = .{ .text = ";" } },                ':' => .{ .operator = .{ .text = ":" } },                '!' => .{ .operator = .{ .text = "!" } },                '?' => .{ .operator = .{ .text = "?" } },                '.' => try self.dotNode(run_numbers),                '&' => self.fail("misplaced '&'", self.tokens.start),                else => self.fail("unsupported character", self.tokens.start),            };        }        var encoded: [4]u8 = undefined;        const length = std.unicode.utf8Encode(code, &encoded) catch return self.fail("unsupported character", self.tokens.start);        return .{ .ident = .{ .text = try self.allocator.dupe(u8, encoded[0..length]) } };    }    fn dotNode(self: *Parser, run_numbers: bool) Error!Node {        const source = self.tokens.source;        if (run_numbers and self.tokens.index < source.len and std.ascii.isDigit(source[self.tokens.index])) {            return try self.numberNode('.');        }        return .{ .operator = .{ .text = "." } };    }    fn numberNode(self: *Parser, first: u8) Error!Node {        var buffer: std.ArrayList(u8) = .empty;        try buffer.append(self.allocator, first);        const source = self.tokens.source;        while (self.tokens.index < source.len) {            const byte = source[self.tokens.index];            if (std.ascii.isDigit(byte)) {                try buffer.append(self.allocator, byte);                self.tokens.index += 1;                continue;            }            if (byte == '.' and self.tokens.index + 1 < source.len and std.ascii.isDigit(source[self.tokens.index + 1])) {                try buffer.append(self.allocator, '.');                self.tokens.index += 1;                continue;            }            break;        }        return .{ .number = try buffer.toOwnedSlice(self.allocator) };    }    fn heap(self: *Parser, node: Node) Error!*Node {        const slot = try self.allocator.create(Node);        slot.* = node;        return slot;    }};fn prefersLimits(node: Node) bool {    return node == .operator and node.operator.prefers_limits;}fn expectInvalid(source: []const u8, reason: []const u8, offset: usize) !void {    var diagnostic: Diagnostic = .{};    try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, source, &diagnostic));    try std.testing.expectEqualStrings(reason, diagnostic.reason);    try std.testing.expectEqual(offset, diagnostic.offset);}test "parse builds scripts around a shared base" {    var parsed = try parse(std.testing.allocator, "x_i^2", null);    defer parsed.deinit();    const items = parsed.root.row;    try std.testing.expectEqual(@as(usize, 1), items.len);    const scripts = items[0].scripts;    try std.testing.expectEqualStrings("x", scripts.base.ident.text);    try std.testing.expectEqualStrings("i", scripts.sub.?.ident.text);    try std.testing.expectEqualStrings("2", scripts.sup.?.number);    try std.testing.expect(!scripts.limits);}test "parse marks sum scripts as limit placed" {    var parsed = try parse(std.testing.allocator, "\\sum_{i}^{n}", null);    defer parsed.deinit();    const scripts = parsed.root.row[0].scripts;    try std.testing.expect(scripts.limits);    try std.testing.expectEqualStrings("∑", scripts.base.operator.text);}test "parse reads numbers fractions radicals and styled letters" {    var parsed = try parse(std.testing.allocator, "\\frac{3.14}{\\sqrt[3]{x}} \\mathbb{R}", null);    defer parsed.deinit();    const items = parsed.root.row;    try std.testing.expectEqual(@as(usize, 2), items.len);    try std.testing.expectEqualStrings("3.14", items[0].frac.first.row[0].number);    const radical = items[0].frac.second.row[0].radical;    try std.testing.expectEqualStrings("3", radical.index.?.row[0].number);    try std.testing.expectEqualStrings("x", radical.radicand.row[0].ident.text);    try std.testing.expectEqualStrings("ℝ", items[1].ident.text);}test "parse builds environment rows and cells" {    var parsed = try parse(std.testing.allocator, "\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}", null);    defer parsed.deinit();    const table = parsed.root.row[0].table;    try std.testing.expectEqualStrings("(", table.env.open.?);    try std.testing.expectEqualStrings(")", table.env.close.?);    try std.testing.expectEqual(@as(usize, 2), table.rows.len);    try std.testing.expectEqual(@as(usize, 2), table.rows[0].len);    try std.testing.expectEqualStrings("a", table.rows[0][0].row[0].ident.text);    try std.testing.expectEqualStrings("d", table.rows[1][1].row[0].ident.text);}test "parse drops a trailing empty environment row" {    var parsed = try parse(std.testing.allocator, "\\begin{cases} x & y \\\\ \\end{cases}", null);    defer parsed.deinit();    const table = parsed.root.row[0].table;    try std.testing.expectEqual(@as(usize, 1), table.rows.len);    try std.testing.expectEqual(@as(usize, 2), table.rows[0].len);}test "parse keeps empty cells and nests environments" {    var parsed = try parse(std.testing.allocator, "\\begin{matrix} & \\begin{pmatrix} 1 \\end{pmatrix} \\end{matrix}", null);    defer parsed.deinit();    const table = parsed.root.row[0].table;    try std.testing.expectEqual(@as(usize, 1), table.rows.len);    try std.testing.expectEqual(@as(usize, 2), table.rows[0].len);    try std.testing.expectEqual(@as(usize, 0), table.rows[0][0].row.len);    const inner = table.rows[0][1].row[0].table;    try std.testing.expectEqualStrings("1", inner.rows[0][0].row[0].number);}test "parse rejects malformed input" {    try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "", null));    try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "{x", null));    try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "^2", null));    try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "x^2^3", null));    try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "\\nonesuch", null));    try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "\\left( x", null));    try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "\\frac{1}", null));}test "parse names the offending span" {    try expectInvalid("", "empty equation", 0);    try expectInvalid("{x", "missing '}'", 2);    try expectInvalid("}", "unmatched '}'", 0);    try expectInvalid("^2", "superscript without a base", 0);    try expectInvalid("x_2_3", "double subscript", 3);    try expectInvalid("x^2^3", "double superscript", 3);    try expectInvalid("'", "prime without a base", 0);    try expectInvalid("\\nonesuch", "unknown command", 0);    try expectInvalid("\\left( x", "missing '\\right'", 8);    try expectInvalid("x \\right)", "unmatched '\\right'", 2);    try expectInvalid("\\left? x \\right)", "invalid delimiter", 5);    try expectInvalid("\\frac{1}", "missing argument", 8);    try expectInvalid("\\text x", "expected '{'", 6);    try expectInvalid("\\mathrm{oops", "missing '}'", 12);    try expectInvalid("\\mathbb{Ω}", "unsupported styled letter", 8);    try expectInvalid("\\mathbb{}", "empty argument", 8);    try expectInvalid("\\sqrt[3{x}", "missing ']'", 10);    try expectInvalid("x#y", "unsupported character", 1);    try expectInvalid("x\\", "incomplete command", 1);}test "parse names the offending environment span" {    try expectInvalid("\\begin pmatrix", "expected '{'", 7);    try expectInvalid("\\begin{nonesuch} x \\end{nonesuch}", "unknown environment", 7);    try expectInvalid("\\begin{pmatrix} 1", "missing '\\end'", 17);    try expectInvalid("\\begin{pmatrix} 1 \\end{cases}", "mismatched '\\end'", 18);    try expectInvalid("x \\end{pmatrix}", "unmatched '\\end'", 2);    try expectInvalid("a & b", "misplaced '&'", 2);    try expectInvalid("a \\\\ b", "misplaced '\\\\'", 2);    try expectInvalid("\\begin{matrix} {a & b} \\end{matrix}", "misplaced '&'", 18);    try expectInvalid("\\begin{matrix} \\left( a & b \\right) \\end{matrix}", "misplaced '&'", 24);}

Source: lib/zen/src/math/root.zig:4

zig
pub const parse = @import("parse.zig");

Complete caller list for math.parse.parse

8 direct callers.

Audit

Definitions14
Public names16
Members36
Version26.7.0
Revisiondaab053ee433