Current section

Files

Jump to
glance_printer src glance_printer.gleam
Raw

src/glance_printer.gleam

import gleam/list
import gleam/string
import gleam/option.{type Option, None, Some}
import glance.{
type AssignmentName, type Attribute, type BinaryOperator,
type BitStringSegmentOption, type Constant, type CustomType, type Definition,
type Expression, type Field, type FnParameter, type Function,
type FunctionParameter, type Import, type Module, type Pattern, type Publicity,
type Statement, type Type, type TypeAlias, type Variant, AddFloat, AddInt, And,
Assert, Assignment, Attribute, BigOption, BinaryOperator, BinaryOption,
BitString, BitStringOption, Block, Call, Case, Clause, Concatenate, Constant,
CustomType, Definition, Discarded, DivFloat, DivInt, Eq, Expression, Field,
FieldAccess, Float, FloatOption, Fn, FnCapture, FnParameter, Function,
FunctionParameter, FunctionType, GtEqFloat, GtEqInt, GtFloat, GtInt, Import,
Int, IntOption, Let, LittleOption, LtEqFloat, LtEqInt, LtFloat, LtInt, Module,
MultFloat, MultInt, Named, NamedType, NativeOption, NegateBool, NegateInt,
NotEq, Or, Panic, PatternAssignment, PatternBitString, PatternConcatenate,
PatternConstructor, PatternDiscard, PatternFloat, PatternInt, PatternList,
PatternString, PatternTuple, PatternVariable, Pipe, Private, Public,
RecordUpdate, RemainderInt, SignedOption, SizeOption, SizeValueOption, String,
SubFloat, SubInt, Todo, Tuple, TupleIndex, TupleType, TypeAlias, UnitOption,
UnsignedOption, Use, Utf16CodepointOption, Utf16Option, Utf32CodepointOption,
Utf32Option, Utf8CodepointOption, Utf8Option, Variable, VariableType, Variant,
}
import glam/doc.{type Document}
import glance_printer/internal/doc_extras.{
comma_separated_in_parentheses, nbsp, nest, trailing_comma,
}
import gleam/int
/// Pretty print a glance module
pub fn print(module module: Module) -> String {
let Module(
imports,
custom_types,
type_aliases,
constants,
_external_types,
_external_functions,
functions,
) = module
// Handle imports separately because they're joined with only on line break
let imports =
imports
|> list.reverse
|> list.map(pretty_import)
|> doc.join(with: doc.line)
// Everything elses gets separated by an empty line (2 line breaks)
let the_rest =
[
list.map(custom_types, pretty_custom_type),
list.map(type_aliases, pretty_type_alias),
list.map(constants, pretty_constant),
list.map(functions, pretty_function),
]
|> list.filter(fn(lst) { !list.is_empty(lst) })
|> list.map(list.reverse)
|> list.map(doc.join(_, with: doc.lines(2)))
[imports, ..the_rest]
|> doc.join(with: doc.lines(2))
|> doc.to_string(81)
|> string.trim
<> "\n"
}
fn pretty_definition(
definition: Definition(inner),
inner_to_doc: fn(inner) -> Document,
) -> Document {
let Definition(attributes, definition) = definition
attributes
|> list.map(pretty_attribute)
|> list.append([inner_to_doc(definition)])
|> doc.join(with: doc.line)
}
fn pretty_attribute(attribute: Attribute) -> Document {
let Attribute(name, arguments) = attribute
let arguments =
arguments
|> list.map(pretty_expression)
|> doc_extras.comma_separated_in_parentheses
[doc.from_string("@" <> name), arguments]
|> doc.concat
}
/// Pretty print a top level function.
fn pretty_function(function: Definition(Function)) -> Document {
use Function(name, publicity, parameters, return, statements, _) <- pretty_definition(
function,
)
let parameters =
parameters
|> list.map(pretty_function_parameter)
|> comma_separated_in_parentheses
let statements = case statements {
[] -> doc.empty
_ ->
[nbsp(), pretty_block(of: statements)]
|> doc.concat
}
[
pretty_public(publicity),
doc.from_string("fn " <> name),
parameters,
pretty_return_signature(return),
statements,
]
|> doc.concat
}
// Pretty print a parameter of a top level function
// For printing an anonymous function paramater, see `pretty_fn_parameter`
fn pretty_function_parameter(parameter: FunctionParameter) -> Document {
let FunctionParameter(label, name, type_) = parameter
let label = case label {
Some(l) -> doc.from_string(l <> " ")
None -> doc.empty
}
[label, pretty_assignment_name(name), pretty_type_annotation(type_)]
|> doc.concat
}
/// Pretty print a statement
fn pretty_statement(statement: Statement) -> Document {
case statement {
Expression(expression) -> pretty_expression(expression)
Assignment(kind, pattern, annotation, value) -> {
let let_declaration = case kind {
Let -> doc.from_string("let ")
Assert -> doc.from_string("let assert ")
}
[
let_declaration,
pretty_pattern(pattern),
pretty_type_annotation(annotation),
doc.from_string(" = "),
pretty_expression(value),
]
|> doc.concat
}
Use(patterns, function) -> {
let patterns =
patterns
|> list.map(pretty_pattern)
|> doc.join(with: doc.from_string(", "))
[
doc.from_string("use "),
patterns,
doc.from_string(" <- "),
pretty_expression(function),
]
|> doc.concat
}
}
}
/// Pretty print a "pattern" (anything that could go in a pattern match branch)
fn pretty_pattern(pattern: Pattern) -> Document {
case pattern {
// Basic patterns
PatternInt(val) | PatternFloat(val) | PatternVariable(val) ->
doc.from_string(val)
PatternString(val) -> doc.from_string("\"" <> val <> "\"")
// A discarded value should start with an underscore
PatternDiscard(val) -> doc.from_string("_" <> val)
// A tuple pattern
PatternTuple(elements) ->
elements
|> list.map(pretty_pattern)
|> pretty_tuple
// A list pattern
PatternList(elements, tail) ->
pretty_list(
of: list.map(elements, pretty_pattern),
with_tail: option.map(tail, pretty_pattern),
)
// Pattern for renaming something with "as"
PatternAssignment(pattern, name) -> {
[pretty_pattern(pattern), pretty_as(Some(name))]
|> doc.concat
}
// Pattern for pulling off the front end of a string
PatternConcatenate(left, right) -> {
[doc.from_string("\"" <> left <> "\" <> "), pretty_assignment_name(right)]
|> doc.concat
}
PatternBitString(segments) -> pretty_bitstring(segments, pretty_pattern)
PatternConstructor(module, constructor, arguments, with_spread) -> {
let module =
module
|> option.map(doc.from_string)
|> option.unwrap(or: doc.empty)
let arguments = list.map(arguments, pretty_field(_, pretty_pattern))
let arguments =
case with_spread {
True -> list.append(arguments, [doc.from_string("..")])
False -> arguments
}
|> comma_separated_in_parentheses
[module, doc.from_string(constructor), arguments]
|> doc.concat
}
}
}
// Pretty print a constant
fn pretty_constant(constant: Definition(Constant)) -> Document {
use Constant(name, publicity, annotation, value) <- pretty_definition(
constant,
)
[
pretty_public(publicity),
doc.from_string("const " <> name),
pretty_type_annotation(annotation),
doc.from_string(" ="),
doc.space,
pretty_expression(value),
]
|> doc.concat
}
/// Pretty print a block of statements
fn pretty_block(of statements: List(Statement)) -> Document {
// Statements are separated by a single line
let statements =
statements
|> list.map(pretty_statement)
|> doc.join(with: doc.line)
// A block gets wrapped in squiggly brackets and indented
doc.concat([doc.from_string("{"), doc.line])
|> doc.append(statements)
|> nest
|> doc.append_docs([doc.line, doc.from_string("}")])
}
// Pretty print a tuple of types, expressions, or patterns
fn pretty_tuple(with elements: List(Document)) -> Document {
let comma_separated_elements =
elements
|> doc.join(with: doc.concat([doc.from_string(","), doc.space]))
doc.concat([doc.from_string("#("), doc.soft_break])
|> doc.append(comma_separated_elements)
|> nest
|> doc.append(doc.concat([trailing_comma(), doc.from_string(")")]))
|> doc.group
}
// Pretty print a list of expressions or patterns
fn pretty_list(
of elements: List(Document),
with_tail tail: Option(Document),
) -> Document {
let tail =
tail
|> option.map(doc.prepend(_, doc.from_string("..")))
let comma_separated_items =
elements
|> list.append(option.values([tail]))
|> doc.concat_join([doc.from_string(","), doc.space])
doc.concat([doc.from_string("["), doc.soft_break])
|> doc.append(comma_separated_items)
|> nest
|> doc.append_docs([doc.soft_break, doc.from_string("]")])
|> doc.group
}
// Expression -------------------------------------
fn pretty_expression(expression: Expression) -> Document {
case expression {
// Int, Float and Variable simply print as their string value
Int(str) | Float(str) | Variable(str) -> doc.from_string(str)
// A string literal needs to bee wrapped in quotes
String(val) -> doc.from_string("\"" <> val <> "\"")
// Negate int gets a - in front
NegateInt(expr) ->
[doc.from_string("-"), pretty_expression(expr)]
|> doc.concat
// Negate bool gets a ! in front
NegateBool(expr) ->
[doc.from_string("!"), pretty_expression(expr)]
|> doc.concat
// A block of statements
Block(statements) -> pretty_block(of: statements)
// Pretty print a panic
Panic(msg) -> {
case msg {
Some(str) -> doc.from_string("panic as \"" <> str <> "\"")
None -> doc.from_string("panic")
}
}
// Pretty print a todo
Todo(msg) -> {
case msg {
Some(str) -> doc.from_string("todo as \"" <> str <> "\"")
None -> doc.from_string("todo")
}
}
// Pretty print a tuple
Tuple(expressions) ->
expressions
|> list.map(pretty_expression)
|> pretty_tuple
// Pretty print a list
glance.List(elements, rest) ->
pretty_list(
list.map(elements, pretty_expression),
option.map(rest, pretty_expression),
)
// Pretty print a function
Fn(arguments, return, body) -> pretty_fn(arguments, return, body)
// Pretty print a record update expression
RecordUpdate(module, constructor, record, fields) -> {
let module = case module {
Some(str) -> doc.from_string(str)
None -> doc.empty
}
let record =
[doc.from_string(".."), pretty_expression(record)]
|> doc.concat
let fields =
list.map(fields, fn(field) {
let #(name, expr) = field
[doc.from_string(name <> ": "), pretty_expression(expr)]
|> doc.concat
})
|> list.prepend(record)
|> comma_separated_in_parentheses
[module, doc.from_string(constructor), fields]
|> doc.concat
}
FieldAccess(container, label) -> {
[pretty_expression(container), doc.from_string("." <> label)]
|> doc.concat
}
Call(function, arguments) -> {
let arguments =
arguments
|> list.map(pretty_field(_, pretty_expression))
|> comma_separated_in_parentheses
[pretty_expression(function), arguments]
|> doc.concat
}
TupleIndex(tuple, index) -> {
[pretty_expression(tuple), doc.from_string("." <> int.to_string(index))]
|> doc.concat
}
FnCapture(label, function, arguments_before, arguments_after) -> {
let arguments_before =
list.map(arguments_before, pretty_field(_, pretty_expression))
let arguments_after =
list.map(arguments_after, pretty_field(_, pretty_expression))
let placeholder = case label {
Some(str) -> doc.from_string(str <> ": _")
None -> doc.from_string("_")
}
let in_parens =
arguments_before
|> list.append([placeholder])
|> list.append(arguments_after)
|> comma_separated_in_parentheses
[pretty_expression(function), in_parens]
|> doc.concat
}
BitString(segments) -> pretty_bitstring(segments, pretty_expression)
Case(subjects, clauses) -> {
let subjects =
subjects
|> list.map(pretty_expression)
|> doc.join(with: doc.from_string(", "))
let clauses =
{
use Clause(lolo_patterns, guard, body) <- list.map(clauses)
let lolo_patterns =
list.map(lolo_patterns, list.map(_, pretty_pattern))
let lolo_patterns =
list.map(lolo_patterns, doc.join(_, with: doc.from_string(", ")))
|> doc.join(with: doc.from_string(" | "))
let guard =
option.map(guard, pretty_expression)
|> option.map(doc.prepend(_, doc.from_string(" if ")))
|> option.unwrap(or: doc.empty)
[
lolo_patterns,
guard,
doc.from_string(" -> "),
pretty_expression(body),
]
|> doc.concat
}
|> doc.join(with: doc.line)
doc.from_string("case ")
|> doc.append(subjects)
|> doc.append_docs([doc.from_string(" {"), doc.line])
|> doc.append(clauses)
|> nest
|> doc.append_docs([doc.line, doc.from_string("}")])
}
BinaryOperator(name, left, right) -> {
[
pretty_expression(left),
nbsp(),
pretty_binary_operator(name),
nbsp(),
pretty_expression(right),
]
|> doc.concat
}
}
}
fn pretty_binary_operator(operator: BinaryOperator) -> Document {
case operator {
And -> doc.from_string("&&")
Or -> doc.from_string("||")
Eq -> doc.from_string("==")
NotEq -> doc.from_string("!=")
LtInt -> doc.from_string("<")
LtEqInt -> doc.from_string("<=")
LtFloat -> doc.from_string("<.")
LtEqFloat -> doc.from_string("<=.")
GtEqInt -> doc.from_string(">=")
GtInt -> doc.from_string(">")
GtEqFloat -> doc.from_string(">=.")
GtFloat -> doc.from_string(">.")
Pipe -> doc.from_string("|>")
AddInt -> doc.from_string("+")
AddFloat -> doc.from_string("+.")
SubInt -> doc.from_string("-")
SubFloat -> doc.from_string("-.")
MultInt -> doc.from_string("*")
MultFloat -> doc.from_string("*.")
DivInt -> doc.from_string("/")
DivFloat -> doc.from_string("/.")
RemainderInt -> doc.from_string("%")
Concatenate -> doc.from_string("<>")
}
}
fn pretty_bitstring(
segments: List(#(as_doc, List(BitStringSegmentOption(as_doc)))),
to_doc: fn(as_doc) -> Document,
) -> Document {
let segments =
{
use segment <- list.map(segments)
let #(expr, options) = segment
let options =
options
|> list.map(pretty_bitstring_option(_, to_doc))
|> doc.join(with: doc.from_string("-"))
[to_doc(expr), doc.from_string(":"), options]
|> doc.concat
}
|> doc.concat_join([doc.from_string(","), doc.flex_break(" ", "")])
[doc.from_string("<<"), doc.soft_break]
|> doc.concat
|> doc.append(segments)
|> nest
|> doc.append_docs([trailing_comma(), doc.from_string(">>")])
|> doc.group
}
fn pretty_bitstring_option(
bitstring_option: BitStringSegmentOption(as_doc),
fun: fn(as_doc) -> Document,
) -> Document {
case bitstring_option {
BinaryOption -> doc.from_string("binary")
IntOption -> doc.from_string("int")
FloatOption -> doc.from_string("float")
BitStringOption -> doc.from_string("bit_string")
Utf8Option -> doc.from_string("utf8")
Utf16Option -> doc.from_string("utf16")
Utf32Option -> doc.from_string("utf32")
Utf8CodepointOption -> doc.from_string("utf8_codepoint")
Utf16CodepointOption -> doc.from_string("utf16_codepoint")
Utf32CodepointOption -> doc.from_string("utf32_codepoint")
SignedOption -> doc.from_string("signed")
UnsignedOption -> doc.from_string("unsigned")
BigOption -> doc.from_string("big")
LittleOption -> doc.from_string("little")
NativeOption -> doc.from_string("native")
SizeValueOption(n) ->
[doc.from_string("size("), fun(n), doc.from_string(")")]
|> doc.concat
SizeOption(n) -> doc.from_string(int.to_string(n))
UnitOption(n) -> doc.from_string("unit(" <> int.to_string(n) <> ")")
}
}
// Pretty print an anonymous functions.
// For a top level function, see `pretty_function`
fn pretty_fn(
arguments: List(FnParameter),
return: Option(Type),
body: List(Statement),
) -> Document {
let arguments =
arguments
|> list.map(pretty_fn_parameter)
|> comma_separated_in_parentheses
let body = case body {
// This never actually happens because the compiler will insert a todo
[] -> doc.from_string("{}")
// If there's only one statement, it might be on one line
[statement] ->
doc.concat([doc.from_string("{"), doc.space])
|> doc.append(pretty_statement(statement))
|> nest
|> doc.append_docs([doc.space, doc.from_string("}")])
|> doc.group
// Multiple statements always break to multiple lines
multiple_statements -> pretty_block(multiple_statements)
}
[
doc.from_string("fn"),
arguments,
pretty_return_signature(return),
nbsp(),
body,
]
|> doc.concat
}
// Pretty print an anonymous function parameter.
// For a top level function parameter, see `pretty_function_parameter`
fn pretty_fn_parameter(fn_parameter: FnParameter) -> Document {
let FnParameter(name, type_) = fn_parameter
[pretty_assignment_name(name), pretty_type_annotation(type_)]
|> doc.concat
}
// Type Alias -------------------------------------
fn pretty_type_alias(type_alias: Definition(TypeAlias)) -> Document {
use TypeAlias(name, publicity, parameters, aliased) <- pretty_definition(
type_alias,
)
let parameters = case parameters {
[] -> doc.empty
_ -> {
parameters
|> list.map(doc.from_string)
|> comma_separated_in_parentheses
}
}
pretty_public(publicity)
|> doc.append(doc.from_string("type " <> name))
|> doc.append(parameters)
|> doc.append(doc.from_string(" ="))
|> doc.append(doc.line)
|> nest
|> doc.append(pretty_type(aliased))
}
// Type -------------------------------------------------
fn pretty_type(type_: Type) -> Document {
case type_ {
NamedType(name, module, parameters) -> {
let parameters = case parameters {
[] -> doc.empty
_ ->
parameters
|> list.map(pretty_type)
|> comma_separated_in_parentheses
}
module
|> option.map(fn(mod) { mod <> "." })
|> option.map(doc.from_string)
|> option.unwrap(or: doc.empty)
|> doc.append(doc.from_string(name))
|> doc.append(parameters)
}
TupleType(elements) ->
elements
|> list.map(pretty_type)
|> pretty_tuple
FunctionType(parameters, return) -> {
doc.from_string("fn")
|> doc.append(
parameters
|> list.map(pretty_type)
|> comma_separated_in_parentheses,
)
|> doc.append(pretty_return_signature(Some(return)))
}
VariableType(name) -> doc.from_string(name)
}
}
fn pretty_custom_type(type_: Definition(CustomType)) -> Document {
use CustomType(name, publicity, opaque_, parameters, variants) <- pretty_definition(
type_,
)
// Opaque or not
let opaque_ = case opaque_ {
True -> "opaque "
False -> ""
}
let parameters = case parameters {
[] -> doc.empty
_ -> {
parameters
|> list.map(doc.from_string)
|> comma_separated_in_parentheses
}
}
// Custom types variants
let variants =
variants
|> list.map(pretty_variant)
|> doc.join(with: doc.line)
let type_body =
doc.concat([doc.from_string("{"), doc.line])
|> doc.append(variants)
|> nest
|> doc.append_docs([doc.line, doc.from_string("}")])
|> doc.group
[
pretty_public(publicity),
doc.from_string(opaque_ <> "type " <> name),
parameters,
nbsp(),
type_body,
]
|> doc.concat
}
fn pretty_variant(variant: Variant) -> Document {
let Variant(name, fields) = variant
fields
|> list.map(pretty_field(_, pretty_type))
|> comma_separated_in_parentheses
|> doc.prepend(doc.from_string(name))
}
fn pretty_field(field: Field(a), a_to_doc: fn(a) -> Document) -> Document {
let Field(label, type_) = field
case label {
Some(l) -> doc.from_string(l <> ": ")
None -> doc.empty
}
|> doc.append(a_to_doc(type_))
}
// Imports --------------------------------------------
// Pretty print an import statement
fn pretty_import(import_: Definition(Import)) -> Document {
use Import(module, alias, unqualified_types, unqualified_values) <- pretty_definition(
import_,
)
let unqualified_values =
unqualified_values
|> list.map(fn(uq) {
doc.concat([doc.from_string(uq.name), pretty_as(uq.alias)])
})
let unqualified_types =
unqualified_types
|> list.map(fn(uq) {
doc.concat([
doc.from_string("type "),
doc.from_string(uq.name),
pretty_as(uq.alias),
])
})
let unqualifieds = case list.append(unqualified_types, unqualified_values) {
[] -> doc.empty
lst ->
lst
|> doc.concat_join([doc.from_string(","), doc.flex_break(" ", "")])
|> doc.group
|> doc.prepend(doc.concat([doc.from_string(".{"), doc.soft_break]))
|> nest
|> doc.append(doc.concat([trailing_comma(), doc.from_string("}")]))
|> doc.group
}
doc.from_string("import " <> module)
|> doc.append(unqualifieds)
|> doc.append(pretty_as(alias))
}
// --------- Little Pieces -------------------------------
// Prints the pub keyword
fn pretty_public(publicity: Publicity) -> Document {
case publicity {
Public -> doc.from_string("pub ")
Private -> doc.empty
}
}
// Simply prints an assignment name normally or prefixed with
// an underscore if it is unused
fn pretty_assignment_name(assignment_name: AssignmentName) -> Document {
case assignment_name {
Named(str) -> doc.from_string(str)
Discarded(str) -> doc.from_string("_" <> str)
}
}
// Pretty prints an optional type annotation
fn pretty_type_annotation(type_: Option(Type)) -> Document {
case type_ {
Some(t) ->
[doc.from_string(": "), pretty_type(t)]
|> doc.concat
None -> doc.empty
}
}
// Pretty return signature
fn pretty_return_signature(type_: Option(Type)) -> Document {
case type_ {
Some(t) ->
[doc.from_string(" -> "), pretty_type(t)]
|> doc.concat
None -> doc.empty
}
}
// Pretty print "as" keyword alias
fn pretty_as(name: Option(String)) -> Document {
case name {
Some(str) -> doc.from_string(" as " <> str)
None -> doc.empty
}
}