sx: step 13 — buffer primitives + buffer-based inspect

Added short aliases make-buffer / buffer? / buffer-append! / buffer->string /
buffer-length on both OCaml and JS hosts, sharing the existing StringBuffer
value type. buffer-append! auto-coerces non-strings via inspect.

Rewrote the OCaml host inspect function to walk a single shared Buffer.t
instead of allocating O(n) intermediate strings via String.concat at every
recursion level. inspect underlies sx-serialize and error-path formatting,
so this benefits the tightest serialization paths.

Median improvements (bin/bench_inspect.exe, best-of-3 of 9-run min):
  tree-d8 (75KB):    5.31ms -> 1.30ms  (-76%)
  tree-d10 (679KB): 81.89ms -> 16.02ms (-80%)
  dict-1000:         0.80ms -> 0.31ms  (-61%)
  list-2000:         0.74ms -> 0.33ms  (-55%)

Tests: OCaml 4545 -> 4550. JS 2591 -> 2596. Zero regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 02:16:59 +00:00
parent c48911e591
commit 0e022ab670
9 changed files with 303 additions and 67 deletions

View File

@@ -1476,6 +1476,22 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
}; };
PRIMITIVES["string-buffer->string"] = function(buf) { return buf.parts.join(""); }; PRIMITIVES["string-buffer->string"] = function(buf) { return buf.parts.join(""); };
PRIMITIVES["string-buffer-length"] = function(buf) { return buf.len; }; PRIMITIVES["string-buffer-length"] = function(buf) { return buf.len; };
// Short aliases — terser names; append accepts any value
PRIMITIVES["make-buffer"] = function() { return new SxStringBuffer(); };
PRIMITIVES["buffer?"] = function(x) { return x instanceof SxStringBuffer; };
PRIMITIVES["buffer-append!"] = function(buf, v) {
var s;
if (v === null || v === undefined || v === NIL) s = "";
else if (typeof v === "string") s = v;
else if (typeof v === "boolean") s = v ? "true" : "false";
else if (typeof v === "number") s = String(v);
else if (v && typeof v === "object" && typeof v.name === "string" && v.constructor && v.constructor.name === "Symbol") s = v.name;
else s = (typeof inspect === "function") ? inspect(v) : String(v);
buf.parts.push(s); buf.len += s.length; return NIL;
};
PRIMITIVES["buffer->string"] = function(buf) { return buf.parts.join(""); };
PRIMITIVES["buffer-length"] = function(buf) { return buf.len; };
''', ''',
"stdlib.format": ''' "stdlib.format": '''

View File

@@ -0,0 +1,46 @@
(* Benchmark inspect on representative SX values.
Takes min of 9 runs of n iterations to dampen GC noise. *)
open Sx_types
let rec make_tree d =
if d = 0 then String "leaf"
else List [String "node"; make_tree (d - 1); make_tree (d - 1); make_tree (d - 1)]
let bench_min label f n runs =
let times = ref [] in
for _ = 1 to runs do
Gc.compact ();
let t0 = Unix.gettimeofday () in
for _ = 1 to n do ignore (f ()) done;
let t1 = Unix.gettimeofday () in
times := (t1 -. t0) :: !times
done;
let sorted = List.sort compare !times in
let min_t = List.nth sorted 0 in
let median = List.nth sorted (runs / 2) in
Printf.printf " %-30s min=%6.2fms median=%6.2fms (n=%d * %d runs)\n%!"
label (min_t *. 1000.0 /. float_of_int n)
(median *. 1000.0 /. float_of_int n) n runs
let () =
let tree8 = make_tree 8 in
let s = inspect tree8 in
Printf.printf "tree-d8 inspect len=%d\n%!" (String.length s);
bench_min "inspect tree-d8" (fun () -> inspect tree8) 50 9;
let tree10 = make_tree 10 in
let s = inspect tree10 in
Printf.printf "tree-d10 inspect len=%d\n%!" (String.length s);
bench_min "inspect tree-d10" (fun () -> inspect tree10) 5 9;
let dict_xs = make_dict () in
for i = 0 to 999 do
Hashtbl.replace dict_xs (string_of_int i) (Integer i)
done;
let d = Dict dict_xs in
bench_min "inspect dict-1000" (fun () -> inspect d) 100 9;
let xs = ref [] in
for i = 0 to 1999 do xs := Integer i :: !xs done;
let lst = List !xs in
bench_min "inspect list-2000" (fun () -> inspect lst) 200 9

