Skip to content
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
89 changes: 88 additions & 1 deletion crates/plotnik-lib/src/infer/tyton.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Tyton: Types Testing Object Notation
//!
//! A compact DSL for constructing `TypeTable` test fixtures.
//! Supports both parsing (text → TypeTable) and emitting (TypeTable → text).
//!
//! # Design
//!
Expand Down Expand Up @@ -48,10 +49,12 @@
//! Stmts = Stmt*
//! ```

use std::fmt::Write;

use indexmap::IndexMap;
use logos::Logos;

use super::{TypeKey, TypeTable, TypeValue};
use indexmap::IndexMap;

#[derive(Logos, Debug, Clone, PartialEq)]
#[logos(skip r"[ \t\n\r]+")]
Expand Down Expand Up @@ -427,3 +430,87 @@ pub fn parse(input: &str) -> Result<TypeTable<'_>, ParseError> {
let mut parser = Parser::new(input)?;
parser.parse_all()
}

/// Emit TypeTable as tyton notation.
pub fn emit(table: &TypeTable<'_>) -> String {
let mut out = String::new();

for (key, value) in table.iter() {
if is_builtin(key) {
continue;
}
if !out.is_empty() {
out.push('\n');
}
emit_key(&mut out, key);
out.push_str(" = ");
emit_value(&mut out, value);
}

out
}

fn is_builtin(key: &TypeKey<'_>) -> bool {
matches!(key, TypeKey::Node | TypeKey::String | TypeKey::Unit)
}

fn emit_key(out: &mut String, key: &TypeKey<'_>) {
match key {
TypeKey::Node => out.push_str("Node"),
TypeKey::String => out.push_str("string"),
TypeKey::Unit => out.push_str("()"),
TypeKey::Named(name) => out.push_str(name),
TypeKey::Synthetic(segments) => {
out.push('<');
for (i, seg) in segments.iter().enumerate() {
if i > 0 {
out.push(' ');
}
out.push_str(seg);
}
out.push('>');
}
}
}

fn emit_value(out: &mut String, value: &TypeValue<'_>) {
match value {
TypeValue::Node => out.push_str("Node"),
TypeValue::String => out.push_str("string"),
TypeValue::Unit => out.push_str("()"),
TypeValue::Struct(fields) => {
out.push_str("{ ");
for (i, (field, key)) in fields.iter().enumerate() {
if i > 0 {
out.push(' ');
}
emit_key(out, key);
write!(out, " @{}", field).unwrap();
}
out.push_str(" }");
}
TypeValue::TaggedUnion(variants) => {
out.push_str("[ ");
for (i, (tag, key)) in variants.iter().enumerate() {
if i > 0 {
out.push(' ');
}
write!(out, "{}: ", tag).unwrap();
emit_key(out, key);
}
out.push_str(" ]");
}
TypeValue::Optional(key) => {
emit_key(out, key);
out.push('?');
}
TypeValue::List(key) => {
emit_key(out, key);
out.push('*');
}
TypeValue::NonEmptyList(key) => {
emit_key(out, key);
out.push('+');
}
}
}
114 changes: 113 additions & 1 deletion crates/plotnik-lib/src/infer/tyton_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::tyton::parse;
use super::tyton::{emit, parse};
use indoc::indoc;

fn dump_table(input: &str) -> String {
Expand Down Expand Up @@ -405,3 +405,115 @@ fn error_invalid_type_value() {
let input = "Foo = @bar";
insta::assert_snapshot!(dump_table(input), @"ERROR: expected type value at 6..7");
}

// === emit tests ===

#[test]
fn emit_empty() {
let table = parse("").unwrap();
insta::assert_snapshot!(emit(&table), @"");
}

#[test]
fn emit_struct_simple() {
let table = parse("Foo = { Node @name }").unwrap();
insta::assert_snapshot!(emit(&table), @"Foo = { Node @name }");
}

#[test]
fn emit_struct_multiple_fields() {
let table = parse("Func = { string @name Node @body Node @params }").unwrap();
insta::assert_snapshot!(emit(&table), @"Func = { string @name Node @body Node @params }");
}

#[test]
fn emit_struct_empty() {
let table = parse("Empty = {}").unwrap();
insta::assert_snapshot!(emit(&table), @"Empty = { }");
}

#[test]
fn emit_tagged_union() {
let table = parse("Stmt = [ Assign: AssignStmt Call: CallStmt ]").unwrap();
insta::assert_snapshot!(emit(&table), @"Stmt = [ Assign: AssignStmt Call: CallStmt ]");
}

#[test]
fn emit_optional() {
let table = parse("MaybeNode = Node?").unwrap();
insta::assert_snapshot!(emit(&table), @"MaybeNode = Node?");
}

#[test]
fn emit_list() {
let table = parse("Nodes = Node*").unwrap();
insta::assert_snapshot!(emit(&table), @"Nodes = Node*");
}

#[test]
fn emit_non_empty_list() {
let table = parse("Nodes = Node+").unwrap();
insta::assert_snapshot!(emit(&table), @"Nodes = Node+");
}

#[test]
fn emit_synthetic_key() {
let table = parse("<Foo bar> = { Node @value }").unwrap();
insta::assert_snapshot!(emit(&table), @"<Foo bar> = { Node @value }");
}

#[test]
fn emit_synthetic_in_wrapper() {
let table = parse("Wrapper = <Foo bar>?").unwrap();
insta::assert_snapshot!(emit(&table), @"Wrapper = <Foo bar>?");
}

#[test]
fn emit_bare_builtins() {
let input = indoc! {r#"
AliasNode = Node
AliasString = string
AliasUnit = ()
"#};
let table = parse(input).unwrap();
insta::assert_snapshot!(emit(&table), @r"
AliasNode = Node
AliasString = string
AliasUnit = ()
");
}

#[test]
fn emit_multiple_definitions() {
let input = indoc! {r#"
AssignStmt = { Node @target Node @value }
CallStmt = { Node @func Node @args }
Stmt = [ Assign: AssignStmt Call: CallStmt ]
Stmts = Stmt*
"#};
let table = parse(input).unwrap();
insta::assert_snapshot!(emit(&table), @r"
AssignStmt = { Node @target Node @value }
CallStmt = { Node @func Node @args }
Stmt = [ Assign: AssignStmt Call: CallStmt ]
Stmts = Stmt*
");
}

#[test]
fn emit_roundtrip() {
let input = indoc! {r#"
FuncInfo = { string @name Node @body }
Param = { string @name string @type_annotation }
Params = Param*
FuncDecl = { FuncInfo @info Params @params }
Stmt = [ Func: FuncDecl Expr: Node ]
MaybeStmt = Stmt?
"#};

let table1 = parse(input).unwrap();
let emitted = emit(&table1);
let table2 = parse(&emitted).unwrap();

assert_eq!(table1.types, table2.types);
}