tiny.zen.math.parse
Defined in math.
API (13)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
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.
lib.zen.src.math.parse.expectInvalid[function] — private source atlib/zen/src/math/parse.zig:472in nearest public ownertiny.zen.math.parselib.zen.src.math.parse.test_parse_builds_environment_rows_and_cells[function] — test source atlib/zen/src/math/parse.zig:511in nearest public ownertiny.zen.math.parselib.zen.src.math.parse.test_parse_builds_scripts_around_a_shared_base[function] — test source atlib/zen/src/math/parse.zig:479in nearest public ownertiny.zen.math.parselib.zen.src.math.parse.test_parse_drops_a_trailing_empty_environment_row[function] — test source atlib/zen/src/math/parse.zig:523in nearest public ownertiny.zen.math.parselib.zen.src.math.parse.test_parse_keeps_empty_cells_and_nests_environments[function] — test source atlib/zen/src/math/parse.zig:531in nearest public ownertiny.zen.math.parselib.zen.src.math.parse.test_parse_marks_sum_scripts_as_limit_placed[function] — test source atlib/zen/src/math/parse.zig:491in nearest public ownertiny.zen.math.parselib.zen.src.math.parse.test_parse_reads_numbers_fractions_radicals_and_styled_letters[function] — test source atlib/zen/src/math/parse.zig:499in nearest public ownertiny.zen.math.parselib.zen.src.math.parse.test_parse_rejects_malformed_input[function] — test source atlib/zen/src/math/parse.zig:542in nearest public ownertiny.zen.math.parse
Audit
| Definitions | 14 |
|---|---|
| Public names | 16 |
| Members | 36 |
| Version | 26.7.0 |
| Revision | daab053ee433 |