Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

String formatting improvements #421

Merged
merged 5 commits into from
Feb 12, 2025
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,21 @@ The Koto project adheres to
x = 1_000_000
y = 0xff_aa_bb
```
- Interpolated values can now be formatted with alternative representations.
- E.g.
```koto
print '{15:b}'
# -> 1111
```
- The `@debug` metakey has been added to allow for additional debug information
to be provided when formatting an object as a string.

#### API

- `Compiler::compile_ast` has been added, useful for tools that want to work with the AST
after checking that it compiles correctly.
- `DisplayContext::debug_enabled` has been added to allow native objects to provide
additional debug information when `KotoObject::display` is called.

### Changed

Expand Down
10 changes: 5 additions & 5 deletions crates/bytecode/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2162,7 +2162,7 @@ impl Compiler {
);
self.push_op_without_span(
Op::StringPush,
&[node_register, 0],
&[node_register, StringFormatFlags::default().into()],
);

self.pop_register()?;
Expand All @@ -2176,10 +2176,7 @@ impl Compiler {
let format_flags = StringFormatFlags::from(*format);
self.push_op_without_span(
Op::StringPush,
&[
expression_result.unwrap(self)?,
format_flags.as_byte(),
],
&[expression_result.unwrap(self)?, format_flags.into()],
);
if let Some(min_width) = format.min_width {
self.push_var_u32(min_width);
Expand All @@ -2190,6 +2187,9 @@ impl Compiler {
if let Some(fill_constant) = format.fill_character {
self.push_var_u32(fill_constant.into());
}
if let Some(style) = format.representation {
self.bytes.push(style as u8);
}

if expression_result.is_temporary {
self.pop_register()?;
Expand Down
119 changes: 71 additions & 48 deletions crates/bytecode/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,75 +428,97 @@ impl From<FunctionFlags> for u8 {
}

/// Format flags used by the [StringPush][crate::Op::StringPush] op
pub struct StringFormatFlags {
/// The alignment of the string
pub alignment: StringAlignment,
/// True if a min width value is specified
pub min_width: bool,
/// True if a precision value is specified
pub precision: bool,
/// True if a fill character is specified
pub fill_character: bool,
}
///
/// This is the bytecode counterpart to [koto_parser::StringFormatOptions].
#[derive(Clone, Copy, Default)]
#[repr(transparent)]
pub struct StringFormatFlags(u8);

impl StringFormatFlags {
/// Set to true when min_width is defined
pub const MIN_WIDTH: u8 = 1 << 2; // The first two bits are taken up by the alignment enum
/// Set to true when a minimum width is defined
pub const MIN_WIDTH: u8 = 1 << 2; // The first two bits correspond to values of StringAlignment
/// Set to true when precision is defined
pub const PRECISION: u8 = 1 << 3;
/// Set to true when fill_character is defined
/// Set to true when a fill character is defined
pub const FILL_CHARACTER: u8 = 1 << 4;
/// Set to true when a format style is defined
pub const REPRESENTATION: u8 = 1 << 5;

/// Decodes a byte into format flags
pub fn from_byte(byte: u8) -> Self {
use StringAlignment::*;
let alignment_bits = byte & 0b11;
let alignment = if alignment_bits == Default as u8 {
Default
} else if alignment_bits == Left as u8 {
Left
} else if alignment_bits == Center as u8 {
Center
/// Returns the flag's string alignment
pub fn alignment(&self) -> StringAlignment {
use StringAlignment as Align;
let bits = self.0 & 0b11;
if bits == Align::Default as u8 {
Align::Default
} else if bits == Align::Left as u8 {
Align::Left
} else if bits == Align::Center as u8 {
Align::Center
} else {
Right
};
Self {
alignment,
min_width: byte & Self::MIN_WIDTH != 0,
precision: byte & Self::PRECISION != 0,
fill_character: byte & Self::FILL_CHARACTER != 0,
Align::Right
}
}

/// Returns a byte containing the packed flags
pub fn as_byte(&self) -> u8 {
let mut result = self.alignment as u8;
/// True if a minimum width has been defined
pub fn has_min_width(&self) -> bool {
self.0 & Self::MIN_WIDTH != 0
}

/// True if a precision has been defined
pub fn has_precision(&self) -> bool {
self.0 & Self::PRECISION != 0
}

/// True if a fill character has been defined
pub fn has_fill_character(&self) -> bool {
self.0 & Self::FILL_CHARACTER != 0
}

/// True if an alternative representation has been defined
pub fn has_representation(&self) -> bool {
self.0 & Self::REPRESENTATION != 0
}
}

impl From<StringFormatOptions> for StringFormatFlags {
fn from(value: StringFormatOptions) -> Self {
let mut flags = value.alignment as u8;

if self.min_width {
result |= Self::MIN_WIDTH;
if value.min_width.is_some() {
flags |= Self::MIN_WIDTH;
}
if self.precision {
result |= Self::PRECISION;
if value.precision.is_some() {
flags |= Self::PRECISION;
}
if self.fill_character {
result |= Self::FILL_CHARACTER;
if value.fill_character.is_some() {
flags |= Self::FILL_CHARACTER;
}
if value.representation.is_some() {
flags |= Self::REPRESENTATION;
}

result
Self(flags)
}
}

impl From<StringFormatOptions> for StringFormatFlags {
fn from(options: StringFormatOptions) -> Self {
Self {
alignment: options.alignment,
min_width: options.min_width.is_some(),
precision: options.precision.is_some(),
fill_character: options.fill_character.is_some(),
impl TryFrom<u8> for StringFormatFlags {
type Error = String;

fn try_from(byte: u8) -> Result<Self, Self::Error> {
if byte <= 0b1111111 {
Ok(Self(byte))
} else {
Err(format!("Invalid string format flags: {byte:#010b}"))
}
}
}

impl From<StringFormatFlags> for u8 {
fn from(value: StringFormatFlags) -> Self {
value.0
}
}

impl fmt::Debug for Instruction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Instruction::*;
Expand Down Expand Up @@ -824,7 +846,8 @@ impl fmt::Debug for Instruction {
id,
} => write!(
f,
"MetaInsert map: {register:<10} id: {id:<11}value: {value}",
"MetaInsert map: {register:<10} id: {:<11} value: {value}",
format!("{id}")
),
MetaInsertNamed {
register,
Expand Down
Loading