Skip to content

Latest commit

 

History

History
685 lines (493 loc) · 24.9 KB

DESIGN.org

File metadata and controls

685 lines (493 loc) · 24.9 KB

Design Notes

Scratch pad for high level design notes and ideas

Mission and Approach

Goal of VRS is to accelerate my own programming. It hopes to enable individuals to build software experiences on the scale of world’s largest software companies.

To this end, it is designed to minimize impedance at every layer of stack, looking at programming as one comprehensive experience.

Every facet of programming - language, execution environment, clients, user interfaces, editing, etc - is designed from the ground up to work together. The goal isn’t to build any one component to be the best, but to provide the best holistic experience - deliberately designed, end-to-end, over every aspect of building software.

In the latent space, runtime is:

  • An environment where making has character of play
    • Find out what is being built during building
  • Computer supports the creative spirit - uniform, dynamic, fun
  • Has feeling of being inside a program
    • Like Common Lisp and Emacs

Technical Goals

Build an environment that enables:

  • Immediate feedback while building
  • Simple, universal abstractions over all aspects of programming
  • One-time integration cost of data and capabilities, over multiple programs

Influences

Emacs - Extensible, uniform, interactive, moldable software environment. Immediate Feedback while building.

Erlang - Message based, distributed software runtime w/ userspace concurrency. “Let It Crash” philosophy. Simple, powerful concurrency.

Hypermedia - Self-describing, uniform interface. Thin general-purpose clients.

Unix - Composability of programs via stdio, pipes, and plaintext. Polyglot.

Plan 9 - Per-process hierarchical namespaces and hypermedia approach to plaintext via plumber

./assets/vrs-venn.png

Experience

interactive High-level of introspection and live molding of environment

hackability Built for hacking software

simplicity Uniform interface, with small number of primitives

Design: Uniformity

Uniformity is a key principle to remove friction.

VRS has uniformity:

  • In representation - over data, code, and markup
  • In execution - over processes, services, tasks, and interactive software
  • In boundaries - over functions, clients, external programs, networks, and devices
  • In interactions - over programs, users, automation, and agents.

Uniform Interface - Common Primitives Compute and Data Substrate Turn O(M*N) problems into O(M+N)

Everything is a Function Call

  • Generate user interfaces off function call signature
  • User interactions are function calls
  • Agents can use function calls
  • Data queries are function calls
  • User interactions directly translate to a program - same for agent

Architectural Components

lyric - Programming language and data format

lyricVM - Bytecode virtual machine for running lyric

vrsd - Runtime daemon that hosts and runs lyric programs

libvrs - shared client libraries

Clients - CLI programs (vrsctl), GUI programs (vrsjmp), and editors (emacs) interact with runtime.

./assets/vrs-arch-stack.png

Client and Runtime

Goals

Communication between client and runtime is a simple message-based and transport agnostic interface.

Runtime code should be able to run in several configurations:

  • Cross-process, for client and runtime processes on the same machine.
  • Over-network, for thin-clients that interact with runtime on different machines.
  • In-process, for platforms that must run client-runtime in single process. (E.g. Mobile)

Client API should be simple and lean so bringing up new clients is low cost.

Runtime

A typical lifecycle of the client and runtime may look like:

  1. Clients opens a bidirectional channel to runtime.
  2. Client and runtime trade message over bidirectional channel via client API.
  3. When the client exists, it closes the channel, and runtime frees resources tied to connection.

The per-client state is maintained in the runtime, so client implemenation is lean.

The client API will offer request-response style APIs on top of message passing for convenience, but the channel is a bidirectional.

How the runtime is started may be platform dependent:

  • On desktop, runtime daemon runs via typical service management (systemd, launchd, etc).
  • On cloud, runtime may be always running as a service.
  • On mobile, a in-process runtime may be spun up during application initialization.

