commit 39118a49151c3bec8acfcdd19d6486861347e8fc
parent 46a7b93d3759b1a64c8c7ded4d95e819bb5af049
Author: Byron Torres <b@torresjrjr.com>
Date: Mon, 13 Sep 2021 14:30:23 +0100
fmt: introduce parametric modifiers
Introduces parametric format modifiers "{%}" as a new feature.
See fmt/README for details.
Introduces type fmt::field = (...fmt::formattable | *fmt::modifiers).
Refactors existing fmt::* functions to use fmt::field instead of
fmt::formattable where applicable.
Exports types fmt::{modifiers,padding,negation}.
Modifies hare::parse::syntaxerr()'s parameter list to handle fmt::field.
Signed-off-by: Byron Torres <b@torresjrjr.com>
Diffstat:
3 files changed, 136 insertions(+), 19 deletions(-)
diff --git a/fmt/README b/fmt/README
@@ -7,6 +7,10 @@ next argument from the parameter list, in order. A specific parameter may be
selected by indexing it from zero: "{0}", "{1}", and so on. To print "{", use
"{{", and for "}", use "}}".
+There are two ways to specify how an argument shall be formatted: inline format
+modifiers, and parametric format modifiers.
+
+Inline format modifiers are a series of characters within a format sequence.
You may use a colon to add format modifiers; for example, "{:x}" will format an
argument in hexadecimal, and "{3:-10}" will left-align the 3rd argument to at
least 10 characters.
@@ -32,7 +36,7 @@ Following the precision, an optional character controls the output format:
- x, X: print in lowercase or uppercase hexadecimal
- o, b: print in octal or binary
-Some examples:
+Some inline modifier examples:
fmt::printf("hello {}", "world"); // "hello world"
fmt::printf("{1} {0}", "hello", "world"); // "world hello"
@@ -40,3 +44,27 @@ Some examples:
fmt::printf("{:-5}", 42); // "42 "
fmt::printf("{:5}", 42); // " 42"
fmt::printf("{:05}", 42); // "00042"
+
+A parametric format modifier is a secondary argument from the parameter list,
+which is a pointer to an instance of [[fmt::modifiers]]. This modifier parameter
+shall describe how the primary formattable argument is formatted.
+
+A parametric format sequence of this sort takes the form of "{i%j}", where i is
+the formattable parameter index, j is the modifiers parameter index, and i & j
+are optional. If either i or j aren't explicitly provided by the user, they
+will evaluate to index of the next unused argument.
+
+Some parametric modifier examples:
+
+ // "hello world hello"
+ fmt::printf("{%} {%} {0%1}", // evaluates to "{0%1} {2%3} {0%1}"
+ "hello", &fmt::modifiers { ... },
+ "world", &fmt::modifiers { ... });
+
+ // "|hello| world|0000000123|BEEF|"
+ fmt::printf("|{%}|{%}|{%}|{%}|",
+ "hello", &fmt::modifiers { ... },
+ "world", &fmt::modifiers { width = 10, ... },
+ 123, &fmt::modifiers { width = 10, padding = fmt::padding::ZEROES, ... },
+ 0xBEEF, &fmt::modifiers { base = strconv::base::HEX, ... });
+
diff --git a/fmt/fmt.ha b/fmt/fmt.ha
@@ -7,31 +7,35 @@ use strconv;
use strings;
use types;
+// Tagged union of the [[fmt::formattable]] types and [[fmt::modifiers]]. Used
+// for functions which accept format strings.
+export type field = (...formattable | *modifiers);
+
// Tagged union of all types which are formattable.
export type formattable =
(...types::numeric | uintptr | str | rune | bool | nullable *void | void);
// Formats text for printing and writes it to [[os::stdout]].
-export fn printf(fmt: str, args: formattable...) (io::error | size) =
+export fn printf(fmt: str, args: field...) (io::error | size) =
fprintf(os::stdout, fmt, args...);
// Formats text for printing and writes it to [[os::stdout]], followed by a line
// feed.
-export fn printfln(fmt: str, args: formattable...) (io::error | size) =
+export fn printfln(fmt: str, args: field...) (io::error | size) =
fprintfln(os::stdout, fmt, args...);
// Formats text for printing and writes it to [[os::stderr]].
-export fn errorf(fmt: str, args: formattable...) (io::error | size) =
+export fn errorf(fmt: str, args: field...) (io::error | size) =
fprintf(os::stderr, fmt, args...);
// Formats text for printing and writes it to [[os::stderr]], followed by a line
// feed.
-export fn errorfln(fmt: str, args: formattable...) (io::error | size) =
+export fn errorfln(fmt: str, args: field...) (io::error | size) =
fprintfln(os::stderr, fmt, args...);
// Formats text for printing and writes it into a heap-allocated string. The
// caller must free the return value.
-export fn asprintf(fmt: str, args: formattable...) str = {
+export fn asprintf(fmt: str, args: field...) str = {
let buf = bufio::dynamic(io::mode::WRITE);
assert(fprintf(buf, fmt, args...) is size);
return strings::fromutf8_unsafe(bufio::finish(buf));
@@ -39,7 +43,7 @@ export fn asprintf(fmt: str, args: formattable...) str = {
// Formats text for printing and writes it into a caller supplied buffer. The
// returned string is borrowed from this buffer.
-export fn bsprintf(buf: []u8, fmt: str, args: formattable...) str = {
+export fn bsprintf(buf: []u8, fmt: str, args: field...) str = {
let sink = bufio::fixed(buf, io::mode::WRITE);
defer io::close(sink);
let l = fprintf(sink, fmt, args...) as size;
@@ -48,7 +52,7 @@ export fn bsprintf(buf: []u8, fmt: str, args: formattable...) str = {
// Formats text for printing and writes it to [[os::stderr]], followed by a line
// feed, then exits the program with an error status.
-export @noreturn fn fatal(fmt: str, args: formattable...) void = {
+export @noreturn fn fatal(fmt: str, args: field...) void = {
fprintfln(os::stderr, fmt, args...)!;
os::exit(1);
};
@@ -58,7 +62,7 @@ export @noreturn fn fatal(fmt: str, args: formattable...) void = {
export fn fprintfln(
s: *io::stream,
fmt: str,
- args: formattable...
+ args: field...
) (io::error | size) = {
return fprintf(s, fmt, args...)? + io::write(s, ['\n': u32: u8])?;
};
@@ -122,19 +126,23 @@ export fn fprint(s: *io::stream, args: formattable...) (io::error | size) = {
return n;
};
-type negation = enum {
+// Specifies for numerical arguments when to prepend a plus or minus sign or a
+// blank space.
+export type negation = enum {
NONE,
SPACE,
PLUS,
};
-type padding = enum {
+// Specifies how to align and pad an argument within a given width.
+export type padding = enum {
ALIGN_RIGHT,
ALIGN_LEFT,
ZEROES,
};
-type modifiers = struct {
+// Specifies how to format an argument.
+export type modifiers = struct {
padding: padding,
negation: negation,
width: uint,
@@ -150,11 +158,15 @@ type modflags = enum uint {
PLUS = 1 << 3,
};
+type paramindex = (uint | nextparam | void);
+
+type nextparam = void;
+
// Formats text for printing and writes it to an [[io::stream]].
export fn fprintf(
s: *io::stream,
fmt: str,
- args: formattable...
+ args: field...
) (io::error | size) = {
let n = 0z, i = 0z;
let iter = strings::iter(fmt);
@@ -170,7 +182,7 @@ export fn fprintf(
r: rune => r,
};
- const arg = if (r == '{') {
+ let arg = if (r == '{') {
n += io::write(s, utf8::encoderune('{'))?;
continue;
} else if (ascii::isdigit(r)) {
@@ -182,18 +194,45 @@ export fn fprintf(
yield args[i - 1];
};
- let mod = modifiers { base = strconv::base::DEC, ... };
+ const arg = match (arg) {
+ arg: formattable => arg,
+ * => abort("Invalid formattable"),
+ };
+
r = match (strings::next(&iter)) {
void => abort("Invalid format string (unterminated '{')"),
r: rune => r,
};
+
+ let mod = &modifiers { ... };
+ let pi: paramindex = void;
switch (r) {
- ':' => scan_modifiers(&iter, &mod),
+ ':' => scan_inline_modifiers(&iter, mod),
+ '%' => scan_parametric_modifiers(&iter, &pi),
'}' => void,
* => abort("Invalid format string"),
};
- n += format(s, arg, &mod)?;
+ match (pi) {
+ pi: uint => match (args[pi]) {
+ pmod: *modifiers => mod = pmod,
+ * => abort("Explicit parameter is not *fmt::modifier"),
+ },
+ nextparam => {
+ i += 1;
+ match (args[i - 1]) {
+ pmod: *modifiers => mod = pmod,
+ * => abort("Implicit parameter is not *fmt::modifier"),
+ };
+ },
+ void => void,
+ };
+
+ if (mod.base == 0) {
+ mod.base = strconv::base::DEC;
+ };
+
+ n += format(s, arg, mod)?;
} else if (r == '}') {
match (strings::next(&iter)) {
void => abort("Invalid format string (hanging '}')"),
@@ -366,7 +405,7 @@ fn scan_modifier_base(iter: *strings::iterator, mod: *modifiers) void = {
};
};
-fn scan_modifiers(iter: *strings::iterator, mod: *modifiers) void = {
+fn scan_inline_modifiers(iter: *strings::iterator, mod: *modifiers) void = {
scan_modifier_flags(iter, mod);
scan_modifier_width(iter, mod);
scan_modifier_precision(iter, mod);
@@ -380,18 +419,68 @@ fn scan_modifiers(iter: *strings::iterator, mod: *modifiers) void = {
assert(terminated, "Invalid format string (unterminated '{')");
};
+fn scan_parameter_index(iter: *strings::iterator, pi: *paramindex) void = {
+ let r = match (strings::next(iter)) {
+ void => abort("Invalid format string (unterminated '{')"),
+ r: rune => r,
+ };
+
+ let is_digit = ascii::isdigit(r);
+ strings::push(iter, r);
+ if (is_digit) {
+ *pi = scan_uint(iter);
+ } else {
+ *pi = nextparam;
+ };
+};
+
+fn scan_parametric_modifiers(iter: *strings::iterator, pi: *paramindex) void = {
+ scan_parameter_index(iter, pi);
+
+ // eat '}'
+ let terminated = match (strings::next(iter)) {
+ void => false,
+ r: rune => r == '}',
+ };
+ assert(terminated, "Invalid format string (unterminated '{')");
+};
+
@test fn fmt() void = {
let buf: [1024]u8 = [0...];
+
assert(bsprintf(buf, "hello world") == "hello world");
assert(bsprintf(buf, "{} {}", "hello", "world") == "hello world");
assert(bsprintf(buf, "{0} {1}", "hello", "world") == "hello world");
assert(bsprintf(buf, "{0} {0}", "hello", "world") == "hello hello");
assert(bsprintf(buf, "{1} {0} {1}", "hello", "world") == "world hello world");
+
+ const mod = &modifiers { width = 7, ... };
+ assert(bsprintf(buf, "{%}", "hello", mod) == " hello");
+ assert(bsprintf(buf, "{%1}", "hello", mod) == " hello");
+ assert(bsprintf(buf, "{0%1}", "hello", mod) == " hello");
+ assert(bsprintf(buf, "{0%2}", "hello", 0, mod) == " hello");
+ assert(bsprintf(buf, "{1%2}", 0, "hello", mod) == " hello");
+ assert(bsprintf(buf, "{2%0}", mod, 0, "hello") == " hello");
+ assert(bsprintf(buf, "{2%}", mod, 0, "hello") == " hello");
+ assert(bsprintf(buf, "|{1%}|{}|", mod, "hello") == "| hello|hello|");
+ assert(bsprintf(buf, "|{}|{2%}|", "hello", mod, "world") == "|hello| world|");
+ assert(bsprintf(buf, "|{%}|{%}|{%}|{%}|",
+ "hello", &modifiers { ... },
+ "world", &modifiers { width = 10, ... },
+ 123, &modifiers { width = 10, padding = padding::ZEROES, ... },
+ 0xBEEF, &modifiers { base = strconv::base::HEX, ... },
+ ) == "|hello| world|0000000123|BEEF|");
+ assert(bsprintf(buf, "|{%}|{%}|{0%1}|",
+ "hello", &modifiers { ... },
+ "world", &modifiers { ... },
+ ) == "|hello|world|hello|");
+
assert(bsprintf(buf, "x: {:08x}", 0xBEEF) == "x: 0000beef");
assert(bsprintf(buf, "x: {:8X}", 0xBEEF) == "x: BEEF");
assert(bsprintf(buf, "x: {:-8X}", 0xBEEF) == "x: BEEF ");
assert(bsprintf(buf, "x: {:o}", 0o755) == "x: 755");
assert(bsprintf(buf, "x: {:b}", 0b11011) == "x: 11011");
+
assert(bsprintf(buf, "{} {} {} {} {}", true, false, null, 'x', void)
== "true false (null) x void");
};
diff --git a/hare/parse/parse.ha b/hare/parse/parse.ha
@@ -14,7 +14,7 @@ export fn strerror(err: error) const str = lex::strerror(err: lex::error);
fn syntaxerr(
loc: lex::location,
fmt: str,
- args: fmt::formattable...
+ args: fmt::field...
) lex::error = {
let why = fmt::asprintf(fmt, args...);
return (loc, why): lex::syntax: lex::error;