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 };