use crate::ast::{
BinaryOp, Cardinality, CompareOp, Declaration, Effect, Expr, ExprKind, InvariantAssertion,
InvariantQuantifier, Literal, NumericLiteral, Parameter, Program, SourceRef, TypeRef, UnaryOp,
};
use crate::diagnostic::Diagnostic;
use crate::lexer::{Comment, Keyword, 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 _ = writeln!(formatter.output, "module {}", program.module.value);
for import in &program.imports {
formatter.output.push_str("import ");
formatter.string(&import.path.value);
formatter.output.push('\n');
}
for declaration in &program.declarations {
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_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();
}
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 trailing = 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_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::Eof
| TokenKind::Comma
| TokenKind::Semicolon
| TokenKind::Question
| TokenKind::Keyword(Keyword::Optional | Keyword::Cardinality) => 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) {
output.extend(std::iter::repeat_n(' ', comment.indent));
output.push_str(&comment.text);
output.push('\n');
}
#[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,
}
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);
}
self.output.push_str("}\n");
}
Declaration::Entity(value) => {
let _ = writeln!(self.output, "entity {} {{", value.name.value);
for field in &value.fields {
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,
" range {}..{}",
numeric(&range.start),
numeric(&range.end)
);
}
if let Some(domain) = &field.domain {
self.output.push_str(" domain {");
for (index, value) in domain.values.iter().enumerate() {
if index > 0 {
self.output.push_str(", ");
}
self.expression(value, 0, false);
}
self.output.push('}');
}
self.output.push_str(",\n");
}
self.output.push_str("}\n");
}
Declaration::State(value) => {
let _ = writeln!(self.output, "state {} {{", value.name.value);
for field in &value.fields {
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,
" range {}..{}",
numeric(&range.start),
numeric(&range.end)
);
}
if let Some(domain) = &field.domain {
self.output.push_str(" domain {");
for (index, domain_value) in domain.values.iter().enumerate() {
if index > 0 {
self.output.push_str(", ");
}
self.expression(domain_value, 0, false);
}
self.output.push('}');
}
self.output.push_str(",\n");
}
self.output.push_str("}\n");
}
Declaration::Derive(value) => {
let _ = write!(self.output, "derive {}(", value.name.value);
self.parameters(&value.parameters);
let _ = write!(
self.output,
") -> {}: ",
type_name(&value.return_type.value)
);
self.expression(&value.expression, 0, false);
self.output.push('\n');
}
Declaration::Decision(value) => {
let _ = write!(self.output, "decision {}(", value.name.value);
self.parameters(&value.parameters);
let _ = write!(self.output, ") -> {}", type_name(&value.return_type.value));
if value.cardinality != Cardinality::ExactlyOne {
let _ = write!(self.output, " {}", cardinality(value.cardinality));
}
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::Source(value) => {
let _ = writeln!(self.output, "source {} {{", value.name.value);
self.output.push_str(" title: ");
self.string(&value.title.value);
self.output.push_str(",\n");
if let Some(section) = &value.section {
self.output.push_str(" section: ");
self.string(§ion.value);
self.output.push_str(",\n");
}
if let Some(version) = &value.version {
self.output.push_str(" version: ");
self.string(&version.value);
self.output.push_str(",\n");
}
if let Some(uri) = &value.uri {
self.output.push_str(" uri: ");
self.string(&uri.value);
self.output.push_str(",\n");
}
self.output.push_str("}\n");
}
Declaration::Rule(value) => {
let _ = write!(self.output, "rule {}(", value.name.value);
self.parameters(&value.parameters);
self.output.push_str("):\n when ");
self.expression(&value.condition, 0, false);
self.output.push('\n');
for effect in &value.effects {
match effect {
Effect::Decide {
decision,
arguments,
value,
..
} => {
let _ = write!(self.output, " then {}(", decision.value);
self.expressions(arguments);
self.output.push_str(") = ");
self.expression(value, 0, false);
self.output.push('\n');
}
Effect::Override { rule, .. } => {
let _ = writeln!(self.output, " then override {}", rule.value);
}
}
}
for source in &value.sources {
self.output.push_str(" source ");
match source {
SourceRef::Named(value) => self.output.push_str(&value.value),
SourceRef::Inline(value) => self.string(&value.value),
}
self.output.push('\n');
}
}
Declaration::Case(value) => {
let _ = writeln!(self.output, "case {}:", value.name.value);
for binding in &value.bindings {
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");
}
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) => {
let _ = writeln!(self.output, "invariant {}:", value.name.value);
self.output.push_str(match value.quantifier {
InvariantQuantifier::All => " for all ",
InvariantQuantifier::Some => " for some ",
});
for (index, variable) in value.variables.iter().enumerate() {
if index > 0 {
self.output.push_str(", ");
}
let _ = write!(
self.output,
"{}: {}",
variable.name.value, variable.ty.value
);
}
self.output.push('\n');
match &value.assertion {
InvariantAssertion::Cardinality {
cardinality: value,
decision,
arguments,
..
} => {
let _ =
write!(self.output, " {} {}(", cardinality(*value), decision.value);
self.expressions(arguments);
self.output.push_str(")\n");
}
InvariantAssertion::Implication {
condition,
expectation,
..
} => {
self.output.push_str(" if ");
self.expression(condition, 0, false);
let _ = write!(self.output, "\n then {}(", 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 _ = writeln!(self.output, "trace {}:", value.name.value);
let _ = writeln!(
self.output,
" for all {}: {}",
value.variable.name.value, value.variable.ty.value
);
self.output.push_str(" 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 _ = write!(
self.output,
"{} {}",
parameter.ty.value, parameter.name.value
);
}
}
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 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 cardinality(value: Cardinality) -> &'static str {
match value {
Cardinality::ExactlyOne => "exactly one",
Cardinality::ZeroOrOne => "zero or one",
Cardinality::Many => "many",
}
}
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_format_is_idempotent() {
let input = "module 성적\n\
enum 등급 { A,F }\n\
entity 학생 { 점수:Int range 0..100, 승인:Bool, }\n\
decision 최종등급(학생 s)->등급 exactly one\n\
rule A등급(학생 s):\n\
when s.점수>=90\n\
then 최종등급(s)=A\n\
case 정상:\n\
let s=학생 { 점수:95, 승인:false, }\n\
expect 최종등급(s)=A\n";
let once = formatted(input);
let twice = formatted(&once);
assert_eq!(once, twice);
}
#[test]
fn formats_state_transition_and_trace_declarations_canonically() {
let input = "module state\n\
enum Phase{Idle,Running}\n\
state Machine{phase:Phase,retries:Int range 0..2,enabled:Bool}\n\
action advance(Machine state)\n\
transition begin(Machine state) on advance(state):\n\
when state.enabled\n\
set state.phase=Running\n\
trace within:\n\
for all state:Machine\n\
initially state.phase=Idle\n\
always state.retries>=0\n\
terminates when state.phase=Running\n\
no dead ends\n\
within 3 steps\n";
let output = formatted(input);
assert!(
output.contains(
"state Machine {\n phase: Phase,\n retries: Int range 0..2,\n enabled: Bool,\n}\n"
),
"{output}"
);
assert!(
output.contains(
"transition begin(Machine state) on advance(state):\n when state.enabled\n set state.phase = Running\n"
),
"{output}"
);
assert!(
output.contains(
"trace within:\n for all state: Machine\n initially state.phase = Idle\n always state.retries >= 0\n terminates when state.phase = Running\n no dead ends\n within 3 steps\n"
),
"{output}"
);
assert_eq!(formatted(&output), output);
}
#[test]
fn preserves_qualified_package_references() {
let input = "module app\n\
derive imported(common::Subject subject)->Bool:common::enabled(subject)\n\
case c:\n\
let subject=common::Subject{enabled:true}\n\
expect common::outcome(subject)=Yes\n";
let output = formatted(input);
assert!(
output.contains(
"derive imported(common::Subject subject) -> Bool: common::enabled(subject)"
),
"{output}"
);
assert!(
output.contains("let subject = common::Subject {\n enabled: true,"),
"{output}"
);
assert!(output.contains("expect common::outcome(subject) = Yes"));
assert_eq!(formatted(&output), output);
}
#[test]
fn groups_large_numeric_literals_with_digit_separators() {
let output = formatted(
"module m\nentity X { value: Int range -1000000..1000000 }\nderive amount(X x) -> Decimal: x.value + 1234567.5000\nrule r(X x):\n when true\n then d(x) = 1\n",
);
assert!(output.contains("range -1_000_000..1_000_000"), "{output}");
assert!(output.contains("1_234_567.5000"), "{output}");
assert_eq!(formatted(&output), output);
}
#[test]
fn formats_finite_field_domains_canonically() {
let output = formatted(
"module m\nentity E { status:String domain{\"draft\",\"approved\"}, rate:Decimal domain{0.05,0.10}, }\n",
);
assert!(
output.contains("status: String domain {\"draft\", \"approved\"}"),
"{output}"
);
assert!(
output.contains("rate: Decimal domain {0.05, 0.10}"),
"{output}"
);
}
#[test]
fn keeps_comments_anchored_when_numeric_spelling_is_canonicalized() {
let output =
formatted("module m\nentity X { value: Int range 0..100000 # upper bound\n}\n");
assert!(output.contains("value: Int range 0..100_000, # upper bound"));
}
#[test]
fn emits_required_commas_and_omits_default_cardinality() {
let output = formatted(
"module m\nenum E { A, B }\nentity X { value: Int }\ndecision d(X x) -> E exactly one\ndecision maybe(X x) -> E zero or one\ndecision multiple(X x) -> E many\n",
);
assert!(output.contains(" A,\n B,\n"));
assert!(output.contains(" value: Int,\n"));
assert!(output.contains("decision d(X x) -> E\n"));
assert!(!output.contains("decision d(X x) -> E exactly one\n"));
assert!(output.contains("decision maybe(X x) -> E zero or one\n"));
assert!(output.contains("decision multiple(X x) -> E many\n"));
}
#[test]
fn preserves_expression_tree_with_parentheses() {
let output = formatted(
"module m\nderive a(X x) -> Int: x.a * (x.b + x.c)\nderive b(X x) -> Int: x.a - (x.b - x.c)\n",
);
assert!(output.contains("x.a * (x.b + x.c)"));
assert!(output.contains("x.a - (x.b - x.c)"));
}
#[test]
fn canonicalizes_string_escaping() {
let output = formatted(
"module m\nrule r(X x):\n when true\n then d(x) = true\n source \"a\\n\\\"b\"\n",
);
assert!(output.contains(r#"source "a\n\"b""#));
assert_eq!(formatted(&output), output);
}
#[test]
fn canonicalizes_structured_sources_and_preserves_multiple_references() {
let output = formatted(
"module m\n\
source policy { uri: \"https://example.test\", version: \"v2\", title: \"Policy\", section: \"3.1\" }\n\
rule r():\n\
when true\n\
then d() = true\n\
source policy\n\
source \"legacy\"\n",
);
assert!(output.contains(
"source policy {\n title: \"Policy\",\n section: \"3.1\",\n version: \"v2\",\n uri: \"https://example.test\",\n}\n"
));
assert!(output.contains(" source policy\n source \"legacy\"\n"));
assert_eq!(formatted(&output), output);
}
#[test]
fn preserves_comments_while_canonicalizing_source_field_order() {
let input = "module m\n\
source policy {\n\
// URI docs\n\
uri: \"https://example.test\", // URI value\n\
// title docs\n\
title: \"Policy\", // title value\n\
}\n";
let output = formatted(input);
for comment in [
"// URI docs",
"// URI value",
"// title docs",
"// title value",
] {
assert!(output.contains(comment), "{output}");
}
assert_eq!(formatted(&output), output);
}
#[test]
fn refuses_to_format_invalid_source() {
let result = format_source(SourceFile::new("test.tes", "module m\nenum E { A B }"));
assert!(result.is_err());
}
#[test]
fn formats_both_invariant_forms() {
let output = formatted(
"module m\ninvariant c:\n for all x: X\n exactly one d(x)\ninvariant i:\n for all x: X\n if x.a>0\n then d(x)!=F\n",
);
assert!(output.contains(" exactly one d(x)\n"));
assert!(output.contains(" if x.a > 0\n then d(x) != F\n"));
}
#[test]
fn preserves_existential_invariant_quantifier() {
let output = formatted(
"module m\ninvariant witness:\n for some x:X\n if x.enabled\n then d(x)=Yes\n",
);
assert!(output.contains(" for some x: X\n"));
assert_eq!(formatted(&output), output);
}
#[test]
fn formats_multiple_quantified_invariant_variables() {
let output = formatted(
"module m\ninvariant c:\n for all o: Order,c:Customer\n exactly one d(o,c)\n",
);
assert!(output.contains(" for all o: Order, c: Customer\n"));
assert_eq!(formatted(&output), output);
}
#[test]
fn formats_imports_between_the_module_and_declarations() {
let output = formatted(
"module m\nimport \"types.tes\"\nimport \"rules/\\\"quoted\\\".tes\"\nenum E { A }\n",
);
assert_eq!(
output,
"module m\nimport \"types.tes\"\nimport \"rules/\\\"quoted\\\".tes\"\n\nenum E {\n A,\n}\n"
);
}
#[test]
fn preserves_hash_and_slash_comments_in_stable_positions() {
let input = concat!(
"# license\r\n",
"module m // module\r\n",
"// dependency\r\n",
"import \"defs.tes\" # imported\r\n",
"\r\n",
"// values\r\n",
"enum E { # enum\r\n",
" A, // first\r\n",
" # before B\r\n",
" B, # second\r\n",
" # closing\r\n",
"} // done\r\n",
"\r\n",
"entity X {\r\n",
" value: Int optional # field\r\n",
"}\r\n",
"// eof",
);
let output = formatted(input);
assert_eq!(
output,
concat!(
"# license\n",
"module m // module\n",
"// dependency\n",
"import \"defs.tes\" # imported\n",
"\n",
"// values\n",
"enum E { # enum\n",
" A, // first\n",
" # before B\n",
" B, # second\n",
" # closing\n",
"} // done\n",
"\n",
"entity X {\n",
" value: Int?, # field\n",
"}\n",
"// eof\n",
)
);
assert_eq!(formatted(&output), output);
let before = lex(&SourceFile::new("before.tes", input))
.comments
.into_iter()
.map(|comment| comment.text)
.collect::<Vec<_>>();
let after = lex(&SourceFile::new("after.tes", output.clone()))
.comments
.into_iter()
.map(|comment| comment.text)
.collect::<Vec<_>>();
assert_eq!(after, before);
}
#[test]
fn preserves_comments_around_multiline_syntax_without_hiding_code() {
let input = "module m\n\
enum E { A }\n\
entity X { value: Int }\n\
// decision docs\n\
decision d( # parameters\n\
// the input\n\
X x\n\
) -> E exactly one // result\n";
let output = formatted(input);
let comments = lex(&SourceFile::new("test.tes", output.clone())).comments;
assert_eq!(comments.len(), 4);
let decision = output.find("decision d(X x) -> E\n").unwrap();
for text in [
"// decision docs\n",
"# parameters\n",
"// the input\n",
"// result\n",
] {
assert!(output.find(text).unwrap() < decision, "{output}");
}
assert_eq!(formatted(&output), output);
}
#[test]
fn preserves_comments_with_classic_carriage_return_lines() {
let output = formatted("# docs\rmodule m // header\renum E { A # value\r}\r");
assert_eq!(
output,
"# docs\nmodule m // header\n\nenum E {\n A, # value\n}\n"
);
assert_eq!(formatted(&output), output);
}
#[test]
fn preserves_comment_order_across_declaration_bodies() {
let input = concat!(
"module complete\n",
"enum Answer { Yes, No }\n",
"entity Subject { flag: Bool }\n",
"derive neg(Subject s) -> Bool: not s.flag # derive\n",
"decision outcome(Subject s) -> Answer exactly one // decision\n",
"rule yes(Subject s):\n",
" # condition docs\n",
" when s.flag // condition\n",
" // effect docs\n",
" then outcome(s) = Yes // effect\n",
" source \"policy\" // source\n",
"case yes_case:\n",
" # binding docs\n",
" let s = Subject { # literal\n",
" flag: true, // field\n",
" } # literal end\n",
" // expectation docs\n",
" expect outcome(s) = Yes # expected\n",
"invariant total:\n",
" # quantified docs\n",
" for all s: Subject // quantified\n",
" # assertion docs\n",
" exactly one outcome(s) // cardinality\n",
);
let output = formatted(input);
let comment_texts = |text: &str| {
lex(&SourceFile::new("test.tes", text))
.comments
.into_iter()
.map(|comment| comment.text)
.collect::<Vec<_>>()
};
assert_eq!(comment_texts(&output), comment_texts(input));
assert_eq!(formatted(&output), output);
}
}