//! Pratt parser for Tally expressions. //! //! Uses top-down operator precedence (Pratt parsing) to convert a token //! stream into an AST. Handles: //! - Operator precedence and associativity //! - Unary prefix operators (-, ~, not) //! - Function calls: identifier(args...) //! - Keyword operators: and, or, xor, rol, ror //! - ^ and ** are always exponentiation (XOR is the `xor` keyword) //! - Variable assignment: X = expr const std = @import("std"); const Allocator = std.mem.Allocator; const ast = @import("ast.zig"); const Expr = ast.Expr; const BinaryOp = ast.BinaryOp; const UnaryOp = ast.UnaryOp; const tokenizer_mod = @import("tokenizer.zig"); const Tokenizer = tokenizer_mod.Tokenizer; const TokenKind = tokenizer_mod.TokenKind; const Token = tokenizer_mod.Token; const parseNumber = tokenizer_mod.parseNumber; const types = @import("types.zig"); const Mode = types.Mode; const CalcError = types.CalcError; /// Precedence levels (higher = tighter binding). const Prec = enum(u8) { none = 0, assignment = 1, // = bit_or = 2, // | or bit_xor = 3, // xor bit_and = 4, // & and shift = 5, // << >> >>> rol ror additive = 6, // + - multiplicative = 7, // * / % power = 8, // ^ ** (always exponentiation) unary = 9, // - ~ not (prefix) call = 10, // function calls }; pub const Parser = struct { source: []const u8, tokenizer: Tokenizer, current: Token, previous: Token, mode: Mode, allocator: Allocator, had_error: bool, error_pos: ?usize, /// Nodes built so far, checked against `max_nodes`. node_count: usize, /// Current parseExpr/parsePrefix nesting, checked against `max_nest_depth`. nest_depth: usize, /// Ceiling on tree size. /// /// This is a stack-safety limit, not a style preference. Everything that walks /// a tree recurses on it (`evalExact`, `freeExpr`, `hasNonDecimalLiteral`), and /// a flat chain like `1+1+1+...` builds a left-deep tree whose depth equals the /// number of operators. Measured on this build: 3000 terms evaluated fine and /// 4000 terms segfaulted in `evalExact`, from about 8 KB of input. A thousand /// nodes is far more than any hand-written expression and leaves a wide margin. pub const max_nodes: usize = 1000; /// Ceiling on nesting, which bounds recursion inside the parser itself. /// Reached long before `max_nodes` by input like `((((...1...))))`. pub const max_nest_depth: usize = 128; pub fn init(allocator: Allocator, source: []const u8, mode: Mode) Parser { var tok = Tokenizer.init(source, mode); const first = tok.next(); return .{ .source = source, .tokenizer = tok, .current = first, .previous = .{ .kind = .eof, .start = 0, .len = 0 }, .mode = mode, .allocator = allocator, .had_error = false, .error_pos = null, .node_count = 0, .nest_depth = 0, }; } /// Parse a complete expression. Returns error if parsing fails. /// /// On success the caller owns the tree and must release it with `freeExpr`. /// On failure nothing is returned and nothing is left allocated: every error /// path below frees what it built. Without that, a single typo in an /// interactive session leaks the partial tree, which is exactly what the TUI /// does on every keystroke-completed expression. pub fn parse(self: *Parser) CalcError!*Expr { const expr = try self.parseExpr(.none); if (self.current.kind != .eof) { // Trailing tokens: the tree parsed so far is unreachable. freeExpr(self.allocator, expr); self.had_error = true; self.error_pos = self.current.start; return CalcError.UnexpectedToken; } return expr; } /// Parse an expression with the given minimum precedence. fn parseExpr(self: *Parser, min_prec: Prec) CalcError!*Expr { if (self.nest_depth >= max_nest_depth) return CalcError.InvalidExpression; self.nest_depth += 1; defer self.nest_depth -= 1; var left = try self.parsePrefix(); // Each successful parseInfix returns a node that has adopted `left`, so // this errdefer always covers the whole tree built so far. errdefer freeExpr(self.allocator, left); while (true) { const prec = self.infixPrecedence(self.current.kind); if (@intFromEnum(prec) <= @intFromEnum(min_prec)) break; left = try self.parseInfix(left, prec); } return left; } /// Parse a prefix expression (number, identifier, unary op, parenthesized). fn parsePrefix(self: *Parser) CalcError!*Expr { const tok = self.current; switch (tok.kind) { .number => { self.advance(); const text = tok.text(self.source); const num = parseNumber(text) catch return CalcError.InvalidNumber; return self.makeNode(.{ .number = .{ .float_value = num.float, .int_value = num.int_value, .base = num.base, .text = text, } }); }, .string_literal => { self.advance(); const text = tok.text(self.source); // Strip quotes: 'abc' -> abc if (text.len < 2) return CalcError.InvalidNumber; const content = text[1 .. text.len - 1]; return self.makeNode(.{ .string_literal = content }); }, .identifier => { self.advance(); const name = tok.text(self.source); // "not" prefix keyword = bitwise NOT if (std.mem.eql(u8, name, "not")) { const operand = try self.parseExpr(.unary); errdefer freeExpr(self.allocator, operand); return self.makeNode(.{ .unary = .{ .op = .bitwise_not, .operand = operand, } }); } // Check for assignment: identifier = expr if (self.current.kind == .equals) { self.advance(); const value = try self.parseExpr(.none); errdefer freeExpr(self.allocator, value); return self.makeNode(.{ .assignment = .{ .name = name, .value = value, } }); } // Check for function call: identifier(args) if (self.current.kind == .left_paren) { self.advance(); // consume ( var args = std.ArrayList(*Expr).empty; defer args.deinit(self.allocator); // Arguments parsed before the failure still own their trees. errdefer for (args.items) |arg| freeExpr(self.allocator, arg); if (self.current.kind != .right_paren) { const first_arg = try self.parseExpr(.none); args.append(self.allocator, first_arg) catch { freeExpr(self.allocator, first_arg); return CalcError.OutOfMemory; }; while (self.current.kind == .comma) { self.advance(); // consume , const arg = try self.parseExpr(.none); args.append(self.allocator, arg) catch { freeExpr(self.allocator, arg); return CalcError.OutOfMemory; }; } } if (self.current.kind != .right_paren) { self.had_error = true; self.error_pos = self.current.start; return CalcError.UnmatchedParen; } self.advance(); // consume ) const args_slice = self.allocator.dupe(*Expr, args.items) catch return CalcError.OutOfMemory; errdefer self.allocator.free(args_slice); return self.makeNode(.{ .call = .{ .name = name, .args = args_slice, } }); } // Check for keyword operators (rol, ror) - these are identifiers // that act as infix operators, handled in parseInfix via infixPrecedence // Only reach here if it's a plain variable reference. return self.makeNode(.{ .variable = name }); }, .left_paren => { self.advance(); // consume ( const inner = try self.parseExpr(.none); if (self.current.kind != .right_paren) { freeExpr(self.allocator, inner); self.had_error = true; self.error_pos = self.current.start; return CalcError.UnmatchedParen; } self.advance(); // consume ) return inner; }, .minus => { self.advance(); const operand = try self.parseExpr(.unary); errdefer freeExpr(self.allocator, operand); return self.makeNode(.{ .unary = .{ .op = .negate, .operand = operand, } }); }, .tilde => { self.advance(); const operand = try self.parseExpr(.unary); errdefer freeExpr(self.allocator, operand); return self.makeNode(.{ .unary = .{ .op = .bitwise_not, .operand = operand, } }); }, .eof => { self.had_error = true; self.error_pos = tok.start; return CalcError.UnexpectedEnd; }, else => { self.had_error = true; self.error_pos = tok.start; return CalcError.UnexpectedToken; }, } } /// Parse an infix expression given the left-hand side and precedence. fn parseInfix(self: *Parser, left: *Expr, prec: Prec) CalcError!*Expr { const tok = self.current; // Handle keyword operators (rol, ror, and, or, xor) if (tok.kind == .identifier) { const name = tok.text(self.source); if (keywordBinaryOp(name)) |op| { self.advance(); const right = try self.parseExpr(prec); // `left` belongs to the caller's errdefer until makeNode adopts // it, so only the right operand is released here. errdefer freeExpr(self.allocator, right); return self.makeNode(.{ .binary = .{ .op = op, .left = left, .right = right, } }); } } self.advance(); const op = self.tokenToBinaryOp(tok.kind) orelse { self.had_error = true; self.error_pos = tok.start; return CalcError.UnexpectedToken; }; // Right-associative for power const next_prec: Prec = if (op == .pow) @enumFromInt(@intFromEnum(prec) - 1) else prec; const right = try self.parseExpr(next_prec); errdefer freeExpr(self.allocator, right); return self.makeNode(.{ .binary = .{ .op = op, .left = left, .right = right, } }); } /// Map a keyword identifier to a binary operator, if it is one. fn keywordBinaryOp(name: []const u8) ?BinaryOp { if (std.mem.eql(u8, name, "rol")) return .rotate_left; if (std.mem.eql(u8, name, "ror")) return .rotate_right; if (std.mem.eql(u8, name, "and")) return .bit_and; if (std.mem.eql(u8, name, "or")) return .bit_or; if (std.mem.eql(u8, name, "xor")) return .bit_xor; return null; } /// Precedence of a keyword infix operator, if the name is one. fn keywordPrec(name: []const u8) ?Prec { if (std.mem.eql(u8, name, "rol") or std.mem.eql(u8, name, "ror")) return .shift; if (std.mem.eql(u8, name, "and")) return .bit_and; if (std.mem.eql(u8, name, "or")) return .bit_or; if (std.mem.eql(u8, name, "xor")) return .bit_xor; return null; } /// Get the infix precedence of a token kind. fn infixPrecedence(self: *Parser, kind: TokenKind) Prec { return switch (kind) { .pipe => .bit_or, .caret => .power, // always exponentiation .ampersand => .bit_and, .shift_left, .shift_right, .shift_right_logical => .shift, .plus, .minus => .additive, .star, .slash, .percent => .multiplicative, .star_star => .power, .identifier => keywordPrec(self.current.text(self.source)) orelse .none, else => .none, }; } /// Map a token kind to a binary operator. fn tokenToBinaryOp(self: *Parser, kind: TokenKind) ?BinaryOp { _ = self; return switch (kind) { .plus => .add, .minus => .sub, .star => .mul, .slash => .div, .percent => .mod, .caret => .pow, // always exponentiation .star_star => .pow, .ampersand => .bit_and, .pipe => .bit_or, .shift_left => .shift_left, .shift_right => .shift_right, .shift_right_logical => .shift_right_logical, else => null, }; } fn advance(self: *Parser) void { self.previous = self.current; self.current = self.tokenizer.next(); } fn makeNode(self: *Parser, expr: Expr) CalcError!*Expr { // Budget checked here so every construction site is covered by one test. if (self.node_count >= max_nodes) { self.had_error = true; self.error_pos = self.current.start; return CalcError.InvalidExpression; } self.node_count += 1; const node = self.allocator.create(Expr) catch return CalcError.OutOfMemory; node.* = expr; return node; } }; // -- Tests -- const testing = std.testing; // Error-path tests used to need an arena because a failed parse leaked its // partial tree. They no longer do (the parser cleans up after itself), but the // arena helper is kept for the tests already written against it. var test_arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); fn testParse(source: []const u8, mode: Mode) !*Expr { var parser = Parser.init(testing.allocator, source, mode); return parser.parse(); } fn testParseArena(source: []const u8, mode: Mode) CalcError!*Expr { const alloc = test_arena_instance.allocator(); var p = Parser.init(alloc, source, mode); return p.parse(); } /// Release a parsed tree. /// /// The parser hands ownership of the tree to the caller, so every successful /// `parse` needs a matching `freeExpr`. Error paths inside the parser clean up /// after themselves, so a failed parse leaves nothing to free. pub fn freeExpr(allocator: Allocator, expr: *Expr) void { switch (expr.*) { .number => {}, .string_literal => {}, .variable => {}, .unary => |u| freeExpr(allocator, u.operand), .binary => |b| { freeExpr(allocator, b.left); freeExpr(allocator, b.right); }, .call => |c| { for (c.args) |arg| freeExpr(allocator, arg); allocator.free(c.args); }, .assignment => |a| freeExpr(allocator, a.value), } allocator.destroy(expr); } test "parse simple number" { const expr = try testParse("42", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqual(@as(f64, 42.0), expr.number.float_value); try testing.expectEqual(@as(?u64, 42), expr.number.int_value); } test "parse hex number" { const expr = try testParse("0xFF", .programmer); defer freeExpr(testing.allocator, expr); try testing.expectEqual(@as(?u64, 255), expr.number.int_value); } test "parse addition" { const expr = try testParse("2 + 3", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqual(BinaryOp.add, expr.binary.op); try testing.expectEqual(@as(f64, 2.0), expr.binary.left.number.float_value); try testing.expectEqual(@as(f64, 3.0), expr.binary.right.number.float_value); } test "parse precedence: mul before add" { // 2 + 3 * 4 should parse as 2 + (3 * 4) const expr = try testParse("2 + 3 * 4", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqual(BinaryOp.add, expr.binary.op); try testing.expectEqual(@as(f64, 2.0), expr.binary.left.number.float_value); try testing.expectEqual(BinaryOp.mul, expr.binary.right.binary.op); } test "parse precedence: power right-associative" { // 2^3^4 should parse as 2^(3^4) const expr = try testParse("2^3^4", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqual(BinaryOp.pow, expr.binary.op); try testing.expectEqual(@as(f64, 2.0), expr.binary.left.number.float_value); try testing.expectEqual(BinaryOp.pow, expr.binary.right.binary.op); } test "parse unary negation" { const expr = try testParse("-5", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqual(UnaryOp.negate, expr.unary.op); try testing.expectEqual(@as(f64, 5.0), expr.unary.operand.number.float_value); } test "parse negation in expression" { // -2 + 3 should be (-2) + 3 const expr = try testParse("-2 + 3", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqual(BinaryOp.add, expr.binary.op); try testing.expectEqual(UnaryOp.negate, expr.binary.left.unary.op); } test "parse parentheses" { // (2 + 3) * 4 const expr = try testParse("(2 + 3) * 4", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqual(BinaryOp.mul, expr.binary.op); try testing.expectEqual(BinaryOp.add, expr.binary.left.binary.op); } test "parse function call" { const expr = try testParse("sin(3.14)", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqualStrings("sin", expr.call.name); try testing.expectEqual(@as(usize, 1), expr.call.args.len); try testing.expectApproxEqAbs(@as(f64, 3.14), expr.call.args[0].number.float_value, 1e-10); } test "parse multi-arg function call" { const expr = try testParse("max(1, 2, 3)", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqualStrings("max", expr.call.name); try testing.expectEqual(@as(usize, 3), expr.call.args.len); } test "parse variable" { const expr = try testParse("pi", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqualStrings("pi", expr.variable); } test "parse assignment" { const expr = try testParse("X = 42", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqualStrings("X", expr.assignment.name); try testing.expectEqual(@as(f64, 42.0), expr.assignment.value.number.float_value); } test "parse adjacent number and identifier is an error (no implicit mul)" { defer _ = test_arena_instance.reset(.retain_capacity); const result = testParseArena("2pi", .standard); try testing.expectError(CalcError.UnexpectedToken, result); } test "parse adjacent number and paren is an error (no implicit mul)" { defer _ = test_arena_instance.reset(.retain_capacity); const result = testParseArena("3(4+5)", .standard); try testing.expectError(CalcError.UnexpectedToken, result); } test "parse adjacent paren paren is an error (no implicit mul)" { defer _ = test_arena_instance.reset(.retain_capacity); const result = testParseArena("(2)(3)", .standard); try testing.expectError(CalcError.UnexpectedToken, result); } test "parse caret is power in programmer mode (not XOR)" { const expr = try testParse("0xF ^ 0x3", .programmer); defer freeExpr(testing.allocator, expr); try testing.expectEqual(BinaryOp.pow, expr.binary.op); } test "parse xor keyword is XOR" { const expr = try testParse("0xF xor 0x3", .programmer); defer freeExpr(testing.allocator, expr); try testing.expectEqual(BinaryOp.bit_xor, expr.binary.op); } test "parse and keyword" { const expr = try testParse("0xF and 0x3", .programmer); defer freeExpr(testing.allocator, expr); try testing.expectEqual(BinaryOp.bit_and, expr.binary.op); } test "parse or keyword" { const expr = try testParse("0xF or 0x3", .programmer); defer freeExpr(testing.allocator, expr); try testing.expectEqual(BinaryOp.bit_or, expr.binary.op); } test "parse not prefix keyword" { const expr = try testParse("not 0xFF", .programmer); defer freeExpr(testing.allocator, expr); try testing.expectEqual(UnaryOp.bitwise_not, expr.unary.op); } test "parse caret as power in standard mode" { const expr = try testParse("2 ^ 10", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqual(BinaryOp.pow, expr.binary.op); } test "parse ** as power in programmer mode" { const expr = try testParse("2 ** 10", .programmer); defer freeExpr(testing.allocator, expr); try testing.expectEqual(BinaryOp.pow, expr.binary.op); } test "parse bitwise operators" { const expr = try testParse("0xF & 0x3 | 0x1", .programmer); defer freeExpr(testing.allocator, expr); // | has lowest precedence of these, so: (0xF & 0x3) | 0x1 try testing.expectEqual(BinaryOp.bit_or, expr.binary.op); try testing.expectEqual(BinaryOp.bit_and, expr.binary.left.binary.op); } test "parse shift operators" { const expr = try testParse("1 << 4", .programmer); defer freeExpr(testing.allocator, expr); try testing.expectEqual(BinaryOp.shift_left, expr.binary.op); } test "parse bitwise not" { const expr = try testParse("~0xFF", .programmer); defer freeExpr(testing.allocator, expr); try testing.expectEqual(UnaryOp.bitwise_not, expr.unary.op); } test "parse error: unmatched paren" { defer _ = test_arena_instance.reset(.retain_capacity); const result = testParseArena("(2 + 3", .standard); try testing.expectError(CalcError.UnmatchedParen, result); } test "parse error: unexpected token" { defer _ = test_arena_instance.reset(.retain_capacity); const result = testParseArena("+ +", .standard); // + at start is not a valid prefix try testing.expectError(CalcError.UnexpectedToken, result); } test "parse error: empty expression" { defer _ = test_arena_instance.reset(.retain_capacity); const result = testParseArena("", .standard); try testing.expectError(CalcError.UnexpectedEnd, result); } test "parse complex expression" { // sin(2*pi) + 1 const expr = try testParse("sin(2*pi) + 1", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqual(BinaryOp.add, expr.binary.op); try testing.expectEqualStrings("sin", expr.binary.left.call.name); } test "parse nested function calls" { const expr = try testParse("max(sin(1), cos(2))", .standard); defer freeExpr(testing.allocator, expr); try testing.expectEqualStrings("max", expr.call.name); try testing.expectEqual(@as(usize, 2), expr.call.args.len); try testing.expectEqualStrings("sin", expr.call.args[0].call.name); try testing.expectEqualStrings("cos", expr.call.args[1].call.name); } test "parse error: unmatched paren in function call args" { defer _ = test_arena_instance.reset(.retain_capacity); const result = testParseArena("max(1, 2", .standard); try testing.expectError(CalcError.UnmatchedParen, result); } test "parse error: identifier in infix position (not a keyword op)" { // "5 foo": 5 parses as prefix, then foo is an identifier that is not // a keyword operator, so it has .none precedence. The loop stops and // parse() reports the leftover token as unexpected. defer _ = test_arena_instance.reset(.retain_capacity); const result = testParseArena("5 foo", .standard); try testing.expectError(CalcError.UnexpectedToken, result); } // -- Ownership on the error paths -- // // These run on testing.allocator rather than an arena, so a partial tree left // behind by a failed parse fails the test. Before the parser cleaned up after // itself, every one of these inputs leaked, which mattered in the TUI: a typo // at the prompt leaked the tree parsed up to that point. test "a failed parse leaves nothing allocated" { const bad = [_][]const u8{ "2 +", // missing right operand "1 2", // trailing token after a complete expression "(1 + 2", // unmatched paren "(1 + (2 * 3)", // unmatched outer paren, nested tree built "max(1, 2", // unterminated argument list "max(1, 2,", // trailing comma then end "sin(", // call with nothing in it "-", // unary with no operand "~", // bitwise not with no operand "not", // keyword not with no operand "x =", // assignment with no value "y = 1 +", // assignment whose value fails to parse "*", // operator in prefix position "", // empty input "1 + 2 3", // trailing token after a binary expression "-(1 + ", // nested failure under a unary }; for (bad) |source| { var parser = Parser.init(testing.allocator, source, .standard); if (parser.parse()) |expr| { freeExpr(testing.allocator, expr); std.debug.print("expected a parse error for \"{s}\"\n", .{source}); return error.TestUnexpectedResult; } else |_| {} } } test "a failed parse in programmer mode also leaves nothing allocated" { const bad = [_][]const u8{ "0xFF and", "1 rol", "not", "0b1010 xor (1", "1 << " }; for (bad) |source| { var parser = Parser.init(testing.allocator, source, .programmer); if (parser.parse()) |expr| { freeExpr(testing.allocator, expr); std.debug.print("expected a parse error for \"{s}\"\n", .{source}); return error.TestUnexpectedResult; } else |_| {} } } test "a successful parse hands over exactly one tree to free" { // The mirror of the above: freeing once must be enough and must not // double-free any shared node. const good = [_][]const u8{ "1 + 2 * 3", "-(4 + 5)", "max(1, min(2, 3))", "x = 2 ^ 3 ^ 4", "not 0xFF", "sqrt(2) + factorial(5)", "cagr(10000, 25000, 5)", "tvm_pmt(360, 0.5, 200000, 0)", }; for (good) |source| { var parser = Parser.init(testing.allocator, source, .standard); const expr = try parser.parse(); freeExpr(testing.allocator, expr); } } test "an allocation failure mid-parse frees whatever was built" { // The cleanup added for the error paths above is mostly reachable only when // an allocation fails partway through, so it is swept rather than assumed. // The wrapped testing.allocator reports any node the parser abandons. const sources = [_][]const u8{ "1 + 2 * 3", "-(1 + 2)", "~5", "not 7", "max(1, 2, 3)", "min(max(1, 2), 3)", "x = 1 + 2", "1 rol 2", "(((1)))", "cagr(10000, 25000, 5)", }; for (sources) |source| { var fail_index: usize = 0; while (fail_index < 64) : (fail_index += 1) { var failing = std.testing.FailingAllocator.init(testing.allocator, .{ .fail_index = fail_index }); const allocator = failing.allocator(); var parser = Parser.init(allocator, source, .standard); if (parser.parse()) |expr| { // Past the last allocation this input makes, so nothing is left // to fail; the tree itself must still be well formed. freeExpr(allocator, expr); break; } else |err| { try testing.expectEqual(CalcError.OutOfMemory, err); } } else { return error.AllocationSweepNeverCompleted; } } } // -- Size and depth limits -- // // Every tree walk in the engine recurses (evalExact, freeExpr, // hasNonDecimalLiteral), and a flat chain builds a left-deep tree whose depth is // the operator count. Before these limits, 4000 terms of "1+1+1+..." segfaulted // the process from roughly 8 KB of input, and 6000 nested parens crashed inside // the parser. test "a tree larger than the node budget is rejected, not built" { // One node per literal plus one per operator, so 1000 nodes is about 500 terms. var over = std.ArrayList(u8).empty; defer over.deinit(testing.allocator); for (0..900) |i| { if (i != 0) try over.appendSlice(testing.allocator, "+"); try over.appendSlice(testing.allocator, "1"); } var parser = Parser.init(testing.allocator, over.items, .standard); try testing.expectError(CalcError.InvalidExpression, parser.parse()); // Nothing is left allocated: testing.allocator would report a leak otherwise. } test "an expression within the node budget still parses" { var ok = std.ArrayList(u8).empty; defer ok.deinit(testing.allocator); for (0..400) |i| { if (i != 0) try ok.appendSlice(testing.allocator, "+"); try ok.appendSlice(testing.allocator, "1"); } var parser = Parser.init(testing.allocator, ok.items, .standard); const expr = try parser.parse(); defer freeExpr(testing.allocator, expr); try testing.expect(parser.node_count <= Parser.max_nodes); } test "nesting deeper than the depth limit is rejected" { var deep = std.ArrayList(u8).empty; defer deep.deinit(testing.allocator); for (0..Parser.max_nest_depth + 10) |_| try deep.append(testing.allocator, '('); try deep.append(testing.allocator, '1'); for (0..Parser.max_nest_depth + 10) |_| try deep.append(testing.allocator, ')'); var parser = Parser.init(testing.allocator, deep.items, .standard); try testing.expectError(CalcError.InvalidExpression, parser.parse()); } test "nesting within the depth limit parses" { var deep = std.ArrayList(u8).empty; defer deep.deinit(testing.allocator); for (0..64) |_| try deep.append(testing.allocator, '('); try deep.append(testing.allocator, '7'); for (0..64) |_| try deep.append(testing.allocator, ')'); var parser = Parser.init(testing.allocator, deep.items, .standard); const expr = try parser.parse(); defer freeExpr(testing.allocator, expr); try testing.expectEqual(@as(f64, 7.0), expr.number.float_value); } test "unbalanced deep nesting is rejected without leaking the partial tree" { var deep = std.ArrayList(u8).empty; defer deep.deinit(testing.allocator); for (0..8000) |_| try deep.append(testing.allocator, '('); try deep.append(testing.allocator, '1'); var parser = Parser.init(testing.allocator, deep.items, .standard); try testing.expect(if (parser.parse()) |_| false else |_| true); } test "the depth limit also covers nested calls and unary operators" { var deep = std.ArrayList(u8).empty; defer deep.deinit(testing.allocator); for (0..Parser.max_nest_depth + 10) |_| try deep.appendSlice(testing.allocator, "sqrt("); try deep.append(testing.allocator, '4'); for (0..Parser.max_nest_depth + 10) |_| try deep.append(testing.allocator, ')'); var parser = Parser.init(testing.allocator, deep.items, .standard); try testing.expectError(CalcError.InvalidExpression, parser.parse()); var unary = std.ArrayList(u8).empty; defer unary.deinit(testing.allocator); for (0..Parser.max_nest_depth + 10) |_| try unary.append(testing.allocator, '-'); try unary.append(testing.allocator, '1'); var unary_parser = Parser.init(testing.allocator, unary.items, .standard); try testing.expectError(CalcError.InvalidExpression, unary_parser.parse()); }