initial VT emulation table

This commit is contained in:
Mitchell Hashimoto
2022-04-18 09:38:52 -07:00
parent dc788ce5b2
commit 8d389b4ea9
3 changed files with 474 additions and 0 deletions

107
src/terminal/Parser.zig Normal file
View File

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

View File

@ -42,3 +42,7 @@ pub fn init(cols: usize, rows: usize) Terminal {
.cursor = .{ .x = 0, .y = 0 }, .cursor = .{ .x = 0, .y = 0 },
}; };
} }
test {
_ = @import("Parser.zig");
}

View File

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