diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig new file mode 100644 index 000000000..0272dc86a --- /dev/null +++ b/src/terminal/Parser.zig @@ -0,0 +1,107 @@ +//! VT-series parser for escape and control sequences. +//! +//! This is implemented directly as the state machine described on +//! vt100.net: https://vt100.net/emu/dec_ansi_parser +const Parser = @This(); + +const std = @import("std"); +const testing = std.testing; +const table = @import("parse_table.zig").table; + +/// States for the state machine +pub const State = enum { + anywhere, + ground, + escape, + escape_intermediate, + csi_entry, + csi_intermediate, + csi_param, + csi_ignore, + dcs_entry, + dcs_param, + dcs_intermediate, + dcs_passthrough, + dcs_ignore, + osc_string, + sos_pm_apc_string, +}; + +pub const Action = enum { + none, + ignore, + print, + execute, + clear, + collect, + param, + esc_dispatch, + csi_dispatch, + hook, + put, + unhook, + osc_start, + osc_put, + osc_end, +}; + +/// Current state of the state machine +state: State = .ground, + +pub fn init() Parser { + return .{}; +} + +pub fn next(self: *Parser, c: u8) void { + const effect = effect: { + // First look up the transition in the anywhere table. + const anywhere = table[c][@enumToInt(State.anywhere)]; + if (anywhere.state != .anywhere) break :effect anywhere; + + // If we don't have any transition from anywhere, use our state. + break :effect table[c][@enumToInt(self.state)]; + }; + + const next_state = effect.state; + const action = effect.action; + + // When going from one state to another, the actions take place in this order: + // + // 1. exit action from old state + // 2. transition action + // 3. entry action to new state + + // Perform exit actions. "The action associated with the exit event happens + // when an incoming symbol causes a transition from this state to another + // state (or even back to the same state)." + switch (self.state) { + .osc_string => {}, // TODO: osc_end + .dcs_passthrough => {}, // TODO: unhook + else => {}, + } + + // Perform the transition action + self.doAction(action); + + // Perform the entry action + // TODO: when _first_ entered only? + switch (self.state) { + .escape, .dcs_entry, .csi_entry => {}, // TODO: clear + .osc_string => {}, // TODO: osc_start + .dcs_passthrough => {}, // TODO: hook + else => {}, + } + + self.state = next_state; +} + +fn doAction(self: *Parser, action: Action) void { + _ = self; + _ = action; +} + +test { + var p = init(); + p.next(0x9E); + try testing.expect(p.state == .sos_pm_apc_string); +} diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index a12e86385..43f486e1b 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -42,3 +42,7 @@ pub fn init(cols: usize, rows: usize) Terminal { .cursor = .{ .x = 0, .y = 0 }, }; } + +test { + _ = @import("Parser.zig"); +} diff --git a/src/terminal/parse_table.zig b/src/terminal/parse_table.zig new file mode 100644 index 000000000..ccb2b9009 --- /dev/null +++ b/src/terminal/parse_table.zig @@ -0,0 +1,363 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const parser = @import("Parser.zig"); +const State = parser.State; +const Action = parser.Action; + +/// The state transition table. The type is [u8][State]Transition but +/// comptime-generated to be exactly-sized. +pub const table = genTable(); + +/// Table is the type of the state table. This is dynamically (comptime) +/// generated to be exactly sized. +pub const Table = genTableType(); + +// Transition is the transition to take within the table +pub const Transition = struct { + state: State, + action: Action, +}; + +/// Table is the type of the state transition table. +fn genTableType() type { + const max_u8 = std.math.maxInt(u8); + const stateInfo = @typeInfo(State); + const max_state = stateInfo.Enum.fields.len; + return [max_u8][max_state]Transition; +} + +/// Function to generate the full state transition table for VT emulation. +fn genTable() Table { + @setEvalBranchQuota(15000); + var result: Table = undefined; + + // In debug mode, we initialize everything so that we can detect if + // anything is overwritten. No value should be set more than once + // since the state machine diagram is exact. + if (builtin.mode == .Debug) { + var i: u8 = 0; + while (i < result.len) : (i += 1) { + var j: u8 = 0; + while (j < result[0].len) : (j += 1) { + result[i][j] = transition(.anywhere, .none); + } + } + } + + // ground + { + // anywhere => + single(&result, 0x18, .anywhere, .ground, .execute); + single(&result, 0x1A, .anywhere, .ground, .execute); + single(&result, 0x9C, .anywhere, .ground, .none); + + // events + single(&result, 0x19, .ground, .ground, .execute); + range(&result, 0, 0x17, .ground, .ground, .execute); + range(&result, 0x1C, 0x1F, .ground, .ground, .execute); + range(&result, 0x20, 0x7F, .ground, .ground, .print); + } + + // escape_intermediate + { + const source = State.escape_intermediate; + + single(&result, 0x19, source, source, .execute); + range(&result, 0, 0x17, source, source, .execute); + range(&result, 0x1C, 0x1F, source, source, .execute); + range(&result, 0x20, 0x2F, source, source, .collect); + single(&result, 0x7F, source, source, .ignore); + + // => ground + range(&result, 0x30, 0x7E, source, .ground, .esc_dispatch); + } + + // sos_pm_apc_string + { + const source = State.sos_pm_apc_string; + + // anywhere => + single(&result, 0x98, .anywhere, source, .none); + single(&result, 0x9E, .anywhere, source, .none); + single(&result, 0x9F, .anywhere, source, .none); + + // events + single(&result, 0x19, source, source, .ignore); + range(&result, 0, 0x17, source, source, .ignore); + range(&result, 0x1C, 0x1F, source, source, .ignore); + range(&result, 0x20, 0x7F, source, source, .ignore); + + // => ground + single(&result, 0x9C, source, .ground, .none); + } + + // escape + { + const source = State.escape; + + // anywhere => + single(&result, 0x1B, .anywhere, source, .none); + + // events + single(&result, 0x19, source, source, .execute); + range(&result, 0, 0x17, source, source, .execute); + range(&result, 0x1C, 0x1F, source, source, .execute); + single(&result, 0x7F, source, source, .ignore); + + // => ground + range(&result, 0x30, 0x4F, source, .ground, .esc_dispatch); + range(&result, 0x51, 0x57, source, .ground, .esc_dispatch); + range(&result, 0x60, 0x7E, source, .ground, .esc_dispatch); + single(&result, 0x59, source, .ground, .esc_dispatch); + single(&result, 0x5A, source, .ground, .esc_dispatch); + single(&result, 0x5C, source, .ground, .esc_dispatch); + + // => escape_intermediate + range(&result, 0x20, 0x2F, source, .escape_intermediate, .collect); + + // => sos_pm_apc_string + single(&result, 0x58, source, .sos_pm_apc_string, .none); + single(&result, 0x5E, source, .sos_pm_apc_string, .none); + single(&result, 0x5F, source, .sos_pm_apc_string, .none); + + // => dcs_entry + single(&result, 0x50, source, .dcs_entry, .none); + + // => csi_entry + single(&result, 0x5B, source, .csi_entry, .none); + + // => osc_string + single(&result, 0x5D, source, .osc_string, .none); + } + + // dcs_entry + { + const source = State.dcs_entry; + + // anywhere => + single(&result, 0x90, .anywhere, source, .ignore); + + // events + single(&result, 0x19, source, source, .ignore); + range(&result, 0, 0x17, source, source, .ignore); + range(&result, 0x1C, 0x1F, source, source, .ignore); + single(&result, 0x7F, source, source, .ignore); + + // => dcs_intermediate + range(&result, 0x20, 0x2F, source, .dcs_intermediate, .collect); + + // => dcs_ignore + single(&result, 0x3A, source, .dcs_ignore, .none); + + // => dcs_param + range(&result, 0x30, 0x39, source, .dcs_param, .param); + single(&result, 0x3B, source, .dcs_param, .param); + range(&result, 0x3C, 0x3F, source, .dcs_param, .collect); + + // => dcs_passthrough + range(&result, 0x40, 0x7E, source, .dcs_passthrough, .none); + } + + // dcs_intermediate + { + const source = State.dcs_intermediate; + + // events + single(&result, 0x19, source, source, .ignore); + range(&result, 0, 0x17, source, source, .ignore); + range(&result, 0x1C, 0x1F, source, source, .ignore); + range(&result, 0x20, 0x2F, source, source, .collect); + single(&result, 0x7F, source, source, .ignore); + + // => dcs_ignore + range(&result, 0x30, 0x3F, source, .dcs_ignore, .none); + + // => dcs_passthrough + range(&result, 0x40, 0x7E, source, .dcs_passthrough, .none); + } + + // dcs_ignore + { + const source = State.dcs_ignore; + + // events + single(&result, 0x19, source, source, .ignore); + range(&result, 0, 0x17, source, source, .ignore); + range(&result, 0x1C, 0x1F, source, source, .ignore); + + // => ground + single(&result, 0x9C, source, .ground, .none); + } + + // dcs_param + { + const source = State.dcs_param; + + // events + single(&result, 0x19, source, source, .ignore); + range(&result, 0, 0x17, source, source, .ignore); + range(&result, 0x1C, 0x1F, source, source, .ignore); + range(&result, 0x30, 0x39, source, source, .param); + single(&result, 0x3B, source, source, .param); + single(&result, 0x7F, source, source, .ignore); + + // => dcs_ignore + single(&result, 0x3A, source, .dcs_ignore, .none); + range(&result, 0x3C, 0x3F, source, .dcs_ignore, .none); + + // => dcs_intermediate + range(&result, 0x20, 0x2F, source, .dcs_intermediate, .collect); + + // => dcs_passthrough + range(&result, 0x40, 0x7E, source, .dcs_passthrough, .none); + } + + // dcs_passthrough + { + const source = State.dcs_passthrough; + + // events + single(&result, 0x19, source, source, .put); + range(&result, 0, 0x17, source, source, .put); + range(&result, 0x1C, 0x1F, source, source, .put); + range(&result, 0x20, 0x7E, source, source, .put); + single(&result, 0x7F, source, source, .ignore); + + // => ground + single(&result, 0x9C, source, .ground, .none); + } + + // csi_param + { + const source = State.csi_param; + + // events + single(&result, 0x19, source, source, .execute); + range(&result, 0, 0x17, source, source, .execute); + range(&result, 0x1C, 0x1F, source, source, .execute); + range(&result, 0x30, 0x39, source, source, .param); + single(&result, 0x3B, source, source, .param); + single(&result, 0x7F, source, source, .ignore); + + // => ground + range(&result, 0x40, 0x7E, source, .ground, .csi_dispatch); + + // => csi_ignore + single(&result, 0x3A, source, .csi_ignore, .none); + range(&result, 0x3C, 0x3F, source, .csi_ignore, .none); + + // => csi_intermediate + range(&result, 0x20, 0x2F, source, .csi_intermediate, .collect); + } + + // csi_ignore + { + const source = State.csi_ignore; + + // events + single(&result, 0x19, source, source, .execute); + range(&result, 0, 0x17, source, source, .execute); + range(&result, 0x1C, 0x1F, source, source, .execute); + range(&result, 0x20, 0x3F, source, source, .ignore); + single(&result, 0x7F, source, source, .ignore); + + // => ground + range(&result, 0x40, 0x7E, source, .ground, .none); + } + + // csi_intermediate + { + const source = State.csi_intermediate; + + // events + single(&result, 0x19, source, source, .execute); + range(&result, 0, 0x17, source, source, .execute); + range(&result, 0x1C, 0x1F, source, source, .execute); + range(&result, 0x20, 0x2F, source, source, .collect); + single(&result, 0x7F, source, source, .ignore); + + // => ground + range(&result, 0x40, 0x7E, source, .ground, .csi_dispatch); + + // => csi_ignore + range(&result, 0x30, 0x3F, source, .csi_ignore, .none); + } + + // csi_entry + { + const source = State.csi_entry; + + // anywhere => + single(&result, 0x9B, .anywhere, source, .none); + + // events + single(&result, 0x19, source, source, .execute); + range(&result, 0, 0x17, source, source, .execute); + range(&result, 0x1C, 0x1F, source, source, .execute); + single(&result, 0x7F, source, source, .ignore); + + // => ground + range(&result, 0x40, 0x7E, source, .ground, .csi_dispatch); + + // => csi_ignore + single(&result, 0x3A, source, .csi_ignore, .none); + + // => csi_intermediate + range(&result, 0x20, 0x2F, source, .csi_intermediate, .collect); + + // => csi_param + range(&result, 0x30, 0x39, source, .csi_param, .param); + single(&result, 0x3B, source, .csi_param, .param); + range(&result, 0x3C, 0x3F, source, .csi_param, .collect); + } + + // osc_string + { + const source = State.osc_string; + + // anywhere => + single(&result, 0x9D, .anywhere, source, .none); + + // events + single(&result, 0x19, source, source, .ignore); + range(&result, 0, 0x17, source, source, .ignore); + range(&result, 0x1C, 0x1F, source, source, .ignore); + range(&result, 0x20, 0x7F, source, source, .osc_put); + + // => ground + single(&result, 0x9C, source, .ground, .none); + } + + return result; +} + +fn single(t: *Table, c: u8, s0: State, s1: State, a: Action) void { + // In debug mode, we want to verify that every state is marked + // exactly once. + if (builtin.mode == .Debug) { + const existing = t[c][@enumToInt(s0)]; + if (existing.state != .anywhere) { + std.debug.print("transition set multiple times c={} s0={} existing={}", .{ + c, s0, existing, + }); + unreachable; + } + } + + t[c][@enumToInt(s0)] = transition(s1, a); +} + +fn range(t: *Table, from: u8, to: u8, s0: State, s1: State, a: Action) void { + var i = from; + while (i <= to) : (i += 1) single(t, i, s0, s1, a); +} + +fn transition(state: State, action: Action) Transition { + return .{ .state = state, .action = action }; +} + +test { + // This forces comptime-evaluation of table, so we're just testing + // that it succeeds in creation. + _ = table; +}