Rules, Emacs comparisons, limits and open points

Rules a plugin author must know

The design states six rules; the rest are the idioms that follow from them.

  1. Your plugin.ss runs top to bottom when your plugin turns on. Everything it sets up through the core (entries made through verbs, hook entries, option layers, timers, jobs, ties, on-end actions) is yours and is undone, newest first, when it turns off or reloads.
  2. Your ordinary variables start fresh each run. Your own working state belongs in them. What must outlive a reload (a reader, a drawing worker) is started again from the top level of your file, from saved data.
  3. You change another plugin’s state only by calling its contract. You cannot write its data; the build refuses set! on another plugin’s saved data.
  4. What the user cares about is saved data; settings are options; what you add to another plugin’s things goes through its contract’s verbs. Keep in saved data only data whose shape your plugin (or a contract you implement) defines, as plain data, a persistent map when keyed, a define-plain-record for a fixed shape. Never change saved lists in place.
  5. Hold other plugins’ things by id. Ids come from new-id and are never reused; check live? (or the provider’s equivalent) before use.
  6. An exported variable is a constant.
  7. Your plugin reruns by itself when something it used while starting changes (a provider reloaded, swapped or turned off). You write nothing for it.
  8. Heavy work goes to a worker: define-worker at the top level, start-job to run it. A worker sees only its input, standard Scheme, a listed set of Chez procedures, the plain-data library, post, next-input and sleep. Code it shares with your plugin goes in a file both include.
  9. Start a job for each change, for: what it is about, and you never see a stale result. Work not restarted on every change checks in on-result that what it was for is still current.
  10. A worker that waits on input needs its own way out (closing what it reads), because a worker blocked in a system call cannot be stopped.
  11. A worker that keeps results across iterations keeps them in a few large flat objects (vectors, strings), not many small lists; this keeps garbage collection short.
  12. A start does no blocking work and cannot wait; it starts a worker instead. An end cannot wait either.
  13. A command paused inside a plugin that changes is quit, as by C-g. After any pause, get values from other plugins again: they may have reloaded with a new shape.
  14. A procedure of yours that makes something for its caller says whose it is: the user’s (it stays) or the caller’s (tie it with on-caller-end, and forget-caller-end when it goes by other means). A verb of yours makes its entries with as-caller, so they are the caller’s. The caller is the code that contains the call.
  15. What you want drawn goes through the display’s verbs, because a frame is due only when core state changes.
  16. Use a contract you can do without under (provided? 'c).
  17. A process the user starts is processes:start!; one your plugin needs for itself is processes:start-own.
  18. A contract meant to be implemented by others has a contract.ss, and declares its saved data there, whole.
  19. A macro use must not expand to definitions followed by an expression (it is refused with its line). Macros and record types change only by reloading.
  20. Do not use eval (reported: it reaches around imports) or fork-thread (refused: use a worker).
  21. Public procedures check their arguments at entry (assert, assertion-violation) and name themselves in the error; code inside trusts its arguments.
  22. Names: a procedure called for its effect on something that exists ends in !; one that makes something new does not; nor does syntax. A contract’s procedures take optional positional arguments or property lists, not keywords: keywords are the core’s.

Rules a user must know

  1. init.ss runs once, top to bottom. (use-plugin p key: value …) picks a plugin and its options; (use-plugin a b c) picks several. A plugin starts at its line, or earlier when something needs it. A plugin not listed never starts.
  2. A change belongs to where you wrote it: a plugin, an init.ss line, or your session (M-:). Removing the line removes the change; ending the session removes the session’s changes.
  3. reload-config applies only what changed. If a changed line fails, what was running stays.
  4. C-M-x on a definition takes effect at once and lasts until that plugin is reloaded from its file.
  5. Your data is saved after every command, when a storage plugin is on. After a crash you lose at most what an unfinished command did.
  6. Turning a plugin off leaves Tuna as if it had never been on, except your data.
  7. Your shells keep running when the terminal goes and when you quit; killing a shell’s buffer ends it; list-processes shows them all.

Emacs comparisons that matter

TunaEmacsSame or different, and why
Plugin: a folder whose file runs when it turns onPackage: a file required onceDifferent: what a plugin sets up is owned and undone when it turns off or reloads.
Contract, import, implementsrequire, the foo--helper naming conventionDifferent: names are checked at build; another plugin can stand in.
Verbs, made for the caller with as-callerkeymap-set, add-to-list, set-face-attributeDifferent: the same lines, but each entry leaves with the plugin that made it.
Turning a plugin offunload-featureDifferent. Emacs refuses if others depend on it, and leaves anonymous hook functions, timers, buffers, and changes to other packages’ variables. Tuna turns dependents off with a message and back on with it, and removes everything except your data.
A buffer whose mode’s plugin is off is treated as Fundamental modeunload--set-major-modeSame.
Hooks with ownershook variablesDifferent: an entry is keyed by its owner, not its function, so evaluating a lambda again replaces it; a failing entry is removed and reported.
Options with one layer per ownerdefcustom, setq, Custom themesDifferent: in Emacs the last setq wins and removing it changes nothing until restart.
Scoped layers, set-local!, modes:set!setq-local, defvar-local, mode hooksDifferent: a scoped layer has an owner and goes with it; a mode-scoped layer applies at once to every buffer in the mode.
Faces and margin marks set with buffers:set-faces!overlays with a face propertyDifferent: a caller’s whole set at once, removed with the caller and the buffer.
Text procedures on the current bufferthe sameSame, with positions from 0.
Saved data, written after every commanddesktop-save-mode, savehist-mode, auto-save filesDifferent: Emacs saves only if asked, at exit or on a timer.
define-plain-recordcl-defstructDifferent: never changed in place, and checked, so it can be saved and sent to a worker.
Undo history saved, with a capundo-limit, in memory onlyDifferent: it survives a restart.
WorkersLisp threads (one at a time), timers, subprocessesDifferent: workers run truly in parallel but cannot touch editor state.
A worker’s results applied in order by on-resulta process filterSimilar.
A command paused at a promptread-from-minibuffer, recursive editSame for you; a continuation underneath, so paused commands do not nest.
C-g, request-quit!keyboard-quit, quit-flagSame, plus a forced quit after a grace period.
Your processes, owned by the process servicemake-process with a bufferDifferent: they outlive the terminal plugin and a clean exit; Emacs ends them at exit, asking first.
A plugin’s view (buffers:view)get-buffer-create for *Help*Different: it closes when its plugin turns off and stays across a reload.
M-:eval-expressionDifferent: its changes belong to the session, which can be ended.
C-M-xeval-defunSame: lasts until the file is loaded again.
Reloading a pluginload-file, eval-bufferDifferent: Emacs runs the file again over the old definitions; Tuna stops the old version and starts the new, and restarts the old code if the new fails.
reload-configevaluating init.el againDifferent: Tuna undoes removed lines.
redefine, advice-adddefun, advice-addSame, but each change is owned and can be taken back.
dabbrev::helperevery name is reachableDifferent: private names are reached on purpose, by the user only.
Drawing: extraction on the editor thread, the page on a worker per tabredisplay on the main threadDifferent.

Known limits

None blocked approval. Each is a thing the design cannot do, or does only by a rule an author must follow.

Open and proposed

For the owner to decide:

Decided by the primary agent (tell him if you disagree):

Not yet named or fixed (small; settled while building): the names of the commands for turning off, reloading, swapping and ending the session; where init.ss and your own plugins live on disk; the grace period before a forced quit; the start time limit (5 s in the prototype); the persistent map library’s name.

The prototype and this guide

The design was proven with a prototype that used older names. This guide follows the approved API; where the prototype differs, the guide is right.

PointPrototypeThis guide
Registrationstable-set! on another plugin’s table, with a key the author inventedverbs, made for the caller with as-caller
Hookstables, with a key per entrydefine-hook, add-hook!, keyed by owner
Text editing(buffers:insert! id (buffers:point id) "Hello")(buffers:insert! "Hello") on the current buffer
Saved datadefine-kept, predicates written by hand, vectors read by indexdefine-saved with a doc, list-of and friends, define-plain-record
Per-buffer valuesa settings plugin with a hard-coded listscoped option layers, set-local!
Lifetimesdefer! with a procedure; (on-caller-end key 'proc arg); forget-tie!on-end; (on-caller-end (proc arg)); forget-caller-end
Workersstart-job! with a positional key; receive; sleep taking a time objectstart-job with for:; next-input; sleep in seconds
Tiesprocedures, not savedplain data, saved with the session
A buffer whose major mode’s plugin is offkept its mode name, with no bindingstreated as Fundamental mode until the plugin returns
The terminalmade fifos and spoke the helper’s protocol itselfthe process service’s follow! and send!
Timersa simulated clock for testsreal timers; the loop sleeps until the next one is due
DrawingHTML strings, one client per test, no browserthe web plugin’s, in the Browser milestone