From 832dae44fbbafb8769f5fdb538429650b2374e0e Mon Sep 17 00:00:00 2001 From: charles Date: Thu, 12 Mar 2026 23:19:46 -0700 Subject: [PATCH] fix: VarChar handling raw string, VarChar Inz silent drop --- rpgrt/src/lib.rs | 55 ++++++++++++++++++ samples/3np1.rpg.stdout | 6 +- src/codegen.rs | 120 +++++++++++++++++++++++++++++----------- 3 files changed, 146 insertions(+), 35 deletions(-) diff --git a/rpgrt/src/lib.rs b/rpgrt/src/lib.rs index 214307e..49e2bf7 100644 --- a/rpgrt/src/lib.rs +++ b/rpgrt/src/lib.rs @@ -11,6 +11,8 @@ //! | `rpg_dsply_cstr` | `(ptr: *const c_char)` | Display a null-terminated C string | //! | `rpg_dsply_i64` | `(n: i64)` | Display a signed 64-bit integer | //! | `rpg_dsply_f64` | `(f: f64)` | Display a double-precision float | +//! | `rpg_dsply_read` | `(prompt: *const c_char, response: *mut i64)` | Display null-term prompt & read i64 | +//! | `rpg_dsply_read_len`| `(ptr: *const u8, len: i64, response: *mut i64)` | Display length-delimited prompt & read i64 | //! | `rpg_halt` | `(code: i32)` | Abnormal program termination | //! | `rpg_char_i64` | `(n: i64) -> *const c_char` | Format integer as null-term C string | //! | `rpg_concat` | `(a: *const c_char, b: *const c_char) -> *const c_char` | Concatenate two C strings | @@ -209,6 +211,59 @@ pub unsafe extern "C" fn rpg_dsply_read( } } +// ───────────────────────────────────────────────────────────────────────────── +// rpg_dsply_read_len — display a length-delimited prompt and read a response +// ───────────────────────────────────────────────────────────────────────────── + +/// Three-operand DSPLY where the prompt is a fixed-length (or VarChar) field +/// passed as `(ptr, len)` rather than a null-terminated C string. +/// +/// This is called when the prompt operand is a `Char` or `VarChar` variable — +/// those are stored as raw byte buffers with no null terminator, so +/// `rpg_dsply_read` (which calls `CStr::from_ptr`) cannot be used safely. +/// +/// Trailing ASCII spaces are stripped from the prompt before display, matching +/// the behaviour of `rpg_dsply`. +/// +/// # Safety +/// +/// * `ptr` must be valid for at least `len` bytes (or be null). +/// * `response` must be a valid, writable `i64` pointer. +#[no_mangle] +pub unsafe extern "C" fn rpg_dsply_read_len( + ptr: *const u8, + len: i64, + response: *mut i64, +) { + use std::io::BufRead; + + // Build the prompt string the same way rpg_dsply does. + let bytes = if ptr.is_null() || len <= 0 { + b"" as &[u8] + } else { + unsafe { slice::from_raw_parts(ptr, len as usize) } + }; + let trimmed = rtrim_spaces(bytes); + let text = String::from_utf8_lossy(trimmed); + + { + let stdout = io::stdout(); + let mut out = stdout.lock(); + let _ = writeln!(out, "DSPLY {}", text); + let _ = out.flush(); + } + + // Read one line from stdin and parse it as i64. + let stdin = io::stdin(); + let mut line = String::new(); + if stdin.lock().read_line(&mut line).is_ok() { + let trimmed_line = line.trim(); + if let Ok(n) = trimmed_line.parse::() { + unsafe { *response = n; } + } + } +} + // ───────────────────────────────────────────────────────────────────────────── // rpg_halt — abnormal termination // ───────────────────────────────────────────────────────────────────────────── diff --git a/samples/3np1.rpg.stdout b/samples/3np1.rpg.stdout index 3d073bb..5408c48 100644 --- a/samples/3np1.rpg.stdout +++ b/samples/3np1.rpg.stdout @@ -1,8 +1,8 @@ -DSPLY +DSPLY Enter a positive integer (or 0 to exit): DSPLY Sequence for 8: DSPLY 4 DSPLY 2 DSPLY 1 DSPLY Reached 1 in 3 iterations. -DSPLY -DSPLY +DSPLY +DSPLY Enter a positive integer (or 0 to exit): diff --git a/src/codegen.rs b/src/codegen.rs index e9cb541..8cd32f1 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -341,13 +341,22 @@ impl<'ctx> Codegen<'ctx> { self.module.add_function("rpg_concat", concat_ty, None); // void rpg_dsply_read(const char *prompt, i64 *response) - // Three-operand DSPLY: display prompt and read an i64 from stdin. + // Three-operand DSPLY: display null-terminated prompt and read an i64 from stdin. let i64_ptr = self.context.ptr_type(AddressSpace::default()); let dsply_read_ty = void_t.fn_type( &[i8_ptr.into(), i64_ptr.into()], false, ); self.module.add_function("rpg_dsply_read", dsply_read_ty, None); + + // void rpg_dsply_read_len(const u8 *ptr, i64 len, i64 *response) + // Three-operand DSPLY where the prompt is a length-delimited byte buffer + // (Char / VarChar variable) rather than a null-terminated C string. + let dsply_read_len_ty = void_t.fn_type( + &[i8_ptr.into(), i64_t.into(), i64_ptr.into()], + false, + ); + self.module.add_function("rpg_dsply_read_len", dsply_read_len_ty, None); } // ── Global declarations ───────────────────────────────────────────────── @@ -745,8 +754,10 @@ impl<'ctx> Codegen<'ctx> { state: &mut FnState<'ctx>, ) -> Result<(), CodegenError> { match ty { - TypeSpec::Char(size_expr) => { - // Copy a string literal into the char buffer (space-padded). + TypeSpec::Char(size_expr) | TypeSpec::VarChar(size_expr) => { + // Copy a string literal into the char/varchar buffer (space-padded to + // the declared length so that rpg_dsply and rpg_dsply_read_len get + // clean data with no uninitialized bytes past the content). if let Expression::Literal(Literal::String(s)) = expr { let field_len = const_int_from_expr(size_expr).unwrap_or(s.len() as u64) as usize; // Build a space-padded string of exactly `field_len` bytes. @@ -1005,30 +1016,6 @@ impl<'ctx> Codegen<'ctx> { // When a response variable is present we display the prompt and then // read an integer from stdin into the response variable. if let Some(resp_name) = &d.response { - // Coerce the prompt expression to a C string pointer. - let prompt_ptr = match &d.expr { - Expression::Variable(qname) => { - let name = qname.leaf(); - if let Some((ptr, _)) = self.resolve_var(name, state) { - ptr.into() - } else { - self.intern_string("").into() - } - } - Expression::Literal(Literal::String(s)) => { - self.intern_string(s).into() - } - other => { - if let Ok(BasicValueEnum::PointerValue(ptr)) = - self.gen_expression(other, state) - { - ptr.into() - } else { - self.intern_string("").into() - } - } - }; - // Resolve the response variable; allocate a fresh i64 slot in the // function entry block if not already present. Using // alloca_i64_in_entry ensures the slot is created once regardless @@ -1055,14 +1042,83 @@ impl<'ctx> Codegen<'ctx> { p }; - if let Some(read_fn) = self.module.get_function("rpg_dsply_read") { - self.builder - .build_call(read_fn, &[prompt_ptr, resp_ptr.into()], "dsply_read") - .ok(); + // Choose the right runtime call based on the prompt operand type: + // + // • Char/VarChar variable → rpg_dsply_read_len(ptr, len, resp) + // The storage is a raw byte buffer with no null terminator, so + // we pass the pointer and byte length directly. + // + // • String literal or other expression → rpg_dsply_read(cstr, resp) + // intern_string always null-terminates its result. + match &d.expr { + Expression::Variable(qname) => { + let name = qname.leaf(); + if let Some((ptr, ty)) = self.resolve_var(name, state) { + match &ty { + TypeSpec::Char(_) | TypeSpec::VarChar(_) => { + // Length-delimited path. + let len = ty.byte_size().unwrap_or(0); + let len_val = self.context.i64_type().const_int(len, false); + if let Some(read_fn) = self.module.get_function("rpg_dsply_read_len") { + self.builder + .build_call( + read_fn, + &[ptr.into(), len_val.into(), resp_ptr.into()], + "dsply_read_len", + ) + .ok(); + } + } + _ => { + // Treat as a null-terminated C string (e.g. a + // pointer returned by %Char / rpg_char_i64). + if let Some(read_fn) = self.module.get_function("rpg_dsply_read") { + self.builder + .build_call(read_fn, &[ptr.into(), resp_ptr.into()], "dsply_read") + .ok(); + } + } + } + } else { + // Unknown variable — display an empty prompt and still read. + let empty = self.intern_string(""); + if let Some(read_fn) = self.module.get_function("rpg_dsply_read") { + self.builder + .build_call(read_fn, &[empty.into(), resp_ptr.into()], "dsply_read") + .ok(); + } + } + } + Expression::Literal(Literal::String(s)) => { + let s = s.clone(); + let cstr = self.intern_string(&s); + if let Some(read_fn) = self.module.get_function("rpg_dsply_read") { + self.builder + .build_call(read_fn, &[cstr.into(), resp_ptr.into()], "dsply_read") + .ok(); + } + } + other => { + // Evaluate the expression; if it yields a pointer (e.g. the + // result of a %Char() / rpg_concat call) use the cstr path. + let other = other.clone(); + let prompt_ptr = if let Ok(BasicValueEnum::PointerValue(ptr)) = + self.gen_expression(&other, state) + { + ptr + } else { + self.intern_string("") + }; + if let Some(read_fn) = self.module.get_function("rpg_dsply_read") { + self.builder + .build_call(read_fn, &[prompt_ptr.into(), resp_ptr.into()], "dsply_read") + .ok(); + } + } } + return Ok(()); } - // ── One-operand form: DSPLY expr ────────────────────────────────── match &d.expr { Expression::Variable(qname) => {