A rewrite of the Plan 9 rc shell in Goose for UNIX-like systems and with structured CLI args
rc ARCHITECTURE.md
4.8 kB · 81 lines
Markdown

Architecture #

rc2.goose: input, startup, and invocation
  |
   +-- editor.goose: terminal input and completion hooks
    |    +-- context.goose: nested completion contexts and scalar prefixes
    |    +-- grapheme.goose, layout.goose: Unicode boundaries and terminal positions
    |    +-- terminal_text.goose: safe terminal text
    +-- history.goose: complete records and persistence policy
  |
  +-- runtime.goose: command evaluation and process coordination
       |
        +-- syntax.goose: lexer and parser
         +-- declaration.goose: literal argument schemas and script metadata
         +-- jobs.goose: process groups, job state, and terminal ownership policy
         +-- resources.goose: resource syntax and process-state commands
        +-- memory.goose: safe-point pool reclamation
       +-- expand.goose: list and word evaluation
       |    +-- glob.goose: path expansion
       |         +-- pattern.goose: Unicode pattern matching
       |
       +-- platform.goose: native interface and descriptor checks
            +-- native/posix.c: POSIX operations

schema.goose
  +-- cli.goose: argument parsing
  +-- completion.goose: completion generation

data.goose: indexed syntax, text, values, variables, and functions

The CLI parser and completion generator use data, not script execution. declaration.goose converts the literal args block to this schema. Metadata extraction parses a script without command execution. The editor uses the same conversion for automatic script completion. Generated rc completions contain literal option metadata. The editor reads it without command execution. Subcommand declarations form an indexed command tree. Each scope retains its own arguments and its parent index. The selected scopes form the implicit $subcommand list. The parser reserves this variable and replaces it on each successful declaration. The argument scanner stops at child-command boundaries without consuming option values as command names. The declaration layer derives scoped help and completion schemas from this tree. Generated shell dispatchers select a scope before the existing option and positional completers run. Generated rc metadata retains a literal command tree. The editor reads this tree without evaluation.

The parser stores fixed-size nodes in an indexed pool. Integer spans refer to a text pool. List values refer to a value pool. These spans avoid references across recursive Goose stack lifetimes.

A hash table maps name hashes to binding chains. Name comparisons resolve hash collisions. Temporary scopes restore previous slots. A free list recycles temporary slots. Assignments to other variables remain global across function calls.

Pattern matching uses a last-star retry point, not recursive star expansion. Path expansion reads directory entries as NUL-separated names. Newlines in filenames do not split directory entries.

The native adapter receives argument and environment packets, not shell command strings. execve and posix_spawn receive argument vectors directly. Pure external pipeline stages use spawn file actions. Other stages use a child evaluator. Interactive job control uses fork so that both parent and child establish the process group before execution. The native adapter transfers terminal ownership and saves terminal modes. The job module retains ordered process statuses. Child evaluators discard inherited parent-job tables. Safe points remove consumed job records.

Descriptor restoration uses close-on-exec descriptors. The adapter moves a restoration descriptor before a script refers to its descriptor number. Here-documents use temporary files to avoid pipe-capacity deadlocks before the reader starts.

The evaluator records pool marks before command sequences, loops, and source evaluation. memory.goose retains each active prefix and copies live objects from its transient tail. Its work list copies syntax without recursive native calls. Roots include live and shadowed variables, current functions, history, status caches, and persistent names. Live jobs also retain command text and saved terminal modes. The implementation is single-threaded. Signal handlers only set pending flags. The adapter uses signal masks and pselect to prevent lost signals before input waits. The evaluator runs signal functions outside the native signal handler. The editor retains partial escape sequences across signal interruptions. Background-child events do not change terminal modes or discard pending input.

Unicode 18 property tables define grapheme boundaries. tools/unicode.mjs generates the tables and conformance cases. The repository includes the source data, checksums, and Unicode license under unicode/. No Unicode runtime library is necessary. Libc supplies code-point display widths.