Runtime - High Level Lifecycle

  1. Runtime is initialized in host process
  2. Host process begins listening on some transport for new connections
  3. Host process hands-off accepted connections to runtime
  4. Runtime spawns a dedicated client process per connection
  5. Interactions from client connection are processed and forwarded from client process to other processes in the runtime.
  6. Responses, if any, are forwarded back on client connection.
  7. When client task ends or connection is disconnected, the client process is terminated.

Client API

The client API should be lightweight.

It should support:

  • Request / Response
  • Pub / Sub - including pushes from runtime
  • Hypermedia-Driven Interface

Client Library

The client API is largely implemented by client library.

Like libdbus, the underlying transport should be opaque to client programs - it may use lower-level IPC mechanisms like unix domain sockets, or TCP, without affecting API surface between client and client library.

Hypermedia Client

The client implementation over client API is hypermedia-driven. See [Hypermedia]

vrsctl - CLI client

The vrsctl CLI is lightweight client to runtime that offers 1-1 mapping to client API.

Per invocation, it will:

  1. Open connection with runtime
  2. Send specified message(s)
  3. Wait for response(s)
  4. Close connection to runtime when stdin closes and all requests were processed.

CLI can be used to bootstrap new clients that is able to launch vrsctl directly, or as a development tool.

Lyric Lang

A good system can’t have a weak command language.

  • Alan Perlis, Epigrams of Programming

Goals

An embedded lisp dialect provides a single, uniform interface for everything - code, data, interfaces, and protocol.

Interactions from the REPL or hypermedia interface should be exactly how the applications are programmed, similar to how the language of the shell can be used to write scripts.

All data, including interface markup, can be captured by the dialect’s s-expressions.

Why write your own Lisp?

Paul Graham put it best:

A language is by definition reusable. The more of your application you can push down into a language for writing that type of application, the more of your software will be reusable. — Paul Graham

When the language is tailored to the environment, software can be simple and rich, similar to how shell languages are designed around IO redirection.

It is also my impression that a bulk of software written today fall into standard, institutionalized patterns - with engineers acting as “human compilers” to write this code out by hand. Why not let the computer write that code via higher-level intermediate language?

See also - Greenspun’s tenth rule - Wikipedia

On Interactivity and Liveliness

Runtime should be highly interactive - the experience of editing, debugging, testing and interacting with a system should seamless - like other Lisp systems

The feeling of being inside a program:

Now second, I learned that I had never truly debugged before. The tools provided particularly by Common Lisp and to a slightly lesser degree Clojure allow me to be inside my program at all times. Why do print-line-debugging to find out what’s happening at a location in code when you can just be inside your program and inspect everything live as it’s running?

It’s okay to start dynamic and tighten down the API later with gradual-typing mechanisms once the domain crystalizes.

  • Simon Peyton-Jones

Lisp as the Uniform Interface

In vrs, it’s Lisp all the way down:

  • Scripting language is Lisp
  • Modules extends runtime via bindings in Lisp
  • User interfaces are s-expressions
  • Hypermedia controls within interface are s-expressions
  • Messages between client and runtime are s-expressions

Lisp is the substrate for code and data that ties the client, runtime, and modules together.

Lisp is a practical choice for highly interactive, moldable, application-specific progamming environments.

leoshimo - Twitter Rant on Lisp

Lisp as Hypermedia

v0.1 sketch of Lisp as Hypermedia

