Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ breaking entries are marked **BREAKING**.
existing implementations keep compiling; not breaking.

### Fixed
- **`printf` refuses an operand it cannot read as a number** — `printf '%d' 0xff`
printed `0`; it now names `255` and `$(( 0xff ))`. `007`, `abc`, a fraction and
a 64-bit overflow refuse too. A missing operand is still `0`; awk is unchanged.
- **`${path:-default}` inside `$(( ))` follows the ordinary `:-` contract** —
unset, null, an empty string, a missing key, and an out-of-bounds index select
the default; a shape error stays loud instead of quietly running the fallback.
Expand Down
2 changes: 1 addition & 1 deletion crates/kaish-kernel/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ pub use result::{apply_output_format, hex_dump, json_to_value, json_to_value_no_
pub use scope::{PathError, Scope};
// Crate-internal: the reduced sync evaluator (scheduler/pipeline.rs) reuses the
// resolver error-message shape without widening the public API.
pub(crate) use eval::format_path;
pub(crate) use eval::{format_path, is_i64_overflow_shape};
7 changes: 4 additions & 3 deletions crates/kaish-kernel/src/interpreter/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,9 +641,10 @@ pub fn value_to_exit_code(value: &Value) -> anyhow::Result<i64> {
}

/// True for a string shaped like `-?[0-9]+` — the only shape whose `i64`
/// parse can fail exclusively by overflow. Shared by `value_to_exit_code`
/// and `value_to_num` so both name the same 64-bit limit the same way.
fn is_i64_overflow_shape(t: &str) -> bool {
/// parse can fail exclusively by overflow. Shared by `value_to_exit_code`,
/// `value_to_num` and printf's operand reader so all three name the same
/// 64-bit limit the same way.
pub(crate) fn is_i64_overflow_shape(t: &str) -> bool {
let digits = t.strip_prefix('-').unwrap_or(t);
!digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit())
}
Expand Down
14 changes: 9 additions & 5 deletions crates/kaish-kernel/src/tools/builtin/awk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1718,13 +1718,16 @@ impl std::fmt::Display for AwkValue {
}
}

// awk never refuses a conversion: in POSIX awk a non-numeric string IS 0, and
// `to_number` is that rule. These Results are always `Ok` on purpose — the
// printf refusal must not reach a language whose own spec answers 0.
impl super::format_string::FormatArg for AwkValue {
fn as_format_string(&self) -> String { self.to_string() }
fn as_format_int(&self) -> i64 { self.to_number() as i64 }
fn as_format_float(&self) -> f64 { self.to_number() }
fn as_format_char(&self) -> Option<char> {
fn as_format_int(&self) -> Result<i64, String> { Ok(self.to_number() as i64) }
fn as_format_float(&self) -> Result<f64, String> { Ok(self.to_number()) }
fn as_format_char(&self) -> Result<Option<char>, String> {
let n = self.to_number() as u32;
char::from_u32(n)
Ok(char::from_u32(n))
}
}

Expand Down Expand Up @@ -2841,7 +2844,8 @@ impl AwkRuntime {
}

fn sprintf(&self, format: &str, args: &[AwkValue]) -> Result<String, String> {
Ok(super::format_string::format_string(format, args))
// Always `Ok` in practice: `AwkValue`'s conversions never refuse.
super::format_string::format_string(format, args)
}
}

Expand Down
Loading