A plugin is a folder whose plugin.ss runs top to bottom when the plugin turns on. A contract is the set of names a plugin offers others. These forms say what a plugin is, what it offers, and what it uses.
(plugin NAME [(export x …)] [(implements (c) …)] [(import (c) …)] body …)core
Where. The only form in NAME/plugin.ss. The three clauses are optional, in any order, before the body.
Does. Defines a plugin. When the plugin turns on, its body runs once, top to bottom, like a script. Definitions (define, define-saved, define-hook, …) and actions (keys:bind!, add-hook!, start-job, …) may be mixed in any order.
Underneath. The build turns the file into an ordinary Chez library whose one procedure runs the body forms in order. While the body runs, every change made through the core goes on this run’s receipt, so it can be undone. The plugin’s plain variables live in that library and are dropped with the run.
Errors. At build, each with file and line: a name from a contract not imported (contract buffers is not imported; write (import (buffers))); a name a contract does not have (contract buffers has no insert); a call to fork-thread (use a worker); a macro use that expands to definitions followed by an expression; folder and name disagree; two plugins with one name. A use of eval is reported, not refused: it is the one way around imports. While starting, an error undoes the start (see a failed start), and a start that takes too long is stopped as if quit.
Tuna · hello/plugin.ss
(plugin hello
(import(buffers)(commands)(keys))(commands:define(say-hello)"Insert a greeting at point."(buffers:insert!"Hello, world!"))(keys:bind!"C-c h"'say-hello))
Emacs · hello.el
(defun say-hello ()"Insert a greeting at point."(interactive)(insert "Hello, world!"))(keymap-global-set "C-c h"#'say-hello)(provide'hello)
Where Tuna differs. Emacs loads a file once with require and forgets what it did; unloading it later cannot find the key it bound. A Tuna plugin’s file runs again at each reload and everything it did is undone first, so editing the file and reloading gives exactly what the file now says.
(export x …)core
Does. When the plugin has no contract.ss, its interface is this list, named after the plugin: (export create insert!) in plugin buffers makes the contract (buffers), used as buffers:create. A plugin with neither offers nothing to others.
Underneath. The build records the kind of each exported name (procedure, saved data, hook, table, option, macro, record, other value), so importers and replacements can be checked.
Errors. An exported variable must be a constant: an exported name the file assigns with set! is refused. Saved data and options are not variables, so they may be exported. A plugin with both export and contract.ss is reported, and contract.ss wins.
Tuna · commands/plugin.ss (shortened)
(plugin commands
(export register! run!)(define-table all "Commands by name.")(define(register! name doc procedure)(as-caller(table-set! all name procedure))procedure)(define(run! name . args)(apply table-call all name args)))
Emacs
;; Emacs has no export list. A package's public;; names are those without a double hyphen:(defun my-pkg-run (name) …); public(defun my-pkg--helper (x) …); private, by convention
Where Tuna differs. The prefix in my-pkg--helper is a convention anyone can ignore. A Tuna plugin that names another’s private definition is refused at build.
(contract (NAME) entry …)core
Where. The only form in a plugin’s contract.ss.
Does. Declares an interface apart from any implementation. Its name is written like a library name, (text-store), so it can differ from the plugin names: plugin rope-store holds the contract.ss for (text-store), and plugin string-store says (implements (text-store)). Entries:
(procedure name (arg …) "doc"): a procedure taking those arguments.
(saved name initial "doc" check): saved data, declared whole: the build writes the define-saved into every implementation, so a replacement reads the same data in the same shape.
(option name default "doc" check), (hook name "doc"): an option or hook the provider defines.
(macro name), (record name), (constant name): a macro, a record type, a constant value.
Underneath. The build reads the entries to check every plugin that imports or implements the contract. A contract’s checks may use only standard procedures and the plain-data library, so contract.ss stays declarations.
Errors. A malformed entry, with a list of the entry forms.
Tuna · minibuffer/contract.ss
(contract(minibuffer)(saved history '()"Answers to prompts by kind, newest first."(list-of (pair-of symbol? (list-of string?))))(procedure read-string (prompt kind)"Ask the user for a string and return it.")(procedure yes-or-no? (question)"Ask a yes-or-no question and return #t or #f.")(hook opened-hook "Called with the kind of a prompt that opened.")(hook closed-hook "Called with the kind of a prompt that closed."))
Emacs
;; The closest Emacs has: a variable holding the;; function that reads, which another package replaces.(defvar completing-read-function #'completing-read-default"The function called by `completing-read'.")(setq completing-read-function #'ivy-completing-read)
Where Tuna differs. Emacs swaps an implementation one variable at a time, and nothing checks that the replacement accepts the same arguments. A Tuna contract is checked whole, and its saved data passes to the replacement.
(import (c) …) and the names c:xcore
Does. Makes a contract’s names usable as c:x: (import (buffers)) gives buffers:create, buffers:insert!, buffers:after-change-hook and the rest. Nothing else of another plugin is reachable. A procedure is called as usual; saved data, options and dynamic values are read by name (buffers:current); a hook is passed to add-hook!.
Underneath. Each c:x is a small macro. A procedure name expands to a wrapper, a stand-in procedure that (1) finds whichever plugin provides c now, so a stored buffers:kill! follows a reload or swap; (2) starts that plugin first if it is chosen in init.ss but has not started (first need); (3) records, while your plugin is starting, that you used c, so your plugin reruns if c’s provider changes; (4) marks the call with your run, so the provider knows its caller (see as-caller and on-caller-end). A call through a wrapper cost about 18 ns in the prototype, against 11 ns for a direct call.
Errors. At build, with file and line: a name the contract lacks; a contract not imported. At run: buffers:insert!: cannot be called: its provider buffers is off; … its provider X failed: reason.
Tuna
(plugin greeter
(import(buffers))(define(greet! id)(buffers:with id (buffers:insert!"Hello\n"))))
Emacs
(require'buffers); Emacs has no such package; for shape only(defun greet (buf)(with-current-buffer buf (insert "Hello\n")))
Where Tuna differs. Emacs’s require loads a file and every name in Emacs is then visible to everyone. Tuna’s import makes visible only the contract’s names, each with its prefix, so a reader always sees where a name comes from. The prefix makes buffers:insert! longer than Emacs’s insert; it is the owner’s choice, made for clarity.
(implements (c) …)core
Does. Says this plugin can stand in for contract c. Its definitions fill c’s names: other plugins call c:x and reach this plugin when init.ss chooses it.
Underneath. At build, every name the contract declares must be defined with a fitting kind. At load, every declared procedure must accept the declared number of arguments (checked with Chez’s procedure-arity-mask). Saved data the contract declares is stored under the contract’s name, and the implementation uses it by its short name without declaring it again.
Errors. All mismatches in one message: ivy: does not fit (minibuffer): read-string is not defined; yes-or-no? takes 2 arguments, the contract says 1.
Tuna · string-store/plugin.ss
(plugin string-store
(implements(text-store))(define(make s)(string-copy s))(define(insert t pos s)(string-append (substring t0 pos) s (substring t pos (string-length t))))(define(delete t from to)(string-append (substring t0 from)(substring t to (string-length t))))(define(kind)'string))
Emacs
;; ivy stands in for the default completion by;; replacing the function variable when its mode turns on.(define-minor-mode ivy-mode "Use ivy for completion.":globalt(setq completing-read-function
(if ivy-mode #'ivy-completing-read#'completing-read-default)))
Where Tuna differs. Emacs has no notion of “this package implements that interface”; a replacement overrides variables or advises functions, and can miss one. Tuna checks the whole interface before the replacement runs.
(include "file.ss")core
Does. Reads the forms of a file in the plugin’s folder in place, as Chez’s include does. Used to split a long plugin, and to share code between a plugin and its worker.
Underneath. Inside a define-worker, the included code is compiled into the worker’s own library, so the worker has its own copy.
Errors. A missing file, at build, with the including line.
Tuna
(define-worker(match in)(include"fuzzy.ss")(post (best-matches (vector-ref in 0)(vector-ref in 1)50)))
Does. Tells whether contract c has a running provider. Use it for a contract you can do without.
Underneath. Asked while your plugin is starting, it counts as a use of c: when the answer changes, your plugin reruns and adapts. If c’s provider is chosen but not started, it is started first, as a call would.
Does. Work as in Chez. A plugin may export a macro (others use it as c:m, as commands:define) and define records.
Underneath. Because the build reads Chez’s expansion, a macro that defines names defines ordinary names: they can be exported, redefined and described like any other. An exported macro whose expansion calls its own contract’s procedures reaches them through the importer’s wrapper, so the importer is the caller: commands:define in plugin hello registers a command owned by hello. Records in plugins are nongenerative, with an identity made from the record’s name and fields, so a reload with the same fields gives the same record type.
Errors. A macro or a record cannot be changed live: C-M-x on either is refused with “reload the plugin, which rebuilds the plugins that use it”. Records are not plain data, so they cannot be saved or handed to a worker; use define-plain-record for those.
Tuna
(define-syntax with-saved-point
(syntax-rules()[(_ body ...)(let([p (buffers:point)])(let([v (begin body ...)])(buffers:goto! p) v))]))