···11+(** Chibi-scheme OCaml bindings - High-level API *)
22+33+(** {1 Exceptions} *)
44+55+exception Chibi_error of string
66+exception Context_destroyed
77+exception Type_error of string
88+99+let () =
1010+ Printexc.register_printer (function
1111+ | Chibi_error msg -> Some ("Chibi_error: " ^ msg)
1212+ | Context_destroyed -> Some "Context_destroyed: chibi context has been destroyed"
1313+ | Type_error msg -> Some ("Type_error: " ^ msg)
1414+ | _ -> None)
1515+1616+(** {1 Sexp - Scheme values} *)
1717+1818+module Sexp = struct
1919+ type t = Chibi_ffi.sexp
2020+2121+ (** Type tag for pattern matching *)
2222+ type tag =
2323+ | Null
2424+ | Boolean of bool
2525+ | Fixnum of int
2626+ | Flonum of float
2727+ | Char of int
2828+ | String of string
2929+ | Symbol of string
3030+ | Pair
3131+ | Vector
3232+ | Bytevector
3333+ | Procedure
3434+ | Port
3535+ | Void
3636+ | Eof
3737+ | Other
3838+3939+ let classify (s : t) : tag =
4040+ if Chibi_ffi.is_null s then Null
4141+ else if Chibi_ffi.is_void s then Void
4242+ else if Chibi_ffi.is_eof s then Eof
4343+ else if Chibi_ffi.is_boolean s then Boolean (Chibi_ffi.unbox_boolean s)
4444+ else if Chibi_ffi.is_fixnum s then Fixnum (Chibi_ffi.unbox_fixnum s)
4545+ else if Chibi_ffi.is_flonum s then Flonum (Chibi_ffi.unbox_flonum s)
4646+ else if Chibi_ffi.is_char s then Char (Chibi_ffi.unbox_char s)
4747+ else if Chibi_ffi.is_string s then String (Chibi_ffi.unbox_string s)
4848+ else if Chibi_ffi.is_symbol s then Symbol (Chibi_ffi.unbox_symbol s)
4949+ else if Chibi_ffi.is_pair s then Pair
5050+ else if Chibi_ffi.is_vector s then Vector
5151+ else if Chibi_ffi.is_bytes s then Bytevector
5252+ else if Chibi_ffi.is_procedure s then Procedure
5353+ else if Chibi_ffi.is_port s then Port
5454+ else Other
5555+5656+ (* Predicates *)
5757+ let is_null = Chibi_ffi.is_null
5858+ let is_pair = Chibi_ffi.is_pair
5959+ let is_symbol = Chibi_ffi.is_symbol
6060+ let is_string = Chibi_ffi.is_string
6161+ let is_fixnum = Chibi_ffi.is_fixnum
6262+ let is_flonum = Chibi_ffi.is_flonum
6363+ let is_number = Chibi_ffi.is_number
6464+ let is_boolean = Chibi_ffi.is_boolean
6565+ let is_char = Chibi_ffi.is_char
6666+ let is_vector = Chibi_ffi.is_vector
6767+ let is_bytevector = Chibi_ffi.is_bytes
6868+ let is_procedure = Chibi_ffi.is_procedure
6969+ let is_port = Chibi_ffi.is_port
7070+ let is_void = Chibi_ffi.is_void
7171+ let is_eof = Chibi_ffi.is_eof
7272+ let is_true = Chibi_ffi.sexp_truep
7373+7474+ (* Pair accessors *)
7575+ let car = Chibi_ffi.car
7676+ let cdr = Chibi_ffi.cdr
7777+7878+ let caar s = car (car s)
7979+ let cadr s = car (cdr s)
8080+ let cdar s = cdr (car s)
8181+ let cddr s = cdr (cdr s)
8282+8383+ (* Extraction *)
8484+ let to_int s =
8585+ if Chibi_ffi.is_fixnum s then Chibi_ffi.unbox_fixnum s
8686+ else raise (Type_error "expected fixnum")
8787+8888+ let to_float s =
8989+ if Chibi_ffi.is_flonum s then Chibi_ffi.unbox_flonum s
9090+ else if Chibi_ffi.is_fixnum s then Float.of_int (Chibi_ffi.unbox_fixnum s)
9191+ else raise (Type_error "expected number")
9292+9393+ let to_string s =
9494+ if Chibi_ffi.is_string s then Chibi_ffi.unbox_string s
9595+ else raise (Type_error "expected string")
9696+9797+ let to_symbol s =
9898+ if Chibi_ffi.is_symbol s then Chibi_ffi.unbox_symbol s
9999+ else raise (Type_error "expected symbol")
100100+101101+ let to_bool s =
102102+ if Chibi_ffi.is_boolean s then Chibi_ffi.unbox_boolean s
103103+ else raise (Type_error "expected boolean")
104104+105105+ let to_char s =
106106+ if Chibi_ffi.is_char s then Chibi_ffi.unbox_char s
107107+ else raise (Type_error "expected char")
108108+109109+ let to_bytes s =
110110+ if Chibi_ffi.is_bytes s then Chibi_ffi.unbox_bytes s
111111+ else raise (Type_error "expected bytevector")
112112+113113+ (** Convert a scheme list to an OCaml list *)
114114+ let to_list (s : t) : t list =
115115+ let rec loop acc s =
116116+ if Chibi_ffi.is_null s then List.rev acc
117117+ else if Chibi_ffi.is_pair s then
118118+ loop (Chibi_ffi.car s :: acc) (Chibi_ffi.cdr s)
119119+ else raise (Type_error "expected proper list")
120120+ in
121121+ loop [] s
122122+123123+ (** Convert a scheme vector to an OCaml array *)
124124+ let to_array (s : t) : t array =
125125+ if not (Chibi_ffi.is_vector s) then
126126+ raise (Type_error "expected vector");
127127+ let len = Chibi_ffi.vector_length s in
128128+ Array.init len (fun i -> Chibi_ffi.vector_ref s i)
129129+130130+ (** Pointer equality *)
131131+ let equal = Chibi_ffi.sexp_equal
132132+133133+ (** List length *)
134134+ let list_length = Chibi_ffi.list_length
135135+end
136136+137137+(** {1 Sandbox - Capability-based security} *)
138138+139139+module Sandbox = struct
140140+ (** Capabilities that can be granted to a sandboxed VM *)
141141+ type capability =
142142+ | File_read (** Allow reading files *)
143143+ | File_write (** Allow writing files *)
144144+ | Net_access (** Allow network access *)
145145+ | Process_exec (** Allow exec/system calls *)
146146+ | Env_access (** Allow environment variable access *)
147147+ | Module_import (** Allow importing modules *)
148148+ | Standard_io (** Allow stdin/stdout/stderr *)
149149+150150+ type t = {
151151+ capabilities : capability list;
152152+ }
153153+154154+ (** No capabilities at all - maximum sandbox *)
155155+ let none = { capabilities = [] }
156156+157157+ (** All capabilities - no restrictions *)
158158+ let full = {
159159+ capabilities = [
160160+ File_read; File_write; Net_access; Process_exec;
161161+ Env_access; Module_import; Standard_io;
162162+ ]
163163+ }
164164+165165+ let allow caps = { capabilities = caps }
166166+167167+ let has_capability sandbox cap =
168168+ List.mem cap sandbox.capabilities
169169+end
170170+171171+(** {1 Context - Scheme VM instance} *)
172172+173173+module Context = struct
174174+ type t = {
175175+ raw : Chibi_ffi.context;
176176+ sandbox : Sandbox.t;
177177+ mutable destroyed : bool;
178178+ }
179179+180180+ type config = {
181181+ heap_size : int;
182182+ max_heap_size : int;
183183+ sandbox : Sandbox.t;
184184+ module_paths : string list;
185185+ }
186186+187187+ let default_config = {
188188+ heap_size = 0; (* 0 = use chibi default (2MB) *)
189189+ max_heap_size = 0; (* 0 = unlimited *)
190190+ sandbox = Sandbox.full; (* full access by default *)
191191+ module_paths = [];
192192+ }
193193+194194+ let sandboxed_config ?(heap_size = 2 * 1024 * 1024)
195195+ ?(max_heap_size = 16 * 1024 * 1024)
196196+ ?(capabilities = []) () =
197197+ {
198198+ heap_size;
199199+ max_heap_size;
200200+ sandbox = Sandbox.allow capabilities;
201201+ module_paths = [];
202202+ }
203203+204204+ let check_alive t =
205205+ if t.destroyed then raise Context_destroyed
206206+207207+ (** Override a list of bindings with void in the current environment.
208208+ Silently ignores bindings that don't exist. *)
209209+ let nullify_bindings ctx names =
210210+ let void = Chibi_ffi.sexp_void ctx in
211211+ List.iter (fun name ->
212212+ try Chibi_ffi.env_define ctx name void with _ -> ()
213213+ ) names
214214+215215+ (** Install sandbox restrictions by overriding dangerous bindings with
216216+ void and redirecting I/O ports.
217217+218218+ The sandbox enforces restrictions on the interaction environment after
219219+ the standard library has been loaded. Each capability that is NOT
220220+ granted causes the corresponding bindings to be replaced with void.
221221+222222+ This covers bindings from (scheme base), (scheme file),
223223+ (chibi filesystem), (chibi process), (chibi net), (chibi system),
224224+ (chibi shell), (scheme process-context), (scheme eval), (scheme load),
225225+ and related modules. *)
226226+ let apply_sandbox (t : t) =
227227+ let sandbox = t.sandbox in
228228+ let ctx = t.raw in
229229+230230+ (* Standard I/O: redirect ports to null string ports *)
231231+ if not (Sandbox.has_capability sandbox Sandbox.Standard_io) then begin
232232+ let null_in = Chibi_ffi.open_input_string ctx "" in
233233+ let null_out = Chibi_ffi.open_output_string ctx in
234234+ let null_err = Chibi_ffi.open_output_string ctx in
235235+ Chibi_ffi.set_current_input_port ctx null_in;
236236+ Chibi_ffi.set_current_output_port ctx null_out;
237237+ Chibi_ffi.set_current_error_port ctx null_err
238238+ end;
239239+240240+ (* File read: all filesystem reading operations *)
241241+ if not (Sandbox.has_capability sandbox Sandbox.File_read) then
242242+ nullify_bindings ctx [
243243+ "open-input-file"; "open-binary-input-file";
244244+ "call-with-input-file"; "with-input-from-file";
245245+ "file-exists?"; "file->string"; "file->bytevector";
246246+ "open-input-file-descriptor";
247247+ "directory-files"; "directory-fold"; "directory-fold-tree";
248248+ "read-link"; "file-status"; "file-link-status";
249249+ "file-device"; "file-inode"; "file-mode"; "file-num-links";
250250+ "file-owner"; "file-group"; "file-represented-device";
251251+ "file-size"; "file-block-size"; "file-num-blocks";
252252+ "file-access-time"; "file-change-time";
253253+ "file-modification-time"; "file-modification-time/safe";
254254+ "file-regular?"; "file-directory?"; "file-character?";
255255+ "file-block?"; "file-fifo?"; "file-link?"; "file-socket?";
256256+ "file-is-readable?"; "file-is-writable?"; "file-is-executable?";
257257+ "get-file-descriptor-flags"; "get-file-descriptor-status";
258258+ "current-directory"; "is-a-tty?"; "file-position";
259259+ "include"; "include-ci"; "load";
260260+ ];
261261+262262+ (* File write: all filesystem writing/modification operations *)
263263+ if not (Sandbox.has_capability sandbox Sandbox.File_write) then
264264+ nullify_bindings ctx [
265265+ "open-output-file"; "open-binary-output-file";
266266+ "call-with-output-file"; "with-output-to-file";
267267+ "delete-file"; "rename-file"; "link-file"; "symbolic-link-file";
268268+ "create-directory"; "create-directory*";
269269+ "delete-directory"; "delete-file-hierarchy";
270270+ "open-output-file-descriptor"; "open-output-file/append";
271271+ "make-fifo"; "file-truncate"; "file-lock";
272272+ "chmod"; "chown";
273273+ "set-file-descriptor-flags!"; "set-file-descriptor-status!";
274274+ "set-file-position!"; "change-directory"; "with-directory";
275275+ "send-file"; "call-with-temp-file"; "call-with-temp-dir";
276276+ (* low-level fd operations that enable write *)
277277+ "duplicate-file-descriptor"; "duplicate-file-descriptor-to";
278278+ "renumber-file-descriptor"; "open-pipe";
279279+ ];
280280+281281+ (* Network: all socket, connection, and HTTP operations *)
282282+ if not (Sandbox.has_capability sandbox Sandbox.Net_access) then
283283+ nullify_bindings ctx [
284284+ "socket"; "connect"; "bind"; "accept"; "listen";
285285+ "open-socket-pair"; "with-net-io"; "open-net-io";
286286+ "make-listener-socket";
287287+ "send"; "receive!"; "receive";
288288+ "send/non-blocking"; "receive!/non-blocking"; "receive/non-blocking";
289289+ "get-address-info"; "make-address-info"; "make-sockaddr";
290290+ "get-socket-option"; "set-socket-option!"; "get-peer-name";
291291+ "run-net-server"; "make-listener-thunk";
292292+ "http-get"; "http-get/headers"; "http-get-to-file";
293293+ "http-head"; "http-post"; "http-put"; "http-delete";
294294+ "call-with-input-url"; "call-with-input-url/headers";
295295+ "with-input-from-url";
296296+ "run-http-server"; "http-file-servlet";
297297+ "http-cgi-bin-dir-servlet"; "http-scheme-script-dir-servlet";
298298+ ];
299299+300300+ (* Process execution: fork, exec, system, signals, shell *)
301301+ if not (Sandbox.has_capability sandbox Sandbox.Process_exec) then
302302+ nullify_bindings ctx [
303303+ "%fork"; "fork"; "execute"; "kill"; "waitpid";
304304+ "system"; "system?"; "exit"; "emergency-exit";
305305+ "sleep"; "alarm";
306306+ "process-command-line"; "process-running?";
307307+ "set-signal-action!"; "signal-mask-block!";
308308+ "signal-mask-unblock!"; "signal-mask-set!";
309309+ "call-with-process-io";
310310+ "process->bytevector"; "process->string";
311311+ "process->sexp"; "process->string-list";
312312+ "process->output+error"; "process->output+error+status";
313313+ (* shell module *)
314314+ "shell"; "shell&"; "shell-pipe"; "call-with-shell-io";
315315+ "shell->string"; "shell->string-list";
316316+ "shell->sexp"; "shell->sexp-list"; "shell->output&error";
317317+ "shell-do"; "shell-command";
318318+ (* privilege escalation *)
319319+ "set-current-user-id!"; "set-current-effective-user-id!";
320320+ "set-current-group-id!"; "set-current-effective-group-id!";
321321+ "set-root-directory!"; "create-session";
322322+ ];
323323+324324+ (* Environment access: env vars, host/user info *)
325325+ if not (Sandbox.has_capability sandbox Sandbox.Env_access) then
326326+ nullify_bindings ctx [
327327+ "get-environment-variable"; "get-environment-variables";
328328+ "command-line";
329329+ "get-host-name"; "user-information"; "group-information";
330330+ "user-name"; "user-password"; "user-id"; "user-group-id";
331331+ "user-gecos"; "user-home"; "user-shell";
332332+ "group-name"; "group-password"; "group-id";
333333+ "current-user-id"; "current-group-id";
334334+ "current-effective-user-id"; "current-effective-group-id";
335335+ "current-process-id"; "parent-process-id";
336336+ "current-session-id";
337337+ ]
338338+339339+ (** Create a new Scheme VM context. *)
340340+ let create ?(config = default_config) () =
341341+ let raw = Chibi_ffi.create_context config.heap_size config.max_heap_size in
342342+ let t = { raw; sandbox = config.sandbox; destroyed = false } in
343343+ (* Set up module path: vendored lib directory must be added BEFORE
344344+ loading the standard environment, since init-7.scm lives there. *)
345345+ (match Chibi_config.find_lib_dir () with
346346+ | Some dir -> Chibi_ffi.add_module_directory raw dir
347347+ | None -> ());
348348+ (* Add user-specified module paths *)
349349+ List.iter (fun dir -> Chibi_ffi.add_module_directory raw dir) config.module_paths;
350350+ (* Load standard environment if module import is allowed *)
351351+ if Sandbox.has_capability config.sandbox Sandbox.Module_import then begin
352352+ Chibi_ffi.load_standard_env raw;
353353+ if Sandbox.has_capability config.sandbox Sandbox.Standard_io then
354354+ Chibi_ffi.load_standard_ports raw
355355+ end;
356356+ (* Apply sandbox restrictions *)
357357+ apply_sandbox t;
358358+ (* Register finalizer *)
359359+ Gc.finalise (fun t ->
360360+ if not t.destroyed then begin
361361+ Chibi_ffi.destroy_context t.raw;
362362+ t.destroyed <- true
363363+ end
364364+ ) t;
365365+ t
366366+367367+ (** Destroy the context, freeing all chibi-scheme resources. *)
368368+ let destroy t =
369369+ if not t.destroyed then begin
370370+ Chibi_ffi.destroy_context t.raw;
371371+ t.destroyed <- true
372372+ end
373373+374374+ (** Check if the context is still alive. *)
375375+ let is_alive t = not t.destroyed && Chibi_ffi.context_is_alive t.raw
376376+377377+ (** Get heap statistics *)
378378+ let heap_size t =
379379+ check_alive t;
380380+ Chibi_ffi.heap_size t.raw
381381+382382+ let heap_max_size t =
383383+ check_alive t;
384384+ Chibi_ffi.heap_max_size t.raw
385385+386386+ (** Trigger garbage collection. Returns approximate bytes freed. *)
387387+ let gc t =
388388+ check_alive t;
389389+ Chibi_ffi.gc t.raw
390390+391391+ (** Get the raw FFI context (for advanced usage) *)
392392+ let raw t =
393393+ check_alive t;
394394+ t.raw
395395+end
396396+397397+(** {1 Value construction} *)
398398+399399+module Value = struct
400400+ let void ctx =
401401+ Context.check_alive ctx;
402402+ Chibi_ffi.sexp_void ctx.Context.raw
403403+404404+ let null ctx =
405405+ Context.check_alive ctx;
406406+ Chibi_ffi.sexp_null ctx.Context.raw
407407+408408+ let eof ctx =
409409+ Context.check_alive ctx;
410410+ Chibi_ffi.sexp_eof ctx.Context.raw
411411+412412+ let of_bool ctx b =
413413+ Context.check_alive ctx;
414414+ if b then Chibi_ffi.sexp_true ctx.Context.raw
415415+ else Chibi_ffi.sexp_false ctx.Context.raw
416416+417417+ let of_int ctx n =
418418+ Context.check_alive ctx;
419419+ Chibi_ffi.make_fixnum ctx.Context.raw n
420420+421421+ let of_float ctx f =
422422+ Context.check_alive ctx;
423423+ Chibi_ffi.make_flonum ctx.Context.raw f
424424+425425+ let of_string ctx s =
426426+ Context.check_alive ctx;
427427+ Chibi_ffi.make_string ctx.Context.raw s
428428+429429+ let of_symbol ctx s =
430430+ Context.check_alive ctx;
431431+ Chibi_ffi.intern ctx.Context.raw s
432432+433433+ let of_char ctx c =
434434+ Context.check_alive ctx;
435435+ Chibi_ffi.make_char ctx.Context.raw (Char.code c)
436436+437437+ let of_char_code ctx c =
438438+ Context.check_alive ctx;
439439+ Chibi_ffi.make_char ctx.Context.raw c
440440+441441+ let of_bytes ctx b =
442442+ Context.check_alive ctx;
443443+ Chibi_ffi.make_bytes ctx.Context.raw b
444444+445445+ let cons ctx a b =
446446+ Context.check_alive ctx;
447447+ Chibi_ffi.cons ctx.Context.raw a b
448448+449449+ (** Build a proper scheme list from an OCaml list *)
450450+ let of_list ctx items =
451451+ Context.check_alive ctx;
452452+ let raw = ctx.Context.raw in
453453+ List.fold_right (fun x acc -> Chibi_ffi.cons raw x acc)
454454+ items (Chibi_ffi.sexp_null raw)
455455+456456+ (** Build a scheme vector from an OCaml array *)
457457+ let of_array ctx arr =
458458+ Context.check_alive ctx;
459459+ let raw = ctx.Context.raw in
460460+ let void = Chibi_ffi.sexp_void raw in
461461+ let vec = Chibi_ffi.make_vector raw (Array.length arr) void in
462462+ Array.iteri (fun i v -> Chibi_ffi.vector_set vec i v) arr;
463463+ vec
464464+465465+ (** Build a scheme list of integers *)
466466+ let of_int_list ctx lst =
467467+ of_list ctx (List.map (fun n -> of_int ctx n) lst)
468468+469469+ (** Build a scheme list of strings *)
470470+ let of_string_list ctx lst =
471471+ of_list ctx (List.map (fun s -> of_string ctx s) lst)
472472+end
473473+474474+(** {1 Eval - Expression evaluation} *)
475475+476476+module Eval = struct
477477+ let string ctx code =
478478+ Context.check_alive ctx;
479479+ try Chibi_ffi.eval_string ctx.Context.raw code
480480+ with Failure msg -> raise (Chibi_error msg)
481481+482482+ let sexp ctx s =
483483+ Context.check_alive ctx;
484484+ try Chibi_ffi.eval ctx.Context.raw s
485485+ with Failure msg -> raise (Chibi_error msg)
486486+487487+ let apply ctx proc args =
488488+ Context.check_alive ctx;
489489+ try Chibi_ffi.apply ctx.Context.raw proc args
490490+ with Failure msg -> raise (Chibi_error msg)
491491+492492+ let load ctx path =
493493+ Context.check_alive ctx;
494494+ try Chibi_ffi.load_file ctx.Context.raw path
495495+ with Failure msg -> raise (Chibi_error msg)
496496+497497+ let load_direct ctx path =
498498+ Context.check_alive ctx;
499499+ try Chibi_ffi.load_file_direct ctx.Context.raw path
500500+ with Failure msg -> raise (Chibi_error msg)
501501+502502+ let read ctx code =
503503+ Context.check_alive ctx;
504504+ try Chibi_ffi.read_from_string ctx.Context.raw code
505505+ with Failure msg -> raise (Chibi_error msg)
506506+507507+ (** Evaluate and convert result to string *)
508508+ let to_string ctx code =
509509+ let result = string ctx code in
510510+ Chibi_ffi.sexp_to_string ctx.Context.raw result
511511+512512+ (** Evaluate and convert to int *)
513513+ let to_int ctx code =
514514+ Sexp.to_int (string ctx code)
515515+516516+ (** Evaluate and convert to float *)
517517+ let to_float ctx code =
518518+ Sexp.to_float (string ctx code)
519519+520520+ (** Evaluate and convert to bool *)
521521+ let to_bool ctx code =
522522+ Sexp.to_bool (string ctx code)
523523+524524+ (** Write a sexp to its string representation *)
525525+ let write ctx s =
526526+ Context.check_alive ctx;
527527+ Chibi_ffi.sexp_to_string ctx.Context.raw s
528528+529529+ (** Display a sexp (strings without quotes) *)
530530+ let display ctx s =
531531+ Context.check_alive ctx;
532532+ Chibi_ffi.sexp_display_string ctx.Context.raw s
533533+end
534534+535535+(** {1 Env - Environment bindings} *)
536536+537537+module Env = struct
538538+ (** Define a binding in the current environment *)
539539+ let define ctx name value =
540540+ Context.check_alive ctx;
541541+ try Chibi_ffi.env_define ctx.Context.raw name value
542542+ with Failure msg -> raise (Chibi_error msg)
543543+544544+ (** Look up a binding *)
545545+ let lookup ctx name =
546546+ Context.check_alive ctx;
547547+ let v = Chibi_ffi.env_ref ctx.Context.raw name in
548548+ if Chibi_ffi.is_void v then None
549549+ else Some v
550550+551551+ (** Look up a binding, raising if not found *)
552552+ let lookup_exn ctx name =
553553+ match lookup ctx name with
554554+ | Some v -> v
555555+ | None -> raise (Chibi_error ("unbound variable: " ^ name))
556556+557557+ (** Register a foreign function callable from Scheme.
558558+ The OCaml function receives a list of Sexp.t arguments. *)
559559+ let define_function ctx name arity (f : Sexp.t list -> Sexp.t) =
560560+ Context.check_alive ctx;
561561+ try Chibi_ffi.define_foreign ctx.Context.raw name arity f
562562+ with Failure msg -> raise (Chibi_error msg)
563563+564564+ (** Convenience: define a 0-arity function *)
565565+ let define_fn0 ctx name (f : unit -> Sexp.t) =
566566+ define_function ctx name 0 (fun _args -> f ())
567567+568568+ (** Convenience: define a 1-arity function *)
569569+ let define_fn1 ctx name (f : Sexp.t -> Sexp.t) =
570570+ define_function ctx name 1 (function
571571+ | [a] -> f a
572572+ | _ -> raise (Chibi_error ("wrong arity for " ^ name)))
573573+574574+ (** Convenience: define a 2-arity function *)
575575+ let define_fn2 ctx name (f : Sexp.t -> Sexp.t -> Sexp.t) =
576576+ define_function ctx name 2 (function
577577+ | [a; b] -> f a b
578578+ | _ -> raise (Chibi_error ("wrong arity for " ^ name)))
579579+580580+ (** Convenience: define a 3-arity function *)
581581+ let define_fn3 ctx name (f : Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t) =
582582+ define_function ctx name 3 (function
583583+ | [a; b; c] -> f a b c
584584+ | _ -> raise (Chibi_error ("wrong arity for " ^ name)))
585585+586586+ (** Convenience: define a 4-arity function *)
587587+ let define_fn4 ctx name (f : Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t) =
588588+ define_function ctx name 4 (function
589589+ | [a; b; c; d] -> f a b c d
590590+ | _ -> raise (Chibi_error ("wrong arity for " ^ name)))
591591+592592+ (** Convenience: define a 5-arity function *)
593593+ let define_fn5 ctx name
594594+ (f : Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t) =
595595+ define_function ctx name 5 (function
596596+ | [a; b; c; d; e] -> f a b c d e
597597+ | _ -> raise (Chibi_error ("wrong arity for " ^ name)))
598598+599599+ (** Convenience: define a 6-arity function *)
600600+ let define_fn6 ctx name
601601+ (f : Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t) =
602602+ define_function ctx name 6 (function
603603+ | [a; b; c; d; e; g] -> f a b c d e g
604604+ | _ -> raise (Chibi_error ("wrong arity for " ^ name)))
605605+end
606606+607607+(** {1 Stream - Effect-based data streaming} *)
608608+609609+module Stream = struct
610610+ (** Effect type for yielding values into the stream *)
611611+ type _ Effect.t += Yield : Sexp.t -> unit Effect.t
612612+613613+ (** A stream is a lazy sequence of sexp values *)
614614+ type t = Sexp.t Seq.t
615615+616616+ (** Create a Seq.t from a function that uses Yield to produce values *)
617617+ let from_producer (f : unit -> unit) : t =
618618+ let open Effect.Deep in
619619+ fun () ->
620620+ match_with f ()
621621+ { retc = (fun () -> Seq.Nil);
622622+ exnc = (fun e -> raise e);
623623+ effc = fun (type a) (eff : a Effect.t) ->
624624+ match eff with
625625+ | Yield v ->
626626+ Some (fun (k : (a, _) continuation) ->
627627+ Seq.Cons (v, fun () -> continue k ()))
628628+ | _ -> None }
629629+630630+ (** Feed an OCaml Seq.t into a Scheme function, one element at a time.
631631+ The Scheme function is called for each element. Returns the
632632+ final accumulator value. *)
633633+ let feed ctx ~(proc : Sexp.t) ~(init : Sexp.t) (seq : Sexp.t Seq.t) : Sexp.t =
634634+ Context.check_alive ctx;
635635+ let raw = ctx.Context.raw in
636636+ let acc = ref init in
637637+ let s = ref seq in
638638+ let continue = ref true in
639639+ while !continue do
640640+ match (!s) () with
641641+ | Seq.Nil -> continue := false
642642+ | Seq.Cons (v, rest) ->
643643+ let args = Chibi_ffi.cons raw v
644644+ (Chibi_ffi.cons raw !acc (Chibi_ffi.sexp_null raw)) in
645645+ (try
646646+ acc := Chibi_ffi.apply raw proc args
647647+ with Failure msg -> raise (Chibi_error msg));
648648+ s := rest
649649+ done;
650650+ !acc
651651+652652+ (** Process a Seq.t through a Scheme expression.
653653+ [expr] should be a string like "(lambda (x acc) (+ acc x))".
654654+ Returns the accumulated result. *)
655655+ let fold ctx ~expr ~init seq =
656656+ Context.check_alive ctx;
657657+ let proc = Eval.string ctx expr in
658658+ feed ctx ~proc ~init seq
659659+660660+ (** Map a Scheme function over a Seq.t, producing a new Seq.t.
661661+ [proc] is a Scheme procedure sexp. *)
662662+ let map ctx ~(proc : Sexp.t) (seq : Sexp.t Seq.t) : Sexp.t Seq.t =
663663+ Context.check_alive ctx;
664664+ let raw = ctx.Context.raw in
665665+ Seq.map (fun v ->
666666+ let args = Chibi_ffi.cons raw v (Chibi_ffi.sexp_null raw) in
667667+ try Chibi_ffi.apply raw proc args
668668+ with Failure msg -> raise (Chibi_error msg)
669669+ ) seq
670670+671671+ (** Filter a Seq.t using a Scheme predicate. *)
672672+ let filter ctx ~(pred : Sexp.t) (seq : Sexp.t Seq.t) : Sexp.t Seq.t =
673673+ Context.check_alive ctx;
674674+ let raw = ctx.Context.raw in
675675+ Seq.filter (fun v ->
676676+ let args = Chibi_ffi.cons raw v (Chibi_ffi.sexp_null raw) in
677677+ let result =
678678+ try Chibi_ffi.apply raw pred args
679679+ with Failure msg -> raise (Chibi_error msg)
680680+ in
681681+ Chibi_ffi.sexp_truep result
682682+ ) seq
683683+684684+ (** Create a stream from an OCaml channel, reading lines as scheme strings *)
685685+ let of_channel ctx (ic : in_channel) : Sexp.t Seq.t =
686686+ let raw = ctx.Context.raw in
687687+ let rec next () =
688688+ match In_channel.input_line ic with
689689+ | None -> Seq.Nil
690690+ | Some line ->
691691+ Seq.Cons (Chibi_ffi.make_string raw line, next)
692692+ in
693693+ next
694694+695695+ (** Create a stream of scheme integers from an int Seq *)
696696+ let of_int_seq ctx (seq : int Seq.t) : Sexp.t Seq.t =
697697+ let raw = ctx.Context.raw in
698698+ Seq.map (fun n -> Chibi_ffi.make_fixnum raw n) seq
699699+700700+ (** Create a stream of scheme strings from a string Seq *)
701701+ let of_string_seq ctx (seq : string Seq.t) : Sexp.t Seq.t =
702702+ let raw = ctx.Context.raw in
703703+ Seq.map (fun s -> Chibi_ffi.make_string raw s) seq
704704+705705+ (** Collect a Sexp.t Seq into a scheme list *)
706706+ let to_scheme_list ctx (seq : Sexp.t Seq.t) : Sexp.t =
707707+ Context.check_alive ctx;
708708+ let raw = ctx.Context.raw in
709709+ let items = List.of_seq seq in
710710+ List.fold_right (fun x acc -> Chibi_ffi.cons raw x acc)
711711+ items (Chibi_ffi.sexp_null raw)
712712+713713+ (** Collect a stream, extracting ints *)
714714+ let to_int_list (seq : Sexp.t Seq.t) : int list =
715715+ List.of_seq (Seq.map Sexp.to_int seq)
716716+717717+ (** Collect a stream, extracting strings *)
718718+ let to_string_list (seq : Sexp.t Seq.t) : string list =
719719+ List.of_seq (Seq.map Sexp.to_string seq)
720720+end
721721+722722+(** {1 IO - Port redirection and output capture} *)
723723+724724+module Io = struct
725725+ (** Redirect current output and error ports to string ports,
726726+ evaluate [f], and return the captured output.
727727+ Original ports are saved and restored afterward. *)
728728+ let capture ctx f =
729729+ Context.check_alive ctx;
730730+ let raw = ctx.Context.raw in
731731+ (* Save original ports *)
732732+ let orig_out = Chibi_ffi.get_current_output_port raw in
733733+ let orig_err = Chibi_ffi.get_current_error_port raw in
734734+ (* Redirect to string ports *)
735735+ let out_port = Chibi_ffi.open_output_string raw in
736736+ let err_port = Chibi_ffi.open_output_string raw in
737737+ Chibi_ffi.set_current_output_port raw out_port;
738738+ Chibi_ffi.set_current_error_port raw err_port;
739739+ let result =
740740+ try Ok (f ())
741741+ with e -> Error e
742742+ in
743743+ let stdout_str = Chibi_ffi.get_output_string raw out_port in
744744+ let stderr_str = Chibi_ffi.get_output_string raw err_port in
745745+ (* Restore original ports *)
746746+ Chibi_ffi.set_current_output_port raw orig_out;
747747+ Chibi_ffi.set_current_error_port raw orig_err;
748748+ (result, stdout_str, stderr_str)
749749+750750+ (** Set the current input port to read from a string. *)
751751+ let set_input_string ctx s =
752752+ Context.check_alive ctx;
753753+ let raw = ctx.Context.raw in
754754+ let port = Chibi_ffi.open_input_string raw s in
755755+ Chibi_ffi.set_current_input_port raw port
756756+757757+ (** Set the current output port to a string port and return
758758+ a function to retrieve the accumulated string. *)
759759+ let redirect_output ctx =
760760+ Context.check_alive ctx;
761761+ let raw = ctx.Context.raw in
762762+ let port = Chibi_ffi.open_output_string raw in
763763+ Chibi_ffi.set_current_output_port raw port;
764764+ fun () -> Chibi_ffi.get_output_string raw port
765765+end
766766+767767+(** {1 Convenience: with_context} *)
768768+769769+(** Create a context, run [f], and destroy it when done.
770770+ Ensures cleanup even if [f] raises. *)
771771+let with_context ?(config = Context.default_config) f =
772772+ let ctx = Context.create ~config () in
773773+ Fun.protect
774774+ ~finally:(fun () -> Context.destroy ctx)
775775+ (fun () -> f ctx)
+369
lib/chibi.mli
···11+(** {1 chibi-ocaml: OCaml bindings for chibi-scheme}
22+33+ Embed one or more sandboxed R7RS Scheme virtual machines in your OCaml
44+ programs. Each VM has an independent heap with configurable memory limits
55+ and capability-based security.
66+77+ {2 Quick example}
88+99+ {[
1010+ open Chibi_ocaml.Chibi
1111+1212+ let () =
1313+ with_context (fun ctx ->
1414+ (* Evaluate Scheme code *)
1515+ let result = Eval.to_int ctx "(+ 2 3)" in
1616+ Printf.printf "2 + 3 = %d\n" result;
1717+1818+ (* Register OCaml functions *)
1919+ Env.define_fn1 ctx "double" (fun x ->
2020+ Value.of_int ctx (Sexp.to_int x * 2));
2121+ let r = Eval.to_int ctx "(double 21)" in
2222+ Printf.printf "double 21 = %d\n" r)
2323+ ]}
2424+2525+ {2 Thread safety}
2626+2727+ Each {!Context.t} is an independent VM with its own heap. Different
2828+ contexts may be used from different OCaml domains without synchronization.
2929+ A single context must {b not} be accessed concurrently from multiple
3030+ threads or domains.
3131+3232+ {2 Memory management}
3333+3434+ Scheme values ({!Sexp.t}) are automatically prevented from collection by
3535+ chibi-scheme's GC while OCaml holds a reference. When the OCaml wrapper
3636+ is garbage-collected, the protection is released. You do not need to
3737+ manually manage Scheme value lifetimes.
3838+3939+ Contexts should be explicitly destroyed with {!Context.destroy} or
4040+ managed with {!with_context}. If you forget, the OCaml GC finalizer
4141+ will clean up, but explicit cleanup is recommended.
4242+4343+ {2 Modules} *)
4444+4545+(** {2 Exceptions} *)
4646+4747+exception Chibi_error of string
4848+(** Raised when chibi-scheme evaluation or API operations fail. *)
4949+5050+exception Context_destroyed
5151+(** Raised when attempting to use a context that has been destroyed. *)
5252+5353+exception Type_error of string
5454+(** Raised when extracting a value of the wrong type from a sexp. *)
5555+5656+(** {2 Sexp - Scheme values} *)
5757+5858+module Sexp : sig
5959+ (** An opaque Scheme value (s-expression). *)
6060+ type t
6161+6262+ (** Type tag for classifying values. *)
6363+ type tag =
6464+ | Null
6565+ | Boolean of bool
6666+ | Fixnum of int
6767+ | Flonum of float
6868+ | Char of int
6969+ | String of string
7070+ | Symbol of string
7171+ | Pair
7272+ | Vector
7373+ | Bytevector
7474+ | Procedure
7575+ | Port
7676+ | Void
7777+ | Eof
7878+ | Other
7979+8080+ val classify : t -> tag
8181+ (** Classify a sexp into its type tag. Useful for pattern matching. *)
8282+8383+ (** {3 Type predicates} *)
8484+8585+ val is_null : t -> bool
8686+ val is_pair : t -> bool
8787+ val is_symbol : t -> bool
8888+ val is_string : t -> bool
8989+ val is_fixnum : t -> bool
9090+ val is_flonum : t -> bool
9191+ val is_number : t -> bool
9292+ val is_boolean : t -> bool
9393+ val is_char : t -> bool
9494+ val is_vector : t -> bool
9595+ val is_bytevector : t -> bool
9696+ val is_procedure : t -> bool
9797+ val is_port : t -> bool
9898+ val is_void : t -> bool
9999+ val is_eof : t -> bool
100100+ val is_true : t -> bool
101101+102102+ (** {3 Pair accessors} *)
103103+104104+ val car : t -> t
105105+ val cdr : t -> t
106106+ val caar : t -> t
107107+ val cadr : t -> t
108108+ val cdar : t -> t
109109+ val cddr : t -> t
110110+111111+ (** {3 Value extraction}
112112+ These raise {!Type_error} if the value has the wrong type. *)
113113+114114+ val to_int : t -> int
115115+ val to_float : t -> float
116116+ val to_string : t -> string
117117+ val to_symbol : t -> string
118118+ val to_bool : t -> bool
119119+ val to_char : t -> int
120120+ val to_bytes : t -> bytes
121121+122122+ (** {3 Collection conversions} *)
123123+124124+ val to_list : t -> t list
125125+ (** Convert a Scheme list to an OCaml list. *)
126126+127127+ val to_array : t -> t array
128128+ (** Convert a Scheme vector to an OCaml array. *)
129129+130130+ val list_length : t -> int
131131+ (** Length of a Scheme list. *)
132132+133133+ (** {3 Equality} *)
134134+135135+ val equal : t -> t -> bool
136136+ (** Pointer equality (eq?) between two sexp values. *)
137137+end
138138+139139+(** {2 Sandbox - Capability-based security} *)
140140+141141+module Sandbox : sig
142142+ (** A capability that can be granted to a VM. *)
143143+ type capability =
144144+ | File_read (** Allow reading files *)
145145+ | File_write (** Allow writing files *)
146146+ | Net_access (** Allow network access *)
147147+ | Process_exec (** Allow exec/system calls *)
148148+ | Env_access (** Allow environment variable access *)
149149+ | Module_import (** Allow importing modules *)
150150+ | Standard_io (** Allow stdin/stdout/stderr *)
151151+152152+ (** Sandbox configuration. *)
153153+ type t
154154+155155+ val none : t
156156+ (** Maximum sandbox: no capabilities granted. *)
157157+158158+ val full : t
159159+ (** No restrictions: all capabilities granted. *)
160160+161161+ val allow : capability list -> t
162162+ (** Create a sandbox granting only the specified capabilities. *)
163163+164164+ val has_capability : t -> capability -> bool
165165+ (** Check if a sandbox has a specific capability. *)
166166+end
167167+168168+(** {2 Context - Scheme VM instance} *)
169169+170170+module Context : sig
171171+ (** A Scheme VM instance with its own independent heap. *)
172172+ type t
173173+174174+ (** Configuration for creating a context. *)
175175+ type config = {
176176+ heap_size : int; (** Initial heap size in bytes (0 = default ~2MB) *)
177177+ max_heap_size : int; (** Maximum heap size in bytes (0 = unlimited) *)
178178+ sandbox : Sandbox.t; (** Sandbox configuration *)
179179+ module_paths : string list; (** Additional module search paths *)
180180+ }
181181+182182+ val default_config : config
183183+ (** Default configuration: full access, default heap, no limits. *)
184184+185185+ val sandboxed_config :
186186+ ?heap_size:int -> ?max_heap_size:int ->
187187+ ?capabilities:Sandbox.capability list ->
188188+ unit -> config
189189+ (** Create a sandboxed configuration. Default: 2MB initial / 16MB max,
190190+ no capabilities. *)
191191+192192+ val create : ?config:config -> unit -> t
193193+ (** Create a new Scheme VM context. *)
194194+195195+ val destroy : t -> unit
196196+ (** Destroy the context, freeing all resources. Safe to call multiple times. *)
197197+198198+ val is_alive : t -> bool
199199+ (** Check if the context has not been destroyed. *)
200200+201201+ val heap_size : t -> int
202202+ (** Current total heap size in bytes. *)
203203+204204+ val heap_max_size : t -> int
205205+ (** Maximum allowed heap size (0 = unlimited). *)
206206+207207+ val gc : t -> int
208208+ (** Trigger garbage collection. Returns approximate bytes freed. *)
209209+210210+ val raw : t -> Chibi_ffi.context
211211+ (** Access the raw FFI context for advanced usage. *)
212212+end
213213+214214+(** {2 Value - Constructing Scheme values} *)
215215+216216+module Value : sig
217217+ val void : Context.t -> Sexp.t
218218+ val null : Context.t -> Sexp.t
219219+ val eof : Context.t -> Sexp.t
220220+ val of_bool : Context.t -> bool -> Sexp.t
221221+ val of_int : Context.t -> int -> Sexp.t
222222+ val of_float : Context.t -> float -> Sexp.t
223223+ val of_string : Context.t -> string -> Sexp.t
224224+ val of_symbol : Context.t -> string -> Sexp.t
225225+ val of_char : Context.t -> char -> Sexp.t
226226+ val of_char_code : Context.t -> int -> Sexp.t
227227+ val of_bytes : Context.t -> bytes -> Sexp.t
228228+ val cons : Context.t -> Sexp.t -> Sexp.t -> Sexp.t
229229+ val of_list : Context.t -> Sexp.t list -> Sexp.t
230230+ val of_array : Context.t -> Sexp.t array -> Sexp.t
231231+ val of_int_list : Context.t -> int list -> Sexp.t
232232+ val of_string_list : Context.t -> string list -> Sexp.t
233233+end
234234+235235+(** {2 Eval - Expression evaluation} *)
236236+237237+module Eval : sig
238238+ val string : Context.t -> string -> Sexp.t
239239+ (** Evaluate a Scheme expression from a string. *)
240240+241241+ val sexp : Context.t -> Sexp.t -> Sexp.t
242242+ (** Evaluate a parsed Scheme s-expression. *)
243243+244244+ val apply : Context.t -> Sexp.t -> Sexp.t -> Sexp.t
245245+ (** Apply a Scheme procedure to a list of arguments. *)
246246+247247+ val load : Context.t -> string -> Sexp.t
248248+ (** Load and evaluate a Scheme file (searches module path). *)
249249+250250+ val load_direct : Context.t -> string -> Sexp.t
251251+ (** Load and evaluate a Scheme file by absolute path (no module path search).
252252+ This properly handles files with top-level import forms. *)
253253+254254+ val read : Context.t -> string -> Sexp.t
255255+ (** Parse a string into an s-expression (without evaluating). *)
256256+257257+ val to_string : Context.t -> string -> string
258258+ (** Evaluate and convert the result to its write representation. *)
259259+260260+ val to_int : Context.t -> string -> int
261261+ (** Evaluate and extract an integer. *)
262262+263263+ val to_float : Context.t -> string -> float
264264+ (** Evaluate and extract a float. *)
265265+266266+ val to_bool : Context.t -> string -> bool
267267+ (** Evaluate and extract a boolean. *)
268268+269269+ val write : Context.t -> Sexp.t -> string
270270+ (** Convert a sexp to its write (machine-readable) string. *)
271271+272272+ val display : Context.t -> Sexp.t -> string
273273+ (** Convert a sexp to its display (human-readable) string. *)
274274+end
275275+276276+(** {2 Env - Environment bindings} *)
277277+278278+module Env : sig
279279+ val define : Context.t -> string -> Sexp.t -> unit
280280+ (** Define a binding in the current environment. *)
281281+282282+ val lookup : Context.t -> string -> Sexp.t option
283283+ (** Look up a binding by name. Returns [None] if unbound. *)
284284+285285+ val lookup_exn : Context.t -> string -> Sexp.t
286286+ (** Look up a binding, raising {!Chibi_error} if not found. *)
287287+288288+ val define_function : Context.t -> string -> int -> (Sexp.t list -> Sexp.t) -> unit
289289+ (** Register a foreign function callable from Scheme.
290290+ [define_function ctx name arity f] registers [f] as a Scheme procedure
291291+ with [arity] parameters (0-6). *)
292292+293293+ val define_fn0 : Context.t -> string -> (unit -> Sexp.t) -> unit
294294+ val define_fn1 : Context.t -> string -> (Sexp.t -> Sexp.t) -> unit
295295+ val define_fn2 : Context.t -> string -> (Sexp.t -> Sexp.t -> Sexp.t) -> unit
296296+ val define_fn3 : Context.t -> string -> (Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t) -> unit
297297+ val define_fn4 : Context.t -> string -> (Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t) -> unit
298298+ val define_fn5 : Context.t -> string -> (Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t) -> unit
299299+ val define_fn6 : Context.t -> string -> (Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t -> Sexp.t) -> unit
300300+end
301301+302302+(** {2 Stream - Effect-based streaming} *)
303303+304304+module Stream : sig
305305+ (** Effect for yielding values from a producer function. *)
306306+ type _ Effect.t += Yield : Sexp.t -> unit Effect.t
307307+308308+ type t = Sexp.t Seq.t
309309+310310+ val from_producer : (unit -> unit) -> t
311311+ (** Create a stream from a function that uses [perform (Yield v)] to
312312+ produce values. *)
313313+314314+ val feed : Context.t -> proc:Sexp.t -> init:Sexp.t -> Sexp.t Seq.t -> Sexp.t
315315+ (** Feed a stream into a Scheme fold function.
316316+ [feed ctx ~proc ~init seq] calls [proc(element, accumulator)] for each
317317+ element, returning the final accumulator. *)
318318+319319+ val fold : Context.t -> expr:string -> init:Sexp.t -> Sexp.t Seq.t -> Sexp.t
320320+ (** Like {!feed} but takes the procedure as a Scheme expression string.
321321+ Example: [fold ctx ~expr:"(lambda (x acc) (+ acc x))" ~init:(Value.of_int ctx 0) seq] *)
322322+323323+ val map : Context.t -> proc:Sexp.t -> Sexp.t Seq.t -> Sexp.t Seq.t
324324+ (** Map a Scheme procedure over a stream. *)
325325+326326+ val filter : Context.t -> pred:Sexp.t -> Sexp.t Seq.t -> Sexp.t Seq.t
327327+ (** Filter a stream using a Scheme predicate. *)
328328+329329+ val of_channel : Context.t -> in_channel -> t
330330+ (** Read lines from a channel as Scheme strings. *)
331331+332332+ val of_int_seq : Context.t -> int Seq.t -> t
333333+ (** Convert an int sequence to a Scheme fixnum stream. *)
334334+335335+ val of_string_seq : Context.t -> string Seq.t -> t
336336+ (** Convert a string sequence to a Scheme string stream. *)
337337+338338+ val to_scheme_list : Context.t -> Sexp.t Seq.t -> Sexp.t
339339+ (** Collect a stream into a Scheme list. *)
340340+341341+ val to_int_list : Sexp.t Seq.t -> int list
342342+ (** Collect a stream, extracting integers. *)
343343+344344+ val to_string_list : Sexp.t Seq.t -> string list
345345+ (** Collect a stream, extracting strings. *)
346346+end
347347+348348+(** {2 IO - Port redirection and output capture} *)
349349+350350+module Io : sig
351351+ val capture : Context.t -> (unit -> 'a) -> ('a, exn) result * string * string
352352+ (** [capture ctx f] redirects stdout/stderr to string ports, runs [f ()],
353353+ and returns [(result, stdout_string, stderr_string)]. The original
354354+ ports are saved before and restored after [f] runs, so the context
355355+ remains usable for normal I/O afterward. *)
356356+357357+ val set_input_string : Context.t -> string -> unit
358358+ (** Set the current input port to read from a string. *)
359359+360360+ val redirect_output : Context.t -> (unit -> string)
361361+ (** Redirect stdout to a string port. Returns a thunk that retrieves
362362+ the accumulated output. *)
363363+end
364364+365365+(** {2 Convenience} *)
366366+367367+val with_context : ?config:Context.config -> (Context.t -> 'a) -> 'a
368368+(** [with_context f] creates a context, runs [f ctx], and ensures the
369369+ context is destroyed afterward (even if [f] raises). *)
+29
lib/chibi_config.ml
···11+(* chibi_config.ml -- Runtime configuration for finding chibi-scheme libraries *)
22+33+(** Find the vendored chibi-scheme lib directory.
44+ Search order:
55+ 1. CHIBI_MODULE_PATH environment variable
66+ 2. Relative to the source tree (for development)
77+ 3. opam share directory (for installed packages) *)
88+let find_lib_dir () =
99+ (* Check env var first *)
1010+ match Sys.getenv_opt "CHIBI_MODULE_PATH" with
1111+ | Some path when Sys.file_exists path -> Some path
1212+ | _ ->
1313+ (* Try paths relative to Sys.executable_name *)
1414+ let exe_dir = Filename.dirname Sys.executable_name in
1515+ let candidates = [
1616+ (* Development: running from _build or project root *)
1717+ Filename.concat exe_dir "../../../vendor/chibi-scheme/lib";
1818+ Filename.concat exe_dir "../../vendor/chibi-scheme/lib";
1919+ Filename.concat exe_dir "../vendor/chibi-scheme/lib";
2020+ Filename.concat exe_dir "vendor/chibi-scheme/lib";
2121+ (* opam installed: share/chibi-ocaml/lib *)
2222+ Filename.concat exe_dir "../share/chibi-ocaml/lib";
2323+ Filename.concat exe_dir "../lib/chibi-ocaml/chibi-scheme/lib";
2424+ (* Relative to CWD for tests *)
2525+ "vendor/chibi-scheme/lib";
2626+ ] in
2727+ List.find_opt (fun p ->
2828+ Sys.file_exists (Filename.concat p "init-7.scm")
2929+ ) candidates