@@ -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+
11171145pub 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