tur/serial
serializable continuations for persistent workflow state.
Defines the Serializable typeclass with primitive and container instances.
Since: Phase 21
Bytes
(defopaque Bytes :ptr<void> :linear)
bytes-alloc
(bytes-alloc [n :int] :Bytes)
allocate a zeroed length-prefixed byte buffer of n bytes.
| n | number of data bytes to allocate (not counting the 8-byte length prefix) |
A :ptr<void> to the new bytes value; free with bytes-free.
(def b (bytes-alloc 16)) ; => 16-byte zeroed buffer
Since: Phase 21
bytes-len
(bytes-len [^borrow b :Bytes] :int)
return the number of data bytes stored in a bytes value.
| b | bytes pointer from bytes-alloc or a serialize call |
The data length as :int (0 for null pointer).
Since: Phase 21
bytes-data
(bytes-data [^borrow b :Bytes] :ptr<void>)
return a raw pointer to the data region of a bytes value (past the length prefix).
| b | bytes pointer from bytes-alloc or a serialize call |
:ptr<void> to the first data byte, or NULL for a null pointer.
Since: Phase 21
bytes-free
(bytes-free [b :Bytes] :void)
free a bytes value allocated by bytes-alloc or serialize.
| b | bytes pointer to free |
Since: Phase 21
bytes-concat
(bytes-concat [^borrow a :Bytes ^borrow b :Bytes] :Bytes)
concatenate two bytes values into a new heap-allocated bytes value.
| a | first bytes value (may be null, treated as empty) | |
| b | second bytes value (may be null, treated as empty) |
A new bytes value containing all bytes of a followed by all bytes of b.
(bytes-concat ba bb) ; => new bytes with ba then bb
Since: Phase 21
Serializable
(defclass Serializable [a])
typeclass for types that can be marshalled to/from bytes.
(serialize [x] :ptr<void>)-
marshal x to a length-prefixed bytes value
(deserialize [b :ptr<void>] :a)-
unmarshal from bytes; panics on malformed input
Since: Phase 21
Serializable[int]
(definstance Serializable [int])
serialize/deserialize int64 as 8 little-endian bytes.
Since: Phase 21
Serializable[bool]
(definstance Serializable [bool])
serialize/deserialize bool as a single byte (0 = false, 1 = true).
Since: Phase 21
Serializable[float]
(definstance Serializable [float])
serialize/deserialize float64 as 8 bytes IEEE 754 little-endian.
Since: Phase 21
Serializable[cstr]
(definstance Serializable [cstr])
serialize/deserialize a cstr as a 4-byte LE length prefix followed by UTF-8 bytes.
Since: Phase 21
Serializable[ptr<void>]
(definstance Serializable [ptr<void>])
serialize/deserialize a bytes value as an 8-byte LE length followed by raw data.
Since: Phase 21
serial-pair-bytes
(serial-pair-bytes [fst :int snd :int] :ptr<void>)
raw [len=16][fst][snd] Pair byte layout from two int64 element carriers.
Fixed-arity (takes the element carriers as plain ints, not the Pair), so the (serialize ...) instance body that calls it stays pure-Turmeric rather than inline-C. That lets the per-instantiation spec machinery (M4c Path A) mint a typed serialize_Pair__spec__(Pair__int__int) and dispatch a by-value Pair directly -- no concrete->carrier bridge spill. The byte layout is identical to the prior inline-C body, so deserialize is unchanged.
| fst | first element's int64 carrier | |
| snd | second element's int64 carrier |
A raw :ptr<void> bytes value (see the module note on the raw-buffer surface).
Since: end-to-end-monomorphization (M4 bucket-D)
Serializable[Pair]
(definstance Serializable [Pair])
serialize a Pair[A B] as its two int64 element carriers.
Layout: [int64 len=16][int64 fst][int64 snd].
Since: cps-transform-plan (value-typed cont env codec)
Serializable[Option]
(definstance Serializable [Option])
serialize an Option[A] as a presence flag + carrier.
Layout: [int64 len=16][int64 is-some][int64 value].
Since: cps-transform-plan (value-typed cont env codec)
cont-to-file
(cont-to-file [b :ptr<void> path :cstr] :int)
write a serialised continuation bytes value to a file.
| b | bytes value from serial-cont->bytes / save-cont! | |
| path | filesystem path to write (overwritten if it exists) |
1 on success, 0 on failure.
(cont-to-file k-bytes "/tmp/checkpoint.bin") ; => 1
Since: Phase 21
cont-from-file
(cont-from-file [path :cstr] :ptr<void>)
read a serialised continuation bytes value from a file.
| path | filesystem path previously written by cont-to-file |
A bytes :ptr<void> on success, NULL on any I/O or allocation failure.
(def k-bytes (cont-from-file "/tmp/checkpoint.bin"))
Since: Phase 21
serial-cont->bytes
(serial-cont->bytes [k :serial-cont] :ptr<void>)
marshal a captured continuation to a bytes value.
The typed spelling of save-cont!: walks the frame chain k and writes each frame's stable tag/name plus its environment (int inline, cstr and Serializable envs by value) to a length-prefixed buffer. No code address is written, so the bytes are valid in another process running the SAME program.
Compiled programs only: the frame chain this walks is the compiled program's. tur --interpret keeps its own continuation structure and stops with inline-C defn 'serial-cont->bytes' takes a parameter of type cont<int>. bytes->serial-cont and the *-cont! / cont-*-file helpers are the same.
| k | the continuation the serial-shift receiver was passed |
A caller-owned bytes :ptr<void> ({int64 len; data}), or NULL if k is nil.
(defn stash [k : serial-cont] : int (do (cont-to-file (serial-cont->bytes k) "/tmp/k.bin") 0)) (serial-reset (+ 10 (serial-shift stash 0)))
Since: Phase 21 (typed surface 2026-09-02)
bytes->serial-cont
(bytes->serial-cont [b :ptr<void>] :(Result serial-cont cstr))
rebuild a continuation from bytes, or say why not.
Validates the buffer before touching the runtime: every frame record must fit, carry a known tag, and -- for a call frame -- name a frame this program registered, with the env kind that frame was registered with. A buffer written by a different program (or a different build whose frame names moved) is therefore an Err naming the problem rather than an abort or a resumed garbage chain. resume-cont! and image/blob-resume! run the same check and panic instead.
Validation is integrity, not authenticity: a well-formed buffer from an untrusted source still rebuilds a continuation of this program's own frames with the envs it chose. Authenticate bytes that crossed a trust boundary before handing them here (the guestbook example keeps them server-side under an HMAC-signed name).
| b | a bytes value from serial-cont->bytes / save-cont! / cont-from-file |
(Ok k) with a resumable serial-cont, or (Err msg) with a static cstr: "null bytes", "short buffer", "bad frame count", "truncated frame", "frame name too long", "unknown frame (written by a different program?)", "truncated env", "bad env kind", "bad frame tag", "env kind does not match the frame" -- each prefixed with "bytes->serial-cont: ".
(match (bytes->serial-cont (cont-from-file "/tmp/k.bin")) (Ok k) (serial-resume k 5) (Err m) (do (println m) 0))
Since: Phase 21 (typed surface 2026-09-02)
serial-resume-owned
(serial-resume-owned [k :serial-cont v :int] :int)
resume a rebuilt continuation, then release it.
A continuation from bytes->serial-cont belongs to the program, and a resume runs a copy of it, so nothing else ever frees it. A serial-cont is consumed by its resume, which leaves no later point to free it at -- so this resumes and frees in one step. Only for a continuation that bytes->serial-cont returned: the one a serial-shift receiver is handed is the compiler's to free (serial-resume it), and freeing it here would free it twice. The strings a cstr-env frame was rebuilt with stay allocated, since the resumed computation may have returned one of them.
| k | a serial-cont from bytes->serial-cont | |
| v | the value to fill the hole with |
The resumed computation's result, as serial-resume.
(match (bytes->serial-cont b) (Ok k) (serial-resume-owned k 5) (Err m) (do (println m) 0))
Since: 2026-10-07 (serial-cont-escaped-or-deserialized-chain-leaks)
serial-cont-free
(serial-cont-free [k :serial-cont] :nil)
release a rebuilt continuation without resuming it.
For a continuation from bytes->serial-cont that the program decides not to resume. Same restriction as serial-resume-owned: never the receiver's k.
| k | a serial-cont from bytes->serial-cont |
(match (bytes->serial-cont b) (Ok k) (serial-cont-free k) (Err m) nil)
Since: 2026-10-07 (serial-cont-escaped-or-deserialized-chain-leaks)
serial-bytes-free
(serial-bytes-free [b :ptr<void>] :nil)
release a buffer from serial-cont->bytes.
serial-cont->bytes, save-cont! and cont-from-file return a caller-owned malloc'd buffer as a bare ptr<void> (cont-to-file and bytes->serial-cont take that form), so bytes-free, which takes the linear Bytes, does not apply. Free it with this once it has been written or rebuilt. NULL is a no-op.
| b | a buffer from serial-cont->bytes / save-cont! / cont-from-file |
(let [b (serial-cont->bytes k)] (cont-to-file b "/tmp/k.bin") (serial-bytes-free b))
Since: 2026-10-07 (serial-cont-escaped-or-deserialized-chain-leaks)
serial-resume
(serial-resume [k :serial-cont v :int] :int)
resume a continuation with a value.
(serial-resume k v) is (k v): the enclosing serial-reset's rest of the computation runs with v in the hole and its result comes back. Works on the continuation the serial-shift receiver was handed and on one rebuilt by bytes->serial-cont alike.
| k | a serial-cont | |
| v | the value to fill the hole with |
The resumed computation's result.
(defn sk [k : serial-cont] : int (serial-resume k 5)) (serial-reset (+ 10 (serial-shift sk 0))) ; => 15
Since: Phase 21 (typed surface 2026-09-02)