///|
pub(all) struct FormatArgument {
name : String
kind : String
offset : Int32
} derive(Eq, Debug)
///|
pub(all) struct FormatContract {
arguments : Array[FormatArgument]
style : String
} derive(Eq, Debug)
///|
pub(all) struct FormatDifference {
code : String
message : String
argument : String
} derive(Eq, Debug)
///|
priv struct PrintfState {
arguments : Array[FormatArgument]
kinds : Map[String, String]
mut next : Int32
mut positional : Bool
mut sequential : Bool
}
///|
fn add_format_argument(
arguments : Array[FormatArgument],
kinds : Map[String, String],
name : String,
kind : String,
offset : Int32,
) -> Result[Unit, String] {
if kinds.get(name) is Some(previous) {
if previous != kind {
return Err(
"Argument " +
name +
" is used with incompatible types " +
previous +
" and " +
kind,
)
}
return Ok(())
}
if arguments.length() >= 4096 {
return Err("Format contract exceeds 4096 distinct arguments")
}
arguments.push({ name, kind, offset, })
kinds.set(name, kind)
Ok(())
}
///|
// A Python field can legally occur with several presentations/conversions.
// Preserve the complete set of uses, independent of order or repetition.
fn add_python_argument(
arguments : Array[FormatArgument],
kinds : Map[String, String],
name : String,
kind : String,
offset : Int32,
) -> Result[Unit, String] {
match kinds.get(name) {
None => add_format_argument(arguments, kinds, name, kind, offset)
Some(previous) => {
let uses = previous.split("|").map(s => s.to_owned()).to_array()
if uses.contains(kind) {
return Ok(())
}
uses.push(kind)
uses.sort()
let combined = join_strings(uses, "|")
kinds.set(name, combined)
for index, argument in arguments {
if argument.name == name {
arguments[index] = { name, kind: combined, offset: argument.offset, }
break
}
}
Ok(())
}
}
}
///|
fn PrintfState::add(
self : PrintfState,
index : Int32?,
kind : String,
offset : Int32,
) -> Result[Unit, String] {
let number = match index {
Some(number) => {
self.positional = true
number
}
None => {
self.sequential = true
let number = self.next
self.next += 1
number
}
}
if self.positional && self.sequential {
return Err("Cannot mix numbered and sequential printf arguments")
}
add_format_argument(
self.arguments,
self.kinds,
number.to_string(),
kind,
offset,
)
}
///|
fn printf_number(chars : Array[Char], start : Int32) -> (Int32?, Int32, Bool) {
let mut end = start
while end < chars.length() && chars[end].is_ascii_digit() {
end += 1
}
if end == start || end >= chars.length() || chars[end] != '$' {
return (None, start, false)
}
let number = decimal_text(char_slice(chars, start, end), 10000)
(number, end + 1, true)
}
///|
fn printf_kind(conversion : Char, length : String) -> Result[String?, String] {
match conversion {
'd' | 'i' => {
if !["", "hh", "h", "l", "ll", "j", "z", "t"].contains(length) {
return Err("Invalid length modifier for signed integer")
}
// hh and h use a promoted int in printf's variadic calling convention.
let width = if length == "hh" || length == "h" { "" } else { length }
Ok(Some("signed:" + width))
}
'u' | 'o' | 'x' | 'X' => {
if !["", "hh", "h", "l", "ll", "j", "z", "t"].contains(length) {
return Err("Invalid length modifier for unsigned integer")
}
let width = if length == "hh" || length == "h" { "" } else { length }
Ok(Some("unsigned:" + width))
}
'f' | 'F' | 'e' | 'E' | 'g' | 'G' | 'a' | 'A' => {
if !["", "l", "L"].contains(length) {
return Err("Invalid length modifier for floating point")
}
Ok(Some(if length == "L" { "long-double" } else { "double" }))
}
'c' =>
match length {
"" => Ok(Some("signed:"))
"l" => Ok(Some("wide-char"))
_ => Err("Invalid length modifier for character")
}
's' =>
match length {
"" => Ok(Some("string"))
"l" => Ok(Some("wide-string"))
_ => Err("Invalid length modifier for string")
}
'p' =>
if length != "" {
Err("Pointer conversion does not accept a length modifier")
} else {
Ok(Some("pointer"))
}
'n' => {
if !["", "hh", "h", "l", "ll", "j", "z", "t"].contains(length) {
return Err("Invalid length modifier for count pointer")
}
Ok(Some("count-pointer:" + length))
}
'm' =>
if length != "" {
Err("GNU %m does not accept a length modifier")
} else {
Ok(None)
}
_ => Err("Unsupported printf conversion: " + conversion.to_string())
}
}
///|
fn printf_options(
conversion : Char,
flags : Array[Char],
width : Bool,
precision : Bool,
) -> Result[Unit, String] {
if conversion == 'n' && (!flags.is_empty() || width || precision) {
return Err("C11 %n does not allow flags, width, or precision")
}
if flags.contains('#') &&
!['o', 'x', 'X', 'a', 'A', 'e', 'E', 'f', 'F', 'g', 'G', 'm'].contains(
conversion,
) {
return Err("Alternate form is not defined for this C conversion")
}
if flags.contains('0') &&
!['d', 'i', 'o', 'u', 'x', 'X', 'a', 'A', 'e', 'E', 'f', 'F', 'g', 'G'].contains(
conversion,
) {
return Err("Zero padding is not defined for this C conversion")
}
if precision && ['c', 'p', 'n'].contains(conversion) {
return Err(
"Precision is not defined for C character, pointer, or count conversion",
)
}
Ok(())
}
///|
/// Extract typed printf arguments, including positional width and precision.
pub fn printf_contract(text : String) -> Result[FormatContract, String] {
let chars = text.to_array()
let state : PrintfState = {
arguments: [],
kinds: Map([]),
next: 1,
positional: false,
sequential: false,
}
let mut i = 0
while i < chars.length() {
if chars[i] != '%' {
i += 1
continue
}
let offset = i
i += 1
if i == chars.length() {
return Err("Trailing percent at character " + (offset + 1).to_string())
}
if chars[i] == '%' {
i += 1
continue
}
let (position, after_position, explicit) = printf_number(chars, i)
if explicit && (position is None || position == Some(0)) {
return Err("Printf argument position must be in 1..10000")
}
i = after_position
let flags : Array[Char] = []
while i < chars.length() &&
['-', '+', ' ', '#', '0', '\'', 'I'].contains(chars[i]) {
flags.push(chars[i])
i += 1
}
let mut has_width = false
if i < chars.length() && chars[i] == '*' {
has_width = true
i += 1
let (width, after, numbered) = printf_number(chars, i)
if numbered && (width is None || width == Some(0)) {
return Err("Printf width argument position must be in 1..10000")
}
i = after
match state.add(width, "signed:", offset) {
Err(error) => return Err(error)
Ok(_) => ()
}
} else {
let width_start = i
while i < chars.length() && chars[i].is_ascii_digit() {
i += 1
}
has_width = i > width_start
}
let mut has_precision = false
if i < chars.length() && chars[i] == '.' {
has_precision = true
i += 1
if i < chars.length() && chars[i] == '*' {
i += 1
let (precision, after, numbered) = printf_number(chars, i)
if numbered && (precision is None || precision == Some(0)) {
return Err("Printf precision argument position must be in 1..10000")
}
i = after
match state.add(precision, "signed:", offset) {
Err(error) => return Err(error)
Ok(_) => ()
}
} else {
while i < chars.length() && chars[i].is_ascii_digit() {
i += 1
}
}
}
let mut length = ""
if i + 1 < chars.length() {
let pair = char_slice(chars, i, i + 2)
if pair == "hh" || pair == "ll" {
length = pair
i += 2
}
}
if length == "" &&
i < chars.length() &&
['h', 'l', 'L', 'j', 'z', 't'].contains(chars[i]) {
length = chars[i].to_string()
i += 1
}
if i >= chars.length() {
return Err(
"Incomplete printf conversion at character " + (offset + 1).to_string(),
)
}
let conversion = chars[i]
i += 1
match printf_options(conversion, flags, has_width, has_precision) {
Err(error) =>
return Err(error + " at character " + (offset + 1).to_string())
Ok(_) => ()
}
match printf_kind(conversion, length) {
Err(error) =>
return Err(error + " at character " + (offset + 1).to_string())
Ok(None) =>
if explicit {
return Err("A conversion without an argument cannot have a position")
}
Ok(Some(kind)) =>
match state.add(position, kind, offset) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
}
if state.positional {
for position = 1
position <= state.arguments.length()
position = position + 1 {
if state.kinds.get(position.to_string()) is None {
return Err("Numbered printf arguments must not leave a gap")
}
}
}
Ok({ arguments: state.arguments, style: "c-format", })
}
///|
/// Compare argument identities and types, allowing reordered numbered arguments.
pub fn compare_contracts(
source : FormatContract,
target : FormatContract,
) -> Array[FormatDifference] {
let differences : Array[FormatDifference] = []
let target_arguments : Map[String, FormatArgument] = Map([])
let source_names : Map[String, Bool] = Map([])
for argument in target.arguments {
if target_arguments.get(argument.name) is None {
target_arguments.set(argument.name, argument)
}
}
for argument in source.arguments {
source_names.set(argument.name, true)
}
if source.style != target.style {
differences.push({
code: "F006",
message: "Format styles differ",
argument: "",
})
return differences
}
for argument in source.arguments {
let found = target_arguments.get(argument.name)
match found {
None =>
differences.push({
code: "F003",
message: "Missing format argument " + argument.name,
argument: argument.name,
})
Some(actual) =>
if actual.kind != argument.kind {
differences.push({
code: "F005",
message: "Argument " +
argument.name +
" changes type from " +
argument.kind +
" to " +
actual.kind,
argument: argument.name,
})
}
}
}
for argument in target.arguments {
if source_names.get(argument.name) is None {
differences.push({
code: "F004",
message: "Unexpected format argument " + argument.name,
argument: argument.name,
})
}
}
differences
}