tiny.python.source.lexer
Defined in source.
The lexer reads the text of a Python program once, from start to end, and turns it into a list of tokens that ends with an end-of-input token.
API (4)
Actions
Public operations.
Stream.deinit: Frees the token list with the given allocator.tokenize: Reads the source text and returns its tokens as aStream.
Types and contracts
Public types and contracts.
Error: The errorstokenizereturns for text the subset cannot read, besides running out of memory.Stream: The token list thattokenizereturns, for the caller to pass toparse.
Source
Source: lib/python/src/source/lexer.zig
zig
//! The lexer reads the text of a Python program once, from start to end, and turns it into a list//! of tokens that ends with an end-of-input token. The lexer has to recover the program's block//! structure from indentation. The lexer has to drop comments. For text outside the package's//! subset, the lexer has to return an error that names the problem.//!//! Python marks blocks by indentation alone. A block starts where lines become more indented and//! ends where they return to an outer level. The lexer therefore has to remember the indentation of//! every block still open.//!//! The lexical analysis in the [Python 3.14 language//! reference](https://docs.python.org/3.14/reference/) tracks indentation with a stack of levels.//! The lexer keeps the same stack, and the stack starts with the single level zero. A line indented//! deeper than the top of the stack pushes its level and yields an indent token. A line indented//! less pops every deeper level and yields a dedent token for each. The line's indentation has to//! match a level still on the stack. At the end of the text, every level still open is popped with//! a dedent token of its own.//!//! Indentation counts spaces only, and a tab in a line's indentation fails with//! `InvalidIndentation`. A newline token marks every line break, including the breaks of blank and//! comment-only lines and breaks inside parentheses, brackets and braces. The lexer knows 19//! keywords, the ones the subset uses. The lexer reads every other word as a name, even a word//! Python reserves, such as `class` or `import`. An integer is a run of decimal digits. A string//! sits on one line between single or double quotes. A string's bytes stay as written, backslashes//! included. A name starts with an ASCII letter or underscore and continues with ASCII letters,//! digits and underscores. A byte that starts no token of the subset can appear only inside a//! string or a comment.//!//! The tokens record byte offsets into the text and keep no pointer to it, so reading a token's//! characters takes the text again. The lexer's entry point, `tokenize`, frees its stack of//! indentation levels before it returns. The only memory the caller receives is the token list//! (`Stream`).const std = @import("std");const token = @import("token.zig");/// The errors `tokenize` returns for text the subset cannot read, besides running out of memory. A/// caller switches on this error set to report why the text could not be read. An error carries no/// position in the text.pub const Error = error{ /// A byte that starts no token of the subset, such as `/`, `%`, `;`, a backslash, or a byte /// above 127 outside a string or comment, or a `!` that no `=` follows. InvalidCharacter, /// A tab in a line's indentation, or a line whose indentation drops below the current block's /// and matches no enclosing block's level. InvalidIndentation, /// A string literal that reaches a line break or the end of the text before its closing quote. UnterminatedString,};/// The token list that `tokenize` returns, for the caller to pass to `parse`. A stream owns the/// memory of its tokens. The caller frees a stream with `deinit` and the allocator it gave/// `tokenize`. The syntax tree that `parse` builds keeps no pointer into the token list, so the/// stream can be freed once `parse` returns.pub const Stream = struct { /// The tokens in source order, ending with the end-of-input token. The slice is one allocation /// from the allocator passed to `tokenize`. tokens: []const token.Token, /// Frees the token list with the given allocator. The package's `execute` calls this function /// as it returns, after the program has run. The allocator has to be the one passed to /// `tokenize`. The call leaves the stream undefined. pub fn deinit(self: *Stream, allocator: std.mem.Allocator) void { allocator.free(self.tokens); self.* = undefined; }};/// Reads the source text and returns its tokens as a `Stream`. The package's `execute` calls this/// function first, and a caller that wants the tokens of a program calls it directly. The function/// borrows the text for the call alone: the tokens record byte offsets and keep no pointer to it./// The function allocates the token list from the given allocator and returns it as one slice. The/// function also allocates a stack of indentation levels, which it frees before returning. The/// caller owns the returned stream and frees it with `Stream.deinit` and the same allocator. The/// function returns `InvalidCharacter`, `InvalidIndentation` or `UnterminatedString` for text it/// cannot read. The function returns `error.OutOfMemory` when an allocation fails. On any error it/// frees everything it allocated. The list ends with a dedent token for each block still open and/// then the end-of-input token. Empty text yields the end-of-input token alone.pub fn tokenize(allocator: std.mem.Allocator, source: []const u8) (Error || std.mem.Allocator.Error)!Stream { var lexer = Lexer{ .source = source, .tokens = .empty, .indents = .empty, }; errdefer lexer.tokens.deinit(allocator); errdefer lexer.indents.deinit(allocator); try lexer.indents.append(allocator, 0); try lexer.run(allocator); const tokens = try lexer.tokens.toOwnedSlice(allocator); lexer.indents.deinit(allocator); return .{ .tokens = tokens };}const Lexer = struct { source: []const u8, index: usize = 0, tokens: std.ArrayListUnmanaged(token.Token), indents: std.ArrayListUnmanaged(usize), line_start: bool = true, fn run(self: *Lexer, allocator: std.mem.Allocator) (Error || std.mem.Allocator.Error)!void { while (self.index < self.source.len) { if (self.line_start) try self.indentation(allocator); if (self.index >= self.source.len) break; const c = self.source[self.index]; switch (c) { ' ', '\t' => self.index += 1, '\n' => try self.newline(allocator, 1), '\r' => try self.carriage(allocator), '#' => self.comment(), '+' => try self.simple(allocator, .plus, 1), '-' => try self.simple(allocator, .minus, 1), '*' => try self.simple(allocator, .star, 1), '=' => try self.singleOrEqual(allocator, .equal, .equal_equal), '!' => try self.bang(allocator), '<' => try self.singleOrEqual(allocator, .less, .less_equal), '>' => try self.singleOrEqual(allocator, .greater, .greater_equal), ',' => try self.simple(allocator, .comma, 1), ':' => try self.simple(allocator, .colon, 1), '.' => try self.simple(allocator, .dot, 1), '(' => try self.simple(allocator, .lparen, 1), ')' => try self.simple(allocator, .rparen, 1), '[' => try self.simple(allocator, .lbracket, 1), ']' => try self.simple(allocator, .rbracket, 1), '{' => try self.simple(allocator, .lbrace, 1), '}' => try self.simple(allocator, .rbrace, 1), '\'', '"' => try self.string(allocator), '0'...'9' => try self.integer(allocator), 'A'...'Z', 'a'...'z', '_' => try self.identifier(allocator), else => return Error.InvalidCharacter, } } while (self.indents.items.len > 1) { _ = self.indents.pop(); try self.tokens.append(allocator, .{ .tag = .dedent, .span = .{ .start = self.source.len, .end = self.source.len }, }); } try self.tokens.append(allocator, .{ .tag = .eof, .span = .{ .start = self.source.len, .end = self.source.len }, }); } fn indentation(self: *Lexer, allocator: std.mem.Allocator) (Error || std.mem.Allocator.Error)!void { const start = self.index; var width: usize = 0; while (self.index < self.source.len and self.source[self.index] == ' ') { self.index += 1; width += 1; } if (self.index < self.source.len and self.source[self.index] == '\t') return Error.InvalidIndentation; if (self.index >= self.source.len or self.source[self.index] == '\n' or self.source[self.index] == '\r' or self.source[self.index] == '#') return; const current = self.indents.items[self.indents.items.len - 1]; if (width > current) { try self.indents.append(allocator, width); try self.tokens.append(allocator, .{ .tag = .indent, .span = .{ .start = start, .end = self.index }, }); } else if (width < current) { while (self.indents.items.len > 1 and width < self.indents.items[self.indents.items.len - 1]) { _ = self.indents.pop(); try self.tokens.append(allocator, .{ .tag = .dedent, .span = .{ .start = start, .end = self.index }, }); } if (width != self.indents.items[self.indents.items.len - 1]) return Error.InvalidIndentation; } self.line_start = false; } fn simple(self: *Lexer, allocator: std.mem.Allocator, tag: token.Tag, width: usize) std.mem.Allocator.Error!void { const start = self.index; self.index += width; self.line_start = false; try self.tokens.append(allocator, .{ .tag = tag, .span = .{ .start = start, .end = self.index }, }); } fn singleOrEqual(self: *Lexer, allocator: std.mem.Allocator, single: token.Tag, equal: token.Tag) std.mem.Allocator.Error!void { const start = self.index; self.index += 1; const tag = if (self.index < self.source.len and self.source[self.index] == '=') blk: { self.index += 1; break :blk equal; } else single; self.line_start = false; try self.tokens.append(allocator, .{ .tag = tag, .span = .{ .start = start, .end = self.index }, }); } fn bang(self: *Lexer, allocator: std.mem.Allocator) (Error || std.mem.Allocator.Error)!void { const start = self.index; self.index += 1; if (self.index >= self.source.len or self.source[self.index] != '=') return Error.InvalidCharacter; self.index += 1; self.line_start = false; try self.tokens.append(allocator, .{ .tag = .bang_equal, .span = .{ .start = start, .end = self.index }, }); } fn carriage(self: *Lexer, allocator: std.mem.Allocator) std.mem.Allocator.Error!void { const start = self.index; self.index += 1; if (self.index < self.source.len and self.source[self.index] == '\n') { self.index += 1; } try self.tokens.append(allocator, .{ .tag = .newline, .span = .{ .start = start, .end = self.index }, }); self.line_start = true; } fn newline(self: *Lexer, allocator: std.mem.Allocator, width: usize) std.mem.Allocator.Error!void { const start = self.index; self.index += width; try self.tokens.append(allocator, .{ .tag = .newline, .span = .{ .start = start, .end = self.index }, }); self.line_start = true; } fn comment(self: *Lexer) void { while (self.index < self.source.len and self.source[self.index] != '\n' and self.source[self.index] != '\r') { self.index += 1; } } fn integer(self: *Lexer, allocator: std.mem.Allocator) std.mem.Allocator.Error!void { const start = self.index; while (self.index < self.source.len and isDigit(self.source[self.index])) { self.index += 1; } self.line_start = false; try self.tokens.append(allocator, .{ .tag = .integer, .span = .{ .start = start, .end = self.index }, }); } fn string(self: *Lexer, allocator: std.mem.Allocator) (Error || std.mem.Allocator.Error)!void { const quote = self.source[self.index]; const start = self.index; self.index += 1; while (self.index < self.source.len and self.source[self.index] != quote) { if (self.source[self.index] == '\n' or self.source[self.index] == '\r') return Error.UnterminatedString; self.index += 1; } if (self.index >= self.source.len) return Error.UnterminatedString; self.index += 1; self.line_start = false; try self.tokens.append(allocator, .{ .tag = .string, .span = .{ .start = start, .end = self.index }, }); } fn identifier(self: *Lexer, allocator: std.mem.Allocator) std.mem.Allocator.Error!void { const start = self.index; while (self.index < self.source.len and isIdentifierContinue(self.source[self.index])) { self.index += 1; } const text = self.source[start..self.index]; self.line_start = false; try self.tokens.append(allocator, .{ .tag = keyword(text), .span = .{ .start = start, .end = self.index }, }); }};fn keyword(text: []const u8) token.Tag { if (std.mem.eql(u8, text, "and")) return .and_kw; if (std.mem.eql(u8, text, "break")) return .break_kw; if (std.mem.eql(u8, text, "continue")) return .continue_kw; if (std.mem.eql(u8, text, "del")) return .del_kw; if (std.mem.eql(u8, text, "def")) return .def_kw; if (std.mem.eql(u8, text, "elif")) return .elif_kw; if (std.mem.eql(u8, text, "else")) return .else_kw; if (std.mem.eql(u8, text, "for")) return .for_kw; if (std.mem.eql(u8, text, "if")) return .if_kw; if (std.mem.eql(u8, text, "in")) return .in_kw; if (std.mem.eql(u8, text, "is")) return .is_kw; if (std.mem.eql(u8, text, "not")) return .not_kw; if (std.mem.eql(u8, text, "or")) return .or_kw; if (std.mem.eql(u8, text, "pass")) return .pass_kw; if (std.mem.eql(u8, text, "return")) return .return_kw; if (std.mem.eql(u8, text, "while")) return .while_kw; if (std.mem.eql(u8, text, "True")) return .true_kw; if (std.mem.eql(u8, text, "False")) return .false_kw; if (std.mem.eql(u8, text, "None")) return .none_kw; return .identifier;}fn isDigit(c: u8) bool { return c >= '0' and c <= '9';}fn isIdentifierContinue(c: u8) bool { return isDigit(c) or (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or c == '_';}test "tokenize arithmetic assignment" { var stream = try tokenize(std.testing.allocator, "x = 1 + 2\n"); defer stream.deinit(std.testing.allocator); try std.testing.expectEqual(token.Tag.identifier, stream.tokens[0].tag); try std.testing.expectEqual(token.Tag.equal, stream.tokens[1].tag); try std.testing.expectEqual(token.Tag.integer, stream.tokens[2].tag); try std.testing.expectEqual(token.Tag.plus, stream.tokens[3].tag); try std.testing.expectEqual(token.Tag.integer, stream.tokens[4].tag); try std.testing.expectEqual(token.Tag.newline, stream.tokens[5].tag); try std.testing.expectEqual(token.Tag.eof, stream.tokens[6].tag);}test "tokenize keywords and comments" { var stream = try tokenize(std.testing.allocator, "True # hidden\nFalse None"); defer stream.deinit(std.testing.allocator); try std.testing.expectEqual(token.Tag.true_kw, stream.tokens[0].tag); try std.testing.expectEqual(token.Tag.newline, stream.tokens[1].tag); try std.testing.expectEqual(token.Tag.false_kw, stream.tokens[2].tag); try std.testing.expectEqual(token.Tag.none_kw, stream.tokens[3].tag);}test "tokenize function indentation" { var stream = try tokenize(std.testing.allocator, \\def f(x): \\ return x \\f(1) ); defer stream.deinit(std.testing.allocator); try std.testing.expectEqual(token.Tag.def_kw, stream.tokens[0].tag); try std.testing.expectEqual(token.Tag.identifier, stream.tokens[1].tag); try std.testing.expectEqual(token.Tag.lparen, stream.tokens[2].tag); try std.testing.expectEqual(token.Tag.identifier, stream.tokens[3].tag); try std.testing.expectEqual(token.Tag.rparen, stream.tokens[4].tag); try std.testing.expectEqual(token.Tag.colon, stream.tokens[5].tag); try std.testing.expectEqual(token.Tag.newline, stream.tokens[6].tag); try std.testing.expectEqual(token.Tag.indent, stream.tokens[7].tag); try std.testing.expectEqual(token.Tag.return_kw, stream.tokens[8].tag); try std.testing.expectEqual(token.Tag.dedent, stream.tokens[11].tag);}test "tokenize control flow and comparisons" { var stream = try tokenize(std.testing.allocator, \\if x <= 3: \\ pass \\else: \\ y = x != 4 ); defer stream.deinit(std.testing.allocator); try std.testing.expectEqual(token.Tag.if_kw, stream.tokens[0].tag); try std.testing.expectEqual(token.Tag.less_equal, stream.tokens[2].tag); try std.testing.expectEqual(token.Tag.pass_kw, stream.tokens[7].tag); try std.testing.expectEqual(token.Tag.else_kw, stream.tokens[10].tag); try std.testing.expectEqual(token.Tag.bang_equal, stream.tokens[17].tag);}test "tokenize loop control" { var stream = try tokenize(std.testing.allocator, \\for x in [1]: \\ break \\ continue ); defer stream.deinit(std.testing.allocator); try std.testing.expectEqual(token.Tag.for_kw, stream.tokens[0].tag); try std.testing.expectEqual(token.Tag.identifier, stream.tokens[1].tag); try std.testing.expectEqual(token.Tag.in_kw, stream.tokens[2].tag); try std.testing.expectEqual(token.Tag.break_kw, stream.tokens[9].tag); try std.testing.expectEqual(token.Tag.continue_kw, stream.tokens[11].tag);}test "tokenize deletion keyword" { var stream = try tokenize(std.testing.allocator, "del x"); defer stream.deinit(std.testing.allocator); try std.testing.expectEqual(token.Tag.del_kw, stream.tokens[0].tag); try std.testing.expectEqual(token.Tag.identifier, stream.tokens[1].tag);}test "tokenize attribute references" { var stream = try tokenize(std.testing.allocator, "xs.append"); defer stream.deinit(std.testing.allocator); try std.testing.expectEqual(token.Tag.identifier, stream.tokens[0].tag); try std.testing.expectEqual(token.Tag.dot, stream.tokens[1].tag); try std.testing.expectEqual(token.Tag.identifier, stream.tokens[2].tag);}test "tokenize logical operators" { var stream = try tokenize(std.testing.allocator, "not True and False or None is"); defer stream.deinit(std.testing.allocator); try std.testing.expectEqual(token.Tag.not_kw, stream.tokens[0].tag); try std.testing.expectEqual(token.Tag.true_kw, stream.tokens[1].tag); try std.testing.expectEqual(token.Tag.and_kw, stream.tokens[2].tag); try std.testing.expectEqual(token.Tag.false_kw, stream.tokens[3].tag); try std.testing.expectEqual(token.Tag.or_kw, stream.tokens[4].tag); try std.testing.expectEqual(token.Tag.none_kw, stream.tokens[5].tag); try std.testing.expectEqual(token.Tag.is_kw, stream.tokens[6].tag);}test "tokenize strings" { var stream = try tokenize(std.testing.allocator, \\"alpha" 'beta' ); defer stream.deinit(std.testing.allocator); try std.testing.expectEqual(token.Tag.string, stream.tokens[0].tag); try std.testing.expectEqualStrings("\"alpha\"", stream.tokens[0].span.text("\"alpha\" 'beta'")); try std.testing.expectEqual(token.Tag.string, stream.tokens[1].tag);}test "tokenize brackets and braces" { var stream = try tokenize(std.testing.allocator, "[1, 2][0] {\"a\": 1}"); defer stream.deinit(std.testing.allocator); try std.testing.expectEqual(token.Tag.lbracket, stream.tokens[0].tag); try std.testing.expectEqual(token.Tag.integer, stream.tokens[1].tag); try std.testing.expectEqual(token.Tag.comma, stream.tokens[2].tag); try std.testing.expectEqual(token.Tag.integer, stream.tokens[3].tag); try std.testing.expectEqual(token.Tag.rbracket, stream.tokens[4].tag); try std.testing.expectEqual(token.Tag.lbracket, stream.tokens[5].tag); try std.testing.expectEqual(token.Tag.integer, stream.tokens[6].tag); try std.testing.expectEqual(token.Tag.rbracket, stream.tokens[7].tag); try std.testing.expectEqual(token.Tag.lbrace, stream.tokens[8].tag); try std.testing.expectEqual(token.Tag.string, stream.tokens[9].tag); try std.testing.expectEqual(token.Tag.colon, stream.tokens[10].tag); try std.testing.expectEqual(token.Tag.integer, stream.tokens[11].tag); try std.testing.expectEqual(token.Tag.rbrace, stream.tokens[12].tag);}test "reject unterminated strings" { try std.testing.expectError(Error.UnterminatedString, tokenize(std.testing.allocator, "\"alpha"));}test "tokenize releases indentation storage once when token transfer fails" { const source = "if x:\n pass"; var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{ .resize_fail_index = 0, }); const counting_allocator = counting.allocator(); var stream = try tokenize(counting_allocator, source); stream.deinit(counting_allocator); try std.testing.expect(counting.alloc_index > 0); var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = counting.alloc_index - 1, .resize_fail_index = 0, }); try std.testing.expectError(error.OutOfMemory, tokenize(failing.allocator(), source)); try std.testing.expect(failing.has_induced_failure);}test "tokenize elif" { var stream = try tokenize(std.testing.allocator, \\if a: \\ pass \\elif b: \\ pass ); defer stream.deinit(std.testing.allocator); try std.testing.expectEqual(token.Tag.if_kw, stream.tokens[0].tag); try std.testing.expectEqual(token.Tag.elif_kw, stream.tokens[8].tag);}Source: lib/python/src/source/root.zig:11
zig
pub const lexer = @import("lexer.zig");Complete caller list for source.lexer.tokenize
13 direct callers.
lib.python.src.source.lexer.test_reject_unterminated_strings[function] — test source atlib/python/src/source/lexer.zig:446in nearest public ownertiny.python.source.lexerlib.python.src.source.lexer.test_tokenize_arithmetic_assignment[function] — test source atlib/python/src/source/lexer.zig:312in nearest public ownertiny.python.source.lexerlib.python.src.source.lexer.test_tokenize_attribute_references[function] — test source atlib/python/src/source/lexer.zig:394in nearest public ownertiny.python.source.lexerlib.python.src.source.lexer.test_tokenize_brackets_and_braces[function] — test source atlib/python/src/source/lexer.zig:427in nearest public ownertiny.python.source.lexerlib.python.src.source.lexer.test_tokenize_control_flow_and_comparisons[function] — test source atlib/python/src/source/lexer.zig:355in nearest public ownertiny.python.source.lexerlib.python.src.source.lexer.test_tokenize_deletion_keyword[function] — test source atlib/python/src/source/lexer.zig:386in nearest public ownertiny.python.source.lexerlib.python.src.source.lexer.test_tokenize_elif[function] — test source atlib/python/src/source/lexer.zig:468in nearest public ownertiny.python.source.lexerlib.python.src.source.lexer.test_tokenize_function_indentation[function] — test source atlib/python/src/source/lexer.zig:335in nearest public ownertiny.python.source.lexerlib.python.src.source.lexer.test_tokenize_keywords_and_comments[function] — test source atlib/python/src/source/lexer.zig:325in nearest public ownertiny.python.source.lexerlib.python.src.source.lexer.test_tokenize_logical_operators[function] — test source atlib/python/src/source/lexer.zig:403in nearest public ownertiny.python.source.lexerlib.python.src.source.lexer.test_tokenize_loop_control[function] — test source atlib/python/src/source/lexer.zig:371in nearest public ownertiny.python.source.lexerlib.python.src.source.lexer.test_tokenize_releases_indentation_storage_once_when_token_transfer_fails[function] — test source atlib/python/src/source/lexer.zig:450in nearest public ownertiny.python.source.lexerlib.python.src.source.lexer.test_tokenize_strings[function] — test source atlib/python/src/source/lexer.zig:416in nearest public ownertiny.python.source.lexer
Audit
| Definitions | 5 |
|---|---|
| Public names | 8 |
| Members | 4 |
| Version | 26.7.0 |
| Revision | daab053ee433 |