///|
priv struct Ctx {
out : Array[Byte]
args : Array[String]
mut arg_index : Int
messages : Array[String]
mut exit_error : Bool
mut stop : Bool
mut fatal : String?
}
///|
fn help_message() -> String {
let message =
#|Usage: printf FORMAT [ARGUMENT...]
#|
#|Format and print ARGUMENTs under control of FORMAT, like printf(1).
#|The FORMAT is reused as necessary to consume all arguments.
#|
#|Escapes: \a \b \e \f \n \r \t \v \\ \" \' \NNN \0NNN \xHH \uHHHH
#|\UHHHHHHHH, and \c (stop all output).
#|
#|Conversions: %s %c %b %d %i %u %o %x %X %f %e %E %g %G %%, with the
#|flags -+ 0#, field width, and precision (both accept *).
#|%b interprets escapes in its argument.
#|
#|Use -- before a FORMAT that starts with '-'.
message
}
///|
fn Ctx::next_arg(self : Ctx) -> String? {
if self.arg_index < self.args.length() {
let value = self.args[self.arg_index]
self.arg_index += 1
Some(value)
} else {
None
}
}
///|
/// Record a diagnostic that also makes the final exit status 1.
fn Ctx::warn(self : Ctx, msg : String) -> Unit {
self.messages.push(msg)
self.exit_error = true
}
///|
/// Record a diagnostic that leaves the exit status unchanged.
fn Ctx::notice(self : Ctx, msg : String) -> Unit {
self.messages.push(msg)
}
///|
fn is_blank(c : Char) -> Bool {
c.to_int() is (0x20 | 0x09 | 0x0A | 0x0D | 0x0B | 0x0C)
}
///|
let uint64_max : UInt64 = 0xFFFF_FFFF_FFFF_FFFF
///|
let int64_max_mag : UInt64 = 0x7FFF_FFFF_FFFF_FFFF
///|
let int64_min_mag : UInt64 = 0x8000_0000_0000_0000
///|
/// Parse a printf integer argument like strtoimax/strtoumax: leading blanks,
/// 'C or "C for a character code, then the longest valid decimal, 0x hex, or
/// leading-0 octal prefix. Trailing text warns; overflow clamps and warns.
/// Returns the value's 64-bit pattern.
fn parse_integer(ctx : Ctx, text : String, unsigned~ : Bool) -> Int64 {
let chars : Array[Char] = text.iter().collect()
let mut i = 0
while i < chars.length() && is_blank(chars[i]) {
i += 1
}
if i >= chars.length() {
ctx.warn("printf: '\{text}': expected a numeric value")
return 0
}
if chars[i] == '\'' || chars[i] == '"' {
if i + 1 >= chars.length() {
ctx.warn("printf: '\{text}': expected a numeric value")
return 0
}
if i + 2 < chars.length() {
let sb = StringBuilder()
for k in (i + 2)..= 0 {
base = 16
digits_start = i + 2
} else if i < chars.length() && chars[i] == '0' {
base = 8
}
let mut j = digits_start
let mut any = false
let mut mag : UInt64 = 0
let mut overflow = false
while j < chars.length() {
let d = match base {
16 => hex_val(chars[j])
8 => if chars[j] is ('0'..='7') { chars[j].to_int() - 0x30 } else { -1 }
_ => if chars[j] is ('0'..='9') { chars[j].to_int() - 0x30 } else { -1 }
}
if d < 0 {
break
}
any = true
let big_base = base.to_int64().reinterpret_as_uint64()
let big_d = d.to_int64().reinterpret_as_uint64()
if mag > (uint64_max - big_d) / big_base {
overflow = true
} else {
mag = mag * big_base + big_d
}
j += 1
}
if !any {
ctx.warn("printf: '\{text}': expected a numeric value")
return 0
}
// A range error supersedes the partial-conversion warning, like GNU.
let range_error = if unsigned {
overflow
} else if neg {
overflow || mag > int64_min_mag
} else {
overflow || mag > int64_max_mag
}
if range_error {
ctx.warn("printf: '\{text}': Result too large")
return if unsigned {
uint64_max.reinterpret_as_int64()
} else if neg {
int64_min_mag.reinterpret_as_int64()
} else {
int64_max_mag.reinterpret_as_int64()
}
}
if j < chars.length() {
ctx.warn("printf: '\{text}': value not completely converted")
}
let bits = mag.reinterpret_as_int64()
if neg {
-bits
} else {
bits
}
}
///|
fn matches_word(chars : Array[Char], start : Int, word : String) -> Bool {
let want : Array[Char] = word.iter().collect()
if start + want.length() > chars.length() {
return false
}
for k, w in want {
let c = chars[start + k]
let lower = if c is ('A'..='Z') {
(c.to_int() + 0x20).unsafe_to_char()
} else {
c
}
if lower != w {
return false
}
}
true
}
///|
/// 2^exp with clamping: doubles overflow past 2^1024 and underflow below
/// 2^-1075, so huge exponents can short-circuit instead of looping.
fn pow2(exp : Int) -> Double {
if exp > 1100 {
return 1.0 / 0.0
}
if exp < -1200 {
return 0.0
}
let mut result = 1.0
if exp >= 0 {
for _ in 0.. Double {
if exp >= -600 {
m * pow2(exp)
} else {
m * pow2(-600) * pow2(exp + 600)
}
}
///|
/// True for nonzero values below the smallest normal double; strtod flags
/// these with ERANGE, and GNU printf reports them as range errors.
fn is_subnormal(x : Double) -> Bool {
x != 0.0 && ((x.reinterpret_as_uint64() >> 52) & 0x7FF) == 0
}
///|
/// Parse a printf float argument like strtod: leading blanks, 'C, inf/nan
/// (with an optional payload), hex floats (0x1.8p+2), or the longest valid
/// decimal prefix. Trailing text warns; out-of-range values become inf or 0
/// with a range diagnostic, like GNU printf.
fn parse_double_arg(ctx : Ctx, text : String) -> Double {
let chars : Array[Char] = text.iter().collect()
let mut i = 0
while i < chars.length() && is_blank(chars[i]) {
i += 1
}
if i >= chars.length() {
ctx.warn("printf: '\{text}': expected a numeric value")
return 0.0
}
if chars[i] == '\'' || chars[i] == '"' {
if i + 1 >= chars.length() {
ctx.warn("printf: '\{text}': expected a numeric value")
return 0.0
}
if i + 2 < chars.length() {
let sb = StringBuilder()
for k in (i + 2)..= 0 || chars[i + 2] == '.') {
// Hexadecimal float: 0xH[.H][p[+-]D]. The significand accumulates in a
// 64-bit integer; digits beyond its capacity only adjust the exponent
// (integer part) or are dropped (fraction), which can differ from the
// correctly rounded result by at most one ULP on extreme inputs.
let mut j = i + 2
let mut mant : UInt64 = 0
let mant_cap : UInt64 = 0x0800_0000_0000_0000
let mut exp4 = 0
let mut any = false
let mut sticky = false
while j < chars.length() && hex_val(chars[j]) >= 0 {
if mant < mant_cap {
mant = mant * 16 + hex_val(chars[j]).to_int64().reinterpret_as_uint64()
} else {
exp4 += 1
if hex_val(chars[j]) != 0 {
sticky = true
}
}
any = true
j += 1
}
if j < chars.length() && chars[j] == '.' {
j += 1
while j < chars.length() && hex_val(chars[j]) >= 0 {
if mant < mant_cap {
mant = mant * 16 +
hex_val(chars[j]).to_int64().reinterpret_as_uint64()
exp4 -= 1
} else if hex_val(chars[j]) != 0 {
sticky = true
}
any = true
j += 1
}
}
// Dropped nonzero digits make the significand odd so the 64-bit to
// double conversion breaks round-half-even ties upward, like a sticky
// bit.
if sticky {
mant = mant | 1
}
if !any {
// "0x" with no digits parses as 0 with trailing garbage.
value = 0.0
end = i + 1
} else {
let mut p = 0
if j < chars.length() && (chars[j] == 'p' || chars[j] == 'P') {
let mut k = j + 1
let mut esign = 1
if k < chars.length() && (chars[k] == '+' || chars[k] == '-') {
if chars[k] == '-' {
esign = -1
}
k += 1
}
let mut ev = 0
let mut edigits = false
while k < chars.length() && chars[k] is ('0'..='9') {
if ev < 1000000 {
ev = ev * 10 + (chars[k].to_int() - 0x30)
}
edigits = true
k += 1
}
if edigits {
if ev > 10000 {
ev = 10000
}
p = esign * ev
j = k
}
}
let total = 4 * exp4 + p
value = if mant == 0 { 0.0 } else { scale2(mant.to_double(), total) }
// strtod raises ERANGE for complete over/underflow and for INEXACT
// subnormals; an exact subnormal like 0x1p-1074 stays silent. The
// conversion is inexact when digits were dropped (sticky) or when the
// significand's lowest set bit lands below 2^-1074.
let inexact_subnormal = is_subnormal(value) &&
(sticky || total + mant.ctz() < -1074)
if mant != 0 && (value.is_inf() || value == 0.0 || inexact_subnormal) {
range_warned = true
ctx.warn("printf: '\{text}': Result too large")
}
end = j
}
} else {
// Decimal float: D[.D][e[+-]D]
let mut j = i
let mut any = false
let mut sig_digits = 0
let mut frac_zeros = 0
let mut seen_nonzero = false
while j < chars.length() && chars[j] is ('0'..='9') {
any = true
if chars[j] != '0' || seen_nonzero {
seen_nonzero = true
sig_digits += 1
}
j += 1
}
let int_sig = sig_digits
if j < chars.length() && chars[j] == '.' {
j += 1
while j < chars.length() && chars[j] is ('0'..='9') {
any = true
if int_sig == 0 && !seen_nonzero {
if chars[j] == '0' {
frac_zeros += 1
} else {
seen_nonzero = true
}
}
j += 1
}
}
if !any {
ctx.warn("printf: '\{text}': expected a numeric value")
return 0.0
}
let mut e10 = 0
if j < chars.length() && (chars[j] == 'e' || chars[j] == 'E') {
let mut k = j + 1
let mut esign = 1
if k < chars.length() && (chars[k] == '+' || chars[k] == '-') {
if chars[k] == '-' {
esign = -1
}
k += 1
}
let mut ev = 0
let mut edigits = false
while k < chars.length() && chars[k] is ('0'..='9') {
if ev < 1000000 {
ev = ev * 10 + (chars[k].to_int() - 0x30)
}
edigits = true
k += 1
}
if edigits {
if ev > 100000 {
ev = 100000
}
e10 = esign * ev
j = k
}
}
let sb = StringBuilder()
sb.write_char('0')
for k in i.. {
// Syntactically valid but out of double range: estimate the decimal
// exponent of the leading digit to pick inf versus 0.
let magnitude10 = if int_sig > 0 {
int_sig + e10
} else {
e10 - frac_zeros
}
range_warned = true
ctx.warn("printf: '\{text}': Result too large")
if magnitude10 > 0 {
1.0 / 0.0
} else {
0.0
}
}
}
// Silent underflow (to 0 or a subnormal) or overflow to inf is still a
// range error, matching strtod's ERANGE.
if !range_warned &&
seen_nonzero &&
(value == 0.0 || value.is_inf() || is_subnormal(value)) {
range_warned = true
ctx.warn("printf: '\{text}': Result too large")
}
end = j
}
// A range error supersedes the partial-conversion warning, like GNU.
if end < chars.length() && !range_warned {
ctx.warn("printf: '\{text}': value not completely converted")
}
if neg {
-value
} else {
value
}
}
///|
fn emit_signed(
ctx : Ctx,
n : Int64,
width : Int?,
prec : Int?,
minus : Bool,
plus : Bool,
space : Bool,
zero : Bool,
) -> Unit {
let negf = n < 0
let mag = if negf {
(-n).reinterpret_as_uint64()
} else {
n.reinterpret_as_uint64()
}
let body = apply_precision(mag.to_string(), prec)
let sign = if negf {
"-"
} else if plus {
"+"
} else if space {
" "
} else {
""
}
let zero_eff = zero && !minus && prec is None
push_string_utf8(ctx.out, pad_field(sign, body, width, minus, zero_eff))
}
///|
fn emit_unsigned(
ctx : Ctx,
u : UInt64,
radix : Int,
upper : Bool,
alt : Bool,
width : Int?,
prec : Int?,
minus : Bool,
zero : Bool,
) -> Unit {
let digits = u.to_string(radix~)
let mut body = apply_precision(
if upper {
digits.to_upper()
} else {
digits
},
prec,
)
let mut prefix = ""
if alt && radix == 8 && !body.has_prefix("0") {
body = "0" + body
}
if alt && radix == 16 && u != 0 {
prefix = if upper { "0X" } else { "0x" }
}
let zero_eff = zero && !minus && prec is None
push_string_utf8(ctx.out, pad_field(prefix, body, width, minus, zero_eff))
}
///|
fn emit_float(
ctx : Ctx,
conv : Char,
x : Double,
width : Int?,
prec : Int?,
minus : Bool,
plus : Bool,
space : Bool,
zero : Bool,
alt : Bool,
) -> Unit {
let upper = conv is ('E' | 'F' | 'G')
let bits_neg = x.reinterpret_as_uint64() >> 63 != 0
if x.is_nan() || x.is_inf() {
let mut body = if x.is_nan() { "nan" } else { "inf" }
if upper {
body = body.to_upper()
}
// GNU printf prints NaN unsigned; infinities honor the sign flags.
let sign = if x.is_nan() {
""
} else if bits_neg {
"-"
} else if plus {
"+"
} else if space {
" "
} else {
""
}
// The zero flag pads non-finite values with spaces, like C printf.
push_string_utf8(ctx.out, pad_field(sign, body, width, minus, false))
return
}
let p = prec.unwrap_or(6)
let dec = dec_of(x)
let body = match conv {
'f' | 'F' => fmt_f(dec, p, alt)
'e' | 'E' => fmt_e(dec, p, alt, upper)
_ => fmt_g(dec, p, alt, upper)
}
let sign = if dec.neg {
"-"
} else if plus {
"+"
} else if space {
" "
} else {
""
}
push_string_utf8(ctx.out, pad_field(sign, body, width, minus, zero && !minus))
}
///|
fn emit_b(ctx : Ctx, text : String) -> Unit {
let chars : Array[Char] = text.iter().collect()
let mut i = 0
while i < chars.length() {
if chars[i] == '\\' {
let (esc, used) = decode_escape(chars, i, in_b=true)
match esc {
Out(chunk) => ctx.out.append(chunk)
Stop => {
ctx.stop = true
return
}
Invalid(msg) => {
ctx.fatal = Some(msg)
return
}
}
i += used
} else {
push_char_utf8(ctx.out, chars[i])
i += 1
}
}
}
///|
/// GNU printf's per-conversion modifier rules, derived empirically:
/// %b and %% take no modifiers; # is limited to o/x/X and floats; the zero
/// flag is invalid for s/c; %c takes no precision.
fn spec_valid(
conv : Char,
minus : Bool,
plus : Bool,
space : Bool,
zero : Bool,
alt : Bool,
width : Int?,
prec : Int?,
) -> Bool {
match conv {
'd' | 'i' | 'u' => !alt
'o' | 'x' | 'X' | 'e' | 'E' | 'f' | 'F' | 'g' | 'G' => true
's' => !zero && !alt
'c' => !zero && !alt && prec is None
'b' | '%' =>
!minus &&
!plus &&
!space &&
!zero &&
!alt &&
width is None &&
prec is None
_ => true
}
}
///|
/// Field widths allocate real padding, so cap them far below INT_MAX.
let max_width : Int = 100000000
///|
/// One pass over the format string; conversions consume arguments from ctx.
fn scan_format(ctx : Ctx, chars : Array[Char]) -> Unit {
let mut i = 0
while i < chars.length() {
if ctx.stop || !(ctx.fatal is None) {
return
}
let c = chars[i]
if c == '\\' {
let (esc, used) = decode_escape(chars, i)
match esc {
Out(bytes) => ctx.out.append(bytes)
Stop => {
ctx.stop = true
return
}
Invalid(msg) => {
ctx.fatal = Some(msg)
return
}
}
i += used
continue
}
if c != '%' {
push_char_utf8(ctx.out, c)
i += 1
continue
}
let spec_start = i
let mut j = i + 1
let mut minus = false
let mut plus = false
let mut space = false
let mut zero = false
let mut alt = false
while j < chars.length() {
match chars[j] {
'-' => minus = true
'+' => plus = true
' ' => space = true
'0' => zero = true
'#' => alt = true
_ => break
}
j += 1
}
let mut width : Int? = None
if j < chars.length() && chars[j] == '*' {
let v = match ctx.next_arg() {
Some(arg) => parse_integer(ctx, arg, unsigned=false)
None => 0
}
// Padding is materialized in memory, so widths are capped well below
// INT_MAX; GNU would print the gigabytes of spaces instead.
if v > max_width.to_int64() || v < -max_width.to_int64() {
ctx.fatal = Some("printf: invalid field width: '\{v}'")
return
}
if v < 0 {
minus = true
width = Some((-v).to_int())
} else {
width = Some(v.to_int())
}
j += 1
} else {
let mut w = 0
let mut any = false
let digits_start = j
while j < chars.length() && chars[j] is ('0'..='9') {
let d = chars[j].to_int() - 0x30
if w > (max_width - d) / 10 {
let sb = StringBuilder()
for k in digits_start.. parse_integer(ctx, arg, unsigned=false)
None => 0
}
// Precision materializes zeros in memory, so it shares the width cap.
if v > max_width.to_int64() {
ctx.fatal = Some("printf: invalid precision: '\{v}'")
return
}
// Any negative precision means the precision is omitted.
prec = if v < 0 { None } else { Some(v.to_int()) }
j += 1
} else {
let mut p = 0
let digits_start = j
while j < chars.length() && chars[j] is ('0'..='9') {
let d = chars[j].to_int() - 0x30
if p > (max_width - d) / 10 {
let sb = StringBuilder()
for k in digits_start..= chars.length() {
ctx.fatal = Some("printf: missing conversion specifier at end of format")
return
}
let conv = chars[j]
i = j + 1
if !spec_valid(conv, minus, plus, space, zero, alt, width, prec) {
let sb = StringBuilder()
for k in spec_start.. ctx.out.push(b'%')
'd' | 'i' => {
// A missing argument formats as zero without a diagnostic.
let n = match ctx.next_arg() {
Some(arg) => parse_integer(ctx, arg, unsigned=false)
None => 0
}
emit_signed(ctx, n, width, prec, minus, plus, space, zero)
}
'u' | 'o' | 'x' | 'X' => {
let n = match ctx.next_arg() {
Some(arg) => parse_integer(ctx, arg, unsigned=true)
None => 0
}
let radix = match conv {
'u' => 10
'o' => 8
_ => 16
}
emit_unsigned(
ctx,
n.reinterpret_as_uint64(),
radix,
conv == 'X',
alt && conv != 'u',
width,
prec,
minus,
zero,
)
}
's' => {
// GNU printf measures %s width and precision in bytes.
let arg = ctx.next_arg().unwrap_or("")
let bytes : Array[Byte] = []
push_string_utf8(bytes, arg)
let limited = match prec {
Some(p) =>
if bytes.length() > p {
bytes[0:p].to_owned()
} else {
bytes
}
None => bytes
}
emit_padded_bytes(ctx.out, limited, width, minus)
}
'c' => {
// GNU printf emits only the first byte of the argument; a missing
// or empty argument emits one NUL byte.
let arg = ctx.next_arg().unwrap_or("")
let bytes : Array[Byte] = []
push_string_utf8(bytes, arg)
let first = if bytes.length() > 0 { [bytes[0]] } else { [b'\x00'] }
emit_padded_bytes(ctx.out, first, width, minus)
}
'b' => {
let arg = ctx.next_arg().unwrap_or("")
emit_b(ctx, arg)
}
'f' | 'F' | 'e' | 'E' | 'g' | 'G' => {
let x = match ctx.next_arg() {
Some(arg) => parse_double_arg(ctx, arg)
None => 0.0
}
emit_float(ctx, conv, x, width, prec, minus, plus, space, zero, alt)
}
_ => {
ctx.fatal = Some("printf: %\{conv}: invalid conversion specification")
return
}
}
}
}
///|
async fn main {
let argv = @env.args()[1:]
if argv.length() > 0 && argv[0] == "--help" {
@stdio.stdout.write(help_message() + "\n")
return
}
let operands = if argv.length() > 0 && argv[0] == "--" {
argv[1:]
} else {
argv
}
if operands.is_empty() {
@stdio.stderr.write(
"printf: missing operand\nTry 'printf --help' for more information.\n",
)
@sys.exit(1)
return
}
let format = operands[0]
let ctx : Ctx = {
out: [],
args: operands[1:].to_owned(),
arg_index: 0,
messages: [],
exit_error: false,
stop: false,
fatal: None,
}
let fmt_chars : Array[Char] = format.iter().collect()
for ;; {
let before = ctx.arg_index
scan_format(ctx, fmt_chars)
if ctx.stop || !(ctx.fatal is None) {
break
}
if ctx.arg_index >= ctx.args.length() {
break
}
if ctx.arg_index == before {
ctx.notice(
"printf: warning: ignoring excess arguments, starting with '\{ctx.args[ctx.arg_index]}'",
)
break
}
}
@stdio.stdout.write(Bytes::from_array(ctx.out))
for msg in ctx.messages {
@stdio.stderr.write(msg + "\n")
}
match ctx.fatal {
Some(msg) => {
@stdio.stderr.write(msg + "\n")
@sys.exit(1)
return
}
None => ()
}
if ctx.exit_error {
@sys.exit(1)
}
}