lib/zen/src/math/parse.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const token = @import("token.zig");
3 const symbol = @import("symbol.zig");
4
5 const Allocator = std.mem.Allocator;
6
7 pub const Error = error{ InvalidEquation, OutOfMemory };
8
9 pub const Diagnostic = struct {
10 reason: []const u8 = "",
11 offset: usize = 0,
12 };
13
14 pub const Ident = struct {
15 text: []const u8,
16 upright: bool = false,
17 };
18
19 pub const Operator = struct {
20 text: []const u8,
21 stretchy: bool = false,
22 rigid: bool = false,
23 movable_word: bool = false,
24 prefers_limits: bool = false,
25 };
26
27 pub const Pair = struct {
28 first: *Node,
29 second: *Node,
30 };
31
32 pub const Radical = struct {
33 index: ?*Node,
34 radicand: *Node,
35 };
36
37 pub const Scripts = struct {
38 base: *Node,
39 sub: ?*Node,
40 sup: ?*Node,
41 limits: bool,
42 };
43
44 pub const Accent = struct {
45 base: *Node,
46 mark: []const u8,
47 };
48
49 pub const Table = struct {
50 rows: []const []Node,
51 env: symbol.Environment,
52 };
53
54 pub const Node = union(enum) {
55 row: []Node,
56 ident: Ident,
57 number: []const u8,
58 operator: Operator,
59 text: []const u8,
60 space: []const u8,
61 frac: Pair,
62 radical: Radical,
63 scripts: Scripts,
64 accent: Accent,
65 table: Table,
66 };
67
68 pub const Parsed = struct {
69 arena: std.heap.ArenaAllocator,
70 root: Node,
71
72 pub fn deinit(self: *Parsed) void {
73 self.arena.deinit();
74 self.* = undefined;
75 }
76 };
77
78 pub fn parse(backing: Allocator, source: []const u8, diagnostic: ?*Diagnostic) Error!Parsed {
79 var arena = std.heap.ArenaAllocator.init(backing);
80 errdefer arena.deinit();
81 var parser = Parser{
82 .allocator = arena.allocator(),
83 .tokens = .{ .source = source },
84 .diagnostic = diagnostic,
85 };
86 const items = try parser.sequence(.end);
87 if (items.len == 0) return parser.fail("empty equation", 0);
88 return .{ .arena = arena, .root = .{ .row = items } };
89 }
90
91 const Terminator = enum { end, close, right, bracket };
92
93 const Parser = struct {
94 allocator: Allocator,
95 tokens: token.Tokenizer,
96 diagnostic: ?*Diagnostic,
97 pending: ?token.Token = null,
98
99 const CellEnd = enum { column, row, done };
100
101 fn fail(self: *Parser, reason: []const u8, offset: usize) error{InvalidEquation} {
102 if (self.diagnostic) |d| d.* = .{ .reason = reason, .offset = offset };
103 return error.InvalidEquation;
104 }
105
106 fn advance(self: *Parser) Error!token.Token {
107 if (self.pending) |tok| {
108 self.pending = null;
109 return tok;
110 }
111 return self.tokens.next() catch self.fail(self.tokens.reason, self.tokens.start);
112 }
113
114 fn sequence(self: *Parser, terminator: Terminator) Error![]Node {
115 var items: std.ArrayList(Node) = .empty;
116 while (true) {
117 const tok = try self.advance();
118 switch (tok) {
119 .end => {
120 if (terminator != .end) return self.fail(switch (terminator) {
121 .close => "missing '}'",
122 .right => "missing '\\right'",
123 .bracket => "missing ']'",
124 .end => unreachable,
125 }, self.tokens.start);
126 return try items.toOwnedSlice(self.allocator);
127 },
128 .close => {
129 if (terminator != .close) return self.fail("unmatched '}'", self.tokens.start);
130 return try items.toOwnedSlice(self.allocator);
131 },
132 .caret => try self.attachScript(&items, true),
133 .underscore => try self.attachScript(&items, false),
134 .prime => try self.attachPrime(&items),
135 .open => {
136 const inner = try self.sequence(.close);
137 try items.append(self.allocator, .{ .row = inner });
138 },
139 .command => |name| {
140 if (std.mem.eql(u8, name, "right")) {
141 if (terminator != .right) return self.fail("unmatched '\\right'", self.tokens.start);
142 return try items.toOwnedSlice(self.allocator);
143 }
144 try items.append(self.allocator, try self.command(name));
145 },
146 .char => |code| {
147 if (terminator == .bracket and code == ']') return try items.toOwnedSlice(self.allocator);
148 try items.append(self.allocator, try self.charNode(code, true));
149 },
150 }
151 }
152 }
153
154 fn attachScript(self: *Parser, items: *std.ArrayList(Node), is_sup: bool) Error!void {
155 const mark = self.tokens.start;
156 if (items.items.len == 0) {
157 return self.fail(if (is_sup) "superscript without a base" else "subscript without a base", mark);
158 }
159 const arg = try self.heap(try self.argument());
160 const last = &items.items[items.items.len - 1];
161 if (last.* == .scripts) {
162 const scripts = &last.scripts;
163 if (is_sup) {
164 if (scripts.sup != null) return self.fail("double superscript", mark);
165 scripts.sup = arg;
166 } else {
167 if (scripts.sub != null) return self.fail("double subscript", mark);
168 scripts.sub = arg;
169 }
170 return;
171 }
172 const base = try self.heap(last.*);
173 last.* = .{ .scripts = .{
174 .base = base,
175 .sub = if (is_sup) null else arg,
176 .sup = if (is_sup) arg else null,
177 .limits = prefersLimits(base.*),
178 } };
179 }
180
181 fn attachPrime(self: *Parser, items: *std.ArrayList(Node)) Error!void {
182 if (items.items.len == 0) return self.fail("prime without a base", self.tokens.start);
183 const last = &items.items[items.items.len - 1];
184 const base = try self.heap(last.*);
185 const mark = try self.heap(Node{ .operator = .{ .text = "′" } });
186 last.* = .{ .scripts = .{
187 .base = base,
188 .sub = null,
189 .sup = mark,
190 .limits = false,
191 } };
192 }
193
194 fn argument(self: *Parser) Error!Node {
195 const tok = try self.advance();
196 return switch (tok) {
197 .open => .{ .row = try self.sequence(.close) },
198 .command => |name| try self.command(name),
199 .char => |code| try self.charNode(code, false),
200 else => self.fail("missing argument", self.tokens.start),
201 };
202 }
203
204 fn command(self: *Parser, name: []const u8) Error!Node {
205 if (std.mem.eql(u8, name, "begin")) return try self.environment();
206 if (std.mem.eql(u8, name, "end")) return self.fail("unmatched '\\end'", self.tokens.start);
207 if (std.mem.eql(u8, name, "\\")) return self.fail("misplaced '\\\\'", self.tokens.start);
208 if (std.mem.eql(u8, name, "frac")) {
209 const numerator = try self.heap(try self.argument());
210 const denominator = try self.heap(try self.argument());
211 return .{ .frac = .{ .first = numerator, .second = denominator } };
212 }
213 if (std.mem.eql(u8, name, "sqrt")) {
214 const tok = try self.advance();
215 if (tok == .char and tok.char == '[') {
216 const index = try self.heap(Node{ .row = try self.sequence(.bracket) });
217 const radicand = try self.heap(try self.argument());
218 return .{ .radical = .{ .index = index, .radicand = radicand } };
219 }
220 self.pending = tok;
221 return .{ .radical = .{ .index = null, .radicand = try self.heap(try self.argument()) } };
222 }
223 if (std.mem.eql(u8, name, "text")) {
224 return .{ .text = try self.allocator.dupe(u8, try self.rawGroup()) };
225 }
226 if (std.mem.eql(u8, name, "mathrm") or std.mem.eql(u8, name, "operatorname")) {
227 return .{ .ident = .{ .text = try self.allocator.dupe(u8, try self.rawGroup()), .upright = true } };
228 }
229 if (std.mem.eql(u8, name, "mathit")) {
230 return .{ .ident = .{ .text = try self.allocator.dupe(u8, try self.rawGroup()), .upright = false } };
231 }
232 if (std.mem.eql(u8, name, "mathbb")) return try self.styledIdent(.bb);
233 if (std.mem.eql(u8, name, "mathcal")) return try self.styledIdent(.cal);
234 if (std.mem.eql(u8, name, "mathfrak")) return try self.styledIdent(.frak);
235 if (std.mem.eql(u8, name, "mathbf")) return try self.styledIdent(.bf);
236 if (std.mem.eql(u8, name, "left")) return try self.leftRight();
237 if (symbol.commands.get(name)) |entry| {
238 return switch (entry.kind) {
239 .identifier => .{ .ident = .{ .text = entry.text, .upright = entry.upright } },
240 .function => .{ .ident = .{ .text = entry.text, .upright = true } },
241 .operator => .{ .operator = .{ .text = entry.text } },
242 .fence => .{ .operator = .{ .text = entry.text, .rigid = true } },
243 .largeop => .{ .operator = .{ .text = entry.text, .prefers_limits = true } },
244 .integral => .{ .operator = .{ .text = entry.text } },
245 .movableop => .{ .operator = .{
246 .text = entry.text,
247 .movable_word = true,
248 .prefers_limits = true,
249 } },
250 .space => .{ .space = entry.text },
251 .accent => .{ .accent = .{
252 .base = try self.heap(try self.argument()),
253 .mark = entry.text,
254 } },
255 };
256 }
257 return self.fail("unknown command", self.tokens.start);
258 }
259
260 fn environment(self: *Parser) Error!Node {
261 const name = try self.rawGroup();
262 const name_offset = @intFromPtr(name.ptr) - @intFromPtr(self.tokens.source.ptr);
263 const env = symbol.environments.get(name) orelse return self.fail("unknown environment", name_offset);
264 var rows: std.ArrayList([]Node) = .empty;
265 var cells: std.ArrayList(Node) = .empty;
266 while (true) {
267 const parsed = try self.cell(name);
268 try cells.append(self.allocator, .{ .row = parsed.items });
269 switch (parsed.end) {
270 .column => continue,
271 .row => try rows.append(self.allocator, try cells.toOwnedSlice(self.allocator)),
272 .done => {
273 try rows.append(self.allocator, try cells.toOwnedSlice(self.allocator));
274 break;
275 },
276 }
277 }
278 const last = rows.items[rows.items.len - 1];
279 if (rows.items.len > 1 and last.len == 1 and last[0].row.len == 0) rows.items.len -= 1;
280 return .{ .table = .{ .rows = try rows.toOwnedSlice(self.allocator), .env = env } };
281 }
282
283 fn cell(self: *Parser, env_name: []const u8) Error!struct { items: []Node, end: CellEnd } {
284 var items: std.ArrayList(Node) = .empty;
285 while (true) {
286 const tok = try self.advance();
287 switch (tok) {
288 .end => return self.fail("missing '\\end'", self.tokens.start),
289 .close => return self.fail("unmatched '}'", self.tokens.start),
290 .caret => try self.attachScript(&items, true),
291 .underscore => try self.attachScript(&items, false),
292 .prime => try self.attachPrime(&items),
293 .open => {
294 const inner = try self.sequence(.close);
295 try items.append(self.allocator, .{ .row = inner });
296 },
297 .command => |name| {
298 if (std.mem.eql(u8, name, "\\")) {
299 return .{ .items = try items.toOwnedSlice(self.allocator), .end = .row };
300 }
301 if (std.mem.eql(u8, name, "end")) {
302 const mark = self.tokens.start;
303 const found = try self.rawGroup();
304 if (!std.mem.eql(u8, found, env_name)) return self.fail("mismatched '\\end'", mark);
305 return .{ .items = try items.toOwnedSlice(self.allocator), .end = .done };
306 }
307 if (std.mem.eql(u8, name, "right")) return self.fail("unmatched '\\right'", self.tokens.start);
308 try items.append(self.allocator, try self.command(name));
309 },
310 .char => |code| {
311 if (code == '&') {
312 return .{ .items = try items.toOwnedSlice(self.allocator), .end = .column };
313 }
314 try items.append(self.allocator, try self.charNode(code, true));
315 },
316 }
317 }
318 }
319
320 fn leftRight(self: *Parser) Error!Node {
321 const open_text = try self.fenceText();
322 const inner = try self.sequence(.right);
323 const close_text = try self.fenceText();
324 var items: std.ArrayList(Node) = .empty;
325 if (open_text) |text| {
326 try items.append(self.allocator, .{ .operator = .{ .text = text, .stretchy = true } });
327 }
328 try items.appendSlice(self.allocator, inner);
329 if (close_text) |text| {
330 try items.append(self.allocator, .{ .operator = .{ .text = text, .stretchy = true } });
331 }
332 return .{ .row = try items.toOwnedSlice(self.allocator) };
333 }
334
335 fn fenceText(self: *Parser) Error!?[]const u8 {
336 const tok = try self.advance();
337 switch (tok) {
338 .char => |code| return switch (code) {
339 '(' => "(",
340 ')' => ")",
341 '[' => "[",
342 ']' => "]",
343 '|' => "|",
344 '/' => "/",
345 '.' => null,
346 else => self.fail("invalid delimiter", self.tokens.start),
347 },
348 .command => |name| {
349 if (symbol.commands.get(name)) |entry| {
350 if (entry.kind == .fence) return entry.text;
351 }
352 return self.fail("invalid delimiter", self.tokens.start);
353 },
354 .end => return self.fail("missing delimiter", self.tokens.start),
355 else => return self.fail("invalid delimiter", self.tokens.start),
356 }
357 }
358
359 fn styledIdent(self: *Parser, style: symbol.Style) Error!Node {
360 const raw = try self.rawGroup();
361 const base = @intFromPtr(raw.ptr) - @intFromPtr(self.tokens.source.ptr);
362 var buffer: std.ArrayList(u8) = .empty;
363 for (raw, 0..) |char, index| {
364 if (char == ' ' or char == '\t') continue;
365 const code = symbol.styled(style, char) orelse return self.fail("unsupported styled letter", base + index);
366 var encoded: [4]u8 = undefined;
367 const length = std.unicode.utf8Encode(code, &encoded) catch return self.fail("unsupported styled letter", base + index);
368 try buffer.appendSlice(self.allocator, encoded[0..length]);
369 }
370 if (buffer.items.len == 0) return self.fail("empty argument", base);
371 return .{ .ident = .{ .text = try buffer.toOwnedSlice(self.allocator) } };
372 }
373
374 fn rawGroup(self: *Parser) Error![]const u8 {
375 const source = self.tokens.source;
376 var index = self.tokens.index;
377 while (index < source.len and (source[index] == ' ' or source[index] == '\t')) index += 1;
378 if (index >= source.len or source[index] != '{') return self.fail("expected '{'", index);
379 index += 1;
380 const start = index;
381 var depth: usize = 1;
382 while (index < source.len) : (index += 1) {
383 if (source[index] == '{') depth += 1;
384 if (source[index] == '}') {
385 depth -= 1;
386 if (depth == 0) break;
387 }
388 }
389 if (depth != 0) return self.fail("missing '}'", source.len);
390 self.tokens.index = index + 1;
391 return source[start..index];
392 }
393
394 fn charNode(self: *Parser, code: u21, run_numbers: bool) Error!Node {
395 if (code < 0x80) {
396 const byte: u8 = @intCast(code);
397 if (std.ascii.isAlphabetic(byte)) {
398 return .{ .ident = .{ .text = try self.allocator.dupe(u8, &.{byte}) } };
399 }
400 if (std.ascii.isDigit(byte)) {
401 if (run_numbers) return try self.numberNode(byte);
402 return .{ .number = try self.allocator.dupe(u8, &.{byte}) };
403 }
404 return switch (byte) {
405 '+' => .{ .operator = .{ .text = "+" } },
406 '-' => .{ .operator = .{ .text = "−" } },
407 '*' => .{ .operator = .{ .text = "∗" } },
408 '=' => .{ .operator = .{ .text = "=" } },
409 '<' => .{ .operator = .{ .text = "<" } },
410 '>' => .{ .operator = .{ .text = ">" } },
411 '(' => .{ .operator = .{ .text = "(", .rigid = true } },
412 ')' => .{ .operator = .{ .text = ")", .rigid = true } },
413 '[' => .{ .operator = .{ .text = "[", .rigid = true } },
414 ']' => .{ .operator = .{ .text = "]", .rigid = true } },
415 '|' => .{ .operator = .{ .text = "|", .rigid = true } },
416 '/' => .{ .operator = .{ .text = "/", .rigid = true } },
417 ',' => .{ .operator = .{ .text = "," } },
418 ';' => .{ .operator = .{ .text = ";" } },
419 ':' => .{ .operator = .{ .text = ":" } },
420 '!' => .{ .operator = .{ .text = "!" } },
421 '?' => .{ .operator = .{ .text = "?" } },
422 '.' => try self.dotNode(run_numbers),
423 '&' => self.fail("misplaced '&'", self.tokens.start),
424 else => self.fail("unsupported character", self.tokens.start),
425 };
426 }
427 var encoded: [4]u8 = undefined;
428 const length = std.unicode.utf8Encode(code, &encoded) catch return self.fail("unsupported character", self.tokens.start);
429 return .{ .ident = .{ .text = try self.allocator.dupe(u8, encoded[0..length]) } };
430 }
431
432 fn dotNode(self: *Parser, run_numbers: bool) Error!Node {
433 const source = self.tokens.source;
434 if (run_numbers and self.tokens.index < source.len and std.ascii.isDigit(source[self.tokens.index])) {
435 return try self.numberNode('.');
436 }
437 return .{ .operator = .{ .text = "." } };
438 }
439
440 fn numberNode(self: *Parser, first: u8) Error!Node {
441 var buffer: std.ArrayList(u8) = .empty;
442 try buffer.append(self.allocator, first);
443 const source = self.tokens.source;
444 while (self.tokens.index < source.len) {
445 const byte = source[self.tokens.index];
446 if (std.ascii.isDigit(byte)) {
447 try buffer.append(self.allocator, byte);
448 self.tokens.index += 1;
449 continue;
450 }
451 if (byte == '.' and self.tokens.index + 1 < source.len and std.ascii.isDigit(source[self.tokens.index + 1])) {
452 try buffer.append(self.allocator, '.');
453 self.tokens.index += 1;
454 continue;
455 }
456 break;
457 }
458 return .{ .number = try buffer.toOwnedSlice(self.allocator) };
459 }
460
461 fn heap(self: *Parser, node: Node) Error!*Node {
462 const slot = try self.allocator.create(Node);
463 slot.* = node;
464 return slot;
465 }
466 };
467
468 fn prefersLimits(node: Node) bool {
469 return node == .operator and node.operator.prefers_limits;
470 }
471
472 fn expectInvalid(source: []const u8, reason: []const u8, offset: usize) !void {
473 var diagnostic: Diagnostic = .{};
474 try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, source, &diagnostic));
475 try std.testing.expectEqualStrings(reason, diagnostic.reason);
476 try std.testing.expectEqual(offset, diagnostic.offset);
477 }
478
479 test "parse builds scripts around a shared base" {
480 var parsed = try parse(std.testing.allocator, "x_i^2", null);
481 defer parsed.deinit();
482 const items = parsed.root.row;
483 try std.testing.expectEqual(@as(usize, 1), items.len);
484 const scripts = items[0].scripts;
485 try std.testing.expectEqualStrings("x", scripts.base.ident.text);
486 try std.testing.expectEqualStrings("i", scripts.sub.?.ident.text);
487 try std.testing.expectEqualStrings("2", scripts.sup.?.number);
488 try std.testing.expect(!scripts.limits);
489 }
490
491 test "parse marks sum scripts as limit placed" {
492 var parsed = try parse(std.testing.allocator, "\\sum_{i}^{n}", null);
493 defer parsed.deinit();
494 const scripts = parsed.root.row[0].scripts;
495 try std.testing.expect(scripts.limits);
496 try std.testing.expectEqualStrings("∑", scripts.base.operator.text);
497 }
498
499 test "parse reads numbers fractions radicals and styled letters" {
500 var parsed = try parse(std.testing.allocator, "\\frac{3.14}{\\sqrt[3]{x}} \\mathbb{R}", null);
501 defer parsed.deinit();
502 const items = parsed.root.row;
503 try std.testing.expectEqual(@as(usize, 2), items.len);
504 try std.testing.expectEqualStrings("3.14", items[0].frac.first.row[0].number);
505 const radical = items[0].frac.second.row[0].radical;
506 try std.testing.expectEqualStrings("3", radical.index.?.row[0].number);
507 try std.testing.expectEqualStrings("x", radical.radicand.row[0].ident.text);
508 try std.testing.expectEqualStrings("ℝ", items[1].ident.text);
509 }
510
511 test "parse builds environment rows and cells" {
512 var parsed = try parse(std.testing.allocator, "\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}", null);
513 defer parsed.deinit();
514 const table = parsed.root.row[0].table;
515 try std.testing.expectEqualStrings("(", table.env.open.?);
516 try std.testing.expectEqualStrings(")", table.env.close.?);
517 try std.testing.expectEqual(@as(usize, 2), table.rows.len);
518 try std.testing.expectEqual(@as(usize, 2), table.rows[0].len);
519 try std.testing.expectEqualStrings("a", table.rows[0][0].row[0].ident.text);
520 try std.testing.expectEqualStrings("d", table.rows[1][1].row[0].ident.text);
521 }
522
523 test "parse drops a trailing empty environment row" {
524 var parsed = try parse(std.testing.allocator, "\\begin{cases} x & y \\\\ \\end{cases}", null);
525 defer parsed.deinit();
526 const table = parsed.root.row[0].table;
527 try std.testing.expectEqual(@as(usize, 1), table.rows.len);
528 try std.testing.expectEqual(@as(usize, 2), table.rows[0].len);
529 }
530
531 test "parse keeps empty cells and nests environments" {
532 var parsed = try parse(std.testing.allocator, "\\begin{matrix} & \\begin{pmatrix} 1 \\end{pmatrix} \\end{matrix}", null);
533 defer parsed.deinit();
534 const table = parsed.root.row[0].table;
535 try std.testing.expectEqual(@as(usize, 1), table.rows.len);
536 try std.testing.expectEqual(@as(usize, 2), table.rows[0].len);
537 try std.testing.expectEqual(@as(usize, 0), table.rows[0][0].row.len);
538 const inner = table.rows[0][1].row[0].table;
539 try std.testing.expectEqualStrings("1", inner.rows[0][0].row[0].number);
540 }
541
542 test "parse rejects malformed input" {
543 try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "", null));
544 try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "{x", null));
545 try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "^2", null));
546 try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "x^2^3", null));
547 try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "\\nonesuch", null));
548 try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "\\left( x", null));
549 try std.testing.expectError(error.InvalidEquation, parse(std.testing.allocator, "\\frac{1}", null));
550 }
551
552 test "parse names the offending span" {
553 try expectInvalid("", "empty equation", 0);
554 try expectInvalid("{x", "missing '}'", 2);
555 try expectInvalid("}", "unmatched '}'", 0);
556 try expectInvalid("^2", "superscript without a base", 0);
557 try expectInvalid("x_2_3", "double subscript", 3);
558 try expectInvalid("x^2^3", "double superscript", 3);
559 try expectInvalid("'", "prime without a base", 0);
560 try expectInvalid("\\nonesuch", "unknown command", 0);
561 try expectInvalid("\\left( x", "missing '\\right'", 8);
562 try expectInvalid("x \\right)", "unmatched '\\right'", 2);
563 try expectInvalid("\\left? x \\right)", "invalid delimiter", 5);
564 try expectInvalid("\\frac{1}", "missing argument", 8);
565 try expectInvalid("\\text x", "expected '{'", 6);
566 try expectInvalid("\\mathrm{oops", "missing '}'", 12);
567 try expectInvalid("\\mathbb{Ω}", "unsupported styled letter", 8);
568 try expectInvalid("\\mathbb{}", "empty argument", 8);
569 try expectInvalid("\\sqrt[3{x}", "missing ']'", 10);
570 try expectInvalid("x#y", "unsupported character", 1);
571 try expectInvalid("x\\", "incomplete command", 1);
572 }
573
574 test "parse names the offending environment span" {
575 try expectInvalid("\\begin pmatrix", "expected '{'", 7);
576 try expectInvalid("\\begin{nonesuch} x \\end{nonesuch}", "unknown environment", 7);
577 try expectInvalid("\\begin{pmatrix} 1", "missing '\\end'", 17);
578 try expectInvalid("\\begin{pmatrix} 1 \\end{cases}", "mismatched '\\end'", 18);
579 try expectInvalid("x \\end{pmatrix}", "unmatched '\\end'", 2);
580 try expectInvalid("a & b", "misplaced '&'", 2);
581 try expectInvalid("a \\\\ b", "misplaced '\\\\'", 2);
582 try expectInvalid("\\begin{matrix} {a & b} \\end{matrix}", "misplaced '&'", 18);
583 try expectInvalid("\\begin{matrix} \\left( a & b \\right) \\end{matrix}", "misplaced '&'", 24);
584 }