Skip to content

Commit 3774163

Browse files
olwangclaude
andcommitted
Merge fix/devstdout: backlog item (sub-agent, worktree)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 parents dad0bf8 + 12cde22 commit 3774163

4 files changed

Lines changed: 204 additions & 27 deletions

File tree

crates/rsscript/src/cli/dev.rs

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
77

88
use rsscript::{
99
EvalError, EvalOutput, NativeValue, eval_package_main_with_args_and_native_bindings,
10-
format_diagnostics_human, format_diagnostics_json, load_package_native_bindings,
11-
reg_vm_eval_source_main_with_args,
10+
eval_package_main_with_args_and_native_bindings_streaming_stdout,
11+
eval_source_main_with_args_streaming_stdout, format_diagnostics_human, format_diagnostics_json,
12+
load_package_native_bindings, reg_vm_eval_source_main_with_args,
1213
};
1314

1415
use super::check::run_check;
1516
use super::lint::run_lint;
16-
use super::run_cmd::run_generated_rust;
17+
use super::run_cmd::{run_generated_rust, run_generated_rust_streaming};
1718
use super::{is_package_directory, print_usage};
1819

1920
/// Action `rss dev` re-runs on every change. The default is the pure frontend
@@ -175,6 +176,11 @@ fn run_once(options: &DevOptions<'_>, path: &str) -> ExitCode {
175176
// reg-VM interpreter tier (no rustc cost); `--release` switches to the
176177
// Rust-lowering AOT tier. Both observe identical semantics + diagnostics
177178
// (the VM↔compiled parity invariant), so the fast tier is trustworthy.
179+
// Stream the compiled program's stdio live in human mode; JSON mode keeps
180+
// the captured path so its structured diagnostics stay intact.
181+
DevAction::Run if options.release && !options.json => {
182+
run_generated_rust_streaming(&run_args(options, path))
183+
}
178184
DevAction::Run if options.release => run_generated_rust(&run_args(options, path)),
179185
DevAction::Run => run_via_vm(options, path),
180186
};
@@ -195,39 +201,67 @@ fn run_once(options: &DevOptions<'_>, path: &str) -> ExitCode {
195201
/// runs through the package VM with its native host bindings loaded; a single
196202
/// file runs through the source VM, mirroring `rss eval`.
197203
fn run_via_vm(options: &DevOptions<'_>, path: &str) -> ExitCode {
204+
// Stream program stdout live in human mode so a slow/looping run shows output
205+
// as it happens instead of buffering until exit. JSON mode keeps capturing so
206+
// the diagnostics JSON is the only thing on stdout.
207+
let stream = !options.json;
198208
let result = if is_package_directory(path) {
199-
run_package_via_vm(path)
209+
run_package_via_vm(path, stream)
200210
} else {
201211
match fs::read_to_string(path) {
202212
Ok(source) => {
203-
reg_vm_eval_source_main_with_args(path, &source, std::iter::empty::<&str>())
213+
if stream {
214+
eval_source_main_with_args_streaming_stdout(
215+
path,
216+
&source,
217+
std::iter::empty::<&str>(),
218+
)
219+
} else {
220+
reg_vm_eval_source_main_with_args(path, &source, std::iter::empty::<&str>())
221+
}
204222
}
205223
Err(error) => {
206224
eprintln!("failed to read {path}: {error}");
207225
return ExitCode::from(2);
208226
}
209227
}
210228
};
211-
finish_vm_run(options, result)
229+
finish_vm_run(options, result, stream)
212230
}
213231

214-
fn run_package_via_vm(path: &str) -> Result<EvalOutput, EvalError> {
232+
fn run_package_via_vm(path: &str, stream: bool) -> Result<EvalOutput, EvalError> {
215233
let package_dir = Path::new(path);
216234
let bindings = load_package_native_bindings(package_dir).map_err(EvalError::Runtime)?;
217-
eval_package_main_with_args_and_native_bindings(
218-
package_dir,
219-
std::iter::empty::<&str>(),
220-
bindings,
221-
)
235+
if stream {
236+
eval_package_main_with_args_and_native_bindings_streaming_stdout(
237+
package_dir,
238+
std::iter::empty::<&str>(),
239+
bindings,
240+
)
241+
} else {
242+
eval_package_main_with_args_and_native_bindings(
243+
package_dir,
244+
std::iter::empty::<&str>(),
245+
bindings,
246+
)
247+
}
222248
}
223249

224250
/// Render a VM run result the same way `rss eval` does: program stdout/stderr,
225251
/// then the `main` return value, with an `Err` return reported as a failed run
226252
/// (matching the AOT backend's exit behavior).
227-
fn finish_vm_run(options: &DevOptions<'_>, result: Result<EvalOutput, EvalError>) -> ExitCode {
253+
fn finish_vm_run(
254+
options: &DevOptions<'_>,
255+
result: Result<EvalOutput, EvalError>,
256+
streamed: bool,
257+
) -> ExitCode {
228258
match result {
229259
Ok(output) => {
230-
print!("{}", output.stdout);
260+
// When the run streamed stdout live, the program output is already on
261+
// the terminal — printing `output.stdout` again would duplicate it.
262+
if !streamed {
263+
print!("{}", output.stdout);
264+
}
231265
eprint!("{}", output.stderr);
232266
if let Some(NativeValue::Variant { name, .. }) = &output.native_value
233267
&& name == "Err"

crates/rsscript/src/cli/run_cmd.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ fn parse_run_args(args: &[String]) -> Result<RunOptions<'_>, String> {
6363
})
6464
}
6565
pub(crate) fn run_generated_rust(args: &[String]) -> ExitCode {
66+
run_generated_rust_inner(args, false)
67+
}
68+
69+
/// Like [`run_generated_rust`] but inherits the child's stdio so the compiled
70+
/// program's output streams live instead of being captured and printed at exit.
71+
/// Used by `rss dev --run --release` so a slow/looping run shows progress. `rss
72+
/// run` keeps capturing (so its runtime-diagnostic parsing is unchanged).
73+
pub(crate) fn run_generated_rust_streaming(args: &[String]) -> ExitCode {
74+
run_generated_rust_inner(args, true)
75+
}
76+
77+
fn run_generated_rust_inner(args: &[String], stream_stdio: bool) -> ExitCode {
6678
let options = match parse_run_args(args) {
6779
Ok(options) => options,
6880
Err(error) => {
@@ -133,6 +145,33 @@ pub(crate) fn run_generated_rust(args: &[String]) -> ExitCode {
133145
if let Ok(current_dir) = std::env::current_dir() {
134146
cargo.env("RSS_RUN_WORKSPACE_ROOT", current_dir);
135147
}
148+
if stream_stdio {
149+
// Inherit stdio so cargo's build progress and the program's stdout stream
150+
// live to the terminal (no buffering until exit). The captured-output
151+
// diagnostic post-processing is intentionally skipped here; it only runs
152+
// for the captured `rss run` path.
153+
let status = match cargo.status() {
154+
Ok(status) => status,
155+
Err(error) => {
156+
eprintln!("failed to run cargo: {error}");
157+
if cleanup_package_dir {
158+
cleanup_temp_dir(&package_dir);
159+
}
160+
return ExitCode::from(2);
161+
}
162+
};
163+
if cleanup_package_dir {
164+
cleanup_temp_dir(&package_dir);
165+
}
166+
return if status.success() {
167+
ExitCode::SUCCESS
168+
} else {
169+
status
170+
.code()
171+
.map(|code| ExitCode::from(code as u8))
172+
.unwrap_or_else(|| ExitCode::from(1))
173+
};
174+
}
136175
let output = match cargo.output() {
137176
Ok(output) => output,
138177
Err(error) => {

crates/rsscript/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ pub use reg_vm::{
9292
reg_vm_compile_source as vm_compile_source, reg_vm_compile_source,
9393
reg_vm_eval_package_main_with_args as eval_package_main_with_args,
9494
reg_vm_eval_package_main_with_args_and_native_bindings as eval_package_main_with_args_and_native_bindings,
95+
reg_vm_eval_package_main_with_args_and_native_bindings_streaming_stdout as eval_package_main_with_args_and_native_bindings_streaming_stdout,
9596
reg_vm_eval_source_main as eval_source_main, reg_vm_eval_source_main_jit,
97+
reg_vm_eval_source_main_with_args_streaming_stdout as eval_source_main_with_args_streaming_stdout,
9698
reg_vm_eval_source_main_with_args,
9799
reg_vm_eval_source_main_with_args as eval_source_main_with_args,
98100
reg_vm_eval_source_main_with_args as vm_eval_source_main_with_args,

crates/rsscript/src/reg_vm/mod.rs

Lines changed: 115 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,34 @@ pub fn reg_vm_eval_source_main_with_args_and_native_bindings(
11141114
.eval_main_with_args_and_native_bindings(args, native_bindings)
11151115
}
11161116

1117+
/// Streaming-stdout source entry point for `rss dev --run`: evaluates `main` and
1118+
/// writes `Log.write` output live (line-flushed) to the real process stdout as it
1119+
/// runs. The captured stdout in the returned `EvalOutput` is unchanged, so it must
1120+
/// not be re-printed by the caller. Other callers and the tests keep using the
1121+
/// non-streaming `reg_vm_eval_source_main_with_args`, whose behavior is untouched.
1122+
pub fn reg_vm_eval_source_main_with_args_streaming_stdout(
1123+
file: &str,
1124+
source: &str,
1125+
args: impl IntoIterator<Item = impl Into<String>>,
1126+
) -> Result<EvalOutput, EvalError> {
1127+
reg_vm_compile_source(file, source)?
1128+
.eval_main_with_args_and_native_bindings_streaming_stdout(
1129+
args,
1130+
std::iter::empty::<(String, NativeInterpreterFn)>(),
1131+
)
1132+
}
1133+
1134+
/// Streaming-stdout package entry point for `rss dev --run`. See
1135+
/// [`reg_vm_eval_source_main_with_args_streaming_stdout`].
1136+
pub fn reg_vm_eval_package_main_with_args_and_native_bindings_streaming_stdout(
1137+
package_dir: &Path,
1138+
args: impl IntoIterator<Item = impl Into<String>>,
1139+
native_bindings: impl IntoIterator<Item = (impl Into<String>, NativeInterpreterFn)>,
1140+
) -> Result<EvalOutput, EvalError> {
1141+
reg_vm_compile_package(package_dir)?
1142+
.eval_main_with_args_and_native_bindings_streaming_stdout(args, native_bindings)
1143+
}
1144+
11171145
pub fn reg_vm_eval_package_main_with_args(
11181146
package_dir: &Path,
11191147
args: impl IntoIterator<Item = impl Into<String>>,
@@ -1387,6 +1415,47 @@ impl RegVmExecutable {
13871415
})
13881416
}
13891417

1418+
/// Like [`Self::eval_main_with_args_and_native_bindings`] but streams program
1419+
/// stdout (`Log.write` output) live to the real process stdout, line-flushed,
1420+
/// as the program runs. Used ONLY by `rss dev --run` so a slow/looping program
1421+
/// shows output immediately instead of buffering until exit. The returned
1422+
/// `EvalOutput.stdout` is still the full captured buffer (identical to the
1423+
/// non-streaming call), so the program output has already been written to the
1424+
/// terminal — the caller must NOT print it a second time.
1425+
pub fn eval_main_with_args_and_native_bindings_streaming_stdout(
1426+
&self,
1427+
args: impl IntoIterator<Item = impl Into<String>>,
1428+
native_bindings: impl IntoIterator<Item = (impl Into<String>, NativeInterpreterFn)>,
1429+
) -> Result<EvalOutput, EvalError> {
1430+
let mut vm = RegVm::new(
1431+
Rc::clone(&self.unit),
1432+
args.into_iter().map(Into::into).collect(),
1433+
native_bindings
1434+
.into_iter()
1435+
.map(|(key, function)| (key.into(), function))
1436+
.collect(),
1437+
);
1438+
vm.stream_stdout = true;
1439+
let result = vm.run_program("main");
1440+
// Flush any final line that lacks a trailing newline so no output is lost.
1441+
if vm.stream_flushed < vm.stdout.len() {
1442+
let mut out = std::io::stdout();
1443+
let _ = out.write_all(vm.stdout[vm.stream_flushed..].as_bytes());
1444+
let _ = out.flush();
1445+
vm.stream_flushed = vm.stdout.len();
1446+
}
1447+
let value = result?;
1448+
let display_value = value.display();
1449+
let native_value = value.native_value();
1450+
Ok(EvalOutput {
1451+
value: display_value.clone(),
1452+
display_value,
1453+
native_value,
1454+
stdout: vm.stdout,
1455+
stderr: vm.stderr,
1456+
})
1457+
}
1458+
13901459
pub fn eval_main_with_args_and_native_bindings(
13911460
&self,
13921461
args: impl IntoIterator<Item = impl Into<String>>,
@@ -6056,6 +6125,15 @@ struct RegVm {
60566125
args: Vec<String>,
60576126
native_bindings: HashMap<String, NativeInterpreterFn>,
60586127
stdout: String,
6128+
/// When set, complete lines appended to `stdout` are also written live to the
6129+
/// real process stdout (line-flushed). Used ONLY by `rss dev --run` so a slow
6130+
/// or looping program shows output as it runs instead of buffering until exit.
6131+
/// `stream_flushed` tracks how many bytes of `stdout` have been streamed so a
6132+
/// partial trailing line is not emitted twice. The captured `stdout` String is
6133+
/// built identically whether or not streaming is on, so every other caller
6134+
/// (and the parity/differential tests) is unaffected.
6135+
stream_stdout: bool,
6136+
stream_flushed: usize,
60596137
stderr: String,
60606138
stack: Vec<VmValue>,
60616139
written: Vec<bool>,
@@ -6376,6 +6454,8 @@ impl RegVm {
63766454
args,
63776455
native_bindings,
63786456
stdout: String::new(),
6457+
stream_stdout: false,
6458+
stream_flushed: 0,
63796459
stderr: String::new(),
63806460
stack: Vec::new(),
63816461
written: Vec::new(),
@@ -6402,6 +6482,32 @@ impl RegVm {
64026482
}
64036483
}
64046484

6485+
/// Append program output to the captured `stdout` buffer, and — when live
6486+
/// streaming is enabled (`rss dev --run`) — flush newly completed lines to the
6487+
/// real process stdout immediately. The captured buffer is appended to exactly
6488+
/// the same way regardless, so callers that read `EvalOutput.stdout` see no
6489+
/// difference.
6490+
fn push_stdout(&mut self, text: &str) {
6491+
self.stdout.push_str(text);
6492+
if self.stream_stdout {
6493+
self.flush_stdout_stream();
6494+
}
6495+
}
6496+
6497+
/// Write every complete (newline-terminated) line appended since the last
6498+
/// flush to the real process stdout, then advance the streamed cursor. A
6499+
/// partial trailing line is left buffered until its newline arrives.
6500+
fn flush_stdout_stream(&mut self) {
6501+
if let Some(offset) = self.stdout[self.stream_flushed..].rfind('\n') {
6502+
let end = self.stream_flushed + offset + 1;
6503+
let chunk = &self.stdout[self.stream_flushed..end];
6504+
let mut out = std::io::stdout();
6505+
let _ = out.write_all(chunk.as_bytes());
6506+
let _ = out.flush();
6507+
self.stream_flushed = end;
6508+
}
6509+
}
6510+
64056511
/// Whether `func` should run on the tier-0 JIT. Reads the analysis cached on
64066512
/// the function (`(eligible, has_loop)`, computed once for the whole unit by
64076513
/// [`compute_jit_eligibility`], which already accounts for cross-function
@@ -9227,8 +9333,7 @@ impl RegVm {
92279333
Ok(json_result(if sql.trim().is_empty() {
92289334
Err(db_error_value("SQL query is empty"))
92299335
} else {
9230-
self.stdout
9231-
.push_str(&format!("db query on {}: {sql}\n", conn.url));
9336+
self.push_stdout(&format!("db query on {}: {sql}\n", conn.url));
92329337
conn.queries.push(sql);
92339338
self.set_reg(base + conn_reg, conn.to_value());
92349339
Ok(VmValue::Unit)
@@ -9803,8 +9908,9 @@ impl RegVm {
98039908
}
98049909
RegIntrinsic::ImageInspect => {
98059910
let image = expect_image_state(intrinsic_arg(&self.stack, base, args, 0)?)?;
9806-
self.stdout.push_str(&image.inspect_line());
9807-
self.stdout.push('\n');
9911+
let line = image.inspect_line();
9912+
self.push_stdout(&line);
9913+
self.push_stdout("\n");
98089914
Ok(VmValue::Unit)
98099915
}
98109916
RegIntrinsic::ImageLoad => {
@@ -9924,24 +10030,20 @@ impl RegVm {
992410030
expect_string_ref(intrinsic_arg(&self.stack, base, args, 0)?)?.to_string();
992510031
let message =
992610032
expect_string_ref(intrinsic_arg(&self.stack, base, args, 1)?)?.to_string();
9927-
self.stdout.push_str("trace ");
9928-
self.stdout.push_str(&event);
9929-
self.stdout.push_str(": ");
9930-
self.stdout.push_str(&message);
9931-
self.stdout.push('\n');
10033+
self.push_stdout(&format!("trace {event}: {message}\n"));
993210034
Ok(VmValue::Unit)
993310035
}
993410036
RegIntrinsic::LogWrite => {
993510037
let line =
993610038
expect_string_ref(intrinsic_arg(&self.stack, base, args, 0)?)?.to_string();
9937-
self.stdout.push_str(&line);
9938-
self.stdout.push('\n');
10039+
self.push_stdout(&line);
10040+
self.push_stdout("\n");
993910041
Ok(VmValue::Unit)
994010042
}
994110043
RegIntrinsic::LogWriteJson => {
994210044
let value = expect_json_ref(intrinsic_arg(&self.stack, base, args, 0)?)?;
9943-
self.stdout.push_str(&value.to_string());
9944-
self.stdout.push('\n');
10045+
self.push_stdout(&value.to_string());
10046+
self.push_stdout("\n");
994510047
Ok(VmValue::Unit)
994610048
}
994710049
RegIntrinsic::MapContainsKey | RegIntrinsic::MapFilter | RegIntrinsic::MapFold | RegIntrinsic::MapForEach | RegIntrinsic::MapGetOrDefault | RegIntrinsic::MapIsEmpty | RegIntrinsic::MapKeys | RegIntrinsic::MapLen | RegIntrinsic::MapMapValues | RegIntrinsic::MapMerge | RegIntrinsic::MapNew | RegIntrinsic::MapTryFold | RegIntrinsic::MapValues => self.exec_map_intrinsics(unit, intrinsic, args, base, next_base),

0 commit comments

Comments
 (0)