lib/syn/src/state.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! What one line of a document leaves open for the next: nothing, or an unfinished comment or
2 //! string together with the language that opened it. A scan that walks a document line by line has
3 //! to know whether the previous line ended inside a comment or string. That fact means something
4 //! only in the language that produced it: a C-style comment left open means nothing to a following
5 //! line of Python. Each kind of unfinished comment or string closes with its own delimiter, so the
6 //! record has to say which kind is open.
7 //!
8 //! The record (*continuation state*) pairs the open kind with the language that opened it, and a
9 //! line in any other language starts fresh. Eight kinds (*continuation mode*) cover every scanner,
10 //! and each language may leave open only the kinds its scanner produces, which the code checks with
11 //! assertions. The record holds a language and nothing about which document it came from, so a
12 //! caller that moves to another document clears it. A Lean model in `verification/syn` proves that
13 //! a record carried from another language gives the same result as a fresh scan, and property tests
14 //! check the Zig code against it.
15 //!
16 //! - *owning language*: the language that left the comment or string open.
17 const std = @import("std");
18 const syn = @import("root.zig");
19
20 const Language = syn.Language;
21
22 /// One kind of comment or string that is still open at the end of a line. Each mode has its own
23 /// closing delimiter, which the next line looks for. A caller reads it from `State.mode` to learn
24 /// which comment or string a line left open.
25 pub const Mode = enum {
26 /// An open `/*` comment, closed by `*/`. C, C++, C#, Java, JavaScript, TypeScript, Go, Rust,
27 /// PHP, Swift, Kotlin, Scala, Dart, Zig and CSS can leave it open.
28 block_comment,
29 /// An open Haskell `{-` comment, closed by `-}`.
30 haskell_comment,
31 /// An open Lua `--[[` comment, closed by `]]`.
32 lua_comment,
33 /// An open `<!--` comment in HTML, XML, Vue or Svelte, closed by `-->`.
34 markup_comment,
35 /// An open Python string that began with `'''`, closed by `'''`. The opening quotes may follow
36 /// a prefix such as `f` or `rb`.
37 python_triple_single,
38 /// An open Python string that began with `"""`, closed by `"""`.
39 python_triple_double,
40 /// An open TOML literal string that began with `'''`, closed by `'''`. A backslash has no
41 /// special meaning inside it.
42 toml_triple_single,
43 /// An open TOML basic string that began with `"""`, closed by `"""`. A backslash escapes the
44 /// next character, so `\"""` leaves the string open.
45 toml_triple_double,
46
47 /// Returns true when the scanner for `language` can leave this mode open. `State` checks it
48 /// before it stores a mode, and the tests use it to pin down which language can leave which
49 /// mode open. A test compares the answer for every language and mode against a fixed list of
50 /// pairs.
51 pub fn admittedBy(self: Mode, language: Language) bool {
52 return switch (self) {
53 .block_comment => switch (language) {
54 .zig,
55 .c,
56 .cpp,
57 .csharp,
58 .java,
59 .javascript,
60 .typescript,
61 .go,
62 .rust,
63 .php,
64 .swift,
65 .kotlin,
66 .scala,
67 .dart,
68 .css,
69 => true,
70 else => false,
71 },
72 .haskell_comment => language == .haskell,
73 .lua_comment => language == .lua,
74 .markup_comment => switch (language) {
75 .html, .xml, .vue, .svelte => true,
76 else => false,
77 },
78 .python_triple_single,
79 .python_triple_double,
80 => language == .python,
81 .toml_triple_single,
82 .toml_triple_double,
83 => language == .toml,
84 };
85 }
86 };
87
88 const Continuation = struct {
89 language: Language,
90 mode: Mode,
91 };
92
93 /// The continuation state: what the last scanned line left open. A caller keeps one for each
94 /// document and passes it to every `highlightLine` call for that document. The value `.{}` is the
95 /// fresh state for the start of a document. The struct holds no pointers and needs no cleanup. The
96 /// state records no document, so a caller that moves to another document calls `reset`.
97 pub const State = struct {
98 /// The open mode with its owning language, or null when nothing is open. The package changes it
99 /// only through `reset`, `rebind` and `retain`, which keep the mode admitted by its language.
100 continuation: ?Continuation = null,
101
102 /// Clears the state, so nothing is open, for a caller starting a new document.
103 pub fn reset(self: *State) void {
104 self.* = .{};
105 }
106
107 /// Keeps the open mode when `language` owns it, and clears the state otherwise, for
108 /// `highlightLine` at the start of each line so an open comment or string from another language
109 /// never reaches this one. The call asserts that a stored mode is admitted by its owning
110 /// language.
111 pub fn rebind(self: *State, language: Language) void {
112 const continuation = self.continuation orelse return;
113 std.debug.assert(continuation.mode.admittedBy(
114 continuation.language,
115 ));
116 if (continuation.language != language) self.reset();
117 }
118
119 /// Stores `next_mode` with `language` as its owner, or clears the state when `next_mode` is
120 /// null, for `highlightLine` after scanning a line to store what the line left open. The call
121 /// asserts that `language` admits `next_mode`.
122 pub fn retain(
123 self: *State,
124 language: Language,
125 next_mode: ?Mode,
126 ) void {
127 if (next_mode) |actual| {
128 std.debug.assert(actual.admittedBy(language));
129 }
130 self.continuation = if (next_mode) |actual|
131 .{ .language = language, .mode = actual }
132 else
133 null;
134 }
135
136 /// Returns the owning language, or null when nothing is open, for a caller or a test reading
137 /// which language left something open.
138 pub fn owner(self: *const State) ?Language {
139 const continuation = self.continuation orelse return null;
140 return continuation.language;
141 }
142
143 /// Returns the open mode, or null when nothing is open, for a caller or a test reading which
144 /// comment or string is open after a line.
145 pub fn mode(self: *const State) ?Mode {
146 const continuation = self.continuation orelse return null;
147 return continuation.mode;
148 }
149 };