hare

The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

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:
Mfmt/README | 30+++++++++++++++++++++++++++++-
Mfmt/fmt.ha | 123++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mhare/parse/parse.ha | 2+-
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;