Edits
An agent can edit .ov files directly, and ovid will check them. It can also edit through ovid: name a node by id, say which version of it was read, and the edit is refused rather than applied to code that has changed since.
- Ids. A declaration's id is its name,
fn:pkg.Name, and stays put. A statement's or expression's id is its position,st:pkg.Func:3, so an insert above it renumbers it. - Guards. Every edit carries the hash that
outlineorshowprinted, the module revision, or an explicit--force. A statement's hash covers its whole declaration, so an edit cannot land on a statement that took another's id. - All or nothing. A batch is planned, the module is reparsed and checked in memory, and nothing is written if any op is stale or the result has new errors.
- Many writers. Writers to one module take a lock on
ovid.modand replace files atomically. The lock does not make a stale read safe; the hash does.
Ids
Ids name every declaration and node. They are derived from source order,
so get fresh ones after edits (edit returns the new ones).
pkg:P package im:P:Q import of Q in P
cn:P.Name const ty:P.Name type
fld:P.Type.f field fn:P.Name func
pa:P.Func.x param
st:P.Func:N statement N in the func ex:P.Func:N expression
Commands that take an id also take a name: Sum, util.Sum (a trailing part of
the package path), app/util.Sum, Pair.next (a field), Sum.n (a param).
A hash is a short digest of a node's source text: edits use it to refuse
writing over text that changed since it was read. A func's, type's, or
const's text includes its doc comment, so editing the comment changes the
hash of the decl and of every statement in it. A st:/ex: id is a
position, so its hash also covers the whole decl it is in: any change to
that decl, anywhere in it, makes every statement hash read before it stale,
while a change to another decl leaves them alone (see ovid help edit).
A name declared twice in a package gives two nodes one id (check reports
it). outline and show list each copy at its own file:line with its own
hash (show types only the first copy, the one check checks); an edit
picks one by that hash as expect (else ambiguous_id), and
refs, rename, and move refuse the id until one copy is gone.
Batch edits
ovid edit: id-addressed edits, applied all or none.
For a single op, ovid replace/insert/append/delete read the text from
stdin and need no JSON; see ovid help commands.
Input (a file, or - for stdin) is {"ops":[...]} or a bare list of ops:
{"op":"replace","id":ID,"text":SRC}
{"op":"delete","id":ID}
{"op":"insert","before":ID,"text":SRC} or "after":ID
{"op":"append","into":FUNC_OR_IF_OR_WHILE_ID,"text":STMTS}
{"op":"append","into":"pkg/path","text":DECLS[,"file":"pkg/path/x.ov"]}
Keys are exact: one that is not listed here, one given twice, or one
that another op takes (a replace with "before", a delete with "text"), fails
with bad_edit and names it; nothing is written. The same holds for the
flags of ovid replace/insert/append/delete.
Every op needs a guard, or it is refused (expect_required, nothing
written): "expect":HASH, the hash of the node it names (from outline, show,
or a receipt); or a top-level "revision" (from check, outline, or a
receipt; --rev on the command line), which guards the whole module; or
--force. If the node changed since it was read the edit is refused with
exit 2 and its current hash and text. The one exception is append into a
package path: it names no node and overwrites nothing, and a replay is
refused as a duplicate name, so it needs no guard (and takes no expect).
A st:/ex: id is a position, renumbered by any insert above it. Its own hash
or its decl's (the one in the header ovid show prints) both work, and both
are bound to the decl as it was read: after any change to that decl the
edit is stale, so a retried or late edit never lands on the statement that
took its id, not even an identical twin. Edits to different decls do not
disturb each other. Re-read with ovid show (or use the decl hash in the
last receipt). A current hash passed with the wrong id is refused and names
its node. --force skips every guard.
An id that several nodes share (a name declared twice; check reports it)
needs the hash of the copy to edit as expect: without one the op fails
with ambiguous_id and "copies":[{file,line,end_line,hash}], and --rev or
--force does not pick one. Copies with the same hash are the same text;
the op takes the first.
ID may be a full id or a decl name (Sum, util.Sum). Text is plain Ovid; its
indentation is normalised to the target's. insert anchors on statements and
decls; to change part of a statement, replace one of its expressions (ovid
show <stmt> lists them with ids and hashes).
A func's, type's, or const's doc comment (the // lines directly above it,
no blank line between) is part of it, as ovid show prints it: replace puts
the text's own doc comment in its place, and text without one keeps it;
delete removes it; insert before puts the new text above it.
After applying, the module is reparsed (a syntax error rejects everything
and names the op) and checked. Result: {"ok":true,"written","files",
"check_ok","errors","errors_before","revision","ops":[{"ids":[...],
"decls":[{"id","hash"}]}]}: ids are the nodes the op wrote, decls the
top-level decls it touched with their new hashes (--show adds "text"), so a
follow-up edit can "expect" them without reading again: the decl it wrote
into, or each decl its text holds (append or insert of several, or a decl
replaced by several), in source order; a delete lists the decl it was in,
none for a whole decl.
Examples:
fix one argument: {"op":"replace","id":"ex:app.main:2","expect":H,"text":"2"}
rewrite a statement: {"op":"replace","id":"st:app.main:3","expect":H,
"text":"if n > 0 {\n return n\n}"}
replace a whole func by name, guarded:
{"op":"replace","id":"Sum","expect":"c67681b88f86","text":"func Sum(...) i64 {...}"}
add a func in a new file: {"op":"append","into":"app/util",
"file":"app/util/extra.ov","text":"func Half(x i64) i64 {\n return x / 2\n}"}
An edit that adds check errors is refused and nothing is written;
--require-clean also refuses one that leaves any, and --allow-broken
writes it anyway (one step of a change that spans several edits).
--dry-run applies in memory and reports, without writing; it fails
(ok:false, exit 1) if the change would add errors.
Writers (edit, rename, move) take a lock on ovid.mod. If another holds it,
one {"fact":"waiting","for":"lock"} line is printed, and after 10s
(OVID_LOCK_TIMEOUT=30s, 500ms, 0 changes it) the command fails with
lock_timeout, writing nothing.