# Ovid Ovid is a small compiled language whose toolchain is built for agents. Every command answers in JSON lines, every error says where it is and what was expected, and every edit is checked before it is written. It is new: the first commit is from 2026-10-03. What it has today is below; what it does not have yet is on the [roadmap](/roadmap). ## Every error has a place ```console $ ovid check {"fact":"error","code":"arity","message":"Twice takes 1 arguments, got 2","id":"ex:hello.Use:3","file":"hello/extra.ov","line":8,"col":10,"end_line":8,"end_col":21,"expected":"1","got":"2","hint":"hello: func Twice(n i64) i64","source":" return Twice(1, 2)"} {"errors":1,"fact":"summary","funcs":47,"ms":2,"ok":false,"packages":3,"revision":"b3cdb13e021da9c8"} exit 1 ``` A diagnostic carries the file, line, column, and source line, the id of the expression, and what was expected and found. The last line always has `"ok"`, and the exit code says the same: 0 ok, 1 errors, 2 a stale edit. ## Edits are checked before they are written Two agents read `Greeting`, and one of them changes it. The other's edit was made from text that is no longer there, so ovid refuses it with exit 2 and sends back the current text and hash: ```console $ ovid show Greeting // func fn:hello.Greeting hello/main.ov:5-7 hash=4e5e8b1a8171 func Greeting() i64 { // @fn:hello.Greeting return strptr("hello, world\n") // @st:hello.Greeting:1 } # agent b $ ovid replace st:hello.Greeting:1 --expect 4e5e8b1a8171 <<'EOF' return strptr("hello, agents\n") EOF {"check_ok":true,"errors":0,"errors_before":0,"files":["hello/main.ov"],"ok":true,"ops":[{"decls":[{"hash":"aebb3dcdacd0","id":"fn:hello.Greeting"}],"ids":["st:hello.Greeting:1"]}],"revision":"e8df8db6cb5aff3f","written":true} # agent a, from what it read before $ ovid replace st:hello.Greeting:1 --expect 4e5e8b1a8171 <<'EOF' return strptr("hi\n") EOF {"decl":"fn:hello.Greeting","decl_hash":"aebb3dcdacd0","error":"stale","hash":"f6419685b5b3","hint":"re-read with `ovid show fn:hello.Greeting` and use the ids and hashes it prints now; text and hash here are st:hello.Greeting:1's current ones","id":"st:hello.Greeting:1","message":"fn:hello.Greeting changed since you read st:hello.Greeting:1; a statement's hash covers its whole decl, and st:/ex: ids are renumbered by edits above them","ok":false,"op":0,"text":"return strptr(\"hello, agents\\n\")"} exit 2 ``` An edit names what it changes by id and carries the hash it read. It is all or nothing: the module is reparsed and checked in memory first, and an edit that adds errors is refused. Writers to one module take a lock and replace files atomically. See [Edits](/edits). ## It says what it can ask of the kernel ```console $ ovid run hello, world $ ovid build {"bytes":745,"ok":true,"output":"hello/bin/hello","syscalls":[1,9,60]} ``` Programs are static Linux x86-64 binaries with no libc. Only the shipped `ovid/io` package may make a system call, and `ovid build` lists the ones a program can reach: for this one write, mmap, and exit. That list is enough to run it under a seccomp filter that allows nothing else. ## It compiles itself The compiler is written twice: in Go, the toolchain agents use today, and in Ovid. The Ovid one, about 6,200 lines, builds itself into a binary byte-identical to the one the Go compiler builds from the same source, and ovid's tests check that on every change. ```sh bin/ovid build -C prog -o /tmp/s1 # Go compiles the Ovid compiler /tmp/s1 build prog -o /tmp/s2 --std std # which compiles itself cmp /tmp/s1 /tmp/s2 # byte-identical ``` ## Measured with agents The repository has tasks for agents, from writing a small program to recovering from a lost edit, each checked by a program. On 2026-10-05, claude-opus-5-5 and claude-sonnet-5-5 passed all 20 runs of the four hardest and claude-haiku-4-5 passed 9. The same runs show what does not work yet: given a 7,600-line module, no agent used `ovid outline`, `refs`, or `grep` to find its way; they used grep and sed. Every run, failures included, is in [AGENT_FEEDBACK.md](https://github.com/ovid-sh/ovid/blob/main/docs/AGENT_FEEDBACK.md). ## This site The site is generated by gen, a program written in Ovid and compiled by the Ovid compiler written in Ovid. The ovid output on it is recorded when the site is built, not typed. Every page is also Markdown: add `.md` to its URL, or start at [llms.txt](/llms.txt). # Start Ovid builds wherever Go does; the programs it compiles run on Linux x86-64. Other targets are [not there yet](/roadmap). ## Install ```sh git clone https://github.com/ovid-sh/ovid cd ovid go build -o bin/ovid ./cmd/ovid ``` The toolchain needs nothing beyond Go. Elsewhere it still checks, edits, and builds Linux binaries; `run` and `test` need Linux x86-64 to run what it built. ## A module ```console $ ovid init hello {"files":["hello/ovid.mod","hello/hello/main.ov","hello/hello/main_test.ov"],"next":"cd hello && ovid run && ovid test; ovid help language","ok":true,"root":"hello"} ``` A module is a directory with `ovid.mod`. Each directory of `.ov` files in it is one package, named by its path. `init` writes a main and a test: ```ov package hello import ovid/io func Greeting() i64 { return strptr("hello, world\n") } func main(io *ovid/io.Cap) i64 { ovid/io.Print(Greeting()) return 0 } ``` ```ov package hello import ovid/io import ovid/mem func TestGreeting(io *ovid/io.Cap) i64 { if ovid/mem.EqC(Greeting(), strptr("hello, world\n"), strlen("hello, world\n")) { return 0 } return 1 } ``` ## Run and test ```console $ ovid run hello, world $ ovid build {"bytes":745,"ok":true,"output":"hello/bin/hello","syscalls":[1,9,60]} ``` ```console $ ovid test {"fact":"test","file":"hello/main_test.ov","id":"fn:hello.TestGreeting","line":6,"ms":0,"ok":true} {"fact":"summary","failed":0,"ok":true,"passed":1} ``` Each test runs in its own process and passes by returning 0. A failure names the `return` that produced it; a crash, its signal, statement, and call stack. ## Give it to an agent `ovid help` is written for an agent to read first, and each topic it names is the reference for that part. This site has them all in one file, [llms-full.txt](/llms-full.txt). ```text ovid: a small compiled language and its toolchain, built for agents. Source is plain .ov text. A module is a directory with ovid.mod; each subdirectory holding .ov files is one package, and its path is the package name. Every command prints JSON lines; the last line always has "ok". Exit codes: 0 ok, 1 errors, 2 stale edit, 64 usage, 124 run: timeout, 125 run: could not build or start the program. Paths in records are relative to the working directory; with OVID_PATHS=module in the environment, to the module root. Start here: ovid init new module with a hello-world entry and a test ovid check errors with file:line:col, expected/got, hint ovid run [-- args] build to a temp file and run it ovid run --json [--timeout 5s] the same, with exit, signal, and output as JSON ovid test [--run Name] run Test* funcs, one process each Read without opening whole files: ovid outline [--pkg P] packages, or one package's decls with hashes (outline, refs, grep print 200 records a page) ovid show ... [--plain] [--json] [--exprs] source of a decl or node, lines tagged with ids ovid refs every use of a func/type/field/const/param/var ovid grep text matches, each with its decl and stmt id Change code (or edit the .ov files directly; both are fine): ovid replace --expect H <<'EOF' one edit, code from stdin (no JSON escaping); also insert --after/--before , append , delete ovid edit id-addressed batch edit, all or nothing ovid rename rename a decl and all its uses ovid move ... move funcs/types/consts to another package Also: ovid build [-o out], ovid dump (program as JSON), ovid version (commit and binary hash: which ovid is this?), ovid help . All commands take -C (default: the module containing the cwd). The language at a glance (all of it: ovid help language): i64, bool, *T; var x i64 = 0; if/else if/else; while; no for/break/continue var p *T = ovid/io.Alloc(io, sizeof(T)) as *T structs live on the heap ovid/io.Print(strptr("hi\n")); ovid/io.PrintInt(io, n) output var sp bool = c == 32 || c == 9 && || ! work anywhere Topics: ovid help language | commands | edit | std | ids ``` # Language This is what `ovid help language` prints, recorded from the ovid this site was built with. It describes v0, the language as it is today. What it lists as absent, such as strings, slices, struct values, methods, and generics, is [not there yet](/roadmap), not ruled out. ```text Ovid language reference (v0). File: package app/util // must equal the directory path import ovid/io // one import per line, the full path const Limit i64 = 64 // consts are i64 type Pair struct { // fields are i64, bool, or *T; one per line a i64 next *Pair } func Sum(p *Pair, n i64) i64 { var t i64 = 0 // every local is declared with a type; var t i64 is 0 while n > 0 { t = t + p.a p = p.next n = n - 1 } return t } Types: i64, bool, *T (T a struct in this package or path.T from an import). No struct values, slices, arrays, strings, generics, methods, globals, or closures. At most 6 params; exactly one result type. Statements: var x T = e | var x T (zero: 0, false, or a null pointer) | x = e | p.f = e | if c { } else if c { } else { } | while c { } | return e | store8(addr, v) | store64(addr, v) | call(...). Every path through a func must return. Expressions: integers (decimal, 0x hex), true/false, names, calls f(a), other packages' funcs and consts by import path: ovid/mem.Copy(d, s, n), ovid/io.O_RDONLY (a one-segment import may also be written util.F()), field reads p.f, casts e as *T (i64 address to pointer and back), load8/load32/load64(addr), strptr("lit") / strlen("lit"), sizeof(T). Binary operators, Go precedence: || && == != < <= > >= + - | ^ * / % << >> & (>> is arithmetic). Unary: ! (bool), - and ^ (i64). Comparisons give bool; if/while conditions must be bool. && and || short-circuit and are ordinary values: var sp bool = c == 32 || c == 9 || c == 10. Strings: there is no string type. strptr("hi\n") is the address of an interned NUL-terminated literal and strlen("hi\n") is its length (3), computed by the compiler, so never count bytes by hand. Literals are read-only: a store into one kills the program (SIGSEGV), so to change the bytes, copy them first: var b i64 = ovid/io.Alloc(io, n) then ovid/mem.Copy(b, strptr("..."), n). Literals are NUL-terminated, so printing one needs only its address: ovid/io.Print(strptr("total: ")) // Eprint writes to stderr ovid/io.PrintInt(io, n) // a number in decimal ovid/io.Stdout(p, n) // n bytes at p, for non-literals Memory: no implicit allocation. ovid/io.Alloc(io, nbytes) returns an i64 address of zeroed bytes from the heap, which grows as needed and is never freed; it does not return 0 (out of memory ends the program, exit 71). Cast the address: var p *Pair = raw as *Pair. Each struct field takes 8 bytes, so a struct is 8 * fields bytes; never count them by hand, write sizeof(T) (T a struct; path.T for another package's), a compile-time i64: var p *Pair = ovid/io.Alloc(io, sizeof(Pair)) as *Pair There is no address-of (&x): locals live in registers or the stack and cannot be pointed at. When a callee must write a value back, allocate a cell and pass its address (the out-param pattern ovid/io.ReadFile uses): var pp i64 = ovid/io.Alloc(io, 8) // receives the data address var nn i64 = ovid/io.Alloc(io, 8) // receives the length if ovid/io.ReadFile(io, path, ovid/io.CLen(path), pp, nn) != 0 { return 1 } var data i64 = load64(pp) var n i64 = load64(nn) Or return a struct: func Read(...) *Result, with the fields you need. Control flow, all of it: if a < b { return 1 } else if a == b { return 0 } else { return -1 } while i < n { i = i + 1 } // no for, break, or continue: use the condition Programs: the entry package (ovid.mod "entry") has func main(io *ovid/io.Cap) i64 // result is the exit code io is the capability for argv, heap, and syscalls. syscall(...) is only allowed inside ovid/io; everyone else calls ovid/io funcs. Tests: any func TestX(io *ovid/io.Cap) i64 in any module package; 0 passes, anything else fails (the value is reported as the exit code). build and run leave out _test.ov files: an error there stops check and test, not them, and the program cannot call what they declare. ``` ## Standard library `ovid/io`, `ovid/mem`, and `ovid/test` ship inside the toolchain, and a module cannot replace them. This is `ovid help std`: ```text Shipped packages. Import them by path; a module cannot have a package of the same path. package ovid/io type Cap struct { argc i64; argv i64; heap i64; used i64; size i64 } type Temp struct { fd i64; name i64 } const SYS_READ SYS_WRITE SYS_CLOSE SYS_FSTAT SYS_MMAP SYS_EXIT SYS_OPENAT SYS_FSTATAT AT_FDCWD O_RDONLY O_WRONLY O_CREAT O_EXCL O_TRUNC EEXIST O_DIRECTORY SYS_GETDENTS64 SYS_GETPID SYS_FSYNC SYS_RENAME SYS_UNLINK SYS_FCHMOD SYS_MKDIRAT EXIT_OOM HEAPCHUNK func Argc(io *Cap) i64 // Argc is the number of command-line arguments, the program name included. func Arg(io *Cap, i i64) i64 // Arg is the address of argument i, NUL-terminated; its length is CLen. Arg(io, 0) is the program name. It is 0 for an i that is not an argument: past argv the stack holds the environment, which is not the program's to read through here. func Alloc(io *Cap, n i64) i64 // Alloc returns n zeroed bytes (8-aligned) from the heap. Nothing is ever freed. A request that does not fit in what is left of the current region starts a new region, or, when it is more than half a region, gets a mapping of its own and leaves the current region in use. The result is never 0: if the kernel refuses, the program ends with exit code 71 and "out of memory" on standard error. func Map(n i64) i64 // Map returns n fresh zeroed bytes from the kernel (mmap, private and anonymous), or ends the program when there is no memory. The mapping is not reserved (MAP_NORESERVE, 0x4000): it is address space until it is touched, so under the kernel's default overcommit heuristic a host with less memory than a region can still grant it. Strict overcommit (vm.overcommit_memory=2) ignores the flag, and other limits still apply. func CLen(p i64) i64 // CLen is the length of the NUL-terminated string at p. func Write(fd i64, p i64, n i64) i64 // Write writes all n bytes at p to fd. It returns 0, or -1 on error. func Stdout(p i64, n i64) i64 // Stdout writes n bytes at p to standard output. func Stderr(p i64, n i64) i64 // Stderr writes n bytes at p to standard error. func CStr(io *Cap, p i64, n i64) i64 // CStr copies n bytes at p into a new NUL-terminated string. func Open(path i64, flags i64, mode i64) i64 // Open opens the NUL-terminated path; it returns an fd, or < 0 on error. func Close(fd i64) i64 // Close closes fd. func Read(fd i64, p i64, n i64) i64 // Read reads up to n bytes from fd into p; it returns the count, 0 at the end, or < 0 on error. func Fstat(fd i64, buf i64) i64 // Fstat fills buf (256 bytes) with struct stat for fd: size at 48. func FstatAt(path i64, buf i64) i64 // FstatAt fills buf (256 bytes) with struct stat for the NUL-terminated path. func ReadFile(io *Cap, path i64, pathn i64, outp i64, outn i64) i64 // ReadFile reads the whole file named by path (pathn bytes). It returns 0 and stores the data address at outp and its length at outn (allocate two 8-byte cells and load64 them), or -1. The data is NUL-terminated. func WriteFile(io *Cap, path i64, pathn i64, data i64, datan i64, mode i64) i64 // WriteFile creates or truncates path with permission mode (493 is 0755, 420 is 0644) and writes datan bytes. It returns 0, or < 0 on error. func Fsync(fd i64) i64 // Fsync waits until everything written to fd is on the disk. On a directory's fd it makes the names in it (a create, rename, or unlink) durable. It returns 0, or < 0 on error. func Rename(from i64, to i64) i64 // Rename moves the file at the NUL-terminated path from to the path to, replacing what is there in one step: a reader sees the old file or the new one, never a part of either. func Unlink(path i64) i64 // Unlink removes the name at the NUL-terminated path. func DirLen(path i64, pathn i64) i64 // DirLen is the length of path's directory part, its last "/" included; 0 when path has none. func PutInt(dst i64, v i64) i64 // PutInt writes v (v >= 0) in decimal at dst, without a NUL, and returns the address after the last digit. func CreateTemp(io *Cap, path i64, pathn i64) *Temp // CreateTemp makes a new, empty file beside path (pathn bytes), named /....ovid-tmp, open for writing. The file is created exclusively and private (0600), so a name someone else put there, a symlink included, is never written through: the next n is tried. Check fd on the result. func FillTemp(t *Temp, data i64, datan i64, mode i64, sync bool) i64 // FillTemp writes datan bytes to the temp file t, gives it permission mode (not masked by the umask) once the data is in, syncs it if sync, and closes it. It returns 0, or < 0 on error, and then the file is removed. An error that only close reports (a write the kernel had put off) counts. func SyncPath(path i64) i64 // SyncPath syncs the file or directory at the NUL-terminated path (see Fsync), which must be readable. It returns 0, or < 0 on error. func WriteFileAtomic(io *Cap, path i64, pathn i64, data i64, datan i64, mode i64) i64 // WriteFileAtomic replaces or creates path (pathn bytes) with datan bytes and permission mode in one step: the data goes to a temp file in the same directory (see CreateTemp), which is renamed over path. Another process sees the old file or the new one, never a part, and a program that is running from path can be replaced. Nothing is synced, so after a power loss the file may be empty: use it for a file that can be made again, WriteFileDurable for one that cannot. It returns 0, or < 0 on error, and then path is untouched. func WriteFileDurable(io *Cap, path i64, pathn i64, data i64, datan i64, mode i64) i64 // WriteFileDurable is WriteFileAtomic that also survives a crash: the temp file is synced before the rename and the directory after it, so once it returns 0 the new content is on the disk. The two syncs cost milliseconds on a disk. It returns < 0 on error. If the temp file or the rename failed, path is untouched; but the last step, syncing the directory, comes after the rename, so when that is what failed path already holds the new content and it is only not known to be durable. A caller that must tell the two apart reads path back. func Getdents(fd i64, buf i64, n i64) i64 // Getdents reads directory entries from fd (opened with O_DIRECTORY) into buf, at most n bytes, as linux_dirent64 records: d_ino at 0, d_off at 8, d_reclen (u16) at 16, d_type at 18 (4 dir, 8 file, 10 symlink, 0 unknown), NUL-terminated name at 19. Returns the bytes read, 0 at the end, or < 0. func Mkdir(path i64) i64 // Mkdir creates the directory at the NUL-terminated path, mode 0755. func IsDir(io *Cap, path i64, pathn i64) i64 // IsDir is 1 if path (pathn bytes) is a directory, else 0. func Print(p i64) i64 // Print writes the NUL-terminated string at p to standard output, so a literal needs no length: ovid/io.Print(strptr("hi\n")). func Eprint(p i64) i64 // Eprint is Print to standard error. func PrintInt(io *Cap, v i64) i64 // PrintInt writes v in decimal to standard output. It uses heap scratch and gives it back, so it may be called in a loop. func WriteInt(io *Cap, fd i64, v i64) i64 // WriteInt writes v in decimal to fd. package ovid/mem type Buf struct { data i64; len i64; cap i64 } func Eq(a i64, an i64, b i64, bn i64) bool // Eq reports whether the an bytes at a equal the bn bytes at b. func EqC(p i64, lit i64, n i64) bool // EqC compares the NUL-terminated string at p with n bytes at lit: ovid/mem.EqC(arg, strptr("-v"), strlen("-v")). func Copy(dst i64, src i64, n i64) i64 // Copy copies n bytes from src to dst and returns n. func New(io *ovid/io.Cap, cap i64) *Buf // New is an empty Buf with room for cap bytes. func Grow(io *ovid/io.Cap, b *Buf, need i64) i64 // Grow makes room for need more bytes and returns 0. func WByte(io *ovid/io.Cap, b *Buf, c i64) i64 // WByte appends the byte c. func WBytes(io *ovid/io.Cap, b *Buf, p i64, n i64) i64 // WBytes appends n bytes at p. func FormatI64(dst i64, v i64) i64 // FormatI64 writes v in decimal at dst and returns the length. dst needs 52 bytes: digits are staged at dst+32. func HexOut(dst i64, v i64, width i64) i64 // HexOut writes the low width nibbles of v as lowercase hex at dst. package ovid/test func Eq(io *ovid/io.Cap, got i64, want i64) bool // Eq reports whether got equals want. If not, it writes "got , want " to stderr, which ovid test shows as the failed test's output: if !ovid/test.Eq(io, Sum(s), 10) { return 1 } func True(ok bool, msg i64) bool // True reports ok. If it is false, it writes msg (a NUL-terminated literal: strptr("...")) and a newline to stderr. ``` # Commands Every command prints JSON lines to stdout, one record a line, and the last line has `"ok"`. The exceptions are `help`, `show` without `--json`, and a program's own output under `run`. A consumer reads every line, takes the last as the result, and ignores keys it does not know: new keys are added, existing ones keep their meaning. | exit | meaning | |---|---| | 0 | ok | | 1 | errors | | 2 | a stale edit: the code changed since it was read | | 64 | usage | | 124 | `run --timeout` ended the program | | 125 | `run` could not build or start the program | The contract the commands share, with every error code and receipt field, is [PROTOCOL.md](https://github.com/ovid-sh/ovid/blob/main/docs/PROTOCOL.md). This is `ovid help commands`: ```text Commands. Each prints JSON lines; the last line has "ok". ovid check [--facts] One {"fact":"error"} line per problem: code, message, id, file, line, col, end_line, end_col, source, and when known expected, got, hint. Summary last: {"fact":"summary","ok",errors,packages,funcs,revision,ms}. ovid build [-o out] default out: /bin/; _test.ov files are left out (so for run) {"ok",output,bytes,syscalls}: syscalls lists the system call numbers the program can make (reachable from main, plus startup's mmap, exit, write), enough to run it under a filter that allows nothing else. ovid run [--] [args...] program stdio and exit code pass through; if the build fails: errors as JSON, exit 125. A program run by run or test gets its arguments and stdio and an empty environment: none of ovid's own variables reach it. run and test execute the program from TMPDIR (else /tmp), or from memory where that is missing or noexec and /proc is mounted; if neither works: {"ok":false,"error":"run",message,hint}, exit 125 (run) or 1 (test). Killed by a signal: exit 128+N and one line on stderr, {"ok":false,"error":"killed",signal,exit,at,stack,fault_addr,hint}, with at/stack (the statement and its callers) for a fault on Linux. --timeout D (5s, 500ms) ends the program: "signal":"timeout", exit 124. ovid run --json [--timeout D] [--max-output N] [--] [args...] captures the output; one last line, and ovid exits 0 if the program ran: {"ok":true,"exit":N,"ms",stdout,stderr}; a signal or timeout gives "signal" (with at/stack) in place of "exit". Each stream keeps N bytes (65536); past that "truncated":true and stdout_bytes/stderr_bytes. A build that fails ends {"ok":false,"errors":N}, exit 125. ovid test [--run substr] [--list] {"fact":"test",id,ok,exit,ms,output} per test; a failure that returned a value adds "returned_by": the return statements that can produce it; if !ovid/test.Eq(io, got, want) { return 1 } also puts "got X, want Y" in its output. Output past 4000 bytes is cut ("...(truncated)") and "output_bytes" gives its full size. A crash adds "signal", "at" (the statement that faulted), "stack" (it and each call leading to it, innermost first), and for a bad load or store "fault_addr"; a hung test reads "signal":"timeout". A test the kernel refused memory reads "error":"out_of_memory", exit 71. --list prints the tests without running them. ovid outline [--pkg P] [--all] [--uses] [--offset N] [--limit N] Per decl: id, kind, sig, file, line, end_line, hash, and when present doc (its doc comment: the // lines directly above it, with no blank line between), size (struct bytes), test. --uses adds used_by: {package: refs}, so {} is dead code and a decl used by only one other package is a candidate to move there. Paged like grep: at most 200 records, and the last line says where the next page starts. A name declared twice in a package lists each copy where it is, with its own hash and id_copies:N (see ovid help ids). ovid show ... [--plain] [--json] [--exprs] Text: "// kind id file:a-b hash=H in=decl type=T" then the source, with " // @id" after each line where a statement starts (--plain omits them). A decl's source starts at its doc comment, and a-b covers it. For a statement or expression (a decl with --exprs), one line per expression inside it follows: "// ex:id line:col text hash=H type=T", in source order, outer before inner. --json: {id,kind,file,line,end_line, hash,decl,parent,text,sig,type}, plus doc and doc_line (where it starts; line is the decl's own first line) for a decl with a doc comment, and "exprs":[{id,line,col,text,hash,type}]. Replace one by id to change part of a statement. ovid refs [--offset N] [--limit N] {id,kind,in,file,line,col,source} per use, in source order: the names the checker resolved to it, so a field or local spelled like a type, func, or const is not a use of it; last: {"ok":true,target,files,by_pkg:{package: n},external} for all the uses, and the paging fields for the ones printed. ovid grep [--pkg P] [--std] [--offset N] [--limit N] {file,line,col,match,source,decl,stmt} per match (RE2 syntax). A match in a doc comment is in that comment's decl. Paging, for outline, refs, and grep: at most 200 records unless --limit (0: all), starting after --offset; last: {"ok",count,total,offset, has_more,next_offset,revision}. count is what was printed, total all there is; pass next_offset as --offset for the next page, and if revision has changed between pages, start again. ovid edit [--rev REV] [--dry-run] [--require-clean|--allow-broken] [--show] [--force] see: ovid help edit ovid replace | insert --after | insert --before | append | delete --expect H | --rev REV | --force [--text-file F] [--dry-run] [--require-clean|--allow-broken] [--show] One edit op; the text is read from stdin (or F), so a heredoc works: ovid replace st:app.main:3 --expect 1f0c9a2b7d4e <<'EOF' ovid/io.Stdout(strptr("a \"quoted\" line\n"), strlen("a \"quoted\" line\n")) EOF Same checks and result as ovid edit. Every op needs a guard: --expect (the hash of the node it names), --rev (the module revision), or --force; only append goes without. ovid rename [--dry-run] Rewrites the declaration's name and each use refs lists, nothing else (a field, local, comment, or string spelled the same is left alone); refuses collisions and changes that add check errors. Like move it takes no --expect: it carries no code, is planned from the module as it is, and a replay is refused. ovid move ... [--file pkg/x.ov] [--dry-run] Moves funcs, types, or consts (with doc comments) to pkg, creating it if needed; requalifies every use and adds the imports files now need. Refuses changes that add check errors. Several names move in order, all or none: each move is planned in memory over the ones before it, and only when all pass are the files written, together; --dry-run writes nothing. One receipt per name, then {"ok":true,"moved":[...],"to","written"}. ovid init [--name N] writes ovid.mod, /main.ov, /main_test.ov ovid dump [--pkg P] [-o file] the program as one JSON document, not paged and large (megabytes for a few thousand lines): for tools, not for reading. --pkg keeps one package; -o writes it to a file and prints {"ok",output,bytes,revision} instead. A string literal that is not UTF-8 is "value_hex", not "value". ovid version {commit, dirty, binary (hash of the executable), path} ovid help [topic|command] a topic, or the entry above for one command ``` # 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 `outline` or `show` printed, 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.mod` and replace files atomically. The lock does not make a stale read safe; the hash does. ## Ids ```text 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 ```text 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 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. ``` # Roadmap Ovid's first commit is from 2026-10-03. v0 is the smallest language that can compile itself, and the toolchain around it; most of what it lacks, it lacks because it is new. ## Committed These are recorded in [COMMITMENTS.md](https://github.com/ovid-sh/ovid/blob/main/COMMITMENTS.md) and not implemented yet. - **HTTP as the first standard library.** Network is part of the language: the best HTTP client, harder to misuse than Go's `net/http`. - **URL imports.** The import syntax stays and the path is still the package name; a path that is a URL is fetched with the HTTP capability instead of read from the module directory. - **Serving programs.** The same capability serves user programs. When it does, this site will be served by one. ## Not there yet The language reference describes v0 by what it does not have. None of it is ruled out: - strings, slices, arrays, and struct values - globals, closures, function pointers, methods, and generics - more than six parameters, or more than one result - `for`, `break`, and `continue` - targets other than Linux x86-64 - a package server ## Not on this site yet - the command protocol as a page, from `docs/PROTOCOL.md` - the agent tasks and every recorded run, from `tests/agent` and `docs/agent-runs` - the self-hosted compiler's size and build times, from ovid's CI benchmark