• Thiago de Arruda's avatar
    main: Start modeling Nvim as pushdown automaton · 8d93621c
    Thiago de Arruda authored
    From a very high level point of view, Vim/Nvim can be described as state
    machines following these instructions in a loop:
    - Read user input
    - Peform some action. The action is determined by the current state and can
      switch states.
    - Possibly display some feedback to the user.
    This is not immediately visible because these instructions spread across dozens
    of nested loops and function calls, making it very hard to modify the state
    machine(to support more event types, for example).
    So far, the approach Nvim has taken to allow more events is this:
    - At the very core function that blocks for input, poll for arbitrary events.
    - If the event received from the OS is user input, just return it normally to
      the callers.
    - If the event is not direct result of user input(possibly a vimscript function
      call coming from a msgpack-rpc socket or a job control callback), return a
      special key code(`K_EVENT`) that is handled by callers where it is safer to
      perform arbitrary actions.
    One problem with this approach is that the `K_EVENT` signal is being sent across
    multiple states that may be unaware of it. This was partially fixed with the
    `input_enable_events`/`input_disable_events` functions, which were added as a
    mechanism that the upper layers can use to tell the core input functions that it
    is ready to accept `K_EVENT`.
    Another problem is that the mapping engine is implemented in getchar.c
    which is called from every state, but the mapping engine is not aware of
    `K_EVENT` so events can break mappings.
    While it is theoretically possible to modify getchar.c to make it aware of
    `K_EVENT`, this commit fixes the problem with a different approach: Model Nvim
    as a pushdown automaton(https://en.wikipedia.org/wiki/Pushdown_automaton). This
    design has many advantages which include:
    - Decoupling the event loop from the states reponsible for handling events.
    - Better control of state transition with less dependency on global variable
      hacks(eg: 'restart_edit' global variable).
    - Easier removal of global variables and function splitting. That is because
      many variables are for state-specific information, and probably ended up being
      global to simplify communication between functions, which we fix by storing
      state-specific information in specialized structures.
    The final goal is to let Nvim have a single top-level event loop represented by
    the following pseudo-code:
    while not quitting
      let event = read_event
    This closely mirrors the state machine description above and makes it easier to
    understand, extend and debug the program.
    Note that while the pseudo code suggests an explicit stack of states that
    doesn't rely on return addresses(as suggested by the principles of
    automata-based programming:
    https://en.wikipedia.org/wiki/Automata-based_programming), for now we'll use the
    call stack as a structure to manage state transitioning as it would be very
    difficult to refactor Nvim to use an explicit stack of states, and the benefits
    would be small.
    While this change may seem like an endless amount of work, it is possible to
    do it incrementally as was shown in the previous commits. The general procedure
    1- Find a blocking `vgetc()`(or derivatives) call. This call represents an
       implicit state of the program.
    2- Split the code before and after the `vgetc()` call into functions that match
       the signature of `state_check_callback` and `state_execute_callback.
       Only `state_execute_callback` is required.
    3- Create a `VimState` "subclass" and a initializer function that sets the
       function pointers and performs any other required initialization steps. If
       the state has no local variables, just use `VimState` without subclassing.
    4- Instead of calling the original function containing the `vgetc()`,
       initialize a stack-allocated `VimState` subclass, then call `state_enter` to
       begin processing events in the state.
    5- The check/execute callbacks can return 1 to continue normally, 0 to break the
       loop or -1 to skip to the next iteration. These callbacks contain code that
       execute before and after the old `vgetc()` call.
    The functions created in step 2 may contain other `vgetc()` calls. These
    represent implicit sub-states of the current state, but it is fine to remove
    them later in smaller steps since we didn't break compatibility with existing
state.h 407 Bytes