View File

@@ -1,5 +1,5 @@
(executables (executables
(names run_tests debug_set sx_server integration_tests bench_cek) (names run_tests debug_set sx_server integration_tests bench_cek bench_inspect)
(libraries sx unix threads.posix otfm yojson)) (libraries sx unix threads.posix otfm yojson))
(executable (executable

View File

@@ -1607,6 +1607,32 @@ let () =
match args with [StringBuffer buf] -> Integer (Buffer.length buf) match args with [StringBuffer buf] -> Integer (Buffer.length buf)
| _ -> raise (Eval_error "string-buffer-length: expected (buffer)")); | _ -> raise (Eval_error "string-buffer-length: expected (buffer)"));
(* Short aliases — same StringBuffer value, terser names for hot paths.
Append accepts any value: strings pass through, others get inspected/coerced. *)
register "make-buffer" (fun _ -> StringBuffer (Buffer.create 64));
register "buffer?" (fun args ->
match args with [StringBuffer _] -> Bool true | [_] -> Bool false
| _ -> raise (Eval_error "buffer?: expected 1 arg"));
register "buffer-append!" (fun args ->
match args with
| [StringBuffer buf; String s] -> Buffer.add_string buf s; Nil
| [StringBuffer buf; Integer n] -> Buffer.add_string buf (string_of_int n); Nil
| [StringBuffer buf; Number n] -> Buffer.add_string buf (Sx_types.format_number n); Nil
| [StringBuffer buf; Symbol s] -> Buffer.add_string buf s; Nil
| [StringBuffer buf; Char n] ->
Buffer.add_utf_8_uchar buf (Uchar.of_int n); Nil
| [StringBuffer buf; Nil] -> Buffer.add_string buf ""; Nil
| [StringBuffer buf; Bool true] -> Buffer.add_string buf "true"; Nil
| [StringBuffer buf; Bool false] -> Buffer.add_string buf "false"; Nil
| [StringBuffer buf; v] -> Buffer.add_string buf (inspect v); Nil
| _ -> raise (Eval_error "buffer-append!: expected (buffer value)"));
register "buffer->string" (fun args ->
match args with [StringBuffer buf] -> String (Buffer.contents buf)
| _ -> raise (Eval_error "buffer->string: expected (buffer)"));
register "buffer-length" (fun args ->
match args with [StringBuffer buf] -> Integer (Buffer.length buf)
| _ -> raise (Eval_error "buffer-length: expected (buffer)"));
(* Capability-based sandboxing — gate IO operations *) (* Capability-based sandboxing — gate IO operations *)
let cap_stack : string list ref = ref [] in let cap_stack : string list ref = ref [] in
register "with-capabilities" (fun args -> register "with-capabilities" (fun args ->

View File

@@ -817,14 +817,15 @@ let dict_vals (d : dict) =
(** {1 Value display} *) (** {1 Value display} *)
let rec inspect = function (* Single shared buffer for the entire inspect recursion — eliminates
| Nil -> "nil" the per-level [String.concat (List.map inspect ...)] allocation. *)
| Bool true -> "true" let rec inspect_into buf = function
| Bool false -> "false" | Nil -> Buffer.add_string buf "nil"
| Integer n -> string_of_int n | Bool true -> Buffer.add_string buf "true"
| Number n -> format_number n | Bool false -> Buffer.add_string buf "false"
| Integer n -> Buffer.add_string buf (string_of_int n)
| Number n -> Buffer.add_string buf (format_number n)
| String s -> | String s ->
let buf = Buffer.create (String.length s + 2) in
Buffer.add_char buf '"'; Buffer.add_char buf '"';
String.iter (function String.iter (function
| '"' -> Buffer.add_string buf "\\\"" | '"' -> Buffer.add_string buf "\\\""
@@ -833,72 +834,129 @@ let rec inspect = function
| '\r' -> Buffer.add_string buf "\\r" | '\r' -> Buffer.add_string buf "\\r"
| '\t' -> Buffer.add_string buf "\\t" | '\t' -> Buffer.add_string buf "\\t"
| c -> Buffer.add_char buf c) s; | c -> Buffer.add_char buf c) s;
Buffer.add_char buf '"'; Buffer.add_char buf '"'
Buffer.contents buf | Symbol s -> Buffer.add_string buf s
| Symbol s -> s | Keyword k -> Buffer.add_char buf ':'; Buffer.add_string buf k
| Keyword k -> ":" ^ k
| List items | ListRef { contents = items } -> | List items | ListRef { contents = items } ->
"(" ^ String.concat " " (List.map inspect items) ^ ")" Buffer.add_char buf '(';
(match items with
| [] -> ()
| x :: rest ->
inspect_into buf x;
List.iter (fun v -> Buffer.add_char buf ' '; inspect_into buf v) rest);
Buffer.add_char buf ')'
| Dict d -> | Dict d ->
let pairs = Hashtbl.fold (fun k v acc -> Buffer.add_char buf '{';
(Printf.sprintf ":%s %s" k (inspect v)) :: acc) d [] in let first = ref true in
"{" ^ String.concat " " pairs ^ "}" Hashtbl.iter (fun k v ->
if !first then first := false else Buffer.add_char buf ' ';
Buffer.add_char buf ':'; Buffer.add_string buf k;
Buffer.add_char buf ' '; inspect_into buf v) d;
Buffer.add_char buf '}'
| Lambda l -> | Lambda l ->
let tag = match l.l_name with Some n -> n | None -> "lambda" in let tag = match l.l_name with Some n -> n | None -> "lambda" in
Printf.sprintf "<%s(%s)>" tag (String.concat ", " l.l_params) Buffer.add_char buf '<'; Buffer.add_string buf tag;
Buffer.add_char buf '('; Buffer.add_string buf (String.concat ", " l.l_params);
Buffer.add_string buf ")>"
| Component c -> | Component c ->
Printf.sprintf "<Component ~%s(%s)>" c.c_name (String.concat ", " c.c_params) Buffer.add_string buf "<Component ~"; Buffer.add_string buf c.c_name;
Buffer.add_char buf '('; Buffer.add_string buf (String.concat ", " c.c_params);
Buffer.add_string buf ")>"
| Island i -> | Island i ->
Printf.sprintf "<Island ~%s(%s)>" i.i_name (String.concat ", " i.i_params) Buffer.add_string buf "<Island ~"; Buffer.add_string buf i.i_name;
Buffer.add_char buf '('; Buffer.add_string buf (String.concat ", " i.i_params);
Buffer.add_string buf ")>"
| Macro m -> | Macro m ->
let tag = match m.m_name with Some n -> n | None -> "macro" in let tag = match m.m_name with Some n -> n | None -> "macro" in
Printf.sprintf "<%s(%s)>" tag (String.concat ", " m.m_params) Buffer.add_char buf '<'; Buffer.add_string buf tag;
| Thunk _ -> "<thunk>" Buffer.add_char buf '('; Buffer.add_string buf (String.concat ", " m.m_params);
| Continuation (_, _) -> "<continuation>" Buffer.add_string buf ")>"
| CallccContinuation (_, _) -> "<callcc-continuation>" | Thunk _ -> Buffer.add_string buf "<thunk>"
| NativeFn (name, _) -> Printf.sprintf "<native:%s>" name | Continuation (_, _) -> Buffer.add_string buf "<continuation>"
| Signal _ -> "<signal>" | CallccContinuation (_, _) -> Buffer.add_string buf "<callcc-continuation>"
| RawHTML s -> Printf.sprintf "\"<raw-html:%d>\"" (String.length s) | NativeFn (name, _) ->
| Spread _ -> "<spread>" Buffer.add_string buf "<native:"; Buffer.add_string buf name; Buffer.add_char buf '>'
| SxExpr s -> Printf.sprintf "\"<sx-expr:%d>\"" (String.length s) | Signal _ -> Buffer.add_string buf "<signal>"
| Env _ -> "<env>" | RawHTML s ->
| CekState _ -> "<cek-state>" Buffer.add_string buf "\"<raw-html:";
| CekFrame f -> Printf.sprintf "<frame:%s>" f.cf_type Buffer.add_string buf (string_of_int (String.length s));
| VmClosure cl -> Printf.sprintf "<vm:%s>" (match cl.vm_name with Some n -> n | None -> "anon") Buffer.add_string buf ">\""
| Spread _ -> Buffer.add_string buf "<spread>"
| SxExpr s ->
Buffer.add_string buf "\"<sx-expr:";
Buffer.add_string buf (string_of_int (String.length s));
Buffer.add_string buf ">\""
| Env _ -> Buffer.add_string buf "<env>"
| CekState _ -> Buffer.add_string buf "<cek-state>"
| CekFrame f ->
Buffer.add_string buf "<frame:"; Buffer.add_string buf f.cf_type; Buffer.add_char buf '>'
| VmClosure cl ->
Buffer.add_string buf "<vm:";
Buffer.add_string buf (match cl.vm_name with Some n -> n | None -> "anon");
Buffer.add_char buf '>'
| Record r -> | Record r ->
let fields = Array.to_list (Array.mapi (fun i v -> Buffer.add_string buf "<record:"; Buffer.add_string buf r.r_type.rt_name;
Printf.sprintf "%s=%s" r.r_type.rt_fields.(i) (inspect v) Array.iteri (fun i v ->
) r.r_fields) in Buffer.add_char buf ' ';
Printf.sprintf "<record:%s %s>" r.r_type.rt_name (String.concat " " fields) Buffer.add_string buf r.r_type.rt_fields.(i);
| Parameter p -> Printf.sprintf "<parameter:%s>" p.pm_uid Buffer.add_char buf '=';
inspect_into buf v) r.r_fields;
Buffer.add_char buf '>'
| Parameter p ->
Buffer.add_string buf "<parameter:"; Buffer.add_string buf p.pm_uid; Buffer.add_char buf '>'
| Vector arr -> | Vector arr ->
let elts = Array.to_list (Array.map inspect arr) in Buffer.add_string buf "#(";
Printf.sprintf "#(%s)" (String.concat " " elts) Array.iteri (fun i v ->
| VmFrame f -> Printf.sprintf "<vm-frame:ip=%d base=%d>" f.vf_ip f.vf_base if i > 0 then Buffer.add_char buf ' ';
| VmMachine m -> Printf.sprintf "<vm-machine:sp=%d frames=%d>" m.vm_sp (List.length m.vm_frames) inspect_into buf v) arr;
| StringBuffer buf -> Printf.sprintf "<string-buffer:%d>" (Buffer.length buf) Buffer.add_char buf ')'
| HashTable ht -> Printf.sprintf "<hash-table:%d>" (Hashtbl.length ht) | VmFrame f ->
Buffer.add_string buf (Printf.sprintf "<vm-frame:ip=%d base=%d>" f.vf_ip f.vf_base)
| VmMachine m ->
Buffer.add_string buf (Printf.sprintf "<vm-machine:sp=%d frames=%d>" m.vm_sp (List.length m.vm_frames))
| StringBuffer b ->
Buffer.add_string buf (Printf.sprintf "<string-buffer:%d>" (Buffer.length b))
| HashTable ht ->
Buffer.add_string buf (Printf.sprintf "<hash-table:%d>" (Hashtbl.length ht))
| Char n -> | Char n ->
let name = match n with Buffer.add_string buf "#\\";
| 32 -> "space" | 10 -> "newline" | 9 -> "tab" (match n with
| 13 -> "return" | 0 -> "nul" | 27 -> "escape" | 32 -> Buffer.add_string buf "space"
| 127 -> "delete" | 8 -> "backspace" | 10 -> Buffer.add_string buf "newline"
| _ -> let buf = Buffer.create 1 in | 9 -> Buffer.add_string buf "tab"
Buffer.add_utf_8_uchar buf (Uchar.of_int n); | 13 -> Buffer.add_string buf "return"
Buffer.contents buf | 0 -> Buffer.add_string buf "nul"
in "#\\" ^ name | 27 -> Buffer.add_string buf "escape"
| Eof -> "#!eof" | 127 -> Buffer.add_string buf "delete"
| 8 -> Buffer.add_string buf "backspace"
| _ -> Buffer.add_utf_8_uchar buf (Uchar.of_int n))
| Eof -> Buffer.add_string buf "#!eof"
| Port { sp_kind = PortInput (_, pos); sp_closed } -> | Port { sp_kind = PortInput (_, pos); sp_closed } ->
Printf.sprintf "<input-port:pos=%d%s>" !pos (if sp_closed then ":closed" else "") Buffer.add_string buf (Printf.sprintf "<input-port:pos=%d%s>" !pos (if sp_closed then ":closed" else ""))
| Port { sp_kind = PortOutput buf; sp_closed } -> | Port { sp_kind = PortOutput b; sp_closed } ->
Printf.sprintf "<output-port:len=%d%s>" (Buffer.length buf) (if sp_closed then ":closed" else "") Buffer.add_string buf (Printf.sprintf "<output-port:len=%d%s>" (Buffer.length b) (if sp_closed then ":closed" else ""))
| Rational (n, d) -> Printf.sprintf "%d/%d" n d | Rational (n, d) ->
| SxSet ht -> Printf.sprintf "<set:%d>" (Hashtbl.length ht) Buffer.add_string buf (string_of_int n); Buffer.add_char buf '/';
| SxRegexp (src, flags, _) -> Printf.sprintf "#/%s/%s" src flags Buffer.add_string buf (string_of_int d)
| SxBytevector b -> Printf.sprintf "#u8(%s)" (String.concat " " (List.init (Bytes.length b) (fun i -> string_of_int (Char.code (Bytes.get b i))))) | SxSet ht ->
Buffer.add_string buf (Printf.sprintf "<set:%d>" (Hashtbl.length ht))
| SxRegexp (src, flags, _) ->
Buffer.add_string buf "#/"; Buffer.add_string buf src;
Buffer.add_char buf '/'; Buffer.add_string buf flags
| SxBytevector b ->
Buffer.add_string buf "#u8(";
let n = Bytes.length b in
for i = 0 to n - 1 do
if i > 0 then Buffer.add_char buf ' ';
Buffer.add_string buf (string_of_int (Char.code (Bytes.get b i)))
done;
Buffer.add_char buf ')'
| AdtValue a -> | AdtValue a ->
if Array.length a.av_fields = 0 then Buffer.add_char buf '('; Buffer.add_string buf a.av_ctor;
Printf.sprintf "(%s)" a.av_ctor Array.iter (fun v -> Buffer.add_char buf ' '; inspect_into buf v) a.av_fields;
else Buffer.add_char buf ')'
let parts = Array.to_list (Array.map inspect a.av_fields) in
Printf.sprintf "(%s %s)" a.av_ctor (String.concat " " parts) let inspect v =
let buf = Buffer.create 64 in
inspect_into buf v;
Buffer.contents buf

View File

@@ -214,6 +214,29 @@ Add `make-buffer`, `buffer-append!`, `buffer->string` primitives. Eliminates the
`(str a b c d ...)` quadratic allocation pattern in serializers and renderers. `(str a b c d ...)` quadratic allocation pattern in serializers and renderers.
Wire into `sx_primitives.ml` and the JS platform. Wire into `sx_primitives.ml` and the JS platform.
**Outcome:** Short aliases `make-buffer`/`buffer?`/`buffer-append!`/`buffer->string`/
`buffer-length` added on both hosts, sharing the existing `StringBuffer` value type.
`buffer-append!` accepts any value (auto-coerces non-strings via inspect), unlike
`string-buffer-append!` which is strict. The hot path converted was the OCaml
host-internal `inspect` function in `sx_types.ml`: rewrote from `(... ^ String.concat
" " (List.map inspect items) ^ ...)` (which allocates O(n) intermediate strings per
recursion level) to a single shared `Buffer.t` accumulator (`inspect_into buf v`
walks the value tree appending into one buffer). `inspect` is called by
`sx-serialize` on both spec and host paths, plus error-path formatting.
Median improvements (`bin/bench_inspect.exe`, best of 3 runs of 9-run min):
| Benchmark | Baseline (best min) | Buffer (best min) | Change |
|-------------------|--------------------:|------------------:|-------:|
| tree-d8 (75KB) | 5.31ms | 1.30ms | -76% |
| tree-d10 (679KB) | 81.89ms | 16.02ms | -80% |
| dict-1000 | 0.80ms | 0.31ms | -61% |
| list-2000 | 0.74ms | 0.33ms | -55% |
5 new tests in `spec/tests/test-string-buffer.sx` covering the new aliases (incl
non-string coercion and interop with the existing `string-buffer-*` API).
OCaml: 4545 → 4550. JS: 2591 → 2596. Zero regressions.
### Step 14: Inline common primitives in JIT ### Step 14: Inline common primitives in JIT
`hosts/ocaml/lib/sx_vm.ml`: add `OP_ADD`, `OP_SUB`, `OP_EQ`, `OP_APPEND` specialised `hosts/ocaml/lib/sx_vm.ml`: add `OP_ADD`, `OP_SUB`, `OP_EQ`, `OP_APPEND` specialised
@@ -238,7 +261,7 @@ these when operands are known numbers/lists.
| 10 — compiler + as converter registry | [x] | d22361e4 | | 10 — compiler + as converter registry | [x] | d22361e4 |
| 11 — plugin migration + worker | [x] | 6328b810 | | 11 — plugin migration + worker | [x] | 6328b810 |
| 12 — frame records | [x] | a66c0f66 (fib -66%, loop -69%, reduce -86% via prim_call fast path) | | 12 — frame records | [x] | a66c0f66 (fib -66%, loop -69%, reduce -86% via prim_call fast path) |
| 13 — buffer primitive | [ ] | | | 13 — buffer primitive | [x] | (pending) (inspect rewrite: tree-d10 -80%, tree-d8 -76%, dict-1000 -61%, list-2000 -55%) |
| 14 — inline primitives JIT | [ ] | — | | 14 — inline primitives JIT | [ ] | — |
--- ---

View File

@@ -41,7 +41,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-05-07T00:02:13Z"; var SX_VERSION = "2026-05-07T02:05:49Z";
function isNil(x) { return x === NIL || x === null || x === undefined; } function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -911,6 +911,22 @@
PRIMITIVES["string-buffer->string"] = function(buf) { return buf.parts.join(""); }; PRIMITIVES["string-buffer->string"] = function(buf) { return buf.parts.join(""); };
PRIMITIVES["string-buffer-length"] = function(buf) { return buf.len; }; PRIMITIVES["string-buffer-length"] = function(buf) { return buf.len; };
// Short aliases — terser names; append accepts any value
PRIMITIVES["make-buffer"] = function() { return new SxStringBuffer(); };
PRIMITIVES["buffer?"] = function(x) { return x instanceof SxStringBuffer; };
PRIMITIVES["buffer-append!"] = function(buf, v) {
var s;
if (v === null || v === undefined || v === NIL) s = "";
else if (typeof v === "string") s = v;
else if (typeof v === "boolean") s = v ? "true" : "false";
else if (typeof v === "number") s = String(v);
else if (v && typeof v === "object" && typeof v.name === "string" && v.constructor && v.constructor.name === "Symbol") s = v.name;
else s = (typeof inspect === "function") ? inspect(v) : String(v);
buf.parts.push(s); buf.len += s.length; return NIL;
};
PRIMITIVES["buffer->string"] = function(buf) { return buf.parts.join(""); };
PRIMITIVES["buffer-length"] = function(buf) { return buf.len; };
// stdlib.format // stdlib.format
PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); }; PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); };