'((:text_field :id search
               :on_change on_search_text_change
               :value "query input")
  (:ul :id search_results
       (:li :content "Element 1"
            :on_click '(action_for_elem_1))
       (:li :content "Element 2"
            :on_click '(action_for_elem_2))
       (:li :content "Element 3"
            :on_click '(action_for_elem_3))))

Runtime Program Execution

The runtime spawns and manages processes. These are not OS processes, but lightweight threads of execution like Erlang – Processes.

Each process has its own Lisp environment for evaluating S-expressions.

Unlike Node, which uses callbacks to implement continuations, lyric is built to support preemptive and cooperative multitasking during evaluation, similar to BEAM VM.

Instead of a subscription + callback mechanism, the process’s code describes when IO is polled, when mailbox is queried, etc.

lyric and the runtime provides conveniences for typical patterns, e.g. implementing long-running services, supervisors, request-response, etc.

Process, Bytecode, and Fibers

The programs written in lyric support yielding - pausing and continuing execution of bytecode sequences.

This makes it possible for runtime to drive evaluation of program via asynchronous IO. Interpreter host captures signals yielded during program execution, dispatches appropriate async IO, and continues execution once the IO resource is ready.

The key motivation for this is to not tie an OS thread per paused program. A lyric program waiting for IO does not block a thread, since it’s runtime state can be captured as a data structure.

E.g. A 10,000 sleeping processes “at rest” does not use 10,000 threads.

This makes it viable to model most things as programs - e.g. cron jobs are programs that sleep in a loop:

(loop (sleep 10000)
      (do_a_thing))

What is a Fiber

A fiber is a sequence of instructions that can be cooperatively scheduled. When it runs an instruction to yield for, control flow is returned to caller that initiated execution of fiber.

A fiber can later be resumed when the event that fiber was paused for occurs, such as IO, message-passing, etc.

Fibers and Yielding

Fibers manipulated from within Lyric itself allows for arbitrary levels of yielding calls:

  • When root-fiber yields for Future
    • Top-level fiber is paused
    • Fiber driver receives future, then polls it
  • When child-fiber yields for Future
    • Top-level Fiber is paused
    • Child Fiber is paused
    • Fiber driver receives future
  • When root-fiber yields a value
    • Root-fiber is paused
    • Fiber driver errors for unexpected pause
  • When child-fiber yields a value
    • Root-fiber is running
    • Child-fiber is paused
    • Fiber driver is not involved

Hypermedia (WIP)

The client interface uses hypermedia between the runtime and interactive client shells.

The design is focused on:

  • Enabling speedy + simple development of interactive applications
  • Supporting many thin, general purpose, client-shell application backed by same application code.
  • Supporting client-specific, multimodal software experiences without unnecessary complexity on client or runtime programs.
  • Ease of Automation and Testing

High Level

Client manages connection Connection transmits request / response / pubsub messages over connection Documents are backed by pubsub topics Browser manages a set of documents

Uniform Hypermedia-Driven Interface

The hypermedia format should have a minimal, uniform interface.

The interface between client implementations should be simple and minimal, so building new client shells requires minimal effort.

The interface should provide minimal set of markup, hypermedia controls, and general-purpose input mechanisms so new software experiences can be built without clients and runtime programs being updated in lockstep.

The interface should also be focused on being multimodal - i.e. the hypermedia format should allow both GUI-driven and voice-driven client shells to leverage the same semantic markup.

Common input mechanisms should be supported, such as Emacs read and completing-read APIs, which should prompt user for input in a client-dependent interface.

As a consequence of leveraging uniform hypermedia, it should be simple to build tooling and automation on top of this interface - the data and interactions available on that data should be represented in the markup.

For example, It should be possible to interact with interactive VRS applications via REPL, using text-based representation of the markup, during development.

The extension of using the REPL to manipulate the “document” is to allow mental model when using the software directly translate to being able to program the software, in the same way CLI interactions translates to shell scripting, and using Emacs translates to programming Emacs via buffer primitive. It should be possible to “log the events” being run during a sequence of interactions, then use that information to write a program that automates that interaction.

Unlike the Browser Ecosystem, which often uses event handlers from JavaScript to hook interactive behavior, hypermedia data will contain the code that defines the behavior on interaction, similar how there is behavior associated with anchor tags and form tags.

Goals

Declarative

The interface programming model is declarative, borrowing from functional-reactive programming patterns from Elm, Phoenix LiveView and React.

This should also enable ease of testing and rapid UI iteration in isolation.

Rich Linking and Object Store

See also: Object Store

The format should enable explicit links (interactions added to application by programmer) and implicit links (interactions natural for the data itself).

Thin Clients

Instead of having to replicate rich interactive experiences per client shell implementation (i.e. build thick client applications), hypermedia should allow thin general-purpose clients, i.e a slim web browser of sorts.

Rich interactivity is enabled by an interactive interface process that runs in the runtime itself, a la Phoenix LiveView

Clients should not accumulate a large body of lyric code in client shell binary itself, so that changes in runtime do not need recompilation of client binary in most cases.

Rapid Interactive Dev Loop

The hypermedia format and tooling should enable interactive development loop for building and testing UI.

Conceptual Analogies to Emacs and Web Browsers

Hypermedia document is to VRS what buffers are to Emacs, and HTML document / DOM is to Web Browsers.

It is the representation for data, interface, and behavior.

In a sense, each client is a minimal pseudo-browser for presenting interfaces for given hypermedia data.

Client as a Pseudobrowser

It may be helpful to conceptualize the client shells as a pseudo-web-browser of sorts, as far as runtime is concerned.

While runtime processes may respond with hypermedia data, the client manages the “tabs” of this markup across variety of different sources, similar to a web browser.

In this way, the contract between client and runtime stays simple - there is a bidirectional channel which is used for RPC and Pub/Sub traffic. For example, there can be a collection of topics published from the runtime, which client subscribes to. Each of those topics can contain the hypermedia representing the “page”.

Inspirations

Lifecycle Overview

When a hypermedia client connects to runtime, a client process representing the user interface is spawned in the runtime. There is a dedicated bidirectional channel between the client and runtime’s process for that client.

The client process communicates with other processes running in the runtime, and sends user interface hypermedia over the channel. The hypermedia contains self-describing data and hypermedia controls.

Client renders the hypermedia format in client-dependent format, e.g:

  • A TUI client shell renders markup as plaintext.
  • A GUI client shell renders markup in Native UI or Browser-based Renderer.
  • A voice client shell “renders” markup as STT audio.

When the user interacts with the interface, registered events are sent from the hypermedia client to the client interface process, which updates internal state, and pushes updated hypermedia data back to client if needed.

The updated markup is received by hypermedia client, which itself updates what is shown to the user.

The connection is bidirectional:

  • Client can dispatch commands, which sends messages to interactive process
  • Runtime can notify clients via Pub / Sub topics the client registered itself for

Features

Element Selector

  • API to specify elements on page, similar to CSS selectors
  • Used to specify targets for different interactions, e.g. toggling visibility of #element

Navigation

  • TODO: What Navigation Primitives make sense? URLs? Stacks of “Views”?

General Purpose Inputs

  • High-level primitives like Emacs read and completing-read allow triggering standardized input behavior in a way that is appropriate for hypermedia client implementation

Object Store

What is it? General Purpose, Object Store that’s focused on linking data and functionality on that data.

At its heart, it hopes to allow rich interop and workflows across different pieces of software in the runtime.

If a “program” in VRS is:

  • Data represented in Object Store
  • Functions that have side effects on data in object store
  • Functions that generate hypermedia interface on data in object store

It should be possible that data, functionality and interfaces between different pieces of software can compose more naturally.

Principle: Programs (including data, capabilities, and interface) should be composable in ways that the wasn’t thought about when original program was written.

The format allows explicit links (interactions added to application by programmer) and implicit links (interactions natural for the data itself).

Inspirations

Rather than manually specifying relationships between bits of information, we need a system that can see these connections simply by taking context and content into account… Hyperbole itself, however, should be thought of as an extensible “information enabler”, automatically turning inert documents into active ones, through the process of recognizing implicit buttons and giving you multiple ways to interact with those buttons. It’s just like what Wiki did for text, but now for lots of other things, and in many more ways.

  • John Wiegley

humans must too often carry their data from program to program… Why should humans do the work? Usually there is one obvious thing to do with a piece of data, and the data itself suggests what this is.

  • Rob Pike

http://doc.cat-v.org/plan_9/4th_edition/papers/plumb

Features

Target-Action Links

  • API to associate a collection of actions with specific types of data - i.e. data shaped a certain way can be passed to some function.
  • Like Embark - allow chaining actions based on the shape of data interactively
  • Read / store data
  • Allow links between pieces of data for a specific “entity” - data graph.
  • Invoke functionality from linked entity data directly, like Emacs Embark
  • Proactively suggests links and functionality

Ideas

An interactive application is: Functions that modify state, Functions that create markup from state.

Like Embark - Support workflows like embark-act and embark-collect across a collection of VRS applications

Entity system that automatically enabled dynamic interface generation and interaction?

Features like Emacs (interactive) that allows an interface to be created from the function implementing that capability directly.

Reusable Interface Components

  • Rendering should allow composition and reuse - a view of data can be used across “applications” or interactive shells without being tied down to specific application

Allow easy “linking” experience - i.e. in one software experience, I can select an entity, search for related entity, and link them all within same workflow.

Internals

Async Tasks in Runtime

A high level mental model for async tasks in runtime is:

  • A single kernel task
  • A collection of shared system services:
    • A single service registry task
    • A single pubsub task
  • Kernel task manages multiple process tasks
  • Each process task has:
    • A dedicated mailbox task
    • A optional controlling terminal task
    • Handles to kernel and shared services such as service registry and pubsub

Characteristics

Interesting Characteristics of System

Idea: Uniformity in Edittime vs. Runtime

Edittime: Phase when program is being actively edited by programmer Runtime: Phase when that program is running

VRS takes advantages of uniformity between these two (generally distinct) stages of developing software, e.g.:

  • Copilot-like codegen experiences used during edittime can be invoked by programs at runtime.
  • Runtime interactions of programs, users, automation, and agents can be reified at edittime (i.e. user interactions can be turned into a program with little effort)

This extends into test and debug phases of programming - debugging and testing can be done via uniform, universal mechanisms without use of secondary tools or concepts.

Idea: Data, Functions, and Interactive Runtime

  • The shape of data implies certain functions can be applied to access or mutate it.
  • System should support querying set of available functions for given shape of data
  • Functions themselves are function-shaped data
  • Applications:
    • Programs can use this to dynamically resolve set of actions for given piece of data (e.g. todo list element actions: toggle, add note, etc)
    • Agents can query for functionality available on piece of data it is working on
    • Editor can leverage this to generate scaffolding code
  • Related: Plan 9 - Plumber (Rob Pike). Emacs - Embark Package. Hypermedia.

Idea: Homoiconicity

  • Symbolic expression is data, code, and interface
  • Datagen is codegen is interfacegen
  • Every piece of data can embed every other piece of data
  • Exact same mechanism (S-expression generation) can be across range of scenarios:
    • for programming - program generation at edit-time (copilot)
    • for agents - program generation at runtime
    • for generated interfaces - interface generation at runtime
    • programs, users, and agents are all modelled like programs
  • Interactions can be reified as code - interaction of user can be recorded as series of function calls - which is a valid program

Word Cloud

  • Homoiconicity:
    • in representations of data, code, interface
    • in interactions of users, programs, automation, agents
  • Data as implicit buttons (GNU Hyperbole, Plan 9 Plumber)
  • Compute Fabric:
    • Software experiences are universal across multiple surfaces, devices, platforms, and modalities
    • The collection of my devices feel like one whole computer

VRS and Agents

VRS’s design naturally complements automation and agents:

  • Agents can be modelled as programs that write themselves in real-time
  • Agents use all capability available to programs (zero integration cost)
  • Agents can use processes to:
    • accumulate complex state in isolation
    • work in parallel, and in tandem, concurrently with other processes
    • await indefinitely, for scheudling, user input, external event,
    • execute durably
  • Agent’s error scenarios leverage full error handling of runtime - stack frames, error catching, supervision trees - to restart, or trigger user intervention
  • Runtime provides virtual, isolated, sandbox execution
  • User Interfaces can be interacted by programs - including agents (Hypermedia)[

References

Strange Loop 2022 - Stop Writing Dead Programs Semantic Compression Bret Victor The Future of Programming - YouTube Andy Matuschak - Premature scaling can stunt system iteration