use crate::ast::{
BinaryOp, BindingDecl, Cardinality, CompareOp, Declaration, DeriveDecl, Effect, Expr, ExprKind,
FieldDecl, InvariantAssertion, InvariantQuantifier, Literal, NumericLiteral, Parameter,
Program, RuleDecl, TypeRef, UnaryOp,
};
use crate::diagnostic::Diagnostic;
use crate::lexer::{Comment, Token, TokenKind, lex};
use crate::parser::{parse, parse_numeric_literal};
use crate::source::{SourceFile, Span};
use crate::value::group_integer_digits;
use std::fmt::Write;
pub fn format_source(source: SourceFile) -> Result<String, Vec<Diagnostic>> {
let parsed = parse(source);
if !parsed.diagnostics.is_empty() {
return Err(parsed.diagnostics);
}
parsed
.program
.as_ref()
.map(format_program)
.ok_or(parsed.diagnostics)
}
#[must_use]
pub fn format_program(program: &Program) -> String {
let canonical = format_program_without_comments(program);
preserve_comments(&program.source, &canonical)
}
fn format_program_without_comments(program: &Program) -> String {
let mut formatter = Formatter::default();
let mut has_header = false;
if program.module.value != "main" {
let _ = writeln!(formatter.output, "mod {}", program.module.value);
has_header = true;
}
for import in &program.imports {
formatter.output.push_str("use ");
formatter.string(&import.path.value);
formatter.output.push('\n');
has_header = true;
}
for (index, declaration) in program.declarations.iter().enumerate() {
if index > 0 || has_header {
formatter.output.push('\n');
}
formatter.declaration(declaration);
}
formatter.output
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CommentPlacement {
Before,
Trailing,
After,
}
#[derive(Debug)]
struct PlacedComment {
text: String,
placement: CommentPlacement,
indent: usize,
source_indent: usize,
source_start: usize,
}
#[derive(Debug)]
struct SignificantToken<'a> {
token: &'a Token,
key: TokenKey,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum TokenKey {
Kind(TokenKind),
Number(NumericLiteral),
}
fn preserve_comments(source: &SourceFile, canonical: &str) -> String {
let original = lex(source);
if original.comments.is_empty() {
return canonical.to_owned();
}
if significant_tokens(&original.tokens).is_empty() {
let mut normalized = source.text.replace("\r\n", "\n").replace('\r', "\n");
if !normalized.ends_with('\n') {
normalized.push('\n');
}
return normalized;
}
let canonical_source = SourceFile::new(source.name.clone(), canonical.to_owned());
let formatted = lex(&canonical_source);
let original_tokens = significant_tokens(&original.tokens);
let formatted_tokens = significant_tokens(&formatted.tokens);
let token_mapping = align_tokens(&original_tokens, &formatted_tokens);
let original_lines = LineMap::new(&source.text);
let formatted_lines = LineMap::new(canonical);
let line_count = formatted_lines.line_count();
let mut comments_by_line: Vec<Vec<PlacedComment>> =
(0..line_count).map(|_| Vec::new()).collect();
for comment in &original.comments {
let original_line = original_lines.line_of(comment.span.start);
let block = comment.text.starts_with("#<");
let trailing = !block && has_code_before_comment(&source.text, &original_lines, comment);
let previous = mapped_token_before(
&original_tokens,
&token_mapping,
comment.span.start,
trailing.then_some(original_line),
&original_lines,
);
let next = mapped_token_after(&original_tokens, &token_mapping, comment.span.end);
let (formatted_token_index, placement, reference_token) = if trailing {
previous.map_or_else(
|| {
next.map_or(
(0, CommentPlacement::Before, original_line),
|(source, target)| (target, CommentPlacement::Before, source),
)
},
|(source, target)| (target, CommentPlacement::Trailing, source),
)
} else {
next.map_or_else(
|| {
previous.map_or(
(0, CommentPlacement::After, original_line),
|(source, target)| (target, CommentPlacement::After, source),
)
},
|(source, target)| (target, CommentPlacement::Before, source),
)
};
let formatted_span = formatted_tokens
.get(formatted_token_index)
.map_or(Span::new(0, 0), |value| value.token.span);
let reference_line = original_tokens
.get(reference_token)
.map_or(original_line, |value| {
original_lines.line_of(value.token.span.start)
});
let formatted_line = formatted_lines
.line_of(formatted_span.start)
.min(line_count.saturating_sub(1));
let reference_indent = original_lines.indent(reference_line);
let comment_indent = original_lines.indent(original_line);
let formatted_indent = formatted_lines.indent(formatted_line);
let indent = adjusted_indent(formatted_indent, reference_indent, comment_indent);
comments_by_line[formatted_line].push(PlacedComment {
text: comment.text.clone(),
placement,
indent,
source_indent: comment_indent,
source_start: comment.span.start,
});
}
for comments in &mut comments_by_line {
comments.sort_by_key(|comment| comment.source_start);
let trailing_count = comments
.iter()
.filter(|comment| comment.placement == CommentPlacement::Trailing)
.count();
if trailing_count > 1 {
for comment in comments {
if comment.placement == CommentPlacement::Trailing {
comment.placement = CommentPlacement::Before;
}
}
}
}
render_with_comments(canonical, &comments_by_line)
}
fn significant_tokens(tokens: &[Token]) -> Vec<SignificantToken<'_>> {
tokens
.iter()
.filter_map(|token| token_key(&token.kind).map(|key| SignificantToken { token, key }))
.collect()
}
fn token_key(kind: &TokenKind) -> Option<TokenKey> {
match kind {
TokenKind::Newline
| TokenKind::Indent
| TokenKind::Dedent
| TokenKind::Eof
| TokenKind::Semicolon => None,
TokenKind::Number(value) => parse_numeric_literal(value).map_or_else(
|| Some(TokenKey::Kind(kind.clone())),
|value| Some(TokenKey::Number(value)),
),
_ => Some(TokenKey::Kind(kind.clone())),
}
}
fn align_tokens(
original: &[SignificantToken<'_>],
formatted: &[SignificantToken<'_>],
) -> Vec<Option<usize>> {
let mut mapping = vec![None; original.len()];
let (mut source, mut target) = (0, 0);
while source < original.len() && target < formatted.len() {
if original[source].key == formatted[target].key {
mapping[source] = Some(target);
source += 1;
target += 1;
continue;
}
let source_skip = original[source + 1..]
.iter()
.position(|candidate| candidate.key == formatted[target].key)
.map(|offset| offset + 1);
let target_skip = formatted[target + 1..]
.iter()
.position(|candidate| candidate.key == original[source].key)
.map(|offset| offset + 1);
match (source_skip, target_skip) {
(Some(source_skip), Some(target_skip)) if source_skip <= target_skip => {
source += source_skip;
}
(Some(source_skip), None) => source += source_skip,
(_, Some(target_skip)) => target += target_skip,
(None, None) => break,
}
}
mapping
}
fn mapped_token_before(
tokens: &[SignificantToken<'_>],
mapping: &[Option<usize>],
byte: usize,
required_line: Option<usize>,
lines: &LineMap,
) -> Option<(usize, usize)> {
tokens
.iter()
.enumerate()
.rev()
.filter(|(_, token)| token.token.span.end <= byte)
.filter(|(_, token)| {
required_line.is_none_or(|line| lines.line_of(token.token.span.start) == line)
})
.find_map(|(index, _)| mapping[index].map(|target| (index, target)))
}
fn mapped_token_after(
tokens: &[SignificantToken<'_>],
mapping: &[Option<usize>],
byte: usize,
) -> Option<(usize, usize)> {
tokens
.iter()
.enumerate()
.filter(|(_, token)| token.token.span.start >= byte)
.find_map(|(index, _)| mapping[index].map(|target| (index, target)))
}
fn has_code_before_comment(text: &str, lines: &LineMap, comment: &Comment) -> bool {
let line = lines.line_of(comment.span.start);
text[lines.start(line)..comment.span.start]
.chars()
.any(|character| !character.is_whitespace())
}
fn adjusted_indent(formatted: usize, reference: usize, comment: usize) -> usize {
if comment >= reference {
formatted.saturating_add(comment - reference)
} else {
formatted.saturating_sub(reference - comment)
}
}
fn render_with_comments(canonical: &str, comments_by_line: &[Vec<PlacedComment>]) -> String {
let lines: Vec<&str> = canonical
.strip_suffix('\n')
.unwrap_or(canonical)
.split('\n')
.collect();
let mut output = String::with_capacity(
canonical.len()
+ comments_by_line
.iter()
.flatten()
.map(|comment| comment.text.len() + comment.indent + 3)
.sum::<usize>(),
);
for (line_index, line) in lines.iter().enumerate() {
let comments = &comments_by_line[line_index];
for comment in comments
.iter()
.filter(|comment| comment.placement == CommentPlacement::Before)
{
write_comment_line(&mut output, comment);
}
output.push_str(line);
if let Some(comment) = comments
.iter()
.find(|comment| comment.placement == CommentPlacement::Trailing)
{
output.push_str(" ");
output.push_str(&comment.text);
}
output.push('\n');
for comment in comments
.iter()
.filter(|comment| comment.placement == CommentPlacement::After)
{
write_comment_line(&mut output, comment);
}
}
output
}
fn write_comment_line(output: &mut String, comment: &PlacedComment) {
if comment.text.starts_with("#<") {
write_multiline_comment(output, comment);
return;
}
output.extend(std::iter::repeat_n(' ', comment.indent));
output.push_str(&comment.text);
output.push('\n');
}
fn write_multiline_comment(output: &mut String, comment: &PlacedComment) {
let normalized = comment.text.replace("\r\n", "\n").replace('\r', "\n");
for (index, line) in normalized.split('\n').enumerate() {
if line.is_empty() {
output.push('\n');
continue;
}
let (line, rebased) = if index == 0 {
(line, true)
} else {
strip_comment_indent(line, comment.source_indent)
};
if rebased {
output.extend(std::iter::repeat_n(' ', comment.indent));
}
output.push_str(line);
output.push('\n');
}
}
fn strip_comment_indent(line: &str, indent: usize) -> (&str, bool) {
if indent == 0 {
return (line, true);
}
let mut width = 0;
for (index, character) in line.char_indices() {
let character_width = match character {
' ' | '\u{000C}' => 1,
'\t' => 2,
_ => break,
};
if width + character_width > indent {
break;
}
width += character_width;
if width == indent {
let end = index + character.len_utf8();
return (&line[end..], true);
}
}
(line, false)
}
#[derive(Debug)]
struct LineMap {
starts: Vec<usize>,
indents: Vec<usize>,
line_count: usize,
}
impl LineMap {
fn new(text: &str) -> Self {
let mut starts = vec![0];
let bytes = text.as_bytes();
let mut index = 0;
while index < bytes.len() {
match bytes[index] {
b'\n' => {
index += 1;
starts.push(index);
}
b'\r' => {
index += 1;
if bytes.get(index) == Some(&b'\n') {
index += 1;
}
starts.push(index);
}
_ => index += 1,
}
}
let indents = starts
.iter()
.map(|start| {
text[*start..]
.chars()
.take_while(|character| matches!(character, ' ' | '\t' | '\u{000C}'))
.map(|character| if character == '\t' { 2 } else { 1 })
.sum()
})
.collect();
let line_count = starts
.len()
.saturating_sub(usize::from(text.ends_with('\n') || text.ends_with('\r')));
Self {
starts,
indents,
line_count,
}
}
fn line_of(&self, byte: usize) -> usize {
self.starts
.partition_point(|start| *start <= byte)
.saturating_sub(1)
}
fn start(&self, line: usize) -> usize {
self.starts[line.min(self.starts.len().saturating_sub(1))]
}
fn indent(&self, line: usize) -> usize {
self.indents[line.min(self.indents.len().saturating_sub(1))]
}
fn line_count(&self) -> usize {
self.line_count
}
}
#[derive(Default)]
struct Formatter {
output: String,
}
const MAX_INLINE_TEST_BINDING_FIELDS: usize = 4;
const MAX_INLINE_TEST_BINDING_WIDTH: usize = 100;
impl Formatter {
fn declaration(&mut self, declaration: &Declaration) {
match declaration {
Declaration::Enum(value) => {
let _ = writeln!(self.output, "enum {}:", value.name.value);
for variant in &value.variants {
let _ = writeln!(self.output, " {}", variant.value);
}
}
Declaration::Entity(value) => {
let _ = writeln!(self.output, "record {}:", value.name.value);
for field in &value.fields {
self.field(field);
}
}
Declaration::State(value) => {
let _ = writeln!(self.output, "state {}:", value.name.value);
for field in &value.fields {
self.field(field);
}
}
Declaration::Derive(value) => {
self.derive(value, 0, None);
}
Declaration::Decision(value) => {
let _ = write!(self.output, "dec {}(", value.name.value);
self.parameters(&value.parameters);
let _ = write!(self.output, ") -> {}", type_name(&value.return_type.value));
match value.cardinality {
Cardinality::ExactlyOne => {}
Cardinality::ZeroOrOne => self.output.push('?'),
Cardinality::Many => self.output.push('*'),
}
self.output.push('\n');
}
Declaration::Action(value) => {
let _ = write!(self.output, "action {}(", value.name.value);
self.parameters(&value.parameters);
self.output.push_str(")\n");
}
Declaration::Transition(value) => {
let _ = write!(self.output, "transition {}(", value.name.value);
self.parameters(&value.parameters);
let _ = write!(self.output, ") on {}(", value.action.value);
self.expressions(&value.action_arguments);
self.output.push_str("):\n when ");
self.expression(&value.condition, 0, false);
self.output.push('\n');
for update in &value.updates {
let _ = write!(
self.output,
" set {}.{} = ",
update.receiver.value, update.field.value
);
self.expression(&update.value, 0, false);
self.output.push('\n');
}
}
Declaration::Fragment(value) => {
let _ = write!(self.output, "{}", value.id.value);
if value.locator.value != value.id.value {
let _ = write!(self.output, " @{}", value.locator.value);
}
self.output.push_str(":\n");
for line in value.text.value.split_terminator('\n') {
if line.is_empty() {
self.output.push('\n');
} else {
let _ = writeln!(self.output, " {line}");
}
}
if !value.refs.is_empty() || !value.derives.is_empty() || !value.rules.is_empty() {
self.output.push('\n');
}
for reference in &value.refs {
let _ = writeln!(self.output, " ref {}", reference.id.value);
}
for (index, derive) in value.derives.iter().enumerate() {
if index > 0 || !value.refs.is_empty() {
self.output.push('\n');
}
self.derive(derive, 4, Some(&value.id.value));
}
for (index, rule) in value.rules.iter().enumerate() {
if index > 0 || !value.refs.is_empty() || !value.derives.is_empty() {
self.output.push('\n');
}
self.rule(rule, 4, Some(&value.id.value));
}
}
Declaration::Rule(value) => {
self.rule(value, 0, None);
}
Declaration::Case(value) => {
let _ = writeln!(self.output, "test {}:", value.name.value);
for binding in &value.bindings {
self.test_binding(binding);
}
for expectation in &value.expectations {
let _ = write!(self.output, " expect {}(", expectation.decision.value);
self.expressions(&expectation.arguments);
let operator = match expectation.operator {
CompareOp::Equal => " = ",
CompareOp::NotEqual => " != ",
};
self.output.push(')');
self.output.push_str(operator);
self.expression(&expectation.value, 0, false);
self.output.push('\n');
}
}
Declaration::Invariant(value) => {
self.output.push_str("assert ");
if value.quantifier == InvariantQuantifier::Some {
self.output.push_str("some ");
}
let _ = write!(self.output, "{}(", value.name.value);
self.parameters(&value.variables);
self.output.push_str("):\n ");
match &value.assertion {
InvariantAssertion::Cardinality {
cardinality: value,
decision,
arguments,
..
} => {
let _ = write!(self.output, "{}(", decision.value);
self.expressions(arguments);
self.output.push(')');
match value {
Cardinality::ExactlyOne => {}
Cardinality::ZeroOrOne => self.output.push('?'),
Cardinality::Many => self.output.push('*'),
}
self.output.push('\n');
}
InvariantAssertion::Implication {
condition,
expectation,
..
} => {
self.expression(condition, 0, false);
let _ = write!(self.output, " => {}(", expectation.decision.value);
self.expressions(&expectation.arguments);
self.output.push_str(match expectation.operator {
CompareOp::Equal => ") = ",
CompareOp::NotEqual => ") != ",
});
self.expression(&expectation.value, 0, false);
self.output.push('\n');
}
}
}
Declaration::Trace(value) => {
let _ = write!(self.output, "trace {}(", value.name.value);
self.parameters(std::slice::from_ref(&value.variable));
self.output.push_str("):\n initially ");
self.expression(&value.initial, 0, false);
self.output.push('\n');
for condition in &value.always {
self.output.push_str(" always ");
self.expression(condition, 0, false);
self.output.push('\n');
}
if let Some(condition) = &value.terminal {
self.output.push_str(" terminates when ");
self.expression(condition, 0, false);
self.output.push('\n');
}
if value.no_dead_ends {
self.output.push_str(" no dead ends\n");
}
let _ = writeln!(self.output, " within {} steps", value.within.value);
}
}
}
fn parameters(&mut self, parameters: &[Parameter]) {
for (index, parameter) in parameters.iter().enumerate() {
if index != 0 {
self.output.push_str(", ");
}
let implicit = parameters.len() == 1
&& parameter.ty.value.rsplit("::").next() == Some(parameter.name.value.as_str());
if implicit {
let _ = write!(self.output, "{}", parameter.ty.value);
} else {
let _ = write!(
self.output,
"{} {}",
parameter.name.value, parameter.ty.value
);
}
}
}
fn derive(&mut self, derive: &DeriveDecl, indent: usize, enclosing: Option<&str>) {
let prefix = " ".repeat(indent);
let body_prefix = " ".repeat(indent + 4);
let _ = write!(self.output, "{prefix}fn {}(", derive.name.value);
self.parameters(&derive.parameters);
let _ = writeln!(
self.output,
") -> {}:",
type_name(&derive.return_type.value)
);
let implicit = usize::from(enclosing.is_some_and(|id| {
derive
.basis
.first()
.is_some_and(|basis| basis.id.value == id)
}));
for basis in derive.basis.iter().skip(implicit) {
let _ = writeln!(self.output, "{body_prefix}basis {}", basis.id.value);
}
self.output.push_str(&body_prefix);
self.expression(&derive.expression, 0, false);
self.output.push('\n');
}
fn test_binding(&mut self, binding: &BindingDecl) {
let mut inline = Self::default();
inline.test_binding_inline(binding);
if binding.fields.len() <= MAX_INLINE_TEST_BINDING_FIELDS
&& estimated_display_width(&inline.output) <= MAX_INLINE_TEST_BINDING_WIDTH
{
self.output.push_str(&inline.output);
self.output.push('\n');
return;
}
if implicit_test_binding(binding) {
let _ = writeln!(self.output, " {} {{", binding.entity.value);
} else {
let _ = writeln!(
self.output,
" let {} = {} {{",
binding.name.value, binding.entity.value
);
}
for field in &binding.fields {
let _ = write!(self.output, " {}: ", field.name.value);
self.expression(&field.value, 0, false);
self.output.push_str(",\n");
}
self.output.push_str(" }\n");
}
fn test_binding_inline(&mut self, binding: &BindingDecl) {
if implicit_test_binding(binding) {
let _ = write!(self.output, " {} {{ ", binding.entity.value);
} else {
let _ = write!(
self.output,
" let {} = {} {{ ",
binding.name.value, binding.entity.value
);
}
for (index, field) in binding.fields.iter().enumerate() {
if index > 0 {
self.output.push_str(", ");
}
let _ = write!(self.output, "{}: ", field.name.value);
self.expression(&field.value, 0, false);
}
self.output.push_str(" }");
}
fn field(&mut self, field: &FieldDecl) {
let _ = write!(
self.output,
" {}: {}",
field.name.value,
type_name(&field.ty)
);
if field.optional {
self.output.push('?');
}
if let Some(range) = &field.range {
let _ = write!(
self.output,
" {}..{}",
numeric(&range.start),
numeric(&range.end)
);
}
if let Some(domain) = &field.domain {
self.output.push_str(" {");
self.expressions(&domain.values);
self.output.push('}');
}
self.output.push('\n');
}
fn rule(&mut self, rule: &RuleDecl, indent: usize, enclosing: Option<&str>) {
let prefix = " ".repeat(indent);
let body_prefix = " ".repeat(indent + 4);
let _ = write!(self.output, "{prefix}rule {}(", rule.name.value);
self.parameters(&rule.parameters);
self.output.push_str("):\n");
let implicit = usize::from(
enclosing
.is_some_and(|id| rule.basis.first().is_some_and(|basis| basis.id.value == id)),
);
for basis in rule.basis.iter().skip(implicit) {
let _ = writeln!(self.output, "{body_prefix}basis {}", basis.id.value);
}
self.output.push_str(&body_prefix);
if !matches!(rule.condition.kind, ExprKind::Literal(Literal::Bool(true))) {
self.expression(&rule.condition, 0, false);
self.output.push(' ');
}
self.output.push_str("=> ");
match &rule.effect {
Effect::Decide {
decision,
arguments,
value,
..
} => {
let _ = write!(self.output, "{}(", decision.value);
self.expressions(arguments);
self.output.push_str(") = ");
self.expression(value, 0, false);
}
Effect::Override { rule, .. } => {
let _ = write!(self.output, "override {}", rule.value);
}
Effect::Invalid { .. } => {
self.output.push_str("__invalid__() = unknown");
}
}
self.output.push('\n');
}
fn expressions(&mut self, expressions: &[Expr]) {
for (index, expression) in expressions.iter().enumerate() {
if index != 0 {
self.output.push_str(", ");
}
self.expression(expression, 0, false);
}
}
fn expression(&mut self, expression: &Expr, parent_precedence: u8, right_child: bool) {
let precedence = expression_precedence(expression);
let parenthesize = precedence < parent_precedence
|| (right_child
&& precedence == parent_precedence
&& matches!(expression.kind, ExprKind::Binary { .. }));
if parenthesize {
self.output.push('(');
}
match &expression.kind {
ExprKind::Literal(Literal::Bool(value)) => {
self.output.push_str(if *value { "true" } else { "false" });
}
ExprKind::Literal(Literal::Number(value)) => self.output.push_str(&numeric(value)),
ExprKind::Literal(Literal::String(value)) => self.string(value),
ExprKind::Literal(Literal::Unknown) => self.output.push_str("unknown"),
ExprKind::Name(value) => self.output.push_str(value),
ExprKind::Field { receiver, field } => {
self.expression(receiver, precedence, false);
self.output.push('.');
self.output.push_str(&field.value);
}
ExprKind::Call { callee, arguments } => {
self.output.push_str(&callee.value);
self.output.push('(');
self.expressions(arguments);
self.output.push(')');
}
ExprKind::Unary { operator, operand } => {
match operator {
UnaryOp::Not => self.output.push_str("not "),
UnaryOp::Negate => self.output.push('-'),
}
self.expression(operand, precedence, false);
}
ExprKind::Binary {
left,
operator,
right,
} => {
self.expression(left, precedence, false);
let _ = write!(self.output, " {} ", binary_operator(*operator));
self.expression(right, precedence, true);
}
}
if parenthesize {
self.output.push(')');
}
}
fn string(&mut self, value: &str) {
self.output.push('"');
for character in value.chars() {
match character {
'\n' => self.output.push_str("\\n"),
'\r' => self.output.push_str("\\r"),
'\t' => self.output.push_str("\\t"),
'\0' => self.output.push_str("\\0"),
'\\' => self.output.push_str("\\\\"),
'"' => self.output.push_str("\\\""),
value if value.is_control() => {
let _ = write!(self.output, "\\u{{{:X}}}", u32::from(value));
}
value => self.output.push(value),
}
}
self.output.push('"');
}
}
fn type_name(value: &TypeRef) -> &str {
match value {
TypeRef::Bool => "Bool",
TypeRef::Int => "Int",
TypeRef::Decimal => "Decimal",
TypeRef::String => "String",
TypeRef::Date => "Date",
TypeRef::Duration => "Duration",
TypeRef::Named(value) => value,
TypeRef::Unknown => "Unknown",
}
}
fn estimated_display_width(value: &str) -> usize {
value
.chars()
.map(|character| if character.is_ascii() { 1 } else { 2 })
.sum()
}
fn implicit_test_binding(binding: &BindingDecl) -> bool {
binding.entity.value.rsplit("::").next() == Some(binding.name.value.as_str())
}
fn numeric(value: &NumericLiteral) -> String {
match value {
NumericLiteral::Int(value) => group_integer_digits(&value.to_string()),
NumericLiteral::Decimal(value) => {
let (integer, fraction) = value.split_once('.').unwrap_or((value, ""));
format!("{}.{}", group_integer_digits(integer), fraction)
}
}
}
const fn expression_precedence(expression: &Expr) -> u8 {
match expression.kind {
ExprKind::Binary {
operator: BinaryOp::Or,
..
} => 1,
ExprKind::Binary {
operator: BinaryOp::And,
..
} => 2,
ExprKind::Binary {
operator:
BinaryOp::Equal
| BinaryOp::NotEqual
| BinaryOp::Greater
| BinaryOp::GreaterEqual
| BinaryOp::Less
| BinaryOp::LessEqual,
..
} => 3,
ExprKind::Binary {
operator: BinaryOp::Add | BinaryOp::Subtract,
..
} => 4,
ExprKind::Binary {
operator: BinaryOp::Multiply | BinaryOp::Divide,
..
} => 5,
ExprKind::Unary { .. } => 6,
ExprKind::Field { .. } | ExprKind::Call { .. } => 7,
ExprKind::Literal(_) | ExprKind::Name(_) => 8,
}
}
const fn binary_operator(value: BinaryOp) -> &'static str {
match value {
BinaryOp::Or => "or",
BinaryOp::And => "and",
BinaryOp::Equal => "=",
BinaryOp::NotEqual => "!=",
BinaryOp::Greater => ">",
BinaryOp::GreaterEqual => ">=",
BinaryOp::Less => "<",
BinaryOp::LessEqual => "<=",
BinaryOp::Add => "+",
BinaryOp::Subtract => "-",
BinaryOp::Multiply => "*",
BinaryOp::Divide => "/",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn formatted(text: &str) -> String {
format_source(SourceFile::new("test.tes", text)).unwrap()
}
#[test]
fn canonical_surface_is_idempotent_and_uses_four_spaces() {
let input = r#"mod policy
enum Result:
Yes
No
record Request:
score: Int 0..100
note: String?
fn eligible(request Request) -> Bool:
request.score>=70
dec result(request Request)->Result
policy::article @article 4 / paragraph 1:
A request must meet the threshold.
rule threshold(request Request):
eligible(request)=>result(request)=Yes
test example:
let request=Request {score:80,note:"ok"}
expect result(request)=Yes
assert total(request Request):
result(request)
"#;
let once = formatted(input);
assert!(once.contains("record Request:\n score: Int 0..100"));
assert!(once.contains("eligible(request) => result(request) = Yes"));
assert_eq!(formatted(&once), once);
}
#[test]
fn omits_default_module_and_exactly_one_marker() {
let output = formatted("dec result(domain::Request)->Bool\n");
assert_eq!(output, "dec result(domain::Request) -> Bool\n");
}
#[test]
fn formats_cardinality_and_assertion_shorthand() {
let input = r"dec maybe(request Request)->Bool?
dec reasons(request Request)->String*
assert some optional(request Request):
maybe(request)?
";
let expected = r"dec maybe(request Request) -> Bool?
dec reasons(request Request) -> String*
assert some optional(request Request):
maybe(request)?
";
assert_eq!(formatted(input), expected);
}
#[test]
fn keeps_short_test_bindings_inline() {
let input = r"test 짧은입력:
let 입력 = 입력 {
금액: 60_000,
승인: true,
}
";
let expected = r"test 짧은입력:
입력 { 금액: 60_000, 승인: true }
";
assert_eq!(formatted(input), expected);
assert_eq!(formatted(expected), expected);
}
#[test]
fn breaks_test_bindings_with_many_fields_across_lines() {
let input = r"test 많은입력:
let 입력 = 입력 { 하나: 1, 둘: 2, 셋: 3, 넷: 4, 다섯: 5 }
";
let expected = r"test 많은입력:
입력 {
하나: 1,
둘: 2,
셋: 3,
넷: 4,
다섯: 5,
}
";
assert_eq!(formatted(input), expected);
assert_eq!(formatted(expected), expected);
}
#[test]
fn breaks_wide_test_bindings_across_lines() {
let input = r#"test 긴입력:
let 사건 = 사건 { 설명: "공동주택의 야간 층간소음 측정과 관리주체의 조치 이후에도 피해가 계속된 사건", 공식측정: true }
"#;
let output = formatted(input);
assert!(
output.contains(
" 사건 {\n 설명: \"공동주택의 야간 층간소음 측정과 관리주체의 조치 이후에도 피해가 계속된 사건\",\n 공식측정: true,\n }"
),
"{output}"
);
assert_eq!(formatted(&output), output);
}
#[test]
fn keeps_explicit_test_binding_when_the_local_name_differs() {
let input = r"test 두입력:
let left = Request { enabled: true }
let right = Request { enabled: false }
";
assert_eq!(formatted(input), input);
}
#[test]
fn canonicalizes_qualified_type_binding_to_its_final_segment() {
let input = r"test 패키지입력:
let Request = shared::Request { enabled: true }
expect allowed(Request) = true
";
let expected = r"test 패키지입력:
shared::Request { enabled: true }
expect allowed(Request) = true
";
assert_eq!(formatted(input), expected);
assert_eq!(formatted(expected), expected);
}
#[test]
fn recovery_effect_formats_as_one_parseable_effect_line() {
let parsed = parse(SourceFile::new(
"test.tes",
"rule recovered():\n => result() = true\n",
));
let mut program = parsed.program.unwrap();
let Declaration::Rule(rule) = &mut program.declarations[0] else {
panic!("expected rule");
};
rule.effect = Effect::Invalid { span: rule.span };
let output = format_program(&program);
assert_eq!(
output,
"rule recovered():\n => __invalid__() = unknown\n"
);
assert!(
parse(SourceFile::new("formatted.tes", output))
.diagnostics
.is_empty()
);
}
#[test]
fn raw_text_stays_raw_and_adjacent_to_its_rules() {
let input = r"law::article @조 4 / 항 1:
제4조 신청인은 주소를 두어야 한다.
1. 들여쓴 원문
# 원문의 일부
rule 주소(a 신청):
=> 결과(a) = 가능
";
let output = formatted(input);
assert!(output.contains(" # 원문의 일부"));
assert!(output.contains(" 1. 들여쓴 원문"));
assert!(output.contains(" # 원문의 일부\n\n rule 주소"));
assert_eq!(formatted(&output), output);
}
#[test]
fn formats_fragment_functions_at_two_structural_depths() {
let input = r"law::formula:
The amount is seventy percent of the stated value.
fn amount(request Request)->Decimal:
basis law::definitions
request.value*0.7
";
let expected = r"law::formula:
The amount is seventy percent of the stated value.
fn amount(request Request) -> Decimal:
basis law::definitions
request.value * 0.7
";
assert_eq!(formatted(input), expected);
assert_eq!(formatted(expected), expected);
}
#[test]
fn preserves_hash_and_multiline_comments() {
let input = r"# file note
record Request: # record note
#<
field note
kept relative
#>
score: Int
";
let output = formatted(input);
assert!(output.contains("# file note"));
assert!(output.contains("record Request: # record note"));
assert!(output.contains(" #<\n field note\n kept relative\n #>"));
assert_eq!(formatted(&output), output);
}
#[test]
fn comment_only_files_do_not_gain_a_leading_blank_line() {
assert_eq!(formatted("# only"), "# only\n");
assert_eq!(formatted("#<\r\nbody\r\n#>\r\n"), "#<\nbody\n#>\n");
}
#[test]
fn opaque_locator_keeps_spacing_and_later_comment_anchors() {
let input = r"policy::source @A B: # fragment note
Original text.
rule applies():
=> result() = true
# declaration note
dec result() -> Bool
";
let output = formatted(input);
assert!(
output.contains("policy::source @A B: # fragment note"),
"{output}"
);
assert!(
output.contains("# declaration note\ndec result()"),
"{output}"
);
assert_eq!(formatted(&output), output);
}
#[test]
fn formats_state_transition_and_trace() {
let input = r"state Flow:
done: Bool
action advance(flow Flow)
transition once(flow Flow) on advance(flow):
when not flow.done
set flow.done=true
trace completion(flow Flow):
initially not flow.done
terminates when flow.done
no dead ends
within 2 steps
";
let output = formatted(input);
assert!(output.contains(" set flow.done = true"));
assert!(output.contains("trace completion(flow Flow):"));
assert_eq!(formatted(&output), output);
}
#[test]
fn refuses_invalid_layout() {
let result = format_source(SourceFile::new(
"bad.tes",
"record Request:\n score: Int\n",
));
assert!(result.is_err());
}
}