View File

@@ -810,6 +810,24 @@
:returns "string-buffer" :returns "string-buffer"
:doc "Create a new empty mutable string buffer for O(1) amortised append.") :doc "Create a new empty mutable string buffer for O(1) amortised append.")
(define-primitive
"make-buffer"
:params ()
:returns "string-buffer"
:doc "Create a new mutable buffer (alias for make-string-buffer with terser name).")
(define-primitive
"buffer-append!"
:params (buf v)
:returns "nil"
:doc "Append a value to a buffer; coerces non-strings to their printed form.")
(define-primitive
"buffer->string"
:params (buf)
:returns "string"
:doc "Finalize a buffer to a single string.")
(define-module :stdlib.coroutines) (define-module :stdlib.coroutines)
(define-module :stdlib.bitwise) (define-module :stdlib.bitwise)

View File

@@ -128,4 +128,37 @@
(string-buffer-append! buf sep) (string-buffer-append! buf sep)
(string-buffer-append! buf (first remaining)) (string-buffer-append! buf (first remaining))
(loop (rest remaining) " "))) (loop (rest remaining) " ")))
(assert= "the quick brown fox" (string-buffer->string buf))))) (assert= "the quick brown fox" (string-buffer->string buf))))
(deftest
"make-buffer alias creates a buffer"
(let ((b (make-buffer))) (assert (buffer? b))))
(deftest
"buffer-append! with string"
(let ((b (make-buffer)))
(buffer-append! b "hello")
(buffer-append! b " ")
(buffer-append! b "world")
(assert= "hello world" (buffer->string b))))
(deftest
"buffer-append! coerces non-string values"
(let ((b (make-buffer)))
(buffer-append! b "n=")
(buffer-append! b 42)
(buffer-append! b ",")
(buffer-append! b true)
(buffer-append! b ",")
(buffer-append! b nil)
(assert= "n=42,true," (buffer->string b))))
(deftest
"buffer-length tracks total length"
(let ((b (make-buffer)))
(buffer-append! b "abc")
(buffer-append! b "de")
(assert= 5 (buffer-length b))))
(deftest
"buffer aliases interop with string-buffer"
(let ((b (make-buffer)))
(buffer-append! b "x")
(string-buffer-append! b "y")
(assert= "xy" (string-buffer->string b))
(assert= "xy" (buffer->string b)))))