From 876dd610f7fcf40cee97b890e6304ac007c747a9 Mon Sep 17 00:00:00 2001 From: Avery Townsend Date: Wed, 26 Aug 2026 12:50:41 -0700 Subject: [PATCH 1/4] feat: add application-oriented IO interfaces --- .../baml_std/ai/ns_mcp/mcp.baml | 24 +- .../baml_std/baml/ns_csv/csv.baml | 40 +- .../baml_std/baml/ns_fs/fs.baml | 55 +- .../baml_std/baml/ns_future/future.baml | 31 + .../baml_std/baml/ns_io/read.baml | 32 + .../baml_std/baml/ns_io/write.baml | 37 + .../baml_std/baml/ns_net/net.baml | 48 +- .../baml_std/baml/ns_sys/sys.baml | 158 ++- .../baml_std/baml/uint8array.baml | 6 + .../baml_std/claude_code/ns_internal/cli.baml | 17 +- .../crates/baml_builtins2/src/lib.rs | 2 + ...tests__render_builtin_package_listing.snap | 132 +-- .../crates/baml_tests/baml_src/ns_fs/fs.baml | 40 +- .../baml_src/ns_fs/fs_from_rust.baml | 24 +- .../crates/baml_tests/baml_src/ns_io/io.baml | 118 +++ .../baml_tests/baml_src/ns_net/net.baml | 8 +- .../ns_with_timeout/with_timeout.baml | 35 + .../diagnostic_errors/builtin_io/fs_ops.baml | 6 +- .../diagnostic_errors/builtin_io/net_ops.baml | 20 +- .../snapshots/baml_src/bytecode.snap | 6 + .../snapshots/baml_src/ns_csv/bytecode.snap | 4 +- .../snapshots/baml_src/ns_fs/bytecode.snap | 246 ++++- .../snapshots/baml_src/ns_io/bytecode.snap | 202 ++++ .../snapshots/baml_src/ns_net/bytecode.snap | 20 +- .../ns_optional_chain_type_args/bytecode.snap | 8 +- .../baml_src/ns_with_timeout/bytecode.snap | 102 ++ .../baml_src/stdlib/ai/bytecode.snap | 125 ++- .../snapshots/baml_src/stdlib/ai/mir.snap | 350 ++++--- .../snapshots/baml_src/stdlib/ai/ppir.snap | 13 +- .../baml_src/stdlib/baml/bytecode.snap | 623 +++++++++--- .../snapshots/baml_src/stdlib/baml/mir.snap | 930 ++++++++++++++---- .../snapshots/baml_src/stdlib/baml/ppir.snap | 120 ++- ...iagnostic_errors__builtin_io__03_ppir.snap | 24 +- ...ors__builtin_io__10_formatter__fs_ops.snap | 6 +- ...rs__builtin_io__10_formatter__net_ops.snap | 20 +- .../baml_tests/src/compiler2_tir/phase6.rs | 2 +- ...__phase5__snapshot_baml_package_items.snap | 18 +- baml_language/crates/baml_tests/tests/net.rs | 141 ++- .../crates/baml_tests/tests/shell.rs | 138 ++- .../bex_vm/src/package_baml/uint8array.rs | 9 + .../crates/bridge_wasm/src/wasm_io_fs.rs | 48 +- .../crates/bridge_wasm/src/wasm_sys.rs | 63 +- .../crates/sys_auth/src/io_bridge.rs | 9 +- baml_language/crates/sys_auth/src/testing.rs | 31 +- .../crates/sys_native/src/io_impls.rs | 603 ++++++------ baml_language/crates/sys_ops/src/lib.rs | 213 ++-- .../crates/sys_wasm/src/web_sysops.rs | 39 +- .../crates/csharp/stdlib_resources/Program.cs | 20 +- .../roundtrip_tests/TestHandles.java | 11 +- .../roundtrip_tests/test_handles.py | 11 +- .../roundtrip_tests/test_handles.rs | 11 +- .../roundtrip_tests/TestHandles.swift | 6 +- .../customizable/roundtrip_handles.test.ts | 7 +- .../sdk_tests/harness_setup/src/csharp.rs | 53 +- 54 files changed, 3502 insertions(+), 1563 deletions(-) create mode 100644 baml_language/crates/baml_builtins2/baml_std/baml/ns_io/read.baml create mode 100644 baml_language/crates/baml_builtins2/baml_std/baml/ns_io/write.baml create mode 100644 baml_language/crates/baml_tests/baml_src/ns_io/io.baml create mode 100644 baml_language/crates/baml_tests/baml_src/ns_with_timeout/with_timeout.baml create mode 100644 baml_language/crates/baml_tests/snapshots/baml_src/ns_io/bytecode.snap create mode 100644 baml_language/crates/baml_tests/snapshots/baml_src/ns_with_timeout/bytecode.snap diff --git a/baml_language/crates/baml_builtins2/baml_std/ai/ns_mcp/mcp.baml b/baml_language/crates/baml_builtins2/baml_std/ai/ns_mcp/mcp.baml index fc25519dedf..9069f2b35d3 100644 --- a/baml_language/crates/baml_builtins2/baml_std/ai/ns_mcp/mcp.baml +++ b/baml_language/crates/baml_builtins2/baml_std/ai/ns_mcp/mcp.baml @@ -24,6 +24,8 @@ function rpc_line(id: int?, method: string, params: map?) -> st class McpConnection { server_name: string, p: baml.sys.Process, + out: baml.sys.ReadPipeLines, + timeout_ms: int, next_id: int, // Spawn the server and perform the initialize handshake. The connection @@ -37,13 +39,7 @@ class McpConnection { let p = baml.sys.start_process( command, args, - baml.sys.ProcessOptions { - cwd: null, - env: null, - timeout_ms: timeout_ms, - stdin: null, - keep_stdin_open: true, - }, + baml.sys.ProcessOptions { cwd: null, env: null, timeout_ms: timeout_ms, stdin: null }, ) catch_all (e) { _ => { throw root.errors.NetworkFailure { status_code: null, @@ -53,7 +49,13 @@ class McpConnection { } }, }; - let conn = McpConnection { server_name: name, p: p, next_id: 1 }; + let conn = McpConnection { + server_name: name, + p: p, + out: p.stdout.lines(), + timeout_ms: timeout_ms, + next_id: 1, + }; let init: map = { "protocolVersion": "2025-06-18", "capabilities": {}, @@ -69,7 +71,7 @@ class McpConnection { // lose the newline the protocol frames on; the newline is a quoted // string, appended before the single write. let framed = [line, "\n"].join(""); - let _ = self.p.write_stdin(framed) catch_all (e) { + let _ = self.p.stdin.write(framed) catch_all (e) { _ => { throw root.errors.NetworkFailure { status_code: null, provider: `mcp/${self.server_name}`, @@ -87,7 +89,9 @@ class McpConnection { self.next_id += 1; self._send(rpc_line(id, method, params)); while (true) { - let next = self.p.stdout.next() catch_all (e) { + let next = baml.future.with_timeout(baml.time.Duration.from_milliseconds(self.timeout_ms), () -> { + self.out.next() + }) catch_all (e) { _ => { throw root.errors.NetworkFailure { status_code: null, provider: `mcp/${self.server_name}`, diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_csv/csv.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_csv/csv.baml index 6b1bca2b5fc..3f253dc52d7 100644 --- a/baml_language/crates/baml_builtins2/baml_std/baml/ns_csv/csv.baml +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_csv/csv.baml @@ -7,7 +7,7 @@ // // The streaming reader is the only parser; every one-shot is a wrapper over // it. File IO goes through `baml.fs` handles: the reader pumps chunks via -// `File.read_bytes` from thin BAML method bodies, while all tokenization, +// `baml.io.Read` from thin BAML method bodies, while all tokenization, // decoding, and encoding is native. /// Classifies a `CsvError` for portable handling. @@ -405,29 +405,21 @@ class CsvReader { /// (internal) Pull the next chunk from the backing file, or mark EOF for /// in-memory sources. IO errors are stream-scoped and fatal: the reader is /// marked exhausted before the throw, so subsequent `next()` calls return - /// `baml.iter.Done`. - function _read_chunk(self) -> null throws CsvError | root.errors.Io { - match (self._file) { + /// `baml.iter.Done`. A closed handle arrives as an IO error because + /// `baml.io.Read` has no separate closed-handle error channel. + function _read_chunk(self) -> void throws root.errors.Io { + let chunk = self._file?.read(65536) catch (e) { + root.errors.Io => { + self._mark_exhausted(); + throw e + }, + }; + match (chunk) { null => { self._feed_eof(); - null }, - let f: root.fs.File => { - let chunk = f.read_bytes(65536) catch (e) { - root.errors.InvalidArgument { message } => { - throw _error(CsvErrorKind.Closed, "underlying file handle is closed: " + message) - }, - root.errors.Io => { - self._mark_exhausted(); - throw e - }, - }; - if (chunk.length() == 0) { - self._feed_eof(); - } else { - self._feed(chunk); - } - null + let bytes: uint8array => { + self._feed(bytes); }, } } @@ -583,11 +575,7 @@ class CsvWriter { null => null, let f: root.fs.File => { if (out.length() > 0) { - f.write(out) catch (_) { - root.errors.InvalidArgument { message } => { - throw root.errors.Io { message: "underlying file handle is closed: " + message } - }, - }; + f.write(out); } null }, diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_fs/fs.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_fs/fs.baml index b93b9ccdf38..aba1542467d 100644 --- a/baml_language/crates/baml_builtins2/baml_std/baml/ns_fs/fs.baml +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_fs/fs.baml @@ -1,37 +1,25 @@ /// A handle to an open file. Use `baml.fs.open` to obtain one. /// -/// Every `File.*` method can throw `InvalidArgument` when the underlying -/// handle has already been closed (e.g. by a prior `close()`); read-string -/// methods can additionally throw `ParseError` on non-UTF-8 input. +/// Read and write operations on a closed handle throw `Io`. `text()` +/// additionally throws `ParseError` when the remaining bytes are not valid +/// UTF-8. class File { _handle: $rust_type, /// Reads the entire remaining file contents as a UTF-8 string. - function text( - self, - ) -> string throws root.errors.Io | root.errors.InvalidArgument | root.errors.ParseError { - $rust_io_function + function text(self) -> string throws root.errors.Io | root.errors.ParseError { + self.as.text() } /// Reads the entire remaining file contents as raw bytes. - function bytes(self) -> uint8array throws root.errors.Io | root.errors.InvalidArgument { - $rust_io_function - } - - /// Reads up to `n` bytes from the file and returns them as a UTF-8 string. - function read( - self, - n: int, - ) -> string throws root.errors.Io | root.errors.InvalidArgument | root.errors.ParseError { - $rust_io_function + function bytes(self) -> uint8array throws root.errors.Io { + self.as.bytes() } - /// Reads up to `n` bytes from the file and returns them as raw bytes. - function read_bytes( - self, - n: int, - ) -> uint8array throws root.errors.Io | root.errors.InvalidArgument { - $rust_io_function + implements root.io.Read { + function read(self, limit: int) -> uint8array? throws root.errors.Io { + $rust_io_function + } } /// Closes the file handle, flushing any pending writes. @@ -51,17 +39,20 @@ class File { $rust_io_function } - /// Writes `data` to the file at the current cursor position. Returns the number of bytes written. - function write(self, data: string) -> int throws root.errors.Io | root.errors.InvalidArgument { - $rust_io_function + implements root.io.Write { + function write_some(self, data: uint8array) -> int throws root.errors.Io { + $rust_io_function + } + + function flush(self) -> void throws root.errors.Io { + $rust_io_function + } } - /// Writes raw bytes `data` to the file at the current cursor position. Returns the number of bytes written. - function write_bytes( - self, - data: uint8array, - ) -> int throws root.errors.Io | root.errors.InvalidArgument { - $rust_io_function + /// Writes all of `data` to the file at the current cursor position. + /// Returns the encoded byte count. + function write(self, data: string | uint8array) -> int throws root.errors.Io { + self.as.write(data) } } diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_future/future.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_future/future.baml index a2039d1ecff..d38d513be15 100644 --- a/baml_language/crates/baml_builtins2/baml_std/baml/ns_future/future.baml +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_future/future.baml @@ -238,3 +238,34 @@ function any( winners[0] } } + +/// Run `body` under a deadline. +function with_timeout( + limit: baml.time.Duration, + body: () -> T throws E, +) -> T throws E | baml.errors.Timeout | baml.panics.Cancelled { + let token = baml.spawn.CancelToken.new(); + let work = spawn with baml.spawn.options(cancel = token) { body() }; + let deadline = spawn { + baml.sys.sleep(limit); + token.cancel() + }; + defer { + deadline.cancel() + } + (await work) catch (e) { + baml.panics.Cancelled => { + if (!token.is_cancelled()) { + throw e; + } + let limit_ms = limit.to_milliseconds(); + let reported = limit_ms.to_int() catch (conversion) { + baml.errors.InvalidArgument => null, + }; + throw baml.errors.Timeout { + message: `operation timed out after ${limit_ms}ms`, + duration_ms: reported, + } + }, + } +} diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_io/read.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_io/read.baml new file mode 100644 index 00000000000..a26ba486ca0 --- /dev/null +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_io/read.baml @@ -0,0 +1,32 @@ +interface Read { + /// Read up to `limit` bytes from the source. + /// Returns `null` after EOF. A non-null result is never empty. + function read(self, limit: int) -> uint8array? throws root.errors.Io + + /// Consume all remaining bytes through EOF. + function bytes(self) -> uint8array throws root.errors.Io { + let result = b""; + while let chunk: uint8array = self.read(65536) { + result = result.concat(chunk); + } + result + } + + /// Consume all remaining bytes through EOF and decode strict UTF-8. + function text( + self, + ) -> string throws root.errors.Io | root.errors.ParseError { + let data = self.bytes(); + _decode_utf8(data) + } +} + +function _decode_utf8(data: uint8array) -> string throws root.errors.ParseError { + string.from_utf8(data) catch (e) { + root.errors.InvalidArgument => { + throw root.errors.ParseError { + message: "invalid utf-8", + } + }, + } +} diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_io/write.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_io/write.baml new file mode 100644 index 00000000000..202fc4d1069 --- /dev/null +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_io/write.baml @@ -0,0 +1,37 @@ +interface Write { + /// Attempt one write of `data` and return the number of bytes accepted. + /// A partial write is successful. + function write_some( + self, + data: uint8array, + ) -> int throws root.errors.Io + + /// Write all of `data` to the destination. + /// Strings are encoded as UTF-8. + /// Returns the number of bytes written. + function write( + self, + data: string | uint8array, + ) -> int throws root.errors.Io { + let remaining = match (data) { + let text: string => text.to_utf8(), + let bytes: uint8array => bytes, + }; + let total = remaining.length(); + + while (remaining.length() > 0) { + let written = self.write_some(remaining); + if (written <= 0 || written > remaining.length()) { + throw root.errors.Io { + message: "writer made invalid progress", + }; + } + remaining = remaining.slice(written, remaining.length()); + } + + total + } + + /// Push data buffered by this writer toward its destination. + function flush(self) -> void throws root.errors.Io +} diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_net/net.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_net/net.baml index 3f6426619d9..a1b2ecc21ec 100644 --- a/baml_language/crates/baml_builtins2/baml_std/baml/ns_net/net.baml +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_net/net.baml @@ -35,45 +35,21 @@ class TcpStream { $rust_io_function } - /// Reads the next available bytes from the stream. Blocks until data arrives. - /// Returns an empty `uint8array` once the peer has closed the connection (EOF). - /// - /// `timeout` bounds the wait for data; `null` (the default) blocks - /// indefinitely. On expiry this throws `root.errors.Timeout` and the stream - /// remains usable for a subsequent read. - function read( - self, - timeout: root.time.Duration? = null, - ) -> uint8array throws root.errors.Io | root.errors.Timeout { - self._read(_timeout_nanos(timeout)) + implements root.io.Read { + /// Read at most `limit` bytes. Blocks until data or EOF is available. + function read(self, limit: int) -> uint8array? throws root.errors.Io { + $rust_io_function + } } - function _read( - self, - timeout_nanos: bigint, - ) -> uint8array throws root.errors.Io | root.errors.Timeout { - $rust_io_function - } + implements root.io.Write { + /// Attempt one write and return the number of bytes accepted. + function write_some(self, data: uint8array) -> int throws root.errors.Io { + $rust_io_function + } - /// Writes `data` to the stream. Blocks until every byte has been flushed. - /// - /// `timeout` bounds the flush; `null` (the default) blocks indefinitely. On - /// expiry this throws `root.errors.Timeout`; an unknown amount of `data` may - /// already have been written. - function write( - self, - data: uint8array, - timeout: root.time.Duration? = null, - ) -> null throws root.errors.Io | root.errors.Timeout { - self._write(data, _timeout_nanos(timeout)) - } - - function _write( - self, - data: uint8array, - timeout_nanos: bigint, - ) -> null throws root.errors.Io | root.errors.Timeout { - $rust_io_function + /// No-op because TCP streams are unbuffered. + function flush(self) -> void throws never {} } /// Closes the connection. diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/ns_sys/sys.baml b/baml_language/crates/baml_builtins2/baml_std/baml/ns_sys/sys.baml index 03787de3dd8..dd79c00d4f5 100644 --- a/baml_language/crates/baml_builtins2/baml_std/baml/ns_sys/sys.baml +++ b/baml_language/crates/baml_builtins2/baml_std/baml/ns_sys/sys.baml @@ -6,14 +6,23 @@ class ProcessOptions { env: map?, /// Maximum time in milliseconds to wait for the process to complete. timeout_ms: int?, - /// Data to write to the child process's stdin. + /// The child process's complete stdin. `start_process` writes it and then + /// closes the pipe. Leave it unset to drive `Process.stdin` incrementally. stdin: string?, - /// Keep a writable stdin pipe on a process returned by `start_process`. - /// - /// When this is `true`, use `Process.write_stdin` to send data incrementally - /// and `Process.close_stdin` to deliver EOF. This option has no effect on - /// `exec` or `shell`. - keep_stdin_open: bool?, + /// Where the child's stderr goes. `null` means `StderrMode.Inherit`, and + /// only `StderrMode.Pipe` populates `Process.stderr`. + stderr: StderrMode?, +} + +/// Where a child process's standard error goes. +enum StderrMode { + /// The child writes to this process's stderr. This is the default. + Inherit, + /// The child's stderr becomes `Process.stderr`. The caller must drain it + /// concurrently with stdout to prevent the child from blocking. + Pipe, + /// The child's stderr is discarded. + Discard, } /// The result of running an external process with `exec` or `shell`. @@ -39,64 +48,80 @@ class ProcessExit { } } -/// A live line-oriented child-process output stream. +/// A line-oriented view over a `ReadPipe`. /// -/// Iteration waits for one complete UTF-8 line without blocking other BAML -/// green threads. The final unterminated line is yielded at EOF. -class ProcessLineStream { - _handle: $rust_type, +/// It reads through to the pipe on demand and holds only bytes past the last +/// newline. The final unterminated line is yielded at EOF. Invalid UTF-8 is +/// replaced with U+FFFD. +class ReadPipeLines { + pipe: ReadPipe, + _pending: uint8array, implements root.iter.Iterable { type Item = string - type Error = root.errors.Io | root.errors.Timeout + type Error = root.errors.Io function iter( self, - ) -> root.iter.Iterator throws never { + ) -> root.iter.Iterator throws never { self } } implements root.iter.Iterator { type Item = string - type Error = root.errors.Io | root.errors.Timeout - - function next(self) -> string | root.iter.Done throws root.errors.Io | root.errors.Timeout { - match (self._next()) { - let line: string => line, - null => root.iter.Done { }, + type Error = root.errors.Io + + /// Wait for and return the next line, or `Done` at EOF. + function next(self) -> string | root.iter.Done throws root.errors.Io { + let line: string | root.iter.Done = root.iter.Done { }; + let framing = true; + while (framing) { + match (self._pending.index_of(10)) { + let index: int => { + let bytes = self._pending.slice(0, index); + self._pending = self._pending.slice(index + 1, self._pending.length()); + line = _strip_carriage_return(bytes).to_string(); + framing = false; + }, + null => { + match (self.pipe.read(65536)) { + let chunk: uint8array => { + self._pending = self._pending.concat(chunk); + }, + null => { + if (self._pending.length() > 0) { + line = _strip_carriage_return(self._pending).to_string(); + self._pending = b""; + } + framing = false; + }, + } + }, + } } + line } } +} - /// Wait for and return the next line, or `null` at EOF. - function _next(self) -> string? throws root.errors.Io | root.errors.Timeout { - $rust_io_function - } - - /// Stop reading this stream. Later reads return EOF. - function close(self) -> null throws never { - $rust_io_function +/// Drop one trailing carriage return so CRLF input produces clean lines. +function _strip_carriage_return(line: uint8array) -> uint8array throws never { + match (line.at(line.length() - 1)) { + 13 => line.slice(0, line.length() - 1), + _ => line, } } /// A live child process created with `start_process`. /// -/// `stdout` is consumed incrementally. Stderr inherits the parent process so -/// diagnostics remain visible without risking a blocked, unread stderr pipe. +/// `stdin` and `stdout` are always caller-owned pipes. `stderr` is populated +/// only when `ProcessOptions.stderr` is `StderrMode.Pipe`. class Process { - stdout: ProcessLineStream, _handle: $rust_type, - - /// Write data to the child process's open stdin pipe. - function write_stdin(self, data: string) -> null throws root.errors.Io { - $rust_io_function - } - - /// Close the child process's stdin pipe, delivering EOF. - function close_stdin(self) -> null throws root.errors.Io { - $rust_io_function - } + stdin: WritePipe, + stdout: ReadPipe, + stderr: ReadPipe?, /// Wait for the child to exit. function wait(self) -> ProcessExit throws root.errors.Io | root.errors.Timeout { @@ -104,11 +129,11 @@ class Process { } /// Request that the child terminate. - function kill(self) -> null throws root.errors.Io { + function kill(self) -> void throws root.errors.Io { $rust_io_function } - /// Terminate an un-waited child and close its stdout stream. + /// Terminate an un-waited child and close its stdin, stdout, and stderr pipes. /// /// Safe to use with `defer { process.close() }`. function close(self) -> null throws never { @@ -116,6 +141,47 @@ class Process { } } +/// A system pipe for reading data, typically from another process. +class ReadPipe { + _pipe: $rust_type, + + /// Close the read pipe, releasing any resources. + function close(self) -> void throws root.errors.Io { + $rust_io_function + } + + /// Return a line-oriented view over this pipe. + function lines(self) -> ReadPipeLines throws never { + ReadPipeLines { pipe: self, _pending: b"" } + } + + implements root.io.Read { + function read(self, limit: int) -> uint8array? throws root.errors.Io { + $rust_io_function + } + } +} + +/// A system pipe for writing data, typically to another process. +class WritePipe { + _pipe: $rust_type, + + /// Close the write pipe, flushing pending data and delivering EOF. + function close(self) -> void throws root.errors.Io { + $rust_io_function + } + + implements root.io.Write { + function write_some(self, data: uint8array) -> int throws root.errors.Io { + $rust_io_function + } + + function flush(self) -> void throws root.errors.Io { + $rust_io_function + } + } +} + /// Runs `program` with the given `args` and returns its output. Throws on I/O failure or timeout. function exec( program: string, @@ -125,10 +191,12 @@ function exec( $rust_io_function } -/// Starts `program` and returns immediately with a live stdout line stream. +/// Starts `program` and returns a live process with caller-owned pipes. /// /// Unlike `exec`, output is not buffered until process exit. Reading -/// `process.stdout` suspends only the current BAML green thread. +/// `process.stdout` suspends only the current BAML green thread. If +/// `ProcessOptions.stdin` is set, this call writes that input and waits for the +/// child to consume it, bounded by `ProcessOptions.timeout_ms`. function start_process( program: string, args: string[]?, diff --git a/baml_language/crates/baml_builtins2/baml_std/baml/uint8array.baml b/baml_language/crates/baml_builtins2/baml_std/baml/uint8array.baml index 5020ab93973..fb63c1e37f6 100644 --- a/baml_language/crates/baml_builtins2/baml_std/baml/uint8array.baml +++ b/baml_language/crates/baml_builtins2/baml_std/baml/uint8array.baml @@ -38,6 +38,12 @@ class Uint8Array { $rust_function } + /// Returns the index of the first occurrence of `item`, or `null` when it + /// is absent. An `item` outside the `u8` range is never present. + function index_of(self, item: int) -> int? throws never { + $rust_function + } + /// Returns a new `uint8array` with the bytes in reverse order. function reverse(self) -> uint8array throws never { $rust_function diff --git a/baml_language/crates/baml_builtins2/baml_std/claude_code/ns_internal/cli.baml b/baml_language/crates/baml_builtins2/baml_std/claude_code/ns_internal/cli.baml index db507095239..b98ece27ebb 100644 --- a/baml_language/crates/baml_builtins2/baml_std/claude_code/ns_internal/cli.baml +++ b/baml_language/crates/baml_builtins2/baml_std/claude_code/ns_internal/cli.baml @@ -125,7 +125,7 @@ Application tool catalog (request via outcome.calls only): // terminal result event. function _read_result(p: baml.sys.Process) -> json { let result: json = null; - for (let line in p.stdout) { + for (let line in p.stdout.lines()) { let trimmed = line.trim(); if (trimmed.length() > 0) { let raw = baml.json.parse(trimmed) catch_all (e) { @@ -295,12 +295,23 @@ function invoke(c: root.ClaudeCodeClient, input: ai.ModelTurnInput) -> ai.ModelT p.close() } // the CLI reads stdin when the pipe is open; deliver EOF immediately - let _ = p.close_stdin() catch_all (e) { + let _ = p.stdin.close() catch_all (e) { _ => null, }; // events stream LIVE: each JSONL line logs as it arrives, and the last // relevant line is the result. Stderr inherits the parent process. - let result = _read_result(p); + let result = baml.future.with_timeout(baml.time.Duration.from_milliseconds(c.timeout_ms), () -> { + _read_result(p) + }) catch (e) { + baml.errors.Timeout => { + throw ai.errors.NetworkFailure { + status_code: null, + provider: "claude-code", + detail: `the Claude Code CLI produced no result within ${c.timeout_ms}ms`, + raw_body: null, + } + }, + }; let exit = p.wait() catch_all (e) { let timeout: baml.errors.Timeout => throw timeout, _ => { diff --git a/baml_language/crates/baml_builtins2/src/lib.rs b/baml_language/crates/baml_builtins2/src/lib.rs index ffebdc7a455..5f349db3ffc 100644 --- a/baml_language/crates/baml_builtins2/src/lib.rs +++ b/baml_language/crates/baml_builtins2/src/lib.rs @@ -104,6 +104,8 @@ pub const ALL: &[BuiltinFile] = &[ builtin!("baml", "ns_panics/panics.baml"), builtin!("baml", "ns_env/env.baml"), builtin!("baml", "ns_io/io.baml"), + builtin!("baml", "ns_io/read.baml"), + builtin!("baml", "ns_io/write.baml"), builtin!("baml", "ns_http/http.baml"), builtin!("baml", "ns_http/server.baml"), builtin!("baml", "ns_events/events.baml"), diff --git a/baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap b/baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap index 2261283cbd7..41a892b3306 100644 --- a/baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap +++ b/baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap @@ -56,36 +56,36 @@ class baml.csv.CsvSkip /baml/ns_csv/csv.baml class baml.csv.CsvHeaders /baml/ns_csv/csv.baml:172 class baml.csv.CsvRecord /baml/ns_csv/csv.baml:181 class baml.csv.CsvReader /baml/ns_csv/csv.baml:246 -class baml.csv.CsvRows /baml/ns_csv/csv.baml:438 -class baml.csv.CsvWriter /baml/ns_csv/csv.baml:484 -function baml.csv.reader /baml/ns_csv/csv.baml:606 -function baml.csv.open /baml/ns_csv/csv.baml:614 -function baml.csv.read /baml/ns_csv/csv.baml:628 -function baml.csv.parse /baml/ns_csv/csv.baml:639 -function baml.csv.decode /baml/ns_csv/csv.baml:665 -function baml.csv.decode_optional /baml/ns_csv/csv.baml:680 -function baml.csv.decode_one /baml/ns_csv/csv.baml:698 -function baml.csv.writer /baml/ns_csv/csv.baml:719 -function baml.csv.create /baml/ns_csv/csv.baml:725 -function baml.csv.buffer /baml/ns_csv/csv.baml:736 -function baml.csv.write /baml/ns_csv/csv.baml:742 -function baml.csv.stringify /baml/ns_csv/csv.baml:754 -function baml.csv.stringify_records /baml/ns_csv/csv.baml:767 -function baml.csv.to_markdown /baml/ns_csv/csv.baml:792 -function baml.csv.to_markdown_records /baml/ns_csv/csv.baml:797 -function baml.csv._reader /baml/ns_csv/csv.baml:809 -function baml.csv._writer /baml/ns_csv/csv.baml:819 -function baml.csv._buffer /baml/ns_csv/csv.baml:829 -function baml.csv._validate_columns /baml/ns_csv/csv.baml:836 -function baml.csv._try_decode /baml/ns_csv/csv.baml:843 -function baml.csv._decode_record /baml/ns_csv/csv.baml:850 -function baml.csv._cell_by_name /baml/ns_csv/csv.baml:856 -function baml.csv._cell_by_index /baml/ns_csv/csv.baml:862 -function baml.csv._encode_row /baml/ns_csv/csv.baml:869 -function baml.csv._encode_rows /baml/ns_csv/csv.baml:876 -function baml.csv._to_markdown /baml/ns_csv/csv.baml:882 -function baml.csv._to_markdown_records /baml/ns_csv/csv.baml:889 -function baml.csv._error /baml/ns_csv/csv.baml:898 +class baml.csv.CsvRows /baml/ns_csv/csv.baml:430 +class baml.csv.CsvWriter /baml/ns_csv/csv.baml:476 +function baml.csv.reader /baml/ns_csv/csv.baml:594 +function baml.csv.open /baml/ns_csv/csv.baml:602 +function baml.csv.read /baml/ns_csv/csv.baml:616 +function baml.csv.parse /baml/ns_csv/csv.baml:627 +function baml.csv.decode /baml/ns_csv/csv.baml:653 +function baml.csv.decode_optional /baml/ns_csv/csv.baml:668 +function baml.csv.decode_one /baml/ns_csv/csv.baml:686 +function baml.csv.writer /baml/ns_csv/csv.baml:707 +function baml.csv.create /baml/ns_csv/csv.baml:713 +function baml.csv.buffer /baml/ns_csv/csv.baml:724 +function baml.csv.write /baml/ns_csv/csv.baml:730 +function baml.csv.stringify /baml/ns_csv/csv.baml:742 +function baml.csv.stringify_records /baml/ns_csv/csv.baml:755 +function baml.csv.to_markdown /baml/ns_csv/csv.baml:780 +function baml.csv.to_markdown_records /baml/ns_csv/csv.baml:785 +function baml.csv._reader /baml/ns_csv/csv.baml:797 +function baml.csv._writer /baml/ns_csv/csv.baml:807 +function baml.csv._buffer /baml/ns_csv/csv.baml:817 +function baml.csv._validate_columns /baml/ns_csv/csv.baml:824 +function baml.csv._try_decode /baml/ns_csv/csv.baml:831 +function baml.csv._decode_record /baml/ns_csv/csv.baml:838 +function baml.csv._cell_by_name /baml/ns_csv/csv.baml:844 +function baml.csv._cell_by_index /baml/ns_csv/csv.baml:850 +function baml.csv._encode_row /baml/ns_csv/csv.baml:857 +function baml.csv._encode_rows /baml/ns_csv/csv.baml:864 +function baml.csv._to_markdown /baml/ns_csv/csv.baml:870 +function baml.csv._to_markdown_records /baml/ns_csv/csv.baml:877 +function baml.csv._error /baml/ns_csv/csv.baml:886 function baml.env.get /baml/ns_env/env.baml:6 function baml.env.get_or_panic /baml/ns_env/env.baml:11 class baml.env.Ref /baml/ns_env/env.baml:29 @@ -109,22 +109,22 @@ class baml.errors.TypeMismatch /baml/ns_errors/error class baml.errors.StackFrame /baml/ns_errors/stack_trace.baml:2 class baml.errors.StackTrace /baml/ns_errors/stack_trace.baml:9 class baml.errors.UnknownError /baml/ns_errors/unknown_error.baml:2 -class baml.fs.File /baml/ns_fs/fs.baml:6 -function baml.fs.open /baml/ns_fs/fs.baml:72 -function baml.fs.exists /baml/ns_fs/fs.baml:80 -function baml.fs.remove /baml/ns_fs/fs.baml:88 -function baml.fs.size /baml/ns_fs/fs.baml:93 -function baml.fs.read /baml/ns_fs/fs.baml:99 -function baml.fs.write /baml/ns_fs/fs.baml:104 -function baml.fs.write_bytes /baml/ns_fs/fs.baml:109 -class baml.fs.DirEntry /baml/ns_fs/fs.baml:114 -class baml.fs.MkdirOptions /baml/ns_fs/fs.baml:122 -function baml.fs.read_dir /baml/ns_fs/fs.baml:128 -function baml.fs.mkdir /baml/ns_fs/fs.baml:133 -function baml.fs.remove_dir /baml/ns_fs/fs.baml:139 -function baml.fs.remove_dir_all /baml/ns_fs/fs.baml:148 -function baml.fs.chmod /baml/ns_fs/fs.baml:171 -function baml.fs.symlink /baml/ns_fs/fs.baml:191 +class baml.fs.File /baml/ns_fs/fs.baml:5 +function baml.fs.open /baml/ns_fs/fs.baml:62 +function baml.fs.exists /baml/ns_fs/fs.baml:70 +function baml.fs.remove /baml/ns_fs/fs.baml:78 +function baml.fs.size /baml/ns_fs/fs.baml:83 +function baml.fs.read /baml/ns_fs/fs.baml:89 +function baml.fs.write /baml/ns_fs/fs.baml:94 +function baml.fs.write_bytes /baml/ns_fs/fs.baml:99 +class baml.fs.DirEntry /baml/ns_fs/fs.baml:104 +class baml.fs.MkdirOptions /baml/ns_fs/fs.baml:112 +function baml.fs.read_dir /baml/ns_fs/fs.baml:118 +function baml.fs.mkdir /baml/ns_fs/fs.baml:123 +function baml.fs.remove_dir /baml/ns_fs/fs.baml:129 +function baml.fs.remove_dir_all /baml/ns_fs/fs.baml:138 +function baml.fs.chmod /baml/ns_fs/fs.baml:161 +function baml.fs.symlink /baml/ns_fs/fs.baml:181 enum baml.future.FutureState /baml/ns_future/future.baml:21 class baml.future.Future /baml/ns_future/future.baml:28 function baml.future.all_complete /baml/ns_future/future.baml:80 @@ -133,6 +133,7 @@ class baml.future.AllFailed /baml/ns_future/futur function baml.future.__await_any /baml/ns_future/future.baml:145 function baml.future.race /baml/ns_future/future.baml:153 function baml.future.any /baml/ns_future/future.baml:169 +function baml.future.with_timeout /baml/ns_future/future.baml:243 class baml.glob.ScanOptions /baml/ns_glob/glob.baml:2 class baml.glob.Glob /baml/ns_glob/glob.baml:18 function baml.glob.new /baml/ns_glob/glob.baml:34 @@ -158,6 +159,9 @@ function baml.io.print /baml/ns_io/io.baml:8 function baml.io.println /baml/ns_io/io.baml:13 function baml.io.eprint /baml/ns_io/io.baml:18 function baml.io.eprintln /baml/ns_io/io.baml:23 +interface baml.io.Read /baml/ns_io/read.baml:1 +function baml.io._decode_utf8 /baml/ns_io/read.baml:24 +interface baml.io.Write /baml/ns_io/write.baml:1 class baml.iter.Done /baml/ns_iter/iter.baml:2 interface baml.iter.Iterable /baml/ns_iter/iter.baml:6 interface baml.iter.Iterator /baml/ns_iter/iter.baml:18 @@ -205,9 +209,9 @@ class baml.media.Video /baml/ns_media/media. class baml.media.Image /baml/ns_media/media.baml:134 function baml.net._timeout_nanos /baml/ns_net/net.baml:5 class baml.net.TcpStream /baml/ns_net/net.baml:14 -class baml.net.TcpListener /baml/ns_net/net.baml:88 -class baml.net.UdpSocket /baml/ns_net/net.baml:110 -class baml.net.Datagram /baml/ns_net/net.baml:168 +class baml.net.TcpListener /baml/ns_net/net.baml:64 +class baml.net.UdpSocket /baml/ns_net/net.baml:86 +class baml.net.Datagram /baml/ns_net/net.baml:144 interface baml.ops.BitAnd /baml/ns_ops/bitwise.baml:28 interface baml.ops.BitOr /baml/ns_ops/bitwise.baml:34 interface baml.ops.BitXor /baml/ns_ops/bitwise.baml:40 @@ -262,19 +266,23 @@ class baml.spawn.TaskGroup /baml/ns_spawn/spawn. class baml.spawn.CancelToken /baml/ns_spawn/spawn.baml:85 function baml.spawn.options /baml/ns_spawn/spawn.baml:123 class baml.sys.ProcessOptions /baml/ns_sys/sys.baml:2 -class baml.sys.ShellOutput /baml/ns_sys/sys.baml:20 -class baml.sys.ProcessExit /baml/ns_sys/sys.baml:32 -class baml.sys.ProcessLineStream /baml/ns_sys/sys.baml:46 -class baml.sys.Process /baml/ns_sys/sys.baml:87 -function baml.sys.exec /baml/ns_sys/sys.baml:120 -function baml.sys.start_process /baml/ns_sys/sys.baml:132 -function baml.sys.shell /baml/ns_sys/sys.baml:141 -function baml.sys.sleep /baml/ns_sys/sys.baml:149 -function baml.sys.collect_garbage /baml/ns_sys/sys.baml:156 -function baml.sys.panic /baml/ns_sys/sys.baml:162 -function baml.sys.exit /baml/ns_sys/sys.baml:168 -function baml.sys.argv /baml/ns_sys/sys.baml:174 -function baml.sys.pid /baml/ns_sys/sys.baml:183 +enum baml.sys.StderrMode /baml/ns_sys/sys.baml:18 +class baml.sys.ShellOutput /baml/ns_sys/sys.baml:29 +class baml.sys.ProcessExit /baml/ns_sys/sys.baml:41 +class baml.sys.ReadPipeLines /baml/ns_sys/sys.baml:56 +function baml.sys._strip_carriage_return /baml/ns_sys/sys.baml:109 +class baml.sys.Process /baml/ns_sys/sys.baml:120 +class baml.sys.ReadPipe /baml/ns_sys/sys.baml:145 +class baml.sys.WritePipe /baml/ns_sys/sys.baml:166 +function baml.sys.exec /baml/ns_sys/sys.baml:186 +function baml.sys.start_process /baml/ns_sys/sys.baml:200 +function baml.sys.shell /baml/ns_sys/sys.baml:209 +function baml.sys.sleep /baml/ns_sys/sys.baml:217 +function baml.sys.collect_garbage /baml/ns_sys/sys.baml:224 +function baml.sys.panic /baml/ns_sys/sys.baml:230 +function baml.sys.exit /baml/ns_sys/sys.baml:236 +function baml.sys.argv /baml/ns_sys/sys.baml:242 +function baml.sys.pid /baml/ns_sys/sys.baml:251 class baml.time.Duration /baml/ns_time/duration.baml:4 class baml.time.Instant /baml/ns_time/instant.baml:2 class baml.time.PlainDate /baml/ns_time/plaindate.baml:5 diff --git a/baml_language/crates/baml_tests/baml_src/ns_fs/fs.baml b/baml_language/crates/baml_tests/baml_src/ns_fs/fs.baml index af34d48ada2..2f38887f4ce 100644 --- a/baml_language/crates/baml_tests/baml_src/ns_fs/fs.baml +++ b/baml_language/crates/baml_tests/baml_src/ns_fs/fs.baml @@ -206,7 +206,7 @@ test "fs_file_rw_write_and_read_back" { } // ============================================================================ -// write_bytes to open file handle +// write bytes to open file handle // ============================================================================ function fs_file_rw_write_bytes_fn() -> bool { @@ -219,7 +219,7 @@ function fs_file_rw_write_bytes_fn() -> bool { let bytes = baml.fs.open(src, "r").bytes(); let file = baml.fs.open(dst, "r+"); file.seek_from("start", 0); - let n = file.write_bytes(bytes); + let n = file.write(bytes); baml.fs.remove(src); baml.fs.remove(dst); // "AB" is 2 bytes @@ -292,48 +292,48 @@ test "fs_size_returns_length" { } // ============================================================================ -// file.read(n) reads up to n bytes as a string +// file.read(n) reads up to n bytes // ============================================================================ -function fs_file_read_n_bytes_fn() -> string { +function fs_file_read_n_bytes_fn() -> uint8array { let path = fs_tmp("read_n.txt"); baml.fs.write(path, "Hello, world!"); let f = baml.fs.open(path, "r"); - let result = f.read(5); + let result = f.read(5) ?? b""; baml.fs.remove(path); result } test "fs_file_read_n_bytes" { - assert.equal(fs_file_read_n_bytes_fn(), "Hello") + assert.equal(fs_file_read_n_bytes_fn(), "Hello".to_utf8()) } // ============================================================================ // file.read(n) truncates at EOF (doesn't error when n > file size) // ============================================================================ -function fs_file_read_truncates_at_eof_fn() -> string { +function fs_file_read_truncates_at_eof_fn() -> uint8array { let path = fs_tmp("read_eof.txt"); baml.fs.write(path, "abc"); let f = baml.fs.open(path, "r"); - let result = f.read(100); + let result = f.read(100) ?? b""; baml.fs.remove(path); result } test "fs_file_read_truncates_at_eof" { - assert.equal(fs_file_read_truncates_at_eof_fn(), "abc") + assert.equal(fs_file_read_truncates_at_eof_fn(), "abc".to_utf8()) } // ============================================================================ -// file.read_bytes(n) reads n raw bytes — verified by length +// file.read(n) reads raw bytes, verified by length // ============================================================================ function fs_file_read_bytes_n_fn() -> bool { let path = fs_tmp("read_bytes_n.bin"); baml.fs.write(path, "ABCDE"); let f = baml.fs.open(path, "r"); - let data = f.read_bytes(3); + let data = f.read(3) ?? b""; baml.fs.remove(path); // should have read exactly 3 bytes data.length() == 3 @@ -347,18 +347,18 @@ test "fs_file_read_bytes_n" { // file.read advances the cursor (sequential reads) // ============================================================================ -function fs_file_read_advances_cursor_fn() -> string { +function fs_file_read_advances_cursor_fn() -> uint8array { let path = fs_tmp("read_advances.txt"); baml.fs.write(path, "ABCDEFGH"); let f = baml.fs.open(path, "r"); f.read(3); - let result = f.read(3); + let result = f.read(3) ?? b""; baml.fs.remove(path); result } test "fs_file_read_advances_cursor" { - assert.equal(fs_file_read_advances_cursor_fn(), "DEF") + assert.equal(fs_file_read_advances_cursor_fn(), "DEF".to_utf8()) } // ============================================================================ @@ -544,9 +544,15 @@ function fs_read_dir_returns_entries_fn() -> bool { baml.fs.remove(dir + "/b.txt"); // 3 entries: a.txt, b.txt, subdir let count_ok = entries.length() == 3; - let has_a = entries.some((e: baml.fs.DirEntry) -> bool { e.name == "a.txt" }); - let has_b = entries.some((e: baml.fs.DirEntry) -> bool { e.name == "b.txt" }); - let has_sub = entries.some((e: baml.fs.DirEntry) -> bool { e.name == "subdir" }); + let has_a = entries.some((e: baml.fs.DirEntry) -> bool { + e.name == "a.txt" + }); + let has_b = entries.some((e: baml.fs.DirEntry) -> bool { + e.name == "b.txt" + }); + let has_sub = entries.some((e: baml.fs.DirEntry) -> bool { + e.name == "subdir" + }); count_ok && has_a && has_b && has_sub } diff --git a/baml_language/crates/baml_tests/baml_src/ns_fs/fs_from_rust.baml b/baml_language/crates/baml_tests/baml_src/ns_fs/fs_from_rust.baml index 7009047200c..8115bfc1efc 100644 --- a/baml_language/crates/baml_tests/baml_src/ns_fs/fs_from_rust.baml +++ b/baml_language/crates/baml_tests/baml_src/ns_fs/fs_from_rust.baml @@ -196,7 +196,7 @@ test "fs_remove_dir_all_on_file_errors" { } // ============================================================================ -// file.read with a negative length errors (InvalidArgument) +// file.read with a negative length errors (Io) // ============================================================================ function fs_file_read_negative_n_errors_fn() -> bool { @@ -207,7 +207,7 @@ function fs_file_read_negative_n_errors_fn() -> bool { f.read(-1); false } catch (e) { - baml.errors.InvalidArgument => true + baml.errors.Io => true }; baml.fs.remove(path); threw @@ -217,6 +217,24 @@ test "fs_file_read_negative_n_errors" { assert.is_true(fs_file_read_negative_n_errors_fn()) } +function fs_file_read_zero_errors_fn() -> bool { + let path = fs_tmp("read_zero.txt"); + baml.fs.write(path, "abc"); + let f = baml.fs.open(path, "r"); + let threw = { + f.read(0); + false + } catch (e) { + baml.errors.Io => true + }; + baml.fs.remove(path); + threw +} + +test "fs_file_read_zero_errors" { + assert.is_true(fs_file_read_zero_errors_fn()) +} + // ============================================================================ // close invalidates the handle: reading after close errors ("File is closed") // ============================================================================ @@ -230,7 +248,7 @@ function fs_file_close_invalidates_handle_fn() -> bool { f.text(); false } catch (e) { - baml.errors.InvalidArgument => true + baml.errors.Io => true }; baml.fs.remove(path); threw diff --git a/baml_language/crates/baml_tests/baml_src/ns_io/io.baml b/baml_language/crates/baml_tests/baml_src/ns_io/io.baml new file mode 100644 index 00000000000..3c22914f70e --- /dev/null +++ b/baml_language/crates/baml_tests/baml_src/ns_io/io.baml @@ -0,0 +1,118 @@ +class ChunkReader { + data: uint8array, + cursor: int, + chunk_size: int, + + implements baml.io.Read { + function read(self, limit: int) -> uint8array? { + if (self.cursor >= self.data.length()) { + return null; + } + let count = if (limit < self.chunk_size) { + limit + } else { + self.chunk_size + }; + let end = if (self.cursor + count < self.data.length()) { + self.cursor + count + } else { + self.data.length() + }; + let chunk = self.data.slice(self.cursor, end); + self.cursor = end; + chunk + } + } +} + +class ChunkWriter { + data: uint8array, + chunk_size: int, + + implements baml.io.Write { + function write_some(self, data: uint8array) -> int { + let count = if (data.length() < self.chunk_size) { + data.length() + } else { + self.chunk_size + }; + self.data = self.data.concat(data.slice(0, count)); + count + } + + function flush(self) -> void {} + } +} + +class ZeroWriter { + implements baml.io.Write { + function write_some(self, data: uint8array) -> int { + 0 + } + function flush(self) -> void {} + } +} + +class OversizedWriter { + implements baml.io.Write { + function write_some(self, data: uint8array) -> int { + data.length() + 1 + } + function flush(self) -> void {} + } +} + +test "read bytes consumes chunks through eof" { + let reader = ChunkReader { data: "abcdef".to_utf8(), cursor: 0, chunk_size: 2 }; + assert.equal(reader.bytes(), "abcdef".to_utf8()); + assert.equal(reader.read(8), null) +} + +test "read text decodes strict utf8" { + let reader = ChunkReader { data: "hello".to_utf8(), cursor: 0, chunk_size: 1 }; + assert.equal(reader.text(), "hello") +} + +test "read text rejects invalid utf8" { + let reader = ChunkReader { data: uint8array.from_array([255]), cursor: 0, chunk_size: 1 }; + let failed = { + reader.text(); + false + } catch (e) { + baml.errors.ParseError => true + }; + assert.is_true(failed) +} + +test "write retries partial writes" { + let writer = ChunkWriter { data: b"", chunk_size: 2 }; + assert.equal(writer.write("hello"), 5); + assert.equal(writer.data, "hello".to_utf8()) +} + +test "write rejects zero progress" { + let failed = { + ZeroWriter { }.write(b"abc"); + false + } catch (e) { + baml.errors.Io => true + }; + assert.is_true(failed) +} + +test "write rejects oversized progress" { + let failed = { + OversizedWriter { }.write(b"abc"); + false + } catch (e) { + baml.errors.Io => true + }; + assert.is_true(failed) +} + +test "uint8array index_of finds bytes" { + let data = uint8array.from_array([10, 20, 10]); + assert.equal(data.index_of(10), 0); + assert.equal(data.index_of(99), null); + assert.equal(data.index_of(256), null) +} diff --git a/baml_language/crates/baml_tests/baml_src/ns_net/net.baml b/baml_language/crates/baml_tests/baml_src/ns_net/net.baml index f420038815e..e289b3d7cdc 100644 --- a/baml_language/crates/baml_tests/baml_src/ns_net/net.baml +++ b/baml_language/crates/baml_tests/baml_src/ns_net/net.baml @@ -22,11 +22,11 @@ // Connect succeeds, then close() invalidates the shared handle so a subsequent // read() on the same socket fails deterministically ("TcpStream is closed"). -function net_tcp_read_after_close_errors_fn() -> uint8array { +function net_tcp_read_after_close_errors_fn() -> uint8array? { let server = baml.http.Server.bind("127.0.0.1:0"); let sock = baml.net.TcpStream.connect(server.addr); sock.close(); - sock.read() + sock.read(4096) } test "net_tcp_read_after_close_errors" { @@ -38,11 +38,11 @@ test "net_tcp_read_after_close_errors" { } } -function net_tcp_listener_accept_after_close_errors_fn() -> uint8array { +function net_tcp_listener_accept_after_close_errors_fn() -> uint8array? { let listener = baml.net.TcpListener.bind("127.0.0.1:0"); listener.close(); let sock = listener.accept(); - sock.read() + sock.read(4096) } test "net_tcp_listener_accept_after_close_errors" { diff --git a/baml_language/crates/baml_tests/baml_src/ns_with_timeout/with_timeout.baml b/baml_language/crates/baml_tests/baml_src/ns_with_timeout/with_timeout.baml new file mode 100644 index 00000000000..4ae41194be8 --- /dev/null +++ b/baml_language/crates/baml_tests/baml_src/ns_with_timeout/with_timeout.baml @@ -0,0 +1,35 @@ +function with_timeout_returns_value() -> int { + baml.future.with_timeout(baml.time.Duration.from_seconds(30), () -> { + 42 + }) +} + +test "with_timeout returns value" { + assert.equal(with_timeout_returns_value(), 42) +} + +function with_timeout_expires() -> string { + baml.future.with_timeout(baml.time.Duration.from_milliseconds(25), () -> { + baml.sys.sleep(baml.time.Duration.from_seconds(60)); + "finished" + }) catch (e) { + baml.errors.Timeout => "timed out", + } +} + +test "with_timeout expires" { + assert.equal(with_timeout_expires(), "timed out") +} + +function with_timeout_preserves_body_error() -> string { + baml.future.with_timeout(baml.time.Duration.from_seconds(30), () -> { + throw baml.errors.Io { message: "body error" } + }) catch (e) { + baml.errors.Timeout => "timed out", + baml.errors.Io => "io", + } +} + +test "with_timeout preserves body error" { + assert.equal(with_timeout_preserves_body_error(), "io") +} diff --git a/baml_language/crates/baml_tests/projects/diagnostic_errors/builtin_io/fs_ops.baml b/baml_language/crates/baml_tests/projects/diagnostic_errors/builtin_io/fs_ops.baml index 6444f21d931..49b8ba95c09 100644 --- a/baml_language/crates/baml_tests/projects/diagnostic_errors/builtin_io/fs_ops.baml +++ b/baml_language/crates/baml_tests/projects/diagnostic_errors/builtin_io/fs_ops.baml @@ -40,14 +40,14 @@ function ReadFileBytesAndClose() -> uint8array { data } -function ReadChunk() -> string { +function ReadChunk() -> uint8array? { let f = baml.fs.open("log.txt", "r"); f.read(512) } function ReadChunkBytes() -> uint8array { let f = baml.fs.open("data.bin", "r"); - f.read_bytes(4096) + f.read(4096) ?? b"" } function WriteFile() -> int { @@ -74,7 +74,7 @@ function FileWriteBytes() -> int { let f = baml.fs.open("data.bin", "r+"); let data = f.bytes(); f.seek_from("start", 0); - f.write_bytes(data) + f.write(data) } function Append() -> int { diff --git a/baml_language/crates/baml_tests/projects/diagnostic_errors/builtin_io/net_ops.baml b/baml_language/crates/baml_tests/projects/diagnostic_errors/builtin_io/net_ops.baml index 85983071ca0..e64fc9b959b 100644 --- a/baml_language/crates/baml_tests/projects/diagnostic_errors/builtin_io/net_ops.baml +++ b/baml_language/crates/baml_tests/projects/diagnostic_errors/builtin_io/net_ops.baml @@ -1,33 +1,33 @@ // Test network operations typecheck correctly -function ConnectAndRead() -> uint8array { +function ConnectAndRead() -> uint8array? { let sock = baml.net.TcpStream.connect("127.0.0.1:8080"); - sock.read() + sock.read(4096) } -function ConnectAndReadInline() -> uint8array { - baml.net.TcpStream.connect("localhost:3000").read() +function ConnectAndReadInline() -> uint8array? { + baml.net.TcpStream.connect("localhost:3000").read(4096) } function MultipleConnections() -> uint8array { let s1 = baml.net.TcpStream.connect("server1:80"); let s2 = baml.net.TcpStream.connect("server2:80"); - let data1 = s1.read(); - let data2 = s2.read(); + let data1 = s1.read(4096) ?? b""; + let data2 = s2.read(4096) ?? b""; data1.concat(data2) } -function ConnectReadAndClose() -> uint8array { +function ConnectReadAndClose() -> uint8array? { let sock = baml.net.TcpStream.connect("127.0.0.1:8080"); - let data = sock.read(); + let data = sock.read(4096); sock.close(); data } -function ListenAndAccept() -> uint8array { +function ListenAndAccept() -> uint8array? { let listener = baml.net.TcpListener.bind("127.0.0.1:0"); let sock = listener.accept(); - sock.read() + sock.read(4096) } function UdpRoundtrip() -> uint8array { diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/bytecode.snap b/baml_language/crates/baml_tests/snapshots/baml_src/bytecode.snap index 3b79ba30ca8..ccf2d859923 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/bytecode.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/bytecode.snap @@ -613,6 +613,12 @@ function $init_test(registry: unknown) -> null { load_var ?1 call ? pop 1 + load_var ?1 + call ? + pop 1 + load_var ?1 + call ? + pop 1 load_const ? return } diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/ns_csv/bytecode.snap b/baml_language/crates/baml_tests/snapshots/baml_src/ns_csv/bytecode.snap index e1cb34c90f9..26695e245b2 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/ns_csv/bytecode.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/ns_csv/bytecode.snap @@ -786,7 +786,9 @@ function csv.csv_caller_owned_handles_stay_open() -> string { pop 1 load_var wf load_const "tail\n" - sys_op baml.fs.File.write + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 store_var appended load_var wf sys_op baml.fs.File.close diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/ns_fs/bytecode.snap b/baml_language/crates/baml_tests/snapshots/baml_src/ns_fs/bytecode.snap index 5994c2befd1..98236e8b8c7 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/ns_fs/bytecode.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/ns_fs/bytecode.snap @@ -359,67 +359,74 @@ function fs.$init_test_ns_fs_fs_from_rust(registry: testing.TestCollector) -> nu pop 1 load_var registry load_const "root.fs" - load_const "fs_file_close_invalidates_handle" + load_const "fs_file_read_zero_errors" make_closure ., 0 load_const null call testing.TestCollector.register_test_at pop 1 load_var registry load_const "root.fs" - load_const "fs_write_wrapper_closes_on_error" + load_const "fs_file_close_invalidates_handle" make_closure ., 0 load_const null call testing.TestCollector.register_test_at pop 1 load_var registry load_const "root.fs" - load_const "fs_read_nonexistent_errors" + load_const "fs_write_wrapper_closes_on_error" make_closure ., 0 load_const null call testing.TestCollector.register_test_at pop 1 load_var registry load_const "root.fs" - load_const "fs_file_seek_negative_errors" + load_const "fs_read_nonexistent_errors" make_closure ., 0 load_const null call testing.TestCollector.register_test_at pop 1 load_var registry load_const "root.fs" - load_const "fs_read_dir_nonexistent_errors" + load_const "fs_file_seek_negative_errors" make_closure ., 0 load_const null call testing.TestCollector.register_test_at pop 1 load_var registry load_const "root.fs" - load_const "fs_read_dir_on_file_errors" + load_const "fs_read_dir_nonexistent_errors" make_closure ., 0 load_const null call testing.TestCollector.register_test_at pop 1 load_var registry load_const "root.fs" - load_const "fs_mkdir_non_recursive_errors_when_parent_missing" + load_const "fs_read_dir_on_file_errors" make_closure ., 0 load_const null call testing.TestCollector.register_test_at pop 1 load_var registry load_const "root.fs" - load_const "fs_mkdir_non_recursive_errors_when_dir_exists" + load_const "fs_mkdir_non_recursive_errors_when_parent_missing" make_closure ., 0 load_const null call testing.TestCollector.register_test_at pop 1 load_var registry load_const "root.fs" - load_const "fs_mkdir_recursive_errors_when_leaf_is_file" + load_const "fs_mkdir_non_recursive_errors_when_dir_exists" make_closure ., 0 load_const null call testing.TestCollector.register_test_at pop 1 + load_var registry + load_const "root.fs" + load_const "fs_mkdir_recursive_errors_when_leaf_is_file" + make_closure ., 0 + load_const null + call testing.TestCollector.register_test_at + pop 1 load_const null return } @@ -577,11 +584,13 @@ function fs.fs_file_close_invalidates_handle_fn() -> bool { sys_op baml.fs.File.close pop 1 load_var f - sys_op baml.fs.File.text + load_type baml.io.Read + load_const "text" + virtual_call nargs=1 ntypeargs=0 pop 1 jump L2 load_var e - is_type baml.errors.InvalidArgument + is_type baml.errors.Io pop_jump_if_false L0 jump L1 @@ -631,7 +640,7 @@ function fs.fs_file_close_is_idempotent_fn() -> void { return } -function fs.fs_file_read_advances_cursor_fn() -> string { +function fs.fs_file_read_advances_cursor_fn() -> uint8array { load_const "read_advances.txt" call user.fs.fs_tmp store_var path @@ -645,11 +654,27 @@ function fs.fs_file_read_advances_cursor_fn() -> string { store_var f load_var f load_const 3 - sys_op baml.fs.File.read + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 pop 1 load_var f load_const 3 - sys_op baml.fs.File.read + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 + store_var _9 + load_var _9 + store_var_load_var _8 + load_const null + cmp_op == + pop_jump_if_false L0 + load_const b"" + call baml.deep_copy + store_var _8 + + L0: + load_var _8 store_var result load_var path sys_op baml.fs.remove @@ -670,21 +695,35 @@ function fs.fs_file_read_bytes_n_fn() -> bool { load_const "r" sys_op baml.fs.open load_const 3 - sys_op baml.fs.File.read_bytes + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 + store_var _8 + load_var _8 + store_var_load_var _7 + load_const null + cmp_op == + pop_jump_if_false L0 + load_const b"" + call baml.deep_copy + store_var _7 + + L0: + load_var _7 store_var data load_var path sys_op baml.fs.remove pop 1 load_var data container_len - store_var _9 - load_var _9 + store_var _12 + load_var _12 load_const 3 cmp_int_op == return } -function fs.fs_file_read_n_bytes_fn() -> string { +function fs.fs_file_read_n_bytes_fn() -> uint8array { load_const "read_n.txt" call user.fs.fs_tmp store_var path @@ -696,7 +735,21 @@ function fs.fs_file_read_n_bytes_fn() -> string { load_const "r" sys_op baml.fs.open load_const 5 - sys_op baml.fs.File.read + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 + store_var _8 + load_var _8 + store_var_load_var _7 + load_const null + cmp_op == + pop_jump_if_false L0 + load_const b"" + call baml.deep_copy + store_var _7 + + L0: + load_var _7 store_var result load_var path sys_op baml.fs.remove @@ -717,11 +770,13 @@ function fs.fs_file_read_negative_n_errors_fn() -> bool { load_const "r" sys_op baml.fs.open load_const -1 - sys_op baml.fs.File.read + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 pop 1 jump L2 load_var e - is_type baml.errors.InvalidArgument + is_type baml.errors.Io pop_jump_if_false L0 jump L1 @@ -746,7 +801,7 @@ function fs.fs_file_read_negative_n_errors_fn() -> bool { return } -function fs.fs_file_read_truncates_at_eof_fn() -> string { +function fs.fs_file_read_truncates_at_eof_fn() -> uint8array { load_const "read_eof.txt" call user.fs.fs_tmp store_var path @@ -758,7 +813,21 @@ function fs.fs_file_read_truncates_at_eof_fn() -> string { load_const "r" sys_op baml.fs.open load_const 100 - sys_op baml.fs.File.read + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 + store_var _8 + load_var _8 + store_var_load_var _7 + load_const null + cmp_op == + pop_jump_if_false L0 + load_const b"" + call baml.deep_copy + store_var _7 + + L0: + load_var _7 store_var result load_var path sys_op baml.fs.remove @@ -767,6 +836,49 @@ function fs.fs_file_read_truncates_at_eof_fn() -> string { return } +function fs.fs_file_read_zero_errors_fn() -> bool { + load_const "read_zero.txt" + call user.fs.fs_tmp + store_var path + load_var path + load_const "abc" + sys_op baml.fs.write + pop 1 + load_var path + load_const "r" + sys_op baml.fs.open + load_const 0 + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 + pop 1 + jump L2 + load_var e + is_type baml.errors.Io + pop_jump_if_false L0 + jump L1 + + L0: + load_var e + rethrow + + L1: + load_const true + store_var threw + jump L3 + + L2: + load_const false + store_var threw + + L3: + load_var path + sys_op baml.fs.remove + pop 1 + load_var threw + return +} + function fs.fs_file_rw_seek_and_read_fn() -> string { load_const "seek_and_read.txt" call user.fs.fs_tmp @@ -785,7 +897,9 @@ function fs.fs_file_rw_seek_and_read_fn() -> string { sys_op baml.fs.File.seek_from pop 1 load_var file - sys_op baml.fs.File.text + load_type baml.io.Read + load_const "text" + virtual_call nargs=1 ntypeargs=0 store_var result load_var path sys_op baml.fs.remove @@ -813,7 +927,9 @@ function fs.fs_file_rw_write_and_read_back_fn() -> string { pop 1 load_var file load_const "to Rust!!" - sys_op baml.fs.File.write + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 pop 1 load_var file load_const "start" @@ -821,7 +937,9 @@ function fs.fs_file_rw_write_and_read_back_fn() -> string { sys_op baml.fs.File.seek_from pop 1 load_var file - sys_op baml.fs.File.text + load_type baml.io.Read + load_const "text" + virtual_call nargs=1 ntypeargs=0 store_var result load_var path sys_op baml.fs.remove @@ -848,7 +966,9 @@ function fs.fs_file_rw_write_bytes_fn() -> bool { load_var src load_const "r" sys_op baml.fs.open - sys_op baml.fs.File.bytes + load_type baml.io.Read + load_const "bytes" + virtual_call nargs=1 ntypeargs=0 store_var bytes load_var dst load_const "r+" @@ -860,7 +980,9 @@ function fs.fs_file_rw_write_bytes_fn() -> bool { sys_op baml.fs.File.seek_from pop 1 load_var2 4 3 - sys_op baml.fs.File.write_bytes + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 store_var n load_var src sys_op baml.fs.remove @@ -928,7 +1050,9 @@ function fs.fs_file_write_on_readonly_errors_fn() -> bool { load_const "r" sys_op baml.fs.open load_const "should fail" - sys_op baml.fs.File.write + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 pop 1 jump L2 load_var e @@ -1132,7 +1256,9 @@ function fs.fs_open_and_read_bytes_fn() -> bool { load_var path load_const "r" sys_op baml.fs.open - sys_op baml.fs.File.bytes + load_type baml.io.Read + load_const "bytes" + virtual_call nargs=1 ntypeargs=0 store_var data load_var path sys_op baml.fs.remove @@ -1157,7 +1283,9 @@ function fs.fs_open_and_read_fn() -> string { load_var path load_const "r" sys_op baml.fs.open - sys_op baml.fs.File.text + load_type baml.io.Read + load_const "text" + virtual_call nargs=1 ntypeargs=0 store_var result load_var path sys_op baml.fs.remove @@ -1178,7 +1306,9 @@ function fs.fs_open_append_creates_and_appends_fn() -> bool { load_const "a" sys_op baml.fs.open load_const "more!" - sys_op baml.fs.File.write + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 store_var n load_var path sys_op baml.fs.read @@ -1207,13 +1337,17 @@ function fs.fs_open_append_creates_missing_file_fn() -> bool { load_const "w" sys_op baml.fs.open load_const "" - sys_op baml.fs.File.write + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 pop 1 load_var path load_const "a" sys_op baml.fs.open load_const "hi" - sys_op baml.fs.File.write + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 store_var n load_var path sys_op baml.fs.read @@ -1248,7 +1382,9 @@ function fs.fs_open_append_plus_can_read_fn() -> string { store_var f load_var f load_const " more" - sys_op baml.fs.File.write + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 pop 1 load_var f load_const "start" @@ -1256,7 +1392,9 @@ function fs.fs_open_append_plus_can_read_fn() -> string { sys_op baml.fs.File.seek_from pop 1 load_var f - sys_op baml.fs.File.text + load_type baml.io.Read + load_const "text" + virtual_call nargs=1 ntypeargs=0 store_var result load_var path sys_op baml.fs.remove @@ -1272,7 +1410,9 @@ function fs.fs_open_nonexistent_file_fn() -> bool { bin_op + load_const "r" sys_op baml.fs.open - sys_op baml.fs.File.text + load_type baml.io.Read + load_const "text" + virtual_call nargs=1 ntypeargs=0 pop 1 jump L2 load_var e @@ -1303,7 +1443,9 @@ function fs.fs_open_w_creates_missing_file_fn() -> bool { load_const "w" sys_op baml.fs.open load_const "hi" - sys_op baml.fs.File.write + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 store_var n load_var path sys_op baml.fs.read @@ -1333,7 +1475,9 @@ function fs.fs_open_w_creates_parent_dirs_fn() -> bool { load_const "w" sys_op baml.fs.open load_const "nested" - sys_op baml.fs.File.write + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 store_var n load_var path sys_op baml.fs.read @@ -1368,7 +1512,9 @@ function fs.fs_open_w_plus_reads_after_write_fn() -> string { store_var f load_var f load_const "fresh" - sys_op baml.fs.File.write + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 pop 1 load_var f load_const "start" @@ -1376,7 +1522,9 @@ function fs.fs_open_w_plus_reads_after_write_fn() -> string { sys_op baml.fs.File.seek_from pop 1 load_var f - sys_op baml.fs.File.text + load_type baml.io.Read + load_const "text" + virtual_call nargs=1 ntypeargs=0 store_var result load_var path sys_op baml.fs.remove @@ -1397,7 +1545,9 @@ function fs.fs_open_w_truncates_existing_fn() -> bool { load_const "w" sys_op baml.fs.open load_const "new" - sys_op baml.fs.File.write + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 store_var n load_var path sys_op baml.fs.read @@ -2032,7 +2182,9 @@ function fs.fs_roundtrip_write_and_read_fn() -> string { load_var path load_const "r" sys_op baml.fs.open - sys_op baml.fs.File.text + load_type baml.io.Read + load_const "text" + virtual_call nargs=1 ntypeargs=0 store_var result load_var path sys_op baml.fs.remove @@ -2161,7 +2313,9 @@ function fs.fs_write_bytes_fn() -> bool { load_var src load_const "r" sys_op baml.fs.open - sys_op baml.fs.File.bytes + load_type baml.io.Read + load_const "bytes" + virtual_call nargs=1 ntypeargs=0 store_var data load_var2 2 3 sys_op baml.fs.write_bytes @@ -2169,7 +2323,9 @@ function fs.fs_write_bytes_fn() -> bool { load_var dst load_const "r" sys_op baml.fs.open - sys_op baml.fs.File.bytes + load_type baml.io.Read + load_const "bytes" + virtual_call nargs=1 ntypeargs=0 store_var readback load_var src sys_op baml.fs.remove diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/ns_io/bytecode.snap b/baml_language/crates/baml_tests/snapshots/baml_src/ns_io/bytecode.snap new file mode 100644 index 00000000000..8f2667ae124 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/baml_src/ns_io/bytecode.snap @@ -0,0 +1,202 @@ +--- +source: crates/baml_tests/src/corpus.rs +--- +function io.$init_test_ns_io_io(registry: testing.TestCollector) -> null { + load_var registry + load_const "root.io" + load_const "read bytes consumes chunks through eof" + make_closure ., 0 + load_const null + call testing.TestCollector.register_test_at + pop 1 + load_var registry + load_const "root.io" + load_const "read text decodes strict utf8" + make_closure ., 0 + load_const null + call testing.TestCollector.register_test_at + pop 1 + load_var registry + load_const "root.io" + load_const "read text rejects invalid utf8" + make_closure ., 0 + load_const null + call testing.TestCollector.register_test_at + pop 1 + load_var registry + load_const "root.io" + load_const "write retries partial writes" + make_closure ., 0 + load_const null + call testing.TestCollector.register_test_at + pop 1 + load_var registry + load_const "root.io" + load_const "write rejects zero progress" + make_closure ., 0 + load_const null + call testing.TestCollector.register_test_at + pop 1 + load_var registry + load_const "root.io" + load_const "write rejects oversized progress" + make_closure ., 0 + load_const null + call testing.TestCollector.register_test_at + pop 1 + load_var registry + load_const "root.io" + load_const "uint8array index_of finds bytes" + make_closure ., 0 + load_const null + call testing.TestCollector.register_test_at + pop 1 + load_const null + return +} + +function io.ChunkReader.baml.io.Read.read(self: io.ChunkReader, limit: int) -> uint8array | null { + load_var self + load_field .cursor + store_var _4 + load_var self + load_field .data + container_len + store_var _5 + load_var2 3 4 + cmp_int_op >= + pop_jump_if_false L0 + jump L7 + + L0: + load_var2 2 1 + load_field .chunk_size + cmp_int_op < + pop_jump_if_false L1 + jump L2 + + L1: + load_var self + load_field .chunk_size + store_var count + jump L3 + + L2: + load_var limit + store_var count + + L3: + load_var self + load_field .cursor + load_var count + add_int + store_var _12 + load_var self + load_field .data + container_len + store_var _15 + load_var2 7 8 + cmp_int_op < + pop_jump_if_false L4 + jump L5 + + L4: + load_var self + load_field .data + container_len + store_var end + jump L6 + + L5: + load_var self + load_field .cursor + load_var count + add_int + store_var end + + L6: + load_var self + load_field .data + load_var self + load_field .cursor + load_var end + call baml.Uint8Array.slice + load_var2 1 6 + store_field .cursor + jump L8 + + L7: + load_const null + + L8: + return +} + +function io.ChunkWriter.baml.io.Write.flush(self: io.ChunkWriter) -> void { + load_const null + return +} + +function io.ChunkWriter.baml.io.Write.write_some(self: io.ChunkWriter, data: uint8array) -> int { + load_var data + container_len + store_var _5 + load_var2 4 1 + load_field .chunk_size + cmp_int_op < + pop_jump_if_false L0 + jump L1 + + L0: + load_var self + load_field .chunk_size + store_var count + jump L2 + + L1: + load_var data + container_len + store_var count + + L2: + load_var self + load_field .data + store_var _7 + load_var data + load_const 0 + load_var count + call baml.Uint8Array.slice + store_var _8 + load_var2 5 6 + call baml.Uint8Array.concat + store_var _10 + load_var2 1 7 + store_field .data + load_var count + return +} + +function io.OversizedWriter.baml.io.Write.flush(self: io.OversizedWriter) -> void { + load_const null + return +} + +function io.OversizedWriter.baml.io.Write.write_some(self: io.OversizedWriter, data: uint8array) -> int { + load_var data + container_len + store_var _3 + load_var _3 + load_const 1 + add_int + return +} + +function io.ZeroWriter.baml.io.Write.flush(self: io.ZeroWriter) -> void { + load_const null + return +} + +function io.ZeroWriter.baml.io.Write.write_some(self: io.ZeroWriter, data: uint8array) -> int { + load_const 0 + return +} diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/ns_net/bytecode.snap b/baml_language/crates/baml_tests/snapshots/baml_src/ns_net/bytecode.snap index 6649e37780f..62a10553ab1 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/ns_net/bytecode.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/ns_net/bytecode.snap @@ -34,7 +34,7 @@ function net.$init_test_ns_net_net(registry: testing.TestCollector) -> null { return } -function net.net_tcp_listener_accept_after_close_errors_fn() -> uint8array { +function net.net_tcp_listener_accept_after_close_errors_fn() -> uint8array | null { load_const "127.0.0.1:0" sys_op baml.net.TcpListener.bind store_var listener @@ -43,12 +43,16 @@ function net.net_tcp_listener_accept_after_close_errors_fn() -> uint8array { pop 1 load_var listener sys_op baml.net.TcpListener.accept - load_const - call baml.net.TcpStream.read + load_const 4096 + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 + store_var _0 + load_var _0 return } -function net.net_tcp_read_after_close_errors_fn() -> uint8array { +function net.net_tcp_read_after_close_errors_fn() -> uint8array | null { load_const "127.0.0.1:0" sys_op baml.http.Server.bind load_field .addr @@ -59,8 +63,12 @@ function net.net_tcp_read_after_close_errors_fn() -> uint8array { sys_op baml.net.TcpStream.close pop 1 load_var sock - load_const - call baml.net.TcpStream.read + load_const 4096 + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 + store_var _0 + load_var _0 return } diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/ns_optional_chain_type_args/bytecode.snap b/baml_language/crates/baml_tests/snapshots/baml_src/ns_optional_chain_type_args/bytecode.snap index 5b025dfed06..7c37af670cc 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/ns_optional_chain_type_args/bytecode.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/ns_optional_chain_type_args/bytecode.snap @@ -1097,7 +1097,9 @@ function optional_chain_type_args.sys_op_optional_receiver_fn() -> string { L0: load_var path call user.optional_chain_type_args.optional_file - sys_op baml.fs.File.text + load_type baml.io.Read + load_const "text" + virtual_call nargs=1 ntypeargs=0 store_var text jump L2 @@ -1133,7 +1135,9 @@ function optional_chain_type_args.sys_op_plain_receiver_fn() -> string { load_var path load_const "r" sys_op baml.fs.open - sys_op baml.fs.File.text + load_type baml.io.Read + load_const "text" + virtual_call nargs=1 ntypeargs=0 store_var text load_var path sys_op baml.fs.remove diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/ns_with_timeout/bytecode.snap b/baml_language/crates/baml_tests/snapshots/baml_src/ns_with_timeout/bytecode.snap new file mode 100644 index 00000000000..13a62061cf3 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/baml_src/ns_with_timeout/bytecode.snap @@ -0,0 +1,102 @@ +--- +source: crates/baml_tests/src/corpus.rs +--- +function with_timeout.$init_test_ns_with_timeout_with_timeout(registry: testing.TestCollector) -> null { + load_var registry + load_const "root.with_timeout" + load_const "with_timeout returns value" + make_closure ., 0 + load_const null + call testing.TestCollector.register_test_at + pop 1 + load_var registry + load_const "root.with_timeout" + load_const "with_timeout expires" + make_closure ., 0 + load_const null + call testing.TestCollector.register_test_at + pop 1 + load_var registry + load_const "root.with_timeout" + load_const "with_timeout preserves body error" + make_closure ., 0 + load_const null + call testing.TestCollector.register_test_at + pop 1 + load_const null + return +} + +function with_timeout.with_timeout_expires() -> string { + load_const 25 + call baml.time.Duration.from_milliseconds + store_var _2 + load_type string + load_type baml.errors.Io + load_var _2 + make_closure ., 0 + call baml.future.with_timeout + jump L2 + load_var e + is_type baml.errors.Timeout + pop_jump_if_false L0 + jump L1 + + L0: + load_var e + rethrow + + L1: + load_const "timed out" + + L2: + return +} + +function with_timeout.with_timeout_preserves_body_error() -> string { + load_const 30 + call baml.time.Duration.from_seconds + store_var _2 + load_type never + load_type baml.errors.Io + load_var _2 + make_closure ., 0 + call baml.future.with_timeout + jump L4 + load_var e + is_type baml.errors.Timeout + pop_jump_if_false L0 + jump L3 + + L0: + load_var e + is_type baml.errors.Io + pop_jump_if_false L1 + jump L2 + + L1: + load_var e + rethrow + + L2: + load_const "io" + jump L4 + + L3: + load_const "timed out" + + L4: + return +} + +function with_timeout.with_timeout_returns_value() -> int { + load_const 30 + call baml.time.Duration.from_seconds + store_var _1 + load_type int + load_type never + load_var _1 + make_closure ., 0 + call baml.future.with_timeout + return +} diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/bytecode.snap b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/bytecode.snap index d9b8890319d..733f3c25200 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/bytecode.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/bytecode.snap @@ -5614,7 +5614,7 @@ function ai.internal._resolve_media_content(url: string | null, file: string | n load_var url store_var _40 load_var _40 - narrow_bind string, slot=18 + narrow_bind string, slot=19 pop_jump_if_false L2 jump L3 @@ -5652,14 +5652,14 @@ function ai.internal._resolve_media_content(url: string | null, file: string | n load_const 30 call baml.time.Duration.from_seconds store_var _69 - load_var2 19 31 + load_var2 20 32 call baml.http.fetch store_var response jump L10 load_var e store_var _70 load_var _70 - narrow_bind baml.errors.Timeout, slot=32 + narrow_bind baml.errors.Timeout, slot=33 pop_jump_if_false L8 jump L9 @@ -5772,7 +5772,7 @@ function ai.internal._resolve_media_content(url: string | null, file: string | n call baml.String.trim store_var _116 load_const null - load_var2 44 45 + load_var2 45 46 init_instance ai.wire.ResolvedMedia .url, .base64, .mime_type jump L27 @@ -5812,7 +5812,7 @@ function ai.internal._resolve_media_content(url: string | null, file: string | n call baml.String.index_of store_var _44 load_var _44 - narrow_bind int, slot=20 + narrow_bind int, slot=21 pop_jump_if_false L18 jump L22 @@ -5862,7 +5862,7 @@ function ai.internal._resolve_media_content(url: string | null, file: string | n load_var media_url call baml.String.length store_var _49 - load_var2 19 23 + load_var2 20 24 load_var _49 call baml.String.slice store_var _46 @@ -5872,7 +5872,7 @@ function ai.internal._resolve_media_content(url: string | null, file: string | n call baml.String.slice store_var _50 load_const null - load_var2 22 25 + load_var2 23 26 init_instance ai.wire.ResolvedMedia .url, .base64, .mime_type jump L27 @@ -5915,7 +5915,11 @@ function ai.internal._resolve_media_content(url: string | null, file: string | n L25: load_var handle - sys_op baml.fs.File.bytes + load_type baml.io.Read + load_const "bytes" + virtual_call nargs=1 ntypeargs=0 + store_var _24 + load_var _24 call baml.Uint8Array.to_base64 store_var data jump L26 @@ -5928,7 +5932,7 @@ function ai.internal._resolve_media_content(url: string | null, file: string | n load_var path call baml.String.from store_var _30 - load_type baml.errors.InvalidArgument | baml.errors.Io + load_type baml.errors.Io load_var e call baml.String.from store_var _34 @@ -6533,10 +6537,13 @@ function ai.internal.validate_output_type(return_type: reflect.Type) -> void { } function ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: string, params: map) -> baml.json.json { - load_var self + load_var ?1 + make_cell + store_var ?1 + load_deref ?1 load_field .next_id store_var id - load_var self + load_deref ?1 copy 0 load_field .next_id load_const 1 @@ -6546,7 +6553,8 @@ function ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: strin load_var params call ai.mcp.rpc_line store_var _6 - load_var2 1 5 + load_deref ?1 + load_var _6 call ai.mcp.McpConnection._send pop 1 @@ -6557,19 +6565,19 @@ function ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: strin L1: load_type string - load_var self + load_deref ?1 load_field .server_name call baml.String.from - store_var _65 + store_var _69 load_type string load_var method call baml.String.from - store_var _69 + store_var _73 load_const "mcp/" - load_var _65 + load_var _69 bin_op + load_const "no response for " - load_var _69 + load_var _73 bin_op + load_const null load_const null @@ -6577,34 +6585,36 @@ function ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: strin throw L2: - load_var self - load_field .p - load_field .stdout - load_type baml.iter.Iterator - load_const "next" - virtual_call nargs=1 ntypeargs=0 - store_var next + load_deref ?1 + load_field .timeout_ms + call baml.time.Duration.from_milliseconds + store_var _10 + load_type string | baml.iter.Done + load_type baml.errors.Io + load_var2 7 1 + make_closure ., 1 + call baml.future.with_timeout jump L3 load_var e throw_if_panic load_type string - load_var self + load_deref ?1 load_field .server_name call baml.String.from - store_var _13 - load_type baml.errors.Io | baml.errors.Timeout + store_var _17 + load_type never load_var e call baml.String.from - store_var _18 + store_var _22 load_type string - load_var _18 + load_var _22 call baml.String.from - store_var _17 + store_var _21 load_const "mcp/" - load_var _13 + load_var _17 bin_op + load_const "reading from the MCP server failed: " - load_var _17 + load_var _21 bin_op + load_const null load_const null @@ -6612,28 +6622,27 @@ function ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: strin throw L3: - load_var next - store_var _21 - load_var _21 + store_var _25 + load_var _25 narrow_bind string, slot=11 pop_jump_if_false L4 jump L5 L4: load_type string - load_var self + load_deref ?1 load_field .server_name call baml.String.from - store_var _57 + store_var _61 load_type string load_var method call baml.String.from - store_var _61 + store_var _65 load_const "mcp/" - load_var _57 + load_var _61 bin_op + load_const "the MCP server stream ended before answering " - load_var _61 + load_var _65 bin_op + load_const null load_const null @@ -6641,7 +6650,7 @@ function ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: strin throw L5: - load_var _21 + load_var _25 call baml.String.trim store_var trimmed load_var trimmed @@ -6690,30 +6699,30 @@ function ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: strin L8: load_type string - load_var self + load_deref ?1 load_field .server_name call baml.String.from - store_var _42 + store_var _46 load_type int | null load_var err load_const ".code" load_const null call baml.json.path_or - store_var _45 + store_var _49 load_type string load_var err load_const ".message" load_const "MCP error" call baml.json.path_or - store_var _48 + store_var _52 load_var err call baml.json.stringify - store_var _51 + store_var _55 load_const "mcp/" - load_var _42 + load_var _46 bin_op + load_var2 17 18 - load_var _51 + load_var _55 init_instance ai.errors.InvalidRequest .provider, .status_code, .detail, .raw_body throw } @@ -6729,8 +6738,11 @@ function ai.mcp.McpConnection._send(self: ai.mcp.McpConnection, line: string) -> store_var framed load_var self load_field .p + load_field .stdin load_var framed - sys_op baml.sys.Process.write_stdin + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 pop 1 jump L0 load_var e @@ -6884,8 +6896,8 @@ function ai.mcp.McpConnection.connect(name: string, command: string, args: strin load_const null load_var timeout_ms load_const null - load_const true - init_instance baml.sys.ProcessOptions .cwd, .env, .timeout_ms, .stdin, .keep_stdin_open + load_const null + init_instance baml.sys.ProcessOptions .cwd, .env, .timeout_ms, .stdin, .stderr sys_op baml.sys.start_process store_var p jump L1 @@ -6915,9 +6927,14 @@ function ai.mcp.McpConnection.connect(name: string, command: string, args: strin throw L1: + load_var p + load_field .stdout + call baml.sys.ReadPipe.lines + store_var _20 load_var2 1 5 + load_var2 11 4 load_const 1 - init_instance ai.mcp.McpConnection .server_name, .p, .next_id + init_instance ai.mcp.McpConnection .server_name, .p, .out, .timeout_ms, .next_id store_var conn load_var conn load_const "initialize" @@ -6944,8 +6961,8 @@ function ai.mcp.McpConnection.connect(name: string, command: string, args: strin load_const "notifications/initialized" load_const null call ai.mcp.rpc_line - store_var _26 - load_var2 10 11 + store_var _28 + load_var2 10 12 call ai.mcp.McpConnection._send pop 1 load_var conn diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/mir.snap b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/mir.snap index b598a185e17..47b62d90836 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/mir.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/mir.snap @@ -5661,7 +5661,7 @@ fn ai.internal._resolve_media_content(url: string | null, file: string | null, i } bb44: { - _24 = sys_op const fn baml.fs.File.bytes(copy _8) -> bb45 unwind bb54; + _24 = virtual_call bytes as baml.io.Read(copy _8) -> [bb45, unwind: bb54]; } bb45: { @@ -5727,7 +5727,7 @@ fn ai.internal._resolve_media_content(url: string | null, file: string | null, i bb57: { _29 = const "media file " + copy _30; _28 = copy _29 + const " could not be read: "; - _35 = load_type(baml.errors.InvalidArgument | baml.errors.Io); + _35 = load_type(baml.errors.Io); _34 = call const fn baml.String.from(copy _23) -> [bb58]; } @@ -6704,13 +6704,15 @@ fn ai.mcp.McpConnection.connect(name: string, command: string, args: string[], t let _17: reflect.Type let _18: ai.mcp.McpConnection // conn let _19: baml.sys.Process - let _20: map // init - let _21: map - let _22: map - let _23: int | float | string | bool | null | baml.json.json[] | map - let _24: map - let _25: void - let _26: string + let _20: baml.sys.ReadPipeLines + let _21: (baml.sys.ReadPipe) -> baml.sys.ReadPipeLines throws never + let _22: map // init + let _23: map + let _24: map + let _25: int | float | string | bool | null | baml.json.json[] | map + let _26: map + let _27: void + let _28: string bb0: { _5 = copy _4 == const ; @@ -6723,58 +6725,63 @@ fn ai.mcp.McpConnection.connect(name: string, command: string, args: string[], t } bb2: { - _8 = baml.sys.ProcessOptions { const null, const null, copy _4, const null, const true }; - _6 = sys_op const fn baml.sys.start_process(copy _2, copy _3, copy _8) -> bb3 unwind bb8; + _8 = baml.sys.ProcessOptions { const null, const null, copy _4, const null, const null }; + _6 = sys_op const fn baml.sys.start_process(copy _2, copy _3, copy _8) -> bb3 unwind bb9; } bb3: { _19 = copy _6; - _18 = ai.mcp.McpConnection { copy _1, copy _19, const 1_i64 }; - _21 = { }: map; - _22 = { const "name": const "baml-mcp", const "version": const "0.1" }: map; - _20 = { const "protocolVersion": const "2025-06-18", const "capabilities": copy _21, const "clientInfo": copy _22 }: map; - _24 = copy _20; - _23 = call const fn ai.mcp.McpConnection._request(copy _18, const "initialize", copy _24) -> [bb4]; + _21 = copy _6.2; + _20 = call const fn baml.sys.ReadPipe.lines(copy _21) -> [bb4]; } bb4: { - _26 = call const fn ai.mcp.rpc_line(const null, const "notifications/initialized", const null) -> [bb5]; + _18 = ai.mcp.McpConnection { copy _1, copy _19, copy _20, copy _4, const 1_i64 }; + _23 = { }: map; + _24 = { const "name": const "baml-mcp", const "version": const "0.1" }: map; + _22 = { const "protocolVersion": const "2025-06-18", const "capabilities": copy _23, const "clientInfo": copy _24 }: map; + _26 = copy _22; + _25 = call const fn ai.mcp.McpConnection._request(copy _18, const "initialize", copy _26) -> [bb5]; } bb5: { - _25 = call const fn ai.mcp.McpConnection._send(copy _18, copy _26) -> [bb6]; + _28 = call const fn ai.mcp.rpc_line(const null, const "notifications/initialized", const null) -> [bb6]; } bb6: { - _0 = copy _18; - goto -> bb7; + _27 = call const fn ai.mcp.McpConnection._send(copy _18, copy _28) -> [bb7]; } bb7: { - return; + _0 = copy _18; + goto -> bb8; } bb8: { - throw_if_panic copy _7 -> bb9; + return; } bb9: { - _12 = load_type(string); - _11 = call const fn baml.String.from(copy _1) -> [bb10]; + throw_if_panic copy _7 -> bb10; } bb10: { + _12 = load_type(string); + _11 = call const fn baml.String.from(copy _1) -> [bb11]; + } + + bb11: { _10 = const "mcp/" + copy _11; _16 = load_type(baml.errors.Io); - _15 = call const fn baml.String.from(copy _7) -> [bb11]; + _15 = call const fn baml.String.from(copy _7) -> [bb12]; } - bb11: { + bb12: { _17 = load_type(string); - _14 = call const fn baml.String.from(copy _15) -> [bb12]; + _14 = call const fn baml.String.from(copy _15) -> [bb13]; } - bb12: { + bb13: { _13 = const "the MCP server could not start: " + copy _14; _9 = ai.errors.NetworkFailure { copy _10, copy _13, const null, const null }; throw copy _9; @@ -6789,9 +6796,9 @@ fn ai.mcp.McpConnection._send(self: ai.mcp.McpConnection, line: string) -> void let _3: string // framed let _4: string[] let _5: reflect.Type - let _6: null + let _6: int let _7: unknown // e - let _8: (baml.sys.Process, string) -> null throws baml.errors.Io + let _8: baml.sys.WritePipe let _9: string let _10: ai.errors.NetworkFailure let _11: string @@ -6811,9 +6818,9 @@ fn ai.mcp.McpConnection._send(self: ai.mcp.McpConnection, line: string) -> void } bb1: { - _8 = copy _1.1; + _8 = copy _1.1.1; _9 = copy _3; - _6 = sys_op const fn baml.sys.Process.write_stdin(copy _8, copy _9) -> bb2 unwind bb4; + _6 = virtual_call write as baml.io.Write(copy _8, copy _9) -> [bb2, unwind: bb4]; } bb2: { @@ -6856,7 +6863,7 @@ fn ai.mcp.McpConnection._send(self: ai.mcp.McpConnection, line: string) -> void fn ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: string, params: map) -> int | float | string | bool | null | baml.json.json[] | map { // Locals: let _0: int | float | string | bool | null | baml.json.json[] | map // _0 // return - let _1: ai.mcp.McpConnection // self // param + let _1: ai.mcp.McpConnection // self // param [captured] let _2: string // method // param let _3: map // params // param let _4: int // id @@ -6865,71 +6872,75 @@ fn ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: string, par let _7: int let _8: string | baml.iter.Done // next let _9: unknown // e - let _10: baml.sys.ProcessLineStream - let _11: ai.errors.NetworkFailure - let _12: string - let _13: string - let _14: string - let _15: reflect.Type + let _10: baml.time.Duration + let _11: int + let _12: () -> string | baml.iter.Done throws baml.errors.Io + let _13: reflect.Type + let _14: reflect.Type + let _15: ai.errors.NetworkFailure let _16: string let _17: string let _18: string let _19: reflect.Type - let _20: reflect.Type - let _21: string | baml.iter.Done - let _22: string // line - let _23: string // trimmed - let _24: bool - let _25: int - let _26: int | float | string | bool | null | baml.json.json[] | map // msg - let _27: unknown // e - let _28: string - let _29: bool - let _30: int - let _31: int | float | string | bool | null | baml.json.json[] | map - let _32: reflect.Type - let _33: int - let _34: int | float | string | bool | null | baml.json.json[] | map // err + let _20: string + let _21: string + let _22: string + let _23: reflect.Type + let _24: reflect.Type + let _25: string | baml.iter.Done + let _26: string // line + let _27: string // trimmed + let _28: bool + let _29: int + let _30: int | float | string | bool | null | baml.json.json[] | map // msg + let _31: unknown // e + let _32: string + let _33: bool + let _34: int let _35: int | float | string | bool | null | baml.json.json[] | map let _36: reflect.Type - let _37: bool - let _38: int | float | string | bool | null | baml.json.json[] | map - let _39: bool - let _40: ai.errors.InvalidRequest - let _41: string - let _42: string - let _43: string - let _44: reflect.Type - let _45: int | null - let _46: int | float | string | bool | baml.json.json[] | map - let _47: reflect.Type - let _48: string - let _49: int | float | string | bool | baml.json.json[] | map - let _50: reflect.Type - let _51: string - let _52: int | float | string | bool | baml.json.json[] | map - let _53: int | float | string | bool | null | baml.json.json[] | map + let _37: int + let _38: int | float | string | bool | null | baml.json.json[] | map // err + let _39: int | float | string | bool | null | baml.json.json[] | map + let _40: reflect.Type + let _41: bool + let _42: int | float | string | bool | null | baml.json.json[] | map + let _43: bool + let _44: ai.errors.InvalidRequest + let _45: string + let _46: string + let _47: string + let _48: reflect.Type + let _49: int | null + let _50: int | float | string | bool | baml.json.json[] | map + let _51: reflect.Type + let _52: string + let _53: int | float | string | bool | baml.json.json[] | map let _54: reflect.Type - let _55: ai.errors.NetworkFailure - let _56: string - let _57: string - let _58: string - let _59: reflect.Type + let _55: string + let _56: int | float | string | bool | baml.json.json[] | map + let _57: int | float | string | bool | null | baml.json.json[] | map + let _58: reflect.Type + let _59: ai.errors.NetworkFailure let _60: string let _61: string - let _62: reflect.Type - let _63: ai.errors.NetworkFailure + let _62: string + let _63: reflect.Type let _64: string let _65: string - let _66: string - let _67: reflect.Type + let _66: reflect.Type + let _67: ai.errors.NetworkFailure let _68: string let _69: string - let _70: reflect.Type + let _70: string + let _71: reflect.Type + let _72: string + let _73: string + let _74: reflect.Type bb0: { - _4 = copy _1.2; - _1.2 = copy _1.2 + const 1_i64; + _4 = copy _1.4; + _1.4 = copy _1.4 + const 1_i64; _7 = copy _4; _6 = call const fn ai.mcp.rpc_line(copy _7, copy _2, copy _3) -> [bb1]; } @@ -6939,24 +6950,24 @@ fn ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: string, par } bb2: { - throw_if_panic copy _27 -> bb3; + throw_if_panic copy _31 -> bb3; } bb3: { - _26 = const null; + _30 = const null; goto -> bb4; } bb4: { - _31 = copy _26; - _32 = load_type(int); - _30 = call const fn baml.json.path_or(copy _31, const ".id", const -1_i64) -> [bb5]; + _35 = copy _30; + _36 = load_type(int); + _34 = call const fn baml.json.path_or(copy _35, const ".id", const -1_i64) -> [bb5]; } bb5: { - _33 = copy _4; - _29 = copy _30 == copy _33; - branch copy _29 -> [bb24, bb6]; + _37 = copy _4; + _33 = copy _34 == copy _37; + branch copy _33 -> [bb25, bb6]; } bb6: { @@ -6964,150 +6975,173 @@ fn ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: string, par } bb7: { - _66 = copy _1.0; - _67 = load_type(string); - _65 = call const fn baml.String.from(copy _66) -> [bb8]; + _70 = copy _1.0; + _71 = load_type(string); + _69 = call const fn baml.String.from(copy _70) -> [bb8]; } bb8: { - _64 = const "mcp/" + copy _65; - _70 = load_type(string); - _69 = call const fn baml.String.from(copy _2) -> [bb9]; + _68 = const "mcp/" + copy _69; + _74 = load_type(string); + _73 = call const fn baml.String.from(copy _2) -> [bb9]; } bb9: { - _68 = const "no response for " + copy _69; - _63 = ai.errors.NetworkFailure { copy _64, copy _68, const null, const null }; - throw copy _63; + _72 = const "no response for " + copy _73; + _67 = ai.errors.NetworkFailure { copy _68, copy _72, const null, const null }; + throw copy _67; } bb10: { - _10 = copy _1.1.0; - _8 = virtual_call next as baml.iter.Iterator(copy _10) -> [bb16, unwind: bb11]; + _11 = copy _1.3; + _10 = call const fn baml.time.Duration.from_milliseconds(copy _11) -> [bb11, unwind: bb12]; } bb11: { - throw_if_panic copy _9 -> bb12; + _12 = make_closure lambda[0](copy _1); + _13 = load_type(string | baml.iter.Done); + _14 = load_type(baml.errors.Io); + _8 = call const fn baml.future.with_timeout(copy _10, copy _12) -> [bb17, unwind: bb12]; } bb12: { - _14 = copy _1.0; - _15 = load_type(string); - _13 = call const fn baml.String.from(copy _14) -> [bb13]; + throw_if_panic copy _9 -> bb13; } bb13: { - _12 = const "mcp/" + copy _13; - _19 = load_type(baml.errors.Io | baml.errors.Timeout); - _18 = call const fn baml.String.from(copy _9) -> [bb14]; + _18 = copy _1.0; + _19 = load_type(string); + _17 = call const fn baml.String.from(copy _18) -> [bb14]; } bb14: { - _20 = load_type(string); - _17 = call const fn baml.String.from(copy _18) -> [bb15]; + _16 = const "mcp/" + copy _17; + _23 = load_type(never); + _22 = call const fn baml.String.from(copy _9) -> [bb15]; } bb15: { - _16 = const "reading from the MCP server failed: " + copy _17; - _11 = ai.errors.NetworkFailure { copy _12, copy _16, const null, const null }; - throw copy _11; + _24 = load_type(string); + _21 = call const fn baml.String.from(copy _22) -> [bb16]; } bb16: { - _21 = copy _8; - _21 = narrow_bind copy _21 as String { attr: TyAttr { sap_parse_without_null: Unset, sap_pending_never: Unset, sap_in_progress_never: Unset } } -> [bb20, bb17]; + _20 = const "reading from the MCP server failed: " + copy _21; + _15 = ai.errors.NetworkFailure { copy _16, copy _20, const null, const null }; + throw copy _15; } bb17: { - _58 = copy _1.0; - _59 = load_type(string); - _57 = call const fn baml.String.from(copy _58) -> [bb18]; + _25 = copy _8; + _25 = narrow_bind copy _25 as String { attr: TyAttr { sap_parse_without_null: Unset, sap_pending_never: Unset, sap_in_progress_never: Unset } } -> [bb21, bb18]; } bb18: { - _56 = const "mcp/" + copy _57; - _62 = load_type(string); - _61 = call const fn baml.String.from(copy _2) -> [bb19]; + _62 = copy _1.0; + _63 = load_type(string); + _61 = call const fn baml.String.from(copy _62) -> [bb19]; } bb19: { - _60 = const "the MCP server stream ended before answering " + copy _61; - _55 = ai.errors.NetworkFailure { copy _56, copy _60, const null, const null }; - throw copy _55; + _60 = const "mcp/" + copy _61; + _66 = load_type(string); + _65 = call const fn baml.String.from(copy _2) -> [bb20]; } bb20: { - _22 = copy _21; - _23 = call const fn baml.String.trim(copy _22) -> [bb21]; + _64 = const "the MCP server stream ended before answering " + copy _65; + _59 = ai.errors.NetworkFailure { copy _60, copy _64, const null, const null }; + throw copy _59; } bb21: { - _25 = call const fn baml.String.length(copy _23) -> [bb22]; + _26 = copy _25; + _27 = call const fn baml.String.trim(copy _26) -> [bb22]; } bb22: { - _24 = copy _25 > const 0_i64; - branch copy _24 -> [bb23, bb6]; + _29 = call const fn baml.String.length(copy _27) -> [bb23]; } bb23: { - _28 = copy _23; - _26 = call const fn baml.json.parse(copy _28) -> [bb4, unwind: bb2]; + _28 = copy _29 > const 0_i64; + branch copy _28 -> [bb24, bb6]; } bb24: { - _35 = copy _26; - _36 = load_type(baml.json.json); - _34 = call const fn baml.json.path_or(copy _35, const ".error", const null) -> [bb25]; + _32 = copy _27; + _30 = call const fn baml.json.parse(copy _32) -> [bb4, unwind: bb2]; } bb25: { - _38 = copy _34; - _39 = call const fn baml.ops.equals_equals(copy _38, const null) -> [bb26]; + _39 = copy _30; + _40 = load_type(baml.json.json); + _38 = call const fn baml.json.path_or(copy _39, const ".error", const null) -> [bb26]; } bb26: { - _37 = !copy _39; - branch copy _37 -> [bb29, bb27]; + _42 = copy _38; + _43 = call const fn baml.ops.equals_equals(copy _42, const null) -> [bb27]; } bb27: { - _53 = copy _26; - _54 = load_type(baml.json.json); - _0 = call const fn baml.json.path_or(copy _53, const ".result", const null) -> [bb28]; + _41 = !copy _43; + branch copy _41 -> [bb30, bb28]; } bb28: { - return; + _57 = copy _30; + _58 = load_type(baml.json.json); + _0 = call const fn baml.json.path_or(copy _57, const ".result", const null) -> [bb29]; } bb29: { - _43 = copy _1.0; - _44 = load_type(string); - _42 = call const fn baml.String.from(copy _43) -> [bb30]; + return; } bb30: { - _41 = const "mcp/" + copy _42; - _46 = copy _34; - _47 = load_type(int | null); - _45 = call const fn baml.json.path_or(copy _46, const ".code", const null) -> [bb31]; + _47 = copy _1.0; + _48 = load_type(string); + _46 = call const fn baml.String.from(copy _47) -> [bb31]; } bb31: { - _49 = copy _34; - _50 = load_type(string); - _48 = call const fn baml.json.path_or(copy _49, const ".message", const "MCP error") -> [bb32]; + _45 = const "mcp/" + copy _46; + _50 = copy _38; + _51 = load_type(int | null); + _49 = call const fn baml.json.path_or(copy _50, const ".code", const null) -> [bb32]; } bb32: { - _52 = copy _34; - _51 = call const fn baml.json.stringify(copy _52) -> [bb33]; + _53 = copy _38; + _54 = load_type(string); + _52 = call const fn baml.json.path_or(copy _53, const ".message", const "MCP error") -> [bb33]; } bb33: { - _40 = ai.errors.InvalidRequest { copy _41, copy _45, copy _48, copy _51 }; - throw copy _40; + _56 = copy _38; + _55 = call const fn baml.json.stringify(copy _56) -> [bb34]; + } + + bb34: { + _44 = ai.errors.InvalidRequest { copy _45, copy _49, copy _52, copy _55 }; + throw copy _44; + } +} + +// lambda[0] +fn .() -> string | baml.iter.Done { + // Locals: + let _0: string | baml.iter.Done // _0 // return + let _1: (baml.sys.ReadPipeLines) -> string | baml.iter.Done throws baml.errors.Io + + bb0: { + _1 = copy capture[0].2; + _0 = call const fn baml.sys.ReadPipeLines.root.iter.Iterator.next(copy _1) -> [bb1]; + } + + bb1: { + return; } } diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/ppir.snap b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/ppir.snap index 1fdc0b2c92f..fcabbb66316 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/ppir.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/ppir.snap @@ -645,18 +645,22 @@ function ai.internal._wire_blocks(content: root.content.Block[], keep_reasoning: class ai.mcp.McpConnection { server_name: string p: baml.sys.Process + out: baml.sys.ReadPipeLines + timeout_ms: int next_id: int } class ai.mcp.McpConnection$stream { server_name: string | null p: baml.sys.Process$stream | null + out: baml.sys.ReadPipeLines$stream | null + timeout_ms: int | null next_id: int | null } function ai.mcp._request(self: ?, method: string, params: map) -> baml.json.json [expr] { - { let id = self.next_id; self.next_id Add= 1; self._send(rpc_line(id, method, params)); while true { let next = self.p.stdout.next() catch_all (e) { _ => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } }; match (next) { line: string => { let trimmed = line.trim() } if (trimmed.length() Gt 0) { let msg = baml.json.parse(trimmed) catch_all (e) { _ => null } } if (baml.json.path_or(msg, ".id", Neg 1) Eq id) { let err = baml.json.path_or(msg, ".error", null); if (err Ne null) { throw root.errors.InvalidRequest { provider: `...`, status_code: baml.json.path_or(err, ".code", null), detail: baml.json.path_or(err, ".message", "MCP error"), raw_body: baml.json.stringify(err) } }; return baml.json.path_or(msg, ".result", null) }, _ => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } } }; throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } + { let id = self.next_id; self.next_id Add= 1; self._send(rpc_line(id, method, params)); while true { let next = baml.future.with_timeout(baml.time.Duration.from_milliseconds(self.timeout_ms), () -> { { } self.out.next() }) catch_all (e) { _ => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } }; match (next) { line: string => { let trimmed = line.trim() } if (trimmed.length() Gt 0) { let msg = baml.json.parse(trimmed) catch_all (e) { _ => null } } if (baml.json.path_or(msg, ".id", Neg 1) Eq id) { let err = baml.json.path_or(msg, ".error", null); if (err Ne null) { throw root.errors.InvalidRequest { provider: `...`, status_code: baml.json.path_or(err, ".code", null), detail: baml.json.path_or(err, ".message", "MCP error"), raw_body: baml.json.stringify(err) } }; return baml.json.path_or(msg, ".result", null) }, _ => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } } }; throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } } function ai.mcp._send(self: ?, line: string) -> void [expr] { - { let framed = [line, "\n"].join(""); let _ = self.p.write_stdin(framed) catch_all (e) { _ => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } } } + { let framed = [line, "\n"].join(""); let _ = self.p.stdin.write(framed) catch_all (e) { _ => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } } } } function ai.mcp.call_tool(self: ?, name: string, args: map) -> string [expr] { { let params: map = map { "name": name, "arguments": args }; let result = self._request("tools/call", params); let texts: string[] = []; for item in baml.json.path_or(result, ".content", []) { } if (baml.json.path_or(item, ".type", "") Eq "text") { texts.push(baml.json.path_or(item, ".text", "")) }; let joined = texts.join("\n"); if (baml.json.path_or(result, ".isError", false)) { throw baml.errors.InvalidArgument { message: `...` } } } joined @@ -665,7 +669,7 @@ function ai.mcp.close(self: ?) -> void [expr] { { self.p.close() } } function ai.mcp.connect(name: string, command: string, args: string[], timeout_ms: int = 30000) -> ai.mcp.McpConnection [expr] { - { let p = baml.sys.start_process(command, args, baml.sys.ProcessOptions { cwd: null, env: null, timeout_ms: timeout_ms, stdin: null, keep_stdin_open: true }) catch_all (e) { _ => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } }; let conn = ai.mcp.McpConnection { server_name: name, p: p, next_id: 1 }; let init: map = map { "protocolVersion": "2025-06-18", "capabilities": map { }, "clientInfo": map { "name": "baml-mcp", "version": "0.1" } }; let _ = conn._request("initialize", init); conn._send(rpc_line(null, "notifications/ini...", null)) } conn + { let p = baml.sys.start_process(command, args, baml.sys.ProcessOptions { cwd: null, env: null, timeout_ms: timeout_ms, stdin: null }) catch_all (e) { _ => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } }; let conn = ai.mcp.McpConnection { server_name: name, p: p, out: p.stdout.lines(), timeout_ms: timeout_ms, next_id: 1 }; let init: map = map { "protocolVersion": "2025-06-18", "capabilities": map { }, "clientInfo": map { "name": "baml-mcp", "version": "0.1" } }; let _ = conn._request("initialize", init); conn._send(rpc_line(null, "notifications/ini...", null)) } conn } function ai.mcp.rpc_line(id: int?, method: string, params: map?) -> string [expr] { { let m: map = map { "jsonrpc": "2.0", "method": method }; if let i: int = id { let _ = m.set("id", i) }; if let p: map = params { let _ = m.set("params", p) } } baml.json.stringify(baml.json.to_json(m)) @@ -674,6 +678,9 @@ function ai.mcp.tools(self: ?) -> root.tools.Tool[] [expr] { { let result = self._request("tools/list", map { }); let out: root.tools.Tool[] = []; for t in baml.json.path_or(result, ".tools", []) { let tool_name = baml.json.path_or(t, ".name", ""); let schema = baml.json.from_json(baml.json.path_or(t, ".inputSchema", map { })) catch_all (e) { _ => { let empty: map = map { } } empty }; out.push(root.tools.raw_tool(tool_name, baml.json.path_or(t, ".description", ""), schema, root.internal._proxy(self, tool_name))) } } out } +--- captures --- +lambda () in ?: captures [self] + --- /ai/ns_stream/stream.baml --- class ai.stream.Done { } diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/baml/bytecode.snap b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/baml/bytecode.snap index d1c3f08cf3d..47bbd33d378 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/baml/bytecode.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/baml/bytecode.snap @@ -1223,6 +1223,9 @@ function baml.Uint8Array.from_hex(hex: string) -> uint8array { function baml.Uint8Array.includes(self: baml.Uint8Array, item: int) -> bool { } +function baml.Uint8Array.index_of(self: baml.Uint8Array, item: int) -> int | null { +} + function baml.Uint8Array.length(self: baml.Uint8Array) -> int { } @@ -1405,85 +1408,64 @@ function baml.csv.CsvReader._poll(self: baml.csv.CsvReader) -> baml.csv.CsvRecor function baml.csv.CsvReader._poll_headers(self: baml.csv.CsvReader) -> baml.csv.CsvHeaders | baml.csv.CsvNeedData { } -function baml.csv.CsvReader._read_chunk(self: baml.csv.CsvReader) -> null { +function baml.csv.CsvReader._read_chunk(self: baml.csv.CsvReader) -> void { load_var self load_field ._file - store_var_load_var _2 load_const null cmp_op == pop_jump_if_false L0 - jump L9 + jump L3 L0: - load_var _2 + load_var self + load_field ._file load_const 65536 - sys_op baml.fs.File.read_bytes + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 store_var chunk - jump L5 - load_var e - is_type baml.errors.InvalidArgument - pop_jump_if_false L1 jump L4 - - L1: load_var e is_type baml.errors.Io - pop_jump_if_false L2 - jump L3 + pop_jump_if_false L1 + jump L2 - L2: + L1: load_var e rethrow - L3: + L2: load_var self call baml.csv.CsvReader._mark_exhausted pop 1 load_var e rethrow - L4: - load_const baml.csv.CsvErrorKind.Closed - alloc_variant baml.csv.CsvErrorKind - load_const "underlying file handle is closed: " - load_var e - load_field .0 - bin_op + - call baml.csv._error - throw + L3: + load_const null + store_var chunk - L5: + L4: load_var chunk - container_len - store_var _17 - load_var _17 - load_const 0 - cmp_int_op == - pop_jump_if_false L6 - jump L7 + load_const null + cmp_op == + pop_jump_if_false L5 + jump L6 - L6: - load_var2 1 3 + L5: + load_var2 1 2 call baml.csv.CsvReader._feed pop 1 - jump L8 - - L7: - load_var self - call baml.csv.CsvReader._feed_eof - pop 1 - - L8: load_const null - jump L10 + jump L7 - L9: + L6: load_var self call baml.csv.CsvReader._feed_eof pop 1 load_const null - L10: + L7: return } @@ -1780,43 +1762,28 @@ function baml.csv.CsvWriter._emit(self: baml.csv.CsvWriter, out: string) -> null load_const null cmp_op == pop_jump_if_false L0 - jump L4 + jump L2 L0: load_var out call baml.String.length load_const 0 cmp_int_op > - pop_jump_if_false L3 + pop_jump_if_false L1 load_var2 3 2 - sys_op baml.fs.File.write + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 pop 1 - jump L3 - load_var _ - is_type baml.errors.InvalidArgument - pop_jump_if_false L1 - jump L2 L1: - load_var _ - rethrow - - L2: - load_const "underlying file handle is closed: " - load_var _ - load_field .0 - bin_op + - init_instance baml.errors.Io .message - throw - - L3: load_const null - jump L5 + jump L3 - L4: + L2: load_const null - L5: + L3: return } @@ -2823,27 +2790,48 @@ function baml.errors.UnknownError.with_message(data: unknown, message: string) - } function baml.fs.File.bytes(self: baml.fs.File) -> uint8array { + load_var self + load_type baml.io.Read + load_const "bytes" + virtual_call nargs=1 ntypeargs=0 + store_var _0 + load_var _0 + return } function baml.fs.File.close(self: baml.fs.File) -> null { } -function baml.fs.File.read(self: baml.fs.File, n: int) -> string { +function baml.fs.File.root.io.Read.read(self: baml.fs.File, limit: int) -> uint8array | null { } -function baml.fs.File.read_bytes(self: baml.fs.File, n: int) -> uint8array { +function baml.fs.File.root.io.Write.flush(self: baml.fs.File) -> void { } -function baml.fs.File.seek_from(self: baml.fs.File, whence: "start" | "current" | "end", offset: int) -> int { +function baml.fs.File.root.io.Write.write_some(self: baml.fs.File, data: uint8array) -> int { } -function baml.fs.File.text(self: baml.fs.File) -> string { +function baml.fs.File.seek_from(self: baml.fs.File, whence: "start" | "current" | "end", offset: int) -> int { } -function baml.fs.File.write(self: baml.fs.File, data: string) -> int { +function baml.fs.File.text(self: baml.fs.File) -> string { + load_var self + load_type baml.io.Read + load_const "text" + virtual_call nargs=1 ntypeargs=0 + store_var _0 + load_var _0 + return } -function baml.fs.File.write_bytes(self: baml.fs.File, data: uint8array) -> int { +function baml.fs.File.write(self: baml.fs.File, data: string | uint8array) -> int { + load_var2 1 2 + load_type baml.io.Write + load_const "write" + virtual_call nargs=2 ntypeargs=0 + store_var _0 + load_var _0 + return } function baml.fs.chmod(path: string, mode: int) -> void { @@ -2975,6 +2963,133 @@ function baml.future.race(futures: baml.future.Future<#0, #1>[]) -> baml.future. return } +function baml.future.with_timeout(limit: baml.time.Duration, body: () -> #0 throws #1) -> #0 { + load_var ?1 + make_cell + store_var ?1 + load_var ?2 + make_cell + store_var ?2 + load_var ?4 + make_cell + store_var ?4 + call baml.spawn.CancelToken.new + store_deref ?4 + load_type #0 + load_type #1 + load_var body + make_closure ., captures=1, ntypeargs=2 + store_var _5 + load_type #0 + load_type #1 + load_const + load_deref ?4 + load_const + call baml.spawn.options + store_var _7 + load_var _5 + load_const null + load_const null + load_const null + load_const false + init_instance baml.spawn.SpawnParams .body, .name, .group, .cancel, .detach + load_var _7 + call_indirect + store_var _11 + load_var _5 + load_const null + load_var _11 + load_type #0 + load_type #1 + spawn + store_var _12 + load_type #0 + load_type #1 + load_var2 1 4 + make_closure ., captures=2, ntypeargs=2 + load_const null + load_const null + load_type int + load_type baml.errors.Io + spawn + store_var _15 + load_var _15 + store_var deadline + load_var _12 + await + store_var _0 + jump L8 + load_var e + is_type baml.panics.Cancelled + pop_jump_if_false L0 + jump L1 + + L0: + load_var e + rethrow + + L1: + load_deref ?4 + call baml.spawn.CancelToken.is_cancelled + unary_op ! + pop_jump_if_false L2 + jump L7 + + L2: + load_deref ?1 + call baml.time.Duration.to_milliseconds + store_var limit_ms + load_var limit_ms + call baml.Bigint.to_int + store_var reported + jump L5 + load_var conversion + is_type baml.errors.InvalidArgument + pop_jump_if_false L3 + jump L4 + + L3: + load_var conversion + rethrow + + L4: + load_const null + store_var reported + + L5: + load_type bigint + load_var limit_ms + call baml.String.from + store_var _30 + jump L6 + load_var deadline + call baml.future.Future.cancel + pop 1 + load_var _16 + rethrow + + L6: + load_const "operation timed out after " + load_var _30 + bin_op + + load_const "ms" + bin_op + + load_var reported + init_instance baml.errors.Timeout .message, .duration_ms + throw + + L7: + load_var e + rethrow + + L8: + load_var deadline + call baml.future.Future.cancel + pop 1 + load_var _0 + return +} + function baml.glob.Glob.matches(self: baml.glob.Glob, path: string) -> bool { } @@ -3224,6 +3339,146 @@ function baml.id.new() -> string { function baml.id.set(id: string) -> string { } +function baml.io.Read.bytes(self: baml.io.Read) -> uint8array { + load_const b"" + call baml.deep_copy + store_var result + + L0: + load_var self + load_const 65536 + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 + store_var _3 + load_var _3 + store_var _4 + load_var _4 + narrow_bind uint8array, slot=4 + pop_jump_if_false L1 + jump L2 + + L1: + load_var result + return + + L2: + load_var2 2 4 + call baml.Uint8Array.concat + store_var result + jump L0 +} + +function baml.io.Read.text(self: baml.io.Read) -> string { + load_var self + load_type baml.io.Read + load_const "bytes" + virtual_call nargs=1 ntypeargs=0 + store_var data + load_var data + call baml.io._decode_utf8 + return +} + +function baml.io.Write.write(self: baml.io.Write, data: string | uint8array) -> int { + load_var data + store_var _4 + load_var _4 + narrow_bind string, slot=4 + pop_jump_if_false L0 + jump L1 + + L0: + load_var data + store_var remaining + jump L2 + + L1: + load_var _4 + call baml.String.to_utf8 + store_var remaining + + L2: + load_var remaining + container_len + store_var total + + L3: + load_var remaining + container_len + store_var _9 + load_var _9 + load_const 0 + cmp_int_op > + pop_jump_if_false L4 + jump L5 + + L4: + load_var total + return + + L5: + load_var2 1 3 + load_type baml.io.Write + load_const "write_some" + virtual_call nargs=2 ntypeargs=0 + store_var written + load_var written + load_const 0 + cmp_int_op <= + jump_if_false L6 + jump L7 + + L6: + pop 1 + load_var remaining + container_len + store_var _16 + load_var2 7 8 + cmp_int_op > + + L7: + pop_jump_if_false L8 + jump L9 + + L8: + load_var remaining + container_len + store_var _19 + load_var2 3 7 + load_var _19 + call baml.Uint8Array.slice + store_var remaining + jump L3 + + L9: + load_const "writer made invalid progress" + init_instance baml.errors.Io .message + throw +} + +function baml.io._decode_utf8(data: uint8array) -> string { + load_var data + call baml.String.from_utf8 + jump L2 + load_var e + is_type baml.errors.InvalidArgument + pop_jump_if_false L0 + jump L1 + + L0: + load_var e + rethrow + + L1: + load_const "invalid utf-8" + init_instance baml.errors.ParseError .message + throw + + L2: + return +} + function baml.io.eprint(s: string) -> null { } @@ -5159,12 +5414,6 @@ function baml.net.TcpListener.close(self: baml.net.TcpListener) -> null { function baml.net.TcpStream._connect(addr: string, timeout_nanos: bigint) -> baml.net.TcpStream { } -function baml.net.TcpStream._read(self: baml.net.TcpStream, timeout_nanos: bigint) -> uint8array { -} - -function baml.net.TcpStream._write(self: baml.net.TcpStream, data: uint8array, timeout_nanos: bigint) -> null { -} - function baml.net.TcpStream.close(self: baml.net.TcpStream) -> null { } @@ -5185,41 +5434,17 @@ function baml.net.TcpStream.connect(addr: string, timeout: baml.time.Duration | return } -function baml.net.TcpStream.read(self: baml.net.TcpStream, timeout: baml.time.Duration | null) -> uint8array { - load_var timeout - load_const - cmp_op == - pop_jump_if_false L0 - load_const null - store_var timeout - - L0: - load_var timeout - call baml.net._timeout_nanos - store_var _4 - load_var2 1 3 - sys_op baml.net.TcpStream._read - return +function baml.net.TcpStream.root.io.Read.read(self: baml.net.TcpStream, limit: int) -> uint8array | null { } -function baml.net.TcpStream.write(self: baml.net.TcpStream, data: uint8array, timeout: baml.time.Duration | null) -> null { - load_var timeout - load_const - cmp_op == - pop_jump_if_false L0 +function baml.net.TcpStream.root.io.Write.flush(self: baml.net.TcpStream) -> void { load_const null - store_var timeout - - L0: - load_var timeout - call baml.net._timeout_nanos - store_var _5 - load_var2 1 2 - load_var _5 - sys_op baml.net.TcpStream._write return } +function baml.net.TcpStream.root.io.Write.write_some(self: baml.net.TcpStream, data: uint8array) -> int { +} + function baml.net.UdpSocket._recv_from(self: baml.net.UdpSocket, timeout_nanos: bigint) -> baml.net.Datagram { } @@ -5902,18 +6127,12 @@ function baml.spawn.options(group: baml.spawn.TaskGroup | null, cancel: baml.spa function baml.sys.Process.close(self: baml.sys.Process) -> null { } -function baml.sys.Process.close_stdin(self: baml.sys.Process) -> null { -} - -function baml.sys.Process.kill(self: baml.sys.Process) -> null { +function baml.sys.Process.kill(self: baml.sys.Process) -> void { } function baml.sys.Process.wait(self: baml.sys.Process) -> baml.sys.ProcessExit { } -function baml.sys.Process.write_stdin(self: baml.sys.Process, data: string) -> null { -} - function baml.sys.ProcessExit.ok(self: baml.sys.ProcessExit) -> bool { load_var self load_field .exit_code @@ -5922,35 +6141,133 @@ function baml.sys.ProcessExit.ok(self: baml.sys.ProcessExit) -> bool { return } -function baml.sys.ProcessLineStream._next(self: baml.sys.ProcessLineStream) -> string | null { +function baml.sys.ReadPipe.close(self: baml.sys.ReadPipe) -> void { } -function baml.sys.ProcessLineStream.close(self: baml.sys.ProcessLineStream) -> null { +function baml.sys.ReadPipe.lines(self: baml.sys.ReadPipe) -> baml.sys.ReadPipeLines { + load_var self + load_const b"" + call baml.deep_copy + init_instance baml.sys.ReadPipeLines .pipe, ._pending + return } -function baml.sys.ProcessLineStream.root.iter.Iterable.iter(self: baml.sys.ProcessLineStream) -> baml.iter.Iterator { +function baml.sys.ReadPipe.root.io.Read.read(self: baml.sys.ReadPipe, limit: int) -> uint8array | null { +} + +function baml.sys.ReadPipeLines.root.iter.Iterable.iter(self: baml.sys.ReadPipeLines) -> baml.iter.Iterator { load_var self return } -function baml.sys.ProcessLineStream.root.iter.Iterator.next(self: baml.sys.ProcessLineStream) -> string | baml.iter.Done { - load_var self - sys_op baml.sys.ProcessLineStream._next - store_var _3 - load_var _3 - narrow_bind string, slot=2 - pop_jump_if_false L0 - jump L1 +function baml.sys.ReadPipeLines.root.iter.Iterator.next(self: baml.sys.ReadPipeLines) -> string | baml.iter.Done { + alloc_instance baml.iter.Done + store_var line + load_const true + store_var framing L0: - alloc_instance baml.iter.Done + load_var framing + pop_jump_if_false L1 jump L2 L1: - load_var _3 + load_var line + return L2: - return + load_var self + load_field ._pending + load_const 10 + call baml.Uint8Array.index_of + store_var _7 + load_var _7 + narrow_bind int, slot=4 + pop_jump_if_false L3 + jump L7 + + L3: + load_var self + load_field .pipe + load_const 65536 + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 + store_var _20 + load_var _20 + store_var _22 + load_var _22 + narrow_bind uint8array, slot=12 + pop_jump_if_false L4 + jump L6 + + L4: + load_var self + load_field ._pending + container_len + store_var _28 + load_var _28 + load_const 0 + cmp_int_op > + pop_jump_if_false L5 + load_var self + load_field ._pending + call baml.sys._strip_carriage_return + call baml.Uint8Array.baml.ToString.to_string + store_var line + load_var self + load_const b"" + call baml.deep_copy + store_field ._pending + + L5: + load_const false + store_var framing + jump L0 + + L6: + load_var self + load_field ._pending + load_var _22 + call baml.Uint8Array.concat + store_var _26 + load_var2 1 13 + store_field ._pending + jump L0 + + L7: + load_var _7 + store_var index + load_var self + load_field ._pending + load_const 0 + load_var index + call baml.Uint8Array.slice + store_var bytes + load_var self + load_field ._pending + store_var _12 + load_var index + load_const 1 + add_int + store_var _13 + load_var self + load_field ._pending + container_len + store_var _15 + load_var2 7 8 + load_var _15 + call baml.Uint8Array.slice + store_var _17 + load_var2 1 10 + store_field ._pending + load_var bytes + call baml.sys._strip_carriage_return + call baml.Uint8Array.baml.ToString.to_string + store_var line + load_const false + store_var framing + jump L0 } function baml.sys.ShellOutput.ok(self: baml.sys.ShellOutput) -> bool { @@ -5961,6 +6278,52 @@ function baml.sys.ShellOutput.ok(self: baml.sys.ShellOutput) -> bool { return } +function baml.sys.WritePipe.close(self: baml.sys.WritePipe) -> void { +} + +function baml.sys.WritePipe.root.io.Write.flush(self: baml.sys.WritePipe) -> void { +} + +function baml.sys.WritePipe.root.io.Write.write_some(self: baml.sys.WritePipe, data: uint8array) -> int { +} + +function baml.sys._strip_carriage_return(line: uint8array) -> uint8array { + load_var line + container_len + store_var _4 + load_var2 1 3 + load_const 1 + sub_int + call baml.Uint8Array.at + store_var _2 + load_var _2 + is_type type tag 0 + pop_jump_if_false L0 + load_var _2 + load_const 13 + cmp_int_op == + pop_jump_if_false L0 + jump L1 + + L0: + load_var line + jump L2 + + L1: + load_var line + container_len + store_var _6 + load_var line + load_const 0 + load_var _6 + load_const 1 + sub_int + call baml.Uint8Array.slice + + L2: + return +} + function baml.sys.argv() -> string[] { } diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/baml/mir.snap b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/baml/mir.snap index 2c37fc04104..2174e9d1f69 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/baml/mir.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/baml/mir.snap @@ -1753,7 +1753,7 @@ fn baml.csv.CsvReader.root.iter.Iterator.next(self: baml.csv.CsvReader) -> baml. let _12: (baml.csv.CsvError) -> null throws never // cb let _13: (baml.csv.CsvError) -> null throws never let _14: baml.csv.CsvError - let _15: null + let _15: void bb0: { goto -> bb1; @@ -1833,7 +1833,7 @@ fn baml.csv.CsvReader.headers(self: baml.csv.CsvReader) -> null | string[] { let _2: baml.csv.CsvHeaders | baml.csv.CsvNeedData let _3: baml.csv.CsvHeaders | baml.csv.CsvNeedData let _4: baml.csv.CsvHeaders // h - let _5: null + let _5: void bb0: { goto -> bb1; @@ -1978,110 +1978,83 @@ fn baml.csv.CsvReader._mark_closed = builtin(vm) fn baml.csv.CsvReader._mark_exhausted = builtin(vm) -fn baml.csv.CsvReader._read_chunk(self: baml.csv.CsvReader) -> null { +fn baml.csv.CsvReader._read_chunk(self: baml.csv.CsvReader) -> void { // Locals: - let _0: null // _0 // return + let _0: void // _0 // return let _1: baml.csv.CsvReader // self // param - let _2: null | baml.fs.File - let _3: bool - let _4: null - let _5: baml.fs.File // f - let _6: uint8array // chunk - let _7: unknown // e - let _8: bool + let _2: null | uint8array // chunk + let _3: unknown // e + let _4: null | baml.fs.File + let _5: bool + let _6: null | baml.fs.File + let _7: bool + let _8: null let _9: bool - let _10: string - let _11: string // message - let _12: baml.csv.CsvError - let _13: string - let _14: string - let _15: null - let _16: bool - let _17: int - let _18: null - let _19: null - let _20: uint8array + let _10: null + let _11: uint8array // bytes + let _12: null + let _13: uint8array bb0: { - _2 = copy _1.1; - _3 = copy _2 == const null; - branch copy _3 -> [bb7, bb1]; + _4 = copy _1.1; + _5 = copy _4 == const null; + branch copy _5 -> [bb2, bb1]; } bb1: { - _5 = copy _2; - _6 = sys_op const fn baml.fs.File.read_bytes(copy _5, const 65536_i64) -> bb2 unwind bb10; + _6 = copy _1.1; + _2 = virtual_call read as baml.io.Read(copy _6, const 65536_i64) -> [bb3, unwind: bb9]; } bb2: { - _17 = len(_6); + _2 = const null; goto -> bb3; } bb3: { - _16 = copy _17 == const 0_i64; - branch copy _16 -> [bb5, bb4]; + _9 = copy _2 == const null; + branch copy _9 -> [bb6, bb4]; } bb4: { - _20 = copy _6; - _19 = call const fn baml.csv.CsvReader._feed(copy _1, copy _20) -> [bb6]; + _11 = copy _2; + _13 = copy _11; + _12 = call const fn baml.csv.CsvReader._feed(copy _1, copy _13) -> [bb5]; } bb5: { - _18 = call const fn baml.csv.CsvReader._feed_eof(copy _1) -> [bb6]; + _0 = const null; + goto -> bb8; } bb6: { - _0 = const null; - goto -> bb9; + _10 = call const fn baml.csv.CsvReader._feed_eof(copy _1) -> [bb7]; } bb7: { - _4 = call const fn baml.csv.CsvReader._feed_eof(copy _1) -> [bb8]; + _0 = const null; + goto -> bb8; } bb8: { - _0 = const null; - goto -> bb9; + return; } bb9: { - return; + _7 = is_type(copy _3, baml.errors.Io); + branch copy _7 -> [bb11, bb10]; } bb10: { - _8 = is_type(copy _7, baml.errors.InvalidArgument); - branch copy _8 -> [bb15, bb11]; + rethrow copy _3; } bb11: { - _9 = is_type(copy _7, baml.errors.Io); - branch copy _9 -> [bb13, bb12]; + _8 = call const fn baml.csv.CsvReader._mark_exhausted(copy _1) -> [bb12]; } bb12: { - rethrow copy _7; - } - - bb13: { - _15 = call const fn baml.csv.CsvReader._mark_exhausted(copy _1) -> [bb14]; - } - - bb14: { - rethrow copy _7; - } - - bb15: { - _10 = copy _7.0; - _11 = copy _10; - _14 = copy _11; - _13 = const "underlying file handle is closed: " + copy _14; - _12 = call const fn baml.csv._error(const baml.csv.CsvErrorKind.Closed, copy _13) -> [bb16]; - } - - bb16: { - throw copy _12; + rethrow copy _3; } } @@ -2378,13 +2351,6 @@ fn baml.csv.CsvWriter._emit(self: baml.csv.CsvWriter, out: string) -> null { let _6: bool let _7: int let _8: int - let _9: unknown // _ - let _10: bool - let _11: string - let _12: string // message - let _13: baml.errors.Io - let _14: string - let _15: string bb0: { _3 = copy _1.1; @@ -2403,7 +2369,7 @@ fn baml.csv.CsvWriter._emit(self: baml.csv.CsvWriter, out: string) -> null { } bb3: { - _8 = sys_op const fn baml.fs.File.write(copy _5, copy _2) -> bb4 unwind bb7; + _8 = virtual_call write as baml.io.Write(copy _5, copy _2) -> [bb4]; } bb4: { @@ -2419,24 +2385,6 @@ fn baml.csv.CsvWriter._emit(self: baml.csv.CsvWriter, out: string) -> null { bb6: { return; } - - bb7: { - _10 = is_type(copy _9, baml.errors.InvalidArgument); - branch copy _10 -> [bb9, bb8]; - } - - bb8: { - rethrow copy _9; - } - - bb9: { - _11 = copy _9.0; - _12 = copy _11; - _15 = copy _12; - _14 = const "underlying file handle is closed: " + copy _15; - _13 = baml.errors.Io { copy _14 }; - throw copy _13; - } } fn baml.csv.reader(source: string | uint8array | baml.fs.File, options: null | baml.csv.ReaderOptions) -> baml.csv.CsvReader { @@ -3808,21 +3756,58 @@ fn baml.errors.UnknownError.baml.ToString.to_string(self: baml.errors.UnknownErr } } -fn baml.fs.File.text = builtin(io) +fn baml.fs.File.text(self: baml.fs.File) -> string { + // Locals: + let _0: string // _0 // return + let _1: baml.fs.File // self // param -fn baml.fs.File.bytes = builtin(io) + bb0: { + _0 = virtual_call text as baml.io.Read(copy _1) -> [bb1]; + } -fn baml.fs.File.read = builtin(io) + bb1: { + return; + } +} -fn baml.fs.File.read_bytes = builtin(io) +fn baml.fs.File.bytes(self: baml.fs.File) -> uint8array { + // Locals: + let _0: uint8array // _0 // return + let _1: baml.fs.File // self // param + + bb0: { + _0 = virtual_call bytes as baml.io.Read(copy _1) -> [bb1]; + } + + bb1: { + return; + } +} + +fn baml.fs.File.root.io.Read.read = builtin(io) fn baml.fs.File.close = builtin(io) fn baml.fs.File.seek_from = builtin(io) -fn baml.fs.File.write = builtin(io) +fn baml.fs.File.root.io.Write.write_some = builtin(io) + +fn baml.fs.File.root.io.Write.flush = builtin(io) -fn baml.fs.File.write_bytes = builtin(io) +fn baml.fs.File.write(self: baml.fs.File, data: string | uint8array) -> int { + // Locals: + let _0: int // _0 // return + let _1: baml.fs.File // self // param + let _2: string | uint8array // data // param + + bb0: { + _0 = virtual_call write as baml.io.Write(copy _1, copy _2) -> [bb1]; + } + + bb1: { + return; + } +} fn baml.fs.open = builtin(io) @@ -4449,6 +4434,194 @@ fn ., 1)>(g: baml.future.Future) -> bool { } } +fn baml.future.with_timeout(limit: baml.time.Duration, body: () -> T throws E) -> T { + // Locals: + let _0: T // _0 // return + let _1: baml.time.Duration // limit // param [captured] + let _2: () -> T throws E // body // param [captured] + let _3: baml.spawn.CancelToken // token [captured] + let _4: baml.future.Future // work + let _5: null + let _6: null + let _7: (baml.spawn.SpawnParams) -> baml.spawn.SpawnParams throws never + let _8: baml.spawn.CancelToken + let _9: reflect.Type + let _10: reflect.Type + let _11: null + let _12: baml.future.Future + let _13: baml.future.Future // deadline + let _14: null + let _15: baml.future.Future + let _16: unknown + let _17: unknown + let _18: unknown // e + let _19: null + let _20: bool + let _21: bool + let _22: bool + let _23: bigint // limit_ms + let _24: int | null // reported + let _25: unknown // conversion + let _26: bool + let _27: baml.errors.Timeout + let _28: string + let _29: string + let _30: string + let _31: bigint + let _32: reflect.Type + let _33: int | null + let _34: void + let _35: void + + bb0: { + _3 = call const fn baml.spawn.CancelToken.new() -> [bb1]; + } + + bb1: { + _5 = make_closure lambda[0]<2 type_args>(copy _2); + _6 = baml.spawn.SpawnParams { copy _5, const null, const null, const null, const false }; + _8 = copy _3; + _9 = load_type(#0); + _10 = load_type(#1); + _7 = call const fn baml.spawn.options(const , copy _8, const ) -> [bb2]; + } + + bb2: { + _11 = call copy _7(copy _6) -> [bb3]; + } + + bb3: { + _12 = spawn<#0, #1> copy _5 name=const null config=copy _11 -> bb4; + } + + bb4: { + _4 = copy _12; + _14 = make_closure lambda[1]<2 type_args>(copy _1, copy _3); + _15 = spawn copy _14 name=const null -> bb5; + } + + bb5: { + _13 = copy _15; + goto -> bb6; + } + + bb6: { + _19 = copy _4; + _0 = await _19 -> [bb7, unwind: bb9]; + } + + bb7: { + _34 = call const fn baml.future.Future.cancel(copy _13) -> [bb8]; + } + + bb8: { + return; + } + + bb9: { + _20 = is_type(copy _18, baml.panics.Cancelled); + branch copy _20 -> [bb11, bb10]; + } + + bb10: { + rethrow copy _18; + } + + bb11: { + _22 = call const fn baml.spawn.CancelToken.is_cancelled(copy _3) -> [bb12, unwind: bb20]; + } + + bb12: { + _21 = !copy _22; + branch copy _21 -> [bb15, bb13]; + } + + bb13: { + _23 = call const fn baml.time.Duration.to_milliseconds(copy _1) -> [bb14, unwind: bb20]; + } + + bb14: { + _24 = call const fn baml.Bigint.to_int(copy _23) -> [bb19, unwind: bb16]; + } + + bb15: { + rethrow copy _18; + } + + bb16: { + _26 = is_type(copy _25, baml.errors.InvalidArgument); + branch copy _26 -> [bb18, bb17]; + } + + bb17: { + rethrow copy _25; + } + + bb18: { + _24 = const null; + goto -> bb19; + } + + bb19: { + _31 = copy _23; + _32 = load_type(bigint); + _30 = call const fn baml.String.from(copy _31) -> [bb22, unwind: bb20]; + } + + bb20: { + _35 = call const fn baml.future.Future.cancel(copy _13) -> [bb21]; + } + + bb21: { + rethrow copy _16; + } + + bb22: { + _29 = const "operation timed out after " + copy _30; + _28 = copy _29 + const "ms"; + _33 = copy _24; + _27 = baml.errors.Timeout { copy _28, copy _33 }; + throw copy _27; + } +} + +// lambda[0] +fn .() -> T { + // Locals: + let _0: T // _0 // return + let _1: () -> T throws E + + bb0: { + _1 = copy capture[0]; + _0 = call copy _1() -> [bb1]; + } + + bb1: { + return; + } +} + +// lambda[1] +fn .() -> int { + // Locals: + let _0: int // _0 // return + let _1: null + let _2: baml.time.Duration + + bb0: { + _2 = copy capture[0]; + _1 = sys_op const fn baml.sys.sleep(copy _2) -> bb1; + } + + bb1: { + _0 = call const fn baml.spawn.CancelToken.cancel(copy capture[1]) -> [bb2]; + } + + bb2: { + return; + } +} + fn baml.glob.Glob.scan = builtin(io) fn baml.glob.Glob.matches = builtin(io) @@ -4748,54 +4921,284 @@ fn baml.http.TlsConfig.new(cert_pem: uint8array, key_pem: uint8array, allow_tls1 let _7: bigint bb0: { - _5 = copy _3 == const ; - branch copy _5 -> [bb1, bb2]; + _5 = copy _3 == const ; + branch copy _5 -> [bb1, bb2]; + } + + bb1: { + _3 = const true; + goto -> bb2; + } + + bb2: { + _6 = copy _4 == const ; + branch copy _6 -> [bb3, bb4]; + } + + bb3: { + _4 = call const fn baml.time.Duration.from_seconds(const 10n) -> [bb4]; + } + + bb4: { + _7 = call const fn baml.time.Duration.to_nanoseconds(copy _4) -> [bb5]; + } + + bb5: { + _0 = sys_op const fn baml.http.TlsConfig._new(copy _1, copy _2, copy _3, copy _7) -> bb6; + } + + bb6: { + return; + } +} + +fn baml.http.TlsConfig._new = builtin(io) + +fn baml.id.current = builtin(vm) + +fn baml.id.new = builtin(vm) + +fn baml.id.set = builtin(vm) + +fn baml.io.input = builtin(io) + +fn baml.io.print = builtin(io) + +fn baml.io.println = builtin(io) + +fn baml.io.eprint = builtin(io) + +fn baml.io.eprintln = builtin(io) + +fn baml.io.Read.read(_1: void, _2: void) -> void { + // Locals: + let _0: void // return + let _1: void // param + let _2: void // param + + bb0: { + unreachable; + } +} + +fn baml.io.Read.bytes(self: baml.io.Read) -> uint8array { + // Locals: + let _0: uint8array // _0 // return + let _1: baml.io.Read // self // param + let _2: uint8array // result + let _3: null | uint8array + let _4: null | uint8array + let _5: uint8array // chunk + let _6: uint8array + + bb0: { + _2 = b"<0 bytes>"; + goto -> bb1; + } + + bb1: { + _3 = virtual_call read as baml.io.Read(copy _1, const 65536_i64) -> [bb2]; + } + + bb2: { + _4 = copy _3; + _4 = narrow_bind copy _4 as Uint8Array { attr: TyAttr { sap_parse_without_null: Unset, sap_pending_never: Unset, sap_in_progress_never: Unset } } -> [bb5, bb3]; + } + + bb3: { + _0 = copy _2; + goto -> bb4; + } + + bb4: { + return; + } + + bb5: { + fresh_cell(_5); + _5 = copy _4; + _6 = copy _5; + _2 = call const fn baml.Uint8Array.concat(copy _2, copy _6) -> [bb1]; + } +} + +fn baml.io.Read.text(self: baml.io.Read) -> string { + // Locals: + let _0: string // _0 // return + let _1: baml.io.Read // self // param + let _2: uint8array // data + let _3: uint8array + + bb0: { + _2 = virtual_call bytes as baml.io.Read(copy _1) -> [bb1]; + } + + bb1: { + _3 = copy _2; + _0 = call const fn baml.io._decode_utf8(copy _3) -> [bb2]; + } + + bb2: { + return; + } +} + +fn baml.io._decode_utf8(data: uint8array) -> string { + // Locals: + let _0: string // _0 // return + let _1: uint8array // data // param + let _2: unknown // e + let _3: bool + let _4: baml.errors.ParseError + + bb0: { + _0 = call const fn baml.String.from_utf8(copy _1) -> [bb1, unwind: bb2]; + } + + bb1: { + return; + } + + bb2: { + _3 = is_type(copy _2, baml.errors.InvalidArgument); + branch copy _3 -> [bb4, bb3]; + } + + bb3: { + rethrow copy _2; + } + + bb4: { + _4 = baml.errors.ParseError { const "invalid utf-8" }; + throw copy _4; + } +} + +fn baml.io.Write.write_some(_1: void, _2: void) -> void { + // Locals: + let _0: void // return + let _1: void // param + let _2: void // param + + bb0: { + unreachable; + } +} + +fn baml.io.Write.write(self: baml.io.Write, data: string | uint8array) -> int { + // Locals: + let _0: int // _0 // return + let _1: baml.io.Write // self // param + let _2: string | uint8array // data // param + let _3: uint8array // remaining + let _4: string | uint8array + let _5: string // text + let _6: uint8array // bytes + let _7: int // total + let _8: bool + let _9: int + let _10: int // written + let _11: uint8array + let _12: bool + let _13: bool + let _14: int + let _15: int + let _16: int + let _17: baml.errors.Io + let _18: int + let _19: int + + bb0: { + _4 = copy _2; + _4 = narrow_bind copy _4 as String { attr: TyAttr { sap_parse_without_null: Unset, sap_pending_never: Unset, sap_in_progress_never: Unset } } -> [bb2, bb1]; } bb1: { - _3 = const true; - goto -> bb2; + _6 = copy _2; + _3 = copy _6; + goto -> bb3; } bb2: { - _6 = copy _4 == const ; - branch copy _6 -> [bb3, bb4]; + _5 = copy _4; + _3 = call const fn baml.String.to_utf8(copy _5) -> [bb3]; } bb3: { - _4 = call const fn baml.time.Duration.from_seconds(const 10n) -> [bb4]; + _7 = len(_3); + goto -> bb4; } bb4: { - _7 = call const fn baml.time.Duration.to_nanoseconds(copy _4) -> [bb5]; + _9 = len(_3); + goto -> bb5; } bb5: { - _0 = sys_op const fn baml.http.TlsConfig._new(copy _1, copy _2, copy _3, copy _7) -> bb6; + _8 = copy _9 > const 0_i64; + branch copy _8 -> [bb8, bb6]; } bb6: { + _0 = copy _7; + goto -> bb7; + } + + bb7: { return; } -} -fn baml.http.TlsConfig._new = builtin(io) + bb8: { + _11 = copy _3; + _10 = virtual_call write_some as baml.io.Write(copy _1, copy _11) -> [bb9]; + } -fn baml.id.current = builtin(vm) + bb9: { + _14 = copy _10; + _13 = copy _14 <= const 0_i64; + _12 = short_circuit(||) copy _13 -> [eval: bb10, join: bb12]; + } -fn baml.id.new = builtin(vm) + bb10: { + _15 = copy _10; + _16 = len(_3); + goto -> bb11; + } -fn baml.id.set = builtin(vm) + bb11: { + _12 = copy _15 > copy _16; + goto -> bb12; + } -fn baml.io.input = builtin(io) + bb12: { + branch copy _12 -> [bb15, bb13]; + } -fn baml.io.print = builtin(io) + bb13: { + _18 = copy _10; + _19 = len(_3); + goto -> bb14; + } -fn baml.io.println = builtin(io) + bb14: { + _3 = call const fn baml.Uint8Array.slice(copy _3, copy _18, copy _19) -> [bb4]; + } -fn baml.io.eprint = builtin(io) + bb15: { + _17 = baml.errors.Io { const "writer made invalid progress" }; + throw copy _17; + } +} -fn baml.io.eprintln = builtin(io) +fn baml.io.Write.flush(_1: void) -> void { + // Locals: + let _0: void // return + let _1: void // param + + bb0: { + unreachable; + } +} fn baml.iter.Iterable.iter(_1: void) -> void { // Locals: @@ -7572,73 +7975,25 @@ fn baml.net.TcpStream.connect(addr: string, timeout: null | baml.time.Duration) fn baml.net.TcpStream._connect = builtin(io) -fn baml.net.TcpStream.read(self: baml.net.TcpStream, timeout: null | baml.time.Duration) -> uint8array { - // Locals: - let _0: uint8array // _0 // return - let _1: baml.net.TcpStream // self // param - let _2: null | baml.time.Duration // timeout // param - let _3: bool - let _4: bigint - - bb0: { - _3 = copy _2 == const ; - branch copy _3 -> [bb1, bb2]; - } - - bb1: { - _2 = const null; - goto -> bb2; - } +fn baml.net.TcpStream.root.io.Read.read = builtin(io) - bb2: { - _4 = call const fn baml.net._timeout_nanos(copy _2) -> [bb3]; - } - - bb3: { - _0 = sys_op const fn baml.net.TcpStream._read(copy _1, copy _4) -> bb4; - } - - bb4: { - return; - } -} - -fn baml.net.TcpStream._read = builtin(io) +fn baml.net.TcpStream.root.io.Write.write_some = builtin(io) -fn baml.net.TcpStream.write(self: baml.net.TcpStream, data: uint8array, timeout: null | baml.time.Duration) -> null { +fn baml.net.TcpStream.root.io.Write.flush(self: baml.net.TcpStream) -> void { // Locals: - let _0: null // _0 // return + let _0: void // _0 // return let _1: baml.net.TcpStream // self // param - let _2: uint8array // data // param - let _3: null | baml.time.Duration // timeout // param - let _4: bool - let _5: bigint bb0: { - _4 = copy _3 == const ; - branch copy _4 -> [bb1, bb2]; + _0 = const null; + goto -> bb1; } bb1: { - _3 = const null; - goto -> bb2; - } - - bb2: { - _5 = call const fn baml.net._timeout_nanos(copy _3) -> [bb3]; - } - - bb3: { - _0 = sys_op const fn baml.net.TcpStream._write(copy _1, copy _2, copy _5) -> bb4; - } - - bb4: { return; } } -fn baml.net.TcpStream._write = builtin(io) - fn baml.net.TcpStream.close = builtin(io) fn baml.net.TcpListener.bind = builtin(io) @@ -8747,10 +9102,10 @@ fn baml.sys.ProcessExit.ok(self: baml.sys.ProcessExit) -> bool { } } -fn baml.sys.ProcessLineStream.root.iter.Iterable.iter(self: baml.sys.ProcessLineStream) -> baml.iter.Iterator { +fn baml.sys.ReadPipeLines.root.iter.Iterable.iter(self: baml.sys.ReadPipeLines) -> baml.iter.Iterator { // Locals: - let _0: baml.iter.Iterator // _0 // return - let _1: baml.sys.ProcessLineStream // self // param + let _0: baml.iter.Iterator // _0 // return + let _1: baml.sys.ReadPipeLines // self // param bb0: { _0 = copy _1; @@ -8762,46 +9117,212 @@ fn baml.sys.ProcessLineStream.root.iter.Iterable.iter(self: baml.sys.ProcessLine } } -fn baml.sys.ProcessLineStream.root.iter.Iterator.next(self: baml.sys.ProcessLineStream) -> string | baml.iter.Done { +fn baml.sys.ReadPipeLines.root.iter.Iterator.next(self: baml.sys.ReadPipeLines) -> string | baml.iter.Done { // Locals: let _0: string | baml.iter.Done // _0 // return - let _1: baml.sys.ProcessLineStream // self // param - let _2: string | null - let _3: string | null - let _4: string // line + let _1: baml.sys.ReadPipeLines // self // param + let _2: string | baml.iter.Done // line + let _3: bool // framing + let _4: bool + let _5: int | null + let _6: (baml.Uint8Array, int) -> int | null throws never + let _7: int | null + let _8: int // index + let _9: uint8array // bytes + let _10: (baml.Uint8Array, int, int) -> uint8array throws never + let _11: int + let _12: (baml.Uint8Array, int, int) -> uint8array throws never + let _13: int + let _14: int + let _15: int + let _16: (baml.Uint8Array) -> int throws never + let _17: uint8array + let _18: uint8array + let _19: uint8array + let _20: null | uint8array + let _21: baml.sys.ReadPipe + let _22: null | uint8array + let _23: uint8array // chunk + let _24: (baml.Uint8Array, uint8array) -> uint8array throws never + let _25: uint8array + let _26: uint8array + let _27: bool + let _28: int + let _29: (baml.Uint8Array) -> int throws never + let _30: uint8array + let _31: uint8array bb0: { - _2 = sys_op const fn baml.sys.ProcessLineStream._next(copy _1) -> bb1; + _2 = baml.iter.Done { }; + _3 = const true; + goto -> bb1; } bb1: { - _3 = copy _2; - _3 = narrow_bind copy _3 as String { attr: TyAttr { sap_parse_without_null: Unset, sap_pending_never: Unset, sap_in_progress_never: Unset } } -> [bb3, bb2]; + _4 = copy _3; + branch copy _4 -> [bb4, bb2]; } bb2: { - _0 = baml.iter.Done { }; - goto -> bb4; + _0 = copy _2; + goto -> bb3; } bb3: { - _4 = copy _3; - _0 = copy _4; - goto -> bb4; + return; } bb4: { - return; + _6 = copy _1.1; + _5 = call const fn baml.Uint8Array.index_of(copy _6, const 10_i64) -> [bb5]; + } + + bb5: { + _7 = copy _5; + _7 = narrow_bind copy _7 as Int { attr: TyAttr { sap_parse_without_null: Unset, sap_pending_never: Unset, sap_in_progress_never: Unset } } -> [bb16, bb6]; + } + + bb6: { + _21 = copy _1.0; + _20 = virtual_call read as baml.io.Read(copy _21, const 65536_i64) -> [bb7]; + } + + bb7: { + _22 = copy _20; + _22 = narrow_bind copy _22 as Uint8Array { attr: TyAttr { sap_parse_without_null: Unset, sap_pending_never: Unset, sap_in_progress_never: Unset } } -> [bb14, bb8]; + } + + bb8: { + _29 = copy _1.1; + _28 = len(_29); + goto -> bb9; + } + + bb9: { + _27 = copy _28 > const 0_i64; + branch copy _27 -> [bb10, bb13]; + } + + bb10: { + _31 = copy _1.1; + _30 = call const fn baml.sys._strip_carriage_return(copy _31) -> [bb11]; + } + + bb11: { + _2 = call const fn baml.Uint8Array.baml.ToString.to_string(copy _30) -> [bb12]; + } + + bb12: { + _1.1 = b"<0 bytes>"; + goto -> bb13; + } + + bb13: { + _3 = const false; + goto -> bb1; + } + + bb14: { + _23 = copy _22; + _24 = copy _1.1; + _25 = copy _23; + _26 = call const fn baml.Uint8Array.concat(copy _24, copy _25) -> [bb15]; + } + + bb15: { + _1.1 = copy _26; + goto -> bb1; + } + + bb16: { + _8 = copy _7; + _10 = copy _1.1; + _11 = copy _8; + _9 = call const fn baml.Uint8Array.slice(copy _10, const 0_i64, copy _11) -> [bb17]; + } + + bb17: { + _12 = copy _1.1; + _14 = copy _8; + _13 = copy _14 + const 1_i64; + _16 = copy _1.1; + _15 = len(_16); + goto -> bb18; + } + + bb18: { + _17 = call const fn baml.Uint8Array.slice(copy _12, copy _13, copy _15) -> [bb19]; + } + + bb19: { + _1.1 = copy _17; + goto -> bb20; + } + + bb20: { + _19 = copy _9; + _18 = call const fn baml.sys._strip_carriage_return(copy _19) -> [bb21]; + } + + bb21: { + _2 = call const fn baml.Uint8Array.baml.ToString.to_string(copy _18) -> [bb22]; + } + + bb22: { + _3 = const false; + goto -> bb1; } } -fn baml.sys.ProcessLineStream._next = builtin(io) +fn baml.sys._strip_carriage_return(line: uint8array) -> uint8array { + // Locals: + let _0: uint8array // _0 // return + let _1: uint8array // line // param + let _2: int | null + let _3: int + let _4: int + let _5: int + let _6: int + let _7: bool + + bb0: { + _4 = len(_1); + goto -> bb1; + } + + bb1: { + _3 = copy _4 - const 1_i64; + _2 = call const fn baml.Uint8Array.at(copy _1, copy _3) -> [bb2]; + } + + bb2: { + _7 = is_type_tag(copy _2, INT); + branch copy _7 -> [bb3, bb4]; + } + + bb3: { + switch copy _2 [13: bb5, otherwise: bb4]; + } + + bb4: { + _0 = copy _1; + goto -> bb7; + } -fn baml.sys.ProcessLineStream.close = builtin(io) + bb5: { + _6 = len(_1); + goto -> bb6; + } -fn baml.sys.Process.write_stdin = builtin(io) + bb6: { + _5 = copy _6 - const 1_i64; + _0 = call const fn baml.Uint8Array.slice(copy _1, const 0_i64, copy _5) -> [bb7]; + } -fn baml.sys.Process.close_stdin = builtin(io) + bb7: { + return; + } +} fn baml.sys.Process.wait = builtin(io) @@ -8809,6 +9330,33 @@ fn baml.sys.Process.kill = builtin(io) fn baml.sys.Process.close = builtin(io) +fn baml.sys.ReadPipe.close = builtin(io) + +fn baml.sys.ReadPipe.lines(self: baml.sys.ReadPipe) -> baml.sys.ReadPipeLines { + // Locals: + let _0: baml.sys.ReadPipeLines // _0 // return + let _1: baml.sys.ReadPipe // self // param + let _2: uint8array + + bb0: { + _2 = b"<0 bytes>"; + _0 = baml.sys.ReadPipeLines { copy _1, copy _2 }; + goto -> bb1; + } + + bb1: { + return; + } +} + +fn baml.sys.ReadPipe.root.io.Read.read = builtin(io) + +fn baml.sys.WritePipe.close = builtin(io) + +fn baml.sys.WritePipe.root.io.Write.write_some = builtin(io) + +fn baml.sys.WritePipe.root.io.Write.flush = builtin(io) + fn baml.sys.exec = builtin(io) fn baml.sys.start_process = builtin(io) @@ -12452,6 +13000,8 @@ fn baml.Uint8Array.concat = builtin(vm) fn baml.Uint8Array.includes = builtin(vm) +fn baml.Uint8Array.index_of = builtin(vm) + fn baml.Uint8Array.reverse = builtin(vm) fn baml.Uint8Array.slice = builtin(vm) diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/baml/ppir.snap b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/baml/ppir.snap index dbf411b95ea..5dfbd6480ed 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/baml/ppir.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/baml/ppir.snap @@ -490,7 +490,7 @@ function baml.csv._cell_by_index(rec: baml.csv.CsvRecord, index: int) -> T? [bu function baml.csv._cell_by_name(rec: baml.csv.CsvRecord, column: string) -> T? [builtin] function baml.csv._decode_record(rec: baml.csv.CsvRecord) -> T [builtin] function baml.csv._emit(self: ?, out: string) -> null [expr] { - { } match (self._file) { null => null, f: root.fs.File => { if (out.length() Gt 0) { f.write(out) catch (_) { root.errors.InvalidArgument { message: message } => { throw root.errors.Io { message: "underlying file h..." Add message } } } } } null } + { } match (self._file) { null => null, f: root.fs.File => { if (out.length() Gt 0) { f.write(out) } } null } } function baml.csv._encode_header(self: ?, names: string[]) -> string [builtin] function baml.csv._encode_record(self: ?, record: baml.csv.CsvValue[]) -> string [builtin] @@ -506,8 +506,8 @@ function baml.csv._mark_closed(self: ?) -> null [builtin] function baml.csv._mark_exhausted(self: ?) -> null [builtin] function baml.csv._poll(self: ?) -> baml.csv.CsvRecord | baml.csv.CsvSkip | baml.csv.CsvNeedData | root.iter.Done [builtin] function baml.csv._poll_headers(self: ?) -> baml.csv.CsvHeaders | baml.csv.CsvNeedData [builtin] -function baml.csv._read_chunk(self: ?) -> null [expr] { - { } match (self._file) { null => { self._feed_eof() } null, f: root.fs.File => { let chunk = f.read_bytes(65536) catch (e) { root.errors.InvalidArgument { message: message } => { throw _error(CsvErrorKind.Closed, "underlying file h..." Add message) }, root.errors.Io => { self._mark_exhausted(); throw e } }; if (chunk.length() Eq 0) { self._feed_eof() } else { self._feed(chunk) } } null } +function baml.csv._read_chunk(self: ?) -> void [expr] { + { let chunk = self._file?.read(65536) catch (e) { root.errors.Io => { self._mark_exhausted(); throw e } } } match (chunk) { null => { self._feed_eof() }, bytes: uint8array => { self._feed(bytes) } } } function baml.csv._reader(source: string | uint8array | root.fs.File, options: baml.csv.ReaderOptions?, owns_file: bool) -> baml.csv.CsvReader [builtin] function baml.csv._to_markdown(rows: T[], max_rows: int) -> string [builtin] @@ -829,15 +829,17 @@ class baml.fs.MkdirOptions { class baml.fs.MkdirOptions$stream { recursive: bool | null } -function baml.fs.bytes(self: ?) -> uint8array [builtin] +function baml.fs.bytes(self: ?) -> uint8array [expr] { + { } self.as.bytes() +} function baml.fs.chmod(path: string, mode: int) -> void [builtin] function baml.fs.close(self: ?) -> null [builtin] function baml.fs.exists(path: string) -> bool [builtin] +function baml.fs.flush(self: ?) -> void [builtin] function baml.fs.mkdir(path: string, options: baml.fs.MkdirOptions) -> null [builtin] function baml.fs.open(path: string, mode: "r" | "r+" | "w" | "w+" | "a" | "a+") -> baml.fs.File [builtin] -function baml.fs.read(self: ?, n: int) -> string [builtin] +function baml.fs.read(self: ?, limit: int) -> uint8array? [builtin] function baml.fs.read(path: string) -> string [builtin] -function baml.fs.read_bytes(self: ?, n: int) -> uint8array [builtin] function baml.fs.read_dir(path: string) -> baml.fs.DirEntry[] [builtin] function baml.fs.remove(path: string) -> null [builtin] function baml.fs.remove_dir(path: string) -> null [builtin] @@ -845,11 +847,15 @@ function baml.fs.remove_dir_all(path: string) -> null [builtin] function baml.fs.seek_from(self: ?, whence: "start" | "current" | "end", offset: int) -> int [builtin] function baml.fs.size(path: string) -> int [builtin] function baml.fs.symlink(target: string, path: string) -> void [builtin] -function baml.fs.text(self: ?) -> string [builtin] -function baml.fs.write(self: ?, data: string) -> int [builtin] +function baml.fs.text(self: ?) -> string [expr] { + { } self.as.text() +} +function baml.fs.write(self: ?, data: string | uint8array) -> int [expr] { + { } self.as.write(data) +} function baml.fs.write(path: string, content: string) -> int [builtin] -function baml.fs.write_bytes(self: ?, data: uint8array) -> int [builtin] function baml.fs.write_bytes(path: string, content: uint8array) -> int [builtin] +function baml.fs.write_some(self: ?, data: uint8array) -> int [builtin] --- /baml/ns_future/future.baml --- class baml.future.AllFailed { @@ -882,6 +888,9 @@ function baml.future.race(futures: baml.future.Future[]) -> baml.future.Fu { } spawn { () -> { { let i = baml.future.__await_any(futures); futures.map((f) -> { { } f.cancel() }) } await futures[i] } } } function baml.future.state(self: ?) -> baml.future.FutureState [builtin] +function baml.future.with_timeout(limit: baml.time.Duration, body: () -> T throws E) -> T [expr] { + { let token = baml.spawn.CancelToken.new(); let work = spawn { () -> { { } body() } }; let deadline = spawn { () -> { { baml.sys.sleep(limit) } token.cancel() } }; defer { } deadline.cancel() } await work catch (e) { baml.panics.Cancelled => { if (Not token.is_cancelled()) { throw e }; let limit_ms = limit.to_milliseconds(); let reported = limit_ms.to_int() catch (conversion) { baml.errors.InvalidArgument => null }; throw baml.errors.Timeout { message: `...`, duration_ms: reported } } } +} --- captures --- lambda () in all_complete: captures [futures] @@ -889,6 +898,8 @@ lambda () in all: captures [futures] lambda (f) in ?: captures [futures] lambda () in race: captures [futures] lambda () in any: captures [futures] +lambda () in with_timeout: captures [body] +lambda () in with_timeout: captures [limit, token] --- /baml/ns_glob/glob.baml --- class baml.glob.Glob { @@ -1031,6 +1042,25 @@ function baml.io.input(prompt: string?) -> string [builtin] function baml.io.print(s: string) -> null [builtin] function baml.io.println(s: string) -> null [builtin] +--- /baml/ns_io/read.baml --- +function baml.io._decode_utf8(data: uint8array) -> string [expr] { + { } string.from_utf8(data) catch (e) { root.errors.InvalidArgument => { throw root.errors.ParseError { message: "invalid utf-8" } } } +} +function baml.io.bytes(self: ?) -> uint8array [expr] { + { let result = b"<0 bytes>"; while let chunk: uint8array = self.read(65536) { result = result.concat(chunk) } } result +} +function baml.io.read(self: ?, limit: int) -> uint8array? [missing] +function baml.io.text(self: ?) -> string [expr] { + { let data = self.bytes() } _decode_utf8(data) +} + +--- /baml/ns_io/write.baml --- +function baml.io.flush(self: ?) -> void [missing] +function baml.io.write(self: ?, data: string | uint8array) -> int [expr] { + { let remaining = match (data) { text: string => text.to_utf8(), bytes: uint8array => bytes }; let total = remaining.length(); while remaining.length() Gt 0 { let written = self.write_some(remaining); if (written Le 0 Or written Gt remaining.length()) { throw root.errors.Io { message: "writer made inval..." } }; remaining = remaining.slice(written, remaining.length()) } } total +} +function baml.io.write_some(self: ?, data: uint8array) -> int [missing] + --- /baml/ns_iter/iter.baml --- class baml.iter.ArrayIterator { arr: T[] @@ -1491,13 +1521,11 @@ class baml.net.UdpSocket$stream { _handle: $rust_type } function baml.net._connect(addr: string, timeout_nanos: bigint) -> baml.net.TcpStream [builtin] -function baml.net._read(self: ?, timeout_nanos: bigint) -> uint8array [builtin] function baml.net._recv_from(self: ?, timeout_nanos: bigint) -> baml.net.Datagram [builtin] function baml.net._send_to(self: ?, data: uint8array, addr: string, timeout_nanos: bigint) -> int [builtin] function baml.net._timeout_nanos(timeout: root.time.Duration?) -> bigint [expr] { { } match (timeout) { null => 0n, t: root.time.Duration => t.to_nanoseconds() } } -function baml.net._write(self: ?, data: uint8array, timeout_nanos: bigint) -> null [builtin] function baml.net.accept(self: ?) -> baml.net.TcpStream [builtin] function baml.net.bind(addr: string) -> baml.net.TcpListener [builtin] function baml.net.bind(addr: string) -> baml.net.UdpSocket [builtin] @@ -1507,18 +1535,17 @@ function baml.net.close(self: ?) -> null [builtin] function baml.net.connect(addr: string, timeout: root.time.Duration? = null) -> baml.net.TcpStream [expr] { { } TcpStream._connect(addr, _timeout_nanos(timeout)) } -function baml.net.read(self: ?, timeout: root.time.Duration? = null) -> uint8array [expr] { - { } self._read(_timeout_nanos(timeout)) +function baml.net.flush(self: ?) -> void [expr] { + { } } +function baml.net.read(self: ?, limit: int) -> uint8array? [builtin] function baml.net.recv_from(self: ?, timeout: root.time.Duration? = null) -> baml.net.Datagram [expr] { { } self._recv_from(_timeout_nanos(timeout)) } function baml.net.send_to(self: ?, data: uint8array, addr: string, timeout: root.time.Duration? = null) -> int [expr] { { } self._send_to(data, addr, _timeout_nanos(timeout)) } -function baml.net.write(self: ?, data: uint8array, timeout: root.time.Duration? = null) -> null [expr] { - { } self._write(data, _timeout_nanos(timeout)) -} +function baml.net.write_some(self: ?, data: uint8array) -> int [builtin] --- /baml/ns_ops/bitwise.baml --- function baml.ops.bit_and(self: ?, rhs: Rhs) -> Self.Output [missing] @@ -1882,12 +1909,16 @@ lambda (params) in options: captures [group, cancel, detach] --- /baml/ns_sys/sys.baml --- class baml.sys.Process { - stdout: baml.sys.ProcessLineStream _handle: $rust_type + stdin: baml.sys.WritePipe + stdout: baml.sys.ReadPipe + stderr: baml.sys.ReadPipe? } class baml.sys.Process$stream { - stdout: baml.sys.ProcessLineStream$stream | null _handle: $rust_type + stdin: baml.sys.WritePipe$stream | null + stdout: baml.sys.ReadPipe$stream | null + stderr: baml.sys.ReadPipe$stream | null } class baml.sys.ProcessExit { exit_code: int @@ -1897,25 +1928,33 @@ class baml.sys.ProcessExit$stream { exit_code: int | null signal: string | null } -class baml.sys.ProcessLineStream { - _handle: $rust_type -} -class baml.sys.ProcessLineStream$stream { - _handle: $rust_type -} class baml.sys.ProcessOptions { cwd: string? env: map? timeout_ms: int? stdin: string? - keep_stdin_open: bool? + stderr: baml.sys.StderrMode? } class baml.sys.ProcessOptions$stream { cwd: string | null env: map | null timeout_ms: int | null stdin: string | null - keep_stdin_open: bool | null + stderr: baml.sys.StderrMode | null +} +class baml.sys.ReadPipe { + _pipe: $rust_type +} +class baml.sys.ReadPipe$stream { + _pipe: $rust_type +} +class baml.sys.ReadPipeLines { + pipe: baml.sys.ReadPipe + _pending: uint8array +} +class baml.sys.ReadPipeLines$stream { + pipe: baml.sys.ReadPipe$stream | null + _pending: uint8array } class baml.sys.ShellOutput { stdout: uint8array @@ -1927,20 +1966,33 @@ class baml.sys.ShellOutput$stream { stderr: uint8array exit_code: int | null } -function baml.sys._next(self: ?) -> string? [builtin] +class baml.sys.WritePipe { + _pipe: $rust_type +} +class baml.sys.WritePipe$stream { + _pipe: $rust_type +} +enum baml.sys.StderrMode {Inherit, Pipe, Discard} +function baml.sys._strip_carriage_return(line: uint8array) -> uint8array [expr] { + { } match (line.at(line.length() Sub 1)) { 13 => line.slice(0, line.length() Sub 1), _ => line } +} function baml.sys.argv() -> string[] [builtin] function baml.sys.close(self: ?) -> null [builtin] -function baml.sys.close(self: ?) -> null [builtin] -function baml.sys.close_stdin(self: ?) -> null [builtin] +function baml.sys.close(self: ?) -> void [builtin] +function baml.sys.close(self: ?) -> void [builtin] function baml.sys.collect_garbage() -> null [builtin] function baml.sys.exec(program: string, args: string[]?, options: baml.sys.ProcessOptions?) -> baml.sys.ShellOutput [builtin] function baml.sys.exit(code: int) -> never [builtin] -function baml.sys.iter(self: ?) -> root.iter.Iterator [expr] { +function baml.sys.flush(self: ?) -> void [builtin] +function baml.sys.iter(self: ?) -> root.iter.Iterator [expr] { { } self } -function baml.sys.kill(self: ?) -> null [builtin] +function baml.sys.kill(self: ?) -> void [builtin] +function baml.sys.lines(self: ?) -> baml.sys.ReadPipeLines [expr] { + { } baml.sys.ReadPipeLines { pipe: self, _pending: b"<0 bytes>" } +} function baml.sys.next(self: ?) -> string | root.iter.Done [expr] { - { } match (self._next()) { line: string => line, null => root.iter.Done { } } + { let line: string | root.iter.Done = root.iter.Done { }; let framing = true; while framing { } match (self._pending.index_of(10)) { index: int => { let bytes = self._pending.slice(0, index); self._pending = self._pending.slice(index Add 1, self._pending.length()); line = _strip_carriage_return(bytes).to_string(); framing = false }, null => { } match (self.pipe.read(65536)) { chunk: uint8array => { self._pending = self._pending.concat(chunk) }, null => { if (self._pending.length() Gt 0) { line = _strip_carriage_return(self._pending).to_string(); self._pending = b"<0 bytes>" }; framing = false } } } } line } function baml.sys.ok(self: ?) -> bool [expr] { { } self.exit_code Eq 0 @@ -1950,11 +2002,12 @@ function baml.sys.ok(self: ?) -> bool [expr] { } function baml.sys.panic(message: string) -> never [builtin] function baml.sys.pid() -> int [builtin] +function baml.sys.read(self: ?, limit: int) -> uint8array? [builtin] function baml.sys.shell(command: string, options: baml.sys.ProcessOptions?) -> baml.sys.ShellOutput [builtin] function baml.sys.sleep(delay: root.time.Duration) -> null [builtin] function baml.sys.start_process(program: string, args: string[]?, options: baml.sys.ProcessOptions?) -> baml.sys.Process [builtin] function baml.sys.wait(self: ?) -> baml.sys.ProcessExit [builtin] -function baml.sys.write_stdin(self: ?, data: string) -> null [builtin] +function baml.sys.write_some(self: ?, data: uint8array) -> int [builtin] --- /baml/ns_time/duration.baml --- class baml.time.Duration { @@ -2494,6 +2547,7 @@ function baml.from_array(array: int[]) -> uint8array [builtin] function baml.from_base64(base64: string) -> uint8array [builtin] function baml.from_hex(hex: string) -> uint8array [builtin] function baml.includes(self: ?, item: int) -> bool [builtin] +function baml.index_of(self: ?, item: int) -> int? [builtin] function baml.length(self: ?) -> int [builtin] function baml.pop(self: ?) -> int? [builtin] function baml.push(self: ?, item: int) -> int [builtin] diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/builtin_io/baml_tests__diagnostic_errors__builtin_io__03_ppir.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/builtin_io/baml_tests__diagnostic_errors__builtin_io__03_ppir.snap index e15976e91ce..e972fae572f 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/builtin_io/baml_tests__diagnostic_errors__builtin_io__03_ppir.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/builtin_io/baml_tests__diagnostic_errors__builtin_io__03_ppir.snap @@ -15,16 +15,16 @@ function user.FileWrite() -> int [expr] { { let f = baml.fs.open("data.txt", "r+") } f.write("hello") } function user.FileWriteBytes() -> int [expr] { - { let f = baml.fs.open("data.bin", "r+"); let data = f.bytes(); f.seek_from("start", 0) } f.write_bytes(data) + { let f = baml.fs.open("data.bin", "r+"); let data = f.bytes(); f.seek_from("start", 0) } f.write(data) } function user.ReadAndClose() -> string [expr] { { let f = baml.fs.open("example.txt", "r"); let content = f.text(); f.close() } content } -function user.ReadChunk() -> string [expr] { +function user.ReadChunk() -> uint8array? [expr] { { let f = baml.fs.open("log.txt", "r") } f.read(512) } function user.ReadChunkBytes() -> uint8array [expr] { - { let f = baml.fs.open("data.bin", "r") } f.read_bytes(4096) + { let f = baml.fs.open("data.bin", "r") } f.read(4096) NullCoalesce b"<0 bytes>" } function user.ReadDir() -> baml.fs.DirEntry[] [expr] { { } baml.fs.read_dir("some/path") @@ -68,20 +68,20 @@ function user.AskName() -> string [expr] { function user.AskNoPrompt() -> string [expr] { { } baml.io.input(null) } -function user.ConnectAndRead() -> uint8array [expr] { - { let sock = baml.net.TcpStream.connect("127.0.0.1:8080") } sock.read() +function user.ConnectAndRead() -> uint8array? [expr] { + { let sock = baml.net.TcpStream.connect("127.0.0.1:8080") } sock.read(4096) } -function user.ConnectAndReadInline() -> uint8array [expr] { - { } baml.net.TcpStream.connect("localhost:3000").read() +function user.ConnectAndReadInline() -> uint8array? [expr] { + { } baml.net.TcpStream.connect("localhost:3000").read(4096) } -function user.ConnectReadAndClose() -> uint8array [expr] { - { let sock = baml.net.TcpStream.connect("127.0.0.1:8080"); let data = sock.read(); sock.close() } data +function user.ConnectReadAndClose() -> uint8array? [expr] { + { let sock = baml.net.TcpStream.connect("127.0.0.1:8080"); let data = sock.read(4096); sock.close() } data } -function user.ListenAndAccept() -> uint8array [expr] { - { let listener = baml.net.TcpListener.bind("127.0.0.1:0"); let sock = listener.accept() } sock.read() +function user.ListenAndAccept() -> uint8array? [expr] { + { let listener = baml.net.TcpListener.bind("127.0.0.1:0"); let sock = listener.accept() } sock.read(4096) } function user.MultipleConnections() -> uint8array [expr] { - { let s1 = baml.net.TcpStream.connect("server1:80"); let s2 = baml.net.TcpStream.connect("server2:80"); let data1 = s1.read(); let data2 = s2.read() } data1.concat(data2) + { let s1 = baml.net.TcpStream.connect("server1:80"); let s2 = baml.net.TcpStream.connect("server2:80"); let data1 = s1.read(4096) NullCoalesce b"<0 bytes>"; let data2 = s2.read(4096) NullCoalesce b"<0 bytes>" } data1.concat(data2) } function user.UdpRoundtrip() -> uint8array [expr] { { let sock = baml.net.UdpSocket.bind("0.0.0.0:0"); sock.send_to("ping".to_utf8(), "127.0.0.1:9"); let dgram = sock.recv_from() } dgram.data diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/builtin_io/baml_tests__diagnostic_errors__builtin_io__10_formatter__fs_ops.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/builtin_io/baml_tests__diagnostic_errors__builtin_io__10_formatter__fs_ops.snap index 10202b083dd..4d56e5bcf0e 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/builtin_io/baml_tests__diagnostic_errors__builtin_io__10_formatter__fs_ops.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/builtin_io/baml_tests__diagnostic_errors__builtin_io__10_formatter__fs_ops.snap @@ -43,14 +43,14 @@ function ReadFileBytesAndClose() -> uint8array { data } -function ReadChunk() -> string { +function ReadChunk() -> uint8array? { let f = baml.fs.open("log.txt", "r"); f.read(512) } function ReadChunkBytes() -> uint8array { let f = baml.fs.open("data.bin", "r"); - f.read_bytes(4096) + f.read(4096) ?? b"" } function WriteFile() -> int { @@ -77,7 +77,7 @@ function FileWriteBytes() -> int { let f = baml.fs.open("data.bin", "r+"); let data = f.bytes(); f.seek_from("start", 0); - f.write_bytes(data) + f.write(data) } function Append() -> int { diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/builtin_io/baml_tests__diagnostic_errors__builtin_io__10_formatter__net_ops.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/builtin_io/baml_tests__diagnostic_errors__builtin_io__10_formatter__net_ops.snap index a4f06424863..6908a7227e2 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/builtin_io/baml_tests__diagnostic_errors__builtin_io__10_formatter__net_ops.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/builtin_io/baml_tests__diagnostic_errors__builtin_io__10_formatter__net_ops.snap @@ -3,34 +3,34 @@ source: crates/baml_tests/src/generated_tests.rs --- // Test network operations typecheck correctly -function ConnectAndRead() -> uint8array { +function ConnectAndRead() -> uint8array? { let sock = baml.net.TcpStream.connect("127.0.0.1:8080"); - sock.read() + sock.read(4096) } -function ConnectAndReadInline() -> uint8array { - baml.net.TcpStream.connect("localhost:3000").read() +function ConnectAndReadInline() -> uint8array? { + baml.net.TcpStream.connect("localhost:3000").read(4096) } function MultipleConnections() -> uint8array { let s1 = baml.net.TcpStream.connect("server1:80"); let s2 = baml.net.TcpStream.connect("server2:80"); - let data1 = s1.read(); - let data2 = s2.read(); + let data1 = s1.read(4096) ?? b""; + let data2 = s2.read(4096) ?? b""; data1.concat(data2) } -function ConnectReadAndClose() -> uint8array { +function ConnectReadAndClose() -> uint8array? { let sock = baml.net.TcpStream.connect("127.0.0.1:8080"); - let data = sock.read(); + let data = sock.read(4096); sock.close(); data } -function ListenAndAccept() -> uint8array { +function ListenAndAccept() -> uint8array? { let listener = baml.net.TcpListener.bind("127.0.0.1:0"); let sock = listener.accept(); - sock.read() + sock.read(4096) } function UdpRoundtrip() -> uint8array { diff --git a/baml_language/crates/baml_tests/src/compiler2_tir/phase6.rs b/baml_language/crates/baml_tests/src/compiler2_tir/phase6.rs index 207a1c98485..014251f2b5e 100644 --- a/baml_language/crates/baml_tests/src/compiler2_tir/phase6.rs +++ b/baml_language/crates/baml_tests/src/compiler2_tir/phase6.rs @@ -1252,7 +1252,7 @@ function f(file: baml.fs.File) -> null { ); let ty = expr_type_in_function(&db, file, "f", "read"); assert!( - ty.starts_with("(n: int) -> string throws "), + ty.starts_with("(limit: int) -> uint8array | null throws "), "expected builtin member value to preserve declared throws, got `{ty}`" ); assert!( diff --git a/baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap b/baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap index 51507c6c5d9..2323df53a1e 100644 --- a/baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap +++ b/baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap @@ -20,7 +20,7 @@ namespace baml: class TaggedString { methods: [] } type ToJson type ToString - class Uint8Array { methods: [length, at, push, pop, concat, includes, reverse, slice, zeroes, from_array, to_array, from_hex, to_hex, from_base64, to_base64, _to_string_impl, sort, to_string] } + class Uint8Array { methods: [length, at, push, pop, concat, includes, index_of, reverse, slice, zeroes, from_array, to_array, from_hex, to_hex, from_base64, to_base64, _to_string_impl, sort, to_string] } function _cleanup_begin function _compare_shim function _float_total_cmp @@ -117,7 +117,7 @@ namespace baml.errors: namespace baml.events: namespace baml.fs: class DirEntry { methods: [] } - class File { methods: [text, bytes, read, read_bytes, close, seek_from, write, write_bytes] } + class File { methods: [text, bytes, close, seek_from, write, read, write_some, flush] } class MkdirOptions { methods: [] } function chmod function exists @@ -141,6 +141,7 @@ namespace baml.future: function all_complete function any function race + function with_timeout namespace baml.glob: class Glob { methods: [scan, matches] } class ScanOptions { methods: [] } @@ -166,6 +167,9 @@ namespace baml.id: function new function set namespace baml.io: + type Read + type Write + function _decode_utf8 function eprint function eprintln function input @@ -222,7 +226,7 @@ namespace baml.media: namespace baml.net: class Datagram { methods: [] } class TcpListener { methods: [bind, accept, close] } - class TcpStream { methods: [connect, _connect, read, _read, write, _write, close] } + class TcpStream { methods: [connect, _connect, close, read, write_some, flush] } class UdpSocket { methods: [bind, send_to, _send_to, recv_from, _recv_from, close] } function _timeout_nanos namespace baml.ops: @@ -284,11 +288,15 @@ namespace baml.spawn: class TaskGroup { methods: [new, cancel, set_limit, limit, name, active_count, queued_count] } function options namespace baml.sys: - class Process { methods: [write_stdin, close_stdin, wait, kill, close] } + class Process { methods: [wait, kill, close] } class ProcessExit { methods: [ok] } - class ProcessLineStream { methods: [_next, close, iter, next] } class ProcessOptions { methods: [] } + class ReadPipe { methods: [close, lines, read] } + class ReadPipeLines { methods: [iter, next] } class ShellOutput { methods: [ok] } + enum StderrMode + class WritePipe { methods: [close, write_some, flush] } + function _strip_carriage_return function argv function collect_garbage function exec diff --git a/baml_language/crates/baml_tests/tests/net.rs b/baml_language/crates/baml_tests/tests/net.rs index 3c2d486c477..52c480b3c7e 100644 --- a/baml_language/crates/baml_tests/tests/net.rs +++ b/baml_language/crates/baml_tests/tests/net.rs @@ -6,7 +6,10 @@ use baml_tests::baml_test; use bex_engine::BexExternalValue; -use tokio::{io::AsyncWriteExt, net::TcpListener}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpListener, +}; /// Replace the dynamic address in bytecode with a stable placeholder. fn stabilize_bytecode(bytecode: &str, addr: &str) -> String { @@ -26,21 +29,25 @@ async fn net_connect_and_read() { let output = baml_test!(&format!( r#" - function main() -> uint8array {{ + function main() -> uint8array? {{ let sock = baml.net.TcpStream.connect("{addr}"); - sock.read() + sock.read(1024) }} "# )); server.await.unwrap(); insta::assert_snapshot!(stabilize_bytecode(&output.bytecode, &addr), @r#" - function main() -> uint8array { + function main() -> uint8array | null { load_const "{ADDR}" load_const call baml.net.TcpStream.connect - load_const - call baml.net.TcpStream.read + load_const 1024 + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 + store_var _0 + load_var _0 return } "#); @@ -54,20 +61,24 @@ async fn net_connect_and_read() { async fn net_connect_failure() { let output = baml_test!( r#" - function main() -> uint8array { + function main() -> uint8array? { let sock = baml.net.TcpStream.connect("127.0.0.1:1"); - sock.read() + sock.read(1024) } "# ); insta::assert_snapshot!(output.bytecode, @r#" - function main() -> uint8array { + function main() -> uint8array | null { load_const "127.0.0.1:1" load_const call baml.net.TcpStream.connect - load_const - call baml.net.TcpStream.read + load_const 1024 + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 + store_var _0 + load_var _0 return } "#); @@ -76,6 +87,33 @@ async fn net_connect_failure() { assert!(output.result.is_err()); } +#[tokio::test] +async fn net_write_accepts_text_and_writes_every_byte() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut socket, _) = listener.accept().await.unwrap(); + let mut data = Vec::new(); + socket.read_to_end(&mut data).await.unwrap(); + data + }); + + let output = baml_test!(&format!( + r#" + function main() -> int {{ + let sock = baml.net.TcpStream.connect("{addr}"); + let written = sock.write("hello"); + sock.close(); + written + }} + "# + )); + + assert_eq!(output.result, Ok(BexExternalValue::Int(5))); + assert_eq!(server.await.unwrap(), b"hello"); +} + #[tokio::test] async fn net_multiple_reads() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); @@ -91,10 +129,10 @@ async fn net_multiple_reads() { let output = baml_test!(&format!( r#" - function main() -> uint8array {{ + function main() -> uint8array? {{ let sock = baml.net.TcpStream.connect("{addr}"); - let first = sock.read(); - let second = sock.read(); + let first = sock.read(1024); + let second = sock.read(1024); first }} "# @@ -102,18 +140,22 @@ async fn net_multiple_reads() { server.await.unwrap(); insta::assert_snapshot!(stabilize_bytecode(&output.bytecode, &addr), @r#" - function main() -> uint8array { + function main() -> uint8array | null { load_const "{ADDR}" load_const call baml.net.TcpStream.connect store_var sock load_var sock - load_const - call baml.net.TcpStream.read + load_const 1024 + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 store_var first load_var sock - load_const - call baml.net.TcpStream.read + load_const 1024 + load_type baml.io.Read + load_const "read" + virtual_call nargs=2 ntypeargs=0 store_var second load_var first return @@ -125,48 +167,49 @@ async fn net_multiple_reads() { ); } -// Kept in Rust (not the baml_src corpus): the silent peer must be a -// controlled Rust listener that accepts and holds the connection open. A -// corpus version using `baml.http.Server.bind` as the silent peer is flaky — -// the BAML listener object can be GC-collected between `connect` and the -// throwing `read`, resetting the connection into an `Io` error instead of the -// expected `Timeout`, intermittently under load. #[tokio::test] -async fn net_read_timeout_fires() { - // The server accepts the connection but never writes, so a bare read() would - // block forever. A short read timeout must surface as baml.errors.Timeout. +async fn net_read_is_cancellable() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap().to_string(); let server = tokio::spawn(async move { let _conn = listener.accept().await.unwrap(); - // Hold the connection open (silent) past the client's deadline. tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; }); let output = baml_test!(&format!( r#" - function main() -> uint8array {{ + function main() -> string {{ let sock = baml.net.TcpStream.connect("{addr}"); - sock.read(timeout = baml.time.Duration.from_milliseconds(50n)) + let tok = baml.spawn.CancelToken.new(); + let read = spawn with baml.spawn.options(cancel = tok) {{ + sock.read(1024) + }}; + let deadline = spawn {{ + baml.sys.sleep(baml.time.Duration.from_milliseconds(50n)); + tok.cancel() + }}; + let outcome = (await read) catch (e) {{ + baml.panics.Cancelled => "cancelled" + }}; + match (outcome) {{ + let reason: string => reason, + null => "eof", + let chunk: uint8array => "read", + }} }} "# )); server.await.unwrap(); - let err = output - .result - .expect_err("read with a 50ms timeout against a silent peer should time out") - .to_string(); - assert!( - err.contains("baml.errors.Timeout"), - "expected a baml.errors.Timeout throw, got: {err}" + assert_eq!( + output.result, + Ok(BexExternalValue::String("cancelled".into())) ); } #[tokio::test] -async fn net_read_succeeds_within_timeout() { - // A generous read timeout must not interfere with a prompt response. +async fn net_read_completes_before_cancellation() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap().to_string(); @@ -178,9 +221,19 @@ async fn net_read_succeeds_within_timeout() { let output = baml_test!(&format!( r#" - function main() -> uint8array {{ + function main() -> uint8array? {{ let sock = baml.net.TcpStream.connect("{addr}"); - sock.read(timeout = baml.time.Duration.from_seconds(5n)) + let tok = baml.spawn.CancelToken.new(); + let read = spawn with baml.spawn.options(cancel = tok) {{ + sock.read(1024) + }}; + let deadline = spawn {{ + baml.sys.sleep(baml.time.Duration.from_seconds(5n)); + tok.cancel() + }}; + let chunk = await read; + deadline.cancel(); + chunk }} "# )); @@ -207,12 +260,12 @@ async fn net_connect_timeout_param_accepted() { let output = baml_test!(&format!( r#" - function main() -> uint8array {{ + function main() -> uint8array? {{ let sock = baml.net.TcpStream.connect( "{addr}", timeout = baml.time.Duration.from_seconds(5n), ); - sock.read() + sock.read(1024) }} "# )); diff --git a/baml_language/crates/baml_tests/tests/shell.rs b/baml_language/crates/baml_tests/tests/shell.rs index 7495e625faf..b8931639d48 100644 --- a/baml_language/crates/baml_tests/tests/shell.rs +++ b/baml_language/crates/baml_tests/tests/shell.rs @@ -264,7 +264,7 @@ async fn start_process_yields_stdout_before_exit() { ); defer { process.close() } - let first = match (process.stdout.next()) { + let first = match (process.stdout.lines().next()) { let line: string => line, baml.iter.Done => "", }; @@ -291,7 +291,7 @@ async fn start_process_iterates_lines_and_final_unterminated_line() { ); defer { process.close() } - let lines = process.stdout.collect(); + let lines = process.stdout.lines().collect(); let exit = process.wait(); if (!exit.ok()) { return "bad exit"; @@ -309,22 +309,54 @@ async fn start_process_iterates_lines_and_final_unterminated_line() { #[tokio::test] #[cfg(not(target_os = "windows"))] -async fn start_process_supports_incremental_stdin() { +async fn start_process_reads_complete_stdout_as_text() { let output = baml_test!( r#" - function main() -> string throws baml.errors.Io | baml.errors.Timeout { + function main() -> string throws baml.errors.Io | baml.errors.ParseError | baml.errors.Timeout { let process = baml.sys.start_process( - "cat", - [], - baml.sys.ProcessOptions { keep_stdin_open: true }, + "sh", + ["-c", "printf 'hello'"], + null, ); defer { process.close() } - process.write_stdin("one\n"); - let one = process.stdout._next() ?? ""; - process.write_stdin("two\n"); - let two = process.stdout._next() ?? ""; - process.close_stdin(); + let stdout = process.stdout.text(); + let exit = process.wait(); + if (!exit.ok()) { + return "bad exit"; + } + stdout + } + "# + ); + + assert_eq!( + output.result, + Ok(BexExternalValue::String("hello".to_string().into())) + ); +} + +#[tokio::test] +#[cfg(not(target_os = "windows"))] +async fn start_process_supports_incremental_stdin() { + let output = baml_test!( + r#" + function main() -> string throws baml.errors.Io | baml.errors.Timeout { + let process = baml.sys.start_process("cat", [], null); + defer { process.close() } + + let out = process.stdout.lines(); + process.stdin.write("one\n"); + let one = match (out.next()) { + let line: string => line, + baml.iter.Done => "", + }; + process.stdin.write("two\n"); + let two = match (out.next()) { + let line: string => line, + baml.iter.Done => "", + }; + process.stdin.close(); let exit = process.wait(); if (!exit.ok()) { return "bad exit"; @@ -342,27 +374,40 @@ async fn start_process_supports_incremental_stdin() { #[tokio::test] #[cfg(not(target_os = "windows"))] -async fn start_process_stdout_read_honors_process_timeout() { +async fn start_process_stdout_read_is_cancellable() { let output = baml_test!( r#" - function main() -> bool { + function main() -> string { let process = baml.sys.start_process( "sh", ["-c", "while :; do :; done"], - baml.sys.ProcessOptions { timeout_ms: 25 }, + null, ); defer { process.close() } - process.stdout.next() catch (e) { - let timeout: baml.errors.Timeout => { return true; }, - _ => { return false; }, + let tok = baml.spawn.CancelToken.new(); + let read = spawn with baml.spawn.options(cancel = tok) { + process.stdout.lines().next() }; - false + let deadline = spawn { + baml.sys.sleep(baml.time.Duration.from_milliseconds(25n)); + tok.cancel() + }; + let outcome = (await read) catch (e) { + baml.panics.Cancelled => "cancelled" + }; + match (outcome) { + let line: string => line, + baml.iter.Done => "eof", + } } "# ); - assert_eq!(output.result, Ok(BexExternalValue::Bool(true))); + assert_eq!( + output.result, + Ok(BexExternalValue::String("cancelled".into())) + ); } #[tokio::test] @@ -508,3 +553,56 @@ async fn shell_stderr_bytes() { panic!("expected Uint8Array, got {:?}", output.result); } } + +#[tokio::test] +#[cfg(not(target_os = "windows"))] +async fn start_process_stderr_pipe_is_readable() { + let output = baml_test!( + r#" + function main() -> string { + let process = baml.sys.start_process( + "sh", + ["-c", "printf 'boom\n' >&2"], + baml.sys.ProcessOptions { stderr: baml.sys.StderrMode.Pipe }, + ); + defer { process.close() } + + match (process.stderr) { + null => "no pipe", + let err: baml.sys.ReadPipe => match (err.lines().next()) { + let line: string => line, + baml.iter.Done => "eof", + }, + } + } + "# + ); + + assert_eq!(output.result, Ok(BexExternalValue::String("boom".into()))); +} + +#[tokio::test] +#[cfg(not(target_os = "windows"))] +async fn start_process_stderr_modes_without_pipe() { + let output = baml_test!( + r#" + function main() -> bool { + let inherited = baml.sys.start_process("sh", ["-c", "printf 'x' >&2"], null); + defer { inherited.close() } + let discarded = baml.sys.start_process( + "sh", + ["-c", "printf 'x' >&2"], + baml.sys.ProcessOptions { stderr: baml.sys.StderrMode.Discard }, + ); + defer { discarded.close() } + + inherited.stderr == null && + discarded.stderr == null && + inherited.wait().ok() && + discarded.wait().ok() + } + "# + ); + + assert_eq!(output.result, Ok(BexExternalValue::Bool(true))); +} diff --git a/baml_language/crates/bex_vm/src/package_baml/uint8array.rs b/baml_language/crates/bex_vm/src/package_baml/uint8array.rs index dce4566797b..9706563593d 100644 --- a/baml_language/crates/bex_vm/src/package_baml/uint8array.rs +++ b/baml_language/crates/bex_vm/src/package_baml/uint8array.rs @@ -41,6 +41,15 @@ impl BamlClassUint8Array for PackageBamlImpl { uint8array.contains(&item) } + #[allow(clippy::cast_possible_wrap)] + fn index_of(uint8array: &[u8], item: i64) -> Option { + let item = u8::try_from(item).ok()?; + uint8array + .iter() + .position(|byte| *byte == item) + .map(|index| index as i64) + } + fn reverse(uint8array: &[u8]) -> Vec { uint8array.iter().copied().rev().collect() } diff --git a/baml_language/crates/bridge_wasm/src/wasm_io_fs.rs b/baml_language/crates/bridge_wasm/src/wasm_io_fs.rs index 130916dca76..c95ca2e6e84 100644 --- a/baml_language/crates/bridge_wasm/src/wasm_io_fs.rs +++ b/baml_language/crates/bridge_wasm/src/wasm_io_fs.rs @@ -186,30 +186,6 @@ fn remove_dir_all_recursive(vfs: &WasmVfs, path: &str) -> Result<(), VmBamlError // ============================================================================ impl io::IoClassFsFile for WasmIoFs { - fn text( - &self, - _h: &Arc, - _c: CallId, - _f: owned::fs::File, - _ctx: &SysOpContext, - ) -> SysOpOutput { - SysOpOutput::err(VmBamlError::Unsupported { - message: "Operation not supported on this platform".to_string(), - }) - } - - fn bytes( - &self, - _h: &Arc, - _c: CallId, - _f: owned::fs::File, - _ctx: &SysOpContext, - ) -> SysOpOutput> { - SysOpOutput::err(VmBamlError::Unsupported { - message: "Operation not supported on this platform".to_string(), - }) - } - fn read( &self, _h: &Arc, @@ -217,20 +193,7 @@ impl io::IoClassFsFile for WasmIoFs { _f: owned::fs::File, _n: i64, _ctx: &SysOpContext, - ) -> SysOpOutput { - SysOpOutput::err(VmBamlError::Unsupported { - message: "Operation not supported on this platform".to_string(), - }) - } - - fn read_bytes( - &self, - _h: &Arc, - _c: CallId, - _f: owned::fs::File, - _n: i64, - _ctx: &SysOpContext, - ) -> SysOpOutput> { + ) -> SysOpOutput>> { SysOpOutput::err(VmBamlError::Unsupported { message: "Operation not supported on this platform".to_string(), }) @@ -262,12 +225,12 @@ impl io::IoClassFsFile for WasmIoFs { }) } - fn write( + fn write_some( &self, _h: &Arc, _c: CallId, _f: owned::fs::File, - _d: String, + _d: Vec, _ctx: &SysOpContext, ) -> SysOpOutput { SysOpOutput::err(VmBamlError::Unsupported { @@ -275,14 +238,13 @@ impl io::IoClassFsFile for WasmIoFs { }) } - fn write_bytes( + fn flush( &self, _h: &Arc, _c: CallId, _f: owned::fs::File, - _d: Vec, _ctx: &SysOpContext, - ) -> SysOpOutput { + ) -> SysOpOutput<()> { SysOpOutput::err(VmBamlError::Unsupported { message: "Operation not supported on this platform".to_string(), }) diff --git a/baml_language/crates/bridge_wasm/src/wasm_sys.rs b/baml_language/crates/bridge_wasm/src/wasm_sys.rs index f64d77a8749..7e46d8309bc 100644 --- a/baml_language/crates/bridge_wasm/src/wasm_sys.rs +++ b/baml_language/crates/bridge_wasm/src/wasm_sys.rs @@ -90,12 +90,8 @@ fn options_to_js(options: Option<&io::owned::sys::ProcessOptions>) -> JsValue { if let Some(ref stdin) = opts.stdin { let _ = Reflect::set(&obj, &"stdin".into(), &stdin.into()); } - if let Some(keep_stdin_open) = opts.keep_stdin_open { - let _ = Reflect::set( - &obj, - &"keep_stdin_open".into(), - &JsValue::from_bool(keep_stdin_open), - ); + if let Some(BexExternalValue::Variant { variant_name, .. }) = opts.stderr.as_ref() { + let _ = Reflect::set(&obj, &"stderr".into(), &variant_name.into()); } js_sys::JSON::stringify(&obj) .map(JsValue::from) @@ -104,49 +100,52 @@ fn options_to_js(options: Option<&io::owned::sys::ProcessOptions>) -> JsValue { } } -impl io::IoClassSysProcess for WasmSys { - fn write_stdin( +impl io::IoClassSysReadPipe for WasmSys { + fn read( &self, _heap: &Arc, _call_id: CallId, - _process: io::owned::sys::Process, - _data: String, + _readpipe: io::owned::sys::ReadPipe, + _limit: i64, _ctx: &SysOpContext, - ) -> SysOpOutput<()> { + ) -> SysOpOutput>> { SysOpOutput::err(VmBamlError::Unsupported { message: "Live processes are not supported on this platform".to_string(), }) } - fn close_stdin( + fn close( &self, _heap: &Arc, _call_id: CallId, - _process: io::owned::sys::Process, + _readpipe: io::owned::sys::ReadPipe, _ctx: &SysOpContext, ) -> SysOpOutput<()> { SysOpOutput::err(VmBamlError::Unsupported { message: "Live processes are not supported on this platform".to_string(), }) } +} - fn wait( +impl io::IoClassSysWritePipe for WasmSys { + fn write_some( &self, _heap: &Arc, _call_id: CallId, - _process: io::owned::sys::Process, + _writepipe: io::owned::sys::WritePipe, + _data: Vec, _ctx: &SysOpContext, - ) -> SysOpOutput { + ) -> SysOpOutput { SysOpOutput::err(VmBamlError::Unsupported { message: "Live processes are not supported on this platform".to_string(), }) } - fn kill( + fn flush( &self, _heap: &Arc, _call_id: CallId, - _process: io::owned::sys::Process, + _writepipe: io::owned::sys::WritePipe, _ctx: &SysOpContext, ) -> SysOpOutput<()> { SysOpOutput::err(VmBamlError::Unsupported { @@ -158,21 +157,35 @@ impl io::IoClassSysProcess for WasmSys { &self, _heap: &Arc, _call_id: CallId, - _process: io::owned::sys::Process, + _writepipe: io::owned::sys::WritePipe, _ctx: &SysOpContext, ) -> SysOpOutput<()> { - SysOpOutput::ok(()) + SysOpOutput::err(VmBamlError::Unsupported { + message: "Live processes are not supported on this platform".to_string(), + }) } } -impl io::IoClassSysProcessLineStream for WasmSys { - fn _next( +impl io::IoClassSysProcess for WasmSys { + fn wait( + &self, + _heap: &Arc, + _call_id: CallId, + _process: io::owned::sys::Process, + _ctx: &SysOpContext, + ) -> SysOpOutput { + SysOpOutput::err(VmBamlError::Unsupported { + message: "Live processes are not supported on this platform".to_string(), + }) + } + + fn kill( &self, _heap: &Arc, _call_id: CallId, - _processlinestream: io::owned::sys::ProcessLineStream, + _process: io::owned::sys::Process, _ctx: &SysOpContext, - ) -> SysOpOutput> { + ) -> SysOpOutput<()> { SysOpOutput::err(VmBamlError::Unsupported { message: "Live processes are not supported on this platform".to_string(), }) @@ -182,7 +195,7 @@ impl io::IoClassSysProcessLineStream for WasmSys { &self, _heap: &Arc, _call_id: CallId, - _processlinestream: io::owned::sys::ProcessLineStream, + _process: io::owned::sys::Process, _ctx: &SysOpContext, ) -> SysOpOutput<()> { SysOpOutput::ok(()) diff --git a/baml_language/crates/sys_auth/src/io_bridge.rs b/baml_language/crates/sys_auth/src/io_bridge.rs index 01c425bd512..e1b73ac2a47 100644 --- a/baml_language/crates/sys_auth/src/io_bridge.rs +++ b/baml_language/crates/sys_auth/src/io_bridge.rs @@ -34,7 +34,7 @@ use std::sync::Arc; use async_trait::async_trait; use indexmap::IndexMap; -use sys_types::{BexExternalValue, runtime_io::RuntimeIo}; +use sys_types::runtime_io::RuntimeIo; /// Total deadline for one credential HTTP round trip, in nanoseconds (the unit /// `RuntimeIo::http__send` takes). @@ -139,12 +139,7 @@ impl BamlAuthIo { } async fn file_text(&self, path: &str) -> Option { - let handle = self - .io - .fs_open(path.to_string(), BexExternalValue::String("r".into())) - .await - .ok()?; - self.io.fs_file_text(&handle).await.ok() + self.io.fs_read(path.to_string()).await.ok() } /// One HTTP round trip through the runtime, returning `(status, body)`. diff --git a/baml_language/crates/sys_auth/src/testing.rs b/baml_language/crates/sys_auth/src/testing.rs index 73aa0ae9709..6c448e7609e 100644 --- a/baml_language/crates/sys_auth/src/testing.rs +++ b/baml_language/crates/sys_auth/src/testing.rs @@ -18,7 +18,7 @@ use std::{ use indexmap::IndexMap; use sys_types::{ BexExternalValue, - runtime_io::{FsFileHandle, HttpResponseHandle, RuntimeIo, RuntimeIoError}, + runtime_io::{HttpResponseHandle, RuntimeIo, RuntimeIoError}, }; /// Counts the IO a resolution actually performed, so a test can assert that @@ -92,35 +92,14 @@ impl RuntimeIo for StubIo { Box::pin(async move { Ok(value) }) } - fn fs_open( + fn fs_read( &self, path: String, - _mode: BexExternalValue, - ) -> Pin> + Send + '_>> { - self.calls.fs.fetch_add(1, Ordering::SeqCst); - let exists = self.files.contains_key(&path); - Box::pin(async move { - if exists { - // The handle carries the path so `fs_file_text` can resolve it. - Ok(FsFileHandle { - raw: BexExternalValue::String(path.into()), - }) - } else { - Err(RuntimeIoError::Other(format!("no such file: {path}"))) - } - }) - } - - fn fs_file_text( - &self, - file: &FsFileHandle, ) -> Pin> + Send + '_>> { - let contents = match &file.raw { - BexExternalValue::String(path) => self.files.get(&path.to_string()).cloned(), - _ => None, - }; + self.calls.fs.fetch_add(1, Ordering::SeqCst); + let contents = self.files.get(&path).cloned(); Box::pin(async move { - contents.ok_or_else(|| RuntimeIoError::Other("unreadable handle".to_string())) + contents.ok_or_else(|| RuntimeIoError::Other(format!("no such file: {path}"))) }) } diff --git a/baml_language/crates/sys_native/src/io_impls.rs b/baml_language/crates/sys_native/src/io_impls.rs index f0e2a26dd43..25fa623ece0 100644 --- a/baml_language/crates/sys_native/src/io_impls.rs +++ b/baml_language/crates/sys_native/src/io_impls.rs @@ -11,6 +11,8 @@ use sys_ops::io::{ self, CallId, SysOpContext, SysOpOutput, VmBamlError, VmPanic, VmRustFnError, owned, }; +const MAX_READ_CHUNK: usize = 64 * 1024; + // Process-level shared BufReader for stdin, preventing data loss when // BufReader over-reads into its internal buffer across multiple io.input() calls. static STDIN_READER: OnceLock>> = @@ -415,81 +417,23 @@ fn closed_err() -> VmBamlError { } } -impl io::IoClassFsFile for NativeSysOps { - fn text( - &self, - _heap: &Arc, - _call_id: CallId, - file: owned::fs::File, - _ctx: &SysOpContext, - ) -> SysOpOutput { - use tokio::io::AsyncReadExt; - - SysOpOutput::async_op(async move { - let handle = downcast_handle(&file)?; - let mut guard = handle.lock().await; - let f = guard.as_mut().ok_or_else(closed_err)?; - let mut contents = String::new(); - f.read_to_string(&mut contents) - .await - .map_err(|e| VmBamlError::Io { - message: format!("Failed to read file: {e}"), - })?; - Ok(contents) - }) - } - - fn bytes( - &self, - _heap: &Arc, - _call_id: CallId, - file: owned::fs::File, - _ctx: &SysOpContext, - ) -> SysOpOutput> { - use tokio::io::AsyncReadExt; - - SysOpOutput::async_op(async move { - let handle = downcast_handle(&file)?; - let mut guard = handle.lock().await; - let f = guard.as_mut().ok_or_else(closed_err)?; - let mut contents = Vec::new(); - f.read_to_end(&mut contents) - .await - .map_err(|e| VmBamlError::Io { - message: format!("Failed to read file: {e}"), - })?; - Ok(contents) - }) +fn closed_io_err() -> VmBamlError { + VmBamlError::Io { + message: "File is closed".into(), } +} +impl io::IoClassFsFile for NativeSysOps { fn read( &self, _heap: &Arc, _call_id: CallId, file: owned::fs::File, - n: i64, + limit: i64, _ctx: &SysOpContext, - ) -> SysOpOutput { - SysOpOutput::async_op(async move { - let bytes = read_up_to(&file, n).await?; - String::from_utf8(bytes) - .map_err(|e| VmBamlError::ParseError { - message: format!("Invalid UTF-8 in file: {e}"), - }) - .map_err(VmRustFnError::from) - }) - } - - fn read_bytes( - &self, - _heap: &Arc, - _call_id: CallId, - file: owned::fs::File, - n: i64, - _ctx: &SysOpContext, - ) -> SysOpOutput> { + ) -> SysOpOutput>> { SysOpOutput::async_op( - async move { read_up_to(&file, n).await.map_err(VmRustFnError::from) }, + async move { read_up_to(&file, limit).await.map_err(VmRustFnError::from) }, ) } @@ -555,46 +499,55 @@ impl io::IoClassFsFile for NativeSysOps { }) } - fn write( + fn write_some( &self, _heap: &Arc, _call_id: CallId, file: owned::fs::File, - data: String, + data: Vec, _ctx: &SysOpContext, ) -> SysOpOutput { SysOpOutput::async_op(async move { - write_all_bytes(&file, data.into_bytes()) + write_all_bytes(&file, data) .await .map_err(VmRustFnError::from) }) } - fn write_bytes( + fn flush( &self, _heap: &Arc, _call_id: CallId, file: owned::fs::File, - data: Vec, _ctx: &SysOpContext, - ) -> SysOpOutput { + ) -> SysOpOutput<()> { + use tokio::io::AsyncWriteExt; + SysOpOutput::async_op(async move { - write_all_bytes(&file, data) - .await - .map_err(VmRustFnError::from) + let handle = downcast_handle(&file)?; + let mut guard = handle.lock().await; + let f = guard.as_mut().ok_or_else(closed_io_err)?; + f.flush().await.map_err(|e| VmBamlError::Io { + message: format!("Failed to flush: {e}"), + })?; + Ok(()) }) } } -async fn read_up_to(file: &owned::fs::File, n: i64) -> Result, VmBamlError> { +async fn read_up_to(file: &owned::fs::File, limit: i64) -> Result>, VmBamlError> { use tokio::io::AsyncReadExt; - let cap = u64::try_from(n).map_err(|_| VmBamlError::InvalidArgument { - message: format!("Negative read length: {n}"), - })?; + let limit = usize::try_from(limit) + .ok() + .filter(|limit| *limit > 0) + .ok_or_else(|| VmBamlError::Io { + message: "read limit must be greater than zero".to_string(), + })?; + let cap = limit.min(MAX_READ_CHUNK) as u64; let handle = downcast_handle(file)?; let mut guard = handle.lock().await; - let f = guard.as_mut().ok_or_else(closed_err)?; + let f = guard.as_mut().ok_or_else(closed_io_err)?; let mut buf = Vec::new(); f.take(cap) .read_to_end(&mut buf) @@ -602,7 +555,7 @@ async fn read_up_to(file: &owned::fs::File, n: i64) -> Result, VmBamlErr .map_err(|e| VmBamlError::Io { message: format!("Failed to read file: {e}"), })?; - Ok(buf) + Ok((!buf.is_empty()).then_some(buf)) } async fn write_all_bytes(file: &owned::fs::File, data: Vec) -> Result { @@ -610,7 +563,7 @@ async fn write_all_bytes(file: &owned::fs::File, data: Vec) -> Result; -type NativeProcessLineResult = Result; struct LiveProcessHandle { kill_tx: tokio::sync::watch::Sender, exit_rx: tokio::sync::watch::Receiver>, - stdin: tokio::sync::Mutex>, deadline: Option, timeout_ms: Option, label: String, } -struct ProcessLineStreamHandle { - lines: tokio::sync::Mutex>, - close_tx: tokio::sync::watch::Sender, - closed: std::sync::atomic::AtomicBool, - deadline: Option, - timeout_ms: Option, +struct ReadPipeHandle { + reader: tokio::sync::Mutex>>, label: String, } +struct WritePipeHandle { + writer: tokio::sync::Mutex>>, + label: String, +} + +fn stderr_stdio( + mode: Option<&BexExternalValue>, +) -> Result<(std::process::Stdio, bool), VmBamlError> { + use std::process::Stdio; + + let Some(mode) = mode else { + return Ok((Stdio::inherit(), false)); + }; + let BexExternalValue::Variant { variant_name, .. } = mode else { + return Err(VmBamlError::InvalidArgument { + message: format!("Invalid stderr mode: {}", mode.type_name()), + }); + }; + match variant_name.as_str() { + "Inherit" => Ok((Stdio::inherit(), false)), + "Pipe" => Ok((Stdio::piped(), true)), + "Discard" => Ok((Stdio::null(), false)), + other => Err(VmBamlError::InvalidArgument { + message: format!( + "Unsupported stderr mode '{other}': expected Inherit, Pipe, or Discard" + ), + }), + } +} + +fn downcast_read_pipe(pipe: &owned::sys::ReadPipe) -> Result, VmBamlError> { + pipe._pipe + .clone() + .downcast::() + .map_err(|_| VmBamlError::DevOther { + message: "Invalid read pipe handle type".into(), + }) +} + +fn downcast_write_pipe(pipe: &owned::sys::WritePipe) -> Result, VmBamlError> { + pipe._pipe + .clone() + .downcast::() + .map_err(|_| VmBamlError::DevOther { + message: "Invalid write pipe handle type".into(), + }) +} + +fn read_pipe( + reader: impl tokio::io::AsyncRead + Send + Unpin + 'static, + label: String, +) -> owned::sys::ReadPipe { + owned::sys::ReadPipe { + _pipe: Arc::new(ReadPipeHandle { + reader: tokio::sync::Mutex::new(Some(Box::new(reader))), + label, + }), + } +} + +fn write_pipe( + writer: impl tokio::io::AsyncWrite + Send + Unpin + 'static, + label: String, +) -> owned::sys::WritePipe { + owned::sys::WritePipe { + _pipe: Arc::new(WritePipeHandle { + writer: tokio::sync::Mutex::new(Some(Box::new(writer))), + label, + }), + } +} + fn downcast_process_handle( process: &owned::sys::Process, ) -> Result, VmBamlError> { @@ -1298,18 +1317,6 @@ fn downcast_process_handle( }) } -fn downcast_process_line_stream_handle( - stream: &owned::sys::ProcessLineStream, -) -> Result, VmBamlError> { - stream - ._handle - .clone() - .downcast::() - .map_err(|_| VmBamlError::DevOther { - message: "Invalid process stdout stream handle type".into(), - }) -} - fn process_exit_from_status(status: std::process::ExitStatus) -> owned::sys::ProcessExit { #[cfg(unix)] let signal = { @@ -1346,103 +1353,43 @@ async fn receive_process_exit( } } -fn start_stdout_line_reader( - stdout: tokio::process::ChildStdout, - deadline: Option, - timeout_ms: Option, - label: String, -) -> owned::sys::ProcessLineStream { - let (lines_tx, lines_rx) = tokio::sync::mpsc::channel(32); - let (close_tx, mut close_rx) = tokio::sync::watch::channel(false); - let reader_label = label.clone(); - - tokio::spawn(async move { - use tokio::io::AsyncBufReadExt as _; - - let mut reader = tokio::io::BufReader::new(stdout); - let mut bytes = Vec::new(); - loop { - bytes.clear(); - let read_result = tokio::select! { - biased; - _ = close_rx.changed() => break, - result = reader.read_until(b'\n', &mut bytes) => result, - }; - - match read_result { - Ok(0) => break, - Ok(_) => { - if bytes.last() == Some(&b'\n') { - bytes.pop(); - } - if bytes.last() == Some(&b'\r') { - bytes.pop(); - } - let line = String::from_utf8_lossy(&bytes).into_owned(); - tokio::select! { - biased; - _ = close_rx.changed() => break, - result = lines_tx.send(Ok(line)) => { - if result.is_err() { - break; - } - } - } - } - Err(error) => { - let _ = lines_tx - .send(Err(format!( - "Failed to read stdout from '{reader_label}': {error}" - ))) - .await; - break; - } - } - } - }); - - owned::sys::ProcessLineStream { - _handle: Arc::new(ProcessLineStreamHandle { - lines: tokio::sync::Mutex::new(lines_rx), - close_tx, - closed: std::sync::atomic::AtomicBool::new(false), - deadline, - timeout_ms, - label, - }), - } -} - -impl io::IoClassSysProcessLineStream for NativeSysOps { - fn _next( +impl io::IoClassSysReadPipe for NativeSysOps { + fn read( &self, _heap: &Arc, _call_id: CallId, - processlinestream: owned::sys::ProcessLineStream, + readpipe: owned::sys::ReadPipe, + limit: i64, _ctx: &SysOpContext, - ) -> SysOpOutput> { + ) -> SysOpOutput>> { + use tokio::io::AsyncReadExt as _; + + let limit = match usize::try_from(limit).ok().filter(|limit| *limit > 0) { + Some(limit) => limit.min(MAX_READ_CHUNK), + None => { + return SysOpOutput::err(VmBamlError::Io { + message: "read limit must be greater than zero".to_string(), + }); + } + }; SysOpOutput::async_op(async move { - let handle = downcast_process_line_stream_handle(&processlinestream)?; - if handle.closed.load(std::sync::atomic::Ordering::Acquire) { + let handle = downcast_read_pipe(&readpipe)?; + let mut guard = handle.reader.lock().await; + let reader = guard.as_mut().ok_or_else(|| VmBamlError::Io { + message: format!("Read pipe for '{}' is closed", handle.label), + })?; + let mut buffer = vec![0u8; limit]; + let read = reader + .read(&mut buffer) + .await + .map_err(|error| VmBamlError::Io { + message: format!("Failed to read from '{}': {error}", handle.label), + })?; + if read == 0 { return Ok(None); } - let mut lines = handle.lines.lock().await; - let next_line = if let Some(deadline) = handle.deadline { - match tokio::time::timeout_at(deadline, lines.recv()).await { - Ok(line) => line, - Err(_) => { - return Err(process_timeout_error(&handle.label, handle.timeout_ms).into()); - } - } - } else { - lines.recv().await - }; - - match next_line { - Some(Ok(line)) => Ok(Some(line)), - Some(Err(message)) => Err(VmBamlError::Io { message }.into()), - None => Ok(None), - } + buffer.truncate(read); + Ok(Some(buffer)) }) } @@ -1450,62 +1397,91 @@ impl io::IoClassSysProcessLineStream for NativeSysOps { &self, _heap: &Arc, _call_id: CallId, - processlinestream: owned::sys::ProcessLineStream, + readpipe: owned::sys::ReadPipe, _ctx: &SysOpContext, ) -> SysOpOutput<()> { - if let Ok(handle) = downcast_process_line_stream_handle(&processlinestream) { - handle - .closed - .store(true, std::sync::atomic::Ordering::Release); - let _ = handle.close_tx.send(true); - } - SysOpOutput::ok(()) + SysOpOutput::async_op(async move { + let handle = downcast_read_pipe(&readpipe)?; + handle.reader.lock().await.take(); + Ok(()) + }) } } -impl io::IoClassSysProcess for NativeSysOps { - fn write_stdin( +impl io::IoClassSysWritePipe for NativeSysOps { + fn write_some( &self, _heap: &Arc, _call_id: CallId, - process: owned::sys::Process, - data: String, + writepipe: owned::sys::WritePipe, + data: Vec, _ctx: &SysOpContext, - ) -> SysOpOutput<()> { + ) -> SysOpOutput { + use tokio::io::AsyncWriteExt as _; + SysOpOutput::async_op(async move { - use tokio::io::AsyncWriteExt as _; + let handle = downcast_write_pipe(&writepipe)?; + let mut guard = handle.writer.lock().await; + let writer = guard.as_mut().ok_or_else(|| VmBamlError::Io { + message: format!("Write pipe for '{}' is closed", handle.label), + })?; + let written = writer.write(&data).await.map_err(|error| VmBamlError::Io { + message: format!("Failed to write to '{}': {error}", handle.label), + })?; + i64::try_from(written).map_err(|_| { + VmRustFnError::from(VmBamlError::Io { + message: "pipe write count exceeds int range".to_string(), + }) + }) + }) + } - let handle = downcast_process_handle(&process)?; - let mut stdin = handle.stdin.lock().await; - let pipe = stdin.as_mut().ok_or_else(|| VmBamlError::Io { - message: format!("Stdin for '{}' is not open", handle.label), + fn flush( + &self, + _heap: &Arc, + _call_id: CallId, + writepipe: owned::sys::WritePipe, + _ctx: &SysOpContext, + ) -> SysOpOutput<()> { + use tokio::io::AsyncWriteExt as _; + + SysOpOutput::async_op(async move { + let handle = downcast_write_pipe(&writepipe)?; + let mut guard = handle.writer.lock().await; + let writer = guard.as_mut().ok_or_else(|| VmBamlError::Io { + message: format!("Write pipe for '{}' is closed", handle.label), })?; - pipe.write_all(data.as_bytes()) - .await - .map_err(|error| VmBamlError::Io { - message: format!("Failed to write stdin to '{}': {error}", handle.label), - })?; - pipe.flush().await.map_err(|error| VmBamlError::Io { - message: format!("Failed to flush stdin for '{}': {error}", handle.label), + writer.flush().await.map_err(|error| VmBamlError::Io { + message: format!("Failed to flush '{}': {error}", handle.label), })?; Ok(()) }) } - fn close_stdin( + fn close( &self, _heap: &Arc, _call_id: CallId, - process: owned::sys::Process, + writepipe: owned::sys::WritePipe, _ctx: &SysOpContext, ) -> SysOpOutput<()> { + use tokio::io::AsyncWriteExt as _; + SysOpOutput::async_op(async move { - let handle = downcast_process_handle(&process)?; - handle.stdin.lock().await.take(); + let handle = downcast_write_pipe(&writepipe)?; + let mut writer = handle.writer.lock().await; + if let Some(writer) = writer.as_mut() { + writer.flush().await.map_err(|error| VmBamlError::Io { + message: format!("Failed to flush '{}' before close: {error}", handle.label), + })?; + } + writer.take(); Ok(()) }) } +} +impl io::IoClassSysProcess for NativeSysOps { fn wait( &self, _heap: &Arc, @@ -1557,16 +1533,23 @@ impl io::IoClassSysProcess for NativeSysOps { process: owned::sys::Process, _ctx: &SysOpContext, ) -> SysOpOutput<()> { - if let Ok(handle) = downcast_process_handle(&process) { - let _ = handle.kill_tx.send(true); - } - if let Ok(handle) = downcast_process_line_stream_handle(&process.stdout) { - handle - .closed - .store(true, std::sync::atomic::Ordering::Release); - let _ = handle.close_tx.send(true); - } - SysOpOutput::ok(()) + SysOpOutput::async_op(async move { + if let Ok(handle) = downcast_process_handle(&process) { + let _ = handle.kill_tx.send(true); + } + if let Ok(handle) = downcast_write_pipe(&process.stdin) { + handle.writer.lock().await.take(); + } + for pipe in [Some(&process.stdout), process.stderr.as_ref()] + .into_iter() + .flatten() + { + if let Ok(handle) = downcast_read_pipe(pipe) { + handle.reader.lock().await.take(); + } + } + Ok(()) + }) } } @@ -1714,40 +1697,28 @@ impl io::IoNamespaceSys for NativeSysOps { .map(|(key, value)| (key.as_str(), value.as_str())), ); } - if options.stdin.is_some() || options.keep_stdin_open == Some(true) { - cmd.stdin(Stdio::piped()); - } } - cmd.stdout(Stdio::piped()) - .stderr(Stdio::inherit()) + let (stderr_stdio, stderr_piped) = + stderr_stdio(options.as_ref().and_then(|options| options.stderr.as_ref()))?; + cmd.stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(stderr_stdio) .kill_on_drop(true); let mut child = cmd.spawn().map_err(|error| VmBamlError::Io { message: format!("Failed to spawn '{program}': {error}"), })?; - let stdout = child.stdout.take().ok_or_else(|| VmBamlError::Io { - message: format!("Failed to capture stdout from '{program}'"), - })?; - - let keep_stdin_open = options - .as_ref() - .and_then(|options| options.keep_stdin_open) - .unwrap_or(false); - let mut stdin = child.stdin.take(); - if let Some(stdin_data) = options.as_ref().and_then(|options| options.stdin.as_ref()) { - if let Some(stdin) = stdin.as_mut() { - stdin - .write_all(stdin_data.as_bytes()) - .await - .map_err(|error| VmBamlError::Io { - message: format!("Failed to write stdin to '{program}': {error}"), - })?; - } - } - if !keep_stdin_open { - stdin = None; - } + let missing_pipe = |stream: &str| VmBamlError::Io { + message: format!("Failed to capture {stream} from '{program}'"), + }; + let child_stdin = child.stdin.take().ok_or_else(|| missing_pipe("stdin"))?; + let child_stdout = child.stdout.take().ok_or_else(|| missing_pipe("stdout"))?; + let child_stderr = if stderr_piped { + Some(child.stderr.take().ok_or_else(|| missing_pipe("stderr"))?) + } else { + None + }; let timeout_ms = options .as_ref() @@ -1758,7 +1729,10 @@ impl io::IoNamespaceSys for NativeSysOps { milliseconds.cast_unsigned(), )) }); - let stdout = start_stdout_line_reader(stdout, deadline, timeout_ms, program.clone()); + let stdin = write_pipe(child_stdin, format!("{program} stdin")); + let stdout = read_pipe(child_stdout, format!("{program} stdout")); + let stderr = child_stderr + .map(|child_stderr| read_pipe(child_stderr, format!("{program} stderr"))); let (kill_tx, mut kill_rx) = tokio::sync::watch::channel(false); let (exit_tx, exit_rx) = tokio::sync::watch::channel(None); @@ -1777,12 +1751,38 @@ impl io::IoNamespaceSys for NativeSysOps { let _ = exit_tx.send(Some(exit)); }); + if let Some(stdin_data) = options.as_ref().and_then(|options| options.stdin.as_ref()) { + let handle = downcast_write_pipe(&stdin)?; + let mut writer = handle.writer.lock().await; + let write_result = if let Some(writer) = writer.as_mut() { + let write = writer.write_all(stdin_data.as_bytes()); + let result = + match deadline { + Some(deadline) => tokio::time::timeout_at(deadline, write) + .await + .map_err(|_| { + let _ = kill_tx.send(true); + process_timeout_error(&program, timeout_ms) + })?, + None => write.await, + }; + result.map_err(|error| VmBamlError::Io { + message: format!("Failed to write stdin to '{program}': {error}"), + }) + } else { + Ok(()) + }; + writer.take(); + write_result?; + } + Ok(owned::sys::Process { + stdin, stdout, + stderr, _handle: Arc::new(LiveProcessHandle { kill_tx, exit_rx, - stdin: tokio::sync::Mutex::new(stdin), deadline, timeout_ms, label: program, @@ -1969,88 +1969,69 @@ impl io::IoClassNetTcpStream for NativeSysOps { }) } - fn _read( + fn read( &self, _heap: &Arc, _call_id: CallId, stream: owned::net::TcpStream, - timeout_nanos: Arc, + limit: i64, _ctx: &SysOpContext, - ) -> SysOpOutput> { + ) -> SysOpOutput>> { use tokio::io::AsyncReadExt; - let timeout = timeout_from_nanos(&timeout_nanos); + let limit = match usize::try_from(limit).ok().filter(|limit| *limit > 0) { + Some(limit) => limit.min(MAX_READ_CHUNK), + None => { + return SysOpOutput::err(VmBamlError::Io { + message: "read limit must be greater than zero".to_string(), + }); + } + }; SysOpOutput::async_op(async move { let handle = downcast_tcpstream(&stream)?; let mut guard = handle.lock().await; let stream = guard.as_mut().ok_or_else(|| VmBamlError::Io { message: "TcpStream is closed".to_string(), })?; - let mut buffer = vec![0u8; 4096]; - let read = stream.read(&mut buffer); - let n = match timeout { - Some(dur) => match tokio::time::timeout(dur, read).await { - Ok(result) => result, - Err(_elapsed) => { - return Err(VmBamlError::Timeout { - message: "Reading from socket timed out".to_string(), - duration_ms: i64::try_from(dur.as_millis()).ok(), - } - .into()); - } - }, - None => read.await, + let mut buffer = vec![0u8; limit]; + let read = stream + .read(&mut buffer) + .await + .map_err(|e| VmBamlError::Io { + message: format!("Failed to read from socket: {e}"), + })?; + if read == 0 { + return Ok(None); } - .map_err(|e| VmBamlError::Io { - message: format!("Failed to read from socket: {e}"), - })?; - buffer.truncate(n); - Ok(buffer) + buffer.truncate(read); + Ok(Some(buffer)) }) } - fn _write( + fn write_some( &self, _heap: &Arc, _call_id: CallId, stream: owned::net::TcpStream, data: Vec, - timeout_nanos: Arc, _ctx: &SysOpContext, - ) -> SysOpOutput<()> { + ) -> SysOpOutput { use tokio::io::AsyncWriteExt; - let timeout = timeout_from_nanos(&timeout_nanos); SysOpOutput::async_op(async move { let handle = downcast_tcpstream(&stream)?; let mut guard = handle.lock().await; let stream = guard.as_mut().ok_or_else(|| VmBamlError::Io { message: "TcpStream is closed".to_string(), })?; - // The whole write (every byte flushed) shares one deadline. - let write = async { - stream.write_all(&data).await.map_err(|e| VmBamlError::Io { - message: format!("Failed to write to socket: {e}"), - })?; - stream.flush().await.map_err(|e| VmBamlError::Io { - message: format!("Failed to flush socket: {e}"), - })?; - Ok::<(), VmBamlError>(()) - }; - match timeout { - Some(dur) => match tokio::time::timeout(dur, write).await { - Ok(result) => result?, - Err(_elapsed) => { - return Err(VmBamlError::Timeout { - message: "Writing to socket timed out".to_string(), - duration_ms: i64::try_from(dur.as_millis()).ok(), - } - .into()); - } - }, - None => write.await?, - } - Ok(()) + let written = stream.write(&data).await.map_err(|e| VmBamlError::Io { + message: format!("Failed to write to socket: {e}"), + })?; + i64::try_from(written).map_err(|_| { + VmRustFnError::from(VmBamlError::Io { + message: "socket write count exceeds int range".to_string(), + }) + }) }) } diff --git a/baml_language/crates/sys_ops/src/lib.rs b/baml_language/crates/sys_ops/src/lib.rs index 2cd44cd62ff..3583bd4f41f 100644 --- a/baml_language/crates/sys_ops/src/lib.rs +++ b/baml_language/crates/sys_ops/src/lib.rs @@ -1020,28 +1020,6 @@ impl io::IoClassReflectSession for DefaultIoOps { impl io::IoNamespaceReflect for DefaultIoOps {} impl io::IoClassFsFile for DefaultIoOps { - fn text( - &self, - _h: &Arc, - _c: CallId, - _f: io::owned::fs::File, - _ctx: &SysOpContext, - ) -> SysOpOutput { - SysOpOutput::err(VmBamlError::Unsupported { - message: "Operation not supported on this platform".to_string(), - }) - } - fn bytes( - &self, - _h: &Arc, - _c: CallId, - _f: io::owned::fs::File, - _ctx: &SysOpContext, - ) -> SysOpOutput> { - SysOpOutput::err(VmBamlError::Unsupported { - message: "Operation not supported on this platform".to_string(), - }) - } fn read( &self, _h: &Arc, @@ -1049,19 +1027,7 @@ impl io::IoClassFsFile for DefaultIoOps { _f: io::owned::fs::File, _n: i64, _ctx: &SysOpContext, - ) -> SysOpOutput { - SysOpOutput::err(VmBamlError::Unsupported { - message: "Operation not supported on this platform".to_string(), - }) - } - fn read_bytes( - &self, - _h: &Arc, - _c: CallId, - _f: io::owned::fs::File, - _n: i64, - _ctx: &SysOpContext, - ) -> SysOpOutput> { + ) -> SysOpOutput>> { SysOpOutput::err(VmBamlError::Unsupported { message: "Operation not supported on this platform".to_string(), }) @@ -1090,26 +1056,25 @@ impl io::IoClassFsFile for DefaultIoOps { message: "Operation not supported on this platform".to_string(), }) } - fn write( + fn write_some( &self, _h: &Arc, _c: CallId, _f: io::owned::fs::File, - _data: String, + _data: Vec, _ctx: &SysOpContext, ) -> SysOpOutput { SysOpOutput::err(VmBamlError::Unsupported { message: "Operation not supported on this platform".to_string(), }) } - fn write_bytes( + fn flush( &self, _h: &Arc, _c: CallId, _f: io::owned::fs::File, - _data: Vec, _ctx: &SysOpContext, - ) -> SysOpOutput { + ) -> SysOpOutput<()> { SysOpOutput::err(VmBamlError::Unsupported { message: "Operation not supported on this platform".to_string(), }) @@ -1541,27 +1506,26 @@ impl io::IoClassNetTcpStream for DefaultIoOps { message: "Operation not supported on this platform".to_string(), }) } - fn _read( + fn read( &self, _h: &Arc, _c: CallId, _s: io::owned::net::TcpStream, - _timeout_nanos: Arc, + _limit: i64, _ctx: &SysOpContext, - ) -> SysOpOutput> { + ) -> SysOpOutput>> { SysOpOutput::err(VmBamlError::Unsupported { message: "Operation not supported on this platform".to_string(), }) } - fn _write( + fn write_some( &self, _h: &Arc, _c: CallId, _s: io::owned::net::TcpStream, _data: Vec, - _timeout_nanos: Arc, _ctx: &SysOpContext, - ) -> SysOpOutput<()> { + ) -> SysOpOutput { SysOpOutput::err(VmBamlError::Unsupported { message: "Operation not supported on this platform".to_string(), }) @@ -1741,20 +1705,19 @@ impl io::IoNamespaceIo for DefaultIoOps { } impl io::IoClassSysProcess for DefaultIoOps { - fn write_stdin( + fn wait( &self, _h: &Arc, _c: CallId, _process: io::owned::sys::Process, - _data: String, _ctx: &SysOpContext, - ) -> SysOpOutput<()> { + ) -> SysOpOutput { SysOpOutput::err(VmBamlError::Unsupported { message: "Operation not supported on this platform".to_string(), }) } - fn close_stdin( + fn kill( &self, _h: &Arc, _c: CallId, @@ -1766,49 +1729,65 @@ impl io::IoClassSysProcess for DefaultIoOps { }) } - fn wait( + fn close( &self, _h: &Arc, _c: CallId, _process: io::owned::sys::Process, _ctx: &SysOpContext, - ) -> SysOpOutput { + ) -> SysOpOutput<()> { + SysOpOutput::ok(()) + } +} + +impl io::IoClassSysReadPipe for DefaultIoOps { + fn read( + &self, + _h: &Arc, + _c: CallId, + _readpipe: io::owned::sys::ReadPipe, + _limit: i64, + _ctx: &SysOpContext, + ) -> SysOpOutput>> { SysOpOutput::err(VmBamlError::Unsupported { message: "Operation not supported on this platform".to_string(), }) } - fn kill( + fn close( &self, _h: &Arc, _c: CallId, - _process: io::owned::sys::Process, + _readpipe: io::owned::sys::ReadPipe, _ctx: &SysOpContext, ) -> SysOpOutput<()> { SysOpOutput::err(VmBamlError::Unsupported { message: "Operation not supported on this platform".to_string(), }) } +} - fn close( +impl io::IoClassSysWritePipe for DefaultIoOps { + fn write_some( &self, _h: &Arc, _c: CallId, - _process: io::owned::sys::Process, + _writepipe: io::owned::sys::WritePipe, + _data: Vec, _ctx: &SysOpContext, - ) -> SysOpOutput<()> { - SysOpOutput::ok(()) + ) -> SysOpOutput { + SysOpOutput::err(VmBamlError::Unsupported { + message: "Operation not supported on this platform".to_string(), + }) } -} -impl io::IoClassSysProcessLineStream for DefaultIoOps { - fn _next( + fn flush( &self, _h: &Arc, _c: CallId, - _processlinestream: io::owned::sys::ProcessLineStream, + _writepipe: io::owned::sys::WritePipe, _ctx: &SysOpContext, - ) -> SysOpOutput> { + ) -> SysOpOutput<()> { SysOpOutput::err(VmBamlError::Unsupported { message: "Operation not supported on this platform".to_string(), }) @@ -1818,10 +1797,12 @@ impl io::IoClassSysProcessLineStream for DefaultIoOps { &self, _h: &Arc, _c: CallId, - _processlinestream: io::owned::sys::ProcessLineStream, + _writepipe: io::owned::sys::WritePipe, _ctx: &SysOpContext, ) -> SysOpOutput<()> { - SysOpOutput::ok(()) + SysOpOutput::err(VmBamlError::Unsupported { + message: "Operation not supported on this platform".to_string(), + }) } } @@ -2282,28 +2263,10 @@ impl IoSysOpsBuilder { t.__glue_baml_fs_write_bytes(heap, permit, args, ctx, call_id) }) }; - self.inner.baml_fs_file_text = { - let t = instance.clone(); - Arc::new(move |heap, permit, args, ctx, call_id| { - t.__glue_baml_fs_file_text(heap, permit, args, ctx, call_id) - }) - }; - self.inner.baml_fs_file_bytes = { + self.inner.baml_fs_file_root_io_read_read = { let t = instance.clone(); Arc::new(move |heap, permit, args, ctx, call_id| { - t.__glue_baml_fs_file_bytes(heap, permit, args, ctx, call_id) - }) - }; - self.inner.baml_fs_file_read = { - let t = instance.clone(); - Arc::new(move |heap, permit, args, ctx, call_id| { - t.__glue_baml_fs_file_read(heap, permit, args, ctx, call_id) - }) - }; - self.inner.baml_fs_file_read_bytes = { - let t = instance.clone(); - Arc::new(move |heap, permit, args, ctx, call_id| { - t.__glue_baml_fs_file_read_bytes(heap, permit, args, ctx, call_id) + t.__glue_baml_fs_file_root_io_read_read(heap, permit, args, ctx, call_id) }) }; self.inner.baml_fs_file_close = { @@ -2318,16 +2281,16 @@ impl IoSysOpsBuilder { t.__glue_baml_fs_file_seek_from(heap, permit, args, ctx, call_id) }) }; - self.inner.baml_fs_file_write = { + self.inner.baml_fs_file_root_io_write_write_some = { let t = instance.clone(); Arc::new(move |heap, permit, args, ctx, call_id| { - t.__glue_baml_fs_file_write(heap, permit, args, ctx, call_id) + t.__glue_baml_fs_file_root_io_write_write_some(heap, permit, args, ctx, call_id) }) }; - self.inner.baml_fs_file_write_bytes = { + self.inner.baml_fs_file_root_io_write_flush = { let t = instance.clone(); Arc::new(move |heap, permit, args, ctx, call_id| { - t.__glue_baml_fs_file_write_bytes(heap, permit, args, ctx, call_id) + t.__glue_baml_fs_file_root_io_write_flush(heap, permit, args, ctx, call_id) }) }; self.inner.baml_fs_read_dir = { @@ -2547,16 +2510,18 @@ impl IoSysOpsBuilder { t.__glue_baml_net_tcpstream__connect(heap, permit, args, ctx, call_id) }) }; - self.inner.baml_net_tcpstream__read = { + self.inner.baml_net_tcpstream_root_io_read_read = { let t = instance.clone(); Arc::new(move |heap, permit, args, ctx, call_id| { - t.__glue_baml_net_tcpstream__read(heap, permit, args, ctx, call_id) + t.__glue_baml_net_tcpstream_root_io_read_read(heap, permit, args, ctx, call_id) }) }; - self.inner.baml_net_tcpstream__write = { + self.inner.baml_net_tcpstream_root_io_write_write_some = { let t = instance.clone(); Arc::new(move |heap, permit, args, ctx, call_id| { - t.__glue_baml_net_tcpstream__write(heap, permit, args, ctx, call_id) + t.__glue_baml_net_tcpstream_root_io_write_write_some( + heap, permit, args, ctx, call_id, + ) }) }; self.inner.baml_net_tcpstream_close = { @@ -2623,12 +2588,68 @@ impl IoSysOpsBuilder { mut self, instance: Arc, ) -> Self { + self.inner.baml_sys_process_wait = { + let t = instance.clone(); + Arc::new(move |heap, permit, args, ctx, call_id| { + t.__glue_baml_sys_process_wait(heap, permit, args, ctx, call_id) + }) + }; + self.inner.baml_sys_process_kill = { + let t = instance.clone(); + Arc::new(move |heap, permit, args, ctx, call_id| { + t.__glue_baml_sys_process_kill(heap, permit, args, ctx, call_id) + }) + }; + self.inner.baml_sys_process_close = { + let t = instance.clone(); + Arc::new(move |heap, permit, args, ctx, call_id| { + t.__glue_baml_sys_process_close(heap, permit, args, ctx, call_id) + }) + }; + self.inner.baml_sys_readpipe_close = { + let t = instance.clone(); + Arc::new(move |heap, permit, args, ctx, call_id| { + t.__glue_baml_sys_readpipe_close(heap, permit, args, ctx, call_id) + }) + }; + self.inner.baml_sys_readpipe_root_io_read_read = { + let t = instance.clone(); + Arc::new(move |heap, permit, args, ctx, call_id| { + t.__glue_baml_sys_readpipe_root_io_read_read(heap, permit, args, ctx, call_id) + }) + }; + self.inner.baml_sys_writepipe_close = { + let t = instance.clone(); + Arc::new(move |heap, permit, args, ctx, call_id| { + t.__glue_baml_sys_writepipe_close(heap, permit, args, ctx, call_id) + }) + }; + self.inner.baml_sys_writepipe_root_io_write_write_some = { + let t = instance.clone(); + Arc::new(move |heap, permit, args, ctx, call_id| { + t.__glue_baml_sys_writepipe_root_io_write_write_some( + heap, permit, args, ctx, call_id, + ) + }) + }; + self.inner.baml_sys_writepipe_root_io_write_flush = { + let t = instance.clone(); + Arc::new(move |heap, permit, args, ctx, call_id| { + t.__glue_baml_sys_writepipe_root_io_write_flush(heap, permit, args, ctx, call_id) + }) + }; self.inner.baml_sys_exec = { let t = instance.clone(); Arc::new(move |heap, permit, args, ctx, call_id| { t.__glue_baml_sys_exec(heap, permit, args, ctx, call_id) }) }; + self.inner.baml_sys_start_process = { + let t = instance.clone(); + Arc::new(move |heap, permit, args, ctx, call_id| { + t.__glue_baml_sys_start_process(heap, permit, args, ctx, call_id) + }) + }; self.inner.baml_sys_shell = { let t = instance.clone(); Arc::new(move |heap, permit, args, ctx, call_id| { @@ -2641,6 +2662,12 @@ impl IoSysOpsBuilder { t.__glue_baml_sys_sleep(heap, permit, args, ctx, call_id) }) }; + self.inner.baml_sys_collect_garbage = { + let t = instance.clone(); + Arc::new(move |heap, permit, args, ctx, call_id| { + t.__glue_baml_sys_collect_garbage(heap, permit, args, ctx, call_id) + }) + }; self.inner.baml_sys_pid = { let t = instance; Arc::new(move |heap, permit, args, ctx, call_id| { diff --git a/baml_language/crates/sys_wasm/src/web_sysops.rs b/baml_language/crates/sys_wasm/src/web_sysops.rs index 83a355f1ddc..455767aabe8 100644 --- a/baml_language/crates/sys_wasm/src/web_sysops.rs +++ b/baml_language/crates/sys_wasm/src/web_sysops.rs @@ -294,24 +294,6 @@ impl WebFs { } impl io::IoClassFsFile for WebFs { - fn text( - &self, - _h: &Arc, - _c: CallId, - _f: io::owned::fs::File, - _ctx: &SysOpContext, - ) -> SysOpOutput { - unsupported() - } - fn bytes( - &self, - _h: &Arc, - _c: CallId, - _f: io::owned::fs::File, - _ctx: &SysOpContext, - ) -> SysOpOutput> { - unsupported() - } fn read( &self, _h: &Arc, @@ -319,17 +301,7 @@ impl io::IoClassFsFile for WebFs { _f: io::owned::fs::File, _n: i64, _ctx: &SysOpContext, - ) -> SysOpOutput { - unsupported() - } - fn read_bytes( - &self, - _h: &Arc, - _c: CallId, - _f: io::owned::fs::File, - _n: i64, - _ctx: &SysOpContext, - ) -> SysOpOutput> { + ) -> SysOpOutput>> { unsupported() } fn close( @@ -352,24 +324,23 @@ impl io::IoClassFsFile for WebFs { ) -> SysOpOutput { unsupported() } - fn write( + fn write_some( &self, _h: &Arc, _c: CallId, _f: io::owned::fs::File, - _data: String, + _data: Vec, _ctx: &SysOpContext, ) -> SysOpOutput { unsupported() } - fn write_bytes( + fn flush( &self, _h: &Arc, _c: CallId, _f: io::owned::fs::File, - _data: Vec, _ctx: &SysOpContext, - ) -> SysOpOutput { + ) -> SysOpOutput<()> { unsupported() } } diff --git a/baml_language/sdk_tests/crates/csharp/stdlib_resources/Program.cs b/baml_language/sdk_tests/crates/csharp/stdlib_resources/Program.cs index d281a95c746..b178022ebd4 100644 --- a/baml_language/sdk_tests/crates/csharp/stdlib_resources/Program.cs +++ b/baml_language/sdk_tests/crates/csharp/stdlib_resources/Program.cs @@ -29,12 +29,10 @@ () => Baml.Fs.Functions.Open(tempPath, "invalid-mode")); using Baml.Fs.File reader = Baml.Fs.Functions.Open(tempPath, "r"); - // Load-bearing parity assertion: separate managed-to-native calls must - // operate on the same native File object and advance one shared cursor. - Require(reader.Read(3) == "012", "first native File.read changed"); - Require(await reader.ReadAsync(3) == "345", "native File cursor did not persist across calls"); + Require(reader.SeekFrom("current", 3) == 3, "first relative seek changed"); + Require(await reader.SeekFromAsync("current", 3) == 6, "file cursor did not persist across calls"); Require(reader.SeekFrom("start", 0) == 0, "native File.seek_from did not rewind"); - Require(reader.Read(2) == "01", "native File.read after seek changed"); + Require(reader.SeekFrom("current", 2) == 2, "relative seek after rewind changed"); Require(reader.Text() == "23456789", "native File.text ignored the current cursor"); _ = reader.Close(); @@ -45,18 +43,16 @@ "baml.fs.write_bytes returned the wrong byte count"); using (Baml.Fs.File binary = await Baml.Fs.Functions.OpenAsync(binaryPath, "r+")) { - Require( - binary.ReadBytes(2).Span.SequenceEqual(new byte[] { 0, 1 }), - "File.read_bytes changed"); - Require(binary.Write("AZ") == 2, "File.write changed"); - Require(binary.SeekFrom("start", 0) == 0, "binary File.seek_from changed"); + Require(binary.SeekFrom("start", 2) == 2, "binary File.seek_from changed"); + Require(binary.Write("AZ") == 2, "File.write over text changed"); + Require(binary.SeekFrom("start", 0) == 0, "binary File.seek_from did not rewind"); Require( (await binary.BytesAsync()).Span.SequenceEqual(new byte[] { 0, 1, (byte)'A', (byte)'Z' }), "File.bytes did not preserve raw data and cursor state"); Require(binary.SeekFrom("end", 0) == 4, "binary File end seek changed"); Require( - await binary.WriteBytesAsync(new byte[] { 5, 6 }) == 2, - "File.write_bytes changed"); + await binary.WriteAsync(new ReadOnlyMemory(new byte[] { 5, 6 })) == 2, + "File.write over raw bytes changed"); _ = await binary.CloseAsync(); } diff --git a/baml_language/sdk_tests/crates/java/type_shapes/customizable/roundtrip_tests/TestHandles.java b/baml_language/sdk_tests/crates/java/type_shapes/customizable/roundtrip_tests/TestHandles.java index cd5b803623b..06fb4152d26 100644 --- a/baml_language/sdk_tests/crates/java/type_shapes/customizable/roundtrip_tests/TestHandles.java +++ b/baml_language/sdk_tests/crates/java/type_shapes/customizable/roundtrip_tests/TestHandles.java @@ -115,16 +115,13 @@ void test_handles_file_cursor_state_persists_across_calls() throws IOException { try { f = Fns.open(path.toString(), "r"); - // Two successive reads on the *same* handle must advance the - // cursor — the second read continues where the first stopped. - // This is the load-bearing assertion: engine-side file state - // survives across separate host->engine FFI calls. - assertEquals("012", f.read(3L)); - assertEquals("345", f.read(3L)); + // Relative seeks verify that separate calls share one engine-side handle. + assertEquals(3L, f.seek_from("current", 3L)); + assertEquals(6L, f.seek_from("current", 3L)); // Seek back to the start and confirm the cursor actually moved. assertEquals(0L, f.seek_from("start", 0L)); - assertEquals("01", f.read(2L)); + assertEquals(2L, f.seek_from("current", 2L)); // text() reads from the current cursor (now at 2) to EOF. assertEquals("23456789", f.text()); diff --git a/baml_language/sdk_tests/crates/python_pydantic2/type_shapes/customizable/roundtrip_tests/test_handles.py b/baml_language/sdk_tests/crates/python_pydantic2/type_shapes/customizable/roundtrip_tests/test_handles.py index 473e42d9af2..94f0ac823ae 100644 --- a/baml_language/sdk_tests/crates/python_pydantic2/type_shapes/customizable/roundtrip_tests/test_handles.py +++ b/baml_language/sdk_tests/crates/python_pydantic2/type_shapes/customizable/roundtrip_tests/test_handles.py @@ -97,16 +97,13 @@ def test_handles_open_file_returns_file_handle(temp_file): def test_handles_file_cursor_state_persists_across_calls(temp_file): f = baml_open(temp_file, "r") - # Two successive reads on the *same* handle must advance the cursor — - # the second read continues where the first stopped. This is the - # load-bearing assertion: engine-side file state survives across - # separate host→engine FFI calls. - assert f.read(3) == "012" - assert f.read(3) == "345" + # Relative seeks verify that separate calls share one engine-side handle. + assert f.seek_from("current", 3) == 3 + assert f.seek_from("current", 3) == 6 # Seek back to the start and confirm the cursor actually moved. assert f.seek_from("start", 0) == 0 - assert f.read(2) == "01" + assert f.seek_from("current", 2) == 2 # text() reads from the current cursor (now at 2) to EOF. assert f.text() == "23456789" diff --git a/baml_language/sdk_tests/crates/rust/type_shapes/customizable/roundtrip_tests/test_handles.rs b/baml_language/sdk_tests/crates/rust/type_shapes/customizable/roundtrip_tests/test_handles.rs index ce32406a9c8..377891355aa 100644 --- a/baml_language/sdk_tests/crates/rust/type_shapes/customizable/roundtrip_tests/test_handles.rs +++ b/baml_language/sdk_tests/crates/rust/type_shapes/customizable/roundtrip_tests/test_handles.rs @@ -103,16 +103,13 @@ fn test_handles_file_cursor_state_persists_across_calls() { let temp_file = temp_file(); let f = baml_open(temp_file.to_str().unwrap().to_string(), "r".to_string()).unwrap(); - // Two successive reads on the *same* handle must advance the cursor — - // the second read continues where the first stopped. This is the - // load-bearing assertion: engine-side file state survives across - // separate host→engine FFI calls. - assert_eq!(f.read(3).unwrap(), "012"); - assert_eq!(f.read(3).unwrap(), "345"); + // Relative seeks verify that separate calls share one engine-side handle. + assert_eq!(f.seek_from("current".to_string(), 3).unwrap(), 3); + assert_eq!(f.seek_from("current".to_string(), 3).unwrap(), 6); // Seek back to the start and confirm the cursor actually moved. assert_eq!(f.seek_from("start".to_string(), 0).unwrap(), 0); - assert_eq!(f.read(2).unwrap(), "01"); + assert_eq!(f.seek_from("current".to_string(), 2).unwrap(), 2); // text() reads from the current cursor (now at 2) to EOF. assert_eq!(f.text().unwrap(), "23456789"); diff --git a/baml_language/sdk_tests/crates/swift/type_shapes/customizable/roundtrip_tests/TestHandles.swift b/baml_language/sdk_tests/crates/swift/type_shapes/customizable/roundtrip_tests/TestHandles.swift index a3109f571ed..44b2797996a 100644 --- a/baml_language/sdk_tests/crates/swift/type_shapes/customizable/roundtrip_tests/TestHandles.swift +++ b/baml_language/sdk_tests/crates/swift/type_shapes/customizable/roundtrip_tests/TestHandles.swift @@ -32,10 +32,10 @@ final class TestHandles: XCTestCase { func test_handles_file_cursor_state_persists_across_calls() throws { let path = try makeTempFile(contents: "0123456789") let f = try Baml.baml.fs.open(path: path, mode: "r") - XCTAssertEqual(try f.read(n: 3), "012") - XCTAssertEqual(try f.read(n: 3), "345") + XCTAssertEqual(try f.seek_from(whence: "current", offset: 3), 3) + XCTAssertEqual(try f.seek_from(whence: "current", offset: 3), 6) XCTAssertEqual(try f.seek_from(whence: "start", offset: 0), 0) - XCTAssertEqual(try f.read(n: 2), "01") + XCTAssertEqual(try f.seek_from(whence: "current", offset: 2), 2) XCTAssertEqual(try f.text(), "23456789") try f.close() } diff --git a/baml_language/sdk_tests/crates/typescript/type_shapes/customizable/roundtrip_handles.test.ts b/baml_language/sdk_tests/crates/typescript/type_shapes/customizable/roundtrip_handles.test.ts index cc3d8b8aa97..9c07011ee19 100644 --- a/baml_language/sdk_tests/crates/typescript/type_shapes/customizable/roundtrip_handles.test.ts +++ b/baml_language/sdk_tests/crates/typescript/type_shapes/customizable/roundtrip_handles.test.ts @@ -95,10 +95,11 @@ describe.runIf(isTestRuntime("node"))( it("handles_file_cursor_state_persists_across_calls", () => { const f = baml.fs.open(filePath, "r"); - expect(f.read(3)).toBe("012"); - expect(f.read(3)).toBe("345"); + // Relative seeks verify that separate calls share one engine-side handle. + expect(f.seek_from("current", 3)).toBe(3); + expect(f.seek_from("current", 3)).toBe(6); expect(f.seek_from("start", 0)).toBe(0); - expect(f.read(2)).toBe("01"); + expect(f.seek_from("current", 2)).toBe(2); expect(f.text()).toBe("23456789"); expect(f.close()).toBeNull(); }); diff --git a/baml_language/sdk_tests/harness_setup/src/csharp.rs b/baml_language/sdk_tests/harness_setup/src/csharp.rs index 32596999a97..f08e7faad43 100644 --- a/baml_language/sdk_tests/harness_setup/src/csharp.rs +++ b/baml_language/sdk_tests/harness_setup/src/csharp.rs @@ -267,7 +267,6 @@ fn verify_stdlib_resources_surface(fixture: &std::path::Path) { for expected in [ "public sealed partial class File : global::System.IDisposable", - "public string Read(", "public long SeekFrom(\n string whence,", "public string Text(", "public File Clone() => new(\n resource.Clone());", @@ -292,6 +291,41 @@ fn verify_stdlib_resources_surface(fixture: &std::path::Path) { assert!(!file.contains("public global::Baml.BamlHandle Handle")); assert!(!file.contains("private readonly global::Baml.BamlHandle")); + for absent in [ + " Read(", + " ReadAsync(", + " ReadBytes(", + " ReadBytesAsync(", + " Flush(", + " FlushAsync(", + ] { + assert!( + !file.contains(absent), + "generated File resource exposed interface member `{absent}`" + ); + } + assert_eq!( + file.matches(" Write(").count(), + 1, + "expected exactly one concrete File.Write method" + ); + + let tcp_stream = fs::read_to_string(generated.join("Net").join("TcpStream.g.cs")) + .expect("failed to read generated typed TcpStream resource surface"); + for absent in [ + " Read(", + " ReadAsync(", + " Write(", + " WriteAsync(", + " Flush(", + " FlushAsync(", + ] { + assert!( + !tcp_stream.contains(absent), + "generated TcpStream resource exposed interface member `{absent}`" + ); + } + let resource_surfaces = [ ( "Fs/File.g.cs", @@ -300,18 +334,12 @@ fn verify_stdlib_resources_surface(fixture: &std::path::Path) { " TextAsync(", " Bytes(", " BytesAsync(", - " Read(", - " ReadAsync(", - " ReadBytes(", - " ReadBytesAsync(", " Close(", " CloseAsync(", " SeekFrom(", " SeekFromAsync(", " Write(", " WriteAsync(", - " WriteBytes(", - " WriteBytesAsync(", ], ), ( @@ -366,16 +394,7 @@ fn verify_stdlib_resources_surface(fixture: &std::path::Path) { ), ( "Net/TcpStream.g.cs", - vec![ - " Connect(", - " ConnectAsync(", - " Read(", - " ReadAsync(", - " Write(", - " WriteAsync(", - " Close(", - " CloseAsync(", - ], + vec![" Connect(", " ConnectAsync(", " Close(", " CloseAsync("], ), ( "Net/TcpListener.g.cs", From c60839a049d97ba8b7f6b306aa753369b2e0b777 Mon Sep 17 00:00:00 2001 From: Avery Townsend Date: Wed, 26 Aug 2026 13:10:50 -0700 Subject: [PATCH 2/4] test: refresh builtin package listing snapshot --- ...tests__render_builtin_package_listing.snap | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap b/baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap index 41a892b3306..499077698a2 100644 --- a/baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap +++ b/baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap @@ -109,22 +109,22 @@ class baml.errors.TypeMismatch /baml/ns_errors/error class baml.errors.StackFrame /baml/ns_errors/stack_trace.baml:2 class baml.errors.StackTrace /baml/ns_errors/stack_trace.baml:9 class baml.errors.UnknownError /baml/ns_errors/unknown_error.baml:2 -class baml.fs.File /baml/ns_fs/fs.baml:5 -function baml.fs.open /baml/ns_fs/fs.baml:62 -function baml.fs.exists /baml/ns_fs/fs.baml:70 -function baml.fs.remove /baml/ns_fs/fs.baml:78 -function baml.fs.size /baml/ns_fs/fs.baml:83 -function baml.fs.read /baml/ns_fs/fs.baml:89 -function baml.fs.write /baml/ns_fs/fs.baml:94 -function baml.fs.write_bytes /baml/ns_fs/fs.baml:99 -class baml.fs.DirEntry /baml/ns_fs/fs.baml:104 -class baml.fs.MkdirOptions /baml/ns_fs/fs.baml:112 -function baml.fs.read_dir /baml/ns_fs/fs.baml:118 -function baml.fs.mkdir /baml/ns_fs/fs.baml:123 -function baml.fs.remove_dir /baml/ns_fs/fs.baml:129 -function baml.fs.remove_dir_all /baml/ns_fs/fs.baml:138 -function baml.fs.chmod /baml/ns_fs/fs.baml:161 -function baml.fs.symlink /baml/ns_fs/fs.baml:181 +class baml.fs.File /baml/ns_fs/fs.baml:6 +function baml.fs.open /baml/ns_fs/fs.baml:63 +function baml.fs.exists /baml/ns_fs/fs.baml:71 +function baml.fs.remove /baml/ns_fs/fs.baml:79 +function baml.fs.size /baml/ns_fs/fs.baml:84 +function baml.fs.read /baml/ns_fs/fs.baml:90 +function baml.fs.write /baml/ns_fs/fs.baml:95 +function baml.fs.write_bytes /baml/ns_fs/fs.baml:100 +class baml.fs.DirEntry /baml/ns_fs/fs.baml:105 +class baml.fs.MkdirOptions /baml/ns_fs/fs.baml:113 +function baml.fs.read_dir /baml/ns_fs/fs.baml:119 +function baml.fs.mkdir /baml/ns_fs/fs.baml:124 +function baml.fs.remove_dir /baml/ns_fs/fs.baml:130 +function baml.fs.remove_dir_all /baml/ns_fs/fs.baml:139 +function baml.fs.chmod /baml/ns_fs/fs.baml:162 +function baml.fs.symlink /baml/ns_fs/fs.baml:182 enum baml.future.FutureState /baml/ns_future/future.baml:21 class baml.future.Future /baml/ns_future/future.baml:28 function baml.future.all_complete /baml/ns_future/future.baml:80 From 98a8e54e7552b5f711def22ef5045a82ac48db98 Mon Sep 17 00:00:00 2001 From: Avery Townsend Date: Wed, 26 Aug 2026 13:20:14 -0700 Subject: [PATCH 3/4] test: migrate C# TCP resource fixture --- .../crates/csharp/stdlib_resources/Program.cs | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/baml_language/sdk_tests/crates/csharp/stdlib_resources/Program.cs b/baml_language/sdk_tests/crates/csharp/stdlib_resources/Program.cs index b178022ebd4..591012b232d 100644 --- a/baml_language/sdk_tests/crates/csharp/stdlib_resources/Program.cs +++ b/baml_language/sdk_tests/crates/csharp/stdlib_resources/Program.cs @@ -159,18 +159,11 @@ await response.TextAsync() == "offline response body", cancellationToken: networkTimeout.Token); using TcpClient outboundPeer = await peerAccept; using NetworkStream outboundPeerStream = outboundPeer.GetStream(); - await outboundPeerStream.WriteAsync(Encoding.UTF8.GetBytes("from-peer"), networkTimeout.Token); - Require( - (await outbound.ReadAsync(cancellationToken: networkTimeout.Token)).Span.SequenceEqual( - Encoding.UTF8.GetBytes("from-peer")), - "TcpStream.connect/read did not retain its native socket state"); - _ = outbound.Write(Encoding.UTF8.GetBytes("from-baml"), cancellationToken: networkTimeout.Token); - byte[] fromBaml = new byte["from-baml".Length]; - await outboundPeerStream.ReadExactlyAsync(fromBaml, networkTimeout.Token); - Require( - fromBaml.SequenceEqual(Encoding.UTF8.GetBytes("from-baml")), - "TcpStream.write changed"); _ = await outbound.CloseAsync(networkTimeout.Token); + byte[] outboundClosedProbe = new byte[1]; + Require( + await outboundPeerStream.ReadAsync(outboundClosedProbe, networkTimeout.Token) == 0, + "TcpStream.connect/close did not retain its native socket state"); systemListener.Stop(); int bamlListenerPort = ReserveTcpPort(); @@ -183,12 +176,12 @@ await response.TextAsync() == "offline response body", using TcpClient inboundPeer = new(); await inboundPeer.ConnectAsync(IPAddress.Loopback, bamlListenerPort, networkTimeout.Token); using Baml.Net.TcpStream inbound = await acceptedStream; - await inboundPeer.GetStream().WriteAsync(Encoding.UTF8.GetBytes("accepted"), networkTimeout.Token); + using NetworkStream inboundPeerStream = inboundPeer.GetStream(); + _ = inbound.Close(); + byte[] inboundClosedProbe = new byte[1]; Require( - inbound.Read(cancellationToken: networkTimeout.Token).Span.SequenceEqual( - Encoding.UTF8.GetBytes("accepted")), + await inboundPeerStream.ReadAsync(inboundClosedProbe, networkTimeout.Token) == 0, "TcpListener.accept returned a stream without live native state"); - _ = inbound.Close(); _ = await clonedListener.CloseAsync(networkTimeout.Token); int bamlUdpPort = ReserveUdpPort(); From 9325e7e04828c5b040d773897b4e9e97f3b3c672 Mon Sep 17 00:00:00 2001 From: Avery Townsend Date: Wed, 26 Aug 2026 14:15:54 -0700 Subject: [PATCH 4/4] fix: address IO review findings --- .../baml_std/ai/ns_mcp/mcp.baml | 98 ++--- .../baml_src/ns_fs/fs_from_rust.baml | 21 +- .../crates/baml_tests/baml_src/ns_io/io.baml | 45 ++- .../snapshots/baml_src/ns_fs/bytecode.snap | 33 +- .../baml_src/stdlib/ai/bytecode.snap | 177 ++------- .../snapshots/baml_src/stdlib/ai/mir.snap | 371 ++++++++++-------- .../snapshots/baml_src/stdlib/ai/ppir.snap | 4 +- baml_language/crates/baml_tests/tests/net.rs | 29 ++ .../crates/baml_tests/tests/shell.rs | 27 ++ .../crates/sys_native/src/io_impls.rs | 143 +++++-- 10 files changed, 509 insertions(+), 439 deletions(-) diff --git a/baml_language/crates/baml_builtins2/baml_std/ai/ns_mcp/mcp.baml b/baml_language/crates/baml_builtins2/baml_std/ai/ns_mcp/mcp.baml index 9069f2b35d3..ab0e4bedd42 100644 --- a/baml_language/crates/baml_builtins2/baml_std/ai/ns_mcp/mcp.baml +++ b/baml_language/crates/baml_builtins2/baml_std/ai/ns_mcp/mcp.baml @@ -88,54 +88,62 @@ class McpConnection { let id = self.next_id; self.next_id += 1; self._send(rpc_line(id, method, params)); - while (true) { - let next = baml.future.with_timeout(baml.time.Duration.from_milliseconds(self.timeout_ms), () -> { - self.out.next() - }) catch_all (e) { - _ => { - throw root.errors.NetworkFailure { status_code: null, - provider: `mcp/${self.server_name}`, - detail: `reading from the MCP server failed: ${e.to_string()}`, - raw_body: null, - } - }, - }; - match (next) { - let line: string => { - let trimmed = line.trim(); - if (trimmed.length() > 0) { - // stdout is protocol-only per the spec; tolerate stray lines - let msg = baml.json.parse(trimmed) catch_all (e) { - _ => null, - }; - if (baml.json.path_or(msg, ".id", -1) == id) { - let err = baml.json.path_or(msg, ".error", null); - if (err != null) { - throw root.errors.InvalidRequest { - provider: `mcp/${self.server_name}`, - status_code: baml.json.path_or(err, ".code", null), - detail: baml.json.path_or(err, ".message", "MCP error"), - raw_body: baml.json.stringify(err), + baml.future.with_timeout(baml.time.Duration.from_milliseconds(self.timeout_ms), () -> { + while (true) { + let next = self.out.next() catch_all (e) { + _ => { + throw root.errors.NetworkFailure { status_code: null, + provider: `mcp/${self.server_name}`, + detail: `reading from the MCP server failed: ${e.to_string()}`, + raw_body: null, + } + }, + }; + match (next) { + let line: string => { + let trimmed = line.trim(); + if (trimmed.length() > 0) { + // stdout is protocol-only per the spec; tolerate stray lines + let msg = baml.json.parse(trimmed) catch_all (e) { + _ => null, + }; + if (baml.json.path_or(msg, ".id", -1) == id) { + let err = baml.json.path_or(msg, ".error", null); + if (err != null) { + throw root.errors.InvalidRequest { + provider: `mcp/${self.server_name}`, + status_code: baml.json.path_or(err, ".code", null), + detail: baml.json.path_or(err, ".message", "MCP error"), + raw_body: baml.json.stringify(err), + } } + return baml.json.path_or(msg, ".result", null); } - return baml.json.path_or(msg, ".result", null); } - } - }, - _ => { - throw root.errors.NetworkFailure { status_code: null, - provider: `mcp/${self.server_name}`, - detail: `the MCP server stream ended before answering ${method}`, - raw_body: null, - } - }, - }; - } - // unreachable: the loop returns on a matching id or throws at EOF - throw root.errors.NetworkFailure { status_code: null, - provider: `mcp/${self.server_name}`, - detail: `no response for ${method}`, - raw_body: null, + }, + _ => { + throw root.errors.NetworkFailure { status_code: null, + provider: `mcp/${self.server_name}`, + detail: `the MCP server stream ended before answering ${method}`, + raw_body: null, + } + }, + }; + } + // unreachable: the loop returns on a matching id or throws at EOF + throw root.errors.NetworkFailure { status_code: null, + provider: `mcp/${self.server_name}`, + detail: `no response for ${method}`, + raw_body: null, + } + }) catch (e) { + baml.errors.Timeout => { + throw root.errors.NetworkFailure { status_code: null, + provider: `mcp/${self.server_name}`, + detail: `reading from the MCP server timed out after ${self.timeout_ms}ms`, + raw_body: null, + } + }, } } diff --git a/baml_language/crates/baml_tests/baml_src/ns_fs/fs_from_rust.baml b/baml_language/crates/baml_tests/baml_src/ns_fs/fs_from_rust.baml index 8115bfc1efc..e228516a22a 100644 --- a/baml_language/crates/baml_tests/baml_src/ns_fs/fs_from_rust.baml +++ b/baml_language/crates/baml_tests/baml_src/ns_fs/fs_from_rust.baml @@ -203,14 +203,13 @@ function fs_file_read_negative_n_errors_fn() -> bool { let path = fs_tmp("read_negative_n.txt"); baml.fs.write(path, "abc"); let f = baml.fs.open(path, "r"); - let threw = { + defer { baml.fs.remove(path) } + { f.read(-1); false } catch (e) { baml.errors.Io => true - }; - baml.fs.remove(path); - threw + } } test "fs_file_read_negative_n_errors" { @@ -221,14 +220,13 @@ function fs_file_read_zero_errors_fn() -> bool { let path = fs_tmp("read_zero.txt"); baml.fs.write(path, "abc"); let f = baml.fs.open(path, "r"); - let threw = { + defer { baml.fs.remove(path) } + { f.read(0); false } catch (e) { baml.errors.Io => true - }; - baml.fs.remove(path); - threw + } } test "fs_file_read_zero_errors" { @@ -244,14 +242,13 @@ function fs_file_close_invalidates_handle_fn() -> bool { baml.fs.write(path, "content"); let f = baml.fs.open(path, "r"); f.close(); - let threw = { + defer { baml.fs.remove(path) } + { f.text(); false } catch (e) { baml.errors.Io => true - }; - baml.fs.remove(path); - threw + } } test "fs_file_close_invalidates_handle" { diff --git a/baml_language/crates/baml_tests/baml_src/ns_io/io.baml b/baml_language/crates/baml_tests/baml_src/ns_io/io.baml index 3c22914f70e..8f58782da69 100644 --- a/baml_language/crates/baml_tests/baml_src/ns_io/io.baml +++ b/baml_language/crates/baml_tests/baml_src/ns_io/io.baml @@ -75,13 +75,14 @@ test "read text decodes strict utf8" { test "read text rejects invalid utf8" { let reader = ChunkReader { data: uint8array.from_array([255]), cursor: 0, chunk_size: 1 }; - let failed = { - reader.text(); - false - } catch (e) { - baml.errors.ParseError => true - }; - assert.is_true(failed) + assert.is_true( + { + reader.text(); + false + } catch (e) { + baml.errors.ParseError => true + }, + ) } test "write retries partial writes" { @@ -91,23 +92,25 @@ test "write retries partial writes" { } test "write rejects zero progress" { - let failed = { - ZeroWriter { }.write(b"abc"); - false - } catch (e) { - baml.errors.Io => true - }; - assert.is_true(failed) + assert.is_true( + { + ZeroWriter { }.write(b"abc"); + false + } catch (e) { + baml.errors.Io => true + }, + ) } test "write rejects oversized progress" { - let failed = { - OversizedWriter { }.write(b"abc"); - false - } catch (e) { - baml.errors.Io => true - }; - assert.is_true(failed) + assert.is_true( + { + OversizedWriter { }.write(b"abc"); + false + } catch (e) { + baml.errors.Io => true + }, + ) } test "uint8array index_of finds bytes" { diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/ns_fs/bytecode.snap b/baml_language/crates/baml_tests/snapshots/baml_src/ns_fs/bytecode.snap index 98236e8b8c7..bd913be5058 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/ns_fs/bytecode.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/ns_fs/bytecode.snap @@ -600,19 +600,24 @@ function fs.fs_file_close_invalidates_handle_fn() -> bool { L1: load_const true - store_var threw + store_var _0 jump L3 L2: load_const false - store_var threw + store_var _0 L3: load_var path sys_op baml.fs.remove pop 1 - load_var threw + load_var _0 return + load_var path + sys_op baml.fs.remove + pop 1 + load_var _7 + rethrow } function fs.fs_file_close_is_idempotent_fn() -> void { @@ -786,19 +791,24 @@ function fs.fs_file_read_negative_n_errors_fn() -> bool { L1: load_const true - store_var threw + store_var _0 jump L3 L2: load_const false - store_var threw + store_var _0 L3: load_var path sys_op baml.fs.remove pop 1 - load_var threw + load_var _0 return + load_var path + sys_op baml.fs.remove + pop 1 + load_var _6 + rethrow } function fs.fs_file_read_truncates_at_eof_fn() -> uint8array { @@ -864,19 +874,24 @@ function fs.fs_file_read_zero_errors_fn() -> bool { L1: load_const true - store_var threw + store_var _0 jump L3 L2: load_const false - store_var threw + store_var _0 L3: load_var path sys_op baml.fs.remove pop 1 - load_var threw + load_var _0 return + load_var path + sys_op baml.fs.remove + pop 1 + load_var _6 + rethrow } function fs.fs_file_rw_seek_and_read_fn() -> string { diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/bytecode.snap b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/bytecode.snap index 733f3c25200..1fe24a37d28 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/bytecode.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/bytecode.snap @@ -6540,16 +6540,23 @@ function ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: strin load_var ?1 make_cell store_var ?1 + load_var ?2 + make_cell + store_var ?2 + load_var ?4 + make_cell + store_var ?4 load_deref ?1 load_field .next_id - store_var id + store_deref ?4 load_deref ?1 copy 0 load_field .next_id load_const 1 bin_op + store_field .next_id - load_var2 4 2 + load_deref ?4 + load_deref ?2 load_var params call ai.mcp.rpc_line store_var _6 @@ -6557,174 +6564,52 @@ function ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: strin load_var _6 call ai.mcp.McpConnection._send pop 1 - - L0: - load_const true - pop_jump_if_false L1 - jump L2 - - L1: - load_type string - load_deref ?1 - load_field .server_name - call baml.String.from - store_var _69 - load_type string - load_var method - call baml.String.from - store_var _73 - load_const "mcp/" - load_var _69 - bin_op + - load_const "no response for " - load_var _73 - bin_op + - load_const null - load_const null - init_instance ai.errors.NetworkFailure .provider, .detail, .status_code, .raw_body - throw - - L2: load_deref ?1 load_field .timeout_ms call baml.time.Duration.from_milliseconds - store_var _10 - load_type string | baml.iter.Done - load_type baml.errors.Io + store_var _9 + load_type never + load_type ai.errors.InvalidRequest | ai.errors.NetworkFailure load_var2 7 1 - make_closure ., 1 + load_var2 4 2 + make_closure ., 3 call baml.future.with_timeout - jump L3 + jump L2 load_var e - throw_if_panic + is_type baml.errors.Timeout + pop_jump_if_false L0 + jump L1 + + L0: + load_var e + rethrow + + L1: load_type string load_deref ?1 load_field .server_name call baml.String.from store_var _17 - load_type never - load_var e + load_type int + load_deref ?1 + load_field .timeout_ms call baml.String.from store_var _22 - load_type string - load_var _22 - call baml.String.from - store_var _21 load_const "mcp/" load_var _17 bin_op + - load_const "reading from the MCP server failed: " - load_var _21 - bin_op + - load_const null - load_const null - init_instance ai.errors.NetworkFailure .provider, .detail, .status_code, .raw_body - throw - - L3: - store_var _25 - load_var _25 - narrow_bind string, slot=11 - pop_jump_if_false L4 - jump L5 - - L4: - load_type string - load_deref ?1 - load_field .server_name - call baml.String.from - store_var _61 - load_type string - load_var method - call baml.String.from - store_var _65 - load_const "mcp/" - load_var _61 + load_const "reading from the MCP server timed out after " + load_var _22 bin_op + - load_const "the MCP server stream ended before answering " - load_var _65 + load_const "ms" bin_op + load_const null load_const null init_instance ai.errors.NetworkFailure .provider, .detail, .status_code, .raw_body throw - L5: - load_var _25 - call baml.String.trim - store_var trimmed - load_var trimmed - call baml.String.length - load_const 0 - cmp_int_op > - pop_jump_if_false L0 - load_var trimmed - call baml.json.parse - store_var msg - jump L6 - load_var e - throw_if_panic - load_const null - store_var msg - - L6: - load_type int - load_var msg - load_const ".id" - load_const -1 - call baml.json.path_or - load_var id - cmp_int_op == - pop_jump_if_false L0 - load_type baml.json.json - load_var msg - load_const ".error" - load_const null - call baml.json.path_or - store_var err - load_var err - load_const null - call baml.ops.equals_equals - unary_op ! - pop_jump_if_false L7 - jump L8 - - L7: - load_type baml.json.json - load_var msg - load_const ".result" - load_const null - call baml.json.path_or + L2: return - - L8: - load_type string - load_deref ?1 - load_field .server_name - call baml.String.from - store_var _46 - load_type int | null - load_var err - load_const ".code" - load_const null - call baml.json.path_or - store_var _49 - load_type string - load_var err - load_const ".message" - load_const "MCP error" - call baml.json.path_or - store_var _52 - load_var err - call baml.json.stringify - store_var _55 - load_const "mcp/" - load_var _46 - bin_op + - load_var2 17 18 - load_var _55 - init_instance ai.errors.InvalidRequest .provider, .status_code, .detail, .raw_body - throw } function ai.mcp.McpConnection._send(self: ai.mcp.McpConnection, line: string) -> void { diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/mir.snap b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/mir.snap index 47b62d90836..bc2b9b1c20f 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/mir.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/mir.snap @@ -6864,19 +6864,19 @@ fn ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: string, par // Locals: let _0: int | float | string | bool | null | baml.json.json[] | map // _0 // return let _1: ai.mcp.McpConnection // self // param [captured] - let _2: string // method // param + let _2: string // method // param [captured] let _3: map // params // param - let _4: int // id + let _4: int // id [captured] let _5: void let _6: string let _7: int - let _8: string | baml.iter.Done // next - let _9: unknown // e - let _10: baml.time.Duration - let _11: int - let _12: () -> string | baml.iter.Done throws baml.errors.Io + let _8: unknown // e + let _9: baml.time.Duration + let _10: int + let _11: () -> never throws ai.errors.InvalidRequest | ai.errors.NetworkFailure + let _12: reflect.Type let _13: reflect.Type - let _14: reflect.Type + let _14: bool let _15: ai.errors.NetworkFailure let _16: string let _17: string @@ -6885,263 +6885,312 @@ fn ai.mcp.McpConnection._request(self: ai.mcp.McpConnection, method: string, par let _20: string let _21: string let _22: string - let _23: reflect.Type + let _23: int let _24: reflect.Type - let _25: string | baml.iter.Done - let _26: string // line - let _27: string // trimmed - let _28: bool - let _29: int - let _30: int | float | string | bool | null | baml.json.json[] | map // msg - let _31: unknown // e - let _32: string - let _33: bool - let _34: int - let _35: int | float | string | bool | null | baml.json.json[] | map - let _36: reflect.Type - let _37: int - let _38: int | float | string | bool | null | baml.json.json[] | map // err - let _39: int | float | string | bool | null | baml.json.json[] | map + + bb0: { + _4 = copy _1.4; + _1.4 = copy _1.4 + const 1_i64; + _7 = copy _4; + _6 = call const fn ai.mcp.rpc_line(copy _7, copy _2, copy _3) -> [bb1]; + } + + bb1: { + _5 = call const fn ai.mcp.McpConnection._send(copy _1, copy _6) -> [bb2]; + } + + bb2: { + _10 = copy _1.3; + _9 = call const fn baml.time.Duration.from_milliseconds(copy _10) -> [bb3, unwind: bb5]; + } + + bb3: { + _11 = make_closure lambda[0](copy _1, copy _4, copy _2); + _12 = load_type(never); + _13 = load_type(ai.errors.InvalidRequest | ai.errors.NetworkFailure); + _0 = call const fn baml.future.with_timeout(copy _9, copy _11) -> [bb4, unwind: bb5]; + } + + bb4: { + return; + } + + bb5: { + _14 = is_type(copy _8, baml.errors.Timeout); + branch copy _14 -> [bb7, bb6]; + } + + bb6: { + rethrow copy _8; + } + + bb7: { + _18 = copy _1.0; + _19 = load_type(string); + _17 = call const fn baml.String.from(copy _18) -> [bb8]; + } + + bb8: { + _16 = const "mcp/" + copy _17; + _23 = copy _1.3; + _24 = load_type(int); + _22 = call const fn baml.String.from(copy _23) -> [bb9]; + } + + bb9: { + _21 = const "reading from the MCP server timed out after " + copy _22; + _20 = copy _21 + const "ms"; + _15 = ai.errors.NetworkFailure { copy _16, copy _20, const null, const null }; + throw copy _15; + } +} + +// lambda[0] +fn .() -> never { + // Locals: + let _0: never // _0 // return + let _1: string | baml.iter.Done // next + let _2: unknown // e + let _3: (baml.sys.ReadPipeLines) -> string | baml.iter.Done throws baml.errors.Io + let _4: ai.errors.NetworkFailure + let _5: string + let _6: string + let _7: string + let _8: reflect.Type + let _9: string + let _10: string + let _11: string + let _12: reflect.Type + let _13: reflect.Type + let _14: string | baml.iter.Done + let _15: string // line + let _16: string // trimmed + let _17: bool + let _18: int + let _19: int | float | string | bool | null | baml.json.json[] | map // msg + let _20: unknown // e + let _21: string + let _22: bool + let _23: int + let _24: int | float | string | bool | null | baml.json.json[] | map + let _25: reflect.Type + let _26: int + let _27: int | float | string | bool | null | baml.json.json[] | map // err + let _28: int | float | string | bool | null | baml.json.json[] | map + let _29: reflect.Type + let _30: bool + let _31: int | float | string | bool | null | baml.json.json[] | map + let _32: bool + let _33: ai.errors.InvalidRequest + let _34: string + let _35: string + let _36: string + let _37: reflect.Type + let _38: int | null + let _39: int | float | string | bool | baml.json.json[] | map let _40: reflect.Type - let _41: bool - let _42: int | float | string | bool | null | baml.json.json[] | map - let _43: bool - let _44: ai.errors.InvalidRequest - let _45: string - let _46: string - let _47: string - let _48: reflect.Type - let _49: int | null - let _50: int | float | string | bool | baml.json.json[] | map - let _51: reflect.Type - let _52: string - let _53: int | float | string | bool | baml.json.json[] | map - let _54: reflect.Type + let _41: string + let _42: int | float | string | bool | baml.json.json[] | map + let _43: reflect.Type + let _44: string + let _45: int | float | string | bool | baml.json.json[] | map + let _46: int | float | string | bool | null | baml.json.json[] | map + let _47: reflect.Type + let _48: ai.errors.NetworkFailure + let _49: string + let _50: string + let _51: string + let _52: reflect.Type + let _53: string + let _54: string let _55: string - let _56: int | float | string | bool | baml.json.json[] | map - let _57: int | float | string | bool | null | baml.json.json[] | map - let _58: reflect.Type - let _59: ai.errors.NetworkFailure + let _56: reflect.Type + let _57: ai.errors.NetworkFailure + let _58: string + let _59: string let _60: string - let _61: string + let _61: reflect.Type let _62: string - let _63: reflect.Type + let _63: string let _64: string - let _65: string - let _66: reflect.Type - let _67: ai.errors.NetworkFailure - let _68: string - let _69: string - let _70: string - let _71: reflect.Type - let _72: string - let _73: string - let _74: reflect.Type + let _65: reflect.Type bb0: { - _4 = copy _1.4; - _1.4 = copy _1.4 + const 1_i64; - _7 = copy _4; - _6 = call const fn ai.mcp.rpc_line(copy _7, copy _2, copy _3) -> [bb1]; + goto -> bb5; } bb1: { - _5 = call const fn ai.mcp.McpConnection._send(copy _1, copy _6) -> [bb6]; + throw_if_panic copy _20 -> bb2; } bb2: { - throw_if_panic copy _31 -> bb3; + _19 = const null; + goto -> bb3; } bb3: { - _30 = const null; - goto -> bb4; + _24 = copy _19; + _25 = load_type(int); + _23 = call const fn baml.json.path_or(copy _24, const ".id", const -1_i64) -> [bb4]; } bb4: { - _35 = copy _30; - _36 = load_type(int); - _34 = call const fn baml.json.path_or(copy _35, const ".id", const -1_i64) -> [bb5]; + _26 = copy capture[1]; + _22 = copy _23 == copy _26; + branch copy _22 -> [bb23, bb5]; } bb5: { - _37 = copy _4; - _33 = copy _34 == copy _37; - branch copy _33 -> [bb25, bb6]; + branch const true -> [bb9, bb6]; } bb6: { - branch const true -> [bb10, bb7]; + _60 = copy capture[0].0; + _61 = load_type(string); + _59 = call const fn baml.String.from(copy _60) -> [bb7]; } bb7: { - _70 = copy _1.0; - _71 = load_type(string); - _69 = call const fn baml.String.from(copy _70) -> [bb8]; + _58 = const "mcp/" + copy _59; + _64 = copy capture[2]; + _65 = load_type(string); + _63 = call const fn baml.String.from(copy _64) -> [bb8]; } bb8: { - _68 = const "mcp/" + copy _69; - _74 = load_type(string); - _73 = call const fn baml.String.from(copy _2) -> [bb9]; + _62 = const "no response for " + copy _63; + _57 = ai.errors.NetworkFailure { copy _58, copy _62, const null, const null }; + throw copy _57; } bb9: { - _72 = const "no response for " + copy _73; - _67 = ai.errors.NetworkFailure { copy _68, copy _72, const null, const null }; - throw copy _67; + _3 = copy capture[0].2; + _1 = call const fn baml.sys.ReadPipeLines.root.iter.Iterator.next(copy _3) -> [bb15, unwind: bb10]; } bb10: { - _11 = copy _1.3; - _10 = call const fn baml.time.Duration.from_milliseconds(copy _11) -> [bb11, unwind: bb12]; + throw_if_panic copy _2 -> bb11; } bb11: { - _12 = make_closure lambda[0](copy _1); - _13 = load_type(string | baml.iter.Done); - _14 = load_type(baml.errors.Io); - _8 = call const fn baml.future.with_timeout(copy _10, copy _12) -> [bb17, unwind: bb12]; + _7 = copy capture[0].0; + _8 = load_type(string); + _6 = call const fn baml.String.from(copy _7) -> [bb12]; } bb12: { - throw_if_panic copy _9 -> bb13; + _5 = const "mcp/" + copy _6; + _12 = load_type(baml.errors.Io); + _11 = call const fn baml.String.from(copy _2) -> [bb13]; } bb13: { - _18 = copy _1.0; - _19 = load_type(string); - _17 = call const fn baml.String.from(copy _18) -> [bb14]; + _13 = load_type(string); + _10 = call const fn baml.String.from(copy _11) -> [bb14]; } bb14: { - _16 = const "mcp/" + copy _17; - _23 = load_type(never); - _22 = call const fn baml.String.from(copy _9) -> [bb15]; + _9 = const "reading from the MCP server failed: " + copy _10; + _4 = ai.errors.NetworkFailure { copy _5, copy _9, const null, const null }; + throw copy _4; } bb15: { - _24 = load_type(string); - _21 = call const fn baml.String.from(copy _22) -> [bb16]; + _14 = copy _1; + _14 = narrow_bind copy _14 as String { attr: TyAttr { sap_parse_without_null: Unset, sap_pending_never: Unset, sap_in_progress_never: Unset } } -> [bb19, bb16]; } bb16: { - _20 = const "reading from the MCP server failed: " + copy _21; - _15 = ai.errors.NetworkFailure { copy _16, copy _20, const null, const null }; - throw copy _15; + _51 = copy capture[0].0; + _52 = load_type(string); + _50 = call const fn baml.String.from(copy _51) -> [bb17]; } bb17: { - _25 = copy _8; - _25 = narrow_bind copy _25 as String { attr: TyAttr { sap_parse_without_null: Unset, sap_pending_never: Unset, sap_in_progress_never: Unset } } -> [bb21, bb18]; + _49 = const "mcp/" + copy _50; + _55 = copy capture[2]; + _56 = load_type(string); + _54 = call const fn baml.String.from(copy _55) -> [bb18]; } bb18: { - _62 = copy _1.0; - _63 = load_type(string); - _61 = call const fn baml.String.from(copy _62) -> [bb19]; + _53 = const "the MCP server stream ended before answering " + copy _54; + _48 = ai.errors.NetworkFailure { copy _49, copy _53, const null, const null }; + throw copy _48; } bb19: { - _60 = const "mcp/" + copy _61; - _66 = load_type(string); - _65 = call const fn baml.String.from(copy _2) -> [bb20]; + _15 = copy _14; + _16 = call const fn baml.String.trim(copy _15) -> [bb20]; } bb20: { - _64 = const "the MCP server stream ended before answering " + copy _65; - _59 = ai.errors.NetworkFailure { copy _60, copy _64, const null, const null }; - throw copy _59; + _18 = call const fn baml.String.length(copy _16) -> [bb21]; } bb21: { - _26 = copy _25; - _27 = call const fn baml.String.trim(copy _26) -> [bb22]; + _17 = copy _18 > const 0_i64; + branch copy _17 -> [bb22, bb5]; } bb22: { - _29 = call const fn baml.String.length(copy _27) -> [bb23]; + _21 = copy _16; + _19 = call const fn baml.json.parse(copy _21) -> [bb3, unwind: bb1]; } bb23: { - _28 = copy _29 > const 0_i64; - branch copy _28 -> [bb24, bb6]; + _28 = copy _19; + _29 = load_type(baml.json.json); + _27 = call const fn baml.json.path_or(copy _28, const ".error", const null) -> [bb24]; } bb24: { - _32 = copy _27; - _30 = call const fn baml.json.parse(copy _32) -> [bb4, unwind: bb2]; + _31 = copy _27; + _32 = call const fn baml.ops.equals_equals(copy _31, const null) -> [bb25]; } bb25: { - _39 = copy _30; - _40 = load_type(baml.json.json); - _38 = call const fn baml.json.path_or(copy _39, const ".error", const null) -> [bb26]; + _30 = !copy _32; + branch copy _30 -> [bb28, bb26]; } bb26: { - _42 = copy _38; - _43 = call const fn baml.ops.equals_equals(copy _42, const null) -> [bb27]; + _46 = copy _19; + _47 = load_type(baml.json.json); + _0 = call const fn baml.json.path_or(copy _46, const ".result", const null) -> [bb27]; } bb27: { - _41 = !copy _43; - branch copy _41 -> [bb30, bb28]; + return; } bb28: { - _57 = copy _30; - _58 = load_type(baml.json.json); - _0 = call const fn baml.json.path_or(copy _57, const ".result", const null) -> [bb29]; + _36 = copy capture[0].0; + _37 = load_type(string); + _35 = call const fn baml.String.from(copy _36) -> [bb29]; } bb29: { - return; + _34 = const "mcp/" + copy _35; + _39 = copy _27; + _40 = load_type(int | null); + _38 = call const fn baml.json.path_or(copy _39, const ".code", const null) -> [bb30]; } bb30: { - _47 = copy _1.0; - _48 = load_type(string); - _46 = call const fn baml.String.from(copy _47) -> [bb31]; + _42 = copy _27; + _43 = load_type(string); + _41 = call const fn baml.json.path_or(copy _42, const ".message", const "MCP error") -> [bb31]; } bb31: { - _45 = const "mcp/" + copy _46; - _50 = copy _38; - _51 = load_type(int | null); - _49 = call const fn baml.json.path_or(copy _50, const ".code", const null) -> [bb32]; + _45 = copy _27; + _44 = call const fn baml.json.stringify(copy _45) -> [bb32]; } bb32: { - _53 = copy _38; - _54 = load_type(string); - _52 = call const fn baml.json.path_or(copy _53, const ".message", const "MCP error") -> [bb33]; - } - - bb33: { - _56 = copy _38; - _55 = call const fn baml.json.stringify(copy _56) -> [bb34]; - } - - bb34: { - _44 = ai.errors.InvalidRequest { copy _45, copy _49, copy _52, copy _55 }; - throw copy _44; - } -} - -// lambda[0] -fn .() -> string | baml.iter.Done { - // Locals: - let _0: string | baml.iter.Done // _0 // return - let _1: (baml.sys.ReadPipeLines) -> string | baml.iter.Done throws baml.errors.Io - - bb0: { - _1 = copy capture[0].2; - _0 = call const fn baml.sys.ReadPipeLines.root.iter.Iterator.next(copy _1) -> [bb1]; - } - - bb1: { - return; + _33 = ai.errors.InvalidRequest { copy _34, copy _38, copy _41, copy _44 }; + throw copy _33; } } diff --git a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/ppir.snap b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/ppir.snap index fcabbb66316..f2dfcb49b40 100644 --- a/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/ppir.snap +++ b/baml_language/crates/baml_tests/snapshots/baml_src/stdlib/ai/ppir.snap @@ -657,7 +657,7 @@ class ai.mcp.McpConnection$stream { next_id: int | null } function ai.mcp._request(self: ?, method: string, params: map) -> baml.json.json [expr] { - { let id = self.next_id; self.next_id Add= 1; self._send(rpc_line(id, method, params)); while true { let next = baml.future.with_timeout(baml.time.Duration.from_milliseconds(self.timeout_ms), () -> { { } self.out.next() }) catch_all (e) { _ => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } }; match (next) { line: string => { let trimmed = line.trim() } if (trimmed.length() Gt 0) { let msg = baml.json.parse(trimmed) catch_all (e) { _ => null } } if (baml.json.path_or(msg, ".id", Neg 1) Eq id) { let err = baml.json.path_or(msg, ".error", null); if (err Ne null) { throw root.errors.InvalidRequest { provider: `...`, status_code: baml.json.path_or(err, ".code", null), detail: baml.json.path_or(err, ".message", "MCP error"), raw_body: baml.json.stringify(err) } }; return baml.json.path_or(msg, ".result", null) }, _ => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } } }; throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } + { let id = self.next_id; self.next_id Add= 1; self._send(rpc_line(id, method, params)) } baml.future.with_timeout(baml.time.Duration.from_milliseconds(self.timeout_ms), () -> { { while true { let next = self.out.next() catch_all (e) { _ => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } }; match (next) { line: string => { let trimmed = line.trim() } if (trimmed.length() Gt 0) { let msg = baml.json.parse(trimmed) catch_all (e) { _ => null } } if (baml.json.path_or(msg, ".id", Neg 1) Eq id) { let err = baml.json.path_or(msg, ".error", null); if (err Ne null) { throw root.errors.InvalidRequest { provider: `...`, status_code: baml.json.path_or(err, ".code", null), detail: baml.json.path_or(err, ".message", "MCP error"), raw_body: baml.json.stringify(err) } }; return baml.json.path_or(msg, ".result", null) }, _ => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } } }; throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } }) catch (e) { baml.errors.Timeout => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } } } function ai.mcp._send(self: ?, line: string) -> void [expr] { { let framed = [line, "\n"].join(""); let _ = self.p.stdin.write(framed) catch_all (e) { _ => { throw root.errors.NetworkFailure { status_code: null, provider: `...`, detail: `...`, raw_body: null } } } } @@ -679,7 +679,7 @@ function ai.mcp.tools(self: ?) -> root.tools.Tool[] [expr] { } --- captures --- -lambda () in ?: captures [self] +lambda () in _request: captures [self, id, method] --- /ai/ns_stream/stream.baml --- class ai.stream.Done { diff --git a/baml_language/crates/baml_tests/tests/net.rs b/baml_language/crates/baml_tests/tests/net.rs index 52c480b3c7e..0543c093a7e 100644 --- a/baml_language/crates/baml_tests/tests/net.rs +++ b/baml_language/crates/baml_tests/tests/net.rs @@ -208,6 +208,35 @@ async fn net_read_is_cancellable() { ); } +#[tokio::test] +async fn net_close_cancels_pending_read() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let _conn = listener.accept().await.unwrap(); + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + }); + + let output = baml_test!(&format!( + r#" + function main() -> string {{ + let sock = baml.net.TcpStream.connect("{addr}"); + let read = spawn {{ sock.read(1024) }}; + baml.sys.sleep(baml.time.Duration.from_milliseconds(25n)); + sock.close(); + (await read) catch (e) {{ + baml.errors.Io => {{ return "closed"; }} + }}; + "completed" + }} + "# + )); + server.await.unwrap(); + + assert_eq!(output.result, Ok(BexExternalValue::String("closed".into()))); +} + #[tokio::test] async fn net_read_completes_before_cancellation() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); diff --git a/baml_language/crates/baml_tests/tests/shell.rs b/baml_language/crates/baml_tests/tests/shell.rs index b8931639d48..4897ab3e673 100644 --- a/baml_language/crates/baml_tests/tests/shell.rs +++ b/baml_language/crates/baml_tests/tests/shell.rs @@ -410,6 +410,33 @@ async fn start_process_stdout_read_is_cancellable() { ); } +#[tokio::test] +#[cfg(not(target_os = "windows"))] +async fn start_process_stdout_close_cancels_pending_read() { + let output = baml_test!( + r#" + function main() -> string { + let process = baml.sys.start_process( + "sh", + ["-c", "while :; do :; done"], + null, + ); + defer { process.close() } + + let read = spawn { process.stdout.read(1024) }; + baml.sys.sleep(baml.time.Duration.from_milliseconds(25n)); + process.stdout.close(); + (await read) catch (e) { + baml.errors.Io => { return "closed"; } + }; + "completed" + } + "# + ); + + assert_eq!(output.result, Ok(BexExternalValue::String("closed".into()))); +} + #[tokio::test] #[cfg(unix)] async fn claude_code_client_preserves_process_wait_timeout() { diff --git a/baml_language/crates/sys_native/src/io_impls.rs b/baml_language/crates/sys_native/src/io_impls.rs index 25fa623ece0..d64fe524613 100644 --- a/baml_language/crates/sys_native/src/io_impls.rs +++ b/baml_language/crates/sys_native/src/io_impls.rs @@ -1230,6 +1230,7 @@ struct LiveProcessHandle { struct ReadPipeHandle { reader: tokio::sync::Mutex>>, + close_tx: tokio::sync::watch::Sender, label: String, } @@ -1285,14 +1286,27 @@ fn read_pipe( reader: impl tokio::io::AsyncRead + Send + Unpin + 'static, label: String, ) -> owned::sys::ReadPipe { + let (close_tx, _close_rx) = tokio::sync::watch::channel(false); owned::sys::ReadPipe { _pipe: Arc::new(ReadPipeHandle { reader: tokio::sync::Mutex::new(Some(Box::new(reader))), + close_tx, label, }), } } +fn read_pipe_closed_error(label: &str) -> VmBamlError { + VmBamlError::Io { + message: format!("Read pipe for '{label}' is closed"), + } +} + +async fn close_read_pipe(handle: &ReadPipeHandle) { + handle.close_tx.send_replace(true); + handle.reader.lock().await.take(); +} + fn write_pipe( writer: impl tokio::io::AsyncWrite + Send + Unpin + 'static, label: String, @@ -1374,17 +1388,27 @@ impl io::IoClassSysReadPipe for NativeSysOps { }; SysOpOutput::async_op(async move { let handle = downcast_read_pipe(&readpipe)?; + let mut close_rx = handle.close_tx.subscribe(); + if *close_rx.borrow() { + return Err(read_pipe_closed_error(&handle.label).into()); + } let mut guard = handle.reader.lock().await; - let reader = guard.as_mut().ok_or_else(|| VmBamlError::Io { - message: format!("Read pipe for '{}' is closed", handle.label), - })?; + if *close_rx.borrow() { + return Err(read_pipe_closed_error(&handle.label).into()); + } + let reader = guard + .as_mut() + .ok_or_else(|| read_pipe_closed_error(&handle.label))?; let mut buffer = vec![0u8; limit]; - let read = reader - .read(&mut buffer) - .await - .map_err(|error| VmBamlError::Io { + let read = tokio::select! { + biased; + _ = close_rx.changed() => { + return Err(read_pipe_closed_error(&handle.label).into()); + } + result = reader.read(&mut buffer) => result.map_err(|error| VmBamlError::Io { message: format!("Failed to read from '{}': {error}", handle.label), - })?; + })?, + }; if read == 0 { return Ok(None); } @@ -1402,7 +1426,7 @@ impl io::IoClassSysReadPipe for NativeSysOps { ) -> SysOpOutput<()> { SysOpOutput::async_op(async move { let handle = downcast_read_pipe(&readpipe)?; - handle.reader.lock().await.take(); + close_read_pipe(&handle).await; Ok(()) }) } @@ -1545,7 +1569,7 @@ impl io::IoClassSysProcess for NativeSysOps { .flatten() { if let Ok(handle) = downcast_read_pipe(pipe) { - handle.reader.lock().await.take(); + close_read_pipe(&handle).await; } } Ok(()) @@ -1868,16 +1892,19 @@ fn sleep_nanos_from_delay(delay: BexExternalValue) -> Result // Network // ============================================================================ -// Network handles mirror `FsFileHandle`: the socket lives inside a -// `Mutex>` so `close()` can `take()` it, after which every other -// reference to the same handle deterministically observes a closed socket. +// Network handles keep each socket in an optional slot so `close()` can take +// it, after which every other reference observes a closed socket. // -// `TcpStream` ops need `&mut` access, so (like `fs::File`) we hold the guard -// across the await. `TcpListener`/`UdpSocket` ops only need `&self`, so we keep -// each socket behind an inner `Arc` and clone it out under a brief lock — that -// way `close()` stays deterministic without serializing concurrent +// `TcpStream` ops need `&mut` access, so they hold the guard across the await +// and select against a close notification. `TcpListener`/`UdpSocket` ops only +// need `&self`, so each socket has an inner `Arc` cloned under a brief lock. +// This keeps `close()` deterministic without serializing concurrent // `accept`/`recv_from`/`send_to` on the same socket. -type NetTcpStreamHandle = tokio::sync::Mutex>; +struct NetTcpStreamHandle { + stream: tokio::sync::Mutex>, + close_tx: tokio::sync::watch::Sender, +} + type NetTcpListenerHandle = tokio::sync::Mutex>>; type NetUdpSocketHandle = tokio::sync::Mutex>>; @@ -1908,6 +1935,22 @@ fn downcast_tcpstream( }) } +fn net_tcp_stream(stream: tokio::net::TcpStream) -> owned::net::TcpStream { + let (close_tx, _close_rx) = tokio::sync::watch::channel(false); + owned::net::TcpStream { + _handle: Arc::new(NetTcpStreamHandle { + stream: tokio::sync::Mutex::new(Some(stream)), + close_tx, + }), + } +} + +fn tcp_stream_closed_error() -> VmBamlError { + VmBamlError::Io { + message: "TcpStream is closed".to_string(), + } +} + fn downcast_tcplistener( listener: &owned::net::TcpListener, ) -> Result, VmBamlError> { @@ -1963,9 +2006,7 @@ impl io::IoClassNetTcpStream for NativeSysOps { .map_err(|e| VmBamlError::Io { message: format!("Failed to connect to '{addr}': {e}"), })?; - let handle: Arc = - Arc::new(tokio::sync::Mutex::new(Some(stream))); - Ok(owned::net::TcpStream { _handle: handle }) + Ok(net_tcp_stream(stream)) }) } @@ -1989,17 +2030,25 @@ impl io::IoClassNetTcpStream for NativeSysOps { }; SysOpOutput::async_op(async move { let handle = downcast_tcpstream(&stream)?; - let mut guard = handle.lock().await; - let stream = guard.as_mut().ok_or_else(|| VmBamlError::Io { - message: "TcpStream is closed".to_string(), - })?; + let mut close_rx = handle.close_tx.subscribe(); + if *close_rx.borrow() { + return Err(tcp_stream_closed_error().into()); + } + let mut guard = handle.stream.lock().await; + if *close_rx.borrow() { + return Err(tcp_stream_closed_error().into()); + } + let stream = guard.as_mut().ok_or_else(tcp_stream_closed_error)?; let mut buffer = vec![0u8; limit]; - let read = stream - .read(&mut buffer) - .await - .map_err(|e| VmBamlError::Io { + let read = tokio::select! { + biased; + _ = close_rx.changed() => { + return Err(tcp_stream_closed_error().into()); + } + result = stream.read(&mut buffer) => result.map_err(|e| VmBamlError::Io { message: format!("Failed to read from socket: {e}"), - })?; + })?, + }; if read == 0 { return Ok(None); } @@ -2020,13 +2069,24 @@ impl io::IoClassNetTcpStream for NativeSysOps { SysOpOutput::async_op(async move { let handle = downcast_tcpstream(&stream)?; - let mut guard = handle.lock().await; - let stream = guard.as_mut().ok_or_else(|| VmBamlError::Io { - message: "TcpStream is closed".to_string(), - })?; - let written = stream.write(&data).await.map_err(|e| VmBamlError::Io { - message: format!("Failed to write to socket: {e}"), - })?; + let mut close_rx = handle.close_tx.subscribe(); + if *close_rx.borrow() { + return Err(tcp_stream_closed_error().into()); + } + let mut guard = handle.stream.lock().await; + if *close_rx.borrow() { + return Err(tcp_stream_closed_error().into()); + } + let stream = guard.as_mut().ok_or_else(tcp_stream_closed_error)?; + let written = tokio::select! { + biased; + _ = close_rx.changed() => { + return Err(tcp_stream_closed_error().into()); + } + result = stream.write(&data) => result.map_err(|e| VmBamlError::Io { + message: format!("Failed to write to socket: {e}"), + })?, + }; i64::try_from(written).map_err(|_| { VmRustFnError::from(VmBamlError::Io { message: "socket write count exceeds int range".to_string(), @@ -2046,9 +2106,10 @@ impl io::IoClassNetTcpStream for NativeSysOps { SysOpOutput::async_op(async move { let handle = downcast_tcpstream(&stream)?; + handle.close_tx.send_replace(true); // Take the stream out of the shared handle so any other reference // observes a closed socket on its next op. Already-closed is a no-op. - let Some(mut stream) = handle.lock().await.take() else { + let Some(mut stream) = handle.stream.lock().await.take() else { return Ok(()); }; // shutdown() flushes pending writes and closes the write half, so @@ -2110,11 +2171,7 @@ impl io::IoClassNetTcpListener for NativeSysOps { let (stream, _peer) = inner.accept().await.map_err(|e| VmBamlError::Io { message: format!("Failed to accept connection: {e}"), })?; - let sock_handle: Arc = - Arc::new(tokio::sync::Mutex::new(Some(stream))); - Ok(owned::net::TcpStream { - _handle: sock_handle, - }) + Ok(net_tcp_stream(stream)) }) }