A plugin’s ordinary variables are lost whenever it reloads, turns off, or Tuna stops. Three kinds of value outlive that, each kept by the core: saved data for what the user cares about, options for settings, and dynamic values for what each command sees as current. The checks and records of plain data go with them.
Saved data
What you care about (buffer text, undo history, prompt history, the window layout) is declared as saved data. The core keeps it, checks it, has it written to disk after every command, and gives it back to the next run of the plugin, to a replacement plugin, or to Tuna after a crash.
(define-saved name initial "doc" check [upgrade])core
Does. Declares one piece of saved data. After it, name reads the current value and (set! name v) replaces it. On the first start ever the value is initial; afterwards it is whatever was saved. check is a predicate the value must always pass. upgrade, if given, turns a value of an old shape into the new shape. The arguments are in the same order as define-option’s.
Underneath.
The value lives in the core under a qualified name, buffers:all, or under the contract’s name when a contract declares it (minibuffer:history, shared by every implementation of (minibuffer)).
It must be plain data. Strings and vectors in it are made read-only. Keyed data should be a persistent map, so a change costs in proportion to the change.
After every command, and whenever a command pauses at a prompt, the core collects what changed and hands it to the (storage) contract; the journal-store plugin writes it to disk, and reads it back at start.
A saved value that fails check is given to upgrade. If there is no working upgrade at start, the value is set aside (kept, not deleted), the plugin starts with initial, and a message names restore-saved!.
If the plugin’s start fails, its own writes are undone, only where nobody wrote after it, and a persistent map key by key.
Errors.define-saved: … is not plain data; on set!, buffers:all: … does not fit its declaration, or for a map, entry k -> v does not fit its declaration; on a reload whose new check rejects the current value with no upgrade, the reload fails and the old code starts again.
(defvar savehist-minibuffer-history-variables nil"List of minibuffer histories.");; and, to have it saved:(add-to-list 'savehist-additional-variables'my-history)(savehist-mode 1)
Where Tuna differs. Emacs saves a variable only if a package such as savehist or desktop is told to, at exit or on a timer, and does not check its shape. Tuna saves saved data after every command and checks it on every change, so a crash loses at most an unfinished command.
(set! name v) on saved datacore
Does. Replaces the whole value. Change keyed data with persistent-map operations: (set! all (pmap-set all id b)).
Underneath. Checks v (for a map-of map, only the changed entries), freezes it, stores it, and notes it for the next save. You write only your own saved data, or the saved data of a contract you implement; the build refuses set! on another plugin’s saved data, with its line. You change another plugin’s state by calling its contract.
Tuna
(set! history (cons (cons kind answers) history))
Emacs
(setq my-history (cons answer my-history))
(restore-saved! 'c:name)user
Does. Brings back the most recently set-aside value of a piece of saved data: set aside because it failed its check at start, or reset by C-M-x on its define-saved. The message that reports the setting aside names this command.
Errors. The restored value is checked like any set!.
Tuna · M-:
(restore-saved! 'undo:records)
Emacs
;; No equivalent: Emacs does not set aside data that fails to load.(desktop-read)
Checks and records of plain data
Saved data and workers take only plain data. These build its checks and shapes. They live in the plain-data library beside the persistent map, so workers can use them too.
(list-of x?), (pair-of a? b?), (maybe x?)core
Does. Make predicates: a list whose every element passes x?; a pair whose car passes a? and cdr passes b?; #f or a value passing x?.
Tuna
(list-of (pair-of symbol? (list-of string?))); ((file "a" "b") (buffer "x"))(maybe fixnum?); #f or 7
Emacs
;; defcustom's :type says the same, for options only:(defcustom my-history nil"…":type'(alist :key-type symbol :value-type(repeat string)))
(map-of key? value?), (map-of entry?)core
Does. A check for a persistent map. With two predicates, each key must pass key? and each value value?. With one, (entry? key value) must hold, for checks that relate the two.
Underneath. Unlike a hand-written predicate, the core can apply it to only the entries a change touched, so checking a 100,000-entry map after one change costs one check.
Tuna
(define-saved all (pmap)"Every buffer by id."(map-of fixnum? buffer?))(define-saved closed (pmap)"Folded directories."(map-of (lambda(path v)(and(string? path)(eq? v #t)))))
Does. Declares a record made of plain data. It defines make-NAME, NAME?, one reader NAME-FIELD per field, and one NAME-with-FIELD per field that returns a new value with that field changed. A field without a check accepts any plain data.
Underneath. A value is a read-only vector, #(buffer name text point mark modified?): plain data, saved and handed to workers as it is. Its predicate checks the tag, the length and every field’s check, and comes from the declaration, so changing the shape changes the check; old saved values then fail it and the core asks for an upgrade. Updates are named -with- because the value is never changed in place, unlike Chez’s NAME-FIELD-set!. Workers can include a file holding the declaration.
Tuna
(define-plain-record buffer
([name string?][text][point fixnum?][mark (maybe fixnum?)][modified? boolean?]))(define b (make-buffer "*scratch*"""0#f#f))(buffer-point b); => 0(buffer-with-point b 7); a new buffer value; b is unchanged(buffer? b); => #t
Emacs
(cl-defstruct my-buffer name text point mark modified)(setq b (make-my-buffer :name"*scratch*":text"":point0))(my-buffer-point b); => 0(setf (my-buffer-point b)7); changes b in place(my-buffer-p b); => t
Where Tuna differs. A cl-defstruct is changed in place and does not check its fields. A plain record cannot be changed in place, which is what lets the core save it and hand it to another thread safely.
The persistent mapcore
Does. A map never changed in place. (pmap k v …) makes one; (pmap-ref m k [default]); (pmap-set m k v) and (pmap-delete m k) return a new map; (pmap-update m k f default); pmap-contains?, pmap-count, pmap-keys, pmap-values; (pmap-fold f init m) with (f key value acc); (pmap-for-each f m); (pmap-diff f init a b) visits only the keys that differ. The procedure comes first, as in Chez. The library’s final name is chosen when it is written.
Tuna
(define m (pmap "a"1))(define m2 (pmap-set m "b"2))(pmap-ref m "b"#f); => #f: m is unchanged(pmap-ref m2 "b"#f); => 2
Emacs
(setq m (make-hash-table :test#'equal))(puthash "a"1 m)(gethash "b" m); a hash table is changed in place
(new-id) → integercore
Does. Returns a number never returned before, in this session or any earlier one. Use it for anything others will hold by id: buffers, markers, texts.
Underneath. A counter in the core’s state, saved like saved data, but no undo moves it back: even when a start that took ids fails, those ids stay used. So an id held where no undo reaches (another plugin’s variable, the browser) never comes to mean a different thing.
Tuna · buffers/plugin.ss (part)
(define(make! name)(let([id (new-id)])(set! all (pmap-set all id (make-buffer name (text-store:make"")0#f#f)))(run-hook created-hook id)
id))
Emacs
;; Emacs hands out buffer objects, not ids; a killed;; buffer object stays and answers nil to buffer-live-p.(let((buf (generate-new-buffer "x")))(buffer-live-p buf))
Where Tuna differs. Emacs plugins hold buffer objects directly. Tuna plugins hold ids, because a buffer’s value is replaced (never changed in place) at every edit, and because ids are plain data that can be saved and sent to a worker.
Options
In Emacs a variable has one value: whoever set it last. Remove the setq from your init file and nothing changes until you restart. In Tuna each owner’s setting is a separate layer, so removing the line, turning off the plugin, or ending the session removes exactly that setting and the one underneath shows again.
(define-option name default "doc" check)core
Does. Declares a setting. Reading name gives the winning value. Export it so the user and other plugins can read and set it as c:name.
Underneath. The default is the bottom layer, owned by the declaring run; a reload with a new default replaces it. When a reload brings a stricter check, other owners’ layers that now fail it are removed and reported.
Errors.default … fails its check; reading an option whose plugin has not defined it: has no value: its plugin has not defined it.
Tuna · settings/plugin.ss (part)
(define-option fill-column 70"Column beyond which filling breaks lines."
fixnum?)
Emacs
(defcustom fill-column 70"Column beyond which automatic line-wrapping should happen.":type'natnum)
(set! option v)coreuser
Does. Gives the option your value. Your layer goes on top, or replaces your earlier layer; the newest layer wins.
Underneath. The owner is where the set! is written: a plugin’s run, an init.ss line, or the session. Options are configuration, not saved data: layers are made again from the files at each start.
Errors.settings:fill-column: "wide" is not a valid value.
Tuna · init.ss
(set!settings:fill-column72)
Emacs
(setopt fill-column 72)
Where Tuna differs. Delete this line and run reload-config, and fill-column is 70 again. In Emacs it stays 72 until restart.
(set-local! option v) and scopescore
Does. Gives the option your value in the most specific current scope only: normally the current buffer. Every reader of the option in that buffer sees your value; elsewhere the unscoped layers apply.
Underneath. A layer may carry a scope, a plain value such as (buffer . 7) or (mode . scheme-mode). The core keeps a table, scopes, of procedures, each returning the scopes that apply now, most specific first. The buffers plugin adds one returning the current buffer’s scope; modes adds one returning the current buffer’s major and minor modes. The core itself knows no buffers or modes, so a directory or project scope can be added later as one more entry. Reading an option takes the newest layer whose scope applies, most specific scope first, then the unscoped layers as before. A scoped layer belongs to its writer and goes with it; a buffer’s scoped layers also go when the buffer is killed.
Tuna
(buffers:with id
(set-local! settings:fill-column80))
Where Tuna differs. An Emacs buffer-local value has no owner: turning off the mode that set it leaves it. A Tuna scoped layer goes when its writer goes, as well as when its buffer goes.
(modes:set! 'mode option v)plugin
Does. Gives the option your value in every buffer whose major or minor mode is mode, now and later.
Underneath. A macro of the modes contract (it names an option, as set! does), adding a layer with the scope (mode . scheme-mode). It applies wherever the modes plugin’s scope procedure reports that mode.
Where Tuna differs. Emacs sets a mode’s local value from a hook that runs when the mode turns on, so a buffer already in that mode keeps the old value until the mode is turned on again. A Tuna mode-scoped layer applies at once to every buffer in the mode, and goes when the line is removed.
Options in (use-plugin p key: value …)user
Does. Sets plugin p’s options as it is chosen. Each key: names one of p’s options.
Underneath. The layer belongs to that init.ss line. It is checked before any of init.ss runs.
Errors.init.ss:6: plugin undo has no option limt; init.ss:6: "many" is not a valid value for limit.
Tuna · init.ss
(use-plugin undo limit:5000)
Emacs
(use-package undo-tree
:custom(undo-limit 5000))
Dynamic values
A dynamic value is one each command sees its own binding of: the current buffer is the main one. Emacs gets this from dynamically bound variables; Tuna, where a command can pause and another run meanwhile, keeps each binding with its command.
(define-dynamic name default "doc")core
Does. Declares a dynamic value. Reading name gives the innermost binding of the running command, or default.
Underneath. A continuation mark: a value attached to a point in a running computation, which Chez keeps with the computation. When a command pauses at a prompt and resumes later, it still sees its own binding, whatever other commands bound meanwhile.
Tuna · buffers/plugin.ss (part)
(define-dynamic current #f"The buffer text procedures act on.")
Emacs
;; Emacs's current buffer is built in; a package's own;; dynamic value is a special variable:(defvar my-current-thing nil"The thing commands act on.")
(dynamic-let ([name v] …) body …)core
Does. Binds dynamic values for the body, including while the body is paused.
Tuna · buffers/plugin.ss (part)
(define-syntax with
(syntax-rules()[(_ id body ...)(dynamic-let([current id]) body ...)]))
Emacs
(let((my-current-thing thing))(do-something))
Where Tuna differs. An Emacs let of a special variable is undone when the body exits; if the body waits in a recursive edit, every other command sees the binding meanwhile. A Tuna binding is seen only by the command that made it.